2018-09-24 15:06:43 +08:00
//
// C r e d e n t i a l P r o v i d e r V i e w C o n t r o l l e r . s w i f t
// p a s s A u t o F i l l E x t e n s i o n
//
// C r e a t e d b y Y i s h i L i n o n 2 0 1 8 / 9 / 2 4 .
// C o p y r i g h t © 2 0 1 8 B o b S u n . A l l r i g h t s r e s e r v e d .
//
import AuthenticationServices
import passKit
2018-09-24 22:03:14 +08:00
fileprivate class PasswordsTableEntry : NSObject {
var title : String
var categoryText : String
var categoryArray : [ String ]
var passwordEntity : PasswordEntity ?
init ( _ entity : PasswordEntity ) {
self . title = entity . name !
self . categoryText = entity . getCategoryText ( )
self . categoryArray = entity . getCategoryArray ( )
self . passwordEntity = entity
}
}
2018-09-24 15:06:43 +08:00
2018-09-24 22:03:14 +08:00
class CredentialProviderViewController : ASCredentialProviderViewController , UITableViewDataSource , UITableViewDelegate , UISearchBarDelegate {
@IBOutlet weak var searchBar : UISearchBar !
@IBOutlet weak var tableView : UITableView !
private let passwordStore = PasswordStore . shared
private var searchActive = false
private var passwordsTableEntries : [ PasswordsTableEntry ] = [ ]
private var filteredPasswordsTableEntries : [ PasswordsTableEntry ] = [ ]
2018-09-24 15:06:43 +08:00
private lazy var passcodelock : PasscodeExtensionDisplay = {
let passcodelock = PasscodeExtensionDisplay ( extensionContext : self . extensionContext )
return passcodelock
} ( )
/*
Prepare your UI to list available credentials for the user to choose from . The items in
' serviceIdentifiers ' describe the service the user is logging in to , so your extension can
prioritize the most relevant credentials in the list .
*/
override func prepareCredentialList ( for serviceIdentifiers : [ ASCredentialServiceIdentifier ] ) {
2018-09-24 22:03:14 +08:00
// c l e a n u p t h e s e a r c h b a r
guard serviceIdentifiers . count > 0 else {
searchBar . text = " "
searchBar . becomeFirstResponder ( )
searchBarSearchButtonClicked ( searchBar )
return
}
// g e t t h e d o m a i n
var identifier = serviceIdentifiers [ 0 ] . identifier
if ! identifier . hasPrefix ( " http:// " ) && ! identifier . hasPrefix ( " https:// " ) {
identifier = " http:// " + identifier
}
let url = URL ( string : identifier ) ? . host ? ? " "
// " c l i c k " s e a r c h
searchBar . text = url
searchBar . becomeFirstResponder ( )
searchBarSearchButtonClicked ( searchBar )
2018-09-24 15:06:43 +08:00
}
/*
Implement this method if your extension support
s showing credentials in the QuickType bar .
When the user selects a credential from your app , this method will be called with the
ASPasswordCredentialIdentity your app has previously saved to the ASCredentialIdentityStore .
Provide the password by completing the extension request with the associated ASPasswordCredential .
If using the credential would require showing custom UI for authenticating the user , cancel
the request with error code ASExtensionError . userInteractionRequired .
override func provideCredentialWithoutUserInteraction ( for credentialIdentity : ASPasswordCredentialIdentity ) {
let databaseIsUnlocked = true
if ( databaseIsUnlocked ) {
let passwordCredential = ASPasswordCredential ( user : " j_appleseed " , password : " apple1234 " )
self . extensionContext . completeRequest ( withSelectedCredential : passwordCredential , completionHandler : nil )
} else {
self . extensionContext . cancelRequest ( withError : NSError ( domain : ASExtensionErrorDomain , code : ASExtensionError . userInteractionRequired . rawValue ) )
}
}
*/
/*
Implement this method if provideCredentialWithoutUserInteraction ( for : ) can fail with
ASExtensionError . userInteractionRequired . In this case , the system may present your extension ' s
UI and call this method . Show appropriate UI for authenticating the user then provide the password
by completing the extension request with the associated ASPasswordCredential .
override func prepareInterfaceToProvideCredential ( for credentialIdentity : ASPasswordCredentialIdentity ) {
}
*/
@IBAction func cancel ( _ sender : AnyObject ? ) {
self . extensionContext . cancelRequest ( withError : NSError ( domain : ASExtensionErrorDomain , code : ASExtensionError . userCanceled . rawValue ) )
}
2018-09-24 22:03:14 +08:00
override func viewWillAppear ( _ animated : Bool ) {
super . viewWillAppear ( animated )
passcodelock . presentPasscodeLockIfNeeded ( self )
}
override func viewDidLoad ( ) {
super . viewDidLoad ( )
// p r e p a r e
searchBar . delegate = self
tableView . delegate = self
tableView . dataSource = self
tableView . register ( UINib ( nibName : " PasswordWithFolderTableViewCell " , bundle : nil ) , forCellReuseIdentifier : " passwordWithFolderTableViewCell " )
// i n i t i a l i z e t a b l e e n t r i e s
initPasswordsTableEntries ( )
}
private func initPasswordsTableEntries ( ) {
passwordsTableEntries . removeAll ( )
filteredPasswordsTableEntries . removeAll ( )
var passwordEntities = [ PasswordEntity ] ( )
passwordEntities = self . passwordStore . fetchPasswordEntityCoreData ( withDir : false )
passwordsTableEntries = passwordEntities . map {
PasswordsTableEntry ( $0 )
}
}
// d e f i n e c e l l c o n t e n t s , a n d s e t l o n g p r e s s a c t i o n
func tableView ( _ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell {
let cell = tableView . dequeueReusableCell ( withIdentifier : " passwordTableViewCell " , for : indexPath )
let entry = getPasswordEntry ( by : indexPath )
if entry . passwordEntity ! . synced {
cell . textLabel ? . text = entry . title
} else {
cell . textLabel ? . text = " ↻ \( entry . title ) "
}
cell . accessoryType = . none
cell . detailTextLabel ? . font = UIFont . preferredFont ( forTextStyle : . footnote )
cell . detailTextLabel ? . text = entry . categoryText
return cell
}
// s e l e c t r o w - > e x t e n s i o n r e t u r n s ( w i t h u s e r n a m e a n d p a s s w o r d )
func tableView ( _ tableView : UITableView , didSelectRowAt indexPath : IndexPath ) {
let entry = getPasswordEntry ( by : indexPath )
guard self . passwordStore . privateKey != nil else {
Utils . alert ( title : " Cannot Copy Password " , message : " PGP Key is not set. Please set your PGP Key first. " , controller : self , completion : nil )
return
}
let passwordEntity = entry . passwordEntity !
UIImpactFeedbackGenerator ( style : . medium ) . impactOccurred ( )
DispatchQueue . global ( qos : . userInteractive ) . async {
var decryptedPassword : Password ?
do {
decryptedPassword = try self . passwordStore . decrypt ( passwordEntity : passwordEntity , requestPGPKeyPassphrase : self . requestPGPKeyPassphrase )
let username = decryptedPassword ? . username ? ? decryptedPassword ? . login ? ? " "
let password = decryptedPassword ? . password ? ? " "
DispatchQueue . main . async { // p r e p a r e a d i c t i o n a r y t o r e t u r n
let passwordCredential = ASPasswordCredential ( user : username , password : password )
self . extensionContext . completeRequest ( withSelectedCredential : passwordCredential , completionHandler : nil )
}
} catch {
DispatchQueue . main . async {
// r e m o v e t h e w r o n g p a s s p h r a s e s o t h a t u s e r s c o u l d e n t e r i t n e x t t i m e
self . passwordStore . pgpKeyPassphrase = nil
Utils . alert ( title : " Cannot Copy Password " , message : error . localizedDescription , controller : self , completion : nil )
}
}
}
}
func numberOfSectionsInTableView ( tableView : UITableView ) -> Int {
return 1
}
func tableView ( _ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int {
if searchActive {
return filteredPasswordsTableEntries . count
}
return passwordsTableEntries . count ;
}
private func requestPGPKeyPassphrase ( ) -> String {
let sem = DispatchSemaphore ( value : 0 )
var passphrase = " "
DispatchQueue . main . async {
let alert = UIAlertController ( title : " Passphrase " , message : " Please fill in the passphrase of your PGP secret key. " , preferredStyle : UIAlertController . Style . alert )
alert . addAction ( UIAlertAction ( title : " OK " , style : UIAlertAction . Style . default , handler : { _ in
passphrase = alert . textFields ! . first ! . text !
sem . signal ( )
} ) )
alert . addTextField ( configurationHandler : { ( textField : UITextField ! ) in
textField . text = " "
textField . isSecureTextEntry = true
} )
self . present ( alert , animated : true , completion : nil )
}
let _ = sem . wait ( timeout : DispatchTime . distantFuture )
if SharedDefaults [ . isRememberPGPPassphraseOn ] {
self . passwordStore . pgpKeyPassphrase = passphrase
}
return passphrase
}
func searchBarCancelButtonClicked ( _ searchBar : UISearchBar ) {
searchBar . text = " "
searchActive = false
self . tableView . reloadData ( )
}
func searchBarSearchButtonClicked ( _ searchBar : UISearchBar ) {
if let searchText = searchBar . text , searchText . isEmpty = = false {
filteredPasswordsTableEntries = passwordsTableEntries . filter { entry in
var matched = false
matched = matched || entry . title . range ( of : searchText , options : . caseInsensitive ) != nil
matched = matched || searchText . range ( of : entry . title , options : . caseInsensitive ) != nil
entry . categoryArray . forEach ( { ( category ) in
matched = matched || category . range ( of : searchText , options : . caseInsensitive ) != nil
matched = matched || searchText . range ( of : category , options : . caseInsensitive ) != nil
} )
return matched
}
searchActive = true
} else {
searchActive = false
}
self . tableView . reloadData ( )
}
func searchBar ( _ searchBar : UISearchBar , textDidChange searchText : String ) {
searchBarSearchButtonClicked ( searchBar )
}
private func getPasswordEntry ( by indexPath : IndexPath ) -> PasswordsTableEntry {
if searchActive {
return filteredPasswordsTableEntries [ indexPath . row ]
} else {
return passwordsTableEntries [ indexPath . row ]
}
2018-09-24 15:06:43 +08:00
}
}