diff --git a/README.md b/README.md index 41f82af..a6fda35 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,41 @@ # Howdy for Ubuntu -Windows Hello™ style authentication for Ubuntu. Use your build in IR emitters and camera in combination with face recognition to prove who you are. +Windows Hello™ style authentication for Ubuntu. Use your built-in IR emitters and camera in combination with face recognition to prove who you are. + +Using the central authentication system in Linux (PAM), this works everywhere you would otherwise need your password: Login, lock screen, sudo, su, etc. ### Installation -First we need to install pam-python, fswebcam and OpenCV from the Ubuntu repositories: +Run the installer by pasting (`ctrl+shift+V`) the following command into the terminal: ``` -sudo apt install libpam-python fswebcam libopencv-dev python-opencv +wget -O /tmp/howdy_install.py https://raw.githubusercontent.com/Boltgolt/howdy/master/installer.py && sudo python3 /tmp/howdy_install.py ``` -After that, install the face_recognition python module. There's an excellent step by step guide on how to do this on [its github page](https://github.com/ageitgey/face_recognition#installation). - -In the root of your cloned repo is a file called `config.ini`. The `device_id` variable in this file is important, make sure it is the IR camera and not your normal webcam. - -Now it's time to let Howdy learn your face. The learn.py script will make 3 models of your face and store them as an encoded set in the `models` folder. To run the script, open a terminal, navigate to this repository and run: - -``` -python3 learn.py -``` - -The script should guide you through the process. - -Finally we need to tell PAM that there's a new module installed. Open `/etc/pam.d/common-auth` as root (`sudo nano /etc/pam.d/common-auth`) and add the following line to the top of the file: - -``` -auth sufficient pam_python.so /path/to/pam.py -``` - -Replace the final argument with the full path to pam.py in this repository. The `sufficient` control tells PAM that Howdy is enough to authenticate the user, but if it fails we can fall back on more traditional methods. +This will guide you through the installation. When that's done run `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. + +### Command line + +The installer adds a `howdy` command to manage face models for the current user. Use `howdy help` to list the available options. + ### Troubleshooting -Any errors in the script itself 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. + +### 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 store this repo in `/etc/pam.d` and to make it read only. +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. -DO NOT USE THIS SCRIPT AS THE SOLE AUTHENTICATION METHOD FOR YOUR SYSTEM. +DO NOT USE HOWDY AS THE SOLE AUTHENTICATION METHOD FOR YOUR SYSTEM. diff --git a/autocomplete.sh b/autocomplete.sh new file mode 100644 index 0000000..8c16937 --- /dev/null +++ b/autocomplete.sh @@ -0,0 +1,15 @@ +# Autocomplete file run in bash +# Will sugest arguments on tab + +_howdy() { + local cur prev opts + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + opts="help list add remove clear" + + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 +} + +complete -F _howdy howdy diff --git a/cli.py b/cli.py new file mode 100755 index 0000000..e820426 --- /dev/null +++ b/cli.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + +# CLI directly called by running the howdy command + +# Import required modules +import sys +import os + +# 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() + +# The command given +cmd = sys.argv[2] + +# Requre sudo for comamnds that need root rights to read the model files +if cmd in ["list", "add", "remove", "clear"] and os.getenv("SUDO_USER") is None: + print("Please run this command with sudo") + sys.exit() + +# Call the right files for the given command +if cmd == "list": + import cli.list +elif cmd == "help": + print("Howdy IR face recognition") + import cli.help +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", "help"]: + print("Usage: howdy ") + else: + print('Unknown command "' + cmd + '"') + import cli.help diff --git a/cli/__init__.py b/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/learn.py b/cli/add.py similarity index 62% rename from learn.py rename to cli/add.py index 9864095..9b2e6d9 100644 --- a/learn.py +++ b/cli/add.py @@ -1,24 +1,35 @@ # Save the face of the user in encoded form # Import required modules -import face_recognition import subprocess import time import os import sys import json - -# Import config and extra functions import configparser -import utils + + +# 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 +try: + import face_recognition +except ImportError as err: + print(err) + + print("\nCan't import the face_recognition module, check the output of") + print("pip3 show face_recognition") + sys.exit() + +# Get the absolute path to the current file +path = os.path.dirname(os.path.abspath(__file__)) # Read config from disk config = configparser.ConfigParser() -config.read(os.path.dirname(os.path.abspath(__file__)) + "/config.ini") +config.read(path + "/../config.ini") def captureFrame(delay): """Capture and encode 1 frame of video""" - global encodings + global insert_model # Call fswebcam to save a frame to /tmp with a set delay exit_code = subprocess.call(["fswebcam", "-S", str(delay), "--no-banner", "-d", "/dev/video" + str(config.get("video", "device_id")), tmp_file]) @@ -54,36 +65,53 @@ def captureFrame(delay): for point in enc[0]: clean_enc.append(point) - encodings.append(clean_enc) + insert_model["data"].append(clean_enc) # The current user -user = os.environ.get("USER") +user = sys.argv[1] # The name of the tmp frame file to user tmp_file = "/tmp/howdy_" + user + ".jpg" # The permanent file to store the encoded model in -enc_file = "./models/" + user + ".dat" +enc_file = path + "/../models/" + user + ".dat" # Known encodings encodings = [] # Make the ./models folder if it doesn't already exist -if not os.path.exists("models"): +if not os.path.exists(path + "/../models"): print("No face model folder found, creating one") - os.makedirs("models") + os.makedirs(path + "/../models") # To try read a premade encodings file if it exists try: encodings = json.load(open(enc_file)) except FileNotFoundError: - encodings = False - -# If a file does exist, ask the user what needs to be done -if encodings != False: - encodings = utils.print_menu(encodings) -else: encodings = [] -print("\nLearning face for the user account " + user) -print("Please look straight into the camera for 5 seconds") +print("Adding face model for the user account " + user) + +# Set the default label +label = "Initial model" + +# If models already exist, set that default label +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 + "]: ") + +# 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 = { + "time": int(time.time()), + "label": label, + "id": len(encodings), + "data": [] +} + +print("\nPlease look straight into the camera for 5 seconds") # Give the user time to read time.sleep(2) @@ -93,6 +121,9 @@ for delay in [30, 6, 0]: time.sleep(.3) captureFrame(delay) +# Insert full object into the list +encodings.append(insert_model) + # Save the new encodings to disk with open(enc_file, "w") as datafile: json.dump(encodings, datafile) diff --git a/cli/clear.py b/cli/clear.py new file mode 100644 index 0000000..4cd56c2 --- /dev/null +++ b/cli/clear.py @@ -0,0 +1,33 @@ +# Clear all models by deleting the whole file + +# Import required modules +import os +import sys + +# Get the full path to this file +path = os.path.dirname(os.path.abspath(__file__)) +# Get the passed user +user = sys.argv[1] + +# 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() + +# 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() + +# 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() + +# Delete otherwise +os.remove(path + "/../models/" + user + ".dat") +print("\nModels cleared") diff --git a/cli/help.py b/cli/help.py new file mode 100644 index 0000000..2b09848 --- /dev/null +++ b/cli/help.py @@ -0,0 +1,16 @@ +# 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 + +For support please visit +https://github.com/Boltgolt/howdy\ +""") diff --git a/cli/list.py b/cli/list.py new file mode 100644 index 0000000..ff37b10 --- /dev/null +++ b/cli/list.py @@ -0,0 +1,46 @@ +# List all models for a user + +# Import required modules +import sys +import os +import json +import time + +# Get the absolute path and the username +path = os.path.dirname(os.path.realpath(__file__)) + "/.." +user = sys.argv[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") + sys.exit(1) + +# Path to the models file +enc_file = path + "/models/" + user + ".dat" + +# Try to load the models file and abort if the user does not have it yet +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") + sys.exit(1) + +# Print a header +print("Known face models for " + user + ":") +print("\n\t\033[1;29mID Date Label\033[0m") + +# Loop through all encodings and print info about them +for enc in encodings: + # Start with a tab and print the id + print("\t" + str(enc["id"]), end="") + # Print padding spaces after the id + print((4 - len(str(enc["id"]))) * " ", end="") + # Format the time as ISO in the local timezone + print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(enc["time"])), end="") + # End with the label + print(" " + enc["label"]) + +# Add a closing enter +print() diff --git a/cli/remove.py b/cli/remove.py new file mode 100644 index 0000000..4322839 --- /dev/null +++ b/cli/remove.py @@ -0,0 +1,78 @@ +# Remove a encoding from the models file + +# Import required modules +import sys +import os +import json + +# Get the absolute path and the username +path = os.path.dirname(os.path.realpath(__file__)) + "/.." +user = sys.argv[1] + +# 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") + print("You can find the IDs by running:") + print("\n\thowdy " + user + " 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") + sys.exit(1) + +# Path to the models file +enc_file = path + "/models/" + user + ".dat" + +# Try to load the models file and abort if the user does not have it yet +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") + sys.exit(1) + +# Tracks if a encoding with that id has been found +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]: ") + + # Abort if the answer isn't yes + if (ans.lower() != "y"): + print('\nInerpeting as a "NO"') + sys.exit() + + # 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) + sys.exit() + +# Remove the entire file if this encoding is the only one +if len(encodings) == 1: + os.remove(path + "/models/" + user + ".dat") + print("Removed last model, howdy disabled for user") +else: + # A place holder to contain the encodings that will remain + new_encodings = [] + + # 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]: + new_encodings.append(enc) + + # Save this new set to disk + with open(enc_file, "w") as datafile: + json.dump(new_encodings, datafile) + + print("Removed model " + sys.argv[3]) diff --git a/compair.py b/compair.py deleted file mode 100644 index fb40905..0000000 --- a/compair.py +++ /dev/null @@ -1,69 +0,0 @@ -# Compair incomming video with known faces -# Running in a local python instance to get around PATH issues - -# Import required modules -import face_recognition -import cv2 -import sys -import os -import json -import configparser - -# Read config from disk -config = configparser.ConfigParser() -config.read(os.path.dirname(os.path.abspath(__file__)) + "/config.ini") - -def stop(status): - """Stop the execution and close video stream""" - video_capture.release() - sys.exit(status) - -# Make sure we were given an username to tast against -try: - if not isinstance(sys.argv[1], str): - sys.exit(1) -except IndexError: - sys.exit(1) - -# The username of the authenticating user -user = sys.argv[1] -# List of known faces, encoded by face_recognition -encodings = [] -# Amount of frames already matched -tries = 0 - -# Try to load the face model from the models folder -try: - encodings = json.load(open(os.path.dirname(os.path.abspath(__file__)) + "/models/" + user + ".dat")) -except FileNotFoundError: - sys.exit(10) - -# Verify that we have a valid model file -if len(encodings) < 3: - sys.exit(1) - -# Start video capture on the IR camera -video_capture = cv2.VideoCapture(int(config.get("video", "device_id"))) - -while True: - # Grab a single frame of video - ret, frame = video_capture.read() - - # Get all faces from that frame as encodings - face_encodings = face_recognition.face_encodings(frame) - - # Loop through each face - for face_encoding in face_encodings: - # Match this found face against a known face - matches = face_recognition.face_distance(encodings, face_encoding) - - # Check if any match is certain enough to be the user we're looking for - for match in matches: - if match < int(config.get("video", "certainty")) and match > 0: - stop(0) - - # Stop if we've exceded the maximum retry count - if tries > int(config.get("video", "frame_count")): - stop(11) - - tries += 1 diff --git a/compare.py b/compare.py new file mode 100644 index 0000000..101f9fe --- /dev/null +++ b/compare.py @@ -0,0 +1,140 @@ +# Compare incomming video with known faces +# Running in a local python instance to get around PATH issues + +# Import required modules +import cv2 +import sys +import os +import json +import time +import math +import configparser + +# Start timing +timings = [time.time()] + +# Read config from disk +config = configparser.ConfigParser() +config.read(os.path.dirname(os.path.abspath(__file__)) + "/config.ini") + +def stop(status): + """Stop the execution and close video stream""" + video_capture.release() + sys.exit(status) + +# Make sure we were given an username to tast against +try: + if not isinstance(sys.argv[1], str): + sys.exit(1) +except IndexError: + sys.exit(1) + +# The username of the authenticating user +user = sys.argv[1] +# The model file contents +models = [] +# Encoded face models +encodings = [] +# Amount of frames already matched +tries = 0 + +# Try to load the face model from the models folder +try: + models = json.load(open(os.path.dirname(os.path.abspath(__file__)) + "/models/" + user + ".dat")) +except FileNotFoundError: + sys.exit(10) + +# Check if the file contains a model +if len(models) < 1: + sys.exit(10) + +# Put all models together into 1 array +for model in models: + encodings += model["data"] + +# Import face recognition, takes some time +timings.append(time.time()) +import face_recognition +timings.append(time.time()) + +# Start video capture on the IR camera +video_capture = cv2.VideoCapture(int(config.get("video", "device_id"))) +timings.append(time.time()) + +# Fetch the max frame height +max_height = int(config.get("video", "max_height")) + +# Start the read loop +frames = 0 +while True: + frames += 1 + + # Grab a single frame of video + # Don't remove ret, it doesn't work without it + ret, frame = video_capture.read() + + # Get the height and with of the image + height, width = frame.shape[:2] + + # If the hight is too high + if max_height < height: + # Calculate the amount the image has to shrink + scaling_factor = max_height / float(height) + # Apply that factor to the frame + frame = cv2.resize(frame, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA) + + # Save the new size for diagnostics + scale_height, scale_width = frame.shape[:2] + + # Convert from BGR to RGB + frame = frame[:, :, ::-1] + + # Get all faces from that frame as encodings + face_encodings = face_recognition.face_encodings(frame) + + # Loop through each face + for face_encoding in face_encodings: + # Match this found face against a known face + matches = face_recognition.face_distance(encodings, face_encoding) + + # Check if any match is certain enough to be the user we're looking for + match_index = 0 + for match in matches: + match_index += 1 + + # Try to find a match that's confident enough + if match * 10 < float(config.get("video", "certainty")) and match > 0: + timings.append(time.time()) + + # If set to true in the config, print debug text + if config.get("debug", "end_report") == "true": + def print_timing(label, offset): + """Helper function to print a timing from the list""" + print(" " + label + ": " + str(round((timings[1 + offset] - timings[offset]) * 1000)) + "ms") + + print("Time spend") + print_timing("Starting up", 0) + print_timing("Importing face_recognition", 1) + print_timing("Opening the camera", 2) + print_timing("Searching for known face", 3) + + print("\nResolution") + print(" Native: " + str(height) + "x" + str(width)) + print(" Used: " + str(scale_height) + "x" + str(scale_width)) + + print("\nFrames searched: " + str(frames) + " (" + str(round(float(frames) / (timings[4] - timings[2]), 2)) + " fps)") + print("Certainty of winning frame: " + str(round(match * 10, 3))) + + exposures = ["long", "medium", "short"] + model_id = math.floor(float(match_index) / 3) + + print("Winning model: " + str(model_id) + " (\"" + models[model_id]["label"] + "\") using " + exposures[match_index % 3] + " exposure\n") + + # End peacegully + stop(0) + + # Stop if we've exceded the maximum retry count + if time.time() - timings[3] > int(config.get("video", "timout")): + stop(11) + + tries += 1 diff --git a/config.ini b/config.ini index cd4819c..a019e35 100644 --- a/config.ini +++ b/config.ini @@ -9,11 +9,19 @@ suppress_unknown = 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 -certainty = 3 +certainty = 3.5 -# The number of frames to capture and to process before timing out -frame_count = 30 +# The number of seconds to search before timing out +timout = 4 # The /dev/videoX id to capture frames from -# In my case, video0 is the normal camera and video1 is the IR version +# Should be set automatically by the installer device_id = 1 + +# Scale down the video feed to this maximum height +# Speeds up face recognition but can make it less precise +max_height = 320 + +[debug] +# Show a short but detailed diagnostic report in console +end_report = false diff --git a/installer.py b/installer.py new file mode 100644 index 0000000..52be40f --- /dev/null +++ b/installer.py @@ -0,0 +1,235 @@ +# 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", "libpam-python", "fswebcam", "libopencv-dev", "python-opencv"])) + +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) + + # 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]: ") + + # 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/pam.py b/pam.py index 49bcd35..bd6cf06 100644 --- a/pam.py +++ b/pam.py @@ -1,4 +1,4 @@ -# PAM interface in python, launches compair.py +# PAM interface in python, launches compare.py # Import required modules import subprocess @@ -15,8 +15,8 @@ config.read(os.path.dirname(os.path.abspath(__file__)) + "/config.ini") def doAuth(pamh): """Start authentication in a seperate process""" - # Run compair as python3 subprocess to circumvent python version and import issues - status = subprocess.call(["python3", os.path.dirname(os.path.abspath(__file__)) + "/compair.py", pamh.get_user()]) + # 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()]) # Status 10 means we couldn't find any face models if status == 10: diff --git a/uninstall.py b/uninstall.py new file mode 100644 index 0000000..8cc56a1 --- /dev/null +++ b/uninstall.py @@ -0,0 +1,36 @@ +# 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\ +""") diff --git a/utils.py b/utils.py deleted file mode 100644 index 848edac..0000000 --- a/utils.py +++ /dev/null @@ -1,25 +0,0 @@ -# Useful support functions - -def print_menu(encodings): - """Show a menu asking the user what he wants to do""" - if len(encodings) == 3: - print("There is 1 existing face model for this user") - else: - print("There are " + str(int(len(encodings) / 3)) + " existing face models for this user") - print("What do you want to do?\n") - - print("1: Add additional face model") - print("2: Overwrite older model(s)") - print("0: Exit") - - com = input("Option: ") - - if com == "1": - return encodings - elif com == "2": - return [] - elif com == "0": - sys.exit() - else: - print("Invalid option '" + com + "'\n") - return print_menu(encodings)