decryption: GopenPGPInterface tries to identify decryption key from message metadata
So the system can have multiple private keys, and the caller doesn't need to specify a specific one regardless. Ideally: If there are several matches we could also take into account which keys have already been unlocked (or passthrases saved in keychain). Right now it only grabs the first match.
This commit is contained in:
parent
510eb8e15e
commit
39dab8c6c0
5 changed files with 75 additions and 14 deletions
|
|
@ -16,6 +16,7 @@ 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)
|
||||
|
|
@ -40,7 +41,24 @@ struct GopenPGPInterface: PGPInterface {
|
|||
}
|
||||
throw AppError.keyImport
|
||||
}
|
||||
privateKeys[cryptoKey.getFingerprint().lowercased()] = cryptoKey
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,15 +88,13 @@ struct GopenPGPInterface: PGPInterface {
|
|||
privateKeys.keys.contains { key in key.hasSuffix(keyID.lowercased()) }
|
||||
}
|
||||
|
||||
func decrypt(encryptedData: Data, keyID: String?, passPhraseForKey: @escaping (String) -> String) throws -> Data? {
|
||||
let key: CryptoKey? = {
|
||||
if let keyID {
|
||||
return privateKeys.first(where: { key, _ in key.hasSuffix(keyID.lowercased()) })?.value
|
||||
}
|
||||
return privateKeys.first?.value
|
||||
}()
|
||||
func decrypt(encryptedData: Data, keyIDHint: String?, passPhraseForKey: @escaping (String) -> String) throws -> Data? {
|
||||
let message = createPGPMessage(from: encryptedData)
|
||||
guard let message else {
|
||||
throw AppError.decryption
|
||||
}
|
||||
|
||||
guard let privateKey = key else {
|
||||
guard let privateKey: CryptoKey = try findDecryptionKey(message: message, keyIDHint: keyIDHint) else {
|
||||
throw AppError.decryption
|
||||
}
|
||||
|
||||
|
|
@ -101,7 +117,6 @@ 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]
|
||||
|
|
@ -148,6 +163,33 @@ 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<String> = 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? {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ struct ObjectivePGPInterface: PGPInterface {
|
|||
}
|
||||
}
|
||||
|
||||
func decrypt(encryptedData: Data, keyID _: String?, passPhraseForKey: @escaping (String) -> String) throws -> Data? {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ public class PGPAgent {
|
|||
return self.keyStore.get(for: AppKeychain.getPGPKeyPassphraseKey(keyID: selectedKeyID)) ?? requestPGPKeyPassphrase(selectedKeyID)
|
||||
}
|
||||
// Decrypt.
|
||||
guard let result = try pgpInterface.decrypt(encryptedData: encryptedData, keyID: keyID, passPhraseForKey: providePassPhraseForKey) else {
|
||||
guard let result = try pgpInterface.decrypt(encryptedData: encryptedData, keyIDHint: keyID, passPhraseForKey: providePassPhraseForKey) else {
|
||||
return nil
|
||||
}
|
||||
// The decryption step has succeed.
|
||||
|
|
@ -114,7 +114,7 @@ public class PGPAgent {
|
|||
return self.keyStore.get(for: AppKeychain.getPGPKeyPassphraseKey(keyID: selectedKeyID)) ?? requestPGPKeyPassphrase(selectedKeyID)
|
||||
}
|
||||
// Decrypt.
|
||||
guard let result = try pgpInterface!.decrypt(encryptedData: encryptedData, keyID: nil, passPhraseForKey: providePassPhraseForKey) else {
|
||||
guard let result = try pgpInterface!.decrypt(encryptedData: encryptedData, keyIDHint: nil, passPhraseForKey: providePassPhraseForKey) else {
|
||||
return nil
|
||||
}
|
||||
// The decryption step has succeed.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
//
|
||||
|
||||
protocol PGPInterface {
|
||||
func decrypt(encryptedData: Data, keyID: String?, passPhraseForKey: @escaping (String) -> String) throws -> Data?
|
||||
func decrypt(encryptedData: Data, keyIDHint: String?, passPhraseForKey: @escaping (String) -> String) throws -> Data?
|
||||
|
||||
func encrypt(plainData: Data, keyID: String?) throws -> Data
|
||||
|
||||
|
|
|
|||
|
|
@ -87,6 +87,25 @@ final class PGPAgentTest: XCTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
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, keyID: 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)
|
||||
}
|
||||
}
|
||||
|
||||
func testNoPrivateKey() throws {
|
||||
try KeyFileManager(keyType: PGPKey.PUBLIC, keyPath: "", keyHandler: keychain.add).importKey(from: RSA2048.publicKey)
|
||||
XCTAssertFalse(pgpAgent.isPrepared)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue