montana/iOS/Apps/Montana/Montana/Auth/PasskeyService.swift

249 lines
8.1 KiB
Swift

//
// PasskeyService.swift
// Montana Post-Quantum Wallet
//
// Passkey integration for device-local authentication
// Cognitive Key = master backup (cross-device)
// Passkey = Face ID / Touch ID convenience (same device)
//
import Foundation
import AuthenticationServices
import LocalAuthentication
/// Passkey service for device-local biometric authentication
/// Works alongside Cognitive Key (not replacement)
@MainActor
public class PasskeyService: NSObject, ObservableObject {
public static let shared = PasskeyService()
// MARK: - Published State
@Published public var isPasskeyAvailable = false
@Published public var hasPasskey = false
@Published public var error: String?
// MARK: - Private
private let context = LAContext()
private var authCompletion: ((Bool) -> Void)?
// Relying Party ID (your domain)
private let relyingPartyID = "efir.org"
// MARK: - Init
private override init() {
super.init()
checkBiometricAvailability()
checkExistingPasskey()
}
// MARK: - Biometric Check
/// Check if device supports biometrics
private func checkBiometricAvailability() {
var error: NSError?
isPasskeyAvailable = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
if let error = error {
print("[Passkey] Biometrics not available: \(error.localizedDescription)")
} else {
let biometryType = context.biometryType
print("[Passkey] Biometrics available: \(biometryType == .faceID ? "Face ID" : "Touch ID")")
}
}
/// Check if passkey already exists for this device
private func checkExistingPasskey() {
hasPasskey = UserDefaults.standard.bool(forKey: "montana_has_passkey")
}
// MARK: - Create Passkey
/// Create passkey after cognitive key registration
/// Links biometric auth to the Montana address
public func createPasskey(for address: String) async -> Bool {
guard isPasskeyAvailable else {
error = "Biometrics not available"
return false
}
// Store address reference in Keychain with biometric protection
let success = saveToKeychainWithBiometrics(
key: "montana_passkey_address",
value: address.data(using: .utf8)!
)
if success {
hasPasskey = true
UserDefaults.standard.set(true, forKey: "montana_has_passkey")
UserDefaults.standard.set(address, forKey: "montana_passkey_linked_address")
print("[Passkey] Created for address: \(address)")
}
return success
}
// MARK: - Authenticate with Passkey
/// Authenticate using Face ID / Touch ID
/// Returns the Montana address if successful
public func authenticate() async -> String? {
guard hasPasskey else {
error = "No passkey configured"
return nil
}
// Read address from biometric-protected Keychain
guard let addressData = readFromKeychainWithBiometrics(key: "montana_passkey_address"),
let address = String(data: addressData, encoding: .utf8) else {
error = "Failed to read passkey"
return nil
}
print("[Passkey] Authenticated: \(address)")
return address
}
/// Quick check if can authenticate with passkey
public func canAuthenticateWithPasskey() -> Bool {
return isPasskeyAvailable && hasPasskey
}
// MARK: - Remove Passkey
/// Remove passkey (user must use cognitive key to restore)
public func removePasskey() {
deleteFromKeychain(key: "montana_passkey_address")
hasPasskey = false
UserDefaults.standard.set(false, forKey: "montana_has_passkey")
UserDefaults.standard.removeObject(forKey: "montana_passkey_linked_address")
print("[Passkey] Removed")
}
// MARK: - Cognitive Key Storage (Biometric Protected)
/// Store cognitive key with biometric protection
/// Like Trust Wallet / MetaMask - user can view seed with Face ID
public func storeCognitiveKey(_ key: String) -> Bool {
guard let keyData = key.data(using: .utf8) else { return false }
let success = saveToKeychainWithBiometrics(key: "montana_cognitive_key", value: keyData)
if success {
UserDefaults.standard.set(true, forKey: "montana_has_stored_seed")
print("[Passkey] Cognitive key stored securely")
}
return success
}
/// Retrieve cognitive key (requires Face ID)
/// Returns nil if biometric auth fails
public func retrieveCognitiveKey() -> String? {
guard let keyData = readFromKeychainWithBiometrics(key: "montana_cognitive_key"),
let key = String(data: keyData, encoding: .utf8) else {
return nil
}
return key
}
/// Check if cognitive key is stored
public var hasCognitiveKeyStored: Bool {
return UserDefaults.standard.bool(forKey: "montana_has_stored_seed")
}
/// Delete stored cognitive key
public func deleteCognitiveKey() {
deleteFromKeychain(key: "montana_cognitive_key")
UserDefaults.standard.set(false, forKey: "montana_has_stored_seed")
print("[Passkey] Cognitive key deleted")
}
// MARK: - Keychain with Biometrics
/// Save to Keychain with biometric protection
private func saveToKeychainWithBiometrics(key: String, value: Data) -> Bool {
// Create access control with biometric requirement
guard let accessControl = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
.biometryCurrentSet,
nil
) else {
print("[Passkey] Failed to create access control")
return false
}
// Delete existing item first
let deleteQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecAttrService as String: "network.montana.passkey"
]
SecItemDelete(deleteQuery as CFDictionary)
// Add new item with biometric protection
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecAttrService as String: "network.montana.passkey",
kSecValueData as String: value,
kSecAttrAccessControl as String: accessControl
]
let status = SecItemAdd(query as CFDictionary, nil)
if status == errSecSuccess {
return true
} else {
print("[Passkey] Keychain save failed: \(status)")
return false
}
}
/// Read from Keychain with biometric prompt
private func readFromKeychainWithBiometrics(key: String) -> Data? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecAttrService as String: "network.montana.passkey",
kSecReturnData as String: true,
kSecUseOperationPrompt as String: "Войти в Montana"
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == errSecSuccess {
return result as? Data
} else {
print("[Passkey] Keychain read failed: \(status)")
return nil
}
}
/// Delete from Keychain
private func deleteFromKeychain(key: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecAttrService as String: "network.montana.passkey"
]
SecItemDelete(query as CFDictionary)
}
}
// MARK: - Biometry Type Extension
extension LABiometryType: CustomStringConvertible {
public var description: String {
switch self {
case .none: return "None"
case .touchID: return "Touch ID"
case .faceID: return "Face ID"
case .opticID: return "Optic ID"
@unknown default: return "Unknown"
}
}
}