0
0
Fork 0
mirror of https://github.com/boltgolt/howdy.git synced 2024-09-19 09:51:19 +02:00

chore: move to howdy folder

This commit is contained in:
musikid 2022-03-14 19:24:23 +01:00
parent b903b426a6
commit f7aefcd0ca
No known key found for this signature in database
GPG key ID: 7567D43648C6E2F4
19 changed files with 381 additions and 913 deletions

View file

@ -1,20 +1,35 @@
# Howdy PAM module # 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 ## Build
```sh ``` sh
meson setup build -Dinih:with_INIReader=true meson setup build
meson compile -C build meson compile -C build
``` ```
## Install ## Install
```sh ``` sh
sudo mv build/libpam_howdy.so /lib/security/pam_howdy.so meson install -C build
``` ```
Change PAM config line to: Add the following line to your PAM configuration (/etc/pam.d/your-service):
```pam ``` pam
auth sufficient pam_howdy.so auth sufficient pam_howdy.so
``` ```

View file

@ -1,12 +1,13 @@
#include <cerrno> #include <cerrno>
#include <csignal> #include <csignal>
#include <cstdlib> #include <cstdlib>
#include <glob.h>
#include <ostream> #include <ostream>
#include <poll.h>
#include <glob.h>
#include <libintl.h>
#include <pthread.h> #include <pthread.h>
#include <spawn.h> #include <spawn.h>
#include <sys/poll.h> #include <stdexcept>
#include <sys/signalfd.h> #include <sys/signalfd.h>
#include <sys/syslog.h> #include <sys/syslog.h>
#include <sys/types.h> #include <sys/types.h>
@ -33,59 +34,62 @@
#include <INIReader.h> #include <INIReader.h>
#include <boost/locale.hpp>
#include <security/pam_appl.h> #include <security/pam_appl.h>
#include <security/pam_ext.h> #include <security/pam_ext.h>
#include <security/pam_modules.h> #include <security/pam_modules.h>
#include "enter_device.hh"
#include "main.hh" #include "main.hh"
#include "optional_task.hh" #include "optional_task.hh"
using namespace std; const auto DEFAULT_TIMEOUT =
using namespace boost::locale; std::chrono::duration<int, std::chrono::milliseconds::period>(100);
using namespace std::chrono_literals; 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 * 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 * @param conv_function The PAM conversation function
* @return A PAM return code * @return A PAM return code
*/ */
int on_howdy_auth(int code, function<int(int, const char *)> conv_function) { auto howdy_error(int status,
const std::function<int(int, const char *)> &conv_function)
-> int {
// If the process has exited // If the process has exited
if (!WIFEXITED(code)) { if (WIFEXITED(status)) {
// Get the status code returned // 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 CompareError::NO_FACE_MODEL:
case 10: conv_function(PAM_ERROR_MSG, S("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"); syslog(LOG_NOTICE, "Failure, no face model known");
break; break;
// Status 11 means we exceded the maximum retry count case CompareError::TIMEOUT_REACHED:
case 11: syslog(LOG_ERR, "Failure, timeout reached");
syslog(LOG_INFO, "Failure, timeout reached");
break; break;
// Status 12 means we aborted case CompareError::ABORT:
case 12: syslog(LOG_ERR, "Failure, general abort");
syslog(LOG_INFO, "Failure, general abort");
break; break;
// Status 13 means the image was too dark case CompareError::TOO_DARK:
case 13: conv_function(PAM_ERROR_MSG, S("Face detection image too dark"));
conv_function(PAM_ERROR_MSG, syslog(LOG_ERR, "Failure, image too dark");
dgettext("pam", "Face detection image too dark"));
syslog(LOG_INFO, "Failure, image too dark");
break; break;
// Otherwise, we can't describe what happened but it wasn't successful
default: default:
conv_function( conv_function(PAM_ERROR_MSG,
PAM_ERROR_MSG, std::string(S("Unknown error: ") + status).c_str());
string(dgettext("pam", "Unknown error:") + to_string(code)).c_str()); syslog(LOG_ERR, "Failure, unknown error %d", status);
syslog(LOG_INFO, "Failure, unknown error %d", code);
} }
} 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 // 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<int(int, const char *)> conv_function) {
} }
/** /**
* Format and send a message to PAM * Format the success message if the status is successful or log the error in
* @param conv PAM conversation function * the other case
* @param type Type of PAM message * @param username Username
* @param message String to show the user * @param status Status code
* @param config INI configuration
* @param conv_function PAM conversation function
* @return Returns the conversation function return code * @return Returns the conversation function return code
*/ */
int send_message(function<int(int, const struct pam_message **, auto howdy_status(char *username, int status, const INIReader &config,
struct pam_response **, void *)> const std::function<int(int, const char *)> &conv_function)
conv, -> int {
int type, const char *message) { if (status != EXIT_SUCCESS) {
// No need to free this, it's allocated on the stack return howdy_error(status, conv_function);
const struct pam_message msg = {.msg_style = type, .msg = message}; }
const struct pam_message *msgp = &msg;
struct pam_response res_ = {}; if (!config.GetBoolean("core", "no_confirmation", true)) {
struct pam_response *resp_ = &res_; // 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 syslog(LOG_INFO, "Login approved");
return conv(1, &msgp, &resp_, nullptr);
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<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);
}
return PAM_SUCCESS;
} }
/** /**
@ -124,271 +191,238 @@ int send_message(function<int(int, const struct pam_message **,
* @param auth_tok True if we should ask for a password too * @param auth_tok True if we should ask for a password too
* @return Returns a PAM return code * @return Returns a PAM return code
*/ */
int identify(pam_handle_t *pamh, int flags, int argc, const char **argv, auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv,
bool auth_tok) { bool auth_tok) -> int {
INIReader reader("/lib/security/howdy/config.ini"); INIReader config("/lib/security/howdy/config.ini");
// Open the system log so we can write to it
openlog("pam_howdy", 0, LOG_AUTHPRIV); 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 // Will contain the responses from PAM functions
int pam_res = PAM_IGNORE; int pam_res = PAM_IGNORE;
// Try to get the conversation function and error out if we can't // Check if we shoud continue
if ((pam_res = pam_get_item(pamh, PAM_CONV, (const void **)&conv)) != if ((pam_res = check_enabled(config)) != PAM_SUCCESS) {
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<const void **>(reinterpret_cast<void **>(&conv));
if ((pam_res = pam_get_item(pamh, PAM_CONV, conv_ptr)) != PAM_SUCCESS) {
syslog(LOG_ERR, "Failed to acquire conversation"); syslog(LOG_ERR, "Failed to acquire conversation");
return pam_res; return pam_res;
} }
// Wrap the PAM conversation function in our own, easier function // Wrap the PAM conversation function in our own, easier function
auto conv_function = auto conv_function = [conv](int msg_type, const char *msg_str) {
bind(send_message, conv->conv, placeholders::_1, placeholders::_2); 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 struct pam_response res = {};
if (reader.ParseError() < 0) { struct pam_response *resp = &res;
syslog(LOG_ERR, "Failed to parse the configuration file");
return PAM_SYSTEM_ERR;
}
// Stop executing if Howdy has been disabled in the config return conv->conv(1, &msgp, &resp, conv->appdata_ptr);
if (reader.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 // Initialize gettext
if (reader.GetBoolean("core", "ignore_ssh", true)) { setlocale(LC_ALL, "");
if (getenv("SSH_CONNECTION") != nullptr || bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR);
getenv("SSH_CLIENT") != nullptr || getenv("SSHD_OPTS") != nullptr) { textdomain(GETTEXT_PACKAGE);
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);
}
// If enabled, send a notice to the user that facial login is being attempted // 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 ((pam_res = conv_function( if ((conv_function(PAM_TEXT_INFO, S("Attempting facial authentication"))) !=
PAM_TEXT_INFO,
dgettext("pam", "Attempting facial authentication"))) !=
PAM_SUCCESS) { PAM_SUCCESS) {
syslog(LOG_ERR, "Failed to send detection notice"); syslog(LOG_ERR, "Failed to send detection notice");
} }
} }
// Get the username from PAM, needed to match correct face model // Get the username from PAM, needed to match correct face model
char *user_ptr = nullptr; char *username = nullptr;
if ((pam_res = pam_get_user(pamh, (const char **)&user_ptr, nullptr)) != if ((pam_res = pam_get_user(pamh, const_cast<const char **>(&username),
PAM_SUCCESS) { nullptr)) != PAM_SUCCESS) {
syslog(LOG_ERR, "Failed to get username"); syslog(LOG_ERR, "Failed to get username");
return pam_res; return pam_res;
} }
posix_spawn_file_actions_t file_actions; const char *const args[] = {PYTHON_EXECUTABLE, // NOLINT
posix_spawn_file_actions_init(&file_actions); COMPARE_PROCESS_PATH, username, nullptr};
// 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};
pid_t child_pid; pid_t child_pid;
// Start the python subprocess // Start the python subprocess
if (posix_spawnp(&child_pid, "/usr/bin/python3", &file_actions, nullptr, if (posix_spawnp(&child_pid, PYTHON_EXECUTABLE, nullptr, nullptr,
(char *const *)args, nullptr) < 0) { const_cast<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; return PAM_SYSTEM_ERR;
} }
mutex m; // NOTE: We should replace mutex and condition_variable by atomic wait, but
condition_variable cv; // it's too recent (C++20)
Type confirmation_type; std::mutex m;
// TODO: Find a clean way to do this std::condition_variable cv;
Type final_type; ConfirmationType confirmation_type(ConfirmationType::Unset);
// This task wait for the status of the python subprocess (we don't want a // This task wait for the status of the python subprocess (we don't want a
// zombie process) // zombie process)
optional_task<int> child_task(packaged_task<int()>([&] { optional_task<int> child_task([&] {
int status; int status;
wait(&status); wait(&status);
{ {
unique_lock<mutex> lk(m); std::unique_lock<std::mutex> lk(m);
confirmation_type = Type::Howdy; if (confirmation_type == ConfirmationType::Unset) {
confirmation_type = ConfirmationType::Howdy;
} }
cv.notify_all(); }
cv.notify_one();
return status; return status;
})); });
child_task.activate(); child_task.activate();
// This task waits for the password input (if the workaround wants it) // This task waits for the password input (if the workaround wants it)
optional_task<tuple<int, char *>> pass_task( optional_task<std::tuple<int, char *>> pass_task([&] {
packaged_task<tuple<int, char *>()>([&] {
char *auth_tok_ptr = nullptr; char *auth_tok_ptr = nullptr;
int pam_res = pam_get_authtok(pamh, PAM_AUTHTOK, int pam_res = pam_get_authtok(
(const char **)&auth_tok_ptr, nullptr); pamh, PAM_AUTHTOK, const_cast<const char **>(&auth_tok_ptr), nullptr);
{ {
unique_lock<mutex> lk(m); std::unique_lock<std::mutex> lk(m);
confirmation_type = Type::Pam; if (confirmation_type == ConfirmationType::Unset) {
confirmation_type = ConfirmationType::Pam;
} }
cv.notify_all(); }
return tuple<int, char *>(pam_res, auth_tok_ptr); cv.notify_one();
}));
if (auth_tok) { return std::tuple<int, char *>(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(); pass_task.activate();
} }
// Wait for the end either of the child or the password input // Wait for the end either of the child or the password input
{ {
unique_lock<mutex> lk(m); std::unique_lock<std::mutex> lk(m);
cv.wait(lk); cv.wait(lk, [&] { return confirmation_type != ConfirmationType::Unset; });
final_type = confirmation_type;
} }
if (final_type == Type::Howdy) { // The password has been entered or an error has occurred
// We need to be sure that we're not going to block forever if the if (confirmation_type == ConfirmationType::Pam) {
// child has a problem // We kill the child because we don't need its result
if (child_task.wait(3s) == future_status::timeout) {
kill(child_pid, SIGTERM); 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();
// 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);
}
}
// 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); child_task.stop(false);
// We just wait for the thread to stop since it's this one which sent us the // We just wait for the thread to stop since it's this one which sent us the
// confirmation type // confirmation type
if (workaround == Workaround::Input && auth_tok) {
pass_task.stop(false); pass_task.stop(false);
}
char *token = nullptr; char *password = nullptr;
tie(pam_res, token) = pass_task.get(); std::tie(pam_res, password) = pass_task.get();
if (pam_res != PAM_SUCCESS) if (pam_res != PAM_SUCCESS) {
return pam_res; 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 // The password has been entered, we are passing it to PAM stack
return PAM_IGNORE; 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 // Called by PAM when a user needs to be authenticated, for example by running
// the sudo command // the sudo command
PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, PAM_EXTERN auto pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc,
const char **argv) { const char **argv) -> int {
return identify(pamh, flags, argc, argv, true); return identify(pamh, flags, argc, argv, true);
} }
// Called by PAM when a session is started, such as by the su command // 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, PAM_EXTERN auto pam_sm_open_session(pam_handle_t *pamh, int flags, int argc,
const char **argv) { const char **argv) -> int {
return identify(pamh, flags, argc, argv, false); return identify(pamh, flags, argc, argv, false);
} }
// The functions below are required by PAM, but not needed in this module // 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, PAM_EXTERN auto pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc,
const char **argv) { const char **argv) -> int {
return PAM_IGNORE; return PAM_IGNORE;
} }
PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, PAM_EXTERN auto pam_sm_close_session(pam_handle_t *pamh, int flags, int argc,
const char **argv) { const char **argv) -> int {
return PAM_IGNORE; return PAM_IGNORE;
} }
PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, PAM_EXTERN auto pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc,
const char **argv) { const char **argv) -> int {
return PAM_IGNORE; return PAM_IGNORE;
} }
PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, PAM_EXTERN auto pam_sm_setcred(pam_handle_t *pamh, int flags, int argc,
const char **argv) { const char **argv) -> int {
return PAM_IGNORE; return PAM_IGNORE;
} }

View file

@ -1,36 +1,30 @@
#ifndef MAIN_H_ #ifndef MAIN_H_
#define MAIN_H_ #define MAIN_H_
#include <cstdint>
#include <string> #include <string>
enum class Type { Howdy, Pam }; enum class ConfirmationType { Unset, Howdy, Pam };
enum class Workaround { Off, Input, Native }; enum class Workaround { Off, Input, Native };
inline bool operator==(const std::string &l, const Workaround &r) { // Exit status codes returned by the compare process
switch (r) { enum CompareError : int {
case Workaround::Off: NO_FACE_MODEL = 10,
return (l == "off"); TIMEOUT_REACHED = 11,
case Workaround::Input: ABORT = 12,
return (l == "input"); TOO_DARK = 13
case Workaround::Native: };
return (l == "native");
default: inline auto get_workaround(const std::string &workaround) -> Workaround {
return false; if (workaround == "input") {
return Workaround::Input;
} }
}
inline bool operator==(const Workaround &l, const std::string &r) { if (workaround == "native") {
return operator==(r, l); return Workaround::Native;
} }
inline bool operator!=(const std::string &l, const Workaround &r) { return Workaround::Off;
return !operator==(l, r);
}
inline bool operator!=(const Workaround &l, const std::string &r) {
return operator!=(r, l);
} }
#endif // MAIN_H_ #endif // MAIN_H_

View file

@ -1,9 +1,24 @@
project('pam_howdy', 'cpp', version: '0.1.0', default_options: ['cpp_std=c++14']) project('pam_howdy', 'cpp', version: '0.1.0', default_options: ['cpp_std=c++14'])
inih = subproject('inih') inih_cpp = dependency('INIReader', fallback: ['inih', 'INIReader_dep'])
inih_cpp = inih.get_variable('INIReader_dep') libevdev = dependency('libevdev')
libpam = meson.get_compiler('cpp').find_library('pam')
libpam = meson.get_compiler('c').find_library('pam')
boost = dependency('boost', modules: ['locale'])
threads = dependency('threads') 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: ''
)

View file

@ -6,64 +6,76 @@
#include <future> #include <future>
#include <thread> #include <thread>
// A task executed only if activated.
template <typename T> class optional_task { template <typename T> class optional_task {
std::thread _thread; std::thread thread;
std::packaged_task<T()> _task; std::packaged_task<T()> task;
std::future<T> _future; std::future<T> future;
std::atomic<bool> _spawned; bool spawned;
std::atomic<bool> _is_active; bool is_active;
public: public:
optional_task(std::packaged_task<T()>); explicit optional_task(std::function<T()> fn);
void activate(); void activate();
template <typename Dur> std::future_status wait(std::chrono::duration<Dur>); template <typename R, typename P>
T get(); auto wait(std::chrono::duration<R, P> dur) -> std::future_status;
bool is_active(); auto get() -> T;
void stop(bool); void stop(bool force);
~optional_task(); ~optional_task();
}; };
template <typename T> template <typename T>
optional_task<T>::optional_task(std::packaged_task<T()> t) optional_task<T>::optional_task(std::function<T()> fn)
: _task(std::move(t)), _future(_task.get_future()) {} : task(std::packaged_task<T()>(std::move(fn))), future(task.get_future()),
spawned(false), is_active(false) {}
// Create a new thread and launch the task on it.
template <typename T> void optional_task<T>::activate() { template <typename T> void optional_task<T>::activate() {
_thread = std::thread(std::move(_task)); thread = std::thread(std::move(task));
_spawned = true; spawned = true;
_is_active = true; is_active = true;
} }
// Wait for `dur` time and return a `future` status.
template <typename T> template <typename T>
template <typename Dur> template <typename R, typename P>
std::future_status optional_task<T>::wait(std::chrono::duration<Dur> dur) { auto optional_task<T>::wait(std::chrono::duration<R, P> dur)
return _future.wait_for(dur); -> std::future_status {
return future.wait_for(dur);
} }
template <typename T> T optional_task<T>::get() { // Get the value.
assert(!_is_active && _spawned); // WARNING: The function hould be run only if the task has successfully been
return _future.get(); // stopped.
template <typename T> auto optional_task<T>::get() -> T {
assert(!is_active && spawned);
return future.get();
} }
template <typename T> bool optional_task<T>::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 <typename T> void optional_task<T>::stop(bool force) { template <typename T> void optional_task<T>::stop(bool force) {
if (!(_is_active && _thread.joinable()) && _spawned) { if (!(is_active && thread.joinable()) && spawned) {
_is_active = false; is_active = false;
return; return;
} }
// We use pthread to cancel the thread // We use pthread to cancel the thread
if (force) { if (force) {
auto native_hd = _thread.native_handle(); auto native_hd = thread.native_handle();
pthread_cancel(native_hd); pthread_cancel(native_hd);
} }
_thread.join(); thread.join();
_is_active = false; is_active = false;
} }
template <typename T> optional_task<T>::~optional_task<T>() { template <typename T> optional_task<T>::~optional_task<T>() {
if (_is_active && _spawned) if (is_active && spawned) {
stop(false); stop(false);
}
} }
#endif // OPTIONAL_TASK_H_ #endif // OPTIONAL_TASK_H_

View file

@ -1,3 +1,13 @@
[wrap-git] [wrap-file]
url = https://github.com/benhoyt/inih.git directory = inih-r53
revision = r52 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

1
src/pam/.gitignore vendored
View file

@ -1 +0,0 @@
subprojects/*/

View file

@ -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
```

View file

@ -1,428 +0,0 @@
#include <cerrno>
#include <csignal>
#include <cstdlib>
#include <ostream>
#include <glob.h>
#include <libintl.h>
#include <pthread.h>
#include <spawn.h>
#include <stdexcept>
#include <sys/signalfd.h>
#include <sys/syslog.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <syslog.h>
#include <unistd.h>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstring>
#include <fstream>
#include <functional>
#include <future>
#include <iostream>
#include <iterator>
#include <memory>
#include <mutex>
#include <string>
#include <system_error>
#include <thread>
#include <tuple>
#include <vector>
#include <INIReader.h>
#include <security/pam_appl.h>
#include <security/pam_ext.h>
#include <security/pam_modules.h>
#include "enter_device.hh"
#include "main.hh"
#include "optional_task.hh"
const auto DEFAULT_TIMEOUT =
std::chrono::duration<int, std::chrono::milliseconds::period>(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<int(int, const char *)> &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<int(int, const char *)> &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<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);
}
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<const void **>(reinterpret_cast<void **>(&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<const char **>(&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<char *const *>(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<int> child_task([&] {
int status;
wait(&status);
{
std::unique_lock<std::mutex> 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<std::tuple<int, char *>> pass_task([&] {
char *auth_tok_ptr = nullptr;
int pam_res = pam_get_authtok(
pamh, PAM_AUTHTOK, const_cast<const char **>(&auth_tok_ptr), nullptr);
{
std::unique_lock<std::mutex> lk(m);
if (confirmation_type == ConfirmationType::Unset) {
confirmation_type = ConfirmationType::Pam;
}
}
cv.notify_one();
return std::tuple<int, char *>(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<std::mutex> 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;
}

View file

@ -1,30 +0,0 @@
#ifndef MAIN_H_
#define MAIN_H_
#include <string>
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_

View file

@ -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: ''
)

View file

@ -1,81 +0,0 @@
#ifndef OPTIONAL_TASK_H_
#define OPTIONAL_TASK_H_
#include <cassert>
#include <chrono>
#include <future>
#include <thread>
// A task executed only if activated.
template <typename T> class optional_task {
std::thread thread;
std::packaged_task<T()> task;
std::future<T> future;
bool spawned;
bool is_active;
public:
explicit optional_task(std::function<T()> fn);
void activate();
template <typename R, typename P>
auto wait(std::chrono::duration<R, P> dur) -> std::future_status;
auto get() -> T;
void stop(bool force);
~optional_task();
};
template <typename T>
optional_task<T>::optional_task(std::function<T()> fn)
: task(std::packaged_task<T()>(std::move(fn))), future(task.get_future()),
spawned(false), is_active(false) {}
// Create a new thread and launch the task on it.
template <typename T> void optional_task<T>::activate() {
thread = std::thread(std::move(task));
spawned = true;
is_active = true;
}
// Wait for `dur` time and return a `future` status.
template <typename T>
template <typename R, typename P>
auto optional_task<T>::wait(std::chrono::duration<R, P> 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 <typename T> auto optional_task<T>::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 <typename T> void optional_task<T>::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 <typename T> optional_task<T>::~optional_task<T>() {
if (is_active && spawned) {
stop(false);
}
}
#endif // OPTIONAL_TASK_H_

View file

@ -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