PGPInterface can encrypt with multiple keys, PGPAgent can encrypt with all keys

This commit is contained in:
Lysann Tranvouez 2026-03-11 00:21:30 +01:00
parent 39dab8c6c0
commit a56193dc86
5 changed files with 87 additions and 11 deletions

View file

@ -123,26 +123,43 @@ struct GopenPGPInterface: PGPInterface {
} }
} }
@available(*, deprecated, message: "Use encrypt(plainData:keyIDs:) instead.")
func encrypt(plainData: Data, keyID: String?) throws -> Data { func encrypt(plainData: Data, keyID: String?) throws -> Data {
let key: CryptoKey? = { guard let keyID = keyID ?? publicKeys.keys.first else {
if let keyID { // this is invalid, but we want the new function to throw the error for us
return publicKeys.first(where: { key, _ in key.hasSuffix(keyID.lowercased()) })?.value return try encrypt(plainData: plainData, keyIDs: [])
}
return try encrypt(plainData: plainData, keyIDs: [keyID])
} }
return publicKeys.first?.value
}()
guard let publicKey = key else { 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] = keyIDs.compactMap { keyID in
publicKeys.first(where: { key, _ in key.hasSuffix(keyID.lowercased()) })?.value
}
guard let firstKey = keys.first else {
throw AppError.encryption throw AppError.encryption
} }
let otherKeys = keys.dropFirst()
var error: NSError? var error: NSError?
guard let keyRing = CryptoNewKeyRing(firstKey, &error) else {
guard let keyRing = CryptoNewKeyRing(publicKey, &error) else {
guard error == nil else { guard error == nil else {
throw error! throw error!
} }
throw AppError.encryption 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) let encryptedData = try keyRing.encrypt(CryptoNewPlainMessage(plainData.mutable as Data), privateKey: nil)
if Defaults.encryptInArmored { if Defaults.encryptInArmored {

View file

@ -33,8 +33,25 @@ struct ObjectivePGPInterface: PGPInterface {
} }
} }
@available(*, deprecated, message: "Use encrypt(plainData:keyIDs:) instead.")
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) // 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)
if Defaults.encryptInArmored { if Defaults.encryptInArmored {
return Armor.armored(encryptedData, as: .message).data(using: .ascii)! return Armor.armored(encryptedData, as: .message).data(using: .ascii)!
} }

View file

@ -97,7 +97,7 @@ public class PGPAgent {
throw AppError.pgpPublicKeyNotFound(keyID: keyID) throw AppError.pgpPublicKeyNotFound(keyID: keyID)
} }
} }
return try pgpInterface.encrypt(plainData: plainData, keyID: keyID) return try pgpInterface.encrypt(plainData: plainData, keyIDs: [keyID])
} }
public func decrypt(encryptedData: Data, requestPGPKeyPassphrase: @escaping (String) -> String) throws -> Data? { public func decrypt(encryptedData: Data, requestPGPKeyPassphrase: @escaping (String) -> String) throws -> Data? {
@ -122,6 +122,7 @@ public class PGPAgent {
return result return result
} }
@available(*, deprecated, message: "Use encrypt(plainData:keyID:) or encryptWithAllKeys(plainData:) instead.")
public func encrypt(plainData: Data) throws -> Data { public func encrypt(plainData: Data) throws -> Data {
try checkAndInit() try checkAndInit()
guard let pgpInterface else { guard let pgpInterface else {
@ -130,6 +131,14 @@ public class PGPAgent {
return try pgpInterface.encrypt(plainData: plainData, keyID: nil) 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 var isPrepared: Bool { public var isPrepared: Bool {
keyStore.contains(key: PGPKey.PUBLIC.getKeychainKey()) keyStore.contains(key: PGPKey.PUBLIC.getKeychainKey())
&& keyStore.contains(key: PGPKey.PRIVATE.getKeychainKey()) && keyStore.contains(key: PGPKey.PRIVATE.getKeychainKey())

View file

@ -9,7 +9,11 @@
protocol PGPInterface { protocol PGPInterface {
func decrypt(encryptedData: Data, keyIDHint: String?, passPhraseForKey: @escaping (String) -> String) throws -> Data? func decrypt(encryptedData: Data, keyIDHint: String?, passPhraseForKey: @escaping (String) -> String) throws -> Data?
@available(*, deprecated, message: "Use encrypt(plainData:keyIDs:) instead.")
func encrypt(plainData: Data, keyID: String?) throws -> Data 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 func containsPublicKey(with keyID: String) -> Bool

View file

@ -180,6 +180,35 @@ final class PGPAgentTest: XCTestCase {
XCTAssertEqual(passphraseRequestCalledCount, 3) XCTAssertEqual(passphraseRequestCalledCount, 3)
} }
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))
}
// add 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)
}
}
private func importKeys(_ publicKey: String, _ privateKey: String) throws { private func importKeys(_ publicKey: String, _ privateKey: String) throws {
try KeyFileManager(keyType: PGPKey.PUBLIC, keyPath: "", keyHandler: keychain.add).importKey(from: publicKey) try KeyFileManager(keyType: PGPKey.PUBLIC, keyPath: "", keyHandler: keychain.add).importKey(from: publicKey)
try KeyFileManager(keyType: PGPKey.PRIVATE, keyPath: "", keyHandler: keychain.add).importKey(from: privateKey) try KeyFileManager(keyType: PGPKey.PRIVATE, keyPath: "", keyHandler: keychain.add).importKey(from: privateKey)