diff --git a/src/pam/.clang-tidy b/howdy/src/pam/.clang-tidy similarity index 100% rename from src/pam/.clang-tidy rename to howdy/src/pam/.clang-tidy 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/src/pam/enter_device.cc b/howdy/src/pam/enter_device.cc similarity index 100% rename from src/pam/enter_device.cc rename to howdy/src/pam/enter_device.cc diff --git a/src/pam/enter_device.hh b/howdy/src/pam/enter_device.hh similarity index 100% rename from src/pam/enter_device.hh rename to howdy/src/pam/enter_device.hh 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/src/pam/po/LINGUAS b/howdy/src/pam/po/LINGUAS similarity index 100% rename from src/pam/po/LINGUAS rename to howdy/src/pam/po/LINGUAS diff --git a/src/pam/po/POTFILES b/howdy/src/pam/po/POTFILES similarity index 100% rename from src/pam/po/POTFILES rename to howdy/src/pam/po/POTFILES diff --git a/src/pam/po/meson.build b/howdy/src/pam/po/meson.build similarity index 100% rename from src/pam/po/meson.build rename to howdy/src/pam/po/meson.build 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 + diff --git a/src/pam/.gitignore b/src/pam/.gitignore deleted file mode 100644 index feaa494..0000000 --- a/src/pam/.gitignore +++ /dev/null @@ -1 +0,0 @@ -subprojects/*/ diff --git a/src/pam/README.md b/src/pam/README.md deleted file mode 100644 index 23d1804..0000000 --- a/src/pam/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# 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 -meson compile -C build -``` - -## Install - -``` sh -meson install -C build -``` - -Add the following line to your PAM configuration (/etc/pam.d/your-service): - -``` pam -auth sufficient pam_howdy.so -``` diff --git a/src/pam/main.cc b/src/pam/main.cc deleted file mode 100644 index 530d5c8..0000000 --- a/src/pam/main.cc +++ /dev/null @@ -1,428 +0,0 @@ -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include - -#include "enter_device.hh" -#include "main.hh" -#include "optional_task.hh" - -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 status The status code - * @param conv_function The PAM conversation function - * @return A PAM return code - */ -auto howdy_error(int status, - const std::function &conv_function) - -> int { - // If the process has exited - if (WIFEXITED(status)) { - // Get the status code returned - status = WEXITSTATUS(status); - - 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; - case CompareError::TIMEOUT_REACHED: - syslog(LOG_ERR, "Failure, timeout reached"); - break; - case CompareError::ABORT: - syslog(LOG_ERR, "Failure, general abort"); - break; - case CompareError::TOO_DARK: - conv_function(PAM_ERROR_MSG, S("Face detection image too dark")); - syslog(LOG_ERR, "Failure, image too dark"); - break; - default: - 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 - // PAM - return PAM_AUTH_ERR; -} - -/** - * 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 - */ -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); - } - - 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()); - } - - 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; -} - -/** - * The main function, runs the identification and authentication - * @param pamh The handle to interface directly with PAM - * @param flags Flags passed on to us by PAM, XORed - * @param argc Amount of rules in the PAM config (disregared) - * @param argv Options defined in the PAM config - * @param auth_tok True if we should ask for a password too - * @return Returns a PAM return code - */ -auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, - bool auth_tok) -> int { - INIReader config("/lib/security/howdy/config.ini"); - openlog("pam_howdy", 0, LOG_AUTHPRIV); - - // 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; - } - - // Will contain the responses from PAM functions - int pam_res = PAM_IGNORE; - - // 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 = [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; - - struct pam_response res = {}; - struct pam_response *resp = &res; - - return conv->conv(1, &msgp, &resp, conv->appdata_ptr); - }; - - // 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 (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 *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; - } - - 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, 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; - } - - // 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([&] { - int status; - wait(&status); - { - std::unique_lock lk(m); - if (confirmation_type == ConfirmationType::Unset) { - confirmation_type = ConfirmationType::Howdy; - } - } - cv.notify_one(); - - return status; - }); - child_task.activate(); - - // This task waits for the password input (if the workaround wants it) - 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(); - - 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 - { - std::unique_lock lk(m); - cv.wait(lk, [&] { return confirmation_type != ConfirmationType::Unset; }); - } - - // 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); - - // 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; - } - - // The password has been entered, we are passing it to PAM stack - return PAM_IGNORE; - } - - // The compare process has finished its execution - child_task.stop(false); - - // 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); - } - - int status = child_task.get(); - - 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 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 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 auto pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, - const char **argv) -> int { - return PAM_IGNORE; -} -PAM_EXTERN auto pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, - const char **argv) -> int { - return PAM_IGNORE; -} -PAM_EXTERN auto pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, - const char **argv) -> int { - return PAM_IGNORE; -} -PAM_EXTERN auto pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, - const char **argv) -> int { - return PAM_IGNORE; -} diff --git a/src/pam/main.hh b/src/pam/main.hh deleted file mode 100644 index 3d250fe..0000000 --- a/src/pam/main.hh +++ /dev/null @@ -1,30 +0,0 @@ -#ifndef MAIN_H_ -#define MAIN_H_ - -#include - -enum class ConfirmationType { Unset, Howdy, Pam }; - -enum class Workaround { Off, Input, Native }; - -// 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; - } - - if (workaround == "native") { - return Workaround::Native; - } - - return Workaround::Off; -} - -#endif // MAIN_H_ diff --git a/src/pam/meson.build b/src/pam/meson.build deleted file mode 100644 index be93919..0000000 --- a/src/pam/meson.build +++ /dev/null @@ -1,24 +0,0 @@ -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') - -# 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/src/pam/optional_task.hh b/src/pam/optional_task.hh deleted file mode 100644 index 36f9929..0000000 --- a/src/pam/optional_task.hh +++ /dev/null @@ -1,81 +0,0 @@ -#ifndef OPTIONAL_TASK_H_ -#define OPTIONAL_TASK_H_ - -#include -#include -#include -#include - -// A task executed only if activated. -template class optional_task { - std::thread thread; - std::packaged_task task; - std::future future; - bool spawned; - bool is_active; - -public: - explicit optional_task(std::function fn); - void activate(); - 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::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; -} - -// Wait for `dur` time and return a `future` status. -template -template -auto optional_task::wait(std::chrono::duration dur) - -> std::future_status { - return future.wait_for(dur); -} - -// 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(); -} - -// 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; - return; - } - - // We use pthread to cancel the thread - if (force) { - auto native_hd = thread.native_handle(); - pthread_cancel(native_hd); - } - thread.join(); - is_active = false; -} - -template optional_task::~optional_task() { - if (is_active && spawned) { - stop(false); - } -} - -#endif // OPTIONAL_TASK_H_ diff --git a/src/pam/subprojects/inih.wrap b/src/pam/subprojects/inih.wrap deleted file mode 100644 index 5423393..0000000 --- a/src/pam/subprojects/inih.wrap +++ /dev/null @@ -1,13 +0,0 @@ -[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 -