spb/os-vault Public
Self-custody, multi-chain crypto wallet for macOS. One recovery phrase, six chain families, zero API keys — nothing leaves your Mac.
Swift 96%
Shell 3.4%
Makefile 0.6%
1//2// VaultCrypto.swift3// OS Vault4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//89import Foundation10import CryptoKit11import CommonCrypto1213/// OS Vault's own encryption mechanism — deliberately independent of the14/// macOS Keychain. The secret (mnemonic) is sealed into a portable JSON file:15///16/// password ── PBKDF2-HMAC-SHA512 (600k rounds, random 32-byte salt) ──▶ 256-bit key17/// secret ──── AES-256-GCM (random nonce, tag authenticates the file) ──▶ ciphertext18///19/// A wrong password or a tampered file both fail GCM authentication; the two20/// cases are indistinguishable by design.21public enum VaultCrypto {2223 public static let currentVersion = 124 public static let defaultIterations = 600_0002526 public struct VaultFile: Codable, Equatable {27 public struct KDF: Codable, Equatable {28 public let algorithm: String // "pbkdf2-hmac-sha512"29 public let iterations: Int30 public let salt: String // base6431 }32 public let version: Int33 public let kdf: KDF34 public let cipher: String // "aes-256-gcm"35 public let ciphertext: String // base64, GCM combined (nonce ‖ ct ‖ tag)36 }3738 public static func seal(secret: Data, password: String,39 iterations: Int = defaultIterations) throws -> VaultFile {40 var salt = Data(count: 32)41 let saltStatus = salt.withUnsafeMutableBytes { ptr in42 SecRandomCopyBytes(kSecRandomDefault, 32, ptr.baseAddress!)43 }44 guard saltStatus == errSecSuccess else { throw WalletError.internalError("Entropy source unavailable.") }4546 let key = try deriveKey(password: password, salt: salt, iterations: iterations)47 let sealed = try AES.GCM.seal(secret, using: key)48 guard let combined = sealed.combined else { throw WalletError.internalError("Encryption failed.") }4950 return VaultFile(51 version: currentVersion,52 kdf: .init(algorithm: "pbkdf2-hmac-sha512", iterations: iterations, salt: salt.base64EncodedString()),53 cipher: "aes-256-gcm",54 ciphertext: combined.base64EncodedString()55 )56 }5758 public static func open(_ vault: VaultFile, password: String) throws -> Data {59 guard vault.version == currentVersion,60 vault.kdf.algorithm == "pbkdf2-hmac-sha512",61 vault.cipher == "aes-256-gcm",62 let salt = Data(base64Encoded: vault.kdf.salt),63 let combined = Data(base64Encoded: vault.ciphertext),64 vault.kdf.iterations >= 10_000 else {65 throw WalletError.vaultCorrupted66 }67 let key = try deriveKey(password: password, salt: salt, iterations: vault.kdf.iterations)68 do {69 let box = try AES.GCM.SealedBox(combined: combined)70 return try AES.GCM.open(box, using: key)71 } catch {72 throw WalletError.wrongPassword73 }74 }7576 static func deriveKey(password: String, salt: Data, iterations: Int) throws -> SymmetricKey {77 let passwordData = Data(password.utf8)78 var derived = Data(count: 32)79 let status = derived.withUnsafeMutableBytes { derivedPtr in80 salt.withUnsafeBytes { saltPtr in81 passwordData.withUnsafeBytes { passPtr in82 CCKeyDerivationPBKDF(83 CCPBKDFAlgorithm(kCCPBKDF2),84 passPtr.bindMemory(to: Int8.self).baseAddress, passwordData.count,85 saltPtr.bindMemory(to: UInt8.self).baseAddress, salt.count,86 CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA512),87 UInt32(iterations),88 derivedPtr.bindMemory(to: UInt8.self).baseAddress, 3289 )90 }91 }92 }93 guard status == kCCSuccess else { throw WalletError.internalError("Key derivation failed.") }94 return SymmetricKey(data: derived)95 }96}97