Commit acf20b14 authored by Martin Flöser's avatar Martin Flöser

Add password checking to screenlocker

A new Authenticator item is introduced and exported to QML. It provides
a slot tryAuthenticate taking the password as a string. If invoked it
creates a KCheckPass object which handles the communication with the
kcheckpass binary to verify the password. If the authentication succeeded
the Authenticator emits a succeeded() signal, on failure a failed()
signal.

The authentication process is bound to a grace timer. While this timer is
active it's not possible to authenticate again. This state is exposed
with a Q_PROPERTY and used by the UI to disable the password field and
unlock button.

REVIEW: 117321
parent 5360e48d
include_directories(
${CMAKE_CURRENT_BINARY_DIR}
${KCheckPass_SOURCE_DIR}/kcheckpass
../kcheckpass
${CMAKE_CURRENT_BINARY_DIR}/../../
)
set(kscreenlocker_greet_SRCS
authenticator.cpp
sessions.cpp
greeterapp.cpp
screensaverwindow.cpp
......@@ -30,3 +33,5 @@ target_link_libraries(kscreenlocker_greet
install(TARGETS kscreenlocker_greet DESTINATION ${LIBEXEC_INSTALL_DIR})
install(DIRECTORY themes/org.kde.passworddialog DESTINATION ${DATA_INSTALL_DIR}/ksmserver/screenlocker)
add_subdirectory(autotests)
/********************************************************************
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 <kcheckpass-enums.h>
#include <config-ksmserver.h>
// Qt
#include <QFile>
#include <QSocketNotifier>
#include <QTimer>
// system
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
Authenticator::Authenticator(QObject *parent)
: QObject(parent)
, m_graceLockTimer(new QTimer(this))
{
m_graceLockTimer->setSingleShot(true);
m_graceLockTimer->setInterval(3000);
connect(m_graceLockTimer, &QTimer::timeout, this, &Authenticator::graceLockedChanged);
}
Authenticator::~Authenticator() = default;
void Authenticator::tryUnlock(const QString &password)
{
if (isGraceLocked()) {
emit failed();
return;
}
m_graceLockTimer->start();
emit graceLockedChanged();
KCheckPass *checkPass = new KCheckPass(password, this);
connect(checkPass, &KCheckPass::succeeded, this, &Authenticator::succeeded);
connect(checkPass, &KCheckPass::failed, this, &Authenticator::failed);
connect(checkPass, &KCheckPass::message, this, &Authenticator::message);
connect(checkPass, &KCheckPass::error, this, &Authenticator::error);
checkPass->start();
}
bool Authenticator::isGraceLocked() const
{
return m_graceLockTimer->isActive();
}
KCheckPass::KCheckPass(const QString &password, QObject *parent)
: QObject(parent)
, m_password(password)
, m_notifier(nullptr)
, m_pid(0)
, m_fd(0)
{
connect(this, &KCheckPass::succeeded, this, &QObject::deleteLater);
connect(this, &KCheckPass::failed, this, &QObject::deleteLater);
}
KCheckPass::~KCheckPass() = default;
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(QStringLiteral(KCHECKPASS_BIN)).data(),
"kcheckpass",
"-m", "classic",
"-S", fdbuf,
(char *)0);
_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 = 0;
return true;
}
if (!(buf = (char *)::malloc (len)))
return false;
*ret = buf;
if (GRead (buf, len)) {
return true;
} else {
::free(buf);
*ret = 0;
return false;
}
}
void KCheckPass::handleVerify()
{
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:
if (!GRecvArr( &arr ))
break;
GSendStr(m_password.toUtf8().constData());
if (!m_password.isEmpty()) {
// IsSecret
GSendInt(1);
}
if (arr)
::free( arr );
return;
case ConvGetHidden:
if (!GRecvArr( &arr ))
break;
GSendStr(m_password.toUtf8().constData());
if (!m_password.isEmpty()) {
// IsSecret
GSendInt(1);
}
if (arr)
::free( arr );
return;
case ConvPutInfo:
if (!GRecvArr( &arr ))
break;
emit message(QString::fromLocal8Bit(arr));
::free( arr );
return;
case ConvPutError:
if (!GRecvArr( &arr ))
break;
emit error(QString::fromLocal8Bit(arr));
::free( arr );
return;
}
}
reapVerify();
}
void KCheckPass::reapVerify()
{
m_notifier->setEnabled( false );
m_notifier->deleteLater();
m_notifier = nullptr;
::close( m_fd );
int status;
while (::waitpid( m_pid, &status, 0 ) < 0)
if (errno != EINTR) { // This should not happen ...
cantCheck();
return;
}
if (WIFEXITED(status))
switch (WEXITSTATUS(status)) {
case AuthOk:
emit succeeded();
return;
case AuthBad:
emit failed();
return;
case AuthAbort:
return;
}
cantCheck();
}
void KCheckPass::cantCheck()
{
// TODO: better signal?
emit failed();
}
/********************************************************************
KSld - the KDE Screenlocker Daemon
This file is part of the KDE project.
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/>.
*********************************************************************/
#ifndef AUTHENTICATOR_H
#define AUTHENTICATOR_H
#include <QObject>
class QSocketNotifier;
class QTimer;
class Authenticator : public QObject
{
Q_OBJECT
Q_PROPERTY(bool graceLocked READ isGraceLocked NOTIFY graceLockedChanged)
public:
explicit Authenticator(QObject *parent = nullptr);
~Authenticator();
bool isGraceLocked() const;
public Q_SLOTS:
void tryUnlock(const QString &password);
Q_SIGNALS:
void failed();
void succeeded();
void graceLockedChanged();
void message(const QString &);
void error(const QString &);
private:
QTimer *m_graceLockTimer;
};
class KCheckPass : public QObject
{
Q_OBJECT
public:
explicit KCheckPass(const QString &password, QObject *parent = nullptr);
~KCheckPass();
void start();
Q_SIGNALS:
void failed();
void succeeded();
void message(const QString &);
void error(const QString &);
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;
};
#endif
add_definitions(-DKSMSERVER_UNIT_TEST)
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_link_libraries(authenticatorTest Qt5::Test)
add_test(ksmserver-authenticatorTest authenticatorTest)
ecm_mark_as_test(authenticatorTest)
/********************************************************************
KSld - the KDE Screenlocker Daemon
This file is part of the KDE project.
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"
// Qt
#include <QtTest/QtTest>
class AuthenticatorTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testIncorrectPassword();
void testCorrectPassword();
void testMessage();
void testError();
};
void AuthenticatorTest::testIncorrectPassword()
{
QScopedPointer<Authenticator> authenticator(new Authenticator(this));
QSignalSpy failedSpy(authenticator.data(), SIGNAL(failed()));
QSignalSpy succeededSpy(authenticator.data(), SIGNAL(succeeded()));
QSignalSpy graceLockSpy(authenticator.data(), SIGNAL(graceLockedChanged()));
QCOMPARE(authenticator->isGraceLocked(), false);
authenticator->tryUnlock(QStringLiteral("incorrect"));
QVERIFY(failedSpy.wait());
QCOMPARE(authenticator->isGraceLocked(), true);
QCOMPARE(failedSpy.count(), 1);
QVERIFY(succeededSpy.isEmpty());
QCOMPARE(graceLockSpy.count(), 1);
graceLockSpy.clear();
// we are currently grace locked, so correct password will fail
authenticator->tryUnlock(QStringLiteral("testpassword"));
QVERIFY(!succeededSpy.wait(100));
QCOMPARE(failedSpy.count(), 2);
QVERIFY(graceLockSpy.wait());
}
void AuthenticatorTest::testCorrectPassword()
{
QScopedPointer<Authenticator> authenticator(new Authenticator(this));
QSignalSpy succeededSpy(authenticator.data(), SIGNAL(succeeded()));
QSignalSpy failedSpy(authenticator.data(), SIGNAL(failed()));
authenticator->tryUnlock(QStringLiteral("testpassword"));
QVERIFY(succeededSpy.wait());
QCOMPARE(succeededSpy.count(), 1);
QVERIFY(failedSpy.isEmpty());
}
void AuthenticatorTest::testMessage()
{
QScopedPointer<Authenticator> authenticator(new Authenticator(this));
QSignalSpy succeededSpy(authenticator.data(), SIGNAL(succeeded()));
QSignalSpy messageSpy(authenticator.data(), SIGNAL(message(QString)));
authenticator->tryUnlock(QStringLiteral("info"));
QVERIFY(succeededSpy.wait());
QCOMPARE(succeededSpy.count(), 1);
QCOMPARE(messageSpy.count(), 1);
QCOMPARE(messageSpy.first().at(0).toString(), QStringLiteral("You wanted some info, here you have it"));
}
void AuthenticatorTest::testError()
{
QScopedPointer<Authenticator> authenticator(new Authenticator(this));
QSignalSpy failedSpy(authenticator.data(), SIGNAL(failed()));
QSignalSpy errorSpy(authenticator.data(), SIGNAL(error(QString)));
authenticator->tryUnlock(QStringLiteral("error"));
QVERIFY(failedSpy.wait());
QCOMPARE(failedSpy.count(), 1);
QCOMPARE(errorSpy.count(), 1);
QCOMPARE(errorSpy.first().at(0).toString(), QStringLiteral("The world is going to explode"));
}
QTEST_MAIN(AuthenticatorTest)
#include "authenticatortest.moc"
/********************************************************************
KSld - the KDE Screenlocker Daemon
This file is part of the KDE project.
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/>.
This test is based on kcheckpass, written by:
Olaf Kirch <okir@caldera.de> General Framework and PAM support
Christian Esken <esken@kde.org> Shadow and /etc/passwd support
Roberto Teixeira <maragato@kde.org> other user (-U) support
Oswald Buddenhagen <ossi@kde.org> Binary server mode
*********************************************************************/
#include <kcheckpass-enums.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <syslog.h>
#include <stdlib.h>
#include <errno.h>
#include <time.h>
#include <unistd.h>
static int havetty, sfd = -1, nullpass;
#ifdef __cplusplus
extern "C" {
#endif
# define ATTR_PRINTFLIKE(fmt,var) __attribute__((format(printf,fmt,var)))
void message(const char *, ...) ATTR_PRINTFLIKE(1, 2);
#ifdef __cplusplus
}
#endif
static int
Reader (void *buf, int count)
{
int ret, rlen;
for (rlen = 0; rlen < count; ) {
dord:
ret = read (sfd, (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;
}
static void
GRead (void *buf, int count)
{
if (Reader (buf, count) != count) {
message ("Communication breakdown on read\n");
exit(15);
}
}
static void
GWrite (const void *buf, int count)
{
if (write (sfd, buf, count) != count) {
message ("Communication breakdown on write\n");
exit(15);
}
}
static void
GSendInt (int val)
{
GWrite (&val, sizeof(val));
}
static void
GSendStr (const char *buf)
{
unsigned len = buf ? strlen (buf) + 1 : 0;
GWrite (&len, sizeof(len));
GWrite (buf, len);
}
static void
GSendArr (int len, const char *buf)
{
GWrite (&len, sizeof(len));
GWrite (buf, len);
}
static int
GRecvInt (void)
{
int val;
GRead (&val, sizeof(val));
return val;
}
static char *
GRecvStr (void)
{
unsigned len;
char *buf;
if (!(len = GRecvInt()))
return (char *)0;
if (len > 0x1000 || !(buf = malloc (len))) {
message ("No memory for read buffer\n");
exit(15);
}
GRead (buf, len);
buf[len - 1] = 0; /* we're setuid ... don't trust "them" */
return buf;