From ee80f0dbe865b8f32aba9846957a73b80498adfe Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Wed, 11 Mar 2026 00:48:21 +0100 Subject: [PATCH] PGPAgent can encrypt with multiple keys --- passKit/Crypto/GopenPGPInterface.swift | 7 ++++-- passKit/Crypto/PGPAgent.swift | 9 ++++++++ passKitTests/Crypto/PGPAgentTest.swift | 30 +++++++++++++++++++++++--- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/passKit/Crypto/GopenPGPInterface.swift b/passKit/Crypto/GopenPGPInterface.swift index 638ce25..3d34a58 100644 --- a/passKit/Crypto/GopenPGPInterface.swift +++ b/passKit/Crypto/GopenPGPInterface.swift @@ -138,8 +138,11 @@ struct GopenPGPInterface: PGPInterface { } func encrypt(plainData: Data, keyIDs: [String]) throws -> Data { - let keys: [CryptoKey] = keyIDs.compactMap { keyID in - publicKeys.first(where: { key, _ in key.hasSuffix(keyID.lowercased()) })?.value + 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) + } + return key } guard let firstKey = keys.first else { throw AppError.encryption diff --git a/passKit/Crypto/PGPAgent.swift b/passKit/Crypto/PGPAgent.swift index 021742c..97524c4 100644 --- a/passKit/Crypto/PGPAgent.swift +++ b/passKit/Crypto/PGPAgent.swift @@ -84,6 +84,7 @@ 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 { @@ -100,6 +101,14 @@ public class PGPAgent { return try pgpInterface.encrypt(plainData: plainData, keyIDs: [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() diff --git a/passKitTests/Crypto/PGPAgentTest.swift b/passKitTests/Crypto/PGPAgentTest.swift index b7ba158..b90e17f 100644 --- a/passKitTests/Crypto/PGPAgentTest.swift +++ b/passKitTests/Crypto/PGPAgentTest.swift @@ -167,7 +167,7 @@ final class PGPAgentTest: XCTestCase { (false, false), ].forEach { encryptInArmored, decryptFromArmored in passKit.Defaults.encryptInArmored = encryptInArmored - let encryptedData = try pgpAgent.encrypt(plainData: testData, keyID: RSA2048.fingerprint) + 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) @@ -177,6 +177,30 @@ final class PGPAgentTest: XCTestCase { // - 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. @@ -197,7 +221,7 @@ final class PGPAgentTest: XCTestCase { XCTAssertEqual($0 as! AppError, AppError.pgpPrivateKeyNotFound(keyID: ED25519.fingerprint)) } - // add private key for ED25519 + // load private key for ED25519 try importKeys(RSA2048_RSA4096.publicKeys | ED25519.publicKey, RSA2048_RSA4096.privateKeys | ED25519.privateKey) try pgpAgent.initKeys() @@ -215,7 +239,7 @@ final class PGPAgentTest: XCTestCase { 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) + let encryptedData = try pgpAgent.encrypt(plainData: testData, keyIDs: [keyID]) passKit.Defaults.encryptInArmored = decryptFromArmored return try pgpAgent.decrypt(encryptedData: encryptedData, keyID: encryptKeyID ?? keyID, requestPGPKeyPassphrase: requestPassphrase) }