lock: add FPC wiring preview
This commit is contained in:
parent
6eda491496
commit
902a724855
5 changed files with 243 additions and 0 deletions
|
|
@ -481,6 +481,13 @@ Singleton {
|
|||
// (background.wallpaperPath); a path pins the lock's own.
|
||||
property string wallpaperPath: ""
|
||||
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
|
||||
// ambient; media metadata remains personal by default.
|
||||
property JsonObject content: JsonObject {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,25 @@ Scope {
|
|||
property var targetAction: LockContext.ActionEnum.Unlock
|
||||
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() {
|
||||
root.targetAction = LockContext.ActionEnum.Unlock;
|
||||
}
|
||||
|
|
@ -40,6 +59,111 @@ Scope {
|
|||
root.clearText();
|
||||
root.unlockInProgress = false;
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
id: restoreTimer
|
||||
interval: 150
|
||||
|
|
|
|||
|
|
@ -193,6 +193,75 @@ MouseArea {
|
|||
TapHandler {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue