lock: add fprint support (#429)
* lock: add fprint support * lock: better fprint detection * lock: cap error retries * nix: fix fprint pam for nix * lock: reset fprint tries * lock: minor pam fixes Delay fprint error retries Reset fprint error retries on lock * lock: loading indicator passwd state Instead of fprint state cause no way of detecting that * dashboard: better visualiser * lock: better fprint availability check * lock: better in/out anim Animating layout sizes is a bad idea :woe: Use scale instead * lock: add better error/fail messages * lock: less fprint icon states Already shown by message * lock: fix fprint reset * lock: include passwd pam * lock: flash message on change * lock: fix message anim Also wrap message instead of eliding * lock: better messages for no fprint
This commit is contained in:
parent
f747d82b93
commit
aea5795832
11 changed files with 311 additions and 29 deletions
3
assets/pam.d/fprint
Normal file
3
assets/pam.d/fprint
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#%PAM-1.0
|
||||
|
||||
auth required pam_fprintd.so max-tries=1
|
||||
6
assets/pam.d/passwd
Normal file
6
assets/pam.d/passwd
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#%PAM-1.0
|
||||
|
||||
auth required pam_faillock.so preauth
|
||||
auth [success=1 default=bad] pam_unix.so nullok
|
||||
auth [default=die] pam_faillock.so authfail
|
||||
auth required pam_faillock.so authsucc
|
||||
|
|
@ -2,6 +2,8 @@ import Quickshell.Io
|
|||
|
||||
JsonObject {
|
||||
property bool recolourLogo: false
|
||||
property bool enableFprint: true
|
||||
property int maxFprintTries: 3
|
||||
property Sizes sizes: Sizes {}
|
||||
|
||||
component Sizes: JsonObject {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.images
|
||||
import qs.services
|
||||
import qs.config
|
||||
|
|
@ -17,6 +18,7 @@ ColumnLayout {
|
|||
readonly property int centerWidth: Config.lock.sizes.centerWidth * centerScale
|
||||
|
||||
Layout.preferredWidth: centerWidth
|
||||
Layout.fillWidth: false
|
||||
Layout.fillHeight: true
|
||||
|
||||
spacing: Appearance.spacing.large * 2
|
||||
|
|
@ -148,9 +150,34 @@ ColumnLayout {
|
|||
anchors.margins: Appearance.padding.small
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
Layout.leftMargin: Appearance.padding.smaller
|
||||
text: "lock"
|
||||
Item {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: fprintIcon.implicitHeight + Appearance.padding.small * 2
|
||||
|
||||
MaterialIcon {
|
||||
id: fprintIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: {
|
||||
if (root.lock.pam.fprint.tries >= Config.lock.maxFprintTries)
|
||||
return "fingerprint_off";
|
||||
if (root.lock.pam.fprint.active)
|
||||
return "fingerprint";
|
||||
return "lock";
|
||||
}
|
||||
color: root.lock.pam.fprint.tries >= Config.lock.maxFprintTries ? Colours.palette.m3error : Colours.palette.m3onSurface
|
||||
opacity: root.lock.pam.passwd.active ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
StyledBusyIndicator {
|
||||
anchors.fill: parent
|
||||
running: root.lock.pam.passwd.active
|
||||
}
|
||||
}
|
||||
|
||||
InputField {
|
||||
|
|
@ -186,6 +213,135 @@ ColumnLayout {
|
|||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: message
|
||||
|
||||
readonly property Pam pam: root.lock.pam
|
||||
readonly property string msg: {
|
||||
if (pam.fprintState === "error")
|
||||
return qsTr("ERROR: %1").arg(pam.fprint.message);
|
||||
if (pam.state === "error")
|
||||
return qsTr("ERROR: %1").arg(pam.passwd.message);
|
||||
|
||||
if (pam.lockMessage)
|
||||
return pam.lockMessage;
|
||||
|
||||
if (pam.state === "max" && pam.fprintState === "max")
|
||||
return qsTr("Maximum password and fingerprint attempts reached.");
|
||||
if (pam.state === "max") {
|
||||
if (pam.fprint.available)
|
||||
return qsTr("Maximum password attempts reached. Please use fingerprint.");
|
||||
return qsTr("Maximum password attempts reached.");
|
||||
}
|
||||
if (pam.fprintState === "max")
|
||||
return qsTr("Maximum fingerprint attempts reached. Please use password.");
|
||||
|
||||
if (pam.state === "fail") {
|
||||
if (pam.fprint.available)
|
||||
return qsTr("Incorrect password. Please try again or use fingerprint.");
|
||||
return qsTr("Incorrect password. Please try again.");
|
||||
}
|
||||
if (pam.fprintState === "fail")
|
||||
return qsTr("Fingerprint not recognized (%1/%2). Please try again or use password.").arg(pam.fprint.tries).arg(Config.lock.maxFprintTries);
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: -Appearance.spacing.large
|
||||
|
||||
scale: 0.7
|
||||
opacity: 0
|
||||
color: Colours.palette.m3error
|
||||
|
||||
font.pointSize: Appearance.font.size.small
|
||||
font.family: Appearance.font.family.mono
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
|
||||
onMsgChanged: {
|
||||
if (msg) {
|
||||
if (opacity > 0) {
|
||||
animate = true;
|
||||
text = msg;
|
||||
animate = false;
|
||||
|
||||
exitAnim.stop();
|
||||
if (scale < 1)
|
||||
appearAnim.restart();
|
||||
else
|
||||
flashAnim.restart();
|
||||
} else {
|
||||
text = msg;
|
||||
exitAnim.stop();
|
||||
appearAnim.restart();
|
||||
}
|
||||
} else {
|
||||
appearAnim.stop();
|
||||
flashAnim.stop();
|
||||
exitAnim.start();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.lock.pam
|
||||
|
||||
function onFlashMsg(): void {
|
||||
exitAnim.stop();
|
||||
if (message.scale < 1)
|
||||
appearAnim.restart();
|
||||
else
|
||||
flashAnim.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Anim {
|
||||
id: appearAnim
|
||||
|
||||
target: message
|
||||
properties: "scale,opacity"
|
||||
to: 1
|
||||
onFinished: flashAnim.restart()
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: flashAnim
|
||||
|
||||
loops: 2
|
||||
|
||||
MessageAnim {
|
||||
to: 0.3
|
||||
}
|
||||
MessageAnim {
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: exitAnim
|
||||
|
||||
Anim {
|
||||
target: message
|
||||
property: "scale"
|
||||
to: 0.7
|
||||
duration: Appearance.anim.durations.large
|
||||
}
|
||||
Anim {
|
||||
target: message
|
||||
property: "opacity"
|
||||
to: 0
|
||||
duration: Appearance.anim.durations.large
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component MessageAnim: NumberAnimation {
|
||||
target: message
|
||||
property: "opacity"
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.type: Easing.Linear
|
||||
}
|
||||
|
||||
component Anim: NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
|
|
|
|||
|
|
@ -8,17 +8,12 @@ RowLayout {
|
|||
id: root
|
||||
|
||||
required property var lock
|
||||
property real centerScale
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.large * 2
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
scale: root.centerScale
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
|
|
@ -62,16 +57,12 @@ RowLayout {
|
|||
}
|
||||
|
||||
Center {
|
||||
Layout.leftMargin: -(1 - scale) * implicitWidth / 2
|
||||
Layout.rightMargin: -(1 - scale) * implicitWidth / 2
|
||||
scale: root.centerScale
|
||||
lock: root.lock
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
scale: root.centerScale
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
|
|
|
|||
|
|
@ -40,19 +40,15 @@ Item {
|
|||
anchors.centerIn: parent
|
||||
|
||||
text: {
|
||||
if (root.pam.active)
|
||||
if (root.pam.passwd.active)
|
||||
return qsTr("Loading...");
|
||||
if (root.pam.state === "error")
|
||||
return qsTr("An error occured");
|
||||
if (root.pam.state === "max")
|
||||
return qsTr("You have reached the maximum number of tries");
|
||||
if (root.pam.state === "fail")
|
||||
return qsTr("Incorrect password");
|
||||
return qsTr("Enter your password");
|
||||
}
|
||||
|
||||
animate: true
|
||||
color: root.pam.active ? Colours.palette.m3secondary : root.pam.state ? Colours.palette.m3error : Colours.palette.m3outline
|
||||
color: root.pam.passwd.active ? Colours.palette.m3secondary : Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.family: Appearance.font.family.mono
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ WlSessionLockSurface {
|
|||
required property Pam pam
|
||||
|
||||
readonly property alias unlocking: unlockAnim.running
|
||||
readonly property bool animating: initAnim.running || unlockAnim.running
|
||||
|
||||
color: "transparent"
|
||||
|
||||
|
|
@ -44,7 +43,7 @@ WlSessionLockSurface {
|
|||
}
|
||||
Anim {
|
||||
target: content
|
||||
property: "centerScale"
|
||||
property: "scale"
|
||||
to: 0
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
|
|
@ -132,7 +131,7 @@ WlSessionLockSurface {
|
|||
}
|
||||
Anim {
|
||||
target: content
|
||||
property: "centerScale"
|
||||
property: "scale"
|
||||
to: 1
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
|
|
@ -219,8 +218,13 @@ WlSessionLockSurface {
|
|||
Content {
|
||||
id: content
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult * Config.lock.sizes.ratio - Appearance.padding.large * 2
|
||||
height: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult - Appearance.padding.large * 2
|
||||
|
||||
lock: root
|
||||
opacity: 0
|
||||
scale: 0
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ Item {
|
|||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: root.lock.animating ? "" : (Players.active?.trackArtUrl ?? "")
|
||||
source: Players.active?.trackArtUrl ?? ""
|
||||
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ ColumnLayout {
|
|||
anchors.centerIn: parent
|
||||
asynchronous: true
|
||||
active: opacity > 0
|
||||
opacity: root.lock.animating || Notifs.list.length > 0 ? 0 : 1
|
||||
opacity: Notifs.list.length > 0 ? 0 : 1
|
||||
|
||||
sourceComponent: ColumnLayout {
|
||||
spacing: Appearance.spacing.large
|
||||
|
|
@ -51,7 +51,7 @@ ColumnLayout {
|
|||
asynchronous: true
|
||||
source: `file://${Quickshell.shellDir}/assets/dino.png`
|
||||
fillMode: Image.PreserveAspectFit
|
||||
sourceSize.width: root.lock.animating ? 0 : clipRect.width * 0.8
|
||||
sourceSize.width: clipRect.width * 0.8
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: Colouriser {
|
||||
|
|
@ -84,7 +84,7 @@ ColumnLayout {
|
|||
clip: true
|
||||
|
||||
model: ScriptModel {
|
||||
values: root.lock.animating ? [] : [...new Set(Notifs.list.map(notif => notif.appName))].reverse()
|
||||
values: [...new Set(Notifs.list.map(notif => notif.appName))].reverse()
|
||||
}
|
||||
|
||||
delegate: NotifGroup {}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import qs.config
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Services.Pam
|
||||
import QtQuick
|
||||
|
|
@ -9,12 +11,16 @@ Scope {
|
|||
required property WlSessionLock lock
|
||||
|
||||
readonly property alias passwd: passwd
|
||||
readonly property bool active: passwd.active
|
||||
readonly property alias fprint: fprint
|
||||
property string lockMessage
|
||||
property string state
|
||||
property string fprintState
|
||||
property string buffer
|
||||
|
||||
signal flashMsg
|
||||
|
||||
function handleKey(event: KeyEvent): void {
|
||||
if (passwd.active)
|
||||
if (passwd.active || state === "max")
|
||||
return;
|
||||
|
||||
if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {
|
||||
|
|
@ -34,6 +40,16 @@ Scope {
|
|||
PamContext {
|
||||
id: passwd
|
||||
|
||||
config: "passwd"
|
||||
configDirectory: Quickshell.shellDir + "/assets/pam.d"
|
||||
|
||||
onMessageChanged: {
|
||||
if (message.startsWith("The account is locked"))
|
||||
root.lockMessage = message;
|
||||
else if (root.lockMessage && message.endsWith(" left to unlock)"))
|
||||
root.lockMessage += "\n" + message;
|
||||
}
|
||||
|
||||
onResponseRequiredChanged: {
|
||||
if (!responseRequired)
|
||||
return;
|
||||
|
|
@ -53,22 +69,125 @@ Scope {
|
|||
else if (res === PamResult.Failed)
|
||||
root.state = "fail";
|
||||
|
||||
root.flashMsg();
|
||||
stateReset.restart();
|
||||
}
|
||||
}
|
||||
|
||||
PamContext {
|
||||
id: fprint
|
||||
|
||||
property bool available
|
||||
property int tries
|
||||
property int errorTries
|
||||
|
||||
function checkAvail(): void {
|
||||
if (!available || !Config.lock.enableFprint || !root.lock.secure) {
|
||||
abort();
|
||||
return;
|
||||
}
|
||||
|
||||
tries = 0;
|
||||
errorTries = 0;
|
||||
start();
|
||||
}
|
||||
|
||||
config: "fprint"
|
||||
configDirectory: Quickshell.shellDir + "/assets/pam.d"
|
||||
|
||||
onCompleted: res => {
|
||||
if (!available)
|
||||
return;
|
||||
|
||||
if (res === PamResult.Success)
|
||||
return root.lock.unlock();
|
||||
|
||||
if (res === PamResult.Error) {
|
||||
root.fprintState = "error";
|
||||
errorTries++;
|
||||
if (errorTries < 5) {
|
||||
abort();
|
||||
errorRetry.restart();
|
||||
}
|
||||
} else if (res === PamResult.MaxTries) {
|
||||
// Isn't actually the real max tries as pam only reports completed
|
||||
// when max tries is reached.
|
||||
tries++;
|
||||
if (tries < Config.lock.maxFprintTries) {
|
||||
// Restart if not actually real max tries
|
||||
root.fprintState = "fail";
|
||||
start();
|
||||
} else {
|
||||
root.fprintState = "max";
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
root.flashMsg();
|
||||
fprintStateReset.start();
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: availProc
|
||||
|
||||
command: ["sh", "-c", "fprintd-list $USER"]
|
||||
onExited: code => {
|
||||
fprint.available = code === 0;
|
||||
fprint.checkAvail();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: errorRetry
|
||||
|
||||
interval: 800
|
||||
onTriggered: fprint.start()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: stateReset
|
||||
|
||||
interval: 4000
|
||||
onTriggered: root.state = ""
|
||||
onTriggered: {
|
||||
if (root.state !== "max")
|
||||
root.state = "";
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: fprintStateReset
|
||||
|
||||
interval: 4000
|
||||
onTriggered: {
|
||||
root.fprintState = "";
|
||||
fprint.errorTries = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.lock
|
||||
|
||||
function onSecureChanged(): void {
|
||||
if (root.lock.secure) {
|
||||
availProc.running = true;
|
||||
root.buffer = "";
|
||||
root.state = "";
|
||||
root.fprintState = "";
|
||||
root.lockMessage = "";
|
||||
}
|
||||
}
|
||||
|
||||
function onUnlock(): void {
|
||||
root.buffer = "";
|
||||
fprint.abort();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Config.lock
|
||||
|
||||
function onEnableFprintChanged(): void {
|
||||
fprint.checkAvail();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,11 @@
|
|||
nativeBuildInputs = [gcc];
|
||||
buildInputs = [aubio pipewire];
|
||||
|
||||
patchPhase = ''
|
||||
substituteInPlace assets/pam.d/fprint \
|
||||
--replace-fail pam_fprintd.so /run/current-system/sw/lib/security/pam_fprintd.so
|
||||
'';
|
||||
|
||||
buildPhase = ''
|
||||
mkdir -p bin
|
||||
g++ -std=c++17 -Wall -Wextra \
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue