diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..1843f5d --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,25 @@ +name: check +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Install required libraries + run: > + sudo apt-get update && sudo apt-get install -y + python3 python3-pip python3-setuptools python3-wheel ninja-build meson + cmake make build-essential clang-tidy + libpam0g-dev libinih-dev libevdev-dev + python3-dev libopencv-dev + + - uses: actions/checkout@v2 + + - name: Build + run: | + meson setup build howdy/src/pam + ninja -C build + + - name: Check source code + run: | + ninja clang-tidy -C build diff --git a/howdy/archlinux/howdy/.SRCINFO b/howdy/archlinux/howdy/.SRCINFO index cf7022d..0fbef43 100644 --- a/howdy/archlinux/howdy/.SRCINFO +++ b/howdy/archlinux/howdy/.SRCINFO @@ -13,6 +13,7 @@ pkgbase = howdy depends = python3 depends = python-dlib depends = python-numpy + depends = python-opencv backup = usr/lib/security/howdy/config.ini source = howdy-2.6.1.tar.gz::https://github.com/boltgolt/howdy/archive/v2.6.1.tar.gz source = https://github.com/davisking/dlib-models/raw/master/dlib_face_recognition_resnet_model_v1.dat.bz2 diff --git a/howdy/src/compare.py b/howdy/src/compare.py index 3fd7f35..9d07dd7 100644 --- a/howdy/src/compare.py +++ b/howdy/src/compare.py @@ -26,7 +26,6 @@ import _thread as thread from i18n import _ from recorders.video_capture import VideoCapture - def exit(code=None): """Exit while closeing howdy-gtk properly""" global gtk_proc diff --git a/howdy/src/config.ini b/howdy/src/config.ini index d02de0d..28d5258 100644 --- a/howdy/src/config.ini +++ b/howdy/src/config.ini @@ -30,6 +30,14 @@ disabled = false # computational power to run, and is meant to be executed on a GPU to attain reasonable speed. use_cnn = false +# WARNING: Changing this key can lead to unstability +# Set a workaround to confirm the prompt +# +# "off" will disable it, so the user needs to confirm manually +# "input" will send an enter keypress to confirm (the prompt needs to be on focus) +# "native" will stop the prompt at PAM level (DANGEROUS!) +workaround = input + [video] # The certainty of the detected face belonging to the user of the account # On a scale from 1 to 10, values above 5 are not recommended diff --git a/howdy/src/pam/.clang-tidy b/howdy/src/pam/.clang-tidy new file mode 100644 index 0000000..887f4c3 --- /dev/null +++ b/howdy/src/pam/.clang-tidy @@ -0,0 +1,7 @@ +Checks: 'clang-diagnostic-*,clang-analyser-*,-clang-diagnostic-unused-command-line-argument,google-*,bugprone-*,modernize-*,performance-*,portability-*,readability-*,-bugprone-easily-swappable-*,-readability-magic-numbers,-google-readability-todo' +WarningsAsErrors: 'clang-diagnostic-*,clang-analyser-*,-clang-diagnostic-unused-command-line-argument,google-*,bugprone-*,modernize-*,performance-*,portability-*,readability-*,-bugprone-easily-swappable-*,-readability-magic-numbers,-google-readability-todo' +CheckOptions: + - key: readability-function-cognitive-complexity.Threshold + value: '50' + +# vim:syntax=yaml diff --git a/howdy/src/pam/README.md b/howdy/src/pam/README.md index 765cba8..23d1804 100644 --- a/howdy/src/pam/README.md +++ b/howdy/src/pam/README.md @@ -1,20 +1,35 @@ # Howdy PAM module +## Prepare + +This module depends on `INIReader` and `libevdev`. +They can be installed with these packages: + +``` +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 git at the subproject's pinned version. + ## Build -```sh -meson setup build -Dinih:with_INIReader=true +``` sh +meson setup build meson compile -C build ``` ## Install -```sh -sudo mv build/libpam_howdy.so /lib/security/pam_howdy.so +``` sh +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 +``` pam +auth sufficient pam_howdy.so ``` diff --git a/howdy/src/pam/enter_device.cc b/howdy/src/pam/enter_device.cc new file mode 100644 index 0000000..0114d8d --- /dev/null +++ b/howdy/src/pam/enter_device.cc @@ -0,0 +1,48 @@ +#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/howdy/src/pam/enter_device.hh b/howdy/src/pam/enter_device.hh new file mode 100644 index 0000000..acdadd1 --- /dev/null +++ b/howdy/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/howdy/src/pam/main.cc b/howdy/src/pam/main.cc index 2595c61..530d5c8 100644 --- a/howdy/src/pam/main.cc +++ b/howdy/src/pam/main.cc @@ -1,12 +1,13 @@ #include #include #include -#include #include -#include + +#include +#include #include #include -#include +#include #include #include #include @@ -33,59 +34,62 @@ #include -#include - #include #include #include +#include "enter_device.hh" #include "main.hh" #include "optional_task.hh" -using namespace std; -using namespace boost::locale; -using namespace std::chrono_literals; +const auto DEFAULT_TIMEOUT = + std::chrono::duration(100); +const auto MAX_RETRIES = 5; +const auto PYTHON_EXECUTABLE = "python3"; +const auto COMPARE_PROCESS_PATH = "/lib/security/howdy/compare.py"; + +#define S(msg) gettext(msg) /** * Inspect the status code returned by the compare process - * @param code The status code + * @param status The status code * @param conv_function The PAM conversation function * @return A PAM return code */ -int on_howdy_auth(int code, function conv_function) { +auto howdy_error(int status, + const std::function &conv_function) + -> int { // If the process has exited - if (!WIFEXITED(code)) { + if (WIFEXITED(status)) { // Get the status code returned - code = WEXITSTATUS(code); + status = WEXITSTATUS(status); - switch (code) { - // Status 10 means we couldn't find any face models - case 10: - conv_function(PAM_ERROR_MSG, - dgettext("pam", "There is no face model known")); + switch (status) { + case CompareError::NO_FACE_MODEL: + conv_function(PAM_ERROR_MSG, S("There is no face model known")); syslog(LOG_NOTICE, "Failure, no face model known"); break; - // Status 11 means we exceded the maximum retry count - case 11: - syslog(LOG_INFO, "Failure, timeout reached"); + case CompareError::TIMEOUT_REACHED: + syslog(LOG_ERR, "Failure, timeout reached"); break; - // Status 12 means we aborted - case 12: - syslog(LOG_INFO, "Failure, general abort"); + case CompareError::ABORT: + syslog(LOG_ERR, "Failure, general abort"); break; - // Status 13 means the image was too dark - case 13: - conv_function(PAM_ERROR_MSG, - dgettext("pam", "Face detection image too dark")); - syslog(LOG_INFO, "Failure, image too dark"); + case CompareError::TOO_DARK: + conv_function(PAM_ERROR_MSG, S("Face detection image too dark")); + syslog(LOG_ERR, "Failure, image too dark"); break; - // Otherwise, we can't describe what happened but it wasn't successful default: - conv_function( - PAM_ERROR_MSG, - string(dgettext("pam", "Unknown error:") + to_string(code)).c_str()); - syslog(LOG_INFO, "Failure, unknown error %d", code); + conv_function(PAM_ERROR_MSG, + std::string(S("Unknown error: ") + status).c_str()); + syslog(LOG_ERR, "Failure, unknown error %d", status); } + } else if (WIFSIGNALED(status)) { + // We get the signal + status = WTERMSIG(status); + + syslog(LOG_ERR, "Child killed by signal %s (%d)", strsignal(status), + status); } // As this function is only called for error status codes, signal an error to @@ -94,25 +98,88 @@ int on_howdy_auth(int code, function conv_function) { } /** - * Format and send a message to PAM - * @param conv PAM conversation function - * @param type Type of PAM message - * @param message String to show the user - * @return Returns the conversation function return code + * Format the success message if the status is successful or log the error in + * the other case + * @param username Username + * @param status Status code + * @param config INI configuration + * @param conv_function PAM conversation function + * @return Returns the conversation function return code */ -int send_message(function - conv, - int type, const char *message) { - // No need to free this, it's allocated on the stack - const struct pam_message msg = {.msg_style = type, .msg = message}; - const struct pam_message *msgp = &msg; +auto howdy_status(char *username, int status, const INIReader &config, + const std::function &conv_function) + -> int { + if (status != EXIT_SUCCESS) { + return howdy_error(status, conv_function); + } - struct pam_response res_ = {}; - struct pam_response *resp_ = &res_; + if (!config.GetBoolean("core", "no_confirmation", true)) { + // Construct confirmation text from i18n string + std::string confirm_text(S("Identified face as {}")); + std::string identify_msg = + confirm_text.replace(confirm_text.find("{}"), 2, std::string(username)); + conv_function(PAM_TEXT_INFO, identify_msg.c_str()); + } - // Call the conversation function with the constructed arguments - return conv(1, &msgp, &resp_, nullptr); + syslog(LOG_INFO, "Login approved"); + + return PAM_SUCCESS; +} + +/** + * Check if Howdy should be enabled according to the configuration and the + * environment. + * @param config INI configuration + * @return Returns PAM_AUTHINFO_UNAVAIL if it shouldn't be enabled, + * PAM_SUCCESS otherwise + */ +auto check_enabled(const INIReader &config) -> int { + // Stop executing if Howdy has been disabled in the config + if (config.GetBoolean("core", "disabled", false)) { + syslog(LOG_INFO, "Skipped authentication, Howdy is disabled"); + return PAM_AUTHINFO_UNAVAIL; + } + + // Stop if we're in a remote shell and configured to exit + if (config.GetBoolean("core", "ignore_ssh", true)) { + if (getenv("SSH_CONNECTION") != nullptr || + getenv("SSH_CLIENT") != nullptr || getenv("SSHD_OPTS") != nullptr) { + syslog(LOG_INFO, "Skipped authentication, SSH session detected"); + return PAM_AUTHINFO_UNAVAIL; + } + } + + // Try to detect the laptop lid state and stop if it's closed + if (config.GetBoolean("core", "ignore_closed_lid", true)) { + glob_t glob_result; + + // Get any files containing lid state + int return_value = + glob("/proc/acpi/button/lid/*/state", 0, nullptr, &glob_result); + + if (return_value != 0) { + syslog(LOG_ERR, "Failed to read files from glob: %d", return_value); + if (errno != 0) { + 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())); + + if (lid_state.find("closed") != std::string::npos) { + globfree(&glob_result); + + syslog(LOG_INFO, "Skipped authentication, closed lid detected"); + return PAM_AUTHINFO_UNAVAIL; + } + } + } + globfree(&glob_result); + } + + return PAM_SUCCESS; } /** @@ -124,271 +191,238 @@ int send_message(function int { + INIReader config("/lib/security/howdy/config.ini"); openlog("pam_howdy", 0, LOG_AUTHPRIV); - string workaround = reader.GetString("core", "workaround", "input"); + // Error out if we could not read the config file + if (config.ParseError() != 0) { + syslog(LOG_ERR, "Failed to parse the configuration file: %d", + config.ParseError()); + return PAM_SYSTEM_ERR; + } - // In this case, we are not asking for the password - if (workaround == Workaround::Off && auth_tok) - auth_tok = false; - - // Will contain PAM conversation function - struct pam_conv *conv = nullptr; // Will contain the responses from PAM functions int pam_res = PAM_IGNORE; - // Try to get the conversation function and error out if we can't - if ((pam_res = pam_get_item(pamh, PAM_CONV, (const void **)&conv)) != - PAM_SUCCESS) { + // Check if we shoud continue + if ((pam_res = check_enabled(config)) != PAM_SUCCESS) { + return pam_res; + } + + Workaround workaround = + get_workaround(config.GetString("core", "workaround", "input")); + + // Will contain PAM conversation structure + struct pam_conv *conv = nullptr; + const void **conv_ptr = + const_cast(reinterpret_cast(&conv)); + + if ((pam_res = pam_get_item(pamh, PAM_CONV, conv_ptr)) != PAM_SUCCESS) { syslog(LOG_ERR, "Failed to acquire conversation"); return pam_res; } // Wrap the PAM conversation function in our own, easier function - auto conv_function = - bind(send_message, conv->conv, placeholders::_1, placeholders::_2); + auto conv_function = [conv](int msg_type, const char *msg_str) { + const struct pam_message msg = {.msg_style = msg_type, .msg = msg_str}; + const struct pam_message *msgp = &msg; - // Error out if we could not ready the config file - if (reader.ParseError() < 0) { - syslog(LOG_ERR, "Failed to parse the configuration file"); - return PAM_SYSTEM_ERR; - } + struct pam_response res = {}; + struct pam_response *resp = &res; - // Stop executing if Howdy has been disabled in the config - if (reader.GetBoolean("core", "disabled", false)) { - syslog(LOG_INFO, "Skipped authentication, Howdy is disabled"); - return PAM_AUTHINFO_UNAVAIL; - } + return conv->conv(1, &msgp, &resp, conv->appdata_ptr); + }; - // Stop if we're in a remote shell and configured to exit - if (reader.GetBoolean("core", "ignore_ssh", true)) { - if (getenv("SSH_CONNECTION") != nullptr || - getenv("SSH_CLIENT") != nullptr || getenv("SSHD_OPTS") != nullptr) { - syslog(LOG_INFO, "Skipped authentication, SSH session detected"); - return PAM_AUTHINFO_UNAVAIL; - } - } - - // Try to detect the laptop lid state and stop if it's closed - if (reader.GetBoolean("core", "ignore_closed_lid", true)) { - glob_t glob_result{}; - - // Get any files containing lid state - int return_value = - glob("/proc/acpi/button/lid/*/state", 0, nullptr, &glob_result); - - // TODO: We ignore the result - if (return_value != 0) { - globfree(&glob_result); - } - - for (size_t i = 0; i < glob_result.gl_pathc; i++) { - ifstream file(string(glob_result.gl_pathv[i])); - string lid_state; - getline(file, lid_state, (char)file.eof()); - - if (lid_state.find("closed") != std::string::npos) { - globfree(&glob_result); - - syslog(LOG_INFO, "Skipped authentication, closed lid detected"); - return PAM_AUTHINFO_UNAVAIL; - } - } - - globfree(&glob_result); - } + // Initialize gettext + setlocale(LC_ALL, ""); + bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR); + textdomain(GETTEXT_PACKAGE); // If enabled, send a notice to the user that facial login is being attempted - if (reader.GetBoolean("core", "detection_notice", false)) { - if ((pam_res = conv_function( - PAM_TEXT_INFO, - dgettext("pam", "Attempting facial authentication"))) != + if (config.GetBoolean("core", "detection_notice", false)) { + if ((conv_function(PAM_TEXT_INFO, S("Attempting facial authentication"))) != PAM_SUCCESS) { syslog(LOG_ERR, "Failed to send detection notice"); } } // Get the username from PAM, needed to match correct face model - char *user_ptr = nullptr; - if ((pam_res = pam_get_user(pamh, (const char **)&user_ptr, nullptr)) != - PAM_SUCCESS) { + char *username = nullptr; + if ((pam_res = pam_get_user(pamh, const_cast(&username), + nullptr)) != PAM_SUCCESS) { syslog(LOG_ERR, "Failed to get username"); return pam_res; } - posix_spawn_file_actions_t file_actions; - posix_spawn_file_actions_init(&file_actions); - // We close standard descriptors for the child - posix_spawn_file_actions_addclose(&file_actions, STDOUT_FILENO); - posix_spawn_file_actions_addclose(&file_actions, STDERR_FILENO); - posix_spawn_file_actions_addclose(&file_actions, STDIN_FILENO); - - const char *const args[] = { - "/usr/bin/python3", "/lib/security/howdy/compare.py", user_ptr, nullptr}; + const char *const args[] = {PYTHON_EXECUTABLE, // NOLINT + COMPARE_PROCESS_PATH, username, nullptr}; pid_t child_pid; // Start the python subprocess - if (posix_spawnp(&child_pid, "/usr/bin/python3", &file_actions, nullptr, - (char *const *)args, nullptr) < 0) { - syslog(LOG_ERR, "Can't spawn the howdy process: %s", strerror(errno)); + if (posix_spawnp(&child_pid, PYTHON_EXECUTABLE, nullptr, nullptr, + const_cast(args), nullptr) != 0) { + syslog(LOG_ERR, "Can't spawn the howdy process: %s (%d)", strerror(errno), + errno); return PAM_SYSTEM_ERR; } - mutex m; - condition_variable cv; - Type confirmation_type; - // TODO: Find a clean way to do this - Type final_type; + // NOTE: We should replace mutex and condition_variable by atomic wait, but + // it's too recent (C++20) + std::mutex m; + std::condition_variable cv; + ConfirmationType confirmation_type(ConfirmationType::Unset); // This task wait for the status of the python subprocess (we don't want a // zombie process) - optional_task child_task(packaged_task([&] { + optional_task child_task([&] { int status; wait(&status); { - unique_lock lk(m); - confirmation_type = Type::Howdy; + std::unique_lock lk(m); + if (confirmation_type == ConfirmationType::Unset) { + confirmation_type = ConfirmationType::Howdy; + } } - cv.notify_all(); + cv.notify_one(); + return status; - })); + }); child_task.activate(); // This task waits for the password input (if the workaround wants it) - optional_task> pass_task( - packaged_task()>([&] { - char *auth_tok_ptr = nullptr; - int pam_res = pam_get_authtok(pamh, PAM_AUTHTOK, - (const char **)&auth_tok_ptr, nullptr); - { - unique_lock lk(m); - confirmation_type = Type::Pam; - } - cv.notify_all(); - return tuple(pam_res, auth_tok_ptr); - })); + optional_task> pass_task([&] { + char *auth_tok_ptr = nullptr; + int pam_res = pam_get_authtok( + pamh, PAM_AUTHTOK, const_cast(&auth_tok_ptr), nullptr); + { + std::unique_lock lk(m); + if (confirmation_type == ConfirmationType::Unset) { + confirmation_type = ConfirmationType::Pam; + } + } + cv.notify_one(); - if (auth_tok) { + return std::tuple(pam_res, auth_tok_ptr); + }); + + // We ask for the password if the function requires it and if a workaround is + // set + if (auth_tok && workaround != Workaround::Off) { pass_task.activate(); } // Wait for the end either of the child or the password input { - unique_lock lk(m); - cv.wait(lk); - final_type = confirmation_type; + std::unique_lock lk(m); + cv.wait(lk, [&] { return confirmation_type != ConfirmationType::Unset; }); } - if (final_type == Type::Howdy) { - // We need to be sure that we're not going to block forever if the - // child has a problem - if (child_task.wait(3s) == future_status::timeout) { - kill(child_pid, SIGTERM); - } + // 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) { - // We cancel the thread using pthread, pam_get_authtok seems to be a - // cancellation point - if (pass_task.is_active()) { - pass_task.stop(true); - } - } - int howdy_status = child_task.get(); + // We just wait for the thread to stop since it's this one which sent us the + // confirmation type + pass_task.stop(false); - // If exited successfully - if (howdy_status == 0) { - if (!reader.GetBoolean("core", "no_confirmation", true)) { - // Construct confirmation text from i18n string - string confirm_text = dgettext("pam", "Identified face as {}"); - string identify_msg( - confirm_text.replace(confirm_text.find("{}"), 2, string(user_ptr))); - // Send confirmation message to user - conv_function(PAM_TEXT_INFO, identify_msg.c_str()); - } - syslog(LOG_INFO, "Login approved"); - return PAM_SUCCESS; - } else { - return on_howdy_auth(howdy_status, conv_function); + char *password = nullptr; + std::tie(pam_res, password) = pass_task.get(); + + if (pam_res != PAM_SUCCESS) { + return pam_res; } + + // The password has been entered, we are passing it to PAM stack + return PAM_IGNORE; } - // The branch with Howdy confirmation type returns early, so we don't need an - // else statement - - // Again, we need to be sure that we're not going to block forever if the - // child has a problem - if (child_task.wait(1.5s) == 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.stop(true); + } else if (workaround == Workaround::Input) { + // We check if we have the right permissions on /dev/uinput + if (euidaccess("/dev/uinput", W_OK | R_OK) != 0) { + syslog(LOG_WARNING, "Insufficient permissions to create the fake device"); + conv_function(PAM_ERROR_MSG, + S("Insufficient permissions to send Enter " + "press, waiting for user to press it instead")); + } else { + try { + EnterDevice enter_device; + int retries; + + // We try to send it + enter_device.send_enter_press(); + + for (retries = 0; + retries < MAX_RETRIES && + pass_task.wait(DEFAULT_TIMEOUT) == std::future_status::timeout; + retries++) { + enter_device.send_enter_press(); + } + + if (retries == MAX_RETRIES) { + syslog(LOG_WARNING, + "Failed to send enter input before the retries limit"); + conv_function(PAM_ERROR_MSG, S("Failed to send Enter press, waiting " + "for user to press it instead")); + } + } 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 press, waiting " + "for user to press it instead")); + } + } + + // 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); } - char *token = nullptr; - tie(pam_res, token) = pass_task.get(); + int status = child_task.get(); - if (pam_res != PAM_SUCCESS) - return pam_res; - - int howdy_status = child_task.get(); - if (strlen(token) == 0) { - if (howdy_status == 0) { - if (!reader.GetBoolean("core", "no_confirmation", true)) { - // Construct confirmation text from i18n string - string confirm_text = dgettext("pam", "Identified face as {}"); - string identify_msg( - confirm_text.replace(confirm_text.find("{}"), 2, string(user_ptr))); - // Send confirmation message to user - conv_function(PAM_TEXT_INFO, identify_msg.c_str()); - } - syslog(LOG_INFO, "Login approved"); - return PAM_SUCCESS; - } else { - return on_howdy_auth(howdy_status, conv_function); - } - } - - // The password has been entered, we are passing it to PAM stack - return PAM_IGNORE; + return howdy_status(username, status, config, conv_function); } // Called by PAM when a user needs to be authenticated, for example by running // the sudo command -PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, - const char **argv) { +PAM_EXTERN auto pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, + const char **argv) -> int { return identify(pamh, flags, argc, argv, true); } // Called by PAM when a session is started, such as by the su command -PAM_EXTERN int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, - const char **argv) { +PAM_EXTERN auto pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, + const char **argv) -> int { return identify(pamh, flags, argc, argv, false); } // The functions below are required by PAM, but not needed in this module -PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, - const char **argv) { +PAM_EXTERN auto pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, + const char **argv) -> int { return PAM_IGNORE; } -PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, - const char **argv) { +PAM_EXTERN auto pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, + const char **argv) -> int { return PAM_IGNORE; } -PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, - const char **argv) { +PAM_EXTERN auto pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, + const char **argv) -> int { return PAM_IGNORE; } -PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, - const char **argv) { +PAM_EXTERN auto pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, + const char **argv) -> int { return PAM_IGNORE; } diff --git a/howdy/src/pam/main.hh b/howdy/src/pam/main.hh index 6b75580..3d250fe 100644 --- a/howdy/src/pam/main.hh +++ b/howdy/src/pam/main.hh @@ -1,36 +1,30 @@ #ifndef MAIN_H_ #define MAIN_H_ -#include #include -enum class Type { Howdy, Pam }; +enum class ConfirmationType { Unset, Howdy, Pam }; enum class Workaround { Off, Input, Native }; -inline bool operator==(const std::string &l, const Workaround &r) { - switch (r) { - case Workaround::Off: - return (l == "off"); - case Workaround::Input: - return (l == "input"); - case Workaround::Native: - return (l == "native"); - default: - return false; +// Exit status codes returned by the compare process +enum CompareError : int { + NO_FACE_MODEL = 10, + TIMEOUT_REACHED = 11, + ABORT = 12, + TOO_DARK = 13 +}; + +inline auto get_workaround(const std::string &workaround) -> Workaround { + if (workaround == "input") { + return Workaround::Input; } -} -inline bool operator==(const Workaround &l, const std::string &r) { - return operator==(r, l); -} + if (workaround == "native") { + return Workaround::Native; + } -inline bool operator!=(const std::string &l, const Workaround &r) { - return !operator==(l, r); -} - -inline bool operator!=(const Workaround &l, const std::string &r) { - return operator!=(r, l); + return Workaround::Off; } #endif // MAIN_H_ diff --git a/howdy/src/pam/meson.build b/howdy/src/pam/meson.build index add0595..be93919 100644 --- a/howdy/src/pam/meson.build +++ b/howdy/src/pam/meson.build @@ -1,9 +1,24 @@ project('pam_howdy', 'cpp', version: '0.1.0', default_options: ['cpp_std=c++14']) -inih = subproject('inih') -inih_cpp = inih.get_variable('INIReader_dep') - -libpam = meson.get_compiler('c').find_library('pam') -boost = dependency('boost', modules: ['locale']) +inih_cpp = dependency('INIReader', fallback: ['inih', 'INIReader_dep']) +libevdev = dependency('libevdev') +libpam = meson.get_compiler('cpp').find_library('pam') threads = dependency('threads') -shared_library('pam_howdy', 'main.cc', dependencies: [boost, libpam, inih_cpp, threads], install: true, install_dir: '/lib/security/') + +# Translations +subdir('po') + +shared_library( + 'pam_howdy', + 'main.cc', + 'enter_device.cc', + dependencies: [ + libpam, + inih_cpp, + threads, + libevdev, + ], + install: true, + install_dir: '/lib/security', + name_prefix: '' +) diff --git a/howdy/src/pam/optional_task.hh b/howdy/src/pam/optional_task.hh index 827c535..36f9929 100644 --- a/howdy/src/pam/optional_task.hh +++ b/howdy/src/pam/optional_task.hh @@ -6,64 +6,76 @@ #include #include +// A task executed only if activated. template class optional_task { - std::thread _thread; - std::packaged_task _task; - std::future _future; - std::atomic _spawned; - std::atomic _is_active; + std::thread thread; + std::packaged_task task; + std::future future; + bool spawned; + bool is_active; public: - optional_task(std::packaged_task); + explicit optional_task(std::function fn); void activate(); - template std::future_status wait(std::chrono::duration); - T get(); - bool is_active(); - void stop(bool); + template + auto wait(std::chrono::duration dur) -> std::future_status; + auto get() -> T; + void stop(bool force); ~optional_task(); }; template -optional_task::optional_task(std::packaged_task t) - : _task(std::move(t)), _future(_task.get_future()) {} +optional_task::optional_task(std::function fn) + : task(std::packaged_task(std::move(fn))), future(task.get_future()), + spawned(false), is_active(false) {} +// Create a new thread and launch the task on it. template void optional_task::activate() { - _thread = std::thread(std::move(_task)); - _spawned = true; - _is_active = true; + thread = std::thread(std::move(task)); + spawned = true; + is_active = true; } +// Wait for `dur` time and return a `future` status. template -template -std::future_status optional_task::wait(std::chrono::duration dur) { - return _future.wait_for(dur); +template +auto optional_task::wait(std::chrono::duration dur) + -> std::future_status { + return future.wait_for(dur); } -template T optional_task::get() { - assert(!_is_active && _spawned); - return _future.get(); +// Get the value. +// WARNING: The function hould be run only if the task has successfully been +// stopped. +template auto optional_task::get() -> T { + assert(!is_active && spawned); + return future.get(); } -template bool optional_task::is_active() { return _is_active; } - +// Stop the thread: +// - if `force` is `false`, by joining the thread. +// - if `force` is `true`, by cancelling the thread using `pthread_cancel`. +// WARNING: This function should be used with extreme caution when `force` is +// set to `true`. template void optional_task::stop(bool force) { - if (!(_is_active && _thread.joinable()) && _spawned) { - _is_active = false; + if (!(is_active && thread.joinable()) && spawned) { + is_active = false; return; } // We use pthread to cancel the thread if (force) { - auto native_hd = _thread.native_handle(); + auto native_hd = thread.native_handle(); pthread_cancel(native_hd); } - _thread.join(); - _is_active = false; + thread.join(); + is_active = false; } template optional_task::~optional_task() { - if (_is_active && _spawned) + if (is_active && spawned) { stop(false); + } } #endif // OPTIONAL_TASK_H_ diff --git a/howdy/src/pam/po/LINGUAS b/howdy/src/pam/po/LINGUAS new file mode 100644 index 0000000..e69de29 diff --git a/howdy/src/pam/po/POTFILES b/howdy/src/pam/po/POTFILES new file mode 100644 index 0000000..11b5c00 --- /dev/null +++ b/howdy/src/pam/po/POTFILES @@ -0,0 +1 @@ +main.cc \ No newline at end of file diff --git a/howdy/src/pam/po/meson.build b/howdy/src/pam/po/meson.build new file mode 100644 index 0000000..762f7a4 --- /dev/null +++ b/howdy/src/pam/po/meson.build @@ -0,0 +1,10 @@ +i18n = import('i18n') + +# define GETTEXT_PACKAGE and LOCALEDIR +gettext_package = '-DGETTEXT_PACKAGE="@0@"'.format(meson.project_name()) +localedir = '-DLOCALEDIR="@0@"'.format(get_option('prefix') / get_option('localedir')) +add_project_arguments(gettext_package, localedir, language: 'cpp') + +i18n.gettext(meson.project_name(), + args: [ '--directory=' + meson.source_root(), '--keyword=S:1' ] +) \ No newline at end of file diff --git a/howdy/src/pam/subprojects/inih.wrap b/howdy/src/pam/subprojects/inih.wrap index 8d02b50..5423393 100644 --- a/howdy/src/pam/subprojects/inih.wrap +++ b/howdy/src/pam/subprojects/inih.wrap @@ -1,3 +1,13 @@ -[wrap-git] -url = https://github.com/benhoyt/inih.git -revision = r52 +[wrap-file] +directory = inih-r53 +source_url = https://github.com/benhoyt/inih/archive/r53.tar.gz +source_filename = inih-r53.tar.gz +source_hash = 01b0366fdfdf6363efc070c2f856f1afa33e7a6546548bada5456ad94a516241 +patch_url = https://wrapdb.mesonbuild.com/v2/inih_r53-1/get_patch +patch_filename = inih-r53-1-wrap.zip +patch_hash = 9a53348e4ed9180a52aafc092fda080ddc70102c9fc55686990e461b22e6e1e7 + +[provide] +inih = inih_dep +inireader = inireader_dep +