diff --git a/pass/Controllers/GeneralSettingsTableViewController.swift b/pass/Controllers/GeneralSettingsTableViewController.swift index 374cd1a..368e6bc 100644 --- a/pass/Controllers/GeneralSettingsTableViewController.swift +++ b/pass/Controllers/GeneralSettingsTableViewController.swift @@ -46,6 +46,15 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController { return uiSwitch }() + let ignoreGPGIDSwitch: UISwitch = { + let uiSwitch = UISwitch() + uiSwitch.onTintColor = Colors.systemBlue + uiSwitch.sizeToFit() + uiSwitch.addTarget(self, action: #selector(ignoreGPGIDSwitchAction(_:)), for: UIControl.Event.valueChanged) + uiSwitch.isOn = Defaults.isIgnoreGPGIDOn + return uiSwitch + }() + let showFolderSwitch: UISwitch = { let uiSwitch = UISwitch() uiSwitch.onTintColor = Colors.systemBlue @@ -77,6 +86,7 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController { // section 2 [ + [.title: "IgnoreGPGID".localize(), .action: "none"], [.title: "ShowFolders".localize(), .action: "none"], [.title: "HidePasswordImages".localize(), .action: "none"], [.title: "HideUnknownFields".localize(), .action: "none"], @@ -129,6 +139,10 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController { cell.accessoryType = .none cell.selectionStyle = .none cell.accessoryView = showFolderSwitch + case "IgnoreGPGID".localize(): + cell.accessoryType = .none + cell.selectionStyle = .none + cell.accessoryView = ignoreGPGIDSwitch case "HidePasswordImages".localize(): cell.accessoryType = .none let detailButton = UIButton(type: .detailDisclosure) @@ -206,6 +220,11 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController { NotificationCenter.default.post(name: .passwordDisplaySettingChanged, object: nil) } + @objc + func ignoreGPGIDSwitchAction(_: Any?) { + Defaults.isIgnoreGPGIDOn = ignoreGPGIDSwitch.isOn + } + @objc func hidePasswordImagesSwitchAction(_: Any?) { Defaults.isHidePasswordImagesOn = hidePasswordImagesSwitch.isOn diff --git a/pass/en.lproj/Localizable.strings b/pass/en.lproj/Localizable.strings index 668b580..c949ae3 100644 --- a/pass/en.lproj/Localizable.strings +++ b/pass/en.lproj/Localizable.strings @@ -41,6 +41,7 @@ "PasswordGeneratorFlavor" = "Style"; "RememberPgpKeyPassphrase" = "Remember PGP Key Passphrase"; "RememberGitCredentialPassphrase" = "Remember Git Credential Passphrase"; +"IgnoreGPGID" = "Ignore .gpg-id"; "ShowFolders" = "Show Folders"; "HidePasswordImages" = "Hide Password Images"; "HidePasswordImagesExplanation." = "Associated favicon images are loaded and shown based upon the URL associated with an entry. Enable this option to hide these images and prevent them from being loaded."; diff --git a/passKit/Crypto/GopenPGPInterface.swift b/passKit/Crypto/GopenPGPInterface.swift index a72ac0a..5dc9529 100644 --- a/passKit/Crypto/GopenPGPInterface.swift +++ b/passKit/Crypto/GopenPGPInterface.swift @@ -70,9 +70,16 @@ struct GopenPGPInterface: PGPInterface { privateKeys.keys.contains { key in key.hasSuffix(keyID.lowercased()) } } - func decrypt(encryptedData: Data, keyID: String, passphrase: String) throws -> Data? { - guard let key = privateKeys.first(where: { key, _ in key.hasSuffix(keyID.lowercased()) }), - let privateKey = privateKeys[key.key] else { + func decrypt(encryptedData: Data, keyID: String?, passphrase: String) throws -> Data? { + let key: CryptoKey? = { + if let keyID = keyID { + return privateKeys.first(where: { key, _ in key.hasSuffix(keyID.lowercased()) })?.value + } else { + return privateKeys.first?.value + } + }() + + guard let privateKey = key else { throw AppError.decryption } @@ -94,9 +101,16 @@ struct GopenPGPInterface: PGPInterface { } } - func encrypt(plainData: Data, keyID: String) throws -> Data { - guard let key = publicKeys.first(where: { key, _ in key.hasSuffix(keyID.lowercased()) }), - let publicKey = publicKeys[key.key] else { + func encrypt(plainData: Data, keyID: String?) throws -> Data { + let key: CryptoKey? = { + if let keyID = keyID { + return publicKeys.first(where: { key, _ in key.hasSuffix(keyID.lowercased()) })?.value + } else { + return publicKeys.first?.value + } + }() + + guard let publicKey = key else { throw AppError.encryption } diff --git a/passKit/Crypto/ObjectivePGPInterface.swift b/passKit/Crypto/ObjectivePGPInterface.swift index 7aee6a3..8eb83c2 100644 --- a/passKit/Crypto/ObjectivePGPInterface.swift +++ b/passKit/Crypto/ObjectivePGPInterface.swift @@ -29,11 +29,11 @@ struct ObjectivePGPInterface: PGPInterface { self.privateKey = privateKey } - func decrypt(encryptedData: Data, keyID _: String, passphrase: String) throws -> Data? { + func decrypt(encryptedData: Data, keyID _: String?, passphrase: String) throws -> Data? { try ObjectivePGP.decrypt(encryptedData, andVerifySignature: false, using: keyring.keys) { _ in passphrase } } - func encrypt(plainData: Data, keyID _: String) throws -> Data { + func encrypt(plainData: Data, keyID _: String?) throws -> Data { let encryptedData = try ObjectivePGP.encrypt(plainData, addSignature: false, using: keyring.keys, passphraseForKey: nil) if Defaults.encryptInArmored { return Armor.armored(encryptedData, as: .message).data(using: .ascii)! diff --git a/passKit/Crypto/PGPAgent.swift b/passKit/Crypto/PGPAgent.swift index 4216857..a62b552 100644 --- a/passKit/Crypto/PGPAgent.swift +++ b/passKit/Crypto/PGPAgent.swift @@ -96,6 +96,36 @@ public class PGPAgent { return try pgpInterface.encrypt(plainData: plainData, keyID: keyID) } + public func decrypt(encryptedData: Data, requestPGPKeyPassphrase: (String) -> String) throws -> Data? { + // Remember the previous status and set the current status + let previousDecryptStatus = self.latestDecryptStatus + self.latestDecryptStatus = false + // Init keys. + try checkAndInit() + // Get the PGP key passphrase. + var passphrase = "" + if previousDecryptStatus == false { + passphrase = requestPGPKeyPassphrase("default") + } else { + passphrase = keyStore.get(for: Globals.pgpKeyPassphrase) ?? requestPGPKeyPassphrase("default") + } + // Decrypt. + guard let result = try pgpInterface!.decrypt(encryptedData: encryptedData, keyID: nil, passphrase: passphrase) else { + return nil + } + // The decryption step has succeed. + self.latestDecryptStatus = true + return result + } + + public func encrypt(plainData: Data) throws -> Data { + try checkAndInit() + guard let pgpInterface = pgpInterface else { + throw AppError.encryption + } + return try pgpInterface.encrypt(plainData: plainData, keyID: nil) + } + public var isPrepared: Bool { keyStore.contains(key: PgpKey.PUBLIC.getKeychainKey()) && keyStore.contains(key: PgpKey.PRIVATE.getKeychainKey()) diff --git a/passKit/Crypto/PGPInterface.swift b/passKit/Crypto/PGPInterface.swift index ec4792f..b77831d 100644 --- a/passKit/Crypto/PGPInterface.swift +++ b/passKit/Crypto/PGPInterface.swift @@ -7,9 +7,9 @@ // protocol PGPInterface { - func decrypt(encryptedData: Data, keyID: String, passphrase: String) throws -> Data? + func decrypt(encryptedData: Data, keyID: String?, passphrase: String) throws -> Data? - func encrypt(plainData: Data, keyID: String) throws -> Data + func encrypt(plainData: Data, keyID: String?) throws -> Data func containsPublicKey(with keyID: String) -> Bool diff --git a/passKit/Helpers/DefaultsKeys.swift b/passKit/Helpers/DefaultsKeys.swift index 7f4f4d4..815143f 100644 --- a/passKit/Helpers/DefaultsKeys.swift +++ b/passKit/Helpers/DefaultsKeys.swift @@ -50,6 +50,7 @@ public extension DefaultsKeys { var isHideOTPOn: DefaultsKey { .init("isHideOTPOn", defaultValue: false) } var isRememberPGPPassphraseOn: DefaultsKey { .init("isRememberPGPPassphraseOn", defaultValue: false) } var isRememberGitCredentialPassphraseOn: DefaultsKey { .init("isRememberGitCredentialPassphraseOn", defaultValue: false) } + var isIgnoreGPGIDOn: DefaultsKey { .init("isIgnoreGPGIDOn", defaultValue: true) } var isShowFolderOn: DefaultsKey { .init("isShowFolderOn", defaultValue: true) } var isHidePasswordImagesOn: DefaultsKey { .init("isHidePasswordImagesOn", defaultValue: false) } var searchDefault: DefaultsKey { .init("searchDefault", defaultValue: .all) } diff --git a/passKit/Models/PasswordStore.swift b/passKit/Models/PasswordStore.swift index 323a11c..24ded80 100644 --- a/passKit/Models/PasswordStore.swift +++ b/passKit/Models/PasswordStore.swift @@ -689,9 +689,16 @@ public class PasswordStore { public func decrypt(passwordEntity: PasswordEntity, keyID: String? = nil, requestPGPKeyPassphrase: @escaping (String) -> String) throws -> Password { let encryptedDataPath = storeURL.appendingPathComponent(passwordEntity.getPath()) - let keyID = keyID ?? findGPGID(from: encryptedDataPath) let encryptedData = try Data(contentsOf: encryptedDataPath) - guard let decryptedData = try PGPAgent.shared.decrypt(encryptedData: encryptedData, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase) else { + let data: Data? = try { + if Defaults.isIgnoreGPGIDOn { + return try PGPAgent.shared.decrypt(encryptedData: encryptedData, requestPGPKeyPassphrase: requestPGPKeyPassphrase) + } else { + let keyID = keyID ?? findGPGID(from: encryptedDataPath) + return try PGPAgent.shared.decrypt(encryptedData: encryptedData, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase) + } + }() + guard let decryptedData = data else { throw AppError.decryption } let plainText = String(data: decryptedData, encoding: .utf8) ?? "" @@ -703,7 +710,11 @@ public class PasswordStore { guard let passwordEntity = fetchPasswordEntity(with: path) else { throw AppError.decryption } - return try decrypt(passwordEntity: passwordEntity, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase) + if Defaults.isIgnoreGPGIDOn { + return try decrypt(passwordEntity: passwordEntity, requestPGPKeyPassphrase: requestPGPKeyPassphrase) + } else { + return try decrypt(passwordEntity: passwordEntity, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase) + } } public func encrypt(password: Password, keyID: String? = nil) throws -> Data { diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index 182ad76..b5adf0f 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -16,6 +16,7 @@ class PasswordStoreTest: XCTestCase { func testCloneAndDecryptMultiKeys() throws { let url = URL(fileURLWithPath: "\(Globals.repositoryPath)-test") + Defaults.isIgnoreGPGIDOn = false let passwordStore = PasswordStore(url: url) try passwordStore.cloneRepository(remoteRepoURL: remoteRepoURL, branchName: "master") expectation(for: NSPredicate { _, _ in FileManager.default.fileExists(atPath: url.path) }, evaluatedWith: nil) @@ -46,6 +47,7 @@ class PasswordStoreTest: XCTestCase { XCTAssertEqual(testPasswordPlain.plainText, "testpassword") passwordStore.erase() + Defaults.isIgnoreGPGIDOn = true } private func decrypt(passwordStore: PasswordStore, path: String, passphrase _: String) throws -> Password {