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:
Scarriffle
2026-09-05 21:22:32 +02:00
co-authored by Claude Opus 5
parent 3da673662f
commit e3b215a1dd
23 changed files with 1605 additions and 1 deletions
+136
View File
@@ -0,0 +1,136 @@
import SwiftUI
import AVFoundation
import OnyxDesign
import FaceProvider
import LockProvider
/// Sperrbildschirm und Gesichtsvergleich.
///
/// **Die Einschränkung steht oben und nicht unten.** Wer diese Seite öffnet,
/// soll zuerst lesen, was das hier ist und was nicht — nicht erst, nachdem er
/// sein Gesicht angelernt hat.
struct LockSettingsView: View {
@Bindable var model: AppModel
let controller: LockController
@State private var enrollment = FaceEnrollment()
@State private var isEnrolling = false
@State private var cameraDenied = false
var body: some View {
SettingsPage {
OnyxFormCard(String(localized: "lock.section.what"),
hint: String(localized: "lock.disclaimer.settings")) {
EmptyView()
}
OnyxFormCard(String(localized: "lock.section.screen")) {
OnyxFormRow(String(localized: "lock.enabled"),
hint: String(localized: "lock.enabled.hint")) {
Toggle("", isOn: $model.lockScreenEnabled).labelsHidden()
}
OnyxFormRow(String(localized: "lock.now")) {
Button(String(localized: "lock.now.action")) { controller.lock() }
}
}
OnyxFormCard(String(localized: "lock.section.face")) {
OnyxFormRow(String(localized: "lock.face.enabled"),
hint: String(localized: "lock.face.hint")) {
Toggle("", isOn: $model.faceUnlockEnabled).labelsHidden()
.disabled(!controller.isEnrolled)
}
OnyxFormRow(String(localized: "lock.face.enrolled"),
hint: controller.isEnrolled
? nil : String(localized: "lock.face.notEnrolled")) {
HStack(spacing: Onyx.Space.lg) {
Button(controller.isEnrolled
? String(localized: "lock.face.again")
: String(localized: "lock.face.enroll")) {
Task { await beginEnrollment() }
}
if controller.isEnrolled {
Button(String(localized: "lock.face.forget"), role: .destructive) {
enrollment.reset()
model.faceUnlockEnabled = false
}
}
}
}
if cameraDenied {
OnyxFormRow(String(localized: "lock.face.denied")) {
Button(String(localized: "lock.face.openSettings")) {
NSWorkspace.shared.open(URL(string:
"x-apple.systempreferences:com.apple.preference.security?Privacy_Camera")!)
}
}
}
}
}
.sheet(isPresented: $isEnrolling) {
EnrollmentSheet(enrollment: enrollment) { isEnrolling = false }
}
}
private func beginEnrollment() async {
guard await FaceCapture.requestAccess() else {
cameraDenied = true
return
}
cameraDenied = false
enrollment.reset()
isEnrolling = true
enrollment.start()
}
}
/// Das Anlernen: Vorschau, Anweisung, Auslöser.
private struct EnrollmentSheet: View {
let enrollment: FaceEnrollment
let onClose: () -> Void
var body: some View {
VStack(spacing: Onyx.Space.xl) {
if let failure = enrollment.failure {
Text(failure).foregroundStyle(Onyx.Color.critical)
} else if enrollment.isFinished {
VStack(spacing: Onyx.Space.md) {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: Onyx.Symbol.hero))
.foregroundStyle(Onyx.Color.positive)
Text("lock.enroll.done")
}
} else {
CameraPreview(session: enrollment.session)
.frame(width: 320, height: 240)
.clipShape(RoundedRectangle(cornerRadius: Onyx.Radius.card,
style: .continuous))
Text(enrollment.pose.localizedName, bundle: .main)
.font(Onyx.Font.headline)
Text(enrollment.seesFace ? "lock.enroll.ready" : "lock.enroll.noFace")
.font(Onyx.Font.footnote)
.foregroundStyle(enrollment.seesFace
? Onyx.Color.positive : Onyx.Color.textTertiary)
Button(String(localized: "lock.enroll.capture")) {
enrollment.captureCurrentPose()
}
.disabled(!enrollment.seesFace)
}
Button(enrollment.isFinished
? String(localized: "lock.enroll.close")
: String(localized: "lock.enroll.cancel")) {
enrollment.stop()
onClose()
}
}
.padding(Onyx.Space.xxl)
.frame(width: 420)
}
}