From 6ae4a02a01ef4add72c84bb7412e89fe08ff14a6 Mon Sep 17 00:00:00 2001 From: Yishi Lin Date: Wed, 17 Jul 2019 02:58:01 +0800 Subject: [PATCH] Seperate PGPAgent from PasswordStore and add tests --- pass.xcodeproj/project.pbxproj | 12 + .../AddPasswordTableViewController.swift | 2 +- .../GeneralSettingsTableViewController.swift | 2 +- ...GPKeyArmorSettingTableViewController.swift | 2 +- .../PGPKeySettingTableViewController.swift | 2 +- .../PasswordDetailTableViewController.swift | 4 +- .../Controllers/PasswordsViewController.swift | 10 +- .../SettingsTableViewController.swift | 28 +-- .../CredentialProviderViewController.swift | 6 +- .../Controllers/ExtensionViewController.swift | 6 +- passKit/Models/PGPAgent.swift | 171 ++++++++++++++ passKit/Models/PasswordStore.swift | 165 ++------------ passKitTests/Models/PGPAgentTest.swift | 83 +++++++ passKitTests/TestPGPKeys.swift | 212 ++++++++++++++++++ 14 files changed, 526 insertions(+), 179 deletions(-) create mode 100644 passKit/Models/PGPAgent.swift create mode 100644 passKitTests/Models/PGPAgentTest.swift create mode 100644 passKitTests/TestPGPKeys.swift diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index 35e31b6..a3f4d62 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -96,6 +96,9 @@ A2A61C171EEF90CB00CFE063 /* OneTimePassword.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCA671DE1E7A73B100D3ABE1 /* OneTimePassword.framework */; }; A2A61C201EEFABAD00CFE063 /* UtilsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A61C1F1EEFABAD00CFE063 /* UtilsExtension.swift */; }; A2A7813F1E97DBD9001311F5 /* QRScannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A7813E1E97DBD9001311F5 /* QRScannerController.swift */; }; + A2AA934422DE30DD00D79A00 /* PGPAgent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2AA934322DE30DD00D79A00 /* PGPAgent.swift */; }; + A2AA934622DE3A8000D79A00 /* PGPAgentTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2AA934522DE3A8000D79A00 /* PGPAgentTest.swift */; }; + A2AA934822DE3F0200D79A00 /* TestPGPKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2AA934722DE3F0200D79A00 /* TestPGPKeys.swift */; }; DA2679F1424EA94B5B8997FB /* Pods_pass.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FE627E8F3DACEDD8FA220081 /* Pods_pass.framework */; }; DC037CA61E4B883900609409 /* OpenSourceComponentsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC037CA51E4B883900609409 /* OpenSourceComponentsTableViewController.swift */; }; DC037CA81E4B898100609409 /* BasicStaticTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC037CA71E4B898100609409 /* BasicStaticTableViewController.swift */; }; @@ -301,6 +304,9 @@ A2802BF81E70813A00879216 /* SliderTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SliderTableViewCell.xib; sourceTree = ""; }; A2A61C1F1EEFABAD00CFE063 /* UtilsExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UtilsExtension.swift; sourceTree = ""; }; A2A7813E1E97DBD9001311F5 /* QRScannerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRScannerController.swift; sourceTree = ""; }; + A2AA934322DE30DD00D79A00 /* PGPAgent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PGPAgent.swift; sourceTree = ""; }; + A2AA934522DE3A8000D79A00 /* PGPAgentTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PGPAgentTest.swift; sourceTree = ""; }; + A2AA934722DE3F0200D79A00 /* TestPGPKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestPGPKeys.swift; sourceTree = ""; }; A2BC54C71EEE5669001FAFBD /* Objective-CBridgingHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Objective-CBridgingHeader.h"; sourceTree = ""; }; DC037CA51E4B883900609409 /* OpenSourceComponentsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenSourceComponentsTableViewController.swift; sourceTree = ""; }; DC037CA71E4B898100609409 /* BasicStaticTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicStaticTableViewController.swift; sourceTree = ""; }; @@ -490,6 +496,7 @@ isa = PBXGroup; children = ( 30B0485F209A5141001013CA /* PasswordTest.swift */, + A2AA934522DE3A8000D79A00 /* PGPAgentTest.swift */, ); path = Models; sourceTree = ""; @@ -566,6 +573,7 @@ A26075871EEC6F34005DB03E /* passKitTests.swift */, 307BF39821BC2297003A082D /* TestBase.swift */, A26075891EEC6F34005DB03E /* Info.plist */, + A2AA934722DE3F0200D79A00 /* TestPGPKeys.swift */, ); path = passKitTests; sourceTree = ""; @@ -601,6 +609,7 @@ 30697C4021F63CAB0064FCAC /* Password.swift */, 30697C3F21F63CAA0064FCAC /* PasswordEntity.swift */, 30697C4321F63CAB0064FCAC /* PasswordStore.swift */, + A2AA934322DE30DD00D79A00 /* PGPAgent.swift */, ); path = Models; sourceTree = ""; @@ -1250,6 +1259,7 @@ 30697C3E21F63C990064FCAC /* String+Utilities.swift in Sources */, 302B2C9822C2BDE700D831EE /* AppKeychain.swift in Sources */, 3032327422C7F710009EBD9C /* KeyFileManager.swift in Sources */, + A2AA934422DE30DD00D79A00 /* PGPAgent.swift in Sources */, 30697C3B21F63C990064FCAC /* String+Localization.swift in Sources */, 302E85612125ECC70031BA64 /* Parser.swift in Sources */, 30697C4621F63CAB0064FCAC /* GitCredential.swift in Sources */, @@ -1275,10 +1285,12 @@ 30A1D2AC21B32C2A00E2D1F7 /* TokenBuilderTest.swift in Sources */, 301F646D216166AA0071A4CE /* AdditionFieldTest.swift in Sources */, 30FD2F78214D9E0E005E0A92 /* ParserTest.swift in Sources */, + A2AA934622DE3A8000D79A00 /* PGPAgentTest.swift in Sources */, 30B04860209A5141001013CA /* PasswordTest.swift in Sources */, 307BF39921BC2298003A082D /* TestBase.swift in Sources */, 30697C5F21F674800064FCAC /* String+UtilitiesTest.swift in Sources */, 3032328A22C9FBA2009EBD9C /* KeyFileManagerTest.swift in Sources */, + A2AA934822DE3F0200D79A00 /* TestPGPKeys.swift in Sources */, 30A1D2AA21B32A0100E2D1F7 /* OtpTypeTest.swift in Sources */, 301F6468216165290071A4CE /* ConstantsTest.swift in Sources */, 30A1D29C21AF451E00E2D1F7 /* PasswordGeneratorFlavourTest.swift in Sources */, diff --git a/pass/Controllers/AddPasswordTableViewController.swift b/pass/Controllers/AddPasswordTableViewController.swift index 16df9f1..e3421a3 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.privateKey != nil else { + guard passwordStore.pgpAgent?.imported ?? false 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 521dde1..34b803c 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.pgpKeyPassphrase = nil + passwordStore.pgpAgent?.passphrase = nil } } diff --git a/pass/Controllers/PGPKeyArmorSettingTableViewController.swift b/pass/Controllers/PGPKeyArmorSettingTableViewController.swift index 9a87aa3..24fc3a9 100644 --- a/pass/Controllers/PGPKeyArmorSettingTableViewController.swift +++ b/pass/Controllers/PGPKeyArmorSettingTableViewController.swift @@ -91,7 +91,7 @@ class PGPKeyArmorSettingTableViewController: AutoCellHeightUITableViewController override func viewDidLoad() { super.viewDidLoad() - pgpPassphrase = passwordStore.pgpKeyPassphrase + pgpPassphrase = passwordStore.pgpAgent?.passphrase scanPublicKeyCell?.textLabel?.text = "ScanPublicKeyQrCodes".localize() scanPublicKeyCell?.textLabel?.textColor = Globals.blue diff --git a/pass/Controllers/PGPKeySettingTableViewController.swift b/pass/Controllers/PGPKeySettingTableViewController.swift index af03282..554155b 100644 --- a/pass/Controllers/PGPKeySettingTableViewController.swift +++ b/pass/Controllers/PGPKeySettingTableViewController.swift @@ -20,7 +20,7 @@ class PGPKeySettingTableViewController: AutoCellHeightUITableViewController { super.viewDidLoad() pgpPublicKeyURLTextField.text = SharedDefaults[.pgpPublicKeyURL]?.absoluteString pgpPrivateKeyURLTextField.text = SharedDefaults[.pgpPrivateKeyURL]?.absoluteString - pgpPassphrase = passwordStore.pgpKeyPassphrase + pgpPassphrase = passwordStore.pgpAgent?.passphrase } private func validatePGPKeyURL(input: String?) -> Bool { diff --git a/pass/Controllers/PasswordDetailTableViewController.swift b/pass/Controllers/PasswordDetailTableViewController.swift index 5d8905f..b16b7cd 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.pgpKeyPassphrase = passphrase + self.passwordStore.pgpAgent?.passphrase = passphrase } 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.pgpKeyPassphrase = nil + self.passwordStore.pgpAgent?.passphrase = nil // 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 ea18752..224e7c1 100644 --- a/pass/Controllers/PasswordsViewController.swift +++ b/pass/Controllers/PasswordsViewController.swift @@ -385,13 +385,13 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV SVProgressHUD.show(withStatus: "Decrypting".localize()) } if SharedDefaults[.isRememberPGPPassphraseOn] { - self.passwordStore.pgpKeyPassphrase = passphrase + self.passwordStore.pgpAgent?.passphrase = passphrase } return passphrase } private func decryptThenCopyPassword(from indexPath: IndexPath) { - guard self.passwordStore.privateKey != nil else { + guard self.passwordStore.pgpAgent?.imported ?? false else { Utils.alert(title: "CannotCopyPassword".localize(), message: "SetPgpKey.".localize(), controller: self, completion: nil) return } @@ -412,7 +412,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV } catch { DispatchQueue.main.async { // remove the wrong passphrase so that users could enter it next time - self.passwordStore.pgpKeyPassphrase = nil + self.passwordStore.pgpAgent?.passphrase = nil Utils.alert(title: "CannotCopyPassword".localize(), message: error.localizedDescription, controller: self, completion: nil) } } @@ -453,7 +453,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { if identifier == "showPasswordDetail" { - guard self.passwordStore.hasPgpKey else { + guard self.passwordStore.pgpAgent?.imported ?? false 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 +462,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV return false } } else if identifier == "addPasswordSegue" { - guard self.passwordStore.hasPgpKey, self.passwordStore.storeRepository != nil else { + guard self.passwordStore.pgpAgent?.imported ?? false && 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 0689fec..57ffd9c 100644 --- a/pass/Controllers/SettingsTableViewController.swift +++ b/pass/Controllers/SettingsTableViewController.swift @@ -29,7 +29,7 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele SharedDefaults[.pgpPrivateKeyURL] = URL(string: controller.pgpPrivateKeyURLTextField.text!.trimmed) SharedDefaults[.pgpPublicKeyURL] = URL(string: controller.pgpPublicKeyURLTextField.text!.trimmed) if SharedDefaults[.isRememberPGPPassphraseOn] { - self.passwordStore.pgpKeyPassphrase = controller.pgpPassphrase + self.passwordStore.pgpAgent?.passphrase = controller.pgpPassphrase } SharedDefaults[.pgpKeySource] = "url" @@ -38,10 +38,10 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele SVProgressHUD.show(withStatus: "FetchingPgpKey".localize()) DispatchQueue.global(qos: .userInitiated).async { [unowned self] in do { - try self.passwordStore.initPGPKey(from: SharedDefaults[.pgpPublicKeyURL]!, keyType: .PUBLIC) - try self.passwordStore.initPGPKey(from: SharedDefaults[.pgpPrivateKeyURL]!, keyType: .PRIVATE) + try self.passwordStore.pgpAgent?.initPGPKey(from: SharedDefaults[.pgpPublicKeyURL]!, keyType: .PUBLIC) + try self.passwordStore.pgpAgent?.initPGPKey(from: SharedDefaults[.pgpPrivateKeyURL]!, keyType: .PRIVATE) DispatchQueue.main.async { - self.pgpKeyTableViewCell.detailTextLabel?.text = self.passwordStore.pgpKeyID + self.pgpKeyTableViewCell.detailTextLabel?.text = self.passwordStore.pgpAgent?.pgpKeyID SVProgressHUD.showSuccess(withStatus: "Success".localize()) SVProgressHUD.dismiss(withDelay: 1) Utils.alert(title: "RememberToRemoveKey".localize(), message: "RememberToRemoveKeyFromServer.".localize(), controller: self, completion: nil) @@ -57,17 +57,17 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele } else if let controller = segue.source as? PGPKeyArmorSettingTableViewController { SharedDefaults[.pgpKeySource] = "armor" if SharedDefaults[.isRememberPGPPassphraseOn] { - self.passwordStore.pgpKeyPassphrase = controller.pgpPassphrase + 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.initPGPKey(with: controller.armorPublicKeyTextView.text ?? "", keyType: .PUBLIC) - try self.passwordStore.initPGPKey(with: controller.armorPrivateKeyTextView.text ?? "", keyType: .PRIVATE) + try self.passwordStore.pgpAgent?.initPGPKey(with: controller.armorPublicKeyTextView.text ?? "", keyType: .PUBLIC) + try self.passwordStore.pgpAgent?.initPGPKey(with: controller.armorPrivateKeyTextView.text ?? "", keyType: .PRIVATE) DispatchQueue.main.async { - self.pgpKeyTableViewCell.detailTextLabel?.text = self.passwordStore.pgpKeyID + self.pgpKeyTableViewCell.detailTextLabel?.text = self.passwordStore.pgpAgent?.pgpKeyID SVProgressHUD.showSuccess(withStatus: "Success".localize()) SVProgressHUD.dismiss(withDelay: 1) } @@ -89,10 +89,10 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele SVProgressHUD.show(withStatus: "FetchingPgpKey".localize()) DispatchQueue.global(qos: .userInitiated).async { [unowned self] in do { - try self.passwordStore.pgpKeyImportFromFileSharing() - try self.passwordStore.initPGPKeys() + try self.passwordStore.pgpAgent?.pgpKeyImportFromFileSharing() + try self.passwordStore.pgpAgent?.initPGPKeys() DispatchQueue.main.async { - self.pgpKeyTableViewCell.detailTextLabel?.text = self.passwordStore.pgpKeyID + self.pgpKeyTableViewCell.detailTextLabel?.text = self.passwordStore.pgpAgent?.pgpKeyID SVProgressHUD.showSuccess(withStatus: "Imported".localize()) SVProgressHUD.dismiss(withDelay: 1) } @@ -136,7 +136,7 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele } private func setPGPKeyTableViewCellDetailText() { - if let pgpKeyID = self.passwordStore.pgpKeyID { + if let pgpKeyID = self.passwordStore.pgpAgent?.pgpKeyID { pgpKeyTableViewCell.detailTextLabel?.text = pgpKeyID } else { pgpKeyTableViewCell.detailTextLabel?.text = "NotSet".localize() @@ -200,7 +200,7 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele 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.pgpKeyPassphrase = nil + self.passwordStore.pgpAgent?.passphrase = nil SharedDefaults[.isRememberPGPPassphraseOn] = false self.saveImportedPGPKey() }) @@ -209,7 +209,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.pgpKeyPassphrase = alert.textFields?.first?.text + self.passwordStore.pgpAgent?.passphrase = alert.textFields?.first?.text SharedDefaults[.isRememberPGPPassphraseOn] = true self.saveImportedPGPKey() })) diff --git a/passAutoFillExtension/Controllers/CredentialProviderViewController.swift b/passAutoFillExtension/Controllers/CredentialProviderViewController.swift index 6bb5be5..66f9ec7 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.privateKey != nil else { + guard self.passwordStore.pgpAgent?.imported ?? false 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.pgpKeyPassphrase = nil + self.passwordStore.pgpAgent?.passphrase = nil 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.pgpKeyPassphrase = passphrase + self.passwordStore.pgpAgent?.passphrase = passphrase } return passphrase } diff --git a/passExtension/Controllers/ExtensionViewController.swift b/passExtension/Controllers/ExtensionViewController.swift index b1ef895..63b33c0 100644 --- a/passExtension/Controllers/ExtensionViewController.swift +++ b/passExtension/Controllers/ExtensionViewController.swift @@ -153,7 +153,7 @@ class ExtensionViewController: UIViewController, UITableViewDataSource, UITableV func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let entry = getPasswordEntry(by: indexPath) - guard self.passwordStore.privateKey != nil else { + guard self.passwordStore.pgpAgent?.imported ?? false else { Utils.alert(title: "CannotCopyPassword".localize(), message: "PgpKeyNotSet.".localize(), controller: self, completion: nil) return } @@ -191,7 +191,7 @@ class ExtensionViewController: UIViewController, UITableViewDataSource, UITableV } catch { DispatchQueue.main.async { // remove the wrong passphrase so that users could enter it next time - self.passwordStore.pgpKeyPassphrase = nil + self.passwordStore.pgpAgent?.passphrase = nil Utils.alert(title: "CannotCopyPassword".localize(), message: error.localizedDescription, controller: self, completion: nil) } } @@ -227,7 +227,7 @@ class ExtensionViewController: UIViewController, UITableViewDataSource, UITableV } let _ = sem.wait(timeout: DispatchTime.distantFuture) if SharedDefaults[.isRememberPGPPassphraseOn] { - self.passwordStore.pgpKeyPassphrase = passphrase + self.passwordStore.pgpAgent?.passphrase = passphrase } return passphrase } diff --git a/passKit/Models/PGPAgent.swift b/passKit/Models/PGPAgent.swift new file mode 100644 index 0000000..a93b104 --- /dev/null +++ b/passKit/Models/PGPAgent.swift @@ -0,0 +1,171 @@ +// +// 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 Gopenpgpwrapper + +public class PGPAgent { + + public var imported: Bool { + get { + return (publicKey != nil || publicKeyV2 != nil) && (privateKey != nil || privateKeyV2 != nil) + } + } + + public var pgpKeyID: String? + // PGP passphrase + public var passphrase: String? { + set { + AppKeychain.add(string: newValue, for: "pgpKeyPassphrase") + } + get { + return AppKeychain.get(for: "pgpKeyPassphrase") + } + } + + // Gopenpgpwrapper + private var publicKey: GopenpgpwrapperKey? { + didSet { + pgpKeyID = publicKey?.getID() + } + } + private var privateKey: GopenpgpwrapperKey? + // ObjectivePGP + private let keyring = ObjectivePGP.defaultKeyring + private var publicKeyV2: Key? { + didSet { + pgpKeyID = publicKeyV2?.keyID.shortIdentifier + } + } + private var privateKeyV2: Key? + + public func initPGPKeys() throws { + try initPGPKey(.PUBLIC) + try initPGPKey(.PRIVATE) + } + + public func initPGPKey(_ keyType: PgpKey) throws { + // Read the key data from keychain. + guard let pgpKeyData: Data = AppKeychain.get(for: keyType.getKeychainKey()) else { + throw AppError.KeyImport + } + + // Remove the key data from keychain temporary, in case the following step crashes repeatedly. + AppKeychain.removeContent(for: keyType.getKeychainKey()) + + // Try GopenpgpwrapperReadKey first. + if let key = GopenpgpwrapperReadKey(pgpKeyData) { + switch keyType { + case .PUBLIC: + self.publicKey = key + case .PRIVATE: + self.privateKey = key + } + AppKeychain.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 + } + AppKeychain.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) + AppKeychain.add(data: pgpKeyData, for: keyType.getKeychainKey()) + try initPGPKey(keyType) + } + + public func initPGPKey(with armorKey: String, keyType: PgpKey) throws { + let pgpKeyData = armorKey.data(using: .ascii)! + AppKeychain.add(data: pgpKeyData, for: keyType.getKeychainKey()) + try initPGPKey(keyType) + } + + public func pgpKeyImportFromFileSharing() throws { + try KeyFileManager.PublicPgp.importKeyAndDeleteFile() + try KeyFileManager.PrivatePgp.importKeyAndDeleteFile() + } + + 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 { + if let decryptedData = privateKey?.decrypt(encryptedData, passphrase: passphrase) { + return decryptedData + } + } + // Try ObjectivePGP. + if privateKeyV2 != nil { + if let decryptedData = try? ObjectivePGP.decrypt(encryptedData, andVerifySignature: false, using: keyring.keys, passphraseForKey: {(_) in passphrase}) { + print(decryptedData.base64EncodedString()) + 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 = publicKey?.encrypt(plainData, armor: SharedDefaults[.encryptInArmored]) { + return encryptedData + } + } + // 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() { + try? FileManager.default.removeItem(atPath: Globals.pgpPublicKeyPath) + try? FileManager.default.removeItem(atPath: Globals.pgpPrivateKeyPath) + SharedDefaults.remove(.pgpKeySource) + SharedDefaults.remove(.pgpPrivateKeyURL) + SharedDefaults.remove(.pgpPublicKeyURL) + SharedDefaults.remove(.pgpPublicKeyArmor) + SharedDefaults.remove(.pgpPrivateKeyArmor) + AppKeychain.removeContent(for: PgpKey.PUBLIC.getKeychainKey()) + AppKeychain.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 f1d6a38..a3a2887 100644 --- a/passKit/Models/PasswordStore.swift +++ b/passKit/Models/PasswordStore.swift @@ -11,9 +11,7 @@ import CoreData import UIKit import SwiftyUserDefaults import ObjectiveGit -import ObjectivePGP import KeychainAccess -import Gopenpgpwrapper public class PasswordStore { public static let shared = PasswordStore() @@ -27,28 +25,9 @@ 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 pgpKeyID: String? - public var hasPgpKey: Bool { - get { - return (publicKey != nil || publicKeyV2 != nil) && (privateKey != nil || privateKeyV2 != nil) - } - } - // Gopenpgpwrapper - public var publicKey: GopenpgpwrapperKey? { - didSet { - pgpKeyID = publicKey?.getID() - } - } - public var privateKey: GopenpgpwrapperKey? - // ObjectivePGP - public let keyring = ObjectivePGP.defaultKeyring - public var publicKeyV2: Key? { - didSet { - pgpKeyID = publicKeyV2?.keyID.shortIdentifier - } - } - public var privateKeyV2: Key? public var gitSignatureForNow: GTSignature? { get { @@ -57,15 +36,6 @@ public class PasswordStore { return GTSignature(name: gitSignatureName, email: gitSignatureEmail, time: Date()) } } - - public var pgpKeyPassphrase: String? { - set { - AppKeychain.add(string: newValue, for: "pgpKeyPassphrase") - } - get { - return AppKeychain.get(for: "pgpKeyPassphrase") - } - } public var gitPassword: String? { set { @@ -134,6 +104,8 @@ public class PasswordStore { } private init() { + self.pgpAgent = PGPAgent() + // File migration to group migrateIfNeeded() backwardCompatibility() @@ -143,7 +115,7 @@ public class PasswordStore { if fm.fileExists(atPath: storeURL.path) { try storeRepository = GTRepository.init(url: storeURL) } - try initPGPKeys() + try self.pgpAgent?.initPGPKeys() } catch { print(error) } @@ -189,10 +161,6 @@ public class PasswordStore { if (self.gitPassword != nil || self.gitSSHPrivateKeyPassphrase != nil) && SharedDefaults[.isRememberGitCredentialPassphraseOn] == false { SharedDefaults[.isRememberGitCredentialPassphraseOn] = true } - // For the renamed isRememberPGPPassphraseOn (20171008) - if self.pgpKeyPassphrase != nil && SharedDefaults[.isRememberPGPPassphraseOn] == false { - SharedDefaults[.isRememberPGPPassphraseOn] = true - } } private func importExistingKeysIntoKeychain() { @@ -208,54 +176,6 @@ public class PasswordStore { AppKeychain.add(string: armorKey, for: SshKey.PRIVATE.getKeychainKey()) } - public func initPGPKeys() throws { - try initPGPKey(.PUBLIC) - try initPGPKey(.PRIVATE) - } - - private func initPGPKey(_ keyType: PgpKey) throws { - // Read the key data from keychain. - let pgpKeyData: Data? = AppKeychain.get(for: keyType.getKeychainKey()) - - // Try GopenpgpwrapperReadKey first. - if let key = GopenpgpwrapperReadKey(pgpKeyData) { - switch keyType { - case .PUBLIC: - self.publicKey = key - case .PRIVATE: - self.privateKey = key - } - return - } - - // Try ObjectivePGP as a backup plan. - 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 - } - return - } - - throw AppError.KeyImport - } - - public func initPGPKey(from url: URL, keyType: PgpKey) throws { - let pgpKeyData = try Data(contentsOf: url) - AppKeychain.add(data: pgpKeyData, for: keyType.getKeychainKey()) - try initPGPKey(keyType) - } - - public func initPGPKey(with armorKey: String, keyType: PgpKey) throws { - let pgpKeyData = armorKey.data(using: .ascii)! - AppKeychain.add(data: pgpKeyData, for: keyType.getKeychainKey()) - try initPGPKey(keyType) - } - public func repositoryExisted() -> Bool { let fm = FileManager() return fm.fileExists(atPath: Globals.repositoryPath) @@ -758,20 +678,13 @@ public class PasswordStore { } } - public func erase() { - publicKey = nil - privateKey = nil - publicKeyV2 = nil - privateKeyV2 = nil - keyring.deleteAll() - - try? fm.removeItem(at: storeURL) + public func erase() {try? fm.removeItem(at: storeURL) try? fm.removeItem(at: tempStoreURL) - try? fm.removeItem(atPath: Globals.pgpPublicKeyPath) - try? fm.removeItem(atPath: Globals.pgpPrivateKeyPath) try? fm.removeItem(atPath: Globals.gitSSHPrivateKeyPath) + self.pgpAgent?.removePGPKeys() + AppKeychain.removeAllContent() deleteCoreData(entityName: "PasswordEntity") @@ -828,66 +741,27 @@ public class PasswordStore { return try storeRepository.localCommitsRelative(toRemoteBranch: remoteBranch) } - - public func decrypt(passwordEntity: PasswordEntity, requestPGPKeyPassphrase: () -> String) throws -> Password? { let encryptedDataPath = storeURL.appendingPathComponent(passwordEntity.getPath()) let encryptedData = try Data(contentsOf: encryptedDataPath) - var passphrase = self.pgpKeyPassphrase - if passphrase == nil { - passphrase = requestPGPKeyPassphrase() + guard let decryptedData = try self.pgpAgent?.decrypt(encryptedData: encryptedData, requestPGPKeyPassphrase: requestPGPKeyPassphrase) else { + throw AppError.Decryption } - // Try Gopenpgp. - if let decryptedData = privateKey?.decrypt(encryptedData, passphrase: passphrase) { - let plainText = String(data: decryptedData, encoding: .utf8) ?? "" - let url = try passwordEntity.getURL() - return Password(name: passwordEntity.getName(), url: url, plainText: plainText) - } - // Try ObjectivePGP. - if let decryptedData = try? ObjectivePGP.decrypt(encryptedData, andVerifySignature: false, using: keyring.keys, passphraseForKey: {(_) in passphrase}) { - let plainText = String(data: decryptedData, encoding: .utf8) ?? "" - let url = try passwordEntity.getURL() - return Password(name: passwordEntity.getName(), url: url, plainText: plainText) - } - throw AppError.Decryption + let plainText = String(data: decryptedData, encoding: .utf8) ?? "" + let url = try passwordEntity.getURL() + return Password(name: passwordEntity.getName(), url: url, plainText: plainText) } public func encrypt(password: Password) throws -> Data { - guard publicKey != nil || keyring.keys.count > 0 else { - throw AppError.PgpPublicKeyNotExist - } let plainData = password.plainData - // Try Gopenpgp. - if let encryptedData = publicKey?.encrypt(plainData, armor: SharedDefaults[.encryptInArmored]) { - return encryptedData + guard let encryptedData = try self.pgpAgent?.encrypt(plainData: plainData) else { + throw AppError.Encryption } - // Try ObjectivePGP. - 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 + return encryptedData } public func removePGPKeys() { - try? fm.removeItem(atPath: Globals.pgpPublicKeyPath) - try? fm.removeItem(atPath: Globals.pgpPrivateKeyPath) - SharedDefaults.remove(.pgpKeySource) - SharedDefaults.remove(.pgpPrivateKeyURL) - SharedDefaults.remove(.pgpPublicKeyURL) - SharedDefaults.remove(.pgpPublicKeyArmor) - SharedDefaults.remove(.pgpPrivateKeyArmor) - AppKeychain.removeContent(for: PgpKey.PUBLIC.getKeychainKey()) - AppKeychain.removeContent(for: PgpKey.PRIVATE.getKeychainKey()) - pgpKeyPassphrase = nil - publicKey = nil - privateKey = nil - publicKeyV2 = nil - privateKeyV2 = nil - keyring.deleteAll() + self.pgpAgent?.removePGPKeys() } public func removeGitSSHKeys() { @@ -910,9 +784,4 @@ public class PasswordStore { public func gitSSHKeyImportFromFileSharing() throws { try KeyFileManager.PrivateSsh.importKeyAndDeleteFile() } - - public func pgpKeyImportFromFileSharing() throws { - try KeyFileManager.PublicPgp.importKeyAndDeleteFile() - try KeyFileManager.PrivatePgp.importKeyAndDeleteFile() - } } diff --git a/passKitTests/Models/PGPAgentTest.swift b/passKitTests/Models/PGPAgentTest.swift new file mode 100644 index 0000000..473009c --- /dev/null +++ b/passKitTests/Models/PGPAgentTest.swift @@ -0,0 +1,83 @@ +// +// 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 { + + override func setUp() { + print("setup") + AppKeychain.removeAllContent() + } + + override func tearDown() { + print("tearDown") + AppKeychain.removeAllContent() + } + + func encrypt_decrypt(pgpAgent: PGPAgent) { + // Encrypt and decrypt. + let plainData = "Hello World!".data(using: .utf8)! + let encryptedData = try? pgpAgent.encrypt(plainData: plainData) + XCTAssertNotNil(encryptedData) + let decryptedData = try? pgpAgent.decrypt(encryptedData: encryptedData!, requestPGPKeyPassphrase: requestPGPKeyPassphrase) + XCTAssertEqual(plainData, decryptedData) + } + + func testInitPGPKey() { + let pgpAgent = PGPAgent() + + // [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.imported) + self.encrypt_decrypt(pgpAgent: pgpAgent) + let pgpAgent2 = PGPAgent() + try? pgpAgent2.initPGPKeys() // load from the keychain + self.encrypt_decrypt(pgpAgent: pgpAgent2) + + // [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.imported) + self.encrypt_decrypt(pgpAgent: pgpAgent) + + // [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.imported) + self.encrypt_decrypt(pgpAgent: pgpAgent) + } + + func testInitPGPKeyBadPrivateKeys() { + let pgpAgent = PGPAgent() + 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)) + } + +} + diff --git a/passKitTests/TestPGPKeys.swift b/passKitTests/TestPGPKeys.swift new file mode 100644 index 0000000..13a0003 --- /dev/null +++ b/passKitTests/TestPGPKeys.swift @@ -0,0 +1,212 @@ +// +// TestPGPKeys.swift +// passKitTests +// +// Created by Yishi Lin on 2019/7/17. +// Copyright © 2019 Bob Sun. All rights reserved. +// + +import XCTest + +@testable import passKit + +func requestPGPKeyPassphrase() -> String { + return "passforios" +} + +let PGP_RSA2048_PUBLIC_KEY = """ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBF0uBLkBCADGYAW5yJQ/lLHmHdb/l0bhYyLnhuyWTaOWIEHGiDnpjO71vBPD +WMPxPulj/GGdqzR4lUUanPG1yJwXTjNutjMBZ/o+ixgdqja391b4L8VyF5Lb7pHX +c6uvbR6p1Tj72PJrVj6Ev71VdnsHNOLUDqWZL4b0dNXPf3sM4ry5yS6Ej6ho9o/l +nuYbe9WfLqUctSM+TsXd84ptc6luIHZN+cojrNGAh3RLYuVFeJjkLkvHdJBWJQS2 +zJ0iAA6T0NbfPGrVL6tMaB7NwMDN33Ql3ARfaHf7/d8DXGYyGUYZFqyKagGUaBxN +arXfzv9gIyc109D8pPMN5QUiEo9K4x3xNpLhABEBAAG0KnBhc3Nmb3Jpb3MgPGRl +dmVsb3BlckBwYXNzZm9yaW9zLm1zc3VuLm1lPokBTgQTAQgAOAIbAwULCQgHAgYV +CgkICwIEFgIDAQIeAQIXgBYhBEcSKGJxIg2ymYg+pwYuZ42hAk2uBQJdLgWNAAoJ +EAYuZ42hAk2uYjIH/iPsoJD+KfJ/c7GTSLbeMtQtuIv2sCiSc+BiXp+EFjbNS7oS +cEYiG/FX1zuivr3wvDGbE/oVLvdTK8tDJvCh17SZZUGeTmflgByig+1/kp5JUj2x +FOH3xXCVCnh+FWB24QHLiFpwgzGuZgCKMn45PcZB7Zmo/wJsKYb040zKltQWmzg8 +rZ1vOKlMgAGgdNQOu+u+8OE6x/dedYvJZePvwND4Rva+EwS+MaEZZAn76ypEblhF +pRZsTb6+PeTY+IWRnne7xUvYRmZLXhqoHq/c0hXXz2xAGmpigd50HUQRUOYtsuBi +RxXLwFIlfPpziomMTdhnOFVDL6UVLL+jqJDPHWu5AQ0EXS4EuQEIAMwr3I+NUsY+ +Iwyyz6C84z90kHPs9zOBvGc73Tgc70lXHUklwQaqLSmLbHQzk6Ykw/o1snQ8Oz2f +AjPlBr9/zWJgOvnFH+FrjpfEpmz4WjmoxBK610JINkKW4Fn4VT/NrBL9LtjaZOLp +0XA6fh8TXTOsz80jDj1VPvNAsu78FC+zXFDvOJpX+bCv3vabO9/ihKL/emvgqzxD +LO9IapPfL1rPvCmfaNZ8hRV8ebHOW0pVLfyOYn4pvwtL/uYIINv1CYAwOGVW1sth +8kS8Acj4VTDkynuobxiLDTpNrRUbt0iPn/2nJ8m1qq9hqWgO2z01UkSKrusDiMNH +TnHFepgG8NMAEQEAAYkBPAQYAQgAJhYhBEcSKGJxIg2ymYg+pwYuZ42hAk2uBQJd +LgS5AhsMBQkDwmcAAAoJEAYuZ42hAk2u+rkIAKEwherMhNV+kZpZMoYLop9ULTNx +RQtBhNK5No7MpGzJnGbJzXo2+CaB7wlOzzDympIVxth+St7S4BiSdMxrnQVzzZOf +Lfvc6+AyWBwpM32i/s5piTCwn+RoQzYHQpiRBmeGRBXC2tTkAh+zXhed7yZR5mMK +lsVq9BRfVFxbkh9S1YWLUC29n32eiAo/6nLyJw/AOlv37ThYPnIeSqrASs0Jh69C +iNbGUq2SzH1yj1cxWvhZqXA8WpbU/pZM6LR6eIeLOa+BTp0Ax6j2QQ3N3bKEcOTz +ncsjMsi8gUJaucHKKRcGXY0AlWAbvH1ytWboh+CgS493JfZbNRCXTWA/BDE= +=2FVp +-----END PGP PUBLIC KEY BLOCK----- +""" + +let PGP_RSA2048_PRIVATE_KEY = """ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +=qgCJ +-----END PGP PRIVATE KEY BLOCK----- +""" + +let PGP_RSA2048_PRIVATE_SUBKEY = """ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +=qBfw +-----END PGP PRIVATE KEY BLOCK----- +""" + +let PGP_ED25519_PUBLIC_KEY = """ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEXS4TMRYJKwYBBAHaRw8BAQdAW/U3l3/bMWFxHaCQ3Kr8Wel5322hgiVGO39K +mk+NLYO0KnBhc3Nmb3Jpb3MgPGRldmVsb3BlckBwYXNzZm9yaW9zLm1zc3VuLm1l +PoiQBBMWCAA4FiEEX8ywgauK9IlymZ4q51Csv+lERIMFAl0uEzECGwMFCwkIBwIG +FQoJCAsCBBYCAwECHgECF4AACgkQ51Csv+lERIM0RgEAtw6VI0hbhIiSCZuHfePn +3XmXQnSkfYw6OrND86vNPEgBAIcymV45aM3IUMi7cczlNemdc4vzvZ2sahN4I9bp +zygDuDgEXS4TMRIKKwYBBAGXVQEFAQEHQDEc6qSqOJHTY/QBMBCuH2NPbc3jfJKe +3zN15IWfji9hAwEIB4h4BBgWCAAgFiEEX8ywgauK9IlymZ4q51Csv+lERIMFAl0u +EzECGwwACgkQ51Csv+lERIMOsAEA43pkVP9br/TmESz/IuTZBfVAexAA3Q9DRL7M +NcZVu2wA/js8TfomM2QBmZYPSZFPzA1nI1FzzgPhFgy6fGLgpZUL +=4EF6 +-----END PGP PUBLIC KEY BLOCK----- +""" + +let PGP_ED25519_PRIVATE_KEY = """ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lIYEXS4TMRYJKwYBBAHaRw8BAQdAW/U3l3/bMWFxHaCQ3Kr8Wel5322hgiVGO39K +mk+NLYP+BwMCw1EywRMGVRHmRf963xPpOVBluE38pFvriDQXbceoPZVdgyr+M2Ef +3HFn8t/ZyTzeZQZIgmlAFNgiDaz3I+CISN89uVFt7yJVGzbuvjgVSrQqcGFzc2Zv +cmlvcyA8ZGV2ZWxvcGVyQHBhc3Nmb3Jpb3MubXNzdW4ubWU+iJAEExYIADgWIQRf +zLCBq4r0iXKZnirnUKy/6UREgwUCXS4TMQIbAwULCQgHAgYVCgkICwIEFgIDAQIe +AQIXgAAKCRDnUKy/6UREgzRGAQC3DpUjSFuEiJIJm4d94+fdeZdCdKR9jDo6s0Pz +q808SAEAhzKZXjlozchQyLtxzOU16Z1zi/O9naxqE3gj1unPKAOciwRdLhMxEgor +BgEEAZdVAQUBAQdAMRzqpKo4kdNj9AEwEK4fY09tzeN8kp7fM3XkhZ+OL2EDAQgH +/gcDAsJwqoLIpKyw5lJwU83TfgSJJLrBR6KGLB1oZigRAasw++69iC19yUS6FY3M +mRiOrYeBatYCoXY8xolbStkhZl2y9KYlPcFNWOQvVKtuUT2IeAQYFggAIBYhBF/M +sIGrivSJcpmeKudQrL/pRESDBQJdLhMxAhsMAAoJEOdQrL/pRESDDrABAON6ZFT/ +W6/05hEs/yLk2QX1QHsQAN0PQ0S+zDXGVbtsAP47PE36JjNkAZmWD0mRT8wNZyNR +c84D4RYMunxi4KWVCw== +=nV4i +-----END PGP PRIVATE KEY BLOCK----- +""" + +let PGP_ED25519_PRIVATE_SUBKEY = """ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lDsEXS4TMRYJKwYBBAHaRw8BAQdAW/U3l3/bMWFxHaCQ3Kr8Wel5322hgiVGO39K +mk+NLYP/AGUAR05VAbQqcGFzc2ZvcmlvcyA8ZGV2ZWxvcGVyQHBhc3Nmb3Jpb3Mu +bXNzdW4ubWU+iJAEExYIADgWIQRfzLCBq4r0iXKZnirnUKy/6UREgwUCXS4TMQIb +AwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRDnUKy/6UREgzRGAQC3DpUjSFuE +iJIJm4d94+fdeZdCdKR9jDo6s0Pzq808SAEAhzKZXjlozchQyLtxzOU16Z1zi/O9 +naxqE3gj1unPKAOciwRdLhMxEgorBgEEAZdVAQUBAQdAMRzqpKo4kdNj9AEwEK4f +Y09tzeN8kp7fM3XkhZ+OL2EDAQgH/gcDAk55ulCfKROW5kyrg18FeoHR+DNPhKVV +7R2CyAinW5c4+SiLk+P4Zbgue1JEmCNqhckywVx6LOFoR3OdJRFumfuacB82QUY+ +p9VjMutDAO+IeAQYFggAIBYhBF/MsIGrivSJcpmeKudQrL/pRESDBQJdLhMxAhsM +AAoJEOdQrL/pRESDDrABAON6ZFT/W6/05hEs/yLk2QX1QHsQAN0PQ0S+zDXGVbts +AP47PE36JjNkAZmWD0mRT8wNZyNRc84D4RYMunxi4KWVCw== +=4Sfz +-----END PGP PRIVATE KEY BLOCK----- +"""