Ein eigener Sperrbildschirm — und ohne gespeichertes Passwort
Kameraaufnahme, Anlernen über drei Posen, Fenster je Bildschirm auf Bildschirmschoner-Ebene, Passwortrückfall. **Das Anmeldepasswort wird nicht gespeichert.** Ein Spike hat den Weg freigeräumt: `ODRecord.verifyPassword` ist öffentliche API, die Eingabe geht direkt hinein, das Ergebnis ist ein `Bool`, danach ist die Zeichenkette wieder weg. Damit fällt der Grund weg, aus dem vergleichbare Programme sich das Passwort geben lassen und in die Keychain legen. Angelernt wird über drei Posen mit je fünf Aufnahmen. Der Merkmalsvektor ist ein allgemeiner Bildmerkmalsvektor und kein Gesichtsmerkmal — er reagiert auf Kopfhaltung und Beleuchtung mit. Eine einzige Aufnahme beschreibt genau eine Haltung bei genau einem Licht. Die Vektoren liegen in der Keychain, nicht in einer Datei: aus ihnen lässt sich kein Bild zurückgewinnen, aber wer sie hat, kann prüfen, ob eine bestimmte Person vor der Kamera stand. Die Einschränkung steht auf dem Sperrbildschirm selbst und nicht im Kleingedruckten: Bequemlichkeit, keine Sicherheitsgrenze; der Vergleich läuft über die Webcam und lässt sich mit einem Foto täuschen. Wer sich darauf verlässt, tut es dort. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3da673662f
commit
e3b215a1dd
@@ -0,0 +1,71 @@
|
||||
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]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user