From 609b8e99df5c76d905861d60d0bc540c89d105bd Mon Sep 17 00:00:00 2001 From: MusiKid Date: Sat, 26 Dec 2020 09:33:19 +0100 Subject: [PATCH 01/80] [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/80] 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/80] 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/80] 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/80] 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/80] 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/80] 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 5d6fecbe547f207c658b4c2f5d5daee73f4b43ac Mon Sep 17 00:00:00 2001 From: Jinn Koriech Date: Tue, 6 Apr 2021 13:48:41 +0100 Subject: [PATCH 08/80] Add python-opencv as a dependency for ArchLinux --- archlinux/howdy/.SRCINFO | 1 + 1 file changed, 1 insertion(+) diff --git a/archlinux/howdy/.SRCINFO b/archlinux/howdy/.SRCINFO index cf7022d..0fbef43 100644 --- a/archlinux/howdy/.SRCINFO +++ b/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 02a831ff8ee943ac988240b779cd03cc09f1ea59 Mon Sep 17 00:00:00 2001 From: MusiKid Date: Wed, 7 Apr 2021 11:14:07 +0200 Subject: [PATCH 09/80] 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 10/80] 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 11/80] 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 12/80] 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 13/80] 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 14/80] 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 15/80] 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 16/80] 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 17/80] 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 18/80] 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 19/80] 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 20/80] 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 21/80] 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 22/80] 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 23/80] 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 24/80] 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 25/80] 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 26/80] 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 27/80] 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 28/80] 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 29/80] 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 30/80] 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 31/80] 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 32/80] 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 33/80] 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 34/80] 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 35/80] 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 36/80] 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 37/80] 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 38/80] 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 39/80] 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 40/80] 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 41/80] 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 42/80] 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 43/80] 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 44/80] 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 45/80] 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 46/80] 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 47/80] 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 48/80] 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 49/80] 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 50/80] 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 51/80] 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 52/80] 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 53/80] 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 54/80] 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 55/80] 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 56/80] 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 57/80] 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 58/80] 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 59/80] 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 60/80] 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 61/80] 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 62/80] 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 From 8bedd5f2c09585d40892d7e1c449a91f1ddccef6 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Mon, 18 Apr 2022 13:19:34 +0200 Subject: [PATCH 63/80] Debian package updates --- howdy/debian/control | 2 +- howdy/debian/postinst | 120 ++++++++++++++++----------------------- howdy/debian/rules | 6 +- howdy/src/pam/.gitignore | 2 +- 4 files changed, 57 insertions(+), 73 deletions(-) diff --git a/howdy/debian/control b/howdy/debian/control index 17b465b..f414ce9 100644 --- a/howdy/debian/control +++ b/howdy/debian/control @@ -9,7 +9,7 @@ Vcs-Git: https://github.com/boltgolt/howdy Package: howdy Homepage: https://github.com/boltgolt/howdy Architecture: amd64 -Depends: ${misc:Depends}, ${shlibs:Depends}, curl|wget, python3, python3-pip, python3-dev, python3-setuptools, libopencv-dev, cmake +Depends: ${misc:Depends}, libc6, libgcc-s1, libpam0g, libstdc++6, curl | wget, python3, python3-pip, python3-dev, python3-setuptools, python3-numpy, python-opencv, libopencv-dev, cmake Recommends: libatlas-base-dev | libopenblas-dev | liblapack-dev, howdy-gtk Suggests: nvidia-cuda-dev (>= 7.5) Description: Howdy: Windows Hello style authentication for Linux. diff --git a/howdy/debian/postinst b/howdy/debian/postinst index 0a31c74..579081e 100755 --- a/howdy/debian/postinst +++ b/howdy/debian/postinst @@ -40,84 +40,68 @@ def col(id): # Create shorthand for subprocess creation sc = subprocess.call -# We're not in fresh configuration mode so don't continue the setup -if not os.path.exists("/tmp/howdy_picked_device"): - # Check if we have an older config we can restore - if len(sys.argv) > 2: - if os.path.exists("/tmp/howdy_config_backup_v" + sys.argv[2] + ".ini"): - # Get the config parser - import configparser +# If the package is being upgraded +if "upgrade" in sys.argv: + # If preinst has made a config backup + if os.path.exists("/tmp/howdy_config_backup_v" + sys.argv[2] + ".ini"): + # Get the config parser + import configparser - # Load th old and new config files - oldConf = configparser.ConfigParser() - oldConf.read("/tmp/howdy_config_backup_v" + sys.argv[2] + ".ini") - newConf = configparser.ConfigParser() - newConf.read("/lib/security/howdy/config.ini") + # Load th old and new config files + oldConf = configparser.ConfigParser() + oldConf.read("/tmp/howdy_config_backup_v" + sys.argv[2] + ".ini") + newConf = configparser.ConfigParser() + newConf.read("/lib/security/howdy/config.ini") - # Go through every setting in the old config and apply it to the new file - for section in oldConf.sections(): - for (key, value) in oldConf.items(section): + # Go through every setting in the old config and apply it to the new file + for section in oldConf.sections(): + for (key, value) in oldConf.items(section): - # MIGRATION 2.3.1 -> 2.4.0 - # If config is still using the old device_id parameter, convert it to a path - if key == "device_id": - key = "device_path" - value = "/dev/video" + value + # MIGRATION 2.3.1 -> 2.4.0 + # If config is still using the old device_id parameter, convert it to a path + if key == "device_id": + key = "device_path" + value = "/dev/video" + value - # MIGRATION 2.4.0 -> 2.5.0 - # Finally correct typo in "timout" config value - if key == "timout": - key = "timeout" + # MIGRATION 2.4.0 -> 2.5.0 + # Finally correct typo in "timout" config value + if key == "timout": + key = "timeout" - # MIGRATION 2.5.0 -> 2.5.1 - # Remove unsafe automatic dismissal of lock screen - if key == "dismiss_lockscreen": - if value == "true": - print("DEPRECATION: Config value dismiss_lockscreen is no longer supported because of login loop issues.") - continue + # MIGRATION 2.5.0 -> 2.5.1 + # Remove unsafe automatic dismissal of lock screen + if key == "dismiss_lockscreen": + if value == "true": + print("DEPRECATION: Config value dismiss_lockscreen is no longer supported because of login loop issues.") + continue - # MIGRATION 2.6.1 -> 3.0.0 - # Fix capture being enabled by default - if key == "capture_failed" or key == "capture_successful": - if value == "true": - print("NOTICE: Howdy login image captures have been disabled by default, change the config to enable them again") - value = "false" + # MIGRATION 2.6.1 -> 3.0.0 + # Fix capture being enabled by default + if key == "capture_failed" or key == "capture_successful": + if value == "true": + print("NOTICE: Howdy login image captures have been disabled by default, change the config to enable them again") + value = "false" - # MIGRATION 2.6.1 -> 3.0.0 - # Rename config options so they don't do the opposite of what is commonly expected - if key == "ignore_ssh": - key = "abort_if_ssh" - if key == "ignore_closed_lid": - key = "abort_if_lid_closed" + # MIGRATION 2.6.1 -> 3.0.0 + # Rename config options so they don't do the opposite of what is commonly expected + if key == "ignore_ssh": + key = "abort_if_ssh" + if key == "ignore_closed_lid": + key = "abort_if_lid_closed" - try: - newConf.set(section, key, value) - # Add a new section where needed - except configparser.NoSectionError: - newConf.add_section(section) - newConf.set(section, key, value) + try: + newConf.set(section, key, value) + # Add a new section where needed + except configparser.NoSectionError: + newConf.add_section(section) + newConf.set(section, key, value) - # Write it all to file - with open("/lib/security/howdy/config.ini", "w") as configfile: - newConf.write(configfile) - - # Install dlib data files if needed - if not os.path.exists("/lib/security/howdy/dlib-data/shape_predictor_5_face_landmarks.dat"): - print("Attempting installation of missing data files") - handleStatus(subprocess.call(["./install.sh"], shell=True, cwd="/lib/security/howdy/dlib-data")) + # Write it all to file + with open("/lib/security/howdy/config.ini", "w") as configfile: + newConf.write(configfile) sys.exit(0) -log("Upgrading pip to the latest version") - -# Update pip -handleStatus(sc(["pip3", "install", "--upgrade", "pip"])) - -log("Upgrading numpy to the latest version") - -# Update numpy -handleStatus(subprocess.call(["pip3", "install", "--upgrade", "numpy"])) - log("Downloading and unpacking data files") # Run the bash script to download and unpack the .dat files needed @@ -193,10 +177,6 @@ rmtree(DLIB_DIR) print("Temporary dlib files removed") -log("Installing OpenCV") - -handleStatus(subprocess.call(["pip3", "install", "--no-cache-dir", "opencv-python"])) - log("Configuring howdy") # Manually change the camera id to the one picked diff --git a/howdy/debian/rules b/howdy/debian/rules index 1176fe0..41a89d7 100755 --- a/howdy/debian/rules +++ b/howdy/debian/rules @@ -10,9 +10,13 @@ include /usr/share/dpkg/default.mk build: # Create build dir meson setup -Dinih:with_INIReader=true build src/pam - # Compile shared object + # Compile shared object meson compile -C build clean: # Delete mason build directory rm -rf ./build + # Force remove temp debian build directory + rm -rf ./debian/howdy + # Make sure subprojects get pulled locally + meson subprojects download --sourcedir src/pam diff --git a/howdy/src/pam/.gitignore b/howdy/src/pam/.gitignore index feaa494..2e4843c 100644 --- a/howdy/src/pam/.gitignore +++ b/howdy/src/pam/.gitignore @@ -1 +1 @@ -subprojects/*/ +subprojects/inih/ From 84ad5ddc52b9848a17e884c0630e0fae2cd83caf Mon Sep 17 00:00:00 2001 From: moonburnt Date: Sat, 18 Sep 2021 11:45:35 +0300 Subject: [PATCH 64/80] fix: removed pointless check "if not" will already trigger on None, no need to specifically mention it --- howdy/src/cli/add.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/howdy/src/cli/add.py b/howdy/src/cli/add.py index 1cab2e9..8e117f4 100644 --- a/howdy/src/cli/add.py +++ b/howdy/src/cli/add.py @@ -176,7 +176,7 @@ while frames < 60: video_capture.release() # If we've found no faces, try to determine why -if face_locations is None or not face_locations: +if not face_locations: if valid_frames == 0: print(_("Camera saw only black frames - is IR emitter working?")) elif valid_frames == dark_tries: From 494021e679ce2d2f7f6a1235cd67430c412b03a9 Mon Sep 17 00:00:00 2001 From: moonburnt Date: Sat, 18 Sep 2021 17:22:12 +0300 Subject: [PATCH 65/80] style: fixed spelling errors --- howdy/src/pam.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/howdy/src/pam.py b/howdy/src/pam.py index 5e72f76..262b24c 100644 --- a/howdy/src/pam.py +++ b/howdy/src/pam.py @@ -15,7 +15,7 @@ config.read(os.path.dirname(os.path.abspath(__file__)) + "/config.ini") def doAuth(pamh): - """Starts authentication in a seperate process""" + """Starts authentication in a separate process""" # Abort if Howdy is disabled if config.getboolean("core", "disabled"): @@ -103,7 +103,7 @@ def doAuth(pamh): syslog.closelog() return pamh.PAM_SUCCESS - # Otherwise, we can't discribe what happend but it wasn't successful + # Otherwise, we can't describe what happened but it wasn't successful pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Unknown error: " + str(status))) syslog.syslog(syslog.LOG_INFO, "Failure, unknown error" + str(status)) syslog.closelog() From 9dabd7e4aae36e0734ee0ae30a7a5b492f830d8b Mon Sep 17 00:00:00 2001 From: ryancastro <4700651+ryancastro@users.noreply.github.com> Date: Thu, 21 Apr 2022 11:03:26 -0600 Subject: [PATCH 66/80] Sentence was missing an 'a'. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7fe3450..eead676 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ If you encounter an error that hasn't been reported yet, don't be afraid to open ## A note on security -This package is in no way as secure as a password and will never be. Although it's harder to fool than normal face recognition, a person who looks similar to you or well-printed photo of you could be enough to do it. Howdy is a more quick and convenient way of logging in, not a more secure one. +This package is in no way as secure as a password and will never be. Although it's harder to fool than normal face recognition, a person who looks similar to you, or a well-printed photo of you could be enough to do it. Howdy is a more quick and convenient way of logging in, not a more secure one. To minimize the chance of this program being compromised, it's recommended to leave Howdy in `/lib/security` and to keep it read-only. From 70987923488f80c3231451c4cfe2532bd541aa2c Mon Sep 17 00:00:00 2001 From: Matthias Braun Date: Tue, 26 Apr 2022 20:58:07 +0200 Subject: [PATCH 67/80] Fix typos in README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index eead676..21e5800 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ Go to the [openSUSE wiki page](https://en.opensuse.org/SDB:Facial_authentication After installation, Howdy needs to learn what you look like so it can recognise you later. Run `sudo howdy add` to add a face model. -If nothing went wrong we should be able to run sudo by just showing your face. Open a new terminal and run `sudo -i` to see it in action. Please check [this wiki page](https://github.com/boltgolt/howdy/wiki/Common-issues) if you've experiencing problems or [search](https://github.com/boltgolt/howdy/issues) for similar issues. +If nothing went wrong we should be able to run sudo by just showing your face. Open a new terminal and run `sudo -i` to see it in action. Please check [this wiki page](https://github.com/boltgolt/howdy/wiki/Common-issues) if you're experiencing problems or [search](https://github.com/boltgolt/howdy/issues) for similar issues. If you're curious you can run `sudo howdy config` to open the central config file and see the options Howdy has to offer. On most systems this will open the nano editor, where you have to press `ctrl`+`x` to save your changes. @@ -84,12 +84,12 @@ howdy [-U user] [-y] command [argument] | Command | Description | |-----------|-----------------------------------------------| -| `add` | Add a new face model for an user | -| `clear` | Remove all face models for an user | +| `add` | Add a new face model for a user | +| `clear` | Remove all face models for a user | | `config` | Open the config file in your default editor | | `disable` | Disable or enable howdy | -| `list` | List all saved face models for an user | -| `remove` | Remove a specific model for an user | +| `list` | List all saved face models for a user | +| `remove` | Remove a specific model for a user | | `snapshot`| Take a snapshot of your camera input | | `test` | Test the camera and recognition methods | | `version` | Print the current version number | @@ -102,7 +102,7 @@ Code contributions are also very welcome. If you want to port Howdy to another d ## Troubleshooting -Any python errors get logged directly into the console and should indicate what went wrong. If authentication still fails but no errors are printed you could take a look at the last lines in `/var/log/auth.log` to see if anything has been reported there. +Any Python errors get logged directly into the console and should indicate what went wrong. If authentication still fails but no errors are printed, you could take a look at the last lines in `/var/log/auth.log` to see if anything has been reported there. If you encounter an error that hasn't been reported yet, don't be afraid to open a new issue. From b1f161da41236d36b2535c82eeadeafc2734ee9c Mon Sep 17 00:00:00 2001 From: Nuttapong Punpipat Date: Fri, 20 May 2022 20:29:40 +0700 Subject: [PATCH 68/80] rename wrong attribute name to prevent AttributeError PYthon Exception --- howdy/src/cli/test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/howdy/src/cli/test.py b/howdy/src/cli/test.py index 8933797..f7b7d01 100644 --- a/howdy/src/cli/test.py +++ b/howdy/src/cli/test.py @@ -192,8 +192,8 @@ try: # are captured and even after a delay it does not # always work. Setting exposure at every frame is # reliable though. - video_capture.intenal.set(cv2.CAP_PROP_AUTO_EXPOSURE, 1.0) # 1 = Manual - video_capture.intenal.set(cv2.CAP_PROP_EXPOSURE, float(exposure)) + video_capture.internal.set(cv2.CAP_PROP_AUTO_EXPOSURE, 1.0) # 1 = Manual + video_capture.internal.set(cv2.CAP_PROP_EXPOSURE, float(exposure)) # On ctrl+C except KeyboardInterrupt: From 544ccb4b2aa4ce4f6a551964fc88b3ce29b224e3 Mon Sep 17 00:00:00 2001 From: FeliiiciaWen Date: Tue, 26 Jul 2022 03:04:38 +0800 Subject: [PATCH 69/80] Fix config.getboolean --- howdy/src/pam.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/howdy/src/pam.py b/howdy/src/pam.py index 262b24c..5197fa0 100644 --- a/howdy/src/pam.py +++ b/howdy/src/pam.py @@ -19,24 +19,24 @@ def doAuth(pamh): # Abort if Howdy is disabled if config.getboolean("core", "disabled"): + pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Howdy is disabled.")) return pamh.PAM_AUTHINFO_UNAVAIL - # Abort if we're in a remote SSH env - if config.getboolean("core", "ignore_ssh"): + if config.getboolean("core", "abort_if_ssh"): if "SSH_CONNECTION" in os.environ or "SSH_CLIENT" in os.environ or "SSHD_OPTS" in os.environ: + pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Howdy is disabled in ssh.")) return pamh.PAM_AUTHINFO_UNAVAIL - # Abort if lid is closed - if config.getboolean("core", "ignore_closed_lid"): + if config.getboolean("core", "abort_if_lid_closed"): if any("closed" in open(f).read() for f in glob.glob("/proc/acpi/button/lid/*/state")): + pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "LID is closed.")) return pamh.PAM_AUTHINFO_UNAVAIL - # Abort if the video device does not exist if not os.path.exists(config.get("video", "device_path")): if config.getboolean("video", "warn_no_device"): - print("Camera path is not configured correctly, please edit the 'device_path' config value.") + pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Camera path is not configured correctly, please edit the 'device_path' config value.")) return pamh.PAM_AUTHINFO_UNAVAIL - + # Set up syslog syslog.openlog("[HOWDY]", 0, syslog.LOG_AUTH) From 39272035aff22bfc827e9ab93d1b1b03579ae7d2 Mon Sep 17 00:00:00 2001 From: Alberto Fanjul Date: Sun, 31 Jan 2021 10:58:37 +0100 Subject: [PATCH 70/80] Close test using Close window button --- src/cli/test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/test.py b/src/cli/test.py index c62fcef..20a10a1 100644 --- a/src/cli/test.py +++ b/src/cli/test.py @@ -80,7 +80,7 @@ rec_tm = 0 # Wrap everything in an keyboard interupt handler try: - while True: + while cv2.getWindowProperty("Howdy Test", cv2.WND_PROP_VISIBLE) > 0: frame_tm = time.time() # Increment the frames From 108ddd5e9409dd38bd2aeb2e7d16c7d2a9d08d4d Mon Sep 17 00:00:00 2001 From: Alberto Fanjul Date: Sun, 31 Jan 2021 11:00:05 +0100 Subject: [PATCH 71/80] Show model label detected --- src/cli/test.py | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/src/cli/test.py b/src/cli/test.py index 20a10a1..bfd12f5 100644 --- a/src/cli/test.py +++ b/src/cli/test.py @@ -2,11 +2,14 @@ # Import required modules import configparser +import builtins import os +import json import sys import time import dlib import cv2 +import numpy as np from recorders.video_capture import VideoCapture # Get the absolute path to the current file @@ -59,6 +62,22 @@ if use_cnn: else: face_detector = dlib.get_frontal_face_detector() +pose_predictor = dlib.shape_predictor(path + "/../dlib-data/shape_predictor_5_face_landmarks.dat") +face_encoder = dlib.face_recognition_model_v1(path + "/../dlib-data/dlib_face_recognition_resnet_model_v1.dat") + +encodings = [] +models = None + +try: + user = builtins.howdy_user + models = json.load(open(path + "/../models/" + user + ".dat")) + + for model in models: + encodings += model["data"] +except FileNotFoundError: + print("No face model known for the user " + user + ", please run:") + print("\n\tsudo howdy -U " + user + " add\n") + clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) # Open the window and attach a a mouse listener @@ -97,7 +116,7 @@ try: sec_frames = 0 # Grab a single frame of video - _, frame = video_capture.read_frame() + orig_frame, frame = video_capture.read_frame() frame = clahe.apply(frame) # Make a frame to put overlays in @@ -155,6 +174,23 @@ try: if use_cnn: loc = loc.rect + color = (0, 0, 230) + if models: + face_landmark = pose_predictor(orig_frame, loc) + face_encoding = np.array(face_encoder.compute_face_descriptor(orig_frame, face_landmark, 1)) + + # Match this found face against a known face + matches = np.linalg.norm(encodings - face_encoding, axis=1) + + # Get best match + match_index = np.argmin(matches) + match = matches[match_index] + + percent = match * 100 + label = models[match_index]["label"] + color = (230, 0, 0) + cv2.putText(overlay, "{} {}(%)".format(label, percent), (width - 68, 32), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 255, 0), 0, cv2.LINE_AA) + # Get the center X and Y from the rectangular points x = int((loc.right() - loc.left()) / 2) + loc.left() y = int((loc.bottom() - loc.top()) / 2) + loc.top() @@ -165,7 +201,7 @@ try: r = int(r + (r * 0.2)) # Draw the Circle in green - cv2.circle(overlay, (x, y), r, (0, 0, 230), 2) + cv2.circle(overlay, (x, y), r, color, 2) # Add the overlay to the frame with some transparency alpha = 0.65 From 2bd8834b8c75b337ba9ee749e2f64705cea42536 Mon Sep 17 00:00:00 2001 From: Mika Cousin Date: Mon, 31 Oct 2022 16:35:21 +0100 Subject: [PATCH 72/80] Add python3 pam-python support --- src/pam.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pam.py b/src/pam.py index 5dd3c5d..6dd46a6 100644 --- a/src/pam.py +++ b/src/pam.py @@ -4,13 +4,17 @@ import subprocess import os import glob +import sys import syslog -# pam-python is running python 2, so we use the old module here -import ConfigParser +if sys.version_info.major < 3: + import ConfigParser + config = ConfigParser.ConfigParser() +else: + import configparser + config = configparser.ConfigParser() # Read config from disk -config = ConfigParser.ConfigParser() config.read(os.path.dirname(os.path.abspath(__file__)) + "/config.ini") From 0803465cd92ffd3bbcf20139d2f06f979f302a52 Mon Sep 17 00:00:00 2001 From: jEzEk Date: Wed, 23 Nov 2022 00:36:16 +0100 Subject: [PATCH 73/80] Use new config variable names in all scripts Changed: capture_failed -> save_failed capture_successful -> save_successful ignore_ssh -> abort_if_ssh ignore_closed_lid -> abort_if_lid_closed --- howdy/debian/postinst | 4 ++++ howdy/src/compare.py | 10 +++++----- howdy/src/pam.py | 2 +- howdy/src/pam/main.cc | 4 ++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/howdy/debian/postinst b/howdy/debian/postinst index 579081e..b8090e1 100755 --- a/howdy/debian/postinst +++ b/howdy/debian/postinst @@ -88,6 +88,10 @@ if "upgrade" in sys.argv: key = "abort_if_ssh" if key == "ignore_closed_lid": key = "abort_if_lid_closed" + if key == "capture_failed": + key = "save_failed" + if key == "capture_successful": + key = "save_successful" try: newConf.set(section, key, value) diff --git a/howdy/src/compare.py b/howdy/src/compare.py index 9d07dd7..7efa64a 100644 --- a/howdy/src/compare.py +++ b/howdy/src/compare.py @@ -146,8 +146,8 @@ timeout = config.getint("video", "timeout", fallback=5) dark_threshold = config.getfloat("video", "dark_threshold", fallback=50.0) video_certainty = config.getfloat("video", "certainty", fallback=3.5) / 10 end_report = config.getboolean("debug", "end_report", fallback=False) -capture_failed = config.getboolean("snapshots", "capture_failed", fallback=False) -capture_successful = config.getboolean("snapshots", "capture_successful", fallback=False) +save_failed = config.getboolean("snapshots", "save_failed", fallback=False) +save_successful = config.getboolean("snapshots", "save_successful", fallback=False) gtk_stdout = config.getboolean("debug", "gtk_stdout", fallback=False) rotate = config.getint("video", "rotate", fallback=0) @@ -232,7 +232,7 @@ while True: # Stop if we've exceded the time limit if time.time() - timings["fr"] > timeout: # Create a timeout snapshot if enabled - if capture_failed: + if save_failed: make_snapshot(_("FAILED")) if dark_tries == valid_frames: @@ -247,7 +247,7 @@ while True: gsframe = clahe.apply(gsframe) # If snapshots have been turned on - if capture_failed or capture_successful: + if save_failed or save_successful: # Start capturing frames for the snapshot if len(snapframes) < 3: snapframes.append(frame) @@ -354,7 +354,7 @@ while True: print(_("Winning model: %d (\"%s\")") % (match_index, models[match_index]["label"])) # Make snapshot if enabled - if capture_successful: + if save_successful: make_snapshot(_("SUCCESSFUL")) # Run rubberstamps if enabled diff --git a/howdy/src/pam.py b/howdy/src/pam.py index 952768a..14aa9e1 100644 --- a/howdy/src/pam.py +++ b/howdy/src/pam.py @@ -40,7 +40,7 @@ def doAuth(pamh): if config.getboolean("video", "warn_no_device"): pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Camera path is not configured correctly, please edit the 'device_path' config value.")) return pamh.PAM_AUTHINFO_UNAVAIL - + # Set up syslog syslog.openlog("[HOWDY]", 0, syslog.LOG_AUTH) diff --git a/howdy/src/pam/main.cc b/howdy/src/pam/main.cc index 530d5c8..135ea03 100644 --- a/howdy/src/pam/main.cc +++ b/howdy/src/pam/main.cc @@ -141,7 +141,7 @@ auto check_enabled(const INIReader &config) -> int { } // Stop if we're in a remote shell and configured to exit - if (config.GetBoolean("core", "ignore_ssh", true)) { + if (config.GetBoolean("core", "abort_if_ssh", true)) { if (getenv("SSH_CONNECTION") != nullptr || getenv("SSH_CLIENT") != nullptr || getenv("SSHD_OPTS") != nullptr) { syslog(LOG_INFO, "Skipped authentication, SSH session detected"); @@ -150,7 +150,7 @@ auto check_enabled(const INIReader &config) -> int { } // Try to detect the laptop lid state and stop if it's closed - if (config.GetBoolean("core", "ignore_closed_lid", true)) { + if (config.GetBoolean("core", "abort_if_lid_closed", true)) { glob_t glob_result; // Get any files containing lid state From 1dc56b170592e7ce403b9710164298ea02ea8097 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Sat, 18 Feb 2023 20:07:32 +0100 Subject: [PATCH 74/80] Finally move fully to the compiled .so --- howdy/debian/control | 4 +- howdy/debian/install | 9 +-- howdy/debian/rules | 2 +- howdy/src/compare.py | 22 +++++-- howdy/src/pam-config/howdy | 2 +- howdy/src/pam.py | 130 ------------------------------------- howdy/src/pam/main.cc | 25 ++++++- howdy/src/pam/main.hh | 3 +- 8 files changed, 48 insertions(+), 149 deletions(-) delete mode 100644 howdy/src/pam.py diff --git a/howdy/debian/control b/howdy/debian/control index f414ce9..d164854 100644 --- a/howdy/debian/control +++ b/howdy/debian/control @@ -2,14 +2,14 @@ Source: howdy Section: misc Priority: optional Standards-Version: 3.9.7 -Build-Depends: python, devscripts, dh-make, debhelper, fakeroot, python3, python3-pip, python3-setuptools, python3-wheel, ninja-build, meson, libpam0g-dev, libboost-all-dev +Build-Depends: devscripts, git, dh-make, debhelper, fakeroot, python3, python3-pip, python3-setuptools, python3-wheel, ninja-build, meson, libpam0g-dev, libboost-all-dev, pkg-config, libevdev-dev, libinih-dev Maintainer: boltgolt Vcs-Git: https://github.com/boltgolt/howdy Package: howdy Homepage: https://github.com/boltgolt/howdy Architecture: amd64 -Depends: ${misc:Depends}, libc6, libgcc-s1, libpam0g, libstdc++6, curl | wget, python3, python3-pip, python3-dev, python3-setuptools, python3-numpy, python-opencv, libopencv-dev, cmake +Depends: ${misc:Depends}, libc6, libgcc-s1, libpam0g, libstdc++6, curl | wget, python3, python3-pip, python3-dev, python3-setuptools, python3-numpy, python-opencv | python3-opencv, libopencv-dev, cmake, libinih-dev Recommends: libatlas-base-dev | libopenblas-dev | liblapack-dev, howdy-gtk Suggests: nvidia-cuda-dev (>= 7.5) Description: Howdy: Windows Hello style authentication for Linux. diff --git a/howdy/debian/install b/howdy/debian/install index 3fadce0..cc3c9ea 100644 --- a/howdy/debian/install +++ b/howdy/debian/install @@ -1,16 +1,17 @@ src/cli/. lib/security/howdy/cli -src/dlib-data/. lib/security/howdy/dlib-data src/locales/. lib/security/howdy/locales src/recorders/. lib/security/howdy/recorders src/rubberstamps/. lib/security/howdy/rubberstamps src/cli.py lib/security/howdy src/compare.py lib/security/howdy -src/config.ini lib/security/howdy src/i18n.py lib/security/howdy src/logo.png lib/security/howdy -src/pam.py lib/security/howdy src/snapshot.py lib/security/howdy +build/pam_howdy.so lib/security/howdy + +src/dlib-data/. etc/howdy/dlib-data +src/config.ini etc/howdy + src/autocomplete/. usr/share/bash-completion/completions src/pam-config/. /usr/share/pam-configs -build/libpam_howdy.so lib/security/howdy diff --git a/howdy/debian/rules b/howdy/debian/rules index 41a89d7..91e16ac 100755 --- a/howdy/debian/rules +++ b/howdy/debian/rules @@ -11,7 +11,7 @@ build: # Create build dir meson setup -Dinih:with_INIReader=true build src/pam # Compile shared object - meson compile -C build + ninja -C build clean: # Delete mason build directory diff --git a/howdy/src/compare.py b/howdy/src/compare.py index 9d07dd7..4bcce8b 100644 --- a/howdy/src/compare.py +++ b/howdy/src/compare.py @@ -1,4 +1,5 @@ -# Compare incomming video with known faces +#!/usr/bin/env python3 +# Compare incoming video with known faces # Running in a local python instance to get around PATH issues # Import time so we can start timing asap @@ -23,11 +24,14 @@ import snapshot import numpy as np import _thread as thread -from i18n import _ +# Allow imports from the local howdy folder +sys.path.append('/lib/security/howdy') + from recorders.video_capture import VideoCapture +from i18n import _ def exit(code=None): - """Exit while closeing howdy-gtk properly""" + """Exit while closing howdy-gtk properly""" global gtk_proc # Exit the auth ui process if there is one @@ -99,8 +103,8 @@ def send_to_ui(type, message): if len(sys.argv) < 2: exit(12) -# Get the absolute path to the current directory -PATH = os.path.abspath(__file__ + "/..") +# Get the absolute path to the config directory +PATH = "/etc/howdy" # The username of the user being authenticated user = sys.argv[1] @@ -110,7 +114,7 @@ models = [] encodings = [] # Amount of ignored 100% black frames black_tries = 0 -# Amount of ingnored dark frames +# Amount of ignored dark frames dark_tries = 0 # Total amount of frames captured frames = 0 @@ -229,7 +233,7 @@ while True: # Show it in the ui as subtext send_to_ui("S", ui_subtext) - # Stop if we've exceded the time limit + # Stop if we've exceeded the time limit if time.time() - timings["fr"] > timeout: # Create a timeout snapshot if enabled if capture_failed: @@ -268,6 +272,7 @@ while True: dark_running_total += darkness valid_frames += 1 + # If the image exceeds darkness threshold due to subject distance, # skip to the next frame if (darkness > dark_threshold): @@ -279,6 +284,7 @@ while True: # Apply that factor to the frame frame = cv2.resize(frame, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA) gsframe = cv2.resize(gsframe, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA) + # If camera is configured to rotate = 1, check portrait in addition to landscape if rotate == 1: if frames % 3 == 1: @@ -287,6 +293,7 @@ while True: if frames % 3 == 2: frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE) gsframe = cv2.rotate(gsframe, cv2.ROTATE_90_CLOCKWISE) + # If camera is configured to rotate = 2, check portrait orientation elif rotate == 2: if frames % 2 == 0: @@ -295,6 +302,7 @@ while True: else: frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE) gsframe = cv2.rotate(gsframe, cv2.ROTATE_90_CLOCKWISE) + # Get all faces from that frame as encodings # Upsamples 1 time face_locations = face_detector(gsframe, 1) diff --git a/howdy/src/pam-config/howdy b/howdy/src/pam-config/howdy index 28fb6b1..ed19039 100644 --- a/howdy/src/pam-config/howdy +++ b/howdy/src/pam-config/howdy @@ -3,4 +3,4 @@ Default: yes Priority: 512 Auth-Type: Primary Auth: - [success=end default=ignore] pam_python.so /lib/security/howdy/pam.py + [success=end default=ignore] /lib/security/howdy/pam_howdy.so diff --git a/howdy/src/pam.py b/howdy/src/pam.py deleted file mode 100644 index 262b24c..0000000 --- a/howdy/src/pam.py +++ /dev/null @@ -1,130 +0,0 @@ -# PAM interface in python, launches compare.py - -# Import required modules -import subprocess -import os -import glob -import syslog - -# pam-python is running python 2, so we use the old module here -import ConfigParser - -# Read config from disk -config = ConfigParser.ConfigParser() -config.read(os.path.dirname(os.path.abspath(__file__)) + "/config.ini") - - -def doAuth(pamh): - """Starts authentication in a separate process""" - - # Abort if Howdy is disabled - if config.getboolean("core", "disabled"): - return pamh.PAM_AUTHINFO_UNAVAIL - - # Abort if we're in a remote SSH env - if config.getboolean("core", "ignore_ssh"): - if "SSH_CONNECTION" in os.environ or "SSH_CLIENT" in os.environ or "SSHD_OPTS" in os.environ: - return pamh.PAM_AUTHINFO_UNAVAIL - - # Abort if lid is closed - if config.getboolean("core", "ignore_closed_lid"): - if any("closed" in open(f).read() for f in glob.glob("/proc/acpi/button/lid/*/state")): - return pamh.PAM_AUTHINFO_UNAVAIL - - # Abort if the video device does not exist - if not os.path.exists(config.get("video", "device_path")): - if config.getboolean("video", "warn_no_device"): - print("Camera path is not configured correctly, please edit the 'device_path' config value.") - return pamh.PAM_AUTHINFO_UNAVAIL - - # Set up syslog - syslog.openlog("[HOWDY]", 0, syslog.LOG_AUTH) - - # Alert the user that we are doing face detection - if config.getboolean("core", "detection_notice"): - pamh.conversation(pamh.Message(pamh.PAM_TEXT_INFO, "Attempting face detection")) - - syslog.syslog(syslog.LOG_INFO, "Attempting facial authentication for user " + pamh.get_user()) - - # Run compare as python3 subprocess to circumvent python version and import issues - status = subprocess.call(["/usr/bin/python3", os.path.dirname(os.path.abspath(__file__)) + "/compare.py", pamh.get_user()]) - - # Status 10 means we couldn't find any face models - if status == 10: - if not config.getboolean("core", "suppress_unknown"): - pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "No face model known")) - - syslog.syslog(syslog.LOG_NOTICE, "Failure, no face model known") - syslog.closelog() - return pamh.PAM_USER_UNKNOWN - - # Status 11 means we exceded the maximum retry count - elif status == 11: - if config.getboolean("core", "timeout_notice"): - pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Face detection timeout reached")) - syslog.syslog(syslog.LOG_INFO, "Failure, timeout reached") - syslog.closelog() - return pamh.PAM_AUTH_ERR - - # Status 12 means we aborted - elif status == 12: - syslog.syslog(syslog.LOG_INFO, "Failure, general abort") - syslog.closelog() - return pamh.PAM_AUTH_ERR - - # Status 13 means the image was too dark - elif status == 13: - pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Face detection image too dark")) - syslog.syslog(syslog.LOG_INFO, "Failure, image too dark") - syslog.closelog() - return pamh.PAM_AUTH_ERR - - # Status 14 means a rubberstamp could not be given - elif status == 14: - pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Rubberstamp denied")) - syslog.syslog(syslog.LOG_INFO, "Failure, rubberstamp did not succeed") - syslog.closelog() - return pamh.PAM_AUTH_ERR - - # Status 1 is probably a python crash - elif status == 1: - pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Howdy encountered error, check stderr")) - syslog.syslog(syslog.LOG_INFO, "Failure, process crashed while authenticating") - syslog.closelog() - return pamh.PAM_SYSTEM_ERR - - # Status 0 is a successful exit - elif status == 0: - # Show the success message if it isn't suppressed - if not config.getboolean("core", "no_confirmation"): - pamh.conversation(pamh.Message(pamh.PAM_TEXT_INFO, "Identified face as " + pamh.get_user())) - - syslog.syslog(syslog.LOG_INFO, "Login approved") - syslog.closelog() - return pamh.PAM_SUCCESS - - # Otherwise, we can't describe what happened but it wasn't successful - pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Unknown error: " + str(status))) - syslog.syslog(syslog.LOG_INFO, "Failure, unknown error" + str(status)) - syslog.closelog() - return pamh.PAM_SYSTEM_ERR - - -def pam_sm_authenticate(pamh, flags, args): - """Called by PAM when the user wants to authenticate, in sudo for example""" - return doAuth(pamh) - - -def pam_sm_open_session(pamh, flags, args): - """Called when starting a session, such as su""" - return doAuth(pamh) - - -def pam_sm_close_session(pamh, flags, argv): - """We don't need to clean anyting up at the end of a session, so returns true""" - return pamh.PAM_SUCCESS - - -def pam_sm_setcred(pamh, flags, argv): - """We don't need set any credentials, so returns true""" - return pamh.PAM_SUCCESS diff --git a/howdy/src/pam/main.cc b/howdy/src/pam/main.cc index 530d5c8..e3e79e2 100644 --- a/howdy/src/pam/main.cc +++ b/howdy/src/pam/main.cc @@ -193,7 +193,7 @@ auto check_enabled(const INIReader &config) -> int { */ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, bool auth_tok) -> int { - INIReader config("/lib/security/howdy/config.ini"); + INIReader config("/etc/howdy/config.ini"); openlog("pam_howdy", 0, LOG_AUTHPRIV); // Error out if we could not read the config file @@ -343,6 +343,27 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // The compare process has finished its execution child_task.stop(false); + // Get python process status code + int status = child_task.get(); + + // If python process ran into a timeout + // Do not send enter presses or terminate the PAM function, as the user might still be typing their password + if (status == CompareError::TIMEOUT_ACTIVE) { + // Wait for the password to be typed + 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; + } + + // 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" @@ -391,8 +412,6 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, pass_task.stop(false); } - int status = child_task.get(); - return howdy_status(username, status, config, conv_function); } diff --git a/howdy/src/pam/main.hh b/howdy/src/pam/main.hh index 3d250fe..5b5dcf8 100644 --- a/howdy/src/pam/main.hh +++ b/howdy/src/pam/main.hh @@ -12,7 +12,8 @@ enum CompareError : int { NO_FACE_MODEL = 10, TIMEOUT_REACHED = 11, ABORT = 12, - TOO_DARK = 13 + TOO_DARK = 13, + TIMEOUT_ACTIVE = 2816, }; inline auto get_workaround(const std::string &workaround) -> Workaround { From d00f7a94fd8b3fcb23cb5208407e79e963eb4287 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Sat, 18 Feb 2023 20:22:51 +0100 Subject: [PATCH 75/80] Fix warnings --- howdy/src/pam/main.cc | 16 ++++++++-------- howdy/src/pam/optional_task.hh | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/howdy/src/pam/main.cc b/howdy/src/pam/main.cc index e3e79e2..68501b4 100644 --- a/howdy/src/pam/main.cc +++ b/howdy/src/pam/main.cc @@ -270,8 +270,8 @@ auto 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) - std::mutex m; - std::condition_variable cv; + std::mutex mutx; + std::condition_variable convar; ConfirmationType confirmation_type(ConfirmationType::Unset); // This task wait for the status of the python subprocess (we don't want a @@ -280,12 +280,12 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, int status; wait(&status); { - std::unique_lock lk(m); + std::unique_lock lock(mutx); if (confirmation_type == ConfirmationType::Unset) { confirmation_type = ConfirmationType::Howdy; } } - cv.notify_one(); + convar.notify_one(); return status; }); @@ -297,12 +297,12 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, int pam_res = pam_get_authtok( pamh, PAM_AUTHTOK, const_cast(&auth_tok_ptr), nullptr); { - std::unique_lock lk(m); + std::unique_lock lock(mutx); if (confirmation_type == ConfirmationType::Unset) { confirmation_type = ConfirmationType::Pam; } } - cv.notify_one(); + convar.notify_one(); return std::tuple(pam_res, auth_tok_ptr); }); @@ -315,8 +315,8 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // 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; }); + std::unique_lock lock(mutx); + convar.wait(lock, [&] { return confirmation_type != ConfirmationType::Unset; }); } // The password has been entered or an error has occurred diff --git a/howdy/src/pam/optional_task.hh b/howdy/src/pam/optional_task.hh index 36f9929..b439628 100644 --- a/howdy/src/pam/optional_task.hh +++ b/howdy/src/pam/optional_task.hh @@ -11,11 +11,11 @@ template class optional_task { std::thread thread; std::packaged_task task; std::future future; - bool spawned; - bool is_active; + bool spawned{false}; + bool is_active{false}; public: - explicit optional_task(std::function fn); + explicit optional_task(std::function func); void activate(); template auto wait(std::chrono::duration dur) -> std::future_status; @@ -25,8 +25,8 @@ public: }; template -optional_task::optional_task(std::function fn) - : task(std::packaged_task(std::move(fn))), future(task.get_future()), +optional_task::optional_task(std::function func) + : task(std::packaged_task(std::move(func))), future(task.get_future()), spawned(false), is_active(false) {} // Create a new thread and launch the task on it. From 2a23fa32fb67182d344f92dbe43d198410716e9b Mon Sep 17 00:00:00 2001 From: boltgolt Date: Sat, 18 Feb 2023 21:25:41 +0100 Subject: [PATCH 76/80] Fixes and comments for models in test command --- howdy/src/cli/test.py | 63 +++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/howdy/src/cli/test.py b/howdy/src/cli/test.py index 540ed04..a66ef04 100644 --- a/howdy/src/cli/test.py +++ b/howdy/src/cli/test.py @@ -10,16 +10,16 @@ import time import dlib import cv2 import numpy as np -from recorders.video_capture import VideoCapture from i18n import _ +from recorders.video_capture import VideoCapture -# Get the absolute path to the current file -path = os.path.dirname(os.path.abspath(__file__)) +# The absolute path to the config directory +path = "/etc/howdy" # Read config from disk config = configparser.ConfigParser() -config.read(path + "/../config.ini") +config.read(path + "/config.ini") if config.get("video", "recording_plugin") != "opencv": print(_("Howdy has been configured to use a recorder which doesn't support the test command yet, aborting")) @@ -27,7 +27,8 @@ if config.get("video", "recording_plugin") != "opencv": video_capture = VideoCapture(config) -# Read exposure and dark_thresholds from config to use in the main loop +# Read config values to use in the main loop +video_certainty = config.getfloat("video", "certainty", fallback=3.5) / 10 exposure = config.getint("video", "exposure", fallback=-1) dark_threshold = config.getfloat("video", "dark_threshold") @@ -58,26 +59,25 @@ use_cnn = config.getboolean('core', 'use_cnn', fallback=False) if use_cnn: face_detector = dlib.cnn_face_detection_model_v1( - path + "/../dlib-data/mmod_human_face_detector.dat" + path + "/dlib-data/mmod_human_face_detector.dat" ) else: face_detector = dlib.get_frontal_face_detector() -pose_predictor = dlib.shape_predictor(path + "/../dlib-data/shape_predictor_5_face_landmarks.dat") -face_encoder = dlib.face_recognition_model_v1(path + "/../dlib-data/dlib_face_recognition_resnet_model_v1.dat") +pose_predictor = dlib.shape_predictor(path + "/dlib-data/shape_predictor_5_face_landmarks.dat") +face_encoder = dlib.face_recognition_model_v1(path + "/dlib-data/dlib_face_recognition_resnet_model_v1.dat") encodings = [] models = None try: user = builtins.howdy_user - models = json.load(open(path + "/../models/" + user + ".dat")) + models = json.load(open(path + "/models/" + user + ".dat")) for model in models: encodings += model["data"] except FileNotFoundError: - print("No face model known for the user " + user + ", please run:") - print("\n\tsudo howdy -U " + user + " add\n") + pass clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) @@ -100,7 +100,7 @@ rec_tm = 0 # Wrap everything in an keyboard interupt handler try: - while cv2.getWindowProperty("Howdy Test", cv2.WND_PROP_VISIBLE) > 0: + while True: frame_tm = time.time() # Increment the frames @@ -119,7 +119,6 @@ try: # Grab a single frame of video orig_frame, frame = video_capture.read_frame() - frame = clahe.apply(frame) # Make a frame to put overlays in overlay = frame.copy() @@ -162,10 +161,11 @@ try: # Show that this is an ignored frame in the top right cv2.putText(overlay, _("DARK FRAME"), (width - 68, 16), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 0, 255), 0, cv2.LINE_AA) else: - # SHow that this is an active frame + # Show that this is an active frame cv2.putText(overlay, _("SCAN FRAME"), (width - 68, 16), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 255, 0), 0, cv2.LINE_AA) rec_tm = time.time() + # Get the locations of all faces and their locations # Upsample it once face_locations = face_detector(frame, 1) @@ -176,8 +176,21 @@ try: if use_cnn: loc = loc.rect + # By default the circle around the face is red for no match color = (0, 0, 230) + + # Get the center X and Y from the rectangular points + x = int((loc.right() - loc.left()) / 2) + loc.left() + y = int((loc.bottom() - loc.top()) / 2) + loc.top() + + # Get the raduis from the with of the square + r = (loc.right() - loc.left()) / 2 + # Add 20% padding + r = int(r + (r * 0.2)) + + # If we have models defined for the current user if models: + # Get the encoding of the face in the frame face_landmark = pose_predictor(orig_frame, loc) face_encoding = np.array(face_encoder.compute_face_descriptor(orig_frame, face_landmark, 1)) @@ -188,19 +201,17 @@ try: match_index = np.argmin(matches) match = matches[match_index] - percent = match * 100 - label = models[match_index]["label"] - color = (230, 0, 0) - cv2.putText(overlay, "{} {}(%)".format(label, percent), (width - 68, 32), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 255, 0), 0, cv2.LINE_AA) + # If a model matches + if 0 < match < video_certainty: + # Turn the circle green + color = (0, 230, 0) - # Get the center X and Y from the rectangular points - x = int((loc.right() - loc.left()) / 2) + loc.left() - y = int((loc.bottom() - loc.top()) / 2) + loc.top() - - # Get the raduis from the with of the square - r = (loc.right() - loc.left()) / 2 - # Add 20% padding - r = int(r + (r * 0.2)) + # Print the name of the model next to the circle + circle_text = "{} (certainty: {})".format(models[match_index]["label"], round(match * 10, 3)) + cv2.putText(overlay, circle_text, (int(x + r / 3), y - r), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 255, 0), 0, cv2.LINE_AA) + # If no approved matches, show red text + else: + cv2.putText(overlay, "no match", (int(x + r / 3), y - r), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 0, 255), 0, cv2.LINE_AA) # Draw the Circle in green cv2.circle(overlay, (x, y), r, color, 2) From cd890b56c184649ae454d7519781c8d2c099bc1e Mon Sep 17 00:00:00 2001 From: boltgolt Date: Sat, 18 Feb 2023 21:41:20 +0100 Subject: [PATCH 77/80] Implement #570, special error code for missing --- howdy/src/pam/main.cc | 3 +++ howdy/src/pam/main.hh | 3 ++- howdy/src/pam/optional_task.hh | 2 +- howdy/src/recorders/video_capture.py | 4 ++-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/howdy/src/pam/main.cc b/howdy/src/pam/main.cc index 68501b4..3662b62 100644 --- a/howdy/src/pam/main.cc +++ b/howdy/src/pam/main.cc @@ -79,6 +79,9 @@ auto howdy_error(int status, conv_function(PAM_ERROR_MSG, S("Face detection image too dark")); syslog(LOG_ERR, "Failure, image too dark"); break; + case CompareError::INVALID_DEVICE: + syslog(LOG_ERR, "Failure, not possible to open camera at configured path"); + break; default: conv_function(PAM_ERROR_MSG, std::string(S("Unknown error: ") + status).c_str()); diff --git a/howdy/src/pam/main.hh b/howdy/src/pam/main.hh index 5b5dcf8..12f687d 100644 --- a/howdy/src/pam/main.hh +++ b/howdy/src/pam/main.hh @@ -13,7 +13,8 @@ enum CompareError : int { TIMEOUT_REACHED = 11, ABORT = 12, TOO_DARK = 13, - TIMEOUT_ACTIVE = 2816, + INVALID_DEVICE = 14, + TIMEOUT_ACTIVE = 2816 }; inline auto get_workaround(const std::string &workaround) -> Workaround { diff --git a/howdy/src/pam/optional_task.hh b/howdy/src/pam/optional_task.hh index b439628..39b60a4 100644 --- a/howdy/src/pam/optional_task.hh +++ b/howdy/src/pam/optional_task.hh @@ -27,7 +27,7 @@ public: template optional_task::optional_task(std::function func) : task(std::packaged_task(std::move(func))), future(task.get_future()), - spawned(false), is_active(false) {} + spawned(), is_active() {} // Create a new thread and launch the task on it. template void optional_task::activate() { diff --git a/howdy/src/recorders/video_capture.py b/howdy/src/recorders/video_capture.py index 50032ad..2c55c57 100644 --- a/howdy/src/recorders/video_capture.py +++ b/howdy/src/recorders/video_capture.py @@ -37,7 +37,7 @@ class VideoCapture: print(_("Howdy could not find a camera device at the path specified in the config file.")) print(_("It is very likely that the path is not configured correctly, please edit the 'device_path' config value by running:")) print("\n\tsudo howdy config\n") - sys.exit(1) + sys.exit(14) # Create reader # The internal video recorder @@ -83,7 +83,7 @@ class VideoCapture: ret, frame = self.internal.read() if not ret: print(_("Failed to read camera specified in the 'device_path' config option, aborting")) - sys.exit(1) + sys.exit(14) try: # Convert from color to grayscale From b89d11dd48bb6ca76a985798a5f89714fc85b69d Mon Sep 17 00:00:00 2001 From: boltgolt Date: Sat, 18 Feb 2023 21:48:40 +0100 Subject: [PATCH 78/80] Remove pam.py from PR --- howdy/src/pam.py | 134 ----------------------------------------------- 1 file changed, 134 deletions(-) delete mode 100644 howdy/src/pam.py diff --git a/howdy/src/pam.py b/howdy/src/pam.py deleted file mode 100644 index 14aa9e1..0000000 --- a/howdy/src/pam.py +++ /dev/null @@ -1,134 +0,0 @@ -# PAM interface in python, launches compare.py - -# Import required modules -import subprocess -import os -import glob -import sys -import syslog - -if sys.version_info.major < 3: - import ConfigParser - config = ConfigParser.ConfigParser() -else: - import configparser - config = configparser.ConfigParser() - -# Read config from disk -config.read(os.path.dirname(os.path.abspath(__file__)) + "/config.ini") - - -def doAuth(pamh): - """Starts authentication in a separate process""" - - # Abort if Howdy is disabled - if config.getboolean("core", "disabled"): - pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Howdy is disabled.")) - return pamh.PAM_AUTHINFO_UNAVAIL - # Abort if we're in a remote SSH env - if config.getboolean("core", "abort_if_ssh"): - if "SSH_CONNECTION" in os.environ or "SSH_CLIENT" in os.environ or "SSHD_OPTS" in os.environ: - pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Howdy is disabled in ssh.")) - return pamh.PAM_AUTHINFO_UNAVAIL - # Abort if lid is closed - if config.getboolean("core", "abort_if_lid_closed"): - if any("closed" in open(f).read() for f in glob.glob("/proc/acpi/button/lid/*/state")): - pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "LID is closed.")) - return pamh.PAM_AUTHINFO_UNAVAIL - # Abort if the video device does not exist - if not os.path.exists(config.get("video", "device_path")): - if config.getboolean("video", "warn_no_device"): - pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Camera path is not configured correctly, please edit the 'device_path' config value.")) - return pamh.PAM_AUTHINFO_UNAVAIL - - # Set up syslog - syslog.openlog("[HOWDY]", 0, syslog.LOG_AUTH) - - # Alert the user that we are doing face detection - if config.getboolean("core", "detection_notice"): - pamh.conversation(pamh.Message(pamh.PAM_TEXT_INFO, "Attempting face detection")) - - syslog.syslog(syslog.LOG_INFO, "Attempting facial authentication for user " + pamh.get_user()) - - # Run compare as python3 subprocess to circumvent python version and import issues - status = subprocess.call(["/usr/bin/python3", os.path.dirname(os.path.abspath(__file__)) + "/compare.py", pamh.get_user()]) - - # Status 10 means we couldn't find any face models - if status == 10: - if not config.getboolean("core", "suppress_unknown"): - pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "No face model known")) - - syslog.syslog(syslog.LOG_NOTICE, "Failure, no face model known") - syslog.closelog() - return pamh.PAM_USER_UNKNOWN - - # Status 11 means we exceded the maximum retry count - elif status == 11: - if config.getboolean("core", "timeout_notice"): - pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Face detection timeout reached")) - syslog.syslog(syslog.LOG_INFO, "Failure, timeout reached") - syslog.closelog() - return pamh.PAM_AUTH_ERR - - # Status 12 means we aborted - elif status == 12: - syslog.syslog(syslog.LOG_INFO, "Failure, general abort") - syslog.closelog() - return pamh.PAM_AUTH_ERR - - # Status 13 means the image was too dark - elif status == 13: - pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Face detection image too dark")) - syslog.syslog(syslog.LOG_INFO, "Failure, image too dark") - syslog.closelog() - return pamh.PAM_AUTH_ERR - - # Status 14 means a rubberstamp could not be given - elif status == 14: - pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Rubberstamp denied")) - syslog.syslog(syslog.LOG_INFO, "Failure, rubberstamp did not succeed") - syslog.closelog() - return pamh.PAM_AUTH_ERR - - # Status 1 is probably a python crash - elif status == 1: - pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Howdy encountered error, check stderr")) - syslog.syslog(syslog.LOG_INFO, "Failure, process crashed while authenticating") - syslog.closelog() - return pamh.PAM_SYSTEM_ERR - - # Status 0 is a successful exit - elif status == 0: - # Show the success message if it isn't suppressed - if not config.getboolean("core", "no_confirmation"): - pamh.conversation(pamh.Message(pamh.PAM_TEXT_INFO, "Identified face as " + pamh.get_user())) - - syslog.syslog(syslog.LOG_INFO, "Login approved") - syslog.closelog() - return pamh.PAM_SUCCESS - - # Otherwise, we can't describe what happened but it wasn't successful - pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Unknown error: " + str(status))) - syslog.syslog(syslog.LOG_INFO, "Failure, unknown error" + str(status)) - syslog.closelog() - return pamh.PAM_SYSTEM_ERR - - -def pam_sm_authenticate(pamh, flags, args): - """Called by PAM when the user wants to authenticate, in sudo for example""" - return doAuth(pamh) - - -def pam_sm_open_session(pamh, flags, args): - """Called when starting a session, such as su""" - return doAuth(pamh) - - -def pam_sm_close_session(pamh, flags, argv): - """We don't need to clean anyting up at the end of a session, so returns true""" - return pamh.PAM_SUCCESS - - -def pam_sm_setcred(pamh, flags, argv): - """We don't need set any credentials, so returns true""" - return pamh.PAM_SUCCESS From ea933e3255e5320a0b2860ddb8f169b0de6b5617 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Sun, 19 Feb 2023 09:57:45 +0100 Subject: [PATCH 79/80] Fix header --- howdy/src/pam/optional_task.hh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/howdy/src/pam/optional_task.hh b/howdy/src/pam/optional_task.hh index 39b60a4..c6da379 100644 --- a/howdy/src/pam/optional_task.hh +++ b/howdy/src/pam/optional_task.hh @@ -26,8 +26,7 @@ public: template optional_task::optional_task(std::function func) - : task(std::packaged_task(std::move(func))), future(task.get_future()), - spawned(), is_active() {} + : task(std::packaged_task(std::move(func))), future(task.get_future()) {} // Create a new thread and launch the task on it. template void optional_task::activate() { From ceca88a4276dba76dcfb41d9bcd42df058cde4a0 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Sun, 19 Feb 2023 10:10:35 +0100 Subject: [PATCH 80/80] Implement arch changes from #546 --- howdy/archlinux/howdy/.SRCINFO | 28 ---------------------------- howdy/archlinux/howdy/.gitignore | 3 ++- howdy/archlinux/howdy/PKGBUILD | 1 + 3 files changed, 3 insertions(+), 29 deletions(-) delete mode 100644 howdy/archlinux/howdy/.SRCINFO diff --git a/howdy/archlinux/howdy/.SRCINFO b/howdy/archlinux/howdy/.SRCINFO deleted file mode 100644 index 0fbef43..0000000 --- a/howdy/archlinux/howdy/.SRCINFO +++ /dev/null @@ -1,28 +0,0 @@ -pkgbase = howdy - pkgdesc = Windows Hello for Linux - pkgver = 2.6.1 - pkgrel = 1 - url = https://github.com/boltgolt/howdy - arch = x86_64 - license = MIT - makedepends = cmake - makedepends = pkgfile - depends = opencv - depends = hdf5 - depends = pam-python - 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 - source = https://github.com/davisking/dlib-models/raw/master/mmod_human_face_detector.dat.bz2 - source = https://github.com/davisking/dlib-models/raw/master/shape_predictor_5_face_landmarks.dat.bz2 - sha256sums = f3f48599f78fd82b049539fcfc34de25c9435cad732697bdda94e85352964794 - sha256sums = abb1f61041e434465855ce81c2bd546e830d28bcbed8d27ffbe5bb408b11553a - sha256sums = db9e9e40f092c118d5eb3e643935b216838170793559515541c56a2b50d9fc84 - sha256sums = 6e787bbebf5c9efdb793f6cd1f023230c4413306605f24f299f12869f95aa472 - -pkgname = howdy - diff --git a/howdy/archlinux/howdy/.gitignore b/howdy/archlinux/howdy/.gitignore index 6748987..cd14529 100644 --- a/howdy/archlinux/howdy/.gitignore +++ b/howdy/archlinux/howdy/.gitignore @@ -4,4 +4,5 @@ src *.zip *.tar.xz *.patch -*.dat.bz2 \ No newline at end of file +*.dat.bz2 +.SRCINFO diff --git a/howdy/archlinux/howdy/PKGBUILD b/howdy/archlinux/howdy/PKGBUILD index 1740ccb..b02dd63 100644 --- a/howdy/archlinux/howdy/PKGBUILD +++ b/howdy/archlinux/howdy/PKGBUILD @@ -17,6 +17,7 @@ depends=( 'python3' 'python-dlib' 'python-numpy' + 'python-opencv' ) makedepends=( 'cmake'