diff --git a/passKit/Crypto/GopenPGPInterface.swift b/passKit/Crypto/GopenPGPInterface.swift index 3d34a58..34f0622 100644 --- a/passKit/Crypto/GopenPGPInterface.swift +++ b/passKit/Crypto/GopenPGPInterface.swift @@ -16,7 +16,6 @@ struct GopenPGPInterface: PGPInterface { private var publicKeys: [String: CryptoKey] = [:] private var privateKeys: [String: CryptoKey] = [:] - private var privateSubkeyToKeyIDMapping: [String: String] = [:] // value is the key in privateKeys map init(publicArmoredKey: String, privateArmoredKey: String) throws { let pubKeys = extractKeysFromArmored(str: publicArmoredKey) @@ -41,24 +40,7 @@ struct GopenPGPInterface: PGPInterface { } throw AppError.keyImport } - - let keyID = cryptoKey.getFingerprint().lowercased() - privateKeys[keyID] = cryptoKey - - guard let subkeyIDsJSON = HelperPassGetHexSubkeyIDsJSON(cryptoKey) else { - guard error == nil else { - throw error! - } - throw AppError.keyImport - } - do { - let subkeyIDs = try JSONDecoder().decode([String].self, from: subkeyIDsJSON) - for subkeyID in subkeyIDs { - privateSubkeyToKeyIDMapping[subkeyID] = keyID - } - } catch { - throw AppError.keyImport - } + privateKeys[cryptoKey.getFingerprint().lowercased()] = cryptoKey } } @@ -88,13 +70,15 @@ struct GopenPGPInterface: PGPInterface { privateKeys.keys.contains { key in key.hasSuffix(keyID.lowercased()) } } - func decrypt(encryptedData: Data, keyIDHint: String?, passPhraseForKey: @escaping (String) -> String) throws -> Data? { - let message = createPGPMessage(from: encryptedData) - guard let message else { - throw AppError.decryption - } + func decrypt(encryptedData: Data, keyID: String?, passphrase: String) throws -> Data? { + let key: CryptoKey? = { + if let keyID { + return privateKeys.first(where: { key, _ in key.hasSuffix(keyID.lowercased()) })?.value + } + return privateKeys.first?.value + }() - guard let privateKey: CryptoKey = try findDecryptionKey(message: message, keyIDHint: keyIDHint) else { + guard let privateKey = key else { throw AppError.decryption } @@ -103,7 +87,6 @@ struct GopenPGPInterface: PGPInterface { try privateKey.isLocked(&isLocked) var unlockedKey: CryptoKey! if isLocked.boolValue { - let passphrase = passPhraseForKey(privateKey.getFingerprint()) unlockedKey = try privateKey.unlock(passphrase.data(using: .utf8)) } else { unlockedKey = privateKey @@ -117,52 +100,33 @@ struct GopenPGPInterface: PGPInterface { throw AppError.decryption } + let message = createPGPMessage(from: encryptedData) return try keyRing.decrypt(message, verifyKey: nil, verifyTime: 0).data } catch { throw Self.errorMapping[error.localizedDescription, default: error] } } - @available(*, deprecated, message: "Use encrypt(plainData:keyIDs:) instead.") func encrypt(plainData: Data, keyID: String?) throws -> Data { - guard let keyID = keyID ?? publicKeys.keys.first else { - // this is invalid, but we want the new function to throw the error for us - return try encrypt(plainData: plainData, keyIDs: []) - } - return try encrypt(plainData: plainData, keyIDs: [keyID]) - } - - func encryptWithAllKeys(plainData: Data) throws -> Data { - let keyIDs = publicKeys.keys.filter { key in privateKeys.keys.contains(key) } - return try encrypt(plainData: plainData, keyIDs: keyIDs) - } - - func encrypt(plainData: Data, keyIDs: [String]) throws -> Data { - let keys: [CryptoKey] = try keyIDs.map { keyID in - guard let key = publicKeys.first(where: { key, _ in key.hasSuffix(keyID.lowercased()) })?.value else { - throw AppError.pgpPublicKeyNotFound(keyID: keyID) + let key: CryptoKey? = { + if let keyID { + return publicKeys.first(where: { key, _ in key.hasSuffix(keyID.lowercased()) })?.value } - return key - } - guard let firstKey = keys.first else { + return publicKeys.first?.value + }() + + guard let publicKey = key else { throw AppError.encryption } - let otherKeys = keys.dropFirst() var error: NSError? - guard let keyRing = CryptoNewKeyRing(firstKey, &error) else { + + guard let keyRing = CryptoNewKeyRing(publicKey, &error) else { guard error == nil else { throw error! } throw AppError.encryption } - do { - try otherKeys.forEach { key in - try keyRing.add(key) - } - } catch { - throw AppError.encryption - } let encryptedData = try keyRing.encrypt(CryptoNewPlainMessage(plainData.mutable as Data), privateKey: nil) if Defaults.encryptInArmored { @@ -183,33 +147,6 @@ struct GopenPGPInterface: PGPInterface { var shortKeyID: [String] { publicKeys.keys.map { $0.suffix(8).uppercased() } } - - private func findDecryptionKey(message: CryptoPGPMessage, keyIDHint: String?) throws -> CryptoKey? { - var keyIDCandidates: any Collection = privateKeys.keys - do { - if let encryptionKeysJSON = message.getHexEncryptionKeyIDsJson() { - // these are the subkeys (encryption keys), not the primaries keys (whose fingerprints we have in the privateKeys map), - // so we need to map them back to the primary keyIDs using privateSubkeyToKeyIDMapping - let validSubkeys = try JSONDecoder().decode([String].self, from: encryptionKeysJSON) - let validKeyIDs = validSubkeys.compactMap { privateSubkeyToKeyIDMapping[$0] } - if #available(iOSApplicationExtension 16.0, *) { - assert(validKeyIDs.isEmpty || !Set(keyIDCandidates).isDisjoint(with: validKeyIDs)) - } - keyIDCandidates = validKeyIDs - } - } catch { - // fall back to legacy approach of trying first in privateKeys (or preferring hint) - } - - if let keyIDHint { - keyIDCandidates = keyIDCandidates.filter { key in key.hasSuffix(keyIDHint.lowercased()) } - } - guard let selectedKeyID = keyIDCandidates.first else { - throw keyIDHint != nil ? AppError.keyExpiredOrIncompatible : AppError.decryption - } - - return privateKeys[selectedKeyID] - } } public func createPGPMessage(from encryptedData: Data) -> CryptoPGPMessage? { diff --git a/passKit/Crypto/ObjectivePGPInterface.swift b/passKit/Crypto/ObjectivePGPInterface.swift index 01a5a2b..768d785 100644 --- a/passKit/Crypto/ObjectivePGPInterface.swift +++ b/passKit/Crypto/ObjectivePGPInterface.swift @@ -24,34 +24,12 @@ struct ObjectivePGPInterface: PGPInterface { } } - func decrypt(encryptedData: Data, keyIDHint _: String?, passPhraseForKey: @escaping (String) -> String) throws -> Data? { - try ObjectivePGP.decrypt(encryptedData, andVerifySignature: false, using: keyring.keys) { selectedKey in - guard let selectedKey else { - return nil - } - return passPhraseForKey(selectedKey.keyID.longIdentifier) - } + func decrypt(encryptedData: Data, keyID _: String?, passphrase: String) throws -> Data? { + try ObjectivePGP.decrypt(encryptedData, andVerifySignature: false, using: keyring.keys) { _ in passphrase } } - @available(*, deprecated, message: "Use encrypt(plainData:keyIDs:) instead.") func encrypt(plainData: Data, keyID _: String?) throws -> Data { - // Backwards compatibility: ignore keyID parameter and encrypted with all keys in the keyring - try encryptWithAllKeys(plainData: plainData) - } - - func encryptWithAllKeys(plainData: Data) throws -> Data { - try encrypt(plainData: plainData, keyIDs: keyID) - } - - func encrypt(plainData: Data, keyIDs: [String]) throws -> Data { - let keys = try keyIDs.map { keyID in - guard let key = keyring.findKey(keyID) else { - throw AppError.pgpPublicKeyNotFound(keyID: keyID) - } - return key - } - - let encryptedData = try ObjectivePGP.encrypt(plainData, addSignature: false, using: keys, passphraseForKey: nil) + 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 97524c4..f193515 100644 --- a/passKit/Crypto/PGPAgent.swift +++ b/passKit/Crypto/PGPAgent.swift @@ -69,14 +69,14 @@ public class PGPAgent { latestDecryptStatus = false // Get the PGP key passphrase. - let providePassPhraseForKey = { (selectedKeyID: String) -> String in - if previousDecryptStatus == false { - return requestPGPKeyPassphrase(selectedKeyID) - } - return self.keyStore.get(for: AppKeychain.getPGPKeyPassphraseKey(keyID: selectedKeyID)) ?? requestPGPKeyPassphrase(selectedKeyID) + var passphrase = "" + if previousDecryptStatus == false { + passphrase = requestPGPKeyPassphrase(keyID) + } else { + passphrase = keyStore.get(for: AppKeychain.getPGPKeyPassphraseKey(keyID: keyID)) ?? requestPGPKeyPassphrase(keyID) } // Decrypt. - guard let result = try pgpInterface.decrypt(encryptedData: encryptedData, keyIDHint: keyID, passPhraseForKey: providePassPhraseForKey) else { + guard let result = try pgpInterface.decrypt(encryptedData: encryptedData, keyID: keyID, passphrase: passphrase) else { return nil } // The decryption step has succeed. @@ -84,7 +84,6 @@ public class PGPAgent { return result } - @available(*, deprecated, message: "Use encrypt(plainData:keyIDs:) instead.") public func encrypt(plainData: Data, keyID: String) throws -> Data { try checkAndInit() guard let pgpInterface else { @@ -98,49 +97,24 @@ public class PGPAgent { throw AppError.pgpPublicKeyNotFound(keyID: keyID) } } - return try pgpInterface.encrypt(plainData: plainData, keyIDs: [keyID]) + return try pgpInterface.encrypt(plainData: plainData, keyID: keyID) } - public func encrypt(plainData: Data, keyIDs: [String]) throws -> Data { - try checkAndInit() - guard let pgpInterface else { - throw AppError.encryption - } - return try pgpInterface.encrypt(plainData: plainData, keyIDs: keyIDs) - } - - @available(*, deprecated, message: "Use encrypt(plainData:keyIDs:) or encryptWithAllKeys(plainData:) instead.") - public func encrypt(plainData: Data) throws -> Data { - try checkAndInit() - guard let pgpInterface else { - throw AppError.encryption - } - return try pgpInterface.encrypt(plainData: plainData, keyID: nil) - } - - public func encryptWithAllKeys(plainData: Data) throws -> Data { - try checkAndInit() - guard let pgpInterface else { - throw AppError.encryption - } - return try pgpInterface.encryptWithAllKeys(plainData: plainData) - } - - public func decrypt(encryptedData: Data, requestPGPKeyPassphrase: @escaping (String) -> String) throws -> Data? { + public func decrypt(encryptedData: Data, requestPGPKeyPassphrase: (String) -> String) throws -> Data? { // Remember the previous status and set the current status let previousDecryptStatus = latestDecryptStatus latestDecryptStatus = false // Init keys. try checkAndInit() // Get the PGP key passphrase. - let providePassPhraseForKey = { (selectedKeyID: String) -> String in - if previousDecryptStatus == false { - return requestPGPKeyPassphrase(selectedKeyID) - } - return self.keyStore.get(for: AppKeychain.getPGPKeyPassphraseKey(keyID: selectedKeyID)) ?? requestPGPKeyPassphrase(selectedKeyID) + var passphrase = "" + if previousDecryptStatus == false { + passphrase = requestPGPKeyPassphrase("") + } else { + passphrase = keyStore.get(for: AppKeychain.getPGPKeyPassphraseKey(keyID: "")) ?? requestPGPKeyPassphrase("") } // Decrypt. - guard let result = try pgpInterface!.decrypt(encryptedData: encryptedData, keyIDHint: nil, passPhraseForKey: providePassPhraseForKey) else { + guard let result = try pgpInterface!.decrypt(encryptedData: encryptedData, keyID: nil, passphrase: passphrase) else { return nil } // The decryption step has succeed. @@ -148,6 +122,14 @@ public class PGPAgent { return result } + public func encrypt(plainData: Data) throws -> Data { + try checkAndInit() + guard let 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 f90c4bd..b77831d 100644 --- a/passKit/Crypto/PGPInterface.swift +++ b/passKit/Crypto/PGPInterface.swift @@ -7,13 +7,9 @@ // protocol PGPInterface { - func decrypt(encryptedData: Data, keyIDHint: String?, passPhraseForKey: @escaping (String) -> String) throws -> Data? + func decrypt(encryptedData: Data, keyID: String?, passphrase: String) throws -> Data? - @available(*, deprecated, message: "Use encrypt(plainData:keyIDs:) instead.") func encrypt(plainData: Data, keyID: String?) throws -> Data - // encrypt with all public keys for which we also have a private key - func encryptWithAllKeys(plainData: Data) throws -> Data - func encrypt(plainData: Data, keyIDs: [String]) throws -> Data func containsPublicKey(with keyID: String) -> Bool diff --git a/passKit/Models/PasswordStore.swift b/passKit/Models/PasswordStore.swift index c822ffa..b918ab4 100644 --- a/passKit/Models/PasswordStore.swift +++ b/passKit/Models/PasswordStore.swift @@ -420,9 +420,9 @@ public class PasswordStore { } public func encrypt(password: Password, keyID: String? = nil) throws -> Data { + let encryptedDataPath = password.fileURL(in: storeURL) + let keyID = keyID ?? findGPGID(from: encryptedDataPath) if Defaults.isEnableGPGIDOn { - let encryptedDataPath = password.fileURL(in: storeURL) - let keyID = keyID ?? findGPGID(from: encryptedDataPath) return try PGPAgent.shared.encrypt(plainData: password.plainData, keyID: keyID) } return try PGPAgent.shared.encrypt(plainData: password.plainData) diff --git a/passKitTests/Crypto/PGPAgentTest.swift b/passKitTests/Crypto/PGPAgentTest.swift index b90e17f..8acb2be 100644 --- a/passKitTests/Crypto/PGPAgentTest.swift +++ b/passKitTests/Crypto/PGPAgentTest.swift @@ -31,7 +31,34 @@ final class PGPAgentTest: XCTestCase { super.tearDown() } - // - MARK: Basic encrypt and decrypt tests + private func basicEncryptDecrypt(using pgpAgent: PGPAgent, keyID: String, encryptKeyID: String? = nil, requestPassphrase: @escaping (String) -> String = requestPGPKeyPassphrase, encryptInArmored: Bool = true, decryptFromArmored: Bool = true) throws -> Data? { + passKit.Defaults.encryptInArmored = encryptInArmored + let encryptedData = try pgpAgent.encrypt(plainData: testData, keyID: keyID) + passKit.Defaults.encryptInArmored = decryptFromArmored + return try pgpAgent.decrypt(encryptedData: encryptedData, keyID: encryptKeyID ?? keyID, requestPGPKeyPassphrase: requestPassphrase) + } + + func testMultiKeys() throws { + try [ + RSA2048_RSA4096, + ED25519_NISTP384, + ].forEach { testKeyInfo in + keychain.removeAllContent() + try importKeys(testKeyInfo.publicKeys, testKeyInfo.privateKeys) + XCTAssert(pgpAgent.isPrepared) + try pgpAgent.initKeys() + try [ + (true, true), + (true, false), + (false, true), + (false, false), + ].forEach { encryptInArmored, decryptFromArmored in + for id in testKeyInfo.fingerprints { + XCTAssertEqual(try basicEncryptDecrypt(using: pgpAgent, keyID: id, encryptInArmored: encryptInArmored, decryptFromArmored: decryptFromArmored), testData) + } + } + } + } func testBasicEncryptDecrypt() throws { try [ @@ -134,113 +161,8 @@ final class PGPAgentTest: XCTestCase { XCTAssertEqual(passphraseRequestCalledCount, 3) } - func testMultipleKeysLoaded() throws { - try [ - RSA2048_RSA4096, - ED25519_NISTP384, - ].forEach { testKeyInfo in - keychain.removeAllContent() - try importKeys(testKeyInfo.publicKeys, testKeyInfo.privateKeys) - XCTAssert(pgpAgent.isPrepared) - try pgpAgent.initKeys() - try [ - (true, true), - (true, false), - (false, true), - (false, false), - ].forEach { encryptInArmored, decryptFromArmored in - for id in testKeyInfo.fingerprints { - XCTAssertEqual(try basicEncryptDecrypt(using: pgpAgent, keyID: id, encryptInArmored: encryptInArmored, decryptFromArmored: decryptFromArmored), testData) - } - } - } - } - - func testMultiKeysSelectMatchingPrivateKeyToDecrypt() throws { - keychain.removeAllContent() - try importKeys(RSA2048_RSA4096.publicKeys, RSA2048_RSA4096.privateKeys) - try pgpAgent.initKeys() - try [ - (true, true), - (true, false), - (false, true), - (false, false), - ].forEach { encryptInArmored, decryptFromArmored in - passKit.Defaults.encryptInArmored = encryptInArmored - let encryptedData = try pgpAgent.encrypt(plainData: testData, keyIDs: [RSA2048.fingerprint]) - passKit.Defaults.encryptInArmored = decryptFromArmored - // Note: not specifying the keyID to decrypt, so that the agent needs to find the matching private key by itself. - let decryptedData = try pgpAgent.decrypt(encryptedData: encryptedData, requestPGPKeyPassphrase: requestPGPKeyPassphrase) - XCTAssertEqual(decryptedData, testData) - } - } - - // - MARK: Encrypt with multiple keys - - func testEncryptWithMultipleKeys() throws { - keychain.removeAllContent() - // no private key for ED25519 - try importKeys(RSA2048_RSA4096.publicKeys | ED25519.publicKey, RSA2048_RSA4096.privateKeys) - try pgpAgent.initKeys() - - let encryptedData = try pgpAgent.encrypt(plainData: testData, keyIDs: RSA2048_RSA4096.fingerprints + [ED25519.fingerprint]) - - try [RSA2048.fingerprint, RSA4096.fingerprint].forEach { keyID in - let decryptedData = try pgpAgent.decrypt(encryptedData: encryptedData, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase) - XCTAssertEqual(decryptedData, testData) - } - - XCTAssertThrowsError(try pgpAgent.decrypt(encryptedData: encryptedData, keyID: ED25519.fingerprint, requestPGPKeyPassphrase: requestPGPKeyPassphrase)) { - XCTAssertEqual($0 as! AppError, AppError.pgpPrivateKeyNotFound(keyID: ED25519.fingerprint)) - } - - // load private key for ED25519 - try importKeys(RSA2048_RSA4096.publicKeys | ED25519.publicKey, RSA2048_RSA4096.privateKeys | ED25519.privateKey) - try pgpAgent.initKeys() - let decryptedData = try pgpAgent.decrypt(encryptedData: encryptedData, keyID: ED25519.fingerprint, requestPGPKeyPassphrase: requestPGPKeyPassphrase) - XCTAssertEqual(decryptedData, testData) - } - - func testEncryptWithAllKeys() throws { - // When multiple keys are imported, the agent should be able to encrypt without specifying the keyID. - // It should use all public keys for which we also have private keys, and the encrypted message should be able to be decrypted by any of the private keys. - - keychain.removeAllContent() - // no private key for ED25519 - try importKeys(RSA2048_RSA4096.publicKeys | ED25519.publicKey, RSA2048_RSA4096.privateKeys) - try pgpAgent.initKeys() - - let encryptedData = try pgpAgent.encryptWithAllKeys(plainData: testData) - - try [RSA2048.fingerprint, RSA4096.fingerprint].forEach { keyID in - let decryptedData = try pgpAgent.decrypt(encryptedData: encryptedData, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase) - XCTAssertEqual(decryptedData, testData) - } - - XCTAssertThrowsError(try pgpAgent.decrypt(encryptedData: encryptedData, keyID: ED25519.fingerprint, requestPGPKeyPassphrase: requestPGPKeyPassphrase)) { - XCTAssertEqual($0 as! AppError, AppError.pgpPrivateKeyNotFound(keyID: ED25519.fingerprint)) - } - - // load private key for ED25519 - try importKeys(RSA2048_RSA4096.publicKeys | ED25519.publicKey, RSA2048_RSA4096.privateKeys | ED25519.privateKey) - try pgpAgent.initKeys() - - XCTAssertThrowsError(try pgpAgent.decrypt(encryptedData: encryptedData, keyID: ED25519.fingerprint, requestPGPKeyPassphrase: requestPGPKeyPassphrase)) { - XCTAssertEqual($0 as! AppError, AppError.keyExpiredOrIncompatible) - } - } - - // - MARK: Helpers - private func importKeys(_ publicKey: String, _ privateKey: String) throws { try KeyFileManager(keyType: PGPKey.PUBLIC, keyPath: "", keyHandler: keychain.add).importKey(from: publicKey) try KeyFileManager(keyType: PGPKey.PRIVATE, keyPath: "", keyHandler: keychain.add).importKey(from: privateKey) } - - private func basicEncryptDecrypt(using pgpAgent: PGPAgent, keyID: String, encryptKeyID: String? = nil, requestPassphrase: @escaping (String) -> String = requestPGPKeyPassphrase, encryptInArmored: Bool = true, decryptFromArmored: Bool = true) throws -> Data? { - passKit.Defaults.encryptInArmored = encryptInArmored - let encryptedData = try pgpAgent.encrypt(plainData: testData, keyIDs: [keyID]) - passKit.Defaults.encryptInArmored = decryptFromArmored - return try pgpAgent.decrypt(encryptedData: encryptedData, keyID: encryptKeyID ?? keyID, requestPGPKeyPassphrase: requestPassphrase) - } } diff --git a/scripts/gopenpgp_build.sh b/scripts/gopenpgp_build.sh index b851cb9..98dc8bf 100755 --- a/scripts/gopenpgp_build.sh +++ b/scripts/gopenpgp_build.sh @@ -14,7 +14,7 @@ GOPENPGP_PATH="$CHECKOUT_PATH/gopenpgp" mkdir -p "$OUTPUT_PATH" mkdir -p "$CHECKOUT_PATH" -git clone --depth 1 --branch "$GOPENPGP_VERSION" https://forgejo.tranvouez.eu/lysann/passforios-gopenpgp.git "$GOPENPGP_PATH" +git clone --depth 1 --branch "$GOPENPGP_VERSION" https://github.com/mssun/gopenpgp.git "$GOPENPGP_PATH" pushd "$GOPENPGP_PATH" mkdir -p dist