#include "pamauthenticators.h" #include #include #include #include #include #include #include // Test-only PAM implementation. No real authentication or system files are used. struct pam_handle { QByteArray service; pam_conv conversation; int attempts = 0; }; int pam_start(const char *service, const char *, const pam_conv *conversation, pam_handle_t **handle) { *handle = new pam_handle{service, *conversation}; return PAM_SUCCESS; } int pam_end(pam_handle_t *handle, int) { delete handle; return PAM_SUCCESS; } int pam_set_item(pam_handle_t *, int, const void *) { return PAM_SUCCESS; } int pam_setcred(pam_handle_t *, int) { return PAM_SUCCESS; } const char *pam_strerror(pam_handle_t *, int) { return "simulated PAM result"; } int pam_authenticate(pam_handle_t *handle, int) { ++handle->attempts; if (handle->service == "missing") { return PAM_MODULE_UNKNOWN; } if (handle->service == "transient" && handle->attempts == 1) { return PAM_AUTHINFO_UNAVAIL; } if (handle->service == "attempt-limit" && handle->attempts == 1) { return PAM_MAXTRIES; } if (handle->service == "idle" && handle->attempts == 1) { pam_message message{PAM_TEXT_INFO, "Test fingerprint instruction"}; const pam_message *messages = &message; pam_response *response = nullptr; const auto &conversation = handle->conversation; const int result = conversation.conv(1, &messages, &response, conversation.appdata_ptr); if (response) { std::free(response->resp); std::free(response); } return result == PAM_SUCCESS ? PAM_AUTHINFO_UNAVAIL : result; } pam_message message{PAM_PROMPT_ECHO_OFF, "Test credential:"}; const pam_message *messages = &message; pam_response *response = nullptr; const auto &conversation = handle->conversation; const int result = conversation.conv(1, &messages, &response, conversation.appdata_ptr); if (result != PAM_SUCCESS) { return result; } const bool accepted = response && response->resp && std::strcmp(response->resp, "correct") == 0; if (response) { std::free(response->resp); std::free(response); } return accepted ? PAM_SUCCESS : PAM_AUTH_ERR; } class RetryTest : public QObject { Q_OBJECT private Q_SLOTS: void idleFingerprintRetriesThroughQml_data() { QTest::addColumn("automaticRetry"); QTest::newRow("automatic retry") << true; QTest::newRow("without the new retry") << false; } void idleFingerprintRetriesThroughQml() { QFETCH(bool, automaticRetry); qmlRegisterUncreatableType("org.kde.kscreenlocker", 1, 0, "Authenticator", "Test instance only"); auto password = std::make_unique(QStringLiteral("password"), QStringLiteral("test")); auto fingerprint = std::make_unique(QStringLiteral("idle"), QStringLiteral("test"), PamAuthenticator::Fingerprint); QSignalSpy passwordPrompts(password.get(), &PamAuthenticator::promptForSecret); QSignalSpy fingerprintPrompts(fingerprint.get(), &PamAuthenticator::promptForSecret); std::vector> others; others.push_back(std::move(fingerprint)); PamAuthenticators authenticators(std::move(password), std::move(others)); QSignalSpy successes(&authenticators, &PamAuthenticators::succeeded); QQmlEngine engine; QQmlComponent component(&engine, QUrl::fromLocalFile(QStringLiteral(RETRY_QML))); QVERIFY2(component.isReady(), qPrintable(component.errorString())); std::unique_ptr retry(component.createWithInitialProperties({ {QStringLiteral("auth"), QVariant::fromValue(&authenticators)}, {QStringLiteral("fingerprintType"), int(PamAuthenticator::Fingerprint)}, {QStringLiteral("interval"), 50}, {QStringLiteral("enabled"), automaticRetry}, })); QVERIFY2(retry, qPrintable(component.errorString())); authenticators.startAuthenticating(); QTRY_COMPARE(passwordPrompts.count(), 1); if (!automaticRetry) { QTest::qWait(200); QCOMPARE(fingerprintPrompts.count(), 0); // Reproduce today's recovery via Ctrl without any daemon restart. authenticators.startAuthenticating(); } // With the helper enabled, no simulated input or manual start is needed. QTRY_COMPARE_WITH_TIMEOUT(fingerprintPrompts.count(), 1, 2000); QCOMPARE(passwordPrompts.count(), 1); QVERIFY(!authenticators.isUnlocked()); QCOMPARE(successes.count(), 0); authenticators.respond("correct"); QTRY_COMPARE(successes.count(), 1); QVERIFY(authenticators.isUnlocked()); } void retryWithActivePassword_data() { QTest::addColumn("service"); QTest::newRow("temporary unavailability") << QStringLiteral("transient"); QTest::newRow("exhausted attempts, manual retry") << QStringLiteral("attempt-limit"); } void retryWithActivePassword() { QFETCH(QString, service); auto password = std::make_unique(QStringLiteral("password"), QStringLiteral("test")); auto fingerprint = std::make_unique(service, QStringLiteral("test"), PamAuthenticator::Fingerprint); auto *fingerprintPtr = fingerprint.get(); QSignalSpy passwordPrompts(password.get(), &PamAuthenticator::promptForSecret); QSignalSpy fingerprintPrompts(fingerprintPtr, &PamAuthenticator::promptForSecret); QSignalSpy fingerprintFailures(fingerprintPtr, &PamAuthenticator::failed); std::vector> others; others.push_back(std::move(fingerprint)); PamAuthenticators authenticators(std::move(password), std::move(others)); QSignalSpy successes(&authenticators, &PamAuthenticators::succeeded); authenticators.startAuthenticating(); QTRY_COMPARE(passwordPrompts.count(), 1); QTRY_COMPARE_WITH_TIMEOUT(fingerprintFailures.count(), 1, 2000); QTRY_VERIFY(!fingerprintPtr->isAvailable()); QCOMPARE(authenticators.state(), PamAuthenticators::Authenticating); QVERIFY(!authenticators.isUnlocked()); authenticators.startAuthenticating(); QTRY_COMPARE_WITH_TIMEOUT(fingerprintPrompts.count(), 1, 2000); QTRY_VERIFY(fingerprintPtr->isAvailable()); // Repeated starts must not duplicate either active conversation. authenticators.startAuthenticating(); QTest::qWait(100); QCOMPARE(passwordPrompts.count(), 1); QCOMPARE(fingerprintPrompts.count(), 1); QCOMPARE(successes.count(), 0); // The original password conversation still accepts a response. authenticators.respond("correct"); QTRY_COMPARE(successes.count(), 1); QVERIFY(authenticators.isUnlocked()); } void permanentUnavailability_data() { QTest::addColumn("service"); QTest::addColumn("type"); QTest::newRow("absent smartcard") << QStringLiteral("transient") << int(PamAuthenticator::Smartcard); QTest::newRow("missing fingerprint module") << QStringLiteral("missing") << int(PamAuthenticator::Fingerprint); } void permanentUnavailability() { QFETCH(QString, service); QFETCH(int, type); PamAuthenticator auth(service, QStringLiteral("test"), PamAuthenticator::NoninteractiveAuthenticatorType(type)); QSignalSpy availability(&auth, &PamAuthenticator::availableChanged); QSignalSpy prompts(&auth, &PamAuthenticator::promptForSecret); auth.tryUnlock(); QTRY_VERIFY(availability.count() >= 3); QVERIFY(!auth.isAvailable()); availability.clear(); auth.tryUnlock(); QTest::qWait(100); QCOMPARE(availability.count(), 0); QCOMPARE(prompts.count(), 0); QVERIFY(!auth.isUnlocked()); } void graceAndIncorrectPassword() { auto password = std::make_unique(QStringLiteral("password"), QStringLiteral("test")); PamAuthenticators authenticators(std::move(password), {}); QSignalSpy prompts(&authenticators, &PamAuthenticators::promptForSecretChanged); QSignalSpy failures(&authenticators, &PamAuthenticators::failed); QSignalSpy successes(&authenticators, &PamAuthenticators::succeeded); authenticators.setGraceLocked(true); authenticators.startAuthenticating(); QTest::qWait(100); QCOMPARE(prompts.count(), 0); authenticators.setGraceLocked(false); authenticators.startAuthenticating(); QTRY_COMPARE(prompts.count(), 1); authenticators.respond("incorrect"); QTRY_COMPARE(failures.count(), 1); QCOMPARE(successes.count(), 0); QVERIFY(!authenticators.isUnlocked()); authenticators.startAuthenticating(); QTRY_COMPARE(prompts.count(), 2); authenticators.respond("correct"); QTRY_COMPARE(successes.count(), 1); } }; QTEST_GUILESS_MAIN(RetryTest) #include "retrytest.moc"