2017-06-14 00:25:38 +08:00
//
// P a s s w o r d s V i e w C o n t r o l l e r . s w i f t
// p a s s
//
// C r e a t e d b y Y i s h i L i n o n 1 3 / 6 / 1 7 .
// C o p y r i g h t © 2 0 1 7 B o b S u n . A l l r i g h t s r e s e r v e d .
//
import Foundation
import MobileCoreServices
import passKit
fileprivate class PasswordsTableEntry : NSObject {
var title : String
var passwordEntity : PasswordEntity ?
init ( title : String , passwordEntity : PasswordEntity ? ) {
self . title = title
self . passwordEntity = passwordEntity
}
}
class ExtensionViewController : UIViewController , UITableViewDataSource , UITableViewDelegate , UISearchBarDelegate , UINavigationBarDelegate {
@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 ] = [ ]
2017-06-23 21:57:03 +08:00
enum Action {
case findLogin , fillBrowser , unknown
}
private var extensionAction = Action . unknown
2017-06-14 19:14:56 +08:00
private lazy var passcodelock : PasscodeExtensionDisplay = {
let passcodelock = PasscodeExtensionDisplay ( extensionContext : self . extensionContext )
return passcodelock
} ( )
2017-06-14 00:25:38 +08:00
private func initPasswordsTableEntries ( ) {
passwordsTableEntries . removeAll ( )
filteredPasswordsTableEntries . removeAll ( )
var passwordEntities = [ PasswordEntity ] ( )
passwordEntities = self . passwordStore . fetchPasswordEntityCoreData ( withDir : false )
passwordsTableEntries = passwordEntities . map {
PasswordsTableEntry ( title : $0 . name ! , passwordEntity : $0 )
}
}
2017-06-14 19:14:56 +08:00
override func viewWillAppear ( _ animated : Bool ) {
super . viewWillAppear ( animated )
passcodelock . presentPasscodeLockIfNeeded ( self )
}
2017-06-14 00:25:38 +08:00
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 ( )
2017-06-23 21:57:03 +08:00
// g e t t h e p r o v i d e r
2017-06-14 00:25:38 +08:00
let item = extensionContext ? . inputItems . first as ! NSExtensionItem
2017-06-23 21:57:03 +08:00
let provider : NSItemProvider
if item . attachments ? . count = = 1 {
// S a f a r i
provider = item . attachments ? . first as ! NSItemProvider
} else {
// e . g . , C h r o m e , a p p s
provider = item . attachments ? [ 1 ] as ! NSItemProvider
}
// s e a r c h u s i n g t h e e x t e n s i o n C o n t e x t i n p u t s
if provider . hasItemConformingToTypeIdentifier ( OnePasswordExtensionActions . findLogin ) {
provider . loadItem ( forTypeIdentifier : OnePasswordExtensionActions . findLogin , options : nil , completionHandler : { ( item , error ) -> Void in
2017-06-14 00:25:38 +08:00
let dictionary = item as ! NSDictionary
2017-06-23 21:57:03 +08:00
var url : String ?
if var urlString = dictionary [ OnePasswordExtensionKey . URLStringKey ] as ? String {
if ! urlString . hasPrefix ( " http:// " ) && ! urlString . hasPrefix ( " https:// " ) {
urlString = " http:// " + urlString
}
url = URL ( string : urlString ) ? . host
}
2017-06-14 00:25:38 +08:00
DispatchQueue . main . async { [ weak self ] in
2017-06-23 21:57:03 +08:00
self ? . extensionAction = . findLogin
// f o r c e s e a r c h ( s e t t e x t , s e t a c t i v e , f o r c e s e a r c h )
self ? . searchBar . text = url
self ? . searchBar . becomeFirstResponder ( )
self ? . searchBarSearchButtonClicked ( ( self ? . searchBar ) ! )
}
} )
} else if provider . hasItemConformingToTypeIdentifier ( kUTTypeURL as String ) {
provider . loadItem ( forTypeIdentifier : kUTTypeURL as String , options : nil , completionHandler : { ( item , error ) -> Void in
let url = ( item as ? NSURL ) ! . host
DispatchQueue . main . async { [ weak self ] in
self ? . extensionAction = . fillBrowser
2017-06-14 00:25:38 +08:00
// f o r c e s e a r c h ( s e t t e x t , s e t a c t i v e , f o r c e s e a r c h )
self ? . searchBar . text = url
self ? . searchBar . becomeFirstResponder ( )
self ? . searchBarSearchButtonClicked ( ( self ? . searchBar ) ! )
}
} )
}
}
// 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 . passwordEntity ? . getCategoryText ( )
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 )
DispatchQueue . main . async {
2017-06-23 21:57:03 +08:00
switch self . extensionAction {
case . findLogin :
// 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
2017-06-14 00:25:38 +08:00
let extensionItem = NSExtensionItem ( )
2017-06-23 21:57:03 +08:00
var returnDictionary = [ OnePasswordExtensionKey . usernameKey : decryptedPassword ? . getUsername ( ) ? ? " " ,
OnePasswordExtensionKey . passwordKey : decryptedPassword ? . password ? ? " " ]
if let totpPassword = decryptedPassword ? . getOtp ( ) {
returnDictionary [ OnePasswordExtensionKey . totpKey ] = totpPassword
}
2017-06-14 00:25:38 +08:00
extensionItem . attachments = [ NSItemProvider ( item : returnDictionary as NSSecureCoding , typeIdentifier : String ( kUTTypePropertyList ) ) ]
self . extensionContext ! . completeRequest ( returningItems : [ extensionItem ] , completionHandler : nil )
2017-06-23 21:57:03 +08:00
default :
// c o p y t h e p a s s w o r d t o t h e c l i p b o a r d
Utils . copyToPasteboard ( textToCopy : decryptedPassword ? . password )
let title = " Password Copied "
let message = " Usename: " + ( decryptedPassword ? . getUsername ( ) ? ? " Unknown " ) + " \r \n (Remember to clear the clipboard.) "
let alert = UIAlertController ( title : title , message : message , preferredStyle : UIAlertControllerStyle . alert )
alert . addAction ( UIAlertAction ( title : " OK " , style : UIAlertActionStyle . default , handler : { _ in
self . extensionContext ! . completeRequest ( returningItems : nil , completionHandler : nil )
} ) )
self . present ( alert , animated : true , completion : nil )
}
2017-06-14 00:25:38 +08:00
}
} catch {
print ( error )
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 : UIAlertControllerStyle . alert )
alert . addAction ( UIAlertAction ( title : " OK " , style : UIAlertActionStyle . 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 [ . isRememberPassphraseOn ] {
self . passwordStore . pgpKeyPassphrase = passphrase
}
return passphrase
}
@IBAction func cancelExtension ( _ sender : Any ) {
extensionContext ! . completeRequest ( returningItems : [ ] , completionHandler : nil )
}
func searchBarCancelButtonClicked ( _ searchBar : UISearchBar ) {
searchBar . text = " "
searchActive = false
self . tableView . reloadData ( )
}
func searchBarSearchButtonClicked ( _ searchBar : UISearchBar ) {
if let searchText = searchBar . text , searchText . isEmpty = = false {
let searchTextLowerCased = searchText . lowercased ( )
filteredPasswordsTableEntries = passwordsTableEntries . filter { entry in
let entryTitle = entry . title . lowercased ( )
return entryTitle . contains ( searchTextLowerCased ) || searchTextLowerCased . contains ( entryTitle )
}
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 ]
}
}
}