diff --git a/.gitignore b/.gitignore index 112d262..58f2dae 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..bbd8344 --- /dev/null +++ b/.travis.yml @@ -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 diff --git a/README.md b/README.md index 3645b24..bf29b42 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/autocomplete.sh b/autocomplete/howdy old mode 100644 new mode 100755 similarity index 96% rename from autocomplete.sh rename to autocomplete/howdy index 8c16937..40ce2a2 --- a/autocomplete.sh +++ b/autocomplete/howdy @@ -1,3 +1,4 @@ +#!/bin/bash # Autocomplete file run in bash # Will sugest arguments on tab diff --git a/cli.py b/cli.py deleted file mode 100755 index 8e9e6ab..0000000 --- a/cli.py +++ /dev/null @@ -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 ") - else: - print('Unknown command "' + cmd + '"') - import cli.help diff --git a/cli/help.py b/cli/help.py deleted file mode 100644 index 2bbd879..0000000 --- a/cli/help.py +++ /dev/null @@ -1,17 +0,0 @@ -# Prints a simple help page for the CLI - -print(""" -Usage: - howdy [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\ -""") diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..ca45c85 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,25 @@ +howdy (2.1.0) xenial; urgency=medium + + * First complete PPA release + * Reworked CLI + + -- boltgolt 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 Thu, 12 Apr 2018 21:42:42 +0000 + +howdy (2.0.0-alpha+2) xenial; urgency=medium + + * Fixed build dependency issue + + -- boltgolt Sat, 07 Apr 2018 21:30:48 +0200 + +howdy (2.0.0-alpha+1) xenial; urgency=low + + * Initial packaged release. + + -- boltgolt Wed, 04 Apr 2018 18:13:15 +0200 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..f599e28 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +10 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..6668311 --- /dev/null +++ b/debian/control @@ -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 +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. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..54f078c --- /dev/null +++ b/debian/copyright @@ -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. diff --git a/debian/howdy.1 b/debian/howdy.1 new file mode 100644 index 0000000..7254e43 --- /dev/null +++ b/debian/howdy.1 @@ -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 diff --git a/debian/howdy.manpages b/debian/howdy.manpages new file mode 100644 index 0000000..2578acd --- /dev/null +++ b/debian/howdy.manpages @@ -0,0 +1 @@ +debian/howdy.1 diff --git a/debian/install b/debian/install new file mode 100644 index 0000000..c35719b --- /dev/null +++ b/debian/install @@ -0,0 +1,2 @@ +src/. lib/security/howdy +autocomplete/. usr/share/bash-completion/completions diff --git a/debian/postinst b/debian/postinst new file mode 100755 index 0000000..efe76a1 --- /dev/null +++ b/debian/postinst @@ -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)) diff --git a/debian/preinst b/debian/preinst new file mode 100755 index 0000000..02960a8 --- /dev/null +++ b/debian/preinst @@ -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("") diff --git a/debian/prerm b/debian/prerm new file mode 100755 index 0000000..542a63c --- /dev/null +++ b/debian/prerm @@ -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)) diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..6d3bdcb --- /dev/null +++ b/debian/rules @@ -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 diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/debian/source/options b/debian/source/options new file mode 100644 index 0000000..593ee7b --- /dev/null +++ b/debian/source/options @@ -0,0 +1,6 @@ +tar-ignore = ".git" +tar-ignore = "models" +tar-ignore = ".gitignore" +tar-ignore = "README.md" +tar-ignore = "LICENSE" +tar-ignore = ".travis.yml" diff --git a/installer.py b/installer.py deleted file mode 100644 index 5b4374c..0000000 --- a/installer.py +++ /dev/null @@ -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") diff --git a/src/cli.py b/src/cli.py new file mode 100755 index 0000000..8ec350a --- /dev/null +++ b/src/cli.py @@ -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 diff --git a/cli/__init__.py b/src/cli/__init__.py similarity index 100% rename from cli/__init__.py rename to src/cli/__init__.py diff --git a/cli/add.py b/src/cli/add.py similarity index 82% rename from cli/add.py rename to src/cli/add.py index 427ce4d..c9f0594 100644 --- a/cli/add.py +++ b/src/cli/add.py @@ -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) diff --git a/cli/clear.py b/src/cli/clear.py similarity index 62% rename from cli/clear.py rename to src/cli/clear.py index 4cd56c2..817de48 100644 --- a/cli/clear.py +++ b/src/cli/clear.py @@ -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") diff --git a/src/cli/config.py b/src/cli/config.py new file mode 100644 index 0000000..d0471a5 --- /dev/null +++ b/src/cli/config.py @@ -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) diff --git a/src/cli/disable.py b/src/cli/disable.py new file mode 100644 index 0000000..2c2b03b --- /dev/null +++ b/src/cli/disable.py @@ -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") diff --git a/cli/list.py b/src/cli/list.py similarity index 96% rename from cli/list.py rename to src/cli/list.py index ff37b10..061bf49 100644 --- a/cli/list.py +++ b/src/cli/list.py @@ -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"): diff --git a/cli/remove.py b/src/cli/remove.py similarity index 62% rename from cli/remove.py rename to src/cli/remove.py index 4322839..deef825 100644 --- a/cli/remove.py +++ b/src/cli/remove.py @@ -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) diff --git a/cli/test.py b/src/cli/test.py similarity index 100% rename from cli/test.py rename to src/cli/test.py diff --git a/compare.py b/src/compare.py similarity index 100% rename from compare.py rename to src/compare.py diff --git a/config.ini b/src/config.ini similarity index 91% rename from config.ini rename to src/config.ini index 3076f96..213890f 100644 --- a/config.ini +++ b/src/config.ini @@ -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 diff --git a/pam.py b/src/pam.py similarity index 96% rename from pam.py rename to src/pam.py index 8c4eabc..13e571b 100644 --- a/pam.py +++ b/src/pam.py @@ -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()]) diff --git a/uninstall.py b/uninstall.py deleted file mode 100644 index 8cc56a1..0000000 --- a/uninstall.py +++ /dev/null @@ -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\ -""")