mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-30 15:07:49 -04:00
459 lines
16 KiB
QML
459 lines
16 KiB
QML
import QtQuick
|
|
import QtTest
|
|
import Quickshell.Io
|
|
import Quickshell.Services.Pam
|
|
import qs.Common
|
|
import qs.Services
|
|
import "../../Modules/Lock" as Lock
|
|
|
|
TestCase {
|
|
id: tests
|
|
name: "FingerprintLifecycle"
|
|
|
|
Component {
|
|
id: factory
|
|
Lock.Pam {}
|
|
}
|
|
|
|
function init() {
|
|
SettingsData.enableFprint = true;
|
|
SettingsData.enableU2f = false;
|
|
SettingsData.u2fMode = "and";
|
|
SettingsData.lockFingerprintReady = true;
|
|
SettingsData.lockPamExternallyManaged = false;
|
|
SettingsData.lockPamPath = "";
|
|
SettingsData.lockU2fPamPath = "";
|
|
SettingsData.lockPamInlineFprint = false;
|
|
SettingsData.lockPamInlineU2f = false;
|
|
SettingsData.maxFprintTries = 2;
|
|
AdminPolicy.disableCustomLockPam = false;
|
|
MockFiles.missing = [];
|
|
Log.warnings = [];
|
|
SessionService.preparingForSleep = false;
|
|
IdleService.monitorsOff = false;
|
|
}
|
|
|
|
function lockedPam() {
|
|
const pam = createTemporaryObject(factory, tests);
|
|
verify(pam !== null);
|
|
pam.lockSecured = true;
|
|
tryCompare(pam.fprint, "active", true);
|
|
compare(pam.fprint.starts, 1);
|
|
return pam;
|
|
}
|
|
|
|
function test_pendingRetryCancelled_data() {
|
|
return [
|
|
{
|
|
tag: "disabled",
|
|
setting: "enableFprint",
|
|
value: false
|
|
},
|
|
{
|
|
tag: "unavailable",
|
|
setting: "lockFingerprintReady",
|
|
value: false
|
|
},
|
|
{
|
|
tag: "managedPam",
|
|
setting: "lockPamExternallyManaged",
|
|
value: true
|
|
},
|
|
{
|
|
tag: "inlineFingerprint",
|
|
setting: "lockPamInlineFprint",
|
|
value: true
|
|
}
|
|
];
|
|
}
|
|
|
|
function test_pendingRetryCancelled(data) {
|
|
SettingsData.lockPamPath = "/test/custom";
|
|
const pam = lockedPam();
|
|
pam.fprint.finish(PamResult.Error);
|
|
verify(pam.fprint.retrying);
|
|
SettingsData[data.setting] = data.value;
|
|
verify(!pam.fprint.retrying);
|
|
verify(!pam.fprint.active);
|
|
compare(pam.fprint.starts, 1);
|
|
compare(pam.fprint.status, "disabled");
|
|
}
|
|
|
|
function test_pamStacks_data() {
|
|
const policy = {
|
|
policy: true,
|
|
missing: [],
|
|
passwd: "/etc/pam.d/dankshell",
|
|
fprint: "/etc/pam.d/dankshell-fprint",
|
|
u2f: "/etc/pam.d/dankshell-u2f",
|
|
fprintStatus: "active",
|
|
secondFactor: true,
|
|
warnings: 0
|
|
};
|
|
return [
|
|
{
|
|
tag: "settings",
|
|
policy: false,
|
|
missing: [],
|
|
passwd: "/test/custom",
|
|
fprint: "/test/assets/pam/fprint",
|
|
u2f: "/test/custom-u2f",
|
|
fprintStatus: "disabled",
|
|
secondFactor: false,
|
|
warnings: 0
|
|
},
|
|
Object.assign({}, policy, {
|
|
tag: "policy"
|
|
}),
|
|
Object.assign({}, policy, {
|
|
tag: "policyWithoutDankshell",
|
|
missing: ["/etc/pam.d/dankshell"],
|
|
passwd: "/etc/pam.d/login"
|
|
}),
|
|
Object.assign({}, policy, {
|
|
tag: "policyWithoutU2fStack",
|
|
missing: ["/etc/pam.d/dankshell-u2f"],
|
|
secondFactor: false,
|
|
warnings: 1
|
|
}),
|
|
Object.assign({}, policy, {
|
|
tag: "policyWithoutFprintStack",
|
|
missing: ["/etc/pam.d/dankshell-fprint"],
|
|
fprintStatus: "disabled"
|
|
})
|
|
];
|
|
}
|
|
|
|
function test_pamStacks(data) {
|
|
AdminPolicy.disableCustomLockPam = data.policy;
|
|
MockFiles.missing = data.missing;
|
|
SettingsData.enableU2f = true;
|
|
SettingsData.lockPamPath = "/test/custom";
|
|
SettingsData.lockU2fPamPath = "/test/custom-u2f";
|
|
SettingsData.lockPamInlineFprint = true;
|
|
SettingsData.lockPamInlineU2f = true;
|
|
SettingsData.lockPamExternallyManaged = true;
|
|
const pam = createTemporaryObject(factory, tests);
|
|
pam.lockSecured = true;
|
|
compare(pam.passwd.configDirectory + "/" + pam.passwd.config, data.passwd);
|
|
compare(pam.fprint.configDirectory + "/" + pam.fprint.config, data.fprint);
|
|
compare(pam.u2f.configDirectory + "/" + pam.u2f.config, data.u2f);
|
|
tryCompare(pam.fprint, "status", data.fprintStatus);
|
|
pam.passwd.start();
|
|
pam.passwd.finish(PamResult.Success);
|
|
compare(pam.u2fPending, data.secondFactor);
|
|
compare(pam.unlockInProgress, !data.secondFactor);
|
|
compare(Log.warnings.length, data.warnings);
|
|
}
|
|
|
|
function test_disabledContextIgnoresLateSuccess() {
|
|
const pam = lockedPam();
|
|
SettingsData.enableFprint = false;
|
|
pam.fprint.finish(PamResult.Success);
|
|
verify(!pam.unlockInProgress);
|
|
verify(!pam.u2fPending);
|
|
}
|
|
|
|
function test_unlockCancelsPendingRetry() {
|
|
const pam = lockedPam();
|
|
pam.fprint.finish(PamResult.Error);
|
|
pam.lockSecured = false;
|
|
verify(!pam.fprint.retrying);
|
|
compare(pam.fprint.starts, 1);
|
|
}
|
|
|
|
function test_passwordSuccessCancelsRetryBeforeSecondFactor() {
|
|
const pam = lockedPam();
|
|
SettingsData.enableU2f = true;
|
|
pam.fprint.finish(PamResult.Error);
|
|
pam.passwd.start();
|
|
pam.passwd.finish(PamResult.Success);
|
|
verify(pam.u2fPending);
|
|
verify(pam.u2f.active);
|
|
verify(!pam.fprint.retrying);
|
|
compare(pam.fprint.status, "paused");
|
|
compare(pam.fprint.starts, 1);
|
|
}
|
|
|
|
function test_passwordRemainsUsableWhileFingerprintActive() {
|
|
const pam = lockedPam();
|
|
pam.passwd.start();
|
|
verify(pam.fprint.active);
|
|
pam.passwd.finish(PamResult.Success);
|
|
verify(pam.unlockInProgress);
|
|
verify(!pam.fprint.active);
|
|
verify(!pam.fprint.retrying);
|
|
}
|
|
|
|
function test_u2fCancellationResumesWithoutResettingBadScans() {
|
|
const pam = lockedPam();
|
|
SettingsData.enableU2f = true;
|
|
pam.fprint.finish(PamResult.MaxTries);
|
|
compare(pam.fprint.tries, 1);
|
|
pam.u2f.startForSecondFactor();
|
|
verify(!pam.fprint.retrying);
|
|
pam.cancelU2fPending();
|
|
compare(pam.fprint.tries, 1);
|
|
verify(pam.fprint.active);
|
|
}
|
|
|
|
function exhaustBadScans(pam) {
|
|
pam.fprint.finish(PamResult.MaxTries);
|
|
verify(pam.fprint.active);
|
|
pam.fprint.finish(PamResult.MaxTries);
|
|
compare(pam.fprint.tries, SettingsData.maxFprintTries);
|
|
compare(pam.fprint.status, "max");
|
|
verify(!pam.fprint.active);
|
|
}
|
|
|
|
function test_badScanLimitSurvivesU2fCancelAndRecovery() {
|
|
const pam = lockedPam();
|
|
exhaustBadScans(pam);
|
|
SettingsData.enableU2f = true;
|
|
pam.u2f.startForSecondFactor();
|
|
pam.cancelU2fPending();
|
|
SettingsData.enableFprint = false;
|
|
SettingsData.enableFprint = true;
|
|
SessionService.lidOpened();
|
|
SessionService.sessionResumed();
|
|
pam.retryFprintOnActivity();
|
|
compare(pam.fprint.tries, SettingsData.maxFprintTries);
|
|
compare(pam.fprint.starts, 2);
|
|
compare(pam.fprint.status, "max");
|
|
}
|
|
|
|
// max-tries=1 in the PAM profile makes one finished session equal one wrong
|
|
// finger, so the setting is spent scan for scan and each scan costs a fresh
|
|
// PAM session and device claim.
|
|
function test_wrongScanBudgetIsSpentScanForScan() {
|
|
SettingsData.maxFprintTries = 5;
|
|
const pam = lockedPam();
|
|
for (let i = 1; i < SettingsData.maxFprintTries; i++) {
|
|
pam.fprint.finish(PamResult.MaxTries);
|
|
compare(pam.fprint.tries, i);
|
|
compare(pam.fprint.starts, i + 1);
|
|
compare(pam.fprint.status, "active");
|
|
}
|
|
pam.fprint.finish(PamResult.MaxTries);
|
|
compare(pam.fprint.tries, SettingsData.maxFprintTries);
|
|
compare(pam.fprint.starts, SettingsData.maxFprintTries);
|
|
compare(pam.fprint.status, "max");
|
|
verify(!pam.fprint.active);
|
|
}
|
|
|
|
function test_newLockResetsBadScanLimit() {
|
|
const pam = lockedPam();
|
|
exhaustBadScans(pam);
|
|
pam.lockSecured = false;
|
|
pam.lockSecured = true;
|
|
tryCompare(pam.fprint, "active", true);
|
|
compare(pam.fprint.tries, 0);
|
|
compare(pam.fprint.errorTries, 0);
|
|
}
|
|
|
|
function test_failedPamResultCountsBadScan() {
|
|
const pam = lockedPam();
|
|
pam.fprint.finish(PamResult.Failed);
|
|
compare(pam.fprint.tries, 1);
|
|
compare(pam.fprint.errorTries, 0);
|
|
compare(pam.fprintState, "fail");
|
|
verify(!pam.fprint.retrying);
|
|
compare(pam.fprint.starts, 2);
|
|
compare(pam.fprint.status, "active");
|
|
}
|
|
|
|
function test_settledSessionExpiryRenewsWithoutError() {
|
|
const pam = lockedPam();
|
|
pam.fprint.finish(PamResult.MaxTries);
|
|
compare(pam.fprint.tries, 1);
|
|
pam.fprint.deliverMessage();
|
|
pam.fprint.attemptStartedAt = Date.now() - (pam.fprint.sessionTimeoutMs + 1000);
|
|
pam.fprint.finish(PamResult.Error);
|
|
compare(pam.fprint.errorTries, 0);
|
|
compare(pam.fprint.tries, 1);
|
|
compare(pam.fprintState, "fail");
|
|
verify(!pam.fprint.retrying);
|
|
compare(pam.fprint.starts, 3);
|
|
compare(pam.fprint.status, "active");
|
|
}
|
|
|
|
function test_earlyErrorStillSpendsErrorBudget() {
|
|
const pam = lockedPam();
|
|
verify(!pam.fprint.attemptSettled());
|
|
pam.fprint.finish(PamResult.Error);
|
|
compare(pam.fprint.errorTries, 1);
|
|
compare(pam.fprintState, "error");
|
|
verify(pam.fprint.retrying);
|
|
compare(pam.fprint.starts, 1);
|
|
}
|
|
|
|
function test_errorLimitRecovery_data() {
|
|
return [
|
|
{
|
|
tag: "lid"
|
|
},
|
|
{
|
|
tag: "resume"
|
|
},
|
|
{
|
|
tag: "activity"
|
|
}
|
|
];
|
|
}
|
|
|
|
function test_errorLimitRecovery(data) {
|
|
const pam = lockedPam();
|
|
pam.fprint.errorTries = pam.fprint.maxErrorTries - 1;
|
|
pam.fprint.finish(PamResult.Error);
|
|
compare(pam.fprint.status, "stopped");
|
|
verify(!pam.fprint.retrying);
|
|
if (data.tag === "lid")
|
|
SessionService.lidOpened();
|
|
else if (data.tag === "resume")
|
|
SessionService.sessionResumed();
|
|
else
|
|
pam.retryFprintOnActivity();
|
|
compare(pam.fprint.errorTries, 0);
|
|
compare(pam.fprint.starts, 2);
|
|
compare(pam.fprint.status, "active");
|
|
compare(pam.fprintState, "");
|
|
}
|
|
|
|
function test_backoffIsCutShortByLidOpen() {
|
|
const pam = lockedPam();
|
|
pam.fprint.errorTries = 5;
|
|
pam.fprint.finish(PamResult.Error);
|
|
verify(pam.fprint.retrying);
|
|
SessionService.lidOpened();
|
|
compare(pam.fprint.errorTries, 0);
|
|
compare(pam.fprint.starts, 2);
|
|
compare(pam.fprint.status, "active");
|
|
}
|
|
|
|
// A verify still running when the machine suspends leaves fprintd holding
|
|
// the device, so the reader must not stay claimed once the screen is off.
|
|
function test_blankScreenReleasesReaderAndWakeRearmsIt() {
|
|
const pam = lockedPam();
|
|
IdleService.monitorsOff = true;
|
|
verify(!pam.fprint.active);
|
|
verify(!pam.fprint.retrying);
|
|
compare(pam.fprint.status, "paused");
|
|
compare(pam.fprint.starts, 1);
|
|
IdleService.monitorsOff = false;
|
|
tryCompare(pam.fprint, "starts", 2);
|
|
compare(pam.fprint.status, "active");
|
|
}
|
|
|
|
function test_blankScreenCancelsPendingRetry() {
|
|
const pam = lockedPam();
|
|
pam.fprint.finish(PamResult.Error);
|
|
verify(pam.fprint.retrying);
|
|
IdleService.monitorsOff = true;
|
|
verify(!pam.fprint.retrying);
|
|
compare(pam.fprint.starts, 1);
|
|
}
|
|
|
|
function test_suspendStopsScanAndResumeRestartsIt() {
|
|
const pam = lockedPam();
|
|
SessionService.preparingForSleep = true;
|
|
verify(!pam.fprint.active);
|
|
verify(!pam.fprint.retrying);
|
|
compare(pam.fprint.status, "paused");
|
|
SessionService.preparingForSleep = false;
|
|
SessionService.sessionResumed();
|
|
compare(pam.fprint.starts, 2);
|
|
compare(pam.fprint.status, "active");
|
|
}
|
|
|
|
function test_failedStartUsesBackoff() {
|
|
const pam = lockedPam();
|
|
pam.fprint.stop();
|
|
pam.fprint.startSucceeds = false;
|
|
pam.fprint.checkAvail();
|
|
compare(pam.fprint.errorTries, 1);
|
|
verify(pam.fprint.retrying);
|
|
compare(pam.fprintState, "error");
|
|
}
|
|
|
|
function test_readinessRecoversExhaustedDeviceErrors_data() {
|
|
return [{tag: "enable", setting: "enableFprint"}, {tag: "device", setting: "lockFingerprintReady"}];
|
|
}
|
|
|
|
function test_readinessRecoversExhaustedDeviceErrors(data) {
|
|
const pam = lockedPam();
|
|
pam.fprint.errorTries = pam.fprint.maxErrorTries - 1;
|
|
pam.fprint.finish(PamResult.Error);
|
|
compare(pam.fprint.status, "stopped");
|
|
SettingsData[data.setting] = false;
|
|
SettingsData[data.setting] = true;
|
|
compare(pam.fprint.errorTries, 0);
|
|
compare(pam.fprint.starts, 2);
|
|
}
|
|
|
|
function test_activityPreservesBackoffAfterRepeatedErrors() {
|
|
const pam = lockedPam();
|
|
pam.fprint.errorTries = 1;
|
|
pam.fprint.finish(PamResult.Error);
|
|
compare(pam.fprint.errorTries, 2);
|
|
compare(pam.fprint.retryInterval, pam.fprint.daemonIdleExitMs + 5000);
|
|
pam.retryFprintOnActivity();
|
|
compare(pam.fprint.errorTries, 2);
|
|
verify(pam.fprint.retrying);
|
|
compare(pam.fprint.starts, 1);
|
|
}
|
|
|
|
function test_synchronousStartFailureSchedulesOneRetry() {
|
|
const pam = lockedPam();
|
|
pam.fprint.stop();
|
|
pam.fprint.startFailsSynchronously = true;
|
|
pam.fprint.checkAvail();
|
|
compare(pam.fprint.errorTries, 1);
|
|
verify(pam.fprint.retrying);
|
|
compare(pam.fprint.starts, 1);
|
|
}
|
|
|
|
// pam_fprintd's timeout runs from the verify, so a claim that drags on must
|
|
// not age a fault into looking like an expiry.
|
|
function test_sessionAgeIsMeasuredFromTheVerifyNotTheClaim() {
|
|
const pam = lockedPam();
|
|
compare(pam.fprint.attemptStartedAt, 0);
|
|
verify(!pam.fprint.attemptSettled());
|
|
pam.fprint.deliverMessage();
|
|
verify(pam.fprint.attemptStartedAt > 0);
|
|
}
|
|
|
|
// VerifyStart is asynchronous: pam_fprintd arms its deadline before the call
|
|
// and the message lands afterwards, so an ordinary expiry reads a little short
|
|
// of the timeout here and must still renew rather than back off.
|
|
function test_expiryShortenedByVerifyStartStillRenews() {
|
|
const pam = lockedPam();
|
|
pam.fprint.deliverMessage();
|
|
pam.fprint.attemptStartedAt = Date.now() - (pam.fprint.sessionTimeoutMs - 900);
|
|
pam.fprint.finish(PamResult.Error);
|
|
compare(pam.fprint.errorTries, 0);
|
|
compare(pam.fprint.starts, 2);
|
|
compare(pam.fprintState, "");
|
|
}
|
|
|
|
function test_faultJustBeforeTimeoutStillSpendsErrorBudget() {
|
|
const pam = lockedPam();
|
|
pam.fprint.deliverMessage();
|
|
pam.fprint.attemptStartedAt = Date.now() - (pam.fprint.sessionTimeoutMs - pam.fprint.verifyStartSlackMs - 1000);
|
|
pam.fprint.finish(PamResult.Error);
|
|
compare(pam.fprint.errorTries, 1);
|
|
verify(pam.fprint.retrying);
|
|
compare(pam.fprint.starts, 1);
|
|
}
|
|
|
|
function test_synchronousStartFailureAtErrorLimitCountsOnce() {
|
|
const pam = lockedPam();
|
|
pam.fprint.stop();
|
|
pam.fprint.errorTries = pam.fprint.maxErrorTries - 1;
|
|
pam.fprint.startFailsSynchronously = true;
|
|
pam.fprint.checkAvail();
|
|
compare(pam.fprint.errorTries, pam.fprint.maxErrorTries);
|
|
compare(pam.fprint.status, "stopped");
|
|
verify(!pam.fprint.retrying);
|
|
}
|
|
}
|