diff --git a/src/pam/README.md b/src/pam/README.md index 0a3b0d6..23d1804 100644 --- a/src/pam/README.md +++ b/src/pam/README.md @@ -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 diff --git a/src/pam/enter_device.cc b/src/pam/enter_device.cc new file mode 100644 index 0000000..66de0eb --- /dev/null +++ b/src/pam/enter_device.cc @@ -0,0 +1,49 @@ +#include "enter_device.hh" + +#include +#include +#include + +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)); + }; +} diff --git a/src/pam/enter_device.hh b/src/pam/enter_device.hh new file mode 100644 index 0000000..0dc7e1d --- /dev/null +++ b/src/pam/enter_device.hh @@ -0,0 +1,19 @@ +#ifndef __ENTER_DEVICE_H_ +#define __ENTER_DEVICE_H_ + +#include +#include +#include + +class EnterDevice { + std::unique_ptr raw_device; + std::unique_ptr + raw_uinput_device; + +public: + EnterDevice(); + void send_enter_press() const; + ~EnterDevice() = default; +}; + +#endif // __ENTER_DEVICE_H diff --git a/src/pam/main.cc b/src/pam/main.cc index 6f55dd1..87af592 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,7 @@ #include #include +#include "enter_device.hh" #include "main.hh" #include "optional_task.hh" @@ -47,7 +49,7 @@ #endif const auto DEFAULT_TIMEOUT = - std::chrono::duration(2500); + std::chrono::duration(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 &conv_function) + const std::function &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(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(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 diff --git a/src/pam/meson.build b/src/pam/meson.build index f6660a9..be93919 100644 --- a/src/pam/meson.build +++ b/src/pam/meson.build @@ -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',