ios_client
This commit is contained in:
106
ios_client 0.8.5/Kecalek/Crypto/KeyEncryption.swift
Normal file
106
ios_client 0.8.5/Kecalek/Crypto/KeyEncryption.swift
Normal file
@@ -0,0 +1,106 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user