montana/macOS/MontanaPresence/KeychainManager.swift

112 lines
3.6 KiB
Swift

//
// 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)
}
}