2019-09-08 23:00:46 +02:00
|
|
|
//
|
2020-04-19 15:41:30 +02:00
|
|
|
// GopenPGPInterface.swift
|
2019-09-08 23:00:46 +02:00
|
|
|
// passKit
|
|
|
|
|
//
|
|
|
|
|
// Created by Danny Moesch on 08.09.19.
|
|
|
|
|
// Copyright © 2019 Bob Sun. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
2021-06-20 00:33:48 +02:00
|
|
|
import Gopenpgp
|
2019-09-08 23:00:46 +02:00
|
|
|
|
2020-04-19 15:41:30 +02:00
|
|
|
struct GopenPGPInterface: PGPInterface {
|
2019-10-20 12:14:51 +02:00
|
|
|
private static let errorMapping: [String: Error] = [
|
2020-09-20 15:07:18 +02:00
|
|
|
"gopenpgp: error in unlocking key: openpgp: invalid data: private key checksum failure": AppError.wrongPassphrase,
|
2021-06-20 00:33:48 +02:00
|
|
|
"gopenpgp: error in reading message: openpgp: incorrect key": AppError.keyExpiredOrIncompatible,
|
2019-10-20 12:14:51 +02:00
|
|
|
]
|
|
|
|
|
|
2020-04-13 01:30:00 -07:00
|
|
|
private var publicKeys: [String: CryptoKey] = [:]
|
|
|
|
|
private var privateKeys: [String: CryptoKey] = [:]
|
2026-03-10 22:16:42 +01:00
|
|
|
private var privateSubkeyToKeyIDMapping: [String: String] = [:] // value is the key in privateKeys map
|
2019-09-08 23:00:46 +02:00
|
|
|
|
|
|
|
|
init(publicArmoredKey: String, privateArmoredKey: String) throws {
|
2020-04-13 01:30:00 -07:00
|
|
|
let pubKeys = extractKeysFromArmored(str: publicArmoredKey)
|
|
|
|
|
let prvKeys = extractKeysFromArmored(str: privateArmoredKey)
|
|
|
|
|
|
|
|
|
|
for key in pubKeys {
|
|
|
|
|
var error: NSError?
|
2020-09-20 15:07:18 +02:00
|
|
|
guard let cryptoKey = CryptoNewKeyFromArmored(key, &error) else {
|
2020-04-13 01:30:00 -07:00
|
|
|
guard error == nil else {
|
|
|
|
|
throw error!
|
|
|
|
|
}
|
2020-09-20 15:07:18 +02:00
|
|
|
throw AppError.keyImport
|
2020-04-13 01:30:00 -07:00
|
|
|
}
|
2020-09-20 15:07:18 +02:00
|
|
|
publicKeys[cryptoKey.getFingerprint().lowercased()] = cryptoKey
|
2020-04-13 01:30:00 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for key in prvKeys {
|
|
|
|
|
var error: NSError?
|
2020-09-20 15:07:18 +02:00
|
|
|
guard let cryptoKey = CryptoNewKeyFromArmored(key, &error) else {
|
2020-04-13 01:30:00 -07:00
|
|
|
guard error == nil else {
|
|
|
|
|
throw error!
|
|
|
|
|
}
|
2020-09-20 15:07:18 +02:00
|
|
|
throw AppError.keyImport
|
2020-04-11 23:23:38 -07:00
|
|
|
}
|
2026-03-10 22:16:42 +01:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
2019-09-08 23:00:46 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-13 01:30:00 -07:00
|
|
|
func extractKeysFromArmored(str: String) -> [String] {
|
2020-06-28 21:25:40 +02:00
|
|
|
var keys: [String] = []
|
2021-12-28 02:57:11 +01:00
|
|
|
var key = ""
|
2020-06-28 21:25:40 +02:00
|
|
|
for line in str.splitByNewline() {
|
|
|
|
|
if line.trimmed.uppercased().hasPrefix("-----BEGIN PGP") {
|
|
|
|
|
key = ""
|
|
|
|
|
key += line
|
|
|
|
|
} else if line.trimmed.uppercased().hasPrefix("-----END PGP") {
|
|
|
|
|
key += line
|
|
|
|
|
keys.append(key)
|
|
|
|
|
} else {
|
|
|
|
|
key += line
|
|
|
|
|
}
|
2020-04-13 19:15:52 -07:00
|
|
|
key += "\n"
|
2020-06-28 21:25:40 +02:00
|
|
|
}
|
|
|
|
|
return keys
|
|
|
|
|
}
|
2020-04-13 01:30:00 -07:00
|
|
|
|
2020-04-14 20:20:16 -07:00
|
|
|
func containsPublicKey(with keyID: String) -> Bool {
|
2020-07-05 23:15:29 +02:00
|
|
|
publicKeys.keys.contains { key in key.hasSuffix(keyID.lowercased()) }
|
2020-04-14 20:20:16 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func containsPrivateKey(with keyID: String) -> Bool {
|
2020-07-05 23:15:29 +02:00
|
|
|
privateKeys.keys.contains { key in key.hasSuffix(keyID.lowercased()) }
|
2020-04-14 20:20:16 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-10 22:16:42 +01:00
|
|
|
func decrypt(encryptedData: Data, keyIDHint: String?, passPhraseForKey: @escaping (String) -> String) throws -> Data? {
|
|
|
|
|
let message = createPGPMessage(from: encryptedData)
|
|
|
|
|
guard let message else {
|
|
|
|
|
throw AppError.decryption
|
|
|
|
|
}
|
2021-01-07 21:58:38 -08:00
|
|
|
|
2026-03-10 22:16:42 +01:00
|
|
|
guard let privateKey: CryptoKey = try findDecryptionKey(message: message, keyIDHint: keyIDHint) else {
|
2020-09-20 15:07:18 +02:00
|
|
|
throw AppError.decryption
|
2020-04-13 01:30:00 -07:00
|
|
|
}
|
|
|
|
|
|
2019-10-20 12:14:51 +02:00
|
|
|
do {
|
2021-01-10 15:01:21 -08:00
|
|
|
var isLocked: ObjCBool = false
|
|
|
|
|
try privateKey.isLocked(&isLocked)
|
|
|
|
|
var unlockedKey: CryptoKey!
|
|
|
|
|
if isLocked.boolValue {
|
2026-03-10 17:14:11 +01:00
|
|
|
let passphrase = passPhraseForKey(privateKey.getFingerprint())
|
2021-01-10 15:01:21 -08:00
|
|
|
unlockedKey = try privateKey.unlock(passphrase.data(using: .utf8))
|
|
|
|
|
} else {
|
|
|
|
|
unlockedKey = privateKey
|
|
|
|
|
}
|
2020-04-11 23:23:38 -07:00
|
|
|
var error: NSError?
|
|
|
|
|
|
|
|
|
|
guard let keyRing = CryptoNewKeyRing(unlockedKey, &error) else {
|
|
|
|
|
guard error == nil else {
|
|
|
|
|
throw error!
|
|
|
|
|
}
|
2020-09-20 15:07:18 +02:00
|
|
|
throw AppError.decryption
|
2020-04-11 23:23:38 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return try keyRing.decrypt(message, verifyKey: nil, verifyTime: 0).data
|
2019-10-20 12:14:51 +02:00
|
|
|
} catch {
|
|
|
|
|
throw Self.errorMapping[error.localizedDescription, default: error]
|
|
|
|
|
}
|
2019-09-08 23:00:46 +02:00
|
|
|
}
|
|
|
|
|
|
2026-03-11 00:21:30 +01:00
|
|
|
@available(*, deprecated, message: "Use encrypt(plainData:keyIDs:) instead.")
|
2021-01-07 21:58:38 -08:00
|
|
|
func encrypt(plainData: Data, keyID: String?) throws -> Data {
|
2026-03-11 00:21:30 +01:00
|
|
|
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])
|
|
|
|
|
}
|
2021-01-07 21:58:38 -08:00
|
|
|
|
2026-03-11 00:21:30 +01:00
|
|
|
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 {
|
2026-03-11 00:48:21 +01:00
|
|
|
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
|
2026-03-11 00:21:30 +01:00
|
|
|
}
|
|
|
|
|
guard let firstKey = keys.first else {
|
2020-09-20 15:07:18 +02:00
|
|
|
throw AppError.encryption
|
2020-04-13 01:30:00 -07:00
|
|
|
}
|
2026-03-11 00:21:30 +01:00
|
|
|
let otherKeys = keys.dropFirst()
|
2020-04-13 01:30:00 -07:00
|
|
|
|
2020-04-11 23:23:38 -07:00
|
|
|
var error: NSError?
|
2026-03-11 00:21:30 +01:00
|
|
|
guard let keyRing = CryptoNewKeyRing(firstKey, &error) else {
|
2020-04-11 23:23:38 -07:00
|
|
|
guard error == nil else {
|
|
|
|
|
throw error!
|
|
|
|
|
}
|
2020-09-20 15:07:18 +02:00
|
|
|
throw AppError.encryption
|
2020-04-11 23:23:38 -07:00
|
|
|
}
|
2026-03-11 00:21:30 +01:00
|
|
|
do {
|
|
|
|
|
try otherKeys.forEach { key in
|
|
|
|
|
try keyRing.add(key)
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
throw AppError.encryption
|
|
|
|
|
}
|
2020-04-11 23:23:38 -07:00
|
|
|
|
|
|
|
|
let encryptedData = try keyRing.encrypt(CryptoNewPlainMessage(plainData.mutable as Data), privateKey: nil)
|
2020-01-02 00:48:00 +01:00
|
|
|
if Defaults.encryptInArmored {
|
2019-09-08 23:00:46 +02:00
|
|
|
var error: NSError?
|
|
|
|
|
let armor = encryptedData.getArmored(&error)
|
|
|
|
|
guard error == nil else {
|
|
|
|
|
throw error!
|
|
|
|
|
}
|
|
|
|
|
return armor.data(using: .ascii)!
|
|
|
|
|
}
|
|
|
|
|
return encryptedData.getBinary()!
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-13 19:15:52 -07:00
|
|
|
var keyID: [String] {
|
2020-06-28 21:25:40 +02:00
|
|
|
publicKeys.keys.map { $0.uppercased() }
|
2020-04-11 23:23:38 -07:00
|
|
|
}
|
|
|
|
|
|
2020-04-13 19:15:52 -07:00
|
|
|
var shortKeyID: [String] {
|
2020-06-28 21:25:40 +02:00
|
|
|
publicKeys.keys.map { $0.suffix(8).uppercased() }
|
2019-09-08 23:00:46 +02:00
|
|
|
}
|
2026-03-10 22:16:42 +01:00
|
|
|
|
|
|
|
|
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]
|
|
|
|
|
}
|
2024-11-30 11:29:27 -08:00
|
|
|
}
|
2019-09-08 23:00:46 +02:00
|
|
|
|
2024-11-30 11:29:27 -08:00
|
|
|
public func createPGPMessage(from encryptedData: Data) -> CryptoPGPMessage? {
|
|
|
|
|
// Important note:
|
|
|
|
|
// Even if Defaults.encryptInArmored is true now, it could be different during the encryption.
|
|
|
|
|
var error: NSError?
|
|
|
|
|
let message = CryptoNewPGPMessageFromArmored(String(data: encryptedData, encoding: .ascii), &error)
|
|
|
|
|
if error == nil {
|
|
|
|
|
return message
|
2019-09-08 23:00:46 +02:00
|
|
|
}
|
2024-11-30 11:29:27 -08:00
|
|
|
return CryptoNewPGPMessage(encryptedData.mutable as Data)
|
2019-09-08 23:00:46 +02:00
|
|
|
}
|