107 lines
3.8 KiB
Swift
107 lines
3.8 KiB
Swift
import Foundation
|
|
import CryptoKit
|
|
import CommonCrypto
|
|
|
|
/// ECP1 key encryption format: PBKDF2-HMAC-SHA256 (600k iterations) + AES-256-GCM
|
|
/// Wire format: magic(4) + salt(16) + nonce(12) + ciphertext_with_tag(N+16)
|
|
enum KeyEncryption {
|
|
|
|
/// Encrypt raw key bytes with password using ECP1 format
|
|
static func encrypt(_ rawBytes: Data, password: Data) throws -> Data {
|
|
let salt = Data.randomBytes(16)
|
|
let derivedKey = try pbkdf2(password: password, salt: salt)
|
|
|
|
let nonce = Data.randomBytes(12)
|
|
let symmetricKey = SymmetricKey(data: derivedKey)
|
|
let gcmNonce = try AES.GCM.Nonce(data: nonce)
|
|
|
|
// AAD = ECP1 magic bytes (matching Python)
|
|
let sealedBox = try AES.GCM.seal(
|
|
rawBytes,
|
|
using: symmetricKey,
|
|
nonce: gcmNonce,
|
|
authenticating: Constants.ecp1Magic
|
|
)
|
|
|
|
// ciphertext + tag concatenated (matches Python's AESGCM.encrypt output)
|
|
var result = Data()
|
|
result.append(Constants.ecp1Magic) // 4 bytes
|
|
result.append(salt) // 16 bytes
|
|
result.append(nonce) // 12 bytes
|
|
result.append(sealedBox.ciphertext) // N bytes
|
|
result.append(sealedBox.tag) // 16 bytes
|
|
return result
|
|
}
|
|
|
|
/// Decrypt ECP1-encrypted key bytes with password
|
|
static func decrypt(_ data: Data, password: Data) throws -> Data {
|
|
guard data.count >= 48 else { // 4 + 16 + 12 + 16 minimum
|
|
throw CryptoError.invalidECP1Format
|
|
}
|
|
guard data.prefix(4) == Constants.ecp1Magic else {
|
|
throw CryptoError.invalidECP1Format
|
|
}
|
|
|
|
let salt = data[4..<20]
|
|
let nonce = data[20..<32]
|
|
let ctWithTag = data[32...]
|
|
|
|
guard ctWithTag.count >= 16 else {
|
|
throw CryptoError.invalidECP1Format
|
|
}
|
|
|
|
let derivedKey = try pbkdf2(password: password, salt: Data(salt))
|
|
let symmetricKey = SymmetricKey(data: derivedKey)
|
|
let gcmNonce = try AES.GCM.Nonce(data: nonce)
|
|
|
|
// Split ciphertext and tag
|
|
let ct = ctWithTag.prefix(ctWithTag.count - 16)
|
|
let tag = ctWithTag.suffix(16)
|
|
|
|
let sealedBox = try AES.GCM.SealedBox(
|
|
nonce: gcmNonce,
|
|
ciphertext: ct,
|
|
tag: tag
|
|
)
|
|
|
|
do {
|
|
return try AES.GCM.open(sealedBox, using: symmetricKey, authenticating: Constants.ecp1Magic)
|
|
} catch {
|
|
throw CryptoError.decryptionFailed("ECP1 decryption failed - wrong password?")
|
|
}
|
|
}
|
|
|
|
/// Check if data starts with ECP1 magic
|
|
static func isECP1Format(_ data: Data) -> Bool {
|
|
data.count >= 4 && data.prefix(4) == Constants.ecp1Magic
|
|
}
|
|
|
|
// MARK: - PBKDF2
|
|
|
|
/// Derive 32-byte key using PBKDF2-HMAC-SHA256 with 600k iterations
|
|
static func pbkdf2(password: Data, salt: Data) throws -> Data {
|
|
var derivedKey = Data(count: 32)
|
|
let status = derivedKey.withUnsafeMutableBytes { derivedKeyPtr in
|
|
password.withUnsafeBytes { passwordPtr in
|
|
salt.withUnsafeBytes { saltPtr in
|
|
CCKeyDerivationPBKDF(
|
|
CCPBKDFAlgorithm(kCCPBKDF2),
|
|
passwordPtr.baseAddress?.assumingMemoryBound(to: Int8.self),
|
|
password.count,
|
|
saltPtr.baseAddress?.assumingMemoryBound(to: UInt8.self),
|
|
salt.count,
|
|
CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
|
|
Constants.pbkdf2Iterations,
|
|
derivedKeyPtr.baseAddress?.assumingMemoryBound(to: UInt8.self),
|
|
32
|
|
)
|
|
}
|
|
}
|
|
}
|
|
guard status == kCCSuccess else {
|
|
throw CryptoError.pbkdf2Failed
|
|
}
|
|
return derivedKey
|
|
}
|
|
}
|