From 609b8e99df5c76d905861d60d0bc540c89d105bd Mon Sep 17 00:00:00 2001 From: MusiKid Date: Sat, 26 Dec 2020 09:33:19 +0100 Subject: [PATCH 01/61] [WIP]: Add native PAM module Signed-off-by: MusiKid --- howdy/src/compare.py | 17 ++- src/pam/.gitignore | 1 + src/pam/README.md | 8 ++ src/pam/main.cc | 202 ++++++++++++++++++++++++++++++++++ src/pam/meson.build | 9 ++ src/pam/subprojects/inih.wrap | 3 + 6 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 src/pam/.gitignore create mode 100644 src/pam/README.md create mode 100644 src/pam/main.cc create mode 100644 src/pam/meson.build create mode 100644 src/pam/subprojects/inih.wrap diff --git a/howdy/src/compare.py b/howdy/src/compare.py index 3fd7f35..6cb346f 100644 --- a/howdy/src/compare.py +++ b/howdy/src/compare.py @@ -25,7 +25,7 @@ import _thread as thread from i18n import _ from recorders.video_capture import VideoCapture - +from evdev import UInput, ecodes as e def exit(code=None): """Exit while closeing howdy-gtk properly""" @@ -374,6 +374,21 @@ while True: "clahe": clahe }) + # Press enter key + if config.getboolean("experimental", "confirm"): + pipe_fd = int(os.getenv("PIPE_FD")) + pipe = os.fdopen(pipe_fd, 'w') + pipe.write('\255') + + enter_cap = { + e.EV_KEY: [e.KEY_ENTER] + } + device = UInput(enter_cap) + device.write(e.EV_KEY, e.KEY_ENTER, 1) + device.syn() + device.write(e.EV_KEY, e.KEY_ENTER, 0) + device.syn() + # End peacefully exit(0) diff --git a/src/pam/.gitignore b/src/pam/.gitignore new file mode 100644 index 0000000..feaa494 --- /dev/null +++ b/src/pam/.gitignore @@ -0,0 +1 @@ +subprojects/*/ diff --git a/src/pam/README.md b/src/pam/README.md new file mode 100644 index 0000000..736b68c --- /dev/null +++ b/src/pam/README.md @@ -0,0 +1,8 @@ +# Howdy PAM module + +## Build + +```sh +meson setup --wipe build -Dinih:with_INIReader=true +meson compile build +``` diff --git a/src/pam/main.cc b/src/pam/main.cc new file mode 100644 index 0000000..fa15083 --- /dev/null +++ b/src/pam/main.cc @@ -0,0 +1,202 @@ +#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 + +using namespace std; + +int on_howdy_auth(int code, function conv_function) { + + switch (code) { + case 10: + conv_function(PAM_ERROR_MSG, "There is no face model known"); + syslog(LOG_NOTICE, "Failure, no face model known"); + break; + case 11: + syslog(LOG_INFO, "Failure, timeout reached"); + break; + case 12: + syslog(LOG_INFO, "Failure, general abort"); + break; + case 13: + syslog(LOG_INFO, "Failure, image too dark"); + break; + default: + conv_function(PAM_ERROR_MSG, + string("Unknown error:" + to_string(code)).c_str()); + syslog(LOG_INFO, "Failure, unknown error %d", code); + } + + return PAM_AUTH_ERR; +} + +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; + + struct pam_response res_ = {}; + struct pam_response *resp_ = &res_; + + return conv(1, &msgp, &resp_, nullptr); +} + +PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, + const char **argv) { + INIReader reader("/lib/security/howdy/config.ini"); + openlog("[PAM_HOWDY]", 0, LOG_AUTHPRIV); + + struct pam_conv *conv = nullptr; + int pam_res = PAM_IGNORE; + + if ((pam_res = pam_get_item(pamh, PAM_CONV, (const void **)&conv)) != + PAM_SUCCESS) { + syslog(LOG_ERR, "Failed to acquire conversation"); + return pam_res; + } + + auto conv_function = + bind(send_message, (*conv->conv), placeholders::_1, placeholders::_2); + + if (reader.ParseError() < 0) { + syslog(LOG_ERR, "Failed to parse the configuration"); + return PAM_SYSTEM_ERR; + } + + if (reader.GetBoolean("core", "disabled", false)) { + return PAM_AUTHINFO_UNAVAIL; + } + + if (reader.GetBoolean("core", "ignore_ssh", true)) { + if (getenv("SSH_CONNECTION") != nullptr || + getenv("SSH_CLIENT") != nullptr || getenv("SSHD_OPTS") != nullptr) { + return PAM_AUTHINFO_UNAVAIL; + } + } + + if (reader.GetBoolean("core", "detection_notice", false)) { + if ((pam_res = conv_function(PAM_TEXT_INFO, + "Attempting facial authentication")) != + PAM_SUCCESS) { + syslog(LOG_ERR, "Failed to send detection notice"); + return pam_res; + } + } + + if (reader.GetBoolean("core", "ignore_closed_lid", true)) { + glob_t glob_result{}; + + 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); + return PAM_AUTHINFO_UNAVAIL; + } + } + + globfree(&glob_result); + } + + char *user_ptr = nullptr; + if ((pam_res = pam_get_user(pamh, (const char **)&user_ptr, nullptr)) != + PAM_SUCCESS) { + syslog(LOG_ERR, "Failed to get username"); + return pam_res; + } + string user(user_ptr); + + posix_spawn_file_actions_t file_actions; + posix_spawn_file_actions_init(&file_actions); + posix_spawn_file_actions_addclose(&file_actions, STDOUT_FILENO); + posix_spawn_file_actions_addclose(&file_actions, STDERR_FILENO); + vector args{"python", "/lib/security/howdy/compare.py", + user.c_str(), nullptr}; + pid_t child_pid; + if (posix_spawnp(&child_pid, "python", &file_actions, nullptr, + (char *const *)args.data(), nullptr) < 0) { + syslog(LOG_ERR, "Can't spawn the howdy process: %s", strerror(errno)); + return PAM_SYSTEM_ERR; + } + + packaged_task child_task([&] { + int status; + wait(&status); + return status; + }); + auto child_future = child_task.get_future(); + thread child_thread(move(child_task)); + + auto pass_future = async(launch::async, [&] { + char *auth_tok_ptr = nullptr; + int pam_res = pam_get_authtok(pamh, PAM_AUTHTOK, + (const char **)&auth_tok_ptr, nullptr); + return make_pair(auth_tok_ptr, pam_res); + }); + + auto pass = pass_future.get(); + + if (child_future.wait_for(1.5s) == future_status::timeout) { + kill(child_pid, SIGTERM); + } + child_thread.join(); + int howdy_status = child_future.get(); + + if (howdy_status == 0) { + if (!reader.GetBoolean("section", "no_confirmation", true)) { + string identify_msg("Identified face as " + user); + conv_function(PAM_TEXT_INFO, identify_msg.c_str()); + } + + syslog(LOG_INFO, "Login approved"); + return PAM_SUCCESS; + } else if ((get(pass) == PAM_SUCCESS && get(pass) != nullptr && + !string(get(pass)).empty()) || + WIFSIGNALED(howdy_status)) { + return PAM_IGNORE; + } else { + return on_howdy_auth(howdy_status, conv_function); + } +} diff --git a/src/pam/meson.build b/src/pam/meson.build new file mode 100644 index 0000000..efe07fa --- /dev/null +++ b/src/pam/meson.build @@ -0,0 +1,9 @@ +project('pam_howdy', 'cpp', version: '0.1.0') + +inih = subproject('inih') +inih_cpp = inih.get_variable('INIReader_dep') + +libpam = meson.get_compiler('c').find_library('pam') +boost = dependency('boost', modules: ['filesystem']) +threads = dependency('threads') +shared_library('pam_howdy', 'main.cc', dependencies: [boost, libpam, inih_cpp, threads], install: true, install_dir: '/lib/security/') diff --git a/src/pam/subprojects/inih.wrap b/src/pam/subprojects/inih.wrap new file mode 100644 index 0000000..8d02b50 --- /dev/null +++ b/src/pam/subprojects/inih.wrap @@ -0,0 +1,3 @@ +[wrap-git] +url = https://github.com/benhoyt/inih.git +revision = r52 From beee0590ede438e899ea44b06275a50726c08da4 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Sun, 17 Jan 2021 17:33:47 +0100 Subject: [PATCH 02/61] Fix the build instructions Signed-off-by: MusiKid --- src/pam/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pam/README.md b/src/pam/README.md index 736b68c..4b95a12 100644 --- a/src/pam/README.md +++ b/src/pam/README.md @@ -1,8 +1,9 @@ # Howdy PAM module -## Build +## Building (for now) ```sh -meson setup --wipe build -Dinih:with_INIReader=true +meson setup build -Dinih:with_INIReader=true meson compile build +sudo mv build/libpam_howdy.so /lib/security/ ``` From 50d52ec423484fea375021f1687f3bf5fbf6a6cb Mon Sep 17 00:00:00 2001 From: MusiKid Date: Mon, 18 Jan 2021 14:46:59 +0100 Subject: [PATCH 03/61] Remove uinput Signed-off-by: MusiKid --- howdy/src/compare.py | 16 ----- src/pam/README.md | 2 +- src/pam/main.cc | 167 +++++++++++++++++++++++++++++-------------- 3 files changed, 116 insertions(+), 69 deletions(-) diff --git a/howdy/src/compare.py b/howdy/src/compare.py index 6cb346f..9d07dd7 100644 --- a/howdy/src/compare.py +++ b/howdy/src/compare.py @@ -25,7 +25,6 @@ import _thread as thread from i18n import _ from recorders.video_capture import VideoCapture -from evdev import UInput, ecodes as e def exit(code=None): """Exit while closeing howdy-gtk properly""" @@ -374,21 +373,6 @@ while True: "clahe": clahe }) - # Press enter key - if config.getboolean("experimental", "confirm"): - pipe_fd = int(os.getenv("PIPE_FD")) - pipe = os.fdopen(pipe_fd, 'w') - pipe.write('\255') - - enter_cap = { - e.EV_KEY: [e.KEY_ENTER] - } - device = UInput(enter_cap) - device.write(e.EV_KEY, e.KEY_ENTER, 1) - device.syn() - device.write(e.EV_KEY, e.KEY_ENTER, 0) - device.syn() - # End peacefully exit(0) diff --git a/src/pam/README.md b/src/pam/README.md index 4b95a12..cac6f3d 100644 --- a/src/pam/README.md +++ b/src/pam/README.md @@ -5,5 +5,5 @@ ```sh meson setup build -Dinih:with_INIReader=true meson compile build -sudo mv build/libpam_howdy.so /lib/security/ +sudo mv build/libpam_howdy.so /lib/security/pam_howdy.so ``` diff --git a/src/pam/main.cc b/src/pam/main.cc index fa15083..a37b061 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -21,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -35,28 +37,31 @@ using namespace std; +enum class Type { Howdy, Pam }; + int on_howdy_auth(int code, function conv_function) { - - switch (code) { - case 10: - conv_function(PAM_ERROR_MSG, "There is no face model known"); - syslog(LOG_NOTICE, "Failure, no face model known"); - break; - case 11: - syslog(LOG_INFO, "Failure, timeout reached"); - break; - case 12: - syslog(LOG_INFO, "Failure, general abort"); - break; - case 13: - syslog(LOG_INFO, "Failure, image too dark"); - break; - default: - conv_function(PAM_ERROR_MSG, - string("Unknown error:" + to_string(code)).c_str()); - syslog(LOG_INFO, "Failure, unknown error %d", code); + if (WIFEXITED(code)) { + code = WEXITSTATUS(code); + switch (code) { + case 10: + conv_function(PAM_ERROR_MSG, "There is no face model known"); + syslog(LOG_NOTICE, "Failure, no face model known"); + break; + case 11: + syslog(LOG_INFO, "Failure, timeout reached"); + break; + case 12: + syslog(LOG_INFO, "Failure, general abort"); + break; + case 13: + syslog(LOG_INFO, "Failure, image too dark"); + break; + default: + conv_function(PAM_ERROR_MSG, + string("Unknown error:" + to_string(code)).c_str()); + syslog(LOG_INFO, "Failure, unknown error %d", code); + } } - return PAM_AUTH_ERR; } @@ -74,10 +79,10 @@ int send_message(functionconv), placeholders::_1, placeholders::_2); + bind(send_message, conv->conv, placeholders::_1, placeholders::_2); if (reader.ParseError() < 0) { syslog(LOG_ERR, "Failed to parse the configuration"); @@ -146,57 +151,115 @@ PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, syslog(LOG_ERR, "Failed to get username"); return pam_res; } - string user(user_ptr); - posix_spawn_file_actions_t file_actions; posix_spawn_file_actions_init(&file_actions); posix_spawn_file_actions_addclose(&file_actions, STDOUT_FILENO); posix_spawn_file_actions_addclose(&file_actions, STDERR_FILENO); - vector args{"python", "/lib/security/howdy/compare.py", - user.c_str(), nullptr}; + const char *const args[] = {"python", "/lib/security/howdy/compare.py", + user_ptr, nullptr}; pid_t child_pid; if (posix_spawnp(&child_pid, "python", &file_actions, nullptr, - (char *const *)args.data(), nullptr) < 0) { + (char *const *)args, nullptr) < 0) { syslog(LOG_ERR, "Can't spawn the howdy process: %s", strerror(errno)); return PAM_SYSTEM_ERR; } + std::mutex m; + std::condition_variable cv; + Type t; packaged_task child_task([&] { int status; wait(&status); + { + unique_lock lk(m); + t = Type::Howdy; + } + cv.notify_all(); return status; }); auto child_future = child_task.get_future(); thread child_thread(move(child_task)); - auto pass_future = async(launch::async, [&] { + packaged_task pass_task([&] { char *auth_tok_ptr = nullptr; int pam_res = pam_get_authtok(pamh, PAM_AUTHTOK, (const char **)&auth_tok_ptr, nullptr); - return make_pair(auth_tok_ptr, pam_res); - }); - - auto pass = pass_future.get(); - - if (child_future.wait_for(1.5s) == future_status::timeout) { - kill(child_pid, SIGTERM); - } - child_thread.join(); - int howdy_status = child_future.get(); - - if (howdy_status == 0) { - if (!reader.GetBoolean("section", "no_confirmation", true)) { - string identify_msg("Identified face as " + user); - conv_function(PAM_TEXT_INFO, identify_msg.c_str()); + { + unique_lock lk(m); + t = Type::Pam; } + cv.notify_all(); + return pam_res; + }); + auto pass_future = pass_task.get_future(); + thread pass_thread; + if (auth_tok) { + pass_thread = thread(move(pass_task)); + } - syslog(LOG_INFO, "Login approved"); - return PAM_SUCCESS; - } else if ((get(pass) == PAM_SUCCESS && get(pass) != nullptr && - !string(get(pass)).empty()) || - WIFSIGNALED(howdy_status)) { - return PAM_IGNORE; + { + unique_lock lk(m); + cv.wait(lk); + } + + if (t == Type::Howdy) { + if (auth_tok) { + auto native_hd = pass_thread.native_handle(); + pthread_cancel(native_hd); + pass_thread.join(); + } + child_thread.join(); + int howdy_status = child_future.get(); + + if (howdy_status == 0) { + if (!reader.GetBoolean("section", "no_confirmation", true)) { + string identify_msg("Identified face as " + string(user_ptr)); + 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); + } } else { - return on_howdy_auth(howdy_status, conv_function); + kill(child_pid, SIGTERM); + child_thread.join(); + pass_thread.join(); + auto pam_res = pass_future.get(); + + if (pam_res != PAM_SUCCESS) + return pam_res; + + return PAM_IGNORE; } } + +PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, + const char **argv) { + return identify(pamh, flags, argc, argv, true); +} + +PAM_EXTERN int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, + const char **argv) { + return identify(pamh, flags, argc, argv, false); +} + +PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, + const char **argv) { + return PAM_IGNORE; +} + +PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, + const char **argv) { + return PAM_IGNORE; +} + +PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, + const char **argv) { + return PAM_IGNORE; +} + +PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, + const char **argv) { + return PAM_IGNORE; +} From 322899dfaa19e6d33b85ed665ec1f813b7354aaa Mon Sep 17 00:00:00 2001 From: MusiKid Date: Mon, 15 Mar 2021 19:12:17 +0100 Subject: [PATCH 04/61] Fix typo in build instructions Signed-off-by: MusiKid --- src/pam/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pam/README.md b/src/pam/README.md index cac6f3d..8cf5c0f 100644 --- a/src/pam/README.md +++ b/src/pam/README.md @@ -1,9 +1,9 @@ # Howdy PAM module -## Building (for now) +## Build ```sh meson setup build -Dinih:with_INIReader=true -meson compile build +meson compile -C build sudo mv build/libpam_howdy.so /lib/security/pam_howdy.so ``` From 2287bc14f81d51b5a8bdc48807ec5a3f7ad91aa4 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Mon, 15 Mar 2021 19:51:04 +0100 Subject: [PATCH 05/61] Add C++11 flag for meson Signed-off-by: MusiKid --- src/pam/meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pam/meson.build b/src/pam/meson.build index efe07fa..bd2adf1 100644 --- a/src/pam/meson.build +++ b/src/pam/meson.build @@ -1,4 +1,4 @@ -project('pam_howdy', 'cpp', version: '0.1.0') +project('pam_howdy', 'cpp', version: '0.1.0', default_options: ['cpp_std=c++11']) inih = subproject('inih') inih_cpp = inih.get_variable('INIReader_dep') From fce326d19a96690fc5ea9373eac1d223d41e1100 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Thu, 18 Mar 2021 23:08:55 +0100 Subject: [PATCH 06/61] Made python3 absolute, added comments --- src/pam/main.cc | 81 ++++++++++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index a37b061..4b32c97 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -39,29 +39,45 @@ using namespace std; enum class Type { Howdy, Pam }; +/** + * Inspect the status code returned by the compare process + * @param code The status code + * @param conv_function The PAM conversation function + * @return A PAM return code + */ int on_howdy_auth(int code, function conv_function) { - if (WIFEXITED(code)) { + // If the process has exited + if (!WIFEXITED(code)) { + // Get the status code returned code = WEXITSTATUS(code); + switch (code) { - case 10: - conv_function(PAM_ERROR_MSG, "There is no face model known"); - syslog(LOG_NOTICE, "Failure, no face model known"); - break; - case 11: - syslog(LOG_INFO, "Failure, timeout reached"); - break; - case 12: - syslog(LOG_INFO, "Failure, general abort"); - break; - case 13: - syslog(LOG_INFO, "Failure, image too dark"); - break; - default: - conv_function(PAM_ERROR_MSG, - string("Unknown error:" + to_string(code)).c_str()); - syslog(LOG_INFO, "Failure, unknown error %d", code); + // Status 10 means we couldn't find any face models + case 10: + conv_function(PAM_ERROR_MSG, "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"); + break; + // Status 12 means we aborted + case 12: + syslog(LOG_INFO, "Failure, general abort"); + break; + // Status 13 means the image was too dark + case 13: + conv_function(PAM_ERROR_MSG, "Face detection image too dark"); + syslog(LOG_INFO, "Failure, image too dark"); + break; + // Otherwise, we can't discribe what happend but it wasn't successful + default: + conv_function(PAM_ERROR_MSG, string("Unknown error:" + to_string(code)).c_str()); + syslog(LOG_INFO, "Failure, unknown error %d", code); } } + + // As this function is only called for error status codes, signal an error to PAM return PAM_AUTH_ERR; } @@ -155,10 +171,11 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, posix_spawn_file_actions_init(&file_actions); posix_spawn_file_actions_addclose(&file_actions, STDOUT_FILENO); posix_spawn_file_actions_addclose(&file_actions, STDERR_FILENO); - const char *const args[] = {"python", "/lib/security/howdy/compare.py", + const char *const args[] = {"/usr/bin/python3", "/lib/security/howdy/compare.py", user_ptr, nullptr}; pid_t child_pid; - if (posix_spawnp(&child_pid, "python", &file_actions, nullptr, + + 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)); return PAM_SYSTEM_ERR; @@ -234,32 +251,26 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, } } -PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, - const char **argv) { +// 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) { return identify(pamh, flags, argc, argv, true); } -PAM_EXTERN int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, - const char **argv) { +// 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) { return identify(pamh, flags, argc, argv, false); } -PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, - const char **argv) { +// 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) { return PAM_IGNORE; } - -PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, - const char **argv) { +PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { return PAM_IGNORE; } - -PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, - const char **argv) { +PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, const char **argv) { return PAM_IGNORE; } - -PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, - const char **argv) { +PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) { return PAM_IGNORE; } From a2e89f004fe86b58b733870e53d39612e30e4ed1 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Fri, 19 Mar 2021 15:11:10 +0100 Subject: [PATCH 07/61] More documentation --- src/pam/main.cc | 58 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index 4b32c97..4faa246 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -50,7 +50,7 @@ int on_howdy_auth(int code, function conv_function) { if (!WIFEXITED(code)) { // Get the status code returned code = WEXITSTATUS(code); - + switch (code) { // Status 10 means we couldn't find any face models case 10: @@ -81,59 +81,78 @@ int on_howdy_auth(int code, function conv_function) { return PAM_AUTH_ERR; } -int send_message(function - conv, - int type, const char *message) { +/** + * 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 + */ +int send_message(function conv, int type, const char *message) { + // Formet the message as PAM expects it // 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; + // Create a variable for the response to be stored in struct pam_response res_ = {}; struct pam_response *resp_ = &res_; + // Call the conversation function with the constructed arguments return conv(1, &msgp, &resp_, nullptr); } -int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, - bool auth_tok) { +/** + * 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 + */ +int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, bool auth_tok) { + // Open and read the config file INIReader reader("/lib/security/howdy/config.ini"); - openlog("pam_howdy.so", 0, LOG_AUTHPRIV); + // Open the system log so we can write to it + openlog("pam_howdy", 0, LOG_AUTHPRIV); + // Will contain PAM conversation function struct pam_conv *conv = nullptr; + // Will contain the responses from PAM functions int pam_res = PAM_IGNORE; - if ((pam_res = pam_get_item(pamh, PAM_CONV, (const void **)&conv)) != - PAM_SUCCESS) { + // 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) { syslog(LOG_ERR, "Failed to acquire conversation"); return pam_res; } - auto conv_function = - bind(send_message, conv->conv, placeholders::_1, placeholders::_2); + // Wrap the PAM conversation function in our own, easier function + auto conv_function = bind(send_message, conv->conv, placeholders::_1, placeholders::_2); + // Error out if we could not ready the config file if (reader.ParseError() < 0) { syslog(LOG_ERR, "Failed to parse the configuration"); return PAM_SYSTEM_ERR; } + // Stop executing if Howdy has been disabled in the config if (reader.GetBoolean("core", "disabled", false)) { return PAM_AUTHINFO_UNAVAIL; } + // 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) { + if (getenv("SSH_CONNECTION") != nullptr || getenv("SSH_CLIENT") != nullptr || getenv("SSHD_OPTS") != nullptr) { return PAM_AUTHINFO_UNAVAIL; } } + // 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, - "Attempting facial authentication")) != - PAM_SUCCESS) { + if ((pam_res = conv_function(PAM_TEXT_INFO, "Attempting facial authentication")) != PAM_SUCCESS) { syslog(LOG_ERR, "Failed to send detection notice"); - return pam_res; } } @@ -199,8 +218,7 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, packaged_task pass_task([&] { char *auth_tok_ptr = nullptr; - int pam_res = pam_get_authtok(pamh, PAM_AUTHTOK, - (const char **)&auth_tok_ptr, nullptr); + int pam_res = pam_get_authtok(pamh, PAM_AUTHTOK, (const char **) &auth_tok_ptr, nullptr); { unique_lock lk(m); t = Type::Pam; From 02a831ff8ee943ac988240b779cd03cc09f1ea59 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Wed, 7 Apr 2021 11:14:07 +0200 Subject: [PATCH 08/61] Fix a typo for the login confirmation --- src/pam/main.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index 4faa246..714e60a 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -247,7 +247,7 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, bool au int howdy_status = child_future.get(); if (howdy_status == 0) { - if (!reader.GetBoolean("section", "no_confirmation", true)) { + if (!reader.GetBoolean("core", "no_confirmation", true)) { string identify_msg("Identified face as " + string(user_ptr)); conv_function(PAM_TEXT_INFO, identify_msg.c_str()); } From 3abc3d5cdc0b5edeefdefc0da434e8ebc06c0484 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Wed, 7 Apr 2021 11:15:01 +0200 Subject: [PATCH 09/61] Add more documentation --- src/pam/main.cc | 98 ++++++++++++++++++++++++++++++------------------- 1 file changed, 60 insertions(+), 38 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index 714e60a..aa2e1af 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -52,32 +52,34 @@ int on_howdy_auth(int code, function conv_function) { code = WEXITSTATUS(code); switch (code) { - // Status 10 means we couldn't find any face models - case 10: - conv_function(PAM_ERROR_MSG, "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"); - break; - // Status 12 means we aborted - case 12: - syslog(LOG_INFO, "Failure, general abort"); - break; - // Status 13 means the image was too dark - case 13: - conv_function(PAM_ERROR_MSG, "Face detection image too dark"); - syslog(LOG_INFO, "Failure, image too dark"); - break; - // Otherwise, we can't discribe what happend but it wasn't successful - default: - conv_function(PAM_ERROR_MSG, string("Unknown error:" + to_string(code)).c_str()); - syslog(LOG_INFO, "Failure, unknown error %d", code); + // Status 10 means we couldn't find any face models + case 10: + conv_function(PAM_ERROR_MSG, "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"); + break; + // Status 12 means we aborted + case 12: + syslog(LOG_INFO, "Failure, general abort"); + break; + // Status 13 means the image was too dark + case 13: + conv_function(PAM_ERROR_MSG, "Face detection image too dark"); + syslog(LOG_INFO, "Failure, image too dark"); + break; + // Otherwise, we can't discribe what happend but it wasn't successful + default: + conv_function(PAM_ERROR_MSG, + string("Unknown error:" + to_string(code)).c_str()); + syslog(LOG_INFO, "Failure, unknown error %d", code); } } - // As this function is only called for error status codes, signal an error to PAM + // As this function is only called for error status codes, signal an error to + // PAM return PAM_AUTH_ERR; } @@ -88,7 +90,10 @@ int on_howdy_auth(int code, function conv_function) { * @param message String to show the user * @return Returns the conversation function return code */ -int send_message(function conv, int type, const char *message) { +int send_message(function + conv, + int type, const char *message) { // Formet the message as PAM expects it // No need to free this, it's allocated on the stack const struct pam_message msg = {.msg_style = type, .msg = message}; @@ -111,7 +116,8 @@ int send_message(functionconv, placeholders::_1, placeholders::_2); + auto conv_function = + bind(send_message, conv->conv, placeholders::_1, placeholders::_2); // Error out if we could not ready the config file if (reader.ParseError() < 0) { @@ -144,14 +152,17 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, bool au // 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) { + if (getenv("SSH_CONNECTION") != nullptr || + getenv("SSH_CLIENT") != nullptr || getenv("SSHD_OPTS") != nullptr) { return PAM_AUTHINFO_UNAVAIL; } } // 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, "Attempting facial authentication")) != PAM_SUCCESS) { + if ((pam_res = conv_function(PAM_TEXT_INFO, + "Attempting facial authentication")) != + PAM_SUCCESS) { syslog(LOG_ERR, "Failed to send detection notice"); } } @@ -188,6 +199,7 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, bool au } posix_spawn_file_actions_t file_actions; posix_spawn_file_actions_init(&file_actions); + // We close stdout and stderr for the child posix_spawn_file_actions_addclose(&file_actions, STDOUT_FILENO); posix_spawn_file_actions_addclose(&file_actions, STDERR_FILENO); const char *const args[] = {"/usr/bin/python3", "/lib/security/howdy/compare.py", @@ -218,7 +230,8 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, bool au packaged_task pass_task([&] { char *auth_tok_ptr = nullptr; - int pam_res = pam_get_authtok(pamh, PAM_AUTHTOK, (const char **) &auth_tok_ptr, nullptr); + int pam_res = pam_get_authtok(pamh, PAM_AUTHTOK, + (const char **)&auth_tok_ptr, nullptr); { unique_lock lk(m); t = Type::Pam; @@ -239,6 +252,8 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, bool au if (t == Type::Howdy) { if (auth_tok) { + // We cancel the thread using pthread, pam_get_authtok seems to be a + // cancellation point auto native_hd = pass_thread.native_handle(); pthread_cancel(native_hd); pass_thread.join(); @@ -258,7 +273,7 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, bool au } } else { kill(child_pid, SIGTERM); - child_thread.join(); + python_thread.join(); pass_thread.join(); auto pam_res = pass_future.get(); @@ -269,26 +284,33 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, bool au } } -// 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) { +// 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) { 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 int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, + const char **argv) { 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 int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, + const char **argv) { return PAM_IGNORE; } -PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { +PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, + const char **argv) { return PAM_IGNORE; } -PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, const char **argv) { +PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, + const char **argv) { return PAM_IGNORE; } -PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) { +PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, + const char **argv) { return PAM_IGNORE; } From 1c25bf035b3908e22f89598b64f6d493a91eb9a0 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Wed, 7 Apr 2021 11:18:53 +0200 Subject: [PATCH 10/61] Fix typo in comments --- src/pam/main.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index aa2e1af..a634a0f 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -70,7 +70,7 @@ int on_howdy_auth(int code, function conv_function) { conv_function(PAM_ERROR_MSG, "Face detection image too dark"); syslog(LOG_INFO, "Failure, image too dark"); break; - // Otherwise, we can't discribe what happend but it wasn't successful + // Otherwise, we can't describe what happened but it wasn't successful default: conv_function(PAM_ERROR_MSG, string("Unknown error:" + to_string(code)).c_str()); From 1cb38b0891964cde8d22ff3e6de84b9528aadbd7 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Wed, 7 Apr 2021 11:32:44 +0200 Subject: [PATCH 11/61] Add instructions for installation --- src/pam/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/pam/README.md b/src/pam/README.md index 8cf5c0f..765cba8 100644 --- a/src/pam/README.md +++ b/src/pam/README.md @@ -5,5 +5,16 @@ ```sh meson setup build -Dinih:with_INIReader=true meson compile -C build +``` + +## Install + +```sh sudo mv build/libpam_howdy.so /lib/security/pam_howdy.so ``` + +Change PAM config line to: + +```pam +auth sufficient pam_howdy.so +``` From 7ee0f361fb079ca3edceb8925a261f45e487a670 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Wed, 7 Apr 2021 18:22:28 +0200 Subject: [PATCH 12/61] Added gettext and more even more comments --- src/pam/main.cc | 49 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index a634a0f..3c1cdba 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -30,12 +30,14 @@ #include #include +#include #include #include #include using namespace std; +using namespace boost::locale; enum class Type { Howdy, Pam }; @@ -54,7 +56,7 @@ int on_howdy_auth(int code, function conv_function) { switch (code) { // Status 10 means we couldn't find any face models case 10: - conv_function(PAM_ERROR_MSG, "There is no face model known"); + conv_function(PAM_ERROR_MSG, dgettext("pam", "There is no face model known")); syslog(LOG_NOTICE, "Failure, no face model known"); break; // Status 11 means we exceded the maximum retry count @@ -67,13 +69,13 @@ int on_howdy_auth(int code, function conv_function) { break; // Status 13 means the image was too dark case 13: - conv_function(PAM_ERROR_MSG, "Face detection image too dark"); + conv_function(PAM_ERROR_MSG, dgettext("pam", "Face detection image too dark")); syslog(LOG_INFO, "Failure, image too dark"); break; // Otherwise, we can't describe what happened but it wasn't successful default: conv_function(PAM_ERROR_MSG, - string("Unknown error:" + to_string(code)).c_str()); + string(dgettext("pam", "Unknown error:") + to_string(code)).c_str()); syslog(LOG_INFO, "Failure, unknown error %d", code); } } @@ -141,12 +143,13 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // Error out if we could not ready the config file if (reader.ParseError() < 0) { - syslog(LOG_ERR, "Failed to parse the configuration"); + syslog(LOG_ERR, "Failed to parse the configuration file"); return PAM_SYSTEM_ERR; } // 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; } @@ -154,36 +157,36 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, 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; } } - // 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, - "Attempting facial authentication")) != - PAM_SUCCESS) { - syslog(LOG_ERR, "Failed to send detection notice"); - } - } + // 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{}; - int return_value = - glob("/proc/acpi/button/lid/*/state", 0, nullptr, &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 each lid status file found for (size_t i = 0; i < glob_result.gl_pathc; ++i) { + // Read the file contents ifstream file(string(glob_result.gl_pathv[i])); string lid_state; getline(file, lid_state, (char)file.eof()); + + // Stop if the lid is closed if (lid_state.find("closed") != std::string::npos) { globfree(&glob_result); + + syslog(LOG_INFO, "Skipped authentication, closed lid detected"); return PAM_AUTHINFO_UNAVAIL; } } @@ -191,21 +194,33 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, globfree(&glob_result); } + // 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"))) != 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) { 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 stdout and stderr for the child posix_spawn_file_actions_addclose(&file_actions, STDOUT_FILENO); posix_spawn_file_actions_addclose(&file_actions, STDERR_FILENO); + + // Prepare the python command that will run the facial authentication const char *const args[] = {"/usr/bin/python3", "/lib/security/howdy/compare.py", user_ptr, 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)); @@ -263,7 +278,10 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, if (howdy_status == 0) { if (!reader.GetBoolean("core", "no_confirmation", true)) { - string identify_msg("Identified face as " + string(user_ptr)); + // 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"); @@ -273,7 +291,6 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, } } else { kill(child_pid, SIGTERM); - python_thread.join(); pass_thread.join(); auto pam_res = pass_future.get(); From d078f81baa4525a40937ce524f00de48deb76c0a Mon Sep 17 00:00:00 2001 From: MusiKid Date: Wed, 30 Jun 2021 22:21:10 +0200 Subject: [PATCH 13/61] feat: add workarounds --- src/pam/main.cc | 144 +++++++++++++++++++++++++++------------ src/pam/main.hh | 8 +++ src/pam/meson.build | 4 +- src/pam/optional_task.hh | 64 +++++++++++++++++ 4 files changed, 174 insertions(+), 46 deletions(-) create mode 100644 src/pam/main.hh create mode 100644 src/pam/optional_task.hh diff --git a/src/pam/main.cc b/src/pam/main.cc index 3c1cdba..2af7270 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -13,6 +14,7 @@ #include #include +#include #include #include #include @@ -30,16 +32,19 @@ #include #include + #include #include #include #include +#include "main.hh" +#include "optional_task.hh" + using namespace std; using namespace boost::locale; - -enum class Type { Howdy, Pam }; +using namespace std::chrono_literals; /** * Inspect the status code returned by the compare process @@ -56,7 +61,8 @@ int on_howdy_auth(int code, function conv_function) { 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")); + conv_function(PAM_ERROR_MSG, + dgettext("pam", "There is no face model known")); syslog(LOG_NOTICE, "Failure, no face model known"); break; // Status 11 means we exceded the maximum retry count @@ -69,13 +75,15 @@ int on_howdy_auth(int code, function conv_function) { break; // Status 13 means the image was too dark case 13: - conv_function(PAM_ERROR_MSG, dgettext("pam", "Face detection image too dark")); + conv_function(PAM_ERROR_MSG, + dgettext("pam", "Face detection image too dark")); syslog(LOG_INFO, "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()); + conv_function( + PAM_ERROR_MSG, + string(dgettext("pam", "Unknown error:") + to_string(code)).c_str()); syslog(LOG_INFO, "Failure, unknown error %d", code); } } @@ -109,6 +117,17 @@ int send_message(function child_task([&] { + // This task wait for the status of the python subprocess (we don't want a + // zombie process). + optional_task child_task(packaged_task([&] { int status; wait(&status); { @@ -239,48 +266,50 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, } cv.notify_all(); return status; - }); - auto child_future = child_task.get_future(); - thread child_thread(move(child_task)); + })); - packaged_task pass_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); - t = Type::Pam; - } - cv.notify_all(); - return pam_res; - }); - auto pass_future = pass_task.get_future(); - thread pass_thread; + 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); + t = Type::Pam; + } + cv.notify_all(); + return tuple(pam_res, auth_tok_ptr); + })); if (auth_tok) { - pass_thread = thread(move(pass_task)); + pass_task.activate(); } - { + // Wait for the end + if (workaround == Workaround::Native) { unique_lock lk(m); cv.wait(lk); + } else if (auth_tok) { + pass_task.stop(false); } if (t == Type::Howdy) { if (auth_tok) { // We cancel the thread using pthread, pam_get_authtok seems to be a // cancellation point - auto native_hd = pass_thread.native_handle(); - pthread_cancel(native_hd); - pass_thread.join(); + if (child_task.wait(1s) == future_status::timeout) { + kill(child_pid, SIGTERM); + } + child_task.stop(false); + pass_task.stop(false); } - child_thread.join(); - int howdy_status = child_future.get(); + int howdy_status = child_task.get(); 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))); + 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()); } @@ -290,13 +319,40 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, return on_howdy_auth(howdy_status, conv_function); } } else { - kill(child_pid, SIGTERM); - pass_thread.join(); - auto pam_res = pass_future.get(); + if (!(workaround == Workaround::Native)) { + if (child_task.wait(1s) == future_status::timeout) { + kill(child_pid, SIGTERM); + } + child_task.stop(false); + } + if (auth_tok) { + pass_task.stop(false); + } + + char *token = nullptr; + tie(pam_res, token) = pass_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); + } + } + return PAM_IGNORE; } } diff --git a/src/pam/main.hh b/src/pam/main.hh new file mode 100644 index 0000000..e3d6b29 --- /dev/null +++ b/src/pam/main.hh @@ -0,0 +1,8 @@ +#ifndef MAIN_H_ +#define MAIN_H_ + +enum class Type { Howdy, Pam }; + +enum class Workaround { Off, Input, Native }; + +#endif // MAIN_H_ diff --git a/src/pam/meson.build b/src/pam/meson.build index bd2adf1..45bd614 100644 --- a/src/pam/meson.build +++ b/src/pam/meson.build @@ -1,9 +1,9 @@ -project('pam_howdy', 'cpp', version: '0.1.0', default_options: ['cpp_std=c++11']) +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: ['filesystem']) +boost = dependency('boost') threads = dependency('threads') shared_library('pam_howdy', 'main.cc', dependencies: [boost, libpam, inih_cpp, threads], install: true, install_dir: '/lib/security/') diff --git a/src/pam/optional_task.hh b/src/pam/optional_task.hh new file mode 100644 index 0000000..5048395 --- /dev/null +++ b/src/pam/optional_task.hh @@ -0,0 +1,64 @@ +#ifndef OPTIONAL_TASK_H_ +#define OPTIONAL_TASK_H_ + +#include +#include +#include +#include + +template class optional_task { + std::thread _thread; + std::packaged_task _task; + std::future _future; + std::atomic _spawned; + std::atomic _is_active; + +public: + optional_task(std::packaged_task); + void activate(); + std::future_status wait(std::chrono::duration); + T get(); + void stop(bool); + ~optional_task(); +}; + +template +optional_task::optional_task(std::packaged_task t) + : _task(std::move(t)), _future(_task.get_future()) {} + +template void optional_task::activate() { + _thread = std::thread(std::move(_task)); + _spawned = true; + _is_active = true; +} +template +std::future_status optional_task::wait(std::chrono::duration dur) { + return _future.wait_for(dur); +} + +template T optional_task::get() { + assert(!_is_active && _spawned); + return _future.get(); +} + +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_ From 3a4b4827fc812b6cca07f64866d9851eccb61de4 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Sun, 25 Jul 2021 00:12:30 +0200 Subject: [PATCH 14/61] fix: fix native and input workarounds --- src/pam/main.cc | 65 ++++++++++++++++++++-------------------- src/pam/main.hh | 28 +++++++++++++++++ src/pam/optional_task.hh | 9 ++++-- 3 files changed, 67 insertions(+), 35 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index 2af7270..8ef6c0f 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -104,12 +104,10 @@ int send_message(function conv, int type, const char *message) { - // Formet the message as PAM expects it // 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; - // Create a variable for the response to be stored in struct pam_response res_ = {}; struct pam_response *resp_ = &res_; @@ -117,17 +115,6 @@ int send_message(function child_task(packaged_task([&] { int status; wait(&status); { unique_lock lk(m); - t = Type::Howdy; + confirmation_type = Type::Howdy; } cv.notify_all(); 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; @@ -275,35 +264,45 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, (const char **)&auth_tok_ptr, nullptr); { unique_lock lk(m); - t = Type::Pam; + confirmation_type = Type::Pam; } cv.notify_all(); return tuple(pam_res, auth_tok_ptr); })); + if (auth_tok) { pass_task.activate(); } - // Wait for the end - if (workaround == Workaround::Native) { + // Wait for the end either of the child or the password input + { unique_lock lk(m); cv.wait(lk); - } else if (auth_tok) { + } + + if (workaround != Workaround::Native && auth_tok) { pass_task.stop(false); } - if (t == Type::Howdy) { + if (confirmation_type == Type::Howdy) { + // This one is just 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); + } + 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 (child_task.wait(1s) == future_status::timeout) { - kill(child_pid, SIGTERM); + if (pass_task.is_active()) { + pass_task.stop(true); } - child_task.stop(false); - pass_task.stop(false); } int howdy_status = child_task.get(); + // If exited succesfully if (howdy_status == 0) { if (!reader.GetBoolean("core", "no_confirmation", true)) { // Construct confirmation text from i18n string @@ -319,13 +318,12 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, return on_howdy_auth(howdy_status, conv_function); } } else { - if (!(workaround == Workaround::Native)) { + if (workaround != Workaround::Native) { if (child_task.wait(1s) == future_status::timeout) { kill(child_pid, SIGTERM); } child_task.stop(false); - } - if (auth_tok) { + } else { pass_task.stop(false); } @@ -353,6 +351,7 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, } } + // The password has been entered, we are passing it to PAM stack return PAM_IGNORE; } } diff --git a/src/pam/main.hh b/src/pam/main.hh index e3d6b29..6b75580 100644 --- a/src/pam/main.hh +++ b/src/pam/main.hh @@ -1,8 +1,36 @@ #ifndef MAIN_H_ #define MAIN_H_ +#include +#include + enum class Type { 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; + } +} + +inline bool operator==(const Workaround &l, const std::string &r) { + return operator==(r, l); +} + +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); +} + #endif // MAIN_H_ diff --git a/src/pam/optional_task.hh b/src/pam/optional_task.hh index 5048395..827c535 100644 --- a/src/pam/optional_task.hh +++ b/src/pam/optional_task.hh @@ -16,8 +16,9 @@ template class optional_task { public: optional_task(std::packaged_task); void activate(); - std::future_status wait(std::chrono::duration); + template std::future_status wait(std::chrono::duration); T get(); + bool is_active(); void stop(bool); ~optional_task(); }; @@ -31,8 +32,10 @@ template void optional_task::activate() { _spawned = true; _is_active = true; } + template -std::future_status optional_task::wait(std::chrono::duration dur) { +template +std::future_status optional_task::wait(std::chrono::duration dur) { return _future.wait_for(dur); } @@ -41,6 +44,8 @@ template T optional_task::get() { return _future.get(); } +template bool optional_task::is_active() { return _is_active; } + template void optional_task::stop(bool force) { if (!(_is_active && _thread.joinable()) && _spawned) { _is_active = false; From b8bc325327ecf2d5dbbfe91bfa25ef5d3ef4f146 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Sun, 25 Jul 2021 00:20:55 +0200 Subject: [PATCH 15/61] chore: fix misspell --- src/pam/main.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index 8ef6c0f..58c8920 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -302,7 +302,7 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, } int howdy_status = child_task.get(); - // If exited succesfully + // If exited successfully if (howdy_status == 0) { if (!reader.GetBoolean("core", "no_confirmation", true)) { // Construct confirmation text from i18n string From 5503cfb4e21067b0dd9ab6b83eb14ed7e1c561b6 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Sun, 25 Jul 2021 00:26:27 +0200 Subject: [PATCH 16/61] chore: add workaround key to config.ini --- howdy/src/config.ini | 8 ++++++++ 1 file changed, 8 insertions(+) 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 From 039217152dd6a2c2bba18cd9f9afd34cdbb6f971 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Wed, 28 Jul 2021 12:16:09 +0200 Subject: [PATCH 17/61] fix: fix possible race condition --- src/pam/main.cc | 92 ++++++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index 58c8920..2595c61 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -133,7 +133,7 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, string workaround = reader.GetString("core", "workaround", "input"); // In this case, we are not asking for the password - if (workaround == Workaround::Off) + if (workaround == Workaround::Off && auth_tok) auth_tok = false; // Will contain PAM conversation function @@ -241,6 +241,8 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, mutex m; condition_variable cv; Type confirmation_type; + // TODO: Find a clean way to do this + Type final_type; // This task wait for the status of the python subprocess (we don't want a // zombie process) @@ -278,14 +280,11 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, { unique_lock lk(m); cv.wait(lk); + final_type = confirmation_type; } - if (workaround != Workaround::Native && auth_tok) { - pass_task.stop(false); - } - - if (confirmation_type == Type::Howdy) { - // This one is just to be sure that we're not going to block forever if the + 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); @@ -317,43 +316,50 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, } else { return on_howdy_auth(howdy_status, conv_function); } - } else { - if (workaround != Workaround::Native) { - if (child_task.wait(1s) == future_status::timeout) { - kill(child_pid, SIGTERM); - } - child_task.stop(false); - } else { - pass_task.stop(false); - } - - char *token = nullptr; - tie(pam_res, token) = pass_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; } + + // 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); + } + 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) { + pass_task.stop(false); + } + + char *token = nullptr; + tie(pam_res, token) = pass_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; } // Called by PAM when a user needs to be authenticated, for example by running From 27460c8198478405caf6b5a2491ca0782e451a97 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Wed, 28 Jul 2021 12:16:41 +0200 Subject: [PATCH 18/61] build: add boost locale module --- src/pam/meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pam/meson.build b/src/pam/meson.build index 45bd614..add0595 100644 --- a/src/pam/meson.build +++ b/src/pam/meson.build @@ -4,6 +4,6 @@ inih = subproject('inih') inih_cpp = inih.get_variable('INIReader_dep') libpam = meson.get_compiler('c').find_library('pam') -boost = dependency('boost') +boost = dependency('boost', modules: ['locale']) threads = dependency('threads') shared_library('pam_howdy', 'main.cc', dependencies: [boost, libpam, inih_cpp, threads], install: true, install_dir: '/lib/security/') From ab4a492c376d9a737cc0497b96dc8f4bd8cabae9 Mon Sep 17 00:00:00 2001 From: Jinn Koriech Date: Tue, 6 Apr 2021 13:48:41 +0100 Subject: [PATCH 19/61] Add python-opencv as a dependency for ArchLinux --- howdy/archlinux/howdy/.SRCINFO | 1 + 1 file changed, 1 insertion(+) 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 From 827f638be62edc7234d8815f4cf80ccb0ecf4005 Mon Sep 17 00:00:00 2001 From: supdrewin <84341772+supdrewin@users.noreply.github.com> Date: Thu, 20 Jan 2022 03:42:11 +0800 Subject: [PATCH 20/61] Turn subproject as fallback (#1) Use system-wide INIReader if available or fallback to use subproject --- src/pam/README.md | 26 ++++++++++++++++++++------ src/pam/meson.build | 21 ++++++++++++++++----- src/pam/subprojects/inih.wrap | 2 +- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/pam/README.md b/src/pam/README.md index 765cba8..d8a3b53 100644 --- a/src/pam/README.md +++ b/src/pam/README.md @@ -1,20 +1,34 @@ # Howdy PAM module +## Prepare + +Howdy PAM module depends on `INIReader`. + +If you are building a distro package, +there is a list for it below: + +``` +Arch Linux - libinih +Debian - libinih-dev +Fedora - inih-devel +OpenSUSE - inih +``` + ## 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 +sudo meson install -C build ``` Change PAM config line to: -```pam -auth sufficient pam_howdy.so +``` pam +auth sufficient pam_howdy.so ``` diff --git a/src/pam/meson.build b/src/pam/meson.build index add0595..fe2d09a 100644 --- a/src/pam/meson.build +++ b/src/pam/meson.build @@ -1,9 +1,20 @@ 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']) +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/') + +shared_library( + 'pam_howdy', + 'main.cc', + dependencies: [ + boost, + libpam, + inih_cpp, + threads + ], + install: true, + install_dir: '/lib/security', + name_prefix: '' +) diff --git a/src/pam/subprojects/inih.wrap b/src/pam/subprojects/inih.wrap index 8d02b50..f1c71a4 100644 --- a/src/pam/subprojects/inih.wrap +++ b/src/pam/subprojects/inih.wrap @@ -1,3 +1,3 @@ [wrap-git] url = https://github.com/benhoyt/inih.git -revision = r52 +revision = head From 9fb3ef5341807ec16f41205bd3c97b2281493e0d Mon Sep 17 00:00:00 2001 From: MusiKid Date: Thu, 20 Jan 2022 13:46:48 +0100 Subject: [PATCH 21/61] fix: use atomic type for confirmation type --- src/pam/main.cc | 50 ++++++++++++++++++++++++++----------------------- src/pam/main.hh | 2 +- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index 2595c61..c4a675a 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -175,7 +175,7 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // 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{}; + glob_t glob_result; // Get any files containing lid state int return_value = @@ -238,22 +238,27 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, return PAM_SYSTEM_ERR; } + // NOTE: We could replace mutex and condition_variable by atomic wait, but + // it's too recent (C++20) mutex m; condition_variable cv; - Type confirmation_type; - // TODO: Find a clean way to do this - Type final_type; + atomic confirmation_type(Type::Unset); // This task wait for the status of the python subprocess (we don't want a // zombie process) optional_task child_task(packaged_task([&] { int status; wait(&status); + { unique_lock lk(m); - confirmation_type = Type::Howdy; + Type type = confirmation_type.load(memory_order_acquire); + if (type == Type::Unset) { + confirmation_type.store(Type::Howdy, memory_order_release); + } } - cv.notify_all(); + cv.notify_one(); + return status; })); child_task.activate(); @@ -266,9 +271,13 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, (const char **)&auth_tok_ptr, nullptr); { unique_lock lk(m); - confirmation_type = Type::Pam; + Type type = confirmation_type.load(memory_order_acquire); + if (type == Type::Unset) { + confirmation_type.store(Type::Pam, memory_order_release); + } } - cv.notify_all(); + cv.notify_one(); + return tuple(pam_res, auth_tok_ptr); })); @@ -279,16 +288,10 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // Wait for the end either of the child or the password input { unique_lock lk(m); - cv.wait(lk); - final_type = confirmation_type; + cv.wait(lk, [&] { return confirmation_type != Type::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); - } + if (confirmation_type == Type::Howdy) { child_task.stop(false); // If the workaround is native @@ -313,13 +316,13 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, } syslog(LOG_INFO, "Login approved"); return PAM_SUCCESS; - } else { - return on_howdy_auth(howdy_status, conv_function); } + + // We got an error + return on_howdy_auth(howdy_status, conv_function); } - // The branch with Howdy confirmation type returns early, so we don't need an - // else statement + // The password has been entered // Again, we need to be sure that we're not going to block forever if the // child has a problem @@ -334,14 +337,15 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, pass_task.stop(false); } - char *token = nullptr; - tie(pam_res, token) = pass_task.get(); + char *password = nullptr; + tie(pam_res, password) = pass_task.get(); if (pam_res != PAM_SUCCESS) return pam_res; int howdy_status = child_task.get(); - if (strlen(token) == 0) { + // If python process sent Enter key + if (strlen(password) == 0) { if (howdy_status == 0) { if (!reader.GetBoolean("core", "no_confirmation", true)) { // Construct confirmation text from i18n string diff --git a/src/pam/main.hh b/src/pam/main.hh index 6b75580..cecadd5 100644 --- a/src/pam/main.hh +++ b/src/pam/main.hh @@ -4,7 +4,7 @@ #include #include -enum class Type { Howdy, Pam }; +enum class Type { Unset, Howdy, Pam }; enum class Workaround { Off, Input, Native }; From fa2607db7d9d897669d4d99fb4c9d673a2e3b927 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Thu, 20 Jan 2022 14:22:01 +0100 Subject: [PATCH 22/61] fix: add appdata_ptr for conv function --- src/pam/main.cc | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index c4a675a..ab8179e 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -100,19 +100,16 @@ int on_howdy_auth(int code, function conv_function) { * @param message String to show the user * @return Returns the conversation function return code */ -int send_message(function - conv, - int type, const char *message) { +int send_message(struct pam_conv *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; - struct pam_response res_ = {}; - struct pam_response *resp_ = &res_; + struct pam_response res = {}; + struct pam_response *resp = &res; // Call the conversation function with the constructed arguments - return conv(1, &msgp, &resp_, nullptr); + return conv->conv(1, &msgp, &resp, conv->appdata_ptr); } /** @@ -150,7 +147,7 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // Wrap the PAM conversation function in our own, easier function auto conv_function = - bind(send_message, conv->conv, placeholders::_1, placeholders::_2); + bind(send_message, conv, placeholders::_1, placeholders::_2); // Error out if we could not ready the config file if (reader.ParseError() < 0) { From d77dd94ec000d7dfb95fdad809d267e8437cde62 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Fri, 21 Jan 2022 11:22:44 +0100 Subject: [PATCH 23/61] refactor: add function to format success message --- src/pam/main.cc | 143 ++++++++++++++++++++++++------------------------ 1 file changed, 71 insertions(+), 72 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index ab8179e..0f27835 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -3,10 +3,9 @@ #include #include #include -#include + #include #include -#include #include #include #include @@ -44,21 +43,20 @@ using namespace std; using namespace boost::locale; -using namespace std::chrono_literals; /** * 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) { +int howdy_error(int status, function conv_function) { // If the process has exited - if (!WIFEXITED(code)) { + if (!WIFEXITED(status)) { // Get the status code returned - code = WEXITSTATUS(code); + status = WEXITSTATUS(status); - switch (code) { + switch (status) { // Status 10 means we couldn't find any face models case 10: conv_function(PAM_ERROR_MSG, @@ -81,10 +79,10 @@ int on_howdy_auth(int code, function conv_function) { 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, string(dgettext("pam", "Unknown error:") + + to_string(status)) + .c_str()); + syslog(LOG_INFO, "Failure, unknown error %d", status); } } @@ -93,6 +91,34 @@ int on_howdy_auth(int code, function conv_function) { 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 reader INI configuration + * @param conv_function PAM conversation function + * @return Returns the conversation function return code + */ +int howdy_msg(char *username, int status, INIReader &reader, + function conv_function) { + if (status != EXIT_SUCCESS) { + return howdy_error(status, conv_function); + } + + 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(username)); + conv_function(PAM_TEXT_INFO, identify_msg.c_str()); + } + + syslog(LOG_INFO, "Login approved"); + + return PAM_SUCCESS; +} + /** * Format and send a message to PAM * @param conv PAM conversation function @@ -130,10 +156,11 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, string workaround = reader.GetString("core", "workaround", "input"); // In this case, we are not asking for the password - if (workaround == Workaround::Off && auth_tok) + if (workaround == Workaround::Off && auth_tok) { auth_tok = false; + } - // Will contain PAM conversation function + // Will contain PAM conversation structure struct pam_conv *conv = nullptr; // Will contain the responses from PAM functions int pam_res = PAM_IGNORE; @@ -210,8 +237,8 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, } // 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)) != + char *username = nullptr; + if ((pam_res = pam_get_user(pamh, (const char **)&username, nullptr)) != PAM_SUCCESS) { syslog(LOG_ERR, "Failed to get username"); return pam_res; @@ -225,7 +252,7 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, posix_spawn_file_actions_addclose(&file_actions, STDIN_FILENO); const char *const args[] = { - "/usr/bin/python3", "/lib/security/howdy/compare.py", user_ptr, nullptr}; + "/usr/bin/python3", "/lib/security/howdy/compare.py", username, nullptr}; pid_t child_pid; // Start the python subprocess @@ -235,7 +262,7 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, return PAM_SYSTEM_ERR; } - // NOTE: We could replace mutex and condition_variable by atomic wait, but + // NOTE: We should replace mutex and condition_variable by atomic wait, but // it's too recent (C++20) mutex m; condition_variable cv; @@ -301,66 +328,38 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, } int howdy_status = child_task.get(); - // 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; + return howdy_msg(username, howdy_status, reader, conv_function); + } else { + // 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(1.5s) == future_status::timeout) { + 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 + if (workaround == Workaround::Input && auth_tok) { + pass_task.stop(false); } - // We got an error - return on_howdy_auth(howdy_status, conv_function); - } + char *password = nullptr; + tie(pam_res, password) = pass_task.get(); - // The password has been entered + if (pam_res != PAM_SUCCESS) + return pam_res; - // 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); - } - 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) { - pass_task.stop(false); - } - - char *password = nullptr; - tie(pam_res, password) = pass_task.get(); - - if (pam_res != PAM_SUCCESS) - return pam_res; - - int howdy_status = child_task.get(); - // If python process sent Enter key - if (strlen(password) == 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); + 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; + // The password has been entered, we are passing it to PAM stack + return PAM_IGNORE; + } } // Called by PAM when a user needs to be authenticated, for example by running From 67fc6f4709ccb8e66acc82134c3540729007b8db Mon Sep 17 00:00:00 2001 From: MusiKid Date: Fri, 21 Jan 2022 11:32:57 +0100 Subject: [PATCH 24/61] fix: fix condition for howdy_error --- src/pam/main.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index 0f27835..0008eac 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -52,7 +52,7 @@ using namespace boost::locale; */ int howdy_error(int status, function conv_function) { // If the process has exited - if (!WIFEXITED(status)) { + if (WIFEXITED(status)) { // Get the status code returned status = WEXITSTATUS(status); From d91181f77218150e7969dc747a4ed4dcc433622b Mon Sep 17 00:00:00 2001 From: MusiKid Date: Fri, 21 Jan 2022 12:11:45 +0100 Subject: [PATCH 25/61] docs: improve documentation --- src/pam/README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/pam/README.md b/src/pam/README.md index d8a3b53..0a3b0d6 100644 --- a/src/pam/README.md +++ b/src/pam/README.md @@ -2,10 +2,8 @@ ## Prepare -Howdy PAM module depends on `INIReader`. - -If you are building a distro package, -there is a list for it below: +This module depends on `INIReader`. +It can be installed with these packages: ``` Arch Linux - libinih @@ -14,6 +12,9 @@ Fedora - inih-devel OpenSUSE - inih ``` +If your distribution doesn't provide `INIReader`, +it will be automatically pulled from the latest git version. + ## Build ``` sh @@ -24,7 +25,7 @@ meson compile -C build ## Install ``` sh -sudo meson install -C build +meson install -C build ``` Change PAM config line to: From c31acece3e5c0b7512125d4a602586f56aa4e038 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Fri, 21 Jan 2022 12:12:01 +0100 Subject: [PATCH 26/61] chore: pin version for subproject --- src/pam/subprojects/inih.wrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pam/subprojects/inih.wrap b/src/pam/subprojects/inih.wrap index f1c71a4..ad75dec 100644 --- a/src/pam/subprojects/inih.wrap +++ b/src/pam/subprojects/inih.wrap @@ -1,3 +1,3 @@ [wrap-git] url = https://github.com/benhoyt/inih.git -revision = head +revision = r53 From e8e1624ea162cdff0c81ac828ea8a691342c8010 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Fri, 21 Jan 2022 12:18:25 +0100 Subject: [PATCH 27/61] refactor: build workaround from string --- src/pam/main.cc | 9 +++++---- src/pam/main.hh | 27 ++++++--------------------- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index 0008eac..7bdd718 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -79,9 +79,9 @@ int howdy_error(int status, function conv_function) { 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(status)) - .c_str()); + conv_function( + PAM_ERROR_MSG, + string(dgettext("pam", "Unknown error: ") + status).c_str()); syslog(LOG_INFO, "Failure, unknown error %d", status); } } @@ -153,7 +153,8 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // Open the system log so we can write to it openlog("pam_howdy", 0, LOG_AUTHPRIV); - string workaround = reader.GetString("core", "workaround", "input"); + Workaround workaround = + get_workaround(reader.GetString("core", "workaround", "input")); // In this case, we are not asking for the password if (workaround == Workaround::Off && auth_tok) { diff --git a/src/pam/main.hh b/src/pam/main.hh index cecadd5..df05f9a 100644 --- a/src/pam/main.hh +++ b/src/pam/main.hh @@ -8,29 +8,14 @@ enum class Type { 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; - } -} +inline Workaround get_workaround(std::string 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_ From 485807153287d4636185ae5c8a750c1c0dc68aa0 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Fri, 21 Jan 2022 12:18:55 +0100 Subject: [PATCH 28/61] refactor: use relaxed ordering for atomics We are still using a lock with the atomics for now, so the ordering doesn't matter. --- src/pam/main.cc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index 7bdd718..ac800d1 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -277,9 +277,9 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, { unique_lock lk(m); - Type type = confirmation_type.load(memory_order_acquire); + Type type = confirmation_type.load(memory_order_relaxed); if (type == Type::Unset) { - confirmation_type.store(Type::Howdy, memory_order_release); + confirmation_type.store(Type::Howdy, memory_order_relaxed); } } cv.notify_one(); @@ -296,9 +296,9 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, (const char **)&auth_tok_ptr, nullptr); { unique_lock lk(m); - Type type = confirmation_type.load(memory_order_acquire); + Type type = confirmation_type.load(memory_order_relaxed); if (type == Type::Unset) { - confirmation_type.store(Type::Pam, memory_order_release); + confirmation_type.store(Type::Pam, memory_order_relaxed); } } cv.notify_one(); @@ -335,7 +335,7 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // 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) { + if (child_task.wait(2.5s) == future_status::timeout) { kill(child_pid, SIGTERM); } child_task.stop(false); From bd0d35427d21441413d7e7aa3cbb66d2e196bb97 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Fri, 21 Jan 2022 12:59:12 +0100 Subject: [PATCH 29/61] fix: let child inherit standard descriptors --- src/pam/main.cc | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index ac800d1..5831818 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -247,10 +247,6 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, 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", username, nullptr}; From 7490705f8f5ab58aec958fb13ff434202cd32d92 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Fri, 21 Jan 2022 13:00:00 +0100 Subject: [PATCH 30/61] feat: improve error messages --- src/pam/main.cc | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index 5831818..067ae56 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -1,9 +1,9 @@ #include #include #include -#include #include +#include #include #include #include @@ -65,25 +65,31 @@ int howdy_error(int status, function conv_function) { break; // Status 11 means we exceded the maximum retry count case 11: - syslog(LOG_INFO, "Failure, timeout reached"); + syslog(LOG_ERR, "Failure, timeout reached"); break; // Status 12 means we aborted case 12: - syslog(LOG_INFO, "Failure, general 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"); + 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: ") + status).c_str()); - syslog(LOG_INFO, "Failure, unknown error %d", status); + syslog(LOG_ERR, "Failure, unknown error %d", status); } + } else { + // We get the signal + status = WIFSIGNALED(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 @@ -255,7 +261,8 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // 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)); + syslog(LOG_ERR, "Can't spawn the howdy process: %s (%d)", strerror(errno), + errno); return PAM_SYSTEM_ERR; } From ec2461a01757dfabe11d87d15fe43f2104fc1cfd Mon Sep 17 00:00:00 2001 From: MusiKid Date: Fri, 21 Jan 2022 13:00:37 +0100 Subject: [PATCH 31/61] chore: use inih subproject from wrapdb --- src/pam/subprojects/inih.wrap | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/pam/subprojects/inih.wrap b/src/pam/subprojects/inih.wrap index ad75dec..5423393 100644 --- a/src/pam/subprojects/inih.wrap +++ b/src/pam/subprojects/inih.wrap @@ -1,3 +1,13 @@ -[wrap-git] -url = https://github.com/benhoyt/inih.git -revision = r53 +[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 + From 314c517ea47be111e0d12463c723547b3fee5e0c Mon Sep 17 00:00:00 2001 From: MusiKid Date: Fri, 21 Jan 2022 20:49:22 +0100 Subject: [PATCH 32/61] chore: add clang-tidy file --- src/pam/.clang-tidy | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/pam/.clang-tidy diff --git a/src/pam/.clang-tidy b/src/pam/.clang-tidy new file mode 100644 index 0000000..cb70ac0 --- /dev/null +++ b/src/pam/.clang-tidy @@ -0,0 +1,8 @@ +Checks: 'clang-diagnostic-*,clang-analyser-*,-clang-diagnostic-unused-command-line-argument,google-*,bugprone-*,modernize-*,performance-*,portability-*,readability-*,-bugprone-easily-swappable-*,-readability-magic-numbers' +WarningsAsErrors: 'clang-diagnostic-*,clang-analyser-*,-clang-diagnostic-unused-command-line-argument,google-*,bugprone-*,modernize-*,performance-*,portability-*,readability-*,-bugprone-easily-swappable-*,-readability-magic-numbers' +HeaderFilterRegex: '.*' +CheckOptions: + - key: readability-function-cognitive-complexity.Threshold + value: '50' + +# vim:syntax=yaml From 59cbabf7342884732fd79e4dfe77d2766b03ac96 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Fri, 21 Jan 2022 20:50:27 +0100 Subject: [PATCH 33/61] refactor: apply suggestions from clang-tidy --- src/pam/main.cc | 203 +++++++++++++++++++++------------------ src/pam/main.hh | 10 +- src/pam/optional_task.hh | 25 +++-- 3 files changed, 130 insertions(+), 108 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index 067ae56..e0eccce 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -1,3 +1,4 @@ +#include #include #include #include @@ -41,8 +42,10 @@ #include "main.hh" #include "optional_task.hh" -using namespace std; -using namespace boost::locale; +const auto DEFAULT_TIMEOUT = + std::chrono::duration(2500); + +#define S(msg) boost::locale::dgettext("pam", msg) /** * Inspect the status code returned by the compare process @@ -50,7 +53,9 @@ using namespace boost::locale; * @param conv_function The PAM conversation function * @return A PAM return code */ -int howdy_error(int status, function conv_function) { +auto howdy_error(int status, + const std::function &conv_function) + -> int { // If the process has exited if (WIFEXITED(status)) { // Get the status code returned @@ -59,8 +64,7 @@ int howdy_error(int status, function conv_function) { switch (status) { // 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")); + conv_function(PAM_ERROR_MSG, S("There is no face model known").c_str()); syslog(LOG_NOTICE, "Failure, no face model known"); break; // Status 11 means we exceded the maximum retry count @@ -73,15 +77,14 @@ int howdy_error(int status, function conv_function) { break; // Status 13 means the image was too dark case 13: - conv_function(PAM_ERROR_MSG, - dgettext("pam", "Face detection image too dark")); + conv_function(PAM_ERROR_MSG, S("Face detection image too dark").c_str()); 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: ") + status).c_str()); + S("Unknown error: ").append(std::to_string(status)).c_str()); syslog(LOG_ERR, "Failure, unknown error %d", status); } } else { @@ -106,17 +109,18 @@ int howdy_error(int status, function conv_function) { * @param conv_function PAM conversation function * @return Returns the conversation function return code */ -int howdy_msg(char *username, int status, INIReader &reader, - function conv_function) { +auto howdy_msg(char *username, int status, INIReader &reader, + const std::function &conv_function) + -> int { if (status != EXIT_SUCCESS) { return howdy_error(status, conv_function); } 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(username)); + 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()); } @@ -132,7 +136,7 @@ int howdy_msg(char *username, int status, INIReader &reader, * @param message String to show the user * @return Returns the conversation function return code */ -int send_message(struct pam_conv *conv, int type, const char *message) { +auto send_message(struct pam_conv *conv, int type, const char *message) -> int { // 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; @@ -153,8 +157,8 @@ int send_message(struct pam_conv *conv, int type, const char *message) { * @param auth_tok True if we should ask for a password too * @return Returns a PAM return code */ -int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, - bool auth_tok) { +auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, + bool auth_tok) -> int { INIReader reader("/lib/security/howdy/config.ini"); // Open the system log so we can write to it openlog("pam_howdy", 0, LOG_AUTHPRIV); @@ -169,19 +173,22 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // Will contain PAM conversation structure struct pam_conv *conv = nullptr; + const void **conv_ptr = + const_cast(reinterpret_cast(&conv)); // 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) { + 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, placeholders::_1, placeholders::_2); + auto conv_function = [conv](int msg_type, const char *msg) { + return send_message(conv, std::forward(msg_type), + std::forward(msg)); + }; // Error out if we could not ready the config file if (reader.ParseError() < 0) { @@ -212,15 +219,18 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, int return_value = glob("/proc/acpi/button/lid/*/state", 0, nullptr, &glob_result); - // TODO: We ignore the 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); + } 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()); + 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); @@ -235,9 +245,8 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // 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 ((conv_function(PAM_TEXT_INFO, + S("Attempting facial authentication").c_str())) != PAM_SUCCESS) { syslog(LOG_ERR, "Failed to send detection notice"); } @@ -245,8 +254,8 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // Get the username from PAM, needed to match correct face model char *username = nullptr; - if ((pam_res = pam_get_user(pamh, (const char **)&username, nullptr)) != - PAM_SUCCESS) { + if ((pam_res = pam_get_user(pamh, const_cast(&username), + nullptr)) != PAM_SUCCESS) { syslog(LOG_ERR, "Failed to get username"); return pam_res; } @@ -254,13 +263,14 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, posix_spawn_file_actions_t file_actions; posix_spawn_file_actions_init(&file_actions); - const char *const args[] = { - "/usr/bin/python3", "/lib/security/howdy/compare.py", username, nullptr}; + const char *const args[] = {"/usr/bin/python3", // NOLINT + "/lib/security/howdy/compare.py", 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) { + const_cast(args), nullptr) > 0) { syslog(LOG_ERR, "Can't spawn the howdy process: %s (%d)", strerror(errno), errno); return PAM_SYSTEM_ERR; @@ -268,21 +278,22 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // NOTE: We should replace mutex and condition_variable by atomic wait, but // it's too recent (C++20) - mutex m; - condition_variable cv; - atomic confirmation_type(Type::Unset); + std::mutex m; + std::condition_variable cv; + std::atomic 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(std::packaged_task([&] { int status; wait(&status); { - unique_lock lk(m); - Type type = confirmation_type.load(memory_order_relaxed); - if (type == Type::Unset) { - confirmation_type.store(Type::Howdy, memory_order_relaxed); + std::unique_lock lk(m); + ConfirmationType type = confirmation_type.load(std::memory_order_relaxed); + if (type == ConfirmationType::Unset) { + confirmation_type.store(ConfirmationType::Howdy, + std::memory_order_relaxed); } } cv.notify_one(); @@ -292,21 +303,24 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, child_task.activate(); // This task waits for the password input (if the workaround wants it) - optional_task> pass_task( - packaged_task()>([&] { + optional_task> pass_task( + std::packaged_task()>([&] { char *auth_tok_ptr = nullptr; - int pam_res = pam_get_authtok(pamh, PAM_AUTHTOK, - (const char **)&auth_tok_ptr, nullptr); + int pam_res = + pam_get_authtok(pamh, PAM_AUTHTOK, + const_cast(&auth_tok_ptr), nullptr); { - unique_lock lk(m); - Type type = confirmation_type.load(memory_order_relaxed); - if (type == Type::Unset) { - confirmation_type.store(Type::Pam, memory_order_relaxed); + std::unique_lock lk(m); + ConfirmationType type = + confirmation_type.load(std::memory_order_relaxed); + if (type == ConfirmationType::Unset) { + confirmation_type.store(ConfirmationType::Pam, + std::memory_order_relaxed); } } cv.notify_one(); - return tuple(pam_res, auth_tok_ptr); + return std::tuple(pam_res, auth_tok_ptr); })); if (auth_tok) { @@ -315,11 +329,11 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // Wait for the end either of the child or the password input { - unique_lock lk(m); - cv.wait(lk, [&] { return confirmation_type != Type::Unset; }); + std::unique_lock lk(m); + cv.wait(lk, [&] { return confirmation_type != ConfirmationType::Unset; }); } - if (confirmation_type == Type::Howdy) { + if (confirmation_type == ConfirmationType::Howdy) { child_task.stop(false); // If the workaround is native @@ -333,66 +347,67 @@ int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, int howdy_status = child_task.get(); return howdy_msg(username, howdy_status, reader, conv_function); - } else { - // 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(2.5s) == future_status::timeout) { - 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 - if (workaround == Workaround::Input && auth_tok) { - pass_task.stop(false); - } - - char *password = nullptr; - 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; } + + // 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); + } + 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) { + 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(); + // 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; } // 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/src/pam/main.hh b/src/pam/main.hh index df05f9a..17a6626 100644 --- a/src/pam/main.hh +++ b/src/pam/main.hh @@ -4,16 +4,18 @@ #include #include -enum class Type { Unset, Howdy, Pam }; +enum class ConfirmationType { Unset, Howdy, Pam }; enum class Workaround { Off, Input, Native }; -inline Workaround get_workaround(std::string workaround) { - if (workaround == "input") +inline auto get_workaround(const std::string &workaround) -> Workaround { + if (workaround == "input") { return Workaround::Input; + } - if (workaround == "native") + if (workaround == "native") { return Workaround::Native; + } return Workaround::Off; } diff --git a/src/pam/optional_task.hh b/src/pam/optional_task.hh index 827c535..892836c 100644 --- a/src/pam/optional_task.hh +++ b/src/pam/optional_task.hh @@ -14,12 +14,13 @@ template class optional_task { std::atomic _is_active; public: - optional_task(std::packaged_task); + explicit optional_task(std::packaged_task task); 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; + auto is_active() -> bool; + void stop(bool force); ~optional_task(); }; @@ -34,17 +35,20 @@ template void optional_task::activate() { } template -template -std::future_status optional_task::wait(std::chrono::duration dur) { +template +auto optional_task::wait(std::chrono::duration dur) + -> std::future_status { return _future.wait_for(dur); } -template T optional_task::get() { +template auto optional_task::get() -> T { assert(!_is_active && _spawned); return _future.get(); } -template bool optional_task::is_active() { return _is_active; } +template auto optional_task::is_active() -> bool { + return _is_active; +} template void optional_task::stop(bool force) { if (!(_is_active && _thread.joinable()) && _spawned) { @@ -62,8 +66,9 @@ template void optional_task::stop(bool force) { } template optional_task::~optional_task() { - if (_is_active && _spawned) + if (_is_active && _spawned) { stop(false); + } } #endif // OPTIONAL_TASK_H_ From 32a08097679a29bc4d13cc24168825a81e09b773 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Fri, 21 Jan 2022 20:54:25 +0100 Subject: [PATCH 34/61] ci: add check workflow --- .github/workflows/check.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/check.yml diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..dbd7a54 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,23 @@ +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 python devscripts dh-make debhelper fakeroot \ + python3 python3-pip python3-setuptools python3-wheel \ + ninja-build meson libpam0g-dev libboost-locale-dev \ + python3-dev python3-setuptools libopencv-dev cmake make build-essential clang-extra-tools + + - uses: actions/checkout@v2 + + - name: Build + run: | + meson setup build src/pam + meson compile -C build + + - name: Check source code + run: | + ninja clang-tidy -C build From 00c2be26e72809c98d97fcebd210cbb79ec926e9 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Fri, 21 Jan 2022 21:26:31 +0100 Subject: [PATCH 35/61] ci: fix workflow --- .github/workflows/check.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index dbd7a54..6359398 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -6,10 +6,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Install required libraries - run: sudo apt-get update && sudo apt-get install -y python devscripts dh-make debhelper fakeroot \ - python3 python3-pip python3-setuptools python3-wheel \ - ninja-build meson libpam0g-dev libboost-locale-dev \ - python3-dev python3-setuptools libopencv-dev cmake make build-essential clang-extra-tools + run: sudo apt-get update && sudo apt-get install -y python devscripts dh-make debhelper fakeroot python3 python3-pip python3-setuptools python3-wheel ninja-build meson libpam0g-dev libboost-locale-dev python3-dev python3-setuptools libopencv-dev cmake make build-essential clang-tidy - uses: actions/checkout@v2 From add27097a3e56991e335264cd70b1d22cdf7af44 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Fri, 21 Jan 2022 21:36:08 +0100 Subject: [PATCH 36/61] ci: use ninja instead of meson compile --- .github/workflows/check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 6359398..a8bef54 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -13,7 +13,7 @@ jobs: - name: Build run: | meson setup build src/pam - meson compile -C build + ninja -C build - name: Check source code run: | From 27287bc03c124a8c1ad1f028b367af7f6fc3be78 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Fri, 21 Jan 2022 21:43:16 +0100 Subject: [PATCH 37/61] ci: install native ininh --- .github/workflows/check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index a8bef54..b843418 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Install required libraries - run: sudo apt-get update && sudo apt-get install -y python devscripts dh-make debhelper fakeroot python3 python3-pip python3-setuptools python3-wheel ninja-build meson libpam0g-dev libboost-locale-dev python3-dev python3-setuptools libopencv-dev cmake make build-essential clang-tidy + run: sudo apt-get update && sudo apt-get install -y python devscripts dh-make debhelper fakeroot python3 python3-pip python3-setuptools python3-wheel ninja-build meson libpam0g-dev libinih-dev libboost-locale-dev python3-dev python3-setuptools libopencv-dev cmake make build-essential clang-tidy - uses: actions/checkout@v2 From 073aea4816ac1049df7fb663e759ff12dd24bc1a Mon Sep 17 00:00:00 2001 From: MusiKid Date: Fri, 21 Jan 2022 21:50:56 +0100 Subject: [PATCH 38/61] refactor: inline send_message --- src/pam/main.cc | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index e0eccce..d002e77 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -129,25 +129,6 @@ auto howdy_msg(char *username, int status, INIReader &reader, return PAM_SUCCESS; } -/** - * 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 - */ -auto send_message(struct pam_conv *conv, int type, const char *message) -> int { - // 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; - - struct pam_response res = {}; - struct pam_response *resp = &res; - - // Call the conversation function with the constructed arguments - return conv->conv(1, &msgp, &resp, conv->appdata_ptr); -} - /** * The main function, runs the identification and authentication * @param pamh The handle to interface directly with PAM @@ -185,9 +166,16 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, } // Wrap the PAM conversation function in our own, easier function - auto conv_function = [conv](int msg_type, const char *msg) { - return send_message(conv, std::forward(msg_type), - std::forward(msg)); + auto conv_function = [conv](int msg_type, const char *msg_str) { + // No need to free this, it's allocated on the stack + 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; + + // Call the conversation function with the constructed arguments + return conv->conv(1, &msgp, &resp, conv->appdata_ptr); }; // Error out if we could not ready the config file From a12908e7e8e08988df62739f25cdc199a90b7cb1 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Sun, 23 Jan 2022 13:57:53 +0100 Subject: [PATCH 39/61] chore: remove todo lint --- src/pam/.clang-tidy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pam/.clang-tidy b/src/pam/.clang-tidy index cb70ac0..9846031 100644 --- a/src/pam/.clang-tidy +++ b/src/pam/.clang-tidy @@ -1,5 +1,5 @@ -Checks: 'clang-diagnostic-*,clang-analyser-*,-clang-diagnostic-unused-command-line-argument,google-*,bugprone-*,modernize-*,performance-*,portability-*,readability-*,-bugprone-easily-swappable-*,-readability-magic-numbers' -WarningsAsErrors: 'clang-diagnostic-*,clang-analyser-*,-clang-diagnostic-unused-command-line-argument,google-*,bugprone-*,modernize-*,performance-*,portability-*,readability-*,-bugprone-easily-swappable-*,-readability-magic-numbers' +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' HeaderFilterRegex: '.*' CheckOptions: - key: readability-function-cognitive-complexity.Threshold From 9d61c0bc20b21906d28b33a851f39fdc5acac597 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Sun, 23 Jan 2022 14:21:08 +0100 Subject: [PATCH 40/61] build: add translation support to build system --- src/pam/meson.build | 3 +++ src/pam/po/LINGUAS | 0 src/pam/po/POTFILES | 1 + src/pam/po/meson.build | 10 ++++++++++ 4 files changed, 14 insertions(+) create mode 100644 src/pam/po/LINGUAS create mode 100644 src/pam/po/POTFILES create mode 100644 src/pam/po/meson.build diff --git a/src/pam/meson.build b/src/pam/meson.build index fe2d09a..465e543 100644 --- a/src/pam/meson.build +++ b/src/pam/meson.build @@ -5,6 +5,9 @@ inih_cpp = dependency('INIReader', fallback: ['inih', 'INIReader_dep']) libpam = meson.get_compiler('cpp').find_library('pam') threads = dependency('threads') +# Translations +subdir('po') + shared_library( 'pam_howdy', 'main.cc', diff --git a/src/pam/po/LINGUAS b/src/pam/po/LINGUAS new file mode 100644 index 0000000..e69de29 diff --git a/src/pam/po/POTFILES b/src/pam/po/POTFILES new file mode 100644 index 0000000..11b5c00 --- /dev/null +++ b/src/pam/po/POTFILES @@ -0,0 +1 @@ +main.cc \ No newline at end of file diff --git a/src/pam/po/meson.build b/src/pam/po/meson.build new file mode 100644 index 0000000..762f7a4 --- /dev/null +++ b/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 From cd11aba4e6f3f32178c406350b163b8b336fd728 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Sun, 23 Jan 2022 14:25:32 +0100 Subject: [PATCH 41/61] refactor: replace boost locale by gnu gettext --- src/pam/main.cc | 31 +++++++++++++++++++------------ src/pam/meson.build | 2 -- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index d002e77..15ce574 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -1,10 +1,10 @@ -#include #include #include #include #include #include +#include #include #include #include @@ -33,8 +33,6 @@ #include -#include - #include #include #include @@ -42,10 +40,16 @@ #include "main.hh" #include "optional_task.hh" +// Should be defined by the build system +#ifndef GETTEXT_PACKAGE +#define GETTEXT_PACKAGE "pam_howdy" +#define LOCALEDIR "/usr/local/share/locales" +#endif + const auto DEFAULT_TIMEOUT = std::chrono::duration(2500); -#define S(msg) boost::locale::dgettext("pam", msg) +#define S(msg) gettext (msg) /** * Inspect the status code returned by the compare process @@ -64,7 +68,7 @@ auto howdy_error(int status, switch (status) { // Status 10 means we couldn't find any face models case 10: - conv_function(PAM_ERROR_MSG, S("There is no face model known").c_str()); + 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 @@ -77,14 +81,13 @@ auto howdy_error(int status, break; // Status 13 means the image was too dark case 13: - conv_function(PAM_ERROR_MSG, S("Face detection image too dark").c_str()); + 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, - S("Unknown error: ").append(std::to_string(status)).c_str()); + conv_function(PAM_ERROR_MSG, + std::string(S("Unknown error: ") + status).c_str()); syslog(LOG_ERR, "Failure, unknown error %d", status); } } else { @@ -178,7 +181,7 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, return conv->conv(1, &msgp, &resp, conv->appdata_ptr); }; - // Error out if we could not ready the config file + // Error out if we could not read the config file if (reader.ParseError() < 0) { syslog(LOG_ERR, "Failed to parse the configuration file"); return PAM_SYSTEM_ERR; @@ -231,10 +234,14 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, 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 ((conv_function(PAM_TEXT_INFO, - S("Attempting facial authentication").c_str())) != + if ((conv_function(PAM_TEXT_INFO, S("Attempting facial authentication"))) != PAM_SUCCESS) { syslog(LOG_ERR, "Failed to send detection notice"); } diff --git a/src/pam/meson.build b/src/pam/meson.build index 465e543..f6660a9 100644 --- a/src/pam/meson.build +++ b/src/pam/meson.build @@ -1,6 +1,5 @@ project('pam_howdy', 'cpp', version: '0.1.0', default_options: ['cpp_std=c++14']) -boost = dependency('boost', modules: ['locale']) inih_cpp = dependency('INIReader', fallback: ['inih', 'INIReader_dep']) libpam = meson.get_compiler('cpp').find_library('pam') threads = dependency('threads') @@ -12,7 +11,6 @@ shared_library( 'pam_howdy', 'main.cc', dependencies: [ - boost, libpam, inih_cpp, threads From 7729f97c187e115a9094c79c3da9179ccfa60410 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Sun, 23 Jan 2022 14:32:35 +0100 Subject: [PATCH 42/61] fix: remove absolute path for `posix_spawnp` We also set the file_actions to `nullptr`. --- src/pam/main.cc | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index 15ce574..5dc5b61 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -112,7 +112,7 @@ auto howdy_error(int status, * @param conv_function PAM conversation function * @return Returns the conversation function return code */ -auto howdy_msg(char *username, int status, INIReader &reader, + auto howdy_msg(char *username, int status, const INIReader &reader, const std::function &conv_function) -> int { if (status != EXIT_SUCCESS) { @@ -155,12 +155,13 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, auth_tok = false; } + // Will contain the responses from PAM functions + int pam_res = PAM_IGNORE; + // Will contain PAM conversation structure struct pam_conv *conv = nullptr; const void **conv_ptr = const_cast(reinterpret_cast(&conv)); - // 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, conv_ptr)) != PAM_SUCCESS) { @@ -255,16 +256,13 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, return pam_res; } - posix_spawn_file_actions_t file_actions; - posix_spawn_file_actions_init(&file_actions); - - const char *const args[] = {"/usr/bin/python3", // NOLINT + const char *const args[] = {"python3", // NOLINT "/lib/security/howdy/compare.py", username, nullptr}; pid_t child_pid; // Start the python subprocess - if (posix_spawnp(&child_pid, "/usr/bin/python3", &file_actions, nullptr, + if (posix_spawnp(&child_pid, "python3", nullptr, nullptr, const_cast(args), nullptr) > 0) { syslog(LOG_ERR, "Can't spawn the howdy process: %s (%d)", strerror(errno), errno); From 2000df3c2ba89a6f3373e78f223fc4f19cb522ed Mon Sep 17 00:00:00 2001 From: MusiKid Date: Sun, 23 Jan 2022 20:17:07 +0100 Subject: [PATCH 43/61] refactor: build `packaged_task` in `optional_task` --- src/pam/main.cc | 45 +++++++++++++++++++--------------------- src/pam/optional_task.hh | 30 +++++++++++++++++++-------- 2 files changed, 42 insertions(+), 33 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index 5dc5b61..d435f3e 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -49,7 +49,7 @@ const auto DEFAULT_TIMEOUT = std::chrono::duration(2500); -#define S(msg) gettext (msg) +#define S(msg) gettext(msg) /** * Inspect the status code returned by the compare process @@ -112,7 +112,7 @@ auto howdy_error(int status, * @param conv_function PAM conversation function * @return Returns the conversation function return code */ - auto howdy_msg(char *username, int status, const INIReader &reader, +auto howdy_msg(char *username, int status, const INIReader &reader, const std::function &conv_function) -> int { if (status != EXIT_SUCCESS) { @@ -277,7 +277,7 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // This task wait for the status of the python subprocess (we don't want a // zombie process) - optional_task child_task(std::packaged_task([&] { + optional_task child_task([&] { int status; wait(&status); @@ -292,29 +292,26 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, cv.notify_one(); return status; - })); + }); child_task.activate(); // This task waits for the password input (if the workaround wants it) - optional_task> pass_task( - std::packaged_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); - ConfirmationType type = - confirmation_type.load(std::memory_order_relaxed); - if (type == ConfirmationType::Unset) { - confirmation_type.store(ConfirmationType::Pam, - std::memory_order_relaxed); - } - } - cv.notify_one(); + 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); + ConfirmationType type = confirmation_type.load(std::memory_order_relaxed); + if (type == ConfirmationType::Unset) { + confirmation_type.store(ConfirmationType::Pam, + std::memory_order_relaxed); + } + } + cv.notify_one(); - return std::tuple(pam_res, auth_tok_ptr); - })); + return std::tuple(pam_res, auth_tok_ptr); + }); if (auth_tok) { pass_task.activate(); @@ -331,8 +328,8 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // If the workaround is native if (auth_tok) { - // We cancel the thread using pthread, pam_get_authtok seems to be a - // cancellation point + // 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); } diff --git a/src/pam/optional_task.hh b/src/pam/optional_task.hh index 892836c..f1fc9db 100644 --- a/src/pam/optional_task.hh +++ b/src/pam/optional_task.hh @@ -6,18 +6,19 @@ #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; + bool _spawned; + bool _is_active; public: - explicit optional_task(std::packaged_task task); + explicit optional_task(std::function fn); void activate(); - template - auto wait(std::chrono::duration dur) -> std::future_status; + template + auto wait(std::chrono::duration dur) -> std::future_status; auto get() -> T; auto is_active() -> bool; void stop(bool force); @@ -25,22 +26,28 @@ public: }; 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()) {} +// 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) +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(); @@ -50,6 +57,11 @@ template auto optional_task::is_active() -> bool { 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; From b9d678a08ebe717f5f7c769fb83b69b1e2165986 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Mon, 24 Jan 2022 15:58:24 +0100 Subject: [PATCH 44/61] refactor: improve structure --- src/pam/main.cc | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index d435f3e..6f55dd1 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -48,6 +48,9 @@ const auto DEFAULT_TIMEOUT = std::chrono::duration(2500); +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) @@ -112,7 +115,7 @@ auto howdy_error(int status, * @param conv_function PAM conversation function * @return Returns the conversation function return code */ -auto howdy_msg(char *username, int status, const INIReader &reader, +auto howdy_status(char *username, int status, const INIReader &reader, const std::function &conv_function) -> int { if (status != EXIT_SUCCESS) { @@ -121,7 +124,7 @@ auto howdy_msg(char *username, int status, const INIReader &reader, if (!reader.GetBoolean("core", "no_confirmation", true)) { // Construct confirmation text from i18n string - std::string confirm_text = S("Identified face as {}"); + 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()); @@ -150,10 +153,9 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, Workaround workaround = get_workaround(reader.GetString("core", "workaround", "input")); - // In this case, we are not asking for the password - if (workaround == Workaround::Off && auth_tok) { - auth_tok = false; - } + // We ask for the password if the function requires it and if a workaround is + // set + auth_tok = auth_tok && workaround != Workaround::Off; // Will contain the responses from PAM functions int pam_res = PAM_IGNORE; @@ -171,20 +173,19 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // Wrap the PAM conversation function in our own, easier function auto conv_function = [conv](int msg_type, const char *msg_str) { - // No need to free this, it's allocated on the stack 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; - // Call the conversation function with the constructed arguments return conv->conv(1, &msgp, &resp, conv->appdata_ptr); }; // Error out if we could not read the config file - if (reader.ParseError() < 0) { - syslog(LOG_ERR, "Failed to parse the configuration file"); + if (reader.ParseError() != 0) { + syslog(LOG_ERR, "Failed to parse the configuration file: %d", + reader.ParseError()); return PAM_SYSTEM_ERR; } @@ -216,9 +217,7 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, if (errno != 0) { syslog(LOG_ERR, "Underlying error: %s (%d)", strerror(errno), errno); } - globfree(&glob_result); - } - + } 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; @@ -231,7 +230,7 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, return PAM_AUTHINFO_UNAVAIL; } } - + } globfree(&glob_result); } @@ -256,13 +255,12 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, return pam_res; } - const char *const args[] = {"python3", // NOLINT - "/lib/security/howdy/compare.py", username, - 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, "python3", nullptr, nullptr, + 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); From 6a395067ad64e71a7b99026e77785b859d3fe64a Mon Sep 17 00:00:00 2001 From: MusiKid Date: Mon, 24 Jan 2022 16:30:01 +0100 Subject: [PATCH 45/61] 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. --- src/pam/README.md | 16 +++--- src/pam/enter_device.cc | 49 ++++++++++++++++++ src/pam/enter_device.hh | 19 +++++++ src/pam/main.cc | 109 +++++++++++++++++++++++----------------- src/pam/meson.build | 5 +- 5 files changed, 143 insertions(+), 55 deletions(-) create mode 100644 src/pam/enter_device.cc create mode 100644 src/pam/enter_device.hh 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', From 8b6b257668d11d551e1ad4d6b28e1014348dedff Mon Sep 17 00:00:00 2001 From: MusiKid Date: Mon, 24 Jan 2022 17:16:10 +0100 Subject: [PATCH 46/61] ci: add libevdev --- .github/workflows/check.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index b843418..69e06f2 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -6,7 +6,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Install required libraries - run: sudo apt-get update && sudo apt-get install -y python devscripts dh-make debhelper fakeroot python3 python3-pip python3-setuptools python3-wheel ninja-build meson libpam0g-dev libinih-dev libboost-locale-dev python3-dev python3-setuptools libopencv-dev cmake make build-essential clang-tidy + 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 From b53d48e43db8d22c26b84c00fb56c268c09bb5a0 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Mon, 24 Jan 2022 20:02:07 +0100 Subject: [PATCH 47/61] refactor: improve structure --- src/pam/main.cc | 127 ++++++++++++++++++++++++++---------------------- 1 file changed, 68 insertions(+), 59 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index 87af592..6e36a04 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -42,12 +42,6 @@ #include "main.hh" #include "optional_task.hh" -// Should be defined by the build system -#ifndef GETTEXT_PACKAGE -#define GETTEXT_PACKAGE "pam_howdy" -#define LOCALEDIR "/usr/local/share/locales" -#endif - const auto DEFAULT_TIMEOUT = std::chrono::duration(100); const auto MAX_RETRIES = 5; @@ -138,59 +132,13 @@ auto howdy_status(char *username, int status, const INIReader &reader, } /** - * 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 + * Check if Howdy should be enabled according to the configuration and the + * environment. + * @param reader INI configuration + * @return Returns PAM_AUTHINFO_UNAVAIL if it shouldn't be enabled, + * PAM_SUCCESS otherwise */ -auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, - bool auth_tok) -> int { - INIReader reader("/lib/security/howdy/config.ini"); - // Open the system log so we can write to it - openlog("pam_howdy", 0, LOG_AUTHPRIV); - - Workaround workaround = - get_workaround(reader.GetString("core", "workaround", "input")); - - // We ask for the password if the function requires it and if a workaround is - // set - auth_tok = auth_tok && workaround != Workaround::Off; - - // Will contain the responses from PAM functions - int pam_res = PAM_IGNORE; - - // Will contain PAM conversation structure - struct pam_conv *conv = nullptr; - const void **conv_ptr = - const_cast(reinterpret_cast(&conv)); - - // Try to get the conversation function and error out if we can't - 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); - }; - - // Error out if we could not read the config file - if (reader.ParseError() != 0) { - syslog(LOG_ERR, "Failed to parse the configuration file: %d", - reader.ParseError()); - return PAM_SYSTEM_ERR; - } - +auto check_enabled(const INIReader &reader) -> int { // Stop executing if Howdy has been disabled in the config if (reader.GetBoolean("core", "disabled", false)) { syslog(LOG_INFO, "Skipped authentication, Howdy is disabled"); @@ -236,6 +184,66 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, 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 reader("/lib/security/howdy/config.ini"); + openlog("pam_howdy", 0, LOG_AUTHPRIV); + + // Error out if we could not read the config file + if (reader.ParseError() != 0) { + syslog(LOG_ERR, "Failed to parse the configuration file: %d", + reader.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(reader)) != PAM_SUCCESS) { + return pam_res; + } + + Workaround workaround = + get_workaround(reader.GetString("core", "workaround", "input")); + + // We ask for the password if the function requires it and if a workaround is + // set + auth_tok = auth_tok && workaround != Workaround::Off; + + // 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); @@ -350,7 +358,7 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // 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()) { @@ -381,6 +389,7 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // 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, reader, conv_function); From 11762b7654271bf34f644a32f670f7998a72bd6d Mon Sep 17 00:00:00 2001 From: MusiKid Date: Tue, 25 Jan 2022 18:20:11 +0100 Subject: [PATCH 48/61] fix: use correct macro for signal number --- src/pam/main.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index 6e36a04..4d00925 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -89,9 +89,9 @@ auto howdy_error(int status, std::string(S("Unknown error: ") + status).c_str()); syslog(LOG_ERR, "Failure, unknown error %d", status); } - } else { + } else if (WIFSIGNALED(status)) { // We get the signal - status = WIFSIGNALED(status); + status = WTERMSIG(status); syslog(LOG_ERR, "Child killed by signal %s (%d)", strsignal(status), status); From 9dae2b66b3a65498a66d50a7b96ac58c3b4ca501 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Tue, 25 Jan 2022 18:23:09 +0100 Subject: [PATCH 49/61] refactor: remove atomic --- src/pam/main.cc | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index 4d00925..4039314 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -281,20 +281,17 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // it's too recent (C++20) std::mutex m; std::condition_variable cv; - std::atomic confirmation_type(ConfirmationType::Unset); + 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); - ConfirmationType type = confirmation_type.load(std::memory_order_relaxed); - if (type == ConfirmationType::Unset) { - confirmation_type.store(ConfirmationType::Howdy, - std::memory_order_relaxed); + if (confirmation_type == ConfirmationType::Unset) { + confirmation_type = ConfirmationType::Howdy; } } cv.notify_one(); @@ -310,10 +307,8 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, pamh, PAM_AUTHTOK, const_cast(&auth_tok_ptr), nullptr); { std::unique_lock lk(m); - ConfirmationType type = confirmation_type.load(std::memory_order_relaxed); - if (type == ConfirmationType::Unset) { - confirmation_type.store(ConfirmationType::Pam, - std::memory_order_relaxed); + if (confirmation_type == ConfirmationType::Unset) { + confirmation_type = ConfirmationType::Pam; } } cv.notify_one(); From 5afb74857e02d499f29469a7d2ee8de61669e93c Mon Sep 17 00:00:00 2001 From: MusiKid Date: Tue, 25 Jan 2022 18:24:26 +0100 Subject: [PATCH 50/61] fix: check non-zero return code for `posix_spawnp` --- src/pam/main.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index 4039314..2f3bd62 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -271,7 +271,7 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // Start the python subprocess if (posix_spawnp(&child_pid, PYTHON_EXECUTABLE, nullptr, nullptr, - const_cast(args), nullptr) > 0) { + const_cast(args), nullptr) != 0) { syslog(LOG_ERR, "Can't spawn the howdy process: %s (%d)", strerror(errno), errno); return PAM_SYSTEM_ERR; From d0077ef1e5a7e9b257dea0942f9f2df49674d7fe Mon Sep 17 00:00:00 2001 From: MusiKid Date: Tue, 25 Jan 2022 18:25:06 +0100 Subject: [PATCH 51/61] refactor: rename reader to config --- src/pam/main.cc | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index 2f3bd62..831f378 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -107,18 +107,18 @@ auto howdy_error(int status, * the other case * @param username Username * @param status Status code - * @param reader INI configuration + * @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 &reader, +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 (!reader.GetBoolean("core", "no_confirmation", true)) { + 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 = @@ -134,19 +134,19 @@ auto howdy_status(char *username, int status, const INIReader &reader, /** * Check if Howdy should be enabled according to the configuration and the * environment. - * @param reader INI configuration + * @param config INI configuration * @return Returns PAM_AUTHINFO_UNAVAIL if it shouldn't be enabled, * PAM_SUCCESS otherwise */ -auto check_enabled(const INIReader &reader) -> int { +auto check_enabled(const INIReader &config) -> int { // Stop executing if Howdy has been disabled in the config - if (reader.GetBoolean("core", "disabled", false)) { + 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 (reader.GetBoolean("core", "ignore_ssh", true)) { + 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"); @@ -155,7 +155,7 @@ auto check_enabled(const INIReader &reader) -> int { } // Try to detect the laptop lid state and stop if it's closed - if (reader.GetBoolean("core", "ignore_closed_lid", true)) { + if (config.GetBoolean("core", "ignore_closed_lid", true)) { glob_t glob_result; // Get any files containing lid state @@ -198,13 +198,13 @@ auto check_enabled(const INIReader &reader) -> int { */ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, bool auth_tok) -> int { - INIReader reader("/lib/security/howdy/config.ini"); + INIReader config("/lib/security/howdy/config.ini"); openlog("pam_howdy", 0, LOG_AUTHPRIV); // Error out if we could not read the config file - if (reader.ParseError() != 0) { + if (config.ParseError() != 0) { syslog(LOG_ERR, "Failed to parse the configuration file: %d", - reader.ParseError()); + config.ParseError()); return PAM_SYSTEM_ERR; } @@ -212,12 +212,12 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, int pam_res = PAM_IGNORE; // Check if we shoud continue - if ((pam_res = check_enabled(reader)) != PAM_SUCCESS) { + if ((pam_res = check_enabled(config)) != PAM_SUCCESS) { return pam_res; } Workaround workaround = - get_workaround(reader.GetString("core", "workaround", "input")); + get_workaround(config.GetString("core", "workaround", "input")); // We ask for the password if the function requires it and if a workaround is // set @@ -250,7 +250,7 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, 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 (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"); @@ -376,7 +376,7 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, 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...")); + S("Failed to send Enter input, waiting for Enter input...")); } } @@ -384,10 +384,10 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // 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, reader, conv_function); + return howdy_status(username, status, config, conv_function); } // Called by PAM when a user needs to be authenticated, for example by running From 42d18c8db5f0cbed02298e734b445617e33b250b Mon Sep 17 00:00:00 2001 From: MusiKid Date: Tue, 25 Jan 2022 18:32:39 +0100 Subject: [PATCH 52/61] refactor: improve error messages --- src/pam/main.cc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index 831f378..fe06872 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -361,8 +361,9 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, } 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...")); + conv_function(PAM_ERROR_MSG, + S("Insufficient permission to send Enter " + "press, waiting for user to press it instead")); } else { try { EnterDevice enter_device; @@ -374,13 +375,12 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, } } 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...")); + 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 + // 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); } From ab79c5b674186697c4f4c08b2d2f188568e5e5b9 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Wed, 26 Jan 2022 12:52:45 +0100 Subject: [PATCH 53/61] fix: use `euidaccess` to check permissions --- src/pam/main.cc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index fe06872..fbbaf66 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -359,10 +359,11 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, 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"); + // 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 permission to send Enter " + S("Insufficient permissions to send Enter " "press, waiting for user to press it instead")); } else { try { From b383164e0644b334735f058361491b24481df5de Mon Sep 17 00:00:00 2001 From: MusiKid Date: Wed, 26 Jan 2022 12:54:12 +0100 Subject: [PATCH 54/61] fix: print message when retries limit is reached --- src/pam/main.cc | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index fbbaf66..f4f77cc 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -368,12 +368,24 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, } else { try { EnterDevice enter_device; - for (int retries = 0; + 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 " From 0f39bcc5fe4c39e1b4bce8f37f5d8425b6c8e52d Mon Sep 17 00:00:00 2001 From: MusiKid Date: Wed, 26 Jan 2022 13:11:21 +0100 Subject: [PATCH 55/61] refactor: remove unused function --- src/pam/main.cc | 2 +- src/pam/optional_task.hh | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/pam/main.cc b/src/pam/main.cc index f4f77cc..91822c9 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -356,7 +356,7 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // UNSAFE: We cancel the thread using pthread, pam_get_authtok seems to be // a cancellation point - if (workaround == Workaround::Native && pass_task.is_active()) { + if (workaround == Workaround::Native) { pass_task.stop(true); } else if (workaround == Workaround::Input) { // We check if we have the right permissions on /dev/uinput diff --git a/src/pam/optional_task.hh b/src/pam/optional_task.hh index f1fc9db..29214bf 100644 --- a/src/pam/optional_task.hh +++ b/src/pam/optional_task.hh @@ -53,10 +53,6 @@ template auto optional_task::get() -> T { return _future.get(); } -template auto optional_task::is_active() -> bool { - 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`. From 6cddf402b71f25dfd7f8ba3d671abf94d3588b5f Mon Sep 17 00:00:00 2001 From: MusiKid Date: Wed, 26 Jan 2022 13:51:49 +0100 Subject: [PATCH 56/61] refactor: improve structure --- src/pam/enter_device.hh | 6 +++--- src/pam/main.cc | 21 +++++++-------------- src/pam/main.hh | 4 +++- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/pam/enter_device.hh b/src/pam/enter_device.hh index 0dc7e1d..acdadd1 100644 --- a/src/pam/enter_device.hh +++ b/src/pam/enter_device.hh @@ -1,5 +1,5 @@ -#ifndef __ENTER_DEVICE_H_ -#define __ENTER_DEVICE_H_ +#ifndef ENTER_DEVICE_H_ +#define ENTER_DEVICE_H_ #include #include @@ -16,4 +16,4 @@ public: ~EnterDevice() = default; }; -#endif // __ENTER_DEVICE_H +#endif // ENTER_DEVICE_H diff --git a/src/pam/main.cc b/src/pam/main.cc index 91822c9..530d5c8 100644 --- a/src/pam/main.cc +++ b/src/pam/main.cc @@ -65,25 +65,20 @@ auto howdy_error(int status, status = WEXITSTATUS(status); switch (status) { - // Status 10 means we couldn't find any face models - case 10: + 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: + case CompareError::TIMEOUT_REACHED: syslog(LOG_ERR, "Failure, timeout reached"); break; - // Status 12 means we aborted - case 12: + case CompareError::ABORT: syslog(LOG_ERR, "Failure, general abort"); break; - // Status 13 means the image was too dark - case 13: + 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, std::string(S("Unknown error: ") + status).c_str()); @@ -219,10 +214,6 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, Workaround workaround = get_workaround(config.GetString("core", "workaround", "input")); - // We ask for the password if the function requires it and if a workaround is - // set - auth_tok = auth_tok && workaround != Workaround::Off; - // Will contain PAM conversation structure struct pam_conv *conv = nullptr; const void **conv_ptr = @@ -316,7 +307,9 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, return std::tuple(pam_res, auth_tok_ptr); }); - if (auth_tok) { + // 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(); } diff --git a/src/pam/main.hh b/src/pam/main.hh index 17a6626..b19d2ec 100644 --- a/src/pam/main.hh +++ b/src/pam/main.hh @@ -1,13 +1,15 @@ #ifndef MAIN_H_ #define MAIN_H_ -#include #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; From 30760be3280a4b82a6bb2e89eb29e20024dce72c Mon Sep 17 00:00:00 2001 From: MusiKid Date: Wed, 26 Jan 2022 14:11:55 +0100 Subject: [PATCH 57/61] fix: fix issues in `optional_task` --- src/pam/optional_task.hh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pam/optional_task.hh b/src/pam/optional_task.hh index 29214bf..ff4c551 100644 --- a/src/pam/optional_task.hh +++ b/src/pam/optional_task.hh @@ -20,7 +20,6 @@ public: template auto wait(std::chrono::duration dur) -> std::future_status; auto get() -> T; - auto is_active() -> bool; void stop(bool force); ~optional_task(); }; @@ -28,7 +27,7 @@ public: template optional_task::optional_task(std::function fn) : _task(std::packaged_task(std::move(fn))), - _future(_task.get_future()) {} + _future(_task.get_future()), _is_active(false), _spawned(false) {} // Create a new thread and launch the task on it. template void optional_task::activate() { From 5126a8ab61300a2d99fc76578f74904ac9ca7aeb Mon Sep 17 00:00:00 2001 From: MusiKid Date: Wed, 26 Jan 2022 14:19:12 +0100 Subject: [PATCH 58/61] refactor: improve variable names --- src/pam/enter_device.cc | 1 - src/pam/main.hh | 7 ++++++- src/pam/optional_task.hh | 38 +++++++++++++++++++------------------- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/pam/enter_device.cc b/src/pam/enter_device.cc index 66de0eb..0114d8d 100644 --- a/src/pam/enter_device.cc +++ b/src/pam/enter_device.cc @@ -21,7 +21,6 @@ EnterDevice::EnterDevice() strerror(-err)); } - raw_uinput_device.reset(uinput_dev_ptr); }; diff --git a/src/pam/main.hh b/src/pam/main.hh index b19d2ec..3d250fe 100644 --- a/src/pam/main.hh +++ b/src/pam/main.hh @@ -8,7 +8,12 @@ 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 }; +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") { diff --git a/src/pam/optional_task.hh b/src/pam/optional_task.hh index ff4c551..36f9929 100644 --- a/src/pam/optional_task.hh +++ b/src/pam/optional_task.hh @@ -8,11 +8,11 @@ // 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; + std::thread thread; + std::packaged_task task; + std::future future; + bool spawned; + bool is_active; public: explicit optional_task(std::function fn); @@ -26,14 +26,14 @@ public: template optional_task::optional_task(std::function fn) - : _task(std::packaged_task(std::move(fn))), - _future(_task.get_future()), _is_active(false), _spawned(false) {} + : 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. @@ -41,15 +41,15 @@ template template auto optional_task::wait(std::chrono::duration dur) -> std::future_status { - return _future.wait_for(dur); + 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(); + assert(!is_active && spawned); + return future.get(); } // Stop the thread: @@ -58,22 +58,22 @@ template auto optional_task::get() -> T { // 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); } } From b903b426a63bc9c88cee3c747ab7c4f62425edcb Mon Sep 17 00:00:00 2001 From: MusiKid Date: Wed, 26 Jan 2022 14:21:00 +0100 Subject: [PATCH 59/61] chore: fix .clang-tidy --- src/pam/.clang-tidy | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pam/.clang-tidy b/src/pam/.clang-tidy index 9846031..887f4c3 100644 --- a/src/pam/.clang-tidy +++ b/src/pam/.clang-tidy @@ -1,6 +1,5 @@ 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' -HeaderFilterRegex: '.*' CheckOptions: - key: readability-function-cognitive-complexity.Threshold value: '50' From f7aefcd0ca1ef050edd2b971778d63b34cdbb2f2 Mon Sep 17 00:00:00 2001 From: musikid Date: Mon, 14 Mar 2022 19:24:23 +0100 Subject: [PATCH 60/61] chore: move to howdy folder --- {src => howdy/src}/pam/.clang-tidy | 0 howdy/src/pam/README.md | 29 +- {src => howdy/src}/pam/enter_device.cc | 0 {src => howdy/src}/pam/enter_device.hh | 0 howdy/src/pam/main.cc | 502 +++++++++++++------------ howdy/src/pam/main.hh | 38 +- howdy/src/pam/meson.build | 27 +- howdy/src/pam/optional_task.hh | 70 ++-- {src => howdy/src}/pam/po/LINGUAS | 0 {src => howdy/src}/pam/po/POTFILES | 0 {src => howdy/src}/pam/po/meson.build | 0 howdy/src/pam/subprojects/inih.wrap | 16 +- src/pam/.gitignore | 1 - src/pam/README.md | 35 -- src/pam/main.cc | 428 --------------------- src/pam/main.hh | 30 -- src/pam/meson.build | 24 -- src/pam/optional_task.hh | 81 ---- src/pam/subprojects/inih.wrap | 13 - 19 files changed, 381 insertions(+), 913 deletions(-) rename {src => howdy/src}/pam/.clang-tidy (100%) rename {src => howdy/src}/pam/enter_device.cc (100%) rename {src => howdy/src}/pam/enter_device.hh (100%) rename {src => howdy/src}/pam/po/LINGUAS (100%) rename {src => howdy/src}/pam/po/POTFILES (100%) rename {src => howdy/src}/pam/po/meson.build (100%) delete mode 100644 src/pam/.gitignore delete mode 100644 src/pam/README.md delete mode 100644 src/pam/main.cc delete mode 100644 src/pam/main.hh delete mode 100644 src/pam/meson.build delete mode 100644 src/pam/optional_task.hh delete mode 100644 src/pam/subprojects/inih.wrap 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 - From 68f06e8c1e56b9f47ddf270b3d9a7f5dcfcb99dd Mon Sep 17 00:00:00 2001 From: musikid Date: Thu, 17 Mar 2022 19:13:57 +0100 Subject: [PATCH 61/61] ci: fix path --- .github/workflows/check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 69e06f2..1843f5d 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -17,7 +17,7 @@ jobs: - name: Build run: | - meson setup build src/pam + meson setup build howdy/src/pam ninja -C build - name: Check source code