Files

224 lines
9.2 KiB
C++

#include "pamauthenticators.h"
#include <QSignalSpy>
#include <QTest>
#include <QQmlComponent>
#include <QQmlEngine>
#include <security/pam_appl.h>
#include <cstdlib>
#include <cstring>
// 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<bool>("automaticRetry");
QTest::newRow("automatic retry") << true;
QTest::newRow("without the new retry") << false;
}
void idleFingerprintRetriesThroughQml()
{
QFETCH(bool, automaticRetry);
qmlRegisterUncreatableType<PamAuthenticator>("org.kde.kscreenlocker", 1, 0, "Authenticator", "Test instance only");
auto password = std::make_unique<PamAuthenticator>(QStringLiteral("password"), QStringLiteral("test"));
auto fingerprint = std::make_unique<PamAuthenticator>(QStringLiteral("idle"), QStringLiteral("test"), PamAuthenticator::Fingerprint);
QSignalSpy passwordPrompts(password.get(), &PamAuthenticator::promptForSecret);
QSignalSpy fingerprintPrompts(fingerprint.get(), &PamAuthenticator::promptForSecret);
std::vector<std::unique_ptr<PamAuthenticator>> 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<QObject> 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<QString>("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<PamAuthenticator>(QStringLiteral("password"), QStringLiteral("test"));
auto fingerprint = std::make_unique<PamAuthenticator>(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<std::unique_ptr<PamAuthenticator>> 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<QString>("service");
QTest::addColumn<int>("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<PamAuthenticator>(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"