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.
|
// (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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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