From 8e5a824ccabb5ebb142e991b6b000db996d8c95e Mon Sep 17 00:00:00 2001 From: Yishi Lin Date: Wed, 14 Jun 2017 00:25:38 +0800 Subject: [PATCH] Add search and copy password in extension - no lock screen for now - share keychain between app and extension --- pass.xcodeproj/project.pbxproj | 30 ++- pass/pass.entitlements | 4 + passKit/Helpers/AppError.swift | 2 +- passKit/Helpers/Utils.swift | 10 +- passKit/Models/PasswordStore.swift | 20 +- passextension/ActionViewController.swift | 72 ------- .../Base.lproj/MainInterface.storyboard | 129 +++++++---- passextension/ExtensionViewController.swift | 203 ++++++++++++++++++ passextension/UtilsExtension.swift | 18 ++ passextension/passProcessor.js | 2 +- passextension/passextension.entitlements | 4 + 11 files changed, 362 insertions(+), 132 deletions(-) delete mode 100644 passextension/ActionViewController.swift create mode 100644 passextension/ExtensionViewController.swift create mode 100644 passextension/UtilsExtension.swift diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index 297d95a..62eb3b0 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -12,6 +12,9 @@ 6930A9D26085DE7CA1A7AACC /* libPods-passextension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B240CA444AC9172F3053651 /* libPods-passextension.a */; }; 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 */; }; + A2367B9C1EEFE2E500C8FE8B /* SwiftyUserDefaults.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCA049951E3357E000522E8F /* SwiftyUserDefaults.framework */; }; + A2367BA01EF0387000C8FE8B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A2367B9F1EF0387000C8FE8B /* Assets.xcassets */; }; A26075811EEC6F34005DB03E /* passKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A26075781EEC6F34005DB03E /* passKit.framework */; }; A26075881EEC6F34005DB03E /* passKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A26075871EEC6F34005DB03E /* passKitTests.swift */; }; A260758A1EEC6F34005DB03E /* passKit.h in Headers */ = {isa = PBXBuildFile; fileRef = A260757A1EEC6F34005DB03E /* passKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -19,10 +22,8 @@ A260758E1EEC6F34005DB03E /* passKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A26075781EEC6F34005DB03E /* passKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; A26075961EEC6F8C005DB03E /* passKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A26075781EEC6F34005DB03E /* passKit.framework */; }; A26075AD1EEC7125005DB03E /* pass.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = A26075A51EEC7125005DB03E /* pass.xcdatamodeld */; }; - A26700271EEC466A00176B8A /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A26700261EEC466A00176B8A /* ActionViewController.swift */; }; A267002A1EEC466A00176B8A /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A26700281EEC466A00176B8A /* MainInterface.storyboard */; }; A267002E1EEC466A00176B8A /* passextension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = A26700241EEC466A00176B8A /* passextension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - A26700361EEC475600176B8A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A26700341EEC475600176B8A /* Assets.xcassets */; }; A26700371EEC475600176B8A /* passProcessor.js in Resources */ = {isa = PBXBuildFile; fileRef = A26700351EEC475600176B8A /* passProcessor.js */; }; A2802BF91E70813A00879216 /* SliderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2802BF71E70813A00879216 /* SliderTableViewCell.swift */; }; A2802BFA1E70813A00879216 /* SliderTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = A2802BF81E70813A00879216 /* SliderTableViewCell.xib */; }; @@ -31,8 +32,8 @@ A2A61C151EEF90CB00CFE063 /* KeychainAccess.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCA742D91E599ED400D54E16 /* KeychainAccess.framework */; }; A2A61C161EEF90CB00CFE063 /* ObjectiveGit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC1208571E35EBE60042942E /* ObjectiveGit.framework */; }; A2A61C171EEF90CB00CFE063 /* OneTimePassword.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCA671DE1E7A73B100D3ABE1 /* OneTimePassword.framework */; }; - A2A61C1A1EEF90CB00CFE063 /* SwiftyUserDefaults.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCA049951E3357E000522E8F /* SwiftyUserDefaults.framework */; }; A2A61C201EEFABAD00CFE063 /* UtilsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A61C1F1EEFABAD00CFE063 /* UtilsExtension.swift */; }; + A2A61C2C1EEFDF3300CFE063 /* ExtensionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A61C2B1EEFDF3300CFE063 /* ExtensionViewController.swift */; }; A2A7813F1E97DBD9001311F5 /* QRScannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A7813E1E97DBD9001311F5 /* QRScannerController.swift */; }; A2F4E2141EED800F0011986E /* GitCredential.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2F4E2101EED800F0011986E /* GitCredential.swift */; }; A2F4E2151EED800F0011986E /* Password.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2F4E2111EED800F0011986E /* Password.swift */; }; @@ -168,6 +169,8 @@ A2227D541EEE5E78002A69A9 /* libObjectivePGP.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libObjectivePGP.a; path = "../../Library/Developer/Xcode/DerivedData/pass-fwlmfsjroyvbfhdyqmglrwfhvjli/Build/Products/Debug-iphonesimulator/ObjectivePGP/libObjectivePGP.a"; sourceTree = ""; }; A2227D551EEE5E78002A69A9 /* libPods-pass.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPods-pass.a"; path = "../../Library/Developer/Xcode/DerivedData/pass-fwlmfsjroyvbfhdyqmglrwfhvjli/Build/Products/Debug-iphonesimulator/libPods-pass.a"; sourceTree = ""; }; A2227D561EEE5E78002A69A9 /* libPods-passKit.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPods-passKit.a"; path = "../../Library/Developer/Xcode/DerivedData/pass-fwlmfsjroyvbfhdyqmglrwfhvjli/Build/Products/Debug-iphonesimulator/libPods-passKit.a"; sourceTree = ""; }; + A2367B9A1EEFE1B300C8FE8B /* UtilsExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UtilsExtension.swift; sourceTree = ""; }; + A2367B9F1EF0387000C8FE8B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A26075781EEC6F34005DB03E /* passKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = passKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A260757A1EEC6F34005DB03E /* passKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = passKit.h; sourceTree = ""; }; A260757B1EEC6F34005DB03E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -177,18 +180,17 @@ A26075A61EEC7125005DB03E /* pass.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = pass.xcdatamodel; sourceTree = ""; }; A262A58C1E68749C006B0890 /* Base32.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Base32.framework; path = Carthage/Build/iOS/Base32.framework; sourceTree = ""; }; A26700241EEC466A00176B8A /* passextension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = passextension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - A26700261EEC466A00176B8A /* ActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionViewController.swift; sourceTree = ""; }; A26700291EEC466A00176B8A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; A267002B1EEC466A00176B8A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A26700321EEC46C400176B8A /* pass.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = pass.entitlements; sourceTree = ""; }; A26700331EEC46C900176B8A /* passextension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passextension.entitlements; sourceTree = ""; }; - A26700341EEC475600176B8A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A26700351EEC475600176B8A /* passProcessor.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = passProcessor.js; sourceTree = ""; }; A2802BF71E70813A00879216 /* SliderTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderTableViewCell.swift; sourceTree = ""; }; A2802BF81E70813A00879216 /* SliderTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SliderTableViewCell.xib; sourceTree = ""; }; A2A61C0C1EEF8DFE00CFE063 /* libPods-passextension.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPods-passextension.a"; path = "../../Library/Developer/Xcode/DerivedData/pass-fwlmfsjroyvbfhdyqmglrwfhvjli/Build/Products/Debug-iphonesimulator/libPods-passextension.a"; sourceTree = ""; }; A2A61C101EEF8E3500CFE063 /* libPods-passKit.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPods-passKit.a"; path = "Pods/../build/Debug-iphoneos/libPods-passKit.a"; sourceTree = ""; }; A2A61C1F1EEFABAD00CFE063 /* UtilsExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UtilsExtension.swift; sourceTree = ""; }; + A2A61C2B1EEFDF3300CFE063 /* ExtensionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtensionViewController.swift; sourceTree = ""; }; A2A7813E1E97DBD9001311F5 /* QRScannerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRScannerController.swift; sourceTree = ""; }; A2BC54C71EEE5669001FAFBD /* Objective-CBridgingHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Objective-CBridgingHeader.h"; sourceTree = ""; }; A2F4E2101EED800F0011986E /* GitCredential.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GitCredential.swift; path = Models/GitCredential.swift; sourceTree = ""; }; @@ -279,7 +281,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A2A61C2A1EEFDCA700CFE063 /* SwiftyUserDefaults.framework in Frameworks */, + A2367B9C1EEFE2E500C8FE8B /* SwiftyUserDefaults.framework in Frameworks */, A2A61C131EEF90CB00CFE063 /* Base32.framework in Frameworks */, A2A61C151EEF90CB00CFE063 /* KeychainAccess.framework in Frameworks */, A2A61C161EEF90CB00CFE063 /* ObjectiveGit.framework in Frameworks */, @@ -334,12 +336,13 @@ A26700251EEC466A00176B8A /* passextension */ = { isa = PBXGroup; children = ( - A26700341EEC475600176B8A /* Assets.xcassets */, + A2367B9F1EF0387000C8FE8B /* Assets.xcassets */, A26700351EEC475600176B8A /* passProcessor.js */, A26700331EEC46C900176B8A /* passextension.entitlements */, - A26700261EEC466A00176B8A /* ActionViewController.swift */, A26700281EEC466A00176B8A /* MainInterface.storyboard */, A267002B1EEC466A00176B8A /* Info.plist */, + A2A61C2B1EEFDF3300CFE063 /* ExtensionViewController.swift */, + A2367B9A1EEFE1B300C8FE8B /* UtilsExtension.swift */, ); path = passextension; sourceTree = ""; @@ -675,6 +678,9 @@ com.apple.ApplicationGroups.iOS = { enabled = 1; }; + com.apple.Keychain = { + enabled = 1; + }; }; }; DC13B14D1E8640810097803F = { @@ -691,6 +697,9 @@ com.apple.ApplicationGroups.iOS = { enabled = 1; }; + com.apple.Keychain = { + enabled = 1; + }; }; }; }; @@ -736,8 +745,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - A26700361EEC475600176B8A /* Assets.xcassets in Resources */, A26700371EEC475600176B8A /* passProcessor.js in Resources */, + A2367BA01EF0387000C8FE8B /* Assets.xcassets in Resources */, A267002A1EEC466A00176B8A /* MainInterface.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -929,7 +938,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - A26700271EEC466A00176B8A /* ActionViewController.swift in Sources */, + A2A61C2C1EEFDF3300CFE063 /* ExtensionViewController.swift in Sources */, + A2367B9B1EEFE1B300C8FE8B /* UtilsExtension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/pass/pass.entitlements b/pass/pass.entitlements index 30d4695..13c5967 100644 --- a/pass/pass.entitlements +++ b/pass/pass.entitlements @@ -6,5 +6,9 @@ group.me.mssun.passforios + keychain-access-groups + + $(AppIdentifierPrefix)group.me.mssun.passforios + diff --git a/passKit/Helpers/AppError.swift b/passKit/Helpers/AppError.swift index d6a8397..6190c66 100644 --- a/passKit/Helpers/AppError.swift +++ b/passKit/Helpers/AppError.swift @@ -8,7 +8,7 @@ import Foundation -enum AppError: Error { +public enum AppError: Error { case RepositoryNotSetError case RepositoryRemoteMasterNotFoundError case KeyImportError diff --git a/passKit/Helpers/Utils.swift b/passKit/Helpers/Utils.swift index 9959d1e..785f892 100644 --- a/passKit/Helpers/Utils.swift +++ b/passKit/Helpers/Utils.swift @@ -63,7 +63,7 @@ public class Utils { } public static func getPasswordFromKeychain(name: String) -> String? { - let keychain = Keychain(service: Globals.bundleIdentifier) + let keychain = Keychain(service: Globals.bundleIdentifier, accessGroup: Globals.groupIdentifier) do { return try keychain.getString(name) } catch { @@ -73,19 +73,21 @@ public class Utils { } public static func addPasswordToKeychain(name: String, password: String?) { - let keychain = Keychain(service: Globals.bundleIdentifier) + let keychain = Keychain(service: Globals.bundleIdentifier, accessGroup: Globals.groupIdentifier) keychain[name] = password } + public static func removeKeychain(name: String) { - let keychain = Keychain(service: Globals.bundleIdentifier) + let keychain = Keychain(service: Globals.bundleIdentifier, accessGroup: Globals.groupIdentifier) do { try keychain.remove(name) } catch { print(error) } } + public static func removeAllKeychain() { - let keychain = Keychain(service: Globals.bundleIdentifier) + let keychain = Keychain(service: Globals.bundleIdentifier, accessGroup: Globals.groupIdentifier) do { try keychain.removeAll() } catch { diff --git a/passKit/Models/PasswordStore.swift b/passKit/Models/PasswordStore.swift index 751c014..941669c 100644 --- a/passKit/Models/PasswordStore.swift +++ b/passKit/Models/PasswordStore.swift @@ -12,6 +12,7 @@ import UIKit import SwiftyUserDefaults import ObjectiveGit import ObjectivePGP +import KeychainAccess public class PasswordStore { public static let shared = PasswordStore() @@ -116,7 +117,8 @@ public class PasswordStore { print(Globals.libraryPath) print(Globals.documentPathLegacy) print(Globals.libraryPathLegacy) - migration() + migrateIfNeeded() + do { if fm.fileExists(atPath: storeURL.path) { try storeRepository = GTRepository.init(url: storeURL) @@ -127,15 +129,29 @@ public class PasswordStore { } } - private func migration() { + private func migrateIfNeeded() { let needMigration = fm.fileExists(atPath: Globals.documentPathLegacy) && !fm.fileExists(atPath: Globals.documentPath) && fm.fileExists(atPath: Globals.libraryPathLegacy) && !fm.fileExists(atPath: Globals.libraryPath) guard needMigration == true else { return } do { + // migrate files try fm.moveItem(atPath: Globals.documentPathLegacy, toPath: Globals.documentPath) try fm.moveItem(atPath: Globals.libraryPathLegacy, toPath: Globals.libraryPath) + // migrate Defaults SharedDefaults = Defaults + // migrate Keychain + let keychainLegacy = Keychain(service: Globals.bundleIdentifier) + if let pgpPassphrase = try keychainLegacy.getString("pgpKeyPassphrase") { + Utils.addPasswordToKeychain(name: "pgpKeyPassphrase", password: pgpPassphrase) + } + if let gitSSHPrivateKeyPassphrase = try keychainLegacy.getString("gitSSHPrivateKeyPassphrase") { + Utils.addPasswordToKeychain(name: "gitSSHPrivateKeyPassphrase", password: gitSSHPrivateKeyPassphrase) + } + if let gitPassword = try keychainLegacy.getString("gitPassword") { + Utils.addPasswordToKeychain(name: "gitPassword", password: gitPassword) + } + try keychainLegacy.removeAll() } catch { print("Cannot migrate: \(error)") } diff --git a/passextension/ActionViewController.swift b/passextension/ActionViewController.swift deleted file mode 100644 index d4e17b7..0000000 --- a/passextension/ActionViewController.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// ActionViewController.swift -// passforiosextension -// -// Created by Yishi Lin on 9/6/17. -// Copyright © 2017 Yishi Lin. All rights reserved. -// - -import UIKit -import MobileCoreServices -import passKit - -class ActionViewController: UIViewController { - - @IBOutlet weak var textView: UITextView! - let passwordStore = PasswordStore.shared - - override func viewDidLoad() { - super.viewDidLoad() - - 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 dictionary = item as! NSDictionary - let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as! NSDictionary - let url = URL(string: (results["url"] as? String)!)?.host - - let numberFormatter = NumberFormatter() - numberFormatter.numberStyle = NumberFormatter.Style.decimal - - let numberOfPasswordsString = "Number of password:" + numberFormatter.string(from: NSNumber(value: self.passwordStore.numberOfPasswords))! - let sizeOfRepositoryString = "Size of repo:" + ByteCountFormatter.string(fromByteCount: Int64(self.passwordStore.sizeOfRepositoryByteCount), countStyle: ByteCountFormatter.CountStyle.file) - var numberOfCommits: UInt = 0 - - do { - if let _ = try self.passwordStore.storeRepository?.currentBranch().oid { - numberOfCommits = self.passwordStore.storeRepository?.numberOfCommits(inCurrentBranch: NSErrorPointer(nilLiteral: ())) ?? 0 - } - } catch { - print(error) - } - let numberOfCommitsString = "Number of commits:" + numberFormatter.string(from: NSNumber(value: numberOfCommits))! - - let gitURL = SharedDefaults[.gitURL]! - - DispatchQueue.main.async { [weak self] in - self?.textView.text = url! - print(numberOfPasswordsString) - print(numberOfCommitsString) - print(sizeOfRepositoryString) - print(gitURL) - } - }) - } else { - print("error") - } - } - - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. - } - - @IBAction func done() { - // Return any edited content to the host app. - // This template doesn't do anything, so we just echo the passed in items. - self.extensionContext!.completeRequest(returningItems: self.extensionContext!.inputItems, completionHandler: nil) - } - -} diff --git a/passextension/Base.lproj/MainInterface.storyboard b/passextension/Base.lproj/MainInterface.storyboard index e65440a..974a830 100644 --- a/passextension/Base.lproj/MainInterface.storyboard +++ b/passextension/Base.lproj/MainInterface.storyboard @@ -1,72 +1,117 @@ - + - + - - + + - + - - + + - - + + - - - - - - - - - - - - - - + + - - + + + + + + - - - + + + + + + + + + + + + + + + - + - - - - - - - + + + + - - + + + + + + + - - + + + - + + + + + + + + + - + + + + + + + + + + + + + + + + + + + diff --git a/passextension/ExtensionViewController.swift b/passextension/ExtensionViewController.swift new file mode 100644 index 0000000..35b6611 --- /dev/null +++ b/passextension/ExtensionViewController.swift @@ -0,0 +1,203 @@ +// +// PasswordsViewController.swift +// pass +// +// Created by Yishi Lin on 13/6/17. +// Copyright © 2017 Bob Sun. All rights reserved. +// + +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 + + // the URL passed to the extension + private var extensionURL: String? + + private var passwordsTableEntries: [PasswordsTableEntry] = [] + private var filteredPasswordsTableEntries: [PasswordsTableEntry] = [] + + 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) + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + // prepare + searchBar.delegate = self + tableView.delegate = self + tableView.dataSource = self + tableView.register(UINib(nibName: "PasswordWithFolderTableViewCell", bundle: nil), forCellReuseIdentifier: "passwordWithFolderTableViewCell") + + // initialize table entries + initPasswordsTableEntries() + + // search using the extensionContext inputs + 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 dictionary = item as! NSDictionary + let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as! NSDictionary + let url = URL(string: (results["url"] as? String)!)?.host + DispatchQueue.main.async { [weak self] in + // force search (set text, set active, force search) + self?.searchBar.text = url + self?.searchBar.becomeFirstResponder() + self?.searchBarSearchButtonClicked((self?.searchBar)!) + } + }) + } else { + print("error") + } + } + + // define cell contents, and set long press action + 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 + } + + // select row -> extension returns (with username and password) + 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 { + 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 + let extensionItem = NSExtensionItem() + let returnDictionary = [ NSExtensionJavaScriptFinalizeArgumentKey : ["username": decryptedPassword?.getUsername() ?? "", "password": decryptedPassword?.password ?? ""]] + extensionItem.attachments = [NSItemProvider(item: returnDictionary as NSSecureCoding, typeIdentifier: String(kUTTypePropertyList))] + self.extensionContext!.completeRequest(returningItems: [extensionItem], completionHandler: nil) + })) + self.present(alert, animated: true, completion: nil) + } + } catch { + print(error) + DispatchQueue.main.async { + // remove the wrong passphrase so that users could enter it next time + 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] + } + } +} diff --git a/passextension/UtilsExtension.swift b/passextension/UtilsExtension.swift new file mode 100644 index 0000000..9bab790 --- /dev/null +++ b/passextension/UtilsExtension.swift @@ -0,0 +1,18 @@ +// +// UtilsExtension.swift +// pass +// +// Created by Yishi Lin on 13/6/17. +// Copyright © 2017 Bob Sun. All rights reserved. +// + +import Foundation +import passKit + +extension Utils { + static func alert(title: String, message: String, controller: UIViewController, handler: ((UIAlertAction) -> Void)? = nil, completion: (() -> Void)? = nil) { + let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertControllerStyle.alert) + alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: handler)) + controller.present(alert, animated: true, completion: completion) + } +} diff --git a/passextension/passProcessor.js b/passextension/passProcessor.js index 0a9d423..6db3d04 100644 --- a/passextension/passProcessor.js +++ b/passextension/passProcessor.js @@ -17,7 +17,7 @@ run: function(arguments) { finalize: function(arguments) { var str = "username: " + arguments["username"] + "\r\npassword: " + arguments["password"]; - alert(str) + // alert(str) // document.body.innerHTML = arguments["content"]; } }; diff --git a/passextension/passextension.entitlements b/passextension/passextension.entitlements index 30d4695..13c5967 100644 --- a/passextension/passextension.entitlements +++ b/passextension/passextension.entitlements @@ -6,5 +6,9 @@ group.me.mssun.passforios + keychain-access-groups + + $(AppIdentifierPrefix)group.me.mssun.passforios +