feat: send enter input from the module

The Enter input is now handled directly in the module,
rather than in the compare process.
The module will try to send it in a loop,
 while the password task is active and until a fixed limit.
 If it didn't succeed, it will wait for the Enter key
 to be manually input or an error.
This commit is contained in:
MusiKid 2022-01-24 16:30:01 +01:00 committed by musikid
parent b9d678a08e
commit 6a395067ad
No known key found for this signature in database
GPG key ID: 7567D43648C6E2F4
5 changed files with 143 additions and 55 deletions

View file

@ -2,18 +2,18 @@
## Prepare
This module depends on `INIReader`.
It can be installed with these packages:
This module depends on `INIReader` and `libevdev`.
They can be installed with these packages:
```
Arch Linux - libinih
Debian - libinih-dev
Fedora - inih-devel
OpenSUSE - inih
Arch Linux - libinih libevdev
Debian - libinih-dev libevdev-dev
Fedora - inih-devel libevdev-devel
OpenSUSE - inih libevdev-devel
```
If your distribution doesn't provide `INIReader`,
it will be automatically pulled from the latest git version.
it will be automatically pulled from git at the subproject's pinned version.
## Build
@ -28,7 +28,7 @@ meson compile -C build
meson install -C build
```
Change PAM config line to:
Add the following line to your PAM configuration (/etc/pam.d/your-service):
``` pam
auth sufficient pam_howdy.so

49
src/pam/enter_device.cc Normal file
View file

@ -0,0 +1,49 @@
#include "enter_device.hh"
#include <cstring>
#include <memory>
#include <stdexcept>
EnterDevice::EnterDevice()
: raw_device(libevdev_new(), &libevdev_free),
raw_uinput_device(nullptr, &libevdev_uinput_destroy) {
auto *dev_ptr = raw_device.get();
libevdev_set_name(dev_ptr, "enter device");
libevdev_enable_event_type(dev_ptr, EV_KEY);
libevdev_enable_event_code(dev_ptr, EV_KEY, KEY_ENTER, nullptr);
int err;
struct libevdev_uinput *uinput_dev_ptr;
if ((err = libevdev_uinput_create_from_device(
dev_ptr, LIBEVDEV_UINPUT_OPEN_MANAGED, &uinput_dev_ptr)) != 0) {
throw std::runtime_error(std::string("Failed to create device: ") +
strerror(-err));
}
raw_uinput_device.reset(uinput_dev_ptr);
};
void EnterDevice::send_enter_press() const {
auto *uinput_dev_ptr = raw_uinput_device.get();
int err;
if ((err = libevdev_uinput_write_event(uinput_dev_ptr, EV_KEY, KEY_ENTER,
1)) != 0) {
throw std::runtime_error(std::string("Failed to write event: ") +
strerror(-err));
}
if ((err = libevdev_uinput_write_event(uinput_dev_ptr, EV_KEY, KEY_ENTER,
0)) != 0) {
throw std::runtime_error(std::string("Failed to write event: ") +
strerror(-err));
}
if ((err = libevdev_uinput_write_event(uinput_dev_ptr, EV_SYN, SYN_REPORT,
0)) != 0) {
throw std::runtime_error(std::string("Failed to write event: ") +
strerror(-err));
};
}

19
src/pam/enter_device.hh Normal file
View file

@ -0,0 +1,19 @@
#ifndef __ENTER_DEVICE_H_
#define __ENTER_DEVICE_H_
#include <libevdev/libevdev-uinput.h>
#include <libevdev/libevdev.h>
#include <memory>
class EnterDevice {
std::unique_ptr<struct libevdev, decltype(&libevdev_free)> raw_device;
std::unique_ptr<struct libevdev_uinput, decltype(&libevdev_uinput_destroy)>
raw_uinput_device;
public:
EnterDevice();
void send_enter_press() const;
~EnterDevice() = default;
};
#endif // __ENTER_DEVICE_H

View file

@ -7,6 +7,7 @@
#include <libintl.h>
#include <pthread.h>
#include <spawn.h>
#include <stdexcept>
#include <sys/signalfd.h>
#include <sys/syslog.h>
#include <sys/types.h>
@ -37,6 +38,7 @@
#include <security/pam_ext.h>
#include <security/pam_modules.h>
#include "enter_device.hh"
#include "main.hh"
#include "optional_task.hh"
@ -47,7 +49,7 @@
#endif
const auto DEFAULT_TIMEOUT =
std::chrono::duration<int, std::chrono::milliseconds::period>(2500);
std::chrono::duration<int, std::chrono::milliseconds::period>(100);
const auto MAX_RETRIES = 5;
const auto PYTHON_EXECUTABLE = "python3";
const auto COMPARE_PROCESS_PATH = "/lib/security/howdy/compare.py";
@ -116,7 +118,7 @@ auto howdy_error(int status,
* @return Returns the conversation function return code
*/
auto howdy_status(char *username, int status, const INIReader &reader,
const std::function<int(int, const char *)> &conv_function)
const std::function<int(int, const char *)> &conv_function)
-> int {
if (status != EXIT_SUCCESS) {
return howdy_error(status, conv_function);
@ -218,19 +220,19 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv,
syslog(LOG_ERR, "Underlying error: %s (%d)", strerror(errno), errno);
}
} else {
for (size_t i = 0; i < glob_result.gl_pathc; i++) {
std::ifstream file(std::string(glob_result.gl_pathv[i]));
std::string lid_state;
std::getline(file, lid_state, static_cast<char>(file.eof()));
for (size_t i = 0; i < glob_result.gl_pathc; i++) {
std::ifstream file(std::string(glob_result.gl_pathv[i]));
std::string lid_state;
std::getline(file, lid_state, static_cast<char>(file.eof()));
if (lid_state.find("closed") != std::string::npos) {
globfree(&glob_result);
if (lid_state.find("closed") != std::string::npos) {
globfree(&glob_result);
syslog(LOG_INFO, "Skipped authentication, closed lid detected");
return PAM_AUTHINFO_UNAVAIL;
syslog(LOG_INFO, "Skipped authentication, closed lid detected");
return PAM_AUTHINFO_UNAVAIL;
}
}
}
}
globfree(&glob_result);
}
@ -321,52 +323,67 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv,
cv.wait(lk, [&] { return confirmation_type != ConfirmationType::Unset; });
}
if (confirmation_type == ConfirmationType::Howdy) {
// The password has been entered or an error has occurred
if (confirmation_type == ConfirmationType::Pam) {
// We kill the child because we don't need its result
kill(child_pid, SIGTERM);
child_task.stop(false);
// If the workaround is native
if (auth_tok) {
// UNSAFE: We cancel the thread using pthread, pam_get_authtok seems to be
// a cancellation point
if (pass_task.is_active()) {
pass_task.stop(true);
}
// We just wait for the thread to stop since it's this one which sent us the
// confirmation type
pass_task.stop(false);
char *password = nullptr;
std::tie(pam_res, password) = pass_task.get();
if (pam_res != PAM_SUCCESS) {
return pam_res;
}
int howdy_status = child_task.get();
return howdy_msg(username, howdy_status, reader, conv_function);
// The password has been entered, we are passing it to PAM stack
return PAM_IGNORE;
}
// The password has been entered
// We need to be sure that we're not going to block forever if the
// child has a problem
if (child_task.wait(DEFAULT_TIMEOUT) == std::future_status::timeout) {
kill(child_pid, SIGTERM);
}
// The compare process has finished its execution
child_task.stop(false);
// We just wait for the thread to stop since it's this one which sent us the
// confirmation type
if (workaround == Workaround::Input && auth_tok) {
// We want to stop the password prompt, either by canceling the thread when
// workaround is set to "native", or by emulating "Enter" input with
// "input"
// UNSAFE: We cancel the thread using pthread, pam_get_authtok seems to be
// a cancellation point
if (workaround == Workaround::Native && pass_task.is_active()) {
pass_task.stop(true);
} else if (workaround == Workaround::Input) {
if (geteuid() != 0) {
syslog(LOG_WARNING, "Insufficient permission to create the fake device");
conv_function(PAM_ERROR_MSG, S("Insufficient permission to send Enter "
"input, waiting for Enter input..."));
} else {
try {
EnterDevice enter_device;
for (int retries = 0;
retries < MAX_RETRIES &&
pass_task.wait(DEFAULT_TIMEOUT) == std::future_status::timeout;
retries++) {
enter_device.send_enter_press();
}
} catch (std::runtime_error &err) {
syslog(LOG_WARNING, "Failed to send enter input: %s", err.what());
conv_function(
PAM_ERROR_MSG,
S("Failed to send enter input, waiting for Enter input..."));
}
}
// We stop the thread (will block until the enter key is pressed, if the
// input wasn't focused or if the uinput device failed to send keypress)
pass_task.stop(false);
}
int status = child_task.get();
char *password = nullptr;
std::tie(pam_res, password) = pass_task.get();
if (pam_res != PAM_SUCCESS) {
return pam_res;
}
int howdy_status = child_task.get();
// If python process (or user) sent Enter key
if (strlen(password) == 0) {
return howdy_msg(username, howdy_status, reader, conv_function);
}
// The password has been entered, we are passing it to PAM stack
return PAM_IGNORE;
return howdy_status(username, status, reader, conv_function);
}
// Called by PAM when a user needs to be authenticated, for example by running

View file

@ -1,6 +1,7 @@
project('pam_howdy', 'cpp', version: '0.1.0', default_options: ['cpp_std=c++14'])
inih_cpp = dependency('INIReader', fallback: ['inih', 'INIReader_dep'])
libevdev = dependency('libevdev')
libpam = meson.get_compiler('cpp').find_library('pam')
threads = dependency('threads')
@ -10,10 +11,12 @@ subdir('po')
shared_library(
'pam_howdy',
'main.cc',
'enter_device.cc',
dependencies: [
libpam,
inih_cpp,
threads
threads,
libevdev,
],
install: true,
install_dir: '/lib/security',