Seperate PGPAgent from PasswordStore and add tests

This commit is contained in:
Yishi Lin 2019-07-17 02:58:01 +08:00
parent 0862c1388e
commit 6ae4a02a01
14 changed files with 526 additions and 179 deletions

View file

@ -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 = "<group>"; };
A2A61C1F1EEFABAD00CFE063 /* UtilsExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UtilsExtension.swift; sourceTree = "<group>"; };
A2A7813E1E97DBD9001311F5 /* QRScannerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRScannerController.swift; sourceTree = "<group>"; };
A2AA934322DE30DD00D79A00 /* PGPAgent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PGPAgent.swift; sourceTree = "<group>"; };
A2AA934522DE3A8000D79A00 /* PGPAgentTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PGPAgentTest.swift; sourceTree = "<group>"; };
A2AA934722DE3F0200D79A00 /* TestPGPKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestPGPKeys.swift; sourceTree = "<group>"; };
A2BC54C71EEE5669001FAFBD /* Objective-CBridgingHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Objective-CBridgingHeader.h"; sourceTree = "<group>"; };
DC037CA51E4B883900609409 /* OpenSourceComponentsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenSourceComponentsTableViewController.swift; sourceTree = "<group>"; };
DC037CA71E4B898100609409 /* BasicStaticTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicStaticTableViewController.swift; sourceTree = "<group>"; };
@ -490,6 +496,7 @@
isa = PBXGroup;
children = (
30B0485F209A5141001013CA /* PasswordTest.swift */,
A2AA934522DE3A8000D79A00 /* PGPAgentTest.swift */,
);
path = Models;
sourceTree = "<group>";
@ -566,6 +573,7 @@
A26075871EEC6F34005DB03E /* passKitTests.swift */,
307BF39821BC2297003A082D /* TestBase.swift */,
A26075891EEC6F34005DB03E /* Info.plist */,
A2AA934722DE3F0200D79A00 /* TestPGPKeys.swift */,
);
path = passKitTests;
sourceTree = "<group>";
@ -601,6 +609,7 @@
30697C4021F63CAB0064FCAC /* Password.swift */,
30697C3F21F63CAA0064FCAC /* PasswordEntity.swift */,
30697C4321F63CAB0064FCAC /* PasswordStore.swift */,
A2AA934322DE30DD00D79A00 /* PGPAgent.swift */,
);
path = Models;
sourceTree = "<group>";
@ -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 */,

View file

@ -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)

View file

@ -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
}
}

View file

@ -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

View file

@ -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 {

View file

@ -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

View file

@ -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
}

View file

@ -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()
}))

View file

@ -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
}

View file

@ -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
}

View file

@ -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()
}
}

View file

@ -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 {
@ -58,15 +37,6 @@ public class PasswordStore {
}
}
public var pgpKeyPassphrase: String? {
set {
AppKeychain.add(string: newValue, for: "pgpKeyPassphrase")
}
get {
return AppKeychain.get(for: "pgpKeyPassphrase")
}
}
public var gitPassword: String? {
set {
AppKeychain.add(string: newValue, for: "gitPassword")
@ -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()
}
}

View file

@ -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))
}
}

View file

@ -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-----
lQPGBF0uBLkBCADGYAW5yJQ/lLHmHdb/l0bhYyLnhuyWTaOWIEHGiDnpjO71vBPD
WMPxPulj/GGdqzR4lUUanPG1yJwXTjNutjMBZ/o+ixgdqja391b4L8VyF5Lb7pHX
c6uvbR6p1Tj72PJrVj6Ev71VdnsHNOLUDqWZL4b0dNXPf3sM4ry5yS6Ej6ho9o/l
nuYbe9WfLqUctSM+TsXd84ptc6luIHZN+cojrNGAh3RLYuVFeJjkLkvHdJBWJQS2
zJ0iAA6T0NbfPGrVL6tMaB7NwMDN33Ql3ARfaHf7/d8DXGYyGUYZFqyKagGUaBxN
arXfzv9gIyc109D8pPMN5QUiEo9K4x3xNpLhABEBAAH+BwMCZUxYUsm8vEjmyFBJ
9ESBdIyoTe5hGiPUJASHc3/ywMTZdNp2hv/R8W3YEuyKEW2hyMGvKllWk4aWF3xL
C6Qm99pPwpuqbPlhPNfPjISj8XqZv4wPiY1CDaLXsvewADr9wQ016JkDnK4En5QL
pZY1QMOupEki9DLa7ybgFm5JqRHBrbPKJKzHz4nkBKGGAyhaSPauxg8fZ8Z4FdOM
IyVgeYLJquK4JaywwVuzjV0RfvF26aGgmCxQrrS4X53GUMpPhrJYQzVpoGku/4V4
8MVaXv/3VM4GVUwq2AGShhw0gF5vJq7fT9vh5y5uD3smZgGvtmIEdcOkbEyNXi+v
7nIKhtEq5wtrC7JlMcyTr6TYMAQ7Q1JEBrzwJl7n8WkULPLkY0JY4IsjoGRA6VDF
wdYn6RxztzX1frBli1srXqdRulW9MbYW+VkbjbyuKjE1CGIfzL31Rb1+RZSKJRcp
rSGi2wSRk6hQpwuX1zYeq2kg4I2E7voiD6wfUH/XlMQVEkhoGc6CL1cMBfInrBZW
Ar21DMjVm4PZTh0alZdhrZZLbWQg8kRSp10uam1DGqhgAKREEnJycrCOoAPNiWA1
dsbYIQQ4sP45V2LCYGdauSR1stw+GN6LVLLvTWB8vbFk2XnWk4TgZ760KVxYVuyO
Whq8YIluiQRNq9ZHDGWoA8ADezhzkEV3AjZlwlHRXeWuk8nEKwLaafDDqwAVQJbv
LT4BubyhXEJdYpUmqZw/zGv1AWT5mAfIZzI7N0ZlkhYFNY9yZChIv1yjX17yxpai
3qM34ZKhIlUlRLVfR4T+nxX2mo3JJYiH1Nepug/HLeGzuB1pCkpudnA7GxsHJIsz
YJHtGTFBf2K7cKCUwAKMU2PmoNj9BkKpj7hAOZqZWVxfOtFcQtym2gOFdMMykT2A
n+LPOIW9CRK0tCpwYXNzZm9yaW9zIDxkZXZlbG9wZXJAcGFzc2Zvcmlvcy5tc3N1
bi5tZT6JAU4EEwEIADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQRHEihi
cSINspmIPqcGLmeNoQJNrgUCXS4FjQAKCRAGLmeNoQJNrmIyB/4j7KCQ/inyf3Ox
k0i23jLULbiL9rAoknPgYl6fhBY2zUu6EnBGIhvxV9c7or698LwxmxP6FS73UyvL
Qybwode0mWVBnk5n5YAcooPtf5KeSVI9sRTh98VwlQp4fhVgduEBy4hacIMxrmYA
ijJ+OT3GQe2ZqP8CbCmG9ONMypbUFps4PK2dbzipTIABoHTUDrvrvvDhOsf3XnWL
yWXj78DQ+Eb2vhMEvjGhGWQJ++sqRG5YRaUWbE2+vj3k2PiFkZ53u8VL2EZmS14a
qB6v3NIV189sQBpqYoHedB1EEVDmLbLgYkcVy8BSJXz6c4qJjE3YZzhVQy+lFSy/
o6iQzx1rnQPGBF0uBLkBCADMK9yPjVLGPiMMss+gvOM/dJBz7PczgbxnO904HO9J
Vx1JJcEGqi0pi2x0M5OmJMP6NbJ0PDs9nwIz5Qa/f81iYDr5xR/ha46XxKZs+Fo5
qMQSutdCSDZCluBZ+FU/zawS/S7Y2mTi6dFwOn4fE10zrM/NIw49VT7zQLLu/BQv
s1xQ7ziaV/mwr972mzvf4oSi/3pr4Ks8QyzvSGqT3y9az7wpn2jWfIUVfHmxzltK
VS38jmJ+Kb8LS/7mCCDb9QmAMDhlVtbLYfJEvAHI+FUw5Mp7qG8Yiw06Ta0VG7dI
j5/9pyfJtaqvYaloDts9NVJEiq7rA4jDR05xxXqYBvDTABEBAAH+BwMCqca+BQok
MInmSRkW8zzDFBqVXZJJxA6k3aD5msGuHYBkGy5/Ybdy4NaG83kAfGnMxgC7C6wE
Ewt891NiMT1ZIrT8Pm9OO+mbJLpZsGMzqAr/oL4JY+BE5fmIfq88GYIFYZ9AQ5ne
AA0ACt38YZ7CMAFnIvzVsjLuZQiRYh96tFPT073Vx4H585p4KTvmly+Lx8R02WDu
OGe2nsQ50JYvcWe94uVHrouhqbqbSTnBETHfhhnQM/iJSFr8I4OSpYP+5xMwzTF9
cJDtRF1XvALMmg8h1nSVNbPSby/FFgmX89odCQzMCzSsjRsNvMNKNSrLrHEcZZmF
uBXB9jO9Ioe1HY15xuyJHM86lxARwd6fh1A8xrA028mL4COnnI527n6xEeX692aw
b5TpUot1AcYPTeMMnumGhno+5uAxuni54KuAG2h/scxk2U3RTom6UaGsL1Qc+K9Z
YKaW5X9uNf45YEMUCwKAx0axGTFuLB0xee4OYLR7IoerbKf2dTCbHT4ADp+h9ORS
u3FhevkhbLIn2cpoALk1vQeGlLeHhoABpYRicX5OOgpSY7YHv6nTNV2ivQIXu308
86V3lKrw+fIQWqRJyAhMPrrvG8kpb5jFupqqpSHuSLp3q4mubyMF57HC8srC7e6y
/xUpeWA2StUzkAFbZyLjDrdzzLXVKzQa17gkV2rOiyicMW3ktgs+XqZGV0y2k9e5
AU7ognsxeRq9OkecRxYyR/jB9NEFNFYaLhM2fr7bD1tla3IKj+6VvjZW3U8cJ2b1
UOsaWgG0aTEVVfZ15qeBBtG6d0XjPrn8FO9lH71MHlteTTqBNwF4z7RUHyak/oTP
ez74XHRvLwvwWTpvT7SXPPktVjCdmj6X3Dd5beYeT61nDXDD3Dfgon8nK/gvPHQm
PaFErweEZEsyij8FMoZTiQE8BBgBCAAmFiEERxIoYnEiDbKZiD6nBi5njaECTa4F
Al0uBLkCGwwFCQPCZwAACgkQBi5njaECTa76uQgAoTCF6syE1X6Rmlkyhguin1Qt
M3FFC0GE0rk2jsykbMmcZsnNejb4JoHvCU7PMPKakhXG2H5K3tLgGJJ0zGudBXPN
k58t+9zr4DJYHCkzfaL+zmmJMLCf5GhDNgdCmJEGZ4ZEFcLa1OQCH7NeF53vJlHm
YwqWxWr0FF9UXFuSH1LVhYtQLb2ffZ6ICj/qcvInD8A6W/ftOFg+ch5KqsBKzQmH
r0KI1sZSrZLMfXKPVzFa+FmpcDxaltT+lkzotHp4h4s5r4FOnQDHqPZBDc3dsoRw
5POdyyMyyLyBQlq5wcopFwZdjQCVYBu8fXK1ZuiH4KBLj3cl9ls1EJdNYD8EMQ==
=qgCJ
-----END PGP PRIVATE KEY BLOCK-----
"""
let PGP_RSA2048_PRIVATE_SUBKEY = """
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
XY0AlWAbvH1ytWboh+CgS493JfZbNRCXTWA/BDE=
=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-----
"""