diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index f1f55ba..899d17e 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 2C58F31EECC494C7A7F00A98 /* libPods-passKitTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB6C63FA1652F925B5C9F0B5 /* libPods-passKitTests.a */; }; 398A8F69C2230A8117820BB7 /* libPods-passKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 45BAA15189E80AA544EAF7AD /* libPods-passKit.a */; }; 6930A9D26085DE7CA1A7AACC /* libPods-passExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B240CA444AC9172F3053651 /* libPods-passExtension.a */; }; + A2168A7F1EFD40D5005EA873 /* OnePasswordExtensionConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2168A7E1EFD40D5005EA873 /* OnePasswordExtensionConstants.swift */; }; A217ACE21E9AB17C00A1A6CF /* OTPScannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A217ACE11E9AB17C00A1A6CF /* OTPScannerController.swift */; }; A217ACE41E9BBBBD00A1A6CF /* GitConfigSettingTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A217ACE31E9BBBBD00A1A6CF /* GitConfigSettingTableViewController.swift */; }; A2367B9B1EEFE1B300C8FE8B /* UtilsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2367B9A1EEFE1B300C8FE8B /* UtilsExtension.swift */; }; @@ -166,6 +167,7 @@ 7592A214C22CEBBEF4596CC1 /* libPods-pass.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-pass.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 7E088A9255B6CB576EF757C1 /* Pods-passKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-passKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-passKit/Pods-passKit.debug.xcconfig"; sourceTree = ""; }; A02ACA4077630047EA669D05 /* Pods-pass.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-pass.debug.xcconfig"; path = "Pods/Target Support Files/Pods-pass/Pods-pass.debug.xcconfig"; sourceTree = ""; }; + A2168A7E1EFD40D5005EA873 /* OnePasswordExtensionConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnePasswordExtensionConstants.swift; sourceTree = ""; }; A217ACE11E9AB17C00A1A6CF /* OTPScannerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTPScannerController.swift; sourceTree = ""; }; A217ACE31E9BBBBD00A1A6CF /* GitConfigSettingTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = GitConfigSettingTableViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; A2227D4C1EEE5E25002A69A9 /* libObjectivePGP.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libObjectivePGP.a; path = "Pods/../build/Debug-iphoneos/ObjectivePGP/libObjectivePGP.a"; sourceTree = ""; }; @@ -326,6 +328,24 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + A2168A801EFD431A005EA873 /* Controllers */ = { + isa = PBXGroup; + children = ( + A2A61C2B1EEFDF3300CFE063 /* ExtensionViewController.swift */, + A28C66671EF10EC900A398A1 /* PasscodeExtensionDisplay.swift */, + ); + name = Controllers; + sourceTree = ""; + }; + A2168A811EFD4322005EA873 /* Helpers */ = { + isa = PBXGroup; + children = ( + A2367B9A1EEFE1B300C8FE8B /* UtilsExtension.swift */, + A2168A7E1EFD40D5005EA873 /* OnePasswordExtensionConstants.swift */, + ); + name = Helpers; + sourceTree = ""; + }; A26075791EEC6F34005DB03E /* passKit */ = { isa = PBXGroup; children = ( @@ -350,14 +370,13 @@ A26700251EEC466A00176B8A /* passExtension */ = { isa = PBXGroup; children = ( + A26700331EEC46C900176B8A /* passExtension.entitlements */, + A267002B1EEC466A00176B8A /* Info.plist */, + A2168A801EFD431A005EA873 /* Controllers */, + A2168A811EFD4322005EA873 /* Helpers */, A2367B9F1EF0387000C8FE8B /* Assets.xcassets */, A26700351EEC475600176B8A /* passProcessor.js */, - A26700331EEC46C900176B8A /* passExtension.entitlements */, A26700281EEC466A00176B8A /* MainInterface.storyboard */, - A267002B1EEC466A00176B8A /* Info.plist */, - A2A61C2B1EEFDF3300CFE063 /* ExtensionViewController.swift */, - A2367B9A1EEFE1B300C8FE8B /* UtilsExtension.swift */, - A28C66671EF10EC900A398A1 /* PasscodeExtensionDisplay.swift */, ); path = passExtension; sourceTree = ""; @@ -1010,6 +1029,7 @@ buildActionMask = 2147483647; files = ( A2A61C2C1EEFDF3300CFE063 /* ExtensionViewController.swift in Sources */, + A2168A7F1EFD40D5005EA873 /* OnePasswordExtensionConstants.swift in Sources */, A28C66681EF10EC900A398A1 /* PasscodeExtensionDisplay.swift in Sources */, A2367B9B1EEFE1B300C8FE8B /* UtilsExtension.swift in Sources */, ); diff --git a/pass/Info.plist b/pass/Info.plist index 584eb13..01bea53 100644 --- a/pass/Info.plist +++ b/pass/Info.plist @@ -18,6 +18,15 @@ APPL CFBundleShortVersionString 0.2.8 + CFBundleURLTypes + + + CFBundleURLSchemes + + org-appextension-feature-password-management + + + CFBundleVersion 1 ITSAppUsesNonExemptEncryption diff --git a/passExtension/ExtensionViewController.swift b/passExtension/ExtensionViewController.swift index 17e55af..fa8871a 100644 --- a/passExtension/ExtensionViewController.swift +++ b/passExtension/ExtensionViewController.swift @@ -26,13 +26,15 @@ class ExtensionViewController: UIViewController, UITableViewDataSource, UITableV private let passwordStore = PasswordStore.shared private var searchActive = false - - // the URL passed to the extension - private var extensionURL: String? - private var passwordsTableEntries: [PasswordsTableEntry] = [] private var filteredPasswordsTableEntries: [PasswordsTableEntry] = [] + enum Action { + case findLogin, fillBrowser, unknown + } + + private var extensionAction = Action.unknown + private lazy var passcodelock: PasscodeExtensionDisplay = { let passcodelock = PasscodeExtensionDisplay(extensionContext: self.extensionContext) return passcodelock @@ -64,24 +66,47 @@ class ExtensionViewController: UIViewController, UITableViewDataSource, UITableV // initialize table entries initPasswordsTableEntries() - // search using the extensionContext inputs + // get the provider let item = extensionContext?.inputItems.first as! NSExtensionItem - let provider = item.attachments?.first as! NSItemProvider - let propertyList = String(kUTTypePropertyList) - if provider.hasItemConformingToTypeIdentifier(propertyList) { - provider.loadItem(forTypeIdentifier: propertyList, options: nil, completionHandler: { (item, error) -> Void in + let provider: NSItemProvider + if item.attachments?.count == 1 { + // Safari + provider = item.attachments?.first as! NSItemProvider + } else { + // e.g., Chrome, apps + provider = item.attachments?[1] as! NSItemProvider + } + + // search using the extensionContext inputs + if provider.hasItemConformingToTypeIdentifier(OnePasswordExtensionActions.findLogin) { + provider.loadItem(forTypeIdentifier: OnePasswordExtensionActions.findLogin, options: nil, completionHandler: { (item, error) -> Void in let dictionary = item as! NSDictionary - let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as! NSDictionary - let url = URL(string: (results["url"] as? String)!)?.host + 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 + } DispatchQueue.main.async { [weak self] in + self?.extensionAction = .findLogin + // force search (set text, set active, force search) + 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 // force search (set text, set active, force search) self?.searchBar.text = url self?.searchBar.becomeFirstResponder() self?.searchBarSearchButtonClicked((self?.searchBar)!) } }) - } else { - print("error") } } @@ -116,18 +141,28 @@ class ExtensionViewController: UIViewController, UITableViewDataSource, UITableV do { decryptedPassword = try self.passwordStore.decrypt(passwordEntity: passwordEntity, requestPGPKeyPassphrase: self.requestPGPKeyPassphrase) DispatchQueue.main.async { - 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 - // return a dictionary for JavaScript for best-effor fill in + switch self.extensionAction { + case .findLogin: + // prepare a dictionary to return let extensionItem = NSExtensionItem() - let returnDictionary = [ NSExtensionJavaScriptFinalizeArgumentKey : ["username": decryptedPassword?.getUsername() ?? "", "password": decryptedPassword?.password ?? ""]] + var returnDictionary = [OnePasswordExtensionKey.usernameKey: decryptedPassword?.getUsername() ?? "", + OnePasswordExtensionKey.passwordKey: decryptedPassword?.password ?? ""] + if let totpPassword = decryptedPassword?.getOtp() { + returnDictionary[OnePasswordExtensionKey.totpKey] = totpPassword + } extensionItem.attachments = [NSItemProvider(item: returnDictionary as NSSecureCoding, typeIdentifier: String(kUTTypePropertyList))] self.extensionContext!.completeRequest(returningItems: [extensionItem], completionHandler: nil) - })) - self.present(alert, animated: true, completion: nil) + default: + // copy the password to the clipboard + 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) + } } } catch { print(error) diff --git a/passExtension/Info.plist b/passExtension/Info.plist index 077ef2c..db41ae3 100644 --- a/passExtension/Info.plist +++ b/passExtension/Info.plist @@ -28,11 +28,9 @@ NSExtensionActivationSupportsText - NSExtensionActivationSupportsWebPageWithMaxCount - 100 + NSExtensionActivationSupportsWebURLWithMaxCount + 1 - NSExtensionJavaScriptPreprocessingFile - passProcessor NSExtensionMainStoryboard MainInterface diff --git a/passExtension/OnePasswordExtensionConstants.swift b/passExtension/OnePasswordExtensionConstants.swift new file mode 100644 index 0000000..894b921 --- /dev/null +++ b/passExtension/OnePasswordExtensionConstants.swift @@ -0,0 +1,51 @@ +// +// OnePasswordExtensionConstants.swift +// pass +// +// Created by Yishi Lin on 2017/6/23. +// Copyright © 2017 Bob Sun. All rights reserved. +// +// This file contains constants from https://github.com/agilebits/onepassword-app-extension/ + +class OnePasswordExtensionActions { + static let findLogin = "org.appextension.find-login-action" + static let saveLogin = "org.appextension.save-login-action" + static let changePassword = "org.appextension.change-password-action" + static let fillWebView = "org.appextension.fill-webview-action" + static let fillBrowser = "org.appextension.fill-browser-action" +} + +class OnePasswordExtensionKey { + // Login Dictionary keys - Used to get or set the properties of a 1Password Login + static let URLStringKey = "url_string" + static let usernameKey = "username" + static let passwordKey = "password" + static let totpKey = "totp" + static let titleKey = "login_title" + static let notesKey = "notes" + static let sectionTitleKey = "section_title" + static let fieldsKey = "fields" + static let returnedFieldsKey = "returned_fields" + static let oldPasswordKey = "old_password" + static let passwordGeneratorOptionsKey = "password_generator_options" + + // Password Generator options - Used to set the 1Password Password Generator options when saving a new Login or when changing the password for for an existing Login + static let generatedPasswordMinLengthKey = "password_min_length" + static let generatedPasswordMaxLengthKey = "password_max_length" + static let generatedPasswordRequireDigitsKey = "password_require_digits" + static let generatedPasswordRequireSymbolsKey = "password_require_symbols" + static let generatedPasswordForbiddenCharactersKey = "password_forbidden_characters" +} + +// Errors codes +class OnePasswordExtensionError { + static let errorDomain = "OnePasswordExtension" + static let errorCodeCancelledByUser = 0 + static let errorCodeAPINotAvailable = 1 + static let errorCodeFailedToContactExtension = 2 + static let errorCodeFailedToLoadItemProviderData = 3 + static let errorCodeCollectFieldsScriptFailed = 4 + static let errorCodeFillFieldsScriptFailed = 5 + static let errorCodeUnexpectedData = 6 + static let errorCodeFailedToObtainURLStringFromWebView = 7 +} diff --git a/passExtension/passextension.entitlements b/passExtension/passExtension.entitlements similarity index 100% rename from passExtension/passextension.entitlements rename to passExtension/passExtension.entitlements