From 730542d5bb6a73ff41956008a0b97c10d6375b93 Mon Sep 17 00:00:00 2001 From: Danny Moesch Date: Sun, 8 Sep 2019 23:00:46 +0200 Subject: [PATCH] Separate encryption/decryption logic for different frameworks used --- pass.xcodeproj/project.pbxproj | 24 +- .../AddPasswordTableViewController.swift | 2 +- .../GeneralSettingsTableViewController.swift | 2 +- ...GPKeyArmorSettingTableViewController.swift | 12 +- .../PGPKeySettingTableViewController.swift | 10 +- .../PasswordDetailTableViewController.swift | 4 +- .../Controllers/PasswordsViewController.swift | 11 +- .../SettingsTableViewController.swift | 45 ++-- pass/de.lproj/Localizable.strings | 1 + pass/en.lproj/Localizable.strings | 1 + .../CredentialProviderViewController.swift | 6 +- .../Controllers/ExtensionViewController.swift | 7 +- passKit/Crypto/GopenPgp.swift | 57 +++++ passKit/Crypto/ObjectivePgp.swift | 48 ++++ passKit/Crypto/PGPAgent.swift | 64 +++++ passKit/Crypto/PgpInterface.swift | 16 ++ passKit/Helpers/AppError.swift | 1 + passKit/Helpers/Globals.swift | 2 + passKit/Helpers/KeyFileManager.swift | 27 ++- passKit/Models/PGPAgent.swift | 223 ------------------ passKit/Models/PasswordStore.swift | 24 +- passKitTests/Crypto/PGPAgentTest.swift | 105 +++++++++ passKitTests/Helpers/KeyFileManagerTest.swift | 42 +++- passKitTests/Models/PGPAgentTest.swift | 108 --------- 24 files changed, 428 insertions(+), 414 deletions(-) create mode 100644 passKit/Crypto/GopenPgp.swift create mode 100644 passKit/Crypto/ObjectivePgp.swift create mode 100644 passKit/Crypto/PGPAgent.swift create mode 100644 passKit/Crypto/PgpInterface.swift delete mode 100644 passKit/Models/PGPAgent.swift create mode 100644 passKitTests/Crypto/PGPAgentTest.swift delete mode 100644 passKitTests/Models/PGPAgentTest.swift diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index 960b76c..9bc6e4e 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -63,6 +63,9 @@ 30C25DD721F4834D00BB27BB /* UILocalizedLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C25DD521F4834D00BB27BB /* UILocalizedLabel.swift */; }; 30C25DD821F4834D00BB27BB /* UICodeHighlightingLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C25DD621F4834D00BB27BB /* UICodeHighlightingLabel.swift */; }; 30CCA90B2325119C0048CA51 /* Data+Mutable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30CCA90A2325119C0048CA51 /* Data+Mutable.swift */; }; + 30CCA91623258C380048CA51 /* PgpInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30CCA91523258C380048CA51 /* PgpInterface.swift */; }; + 30CCA91823258E760048CA51 /* GopenPgp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30CCA91723258E760048CA51 /* GopenPgp.swift */; }; + 30CCA91A232591320048CA51 /* ObjectivePgp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30CCA919232591320048CA51 /* ObjectivePgp.swift */; }; 30FD2F78214D9E0E005E0A92 /* ParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FD2F77214D9E0E005E0A92 /* ParserTest.swift */; }; 3EA2386CD0E9CE2A702A0B3E /* Pods_pass.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FE627E8F3DACEDD8FA220081 /* Pods_pass.framework */; }; 556EC3D322335C5F00934F9C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 30BF5ECA21EA8FB5000E4154 /* Localizable.strings */; }; @@ -280,6 +283,9 @@ 30C25DD521F4834D00BB27BB /* UILocalizedLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UILocalizedLabel.swift; sourceTree = ""; }; 30C25DD621F4834D00BB27BB /* UICodeHighlightingLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UICodeHighlightingLabel.swift; sourceTree = ""; }; 30CCA90A2325119C0048CA51 /* Data+Mutable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Mutable.swift"; sourceTree = ""; }; + 30CCA91523258C380048CA51 /* PgpInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PgpInterface.swift; sourceTree = ""; }; + 30CCA91723258E760048CA51 /* GopenPgp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GopenPgp.swift; sourceTree = ""; }; + 30CCA919232591320048CA51 /* ObjectivePgp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectivePgp.swift; sourceTree = ""; }; 30FD2F77214D9E0E005E0A92 /* ParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParserTest.swift; sourceTree = ""; }; 3B2B2F844061EFA534FE9506 /* Pods_passKitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_passKitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 62DEE9943E0F2B8C79E3FC5B /* Pods-passExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-passExtension.release.xcconfig"; path = "Pods/Target Support Files/Pods-passExtension/Pods-passExtension.release.xcconfig"; sourceTree = ""; }; @@ -469,6 +475,7 @@ isa = PBXGroup; children = ( 30A86F94230F237000F821A4 /* CryptoFrameworkTest.swift */, + A2AA934522DE3A8000D79A00 /* PGPAgentTest.swift */, ); path = Crypto; sourceTree = ""; @@ -525,7 +532,6 @@ isa = PBXGroup; children = ( 30B0485F209A5141001013CA /* PasswordTest.swift */, - A2AA934522DE3A8000D79A00 /* PGPAgentTest.swift */, ); path = Models; sourceTree = ""; @@ -540,6 +546,17 @@ path = UserInterface; sourceTree = ""; }; + 30CCA90C232584560048CA51 /* Crypto */ = { + isa = PBXGroup; + children = ( + 30CCA91723258E760048CA51 /* GopenPgp.swift */, + 30CCA919232591320048CA51 /* ObjectivePgp.swift */, + A2AA934322DE30DD00D79A00 /* PGPAgent.swift */, + 30CCA91523258C380048CA51 /* PgpInterface.swift */, + ); + path = Crypto; + sourceTree = ""; + }; A2168A801EFD431A005EA873 /* Controllers */ = { isa = PBXGroup; children = ( @@ -580,6 +597,7 @@ A26075791EEC6F34005DB03E /* passKit */ = { isa = PBXGroup; children = ( + 30CCA90C232584560048CA51 /* Crypto */, A2C532B9201DD07500DB9F53 /* Controllers */, 30B6AABA21F49095006B352D /* Extensions */, A2F4E20F1EED7F0A0011986E /* Helpers */, @@ -638,7 +656,6 @@ 30697C4021F63CAB0064FCAC /* Password.swift */, 30697C3F21F63CAA0064FCAC /* PasswordEntity.swift */, 30697C4321F63CAB0064FCAC /* PasswordStore.swift */, - A2AA934322DE30DD00D79A00 /* PGPAgent.swift */, ); path = Models; sourceTree = ""; @@ -1293,10 +1310,12 @@ A2AA934422DE30DD00D79A00 /* PGPAgent.swift in Sources */, 30697C3B21F63C990064FCAC /* String+Localization.swift in Sources */, 302E85612125ECC70031BA64 /* Parser.swift in Sources */, + 30CCA91A232591320048CA51 /* ObjectivePgp.swift in Sources */, 30697C4621F63CAB0064FCAC /* GitCredential.swift in Sources */, 30A1D2A621B2D46100E2D1F7 /* OtpType.swift in Sources */, 3032328E22CBD4CD009EBD9C /* CryptographicKeys.swift in Sources */, 30697C2A21F63C5A0064FCAC /* NotificationNames.swift in Sources */, + 30CCA91623258C380048CA51 /* PgpInterface.swift in Sources */, 30697C4721F63CAB0064FCAC /* PasscodeLock.swift in Sources */, 30697C3421F63C8B0064FCAC /* PasscodeLockViewController.swift in Sources */, 30697C2C21F63C5A0064FCAC /* FileManagerExtension.swift in Sources */, @@ -1304,6 +1323,7 @@ 30697C3D21F63C990064FCAC /* UIViewExtension.swift in Sources */, 30697C3A21F63C990064FCAC /* UIViewControllerExtension.swift in Sources */, 30697C2E21F63C5A0064FCAC /* Utils.swift in Sources */, + 30CCA91823258E760048CA51 /* GopenPgp.swift in Sources */, 30697C4521F63CAB0064FCAC /* Password.swift in Sources */, 30697C4421F63CAB0064FCAC /* PasswordEntity.swift in Sources */, 30BAC8CD22E3BB9700438475 /* KeyStore.swift in Sources */, diff --git a/pass/Controllers/AddPasswordTableViewController.swift b/pass/Controllers/AddPasswordTableViewController.swift index 64dce89..3647912 100644 --- a/pass/Controllers/AddPasswordTableViewController.swift +++ b/pass/Controllers/AddPasswordTableViewController.swift @@ -31,7 +31,7 @@ class AddPasswordTableViewController: PasswordEditorTableViewController { override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { if identifier == "saveAddPasswordSegue" { // check PGP key - guard passwordStore.pgpAgent.isImported else { + guard PGPAgent.shared.isPrepared else { let alertTitle = "CannotAddPassword".localize() let alertMessage = "PgpKeyNotSet.".localize() Utils.alert(title: alertTitle, message: alertMessage, controller: self, completion: nil) diff --git a/pass/Controllers/GeneralSettingsTableViewController.swift b/pass/Controllers/GeneralSettingsTableViewController.swift index c63ed07..bb8cde7 100644 --- a/pass/Controllers/GeneralSettingsTableViewController.swift +++ b/pass/Controllers/GeneralSettingsTableViewController.swift @@ -221,7 +221,7 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController { @objc func rememberPGPPassphraseSwitchAction(_ sender: Any?) { SharedDefaults[.isRememberPGPPassphraseOn] = rememberPGPPassphraseSwitch.isOn if rememberPGPPassphraseSwitch.isOn == false { - passwordStore.pgpAgent.passphrase = nil + AppKeychain.shared.removeContent(for: Globals.pgpKeyPassphrase) } } diff --git a/pass/Controllers/PGPKeyArmorSettingTableViewController.swift b/pass/Controllers/PGPKeyArmorSettingTableViewController.swift index 6b85e30..119b661 100644 --- a/pass/Controllers/PGPKeyArmorSettingTableViewController.swift +++ b/pass/Controllers/PGPKeyArmorSettingTableViewController.swift @@ -15,9 +15,9 @@ class PGPKeyArmorSettingTableViewController: AutoCellHeightUITableViewController @IBOutlet weak var scanPublicKeyCell: UITableViewCell! @IBOutlet weak var scanPrivateKeyCell: UITableViewCell! - var pgpPassphrase: String? let passwordStore = PasswordStore.shared - + let keychain = AppKeychain.shared + class ScannedPGPKey { static let maxNumberOfGif = 100 enum KeyType { @@ -90,8 +90,6 @@ class PGPKeyArmorSettingTableViewController: AutoCellHeightUITableViewController override func viewDidLoad() { super.viewDidLoad() - - pgpPassphrase = passwordStore.pgpAgent.passphrase scanPublicKeyCell?.textLabel?.text = "ScanPublicKeyQrCodes".localize() scanPublicKeyCell?.textLabel?.textColor = Globals.blue @@ -116,7 +114,7 @@ class PGPKeyArmorSettingTableViewController: AutoCellHeightUITableViewController let savePassphraseAlert = UIAlertController(title: "Passphrase".localize(), message: "WantToSavePassphrase?".localize(), preferredStyle: UIAlertController.Style.alert) // no savePassphraseAlert.addAction(UIAlertAction(title: "No".localize(), style: UIAlertAction.Style.default) { _ in - self.pgpPassphrase = nil + self.keychain.removeContent(for: Globals.pgpKeyPassphrase) SharedDefaults[.isRememberPGPPassphraseOn] = false self.performSegue(withIdentifier: "savePGPKeySegue", sender: self) }) @@ -125,12 +123,12 @@ class PGPKeyArmorSettingTableViewController: AutoCellHeightUITableViewController // ask for the passphrase let alert = UIAlertController(title: "Passphrase".localize(), message: "FillInPgpPassphrase.".localize(), preferredStyle: UIAlertController.Style.alert) alert.addAction(UIAlertAction(title: "Ok".localize(), style: UIAlertAction.Style.default, handler: {_ in - self.pgpPassphrase = alert.textFields?.first?.text + self.keychain.add(string: alert.textFields?.first?.text, for: Globals.pgpKeyPassphrase) SharedDefaults[.isRememberPGPPassphraseOn] = true self.performSegue(withIdentifier: "savePGPKeySegue", sender: self) })) alert.addTextField(configurationHandler: {(textField: UITextField!) in - textField.text = self.pgpPassphrase + textField.text = self.keychain.get(for: Globals.pgpKeyPassphrase) textField.isSecureTextEntry = true }) self.present(alert, animated: true, completion: nil) diff --git a/pass/Controllers/PGPKeySettingTableViewController.swift b/pass/Controllers/PGPKeySettingTableViewController.swift index 95492dd..6498057 100644 --- a/pass/Controllers/PGPKeySettingTableViewController.swift +++ b/pass/Controllers/PGPKeySettingTableViewController.swift @@ -13,14 +13,14 @@ class PGPKeySettingTableViewController: AutoCellHeightUITableViewController { @IBOutlet weak var pgpPublicKeyURLTextField: UITextField! @IBOutlet weak var pgpPrivateKeyURLTextField: UITextField! - var pgpPassphrase: String? + let passwordStore = PasswordStore.shared + let keychain = AppKeychain.shared override func viewDidLoad() { super.viewDidLoad() pgpPublicKeyURLTextField.text = SharedDefaults[.pgpPublicKeyURL]?.absoluteString pgpPrivateKeyURLTextField.text = SharedDefaults[.pgpPrivateKeyURL]?.absoluteString - pgpPassphrase = passwordStore.pgpAgent.passphrase } private func validatePGPKeyURL(input: String?) -> Bool { @@ -43,7 +43,7 @@ class PGPKeySettingTableViewController: AutoCellHeightUITableViewController { let savePassphraseAlert = UIAlertController(title: "Passphrase".localize(), message: "WantToSavePassphrase?".localize(), preferredStyle: UIAlertController.Style.alert) // no savePassphraseAlert.addAction(UIAlertAction(title: "No".localize(), style: UIAlertAction.Style.default) { _ in - self.pgpPassphrase = nil + self.keychain.removeContent(for: Globals.pgpKeyPassphrase) SharedDefaults[.isRememberPGPPassphraseOn] = false self.performSegue(withIdentifier: "savePGPKeySegue", sender: self) }) @@ -52,12 +52,12 @@ class PGPKeySettingTableViewController: AutoCellHeightUITableViewController { // ask for the passphrase let alert = UIAlertController(title: "Passphrase".localize(), message: "FillInPgpPassphrase.".localize(), preferredStyle: UIAlertController.Style.alert) alert.addAction(UIAlertAction(title: "Ok".localize(), style: UIAlertAction.Style.default, handler: {_ in - self.pgpPassphrase = alert.textFields?.first?.text + self.keychain.add(string: alert.textFields?.first?.text, for: Globals.pgpKeyPassphrase) SharedDefaults[.isRememberPGPPassphraseOn] = true self.performSegue(withIdentifier: "savePGPKeySegue", sender: self) })) alert.addTextField(configurationHandler: {(textField: UITextField!) in - textField.text = self.pgpPassphrase + textField.text = self.keychain.get(for: Globals.pgpKeyPassphrase) textField.isSecureTextEntry = true }) self.present(alert, animated: true, completion: nil) diff --git a/pass/Controllers/PasswordDetailTableViewController.swift b/pass/Controllers/PasswordDetailTableViewController.swift index 11a91b0..d9c886f 100644 --- a/pass/Controllers/PasswordDetailTableViewController.swift +++ b/pass/Controllers/PasswordDetailTableViewController.swift @@ -103,7 +103,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni } let _ = sem.wait(timeout: DispatchTime.distantFuture) if SharedDefaults[.isRememberPGPPassphraseOn] { - self.passwordStore.pgpAgent.passphrase = passphrase + AppKeychain.shared.add(string: passphrase, for: Globals.pgpKeyPassphrase) } return passphrase } @@ -122,7 +122,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni } catch { DispatchQueue.main.async { // remove the wrong passphrase so that users could enter it next time - self.passwordStore.pgpAgent.passphrase = nil + AppKeychain.shared.removeContent(for: Globals.pgpKeyPassphrase) // alert: cancel or try again let alert = UIAlertController(title: "CannotShowPassword".localize(), message: error.localizedDescription, preferredStyle: UIAlertController.Style.alert) alert.addAction(UIAlertAction(title: "Cancel".localize(), style: UIAlertAction.Style.default) { _ in diff --git a/pass/Controllers/PasswordsViewController.swift b/pass/Controllers/PasswordsViewController.swift index 9ae28b2..315034f 100644 --- a/pass/Controllers/PasswordsViewController.swift +++ b/pass/Controllers/PasswordsViewController.swift @@ -27,6 +27,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV private var filteredPasswordsTableEntries: [PasswordsTableEntry] = [] private var parentPasswordEntity: PasswordEntity? = nil private let passwordStore = PasswordStore.shared + private let keychain = AppKeychain.shared private var tapTabBarTime: TimeInterval = 0 @@ -385,13 +386,13 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV SVProgressHUD.show(withStatus: "Decrypting".localize()) } if SharedDefaults[.isRememberPGPPassphraseOn] { - self.passwordStore.pgpAgent.passphrase = passphrase + keychain.add(string: passphrase, for: Globals.pgpKeyPassphrase) } return passphrase } private func decryptThenCopyPassword(from indexPath: IndexPath) { - guard self.passwordStore.pgpAgent.isImported else { + guard PGPAgent.shared.isPrepared else { Utils.alert(title: "CannotCopyPassword".localize(), message: "SetPgpKey.".localize(), controller: self, completion: nil) return } @@ -412,7 +413,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV } catch { DispatchQueue.main.async { // remove the wrong passphrase so that users could enter it next time - self.passwordStore.pgpAgent.passphrase = nil + self.keychain.removeContent(for: Globals.pgpKeyPassphrase) Utils.alert(title: "CannotCopyPassword".localize(), message: error.localizedDescription, controller: self, completion: nil) } } @@ -453,7 +454,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { if identifier == "showPasswordDetail" { - guard self.passwordStore.pgpAgent.isImported else { + guard PGPAgent.shared.isPrepared else { Utils.alert(title: "CannotShowPassword".localize(), message: "SetPgpKey.".localize(), controller: self, completion: nil) if let s = sender as? UITableViewCell { let selectedIndexPath = tableView.indexPath(for: s)! @@ -462,7 +463,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV return false } } else if identifier == "addPasswordSegue" { - guard self.passwordStore.pgpAgent.isImported && self.passwordStore.storeRepository != nil else { + guard PGPAgent.shared.isPrepared && self.passwordStore.storeRepository != nil else { Utils.alert(title: "CannotAddPassword".localize(), message: "MakeSurePgpAndGitProperlySet.".localize(), controller: self, completion: nil) return false } diff --git a/pass/Controllers/SettingsTableViewController.swift b/pass/Controllers/SettingsTableViewController.swift index 3e7ac60..9a5f0b7 100644 --- a/pass/Controllers/SettingsTableViewController.swift +++ b/pass/Controllers/SettingsTableViewController.swift @@ -18,6 +18,7 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele var setPasscodeLockAlert: UIAlertController? let passwordStore = PasswordStore.shared + let keychain = AppKeychain.shared var passcodeLock = PasscodeLock.shared func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) { @@ -28,9 +29,6 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele if let controller = segue.source as? PGPKeySettingTableViewController { SharedDefaults[.pgpPrivateKeyURL] = URL(string: controller.pgpPrivateKeyURLTextField.text!.trimmed) SharedDefaults[.pgpPublicKeyURL] = URL(string: controller.pgpPublicKeyURLTextField.text!.trimmed) - if SharedDefaults[.isRememberPGPPassphraseOn] { - self.passwordStore.pgpAgent.passphrase = controller.pgpPassphrase - } SharedDefaults[.pgpKeySource] = "url" SVProgressHUD.setDefaultMaskType(.black) @@ -38,10 +36,11 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele SVProgressHUD.show(withStatus: "FetchingPgpKey".localize()) DispatchQueue.global(qos: .userInitiated).async { [unowned self] in do { - try self.passwordStore.pgpAgent.initPGPKey(from: SharedDefaults[.pgpPublicKeyURL]!, keyType: .PUBLIC) - try self.passwordStore.pgpAgent.initPGPKey(from: SharedDefaults[.pgpPrivateKeyURL]!, keyType: .PRIVATE) + try KeyFileManager.PublicPgp.importKey(from: SharedDefaults[.pgpPublicKeyURL]!) + try KeyFileManager.PrivatePgp.importKey(from: SharedDefaults[.pgpPrivateKeyURL]!) + try PGPAgent.shared.initKeys() DispatchQueue.main.async { - self.pgpKeyTableViewCell.detailTextLabel?.text = self.passwordStore.pgpAgent.pgpKeyID + self.pgpKeyTableViewCell.detailTextLabel?.text = PGPAgent.shared.keyId SVProgressHUD.showSuccess(withStatus: "Success".localize()) SVProgressHUD.dismiss(withDelay: 1) Utils.alert(title: "RememberToRemoveKey".localize(), message: "RememberToRemoveKeyFromServer.".localize(), controller: self, completion: nil) @@ -56,18 +55,17 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele } else if let controller = segue.source as? PGPKeyArmorSettingTableViewController { SharedDefaults[.pgpKeySource] = "armor" - if SharedDefaults[.isRememberPGPPassphraseOn] { - self.passwordStore.pgpAgent.passphrase = controller.pgpPassphrase - } + SVProgressHUD.setDefaultMaskType(.black) SVProgressHUD.setDefaultStyle(.light) SVProgressHUD.show(withStatus: "FetchingPgpKey".localize()) DispatchQueue.global(qos: .userInitiated).async { [unowned self] in do { - try self.passwordStore.pgpAgent.initPGPKey(with: controller.armorPublicKeyTextView.text ?? "", keyType: .PUBLIC) - try self.passwordStore.pgpAgent.initPGPKey(with: controller.armorPrivateKeyTextView.text ?? "", keyType: .PRIVATE) + try KeyFileManager.PublicPgp.importKey(from: controller.armorPublicKeyTextView.text ?? "") + try KeyFileManager.PrivatePgp.importKey(from: controller.armorPrivateKeyTextView.text ?? "") + try PGPAgent.shared.initKeys() DispatchQueue.main.async { - self.pgpKeyTableViewCell.detailTextLabel?.text = self.passwordStore.pgpAgent.pgpKeyID + self.pgpKeyTableViewCell.detailTextLabel?.text = PGPAgent.shared.keyId SVProgressHUD.showSuccess(withStatus: "Success".localize()) SVProgressHUD.dismiss(withDelay: 1) } @@ -89,9 +87,11 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele SVProgressHUD.show(withStatus: "FetchingPgpKey".localize()) DispatchQueue.global(qos: .userInitiated).async { [unowned self] in do { - try self.passwordStore.pgpAgent.initPGPKeyFromFileSharing() + try KeyFileManager.PublicPgp.importKeyFromFileSharing() + try KeyFileManager.PrivatePgp.importKeyFromFileSharing() + try PGPAgent.shared.initKeys() DispatchQueue.main.async { - self.pgpKeyTableViewCell.detailTextLabel?.text = self.passwordStore.pgpAgent.pgpKeyID + self.pgpKeyTableViewCell.detailTextLabel?.text = PGPAgent.shared.keyId SVProgressHUD.showSuccess(withStatus: "Imported".localize()) SVProgressHUD.dismiss(withDelay: 1) } @@ -135,11 +135,8 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele } private func setPGPKeyTableViewCellDetailText() { - if let pgpKeyID = self.passwordStore.pgpAgent.pgpKeyID { - pgpKeyTableViewCell.detailTextLabel?.text = pgpKeyID - } else { - pgpKeyTableViewCell.detailTextLabel?.text = "NotSet".localize() - } + try? PGPAgent.shared.initKeys() + pgpKeyTableViewCell.detailTextLabel?.text = PGPAgent.shared.keyId ?? "NotSet".localize() } private func setPasswordRepositoryTableViewCellDetailText() { @@ -192,14 +189,14 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele optionMenu.addAction(urlAction) optionMenu.addAction(armorAction) - if passwordStore.pgpAgent.isFileSharingReady { + if KeyFileManager.PublicPgp.doesKeyFileExist() && KeyFileManager.PrivatePgp.doesKeyFileExist() { fileActionTitle.append(" (\("Import".localize()))") let fileAction = UIAlertAction(title: fileActionTitle, style: .default) { _ in // passphrase related let savePassphraseAlert = UIAlertController(title: "Passphrase".localize(), message: "WantToSavePassphrase?".localize(), preferredStyle: UIAlertController.Style.alert) // no savePassphraseAlert.addAction(UIAlertAction(title: "No".localize(), style: UIAlertAction.Style.default) { _ in - self.passwordStore.pgpAgent.passphrase = nil + self.keychain.removeContent(for: Globals.pgpKeyPassphrase) SharedDefaults[.isRememberPGPPassphraseOn] = false self.saveImportedPGPKey() }) @@ -208,7 +205,7 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele // ask for the passphrase let alert = UIAlertController(title: "Passphrase".localize(), message: "FillInPgpPassphrase.".localize(), preferredStyle: UIAlertController.Style.alert) alert.addAction(UIAlertAction(title: "Ok".localize(), style: UIAlertAction.Style.default, handler: {_ in - self.passwordStore.pgpAgent.passphrase = alert.textFields?.first?.text + self.keychain.add(string: alert.textFields?.first?.text, for: Globals.pgpKeyPassphrase) SharedDefaults[.isRememberPGPPassphraseOn] = true self.saveImportedPGPKey() })) @@ -234,7 +231,9 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele if SharedDefaults[.pgpKeySource] != nil { let deleteAction = UIAlertAction(title: "RemovePgpKeys".localize(), style: .destructive) { _ in - self.passwordStore.removePGPKeys() + self.keychain.removeContent(for: PgpKey.PUBLIC.getKeychainKey()) + self.keychain.removeContent(for: PgpKey.PRIVATE.getKeychainKey()) + PGPAgent.shared.uninitKeys() self.pgpKeyTableViewCell.detailTextLabel?.text = "NotSet".localize() } optionMenu.addAction(deleteAction) diff --git a/pass/de.lproj/Localizable.strings b/pass/de.lproj/Localizable.strings index 8d9ffdf..024678d 100644 --- a/pass/de.lproj/Localizable.strings +++ b/pass/de.lproj/Localizable.strings @@ -66,6 +66,7 @@ "PGPPublicKeyNotExistError." = "Der öffentliche PGP-Schlüssen existiert nicht"; "WrongPasswordFilename." = "Schreiben der Passwort-Datei nicht möglich ."; "DecryptionError." = "Passwort kann nicht entschlüsselt werden."; +"EncodingError." = "Schlüssel ist nicht in ASCII kodiert."; "UnknownError." = "Ein unbekannter Fehler ist aufgetreten."; "PrepareRepository" = "Repository vorbereiten"; "CheckingOutBranch" = "Branch '%@' wird ausgecheckt"; diff --git a/pass/en.lproj/Localizable.strings b/pass/en.lproj/Localizable.strings index 4d5b802..a4d4390 100644 --- a/pass/en.lproj/Localizable.strings +++ b/pass/en.lproj/Localizable.strings @@ -67,6 +67,7 @@ "PgpPublicKeyNotExistError." = "PGP public key doesn't exist."; "WrongPasswordFilenameError." = "Cannot write to the password file."; "DecryptionError." = "Cannot decrypt password."; +"EncodingError." = "Key is not ASCII encoded."; "UnknownError." = "Unknown error."; "PrepareRepository" = "Prepare Repository"; "CheckingOutBranch" = "Checking out branch '%@'"; diff --git a/passAutoFillExtension/Controllers/CredentialProviderViewController.swift b/passAutoFillExtension/Controllers/CredentialProviderViewController.swift index a006e80..ba1c108 100644 --- a/passAutoFillExtension/Controllers/CredentialProviderViewController.swift +++ b/passAutoFillExtension/Controllers/CredentialProviderViewController.swift @@ -145,7 +145,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController, UITa func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let entry = getPasswordEntry(by: indexPath) - guard self.passwordStore.pgpAgent.isImported else { + guard PGPAgent.shared.isPrepared else { Utils.alert(title: "CannotCopyPassword".localize(), message: "PgpKeyNotSet.".localize(), controller: self, completion: nil) return } @@ -165,7 +165,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController, UITa } catch { DispatchQueue.main.async { // remove the wrong passphrase so that users could enter it next time - self.passwordStore.pgpAgent.passphrase = nil + AppKeychain.shared.removeContent(for: Globals.pgpKeyPassphrase) Utils.alert(title: "CannotCopyPassword".localize(), message: error.localizedDescription, controller: self, completion: nil) } } @@ -200,7 +200,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController, UITa } let _ = sem.wait(timeout: DispatchTime.distantFuture) if SharedDefaults[.isRememberPGPPassphraseOn] { - self.passwordStore.pgpAgent.passphrase = passphrase + AppKeychain.shared.add(string: passphrase, for: Globals.pgpKeyPassphrase) } return passphrase } diff --git a/passExtension/Controllers/ExtensionViewController.swift b/passExtension/Controllers/ExtensionViewController.swift index 72db039..e57783b 100644 --- a/passExtension/Controllers/ExtensionViewController.swift +++ b/passExtension/Controllers/ExtensionViewController.swift @@ -28,6 +28,7 @@ class ExtensionViewController: UIViewController, UITableViewDataSource, UITableV @IBOutlet weak var tableView: UITableView! private let passwordStore = PasswordStore.shared + private let keychain = AppKeychain.shared private var searchActive = false private var passwordsTableEntries: [PasswordsTableEntry] = [] @@ -153,7 +154,7 @@ class ExtensionViewController: UIViewController, UITableViewDataSource, UITableV func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let entry = getPasswordEntry(by: indexPath) - guard self.passwordStore.pgpAgent.isImported else { + guard PGPAgent.shared.isPrepared else { Utils.alert(title: "CannotCopyPassword".localize(), message: "PgpKeyNotSet.".localize(), controller: self, completion: nil) return } @@ -191,7 +192,7 @@ class ExtensionViewController: UIViewController, UITableViewDataSource, UITableV } catch { DispatchQueue.main.async { // remove the wrong passphrase so that users could enter it next time - self.passwordStore.pgpAgent.passphrase = nil + self.keychain.removeContent(for: Globals.pgpKeyPassphrase) Utils.alert(title: "CannotCopyPassword".localize(), message: error.localizedDescription, controller: self, completion: nil) } } @@ -227,7 +228,7 @@ class ExtensionViewController: UIViewController, UITableViewDataSource, UITableV } let _ = sem.wait(timeout: DispatchTime.distantFuture) if SharedDefaults[.isRememberPGPPassphraseOn] { - self.passwordStore.pgpAgent.passphrase = passphrase + self.keychain.add(string: passphrase, for: Globals.pgpKeyPassphrase) } return passphrase } diff --git a/passKit/Crypto/GopenPgp.swift b/passKit/Crypto/GopenPgp.swift new file mode 100644 index 0000000..af35526 --- /dev/null +++ b/passKit/Crypto/GopenPgp.swift @@ -0,0 +1,57 @@ +// +// GopenPgp.swift +// passKit +// +// Created by Danny Moesch on 08.09.19. +// Copyright © 2019 Bob Sun. All rights reserved. +// + +import Crypto + +struct GopenPgp: PgpInterface { + + private let publicKey: CryptoKeyRing + private let privateKey: CryptoKeyRing + + init(publicArmoredKey: String, privateArmoredKey: String) throws { + guard let pgp = CryptoGetGopenPGP() else { + throw AppError.KeyImport + } + publicKey = try pgp.buildKeyRingArmored(publicArmoredKey) + privateKey = try pgp.buildKeyRingArmored(privateArmoredKey) + } + + func decrypt(encryptedData: Data, passphrase: String) throws -> Data? { + try privateKey.unlock(withPassphrase: passphrase) + let message = createPgpMessage(from: encryptedData) + return try privateKey.decrypt(message, verifyKey: nil, verifyTime: 0).data + } + + func encrypt(plainData: Data) throws -> Data { + let encryptedData = try publicKey.encrypt(CryptoNewPlainMessage(plainData.mutable as Data), privateKey: nil) + if SharedDefaults[.encryptInArmored] { + var error: NSError? + let armor = encryptedData.getArmored(&error) + guard error == nil else { + throw error! + } + return armor.data(using: .ascii)! + } + return encryptedData.getBinary()! + } + + var keyId: String { + var error: NSError? + let fingerprint = publicKey.getFingerprint(&error) + return error == nil ? String(fingerprint.suffix(8)).uppercased() : "" + } + + private func createPgpMessage(from encryptedData: Data) -> CryptoPGPMessage? { + if SharedDefaults[.encryptInArmored] { + var error: NSError? + let message = CryptoNewPGPMessageFromArmored(String(data: encryptedData, encoding: .ascii), &error) + return error == nil ? message : nil + } + return CryptoNewPGPMessage(encryptedData.mutable as Data) + } +} diff --git a/passKit/Crypto/ObjectivePgp.swift b/passKit/Crypto/ObjectivePgp.swift new file mode 100644 index 0000000..0a80c3b --- /dev/null +++ b/passKit/Crypto/ObjectivePgp.swift @@ -0,0 +1,48 @@ +// +// ObjectivePgp.swift +// passKit +// +// Created by Danny Moesch on 08.09.19. +// Copyright © 2019 Bob Sun. All rights reserved. +// + +import ObjectivePGP + +struct ObjectivePgp: PgpInterface { + + private let publicKey: Key + private let privateKey: Key + + private let keyring = ObjectivePGP.defaultKeyring + + init(publicArmoredKey: String, privateArmoredKey: String) throws { + guard let publicKeyData = publicArmoredKey.data(using: .ascii), let privateKeyData = privateArmoredKey.data(using: .ascii) else { + throw AppError.KeyImport + } + let publicKeys = try ObjectivePGP.readKeys(from: publicKeyData) + let privateKeys = try ObjectivePGP.readKeys(from: privateKeyData) + keyring.import(keys: publicKeys) + keyring.import(keys: privateKeys) + guard let publicKey = publicKeys.first, let privateKey = privateKeys.first else { + throw AppError.KeyImport + } + self.publicKey = publicKey + self.privateKey = privateKey + } + + func decrypt(encryptedData: Data, passphrase: String) throws -> Data? { + return try ObjectivePGP.decrypt(encryptedData, andVerifySignature: false, using: keyring.keys) { _ in passphrase } + } + + func encrypt(plainData: Data) throws -> Data { + let encryptedData = try ObjectivePGP.encrypt(plainData, addSignature: false, using: keyring.keys, passphraseForKey: nil) + if SharedDefaults[.encryptInArmored] { + return Armor.armored(encryptedData, as: .message).data(using: .ascii)! + } + return encryptedData + } + + var keyId: String { + return publicKey.keyID.shortIdentifier + } +} diff --git a/passKit/Crypto/PGPAgent.swift b/passKit/Crypto/PGPAgent.swift new file mode 100644 index 0000000..5f69df2 --- /dev/null +++ b/passKit/Crypto/PGPAgent.swift @@ -0,0 +1,64 @@ +// +// PGPAgent.swift +// passKit +// +// Created by Yishi Lin on 2019/7/17. +// Copyright © 2019 Bob Sun. All rights reserved. +// + +public class PGPAgent { + + public static let shared: PGPAgent = PGPAgent() + + private let keyStore: KeyStore + private var pgpInterface: PgpInterface? + + public init(keyStore: KeyStore = AppKeychain.shared) { + self.keyStore = keyStore + } + + public func initKeys() throws { + guard let publicKey: String = keyStore.get(for: PgpKey.PUBLIC.getKeychainKey()), + let privateKey: String = keyStore.get(for: PgpKey.PRIVATE.getKeychainKey()) else { + throw AppError.KeyImport + } + do { + pgpInterface = try GopenPgp(publicArmoredKey: publicKey, privateArmoredKey: privateKey) + } catch { + pgpInterface = try ObjectivePgp(publicArmoredKey: publicKey, privateArmoredKey: privateKey) + } + } + + public func uninitKeys() { + pgpInterface = nil + } + + public var keyId: String? { + return pgpInterface?.keyId + } + + public func decrypt(encryptedData: Data, requestPGPKeyPassphrase: () -> String) throws -> Data? { + try checkAndInit() + let passphrase = keyStore.get(for: Globals.pgpKeyPassphrase) ?? requestPGPKeyPassphrase() + return try pgpInterface!.decrypt(encryptedData: encryptedData, passphrase: passphrase) + } + + public func encrypt(plainData: Data) throws -> Data { + try checkAndInit() + guard let pgpInterface = pgpInterface else { + throw AppError.Encryption + } + return try pgpInterface.encrypt(plainData: plainData) + } + + public var isPrepared: Bool { + return keyStore.contains(key: PgpKey.PUBLIC.getKeychainKey()) + && keyStore.contains(key: PgpKey.PRIVATE.getKeychainKey()) + } + + private func checkAndInit() throws { + if pgpInterface == nil || !keyStore.contains(key: Globals.pgpKeyPassphrase) { + try initKeys() + } + } +} diff --git a/passKit/Crypto/PgpInterface.swift b/passKit/Crypto/PgpInterface.swift new file mode 100644 index 0000000..ff25666 --- /dev/null +++ b/passKit/Crypto/PgpInterface.swift @@ -0,0 +1,16 @@ +// +// PgpInterface.swift +// passKit +// +// Created by Danny Moesch on 08.09.19. +// Copyright © 2019 Bob Sun. All rights reserved. +// + +protocol PgpInterface { + + func decrypt(encryptedData: Data, passphrase: String) throws -> Data? + + func encrypt(plainData: Data) throws -> Data + + var keyId: String { get } +} diff --git a/passKit/Helpers/AppError.swift b/passKit/Helpers/AppError.swift index a9bfad2..507e0ca 100644 --- a/passKit/Helpers/AppError.swift +++ b/passKit/Helpers/AppError.swift @@ -20,6 +20,7 @@ public enum AppError: Error, Equatable { case WrongPasswordFilename case Decryption case Encryption + case Encoding case Unknown } diff --git a/passKit/Helpers/Globals.swift b/passKit/Helpers/Globals.swift index 0a824c7..5930bb1 100644 --- a/passKit/Helpers/Globals.swift +++ b/passKit/Helpers/Globals.swift @@ -29,6 +29,8 @@ public class Globals { public static let iTunesFileSharingPGPPrivate = iTunesFileSharingPath + "/gpg_key" public static let iTunesFileSharingSSHPrivate = iTunesFileSharingPath + "/ssh_key" + public static let pgpKeyPassphrase = "pgpKeyPassphrase" + public static let gitSignatureDefaultName = "Pass for iOS" public static let gitSignatureDefaultEmail = "user@passforios" diff --git a/passKit/Helpers/KeyFileManager.swift b/passKit/Helpers/KeyFileManager.swift index 2dae9eb..c01788c 100644 --- a/passKit/Helpers/KeyFileManager.swift +++ b/passKit/Helpers/KeyFileManager.swift @@ -7,7 +7,7 @@ // public class KeyFileManager { - public typealias KeyHandler = (Data, String) -> () + public typealias KeyHandler = (String, String) -> Void public static let PublicPgp = KeyFileManager(keyType: PgpKey.PUBLIC) public static let PrivatePgp = KeyFileManager(keyType: PgpKey.PRIVATE) @@ -15,24 +15,35 @@ public class KeyFileManager { private let keyType: CryptographicKey private let keyPath: String + private let keyHandler: KeyHandler private convenience init(keyType: CryptographicKey) { self.init(keyType: keyType, keyPath: keyType.getFileSharingPath()) } - public init(keyType: CryptographicKey, keyPath: String) { + public init(keyType: CryptographicKey, keyPath: String, keyHandler: @escaping KeyHandler = AppKeychain.shared.add) { self.keyType = keyType self.keyPath = keyPath + self.keyHandler = keyHandler } - public func importKeyAndDeleteFile(keyHandler: KeyHandler = AppKeychain.shared.add) throws { - guard let keyFileContent = FileManager.default.contents(atPath: keyPath) else { - throw AppError.ReadingFile(URL(fileURLWithPath: keyPath).lastPathComponent) - } - keyHandler(keyFileContent, keyType.getKeychainKey()) + public func importKeyFromFileSharing() throws { + let keyFileContent = try String(contentsOfFile: keyPath, encoding: .ascii) + try importKey(from: keyFileContent) try FileManager.default.removeItem(atPath: keyPath) } - + + public func importKey(from string: String) throws { + guard string.unicodeScalars.allSatisfy({ $0.isASCII }) else { + throw AppError.Encoding + } + keyHandler(string, keyType.getKeychainKey()) + } + + public func importKey(from url: URL) throws { + try importKey(from: String(contentsOf: url, encoding: .ascii)) + } + public func doesKeyFileExist() -> Bool { return FileManager.default.fileExists(atPath: keyPath) } diff --git a/passKit/Models/PGPAgent.swift b/passKit/Models/PGPAgent.swift deleted file mode 100644 index f32ae6a..0000000 --- a/passKit/Models/PGPAgent.swift +++ /dev/null @@ -1,223 +0,0 @@ -// -// PGPAgent.swift -// passKit -// -// Created by Yishi Lin on 2019/7/17. -// Copyright © 2019 Bob Sun. All rights reserved. -// - -import Foundation -import ObjectivePGP -import KeychainAccess -import Crypto - -public class PGPAgent { - - private let keyStore: KeyStore - - public init(keyStore: KeyStore = AppKeychain.shared) { - self.keyStore = keyStore - } - - public var pgpKeyID: String? - // PGP passphrase - public var passphrase: String? { - set { - keyStore.add(string: newValue, for: "pgpKeyPassphrase") - } - get { - return keyStore.get(for: "pgpKeyPassphrase") - } - } - - // Gopenpgpwrapper - private var publicKey: CryptoKeyRing? { - didSet { - var err: NSError? = nil - let fp = publicKey?.getFingerprint(&err) - if err == nil && fp != nil { - pgpKeyID = String(fp!.suffix(8)).uppercased() - } else { - pgpKeyID = "" - } - } - } - private var privateKey: CryptoKeyRing? - // ObjectivePGP - private let keyring = ObjectivePGP.defaultKeyring - private var publicKeyV2: Key? { - didSet { - pgpKeyID = publicKeyV2?.keyID.shortIdentifier - } - } - private var privateKeyV2: Key? - - public var isImported: Bool { - get { - return (publicKey != nil || publicKeyV2 != nil) && (privateKey != nil || privateKeyV2 != nil) - } - } - public var isFileSharingReady: Bool { - get { - return KeyFileManager.PublicPgp.doesKeyFileExist() && KeyFileManager.PrivatePgp.doesKeyFileExist() - } - } - - public func initPGPKeys() throws { - try initPGPKey(.PUBLIC) - try initPGPKey(.PRIVATE) - } - - public func initPGPKey(_ keyType: PgpKey) throws { - // Clean up the previously set public/private key. - switch keyType { - case .PUBLIC: - self.publicKey = nil - self.publicKeyV2 = nil - case .PRIVATE: - self.privateKey = nil - self.privateKeyV2 = nil - } - - // Read the key data from keychain. - guard let pgpKeyData: Data = keyStore.get(for: keyType.getKeychainKey()) else { - throw AppError.KeyImport - } - - // Remove the key data from keychain temporary, in case the following step crashes repeatedly. - keyStore.removeContent(for: keyType.getKeychainKey()) - - // Try GopenPGP first. - let pgp = CryptoGetGopenPGP() - - // Treat keys as binary first - if let key = try? pgp?.buildKeyRing(pgpKeyData) { - switch keyType { - case .PUBLIC: - self.publicKey = key - case .PRIVATE: - self.privateKey = key - } - keyStore.add(data: pgpKeyData, for: keyType.getKeychainKey()) - return - } - - // Treat key as ASCII armored keys if binary fails - if let key = try? pgp?.buildKeyRingArmored(String(data: pgpKeyData, encoding: .ascii)) { - switch keyType { - case .PUBLIC: - self.publicKey = key - case .PRIVATE: - self.privateKey = key - } - keyStore.add(data: pgpKeyData, for: keyType.getKeychainKey()) - return - } - - // Try ObjectivePGP as a backup plan. - // [ObjectivePGP.readKeys MAY CRASH!!!] - if let keys = try? ObjectivePGP.readKeys(from: pgpKeyData), - let key = keys.first { - keyring.import(keys: keys) - switch keyType { - case .PUBLIC: - self.publicKeyV2 = key - case .PRIVATE: - self.privateKeyV2 = key - } - keyStore.add(data: pgpKeyData, for: keyType.getKeychainKey()) - return - } - - throw AppError.KeyImport - } - - public func initPGPKey(from url: URL, keyType: PgpKey) throws { - let pgpKeyData = try Data(contentsOf: url) - keyStore.add(data: pgpKeyData, for: keyType.getKeychainKey()) - try initPGPKey(keyType) - } - - public func initPGPKey(with armorKey: String, keyType: PgpKey) throws { - let pgpKeyData = armorKey.data(using: .ascii)! - keyStore.add(data: pgpKeyData, for: keyType.getKeychainKey()) - try initPGPKey(keyType) - } - - public func initPGPKeyFromFileSharing() throws { - try KeyFileManager.PublicPgp.importKeyAndDeleteFile(keyHandler: keyStore.add) - try KeyFileManager.PrivatePgp.importKeyAndDeleteFile(keyHandler: keyStore.add) - try initPGPKeys() - } - - public func decrypt(encryptedData: Data, requestPGPKeyPassphrase: () -> String) throws -> Data? { - guard privateKey != nil || privateKeyV2 != nil else { - throw AppError.PgpPublicKeyNotExist - } - let passphrase = self.passphrase ?? requestPGPKeyPassphrase() - // Try Gopenpgp. - if privateKey != nil { - try privateKey?.unlock(withPassphrase: passphrase) - - var err : NSError? = nil - var message = CryptoNewPGPMessageFromArmored(String(data: encryptedData, encoding: .ascii), &err) - if err != nil { - message = CryptoNewPGPMessage(encryptedData) - } - - if let decryptedData = try? privateKey?.decrypt(message, verifyKey: nil, verifyTime: 0) { - return decryptedData.data - } - } - // Try ObjectivePGP. - if privateKeyV2 != nil { - if let decryptedData = try? ObjectivePGP.decrypt(encryptedData, andVerifySignature: false, using: keyring.keys, passphraseForKey: {(_) in passphrase}) { - return decryptedData - } - } - throw AppError.Decryption - } - - public func encrypt(plainData: Data) throws -> Data { - guard publicKey != nil || publicKeyV2 != nil else { - throw AppError.PgpPublicKeyNotExist - } - // Try Gopenpgp. - if publicKey != nil { - if let encryptedData = try? publicKey?.encrypt(CryptoNewPlainMessageFromString(String(data: plainData, encoding: .utf8)), privateKey: nil) { - if SharedDefaults[.encryptInArmored] { - var err : NSError? = nil - let armor = encryptedData.getArmored(&err) - if err == nil { - return armor.data(using: .ascii)! - } - } else { - return encryptedData.getBinary()! - } - } - } - - // Try ObjectivePGP. - if publicKeyV2 != nil { - if let encryptedData = try? ObjectivePGP.encrypt(plainData, addSignature: false, using: keyring.keys, passphraseForKey: nil) { - if SharedDefaults[.encryptInArmored] { - return Armor.armored(encryptedData, as: .message).data(using: .utf8)! - } else { - return encryptedData - } - } - } - throw AppError.Encryption - } - - public func removePGPKeys() { - keyStore.removeContent(for: PgpKey.PUBLIC.getKeychainKey()) - keyStore.removeContent(for: PgpKey.PRIVATE.getKeychainKey()) - passphrase = nil - publicKey = nil - privateKey = nil - publicKeyV2 = nil - privateKeyV2 = nil - keyring.deleteAll() - } -} diff --git a/passKit/Models/PasswordStore.swift b/passKit/Models/PasswordStore.swift index 98e2304..b7a1eea 100644 --- a/passKit/Models/PasswordStore.swift +++ b/passKit/Models/PasswordStore.swift @@ -24,9 +24,7 @@ public class PasswordStore { public let storeURL = URL(fileURLWithPath: "\(Globals.repositoryPath)") public let tempStoreURL = URL(fileURLWithPath: "\(Globals.repositoryPath)-temp") - - public let pgpAgent = PGPAgent() - + public var storeRepository: GTRepository? public var gitSignatureForNow: GTSignature? { @@ -111,7 +109,6 @@ public class PasswordStore { if fm.fileExists(atPath: storeURL.path) { try storeRepository = GTRepository.init(url: storeURL) } - try self.pgpAgent.initPGPKeys() } catch { print(error) } @@ -119,9 +116,9 @@ public class PasswordStore { private func importExistingKeysIntoKeychain() { // App Store update: v0.5.1 -> v0.6.0 - try? KeyFileManager(keyType: PgpKey.PUBLIC, keyPath: Globals.pgpPublicKeyPath).importKeyAndDeleteFile() - try? KeyFileManager(keyType: PgpKey.PRIVATE, keyPath: Globals.pgpPrivateKeyPath).importKeyAndDeleteFile() - try? KeyFileManager(keyType: SshKey.PRIVATE, keyPath: Globals.gitSSHPrivateKeyPath).importKeyAndDeleteFile() + try? KeyFileManager(keyType: PgpKey.PUBLIC, keyPath: Globals.pgpPublicKeyPath).importKeyFromFileSharing() + try? KeyFileManager(keyType: PgpKey.PRIVATE, keyPath: Globals.pgpPrivateKeyPath).importKeyFromFileSharing() + try? KeyFileManager(keyType: SshKey.PRIVATE, keyPath: Globals.gitSSHPrivateKeyPath).importKeyFromFileSharing() SharedDefaults.remove(.pgpPublicKeyArmor) SharedDefaults.remove(.pgpPrivateKeyArmor) SharedDefaults.remove(.gitSSHPrivateKeyArmor) @@ -638,7 +635,8 @@ public class PasswordStore { try? fm.removeItem(atPath: Globals.gitSSHPrivateKeyPath) - self.pgpAgent.removePGPKeys() + AppKeychain.shared.removeContent(for: PgpKey.PUBLIC.getKeychainKey()) + AppKeychain.shared.removeContent(for: PgpKey.PRIVATE.getKeychainKey()) AppKeychain.shared.removeAllContent() @@ -699,7 +697,7 @@ public class PasswordStore { public func decrypt(passwordEntity: PasswordEntity, requestPGPKeyPassphrase: () -> String) throws -> Password? { let encryptedDataPath = storeURL.appendingPathComponent(passwordEntity.getPath()) let encryptedData = try Data(contentsOf: encryptedDataPath) - guard let decryptedData = try self.pgpAgent.decrypt(encryptedData: encryptedData, requestPGPKeyPassphrase: requestPGPKeyPassphrase) else { + guard let decryptedData = try PGPAgent.shared.decrypt(encryptedData: encryptedData, requestPGPKeyPassphrase: requestPGPKeyPassphrase) else { throw AppError.Decryption } let plainText = String(data: decryptedData, encoding: .utf8) ?? "" @@ -708,11 +706,7 @@ public class PasswordStore { } public func encrypt(password: Password) throws -> Data { - return try self.pgpAgent.encrypt(plainData: password.plainData) - } - - public func removePGPKeys() { - self.pgpAgent.removePGPKeys() + return try PGPAgent.shared.encrypt(plainData: password.plainData) } public func removeGitSSHKeys() { @@ -725,6 +719,6 @@ public class PasswordStore { } public func gitSSHKeyImportFromFileSharing() throws { - try KeyFileManager.PrivateSsh.importKeyAndDeleteFile() + try KeyFileManager.PrivateSsh.importKeyFromFileSharing() } } diff --git a/passKitTests/Crypto/PGPAgentTest.swift b/passKitTests/Crypto/PGPAgentTest.swift new file mode 100644 index 0000000..248c2ef --- /dev/null +++ b/passKitTests/Crypto/PGPAgentTest.swift @@ -0,0 +1,105 @@ +// +// PGPAgent.swift +// passKitTests +// +// Created by Yishi Lin on 2019/7/17. +// Copyright © 2019 Bob Sun. All rights reserved. +// + +import XCTest + +@testable import passKit + +class PGPAgentTest: XCTestCase { + + private var keychain: KeyStore! + private var pgpAgent: PGPAgent! + + private let testData = "Hello World!".data(using: .utf8)! + + override func setUp() { + super.setUp() + keychain = DictBasedKeychain() + pgpAgent = PGPAgent(keyStore: keychain) + } + + override func tearDown() { + keychain.removeAllContent() + super.tearDown() + } + + func basicEncryptDecrypt(using pgpAgent: PGPAgent) throws -> Data? { + let encryptedData = try pgpAgent.encrypt(plainData: testData) + return try pgpAgent.decrypt(encryptedData: encryptedData, requestPGPKeyPassphrase: requestPGPKeyPassphrase) + } + + func testBasicEncryptDecrypt() throws { + try [ + RSA2048, + RSA2048_SUB, + ED25519, + //ED25519_SUB, + ].forEach { keyTriple in + let keychain = DictBasedKeychain() + let pgpAgent = PGPAgent(keyStore: keychain) + try KeyFileManager(keyType: PgpKey.PUBLIC, keyPath: "", keyHandler: keychain.add).importKey(from: keyTriple.publicKey) + try KeyFileManager(keyType: PgpKey.PRIVATE, keyPath: "", keyHandler: keychain.add).importKey(from: keyTriple.privateKey) + XCTAssert(pgpAgent.isPrepared) + try pgpAgent.initKeys() + XCTAssert(pgpAgent.keyId!.lowercased().hasSuffix(keyTriple.fingerprint)) + XCTAssertEqual(try basicEncryptDecrypt(using: pgpAgent), testData) + } + } + + func testNoPrivateKey() throws { + try KeyFileManager(keyType: PgpKey.PUBLIC, keyPath: "", keyHandler: keychain.add).importKey(from: RSA2048.publicKey) + XCTAssertFalse(pgpAgent.isPrepared) + XCTAssertThrowsError(try pgpAgent.initKeys()) { + XCTAssertEqual($0 as! AppError, AppError.KeyImport) + } + XCTAssertThrowsError(try basicEncryptDecrypt(using: pgpAgent)) { + XCTAssertEqual($0 as! AppError, AppError.KeyImport) + } + } + + func testInterchangePublicAndPrivateKey() throws { + try importKeys(RSA2048.privateKey, RSA2048.publicKey) + XCTAssert(pgpAgent.isPrepared) + XCTAssertThrowsError(try basicEncryptDecrypt(using: pgpAgent)) { + XCTAssert($0.localizedDescription.contains("gopenpgp: cannot unlock key ring, no private key available")) + } + } + + func testIncompatibleKeyTypes() throws { + try importKeys(ED25519.publicKey, RSA2048.privateKey) + XCTAssert(pgpAgent.isPrepared) + XCTAssertThrowsError(try basicEncryptDecrypt(using: pgpAgent)) { + XCTAssert($0.localizedDescription.contains("openpgp: incorrect key")) + } + } + + func testCorruptedKey() throws { + try importKeys(RSA2048.publicKey.replacingOccurrences(of: "1", with: ""), RSA2048.privateKey) + XCTAssert(pgpAgent.isPrepared) + XCTAssertThrowsError(try basicEncryptDecrypt(using: pgpAgent)) { + XCTAssert($0.localizedDescription.contains("Can't read keys. Invalid input.")) + } + } + + func testUnsettKeys() throws { + try importKeys(ED25519.publicKey, ED25519.privateKey) + XCTAssert(pgpAgent.isPrepared) + XCTAssertEqual(try basicEncryptDecrypt(using: pgpAgent), testData) + keychain.removeContent(for: PgpKey.PUBLIC.getKeychainKey()) + keychain.removeContent(for: PgpKey.PRIVATE.getKeychainKey()) + XCTAssertThrowsError(try basicEncryptDecrypt(using: pgpAgent)) { + XCTAssertEqual($0 as! AppError, AppError.KeyImport) + } + } + + private func importKeys(_ publicKey: String, _ privateKey: String) throws { + try KeyFileManager(keyType: PgpKey.PUBLIC, keyPath: "", keyHandler: keychain.add).importKey(from: publicKey) + try KeyFileManager(keyType: PgpKey.PRIVATE, keyPath: "", keyHandler: keychain.add).importKey(from: privateKey) + } +} + diff --git a/passKitTests/Helpers/KeyFileManagerTest.swift b/passKitTests/Helpers/KeyFileManagerTest.swift index 9bed027..3de0cf0 100644 --- a/passKitTests/Helpers/KeyFileManagerTest.swift +++ b/passKitTests/Helpers/KeyFileManagerTest.swift @@ -12,28 +12,54 @@ import XCTest class KeyFileManagerTest: XCTestCase { private static let filePath = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("test.txt").path - private static let keyFileManager = KeyFileManager(keyType: PgpKey.PUBLIC, keyPath: filePath) + private static let keyFileManager = KeyFileManager(keyType: PgpKey.PUBLIC, keyPath: filePath) { _, _ in } override func tearDown() { try? FileManager.default.removeItem(atPath: KeyFileManagerTest.filePath) super.tearDown() } - func testImportKeyAndDeleteFile() throws { + func testImportKeyFromFileSharing() throws { let fileContent = "content".data(using: .ascii) - var storage: [String: Data] = [:] - let keyFileManager = KeyFileManager(keyType: PgpKey.PRIVATE, keyPath: KeyFileManagerTest.filePath) + var storage: [String: String] = [:] + let keyFileManager = KeyFileManager(keyType: PgpKey.PRIVATE, keyPath: KeyFileManagerTest.filePath) { storage[$1] = $0 } FileManager.default.createFile(atPath: KeyFileManagerTest.filePath, contents: fileContent, attributes: nil) - try keyFileManager.importKeyAndDeleteFile { storage[$1] = $0 } + try keyFileManager.importKeyFromFileSharing() XCTAssertFalse(FileManager.default.fileExists(atPath: KeyFileManagerTest.filePath)) - XCTAssertTrue(storage[PgpKey.PRIVATE.getKeychainKey()] == fileContent) + XCTAssertEqual(storage[PgpKey.PRIVATE.getKeychainKey()], "content") } func testErrorReadingFile() throws { - XCTAssertThrowsError(try KeyFileManagerTest.keyFileManager.importKeyAndDeleteFile { _, _ in }) { - XCTAssertEqual($0 as! AppError, AppError.ReadingFile("test.txt")) + XCTAssertThrowsError(try KeyFileManagerTest.keyFileManager.importKeyFromFileSharing()) + } + + func testImportKeyFromUrl() throws { + let fileContent = "content".data(using: .ascii) + let url = URL(fileURLWithPath: KeyFileManagerTest.filePath) + var storage: [String: String] = [:] + let keyFileManager = KeyFileManager(keyType: PgpKey.PRIVATE, keyPath: KeyFileManagerTest.filePath) { storage[$1] = $0 } + + FileManager.default.createFile(atPath: KeyFileManagerTest.filePath, contents: fileContent, attributes: nil) + try keyFileManager.importKey(from: url) + + XCTAssertEqual(storage[PgpKey.PRIVATE.getKeychainKey()], "content") + } + + func testImportKeyFromString() throws { + let string = "content" + var storage: [String: String] = [:] + let keyFileManager = KeyFileManager(keyType: PgpKey.PRIVATE, keyPath: KeyFileManagerTest.filePath) { storage[$1] = $0 } + + try keyFileManager.importKey(from: string) + + XCTAssertEqual(storage[PgpKey.PRIVATE.getKeychainKey()], string) + } + + func testImportKeyFromNonAsciiString() throws { + XCTAssertThrowsError(try KeyFileManagerTest.keyFileManager.importKey(from: "≠")) { + XCTAssertEqual($0 as! AppError, AppError.Encoding) } } diff --git a/passKitTests/Models/PGPAgentTest.swift b/passKitTests/Models/PGPAgentTest.swift deleted file mode 100644 index 2c1f18d..0000000 --- a/passKitTests/Models/PGPAgentTest.swift +++ /dev/null @@ -1,108 +0,0 @@ -// -// PGPAgent.swift -// passKitTests -// -// Created by Yishi Lin on 2019/7/17. -// Copyright © 2019 Bob Sun. All rights reserved. -// - -import XCTest - -@testable import passKit - -class PGPAgentTest: XCTestCase { - - private let keychain = DictBasedKeychain() - - func basicEncryptDecrypt(pgpAgent: PGPAgent) -> Bool { - // Encrypt and decrypt. - let plainData = "Hello World!".data(using: .utf8)! - guard let encryptedData = try? pgpAgent.encrypt(plainData: plainData) else { - return false - } - guard let decryptedData = try? pgpAgent.decrypt(encryptedData: encryptedData, requestPGPKeyPassphrase: requestPGPKeyPassphrase) else { - return false - } - return plainData == decryptedData - } - - func testInitPGPKey() { - let pgpAgent = PGPAgent(keyStore: keychain) - - // [RSA2048] Setup keys. - try? pgpAgent.initPGPKey(with: PGP_RSA2048_PUBLIC_KEY, keyType: .PUBLIC) - try? pgpAgent.initPGPKey(with: PGP_RSA2048_PRIVATE_KEY, keyType: .PRIVATE) - XCTAssertTrue(pgpAgent.isImported) - XCTAssertEqual(pgpAgent.pgpKeyID, "A1024DAE") - XCTAssertTrue(self.basicEncryptDecrypt(pgpAgent: pgpAgent)) - let pgpAgent2 = PGPAgent(keyStore: keychain) - try? pgpAgent2.initPGPKeys() // load from the keychain - XCTAssertTrue(self.basicEncryptDecrypt(pgpAgent: pgpAgent2)) - pgpAgent.removePGPKeys() - - // [RSA2048] Setup keys. The private key is a subkey. - try? pgpAgent.initPGPKey(with: PGP_RSA2048_PUBLIC_KEY, keyType: .PUBLIC) - try? pgpAgent.initPGPKey(with: PGP_RSA2048_PRIVATE_SUBKEY, keyType: .PRIVATE) - XCTAssertTrue(pgpAgent.isImported) - XCTAssertEqual(pgpAgent.pgpKeyID, "A1024DAE") - XCTAssertTrue(self.basicEncryptDecrypt(pgpAgent: pgpAgent)) - pgpAgent.removePGPKeys() - - // [ED25519] Setup keys. - try? pgpAgent.initPGPKey(with: PGP_ED25519_PUBLIC_KEY, keyType: .PUBLIC) - try? pgpAgent.initPGPKey(with: PGP_ED25519_PRIVATE_KEY, keyType: .PRIVATE) - XCTAssertTrue(pgpAgent.isImported) - XCTAssertEqual(pgpAgent.pgpKeyID, "E9444483") - XCTAssertTrue(self.basicEncryptDecrypt(pgpAgent: pgpAgent)) - pgpAgent.removePGPKeys() - - // [RSA2048] Setup keys from URL. - let publicKeyURL = URL(fileURLWithPath: PgpKey.PUBLIC.getFileSharingPath()) - let privateKeyURL = URL(fileURLWithPath: PgpKey.PRIVATE.getFileSharingPath()) - try? PGP_RSA2048_PUBLIC_KEY.write(to: publicKeyURL, atomically: false, encoding: .utf8) - try? PGP_RSA2048_PRIVATE_KEY.write(to: privateKeyURL, atomically: false, encoding: .utf8) - try? pgpAgent.initPGPKey(from: publicKeyURL, keyType: .PUBLIC) - try? pgpAgent.initPGPKey(from: privateKeyURL, keyType: .PRIVATE) - XCTAssertTrue(pgpAgent.isImported) - XCTAssertEqual(pgpAgent.pgpKeyID, "A1024DAE") - XCTAssertTrue(self.basicEncryptDecrypt(pgpAgent: pgpAgent)) - pgpAgent.removePGPKeys() - - // [RSA2048] Setup keys from iTunes file sharing. - try? PGP_RSA2048_PUBLIC_KEY.write(to: publicKeyURL, atomically: false, encoding: .utf8) - try? PGP_RSA2048_PRIVATE_KEY.write(to: privateKeyURL, atomically: false, encoding: .utf8) - XCTAssertTrue(pgpAgent.isFileSharingReady) - try? pgpAgent.initPGPKeyFromFileSharing() - XCTAssertTrue(pgpAgent.isImported) - XCTAssertEqual(pgpAgent.pgpKeyID, "A1024DAE") - XCTAssertTrue(self.basicEncryptDecrypt(pgpAgent: pgpAgent)) - XCTAssertFalse(FileManager.default.fileExists(atPath: publicKeyURL.absoluteString)) - XCTAssertFalse(FileManager.default.fileExists(atPath: privateKeyURL.absoluteString)) - pgpAgent.removePGPKeys() - } - - func testInitPGPKeyBadPrivateKeys() { - let pgpAgent = PGPAgent(keyStore: keychain) - let plainData = "Hello World!".data(using: .utf8)! - - // [RSA2048] Setup the public key. - try? pgpAgent.initPGPKey(with: PGP_RSA2048_PUBLIC_KEY, keyType: .PUBLIC) - let encryptedData = try? pgpAgent.encrypt(plainData: plainData) - XCTAssertNotNil(encryptedData) - XCTAssertThrowsError(try pgpAgent.decrypt(encryptedData: encryptedData!, requestPGPKeyPassphrase: requestPGPKeyPassphrase)) - - // Wrong private key: a public key. - try? pgpAgent.initPGPKey(with: PGP_RSA2048_PUBLIC_KEY, keyType: .PRIVATE) - XCTAssertThrowsError(try pgpAgent.decrypt(encryptedData: encryptedData!, requestPGPKeyPassphrase: requestPGPKeyPassphrase)) - - // Wrong private key: an unmatched private key. - try? pgpAgent.initPGPKey(with: PGP_ED25519_PRIVATE_KEY, keyType: .PRIVATE) - XCTAssertThrowsError(try pgpAgent.decrypt(encryptedData: encryptedData!, requestPGPKeyPassphrase: requestPGPKeyPassphrase)) - - /// Wrong private key: a corrupted private key. - try? pgpAgent.initPGPKey(with: PGP_RSA2048_PRIVATE_KEY.replacingOccurrences(of: "1", with: ""), keyType: .PRIVATE) - XCTAssertThrowsError(try pgpAgent.decrypt(encryptedData: encryptedData!, requestPGPKeyPassphrase: requestPGPKeyPassphrase)) - } - -} -