261 lines
9 KiB
QML
261 lines
9 KiB
QML
import qs
|
|
import qs.modules.common
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.Services.Pam
|
|
|
|
Scope {
|
|
id: root
|
|
|
|
enum ActionEnum { Unlock, Poweroff, Reboot }
|
|
|
|
signal shouldReFocus()
|
|
signal unlocked(targetAction: var)
|
|
signal failed()
|
|
|
|
// These properties are in the context and not individual lock surfaces
|
|
// so all surfaces can share the same state.
|
|
property string currentText: ""
|
|
property bool unlockInProgress: false
|
|
property bool showFailure: false
|
|
property bool fingerprintsConfigured: false
|
|
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;
|
|
}
|
|
|
|
function clearText() {
|
|
root.currentText = "";
|
|
}
|
|
|
|
function resetClearTimer() {
|
|
passwordClearTimer.restart();
|
|
}
|
|
|
|
function reset() {
|
|
root.resetTargetAction();
|
|
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 {
|
|
id: passwordClearTimer
|
|
interval: 10000
|
|
onTriggered: {
|
|
root.reset();
|
|
}
|
|
}
|
|
|
|
onCurrentTextChanged: {
|
|
if (currentText.length > 0) {
|
|
showFailure = false;
|
|
GlobalStates.screenUnlockFailed = false;
|
|
}
|
|
GlobalStates.screenLockContainsCharacters = currentText.length > 0;
|
|
passwordClearTimer.restart();
|
|
}
|
|
|
|
function tryUnlock(alsoInhibitIdle = false) {
|
|
root.alsoInhibitIdle = alsoInhibitIdle;
|
|
root.unlockInProgress = true;
|
|
pam.start();
|
|
}
|
|
|
|
function tryFingerUnlock() {
|
|
if (root.fingerprintsConfigured) {
|
|
fingerPam.start();
|
|
}
|
|
}
|
|
|
|
function stopFingerPam() {
|
|
if (fingerPam.active) {
|
|
fingerPam.abort();
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: fingerprintCheckProc
|
|
running: true
|
|
command: ["bash", "-c", "fprintd-list $(whoami)"]
|
|
stdout: StdioCollector {
|
|
id: fingerprintOutputCollector
|
|
onStreamFinished: {
|
|
root.fingerprintsConfigured = fingerprintOutputCollector.text.includes("Fingerprints for user");
|
|
}
|
|
}
|
|
onExited: (exitCode, exitStatus) => {
|
|
if (exitCode !== 0) {
|
|
// console.warn("[LockContext] fprintd-list command exited with error:", exitCode, exitStatus);
|
|
root.fingerprintsConfigured = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
PamContext {
|
|
id: pam
|
|
|
|
// pam_unix will ask for a response for the password prompt
|
|
onPamMessage: {
|
|
if (this.responseRequired) {
|
|
this.respond(root.currentText);
|
|
}
|
|
}
|
|
|
|
// pam_unix won't send any important messages so all we need is the completion status.
|
|
onCompleted: result => {
|
|
if (result == PamResult.Success) {
|
|
root.unlocked(root.targetAction);
|
|
stopFingerPam();
|
|
} else {
|
|
root.clearText();
|
|
root.unlockInProgress = false;
|
|
GlobalStates.screenUnlockFailed = true;
|
|
root.showFailure = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
PamContext {
|
|
id: fingerPam
|
|
|
|
configDirectory: "pam"
|
|
config: "fprintd.conf"
|
|
|
|
onCompleted: result => {
|
|
if (result == PamResult.Success) {
|
|
root.unlocked(root.targetAction);
|
|
stopFingerPam();
|
|
} else if (result == PamResult.Error) { // if timeout or etc..
|
|
tryFingerUnlock()
|
|
}
|
|
}
|
|
}
|
|
}
|