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. 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. 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 | | Command | Description |
|-----------|-----------------------------------------------| |-----------|-----------------------------------------------|
| `add` | Add a new face model for an user | | `add` | Add a new face model for a user |
| `clear` | Remove all face models for an user | | `clear` | Remove all face models for a user |
| `config` | Open the config file in your default editor | | `config` | Open the config file in your default editor |
| `disable` | Disable or enable howdy | | `disable` | Disable or enable howdy |
| `list` | List all saved face models for an user | | `list` | List all saved face models for a user |
| `remove` | Remove a specific model for an user | | `remove` | Remove a specific model for a user |
| `snapshot`| Take a snapshot of your camera input | | `snapshot`| Take a snapshot of your camera input |
| `test` | Test the camera and recognition methods | | `test` | Test the camera and recognition methods |
| `version` | Print the current version number | | `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 ## 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. If you encounter an error that hasn't been reported yet, don't be afraid to open a new issue.
## A note on security ## 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. 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 *.zip
*.tar.xz *.tar.xz
*.patch *.patch
*.dat.bz2 *.dat.bz2
.SRCINFO

View file

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

View file

@ -2,14 +2,14 @@ Source: howdy
Section: misc Section: misc
Priority: optional Priority: optional
Standards-Version: 3.9.7 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> Maintainer: boltgolt <boltgolt@gmail.com>
Vcs-Git: https://github.com/boltgolt/howdy Vcs-Git: https://github.com/boltgolt/howdy
Package: howdy Package: howdy
Homepage: https://github.com/boltgolt/howdy Homepage: https://github.com/boltgolt/howdy
Architecture: amd64 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 Recommends: libatlas-base-dev | libopenblas-dev | liblapack-dev, howdy-gtk
Suggests: nvidia-cuda-dev (>= 7.5) Suggests: nvidia-cuda-dev (>= 7.5)
Description: Howdy: Windows Hello style authentication for Linux. Description: Howdy: Windows Hello style authentication for Linux.

View file

@ -1,16 +1,17 @@
src/cli/. lib/security/howdy/cli src/cli/. lib/security/howdy/cli
src/dlib-data/. lib/security/howdy/dlib-data
src/locales/. lib/security/howdy/locales src/locales/. lib/security/howdy/locales
src/recorders/. lib/security/howdy/recorders src/recorders/. lib/security/howdy/recorders
src/rubberstamps/. lib/security/howdy/rubberstamps src/rubberstamps/. lib/security/howdy/rubberstamps
src/cli.py lib/security/howdy src/cli.py lib/security/howdy
src/compare.py lib/security/howdy src/compare.py lib/security/howdy
src/config.ini lib/security/howdy
src/i18n.py lib/security/howdy src/i18n.py lib/security/howdy
src/logo.png lib/security/howdy src/logo.png lib/security/howdy
src/pam.py lib/security/howdy
src/snapshot.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/autocomplete/. usr/share/bash-completion/completions
src/pam-config/. /usr/share/pam-configs 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 # Create shorthand for subprocess creation
sc = subprocess.call sc = subprocess.call
# We're not in fresh configuration mode so don't continue the setup # If the package is being upgraded
if not os.path.exists("/tmp/howdy_picked_device"): if "upgrade" in sys.argv:
# Check if we have an older config we can restore # If preinst has made a config backup
if len(sys.argv) > 2: if os.path.exists("/tmp/howdy_config_backup_v" + sys.argv[2] + ".ini"):
if os.path.exists("/tmp/howdy_config_backup_v" + sys.argv[2] + ".ini"): # Get the config parser
# Get the config parser import configparser
import configparser
# Load th old and new config files # Load th old and new config files
oldConf = configparser.ConfigParser() oldConf = configparser.ConfigParser()
oldConf.read("/tmp/howdy_config_backup_v" + sys.argv[2] + ".ini") oldConf.read("/tmp/howdy_config_backup_v" + sys.argv[2] + ".ini")
newConf = configparser.ConfigParser() newConf = configparser.ConfigParser()
newConf.read("/etc/howdy/config.ini") newConf.read("/etc/howdy/config.ini")
# Go through every setting in the old config and apply it to the new file # Go through every setting in the old config and apply it to the new file
for section in oldConf.sections(): for section in oldConf.sections():
for (key, value) in oldConf.items(section): for (key, value) in oldConf.items(section):
# MIGRATION 2.3.1 -> 2.4.0 # MIGRATION 2.3.1 -> 2.4.0
# If config is still using the old device_id parameter, convert it to a path # If config is still using the old device_id parameter, convert it to a path
if key == "device_id": if key == "device_id":
key = "device_path" key = "device_path"
value = "/dev/video" + value value = "/dev/video" + value
# MIGRATION 2.4.0 -> 2.5.0 # MIGRATION 2.4.0 -> 2.5.0
# Finally correct typo in "timout" config value # Finally correct typo in "timout" config value
if key == "timout": if key == "timout":
key = "timeout" key = "timeout"
# MIGRATION 2.5.0 -> 2.5.1 # MIGRATION 2.5.0 -> 2.5.1
# Remove unsafe automatic dismissal of lock screen # Remove unsafe automatic dismissal of lock screen
if key == "dismiss_lockscreen": if key == "dismiss_lockscreen":
if value == "true": if value == "true":
print("DEPRECATION: Config value dismiss_lockscreen is no longer supported because of login loop issues.") print("DEPRECATION: Config value dismiss_lockscreen is no longer supported because of login loop issues.")
continue continue
# MIGRATION 2.6.1 -> 3.0.0 # MIGRATION 2.6.1 -> 3.0.0
# Fix capture being enabled by default # Fix capture being enabled by default
if key == "capture_failed" or key == "capture_successful": if key == "capture_failed" or key == "capture_successful":
if value == "true": if value == "true":
print("NOTICE: Howdy login image captures have been disabled by default, change the config to enable them again") print("NOTICE: Howdy login image captures have been disabled by default, change the config to enable them again")
value = "false" value = "false"
# MIGRATION 2.6.1 -> 3.0.0 # MIGRATION 2.6.1 -> 3.0.0
# Rename config options so they don't do the opposite of what is commonly expected # Rename config options so they don't do the opposite of what is commonly expected
if key == "ignore_ssh": if key == "ignore_ssh":
key = "abort_if_ssh" key = "abort_if_ssh"
if key == "ignore_closed_lid": if key == "ignore_closed_lid":
key = "abort_if_lid_closed" key = "abort_if_lid_closed"
if key == "capture_failed":
key = "save_failed"
if key == "capture_successful":
key = "save_successful"
try: try:
newConf.set(section, key, value) newConf.set(section, key, value)
# Add a new section where needed # Add a new section where needed
except configparser.NoSectionError: except configparser.NoSectionError:
newConf.add_section(section) newConf.add_section(section)
newConf.set(section, key, value) newConf.set(section, key, value)
# Write it all to file # Write it all to file
with open("/etc/howdy/config.ini", "w") as configfile: with open("/etc/howdy/config.ini", "w") as configfile:
newConf.write(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"))
sys.exit(0) 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") log("Downloading and unpacking data files")
# Run the bash script to download and unpack the .dat files needed # Run the bash script to download and unpack the .dat files needed
@ -193,10 +181,6 @@ rmtree(DLIB_DIR)
print("Temporary dlib files removed") print("Temporary dlib files removed")
log("Installing OpenCV")
handleStatus(subprocess.call(["pip3", "install", "--no-cache-dir", "opencv-python"]))
log("Configuring howdy") log("Configuring howdy")
# Manually change the camera id to the one picked # Manually change the camera id to the one picked

View file

@ -10,9 +10,13 @@ include /usr/share/dpkg/default.mk
build: build:
# Create build dir # Create build dir
meson setup -Dinih:with_INIReader=true build src/pam meson setup -Dinih:with_INIReader=true build src/pam
# Compile shared object # Compile shared object
meson compile -C build ninja -C build
clean: clean:
# Delete mason build directory # Delete mason build directory
rm -rf ./build 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() video_capture.release()
# If we've found no faces, try to determine why # 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: if valid_frames == 0:
print(_("Camera saw only black frames - is IR emitter working?")) print(_("Camera saw only black frames - is IR emitter working?"))
elif valid_frames == dark_tries: elif valid_frames == dark_tries:

View file

@ -2,21 +2,24 @@
# Import required modules # Import required modules
import configparser import configparser
import builtins
import os import os
import json
import sys import sys
import time import time
import dlib import dlib
import cv2 import cv2
from recorders.video_capture import VideoCapture import numpy as np
from i18n import _ from i18n import _
from recorders.video_capture import VideoCapture
# Get the absolute path to the current file # The absolute path to the config directory
path = os.path.dirname(os.path.abspath(__file__)) path = "/etc/howdy"
# Read config from disk # Read config from disk
config = configparser.ConfigParser() config = configparser.ConfigParser()
config.read("/etc/howdy/config.ini") config.read(path + "/config.ini")
if config.get("video", "recording_plugin") != "opencv": 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")) 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) 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) exposure = config.getint("video", "exposure", fallback=-1)
dark_threshold = config.getfloat("video", "dark_threshold") dark_threshold = config.getfloat("video", "dark_threshold")
@ -55,11 +59,26 @@ use_cnn = config.getboolean('core', 'use_cnn', fallback=False)
if use_cnn: if use_cnn:
face_detector = dlib.cnn_face_detection_model_v1( 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: else:
face_detector = dlib.get_frontal_face_detector() 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)) clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
# Open the window and attach a a mouse listener # Open the window and attach a a mouse listener
@ -98,7 +117,7 @@ try:
sec_frames = 0 sec_frames = 0
# Grab a single frame of video # Grab a single frame of video
ret, frame = video_capture.read_frame() orig_frame, frame = video_capture.read_frame()
frame = clahe.apply(frame) frame = clahe.apply(frame)
# Make a frame to put overlays in # Make a frame to put overlays in
@ -142,10 +161,11 @@ try:
# Show that this is an ignored frame in the top right # 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) cv2.putText(overlay, _("DARK FRAME"), (width - 68, 16), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 0, 255), 0, cv2.LINE_AA)
else: 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) cv2.putText(overlay, _("SCAN FRAME"), (width - 68, 16), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 255, 0), 0, cv2.LINE_AA)
rec_tm = time.time() rec_tm = time.time()
# Get the locations of all faces and their locations # Get the locations of all faces and their locations
# Upsample it once # Upsample it once
face_locations = face_detector(frame, 1) face_locations = face_detector(frame, 1)
@ -156,6 +176,9 @@ try:
if use_cnn: if use_cnn:
loc = loc.rect 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 # Get the center X and Y from the rectangular points
x = int((loc.right() - loc.left()) / 2) + loc.left() x = int((loc.right() - loc.left()) / 2) + loc.left()
y = int((loc.bottom() - loc.top()) / 2) + loc.top() y = int((loc.bottom() - loc.top()) / 2) + loc.top()
@ -165,8 +188,33 @@ try:
# Add 20% padding # Add 20% padding
r = int(r + (r * 0.2)) 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 # 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 # Add the overlay to the frame with some transparency
alpha = 0.65 alpha = 0.65
@ -192,8 +240,8 @@ try:
# are captured and even after a delay it does not # are captured and even after a delay it does not
# always work. Setting exposure at every frame is # always work. Setting exposure at every frame is
# reliable though. # reliable though.
video_capture.intenal.set(cv2.CAP_PROP_AUTO_EXPOSURE, 1.0) # 1 = Manual video_capture.internal.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_EXPOSURE, float(exposure))
# On ctrl+C # On ctrl+C
except KeyboardInterrupt: 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 # Running in a local python instance to get around PATH issues
# Import time so we can start timing asap # Import time so we can start timing asap
@ -23,12 +24,14 @@ import snapshot
import numpy as np import numpy as np
import _thread as thread import _thread as thread
from i18n import _ # Allow imports from the local howdy folder
from recorders.video_capture import VideoCapture sys.path.append('/lib/security/howdy')
from recorders.video_capture import VideoCapture
from i18n import _
def exit(code=None): def exit(code=None):
"""Exit while closeing howdy-gtk properly""" """Exit while closing howdy-gtk properly"""
global gtk_proc global gtk_proc
# Exit the auth ui process if there is one # Exit the auth ui process if there is one
@ -100,8 +103,8 @@ def send_to_ui(type, message):
if len(sys.argv) < 2: if len(sys.argv) < 2:
exit(12) exit(12)
# Get the absolute path to the current directory # Get the absolute path to the config directory
PATH = os.path.abspath(__file__ + "/..") PATH = "/etc/howdy"
# The username of the user being authenticated # The username of the user being authenticated
user = sys.argv[1] user = sys.argv[1]
@ -111,7 +114,7 @@ models = []
encodings = [] encodings = []
# Amount of ignored 100% black frames # Amount of ignored 100% black frames
black_tries = 0 black_tries = 0
# Amount of ingnored dark frames # Amount of ignored dark frames
dark_tries = 0 dark_tries = 0
# Total amount of frames captured # Total amount of frames captured
frames = 0 frames = 0
@ -147,8 +150,8 @@ timeout = config.getint("video", "timeout", fallback=5)
dark_threshold = config.getfloat("video", "dark_threshold", fallback=50.0) dark_threshold = config.getfloat("video", "dark_threshold", fallback=50.0)
video_certainty = config.getfloat("video", "certainty", fallback=3.5) / 10 video_certainty = config.getfloat("video", "certainty", fallback=3.5) / 10
end_report = config.getboolean("debug", "end_report", fallback=False) end_report = config.getboolean("debug", "end_report", fallback=False)
capture_failed = config.getboolean("snapshots", "capture_failed", fallback=False) save_failed = config.getboolean("snapshots", "save_failed", fallback=False)
capture_successful = config.getboolean("snapshots", "capture_successful", fallback=False) save_successful = config.getboolean("snapshots", "save_successful", fallback=False)
gtk_stdout = config.getboolean("debug", "gtk_stdout", fallback=False) gtk_stdout = config.getboolean("debug", "gtk_stdout", fallback=False)
rotate = config.getint("video", "rotate", fallback=0) rotate = config.getint("video", "rotate", fallback=0)
@ -230,10 +233,10 @@ while True:
# Show it in the ui as subtext # Show it in the ui as subtext
send_to_ui("S", ui_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: if time.time() - timings["fr"] > timeout:
# Create a timeout snapshot if enabled # Create a timeout snapshot if enabled
if capture_failed: if save_failed:
make_snapshot(_("FAILED")) make_snapshot(_("FAILED"))
if dark_tries == valid_frames: if dark_tries == valid_frames:
@ -248,7 +251,7 @@ while True:
gsframe = clahe.apply(gsframe) gsframe = clahe.apply(gsframe)
# If snapshots have been turned on # If snapshots have been turned on
if capture_failed or capture_successful: if save_failed or save_successful:
# Start capturing frames for the snapshot # Start capturing frames for the snapshot
if len(snapframes) < 3: if len(snapframes) < 3:
snapframes.append(frame) snapframes.append(frame)
@ -269,6 +272,7 @@ while True:
dark_running_total += darkness dark_running_total += darkness
valid_frames += 1 valid_frames += 1
# If the image exceeds darkness threshold due to subject distance, # If the image exceeds darkness threshold due to subject distance,
# skip to the next frame # skip to the next frame
if (darkness > dark_threshold): if (darkness > dark_threshold):
@ -280,6 +284,7 @@ while True:
# Apply that factor to the frame # Apply that factor to the frame
frame = cv2.resize(frame, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA) 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) 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 camera is configured to rotate = 1, check portrait in addition to landscape
if rotate == 1: if rotate == 1:
if frames % 3 == 1: if frames % 3 == 1:
@ -288,6 +293,7 @@ while True:
if frames % 3 == 2: if frames % 3 == 2:
frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE) frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE)
gsframe = cv2.rotate(gsframe, cv2.ROTATE_90_CLOCKWISE) gsframe = cv2.rotate(gsframe, cv2.ROTATE_90_CLOCKWISE)
# If camera is configured to rotate = 2, check portrait orientation # If camera is configured to rotate = 2, check portrait orientation
elif rotate == 2: elif rotate == 2:
if frames % 2 == 0: if frames % 2 == 0:
@ -296,6 +302,7 @@ while True:
else: else:
frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE) frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE)
gsframe = cv2.rotate(gsframe, cv2.ROTATE_90_CLOCKWISE) gsframe = cv2.rotate(gsframe, cv2.ROTATE_90_CLOCKWISE)
# Get all faces from that frame as encodings # Get all faces from that frame as encodings
# Upsamples 1 time # Upsamples 1 time
face_locations = face_detector(gsframe, 1) face_locations = face_detector(gsframe, 1)
@ -355,7 +362,7 @@ while True:
print(_("Winning model: %d (\"%s\")") % (match_index, models[match_index]["label"])) print(_("Winning model: %d (\"%s\")") % (match_index, models[match_index]["label"]))
# Make snapshot if enabled # Make snapshot if enabled
if capture_successful: if save_successful:
make_snapshot(_("SUCCESSFUL")) make_snapshot(_("SUCCESSFUL"))
# Run rubberstamps if enabled # 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. # computational power to run, and is meant to be executed on a GPU to attain reasonable speed.
use_cnn = false 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] [video]
# The certainty of the detected face belonging to the user of the account # 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 # On a scale from 1 to 10, values above 5 are not recommended

View file

@ -3,4 +3,4 @@ Default: yes
Priority: 512 Priority: 512
Auth-Type: Primary Auth-Type: Primary
Auth: 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 # 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

@ -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 <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,65 @@
#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")); break;
syslog(LOG_INFO, "Failure, image too dark"); case CompareError::INVALID_DEVICE:
syslog(LOG_ERR, "Failure, not possible to open camera at configured path");
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 +101,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
* @return Returns the conversation function return 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 **, 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", "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 * @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("/etc/howdy/config.ini"); INIReader config("/etc/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 mutx;
// TODO: Find a clean way to do this std::condition_variable convar;
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> lock(mutx);
confirmation_type = Type::Howdy; if (confirmation_type == ConfirmationType::Unset) {
confirmation_type = ConfirmationType::Howdy;
}
} }
cv.notify_all(); convar.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(
int pam_res = pam_get_authtok(pamh, PAM_AUTHTOK, pamh, PAM_AUTHTOK, const_cast<const char **>(&auth_tok_ptr), nullptr);
(const char **)&auth_tok_ptr, nullptr); {
{ std::unique_lock<std::mutex> lock(mutx);
unique_lock<mutex> lk(m); if (confirmation_type == ConfirmationType::Unset) {
confirmation_type = Type::Pam; confirmation_type = ConfirmationType::Pam;
} }
cv.notify_all(); }
return tuple<int, char *>(pam_res, auth_tok_ptr); 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(); 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> lock(mutx);
cv.wait(lk); convar.wait(lock, [&] { 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); child_task.stop(false);
// If the workaround is native // We just wait for the thread to stop since it's this one which sent us the
if (auth_tok) { // confirmation type
// We cancel the thread using pthread, pam_get_authtok seems to be a pass_task.stop(false);
// cancellation point
if (pass_task.is_active()) {
pass_task.stop(true);
}
}
int howdy_status = child_task.get();
// If exited successfully char *password = nullptr;
if (howdy_status == 0) { std::tie(pam_res, password) = pass_task.get();
if (!reader.GetBoolean("core", "no_confirmation", true)) {
// Construct confirmation text from i18n string if (pam_res != PAM_SUCCESS) {
string confirm_text = dgettext("pam", "Identified face as {}"); return pam_res;
string identify_msg(
confirm_text.replace(confirm_text.find("{}"), 2, string(user_ptr)));
// Send confirmation message to user
conv_function(PAM_TEXT_INFO, identify_msg.c_str());
}
syslog(LOG_INFO, "Login approved");
return PAM_SUCCESS;
} else {
return on_howdy_auth(howdy_status, conv_function);
} }
// The password has been entered, we are passing it to PAM stack
return PAM_IGNORE;
} }
// The branch with Howdy confirmation type returns early, so we don't need an // The compare process has finished its execution
// 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 // Get python process status code
// confirmation type int status = child_task.get();
if (workaround == Workaround::Input && auth_tok) {
// 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); pass_task.stop(false);
} }
char *token = nullptr; return howdy_status(username, status, config, conv_function);
tie(pam_res, token) = pass_task.get();
if (pam_res != PAM_SUCCESS)
return pam_res;
int howdy_status = child_task.get();
if (strlen(token) == 0) {
if (howdy_status == 0) {
if (!reader.GetBoolean("core", "no_confirmation", true)) {
// Construct confirmation text from i18n string
string confirm_text = dgettext("pam", "Identified face as {}");
string identify_msg(
confirm_text.replace(confirm_text.find("{}"), 2, string(user_ptr)));
// Send confirmation message to user
conv_function(PAM_TEXT_INFO, identify_msg.c_str());
}
syslog(LOG_INFO, "Login approved");
return PAM_SUCCESS;
} else {
return on_howdy_auth(howdy_status, conv_function);
}
}
// The password has been entered, we are passing it to PAM stack
return PAM_IGNORE;
} }
// Called by PAM when a user needs to be authenticated, for example by running // 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,32 @@
#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: INVALID_DEVICE = 14,
return (l == "native"); TIMEOUT_ACTIVE = 2816
default: };
return false;
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) { 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,75 @@
#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{false};
std::atomic<bool> _is_active; bool is_active{false};
public: public:
optional_task(std::packaged_task<T()>); explicit optional_task(std::function<T()> func);
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()> func)
: _task(std::move(t)), _future(_task.get_future()) {} : 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() { 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_

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] [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

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(_("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(_("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") print("\n\tsudo howdy config\n")
sys.exit(1) sys.exit(14)
# Create reader # Create reader
# The internal video recorder # The internal video recorder
@ -83,7 +83,7 @@ class VideoCapture:
ret, frame = self.internal.read() ret, frame = self.internal.read()
if not ret: if not ret:
print(_("Failed to read camera specified in the 'device_path' config option, aborting")) print(_("Failed to read camera specified in the 'device_path' config option, aborting"))
sys.exit(1) sys.exit(14)
try: try:
# Convert from color to grayscale # Convert from color to grayscale