Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ jobs:
steps:
- name: Install required libraries
run: >
sudo apt-get update && sudo apt-get install -y
sudo apt-get update && sudo apt-get install -y
python3 python3-pip python3-setuptools python3-wheel
cmake make build-essential clang-tidy
libpam0g-dev libinih-dev libevdev-dev
libpam0g-dev libinih-dev libevdev-dev
python3-dev libopencv-dev

- name: Install meson
run: sudo python3 -m pip install meson ninja

- uses: actions/checkout@v2
- uses: actions/checkout@v4

- name: Build
run: |
Expand All @@ -26,3 +26,27 @@ jobs:
- name: Check source code
run: |
ninja clang-tidy -C build

python-lint:
runs-on: ubuntu-latest
container: fedora:41
steps:
- uses: actions/checkout@v4
- name: Install Python
run: dnf install -y python3.12 python3-pip
- name: Install tools
run: pip3 install ruff
- name: Ruff lint
run: ruff check .
- name: Ruff format check
run: ruff format --check .
- name: Import / syntax test
run: python3.12 -c "
import py_compile, glob, sys;
files = glob.glob('howdy/src/**/*.py', recursive=True)
+ glob.glob('howdy-gtk/src/**/*.py', recursive=True);
errors = [];
[errors.append(f) for f in files
if not (py_compile.compile(f, doraise=False))];
sys.exit(1) if errors else None
"
102 changes: 102 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# AGENTS.md

This file provides guidance to WARP (warp.dev) when working with code in this repository.

## Project Overview

Howdy provides Windows Hello™ style facial authentication for Linux using PAM (Pluggable Authentication Modules). It uses IR cameras and dlib-based face recognition to authenticate users at login, lock screen, sudo, su, etc.

## Build Commands

```bash
# Configure and build
meson setup build
meson compile -C build

# Install to system (requires sudo)
sudo meson install -C build

# Run clang-tidy linting
ninja clang-tidy -C build
```

### Build Dependencies

- Python 3.6+, pip, setuptools, wheel
- meson (>= 0.64), ninja
- libpam, libinih (INIReader), libevdev
- OpenCV (with Python bindings)
- dlib (for face recognition models)

## Architecture

### Two Main Components

**howdy/** - Core module containing:
- `src/pam/` - C++ PAM module (`pam_howdy.so`) that integrates with Linux authentication
- `src/compare.py` - Python face recognition engine using dlib
- `src/cli.py` - CLI entry point for the `howdy` command
- `src/cli/` - Subcommands: add, clear, config, disable, list, remove, set, snap, test
- `src/recorders/` - Video capture backends (opencv, ffmpeg, pyv4l2)
- `src/rubberstamps/` - Optional post-recognition verification stamps (nod detection, hotkey)

**howdy-gtk/** - GTK user interface for:
- Authentication feedback overlay during login
- Configuration management
- Onboarding flow

### Authentication Flow

1. PAM module (`main.cc`) receives authentication request
2. Checks if Howdy is enabled (SSH detection, lid state, user has face model)
3. Spawns Python subprocess running `compare.py` with username
4. `compare.py` loads user's face encodings from `{models_dir}/{user}.dat`
5. Captures frames from IR camera via `VideoCapture` class
6. Uses dlib to detect faces and compute face descriptors
7. Compares against stored encodings using numpy vector distance
8. Returns exit code to PAM module (0 = success, others = various failures)
9. If rubberstamps enabled, runs additional verification

### Exit Codes (from compare.py)

- 0: Success
- 10: No face model
- 11: Timeout reached
- 12: Abort/missing username
- 13: Image too dark
- 14: Invalid camera device
- 15: Rubberstamp failed

### Configuration

Main config: `/etc/howdy/config.ini` (default location)

Key sections:
- `[core]` - Enable/disable, CNN mode, SSH detection, workaround mode
- `[video]` - Camera settings (device_path, certainty, timeout, dark_threshold)
- `[rubberstamps]` - Additional verification rules
- `[debug]` - Diagnostic output options

### Path Configuration (meson.options)

- `pam_dir` - PAM module install location
- `config_dir` - Config directory (default: `/etc/howdy`)
- `dlib_data_dir` - dlib model files location
- `user_models_dir` - User face model storage
- `python_path` - Python interpreter path

## Code Style

- C++ code follows Google style with clang-tidy checks (see `.clang-tidy`)
- Python code uses tabs for indentation
- Internationalization via gettext (`i18n.py` provides `_()` function)

## Testing Locally

After building, test face recognition:
```bash
sudo howdy test # Test camera and recognition
sudo howdy add # Add your face model
sudo howdy list # List stored face models
sudo howdy config # Edit configuration
```
21 changes: 12 additions & 9 deletions howdy-gtk/src/authsticky.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
# Shows a floating window when authenticating
import cairo
import gi
from __future__ import annotations

import signal
import sys
import paths_factory
import os
from typing import Any

import cairo
import gi

import paths_factory
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
from gi.repository import Gdk as gdk
from gi.repository import GObject as gobject
from gi.repository import Gtk as gtk

# Set window size constants
windowWidth = 400
Expand All @@ -27,7 +30,7 @@ class StickyWindow(gtk.Window):
message = _("Loading... ")
subtext = ""

def __init__(self):
def __init__(self) -> None:
"""Initialize the sticky window"""
# Make the class a GTK window
gtk.Window.__init__(self)
Expand Down Expand Up @@ -87,7 +90,7 @@ def __init__(self):
# Start GTK main loop
gtk.main()

def draw(self, widget, ctx):
def draw(self, widget: Any, ctx: cairo.Context) -> None:
"""Draw the UI"""
# Change cursor to the kill icon
self.get_window().set_cursor(gdk.Cursor(gdk.CursorType.PIRATE))
Expand Down Expand Up @@ -124,7 +127,7 @@ def draw(self, widget, ctx):
ctx.select_font_face("Arial", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
ctx.show_text(self.subtext)

def catch_stdin(self):
def catch_stdin(self) -> None:
"""Catch input from stdin and redraw"""
# Wait for a line on stdin
comm = sys.stdin.readline()[:-1]
Expand All @@ -145,7 +148,7 @@ def catch_stdin(self):
# 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):
def exit(self, widget: Any, context: Any) -> bool:
"""Cleanly exit"""
gtk.main_quit()
return True
Expand Down
Loading