112 lines
3.6 KiB
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)
|
|
}
|
|
}
|