import Foundation import Security import os private let log = Logger(subsystem: "com.scarriffleservices.onyx", category: "Face") /// Wo die angelernten Gesichter liegen. /// /// **In der Keychain und nicht in einer Datei.** Ein Merkmalsvektor ist kein /// Bild und lässt sich nicht zurückverwandeln, aber er ist ein biometrisches /// Datum: wer ihn hat, kann prüfen, ob eine bestimmte Person vor der Kamera /// stand. Das gehört nicht in `Application Support`, wo jedes Programm des /// Benutzers mitliest. /// /// Dasselbe Vorgehen wie bei `CalendarrCredentials`. public struct FaceStore: Sendable { private static let service = "com.scarriffleservices.onyx.face" private static let account = "templates" public init() {} public func load() -> [FaceTemplate] { var query = Self.query() query[kSecReturnData as String] = true query[kSecMatchLimit as String] = kSecMatchLimitOne var item: CFTypeRef? guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess, let data = item as? Data else { return [] } do { return try JSONDecoder().decode([FaceTemplate].self, from: data) } catch { // Ein unlesbarer Bestand ist wie keiner: dann wird eben neu // angelernt. Ihn stehen zu lassen hieße, dass die Anmeldung // stumm nie wieder funktioniert. log.error("Gesichtsdaten unlesbar — sie gelten als nicht vorhanden") return [] } } public func save(_ templates: [FaceTemplate]) { guard !templates.isEmpty else { clear(); return } guard let data = try? JSONEncoder().encode(templates) else { return } let query = Self.query() let attributes: [String: Any] = [kSecValueData as String: data] if SecItemUpdate(query as CFDictionary, attributes as CFDictionary) == errSecItemNotFound { var insert = query insert[kSecValueData as String] = data // Nur auf diesem Gerät und nur nach dem ersten Entsperren: ein // Gesichtsmerkmal hat auf keinem anderen Rechner etwas zu suchen. insert[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly let status = SecItemAdd(insert as CFDictionary, nil) if status != errSecSuccess { log.error("Gesichtsdaten nicht speicherbar: \(status, privacy: .public)") } } } public func clear() { SecItemDelete(Self.query() as CFDictionary) } private static func query() -> [String: Any] { [kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account] } }