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 } }