Merge branch 'beta' of github.com:boltgolt/howdy into beta
This commit is contained in:
commit
373f002f96
27 changed files with 684 additions and 417 deletions
25
.github/workflows/check.yml
vendored
Normal file
25
.github/workflows/check.yml
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
name: check
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Install required libraries
|
||||||
|
run: >
|
||||||
|
sudo apt-get update && sudo apt-get install -y
|
||||||
|
python3 python3-pip python3-setuptools python3-wheel ninja-build meson
|
||||||
|
cmake make build-essential clang-tidy
|
||||||
|
libpam0g-dev libinih-dev libevdev-dev
|
||||||
|
python3-dev libopencv-dev
|
||||||
|
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
meson setup build howdy/src/pam
|
||||||
|
ninja -C build
|
||||||
|
|
||||||
|
- name: Check source code
|
||||||
|
run: |
|
||||||
|
ninja clang-tidy -C build
|
||||||
14
README.md
14
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.
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
3
howdy/archlinux/howdy/.gitignore
vendored
3
howdy/archlinux/howdy/.gitignore
vendored
|
|
@ -4,4 +4,5 @@ src
|
||||||
*.zip
|
*.zip
|
||||||
*.tar.xz
|
*.tar.xz
|
||||||
*.patch
|
*.patch
|
||||||
*.dat.bz2
|
*.dat.bz2
|
||||||
|
.SRCINFO
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ depends=(
|
||||||
'python3'
|
'python3'
|
||||||
'python-dlib'
|
'python-dlib'
|
||||||
'python-numpy'
|
'python-numpy'
|
||||||
|
'python-opencv'
|
||||||
)
|
)
|
||||||
makedepends=(
|
makedepends=(
|
||||||
'cmake'
|
'cmake'
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
7
howdy/src/pam/.clang-tidy
Normal file
7
howdy/src/pam/.clang-tidy
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
Checks: 'clang-diagnostic-*,clang-analyser-*,-clang-diagnostic-unused-command-line-argument,google-*,bugprone-*,modernize-*,performance-*,portability-*,readability-*,-bugprone-easily-swappable-*,-readability-magic-numbers,-google-readability-todo'
|
||||||
|
WarningsAsErrors: 'clang-diagnostic-*,clang-analyser-*,-clang-diagnostic-unused-command-line-argument,google-*,bugprone-*,modernize-*,performance-*,portability-*,readability-*,-bugprone-easily-swappable-*,-readability-magic-numbers,-google-readability-todo'
|
||||||
|
CheckOptions:
|
||||||
|
- key: readability-function-cognitive-complexity.Threshold
|
||||||
|
value: '50'
|
||||||
|
|
||||||
|
# vim:syntax=yaml
|
||||||
2
howdy/src/pam/.gitignore
vendored
2
howdy/src/pam/.gitignore
vendored
|
|
@ -1 +1 @@
|
||||||
subprojects/*/
|
subprojects/inih/
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
|
||||||
48
howdy/src/pam/enter_device.cc
Normal file
48
howdy/src/pam/enter_device.cc
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
#include "enter_device.hh"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <memory>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
EnterDevice::EnterDevice()
|
||||||
|
: raw_device(libevdev_new(), &libevdev_free),
|
||||||
|
raw_uinput_device(nullptr, &libevdev_uinput_destroy) {
|
||||||
|
auto *dev_ptr = raw_device.get();
|
||||||
|
|
||||||
|
libevdev_set_name(dev_ptr, "enter device");
|
||||||
|
libevdev_enable_event_type(dev_ptr, EV_KEY);
|
||||||
|
libevdev_enable_event_code(dev_ptr, EV_KEY, KEY_ENTER, nullptr);
|
||||||
|
|
||||||
|
int err;
|
||||||
|
struct libevdev_uinput *uinput_dev_ptr;
|
||||||
|
if ((err = libevdev_uinput_create_from_device(
|
||||||
|
dev_ptr, LIBEVDEV_UINPUT_OPEN_MANAGED, &uinput_dev_ptr)) != 0) {
|
||||||
|
throw std::runtime_error(std::string("Failed to create device: ") +
|
||||||
|
strerror(-err));
|
||||||
|
}
|
||||||
|
|
||||||
|
raw_uinput_device.reset(uinput_dev_ptr);
|
||||||
|
};
|
||||||
|
|
||||||
|
void EnterDevice::send_enter_press() const {
|
||||||
|
auto *uinput_dev_ptr = raw_uinput_device.get();
|
||||||
|
|
||||||
|
int err;
|
||||||
|
if ((err = libevdev_uinput_write_event(uinput_dev_ptr, EV_KEY, KEY_ENTER,
|
||||||
|
1)) != 0) {
|
||||||
|
throw std::runtime_error(std::string("Failed to write event: ") +
|
||||||
|
strerror(-err));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((err = libevdev_uinput_write_event(uinput_dev_ptr, EV_KEY, KEY_ENTER,
|
||||||
|
0)) != 0) {
|
||||||
|
throw std::runtime_error(std::string("Failed to write event: ") +
|
||||||
|
strerror(-err));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((err = libevdev_uinput_write_event(uinput_dev_ptr, EV_SYN, SYN_REPORT,
|
||||||
|
0)) != 0) {
|
||||||
|
throw std::runtime_error(std::string("Failed to write event: ") +
|
||||||
|
strerror(-err));
|
||||||
|
};
|
||||||
|
}
|
||||||
19
howdy/src/pam/enter_device.hh
Normal file
19
howdy/src/pam/enter_device.hh
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
#ifndef ENTER_DEVICE_H_
|
||||||
|
#define ENTER_DEVICE_H_
|
||||||
|
|
||||||
|
#include <libevdev/libevdev-uinput.h>
|
||||||
|
#include <libevdev/libevdev.h>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
class EnterDevice {
|
||||||
|
std::unique_ptr<struct libevdev, decltype(&libevdev_free)> raw_device;
|
||||||
|
std::unique_ptr<struct libevdev_uinput, decltype(&libevdev_uinput_destroy)>
|
||||||
|
raw_uinput_device;
|
||||||
|
|
||||||
|
public:
|
||||||
|
EnterDevice();
|
||||||
|
void send_enter_press() const;
|
||||||
|
~EnterDevice() = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // ENTER_DEVICE_H
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
#include <cerrno>
|
#include <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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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_
|
||||||
|
|
|
||||||
|
|
@ -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: ''
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
0
howdy/src/pam/po/LINGUAS
Normal file
1
howdy/src/pam/po/POTFILES
Normal file
1
howdy/src/pam/po/POTFILES
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
main.cc
|
||||||
10
howdy/src/pam/po/meson.build
Normal file
10
howdy/src/pam/po/meson.build
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
i18n = import('i18n')
|
||||||
|
|
||||||
|
# define GETTEXT_PACKAGE and LOCALEDIR
|
||||||
|
gettext_package = '-DGETTEXT_PACKAGE="@0@"'.format(meson.project_name())
|
||||||
|
localedir = '-DLOCALEDIR="@0@"'.format(get_option('prefix') / get_option('localedir'))
|
||||||
|
add_project_arguments(gettext_package, localedir, language: 'cpp')
|
||||||
|
|
||||||
|
i18n.gettext(meson.project_name(),
|
||||||
|
args: [ '--directory=' + meson.source_root(), '--keyword=S:1' ]
|
||||||
|
)
|
||||||
|
|
@ -1,3 +1,13 @@
|
||||||
[wrap-git]
|
[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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue