SPB Git

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%
4.0 KB · 97 lines swift
Raw Blame History
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