diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..1843f5d --- /dev/null +++ b/.github/workflows/check.yml @@ -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 diff --git a/README.md b/README.md index 7fe3450..21e5800 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ Go to the [openSUSE wiki page](https://en.opensuse.org/SDB:Facial_authentication After installation, Howdy needs to learn what you look like so it can recognise you later. Run `sudo howdy add` to add a face model. -If nothing went wrong we should be able to run sudo by just showing your face. Open a new terminal and run `sudo -i` to see it in action. Please check [this wiki page](https://github.com/boltgolt/howdy/wiki/Common-issues) if you've experiencing problems or [search](https://github.com/boltgolt/howdy/issues) for similar issues. +If nothing went wrong we should be able to run sudo by just showing your face. Open a new terminal and run `sudo -i` to see it in action. Please check [this wiki page](https://github.com/boltgolt/howdy/wiki/Common-issues) if you're experiencing problems or [search](https://github.com/boltgolt/howdy/issues) for similar issues. If you're curious you can run `sudo howdy config` to open the central config file and see the options Howdy has to offer. On most systems this will open the nano editor, where you have to press `ctrl`+`x` to save your changes. @@ -84,12 +84,12 @@ howdy [-U user] [-y] command [argument] | Command | Description | |-----------|-----------------------------------------------| -| `add` | Add a new face model for an user | -| `clear` | Remove all face models for an user | +| `add` | Add a new face model for a user | +| `clear` | Remove all face models for a user | | `config` | Open the config file in your default editor | | `disable` | Disable or enable howdy | -| `list` | List all saved face models for an user | -| `remove` | Remove a specific model for an user | +| `list` | List all saved face models for a user | +| `remove` | Remove a specific model for a user | | `snapshot`| Take a snapshot of your camera input | | `test` | Test the camera and recognition methods | | `version` | Print the current version number | @@ -102,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. diff --git a/howdy/archlinux/howdy/.gitignore b/howdy/archlinux/howdy/.gitignore index 6748987..cd14529 100644 --- a/howdy/archlinux/howdy/.gitignore +++ b/howdy/archlinux/howdy/.gitignore @@ -4,4 +4,5 @@ src *.zip *.tar.xz *.patch -*.dat.bz2 \ No newline at end of file +*.dat.bz2 +.SRCINFO diff --git a/howdy/archlinux/howdy/PKGBUILD b/howdy/archlinux/howdy/PKGBUILD index b3964fc..dc66290 100644 --- a/howdy/archlinux/howdy/PKGBUILD +++ b/howdy/archlinux/howdy/PKGBUILD @@ -17,6 +17,7 @@ depends=( 'python3' 'python-dlib' 'python-numpy' + 'python-opencv' ) makedepends=( 'cmake' diff --git a/howdy/debian/control b/howdy/debian/control index 17b465b..d164854 100644 --- a/howdy/debian/control +++ b/howdy/debian/control @@ -2,14 +2,14 @@ Source: howdy Section: misc Priority: optional Standards-Version: 3.9.7 -Build-Depends: python, devscripts, dh-make, debhelper, fakeroot, python3, python3-pip, python3-setuptools, python3-wheel, ninja-build, meson, libpam0g-dev, libboost-all-dev +Build-Depends: devscripts, git, dh-make, debhelper, fakeroot, python3, python3-pip, python3-setuptools, python3-wheel, ninja-build, meson, libpam0g-dev, libboost-all-dev, pkg-config, libevdev-dev, libinih-dev Maintainer: boltgolt Vcs-Git: https://github.com/boltgolt/howdy Package: howdy Homepage: https://github.com/boltgolt/howdy Architecture: amd64 -Depends: ${misc:Depends}, ${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. diff --git a/howdy/debian/install b/howdy/debian/install index 3fadce0..cc3c9ea 100644 --- a/howdy/debian/install +++ b/howdy/debian/install @@ -1,16 +1,17 @@ src/cli/. lib/security/howdy/cli -src/dlib-data/. lib/security/howdy/dlib-data src/locales/. lib/security/howdy/locales src/recorders/. lib/security/howdy/recorders src/rubberstamps/. lib/security/howdy/rubberstamps src/cli.py lib/security/howdy src/compare.py lib/security/howdy -src/config.ini lib/security/howdy src/i18n.py lib/security/howdy src/logo.png lib/security/howdy -src/pam.py lib/security/howdy src/snapshot.py lib/security/howdy +build/pam_howdy.so lib/security/howdy + +src/dlib-data/. etc/howdy/dlib-data +src/config.ini etc/howdy + src/autocomplete/. usr/share/bash-completion/completions src/pam-config/. /usr/share/pam-configs -build/libpam_howdy.so lib/security/howdy diff --git a/howdy/debian/postinst b/howdy/debian/postinst index f92a7a7..1cf7b43 100755 --- a/howdy/debian/postinst +++ b/howdy/debian/postinst @@ -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 diff --git a/howdy/debian/rules b/howdy/debian/rules index 1176fe0..91e16ac 100755 --- a/howdy/debian/rules +++ b/howdy/debian/rules @@ -10,9 +10,13 @@ include /usr/share/dpkg/default.mk build: # Create build dir meson setup -Dinih:with_INIReader=true build src/pam - # Compile shared object - 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 diff --git a/howdy/src/cli/add.py b/howdy/src/cli/add.py index 3dcd9ff..c4e1e8e 100644 --- a/howdy/src/cli/add.py +++ b/howdy/src/cli/add.py @@ -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: diff --git a/howdy/src/cli/test.py b/howdy/src/cli/test.py index 66a18f6..c984c5b 100644 --- a/howdy/src/cli/test.py +++ b/howdy/src/cli/test.py @@ -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: diff --git a/howdy/src/compare.py b/howdy/src/compare.py index 1e6e936..92bab60 100644 --- a/howdy/src/compare.py +++ b/howdy/src/compare.py @@ -1,4 +1,5 @@ -# Compare incomming video with known faces +#!/usr/bin/env python3 +# Compare incoming video with known faces # Running in a local python instance to get around PATH issues # Import time so we can start timing asap @@ -23,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 diff --git a/howdy/src/config.ini b/howdy/src/config.ini index d02de0d..28d5258 100644 --- a/howdy/src/config.ini +++ b/howdy/src/config.ini @@ -30,6 +30,14 @@ disabled = false # computational power to run, and is meant to be executed on a GPU to attain reasonable speed. use_cnn = false +# WARNING: Changing this key can lead to unstability +# Set a workaround to confirm the prompt +# +# "off" will disable it, so the user needs to confirm manually +# "input" will send an enter keypress to confirm (the prompt needs to be on focus) +# "native" will stop the prompt at PAM level (DANGEROUS!) +workaround = input + [video] # The certainty of the detected face belonging to the user of the account # On a scale from 1 to 10, values above 5 are not recommended diff --git a/howdy/src/pam-config/howdy b/howdy/src/pam-config/howdy index 28fb6b1..ed19039 100644 --- a/howdy/src/pam-config/howdy +++ b/howdy/src/pam-config/howdy @@ -3,4 +3,4 @@ Default: yes Priority: 512 Auth-Type: Primary Auth: - [success=end default=ignore] pam_python.so /lib/security/howdy/pam.py + [success=end default=ignore] /lib/security/howdy/pam_howdy.so diff --git a/howdy/src/pam/.clang-tidy b/howdy/src/pam/.clang-tidy new file mode 100644 index 0000000..887f4c3 --- /dev/null +++ b/howdy/src/pam/.clang-tidy @@ -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 diff --git a/howdy/src/pam/.gitignore b/howdy/src/pam/.gitignore index feaa494..2e4843c 100644 --- a/howdy/src/pam/.gitignore +++ b/howdy/src/pam/.gitignore @@ -1 +1 @@ -subprojects/*/ +subprojects/inih/ diff --git a/howdy/src/pam/README.md b/howdy/src/pam/README.md index 765cba8..23d1804 100644 --- a/howdy/src/pam/README.md +++ b/howdy/src/pam/README.md @@ -1,20 +1,35 @@ # Howdy PAM module +## Prepare + +This module depends on `INIReader` and `libevdev`. +They can be installed with these packages: + +``` +Arch Linux - libinih libevdev +Debian - libinih-dev libevdev-dev +Fedora - inih-devel libevdev-devel +OpenSUSE - inih libevdev-devel +``` + +If your distribution doesn't provide `INIReader`, +it will be automatically pulled from git at the subproject's pinned version. + ## Build -```sh -meson setup build -Dinih:with_INIReader=true +``` sh +meson setup build meson compile -C build ``` ## Install -```sh -sudo mv build/libpam_howdy.so /lib/security/pam_howdy.so +``` sh +meson install -C build ``` -Change PAM config line to: +Add the following line to your PAM configuration (/etc/pam.d/your-service): -```pam -auth sufficient pam_howdy.so +``` pam +auth sufficient pam_howdy.so ``` diff --git a/howdy/src/pam/enter_device.cc b/howdy/src/pam/enter_device.cc new file mode 100644 index 0000000..0114d8d --- /dev/null +++ b/howdy/src/pam/enter_device.cc @@ -0,0 +1,48 @@ +#include "enter_device.hh" + +#include +#include +#include + +EnterDevice::EnterDevice() + : raw_device(libevdev_new(), &libevdev_free), + raw_uinput_device(nullptr, &libevdev_uinput_destroy) { + auto *dev_ptr = raw_device.get(); + + libevdev_set_name(dev_ptr, "enter device"); + libevdev_enable_event_type(dev_ptr, EV_KEY); + libevdev_enable_event_code(dev_ptr, EV_KEY, KEY_ENTER, nullptr); + + int err; + struct libevdev_uinput *uinput_dev_ptr; + if ((err = libevdev_uinput_create_from_device( + dev_ptr, LIBEVDEV_UINPUT_OPEN_MANAGED, &uinput_dev_ptr)) != 0) { + throw std::runtime_error(std::string("Failed to create device: ") + + strerror(-err)); + } + + raw_uinput_device.reset(uinput_dev_ptr); +}; + +void EnterDevice::send_enter_press() const { + auto *uinput_dev_ptr = raw_uinput_device.get(); + + int err; + if ((err = libevdev_uinput_write_event(uinput_dev_ptr, EV_KEY, KEY_ENTER, + 1)) != 0) { + throw std::runtime_error(std::string("Failed to write event: ") + + strerror(-err)); + } + + if ((err = libevdev_uinput_write_event(uinput_dev_ptr, EV_KEY, KEY_ENTER, + 0)) != 0) { + throw std::runtime_error(std::string("Failed to write event: ") + + strerror(-err)); + } + + if ((err = libevdev_uinput_write_event(uinput_dev_ptr, EV_SYN, SYN_REPORT, + 0)) != 0) { + throw std::runtime_error(std::string("Failed to write event: ") + + strerror(-err)); + }; +} diff --git a/howdy/src/pam/enter_device.hh b/howdy/src/pam/enter_device.hh new file mode 100644 index 0000000..acdadd1 --- /dev/null +++ b/howdy/src/pam/enter_device.hh @@ -0,0 +1,19 @@ +#ifndef ENTER_DEVICE_H_ +#define ENTER_DEVICE_H_ + +#include +#include +#include + +class EnterDevice { + std::unique_ptr raw_device; + std::unique_ptr + raw_uinput_device; + +public: + EnterDevice(); + void send_enter_press() const; + ~EnterDevice() = default; +}; + +#endif // ENTER_DEVICE_H diff --git a/howdy/src/pam/main.cc b/howdy/src/pam/main.cc index 66c750b..4090cba 100644 --- a/howdy/src/pam/main.cc +++ b/howdy/src/pam/main.cc @@ -1,12 +1,13 @@ #include #include #include -#include #include -#include + +#include +#include #include #include -#include +#include #include #include #include @@ -33,59 +34,65 @@ #include -#include - #include #include #include +#include "enter_device.hh" #include "main.hh" #include "optional_task.hh" -using namespace std; -using namespace boost::locale; -using namespace std::chrono_literals; +const auto DEFAULT_TIMEOUT = + std::chrono::duration(100); +const auto MAX_RETRIES = 5; +const auto PYTHON_EXECUTABLE = "python3"; +const auto COMPARE_PROCESS_PATH = "/lib/security/howdy/compare.py"; + +#define S(msg) gettext(msg) /** * Inspect the status code returned by the compare process - * @param code The status code + * @param status The status code * @param conv_function The PAM conversation function * @return A PAM return code */ -int on_howdy_auth(int code, function conv_function) { +auto howdy_error(int status, + const std::function &conv_function) + -> int { // If the process has exited - if (!WIFEXITED(code)) { + if (WIFEXITED(status)) { // Get the status code returned - code = WEXITSTATUS(code); + status = WEXITSTATUS(status); - switch (code) { - // Status 10 means we couldn't find any face models - case 10: - conv_function(PAM_ERROR_MSG, - dgettext("pam", "There is no face model known")); + switch (status) { + case CompareError::NO_FACE_MODEL: + conv_function(PAM_ERROR_MSG, S("There is no face model known")); syslog(LOG_NOTICE, "Failure, no face model known"); break; - // Status 11 means we exceded the maximum retry count - case 11: - syslog(LOG_INFO, "Failure, timeout reached"); + case CompareError::TIMEOUT_REACHED: + syslog(LOG_ERR, "Failure, timeout reached"); break; - // Status 12 means we aborted - case 12: - syslog(LOG_INFO, "Failure, general abort"); + case CompareError::ABORT: + syslog(LOG_ERR, "Failure, general abort"); break; - // Status 13 means the image was too dark - case 13: - conv_function(PAM_ERROR_MSG, - dgettext("pam", "Face detection image too dark")); - syslog(LOG_INFO, "Failure, image too dark"); + case CompareError::TOO_DARK: + conv_function(PAM_ERROR_MSG, S("Face detection image too dark")); + syslog(LOG_ERR, "Failure, image too dark"); + break; + 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 conv_function) { } /** - * Format and send a message to PAM - * @param conv PAM conversation function - * @param type Type of PAM message - * @param message String to show the user - * @return Returns the conversation function return code + * Format the success message if the status is successful or log the error in + * the other case + * @param username Username + * @param status Status code + * @param config INI configuration + * @param conv_function PAM conversation function + * @return Returns the conversation function return code */ -int send_message(function - conv, - int type, const char *message) { - // No need to free this, it's allocated on the stack - const struct pam_message msg = {.msg_style = type, .msg = message}; - const struct pam_message *msgp = &msg; +auto howdy_status(char *username, int status, const INIReader &config, + const std::function &conv_function) + -> int { + if (status != EXIT_SUCCESS) { + return howdy_error(status, conv_function); + } - struct pam_response res_ = {}; - struct pam_response *resp_ = &res_; + if (!config.GetBoolean("core", "no_confirmation", true)) { + // Construct confirmation text from i18n string + std::string confirm_text(S("Identified face as {}")); + std::string identify_msg = + confirm_text.replace(confirm_text.find("{}"), 2, std::string(username)); + conv_function(PAM_TEXT_INFO, identify_msg.c_str()); + } - // Call the conversation function with the constructed arguments - return conv(1, &msgp, &resp_, nullptr); + syslog(LOG_INFO, "Login approved"); + + return PAM_SUCCESS; +} + +/** + * Check if Howdy should be enabled according to the configuration and the + * environment. + * @param config INI configuration + * @return Returns PAM_AUTHINFO_UNAVAIL if it shouldn't be enabled, + * PAM_SUCCESS otherwise + */ +auto check_enabled(const INIReader &config) -> int { + // Stop executing if Howdy has been disabled in the config + if (config.GetBoolean("core", "disabled", false)) { + syslog(LOG_INFO, "Skipped authentication, Howdy is disabled"); + return PAM_AUTHINFO_UNAVAIL; + } + + // Stop if we're in a remote shell and configured to exit + if (config.GetBoolean("core", "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(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 { + 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(reinterpret_cast(&conv)); + + if ((pam_res = pam_get_item(pamh, PAM_CONV, conv_ptr)) != PAM_SUCCESS) { syslog(LOG_ERR, "Failed to acquire conversation"); return pam_res; } // Wrap the PAM conversation function in our own, easier function - auto conv_function = - bind(send_message, conv->conv, placeholders::_1, placeholders::_2); + auto conv_function = [conv](int msg_type, const char *msg_str) { + const struct pam_message msg = {.msg_style = msg_type, .msg = msg_str}; + const struct pam_message *msgp = &msg; - // Error out if we could not ready the config file - if (reader.ParseError() < 0) { - syslog(LOG_ERR, "Failed to parse the configuration file"); - return PAM_SYSTEM_ERR; - } + struct pam_response res = {}; + struct pam_response *resp = &res; - // Stop executing if Howdy has been disabled in the config - if (reader.GetBoolean("core", "disabled", false)) { - syslog(LOG_INFO, "Skipped authentication, Howdy is disabled"); - return PAM_AUTHINFO_UNAVAIL; - } + return conv->conv(1, &msgp, &resp, conv->appdata_ptr); + }; - // Stop if we're in a remote shell and configured to exit - if (reader.GetBoolean("core", "ignore_ssh", true)) { - if (getenv("SSH_CONNECTION") != nullptr || - getenv("SSH_CLIENT") != nullptr || getenv("SSHD_OPTS") != nullptr) { - syslog(LOG_INFO, "Skipped authentication, SSH session detected"); - return PAM_AUTHINFO_UNAVAIL; - } - } - - // Try to detect the laptop lid state and stop if it's closed - if (reader.GetBoolean("core", "ignore_closed_lid", true)) { - glob_t glob_result{}; - - // Get any files containing lid state - int return_value = - glob("/proc/acpi/button/lid/*/state", 0, nullptr, &glob_result); - - // TODO: We ignore the result - if (return_value != 0) { - globfree(&glob_result); - } - - for (size_t i = 0; i < glob_result.gl_pathc; i++) { - ifstream file(string(glob_result.gl_pathv[i])); - string lid_state; - getline(file, lid_state, (char)file.eof()); - - if (lid_state.find("closed") != std::string::npos) { - globfree(&glob_result); - - syslog(LOG_INFO, "Skipped authentication, closed lid detected"); - return PAM_AUTHINFO_UNAVAIL; - } - } - - globfree(&glob_result); - } + // Initialize gettext + setlocale(LC_ALL, ""); + bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR); + textdomain(GETTEXT_PACKAGE); // If enabled, send a notice to the user that facial login is being attempted - if (reader.GetBoolean("core", "detection_notice", false)) { - if ((pam_res = conv_function( - PAM_TEXT_INFO, - dgettext("pam", "Attempting facial authentication"))) != + if (config.GetBoolean("core", "detection_notice", false)) { + if ((conv_function(PAM_TEXT_INFO, S("Attempting facial authentication"))) != PAM_SUCCESS) { syslog(LOG_ERR, "Failed to send detection notice"); } } // Get the username from PAM, needed to match correct face model - char *user_ptr = nullptr; - if ((pam_res = pam_get_user(pamh, (const char **)&user_ptr, nullptr)) != - PAM_SUCCESS) { + char *username = nullptr; + if ((pam_res = pam_get_user(pamh, const_cast(&username), + nullptr)) != PAM_SUCCESS) { syslog(LOG_ERR, "Failed to get username"); return pam_res; } - posix_spawn_file_actions_t file_actions; - posix_spawn_file_actions_init(&file_actions); - // We close standard descriptors for the child - posix_spawn_file_actions_addclose(&file_actions, STDOUT_FILENO); - posix_spawn_file_actions_addclose(&file_actions, STDERR_FILENO); - posix_spawn_file_actions_addclose(&file_actions, STDIN_FILENO); - - const char *const args[] = { - "/usr/bin/python3", "/lib/security/howdy/compare.py", user_ptr, nullptr}; + const char *const args[] = {PYTHON_EXECUTABLE, // NOLINT + COMPARE_PROCESS_PATH, username, nullptr}; pid_t child_pid; // Start the python subprocess - if (posix_spawnp(&child_pid, "/usr/bin/python3", &file_actions, nullptr, - (char *const *)args, nullptr) < 0) { - syslog(LOG_ERR, "Can't spawn the howdy process: %s", strerror(errno)); + if (posix_spawnp(&child_pid, PYTHON_EXECUTABLE, nullptr, nullptr, + const_cast(args), nullptr) != 0) { + syslog(LOG_ERR, "Can't spawn the howdy process: %s (%d)", strerror(errno), + errno); return PAM_SYSTEM_ERR; } - mutex m; - condition_variable cv; - Type confirmation_type; - // TODO: Find a clean way to do this - Type final_type; + // NOTE: We should replace mutex and condition_variable by atomic wait, but + // it's too recent (C++20) + std::mutex 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 child_task(packaged_task([&] { + optional_task child_task([&] { int status; wait(&status); { - unique_lock lk(m); - confirmation_type = Type::Howdy; + std::unique_lock 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> pass_task( - packaged_task()>([&] { - char *auth_tok_ptr = nullptr; - int pam_res = pam_get_authtok(pamh, PAM_AUTHTOK, - (const char **)&auth_tok_ptr, nullptr); - { - unique_lock lk(m); - confirmation_type = Type::Pam; - } - cv.notify_all(); - return tuple(pam_res, auth_tok_ptr); - })); + optional_task> pass_task([&] { + char *auth_tok_ptr = nullptr; + int pam_res = pam_get_authtok( + pamh, PAM_AUTHTOK, const_cast(&auth_tok_ptr), nullptr); + { + std::unique_lock lock(mutx); + if (confirmation_type == ConfirmationType::Unset) { + confirmation_type = ConfirmationType::Pam; + } + } + convar.notify_one(); - if (auth_tok) { + return std::tuple(pam_res, auth_tok_ptr); + }); + + // We ask for the password if the function requires it and if a workaround is + // set + if (auth_tok && workaround != Workaround::Off) { pass_task.activate(); } // Wait for the end either of the child or the password input { - unique_lock lk(m); - cv.wait(lk); - final_type = confirmation_type; + std::unique_lock 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; } diff --git a/howdy/src/pam/main.hh b/howdy/src/pam/main.hh index 6b75580..12f687d 100644 --- a/howdy/src/pam/main.hh +++ b/howdy/src/pam/main.hh @@ -1,36 +1,32 @@ #ifndef MAIN_H_ #define MAIN_H_ -#include #include -enum class Type { Howdy, Pam }; +enum class ConfirmationType { Unset, Howdy, Pam }; enum class Workaround { Off, Input, Native }; -inline bool operator==(const std::string &l, const Workaround &r) { - switch (r) { - case Workaround::Off: - return (l == "off"); - case Workaround::Input: - return (l == "input"); - case Workaround::Native: - return (l == "native"); - default: - return false; +// Exit status codes returned by the compare process +enum CompareError : int { + NO_FACE_MODEL = 10, + TIMEOUT_REACHED = 11, + ABORT = 12, + TOO_DARK = 13, + 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_ diff --git a/howdy/src/pam/meson.build b/howdy/src/pam/meson.build index add0595..be93919 100644 --- a/howdy/src/pam/meson.build +++ b/howdy/src/pam/meson.build @@ -1,9 +1,24 @@ project('pam_howdy', 'cpp', version: '0.1.0', default_options: ['cpp_std=c++14']) -inih = subproject('inih') -inih_cpp = inih.get_variable('INIReader_dep') - -libpam = meson.get_compiler('c').find_library('pam') -boost = dependency('boost', modules: ['locale']) +inih_cpp = dependency('INIReader', fallback: ['inih', 'INIReader_dep']) +libevdev = dependency('libevdev') +libpam = meson.get_compiler('cpp').find_library('pam') threads = dependency('threads') -shared_library('pam_howdy', 'main.cc', dependencies: [boost, libpam, inih_cpp, threads], install: true, install_dir: '/lib/security/') + +# Translations +subdir('po') + +shared_library( + 'pam_howdy', + 'main.cc', + 'enter_device.cc', + dependencies: [ + libpam, + inih_cpp, + threads, + libevdev, + ], + install: true, + install_dir: '/lib/security', + name_prefix: '' +) diff --git a/howdy/src/pam/optional_task.hh b/howdy/src/pam/optional_task.hh index 827c535..c6da379 100644 --- a/howdy/src/pam/optional_task.hh +++ b/howdy/src/pam/optional_task.hh @@ -6,64 +6,75 @@ #include #include +// A task executed only if activated. template class optional_task { - std::thread _thread; - std::packaged_task _task; - std::future _future; - std::atomic _spawned; - std::atomic _is_active; + std::thread thread; + std::packaged_task task; + std::future future; + bool spawned{false}; + bool is_active{false}; public: - optional_task(std::packaged_task); + explicit optional_task(std::function func); void activate(); - template std::future_status wait(std::chrono::duration); - T get(); - bool is_active(); - void stop(bool); + template + auto wait(std::chrono::duration dur) -> std::future_status; + auto get() -> T; + void stop(bool force); ~optional_task(); }; template -optional_task::optional_task(std::packaged_task t) - : _task(std::move(t)), _future(_task.get_future()) {} +optional_task::optional_task(std::function func) + : task(std::packaged_task(std::move(func))), future(task.get_future()) {} +// Create a new thread and launch the task on it. template void optional_task::activate() { - _thread = std::thread(std::move(_task)); - _spawned = true; - _is_active = true; + thread = std::thread(std::move(task)); + spawned = true; + is_active = true; } +// Wait for `dur` time and return a `future` status. template -template -std::future_status optional_task::wait(std::chrono::duration dur) { - return _future.wait_for(dur); +template +auto optional_task::wait(std::chrono::duration dur) + -> std::future_status { + return future.wait_for(dur); } -template T optional_task::get() { - assert(!_is_active && _spawned); - return _future.get(); +// Get the value. +// WARNING: The function hould be run only if the task has successfully been +// stopped. +template auto optional_task::get() -> T { + assert(!is_active && spawned); + return future.get(); } -template bool optional_task::is_active() { return _is_active; } - +// Stop the thread: +// - if `force` is `false`, by joining the thread. +// - if `force` is `true`, by cancelling the thread using `pthread_cancel`. +// WARNING: This function should be used with extreme caution when `force` is +// set to `true`. template void optional_task::stop(bool force) { - if (!(_is_active && _thread.joinable()) && _spawned) { - _is_active = false; + if (!(is_active && thread.joinable()) && spawned) { + is_active = false; return; } // We use pthread to cancel the thread if (force) { - auto native_hd = _thread.native_handle(); + auto native_hd = thread.native_handle(); pthread_cancel(native_hd); } - _thread.join(); - _is_active = false; + thread.join(); + is_active = false; } template optional_task::~optional_task() { - if (_is_active && _spawned) + if (is_active && spawned) { stop(false); + } } #endif // OPTIONAL_TASK_H_ diff --git a/howdy/src/pam/po/LINGUAS b/howdy/src/pam/po/LINGUAS new file mode 100644 index 0000000..e69de29 diff --git a/howdy/src/pam/po/POTFILES b/howdy/src/pam/po/POTFILES new file mode 100644 index 0000000..11b5c00 --- /dev/null +++ b/howdy/src/pam/po/POTFILES @@ -0,0 +1 @@ +main.cc \ No newline at end of file diff --git a/howdy/src/pam/po/meson.build b/howdy/src/pam/po/meson.build new file mode 100644 index 0000000..762f7a4 --- /dev/null +++ b/howdy/src/pam/po/meson.build @@ -0,0 +1,10 @@ +i18n = import('i18n') + +# define GETTEXT_PACKAGE and LOCALEDIR +gettext_package = '-DGETTEXT_PACKAGE="@0@"'.format(meson.project_name()) +localedir = '-DLOCALEDIR="@0@"'.format(get_option('prefix') / get_option('localedir')) +add_project_arguments(gettext_package, localedir, language: 'cpp') + +i18n.gettext(meson.project_name(), + args: [ '--directory=' + meson.source_root(), '--keyword=S:1' ] +) \ No newline at end of file diff --git a/howdy/src/pam/subprojects/inih.wrap b/howdy/src/pam/subprojects/inih.wrap index 8d02b50..5423393 100644 --- a/howdy/src/pam/subprojects/inih.wrap +++ b/howdy/src/pam/subprojects/inih.wrap @@ -1,3 +1,13 @@ -[wrap-git] -url = https://github.com/benhoyt/inih.git -revision = r52 +[wrap-file] +directory = inih-r53 +source_url = https://github.com/benhoyt/inih/archive/r53.tar.gz +source_filename = inih-r53.tar.gz +source_hash = 01b0366fdfdf6363efc070c2f856f1afa33e7a6546548bada5456ad94a516241 +patch_url = https://wrapdb.mesonbuild.com/v2/inih_r53-1/get_patch +patch_filename = inih-r53-1-wrap.zip +patch_hash = 9a53348e4ed9180a52aafc092fda080ddc70102c9fc55686990e461b22e6e1e7 + +[provide] +inih = inih_dep +inireader = inireader_dep + diff --git a/howdy/src/recorders/video_capture.py b/howdy/src/recorders/video_capture.py index 50032ad..2c55c57 100644 --- a/howdy/src/recorders/video_capture.py +++ b/howdy/src/recorders/video_capture.py @@ -37,7 +37,7 @@ class VideoCapture: print(_("Howdy could not find a camera device at the path specified in the config file.")) print(_("It is very likely that the path is not configured correctly, please edit the 'device_path' config value by running:")) print("\n\tsudo howdy config\n") - sys.exit(1) + sys.exit(14) # Create reader # The internal video recorder @@ -83,7 +83,7 @@ class VideoCapture: ret, frame = self.internal.read() if not ret: print(_("Failed to read camera specified in the 'device_path' config option, aborting")) - sys.exit(1) + sys.exit(14) try: # Convert from color to grayscale