polkit: add temporary FPC confirmation
This commit is contained in:
parent
902a724855
commit
658217e72e
7 changed files with 374 additions and 110 deletions
|
|
@ -481,13 +481,20 @@ 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.
|
||||
// A visible wiring exercise for the FPC1020 path. It never
|
||||
// unlocks the session; the separate Polkit setting below is
|
||||
// the temporary, post-login-only factor.
|
||||
property JsonObject fingerprintPreview: JsonObject {
|
||||
property bool enabled: false
|
||||
property int holdMs: 3000
|
||||
}
|
||||
// The temporary FPC factor belongs only to polkit-1. It cannot
|
||||
// satisfy first login, disk unlock, sudo, SSH, or lock-screen
|
||||
// PAM. The real souveraine-fpd match result will replace this
|
||||
// raw-reader bridge without changing the user-facing surface.
|
||||
property JsonObject fingerprintPolkit: JsonObject {
|
||||
property bool enabled: true
|
||||
}
|
||||
// Souveraine-owned lock cards. Transport controls are
|
||||
// ambient; media metadata remains personal by default.
|
||||
property JsonObject content: JsonObject {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
|
@ -23,24 +24,20 @@ 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.
|
||||
// FingerprintPreview owns the FPC pulse and hold state for both lock and
|
||||
// step-up. This context only exposes it to the lock surface; it never
|
||||
// decides whether a hold unlocks the session.
|
||||
readonly property bool provisionalFingerprintEnabled:
|
||||
Config.options?.lock?.fingerprintPreview?.enabled ?? false
|
||||
FingerprintPreview.previewEnabled
|
||||
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()
|
||||
FingerprintPreview.holdMs
|
||||
readonly property bool provisionalFingerprintHolding:
|
||||
FingerprintPreview.holding && FingerprintPreview.activePurpose === "lock"
|
||||
readonly property bool provisionalFingerprintConfirmed:
|
||||
FingerprintPreview.confirmed && FingerprintPreview.confirmedPurpose === "lock"
|
||||
readonly property bool provisionalFingerprintPulseSeen: FingerprintPreview.pulseSeen
|
||||
readonly property real provisionalFingerprintHoldProgress:
|
||||
FingerprintPreview.activePurpose === "lock" ? FingerprintPreview.holdProgress : 0
|
||||
|
||||
function resetTargetAction() {
|
||||
root.targetAction = LockContext.ActionEnum.Unlock;
|
||||
|
|
@ -63,107 +60,25 @@ Scope {
|
|||
}
|
||||
|
||||
function beginProvisionalFingerprintHold() {
|
||||
if (!root.provisionalFingerprintEnabled) return;
|
||||
root.provisionalFingerprintConfirmed = false;
|
||||
root.provisionalFingerprintHolding = true;
|
||||
root.provisionalFingerprintHoldStartedAt = Date.now();
|
||||
root.provisionalFingerprintHoldProgress = 0;
|
||||
provisionalFingerprintHoldTimer.start();
|
||||
FingerprintPreview.beginHold("lock");
|
||||
}
|
||||
|
||||
function cancelProvisionalFingerprintHold() {
|
||||
if (!root.provisionalFingerprintHolding) return;
|
||||
root.provisionalFingerprintHolding = false;
|
||||
root.provisionalFingerprintHoldProgress = 0;
|
||||
provisionalFingerprintHoldTimer.stop();
|
||||
FingerprintPreview.cancelHold("lock");
|
||||
}
|
||||
|
||||
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");
|
||||
FingerprintPreview.confirmHold("lock");
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Called by the diagnostic `fingerprint.signal` IPC seam. It reaches the
|
||||
// same one-owner state as the root-owned FPC producer record.
|
||||
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" };
|
||||
return FingerprintPreview.notePulse();
|
||||
}
|
||||
|
||||
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
|
||||
FingerprintPreview.reset("lock");
|
||||
}
|
||||
|
||||
Timer {
|
||||
|
|
|
|||
193
surfaces/quickshell/modules/ii/polkit/PolkitContent.qml
Normal file
193
surfaces/quickshell/modules/ii/polkit/PolkitContent.qml
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
// Phone Polkit content with a first-class FPC1020 temporary factor.
|
||||
//
|
||||
// The PAM module owns acceptance. This surface only recognizes its prompt,
|
||||
// waits for a pulse that came from blueline-fingerprintd (not generic wake
|
||||
// input), and submits the blank PAM response after the visible confirmation
|
||||
// interval. The normal PIN/password conversation stays intact as fallback.
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property bool usePasswordChars: !PolkitService.flow?.responseVisible ?? true
|
||||
readonly property bool fpcPrompt:
|
||||
FingerprintPreview.polkitEnabled
|
||||
&& PolkitService.cleanPrompt === "Touch and hold the fingerprint reader"
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape)
|
||||
PolkitService.cancel();
|
||||
}
|
||||
|
||||
function submitPassword() {
|
||||
PolkitService.submit(inputField.text);
|
||||
}
|
||||
|
||||
function usePinInstead() {
|
||||
// An empty response tells pam_souveraine_fpc to decline. PAM then
|
||||
// reaches the ordinary system-auth conversation, where this same
|
||||
// window shows the normal password field.
|
||||
PolkitService.submit("");
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: PolkitService
|
||||
function onInteractionAvailableChanged() {
|
||||
if (!PolkitService.interactionAvailable || root.fpcPrompt)
|
||||
return;
|
||||
inputField.text = "";
|
||||
inputField.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: FingerprintPreview
|
||||
function onPulseObserved() {
|
||||
if (PolkitService.active && PolkitService.interactionAvailable && root.fpcPrompt)
|
||||
FingerprintPreview.beginHold("polkit");
|
||||
}
|
||||
function onHoldConfirmed(purpose) {
|
||||
if (purpose === "polkit" && root.fpcPrompt && PolkitService.interactionAvailable)
|
||||
PolkitService.submit("");
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Appearance.colors.colScrim
|
||||
opacity: 0
|
||||
Component.onCompleted: opacity = 1
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
|
||||
WindowDialog {
|
||||
anchors.centerIn: parent
|
||||
backgroundWidth: 450
|
||||
show: false
|
||||
Component.onCompleted: show = true
|
||||
|
||||
MaterialSymbol {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
iconSize: 26
|
||||
text: root.fpcPrompt ? "fingerprint" : "security"
|
||||
color: Appearance.colors.colSecondary
|
||||
}
|
||||
|
||||
WindowDialogTitle {
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Translation.tr("Authentication")
|
||||
}
|
||||
|
||||
WindowDialogParagraph {
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
text: PolkitService.cleanMessage
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
visible: root.fpcPrompt
|
||||
implicitHeight: visible ? 104 : 0
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Appearance.rounding.normal
|
||||
color: FingerprintPreview.confirmedPurpose === "polkit"
|
||||
? Appearance.colors.colPrimary : "#2a000000"
|
||||
border.width: FingerprintPreview.pulseSeen ? 2 : 1
|
||||
border.color: FingerprintPreview.pulseSeen
|
||||
? Appearance.colors.colPrimary : "#66ffffff"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width * (FingerprintPreview.activePurpose === "polkit"
|
||||
? FingerprintPreview.holdProgress : 0)
|
||||
height: 3
|
||||
radius: 2
|
||||
color: Appearance.colors.colPrimary
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
|
||||
MaterialSymbol {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "fingerprint"
|
||||
iconSize: 32
|
||||
color: FingerprintPreview.confirmedPurpose === "polkit"
|
||||
? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer1
|
||||
}
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: FingerprintPreview.activePurpose === "polkit"
|
||||
? Translation.tr("Reader contact received — confirming")
|
||||
: Translation.tr("Touch and hold the fingerprint reader")
|
||||
color: FingerprintPreview.confirmedPurpose === "polkit"
|
||||
? Appearance.colors.colOnPrimary : Appearance.colors.colSubtext
|
||||
font.pixelSize: Appearance.font.pixelSize.smaller
|
||||
}
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: FingerprintPreview.activePurpose === "polkit"
|
||||
text: Translation.tr("%1 seconds").arg(Math.round(FingerprintPreview.holdMs / 1000))
|
||||
color: Appearance.colors.colSubtext
|
||||
font.pixelSize: Appearance.font.pixelSize.smaller
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTextField {
|
||||
id: inputField
|
||||
Layout.fillWidth: true
|
||||
visible: !root.fpcPrompt
|
||||
focus: visible
|
||||
enabled: PolkitService.interactionAvailable
|
||||
placeholderText: PolkitService.cleanPrompt
|
||||
echoMode: root.usePasswordChars ? TextInput.Password : TextInput.Normal
|
||||
onAccepted: root.submitPassword()
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape)
|
||||
PolkitService.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
WindowDialogButtonRow {
|
||||
Layout.bottomMargin: 10
|
||||
Item { Layout.fillWidth: true }
|
||||
DialogButton {
|
||||
buttonText: Translation.tr("Cancel")
|
||||
onClicked: PolkitService.cancel()
|
||||
}
|
||||
DialogButton {
|
||||
visible: root.fpcPrompt
|
||||
enabled: PolkitService.interactionAvailable
|
||||
buttonText: Translation.tr("Use PIN instead")
|
||||
onClicked: root.usePinInstead()
|
||||
}
|
||||
DialogButton {
|
||||
visible: !root.fpcPrompt
|
||||
enabled: PolkitService.interactionAvailable
|
||||
buttonText: Translation.tr("OK")
|
||||
onClicked: root.submitPassword()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onFpcPromptChanged: {
|
||||
if (!fpcPrompt)
|
||||
FingerprintPreview.reset("polkit");
|
||||
}
|
||||
}
|
||||
|
|
@ -289,7 +289,7 @@ ContentPage {
|
|||
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.")
|
||||
text: Translation.tr("Shows a three-second hold test on the lock screen. It never unlocks the device; post-login Polkit is configured separately below.")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -303,7 +303,17 @@ ContentPage {
|
|||
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.")
|
||||
text: Translation.tr("This controls the visible lock-screen exercise only. It does not change the temporary post-login Polkit factor.")
|
||||
}
|
||||
}
|
||||
|
||||
ConfigSwitch {
|
||||
buttonIcon: "admin_panel_settings"
|
||||
text: Translation.tr("Allow temporary FPC confirmation for Polkit")
|
||||
checked: Config.options.lock.fingerprintPolkit.enabled
|
||||
onCheckedChanged: Config.options.lock.fingerprintPolkit.enabled = checked
|
||||
StyledToolTip {
|
||||
text: Translation.tr("After PIN login, user-facing Polkit prompts may accept a fresh reader assertion after the visible confirmation interval. It never unlocks the session or replaces first-login PIN.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue