// // VaultCrypto.swift // OS Vault // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // import Foundation import CryptoKit import CommonCrypto /// OS Vault's own encryption mechanism — deliberately independent of the /// macOS Keychain. The secret (mnemonic) is sealed into a portable JSON file: /// /// password ── PBKDF2-HMAC-SHA512 (600k rounds, random 32-byte salt) ──▶ 256-bit key /// secret ──── AES-256-GCM (random nonce, tag authenticates the file) ──▶ ciphertext /// /// A wrong password or a tampered file both fail GCM authentication; the two /// cases are indistinguishable by design. public enum VaultCrypto { public static let currentVersion = 1 public static let defaultIterations = 600_000 public struct VaultFile: Codable, Equatable { public struct KDF: Codable, Equatable { public let algorithm: String // "pbkdf2-hmac-sha512" public let iterations: Int public let salt: String // base64 } public let version: Int public let kdf: KDF public let cipher: String // "aes-256-gcm" public let ciphertext: String // base64, GCM combined (nonce ‖ ct ‖ tag) } public static func seal(secret: Data, password: String, iterations: Int = defaultIterations) throws -> VaultFile { var salt = Data(count: 32) let saltStatus = salt.withUnsafeMutableBytes { ptr in SecRandomCopyBytes(kSecRandomDefault, 32, ptr.baseAddress!) } guard saltStatus == errSecSuccess else { throw WalletError.internalError("Entropy source unavailable.") } let key = try deriveKey(password: password, salt: salt, iterations: iterations) let sealed = try AES.GCM.seal(secret, using: key) guard let combined = sealed.combined else { throw WalletError.internalError("Encryption failed.") } return VaultFile( version: currentVersion, kdf: .init(algorithm: "pbkdf2-hmac-sha512", iterations: iterations, salt: salt.base64EncodedString()), cipher: "aes-256-gcm", ciphertext: combined.base64EncodedString() ) } public static func open(_ vault: VaultFile, password: String) throws -> Data { guard vault.version == currentVersion, vault.kdf.algorithm == "pbkdf2-hmac-sha512", vault.cipher == "aes-256-gcm", let salt = Data(base64Encoded: vault.kdf.salt), let combined = Data(base64Encoded: vault.ciphertext), vault.kdf.iterations >= 10_000 else { throw WalletError.vaultCorrupted } let key = try deriveKey(password: password, salt: salt, iterations: vault.kdf.iterations) do { let box = try AES.GCM.SealedBox(combined: combined) return try AES.GCM.open(box, using: key) } catch { throw WalletError.wrongPassword } } static func deriveKey(password: String, salt: Data, iterations: Int) throws -> SymmetricKey { let passwordData = Data(password.utf8) var derived = Data(count: 32) let status = derived.withUnsafeMutableBytes { derivedPtr in salt.withUnsafeBytes { saltPtr in passwordData.withUnsafeBytes { passPtr in CCKeyDerivationPBKDF( CCPBKDFAlgorithm(kCCPBKDF2), passPtr.bindMemory(to: Int8.self).baseAddress, passwordData.count, saltPtr.bindMemory(to: UInt8.self).baseAddress, salt.count, CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA512), UInt32(iterations), derivedPtr.bindMemory(to: UInt8.self).baseAddress, 32 ) } } } guard status == kCCSuccess else { throw WalletError.internalError("Key derivation failed.") } return SymmetricKey(data: derived) } }