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

Merge branch 'beta' of github.com:boltgolt/howdy into beta

This commit is contained in:
boltgolt 2023-02-19 10:23:18 +01:00
commit 373f002f96
No known key found for this signature in database
GPG key ID: BECEC9937E1AAE26
27 changed files with 684 additions and 417 deletions

25
.github/workflows/check.yml vendored Normal file
View 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

View file

@ -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,13 +102,13 @@ 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.
## 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.

View file

@ -4,4 +4,5 @@ src
*.zip
*.tar.xz
*.patch
*.dat.bz2
*.dat.bz2
.SRCINFO

View file

@ -17,6 +17,7 @@ depends=(
'python3'
'python-dlib'
'python-numpy'
'python-opencv'
)
makedepends=(
'cmake'

View file

@ -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 <boltgolt@gmail.com>
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 | 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.

View file

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

View file

@ -40,84 +40,72 @@ 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("/etc/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("/etc/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"
if key == "capture_failed":
key = "save_failed"
if key == "capture_successful":
key = "save_successful"
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("/etc/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") and not os.path.exists("/etc/howdy/dlib-data/shape_predictor_5_face_landmarks.dat"):
print("Attempting installation of missing data files")
handleStatus(subprocess.call(["./install.sh"], shell=True, cwd="/etc/howdy/dlib-data"))
# Write it all to file
with open("/etc/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 +181,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

View file

@ -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
meson compile -C build
# Compile shared object
ninja -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

View file

@ -178,7 +178,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:

View file

@ -2,21 +2,24 @@
# Import required modules
import configparser
import builtins
import os
import json
import sys
import time
import dlib
import cv2
from recorders.video_capture import VideoCapture
import numpy as np
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("/etc/howdy/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"))
@ -24,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")
@ -55,11 +59,26 @@ 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")
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:
pass
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
# Open the window and attach a a mouse listener
@ -98,7 +117,7 @@ try:
sec_frames = 0
# Grab a single frame of video
ret, frame = video_capture.read_frame()
orig_frame, frame = video_capture.read_frame()
frame = clahe.apply(frame)
# Make a frame to put overlays in
@ -142,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)
@ -156,6 +176,9 @@ 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()
@ -165,8 +188,33 @@ try:
# 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))
# 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]
# If a model matches
if 0 < match < video_certainty:
# Turn the circle green
color = (0, 230, 0)
# 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, (0, 0, 230), 2)
cv2.circle(overlay, (x, y), r, color, 2)
# Add the overlay to the frame with some transparency
alpha = 0.65
@ -192,8 +240,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:

View file

@ -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,12 +24,14 @@ import snapshot
import numpy as np
import _thread as thread
from i18n import _
from recorders.video_capture import VideoCapture
# 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
@ -100,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]
@ -111,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
@ -147,8 +150,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)
@ -230,10 +233,10 @@ 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:
if save_failed:
make_snapshot(_("FAILED"))
if dark_tries == valid_frames:
@ -248,7 +251,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)
@ -269,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):
@ -280,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:
@ -288,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:
@ -296,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)
@ -355,7 +362,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

View file

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

View file

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

View 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

View file

@ -1 +1 @@
subprojects/*/
subprojects/inih/

View file

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

View 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));
};
}

View 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

View file

@ -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,65 @@
#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;
case CompareError::INVALID_DEVICE:
syslog(LOG_ERR, "Failure, not possible to open camera at configured path");
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 +101,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
* @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<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", "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");
return PAM_AUTHINFO_UNAVAIL;
}
}
// Try to detect the laptop lid state and stop if it's closed
if (config.GetBoolean("core", "abort_if_lid_closed", 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 +194,257 @@ 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("/etc/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("/etc/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 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
// 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> lock(mutx);
if (confirmation_type == ConfirmationType::Unset) {
confirmation_type = ConfirmationType::Howdy;
}
}
cv.notify_all();
convar.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 *>()>([&] {
char *auth_tok_ptr = nullptr;
int pam_res = pam_get_authtok(pamh, PAM_AUTHTOK,
(const char **)&auth_tok_ptr, nullptr);
{
unique_lock<mutex> lk(m);
confirmation_type = Type::Pam;
}
cv.notify_all();
return tuple<int, char *>(pam_res, auth_tok_ptr);
}));
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> lock(mutx);
if (confirmation_type == ConfirmationType::Unset) {
confirmation_type = ConfirmationType::Pam;
}
}
convar.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> lock(mutx);
convar.wait(lock, [&] { 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) {
// 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"
// 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();
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;
}

View file

@ -1,36 +1,32 @@
#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,
INVALID_DEVICE = 14,
TIMEOUT_ACTIVE = 2816
};
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_

View file

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

View file

@ -6,64 +6,75 @@
#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{false};
bool is_active{false};
public:
optional_task(std::packaged_task<T()>);
explicit optional_task(std::function<T()> func);
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()> func)
: task(std::packaged_task<T()>(std::move(func))), future(task.get_future()) {}
// 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
View file

View file

@ -0,0 +1 @@
main.cc

View 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' ]
)

View file

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

View file

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