New CLI and debian package structure

This commit is contained in:
boltgolt 2018-04-14 00:11:52 +02:00
commit e700de775a
33 changed files with 759 additions and 417 deletions

8
.gitignore vendored
View file

@ -101,4 +101,10 @@ ENV/
.mypy_cache/
# generated models
/models
/src/models
# build files
debian/howdy.substvars
debian/files
debian/debhelper-build-stamp
debian/howdy

25
.travis.yml Normal file
View file

@ -0,0 +1,25 @@
sudo: required
language: python
python:
- "3.4"
- "3.6"
install:
# Install build tools and ack-grep for checks
- sudo apt install devscripts dh-make ack-grep -y
script:
# Build the binary (.deb)
- debuild -i -us -uc -b
# Install the binary, also fireing the debian scripts
- sudo apt install ../*.deb -y
# Check if the username passthough works correctly with sudo
- 'howdy | ack-grep --passthru --color "current active user: travis"'
- 'sudo howdy | ack-grep --passthru --color "current active user: travis"'
# Remove howdy from the installation
- sudo apt purge howdy -y
notifications:
email:
on_success: never
on_failure: always

View file

@ -1,4 +1,4 @@
# Howdy for Ubuntu
# Howdy for Ubuntu [![](https://img.shields.io/travis/Boltgolt/howdy/master.svg)](https://travis-ci.org/Boltgolt/howdy) [![](https://img.shields.io/github/release/Boltgolt/howdy.svg?colorB=4c1)](https://github.com/Boltgolt/howdy/releases) [![](https://img.shields.io/github/issues-raw/Boltgolt/howdy/enhancement.svg?label=feature+requests&colorB=4c1)](https://github.com/Boltgolt/howdy/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement)
Windows Hello™ style authentication for Ubuntu. Use your built-in IR emitters and camera in combination with face recognition to prove who you are.
@ -6,21 +6,40 @@ Using the central authentication system in Linux (PAM), this works everywhere yo
### Installation
Run the installer by pasting (`ctrl+shift+V`) the following command into the terminal:
Run the installer by pasting (`ctrl+shift+V`) the following commands into the terminal one at a time:
```
wget -O /tmp/howdy_install.py https://raw.githubusercontent.com/Boltgolt/howdy/master/installer.py && sudo python3 /tmp/howdy_install.py
sudo add-apt-repository ppa:boltgolt/howdy
sudo apt update
sudo apt install howdy
```
This will guide you through the installation. When that's done run `sudo howdy USER add` and replace `USER` with your username 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.
**Note:** The build of dlib can hang on 100% for over a minute, give it time.
This will guide you through the installation. When that's done 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.
If you're curious you can run `sudo howdy config` to open the central config file and see the options Howdy has.
### Command line
The installer adds a `howdy` command to manage face models for the current user. Use `howdy help` to list the available options.
The installer adds a `howdy` command to manage face models for the current user. Use `howdy --help` or `man howdy` to list the available options.
Usage:
```
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 |
| `config` | Open the config file in gedit |
| `disable` | Disable or enable howdy |
| `list` | List all saved face models for an user |
| `remove` | Remove a specific model for an user |
| `test` | Test the camera and recognition methods |
### Troubleshooting
@ -28,14 +47,10 @@ Any python errors get logged directly into the console and should indicate what
If you encounter an error that hasn't been reported yet, don't be afraid to open a new issue.
### Uninstalling
There is an uninstaller available, run `sudo python3 /lib/security/howdy/uninstall.py` to remove Howdy from your system.
### A note on security
This script 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.
To minimize the chance of this script being compromised, it's recommend to leave this repo in /lib/security and to keep it read only.
To minimize the chance of this program being compromised, it's recommend to leave Howdy in /lib/security and to keep it read only.
DO NOT USE HOWDY AS THE SOLE AUTHENTICATION METHOD FOR YOUR SYSTEM.

1
autocomplete.sh → autocomplete/howdy Normal file → Executable file
View file

@ -1,3 +1,4 @@
#!/bin/bash
# Autocomplete file run in bash
# Will sugest arguments on tab

53
cli.py
View file

@ -1,53 +0,0 @@
#!/usr/bin/env python3
# CLI directly called by running the howdy command
# Import required modules
import sys
import os
# Check if if a command has been given and print help otherwise
if (len(sys.argv) < 2):
print("Howdy IR face recognition help")
import cli.help
sys.exit()
# The command given
cmd = sys.argv[1]
# Call the right files for commands that don't need root
if cmd == "help":
print("Howdy IR face recognition")
import cli.help
elif cmd == "test":
import cli.test
else:
# Check if the minimum of 3 arugemnts has been met and print help otherwise
if (len(sys.argv) < 3):
print("Howdy IR face recognition help")
import cli.help
sys.exit()
# Requre sudo for comamnds that need root rights to read the model files
if os.getenv("SUDO_USER") is None:
print("Please run this command with sudo")
sys.exit()
# Frome here on we require the second argument to be the username,
# switching the command to the 3rd
cmd = sys.argv[2]
if cmd == "list":
import cli.list
elif cmd == "add":
import cli.add
elif cmd == "remove":
import cli.remove
elif cmd == "clear":
import cli.clear
else:
# If the comand is invalid, check if the user hasn't swapped the username and command
if sys.argv[1] in ["list", "add", "remove", "clear"]:
print("Usage: howdy <user> <command>")
else:
print('Unknown command "' + cmd + '"')
import cli.help

View file

@ -1,17 +0,0 @@
# Prints a simple help page for the CLI
print("""
Usage:
howdy <user> <command> [argument]
Commands:
help Show this help page
list List all saved face models for the current user
add Add a new face model for the current user
remove [id] Remove a specific model
clear Remove all face models for the current user
test Test the camera and recognition methods
For support please visit
https://github.com/Boltgolt/howdy\
""")

25
debian/changelog vendored Normal file
View file

@ -0,0 +1,25 @@
howdy (2.1.0) xenial; urgency=medium
* First complete PPA release
* Reworked CLI
-- boltgolt <boltgolt@gmail.com> Fri, 13 Apr 2018 22:22:27 +0200
howdy (2.0.0-alpha+3) xenial; urgency=medium
* Fixed issue where dlib dependency failed to install on some installations
* Added preinst script for camera detection
-- boltgolt <boltgolt@gmail.com> Thu, 12 Apr 2018 21:42:42 +0000
howdy (2.0.0-alpha+2) xenial; urgency=medium
* Fixed build dependency issue
-- boltgolt <boltgolt@gmail.com> Sat, 07 Apr 2018 21:30:48 +0200
howdy (2.0.0-alpha+1) xenial; urgency=low
* Initial packaged release.
-- boltgolt <boltgolt@gmail.com> Wed, 04 Apr 2018 18:13:15 +0200

1
debian/compat vendored Normal file
View file

@ -0,0 +1 @@
10

15
debian/control vendored Normal file
View file

@ -0,0 +1,15 @@
Source: howdy
Section: misc
Priority: optional
Standards-Version: 3.9.7
Build-Depends: python, dh-python, devscripts, dh-make, debhelper
Maintainer: boltgolt <boltgolt@gmail.com>
Vcs-Git: https://github.com/Boltgolt/howdy
Package: howdy
Homepage: https://github.com/Boltgolt/howdy
Architecture: all
Depends: ${misc:Depends}, git, python3, python3-pip, python3-dev, python3-setuptools, build-essential, libpam-python, fswebcam, libopencv-dev, python-opencv, cmake
Description: Windows Hello style authentication for Ubuntu.
Use your built-in IR emitters and camera in combination with face recognition
to prove who you are.

21
debian/copyright vendored Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 boltgolt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

47
debian/howdy.1 vendored Normal file
View file

@ -0,0 +1,47 @@
.\" Please adjust this date whenever revising the manpage.
.TH HOWDY 1 "April 9, 2018" "Howdy help" "User Commands"
.SH NAME
howdy \- Windows Hello style authentication for Ubuntu
.SH DESCRIPTION
Howdy IR face recognition implements a PAM module to use your face as a authentication method.
.SS "Usage:"
.IP
howdy [\-U USER] [\-y] [\-h] command [argument]
.SS "Commands:"
.TP
add
Add a new face model for an user.
.TP
clear
Remove all face models for an user.
.TP
config
Open the config file in gedit.
.TP
disable
Disable or enable howdy.
.TP
list
List all saved face models for an user.
.TP
remove
Remove a specific model for an user.
.TP
clear
Remove all face models for an user.
.TP
test
Test the camera and recognition methods.
.SS "Optional arguments:"
.TP
\fB\-U\fR USER, \fB\-\-user\fR USER
Set the user account to use.
.TP
\fB\-y\fR
Skip all questions.
.TP
\fB\-h\fR, \fB\-\-help\fR
Show this help message and exit.
.PP
.SH AUTHOR
Howdy was written by boltgolt. For more information visit https://github.com/Boltgolt/howdy

1
debian/howdy.manpages vendored Normal file
View file

@ -0,0 +1 @@
debian/howdy.1

2
debian/install vendored Normal file
View file

@ -0,0 +1,2 @@
src/. lib/security/howdy
autocomplete/. usr/share/bash-completion/completions

204
debian/postinst vendored Executable file
View file

@ -0,0 +1,204 @@
#!/usr/bin/env python3
# Installation script to install howdy
# Executed after primary apt install
def col(id):
"""Add color escape sequences"""
if id == 1: return "\033[32m"
if id == 2: return "\033[33m"
if id == 3: return "\033[31m"
return "\033[0m"
# Import required modules
import subprocess
import time
import sys
import os
import re
import signal
import fileinput
import urllib.parse
# Don't run unless we need to configure the install
# Will also happen on upgrade but we will catch that later on
if "configure" not in sys.argv:
sys.exit(0)
def log(text):
"""Print a nicely formatted line to stdout"""
print("\n>>> " + col(1) + text + col(0) + "\n")
def handleStatus(status):
"""Abort if a command fails"""
if (status != 0):
print(col(3) + "Error while running last command" + col(0))
sys.exit(1)
# We're not in fresh configuration mode (probably an upgrade), so exit
if not os.path.exists("/tmp/howdy_picked_device"):
sys.exit(0)
# Open the temporary file containing the device ID
in_file = open("/tmp/howdy_picked_device", "r")
# Load it in, it should be a string
picked = in_file.read()
in_file.close()
# Remove the temporary file
subprocess.call(["rm /tmp/howdy_picked_device"], shell=True)
log("Upgrading pip to the latest version")
# Update pip
handleStatus(subprocess.call(["pip3 install --upgrade pip"], shell=True))
log("Cloning dlib")
# Clone the dlib git to /tmp, but only the last commit
handleStatus(subprocess.call(["git", "clone", "--depth", "1", "https://github.com/davisking/dlib.git", "/tmp/dlib_clone"]))
log("Building dlib")
# Start the build without GPU
handleStatus(subprocess.call(["cd /tmp/dlib_clone/; python3 setup.py install --yes USE_AVX_INSTRUCTIONS --no DLIB_USE_CUDA"], shell=True))
log("Cleaning up dlib")
# Remove the no longer needed git clone
handleStatus(subprocess.call(["rm", "-rf", "/tmp/dlib_clone"]))
print("Temporary dlib files removed")
log("Installing python dependencies")
# Install face_recognition though pip
handleStatus(subprocess.call(["pip3", "install", "--cache-dir", "/tmp/pip_howdy", "face_recognition_models==0.3.0", "Click>=6.0", "numpy", "Pillow"]))
log("Installing face_recognition")
# Install face_recognition though pip
handleStatus(subprocess.call(["pip3", "install", "--cache-dir", "/tmp/pip_howdy", "--no-deps", "face_recognition==1.2.2"]))
log("Configuring howdy")
# Manually change the camera id to the one picked
for line in fileinput.input(["/lib/security/howdy/config.ini"], inplace = 1):
print(line.replace("device_id = 1", "device_id = " + picked), end="")
# Secure the howdy folder
handleStatus(subprocess.call(["chmod 600 -R /lib/security/howdy/"], shell=True))
# Allow anyone to execute the python CLI
handleStatus(subprocess.call(["chmod 755 /lib/security/howdy"], shell=True))
handleStatus(subprocess.call(["chmod 744 /lib/security/howdy/cli.py"], shell=True))
handleStatus(subprocess.call(["chmod 744 -R /lib/security/howdy/cli"], shell=True))
print("Permissions set")
# Make the CLI executable as howdy
handleStatus(subprocess.call(["ln -s /lib/security/howdy/cli.py /usr/local/bin/howdy"], shell=True))
handleStatus(subprocess.call(["chmod +x /usr/local/bin/howdy"], shell=True))
print("Howdy command installed")
log("Adding howdy as PAM module")
# Will be filled with the actual output lines
outlines = []
# Will be fillled with lines that contain coloring
printlines = []
# Track if the new lines have been insterted yet
inserted = False
# Open the PAM config file
with open("/etc/pam.d/common-auth") as fp:
# Read the first line
line = fp.readline()
while line:
# Add the line to the output directly, we're not deleting anything
outlines.append(line)
# Print the comments in gray and don't insert into comments
if line[:1] == "#":
printlines.append("\033[37m" + line + "\033[0m")
else:
printlines.append(line)
# If it's not a comment and we haven't inserted yet
if not inserted:
# Set both the comment and the linking line
line_comment = "# Howdy IR face recognition\n"
line_link = "auth sufficient pam_python.so /lib/security/howdy/pam.py\n\n"
# Add them to the output without any markup
outlines.append(line_comment)
outlines.append(line_link)
# Make the print orange to make it clear what's being added
printlines.append("\033[33m" + line_comment + "\033[0m")
printlines.append("\033[33m" + line_link + "\033[0m")
# Mark as inserted
inserted = True
# Go to the next line
line = fp.readline()
# Print a file Header
print("\033[33m" + ">>> START OF /etc/pam.d/common-auth" + "\033[0m")
# Loop though all printing lines and use the enters from the file
for line in printlines:
print(line, end="")
# Print a footer
print("\033[33m" + ">>> END OF /etc/pam.d/common-auth" + "\033[0m" + "\n")
# Do not prompt for a yes if we're in no promt mode
if "HOWDY_NO_PROMPT" not in os.environ:
# Ask the user if this change is okay
print("Lines will be insterted in /etc/pam.d/common-auth as shown above")
ans = input("Apply this change? [y/N]: ")
# Abort the whole thing if it's not
if (ans.lower() != "y"):
print("Inerpeting as a \"NO\", aborting")
sys.exit(1)
print("Adding lines to PAM\n")
# Write to PAM
common_auth = open("/etc/pam.d/common-auth", "w")
common_auth.write("".join(outlines))
common_auth.close()
# From here onwards the installation is complete
# We want to gather more information about the types or IR camera's
# used though, and the following lines are data gathering
# No data is ever uploaded without permission
if "HOWDY_NO_PROMPT" not in os.environ:
# List all video devices
diag_out = "Video devices [IR=" + picked + "]\n"
diag_out += "```\n"
diag_out += subprocess.check_output(['ls /dev/ | grep video'], shell=True).decode("utf-8")
diag_out += "```\n"
# Get some info from the USB kernel listings
diag_out += "Lsusb output\n"
diag_out += "```\n"
diag_out += subprocess.check_output(['lsusb -vvvv | grep -i "Camera\|iFunction"'], shell=True).decode("utf-8")
diag_out += "```\n"
# Get camera information from video4linux
diag_out += "Udevadm\n"
diag_out += "```\n"
diag_out += subprocess.check_output(['udevadm info -r --query=all -n /dev/video' + picked + ' | grep -i "ID_BUS\|ID_MODEL_ID\|ID_VENDOR_ID\|ID_V4L_PRODUCT\|ID_MODEL"'], shell=True).decode("utf-8")
diag_out += "```"
# Print it all as a clickable link to a new github issue
print("https://github.com/Boltgolt/howdy-reports/issues/new?title=Post-installation%20camera%20information&body=" + urllib.parse.quote_plus(diag_out) + "\n")
# Let the user know what to do with the link
print("Installation complete.")
print(col(2) + "If you want to help the development, please use the link above to post some camera-related information to github!" + col(0))

94
debian/preinst vendored Executable file
View file

@ -0,0 +1,94 @@
#!/usr/bin/env python3
# Used to check cameras before commiting to install
# Executed before primary apt install of files
def col(id):
"""Add color escape sequences"""
if id == 1: return "\033[32m"
if id == 2: return "\033[33m"
if id == 3: return "\033[31m"
return "\033[0m"
import subprocess
import time
import sys
import os
import re
import signal
# Don't run if we're not trying to install fresh
if "install" not in sys.argv:
sys.exit(0)
# The default picked video device id
picked = -1
print(col(1) + "Starting IR camera check...\n" + col(0))
# If prompting has been disabled, skip camera check
if "HOWDY_NO_PROMPT" in os.environ:
print(col(2) + "AUTOMATED INSTALL, YOU WILL NOT BE ASKED FOR INPUT AND CHECKS WILL BE SKIPPED" + col(0))
# Write the default device to disk and exit
with open("/tmp/howdy_picked_device", "w") as out_file:
out_file.write("0")
sys.exit(0)
# Get all devices
devices = os.listdir("/dev")
# Loop though all devices
for dev in devices:
# Only use the video devices
if (dev[:5] == "video"):
time.sleep(.5)
# The full path to the device is the default name
device_name = "/dev/" + dev
# Get the udevadm details to try to get a better name
udevadm = subprocess.check_output(["udevadm info -r --query=all -n " + device_name], shell=True).decode("utf-8")
# Loop though udevadm to search for a better name
for line in udevadm.split("\n"):
# Match it and encase it in quotes
re_name = re.search('product.*=(.*)$', line, re.IGNORECASE)
if re_name:
device_name = '"' + re_name.group(1) + '"'
# Show what device we're using
print("Trying " + device_name)
# Let fswebcam keep the camera open in the background
sub = subprocess.Popen(["fswebcam -S 9999999999 -d /dev/" + dev + " /dev/null 2>/dev/null"], shell=True, preexec_fn=os.setsid)
try:
# Ask the user if this is the right one
print(col(2) + "One of your cameras should now be on." + col(0))
ans = input("Did your IR emitters turn on? [y/N]: ")
except KeyboardInterrupt:
# Kill fswebcam if the user aborts
os.killpg(os.getpgid(sub.pid), signal.SIGTERM)
raise
# The user has answered, kill fswebcam
os.killpg(os.getpgid(sub.pid), signal.SIGTERM)
# Set this camera as picked if the answer was yes, go to the next one if no
if ans.lower() == "y" or ans.lower() == "yes":
picked = dev[5:]
break
else:
print("Interpreting as a " + col(3) + "\"NO\"\n" + col(0))
# Abort if no camera was picked
if picked == -1:
print(col(3) + "No suitable IR camera found, aborting install." + col(0))
sys.exit(23)
# Write the result to disk so postinst can have a look at it
with open("/tmp/howdy_picked_device", "w") as out_file:
out_file.write(str(picked))
# Add a line break
print("")

47
debian/prerm vendored Executable file
View file

@ -0,0 +1,47 @@
#!/usr/bin/env python3
# Executed on deinstallation
# Completely remove howdy from the system
def col(id):
"""Add color escape sequences"""
if id == 1: return "\033[32m"
if id == 2: return "\033[33m"
if id == 3: return "\033[31m"
return "\033[0m"
# Import required modules
import subprocess
import sys
import os
# Only run when we actually want to remove
if "remove" not in sys.argv and "purge" not in sys.argv:
sys.exit(0)
# Don't try running this if it's already gome
if not os.path.exists("/lib/security/howdy/cli"):
sys.exit(0)
# Remove files and symlinks
try:
subprocess.call(["rm /usr/local/bin/howdy"], shell=True)
except e:
print("Can't remove executable")
try:
subprocess.call(["rm /usr/share/bash-completion/completions/howdy"], shell=True)
except e:
print("Can't remove autocompletion script")
try:
subprocess.call(["rm -rf /lib/security/howdy"], shell=True)
except e:
# This error is normal
pass
# Remove face_recognition and dlib
subprocess.call(["pip3 uninstall face_recognition face_recognition_models dlib -y --no-cache-dir"], shell=True)
# Print a tearbending message
print(col(2) + """
There are still lines in /etc/pam.d/common-auth that can't be removed automatically
Run "nano /etc/pam.d/common-auth" to remove them by hand\
""" + col(0))

11
debian/rules vendored Executable file
View file

@ -0,0 +1,11 @@
#!/usr/bin/make -f
DH_VERBOSE = 1
DPKG_EXPORT_BUILDFLAGS = 1
include /usr/share/dpkg/default.mk
%:
dh $@
binary:
dh binary

1
debian/source/format vendored Normal file
View file

@ -0,0 +1 @@
3.0 (native)

6
debian/source/options vendored Normal file
View file

@ -0,0 +1,6 @@
tar-ignore = ".git"
tar-ignore = "models"
tar-ignore = ".gitignore"
tar-ignore = "README.md"
tar-ignore = "LICENSE"
tar-ignore = ".travis.yml"

View file

@ -1,252 +0,0 @@
# Installation script to install howdy
# Runs completely independent of the others
# Import required modules
import subprocess
import time
import sys
import os
import re
import signal
import fileinput
import urllib.parse
def log(text):
"""Print a nicely formatted line to stdout"""
print("\n>>> \033[32m" + text + "\033[0m\n")
def handleStatus(status):
"""Abort if a command fails"""
if (status != 0):
print("\033[31mError while running last command\033[0m")
sys.exit()
# Check if we're running as root
user = os.getenv("SUDO_USER")
if user is None:
print("Please run this script as a sudo user")
sys.exit()
# Print some nice intro text
print("\n\033[33m HOWDY INSTALLER FOR UBUNTU\033[0m")
print(" Version 1, 2016/02/05\n")
# Let it sink in
time.sleep(.5)
log("Installing required apt packages")
# Install packages though apt
handleStatus(subprocess.call(["apt", "install", "-y", "git",
"python3-pip",
"python3-dev",
"python3-setuptools",
"build-essential",
"libpam-python",
"fswebcam",
"libopencv-dev",
"python-opencv",
"cmake"]))
# Update pip
handleStatus(subprocess.call(["pip3 install --upgrade pip"], shell=True))
log("Starting camera check")
# Get all devices
devices = os.listdir("/dev")
# The picked video device id
picked = False
# Loop though all devices
for dev in devices:
# Only use the video devices
if (dev[:5] == "video"):
time.sleep(.5)
# The full path to the device is the default name
device_name = "/dev/" + dev
# Get the udevadm details to try to get a better name
udevadm = subprocess.check_output(["udevadm info -r --query=all -n " + device_name], shell=True).decode("utf-8")
# Loop though udevadm to search for a better name
for line in udevadm.split("\n"):
# Match it and encase it in quotes
re_name = re.search('product.*=(.*)$', line, re.IGNORECASE)
if re_name:
device_name = '"' + re_name.group(1) + '"'
# Show what device we're using
print("Trying " + device_name)
# Let fswebcam keep the camera open in the background
sub = subprocess.Popen(["fswebcam -S 9999999999 -d /dev/" + dev + " /dev/null 2>/dev/null"], shell=True, preexec_fn=os.setsid)
try:
# Ask the user if this is the right one
print("\033[33mOne of your cameras should now be on.\033[0m")
ans = input("Did your IR emitters turn on? [y/N]: ")
except KeyboardInterrupt:
# Kill fswebcam if the user aborts
os.killpg(os.getpgid(sub.pid), signal.SIGTERM)
raise
# The user has answered, kill fswebcam
os.killpg(os.getpgid(sub.pid), signal.SIGTERM)
# Set this camera as picked if the answer was yes, go to the next one if no
if (ans.lower() == "y"):
picked = dev[5:]
break
else:
print("Inerpeting as a \"NO\"\n")
# Abort if no camera was picked
if (picked == False):
print("\033[31mNo suitable IR camera found\033[0m")
sys.exit()
log("Cloning dlib")
# Clone the git to /tmp
handleStatus(subprocess.call(["git", "clone", "https://github.com/davisking/dlib.git", "/tmp/dlib_clone"]))
log("Building dlib")
# Start the build without GPU
handleStatus(subprocess.call(["cd /tmp/dlib_clone/; python3 setup.py install --yes USE_AVX_INSTRUCTIONS --no DLIB_USE_CUDA"], shell=True))
log("Cleaning up dlib")
# Remove the no longer needed git clone
handleStatus(subprocess.call(["rm", "-rf", "/tmp/dlib_clone"]))
log("Installing face_recognition")
# Install face_recognition though pip
handleStatus(subprocess.call(["pip3", "install", "face_recognition"]))
log("Cloning howdy")
# Make sure /lib/security exists
if not os.path.exists("/lib/security"):
os.makedirs("/lib/security")
# Clone howdy into it
handleStatus(subprocess.call(["git", "clone", "https://github.com/Boltgolt/howdy.git", "/lib/security/howdy"]))
# Manually change the camera id to the one picked
for line in fileinput.input(["/lib/security/howdy/config.ini"], inplace = 1):
print(line.replace("device_id = 1", "device_id = " + picked), end="")
# Secure the howdy folder
handleStatus(subprocess.call(["chmod 600 -R /lib/security/howdy/"], shell=True))
# Make the CLI executable as howdy
handleStatus(subprocess.call(["ln -s /lib/security/howdy/cli.py /usr/bin/howdy"], shell=True))
handleStatus(subprocess.call(["chmod +x /usr/bin/howdy"], shell=True))
# Install the command autocomplete, don't error on failure
subprocess.call(["sudo cp /lib/security/howdy/autocomplete.sh /etc/bash_completion.d/howdy"], shell=True)
log("Adding howdy as PAM module")
# Will be filled with the actual output lines
outlines = []
# Will be fillled with lines that contain coloring
printlines = []
# Track if the new lines have been insterted yet
inserted = False
# Open the PAM config file
with open("/etc/pam.d/common-auth") as fp:
# Read the first line
line = fp.readline()
while line:
# Add the line to the output directly, we're not deleting anything
outlines.append(line)
# Print the comments in gray and don't insert into comments
if line[:1] == "#":
printlines.append("\033[37m" + line + "\033[0m")
else:
printlines.append(line)
# If it's not a comment and we haven't inserted yet
if not inserted:
# Set both the comment and the linking line
line_comment = "# Howdy IR face recognition\n"
line_link = "auth sufficient pam_python.so /lib/security/howdy/pam.py\n\n"
# Add them to the output without any markup
outlines.append(line_comment)
outlines.append(line_link)
# Make the print orange to make it clear what's being added
printlines.append("\033[33m" + line_comment + "\033[0m")
printlines.append("\033[33m" + line_link + "\033[0m")
# Mark as inserted
inserted = True
# Go to the next line
line = fp.readline()
# Print a file Header
print("\033[33m" + ">>> START OF /etc/pam.d/common-auth" + "\033[0m")
# Loop though all printing lines and use the enters from the file
for line in printlines:
print(line, end="")
# Print a footer
print("\033[33m" + ">>> END OF /etc/pam.d/common-auth" + "\033[0m" + "\n")
# Ask the user if this change is okay
print("Lines will be insterted in /etc/pam.d/common-auth as shown above")
ans = input("Apply this change? [y/N]: ")
# Abort the whole thing if it's not
if (ans.lower() != "y"):
print("Inerpeting as a \"NO\", aborting")
sys.exit()
print("Adding lines to PAM\n")
# Write to PAM
common_auth = open("/etc/pam.d/common-auth", "w")
common_auth.write("".join(outlines))
common_auth.close()
# From here onwards the installation is complete
# We want to gather more information about the types or IR camera's
# used though, and the following lines are data collection
# List all video devices
diag_out = "Video devices [IR=" + picked + "]\n"
diag_out += "```\n"
diag_out += subprocess.check_output(['ls /dev/ | grep video'], shell=True).decode("utf-8")
diag_out += "```\n"
# Get some info from the USB kernel listings
diag_out += "Lsusb output\n"
diag_out += "```\n"
diag_out += subprocess.check_output(['lsusb -vvvv | grep -i "Camera\|iFunction"'], shell=True).decode("utf-8")
diag_out += "```\n"
# Get camera information from video4linux
diag_out += "Udevadm\n"
diag_out += "```\n"
diag_out += subprocess.check_output(['udevadm info -r --query=all -n /dev/video' + picked + ' | grep -i "ID_BUS\|ID_MODEL_ID\|ID_VENDOR_ID\|ID_V4L_PRODUCT\|ID_MODEL"'], shell=True).decode("utf-8")
diag_out += "```"
# Print it all as a clickable link to a new github issue
print("https://github.com/Boltgolt/howdy-reports/issues/new?title=Post-installation%20camera%20information&body=" + urllib.parse.quote_plus(diag_out) + "\n")
# Let the user know what to do with the link
print("Installation complete.")
print("If you want to help the development, please use the link above to post some camera-related information to github")
# Remove the installer if it was downloaded to /tmp
if os.path.exists("/tmp/howdy_install.py"):
os.remove("/tmp/howdy_install.py")

98
src/cli.py Executable file
View file

@ -0,0 +1,98 @@
#!/usr/bin/env python3
# CLI directly called by running the howdy command
# Import required modules
import sys
import os
import subprocess
import getpass
import argparse
import builtins
# Try to get the original username (not "root") from shell
user = subprocess.check_output("echo $(logname 2>/dev/null || echo $SUDO_USER)", shell=True).decode("ascii").strip()
# If that fails, try to get the direct user
if user == "root" or user == "":
env_user = getpass.getuser().strip()
# If even that fails, error out
if env_user == "":
print("Could not determine user, please use the --user flag")
sys.exit(1)
else:
user = env_user
# Basic command setup
parser = argparse.ArgumentParser(description="Command line interface for Howdy face authentication.",
formatter_class=argparse.RawDescriptionHelpFormatter,
add_help=False,
prog="howdy",
epilog="For support please visit\nhttps://github.com/Boltgolt/howdy")
# Add an argument for the command
parser.add_argument("command",
help="The command option to execute, can be one of the following: add, clear, config, disable, list, remove or test.",
metavar="command",
choices=["add", "clear", "config", "disable", "list", "remove", "test"])
# Add an argument for the extra arguments of diable and remove
parser.add_argument("argument",
help="Either 0 (enable) or 1 (disable) for the disable command, or the model ID for the remove command.",
nargs="?")
# Add the user flag
parser.add_argument("-U", "--user",
default=user,
help="Set the user account to use.")
# Add the -y flag
parser.add_argument("-y",
help="Skip all questions.",
action="store_true")
# Overwrite the default help message so we can use a uppercase S
parser.add_argument("-h", "--help",
action="help",
default=argparse.SUPPRESS,
help="Show this help message and exit.")
# If we only have 1 argument we print the help text
if len(sys.argv) < 2:
print("current active user: " + user + "\n")
parser.print_help()
sys.exit(0)
# Parse all arguments above
args = parser.parse_args()
# Save the args and user as builtins which can be accessed by the imports
builtins.howdy_args = args
builtins.howdy_user = args.user
# Check if we have rootish rights
# This is this far down the file so running the command for help is always possible
if os.getenv("SUDO_USER") is None:
print("Please run this command with sudo")
sys.exit(1)
# Beond this point the user can't change anymore, if we still have root as user we need to abort
if args.user == "root":
print("Can't run howdy commands as root, please run this command with the --user flag")
sys.exit(1)
# Execute the right command
if args.command == "add":
import cli.add
elif args.command == "clear":
import cli.clear
elif args.command == "config":
import cli.config
elif args.command == "disable":
import cli.disable
elif args.command == "list":
import cli.list
elif args.command == "remove":
import cli.remove
elif args.command == "test":
import cli.test

View file

@ -8,6 +8,7 @@ import sys
import json
import cv2
import configparser
import builtins
# Try to import face_recognition and give a nice error if we can't
# Add should be the first point where import issues show up
@ -18,7 +19,7 @@ except ImportError as err:
print("\nCan't import the face_recognition module, check the output of")
print("pip3 show face_recognition")
sys.exit()
sys.exit(1)
# Get the absolute path to the current file
path = os.path.dirname(os.path.abspath(__file__))
@ -27,8 +28,7 @@ path = os.path.dirname(os.path.abspath(__file__))
config = configparser.ConfigParser()
config.read(path + "/../config.ini")
# The current user
user = sys.argv[1]
user = builtins.howdy_user
# The permanent file to store the encoded model in
enc_file = path + "/../models/" + user + ".dat"
# Known encodings
@ -46,11 +46,11 @@ except FileNotFoundError:
encodings = []
# Print a warning if too many encodings are being added
if len(encodings) > 2:
if len(encodings) > 3:
print("WARNING: Every additional model slows down the face recognition engine")
print("Press ctrl+C to cancel")
print("Press ctrl+C to cancel\n")
print("Adding face model for the user account " + user)
print("Adding face model for the user " + user)
# Set the default label
label = "Initial model"
@ -59,12 +59,16 @@ label = "Initial model"
if len(encodings) > 0:
label = "Model #" + str(len(encodings) + 1)
# Ask the user for a custom label
label_in = input("Enter a label for this new model [" + label + "]: ")
# Keep de default name if we can't ask questions
if builtins.howdy_args.y:
print("Using default label \"" + label + "\" because of -y flag")
else:
# Ask the user for a custom label
label_in = input("Enter a label for this new model [" + label + "]: ")
# Set the custom label (if any) and limit it to 24 characters
if label_in != "":
label = label_in[:24]
# Set the custom label (if any) and limit it to 24 characters
if label_in != "":
label = label_in[:24]
# Prepare the metadata for insertion
insert_model = {
@ -106,12 +110,12 @@ while frames < 60:
# If 0 faces are detected we can't continue
if len(enc) == 0:
print("No face detected, aborting")
sys.exit()
sys.exit(1)
# If more than 1 faces are detected we can't know wich one belongs to the user
if len(enc) > 1:
print("Multiple faces detected, aborting")
sys.exit()
sys.exit(1)
# Totally clean array that can be exported as JSON
clean_enc = []
@ -130,5 +134,6 @@ with open(enc_file, "w") as datafile:
json.dump(encodings, datafile)
# Give let the user know how it went
print("Scan complete")
print("\nAdded a new model to " + user)
print("""Scan complete
Added a new model to """ + user)

View file

@ -3,30 +3,33 @@
# Import required modules
import os
import sys
import builtins
# Get the full path to this file
path = os.path.dirname(os.path.abspath(__file__))
# Get the passed user
user = sys.argv[1]
user = builtins.howdy_user
# Check if the models folder is there
if not os.path.exists(path + "/../models"):
print("No models created yet, can't clear them if they don't exist")
sys.exit()
sys.exit(1)
# Check if the user has a models file to delete
if not os.path.isfile(path + "/../models/" + user + ".dat"):
print(user + " has no models or they have been cleared already")
sys.exit()
sys.exit(1)
# Double check with the user
print("This will clear all models for " + user)
ans = input("Do you want to continue [y/N]: ")
# Only ask the user if there's no -y flag
if not builtins.howdy_args.y:
# Double check with the user
print("This will clear all models for " + user)
ans = input("Do you want to continue [y/N]: ")
# Abort if they don't answer y or Y
if (ans.lower() != "y"):
print('\nInerpeting as a "NO"')
sys.exit()
# Abort if they don't answer y or Y
if (ans.lower() != "y"):
print('\nInerpeting as a "NO"')
sys.exit(1)
# Delete otherwise
os.remove(path + "/../models/" + user + ".dat")

15
src/cli/config.py Normal file
View file

@ -0,0 +1,15 @@
# Open the config file in gedit
# Import required modules
import os
import time
import subprocess
# Let the user know what we're doing
print("Opening condig.ini in gedit")
# Open gedit as a subprocess and fork it
subprocess.Popen(["gedit", os.path.dirname(os.path.realpath(__file__)) + "/../config.ini"],
cwd="/",
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)

41
src/cli/disable.py Normal file
View file

@ -0,0 +1,41 @@
# Set the disable flag
# Import required modules
import sys
import os
import json
import builtins
import fileinput
import configparser
# Get the absolute filepath
config_path = os.path.dirname(os.path.abspath(__file__)) + "/../config.ini"
# Read config from disk
config = configparser.ConfigParser()
config.read(config_path)
# Check if enough arguments have been passed
if builtins.howdy_args.argument == None:
print("Please add a 0 (enable) or a 1 (disable) as an argument")
sys.exit(1)
# Translate the argument to the right string
if builtins.howdy_args.argument == "1":
out_value = "true"
elif builtins.howdy_args.argument == "0":
out_value = "false"
else:
# Of it's not a 0 or a 1, it's invalid
print("Please only use a 0 (enable) or a 1 (disable) as an argument")
sys.exit(1)
# Loop though the config file and only replace the line containing the disable config
for line in fileinput.input([config_path], inplace=1):
print(line.replace("disabled = " + config.get("core", "disabled"), "disabled = " + out_value), end="")
# Print what we just did
if builtins.howdy_args.argument == "1":
print("Howdy has been disabled")
else:
print("Howdy has been enabled")

View file

@ -5,10 +5,11 @@ import sys
import os
import json
import time
import builtins
# Get the absolute path and the username
path = os.path.dirname(os.path.realpath(__file__)) + "/.."
user = sys.argv[1]
user = builtins.howdy_user
# Check if the models file has been created yet
if not os.path.exists(path + "/models"):

View file

@ -4,22 +4,23 @@
import sys
import os
import json
import builtins
# Get the absolute path and the username
path = os.path.dirname(os.path.realpath(__file__)) + "/.."
user = sys.argv[1]
user = builtins.howdy_user
# Check if enough arguments have been passed
if len(sys.argv) == 3:
print("Please add the ID of the model to remove as an argument")
if builtins.howdy_args.argument == None:
print("Please add the ID of the model you want to remove as an argument")
print("You can find the IDs by running:")
print("\n\thowdy " + user + " list\n")
print("\n\thowdy list\n")
sys.exit(1)
# Check if the models file has been created yet
if not os.path.exists(path + "/models"):
print("Face models have not been initialized yet, please run:")
print("\n\thowdy " + user + " add\n")
print("\n\thowdy add\n")
sys.exit(1)
# Path to the models file
@ -30,7 +31,7 @@ try:
encodings = json.load(open(enc_file))
except FileNotFoundError:
print("No face model known for the user " + user + ", please run:")
print("\n\thowdy " + user + " add\n")
print("\n\thowdy add\n")
sys.exit(1)
# Tracks if a encoding with that id has been found
@ -38,24 +39,28 @@ found = False
# Loop though all encodings and check if they match the argument
for enc in encodings:
if str(enc["id"]) == sys.argv[3]:
# Double check with the user
print('This will remove the model called "' + enc["label"] + '" for ' + user)
ans = input("Do you want to continue [y/N]: ")
if str(enc["id"]) == builtins.howdy_args.argument:
# Only ask the user if there's no -y flag
if not builtins.howdy_args.y:
# Double check with the user
print('This will remove the model called "' + enc["label"] + '" for ' + user)
ans = input("Do you want to continue [y/N]: ")
# Abort if the answer isn't yes
if (ans.lower() != "y"):
print('\nInerpeting as a "NO"')
sys.exit()
# Abort if the answer isn't yes
if (ans.lower() != "y"):
print('\nInerpeting as a "NO"')
sys.exit()
# Add a padding empty line
print()
# Mark as found and print an enter
found = True
print()
break
# Abort if no matching id was found
if not found:
print("No model with ID " + sys.argv[3] + " exists for " + user)
print("No model with ID " + builtins.howdy_args.argument + " exists for " + user)
sys.exit()
# Remove the entire file if this encoding is the only one
@ -68,11 +73,11 @@ else:
# Loop though all encodin and only add thos that don't need to be removed
for enc in encodings:
if str(enc["id"]) != sys.argv[3]:
if str(enc["id"]) != builtins.howdy_args.argument:
new_encodings.append(enc)
# Save this new set to disk
# Save this new set to disk
with open(enc_file, "w") as datafile:
json.dump(new_encodings, datafile)
print("Removed model " + sys.argv[3])
print("Removed model " + builtins.howdy_args.argument)

View file

@ -1,3 +1,5 @@
# Howdy config file
[core]
# Do not print anything when a face verification succeeds
no_confirmation = false
@ -11,6 +13,10 @@ suppress_unknown = false
# Expirimental, can behave incorrectly on some systems
dismiss_lockscreen = false
# Disable howdy in the PAM
# The howdy command will still function
disabled = false
[video]
# The certainty of the detected face belonging to the user of the account
# On a scale from 1 to 10, values above 5 are not recommended

View file

@ -15,6 +15,10 @@ config.read(os.path.dirname(os.path.abspath(__file__)) + "/config.ini")
def doAuth(pamh):
"""Start authentication in a seperate process"""
# Abort is Howdy is disabled
if config.get("core", "disabled") == "true":
sys.exit(0)
# Run compare as python3 subprocess to circumvent python version and import issues
status = subprocess.call(["python3", os.path.dirname(os.path.abspath(__file__)) + "/compare.py", pamh.get_user()])

View file

@ -1,36 +0,0 @@
# Completely remove howdy from the system
# Import required modules
import subprocess
import sys
import os
# Check if we're running as root
user = os.getenv("SUDO_USER")
if user is None:
print("Please run the uninstaller as a sudo user")
sys.exit()
# Double check with the user for the last time
print("This will remove Howdy and all models generated with it")
ans = input("Do you want to continue? [y/N]: ")
# Abort if they don't say yes
if (ans.lower() != "y"):
sys.exit()
# Remove files and symlinks
subprocess.call(["rm -rf /lib/security/howdy/"], shell=True)
subprocess.call(["rm /usr/bin/howdy"], shell=True)
subprocess.call(["rm /etc/bash_completion.d/howdy"], shell=True)
# Remove face_recognition and dlib
subprocess.call(["pip3 uninstall face_recognition dlib -y"], shell=True)
# Print a tearbending message
print("""
Howdy has been uninstalled :'(
There are still lines in /etc/pam.d/common-auth that can't be removed automatically
Run "nano /etc/pam.d/common-auth" to remove them by hand\
""")