Watch
1
0
Fork
You've already forked souveraine
0

lock: add FPC wiring preview

This commit is contained in:
Fimeg 2026-08-10 20:10:50 -04:00
commit 902a724855
5 changed files with 243 additions and 0 deletions

View file

@ -481,6 +481,13 @@ Singleton {
// (background.wallpaperPath); a path pins the lock's own. // (background.wallpaperPath); a path pins the lock's own.
property string wallpaperPath: "" property string wallpaperPath: ""
property bool showLockedText: true property bool showLockedText: true
// A visible wiring exercise for the FPC1020 path. It is not
// biometric authentication: it may acknowledge a sensor pulse
// and a local hold, but it never unlocks or mints a grant.
property JsonObject fingerprintPreview: JsonObject {
property bool enabled: false
property int holdMs: 3000
}
// Souveraine-owned lock cards. Transport controls are // Souveraine-owned lock cards. Transport controls are
// ambient; media metadata remains personal by default. // ambient; media metadata remains personal by default.
property JsonObject content: JsonObject { property JsonObject content: JsonObject {

View file

@ -23,6 +23,25 @@ Scope {
property var targetAction: LockContext.ActionEnum.Unlock property var targetAction: LockContext.ActionEnum.Unlock
property bool alsoInhibitIdle: false property bool alsoInhibitIdle: false
// This is deliberately an integration preview, not a biometric factor.
// The FPC1020's KEY_WAKEUP edge proves only that something asserted the
// line; it does not prove a matched enrolled fingerprint. Until the
// attested souveraine-fpd path exists, neither a pulse nor a touch hold
// may reach unlocked() or StepUpAuth.
readonly property bool provisionalFingerprintEnabled:
Config.options?.lock?.fingerprintPreview?.enabled ?? false
readonly property int provisionalFingerprintHoldMs:
Config.options?.lock?.fingerprintPreview?.holdMs ?? 3000
property bool provisionalFingerprintHolding: false
property bool provisionalFingerprintConfirmed: false
property bool provisionalFingerprintPulseSeen: false
property real provisionalFingerprintHoldProgress: 0
property double provisionalFingerprintHoldStartedAt: 0
property string provisionalFingerprintPulseToken: ""
signal provisionalFingerprintPulse()
signal provisionalFingerprintPreviewConfirmed()
function resetTargetAction() { function resetTargetAction() {
root.targetAction = LockContext.ActionEnum.Unlock; root.targetAction = LockContext.ActionEnum.Unlock;
} }
@ -40,6 +59,111 @@ Scope {
root.clearText(); root.clearText();
root.unlockInProgress = false; root.unlockInProgress = false;
stopFingerPam(); stopFingerPam();
root.resetProvisionalFingerprint();
}
function beginProvisionalFingerprintHold() {
if (!root.provisionalFingerprintEnabled) return;
root.provisionalFingerprintConfirmed = false;
root.provisionalFingerprintHolding = true;
root.provisionalFingerprintHoldStartedAt = Date.now();
root.provisionalFingerprintHoldProgress = 0;
provisionalFingerprintHoldTimer.start();
}
function cancelProvisionalFingerprintHold() {
if (!root.provisionalFingerprintHolding) return;
root.provisionalFingerprintHolding = false;
root.provisionalFingerprintHoldProgress = 0;
provisionalFingerprintHoldTimer.stop();
}
function confirmProvisionalFingerprintHold() {
if (!root.provisionalFingerprintHolding) return;
root.provisionalFingerprintHolding = false;
root.provisionalFingerprintHoldProgress = 1;
provisionalFingerprintHoldTimer.stop();
root.provisionalFingerprintConfirmed = true;
root.provisionalFingerprintPreviewConfirmed();
provisionalFingerprintConfirmTimer.restart();
console.log("[fingerprint-preview] hold confirmed; no authentication granted");
}
// Called by the narrow future producer seam (`fingerprint.signal`). This
// records only that an input path reached the surface. Any same-user IPC
// caller can cause this today, so it is intentionally visual-only.
function noteProvisionalFingerprintPulse() {
if (!root.provisionalFingerprintEnabled)
return { ok: false, code: "not_enabled", reason: "fingerprint preview is disabled" };
root.provisionalFingerprintPulseSeen = true;
root.provisionalFingerprintPulse();
provisionalFingerprintPulseTimer.restart();
console.log("[fingerprint-preview] pulse observed; no authentication granted");
return { ok: true, status: "observed" };
}
function resetProvisionalFingerprint() {
root.provisionalFingerprintHolding = false;
root.provisionalFingerprintConfirmed = false;
root.provisionalFingerprintPulseSeen = false;
root.provisionalFingerprintHoldProgress = 0;
provisionalFingerprintHoldTimer.stop();
provisionalFingerprintConfirmTimer.stop();
provisionalFingerprintPulseTimer.stop();
}
// blueline-fingerprintd is the only source that may write this root-owned
// status file. Its record says an FPC IRQ reached the daemon; it is not a
// match, and this listener has no authority beyond the preview pulse.
FileView {
id: provisionalFingerprintPulseFile
path: "/run/blueline-fingerprintd/preview-pulse"
watchChanges: true
printErrors: false
onFileChanged: reload()
onLoaded: {
try {
const record = JSON.parse(provisionalFingerprintPulseFile.text());
const token = `${record.sequence}:${record.at_ms}`;
if (token === root.provisionalFingerprintPulseToken) return;
root.provisionalFingerprintPulseToken = token;
// Do not replay an old event merely because the lock surface
// appeared after it. A new reader assertion has a fresh wall
// timestamp and reaches the same visual-only method as the IPC
// diagnostic seam.
const ageMs = Date.now() - Number(record.at_ms);
if (Number(record.sequence) > 0 && ageMs >= 0 && ageMs < 5000)
root.noteProvisionalFingerprintPulse();
} catch (error) {
console.warn("[fingerprint-preview] invalid FPC pulse record:", error);
}
}
}
Timer {
id: provisionalFingerprintHoldTimer
interval: 50
repeat: true
onTriggered: {
const elapsed = Date.now() - root.provisionalFingerprintHoldStartedAt;
root.provisionalFingerprintHoldProgress = Math.min(1,
elapsed / Math.max(1, root.provisionalFingerprintHoldMs));
if (root.provisionalFingerprintHoldProgress >= 1)
root.confirmProvisionalFingerprintHold();
}
}
Timer {
id: provisionalFingerprintConfirmTimer
interval: 3500
onTriggered: root.provisionalFingerprintConfirmed = false
}
Timer {
id: provisionalFingerprintPulseTimer
interval: 3500
onTriggered: root.provisionalFingerprintPulseSeen = false
} }
Timer { Timer {

View file

@ -97,6 +97,20 @@ LockScreen {
} }
} }
// Preview-only diagnostic ingress. The physical FPC1020 producer publishes
// its root-owned pulse record for LockContext to watch; it cannot use
// generic XF86WakeUp here because the touch controller emits that key too.
// This target lets us exercise the same visual-only path manually. It
// never unlocks, reveals Personal content, or mints step-up. Task 41 owns
// replacing this with an attested, source-specific producer.
IpcHandler {
target: "fingerprint"
function signal(): string {
return JSON.stringify(root.context.noteProvisionalFingerprintPulse());
}
}
Timer { Timer {
id: restoreTimer id: restoreTimer
interval: 150 interval: 150

View file

@ -193,6 +193,75 @@ MouseArea {
TapHandler { TapHandler {
onTapped: root.pinRevealed = true onTapped: root.pinRevealed = true
} }
Item {
id: fingerprintPreview
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 18
visible: root.context.provisionalFingerprintEnabled
implicitWidth: 196
implicitHeight: 78
Rectangle {
anchors.fill: parent
radius: Appearance.rounding.normal
color: root.context.provisionalFingerprintConfirmed
? Appearance.colors.colPrimary
: "#2a000000"
border.width: root.context.provisionalFingerprintPulseSeen ? 2 : 1
border.color: root.context.provisionalFingerprintPulseSeen
? Appearance.colors.colPrimary : "#66ffffff"
}
Rectangle {
anchors.left: parent.left
anchors.bottom: parent.bottom
width: parent.width * root.context.provisionalFingerprintHoldProgress
height: 3
radius: 2
color: Appearance.colors.colPrimary
}
ColumnLayout {
anchors.centerIn: parent
spacing: 2
MaterialSymbol {
Layout.alignment: Qt.AlignHCenter
text: "fingerprint"
iconSize: 30
color: root.context.provisionalFingerprintConfirmed
? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer1
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: root.context.provisionalFingerprintConfirmed
? Translation.tr("Hold recorded — PIN still required")
: root.context.provisionalFingerprintPulseSeen
? Translation.tr("Sensor pulse received — hold to confirm")
: Translation.tr("Hold %1 seconds to test fingerprint wiring")
.arg(Math.round(root.context.provisionalFingerprintHoldMs / 1000))
color: root.context.provisionalFingerprintConfirmed
? Appearance.colors.colOnPrimary : Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
horizontalAlignment: Text.AlignHCenter
}
}
MouseArea {
anchors.fill: parent
enabled: root.context.provisionalFingerprintEnabled
preventStealing: true
pressAndHoldInterval: root.context.provisionalFingerprintHoldMs
onPressed: {
root.context.beginProvisionalFingerprintHold();
mouse.accepted = true;
}
onReleased: root.context.cancelProvisionalFingerprintHold()
onCanceled: root.context.cancelProvisionalFingerprintHold()
onPressAndHold: root.context.confirmProvisionalFingerprintHold()
}
}
} }
ColumnLayout { ColumnLayout {

View file

@ -278,4 +278,33 @@ ContentPage {
} }
} }
} }
ContentSection {
icon: "fingerprint"
title: Translation.tr("Fingerprint integration")
ConfigSwitch {
buttonIcon: "fingerprint"
text: Translation.tr("Show fingerprint wiring preview")
checked: Config.options.lock.fingerprintPreview.enabled
onCheckedChanged: Config.options.lock.fingerprintPreview.enabled = checked
StyledToolTip {
text: Translation.tr("Shows a three-second hold test on the lock screen. It never unlocks the device or grants step-up; it is only the UI and sensor-routing preview.")
}
}
ConfigSpinBox {
icon: "timer"
text: Translation.tr("Preview hold (seconds)")
value: Config.options.lock.fingerprintPreview.holdMs / 1000
from: 1
to: 10
stepSize: 1
enabled: Config.options.lock.fingerprintPreview.enabled
onValueChanged: Config.options.lock.fingerprintPreview.holdMs = value * 1000
StyledToolTip {
text: Translation.tr("This controls only the visible hold exercise. A real fingerprint match must arrive through the attested FPC daemon before it can authenticate anything.")
}
}
}
} }