From 10563589c2fe48d046febfbaa0186113a5cb315f Mon Sep 17 00:00:00 2001 From: boltgolt Date: Mon, 5 Feb 2018 23:43:32 +0100 Subject: [PATCH 01/14] Added installation script --- installer.py | 153 +++++++++++++++++++++++++++++++++++++++++++++++++++ utils.py | 2 + 2 files changed, 155 insertions(+) create mode 100644 installer.py diff --git a/installer.py b/installer.py new file mode 100644 index 0000000..2bd2abb --- /dev/null +++ b/installer.py @@ -0,0 +1,153 @@ +import subprocess +import time +import sys +import os +import signal +import fileinput +import urllib.parse + +def log(text): + print("\n>>> \033[32m" + text + "\033[0m\n") + +def handleStatus(status): + if (status != 0): + print("\033[31mError while running last command\033[0m") + sys.exit() + +user = os.getenv("SUDO_USER") + +if user is None: + print("Please run this script as a sudo user") + sys.exit() + +print("\n\033[33m HOWDY INSTALLER FOR UBUNTU\033[0m") +print(" Version 1, 2016/02/05\n") + +time.sleep(.5) +log("Installing required apt packages") + +handleStatus(subprocess.call(["apt", "install", "-y", "libpam-python", "fswebcam", "libopencv-dev", "python-opencv"])) + +log("Starting camera check") + +devices = os.listdir("/dev") +picked = False + +for dev in devices: + if (dev[:5] == "video"): + time.sleep(.5) + + print("Trying /dev/" + dev) + + sub = subprocess.Popen(["fswebcam -S 9999999999 -d /dev/" + dev + " /dev/null 2>/dev/null"], shell=True, preexec_fn=os.setsid) + + print("\033[33mOne of your cameras should now be on.\033[0m") + ans = input("Did your IR emitters turn on? [y/N]: ") + + os.killpg(os.getpgid(sub.pid), signal.SIGTERM) + + if (ans.lower() == "y"): + picked = dev[5:] + break + else: + print("Inerpeting as a \"NO\"\n") + +if (picked == False): + print("\033[31mNo suitable IR camera found\033[0m") + +log("Cloning dlib") + +handleStatus(subprocess.call(["git", "clone", "https://github.com/davisking/dlib.git", "/tmp/dlib_clone"])) + +log("Building dlib") + +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") + +handleStatus(subprocess.call(["rm", "-rf", "/tmp/dlib_clone"])) + +log("Installing face_recognition") + +handleStatus(subprocess.call(["pip3", "install", "face_recognition"])) + +log("Cloning howdy") + +if not os.path.exists("/lib/security"): + os.makedirs("/lib/security") + +handleStatus(subprocess.call(["git", "clone", "https://github.com/Boltgolt/howdy.git", "/lib/security/howdy"])) + +for line in fileinput.input(["/lib/security/howdy/config.ini"], inplace = 1): + print(line.replace("device_id = 1", "device_id = " + picked), end="") + +handleStatus(subprocess.call(["chmod 600 -R /lib/security/howdy/"], shell=True)) + +log("Adding howdy as PAM module") + +outlines = [] +printlines = [] +inserted = False + +with open("/etc/pam.d/common-auth") as fp: + line = fp.readline() + cnt = 1 + while line: + outlines.append(line) + + if line[:1] != "#": + printlines.append(line) + + if not inserted: + line_comment = "# Howdy IR face recognition\n" + line_Link = "auth sufficient pam_python.so /lib/security/howdy/pam.py\n\n" + + outlines.append(line_comment) + outlines.append(line_Link) + + printlines.append("\033[33m" + line_comment + "\033[0m") + printlines.append("\033[33m" + line_Link + "\033[0m") + + inserted = True + else: + printlines.append("\033[37m" + line + "\033[0m") + + line = fp.readline() + cnt += 1 + +print("\033[33m" + ">>> START OF /etc/pam.d/common-auth" + "\033[0m") + +for line in printlines: + print(line, end="") + +print("\033[33m" + ">>> END OF /etc/pam.d/common-auth" + "\033[0m" + "\n") + +print("Lines will be insterted in /etc/pam.d/common-auth as shown above") +ans = input("Apply this change? [y/N]: ") + +if (ans.lower() != "y"): + print("Inerpeting as a \"NO\", aborting") + sys.exit() + +print("Adding lines to PAM\n") + +common_auth = open("/etc/pam.d/common-auth", "w") +common_auth.write("".join(outlines)) +common_auth.close() + +diag_out = "" + +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" + +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 += "```" + +print("https://github.com/Boltgolt/howdy/issues/new?title=%5Bdiag%5D%20Post-installation%20camera%20information&body=" + urllib.parse.quote_plus(diag_out) + "\n") + +print("Installation complete.") +print("If you want to help the development, please use the link above to post some camera-related information as a new github issue") diff --git a/utils.py b/utils.py index 848edac..caefdae 100644 --- a/utils.py +++ b/utils.py @@ -1,5 +1,7 @@ # Useful support functions +import sys + def print_menu(encodings): """Show a menu asking the user what he wants to do""" if len(encodings) == 3: From 590d488c34a8e279e73a94b830313fadaa6d00ed Mon Sep 17 00:00:00 2001 From: boltgolt Date: Thu, 8 Feb 2018 01:11:47 +0100 Subject: [PATCH 02/14] Started on CLI --- cli.py | 35 +++++++++++++++++++++ installer.py | 86 +++++++++++++++++++++++++++------------------------- 2 files changed, 79 insertions(+), 42 deletions(-) create mode 100755 cli.py diff --git a/cli.py b/cli.py new file mode 100755 index 0000000..2f1bb96 --- /dev/null +++ b/cli.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +import sys +import os +import json +import time + +path = os.path.dirname(os.path.realpath(__file__)) +cmd = sys.argv[1] + +if cmd == "list": + if not os.path.exists(path + "/models"): + print("Face models have not been initialized yet, please run:") + print("\n\thowdy add\n") + sys.exit(1) + + user = os.environ.get("USER") + enc_file = path + "/models/" + user + ".dat" + + try: + encodings = json.load(open(enc_file)) + except FileNotFoundError: + print("No face model known for the current user (" + user + "), please run:") + print("\n\thowdy add\n") + sys.exit(1) + + print("Known face models for " + user + ":") + print("\n\t\033[1;29mID Date Label\033[0m") + + for enc in encodings: + print("\t" + str(enc["id"]), end="") + print((4 - len(str(enc["id"]))) * " ", end="") + print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(enc["time"])), end="") + print(" " + enc["label"]) + + print() diff --git a/installer.py b/installer.py index 2bd2abb..2004fa0 100644 --- a/installer.py +++ b/installer.py @@ -7,18 +7,18 @@ import fileinput import urllib.parse def log(text): - print("\n>>> \033[32m" + text + "\033[0m\n") + print("\n>>> \033[32m" + text + "\033[0m\n") def handleStatus(status): - if (status != 0): - print("\033[31mError while running last command\033[0m") - sys.exit() + if (status != 0): + print("\033[31mError while running last command\033[0m") + sys.exit() user = os.getenv("SUDO_USER") if user is None: - print("Please run this script as a sudo user") - sys.exit() + print("Please run this script as a sudo user") + sys.exit() print("\n\033[33m HOWDY INSTALLER FOR UBUNTU\033[0m") print(" Version 1, 2016/02/05\n") @@ -34,26 +34,26 @@ devices = os.listdir("/dev") picked = False for dev in devices: - if (dev[:5] == "video"): - time.sleep(.5) + if (dev[:5] == "video"): + time.sleep(.5) - print("Trying /dev/" + dev) + print("Trying /dev/" + dev) - sub = subprocess.Popen(["fswebcam -S 9999999999 -d /dev/" + dev + " /dev/null 2>/dev/null"], shell=True, preexec_fn=os.setsid) + sub = subprocess.Popen(["fswebcam -S 9999999999 -d /dev/" + dev + " /dev/null 2>/dev/null"], shell=True, preexec_fn=os.setsid) - print("\033[33mOne of your cameras should now be on.\033[0m") - ans = input("Did your IR emitters turn on? [y/N]: ") + print("\033[33mOne of your cameras should now be on.\033[0m") + ans = input("Did your IR emitters turn on? [y/N]: ") - os.killpg(os.getpgid(sub.pid), signal.SIGTERM) + os.killpg(os.getpgid(sub.pid), signal.SIGTERM) - if (ans.lower() == "y"): - picked = dev[5:] - break - else: - print("Inerpeting as a \"NO\"\n") + if (ans.lower() == "y"): + picked = dev[5:] + break + else: + print("Inerpeting as a \"NO\"\n") if (picked == False): - print("\033[31mNo suitable IR camera found\033[0m") + print("\033[31mNo suitable IR camera found\033[0m") log("Cloning dlib") @@ -74,14 +74,16 @@ handleStatus(subprocess.call(["pip3", "install", "face_recognition"])) log("Cloning howdy") if not os.path.exists("/lib/security"): - os.makedirs("/lib/security") + os.makedirs("/lib/security") handleStatus(subprocess.call(["git", "clone", "https://github.com/Boltgolt/howdy.git", "/lib/security/howdy"])) for line in fileinput.input(["/lib/security/howdy/config.ini"], inplace = 1): - print(line.replace("device_id = 1", "device_id = " + picked), end="") + print(line.replace("device_id = 1", "device_id = " + picked), end="") handleStatus(subprocess.call(["chmod 600 -R /lib/security/howdy/"], shell=True)) +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)) log("Adding howdy as PAM module") @@ -90,35 +92,35 @@ printlines = [] inserted = False with open("/etc/pam.d/common-auth") as fp: - line = fp.readline() - cnt = 1 - while line: - outlines.append(line) + line = fp.readline() + cnt = 1 + while line: + outlines.append(line) - if line[:1] != "#": - printlines.append(line) + if line[:1] != "#": + printlines.append(line) - if not inserted: - line_comment = "# Howdy IR face recognition\n" - line_Link = "auth sufficient pam_python.so /lib/security/howdy/pam.py\n\n" + if not inserted: + line_comment = "# Howdy IR face recognition\n" + line_Link = "auth sufficient pam_python.so /lib/security/howdy/pam.py\n\n" - outlines.append(line_comment) - outlines.append(line_Link) + outlines.append(line_comment) + outlines.append(line_Link) - printlines.append("\033[33m" + line_comment + "\033[0m") - printlines.append("\033[33m" + line_Link + "\033[0m") + printlines.append("\033[33m" + line_comment + "\033[0m") + printlines.append("\033[33m" + line_Link + "\033[0m") - inserted = True - else: - printlines.append("\033[37m" + line + "\033[0m") + inserted = True + else: + printlines.append("\033[37m" + line + "\033[0m") - line = fp.readline() - cnt += 1 + line = fp.readline() + cnt += 1 print("\033[33m" + ">>> START OF /etc/pam.d/common-auth" + "\033[0m") for line in printlines: - print(line, end="") + print(line, end="") print("\033[33m" + ">>> END OF /etc/pam.d/common-auth" + "\033[0m" + "\n") @@ -126,8 +128,8 @@ print("Lines will be insterted in /etc/pam.d/common-auth as shown above") ans = input("Apply this change? [y/N]: ") if (ans.lower() != "y"): - print("Inerpeting as a \"NO\", aborting") - sys.exit() + print("Inerpeting as a \"NO\", aborting") + sys.exit() print("Adding lines to PAM\n") From 92a8d0a25966940b9ecddefe56c4fdad2a6d57d1 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Sat, 10 Feb 2018 12:40:26 +0100 Subject: [PATCH 03/14] That's not how you spell compare --- compair.py => compare.py | 2 +- pam.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename compair.py => compare.py (97%) diff --git a/compair.py b/compare.py similarity index 97% rename from compair.py rename to compare.py index fb40905..8d0e33f 100644 --- a/compair.py +++ b/compare.py @@ -1,4 +1,4 @@ -# Compair incomming video with known faces +# Compare incomming video with known faces # Running in a local python instance to get around PATH issues # Import required modules 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: From 22c72432a7090bc03accf2937d72d5ac3e54a4ed Mon Sep 17 00:00:00 2001 From: boltgolt Date: Sat, 10 Feb 2018 15:09:49 +0100 Subject: [PATCH 04/14] Moveing CLI commands to seperate files --- cli.py | 30 +----------------------------- cli/__init__.py | 0 cli/list.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 29 deletions(-) create mode 100644 cli/__init__.py create mode 100644 cli/list.py diff --git a/cli.py b/cli.py index 2f1bb96..2c645f1 100755 --- a/cli.py +++ b/cli.py @@ -1,35 +1,7 @@ #!/usr/bin/env python3 import sys -import os -import json -import time -path = os.path.dirname(os.path.realpath(__file__)) cmd = sys.argv[1] if cmd == "list": - if not os.path.exists(path + "/models"): - print("Face models have not been initialized yet, please run:") - print("\n\thowdy add\n") - sys.exit(1) - - user = os.environ.get("USER") - enc_file = path + "/models/" + user + ".dat" - - try: - encodings = json.load(open(enc_file)) - except FileNotFoundError: - print("No face model known for the current user (" + user + "), please run:") - print("\n\thowdy add\n") - sys.exit(1) - - print("Known face models for " + user + ":") - print("\n\t\033[1;29mID Date Label\033[0m") - - for enc in encodings: - print("\t" + str(enc["id"]), end="") - print((4 - len(str(enc["id"]))) * " ", end="") - print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(enc["time"])), end="") - print(" " + enc["label"]) - - print() + import cli.list diff --git a/cli/__init__.py b/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/list.py b/cli/list.py new file mode 100644 index 0000000..78a62aa --- /dev/null +++ b/cli/list.py @@ -0,0 +1,32 @@ +import sys +import os +import json +import time + +path = os.path.dirname(os.path.realpath(__file__)) + "/.." + +if not os.path.exists(path + "/models"): + print("Face models have not been initialized yet, please run:") + print("\n\thowdy add\n") + sys.exit(1) + +user = os.environ.get("USER") +enc_file = path + "/models/" + user + ".dat" + +try: + encodings = json.load(open(enc_file)) +except FileNotFoundError: + print("No face model known for the current user (" + user + "), please run:") + print("\n\thowdy add\n") + sys.exit(1) + +print("Known face models for " + user + ":") +print("\n\t\033[1;29mID Date Label\033[0m") + +for enc in encodings: + print("\t" + str(enc["id"]), end="") + print((4 - len(str(enc["id"]))) * " ", end="") + print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(enc["time"])), end="") + print(" " + enc["label"]) + +print() From 494e414949ce47ae421666e8297875d8cf920365 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Sat, 10 Feb 2018 16:39:41 +0100 Subject: [PATCH 05/14] Moved learn.py to the CLI and added a help page --- cli.py | 8 ++++++ learn.py => cli/add.py | 57 +++++++++++++++++++++++++++++------------- cli/help.py | 14 +++++++++++ utils.py | 27 -------------------- 4 files changed, 62 insertions(+), 44 deletions(-) rename learn.py => cli/add.py (70%) create mode 100644 cli/help.py delete mode 100644 utils.py diff --git a/cli.py b/cli.py index 2c645f1..41a3528 100755 --- a/cli.py +++ b/cli.py @@ -1,7 +1,15 @@ #!/usr/bin/env python3 import sys +if (len(sys.argv) == 1): + import cli.help + sys.exit() + cmd = sys.argv[1] if cmd == "list": import cli.list +elif cmd == "help": + import cli.help +elif cmd == "add": + import cli.add diff --git a/learn.py b/cli/add.py similarity index 70% rename from learn.py rename to cli/add.py index 9864095..e3bd9b1 100644 --- a/learn.py +++ b/cli/add.py @@ -1,24 +1,33 @@ # 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 config import configparser -import utils + +try: + import face_recognition +except ImportError as err: + print(err) + + print("\nCan't import face_recognition module, check the output of") + print("pip3 show face_recognition") + sys.exit() + +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 +63,48 @@ 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") # 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) + +label = "Initial model" + +if len(encodings) > 0: + label = "Model #" + str(len(encodings) + 1) + +label_in = input("Enter a label for this new model [" + label + "]: ") + +if label_in != "": + label = label_in + +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 +114,8 @@ for delay in [30, 6, 0]: time.sleep(.3) captureFrame(delay) +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/help.py b/cli/help.py new file mode 100644 index 0000000..f8bdbd7 --- /dev/null +++ b/cli/help.py @@ -0,0 +1,14 @@ +print("Howdy IR face recognition\n") + +print("Usage:") +print(" howdy [argument]\n") + +print("Commands:") +print(" help Show this help page") +print(" list List all saved face models for the current user") +print(" add Add a new face model for the current user") +print(" remove [id] Remove a specific model") +print(" clear Remove all face models for the current user") + +print("\nFor support please visit") +print("https://github.com/Boltgolt/howdy") diff --git a/utils.py b/utils.py deleted file mode 100644 index caefdae..0000000 --- a/utils.py +++ /dev/null @@ -1,27 +0,0 @@ -# Useful support functions - -import sys - -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) From 7421eac0235323ab812f805e72abfd5fa97b42be Mon Sep 17 00:00:00 2001 From: boltgolt Date: Sat, 10 Feb 2018 17:03:42 +0100 Subject: [PATCH 06/14] Added command autocomplete --- autocomplete.sh | 12 ++++++++++++ installer.py | 6 ++++++ 2 files changed, 18 insertions(+) create mode 100644 autocomplete.sh diff --git a/autocomplete.sh b/autocomplete.sh new file mode 100644 index 0000000..117ba6e --- /dev/null +++ b/autocomplete.sh @@ -0,0 +1,12 @@ +_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/installer.py b/installer.py index 2004fa0..db1a38c 100644 --- a/installer.py +++ b/installer.py @@ -81,10 +81,16 @@ handleStatus(subprocess.call(["git", "clone", "https://github.com/Boltgolt/howdy 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 +handleStatus(subprocess.call(["sudo cp /lib/security/howdy/autocomplete.sh /etc/bash_completion.d/howdy"], shell=True)) + log("Adding howdy as PAM module") outlines = [] From 2f5b217c1b65ac45bc1eee321175bc1ae7796b37 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Sun, 11 Feb 2018 01:54:44 +0100 Subject: [PATCH 07/14] Fixes and readme update --- README.md | 41 ++++++++++++++--------------------------- cli/add.py | 2 +- installer.py | 7 ++++++- 3 files changed, 21 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 41f82af..3634193 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,33 @@ # 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 add` to add a face model for the current user. 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. +### 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. ### 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/cli/add.py b/cli/add.py index e3bd9b1..f7b3c2a 100644 --- a/cli/add.py +++ b/cli/add.py @@ -95,7 +95,7 @@ if len(encodings) > 0: label_in = input("Enter a label for this new model [" + label + "]: ") if label_in != "": - label = label_in + label = label_in[:24] insert_model = { "time": int(time.time()), diff --git a/installer.py b/installer.py index db1a38c..6fb14c2 100644 --- a/installer.py +++ b/installer.py @@ -54,6 +54,7 @@ for dev in devices: if (picked == False): print("\033[31mNo suitable IR camera found\033[0m") + sys.exit() log("Cloning dlib") @@ -158,4 +159,8 @@ diag_out += "```" print("https://github.com/Boltgolt/howdy/issues/new?title=%5Bdiag%5D%20Post-installation%20camera%20information&body=" + urllib.parse.quote_plus(diag_out) + "\n") print("Installation complete.") -print("If you want to help the development, please use the link above to post some camera-related information as a new github issue") +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 downloaded to tmp +if os.path.exists("/tmp/howdy_install.py"): + os.remove("/tmp/howdy_install.py") From bf8c88f0b0475412b8085be6285359aac6675052 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Tue, 13 Feb 2018 15:00:30 +0100 Subject: [PATCH 08/14] Added udevadm info and a very simple uninstaller --- README.md | 6 +++++- installer.py | 27 ++++++++++++++++++++------- uninstall.py | 4 ++++ 3 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 uninstall.py diff --git a/README.md b/README.md index 3634193..931a046 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,16 @@ Using the central authentication system in Linux (PAM), this works everywhere yo Run the installer by pasting (`ctrl+shift+V`) the following command into the terminal: -`wget -O /tmp/howdy_install.py https://raw.githubusercontent.com/Boltgolt/howdy/master/installer.py && sudo python3 /tmp/howdy_install.py` +``` +wget -O /tmp/howdy_install.py https://raw.githubusercontent.com/Boltgolt/howdy/master/installer.py && sudo python3 /tmp/howdy_install.py +``` This will guide you through the installation. When that's done run `howdy add` to add a face model for the current user. 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. diff --git a/installer.py b/installer.py index 6fb14c2..7759d71 100644 --- a/installer.py +++ b/installer.py @@ -2,6 +2,7 @@ import subprocess import time import sys import os +import re import signal import fileinput import urllib.parse @@ -37,7 +38,16 @@ for dev in devices: if (dev[:5] == "video"): time.sleep(.5) - print("Trying /dev/" + dev) + device_name = "/dev/" + dev + udevadm = subprocess.check_output(["udevadm info -r --query=all -n " + device_name], shell=True).decode("utf-8") + + for line in udevadm.split("\n"): + re_name = re.search('product.*=(.*)$', line, re.IGNORECASE) + + if re_name: + device_name = '"' + re_name.group(1) + '"' + + print("Trying " + device_name) sub = subprocess.Popen(["fswebcam -S 9999999999 -d /dev/" + dev + " /dev/null 2>/dev/null"], shell=True, preexec_fn=os.setsid) @@ -89,8 +99,8 @@ handleStatus(subprocess.call(["chmod 600 -R /lib/security/howdy/"], shell=True)) 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 -handleStatus(subprocess.call(["sudo cp /lib/security/howdy/autocomplete.sh /etc/bash_completion.d/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") @@ -144,9 +154,7 @@ common_auth = open("/etc/pam.d/common-auth", "w") common_auth.write("".join(outlines)) common_auth.close() -diag_out = "" - -diag_out += "Video devices (IR:" + picked + ")\n" +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" @@ -154,9 +162,14 @@ diag_out += "```\n" 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" + +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("https://github.com/Boltgolt/howdy/issues/new?title=%5Bdiag%5D%20Post-installation%20camera%20information&body=" + urllib.parse.quote_plus(diag_out) + "\n") +print("https://github.com/Boltgolt/howdy-reports/issues/new?title=Post-installation%20camera%20information&body=" + urllib.parse.quote_plus(diag_out) + "\n") print("Installation complete.") print("If you want to help the development, please use the link above to post some camera-related information to github") diff --git a/uninstall.py b/uninstall.py new file mode 100644 index 0000000..e3dd28f --- /dev/null +++ b/uninstall.py @@ -0,0 +1,4 @@ +import subprocess + +subprocess.call(["rm -rf /lib/security/howdy/"], shell=True) +subprocess.call(["rm /usr/bin/howdy"], shell=True) From b55f20c0e0fad2e2c4c3b990991f15b8bf74452e Mon Sep 17 00:00:00 2001 From: boltgolt Date: Tue, 13 Feb 2018 16:31:30 +0100 Subject: [PATCH 09/14] Fixed compare and added diagnostics --- compare.py | 58 +++++++++++++++++++++++++++++++++++++++++++++++------- config.ini | 8 ++++++-- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/compare.py b/compare.py index 8d0e33f..a3b53e2 100644 --- a/compare.py +++ b/compare.py @@ -2,13 +2,21 @@ # 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 time +import math import configparser +# Start timing +timings = [time.time()] + +# Import face recognition, takes some time +import face_recognition +timings.append(time.time()) + # Read config from disk config = configparser.ConfigParser() config.read(os.path.dirname(os.path.abspath(__file__)) + "/config.ini") @@ -27,26 +35,38 @@ except IndexError: # The username of the authenticating user user = sys.argv[1] -# List of known faces, encoded by face_recognition +# 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: - encodings = json.load(open(os.path.dirname(os.path.abspath(__file__)) + "/models/" + user + ".dat")) + models = 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) +# 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"] # Start video capture on the IR camera video_capture = cv2.VideoCapture(int(config.get("video", "device_id"))) +timings.append(time.time()) +# 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 all faces from that frame as encodings @@ -58,8 +78,32 @@ while True: 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: - if match < int(config.get("video", "certainty")) and match > 0: + 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": + print("DEBUG END REPORT\n") + + print("Time spend") + print(" Importing face_recognition: " + str(round((timings[1] - timings[0]) * 1000)) + "ms") + print(" Opening the camera: " + str(round((timings[2] - timings[1]) * 1000)) + "ms") + print(" Searching for known face: " + str(round((timings[3] - timings[2]) * 1000)) + "ms\n") + + print("Frames searched: " + str(frames)) + print("Certainty 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 diff --git a/config.ini b/config.ini index cd4819c..428b68f 100644 --- a/config.ini +++ b/config.ini @@ -9,11 +9,15 @@ 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 /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 + +[debug] +# Show a short but detailed diagnostic report in console +end_report = false From 84889c278538b653a6cac6eddda40f32651bbabb Mon Sep 17 00:00:00 2001 From: boltgolt Date: Tue, 13 Feb 2018 22:03:03 +0100 Subject: [PATCH 10/14] Added clear command and requred sudo --- cli.py | 20 ++++++++++++++++++-- cli/add.py | 2 +- cli/clear.py | 25 +++++++++++++++++++++++++ cli/help.py | 24 ++++++++++++------------ cli/list.py | 8 ++++---- compare.py | 26 ++++++++++++++++---------- 6 files changed, 76 insertions(+), 29 deletions(-) create mode 100644 cli/clear.py diff --git a/cli.py b/cli.py index 41a3528..3a90180 100755 --- a/cli.py +++ b/cli.py @@ -1,15 +1,31 @@ #!/usr/bin/env python3 import sys +import os -if (len(sys.argv) == 1): +if (len(sys.argv) < 3): + print("Howdy IR face recognition help") import cli.help sys.exit() -cmd = sys.argv[1] +user = sys.argv[1] +cmd = sys.argv[2] + +if cmd in ["list", "add", "remove", "clear"] and os.getenv("SUDO_USER") is None: + print("Please run this command with sudo") + sys.exit() 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 == "clear": + import cli.clear +else: + 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/add.py b/cli/add.py index f7b3c2a..f4fe2cb 100644 --- a/cli/add.py +++ b/cli/add.py @@ -66,7 +66,7 @@ def captureFrame(delay): 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 diff --git a/cli/clear.py b/cli/clear.py new file mode 100644 index 0000000..3d12715 --- /dev/null +++ b/cli/clear.py @@ -0,0 +1,25 @@ +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] + +if not os.path.exists(path + "/../models"): + print("No models created yet, can't clear them if they don't exist") + sys.exit() + +if not os.path.isfile(path + "/../models/" + user + ".dat"): + print(user + " has no models or they have been cleared already") + sys.exit() + +print("This will clear all models for " + user) +ans = input("Do you want to continue [y/N]: ") + +if (ans.lower() != "y"): + print('\nInerpeting as a "NO"') + sys.exit() + +os.remove(path + "/../models/" + user + ".dat") +print("\nModels cleared") diff --git a/cli/help.py b/cli/help.py index f8bdbd7..3b7641e 100644 --- a/cli/help.py +++ b/cli/help.py @@ -1,14 +1,14 @@ -print("Howdy IR face recognition\n") +print(""" +Usage: + howdy [argument] -print("Usage:") -print(" howdy [argument]\n") +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 -print("Commands:") -print(" help Show this help page") -print(" list List all saved face models for the current user") -print(" add Add a new face model for the current user") -print(" remove [id] Remove a specific model") -print(" clear Remove all face models for the current user") - -print("\nFor support please visit") -print("https://github.com/Boltgolt/howdy") +For support please visit +https://github.com/Boltgolt/howdy\ +""") diff --git a/cli/list.py b/cli/list.py index 78a62aa..64e7480 100644 --- a/cli/list.py +++ b/cli/list.py @@ -4,20 +4,20 @@ import json import time path = os.path.dirname(os.path.realpath(__file__)) + "/.." +user = sys.argv[1] if not os.path.exists(path + "/models"): print("Face models have not been initialized yet, please run:") - print("\n\thowdy add\n") + print("\n\thowdy " + user + " add\n") sys.exit(1) -user = os.environ.get("USER") enc_file = path + "/models/" + user + ".dat" try: encodings = json.load(open(enc_file)) except FileNotFoundError: - print("No face model known for the current user (" + user + "), please run:") - print("\n\thowdy add\n") + print("No face model known for the user " + user + ", please run:") + print("\n\thowdy " + user + " add\n") sys.exit(1) print("Known face models for " + user + ":") diff --git a/compare.py b/compare.py index a3b53e2..d075aa2 100644 --- a/compare.py +++ b/compare.py @@ -13,10 +13,6 @@ import configparser # Start timing timings = [time.time()] -# Import face recognition, takes some time -import face_recognition -timings.append(time.time()) - # Read config from disk config = configparser.ConfigParser() config.read(os.path.dirname(os.path.abspath(__file__)) + "/config.ini") @@ -56,6 +52,11 @@ if len(models) < 1: 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()) @@ -90,13 +91,18 @@ while True: if config.get("debug", "end_report") == "true": print("DEBUG END REPORT\n") - print("Time spend") - print(" Importing face_recognition: " + str(round((timings[1] - timings[0]) * 1000)) + "ms") - print(" Opening the camera: " + str(round((timings[2] - timings[1]) * 1000)) + "ms") - print(" Searching for known face: " + str(round((timings[3] - timings[2]) * 1000)) + "ms\n") + 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("Frames searched: " + str(frames)) - print("Certainty winning frame: " + str(round(match * 10, 3))) + 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("\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) From 3910755c1a9fea68661c7f166ad76009bdc75d07 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Tue, 13 Feb 2018 22:22:02 +0100 Subject: [PATCH 11/14] Added remove command --- cli.py | 2 ++ cli/remove.py | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 cli/remove.py diff --git a/cli.py b/cli.py index 3a90180..b25a8c3 100755 --- a/cli.py +++ b/cli.py @@ -21,6 +21,8 @@ elif cmd == "help": import cli.help elif cmd == "add": import cli.add +elif cmd == "remove": + import cli.remove elif cmd == "clear": import cli.clear else: diff --git a/cli/remove.py b/cli/remove.py new file mode 100644 index 0000000..e4edf6a --- /dev/null +++ b/cli/remove.py @@ -0,0 +1,60 @@ +import sys +import os +import json + +path = os.path.dirname(os.path.realpath(__file__)) + "/.." +user = sys.argv[1] + +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) + +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) + +enc_file = path + "/models/" + user + ".dat" + +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) + +found = False + +for enc in encodings: + if str(enc["id"]) == sys.argv[3]: + print('This will remove the model called "' + enc["label"] + '" for ' + user) + ans = input("Do you want to continue [y/N]: ") + + if (ans.lower() != "y"): + print('\nInerpeting as a "NO"') + sys.exit() + + found = True + print() + break + +if not found: + print("No model with ID " + sys.argv[3] + " exists for " + user) + sys.exit() + +if len(encodings) == 1: + os.remove(path + "/models/" + user + ".dat") + print("Removed last model, howdy disabled for user") +else: + new_encodings = [] + + for enc in encodings: + if str(enc["id"]) != sys.argv[3]: + new_encodings.append(enc) + + with open(enc_file, "w") as datafile: + json.dump(new_encodings, datafile) + + print("Removed model " + sys.argv[3]) From d7f85102264c0340f77b0bd1bc3f13ffa9003af0 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Tue, 13 Feb 2018 22:49:29 +0100 Subject: [PATCH 12/14] Timeouts are now by seconds and added max_height --- compare.py | 22 +++++++++++++++++++--- config.ini | 8 ++++++-- installer.py | 2 +- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/compare.py b/compare.py index d075aa2..6c5adc4 100644 --- a/compare.py +++ b/compare.py @@ -61,6 +61,9 @@ timings.append(time.time()) 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: @@ -70,6 +73,17 @@ while True: # Don't remove ret, it doesn't work without it ret, frame = video_capture.read() + height, width = frame.shape[:2] + + if max_height < height: + scaling_factor = max_height / float(height) + frame = cv2.resize(frame, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA) + + 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) @@ -89,8 +103,6 @@ while True: # If set to true in the config, print debug text if config.get("debug", "end_report") == "true": - print("DEBUG END REPORT\n") - 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") @@ -101,6 +113,10 @@ while True: 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))) @@ -113,7 +129,7 @@ while True: stop(0) # Stop if we've exceded the maximum retry count - if tries > int(config.get("video", "frame_count")): + if time.time() - timings[3] > int(config.get("video", "timout")): stop(11) tries += 1 diff --git a/config.ini b/config.ini index 428b68f..a019e35 100644 --- a/config.ini +++ b/config.ini @@ -11,13 +11,17 @@ suppress_unknown = false # On a scale from 1 to 10, values above 5 are not recommended 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 # 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 index 7759d71..fc876a2 100644 --- a/installer.py +++ b/installer.py @@ -80,7 +80,7 @@ handleStatus(subprocess.call(["rm", "-rf", "/tmp/dlib_clone"])) log("Installing face_recognition") -handleStatus(subprocess.call(["pip3", "install", "face_recognition"])) +handleStatus(subprocess.call(["pip3", "install", "face_recognition", "sty"])) log("Cloning howdy") From 818be928385a060441d2517f6ae32d2e830f3c8a Mon Sep 17 00:00:00 2001 From: boltgolt Date: Tue, 13 Feb 2018 23:22:13 +0100 Subject: [PATCH 13/14] Commented everything --- README.md | 2 +- autocomplete.sh | 3 +++ cli.py | 10 +++++++++- cli/add.py | 14 +++++++++++--- cli/clear.py | 8 ++++++++ cli/help.py | 2 ++ cli/list.py | 14 ++++++++++++++ cli/remove.py | 18 ++++++++++++++++++ compare.py | 5 +++++ installer.py | 2 +- 10 files changed, 72 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 931a046..14af4b9 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Run the installer by pasting (`ctrl+shift+V`) the following command into the ter wget -O /tmp/howdy_install.py https://raw.githubusercontent.com/Boltgolt/howdy/master/installer.py && sudo python3 /tmp/howdy_install.py ``` -This will guide you through the installation. When that's done run `howdy add` to add a face model for the current user. +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. diff --git a/autocomplete.sh b/autocomplete.sh index 117ba6e..8c16937 100644 --- a/autocomplete.sh +++ b/autocomplete.sh @@ -1,3 +1,6 @@ +# Autocomplete file run in bash +# Will sugest arguments on tab + _howdy() { local cur prev opts COMPREPLY=() diff --git a/cli.py b/cli.py index b25a8c3..e820426 100755 --- a/cli.py +++ b/cli.py @@ -1,19 +1,26 @@ #!/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() -user = sys.argv[1] +# 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": @@ -26,6 +33,7 @@ elif cmd == "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: diff --git a/cli/add.py b/cli/add.py index f4fe2cb..9b2e6d9 100644 --- a/cli/add.py +++ b/cli/add.py @@ -6,19 +6,21 @@ import time import os import sys import json - -# Import config import configparser + +# 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 face_recognition module, check the output of") + 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 @@ -87,16 +89,21 @@ except FileNotFoundError: 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, @@ -114,6 +121,7 @@ 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 diff --git a/cli/clear.py b/cli/clear.py index 3d12715..4cd56c2 100644 --- a/cli/clear.py +++ b/cli/clear.py @@ -1,3 +1,6 @@ +# Clear all models by deleting the whole file + +# Import required modules import os import sys @@ -6,20 +9,25 @@ 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 index 3b7641e..2b09848 100644 --- a/cli/help.py +++ b/cli/help.py @@ -1,3 +1,5 @@ +# Prints a simple help page for the CLI + print(""" Usage: howdy [argument] diff --git a/cli/list.py b/cli/list.py index 64e7480..ff37b10 100644 --- a/cli/list.py +++ b/cli/list.py @@ -1,18 +1,25 @@ +# 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: @@ -20,13 +27,20 @@ except FileNotFoundError: 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 index e4edf6a..4322839 100644 --- a/cli/remove.py +++ b/cli/remove.py @@ -1,23 +1,31 @@ +# 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: @@ -25,35 +33,45 @@ except FileNotFoundError: 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) diff --git a/compare.py b/compare.py index 6c5adc4..101f9fe 100644 --- a/compare.py +++ b/compare.py @@ -73,12 +73,17 @@ while True: # 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 diff --git a/installer.py b/installer.py index fc876a2..7759d71 100644 --- a/installer.py +++ b/installer.py @@ -80,7 +80,7 @@ handleStatus(subprocess.call(["rm", "-rf", "/tmp/dlib_clone"])) log("Installing face_recognition") -handleStatus(subprocess.call(["pip3", "install", "face_recognition", "sty"])) +handleStatus(subprocess.call(["pip3", "install", "face_recognition"])) log("Cloning howdy") From f084411861aae1070035ae072ad4617beae42253 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Tue, 13 Feb 2018 23:59:23 +0100 Subject: [PATCH 14/14] Uninstaller working and comments in installer --- README.md | 4 +++ installer.py | 78 ++++++++++++++++++++++++++++++++++++++++++++-------- uninstall.py | 34 ++++++++++++++++++++++- 3 files changed, 104 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 14af4b9..a6fda35 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,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. diff --git a/installer.py b/installer.py index 7759d71..52be40f 100644 --- a/installer.py +++ b/installer.py @@ -1,3 +1,7 @@ +# Installation script to install howdy +# Runs completely independent of the others + +# Import required modules import subprocess import time import sys @@ -8,87 +12,112 @@ 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="") @@ -104,76 +133,103 @@ subprocess.call(["sudo cp /lib/security/howdy/autocomplete.sh /etc/bash_completi 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() - cnt = 1 + while line: + # Add the line to the output directly, we're not deleting anything outlines.append(line) - if line[:1] != "#": + # 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" + 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) + 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") + printlines.append("\033[33m" + line_link + "\033[0m") + # Mark as inserted inserted = True - else: - printlines.append("\033[37m" + line + "\033[0m") + # Go to the next line line = fp.readline() - cnt += 1 +# 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 downloaded to tmp +# 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/uninstall.py b/uninstall.py index e3dd28f..8cc56a1 100644 --- a/uninstall.py +++ b/uninstall.py @@ -1,4 +1,36 @@ -import subprocess +# 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\ +""")