// // KeychainManager.swift // Montana Protocol — macOS // // Secure storage for ML-DSA-65 keys in macOS Keychain // import Foundation import Security class KeychainManager { static let shared = KeychainManager() private let service = "network.montana.presence" enum Key: String { case privateKey = "private_key" // plaintext (legacy, deleted after PIN creation) case privateKeyEncrypted = "private_key_enc" // AES-256 encrypted with PIN case publicKey = "public_key" case mnemonic = "mnemonic" // plaintext (legacy, deleted after PIN creation) case mnemonicEncrypted = "mnemonic_enc" // AES-256 encrypted with PIN case pinHash = "pin_hash" case pinSalt = "pin_salt" // Random 16-byte salt for PBKDF2 case lockoutFails = "lockout_fails" // Failed PIN attempts counter case lockoutEnd = "lockout_end" // Lockout end timestamp case lockoutLevel = "lockout_level" // Lockout escalation level // v3.11.0 — Argon2id + Secure Enclave case argon2Salt = "argon2_salt" // 16-byte random salt for Argon2id case seWrappedKey = "se_wrapped_key" // SE-encrypted random wrap key envelope case seKeyTag = "se_key_tag" // Secure Enclave key data reference case seEnabled = "se_enabled" // 1 byte: 0x01 = SE active } // MARK: - Save @discardableResult func save(_ key: Key, data: Data) -> Bool { // Delete existing first delete(key) let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: key.rawValue, kSecValueData as String: data, kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly ] let status = SecItemAdd(query as CFDictionary, nil) return status == errSecSuccess } // MARK: - Load func load(_ key: Key) -> Data? { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: key.rawValue, kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitOne ] var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) guard status == errSecSuccess else { return nil } return result as? Data } // MARK: - Delete @discardableResult func delete(_ key: Key) -> Bool { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: key.rawValue ] let status = SecItemDelete(query as CFDictionary) return status == errSecSuccess || status == errSecItemNotFound } // MARK: - Helpers func hasKeys() -> Bool { let hasPrivate = load(.privateKey) != nil || load(.privateKeyEncrypted) != nil return hasPrivate && load(.publicKey) != nil } func deleteAll() { delete(.privateKey) delete(.privateKeyEncrypted) delete(.publicKey) delete(.mnemonic) delete(.mnemonicEncrypted) delete(.pinHash) delete(.pinSalt) delete(.lockoutFails) delete(.lockoutEnd) delete(.lockoutLevel) delete(.argon2Salt) delete(.seWrappedKey) delete(.seKeyTag) delete(.seEnabled) } }