mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-14 17:52:10 -04:00
feat: Add FIDO2/U2F security key support for lock screen (#1842)
* feat: Add FIDO2/U2F security key support for lock screen Adds hardware security key authentication (e.g. YubiKey) with two modes: Alternative (OR) and Second Factor (AND). Includes settings UI, PAM integration, availability detection, and proper state cleanup. Also fixes persist:false properties being reset on settings file reload. * feat: Add U2F pending timeout and Escape to cancel Cancel U2F second factor after 30s or on Escape key press, returning to password/fingerprint input. * fix: U2F detection honors custom PAM override for non-default key paths
This commit is contained in:
@@ -494,6 +494,9 @@ Singleton {
|
|||||||
property bool enableFprint: false
|
property bool enableFprint: false
|
||||||
property int maxFprintTries: 15
|
property int maxFprintTries: 15
|
||||||
property bool fprintdAvailable: false
|
property bool fprintdAvailable: false
|
||||||
|
property bool enableU2f: false
|
||||||
|
property string u2fMode: "or"
|
||||||
|
property bool u2fAvailable: false
|
||||||
property string lockScreenActiveMonitor: "all"
|
property string lockScreenActiveMonitor: "all"
|
||||||
property string lockScreenInactiveColor: "#000000"
|
property string lockScreenInactiveColor: "#000000"
|
||||||
property int lockScreenNotificationMode: 0
|
property int lockScreenNotificationMode: 0
|
||||||
@@ -985,6 +988,7 @@ Singleton {
|
|||||||
loadSettings();
|
loadSettings();
|
||||||
initializeListModels();
|
initializeListModels();
|
||||||
Processes.detectFprintd();
|
Processes.detectFprintd();
|
||||||
|
Processes.detectU2f();
|
||||||
Processes.checkPluginSettings();
|
Processes.checkPluginSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ Singleton {
|
|||||||
fprintdDetectionProcess.running = true;
|
fprintdDetectionProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectU2f() {
|
||||||
|
u2fDetectionProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
function checkPluginSettings() {
|
function checkPluginSettings() {
|
||||||
pluginSettingsCheckProcess.running = true;
|
pluginSettingsCheckProcess.running = true;
|
||||||
}
|
}
|
||||||
@@ -57,6 +61,16 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property var u2fDetectionProcess: Process {
|
||||||
|
command: ["sh", "-c", "(test -f /usr/lib/security/pam_u2f.so || test -f /usr/lib64/security/pam_u2f.so) && (test -f /etc/pam.d/dankshell-u2f || test -f \"$HOME/.config/Yubico/u2f_keys\")"]
|
||||||
|
running: false
|
||||||
|
onExited: function (exitCode) {
|
||||||
|
if (!settingsRoot)
|
||||||
|
return;
|
||||||
|
settingsRoot.u2fAvailable = (exitCode === 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
property var pluginSettingsCheckProcess: Process {
|
property var pluginSettingsCheckProcess: Process {
|
||||||
command: ["test", "-f", settingsRoot?.pluginSettingsPath || ""]
|
command: ["test", "-f", settingsRoot?.pluginSettingsPath || ""]
|
||||||
running: false
|
running: false
|
||||||
|
|||||||
@@ -317,6 +317,9 @@ var SPEC = {
|
|||||||
enableFprint: { def: false },
|
enableFprint: { def: false },
|
||||||
maxFprintTries: { def: 15 },
|
maxFprintTries: { def: 15 },
|
||||||
fprintdAvailable: { def: false, persist: false },
|
fprintdAvailable: { def: false, persist: false },
|
||||||
|
enableU2f: { def: false },
|
||||||
|
u2fMode: { def: "or" },
|
||||||
|
u2fAvailable: { def: false, persist: false },
|
||||||
lockScreenActiveMonitor: { def: "all" },
|
lockScreenActiveMonitor: { def: "all" },
|
||||||
lockScreenInactiveColor: { def: "#000000" },
|
lockScreenInactiveColor: { def: "#000000" },
|
||||||
lockScreenNotificationMode: { def: 0 },
|
lockScreenNotificationMode: { def: 0 },
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ function parse(root, jsonObj) {
|
|||||||
|
|
||||||
for (var k in SPEC) {
|
for (var k in SPEC) {
|
||||||
if (k === "pluginSettings") continue;
|
if (k === "pluginSettings") continue;
|
||||||
|
// Runtime-only keys are never in the JSON; resetting them here
|
||||||
|
// would wipe values set by detection processes on every reload.
|
||||||
|
if (SPEC[k].persist === false) continue;
|
||||||
if (!(k in jsonObj)) {
|
if (!(k in jsonObj)) {
|
||||||
root[k] = SPEC[k].def;
|
root[k] = SPEC[k].def;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -687,14 +687,24 @@ Item {
|
|||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: {
|
name: {
|
||||||
|
if (pam.u2fPending)
|
||||||
|
return "passkey";
|
||||||
if (pam.fprint.tries >= SettingsData.maxFprintTries)
|
if (pam.fprint.tries >= SettingsData.maxFprintTries)
|
||||||
return "fingerprint_off";
|
return "fingerprint_off";
|
||||||
if (pam.fprint.active)
|
if (pam.fprint.active)
|
||||||
return "fingerprint";
|
return "fingerprint";
|
||||||
|
if (pam.u2f.active)
|
||||||
|
return "passkey";
|
||||||
return "lock";
|
return "lock";
|
||||||
}
|
}
|
||||||
size: 20
|
size: 20
|
||||||
color: pam.fprint.tries >= SettingsData.maxFprintTries ? Theme.error : (passwordField.activeFocus ? Theme.primary : Theme.surfaceVariantText)
|
color: {
|
||||||
|
if (pam.fprint.tries >= SettingsData.maxFprintTries)
|
||||||
|
return Theme.error;
|
||||||
|
if (pam.u2fState !== "")
|
||||||
|
return Theme.tertiary;
|
||||||
|
return passwordField.activeFocus ? Theme.primary : Theme.surfaceVariantText;
|
||||||
|
}
|
||||||
opacity: pam.passwd.active ? 0 : 1
|
opacity: pam.passwd.active ? 0 : 1
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
@@ -745,8 +755,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
if (!demoMode && !pam.passwd.active) {
|
if (!demoMode && !pam.passwd.active && !pam.u2fPending) {
|
||||||
console.log("Enter pressed, starting PAM authentication");
|
|
||||||
pam.passwd.start();
|
pam.passwd.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -756,6 +765,11 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
if (pam.u2fPending) {
|
||||||
|
pam.cancelU2fPending();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
clear();
|
clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -820,6 +834,11 @@ Item {
|
|||||||
if (root.unlocking) {
|
if (root.unlocking) {
|
||||||
return "Unlocking...";
|
return "Unlocking...";
|
||||||
}
|
}
|
||||||
|
if (pam.u2fPending) {
|
||||||
|
if (pam.u2fState === "insert")
|
||||||
|
return "Insert your security key...";
|
||||||
|
return "Touch your security key...";
|
||||||
|
}
|
||||||
if (pam.passwd.active) {
|
if (pam.passwd.active) {
|
||||||
return "Authenticating...";
|
return "Authenticating...";
|
||||||
}
|
}
|
||||||
@@ -894,7 +913,7 @@ Item {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
iconName: "keyboard"
|
iconName: "keyboard"
|
||||||
buttonSize: 32
|
buttonSize: 32
|
||||||
visible: !demoMode && !pam.passwd.active && !root.unlocking
|
visible: !demoMode && !pam.passwd.active && !root.unlocking && !pam.u2fPending
|
||||||
enabled: visible
|
enabled: visible
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (keyboardController.isKeyboardActive) {
|
if (keyboardController.isKeyboardActive) {
|
||||||
@@ -995,11 +1014,10 @@ Item {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
iconName: "keyboard_return"
|
iconName: "keyboard_return"
|
||||||
buttonSize: 36
|
buttonSize: 36
|
||||||
visible: (demoMode || (!pam.passwd.active && !root.unlocking))
|
visible: (demoMode || (!pam.passwd.active && !root.unlocking && !pam.u2fPending))
|
||||||
enabled: !demoMode
|
enabled: !demoMode
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!demoMode) {
|
if (!demoMode && !pam.u2fPending) {
|
||||||
console.log("Enter button clicked, starting PAM authentication");
|
|
||||||
pam.passwd.start();
|
pam.passwd.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1025,6 +1043,12 @@ Item {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: 20
|
Layout.preferredHeight: 20
|
||||||
text: {
|
text: {
|
||||||
|
if (pam.u2fState === "insert" && !pam.u2fPending) {
|
||||||
|
return "Insert your security key...";
|
||||||
|
}
|
||||||
|
if (pam.u2fState === "waiting" && !pam.u2fPending) {
|
||||||
|
return "Touch your security key...";
|
||||||
|
}
|
||||||
if (root.pamState === "error") {
|
if (root.pamState === "error") {
|
||||||
return "Authentication error - try again";
|
return "Authentication error - try again";
|
||||||
}
|
}
|
||||||
@@ -1036,10 +1060,10 @@ Item {
|
|||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
color: Theme.error
|
color: (pam.u2fState === "waiting" || pam.u2fState === "insert") ? Theme.outline : Theme.error
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
opacity: root.pamState !== "" ? 1 : 0
|
opacity: (root.pamState !== "" || ((pam.u2fState === "waiting" || pam.u2fState === "insert") && !pam.u2fPending)) ? 1 : 0
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
@@ -1607,6 +1631,14 @@ Item {
|
|||||||
root.passwordBuffer = "";
|
root.passwordBuffer = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onU2fPendingChanged: {
|
||||||
|
if (u2fPending) {
|
||||||
|
passwordField.text = "";
|
||||||
|
root.passwordBuffer = "";
|
||||||
|
if (keyboardController.isKeyboardActive)
|
||||||
|
keyboardController.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Binding {
|
Binding {
|
||||||
|
|||||||
@@ -14,14 +14,51 @@ Scope {
|
|||||||
|
|
||||||
readonly property alias passwd: passwd
|
readonly property alias passwd: passwd
|
||||||
readonly property alias fprint: fprint
|
readonly property alias fprint: fprint
|
||||||
|
readonly property alias u2f: u2f
|
||||||
property string lockMessage
|
property string lockMessage
|
||||||
property string state
|
property string state
|
||||||
property string fprintState
|
property string fprintState
|
||||||
|
property string u2fState
|
||||||
|
property bool u2fPending: false
|
||||||
property string buffer
|
property string buffer
|
||||||
|
|
||||||
signal flashMsg
|
signal flashMsg
|
||||||
signal unlockRequested
|
signal unlockRequested
|
||||||
|
|
||||||
|
function completeUnlock(): void {
|
||||||
|
if (!unlockInProgress) {
|
||||||
|
unlockInProgress = true;
|
||||||
|
passwd.abort();
|
||||||
|
fprint.abort();
|
||||||
|
u2f.abort();
|
||||||
|
errorRetry.running = false;
|
||||||
|
u2fErrorRetry.running = false;
|
||||||
|
u2fPendingTimeout.running = false;
|
||||||
|
u2fPending = false;
|
||||||
|
u2fState = "";
|
||||||
|
unlockRequested();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function proceedAfterPrimaryAuth(): void {
|
||||||
|
if (SettingsData.enableU2f && SettingsData.u2fMode === "and" && u2f.available) {
|
||||||
|
u2f.startForSecondFactor();
|
||||||
|
} else {
|
||||||
|
completeUnlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelU2fPending(): void {
|
||||||
|
if (!u2fPending)
|
||||||
|
return;
|
||||||
|
u2f.abort();
|
||||||
|
u2fErrorRetry.running = false;
|
||||||
|
u2fPendingTimeout.running = false;
|
||||||
|
u2fPending = false;
|
||||||
|
u2fState = "";
|
||||||
|
fprint.checkAvail();
|
||||||
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: dankshellConfigWatcher
|
id: dankshellConfigWatcher
|
||||||
|
|
||||||
@@ -30,9 +67,9 @@ Scope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: loginConfigWatcher
|
id: u2fConfigWatcher
|
||||||
|
|
||||||
path: "/etc/pam.d/login"
|
path: "/etc/pam.d/dankshell-u2f"
|
||||||
printErrors: false
|
printErrors: false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +77,7 @@ Scope {
|
|||||||
id: passwd
|
id: passwd
|
||||||
|
|
||||||
config: dankshellConfigWatcher.loaded ? "dankshell" : "login"
|
config: dankshellConfigWatcher.loaded ? "dankshell" : "login"
|
||||||
configDirectory: dankshellConfigWatcher.loaded || loginConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
|
configDirectory: dankshellConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
|
||||||
|
|
||||||
onMessageChanged: {
|
onMessageChanged: {
|
||||||
if (message.startsWith("The account is locked"))
|
if (message.startsWith("The account is locked"))
|
||||||
@@ -59,9 +96,8 @@ Scope {
|
|||||||
onCompleted: res => {
|
onCompleted: res => {
|
||||||
if (res === PamResult.Success) {
|
if (res === PamResult.Success) {
|
||||||
if (!root.unlockInProgress) {
|
if (!root.unlockInProgress) {
|
||||||
root.unlockInProgress = true;
|
|
||||||
fprint.abort();
|
fprint.abort();
|
||||||
root.unlockRequested();
|
root.proceedAfterPrimaryAuth();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -105,9 +141,8 @@ Scope {
|
|||||||
|
|
||||||
if (res === PamResult.Success) {
|
if (res === PamResult.Success) {
|
||||||
if (!root.unlockInProgress) {
|
if (!root.unlockInProgress) {
|
||||||
root.unlockInProgress = true;
|
|
||||||
passwd.abort();
|
passwd.abort();
|
||||||
root.unlockRequested();
|
root.proceedAfterPrimaryAuth();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -135,6 +170,74 @@ Scope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PamContext {
|
||||||
|
id: u2f
|
||||||
|
|
||||||
|
property bool available
|
||||||
|
|
||||||
|
function checkAvail(): void {
|
||||||
|
if (!available || !SettingsData.enableU2f || !root.lockSecured) {
|
||||||
|
abort();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SettingsData.u2fMode === "or") {
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startForSecondFactor(): void {
|
||||||
|
if (!available || !SettingsData.enableU2f) {
|
||||||
|
root.completeUnlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
abort();
|
||||||
|
root.u2fPending = true;
|
||||||
|
root.u2fState = "";
|
||||||
|
u2fPendingTimeout.restart();
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
|
||||||
|
config: u2fConfigWatcher.loaded ? "dankshell-u2f" : "u2f"
|
||||||
|
configDirectory: u2fConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
|
||||||
|
|
||||||
|
onMessageChanged: {
|
||||||
|
if (message.toLowerCase().includes("touch"))
|
||||||
|
root.u2fState = "waiting";
|
||||||
|
}
|
||||||
|
|
||||||
|
onCompleted: res => {
|
||||||
|
if (!available || root.unlockInProgress)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (res === PamResult.Success) {
|
||||||
|
root.completeUnlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res === PamResult.Error || res === PamResult.MaxTries || res === PamResult.Failed) {
|
||||||
|
abort();
|
||||||
|
|
||||||
|
if (root.u2fPending) {
|
||||||
|
if (root.u2fState === "waiting") {
|
||||||
|
// AND mode: device was found but auth failed → back to password
|
||||||
|
root.u2fPending = false;
|
||||||
|
root.u2fState = "";
|
||||||
|
fprint.checkAvail();
|
||||||
|
} else {
|
||||||
|
// AND mode: no device found → keep pending, show "Insert...", retry
|
||||||
|
root.u2fState = "insert";
|
||||||
|
u2fErrorRetry.restart();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// OR mode: prompt to insert key, silently retry
|
||||||
|
root.u2fState = "insert";
|
||||||
|
u2fErrorRetry.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: availProc
|
id: availProc
|
||||||
|
|
||||||
@@ -145,6 +248,16 @@ Scope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: u2fAvailProc
|
||||||
|
|
||||||
|
command: ["sh", "-c", "(test -f /usr/lib/security/pam_u2f.so || test -f /usr/lib64/security/pam_u2f.so) && (test -f /etc/pam.d/dankshell-u2f || test -f \"$HOME/.config/Yubico/u2f_keys\")"]
|
||||||
|
onExited: code => {
|
||||||
|
u2f.available = code === 0;
|
||||||
|
u2f.checkAvail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: errorRetry
|
id: errorRetry
|
||||||
|
|
||||||
@@ -152,6 +265,20 @@ Scope {
|
|||||||
onTriggered: fprint.start()
|
onTriggered: fprint.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: u2fErrorRetry
|
||||||
|
|
||||||
|
interval: 800
|
||||||
|
onTriggered: u2f.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: u2fPendingTimeout
|
||||||
|
|
||||||
|
interval: 30000
|
||||||
|
onTriggered: root.cancelU2fPending()
|
||||||
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: stateReset
|
id: stateReset
|
||||||
|
|
||||||
@@ -175,13 +302,22 @@ Scope {
|
|||||||
onLockSecuredChanged: {
|
onLockSecuredChanged: {
|
||||||
if (lockSecured) {
|
if (lockSecured) {
|
||||||
availProc.running = true;
|
availProc.running = true;
|
||||||
|
u2fAvailProc.running = true;
|
||||||
root.state = "";
|
root.state = "";
|
||||||
root.fprintState = "";
|
root.fprintState = "";
|
||||||
|
root.u2fState = "";
|
||||||
|
root.u2fPending = false;
|
||||||
root.lockMessage = "";
|
root.lockMessage = "";
|
||||||
root.unlockInProgress = false;
|
root.unlockInProgress = false;
|
||||||
} else {
|
} else {
|
||||||
fprint.abort();
|
fprint.abort();
|
||||||
passwd.abort();
|
passwd.abort();
|
||||||
|
u2f.abort();
|
||||||
|
errorRetry.running = false;
|
||||||
|
u2fErrorRetry.running = false;
|
||||||
|
u2fPendingTimeout.running = false;
|
||||||
|
root.u2fPending = false;
|
||||||
|
root.u2fState = "";
|
||||||
root.unlockInProgress = false;
|
root.unlockInProgress = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,5 +328,20 @@ Scope {
|
|||||||
function onEnableFprintChanged(): void {
|
function onEnableFprintChanged(): void {
|
||||||
fprint.checkAvail();
|
fprint.checkAvail();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onEnableU2fChanged(): void {
|
||||||
|
u2f.checkAvail();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onU2fModeChanged(): void {
|
||||||
|
if (root.lockSecured) {
|
||||||
|
u2f.abort();
|
||||||
|
u2fErrorRetry.running = false;
|
||||||
|
u2fPendingTimeout.running = false;
|
||||||
|
root.u2fPending = false;
|
||||||
|
root.u2fState = "";
|
||||||
|
u2f.checkAvail();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,11 +172,39 @@ Item {
|
|||||||
settingKey: "enableFprint"
|
settingKey: "enableFprint"
|
||||||
tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"]
|
tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"]
|
||||||
text: I18n.tr("Enable fingerprint authentication")
|
text: I18n.tr("Enable fingerprint authentication")
|
||||||
description: I18n.tr("Use fingerprint reader for lock screen authentication (requires enrolled fingerprints)")
|
description: SettingsData.fprintdAvailable ? I18n.tr("Use fingerprint reader for lock screen authentication (requires enrolled fingerprints)") : I18n.tr("Not enrolled", "fingerprint not detected status")
|
||||||
|
descriptionColor: SettingsData.fprintdAvailable ? Theme.surfaceVariantText : Theme.warning
|
||||||
checked: SettingsData.enableFprint
|
checked: SettingsData.enableFprint
|
||||||
visible: SettingsData.fprintdAvailable
|
enabled: SettingsData.fprintdAvailable
|
||||||
onToggled: checked => SettingsData.set("enableFprint", checked)
|
onToggled: checked => SettingsData.set("enableFprint", checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "enableU2f"
|
||||||
|
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "fido", "authentication", "hardware"]
|
||||||
|
text: I18n.tr("Enable security key authentication", "Enable FIDO2/U2F hardware security key for lock screen")
|
||||||
|
description: SettingsData.u2fAvailable ? I18n.tr("Use a FIDO2/U2F security key (e.g. YubiKey) for lock screen authentication (requires enrolled keys)", "lock screen U2F security key setting") : I18n.tr("Not enrolled", "security key not detected status")
|
||||||
|
descriptionColor: SettingsData.u2fAvailable ? Theme.surfaceVariantText : Theme.warning
|
||||||
|
checked: SettingsData.enableU2f
|
||||||
|
enabled: SettingsData.u2fAvailable
|
||||||
|
onToggled: checked => SettingsData.set("enableU2f", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsDropdownRow {
|
||||||
|
settingKey: "u2fMode"
|
||||||
|
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "mode", "factor", "second"]
|
||||||
|
text: I18n.tr("Security key mode", "lock screen U2F security key mode setting")
|
||||||
|
description: I18n.tr("'Alternative' lets the key unlock on its own. 'Second factor' requires password or fingerprint first, then the key.", "lock screen U2F security key mode setting")
|
||||||
|
visible: SettingsData.u2fAvailable && SettingsData.enableU2f
|
||||||
|
options: [I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method"), I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint")]
|
||||||
|
currentValue: SettingsData.u2fMode === "and" ? I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint") : I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method")
|
||||||
|
onValueChanged: value => {
|
||||||
|
if (value === I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint"))
|
||||||
|
SettingsData.set("u2fMode", "and");
|
||||||
|
else
|
||||||
|
SettingsData.set("u2fMode", "or");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
|
|||||||
3
quickshell/assets/pam/u2f
Normal file
3
quickshell/assets/pam/u2f
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#%PAM-1.0
|
||||||
|
|
||||||
|
auth required pam_u2f.so cue nouserok timeout=10
|
||||||
@@ -3771,6 +3771,48 @@
|
|||||||
],
|
],
|
||||||
"description": "Use fingerprint reader for lock screen authentication (requires enrolled fingerprints)"
|
"description": "Use fingerprint reader for lock screen authentication (requires enrolled fingerprints)"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": "enableU2f",
|
||||||
|
"label": "Enable security key authentication",
|
||||||
|
"tabIndex": 11,
|
||||||
|
"category": "Lock Screen",
|
||||||
|
"keywords": [
|
||||||
|
"authentication",
|
||||||
|
"enable",
|
||||||
|
"fido",
|
||||||
|
"hardware",
|
||||||
|
"key",
|
||||||
|
"lock",
|
||||||
|
"lockscreen",
|
||||||
|
"login",
|
||||||
|
"password",
|
||||||
|
"screen",
|
||||||
|
"security",
|
||||||
|
"u2f",
|
||||||
|
"yubikey"
|
||||||
|
],
|
||||||
|
"description": "Use a FIDO2/U2F security key (e.g. YubiKey) for lock screen authentication (requires enrolled keys)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "u2fMode",
|
||||||
|
"label": "Security key mode",
|
||||||
|
"tabIndex": 11,
|
||||||
|
"category": "Lock Screen",
|
||||||
|
"keywords": [
|
||||||
|
"alternative",
|
||||||
|
"authentication",
|
||||||
|
"factor",
|
||||||
|
"key",
|
||||||
|
"lock",
|
||||||
|
"lockscreen",
|
||||||
|
"mode",
|
||||||
|
"second",
|
||||||
|
"security",
|
||||||
|
"u2f",
|
||||||
|
"yubikey"
|
||||||
|
],
|
||||||
|
"description": "Alternative lets the key unlock on its own. Second factor requires password or fingerprint first, then the key."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": "loginctlLockIntegration",
|
"section": "loginctlLockIntegration",
|
||||||
"label": "Enable loginctl lock integration",
|
"label": "Enable loginctl lock integration",
|
||||||
|
|||||||
Reference in New Issue
Block a user