diff --git a/debian/control b/debian/control index 0d2ae80..76bb599 100644 --- a/debian/control +++ b/debian/control @@ -10,7 +10,7 @@ Package: howdy Homepage: https://github.com/boltgolt/howdy Architecture: all Depends: ${misc:Depends}, curl|wget, python3, python3-pip, python3-dev, python3-setuptools, libpam-python, libopencv-dev, cmake -Recommends: libatlas-base-dev | libopenblas-dev | liblapack-dev, streamer +Recommends: libatlas-base-dev | libopenblas-dev | liblapack-dev, streamer, howdy-gtk Suggests: nvidia-cuda-dev (>= 7.5) Description: Howdy: Windows Hello style authentication for Linux. Use your built-in IR emitters and camera in combination with face recognition diff --git a/howdy-gtk/debian/changelog b/howdy-gtk/debian/changelog new file mode 100644 index 0000000..627952a --- /dev/null +++ b/howdy-gtk/debian/changelog @@ -0,0 +1,5 @@ +howdy-gtk (0.0.1) xenial; urgency=medium + + * Initial testing release with sticky authentication ui + + -- boltgolt Thu, 03 Dec 2020 00:08:49 +0200 diff --git a/howdy-gtk/debian/compat b/howdy-gtk/debian/compat new file mode 100644 index 0000000..f599e28 --- /dev/null +++ b/howdy-gtk/debian/compat @@ -0,0 +1 @@ +10 diff --git a/howdy-gtk/debian/control b/howdy-gtk/debian/control new file mode 100644 index 0000000..c192788 --- /dev/null +++ b/howdy-gtk/debian/control @@ -0,0 +1,13 @@ +Source: howdy-gtk +Section: misc +Priority: optional +Standards-Version: 3.9.7 +Build-Depends: python, dh-python, devscripts, dh-make, debhelper, fakeroot +Maintainer: boltgolt +Vcs-Git: https://github.com/boltgolt/howdy + +Package: howdy-gtk +Homepage: https://github.com/boltgolt/howdy +Architecture: all +Depends: ${misc:Depends}, curl|wget, python3, python3-pip, python3-dev, python-gtk2, python-gtk2-dev, cmake +Description: Optional UI package for Howdy, written in Gtk diff --git a/howdy-gtk/debian/copyright b/howdy-gtk/debian/copyright new file mode 100644 index 0000000..38c6a9a --- /dev/null +++ b/howdy-gtk/debian/copyright @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 boltgolt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/howdy-gtk/debian/howdy-gtk.links b/howdy-gtk/debian/howdy-gtk.links new file mode 100644 index 0000000..a5a7a74 --- /dev/null +++ b/howdy-gtk/debian/howdy-gtk.links @@ -0,0 +1 @@ +/usr/lib/howdy-gtk/init.py /usr/bin/howdy-gtk diff --git a/howdy-gtk/debian/howdy-gtk.lintian-overrides b/howdy-gtk/debian/howdy-gtk.lintian-overrides new file mode 100644 index 0000000..250017d --- /dev/null +++ b/howdy-gtk/debian/howdy-gtk.lintian-overrides @@ -0,0 +1,2 @@ +# W: Don't require ugly linebreaks in last 5 chars +howdy: debian-changelog-line-too-long diff --git a/howdy-gtk/debian/install b/howdy-gtk/debian/install new file mode 100644 index 0000000..279038f --- /dev/null +++ b/howdy-gtk/debian/install @@ -0,0 +1 @@ +src/. /usr/lib/howdy-gtk diff --git a/howdy-gtk/debian/postinst b/howdy-gtk/debian/postinst new file mode 100644 index 0000000..1351668 --- /dev/null +++ b/howdy-gtk/debian/postinst @@ -0,0 +1,2 @@ +#!/bin/sh +pip3 install elevate diff --git a/howdy-gtk/debian/rules b/howdy-gtk/debian/rules new file mode 100755 index 0000000..08b0880 --- /dev/null +++ b/howdy-gtk/debian/rules @@ -0,0 +1,8 @@ +#!/usr/bin/make -f +DH_VERBOSE = 1 + +DPKG_EXPORT_BUILDFLAGS = 1 +include /usr/share/dpkg/default.mk + +%: + dh $@ diff --git a/howdy-gtk/debian/source/format b/howdy-gtk/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/howdy-gtk/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/howdy-gtk/debian/source/options b/howdy-gtk/debian/source/options new file mode 100644 index 0000000..1d6e494 --- /dev/null +++ b/howdy-gtk/debian/source/options @@ -0,0 +1,8 @@ +tar-ignore = ".git" +tar-ignore = ".gitignore" +tar-ignore = ".github" +tar-ignore = "README.md" +tar-ignore = ".travis.yml" +tar-ignore = "fedora" +tar-ignore = "opensuse" +tar-ignore = "archlinux" diff --git a/howdy-gtk/src/authsticky.py b/howdy-gtk/src/authsticky.py new file mode 100644 index 0000000..2c0c590 --- /dev/null +++ b/howdy-gtk/src/authsticky.py @@ -0,0 +1,157 @@ +# Shows a floating window when authenticating +import cairo +import gi +import signal +import sys +import os + +# Make sure we have the libs we need +gi.require_version("Gtk", "3.0") +gi.require_version("Gdk", "3.0") + +# Import them +from gi.repository import Gtk as gtk +from gi.repository import Gdk as gdk +from gi.repository import GObject as gobject + +# Set window size constants +windowWidth = 400 +windowHeight = 100 + +# Set default messages to show in the popup +message = "Starting up... " +subtext = "" + + +class StickyWindow(gtk.Window): + def __init__(self): + """Initialize the sticky window""" + # Make the class a GTK window + gtk.Window.__init__(self) + + # Set the title of the window + self.set_title("Howdy Authentication UI") + + # Set a bunch of options to make the window stick and be on top of everything + self.stick() + self.set_gravity(gdk.Gravity.STATIC) + self.set_resizable(False) + self.set_keep_above(True) + self.set_app_paintable(True) + self.set_skip_pager_hint(True) + self.set_skip_taskbar_hint(True) + self.set_can_focus(False) + self.set_can_default(False) + self.set_focus(None) + self.set_type_hint(gdk.WindowTypeHint.NOTIFICATION) + self.set_decorated(False) + + # Listen for a window redraw + self.connect("draw", self.draw) + # Listen for a force close or click event and exit + self.connect("destroy", self.exit) + self.connect("delete_event", self.exit) + self.connect("button-press-event", self.exit) + + # Create a GDK drawing, restricts the window size + darea = gtk.DrawingArea() + darea.set_size_request(windowWidth, windowHeight) + self.add(darea) + + # Get the default screen + screen = gdk.Screen.get_default() + visual = screen.get_rgba_visual() + if visual and screen.is_composited(): + self.set_visual(visual) + + # Move the window to the center top of the default window, where a webcam usually is + self.move((screen.get_width() / 2) - (windowWidth / 2), 0) + + # Show window and force a resize again + self.show_all() + self.resize(windowWidth, windowHeight) + + # Add a timeout to catch input passed from compare.py + gobject.timeout_add(100, self.catch_stdin) + + # Start GTK main loop + gtk.main() + + def draw(self, widget, ctx): + """Draw the UI""" + # Change cursor to the kill icon + self.get_window().set_cursor(gdk.Cursor(gdk.CursorType.PIRATE)) + + # Draw a semi transparent background + ctx.set_source_rgba(0, 0, 0, .7) + ctx.set_operator(cairo.OPERATOR_SOURCE) + ctx.paint() + ctx.set_operator(cairo.OPERATOR_OVER) + + # Get absolute or relative logo path + path = "/usr/lib/howdy-gtk/logo.png" + if not os.access(path, os.R_OK): + path = "./logo.png" + + # Create image and calculate scale size based on image size + image_surface = cairo.ImageSurface.create_from_png(path) + ratio = float(windowHeight - 20) / float(image_surface.get_height()) + + # Position and draw the logo + ctx.translate(15, 10) + ctx.scale(ratio, ratio) + ctx.set_source_surface(image_surface) + ctx.paint() + + # Calculate main message positioning, as the text is heigher if there's a subtext + if subtext: + ctx.move_to(380, 145) + else: + ctx.move_to(380, 170) + + # Draw the main message + ctx.set_source_rgba(255, 255, 255, .9) + ctx.set_font_size(80) + ctx.select_font_face("Arial", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) + ctx.show_text(message) + + # Draw the subtext if there is one + if subtext: + ctx.move_to(380, 210) + ctx.set_source_rgba(230, 230, 230, .8) + ctx.set_font_size(40) + ctx.select_font_face("Arial", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) + ctx.show_text(subtext) + + def catch_stdin(self): + """Catch input from stdin and redraw""" + global message, subtext + + # Wait for a line on stdin + comm = sys.stdin.readline()[:-1] + + # If the line is not empty + if comm: + # Parse a message + if comm[0] == "M": + message = comm[2:] + # Parse subtext + if comm[0] == "S": + subtext = comm[2:] + + # Redraw the ui + self.queue_draw() + + # Fire this function again in 10ms, as we're waiting on IO in readline anyway + gobject.timeout_add(10, self.catch_stdin) + + def exit(self, widget, context): + """Cleanly exit""" + gtk.main_quit() + + +# Make sure we quit on a SIGINT +signal.signal(signal.SIGINT, signal.SIG_DFL) + +# Open the GTK window +window = StickyWindow() diff --git a/howdy-gtk/src/i18n.py b/howdy-gtk/src/i18n.py new file mode 100644 index 0000000..2b3afb2 --- /dev/null +++ b/howdy-gtk/src/i18n.py @@ -0,0 +1,12 @@ +# Support file for translations + +# Import modules +import gettext +import os + +# Get the right translation based on locale, falling back to base if none found +translation = gettext.translation("gtk", localedir=os.path.join(os.path.dirname(__file__), "locales"), fallback=True) +translation.install() + +# Export translation function as _ +_ = translation.gettext diff --git a/howdy-gtk/src/init.py b/howdy-gtk/src/init.py new file mode 100755 index 0000000..01a7bb3 --- /dev/null +++ b/howdy-gtk/src/init.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +# Opens auth ui if requested, otherwise starts normal ui +import sys + +if "--start-auth-ui" in sys.argv: + import authsticky +else: + import window diff --git a/howdy-gtk/src/logo.png b/howdy-gtk/src/logo.png new file mode 100644 index 0000000..78c620b Binary files /dev/null and b/howdy-gtk/src/logo.png differ diff --git a/howdy-gtk/src/logo_about.png b/howdy-gtk/src/logo_about.png new file mode 100644 index 0000000..3d0c12e Binary files /dev/null and b/howdy-gtk/src/logo_about.png differ diff --git a/howdy-gtk/src/main.glade b/howdy-gtk/src/main.glade new file mode 100644 index 0000000..7cb12e7 --- /dev/null +++ b/howdy-gtk/src/main.glade @@ -0,0 +1,512 @@ + + + + + + True + False + 5 + gtk-add + + + True + False + 5 + gtk-add + + + True + False + 5 + gtk-delete + + + False + 5 + Howdy Configuration + center + logo.png + + + True + True + 2 + left + False + + + + True + False + vertical + + + True + False + 10 + 10 + 10 + 5 + + + True + False + center + Showing saved models for: + + + False + False + 0 + + + + + True + False + + + + False + False + 1 + + + + + Add new user + True + True + True + 15 + iconadduser + none + True + + + + False + False + end + 3 + + + + + False + True + 0 + + + + + True + False + 10 + 10 + vertical + + + True + False + 10 + 10 + + + False + True + 0 + + + + + False + True + 1 + + + + + True + False + 10 + 8 + 10 + + + + + + Add + True + True + True + iconadd + 0.5899999737739563 + True + + + + False + True + end + 1 + + + + + Delete + True + True + True + 11 + icondelete + True + + + + False + True + end + 2 + + + + + False + True + end + 2 + + + + + + + True + False + 10 + 10 + Models + + + 1 + False + + + + + True + False + 10 + 10 + 10 + 10 + + + True + False + 10 + vertical + + + True + False + start + Camera ID: + + + + + + False + True + 0 + + + + + True + False + start + True + end + + + False + True + 1 + + + + + True + False + start + 10 + Real resolution: + + + + + + False + True + 2 + + + + + True + False + start + True + + + False + True + 3 + + + + + True + False + start + 10 + Used resolution: + + + + + + False + True + 4 + + + + + True + False + start + True + + + False + True + 5 + + + + + True + False + start + 10 + Recorder: + + + + + + False + True + 6 + + + + + True + False + start + True + + + False + True + 7 + + + + + + + + False + True + end + 0 + + + + + 300 + 300 + True + False + + + True + False + gtk-execute + 6 + + + + + False + True + 1 + + + + + + + + 1 + + + + + True + False + Video + + + 1 + False + + + + + True + False + center + center + vertical + + + True + False + 20 + 12 + logo_about.png + + + False + True + 0 + + + + + True + False + Howdy + + + + + + False + True + 1 + + + + + True + False + 5 + Facial authentication for Linux + + + False + True + 2 + + + + + True + False + center + center + 15 + 35 + + + True + False + 3 + <a href="https://github.com/boltgolt/howdy">Open GitHub link</a> + True + False + + + + + + + + + False + True + 1 + + + + + True + False + 3 + <a href="https://www.buymeacoffee.com/boltgolt">Donate to the project</a> + True + + + + + + + + + False + True + 2 + + + + + False + True + 3 + + + + + 2 + + + + + True + False + About + + + 2 + False + + + + + + + + + + + + diff --git a/howdy-gtk/src/onboarding.glade b/howdy-gtk/src/onboarding.glade new file mode 100644 index 0000000..cd4dbe2 --- /dev/null +++ b/howdy-gtk/src/onboarding.glade @@ -0,0 +1,697 @@ + + + + + + True + False + 4 + gtk-cancel + + + True + False + 5 + gtk-apply + + + True + False + 4 + gtk-go-forward + + + True + False + 5 + gtk-media-play + + + 500 + 400 + False + Welcome to Howdy + center + logo.png + menu + center + + + True + False + immediate + vertical + + + False + True + vertical + + + True + False + 20 + Setup is done! + + + + + + False + True + 0 + + + + + True + False + 10 + 10 + 10 + 20 + We're done! Howdy is now active on this computer. Try doing anything you would normally have to type your password for to authenticate, like running a command with sudo. + +You can open the Howdy Configurator later on to change more advanced settings or add additional models. Press Finish below to close this window and open the Howdy Configurator. + center + True + + + False + True + 1 + + + + + False + True + 0 + + + + + False + True + vertical + + + True + False + 20 + Setting a certainty policy + + + + + + False + True + 0 + + + + + True + False + 10 + 10 + 10 + 40 + Because of changes in angles, distance, and other factors a face match is never exactly the same as the stored face model. On this page you can set how strict Howdy should be. + center + True + + + False + True + 1 + + + + + True + False + 60 + 60 + vertical + + + True + True + False + 10 + 0 + 0.50999999046325684 + True + True + radiobalanced + + + True + False + 5 + vertical + + + True + False + start + Fast + + + + + + False + True + 0 + + + + + True + False + start + Allows more fuzzy matches, +but speeds up the scanning process greatly. + True + + + False + True + 1 + + + + + + + False + True + 0 + + + + + True + True + False + 10 + 0 + True + True + + + True + False + 5 + vertical + + + True + False + start + Balanced + + + + + + False + True + 0 + + + + + True + False + start + Still relatively quick detection, +but might not log you in when further away. + True + + + False + True + 1 + + + + + + + False + True + 1 + + + + + True + True + False + 0 + True + True + radiobalanced + + + True + False + 5 + vertical + + + True + False + start + Secure + + + + + + False + False + 0 + + + + + True + False + start + The slightly safer option, +but will take much longer to authenticate you + True + + + False + False + 1 + + + + + + + False + True + 2 + + + + + False + True + 2 + + + + + False + True + 1 + + + + + False + True + vertical + + + True + False + 20 + Adding a face model + + + + + + False + True + 0 + + + + + True + False + 10 + 10 + 10 + 20 + To authenticate you Howdy needs to save a model of your face to recognise you. Press the Scan button below to start the facial scan. + center + True + + + False + True + 1 + + + + + True + False + + + + + + Start face scan + True + True + True + True + True + 50 + 50 + iconscan + none + right + True + + + + True + True + 1 + + + + + + + + False + True + 2 + + + + + False + True + 2 + + + + + False + True + vertical + + + True + False + 20 + Configuring webcam + + + + + + False + True + 0 + + + + + True + False + 10 + 10 + 10 + 20 + Howdy will search your system automatically for any available cameras, so make sure your webcam is connected. After detection a list of usable webcams will be shown. Pick the one you want to use and click Next. + center + True + + + False + True + 1 + + + + + True + False + 0.89000000000000001 + 10 + 10 + vertical + + + True + False + 15 + Testing your webcams, please wait... + + + + + + False + True + 0 + + + + + + + + False + True + 2 + + + + + False + True + 3 + + + + + False + True + 10 + vertical + + + True + False + 20 + Downloading data files + + + + + + False + True + 0 + + + + + True + False + 10 + 10 + 10 + 20 + Howdy needs three pre trained facial recognition datasets to be able to recognise you, which will be downloaded now. You can see the download progress below. + center + True + + + False + True + 1 + + + + + True + False + + + True + False + 10 + 10 + 10 + Starting download... + center + + + + + + + + + True + True + 3 + + + + + True + True + 4 + + + + + True + False + vertical + + + False + 20 + 10 + 7 + 13 + logo_about.png + + + False + True + 0 + + + + + True + False + 5 + Welcome to Howdy! + + + + + + False + True + 1 + + + + + 100 + True + False + center + center + 20 + 20 + 10 + This wizard will walk you through the setup process and automatically configure Howdy for you. Press next to continue. + center + True + + + False + True + 2 + + + + + False + True + 5 + + + + + True + False + 10 + + + Cancel + True + True + True + 10 + iconcancel + True + + + + False + True + 0 + + + + + + + + Next + True + True + True + True + True + 10 + iconforward + none + right + True + + + + False + True + end + 2 + + + + + Finish setup + True + True + True + 10 + iconfinish + none + True + + + + False + True + end + 3 + + + + + False + True + end + 8 + + + + + + diff --git a/howdy-gtk/src/onboarding.py b/howdy-gtk/src/onboarding.py new file mode 100644 index 0000000..affa4fa --- /dev/null +++ b/howdy-gtk/src/onboarding.py @@ -0,0 +1,288 @@ +import sys +import os +import re +import time +import subprocess + +from i18n import _ + +from gi.repository import Gtk as gtk +from gi.repository import Gdk as gdk +from gi.repository import GObject as gobject +from gi.repository import Pango as pango + + +class OnboardingWindow(gtk.Window): + def __init__(self): + """Initialize the sticky window""" + # Make the class a GTK window + gtk.Window.__init__(self) + + self.connect("destroy", self.exit) + self.connect("delete_event", self.exit) + + self.builder = gtk.Builder() + self.builder.add_from_file("./onboarding.glade") + self.builder.connect_signals(self) + + self.window = self.builder.get_object("onboardingwindow") + self.nextbutton = self.builder.get_object("nextbutton") + + self.slides = [ + self.builder.get_object("slide0"), + self.builder.get_object("slide1"), + self.builder.get_object("slide2"), + self.builder.get_object("slide3"), + self.builder.get_object("slide4"), + self.builder.get_object("slide5") + ] + + self.window.show_all() + self.window.resize(500, 400) + + self.window.current_slide = 0 + + # Start GTK main loop + gtk.main() + + def go_next_slide(self, button=None): + self.nextbutton.set_sensitive(False) + + self.slides[self.window.current_slide].hide() + self.slides[self.window.current_slide + 1].show() + self.window.current_slide += 1 + + if self.window.current_slide == 1: + self.execute_slide1() + elif self.window.current_slide == 2: + gobject.timeout_add(10, self.execute_slide2) + elif self.window.current_slide == 3: + self.execute_slide3() + elif self.window.current_slide == 4: + self.execute_slide4() + elif self.window.current_slide == 5: + self.execute_slide5() + + def execute_slide1(self): + self.downloadoutputlabel = self.builder.get_object("downloadoutputlabel") + eventbox = self.builder.get_object("downloadeventbox") + eventbox.modify_bg(gtk.StateType.NORMAL, gdk.Color(red=0, green=0, blue=0)) + + if os.path.exists("/lib/security/howdy/dlib-data/shape_predictor_5_face_landmarks.dat"): + self.downloadoutputlabel.set_text(_("Datafiles have already been downloaded!\nClick Next to continue")) + self.enable_next() + return + + self.proc = subprocess.Popen("./install.sh", stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd="/lib/security/howdy/dlib-data") + + self.download_lines = [] + self.read_download_line() + + def read_download_line(self): + line = self.proc.stdout.readline() + self.download_lines.append(line.decode("utf-8")) + + print("install.sh output:") + print(line.decode("utf-8")) + + if len(self.download_lines) > 10: + self.download_lines.pop(0) + + self.downloadoutputlabel.set_text(" ".join(self.download_lines)) + + if line: + gobject.timeout_add(10, self.read_download_line) + return + + # Wait for the process to finish and check the status code + if self.proc.wait(5) != 0: + self.show_error(_("Error while downloading datafiles"), " ".join(self.download_lines)) + + self.downloadoutputlabel.set_text(_("Done!\nClick Next to continue")) + self.enable_next() + + def execute_slide2(self): + def is_gray(frame): + for row in frame: + for pixel in row: + if not pixel[0] == pixel[1] == pixel[2]: + return False + return True + + try: + import cv2 + except Exception: + self.show_error(_("Error while importing OpenCV2"), _("Try reinstalling cv2")) + + device_ids = os.listdir("/dev/v4l/by-path") + device_rows = [] + + if not device_ids: + self.show_error(_("No webcams found on system"), _("Please configure your camera yourself if you are sure a compatible camera is connected")) + + # Loop though all devices + for dev in device_ids: + time.sleep(.5) + + # The full path to the device is the default name + device_path = "/dev/v4l/by-path/" + dev + device_name = dev + + # Get the udevadm details to try to get a better name + udevadm = subprocess.check_output(["udevadm info -r --query=all -n " + device_path], 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) + + try: + capture = cv2.VideoCapture(device_path) + capture.grab() + ret, frame = capture.read() + except Exception: + device_rows.append([device_name, device_path, -9, _("No, camera can't be opened")]) + continue + + if not is_gray(frame): + device_rows.append([device_name, device_path, -5, _("No, not an infrared camera")]) + continue + + time.sleep(.2) + + ret, frame = capture.read() + + if not is_gray(frame): + device_rows.append([device_name, device_path, -5, _("No, not an infrared camera")]) + continue + + device_rows.append([device_name, device_path, 5, _("Yes, compatible infrared camera")]) + + capture.release() + + device_rows = sorted(device_rows, key=lambda k: -k[2]) + + self.loadinglabel = self.builder.get_object("loadinglabel") + self.devicelistbox = self.builder.get_object("devicelistbox") + + self.treeview = gtk.TreeView() + self.treeview.set_vexpand(True) + + # Set the coloums + for i, column in enumerate([_("Camera identifier or path"), _("Recommended")]): + cell = gtk.CellRendererText() + cell.set_property("ellipsize", pango.EllipsizeMode.END) + col = gtk.TreeViewColumn(column, cell, text=i) + self.treeview.append_column(col) + + # Add the treeview + self.devicelistbox.add(self.treeview) + + # Create a datamodel + self.listmodel = gtk.ListStore(str, str, str) + + for device in device_rows: + self.listmodel.append([device[0], device[3], device[1]]) + + self.treeview.set_model(self.listmodel) + self.treeview.set_cursor(0) + + self.loadinglabel.hide() + self.treeview.show() + self.enable_next() + + def execute_slide3(self): + selection = self.treeview.get_selection() + (listmodel, rowlist) = selection.get_selected_rows() + + if len(rowlist) != 1: + self.show_error(_("Error selecting camera")) + + device_path = listmodel.get_value(listmodel.get_iter(rowlist[0]), 2) + self.proc = subprocess.Popen("howdy set device_path " + device_path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True) + + self.window.set_focus(self.builder.get_object("scanbutton")) + + def on_scanbutton_click(self, button): + status = self.proc.wait(2) + + # if status != 0: + # self.show_error(_("Error setting camera path"), _("Please set the camera path manually")) + + self.dialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL) + self.dialog.set_title(_("Creating Model")) + self.dialog.props.text = _("Please look directly into the camera") + self.dialog.show_all() + + # Wait a bit to allow the user to read the dialog + gobject.timeout_add(600, self.run_add) + + def run_add(self): + status, output = subprocess.getstatusoutput(["howdy add -y"]) + + print("howdy add output:") + print(output) + + self.dialog.destroy() + + if status != 0: + self.show_error(_("Can't save face model"), output) + + gobject.timeout_add(10, self.go_next_slide) + + def execute_slide4(self): + self.enable_next() + + def execute_slide5(self): + radio_buttons = self.builder.get_object("radiobalanced").get_group() + radio_selected = False + radio_certanty = 5.0 + + for button in radio_buttons: + if button.get_active(): + radio_selected = gtk.Buildable.get_name(button) + + if not radio_selected: + self.show_error(_("Error reading radio buttons")) + elif radio_selected == "radiofast": + radio_certanty = 4.2 + elif radio_selected == "radiobalanced": + radio_certanty = 3.5 + elif radio_selected == "radiosecure": + radio_certanty = 2.2 + + self.proc = subprocess.Popen("howdy set certainty " + str(radio_certanty), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True) + + self.nextbutton.hide() + self.builder.get_object("cancelbutton").hide() + + finishbutton = self.builder.get_object("finishbutton") + finishbutton.show() + self.window.set_focus(finishbutton) + + status = self.proc.wait(2) + + if status != 0: + self.show_error(_("Error setting certainty"), _("Certainty is set to the default value, Howdy setup is complete")) + + def enable_next(self): + self.nextbutton.set_sensitive(True) + self.window.set_focus(self.nextbutton) + + def show_error(self, error, secon=""): + dialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL, type=gtk.MessageType.ERROR, buttons=gtk.ButtonsType.CLOSE) + dialog.set_title(_("Howdy Error")) + dialog.props.text = error + dialog.format_secondary_text(secon) + + dialog.run() + + dialog.destroy() + self.exit() + + def exit(self, widget=None): + """Cleanly exit""" + gtk.main_quit() + sys.exit(0) diff --git a/howdy-gtk/src/tab_models.py b/howdy-gtk/src/tab_models.py new file mode 100644 index 0000000..fe61619 --- /dev/null +++ b/howdy-gtk/src/tab_models.py @@ -0,0 +1,118 @@ +import subprocess +import time + +from i18n import _ +from gi.repository import Gtk as gtk + + +def on_user_change(self, select): + self.active_user = select.get_active_text() + self.load_model_list() + + +def on_user_add(self, button): + # Open question dialog + dialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL, type=gtk.MessageType.QUESTION, buttons=gtk.ButtonsType.OK_CANCEL) + dialog.set_title(_("Confirm User Creation")) + dialog.props.text = _("Please enter the username of the user you want to add to Howdy") + + # Create the input field + entry = gtk.Entry() + + # Add a label to ask for a model name + hbox = gtk.HBox() + hbox.pack_start(gtk.Label(_("Username:")), False, 5, 5) + hbox.pack_end(entry, True, True, 5) + + # Add the box and show the dialog + dialog.vbox.pack_end(hbox, True, True, 0) + dialog.show_all() + + # Show dialog + response = dialog.run() + + entered_user = entry.get_text() + dialog.destroy() + + if response == gtk.ResponseType.OK: + self.userlist.append_text(entered_user) + self.userlist.set_active(self.userlist.items) + self.userlist.items += 1 + + self.active_user = entered_user + self.load_model_list() + + +def on_model_add(self, button): + # Open question dialog + dialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL, type=gtk.MessageType.QUESTION, buttons=gtk.ButtonsType.OK_CANCEL) + dialog.set_title(_("Confirm Model Creation")) + dialog.props.text = _("Please enter a name for the new model, 24 characters max") + + # Create the input field + entry = gtk.Entry() + + # Add a label to ask for a model name + hbox = gtk.HBox() + hbox.pack_start(gtk.Label(_("Model name:")), False, 5, 5) + hbox.pack_end(entry, True, True, 5) + + # Add the box and show the dialog + dialog.vbox.pack_end(hbox, True, True, 0) + dialog.show_all() + + # Show dialog + response = dialog.run() + + entered_name = entry.get_text() + dialog.destroy() + + if response == gtk.ResponseType.OK: + dialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL) + dialog.set_title(_("Creating Model")) + dialog.props.text = _("Please look directly into the camera") + dialog.show_all() + + time.sleep(1) + + status, output = subprocess.getstatusoutput(["howdy add -y -U " + self.active_user + " '" + entered_name + "'"]) + + dialog.destroy() + + if status != 0: + dialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL, type=gtk.MessageType.ERROR, buttons=gtk.ButtonsType.CLOSE) + dialog.set_title(_("Howdy Error")) + dialog.props.text = _("Error while adding model, error code {}: \n\n").format(str(status)) + dialog.format_secondary_text(output) + dialog.run() + dialog.destroy() + + self.load_model_list() + + +def on_model_delete(self, button): + selection = self.treeview.get_selection() + (listmodel, rowlist) = selection.get_selected_rows() + + if len(rowlist) == 1: + id = listmodel.get_value(listmodel.get_iter(rowlist[0]), 0) + name = listmodel.get_value(listmodel.get_iter(rowlist[0]), 2) + + dialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL, buttons=gtk.ButtonsType.OK_CANCEL) + dialog.set_title(_("Confirm Model Deletion")) + dialog.props.text = _("Are you sure you want to delete model {id} ({name})?").format(id=id, name=name) + response = dialog.run() + dialog.destroy() + + if response == gtk.ResponseType.OK: + status, output = subprocess.getstatusoutput(["howdy remove " + id + " -y -U " + self.active_user]) + + if status != 0: + dialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL, type=gtk.MessageType.ERROR, buttons=gtk.ButtonsType.CLOSE) + dialog.set_title(_("Howdy Error")) + dialog.props.text = _("Error while deleting model, error code {}: \n\n").format(status) + dialog.format_secondary_text(output) + dialog.run() + dialog.destroy() + + self.load_model_list() diff --git a/howdy-gtk/src/tab_video.py b/howdy-gtk/src/tab_video.py new file mode 100644 index 0000000..cd67740 --- /dev/null +++ b/howdy-gtk/src/tab_video.py @@ -0,0 +1,79 @@ +import configparser + +from i18n import _ + +from gi.repository import Gtk as gtk +from gi.repository import Gdk as gdk +from gi.repository import GdkPixbuf as pixbuf +from gi.repository import GObject as gobject + +MAX_HEIGHT = 300 +MAX_WIDTH = 300 + + +def on_page_switch(self, notebook, page, page_num): + if page_num == 1: + path = "/dev/video1" + + try: + self.config = configparser.ConfigParser() + self.config.read("/lib/security/howdy/config.ini") + except Exception: + print(_("Can't open camera")) + + try: + # if not self.cv2: + import cv2 + self.cv2 = cv2 + except Exception: + print(_("Can't import OpenCV2")) + + try: + self.capture = cv2.VideoCapture(self.config.get("video", "device_path")) + except Exception: + print(_("Can't open camera")) + + opencvbox = self.builder.get_object("opencvbox") + opencvbox.modify_bg(gtk.StateType.NORMAL, gdk.Color(red=0, green=0, blue=0)) + + height = self.capture.get(self.cv2.CAP_PROP_FRAME_HEIGHT) or 1 + width = self.capture.get(self.cv2.CAP_PROP_FRAME_WIDTH) or 1 + + self.scaling_factor = (MAX_HEIGHT / height) or 1 + + if width * self.scaling_factor > MAX_WIDTH: + self.scaling_factor = (MAX_WIDTH / width) or 1 + + config_height = self.config.getfloat("video", "max_height", fallback=0.0) + config_scaling = (config_height / height) or 1 + + self.builder.get_object("videoid").set_text(path.split("/")[-1]) + self.builder.get_object("videores").set_text(str(int(width)) + "x" + str(int(height))) + self.builder.get_object("videoresused").set_text(str(int(width * config_scaling)) + "x" + str(int(height * config_scaling))) + self.builder.get_object("videorecorder").set_text(self.config.get("video", "recording_plugin", fallback=_("Unknown"))) + + gobject.timeout_add(10, self.capture_frame) + + elif self.capture is not None: + self.capture.release() + self.capture = None + + +def capture_frame(self): + if self.capture is None: + return + + ret, frame = self.capture.read() + + frame = self.cv2.resize(frame, None, fx=self.scaling_factor, fy=self.scaling_factor, interpolation=self.cv2.INTER_AREA) + + retval, buffer = self.cv2.imencode(".png", frame) + + loader = pixbuf.PixbufLoader() + loader.write(buffer) + loader.close() + buffer = loader.get_pixbuf() + + self.opencvimage.set_from_pixbuf(buffer) + + gobject.timeout_add(20, self.capture_frame) diff --git a/howdy-gtk/src/window.py b/howdy-gtk/src/window.py new file mode 100644 index 0000000..1bd58ff --- /dev/null +++ b/howdy-gtk/src/window.py @@ -0,0 +1,137 @@ +# Opens and controls main ui window +import gi +import signal +import sys +import os +import elevate +import subprocess + +from i18n import _ + +# Make sure we have the libs we need +gi.require_version("Gtk", "3.0") +gi.require_version("Gdk", "3.0") + +# Import them +from gi.repository import Gtk as gtk + + +class MainWindow(gtk.Window): + def __init__(self): + """Initialize the sticky window""" + # Make the class a GTK window + gtk.Window.__init__(self) + + self.connect("destroy", self.exit) + self.connect("delete_event", self.exit) + + self.builder = gtk.Builder() + self.builder.add_from_file("./main.glade") + self.builder.connect_signals(self) + + self.window = self.builder.get_object("mainwindow") + self.userlist = self.builder.get_object("userlist") + self.modellistbox = self.builder.get_object("modellistbox") + self.opencvimage = self.builder.get_object("opencvimage") + + # Init capture for video tab + self.capture = None + + # Create a treeview that will list the model data + self.treeview = gtk.TreeView() + self.treeview.set_vexpand(True) + + # Set the coloums + for i, column in enumerate([_("ID"), _("Created"), _("Label")]): + col = gtk.TreeViewColumn(column, gtk.CellRendererText(), text=i) + self.treeview.append_column(col) + + # Add the treeview + self.modellistbox.add(self.treeview) + + filelist = os.listdir("/lib/security/howdy/models") + self.active_user = "" + + self.userlist.items = 0 + + for file in filelist: + self.userlist.append_text(file[:-4]) + self.userlist.items += 1 + + if not self.active_user: + self.active_user = file[:-4] + + self.userlist.set_active(0) + + self.window.show_all() + + # Start GTK main loop + gtk.main() + + def load_model_list(self): + """(Re)load the model list""" + + # Execute the list commond to get the models + # status, output = subprocess.getstatusoutput(["howdy list --plain -U " + self.active_user]) + status = 0 + output = "1,2020-12-05 14:10:22,sd\n2,2020-12-05 14:22:41,\n3,2020-12-05 14:57:37,Model #3" + self.active_user + + # Create a datamodel + self.listmodel = gtk.ListStore(str, str, str) + + # If there was no error + if status == 0: + # Split the output per line + # lines = output.decode("utf-8").split("\n") + lines = output.split("\n") + + # Add the models to the datamodel + for i in range(len(lines)): + self.listmodel.append(lines[i].split(",")) + + self.treeview.set_model(self.listmodel) + + def on_about_link(self, label, uri): + """Open links on about page as a non-root user""" + try: + user = os.getlogin() + except Exception: + user = os.environ.get("SUDO_USER") + + status, output = subprocess.getstatusoutput(["sudo -u " + user + " timeout 10 xdg-open " + uri]) + return True + + def exit(self, widget, context): + """Cleanly exit""" + if self.capture is not None: + self.capture.release() + + gtk.main_quit() + sys.exit(0) + + +# Make sure we quit on a SIGINT +signal.signal(signal.SIGINT, signal.SIG_DFL) + +# Make sure we run as sudo +elevate.elevate() + +# If no models have been created yet or when it is forced, start the onboarding +if "--force-onboarding" in sys.argv or not os.path.exists("/lib/security/howdy/models"): + import onboarding + onboarding.OnboardingWindow() + + sys.exit(0) + +# Class is split so it isn't too long, import split functions +import tab_models +MainWindow.on_user_add = tab_models.on_user_add +MainWindow.on_user_change = tab_models.on_user_change +MainWindow.on_model_add = tab_models.on_model_add +MainWindow.on_model_delete = tab_models.on_model_delete +import tab_video +MainWindow.on_page_switch = tab_video.on_page_switch +MainWindow.capture_frame = tab_video.capture_frame + +# Open the GTK window +window = MainWindow() diff --git a/src/cli/test.py b/src/cli/test.py index ae66a90..8933797 100644 --- a/src/cli/test.py +++ b/src/cli/test.py @@ -55,7 +55,7 @@ use_cnn = config.getboolean('core', 'use_cnn', fallback=False) if use_cnn: face_detector = dlib.cnn_face_detection_model_v1( - path + '/../dlib-data/mmod_human_face_detector.dat' + path + "/../dlib-data/mmod_human_face_detector.dat" ) else: face_detector = dlib.get_frontal_face_detector() diff --git a/src/compare.py b/src/compare.py index 902eaaf..e75e489 100644 --- a/src/compare.py +++ b/src/compare.py @@ -17,6 +17,7 @@ import configparser import dlib import cv2 import datetime +import subprocess import snapshot import numpy as np import _thread as thread @@ -25,6 +26,18 @@ from recorders.video_capture import VideoCapture from i18n import _ +def exit(code): + """Exit while closeing howdy-gtk properly""" + global gtk_proc + + # Exit the auth ui process if there is one + if "gtk_proc" in globals(): + gtk_proc.terminate() + + # Exit compare + sys.exit(code) + + def init_detector(lock): """Start face detector, encoder and predictor in a new thread""" global face_detector, pose_predictor, face_encoder @@ -35,7 +48,7 @@ def init_detector(lock): print("\n\tcd " + PATH + "/dlib-data") print("\tsudo ./install.sh\n") lock.release() - sys.exit(1) + exit(1) # Use the CNN detector if enabled if use_cnn: @@ -64,9 +77,26 @@ def make_snapshot(type): ]) +def send_to_ui(type, message): + """Send message to the auth ui""" + global gtk_proc + + # Only execute of the proccess started + if 'gtk_proc' in globals(): + # Format message so the ui can parse it + message = type + "=" + message + " \n" + + # Try to send the message to the auth ui, but it's okay if that fails + try: + gtk_proc.stdin.write(bytearray(message.encode("ascii"))) + gtk_proc.stdin.flush() + except IOError as err: + pass + + # Make sure we were given an username to tast against if len(sys.argv) < 2: - sys.exit(12) + exit(12) # Get the absolute path to the current directory PATH = os.path.abspath(__file__ + "/..") @@ -92,6 +122,15 @@ face_detector = None pose_predictor = None face_encoder = None +# Start the auth ui +try: + gtk_proc = subprocess.Popen(["howdy-gtk", "--start-auth-ui"], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) +except FileNotFoundError as err: + pass + +# Write to the stdin to redraw ui +send_to_ui("M", "Starting up...") + # Try to load the face model from the models folder try: models = json.load(open(PATH + "/models/" + user + ".dat")) @@ -99,11 +138,11 @@ try: for model in models: encodings += model["data"] except FileNotFoundError: - sys.exit(10) + exit(10) # Check if the file contains a model if len(models) < 1: - sys.exit(10) + exit(10) # Read config from disk config = configparser.ConfigParser() @@ -158,18 +197,29 @@ timeout = config.getint("video", "timeout") dark_threshold = config.getfloat("video", "dark_threshold") end_report = config.getboolean("debug", "end_report") +# Initiate histogram equalization +clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) + +# Let the ui know that we're ready +send_to_ui("M", "Identifying you...") + # Start the read loop frames = 0 valid_frames = 0 timings["fr"] = time.time() dark_running_total = 0 -clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) - while True: # Increment the frame count every loop frames += 1 + # Form a string to let the user know we're real busy + ui_subtext = "Scanned " + str(valid_frames - dark_tries) + " frames" + if (dark_tries > 1): + ui_subtext += " (skipped " + str(dark_tries) + " dark frames)" + # Show it in the ui as subtext + send_to_ui("S", ui_subtext) + # Stop if we've exceded the time limit if time.time() - timings["fr"] > timeout: # Create a timeout snapshot if enabled @@ -179,9 +229,9 @@ while True: if dark_tries == valid_frames: print(_("All frames were too dark, please check dark_threshold in config")) print(_("Average darkness: {avg}, Threshold: {threshold}").format(avg=str(dark_running_total / max(1, valid_frames)), threshold=str(dark_threshold))) - sys.exit(13) + exit(13) else: - sys.exit(11) + exit(11) # Grab a single frame of video frame, gsframe = video_capture.read_frame() @@ -295,7 +345,7 @@ while True: }) # End peacefully - sys.exit(0) + exit(0) if exposure != -1: # For a strange reason on some cameras (e.g. Lenoxo X1E) diff --git a/src/dlib-data/install.sh b/src/dlib-data/install.sh index 5698f01..f74a09f 100755 --- a/src/dlib-data/install.sh +++ b/src/dlib-data/install.sh @@ -21,5 +21,6 @@ else fi # Uncompress the data files and delete the original archive +echo " " echo "Unpacking..." bzip2 -d -f *.bz2