Commit 132adacf authored by David Edmundson's avatar David Edmundson
Browse files

Cleanup kcheckpass

kcheckpass existed because historically we needed to be root to check
passwords. This hasn't been true for tens of years. There are no
security benefits, all authentication is based on the exit status of the
greeter application.

This patch drops kcheckpass and brings everything in process, but in
another thread.

This also reports back more fine grained PAM control at the same time
(following on from !29) forwarding all prompts and messages.

The unit test has been replaced with one that actually checks against a
real PAM using pam_wrapper to force a fake user.
parent 6493ccdb
Pipeline #172207 passed with stage
in 3 minutes and 12 seconds
......@@ -41,20 +41,6 @@ add_feature_info("prctl/procctl tracing control"
CAN_DISABLE_PTRACE
"Required for disallowing ptrace on greeter and kcheckpass process")
check_include_file("sys/signalfd.h" HAVE_SIGNALFD_H)
if (NOT HAVE_SIGNALFD_H)
check_include_files("sys/types.h;sys/event.h" HAVE_EVENT_H)
endif ()
if (NOT (HAVE_SIGNALFD_H OR HAVE_EVENT_H))
message(FATAL_ERROR "kcheckpass either needs signalfd() or kevent()&sigtimedwait() to work")
endif ()
add_feature_info("sys/signalfd.h"
HAVE_SIGNALFD_H
"Use the signalfd() api for signalhandling")
add_feature_info("sys/event.h"
HAVE_EVENT_H
"Use the kevent() and sigwaitinfo() api for signalhandling")
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS DBus Widgets Quick Test)
find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS
Crash
......@@ -148,7 +134,6 @@ set(KSLD_INCLUDEDIR "${KDE_INSTALL_INCLUDEDIR}/KScreenLocker")
include(ECMQtDeclareLoggingCategory)
add_subdirectory(kcheckpass)
add_subdirectory(greeter)
add_subdirectory(kcm)
add_subdirectory(settings)
......
......@@ -15,6 +15,21 @@ target_link_libraries(logindTest Qt::DBus Qt::Test KF5::I18n)
add_test(NAME ksmserver-logindTest COMMAND logindTest)
ecm_mark_as_test(logindTest)
#######################################
# PamTest
#######################################
pkg_search_module(pam_wrapper OPTIONAL)
if (pam_wrapper_FOUND)
add_executable(pamTest pamtest.cpp)
target_link_libraries(pamTest Qt::Test kscreenlocker_authenticator)
add_test(NAME ksmserver-pamTest COMMAND pamTest)
ecm_mark_as_test(pamTest)
set_property(TEST ksmserver-pamTest
PROPERTY
ENVIRONMENT LD_PRELOAD=libpam_wrapper.so)
endif()
#######################################
# KSldTest
#######################################
......
test_user:my_password:test_service
auth required /usr/lib/pam_wrapper/pam_matrix.so verbose
account required /usr/lib/pam_wrapper/pam_matrix.so verbose
password required /usr/lib/pam_wrapper/pam_matrix.so verbose
session required /usr/lib/pam_wrapper/pam_matrix.so verbose
......@@ -2,7 +2,7 @@
KSld - the KDE Screenlocker Daemon
This file is part of the KDE project.
Copyright (C) 2014 Martin Gräßlin <mgraesslin@kde.org>
Copyright (C) 2022 David Edmundson <davidedmundson@kde.org>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
......@@ -17,95 +17,60 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*********************************************************************/
#ifndef AUTHENTICATOR_H
#define AUTHENTICATOR_H
#include <QObject>
#include <QSignalSpy>
#include <QtTest>
class QSocketNotifier;
class QTimer;
class KCheckPass;
#include "../greeter/pamauthenticator.h"
enum class AuthenticationMode {
Delayed,
Direct,
};
// This test runs under the expectation that
// we are run with LD_PRELOAD=libpam_wrapper.so pamTest
class Authenticator : public QObject
class PamTest : public QObject
{
Q_OBJECT
Q_PROPERTY(bool graceLocked READ isGraceLocked NOTIFY graceLockedChanged)
public:
explicit Authenticator(AuthenticationMode mode = AuthenticationMode::Direct, QObject *parent = nullptr);
~Authenticator() override;
bool isGraceLocked() const;
public Q_SLOTS:
void tryUnlock(const QString &password);
Q_SIGNALS:
void failed();
void succeeded();
void graceLockedChanged();
void message(const QString &msg); // don't remove the "msg" param, used in QML!!!
void error(const QString &err); // don't remove the "err" param, used in QML!!!
private:
void setupCheckPass();
QTimer *m_graceLockTimer;
KCheckPass *m_checkPass;
PamTest();
private Q_SLOTS:
void testLogin();
};
class KCheckPass : public QObject
PamTest::PamTest()
{
Q_OBJECT
public:
explicit KCheckPass(AuthenticationMode mode, QObject *parent = nullptr);
~KCheckPass() override;
void start();
bool isReady() const
{
return m_ready;
}
void setPassword(const QString &password)
{
m_password = password;
if (!qgetenv("LD_PRELOAD").contains("libpam_wrapper.so")) {
qFatal("This test must be run with pam_wrapper. See ctest");
}
void startAuth();
Q_SIGNALS:
void failed();
void succeeded();
void message(const QString &);
void error(const QString &);
qputenv("PAM_WRAPPER", "1");
qputenv("PAM_WRAPPER_DEBUGLEVEL", "2"); // DEBUG level
qputenv("PAM_WRAPPER_SERVICE_DIR", QFINDTESTDATA("data").toUtf8());
qputenv("PAM_MATRIX_PASSWD", QFINDTESTDATA("data/test_db").toUtf8());
}
private Q_SLOTS:
void handleVerify();
private:
void cantCheck();
void reapVerify();
// kcheckpass interface
int Reader(void *buf, int count);
bool GRead(void *buf, int count);
bool GWrite(const void *buf, int count);
bool GSendInt(int val);
bool GSendStr(const char *buf);
bool GSendArr(int len, const char *buf);
bool GRecvInt(int *val);
bool GRecvArr(char **buf);
QString m_password;
QSocketNotifier *m_notifier;
int m_pid;
int m_fd;
bool m_ready = false;
AuthenticationMode m_mode;
};
#endif
void PamTest::testLogin()
{
PamAuthenticator auth("test_service", "test_user");
QSignalSpy promptSpy(&auth, &PamAuthenticator::prompt);
QSignalSpy promptForSecretSpy(&auth, &PamAuthenticator::promptForSecret);
QSignalSpy succeededSpy(&auth, &PamAuthenticator::succeeded);
QSignalSpy failedSpy(&auth, &PamAuthenticator::failed);
// invalid password
auth.tryUnlock();
QVERIFY(promptForSecretSpy.wait());
auth.respond("not_my_password");
QVERIFY(failedSpy.wait());
QVERIFY(promptSpy.count() == 0);
QVERIFY(succeededSpy.count() == 0);
// try again, with the right password
auth.tryUnlock();
QVERIFY(promptForSecretSpy.wait());
auth.respond("my_password");
QVERIFY(succeededSpy.wait());
}
QTEST_MAIN(PamTest)
#include "pamtest.moc"
......@@ -3,19 +3,14 @@ add_definitions(-DTRANSLATION_DOMAIN=\"kscreenlocker_greet\")
include_directories(
${CMAKE_CURRENT_BINARY_DIR}
../kcheckpass
${CMAKE_CURRENT_BINARY_DIR}/../
)
set(kscreenlocker_greet_SRCS
authenticator.cpp
greeterapp.cpp
main.cpp
powermanagement.cpp
noaccessnetworkaccessmanagerfactory.cpp
set(kscreenlocker_authenticator_SRCS
pamauthenticator.cpp
)
ecm_qt_declare_logging_category(kscreenlocker_greet_SRCS
ecm_qt_declare_logging_category(kscreenlocker_authenticator_SRCS
HEADER
kscreenlocker_greet_logging.h
IDENTIFIER
......@@ -26,8 +21,20 @@ ecm_qt_declare_logging_category(kscreenlocker_greet_SRCS
Critical
)
qt5_add_resources(kscreenlocker_greet_SRCS fallbacktheme.qrc)
set(kscreenlocker_greet_SRCS
greeterapp.cpp
main.cpp
powermanagement.cpp
noaccessnetworkaccessmanagerfactory.cpp
)
add_library(kscreenlocker_authenticator OBJECT ${kscreenlocker_authenticator_SRCS})
target_link_libraries(kscreenlocker_authenticator
Qt::Core
${PAM_LIBRARIES}
)
qt5_add_resources(kscreenlocker_greet_SRCS fallbacktheme.qrc)
ecm_add_wayland_client_protocol(kscreenlocker_greet_SRCS
PROTOCOL ../protocols/ksld.xml
......@@ -38,6 +45,7 @@ add_executable(kscreenlocker_greet ${kscreenlocker_greet_SRCS})
target_link_libraries(kscreenlocker_greet
settings
kscreenlocker_authenticator
KF5::Package
KF5::Crash
KF5::I18n
......
/********************************************************************
KSld - the KDE Screenlocker Daemon
This file is part of the KDE project.
Copyright (C) 1999 Martin R. Jones <mjones@kde.org>
Copyright (C) 2002 Luboš Luňák <l.lunak@kde.org>
Copyright (C) 2003 Oswald Buddenhagen <ossi@kde.org>
Copyright (C) 2014 Martin Gräßlin <mgraesslin@kde.org>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*********************************************************************/
#include "authenticator.h"
#include <config-kscreenlocker.h>
#include <kcheckpass-enums.h>
// KF
#include <KLibexec>
// Qt
#include <QCoreApplication>
#include <QFile>
#include <QSocketNotifier>
#include <QTimer>
// system
#include <errno.h>
#include <signal.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
Authenticator::Authenticator(AuthenticationMode mode, QObject *parent)
: QObject(parent)
, m_graceLockTimer(new QTimer(this))
, m_checkPass(nullptr)
{
m_graceLockTimer->setSingleShot(true);
m_graceLockTimer->setInterval(3000);
connect(m_graceLockTimer, &QTimer::timeout, this, &Authenticator::graceLockedChanged);
if (mode == AuthenticationMode::Delayed) {
m_checkPass = new KCheckPass(AuthenticationMode::Delayed, this);
setupCheckPass();
}
}
Authenticator::~Authenticator() = default;
void Authenticator::tryUnlock(const QString &password)
{
if (isGraceLocked()) {
Q_EMIT failed();
return;
}
m_graceLockTimer->start();
Q_EMIT graceLockedChanged();
if (!m_checkPass) {
m_checkPass = new KCheckPass(AuthenticationMode::Direct, this);
m_checkPass->setPassword(password);
setupCheckPass();
} else {
if (!m_checkPass->isReady()) {
Q_EMIT failed();
return;
}
m_checkPass->setPassword(password);
m_checkPass->startAuth();
}
}
void Authenticator::setupCheckPass()
{
connect(m_checkPass, &KCheckPass::succeeded, this, &Authenticator::succeeded);
connect(m_checkPass, &KCheckPass::failed, this, &Authenticator::failed);
connect(m_checkPass, &KCheckPass::message, this, &Authenticator::message);
connect(m_checkPass, &KCheckPass::error, this, &Authenticator::error);
connect(m_checkPass, &KCheckPass::destroyed, this, [this] {
m_checkPass = nullptr;
});
m_checkPass->start();
}
bool Authenticator::isGraceLocked() const
{
return m_graceLockTimer->isActive();
}
KCheckPass::KCheckPass(AuthenticationMode mode, QObject *parent)
: QObject(parent)
, m_notifier(nullptr)
, m_pid(0)
, m_fd(0)
, m_mode(mode)
{
if (mode == AuthenticationMode::Direct) {
connect(this, &KCheckPass::succeeded, this, &QObject::deleteLater);
connect(this, &KCheckPass::failed, this, &QObject::deleteLater);
}
}
KCheckPass::~KCheckPass()
{
reapVerify();
}
void KCheckPass::start()
{
int sfd[2];
char fdbuf[16];
if (m_notifier) {
return;
}
if (::socketpair(AF_LOCAL, SOCK_STREAM, 0, sfd)) {
cantCheck();
return;
}
if ((m_pid = ::fork()) < 0) {
::close(sfd[0]);
::close(sfd[1]);
cantCheck();
return;
}
if (!m_pid) {
::close(sfd[0]);
sprintf(fdbuf, "%d", sfd[1]);
execlp(QFile::encodeName(KLibexec::path(KCHECKPASS_BIN)).data(), "kcheckpass", "-m", "classic", "-S", fdbuf, (char *)nullptr);
_exit(20);
}
::close(sfd[1]);
m_fd = sfd[0];
m_notifier = new QSocketNotifier(m_fd, QSocketNotifier::Read, this);
connect(m_notifier, &QSocketNotifier::activated, this, &KCheckPass::handleVerify);
}
////// kckeckpass interface code
int KCheckPass::Reader(void *buf, int count)
{
int ret, rlen;
for (rlen = 0; rlen < count;) {
dord:
ret = ::read(m_fd, (void *)((char *)buf + rlen), count - rlen);
if (ret < 0) {
if (errno == EINTR) {
goto dord;
}
if (errno == EAGAIN) {
break;
}
return -1;
}
if (!ret) {
break;
}
rlen += ret;
}
return rlen;
}
bool KCheckPass::GRead(void *buf, int count)
{
return Reader(buf, count) == count;
}
bool KCheckPass::GWrite(const void *buf, int count)
{
return ::write(m_fd, buf, count) == count;
}
bool KCheckPass::GSendInt(int val)
{
return GWrite(&val, sizeof(val));
}
bool KCheckPass::GSendStr(const char *buf)
{
int len = buf ? ::strlen(buf) + 1 : 0;
return GWrite(&len, sizeof(len)) && GWrite(buf, len);
}
bool KCheckPass::GSendArr(int len, const char *buf)
{
return GWrite(&len, sizeof(len)) && GWrite(buf, len);
}
bool KCheckPass::GRecvInt(int *val)
{
return GRead(val, sizeof(*val));
}
bool KCheckPass::GRecvArr(char **ret)
{
int len;
char *buf;
if (!GRecvInt(&len)) {
return false;
}
if (!len) {
*ret = nullptr;
return true;
}
if (!(buf = (char *)::malloc(len))) {
return false;
}
*ret = buf;
if (GRead(buf, len)) {
return true;
} else {
::free(buf);
*ret = nullptr;
return false;
}
}
void KCheckPass::handleVerify()
{
m_ready = false;
int ret;
char *arr;
if (GRecvInt(&ret)) {
switch (ret) {
case ConvGetBinary:
if (!GRecvArr(&arr)) {
break;
}
// FIXME: not supported
cantCheck();
if (arr) {
::free(arr);
}
return;
case ConvGetNormal:
case ConvGetHidden: {
if (!GRecvArr(&arr)) {
break;
}
if (m_password.isNull()) {
GSendStr(nullptr);
} else {
QByteArray utf8pass = m_password.toUtf8();
GSendStr(utf8pass.constData());
GSendInt(IsPassword);
}
m_password.clear();
if (arr) {
::free(arr);
}
return;
}
case ConvPutInfo:
if (!GRecvArr(&arr)) {
break;
}
Q_EMIT message(QString::fromLocal8Bit(arr));
::free(arr);
return;
case ConvPutError:
if (!GRecvArr(&arr)) {
break;
}
Q_EMIT error(QString::fromLocal8Bit(arr));
::free(arr);
return;
case ConvPutAuthSucceeded:
Q_EMIT succeeded();
return;
case ConvPutAuthFailed:
Q_EMIT failed();
return;
case ConvPutAuthError:
case ConvPutAuthAbort:
cantCheck();
return;
case ConvPutReadyForAuthentication:
m_ready = true;
if (m_mode == AuthenticationMode::Direct) {
::kill(m_pid, SIGUSR1);
}
return;
}
}
if (m_mode == AuthenticationMode::Direct) {
reapVerify();
} else {
// we broke, let's restart the greeter
// error code 1 will result in a restart through the system
qApp->exit(1);
}
}
void KCheckPass::reapVerify()
{
m_notifier->setEnabled(false);
m_notifier->deleteLater();
m_notifier = nullptr;
::close(m_fd);
int status;
::kill(m_pid, SIGUSR2);
while (::waitpid(m_pid, &status, 0) < 0) {
if (errno != EINTR) { // This should not happen ...
cantCheck();
return;
}
}
}
void KCheckPass::cantCheck()
{
// TODO: better signal?
Q_EMIT failed();
}
void KCheckPass::startAuth()
{
::kill(m_pid, SIGUSR1);
}
include(ECMMarkAsTest)
#####################################
# fakekcheckpass
#####################################
add_executable(fakekcheckpass fakekcheckpass.c)
ecm_mark_nongui_executable(fakekcheckpass)
target_link_libraries(fakekcheckpass ${SOCKET_LIBRARIES})
#######################################
# AuthenticatorTest
#######################################
set( authenticatorTest_SRCS
authenticatortest.cpp
../authenticator.cpp
)
add_executable(authenticatorTest ${authenticatorTest_SRCS})
target_compile_definitions(authenticatorTest PRIVATE KCHECKPASS_BIN="fakekcheckpass")
target_link_libraries(authenticatorTest KF5::CoreAddons Qt::Test)
add_test(NAME ksmserver-authenticatorTest COMMAND authenticatorTest)
ecm_mark_as_test(authenticatorTest)