mirror of
https://github.com/boltgolt/howdy.git
synced 2024-09-19 09:51:19 +02:00
commit
1c5dab4e1c
16 changed files with 500 additions and 302 deletions
25
.github/workflows/check.yml
vendored
Normal file
25
.github/workflows/check.yml
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
name: check
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install required libraries
|
||||
run: >
|
||||
sudo apt-get update && sudo apt-get install -y
|
||||
python3 python3-pip python3-setuptools python3-wheel ninja-build meson
|
||||
cmake make build-essential clang-tidy
|
||||
libpam0g-dev libinih-dev libevdev-dev
|
||||
python3-dev libopencv-dev
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
meson setup build howdy/src/pam
|
||||
ninja -C build
|
||||
|
||||
- name: Check source code
|
||||
run: |
|
||||
ninja clang-tidy -C build
|
|
@ -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
|
||||
|
|
|
@ -26,7 +26,6 @@ import _thread as thread
|
|||
from i18n import _
|
||||
from recorders.video_capture import VideoCapture
|
||||
|
||||
|
||||
def exit(code=None):
|
||||
"""Exit while closeing howdy-gtk properly"""
|
||||
global gtk_proc
|
||||
|
|
|
@ -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
|
||||
|
|
7
howdy/src/pam/.clang-tidy
Normal file
7
howdy/src/pam/.clang-tidy
Normal file
|
@ -0,0 +1,7 @@
|
|||
Checks: 'clang-diagnostic-*,clang-analyser-*,-clang-diagnostic-unused-command-line-argument,google-*,bugprone-*,modernize-*,performance-*,portability-*,readability-*,-bugprone-easily-swappable-*,-readability-magic-numbers,-google-readability-todo'
|
||||
WarningsAsErrors: 'clang-diagnostic-*,clang-analyser-*,-clang-diagnostic-unused-command-line-argument,google-*,bugprone-*,modernize-*,performance-*,portability-*,readability-*,-bugprone-easily-swappable-*,-readability-magic-numbers,-google-readability-todo'
|
||||
CheckOptions:
|
||||
- key: readability-function-cognitive-complexity.Threshold
|
||||
value: '50'
|
||||
|
||||
# vim:syntax=yaml
|
|
@ -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
|
||||
``` pam
|
||||
auth sufficient pam_howdy.so
|
||||
```
|
||||
|
|
48
howdy/src/pam/enter_device.cc
Normal file
48
howdy/src/pam/enter_device.cc
Normal file
|
@ -0,0 +1,48 @@
|
|||
#include "enter_device.hh"
|
||||
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
|
||||
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));
|
||||
};
|
||||
}
|
19
howdy/src/pam/enter_device.hh
Normal file
19
howdy/src/pam/enter_device.hh
Normal file
|
@ -0,0 +1,19 @@
|
|||
#ifndef ENTER_DEVICE_H_
|
||||
#define ENTER_DEVICE_H_
|
||||
|
||||
#include <libevdev/libevdev-uinput.h>
|
||||
#include <libevdev/libevdev.h>
|
||||
#include <memory>
|
||||
|
||||
class EnterDevice {
|
||||
std::unique_ptr<struct libevdev, decltype(&libevdev_free)> raw_device;
|
||||
std::unique_ptr<struct libevdev_uinput, decltype(&libevdev_uinput_destroy)>
|
||||
raw_uinput_device;
|
||||
|
||||
public:
|
||||
EnterDevice();
|
||||
void send_enter_press() const;
|
||||
~EnterDevice() = default;
|
||||
};
|
||||
|
||||
#endif // ENTER_DEVICE_H
|
|
@ -1,12 +1,13 @@
|
|||
#include <cerrno>
|
||||
#include <csignal>
|
||||
#include <cstdlib>
|
||||
#include <glob.h>
|
||||
#include <ostream>
|
||||
#include <poll.h>
|
||||
|
||||
#include <glob.h>
|
||||
#include <libintl.h>
|
||||
#include <pthread.h>
|
||||
#include <spawn.h>
|
||||
#include <sys/poll.h>
|
||||
#include <stdexcept>
|
||||
#include <sys/signalfd.h>
|
||||
#include <sys/syslog.h>
|
||||
#include <sys/types.h>
|
||||
|
@ -33,59 +34,62 @@
|
|||
|
||||
#include <INIReader.h>
|
||||
|
||||
#include <boost/locale.hpp>
|
||||
|
||||
#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"
|
||||
|
||||
using namespace std;
|
||||
using namespace boost::locale;
|
||||
using namespace std::chrono_literals;
|
||||
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 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<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 (!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<int(int, const char *)> 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
|
||||
* 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<int(int, const struct pam_message **,
|
||||
struct pam_response **, void *)>
|
||||
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<int(int, const char *)> &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<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
|
||||
* @return Returns a PAM return code
|
||||
*/
|
||||
int identify(pam_handle_t *pamh, int flags, int argc, const char **argv,
|
||||
bool auth_tok) {
|
||||
INIReader reader("/lib/security/howdy/config.ini");
|
||||
// Open the system log so we can write to it
|
||||
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);
|
||||
|
||||
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<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 =
|
||||
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<const char **>(&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<char *const *>(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<int> child_task(packaged_task<int()>([&] {
|
||||
optional_task<int> child_task([&] {
|
||||
int status;
|
||||
wait(&status);
|
||||
{
|
||||
unique_lock<mutex> lk(m);
|
||||
confirmation_type = Type::Howdy;
|
||||
std::unique_lock<std::mutex> 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<tuple<int, char *>> pass_task(
|
||||
packaged_task<tuple<int, char *>()>([&] {
|
||||
optional_task<std::tuple<int, char *>> 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_cast<const char **>(&auth_tok_ptr), nullptr);
|
||||
{
|
||||
unique_lock<mutex> lk(m);
|
||||
confirmation_type = Type::Pam;
|
||||
std::unique_lock<std::mutex> lk(m);
|
||||
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();
|
||||
}
|
||||
|
||||
// Wait for the end either of the child or the password input
|
||||
{
|
||||
unique_lock<mutex> lk(m);
|
||||
cv.wait(lk);
|
||||
final_type = confirmation_type;
|
||||
std::unique_lock<std::mutex> 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) {
|
||||
// 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();
|
||||
|
||||
// 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);
|
||||
|
||||
// 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();
|
||||
char *password = nullptr;
|
||||
std::tie(pam_res, password) = pass_task.get();
|
||||
|
||||
if (pam_res != PAM_SUCCESS)
|
||||
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 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 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;
|
||||
}
|
||||
|
|
|
@ -1,36 +1,30 @@
|
|||
#ifndef MAIN_H_
|
||||
#define MAIN_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
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_
|
||||
|
|
|
@ -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: ''
|
||||
)
|
||||
|
|
|
@ -6,64 +6,76 @@
|
|||
#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;
|
||||
std::atomic<bool> _spawned;
|
||||
std::atomic<bool> _is_active;
|
||||
std::thread thread;
|
||||
std::packaged_task<T()> task;
|
||||
std::future<T> future;
|
||||
bool spawned;
|
||||
bool is_active;
|
||||
|
||||
public:
|
||||
optional_task(std::packaged_task<T()>);
|
||||
explicit optional_task(std::function<T()> fn);
|
||||
void activate();
|
||||
template <typename Dur> std::future_status wait(std::chrono::duration<Dur>);
|
||||
T get();
|
||||
bool is_active();
|
||||
void stop(bool);
|
||||
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::packaged_task<T()> t)
|
||||
: _task(std::move(t)), _future(_task.get_future()) {}
|
||||
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;
|
||||
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 Dur>
|
||||
std::future_status optional_task<T>::wait(std::chrono::duration<Dur> dur) {
|
||||
return _future.wait_for(dur);
|
||||
template <typename R, typename P>
|
||||
auto optional_task<T>::wait(std::chrono::duration<R, P> dur)
|
||||
-> std::future_status {
|
||||
return future.wait_for(dur);
|
||||
}
|
||||
|
||||
template <typename T> T optional_task<T>::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 <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) {
|
||||
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 <typename T> optional_task<T>::~optional_task<T>() {
|
||||
if (_is_active && _spawned)
|
||||
if (is_active && spawned) {
|
||||
stop(false);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // OPTIONAL_TASK_H_
|
||||
|
|
0
howdy/src/pam/po/LINGUAS
Normal file
0
howdy/src/pam/po/LINGUAS
Normal file
1
howdy/src/pam/po/POTFILES
Normal file
1
howdy/src/pam/po/POTFILES
Normal file
|
@ -0,0 +1 @@
|
|||
main.cc
|
10
howdy/src/pam/po/meson.build
Normal file
10
howdy/src/pam/po/meson.build
Normal file
|
@ -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' ]
|
||||
)
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue