initial commit
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
/build/
|
||||||
|
/result
|
||||||
|
/result-*
|
||||||
|
*.o
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project
|
||||||
|
|
||||||
|
`att_lock` — an Android-style 4×4 dot-pattern screen locker for wlroots-based
|
||||||
|
Wayland compositors. Single C11 binary, ~1500 LOC across four `src/*.c` files. No test suite exists.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Nix flakes are the build system; the host is NixOS and has **no** system Wayland/cairo/pam/xkbcommon
|
||||||
|
dev libraries outside the flake, so meson/ninja must always run inside `nix develop`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix build # -> ./result/bin/att_lock
|
||||||
|
nix develop --command bash -c 'meson setup build && ninja -C build' # -> ./build/att_lock
|
||||||
|
nix develop --command ninja -C build # incremental rebuild
|
||||||
|
```
|
||||||
|
|
||||||
|
Add new dependencies to `nativeDeps`/`libDeps` in `flake.nix` *and* to `meson.build`, never to a
|
||||||
|
global environment.
|
||||||
|
|
||||||
|
**Gotcha:** this repo is not git-tracked, so a plain `src = ./.` would vacuum the local `build/` dir
|
||||||
|
into the Nix sandbox and break `nix build` (meson chokes on a stale pre-configured build dir).
|
||||||
|
`flake.nix` filters `build` and `result*` out via `lib.cleanSourceWith` — keep that filter (or init
|
||||||
|
a git repo) whenever touching `flake.nix`.
|
||||||
|
|
||||||
|
## Running it
|
||||||
|
|
||||||
|
`att_lock` takes over the screen via `ext-session-lock-v1`. Launching it in the compositor you are
|
||||||
|
working in will lock that session — the protocol guarantees the session *stays* locked if the
|
||||||
|
process dies. Test in a nested compositor (e.g. `sway` inside a window) rather than the live session.
|
||||||
|
`--setup` re-records the pattern; delete `$XDG_DATA_HOME/att_lock/pattern.hash` to reset state.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The Wayland protocol bindings are generated at build time by `wayland-scanner` from the vendored
|
||||||
|
`protocols/ext-session-lock-v1.xml` into `ext-session-lock-v1-client-protocol.h` /
|
||||||
|
`-protocol.c` — they live only in the build dir, so grepping the source for
|
||||||
|
`ext_session_lock_v1_*` symbols finds call sites but not definitions.
|
||||||
|
|
||||||
|
One `struct att` (in `src/att_lock.h`) is the whole application state and is passed to essentially
|
||||||
|
every listener; `struct att_output` and `struct att_seat` hang off it in `wl_list`s, one per
|
||||||
|
compositor global.
|
||||||
|
|
||||||
|
**Control flow** — `main()` binds globals, calls `ext_session_lock_manager_v1_lock()`, then creates a
|
||||||
|
lock surface per output. A lock surface only becomes visible once its `configure` handler commits a
|
||||||
|
buffer, so `lock_surface_configure()` calls `render_output()` directly. Input handlers never render
|
||||||
|
inline; they set a `dirty` flag via `schedule_redraw()` and the loop renders all outputs once per
|
||||||
|
iteration.
|
||||||
|
|
||||||
|
The loop is the `prepare_read`/`poll`/`read_events` idiom rather than `wl_display_dispatch()`,
|
||||||
|
because it needs a **timeout**: an active lockout has to wake the process to repaint its countdown,
|
||||||
|
and `wl_display_dispatch()` would sleep until the next input event with the displayed seconds
|
||||||
|
frozen. The timeout is computed to fire exactly when the displayed second changes, so an idle
|
||||||
|
lockout costs one repaint per second and an idle unlock screen blocks indefinitely. If you touch
|
||||||
|
this loop, every path must pair `wl_display_prepare_read()` with exactly one of
|
||||||
|
`wl_display_read_events()` or `wl_display_cancel_read()` — leaking a prepared read deadlocks the
|
||||||
|
locker, which means a session nobody can unlock.
|
||||||
|
|
||||||
|
**Mode machine** — `enum att_mode` drives both input handling and what `prompt_text()`/`render_output()`
|
||||||
|
draw: `MODE_UNLOCK` ⇄ `MODE_PASSWORD` (any printable keypress enters password mode, Esc returns), and
|
||||||
|
`MODE_SETUP_FIRST` → `MODE_SETUP_CONFIRM` on first run or `--setup`. Setup modes are pattern-only.
|
||||||
|
|
||||||
|
**Failed-attempt throttling** — `note_failure()` / `locked_out()` in `main.c`. Both `begin_stroke()`
|
||||||
|
and `submit_password()` are gated, deliberately sharing one counter so that switching to the password
|
||||||
|
field cannot sidestep an active pattern lockout; setup modes are not counted (there is no secret to
|
||||||
|
guess yet), and strokes shorter than `min_dots` are rejected before `auth_verify()` and so are not
|
||||||
|
failures either. Deadlines use `CLOCK_MONOTONIC` (`att_now_ms()`) precisely because it ignores
|
||||||
|
wall-clock changes and does not advance across suspend — a lockout must not be sleepable-off.
|
||||||
|
|
||||||
|
**Coordinates** — everything in `struct grid` and `struct att_pattern` is in *logical* (surface-local)
|
||||||
|
pixels. `render_output()` allocates a device-pixel buffer of `width * scale` and applies
|
||||||
|
`cairo_scale(scale)` so all drawing code stays logical. The grid is a centred square at 70% of the
|
||||||
|
shorter output dimension (`att_grid()`), which is what makes rotation and arbitrary aspect ratios
|
||||||
|
work with no special cases.
|
||||||
|
|
||||||
|
**Pattern model** (`src/pattern.c`) — a sequence of dot indices (`row * 4 + col`). `pattern_add()`
|
||||||
|
implements the Android rule that unvisited dots lying exactly on the straight line between the
|
||||||
|
previous and new dot get captured first, using the gcd of the row/col delta. `pattern_to_string()`
|
||||||
|
produces the canonical `"0-1-2-5"` form, which feeds the hash input, so any change to indexing or
|
||||||
|
serialisation invalidates every stored pattern.
|
||||||
|
|
||||||
|
**Auth** (`src/auth.c`) — the pattern is verified against a salted `crypt_r` hash (yescrypt `$y$`,
|
||||||
|
falling back to sha512crypt `$6$`) at `$XDG_DATA_HOME/att_lock/pattern.hash`, mode 0600, dir 0700.
|
||||||
|
PAM is used *only* for the password fallback (`auth_pam()`, service from `--pam-service`, default
|
||||||
|
`login`). Comparison uses the constant-time `ct_streq()`, and secrets (password buffer, `crypt_data`,
|
||||||
|
hash input, first setup entry) are wiped with `explicit_bzero()` — preserve both when editing.
|
||||||
|
|
||||||
|
Three things about the stored hash are deliberate and load-bearing:
|
||||||
|
|
||||||
|
- **The hash input is `att_lock:v3:<user>:<pattern>`, not the bare pattern** (`auth_input()`). The
|
||||||
|
pattern space is tiny — only 43,680 four-dot sequences — so binding the hash to the account stops
|
||||||
|
an attacker amortising one cracking run across users or machines.
|
||||||
|
- **Cost is set explicitly**, `ATT_YESCRYPT_COST 9` / `ATT_SHA512_ROUNDS 600000`, not the `0`
|
||||||
|
"low default". Cost 9 measures ~0.22 s and ~258 MB here; 11 would be ~0.88 s and ~1 GB, and an
|
||||||
|
allocation that large in the unlock path can fail on a memory-pressured machine — a user who
|
||||||
|
cannot unlock is worse than a marginally cheaper hash. Re-measure before changing it.
|
||||||
|
- **The file is versioned**: line 1 is `ATT_HASH_MAGIC`, line 2 the crypt string. Any change to the
|
||||||
|
hash input must bump that tag, otherwise stored patterns silently stop verifying and the user is
|
||||||
|
stuck at "Wrong pattern" forever. `auth_hash_state()` returns `AUTH_HASH_STALE` for an untagged or
|
||||||
|
corrupt file and `main()` routes that into the setup flow.
|
||||||
|
|
||||||
|
**Rendering** (`src/render.c`) — `wl_shm` + Cairo only, no EGL/GPU. Two buffers per output with a
|
||||||
|
`busy` flag driven by `wl_buffer.release`; `buffer_get()` reallocates on size change. Background
|
||||||
|
images are PNG-only (Cairo's built-in loader).
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Linux kernel style: tabs, `snake_case`, opening brace on its own line for functions.
|
||||||
|
- Wayland listener structs are exhaustive — unused events get `(void)`-cast no-op stubs rather than
|
||||||
|
`NULL` entries.
|
||||||
|
- Failure while locked must never unlock: `lock_finished()` sets `running = false` without calling
|
||||||
|
`unlock_and_destroy()`, and `main()` exits non-zero when `app.lock` still exists.
|
||||||
|
- Built with `warning_level=2` and `-D_GNU_SOURCE`; keep the build warning-free.
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
# att_lock
|
||||||
|
|
||||||
|
An Android-style **4×4 dot pattern** screen locker for `wlroots`-based Wayland
|
||||||
|
compositors (Sway, Hyprland, river, labwc, …). It uses the secure
|
||||||
|
[`ext-session-lock-v1`] protocol, verifies the drawn pattern against a salted
|
||||||
|
hash stored in an XDG-compliant directory, and falls back to **PAM** password
|
||||||
|
authentication.
|
||||||
|
|
||||||
|
[`ext-session-lock-v1`]: https://wayland.app/protocols/ext-session-lock-v1
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **4×4 dot grid**, drawn by dragging with a **pointer or touch** — including
|
||||||
|
Android's rule that intermediate dots on a straight line get captured.
|
||||||
|
- **Rotation / any aspect ratio**: the grid is a centred square sized from the
|
||||||
|
compositor-provided surface dimensions, so portrait, landscape and rotated
|
||||||
|
outputs all just work. HiDPI (integer `wl_output` scale) is honoured.
|
||||||
|
- **First-run setup**: with no stored pattern (or `--setup`) you draw the
|
||||||
|
pattern twice to confirm it. It is stored as a salted **yescrypt** hash
|
||||||
|
(falling back to sha512crypt) at `$XDG_DATA_HOME/att_lock/pattern.hash`
|
||||||
|
(mode `0600`), never in plaintext. The hash uses cost factor 9 (~0.22 s per
|
||||||
|
attempt) and covers your username as well as the pattern, so the same pattern
|
||||||
|
on two accounts produces unrelated hashes.
|
||||||
|
- **PAM password fallback**: start typing at the unlock screen to switch to a
|
||||||
|
password field authenticated through PAM (for "forgot pattern").
|
||||||
|
- **Background image** via a command-line argument (PNG), scaled to cover.
|
||||||
|
- Secure by construction: if the process dies while locked, the compositor
|
||||||
|
keeps the session locked (a guarantee of `ext-session-lock-v1`).
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
The build system is **Nix flakes**.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# One-off build:
|
||||||
|
nix build # -> ./result/bin/att_lock
|
||||||
|
|
||||||
|
# Or a dev shell + meson:
|
||||||
|
nix develop
|
||||||
|
meson setup build
|
||||||
|
ninja -C build # -> ./build/att_lock
|
||||||
|
```
|
||||||
|
|
||||||
|
Dependencies (provided by the flake): `wayland`, `wayland-scanner`, `cairo`,
|
||||||
|
`libxkbcommon`, `pam` (linux-pam), `libxcrypt`, plus `meson`/`ninja`/`pkg-config`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
att_lock [options] [IMAGE]
|
||||||
|
|
||||||
|
-i, --image PATH Background image (PNG); also accepted positionally
|
||||||
|
-p, --pam-service NAME PAM service for the password fallback (default: login)
|
||||||
|
-m, --min-dots N Minimum dots in a pattern (default: 4)
|
||||||
|
--setup Force (re)creation of the stored pattern
|
||||||
|
-h, --help Show help
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
att_lock ~/wallpaper.png # lock with a background
|
||||||
|
att_lock --setup # re-record the pattern, then unlock
|
||||||
|
att_lock -p att_lock -i bg.png # use a dedicated PAM service
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unlocking
|
||||||
|
|
||||||
|
- **Draw** your pattern (≥ `--min-dots` dots) and release to unlock.
|
||||||
|
- **Type** any character to switch to the **password** field; press **Enter** to
|
||||||
|
authenticate via PAM, **Esc** to go back to the pattern.
|
||||||
|
|
||||||
|
After 4 failed attempts, further attempts are locked out for an escalating
|
||||||
|
delay — 15 s, then 30 s, 60 s, 120 s, 240 s, capped at 5 minutes — with the
|
||||||
|
remaining time shown on screen. The counter is shared between the pattern and
|
||||||
|
the password field, so switching to the password does not sidestep an active
|
||||||
|
lockout, and it is held in memory only (there is no way to restart the locker
|
||||||
|
to clear it, since the compositor keeps the session locked regardless). The
|
||||||
|
countdown uses a monotonic clock, so it cannot be skipped by changing the
|
||||||
|
system time or by suspending the machine.
|
||||||
|
|
||||||
|
## PAM configuration
|
||||||
|
|
||||||
|
The password fallback authenticates through PAM using the service named by
|
||||||
|
`--pam-service` (default `login`, which exists on most systems). For a
|
||||||
|
dedicated service, create one that stacks `pam_unix`:
|
||||||
|
|
||||||
|
**NixOS** (`configuration.nix`):
|
||||||
|
|
||||||
|
```nix
|
||||||
|
security.pam.services.att_lock = {}; # inherits sane defaults (pam_unix)
|
||||||
|
```
|
||||||
|
|
||||||
|
then run `att_lock --pam-service att_lock`.
|
||||||
|
|
||||||
|
**Other distros** (`/etc/pam.d/att_lock`):
|
||||||
|
|
||||||
|
```
|
||||||
|
auth include login
|
||||||
|
account include login
|
||||||
|
```
|
||||||
|
|
||||||
|
> The pattern itself is **not** checked by PAM — it is verified against the
|
||||||
|
> local salted hash. PAM only backs the password fallback.
|
||||||
|
|
||||||
|
## Wiring it into your compositor
|
||||||
|
|
||||||
|
Use it as your lock command, e.g. with `swayidle`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
swayidle -w \
|
||||||
|
timeout 300 'att_lock ~/wallpaper.png' \
|
||||||
|
before-sleep 'att_lock ~/wallpaper.png'
|
||||||
|
```
|
||||||
|
|
||||||
|
or bind it in Sway:
|
||||||
|
|
||||||
|
```
|
||||||
|
bindsym $mod+Escape exec att_lock ~/wallpaper.png
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes & limitations
|
||||||
|
|
||||||
|
- A pattern is not a password. The lockout above throttles *online* guessing at
|
||||||
|
the lock screen, but it cannot help offline: even at cost factor 9 there are
|
||||||
|
only 43,680 possible 4-dot patterns, so anyone who obtains `pattern.hash` can
|
||||||
|
search that space at their own pace. Raise `--min-dots` if that matters to
|
||||||
|
you; each extra dot multiplies the space by roughly an order of magnitude.
|
||||||
|
- Patterns are also far more shoulder-surfable than typed passwords, and on
|
||||||
|
touchscreens finger smudges can reveal them.
|
||||||
|
- Hash files written by versions before the `att_lock-hash-v3` format are not
|
||||||
|
readable; att_lock reports this and asks you to draw a new pattern. The PAM
|
||||||
|
password fallback still works in the meantime. In particular, v3 is the
|
||||||
|
format written since this project was renamed from `wlpattern`: the account
|
||||||
|
name and the project name are both baked into the hash input, so a pattern
|
||||||
|
enrolled under `wlpattern` (in `$XDG_DATA_HOME/wlpattern/`) cannot be carried
|
||||||
|
over and must be re-drawn once.
|
||||||
|
- Background images must be **PNG** (loaded via Cairo). Convert other formats
|
||||||
|
first (e.g. `magick in.jpg out.png`).
|
||||||
|
- The pattern is rendered identically on every connected output; the live
|
||||||
|
drag segment is drawn on the output receiving input.
|
||||||
|
- Rendering uses `wl_shm` + Cairo (no GPU/EGL dependency).
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
flake.nix Nix flake: package + dev shell
|
||||||
|
meson.build build definition
|
||||||
|
protocols/ext-session-lock-v1.xml vendored Wayland protocol
|
||||||
|
src/main.c Wayland client, input, unlock/setup logic
|
||||||
|
src/pattern.c grid geometry + pattern sequence model
|
||||||
|
src/render.c wl_shm buffers + Cairo drawing
|
||||||
|
src/auth.c hashing/storage (crypt) + PAM
|
||||||
|
src/att_lock.h shared types and interfaces
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT. The vendored protocol XML is MIT (wlroots contributors).
|
||||||
Generated
+27
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1784497964,
|
||||||
|
"narHash": "sha256-vlHUuqAcbcH2RKmHbPiuQzbv1pnzzavXnI62RD0bqCU=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "241313f4e8e508cb9b13278c2b0fa25b9ca27163",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
description = "att_lock — Android-style pattern screen locker for wlroots compositors (ext-session-lock-v1 + PAM)";
|
||||||
|
|
||||||
|
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs }:
|
||||||
|
let
|
||||||
|
systems = [ "x86_64-linux" "aarch64-linux" ];
|
||||||
|
forAll = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
|
||||||
|
|
||||||
|
nativeDeps = pkgs: with pkgs; [ meson ninja pkg-config wayland-scanner ];
|
||||||
|
# libxcrypt provides crypt.h + crypt_gensalt; pam is linux-pam.
|
||||||
|
libDeps = pkgs: with pkgs; [ wayland cairo libxkbcommon pam libxcrypt ];
|
||||||
|
|
||||||
|
# Copy only the sources, never local build artifacts (build/, result).
|
||||||
|
# This repo isn't tracked by git, so `./.` would otherwise vacuum in a
|
||||||
|
# stale meson build dir and break the sandboxed build.
|
||||||
|
cleanSrc = pkgs: pkgs.lib.cleanSourceWith {
|
||||||
|
src = pkgs.lib.cleanSource ./.;
|
||||||
|
filter = path: type:
|
||||||
|
let base = baseNameOf path;
|
||||||
|
in base != "build" && !(pkgs.lib.hasPrefix "result" base);
|
||||||
|
};
|
||||||
|
|
||||||
|
mkPackage = pkgs: pkgs.stdenv.mkDerivation {
|
||||||
|
pname = "att_lock";
|
||||||
|
version = "0.1.0";
|
||||||
|
src = cleanSrc pkgs;
|
||||||
|
nativeBuildInputs = nativeDeps pkgs;
|
||||||
|
buildInputs = libDeps pkgs;
|
||||||
|
meta = with pkgs.lib; {
|
||||||
|
description = "Android-style 4x4 pattern screen locker for wlroots compositors";
|
||||||
|
license = licenses.mit;
|
||||||
|
platforms = platforms.linux;
|
||||||
|
mainProgram = "att_lock";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages = forAll (pkgs: {
|
||||||
|
default = mkPackage pkgs;
|
||||||
|
att_lock = mkPackage pkgs;
|
||||||
|
});
|
||||||
|
|
||||||
|
devShells = forAll (pkgs: {
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
nativeBuildInputs = nativeDeps pkgs;
|
||||||
|
buildInputs = libDeps pkgs;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
formatter = forAll (pkgs: pkgs.nixpkgs-fmt);
|
||||||
|
};
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
project('att_lock', 'c',
|
||||||
|
version : '0.1.0',
|
||||||
|
license : 'MIT',
|
||||||
|
default_options : ['c_std=c11', 'warning_level=2'])
|
||||||
|
|
||||||
|
add_project_arguments('-D_GNU_SOURCE', language : 'c')
|
||||||
|
|
||||||
|
cc = meson.get_compiler('c')
|
||||||
|
|
||||||
|
wayland_client = dependency('wayland-client')
|
||||||
|
cairo = dependency('cairo')
|
||||||
|
xkbcommon = dependency('xkbcommon')
|
||||||
|
|
||||||
|
# libxcrypt ships a pkg-config file (libcrypt); fall back to a bare library lookup.
|
||||||
|
crypt = dependency('libcrypt', required : false)
|
||||||
|
if not crypt.found()
|
||||||
|
crypt = cc.find_library('crypt', has_headers : 'crypt.h')
|
||||||
|
endif
|
||||||
|
|
||||||
|
pam = cc.find_library('pam', has_headers : 'security/pam_appl.h')
|
||||||
|
|
||||||
|
wayland_scanner = find_program('wayland-scanner')
|
||||||
|
|
||||||
|
protocol_xml = 'protocols/ext-session-lock-v1.xml'
|
||||||
|
|
||||||
|
session_lock_client_header = custom_target('ext-session-lock-client-header',
|
||||||
|
input : protocol_xml,
|
||||||
|
output : 'ext-session-lock-v1-client-protocol.h',
|
||||||
|
command : [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@'])
|
||||||
|
|
||||||
|
session_lock_private_code = custom_target('ext-session-lock-private-code',
|
||||||
|
input : protocol_xml,
|
||||||
|
output : 'ext-session-lock-v1-protocol.c',
|
||||||
|
command : [wayland_scanner, 'private-code', '@INPUT@', '@OUTPUT@'])
|
||||||
|
|
||||||
|
sources = files(
|
||||||
|
'src/main.c',
|
||||||
|
'src/pattern.c',
|
||||||
|
'src/render.c',
|
||||||
|
'src/auth.c',
|
||||||
|
)
|
||||||
|
|
||||||
|
executable('att_lock',
|
||||||
|
sources,
|
||||||
|
session_lock_client_header,
|
||||||
|
session_lock_private_code,
|
||||||
|
dependencies : [wayland_client, cairo, xkbcommon, crypt, pam],
|
||||||
|
install : true)
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<protocol name="ext_session_lock_v1">
|
||||||
|
<copyright>
|
||||||
|
Copyright 2021 The wlroots contributors
|
||||||
|
|
||||||
|
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 (including the next
|
||||||
|
paragraph) 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.
|
||||||
|
</copyright>
|
||||||
|
|
||||||
|
<description summary="secure session locking with arbitrary graphics">
|
||||||
|
This protocol allows for a privileged Wayland client to lock the session
|
||||||
|
and display arbitrary graphics while the session is locked.
|
||||||
|
|
||||||
|
The compositor may choose to restrict this protocol to a special client
|
||||||
|
launched by the compositor itself or expose it to all privileged clients,
|
||||||
|
this is compositor policy.
|
||||||
|
|
||||||
|
The client is responsible for performing authentication and informing the
|
||||||
|
compositor when the session should be unlocked. If the client dies while
|
||||||
|
the session is locked the session remains locked, possibly permanently
|
||||||
|
depending on compositor policy.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<interface name="ext_session_lock_manager_v1" version="1">
|
||||||
|
<description summary="manager used to lock the session">
|
||||||
|
This interface is used to request that the session be locked.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor">
|
||||||
|
<description summary="destroy the session lock manager object">
|
||||||
|
This informs the compositor that the lock manager object will no
|
||||||
|
longer be used. Existing objects created through this interface remain
|
||||||
|
valid.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="lock">
|
||||||
|
<description summary="attempt to lock the session">
|
||||||
|
This request creates a session lock and asks the compositor to lock the
|
||||||
|
session. The compositor will send either the ext_session_lock_v1.locked
|
||||||
|
or ext_session_lock_v1.finished event on the created object in response
|
||||||
|
to this request.
|
||||||
|
</description>
|
||||||
|
<arg name="id" type="new_id" interface="ext_session_lock_v1"/>
|
||||||
|
</request>
|
||||||
|
</interface>
|
||||||
|
|
||||||
|
<interface name="ext_session_lock_v1" version="1">
|
||||||
|
<description summary="manage lock state and create lock surfaces">
|
||||||
|
In response to the creation of this object the compositor must send
|
||||||
|
either the locked or finished event.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<enum name="error">
|
||||||
|
<entry name="invalid_destroy" value="0"
|
||||||
|
summary="attempted to destroy session lock while locked"/>
|
||||||
|
<entry name="invalid_unlock" value="1"
|
||||||
|
summary="unlock requested but locked event was never sent"/>
|
||||||
|
<entry name="role" value="2"
|
||||||
|
summary="given wl_surface already has a role"/>
|
||||||
|
<entry name="duplicate_output" value="3"
|
||||||
|
summary="given output already has a lock surface"/>
|
||||||
|
<entry name="already_constructed" value="4"
|
||||||
|
summary="given wl_surface has a buffer attached or committed"/>
|
||||||
|
</enum>
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor">
|
||||||
|
<description summary="destroy the session lock">
|
||||||
|
This informs the compositor that the lock object will no longer be
|
||||||
|
used. It is a protocol error to make this request if the locked event
|
||||||
|
was sent, the unlock_and_destroy request must be used instead.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<event name="locked">
|
||||||
|
<description summary="session successfully locked">
|
||||||
|
This client is now responsible for displaying graphics while the
|
||||||
|
session is locked and deciding when to unlock the session.
|
||||||
|
</description>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<event name="finished">
|
||||||
|
<description summary="the session lock object should be destroyed">
|
||||||
|
The compositor has decided that the session lock should be destroyed
|
||||||
|
as it will no longer be used by the compositor. Exactly when this
|
||||||
|
event is sent is compositor policy, but it must never be sent more
|
||||||
|
than once for a given session lock object.
|
||||||
|
</description>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<request name="get_lock_surface">
|
||||||
|
<description summary="create a lock surface for a given output">
|
||||||
|
The client is expected to create lock surfaces for all outputs
|
||||||
|
currently present and any new outputs as they are advertised. These
|
||||||
|
won't be displayed by the compositor unless the lock is successful.
|
||||||
|
</description>
|
||||||
|
<arg name="id" type="new_id" interface="ext_session_lock_surface_v1"/>
|
||||||
|
<arg name="surface" type="object" interface="wl_surface"/>
|
||||||
|
<arg name="output" type="object" interface="wl_output"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="unlock_and_destroy" type="destructor">
|
||||||
|
<description summary="unlock the session, destroying the object">
|
||||||
|
This request indicates that the session should be unlocked, for
|
||||||
|
example because the user has entered their password and it has been
|
||||||
|
verified by the client.
|
||||||
|
|
||||||
|
This request also informs the compositor that the lock object will no
|
||||||
|
longer be used and should be destroyed.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
</interface>
|
||||||
|
|
||||||
|
<interface name="ext_session_lock_surface_v1" version="1">
|
||||||
|
<description summary="a surface displayed while the session is locked">
|
||||||
|
The client may use lock surfaces to display a screensaver, render a
|
||||||
|
dialog to enter a password and unlock the session, or however else it
|
||||||
|
sees fit.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<enum name="error">
|
||||||
|
<entry name="commit_before_first_ack" value="0"
|
||||||
|
summary="surface committed before first ack_configure request"/>
|
||||||
|
<entry name="null_buffer" value="1"
|
||||||
|
summary="surface committed with a null buffer"/>
|
||||||
|
<entry name="dimensions_mismatch" value="2"
|
||||||
|
summary="failed to match ack'd width/height"/>
|
||||||
|
<entry name="invalid_serial" value="3"
|
||||||
|
summary="serial provided in ack_configure is invalid"/>
|
||||||
|
</enum>
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor">
|
||||||
|
<description summary="destroy the lock surface object"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="ack_configure">
|
||||||
|
<description summary="ack a configure event">
|
||||||
|
When a configure event is received, if a client commits the surface
|
||||||
|
in response to the configure event, then the client must make an
|
||||||
|
ack_configure request sometime before the commit request, passing
|
||||||
|
along the serial of the configure event.
|
||||||
|
</description>
|
||||||
|
<arg name="serial" type="uint" summary="serial of configure event"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<event name="configure">
|
||||||
|
<description summary="the client should resize its surface">
|
||||||
|
This event is sent once on binding the interface and may be sent again
|
||||||
|
at the compositor's discretion, for example if output geometry changes.
|
||||||
|
|
||||||
|
The width and height are in surface-local coordinates and the client
|
||||||
|
must create a buffer with exactly these dimensions.
|
||||||
|
</description>
|
||||||
|
<arg name="serial" type="uint" summary="serial for use in ack_configure"/>
|
||||||
|
<arg name="width" type="uint"/>
|
||||||
|
<arg name="height" type="uint"/>
|
||||||
|
</event>
|
||||||
|
</interface>
|
||||||
|
</protocol>
|
||||||
+151
@@ -0,0 +1,151 @@
|
|||||||
|
#ifndef ATT_LOCK_H
|
||||||
|
#define ATT_LOCK_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <wayland-client.h>
|
||||||
|
#include <xkbcommon/xkbcommon.h>
|
||||||
|
|
||||||
|
/* 4x4 dot grid. Dot index = row * GRID_N + col, row/col in [0, GRID_N). */
|
||||||
|
#define GRID_N 4
|
||||||
|
#define DOT_COUNT (GRID_N * GRID_N)
|
||||||
|
#define MAX_SEQ DOT_COUNT
|
||||||
|
|
||||||
|
struct att;
|
||||||
|
struct att_output;
|
||||||
|
|
||||||
|
enum att_mode {
|
||||||
|
MODE_UNLOCK, /* verify a drawn pattern against the stored hash */
|
||||||
|
MODE_SETUP_FIRST, /* first run: draw a new pattern */
|
||||||
|
MODE_SETUP_CONFIRM, /* first run: draw it again to confirm */
|
||||||
|
MODE_PASSWORD, /* PAM password fallback (keyboard entry) */
|
||||||
|
};
|
||||||
|
|
||||||
|
enum att_feedback {
|
||||||
|
FB_NONE,
|
||||||
|
FB_ERROR,
|
||||||
|
FB_OK,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Layout of the grid within one output, in surface-local (logical) pixels. */
|
||||||
|
struct grid {
|
||||||
|
double ox, oy; /* top-left of the grid square */
|
||||||
|
double cell; /* size of one cell */
|
||||||
|
double radius; /* drawn dot radius */
|
||||||
|
double hit; /* hit-test radius */
|
||||||
|
};
|
||||||
|
|
||||||
|
struct att_pattern {
|
||||||
|
int seq[MAX_SEQ];
|
||||||
|
int len;
|
||||||
|
bool visited[DOT_COUNT];
|
||||||
|
bool drawing;
|
||||||
|
double cur_x, cur_y; /* live fingertip, in the active output's logical coords */
|
||||||
|
};
|
||||||
|
|
||||||
|
struct att_buffer {
|
||||||
|
struct wl_buffer *wl_buffer;
|
||||||
|
void *data;
|
||||||
|
size_t size;
|
||||||
|
int width, height; /* in device pixels */
|
||||||
|
bool busy;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct att_output {
|
||||||
|
struct wl_list link;
|
||||||
|
struct att *app;
|
||||||
|
struct wl_output *wl_output;
|
||||||
|
uint32_t global_name;
|
||||||
|
int32_t scale;
|
||||||
|
struct wl_surface *surface;
|
||||||
|
struct ext_session_lock_surface_v1 *lock_surface;
|
||||||
|
int32_t width, height; /* logical size from configure */
|
||||||
|
bool configured;
|
||||||
|
struct att_buffer buffers[2];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct att_seat {
|
||||||
|
struct wl_list link;
|
||||||
|
struct att *app;
|
||||||
|
struct wl_seat *wl_seat;
|
||||||
|
uint32_t global_name;
|
||||||
|
struct wl_pointer *pointer;
|
||||||
|
struct wl_touch *touch;
|
||||||
|
struct wl_keyboard *keyboard;
|
||||||
|
double px, py; /* latest pointer position (logical) */
|
||||||
|
int32_t touch_id; /* active touch point, -1 if none */
|
||||||
|
struct xkb_context *xkb_ctx;
|
||||||
|
struct xkb_keymap *xkb_keymap;
|
||||||
|
struct xkb_state *xkb_state;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct att {
|
||||||
|
struct wl_display *display;
|
||||||
|
struct wl_registry *registry;
|
||||||
|
struct wl_compositor *compositor;
|
||||||
|
struct wl_shm *shm;
|
||||||
|
struct ext_session_lock_manager_v1 *lock_manager;
|
||||||
|
struct ext_session_lock_v1 *lock;
|
||||||
|
|
||||||
|
struct wl_list outputs; /* struct att_output.link */
|
||||||
|
struct wl_list seats; /* struct att_seat.link */
|
||||||
|
|
||||||
|
bool locked;
|
||||||
|
bool running;
|
||||||
|
bool dirty;
|
||||||
|
|
||||||
|
enum att_mode mode;
|
||||||
|
enum att_feedback feedback;
|
||||||
|
|
||||||
|
struct att_pattern pattern;
|
||||||
|
struct att_output *active_output; /* output currently receiving input */
|
||||||
|
char first_entry[128]; /* canonical string of first setup draw */
|
||||||
|
|
||||||
|
char password[256];
|
||||||
|
int password_len;
|
||||||
|
|
||||||
|
/* Failed-attempt throttling. Kept in memory only: if this process dies
|
||||||
|
* the compositor keeps the session locked and there is no unlock UI at
|
||||||
|
* all, so there is no restart an attacker could use to clear these. */
|
||||||
|
int fail_count; /* failed unlock attempts, pattern or password */
|
||||||
|
int64_t lockout_until; /* CLOCK_MONOTONIC ms; 0 when not locked out */
|
||||||
|
|
||||||
|
/* configuration */
|
||||||
|
const char *image_path;
|
||||||
|
const char *pam_service;
|
||||||
|
int min_dots;
|
||||||
|
|
||||||
|
void *bg_surface; /* cairo_surface_t*, or NULL */
|
||||||
|
char *hash_path; /* XDG data path for the pattern hash */
|
||||||
|
};
|
||||||
|
|
||||||
|
/* main.c ------------------------------------------------------------------- */
|
||||||
|
int64_t att_now_ms(void);
|
||||||
|
int att_lockout_secs(const struct att *app); /* 0 when input is allowed */
|
||||||
|
|
||||||
|
/* pattern.c ---------------------------------------------------------------- */
|
||||||
|
void att_grid(const struct att_output *o, struct grid *g);
|
||||||
|
void pattern_reset(struct att_pattern *p);
|
||||||
|
int pattern_hit(const struct grid *g, double x, double y);
|
||||||
|
void pattern_add(struct att_pattern *p, int idx);
|
||||||
|
int pattern_to_string(const struct att_pattern *p, char *buf, size_t n);
|
||||||
|
|
||||||
|
/* render.c ----------------------------------------------------------------- */
|
||||||
|
void render_output(struct att_output *o);
|
||||||
|
void render_all(struct att *app);
|
||||||
|
|
||||||
|
/* auth.c ------------------------------------------------------------------- */
|
||||||
|
enum auth_hash_state {
|
||||||
|
AUTH_HASH_NONE, /* no stored pattern yet */
|
||||||
|
AUTH_HASH_STALE, /* present but unreadable / written by an older ver */
|
||||||
|
AUTH_HASH_OK, /* usable with auth_verify() */
|
||||||
|
};
|
||||||
|
|
||||||
|
char *auth_hash_path(void);
|
||||||
|
enum auth_hash_state auth_hash_state(const char *path);
|
||||||
|
bool auth_store(const char *path, const char *pattern_str);
|
||||||
|
bool auth_verify(const char *path, const char *pattern_str);
|
||||||
|
bool auth_pam(const char *service, const char *password);
|
||||||
|
|
||||||
|
#endif /* ATT_LOCK_H */
|
||||||
+294
@@ -0,0 +1,294 @@
|
|||||||
|
#include "att_lock.h"
|
||||||
|
|
||||||
|
#include <crypt.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <pwd.h>
|
||||||
|
#include <security/pam_appl.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/random.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
/* Processing cost. Per crypt(5), yescrypt accepts 1..11 (logarithmic, and it
|
||||||
|
* scales memory too) and sha512crypt accepts 1000..999999999 with a far too low
|
||||||
|
* default of 5000. The pattern space is tiny -- at the default --min-dots there
|
||||||
|
* are only 16*15*14*13 = 43680 possible sequences -- so once the hash file
|
||||||
|
* leaks, this cost is the only thing between an attacker and the pattern.
|
||||||
|
*
|
||||||
|
* Measured here: yescrypt cost 9 is ~0.22s and ~258MB peak RSS, cost 11 is
|
||||||
|
* ~0.88s and ~1GB. Cost 9 is the balance point: a ~1GB allocation in the unlock
|
||||||
|
* path risks failing on a memory-pressured machine, and a hash that cannot be
|
||||||
|
* computed means a user who cannot unlock. */
|
||||||
|
#define ATT_YESCRYPT_COST 9
|
||||||
|
#define ATT_SHA512_ROUNDS 600000
|
||||||
|
|
||||||
|
/* The hash covers "att_lock:v3:<user>:<pattern>", not the bare pattern string,
|
||||||
|
* so that the same pattern on two accounts (or two machines) yields unrelated
|
||||||
|
* hashes and an attacker cannot amortise one cracking run across targets.
|
||||||
|
*
|
||||||
|
* Any change to that input invalidates every stored hash, so the file carries a
|
||||||
|
* format tag; auth_hash_state() reports a stale file and main() then forces the
|
||||||
|
* setup flow rather than silently rejecting every correct pattern. */
|
||||||
|
#define ATT_HASH_MAGIC "att_lock-hash-v3"
|
||||||
|
|
||||||
|
/* Build "$XDG_DATA_HOME/att_lock/pattern.hash" (falling back to
|
||||||
|
* "$HOME/.local/share/..."). Caller frees. */
|
||||||
|
char *auth_hash_path(void)
|
||||||
|
{
|
||||||
|
const char *xdg = getenv("XDG_DATA_HOME");
|
||||||
|
char base[4096];
|
||||||
|
|
||||||
|
if (xdg && xdg[0] == '/') {
|
||||||
|
snprintf(base, sizeof(base), "%s/att_lock", xdg);
|
||||||
|
} else {
|
||||||
|
const char *home = getenv("HOME");
|
||||||
|
if (!home || home[0] != '/') {
|
||||||
|
struct passwd *pw = getpwuid(getuid());
|
||||||
|
home = pw ? pw->pw_dir : NULL;
|
||||||
|
}
|
||||||
|
if (!home)
|
||||||
|
return NULL;
|
||||||
|
snprintf(base, sizeof(base), "%s/.local/share/att_lock", home);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t len = strlen(base) + strlen("/pattern.hash") + 1;
|
||||||
|
char *path = malloc(len);
|
||||||
|
if (!path)
|
||||||
|
return NULL;
|
||||||
|
snprintf(path, len, "%s/pattern.hash", base);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compose the string that actually gets hashed. Fails if the account has no
|
||||||
|
* name, since without one the hash would not be bound to a user at all. */
|
||||||
|
static bool auth_input(const char *pattern_str, char *out, size_t n)
|
||||||
|
{
|
||||||
|
struct passwd *pw = getpwuid(getuid());
|
||||||
|
if (!pw || !pw->pw_name || !pw->pw_name[0])
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int w = snprintf(out, n, "att_lock:v3:%s:%s", pw->pw_name, pattern_str);
|
||||||
|
return w > 0 && (size_t)w < n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read the stored record and hand back its hash line, rejecting anything that
|
||||||
|
* does not carry the current format tag. */
|
||||||
|
static bool read_hash(const char *path, char *out, size_t n)
|
||||||
|
{
|
||||||
|
int fd = open(path, O_RDONLY | O_CLOEXEC);
|
||||||
|
if (fd < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
char buf[1024];
|
||||||
|
ssize_t r = read(fd, buf, sizeof(buf) - 1);
|
||||||
|
close(fd);
|
||||||
|
if (r <= 0)
|
||||||
|
return false;
|
||||||
|
buf[r] = '\0';
|
||||||
|
|
||||||
|
bool ok = false;
|
||||||
|
char *nl = strchr(buf, '\n');
|
||||||
|
if (nl) {
|
||||||
|
*nl = '\0';
|
||||||
|
char *hash = nl + 1;
|
||||||
|
hash[strcspn(hash, "\r\n")] = '\0';
|
||||||
|
ok = strcmp(buf, ATT_HASH_MAGIC) == 0 && hash[0] != '\0' &&
|
||||||
|
(size_t)snprintf(out, n, "%s", hash) < n;
|
||||||
|
}
|
||||||
|
|
||||||
|
explicit_bzero(buf, sizeof(buf));
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum auth_hash_state auth_hash_state(const char *path)
|
||||||
|
{
|
||||||
|
struct stat st;
|
||||||
|
if (!path || stat(path, &st) != 0 || !S_ISREG(st.st_mode))
|
||||||
|
return AUTH_HASH_NONE;
|
||||||
|
|
||||||
|
char hash[512];
|
||||||
|
bool ok = read_hash(path, hash, sizeof(hash));
|
||||||
|
explicit_bzero(hash, sizeof(hash));
|
||||||
|
return ok ? AUTH_HASH_OK : AUTH_HASH_STALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* mkdir -p on the directory portion of `path`, each component 0700. */
|
||||||
|
static bool ensure_parent_dir(const char *path)
|
||||||
|
{
|
||||||
|
char dir[4096];
|
||||||
|
snprintf(dir, sizeof(dir), "%s", path);
|
||||||
|
char *slash = strrchr(dir, '/');
|
||||||
|
if (!slash)
|
||||||
|
return true;
|
||||||
|
*slash = '\0';
|
||||||
|
|
||||||
|
for (char *p = dir + 1; *p; p++) {
|
||||||
|
if (*p == '/') {
|
||||||
|
*p = '\0';
|
||||||
|
if (mkdir(dir, 0700) != 0 && errno != EEXIST)
|
||||||
|
return false;
|
||||||
|
*p = '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mkdir(dir, 0700) != 0 && errno != EEXIST)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generate a yescrypt (falling back to sha512crypt) salt with fresh entropy. */
|
||||||
|
static bool make_salt(char *out, size_t outlen)
|
||||||
|
{
|
||||||
|
unsigned char rnd[16];
|
||||||
|
if (getrandom(rnd, sizeof(rnd), 0) != (ssize_t)sizeof(rnd))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (crypt_gensalt_rn("$y$", ATT_YESCRYPT_COST, (const char *)rnd,
|
||||||
|
sizeof(rnd), out, (int)outlen))
|
||||||
|
return true;
|
||||||
|
if (crypt_gensalt_rn("$6$", ATT_SHA512_ROUNDS, (const char *)rnd,
|
||||||
|
sizeof(rnd), out, (int)outlen))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool auth_store(const char *path, const char *pattern_str)
|
||||||
|
{
|
||||||
|
char input[512];
|
||||||
|
if (!auth_input(pattern_str, input, sizeof(input)))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
char salt[CRYPT_GENSALT_OUTPUT_SIZE];
|
||||||
|
if (!make_salt(salt, sizeof(salt))) {
|
||||||
|
explicit_bzero(input, sizeof(input));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct crypt_data data;
|
||||||
|
memset(&data, 0, sizeof(data));
|
||||||
|
char *hash = crypt_r(input, salt, &data);
|
||||||
|
explicit_bzero(input, sizeof(input));
|
||||||
|
|
||||||
|
char record[1024];
|
||||||
|
int reclen = -1;
|
||||||
|
if (hash && hash[0] != '*')
|
||||||
|
reclen = snprintf(record, sizeof(record), "%s\n%s\n",
|
||||||
|
ATT_HASH_MAGIC, hash);
|
||||||
|
explicit_bzero(&data, sizeof(data));
|
||||||
|
if (reclen <= 0 || (size_t)reclen >= sizeof(record))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!ensure_parent_dir(path)) {
|
||||||
|
explicit_bzero(record, sizeof(record));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600);
|
||||||
|
if (fd < 0) {
|
||||||
|
explicit_bzero(record, sizeof(record));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ok = write(fd, record, (size_t)reclen) == (ssize_t)reclen;
|
||||||
|
if (fsync(fd) != 0)
|
||||||
|
ok = false;
|
||||||
|
close(fd);
|
||||||
|
|
||||||
|
explicit_bzero(record, sizeof(record));
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ct_streq(const char *a, const char *b)
|
||||||
|
{
|
||||||
|
size_t la = strlen(a), lb = strlen(b);
|
||||||
|
unsigned char diff = (unsigned char)(la ^ lb);
|
||||||
|
size_t n = la < lb ? la : lb;
|
||||||
|
for (size_t i = 0; i < n; i++)
|
||||||
|
diff |= (unsigned char)(a[i] ^ b[i]);
|
||||||
|
return diff == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool auth_verify(const char *path, const char *pattern_str)
|
||||||
|
{
|
||||||
|
char stored[512];
|
||||||
|
if (!read_hash(path, stored, sizeof(stored)))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
char input[512];
|
||||||
|
if (!auth_input(pattern_str, input, sizeof(input))) {
|
||||||
|
explicit_bzero(stored, sizeof(stored));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct crypt_data data;
|
||||||
|
memset(&data, 0, sizeof(data));
|
||||||
|
char *hash = crypt_r(input, stored, &data);
|
||||||
|
bool ok = hash && hash[0] != '*' && ct_streq(hash, stored);
|
||||||
|
|
||||||
|
explicit_bzero(input, sizeof(input));
|
||||||
|
explicit_bzero(&data, sizeof(data));
|
||||||
|
explicit_bzero(stored, sizeof(stored));
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- PAM password fallback ------------------------------------------------ */
|
||||||
|
|
||||||
|
struct conv_ctx {
|
||||||
|
const char *password;
|
||||||
|
};
|
||||||
|
|
||||||
|
static int pam_conv_cb(int num_msg, const struct pam_message **msg,
|
||||||
|
struct pam_response **resp, void *appdata)
|
||||||
|
{
|
||||||
|
if (num_msg <= 0 || num_msg > PAM_MAX_NUM_MSG)
|
||||||
|
return PAM_CONV_ERR;
|
||||||
|
|
||||||
|
struct conv_ctx *ctx = appdata;
|
||||||
|
struct pam_response *replies = calloc(num_msg, sizeof(*replies));
|
||||||
|
if (!replies)
|
||||||
|
return PAM_BUF_ERR;
|
||||||
|
|
||||||
|
for (int i = 0; i < num_msg; i++) {
|
||||||
|
switch (msg[i]->msg_style) {
|
||||||
|
case PAM_PROMPT_ECHO_OFF:
|
||||||
|
case PAM_PROMPT_ECHO_ON:
|
||||||
|
replies[i].resp = strdup(ctx->password ? ctx->password : "");
|
||||||
|
if (!replies[i].resp) {
|
||||||
|
for (int j = 0; j < i; j++)
|
||||||
|
free(replies[j].resp);
|
||||||
|
free(replies);
|
||||||
|
return PAM_BUF_ERR;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break; /* PAM_ERROR_MSG / PAM_TEXT_INFO: no reply */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*resp = replies;
|
||||||
|
return PAM_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool auth_pam(const char *service, const char *password)
|
||||||
|
{
|
||||||
|
struct passwd *pw = getpwuid(getuid());
|
||||||
|
if (!pw || !pw->pw_name)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
struct conv_ctx ctx = { .password = password };
|
||||||
|
struct pam_conv conv = { .conv = pam_conv_cb, .appdata_ptr = &ctx };
|
||||||
|
pam_handle_t *pamh = NULL;
|
||||||
|
|
||||||
|
if (pam_start(service, pw->pw_name, &conv, &pamh) != PAM_SUCCESS)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int rc = pam_authenticate(pamh, 0);
|
||||||
|
if (rc == PAM_SUCCESS)
|
||||||
|
rc = pam_acct_mgmt(pamh, 0);
|
||||||
|
|
||||||
|
pam_end(pamh, rc);
|
||||||
|
return rc == PAM_SUCCESS;
|
||||||
|
}
|
||||||
+865
@@ -0,0 +1,865 @@
|
|||||||
|
#include "att_lock.h"
|
||||||
|
#include "ext-session-lock-v1-client-protocol.h"
|
||||||
|
|
||||||
|
#include <cairo.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <getopt.h>
|
||||||
|
#include <linux/input-event-codes.h>
|
||||||
|
#include <poll.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <wayland-client.h>
|
||||||
|
#include <xkbcommon/xkbcommon.h>
|
||||||
|
|
||||||
|
static void schedule_redraw(struct att *app)
|
||||||
|
{
|
||||||
|
app->dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- failed-attempt throttling -------------------------------------------- */
|
||||||
|
|
||||||
|
/* The pattern is checked against a local hash and never goes through PAM, so it
|
||||||
|
* inherits none of pam_faildelay/pam_faillock's rate limiting -- without this a
|
||||||
|
* wrong guess costs only the ~0.22s hash. The password path shares the counter
|
||||||
|
* so that switching to it cannot dodge an active lockout. */
|
||||||
|
#define FAIL_GRACE 4 /* wrong attempts allowed before throttling */
|
||||||
|
#define LOCKOUT_BASE 15000 /* ms for the first lockout, doubling after */
|
||||||
|
#define LOCKOUT_MAX 300000 /* ms ceiling */
|
||||||
|
|
||||||
|
/* CLOCK_MONOTONIC deliberately: it is immune to wall-clock changes, and it does
|
||||||
|
* not advance across suspend, so a lockout cannot be slept off. */
|
||||||
|
int64_t att_now_ms(void)
|
||||||
|
{
|
||||||
|
struct timespec ts;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
|
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
int att_lockout_secs(const struct att *app)
|
||||||
|
{
|
||||||
|
if (!app->lockout_until)
|
||||||
|
return 0;
|
||||||
|
int64_t left = app->lockout_until - att_now_ms();
|
||||||
|
return left > 0 ? (int)((left + 999) / 1000) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool locked_out(struct att *app)
|
||||||
|
{
|
||||||
|
if (att_lockout_secs(app) > 0)
|
||||||
|
return true;
|
||||||
|
app->lockout_until = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void note_failure(struct att *app)
|
||||||
|
{
|
||||||
|
app->fail_count++;
|
||||||
|
app->feedback = FB_ERROR;
|
||||||
|
|
||||||
|
int steps = app->fail_count - FAIL_GRACE - 1;
|
||||||
|
if (steps < 0)
|
||||||
|
return;
|
||||||
|
if (steps > 20)
|
||||||
|
steps = 20; /* keep the shift well clear of overflow */
|
||||||
|
int64_t ms = (int64_t)LOCKOUT_BASE << steps;
|
||||||
|
if (ms > LOCKOUT_MAX)
|
||||||
|
ms = LOCKOUT_MAX;
|
||||||
|
app->lockout_until = att_now_ms() + ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- unlock / setup logic ------------------------------------------------- */
|
||||||
|
|
||||||
|
static void do_unlock(struct att *app)
|
||||||
|
{
|
||||||
|
if (app->lock) {
|
||||||
|
ext_session_lock_v1_unlock_and_destroy(app->lock);
|
||||||
|
app->lock = NULL;
|
||||||
|
wl_display_flush(app->display);
|
||||||
|
wl_display_roundtrip(app->display);
|
||||||
|
}
|
||||||
|
app->running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clear_password(struct att *app)
|
||||||
|
{
|
||||||
|
explicit_bzero(app->password, sizeof(app->password));
|
||||||
|
app->password_len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void begin_stroke(struct att *app, struct att_output *o,
|
||||||
|
double x, double y)
|
||||||
|
{
|
||||||
|
if (app->mode == MODE_PASSWORD)
|
||||||
|
return;
|
||||||
|
if (locked_out(app)) {
|
||||||
|
schedule_redraw(app); /* keep the countdown text current */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pattern_reset(&app->pattern);
|
||||||
|
app->feedback = FB_NONE;
|
||||||
|
app->active_output = o;
|
||||||
|
app->pattern.drawing = true;
|
||||||
|
app->pattern.cur_x = x;
|
||||||
|
app->pattern.cur_y = y;
|
||||||
|
|
||||||
|
struct grid gr;
|
||||||
|
att_grid(o, &gr);
|
||||||
|
pattern_add(&app->pattern, pattern_hit(&gr, x, y));
|
||||||
|
schedule_redraw(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void move_stroke(struct att *app, struct att_output *o,
|
||||||
|
double x, double y)
|
||||||
|
{
|
||||||
|
if (!app->pattern.drawing || o != app->active_output)
|
||||||
|
return;
|
||||||
|
app->pattern.cur_x = x;
|
||||||
|
app->pattern.cur_y = y;
|
||||||
|
|
||||||
|
struct grid gr;
|
||||||
|
att_grid(o, &gr);
|
||||||
|
int idx = pattern_hit(&gr, x, y);
|
||||||
|
if (idx >= 0 && !app->pattern.visited[idx])
|
||||||
|
pattern_add(&app->pattern, idx);
|
||||||
|
schedule_redraw(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void finish_stroke(struct att *app)
|
||||||
|
{
|
||||||
|
struct att_pattern *p = &app->pattern;
|
||||||
|
if (!p->drawing)
|
||||||
|
return;
|
||||||
|
p->drawing = false;
|
||||||
|
|
||||||
|
char str[128];
|
||||||
|
if (pattern_to_string(p, str, sizeof(str)) < 0) {
|
||||||
|
pattern_reset(p);
|
||||||
|
schedule_redraw(app);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p->len < app->min_dots) {
|
||||||
|
app->feedback = p->len > 0 ? FB_ERROR : FB_NONE;
|
||||||
|
pattern_reset(p);
|
||||||
|
schedule_redraw(app);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (app->mode) {
|
||||||
|
case MODE_UNLOCK:
|
||||||
|
if (auth_verify(app->hash_path, str)) {
|
||||||
|
do_unlock(app);
|
||||||
|
} else {
|
||||||
|
note_failure(app);
|
||||||
|
pattern_reset(p);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MODE_SETUP_FIRST:
|
||||||
|
snprintf(app->first_entry, sizeof(app->first_entry), "%s", str);
|
||||||
|
app->mode = MODE_SETUP_CONFIRM;
|
||||||
|
app->feedback = FB_OK;
|
||||||
|
pattern_reset(p);
|
||||||
|
break;
|
||||||
|
case MODE_SETUP_CONFIRM:
|
||||||
|
if (strcmp(str, app->first_entry) == 0 &&
|
||||||
|
auth_store(app->hash_path, str)) {
|
||||||
|
explicit_bzero(app->first_entry, sizeof(app->first_entry));
|
||||||
|
do_unlock(app);
|
||||||
|
} else {
|
||||||
|
app->feedback = FB_ERROR;
|
||||||
|
app->mode = MODE_SETUP_FIRST;
|
||||||
|
explicit_bzero(app->first_entry, sizeof(app->first_entry));
|
||||||
|
pattern_reset(p);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MODE_PASSWORD:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
explicit_bzero(str, sizeof(str));
|
||||||
|
schedule_redraw(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void submit_password(struct att *app)
|
||||||
|
{
|
||||||
|
if (locked_out(app)) {
|
||||||
|
schedule_redraw(app);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (auth_pam(app->pam_service, app->password)) {
|
||||||
|
clear_password(app);
|
||||||
|
do_unlock(app);
|
||||||
|
} else {
|
||||||
|
clear_password(app);
|
||||||
|
note_failure(app);
|
||||||
|
}
|
||||||
|
schedule_redraw(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- keyboard ------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static void handle_key(struct att *app, xkb_keysym_t sym,
|
||||||
|
const char *utf8, int utf8_len)
|
||||||
|
{
|
||||||
|
switch (sym) {
|
||||||
|
case XKB_KEY_Escape:
|
||||||
|
if (app->mode == MODE_PASSWORD) {
|
||||||
|
clear_password(app);
|
||||||
|
app->mode = MODE_UNLOCK;
|
||||||
|
app->feedback = FB_NONE;
|
||||||
|
} else if (app->mode == MODE_UNLOCK) {
|
||||||
|
pattern_reset(&app->pattern);
|
||||||
|
app->feedback = FB_NONE;
|
||||||
|
}
|
||||||
|
schedule_redraw(app);
|
||||||
|
return;
|
||||||
|
case XKB_KEY_Return:
|
||||||
|
case XKB_KEY_KP_Enter:
|
||||||
|
if (app->mode == MODE_PASSWORD)
|
||||||
|
submit_password(app);
|
||||||
|
return;
|
||||||
|
case XKB_KEY_BackSpace:
|
||||||
|
if (app->mode == MODE_PASSWORD && app->password_len > 0)
|
||||||
|
app->password[--app->password_len] = '\0';
|
||||||
|
schedule_redraw(app);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Printable input: switch to the PAM password fallback from unlock mode.
|
||||||
|
* Setup modes are pattern-only. */
|
||||||
|
if (utf8_len > 0 && (unsigned char)utf8[0] >= 0x20) {
|
||||||
|
if (app->mode == MODE_UNLOCK) {
|
||||||
|
app->mode = MODE_PASSWORD;
|
||||||
|
app->feedback = FB_NONE;
|
||||||
|
pattern_reset(&app->pattern);
|
||||||
|
}
|
||||||
|
if (app->mode == MODE_PASSWORD &&
|
||||||
|
app->password_len + utf8_len < (int)sizeof(app->password) - 1) {
|
||||||
|
memcpy(app->password + app->password_len, utf8, utf8_len);
|
||||||
|
app->password_len += utf8_len;
|
||||||
|
app->password[app->password_len] = '\0';
|
||||||
|
}
|
||||||
|
schedule_redraw(app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void kb_keymap(void *data, struct wl_keyboard *kb, uint32_t format,
|
||||||
|
int32_t fd, uint32_t size)
|
||||||
|
{
|
||||||
|
(void)kb;
|
||||||
|
struct att_seat *seat = data;
|
||||||
|
if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) {
|
||||||
|
close(fd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
char *map = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
|
||||||
|
close(fd);
|
||||||
|
if (map == MAP_FAILED)
|
||||||
|
return;
|
||||||
|
|
||||||
|
struct xkb_keymap *keymap = xkb_keymap_new_from_string(
|
||||||
|
seat->xkb_ctx, map, XKB_KEYMAP_FORMAT_TEXT_V1,
|
||||||
|
XKB_KEYMAP_COMPILE_NO_FLAGS);
|
||||||
|
munmap(map, size);
|
||||||
|
if (!keymap)
|
||||||
|
return;
|
||||||
|
|
||||||
|
struct xkb_state *state = xkb_state_new(keymap);
|
||||||
|
if (!state) {
|
||||||
|
xkb_keymap_unref(keymap);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (seat->xkb_state)
|
||||||
|
xkb_state_unref(seat->xkb_state);
|
||||||
|
if (seat->xkb_keymap)
|
||||||
|
xkb_keymap_unref(seat->xkb_keymap);
|
||||||
|
seat->xkb_keymap = keymap;
|
||||||
|
seat->xkb_state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void kb_enter(void *d, struct wl_keyboard *k, uint32_t s,
|
||||||
|
struct wl_surface *surf, struct wl_array *keys)
|
||||||
|
{
|
||||||
|
(void)d; (void)k; (void)s; (void)surf; (void)keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void kb_leave(void *d, struct wl_keyboard *k, uint32_t s,
|
||||||
|
struct wl_surface *surf)
|
||||||
|
{
|
||||||
|
(void)d; (void)k; (void)s; (void)surf;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void kb_key(void *data, struct wl_keyboard *k, uint32_t serial,
|
||||||
|
uint32_t time, uint32_t key, uint32_t state)
|
||||||
|
{
|
||||||
|
(void)k; (void)serial; (void)time;
|
||||||
|
struct att_seat *seat = data;
|
||||||
|
if (!seat->xkb_state || state != WL_KEYBOARD_KEY_STATE_PRESSED)
|
||||||
|
return;
|
||||||
|
|
||||||
|
xkb_keycode_t code = key + 8;
|
||||||
|
xkb_keysym_t sym = xkb_state_key_get_one_sym(seat->xkb_state, code);
|
||||||
|
char buf[16];
|
||||||
|
int n = xkb_state_key_get_utf8(seat->xkb_state, code, buf, sizeof(buf));
|
||||||
|
handle_key(seat->app, sym, buf, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void kb_modifiers(void *data, struct wl_keyboard *k, uint32_t serial,
|
||||||
|
uint32_t depressed, uint32_t latched,
|
||||||
|
uint32_t locked, uint32_t group)
|
||||||
|
{
|
||||||
|
(void)k; (void)serial;
|
||||||
|
struct att_seat *seat = data;
|
||||||
|
if (seat->xkb_state)
|
||||||
|
xkb_state_update_mask(seat->xkb_state, depressed, latched,
|
||||||
|
locked, 0, 0, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void kb_repeat(void *d, struct wl_keyboard *k, int32_t rate, int32_t delay)
|
||||||
|
{
|
||||||
|
(void)d; (void)k; (void)rate; (void)delay;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const struct wl_keyboard_listener keyboard_listener = {
|
||||||
|
.keymap = kb_keymap,
|
||||||
|
.enter = kb_enter,
|
||||||
|
.leave = kb_leave,
|
||||||
|
.key = kb_key,
|
||||||
|
.modifiers = kb_modifiers,
|
||||||
|
.repeat_info = kb_repeat,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* --- pointer -------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static struct att_output *output_from_surface(struct att *app,
|
||||||
|
struct wl_surface *surface)
|
||||||
|
{
|
||||||
|
struct att_output *o;
|
||||||
|
wl_list_for_each(o, &app->outputs, link)
|
||||||
|
if (o->surface == surface)
|
||||||
|
return o;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ptr_enter(void *data, struct wl_pointer *p, uint32_t serial,
|
||||||
|
struct wl_surface *surface, wl_fixed_t sx, wl_fixed_t sy)
|
||||||
|
{
|
||||||
|
struct att_seat *seat = data;
|
||||||
|
/* Hide the cursor over the lock surface. */
|
||||||
|
wl_pointer_set_cursor(p, serial, NULL, 0, 0);
|
||||||
|
seat->app->active_output = output_from_surface(seat->app, surface);
|
||||||
|
seat->px = wl_fixed_to_double(sx);
|
||||||
|
seat->py = wl_fixed_to_double(sy);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ptr_leave(void *d, struct wl_pointer *p, uint32_t s,
|
||||||
|
struct wl_surface *surface)
|
||||||
|
{
|
||||||
|
(void)d; (void)p; (void)s; (void)surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ptr_motion(void *data, struct wl_pointer *p, uint32_t time,
|
||||||
|
wl_fixed_t sx, wl_fixed_t sy)
|
||||||
|
{
|
||||||
|
(void)p; (void)time;
|
||||||
|
struct att_seat *seat = data;
|
||||||
|
seat->px = wl_fixed_to_double(sx);
|
||||||
|
seat->py = wl_fixed_to_double(sy);
|
||||||
|
if (seat->app->active_output)
|
||||||
|
move_stroke(seat->app, seat->app->active_output,
|
||||||
|
seat->px, seat->py);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ptr_button(void *data, struct wl_pointer *p, uint32_t serial,
|
||||||
|
uint32_t time, uint32_t button, uint32_t state)
|
||||||
|
{
|
||||||
|
(void)p; (void)serial; (void)time;
|
||||||
|
struct att_seat *seat = data;
|
||||||
|
struct att *app = seat->app;
|
||||||
|
if (button != BTN_LEFT || !app->active_output)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (state == WL_POINTER_BUTTON_STATE_PRESSED)
|
||||||
|
begin_stroke(app, app->active_output, seat->px, seat->py);
|
||||||
|
else
|
||||||
|
finish_stroke(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ptr_noop_axis(void *d, struct wl_pointer *p, uint32_t t, uint32_t a,
|
||||||
|
wl_fixed_t v)
|
||||||
|
{ (void)d; (void)p; (void)t; (void)a; (void)v; }
|
||||||
|
static void ptr_noop_frame(void *d, struct wl_pointer *p) { (void)d; (void)p; }
|
||||||
|
static void ptr_noop_axis_source(void *d, struct wl_pointer *p, uint32_t s)
|
||||||
|
{ (void)d; (void)p; (void)s; }
|
||||||
|
static void ptr_noop_axis_stop(void *d, struct wl_pointer *p, uint32_t t, uint32_t a)
|
||||||
|
{ (void)d; (void)p; (void)t; (void)a; }
|
||||||
|
static void ptr_noop_axis_disc(void *d, struct wl_pointer *p, uint32_t a, int32_t v)
|
||||||
|
{ (void)d; (void)p; (void)a; (void)v; }
|
||||||
|
|
||||||
|
static const struct wl_pointer_listener pointer_listener = {
|
||||||
|
.enter = ptr_enter,
|
||||||
|
.leave = ptr_leave,
|
||||||
|
.motion = ptr_motion,
|
||||||
|
.button = ptr_button,
|
||||||
|
.axis = ptr_noop_axis,
|
||||||
|
.frame = ptr_noop_frame,
|
||||||
|
.axis_source = ptr_noop_axis_source,
|
||||||
|
.axis_stop = ptr_noop_axis_stop,
|
||||||
|
.axis_discrete = ptr_noop_axis_disc,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* --- touch ---------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static void touch_down(void *data, struct wl_touch *t, uint32_t serial,
|
||||||
|
uint32_t time, struct wl_surface *surface, int32_t id,
|
||||||
|
wl_fixed_t x, wl_fixed_t y)
|
||||||
|
{
|
||||||
|
(void)t; (void)serial; (void)time;
|
||||||
|
struct att_seat *seat = data;
|
||||||
|
if (seat->touch_id != -1)
|
||||||
|
return; /* track a single finger */
|
||||||
|
struct att_output *o = output_from_surface(seat->app, surface);
|
||||||
|
if (!o)
|
||||||
|
return;
|
||||||
|
seat->touch_id = id;
|
||||||
|
seat->app->active_output = o;
|
||||||
|
begin_stroke(seat->app, o, wl_fixed_to_double(x), wl_fixed_to_double(y));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void touch_up(void *data, struct wl_touch *t, uint32_t serial,
|
||||||
|
uint32_t time, int32_t id)
|
||||||
|
{
|
||||||
|
(void)t; (void)serial; (void)time;
|
||||||
|
struct att_seat *seat = data;
|
||||||
|
if (id != seat->touch_id)
|
||||||
|
return;
|
||||||
|
seat->touch_id = -1;
|
||||||
|
finish_stroke(seat->app);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void touch_motion(void *data, struct wl_touch *t, uint32_t time,
|
||||||
|
int32_t id, wl_fixed_t x, wl_fixed_t y)
|
||||||
|
{
|
||||||
|
(void)t; (void)time;
|
||||||
|
struct att_seat *seat = data;
|
||||||
|
if (id != seat->touch_id || !seat->app->active_output)
|
||||||
|
return;
|
||||||
|
move_stroke(seat->app, seat->app->active_output,
|
||||||
|
wl_fixed_to_double(x), wl_fixed_to_double(y));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void touch_frame(void *d, struct wl_touch *t) { (void)d; (void)t; }
|
||||||
|
|
||||||
|
static void touch_cancel(void *data, struct wl_touch *t)
|
||||||
|
{
|
||||||
|
(void)t;
|
||||||
|
struct att_seat *seat = data;
|
||||||
|
seat->touch_id = -1;
|
||||||
|
pattern_reset(&seat->app->pattern);
|
||||||
|
schedule_redraw(seat->app);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void touch_shape(void *d, struct wl_touch *t, int32_t id,
|
||||||
|
wl_fixed_t maj, wl_fixed_t min)
|
||||||
|
{ (void)d; (void)t; (void)id; (void)maj; (void)min; }
|
||||||
|
static void touch_orient(void *d, struct wl_touch *t, int32_t id, wl_fixed_t o)
|
||||||
|
{ (void)d; (void)t; (void)id; (void)o; }
|
||||||
|
|
||||||
|
static const struct wl_touch_listener touch_listener = {
|
||||||
|
.down = touch_down,
|
||||||
|
.up = touch_up,
|
||||||
|
.motion = touch_motion,
|
||||||
|
.frame = touch_frame,
|
||||||
|
.cancel = touch_cancel,
|
||||||
|
.shape = touch_shape,
|
||||||
|
.orientation = touch_orient,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* --- seat ----------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static void seat_capabilities(void *data, struct wl_seat *wl_seat, uint32_t caps)
|
||||||
|
{
|
||||||
|
struct att_seat *seat = data;
|
||||||
|
|
||||||
|
if ((caps & WL_SEAT_CAPABILITY_POINTER) && !seat->pointer) {
|
||||||
|
seat->pointer = wl_seat_get_pointer(wl_seat);
|
||||||
|
wl_pointer_add_listener(seat->pointer, &pointer_listener, seat);
|
||||||
|
} else if (!(caps & WL_SEAT_CAPABILITY_POINTER) && seat->pointer) {
|
||||||
|
wl_pointer_release(seat->pointer);
|
||||||
|
seat->pointer = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((caps & WL_SEAT_CAPABILITY_TOUCH) && !seat->touch) {
|
||||||
|
seat->touch = wl_seat_get_touch(wl_seat);
|
||||||
|
wl_touch_add_listener(seat->touch, &touch_listener, seat);
|
||||||
|
} else if (!(caps & WL_SEAT_CAPABILITY_TOUCH) && seat->touch) {
|
||||||
|
wl_touch_release(seat->touch);
|
||||||
|
seat->touch = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((caps & WL_SEAT_CAPABILITY_KEYBOARD) && !seat->keyboard) {
|
||||||
|
seat->keyboard = wl_seat_get_keyboard(wl_seat);
|
||||||
|
wl_keyboard_add_listener(seat->keyboard, &keyboard_listener, seat);
|
||||||
|
} else if (!(caps & WL_SEAT_CAPABILITY_KEYBOARD) && seat->keyboard) {
|
||||||
|
wl_keyboard_release(seat->keyboard);
|
||||||
|
seat->keyboard = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void seat_name(void *d, struct wl_seat *s, const char *name)
|
||||||
|
{ (void)d; (void)s; (void)name; }
|
||||||
|
|
||||||
|
static const struct wl_seat_listener seat_listener = {
|
||||||
|
.capabilities = seat_capabilities,
|
||||||
|
.name = seat_name,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* --- output --------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static void out_geometry(void *d, struct wl_output *o, int32_t x, int32_t y,
|
||||||
|
int32_t pw, int32_t ph, int32_t subpixel,
|
||||||
|
const char *make, const char *model, int32_t transform)
|
||||||
|
{ (void)d; (void)o; (void)x; (void)y; (void)pw; (void)ph; (void)subpixel;
|
||||||
|
(void)make; (void)model; (void)transform; }
|
||||||
|
|
||||||
|
static void out_mode(void *d, struct wl_output *o, uint32_t flags,
|
||||||
|
int32_t w, int32_t h, int32_t refresh)
|
||||||
|
{ (void)d; (void)o; (void)flags; (void)w; (void)h; (void)refresh; }
|
||||||
|
|
||||||
|
static void out_scale(void *data, struct wl_output *o, int32_t factor)
|
||||||
|
{
|
||||||
|
(void)o;
|
||||||
|
struct att_output *out = data;
|
||||||
|
out->scale = factor > 0 ? factor : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void out_done(void *d, struct wl_output *o) { (void)d; (void)o; }
|
||||||
|
static void out_name(void *d, struct wl_output *o, const char *name)
|
||||||
|
{ (void)d; (void)o; (void)name; }
|
||||||
|
static void out_description(void *d, struct wl_output *o, const char *desc)
|
||||||
|
{ (void)d; (void)o; (void)desc; }
|
||||||
|
|
||||||
|
static const struct wl_output_listener output_listener = {
|
||||||
|
.geometry = out_geometry,
|
||||||
|
.mode = out_mode,
|
||||||
|
.done = out_done,
|
||||||
|
.scale = out_scale,
|
||||||
|
.name = out_name,
|
||||||
|
.description = out_description,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* --- lock surface --------------------------------------------------------- */
|
||||||
|
|
||||||
|
static void lock_surface_configure(void *data,
|
||||||
|
struct ext_session_lock_surface_v1 *surf,
|
||||||
|
uint32_t serial, uint32_t width, uint32_t height)
|
||||||
|
{
|
||||||
|
struct att_output *o = data;
|
||||||
|
ext_session_lock_surface_v1_ack_configure(surf, serial);
|
||||||
|
o->width = (int32_t)width;
|
||||||
|
o->height = (int32_t)height;
|
||||||
|
o->configured = true;
|
||||||
|
render_output(o); /* must commit a buffer for the compositor to lock */
|
||||||
|
}
|
||||||
|
|
||||||
|
static const struct ext_session_lock_surface_v1_listener lock_surface_listener = {
|
||||||
|
.configure = lock_surface_configure,
|
||||||
|
};
|
||||||
|
|
||||||
|
static void create_lock_surface(struct att *app, struct att_output *o)
|
||||||
|
{
|
||||||
|
if (!app->lock || o->lock_surface)
|
||||||
|
return;
|
||||||
|
o->surface = wl_compositor_create_surface(app->compositor);
|
||||||
|
o->lock_surface = ext_session_lock_v1_get_lock_surface(
|
||||||
|
app->lock, o->surface, o->wl_output);
|
||||||
|
ext_session_lock_surface_v1_add_listener(o->lock_surface,
|
||||||
|
&lock_surface_listener, o);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void output_destroy(struct att_output *o)
|
||||||
|
{
|
||||||
|
wl_list_remove(&o->link);
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
if (o->buffers[i].wl_buffer)
|
||||||
|
wl_buffer_destroy(o->buffers[i].wl_buffer);
|
||||||
|
if (o->buffers[i].data && o->buffers[i].data != MAP_FAILED)
|
||||||
|
munmap(o->buffers[i].data, o->buffers[i].size);
|
||||||
|
}
|
||||||
|
if (o->lock_surface)
|
||||||
|
ext_session_lock_surface_v1_destroy(o->lock_surface);
|
||||||
|
if (o->surface)
|
||||||
|
wl_surface_destroy(o->surface);
|
||||||
|
if (o->wl_output)
|
||||||
|
wl_output_destroy(o->wl_output);
|
||||||
|
if (o->app->active_output == o)
|
||||||
|
o->app->active_output = NULL;
|
||||||
|
free(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- lock object ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
static void lock_locked(void *data, struct ext_session_lock_v1 *lock)
|
||||||
|
{
|
||||||
|
(void)lock;
|
||||||
|
struct att *app = data;
|
||||||
|
app->locked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void lock_finished(void *data, struct ext_session_lock_v1 *lock)
|
||||||
|
{
|
||||||
|
(void)lock;
|
||||||
|
struct att *app = data;
|
||||||
|
/* The compositor refused or revoked the lock. Do NOT unlock; just exit
|
||||||
|
* with failure so the session stays in whatever state the compositor
|
||||||
|
* decided. */
|
||||||
|
fprintf(stderr, "att_lock: session lock finished by compositor\n");
|
||||||
|
app->running = false;
|
||||||
|
app->locked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const struct ext_session_lock_v1_listener lock_listener = {
|
||||||
|
.locked = lock_locked,
|
||||||
|
.finished = lock_finished,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* --- registry ------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static void registry_global(void *data, struct wl_registry *reg, uint32_t name,
|
||||||
|
const char *iface, uint32_t version)
|
||||||
|
{
|
||||||
|
struct att *app = data;
|
||||||
|
|
||||||
|
if (strcmp(iface, wl_compositor_interface.name) == 0) {
|
||||||
|
app->compositor = wl_registry_bind(reg, name,
|
||||||
|
&wl_compositor_interface, version < 4 ? version : 4);
|
||||||
|
} else if (strcmp(iface, wl_shm_interface.name) == 0) {
|
||||||
|
app->shm = wl_registry_bind(reg, name, &wl_shm_interface, 1);
|
||||||
|
} else if (strcmp(iface, ext_session_lock_manager_v1_interface.name) == 0) {
|
||||||
|
app->lock_manager = wl_registry_bind(reg, name,
|
||||||
|
&ext_session_lock_manager_v1_interface, 1);
|
||||||
|
} else if (strcmp(iface, wl_seat_interface.name) == 0) {
|
||||||
|
struct att_seat *seat = calloc(1, sizeof(*seat));
|
||||||
|
if (!seat)
|
||||||
|
return;
|
||||||
|
seat->app = app;
|
||||||
|
seat->global_name = name;
|
||||||
|
seat->touch_id = -1;
|
||||||
|
seat->xkb_ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
||||||
|
seat->wl_seat = wl_registry_bind(reg, name,
|
||||||
|
&wl_seat_interface, version < 5 ? version : 5);
|
||||||
|
wl_seat_add_listener(seat->wl_seat, &seat_listener, seat);
|
||||||
|
wl_list_insert(&app->seats, &seat->link);
|
||||||
|
} else if (strcmp(iface, wl_output_interface.name) == 0) {
|
||||||
|
struct att_output *o = calloc(1, sizeof(*o));
|
||||||
|
if (!o)
|
||||||
|
return;
|
||||||
|
o->app = app;
|
||||||
|
o->global_name = name;
|
||||||
|
o->scale = 1;
|
||||||
|
o->wl_output = wl_registry_bind(reg, name,
|
||||||
|
&wl_output_interface, version < 4 ? version : 4);
|
||||||
|
wl_output_add_listener(o->wl_output, &output_listener, o);
|
||||||
|
wl_list_insert(&app->outputs, &o->link);
|
||||||
|
create_lock_surface(app, o); /* no-op until the lock exists */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void registry_global_remove(void *data, struct wl_registry *reg,
|
||||||
|
uint32_t name)
|
||||||
|
{
|
||||||
|
(void)reg;
|
||||||
|
struct att *app = data;
|
||||||
|
struct att_output *o, *otmp;
|
||||||
|
wl_list_for_each_safe(o, otmp, &app->outputs, link) {
|
||||||
|
if (o->global_name == name) {
|
||||||
|
output_destroy(o);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const struct wl_registry_listener registry_listener = {
|
||||||
|
.global = registry_global,
|
||||||
|
.global_remove = registry_global_remove,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* --- setup / teardown ----------------------------------------------------- */
|
||||||
|
|
||||||
|
static void usage(const char *argv0)
|
||||||
|
{
|
||||||
|
fprintf(stderr,
|
||||||
|
"Usage: %s [options] [IMAGE]\n"
|
||||||
|
"\n"
|
||||||
|
"Android-style 4x4 pattern screen locker for wlroots compositors.\n"
|
||||||
|
"\n"
|
||||||
|
"Options:\n"
|
||||||
|
" -i, --image PATH Background image (PNG), also accepted positionally\n"
|
||||||
|
" -p, --pam-service NAME PAM service for the password fallback (default: login)\n"
|
||||||
|
" -m, --min-dots N Minimum dots in a pattern (default: 4)\n"
|
||||||
|
" --setup Force (re)creation of the stored pattern\n"
|
||||||
|
" -h, --help Show this help\n"
|
||||||
|
"\n"
|
||||||
|
"On first run (or with --setup) you draw the pattern twice to store it as a\n"
|
||||||
|
"salted hash under $XDG_DATA_HOME/att_lock/pattern.hash. Start typing at the\n"
|
||||||
|
"unlock screen to fall back to PAM password authentication.\n",
|
||||||
|
argv0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
struct att app = {0};
|
||||||
|
app.running = true;
|
||||||
|
app.pam_service = "login";
|
||||||
|
app.min_dots = 4;
|
||||||
|
wl_list_init(&app.outputs);
|
||||||
|
wl_list_init(&app.seats);
|
||||||
|
|
||||||
|
bool force_setup = false;
|
||||||
|
static const struct option opts[] = {
|
||||||
|
{"image", required_argument, 0, 'i'},
|
||||||
|
{"pam-service", required_argument, 0, 'p'},
|
||||||
|
{"min-dots", required_argument, 0, 'm'},
|
||||||
|
{"setup", no_argument, 0, 's'},
|
||||||
|
{"help", no_argument, 0, 'h'},
|
||||||
|
{0, 0, 0, 0},
|
||||||
|
};
|
||||||
|
int c;
|
||||||
|
while ((c = getopt_long(argc, argv, "i:p:m:h", opts, NULL)) != -1) {
|
||||||
|
switch (c) {
|
||||||
|
case 'i': app.image_path = optarg; break;
|
||||||
|
case 'p': app.pam_service = optarg; break;
|
||||||
|
case 'm': app.min_dots = atoi(optarg); break;
|
||||||
|
case 's': force_setup = true; break;
|
||||||
|
case 'h': usage(argv[0]); return 0;
|
||||||
|
default: usage(argv[0]); return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (optind < argc && !app.image_path)
|
||||||
|
app.image_path = argv[optind];
|
||||||
|
if (app.min_dots < 2)
|
||||||
|
app.min_dots = 2;
|
||||||
|
if (app.min_dots > DOT_COUNT)
|
||||||
|
app.min_dots = DOT_COUNT;
|
||||||
|
|
||||||
|
app.hash_path = auth_hash_path();
|
||||||
|
if (!app.hash_path) {
|
||||||
|
fprintf(stderr, "att_lock: cannot determine data directory\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
enum auth_hash_state hstate = auth_hash_state(app.hash_path);
|
||||||
|
if (hstate == AUTH_HASH_STALE)
|
||||||
|
fprintf(stderr, "att_lock: stored pattern is unreadable or was "
|
||||||
|
"written by an older version; a new one must be drawn\n");
|
||||||
|
app.mode = (force_setup || hstate != AUTH_HASH_OK)
|
||||||
|
? MODE_SETUP_FIRST : MODE_UNLOCK;
|
||||||
|
|
||||||
|
if (app.image_path) {
|
||||||
|
cairo_surface_t *bg =
|
||||||
|
cairo_image_surface_create_from_png(app.image_path);
|
||||||
|
if (cairo_surface_status(bg) == CAIRO_STATUS_SUCCESS) {
|
||||||
|
app.bg_surface = bg;
|
||||||
|
} else {
|
||||||
|
fprintf(stderr, "att_lock: could not load image '%s' "
|
||||||
|
"(PNG only): %s\n", app.image_path,
|
||||||
|
cairo_status_to_string(cairo_surface_status(bg)));
|
||||||
|
cairo_surface_destroy(bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.display = wl_display_connect(NULL);
|
||||||
|
if (!app.display) {
|
||||||
|
fprintf(stderr, "att_lock: failed to connect to Wayland display\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
app.registry = wl_display_get_registry(app.display);
|
||||||
|
wl_registry_add_listener(app.registry, ®istry_listener, &app);
|
||||||
|
wl_display_roundtrip(app.display); /* bind globals */
|
||||||
|
wl_display_roundtrip(app.display); /* settle seat caps / output scale */
|
||||||
|
|
||||||
|
if (!app.compositor || !app.shm || !app.lock_manager) {
|
||||||
|
fprintf(stderr, "att_lock: compositor lacks required globals "
|
||||||
|
"(ext-session-lock-v1 support needed)\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.lock = ext_session_lock_manager_v1_lock(app.lock_manager);
|
||||||
|
ext_session_lock_v1_add_listener(app.lock, &lock_listener, &app);
|
||||||
|
|
||||||
|
struct att_output *o;
|
||||||
|
wl_list_for_each(o, &app.outputs, link)
|
||||||
|
create_lock_surface(&app, o);
|
||||||
|
|
||||||
|
/* Poll rather than wl_display_dispatch() so a running lockout can wake us
|
||||||
|
* to repaint its countdown; without a timeout we would sleep until the
|
||||||
|
* next input event and the displayed seconds would stall. */
|
||||||
|
struct pollfd pfd = { .fd = wl_display_get_fd(app.display), .events = POLLIN };
|
||||||
|
|
||||||
|
while (app.running) {
|
||||||
|
int timeout = -1;
|
||||||
|
if (app.lockout_until) {
|
||||||
|
int64_t left = app.lockout_until - att_now_ms();
|
||||||
|
if (left > 0) {
|
||||||
|
/* Wake exactly when the displayed second changes. */
|
||||||
|
timeout = (int)((left - 1) % 1000) + 1;
|
||||||
|
} else {
|
||||||
|
app.lockout_until = 0;
|
||||||
|
schedule_redraw(&app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app.dirty) {
|
||||||
|
app.dirty = false;
|
||||||
|
render_all(&app);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (wl_display_prepare_read(app.display) != 0)
|
||||||
|
wl_display_dispatch_pending(app.display);
|
||||||
|
if (wl_display_flush(app.display) < 0 && errno != EAGAIN) {
|
||||||
|
wl_display_cancel_read(app.display);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret = poll(&pfd, 1, timeout);
|
||||||
|
if (ret < 0) {
|
||||||
|
wl_display_cancel_read(app.display);
|
||||||
|
if (errno == EINTR)
|
||||||
|
continue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (ret > 0 && (pfd.revents & (POLLERR | POLLHUP))) {
|
||||||
|
wl_display_cancel_read(app.display);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (ret > 0 && (pfd.revents & POLLIN)) {
|
||||||
|
if (wl_display_read_events(app.display) < 0)
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
wl_display_cancel_read(app.display);
|
||||||
|
schedule_redraw(&app); /* timed out: countdown tick */
|
||||||
|
}
|
||||||
|
if (wl_display_dispatch_pending(app.display) < 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cleanup */
|
||||||
|
if (app.bg_surface)
|
||||||
|
cairo_surface_destroy(app.bg_surface);
|
||||||
|
clear_password(&app);
|
||||||
|
free(app.hash_path);
|
||||||
|
|
||||||
|
/* If the lock still exists we exited without unlocking (e.g. the
|
||||||
|
* compositor sent `finished`): report failure. */
|
||||||
|
int rc = app.lock ? 1 : 0;
|
||||||
|
if (app.lock)
|
||||||
|
ext_session_lock_v1_destroy(app.lock);
|
||||||
|
wl_display_flush(app.display);
|
||||||
|
wl_display_disconnect(app.display);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
#include "att_lock.h"
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static int igcd(int a, int b)
|
||||||
|
{
|
||||||
|
a = a < 0 ? -a : a;
|
||||||
|
b = b < 0 ? -b : b;
|
||||||
|
while (b) {
|
||||||
|
int t = a % b;
|
||||||
|
a = b;
|
||||||
|
b = t;
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compute the grid layout for an output. The grid is a centred square whose
|
||||||
|
* side is 70% of the shorter output dimension, so it adapts to rotation and
|
||||||
|
* aspect ratio automatically. */
|
||||||
|
void att_grid(const struct att_output *o, struct grid *g)
|
||||||
|
{
|
||||||
|
double w = o->width, h = o->height;
|
||||||
|
double side = (w < h ? w : h) * 0.7;
|
||||||
|
g->cell = side / GRID_N;
|
||||||
|
g->ox = (w - side) / 2.0;
|
||||||
|
g->oy = (h - side) / 2.0;
|
||||||
|
g->radius = g->cell * 0.10;
|
||||||
|
g->hit = g->cell * 0.33;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pattern_reset(struct att_pattern *p)
|
||||||
|
{
|
||||||
|
p->len = 0;
|
||||||
|
p->drawing = false;
|
||||||
|
memset(p->visited, 0, sizeof(p->visited));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Return the dot index whose centre is within the hit radius of (x, y), or -1. */
|
||||||
|
int pattern_hit(const struct grid *g, double x, double y)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < DOT_COUNT; i++) {
|
||||||
|
int row = i / GRID_N;
|
||||||
|
int col = i % GRID_N;
|
||||||
|
double cx = g->ox + g->cell * (col + 0.5);
|
||||||
|
double cy = g->oy + g->cell * (row + 0.5);
|
||||||
|
double dx = x - cx;
|
||||||
|
double dy = y - cy;
|
||||||
|
if (dx * dx + dy * dy <= g->hit * g->hit)
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Append a dot to the sequence. As on Android, any not-yet-visited dots lying
|
||||||
|
* exactly on the straight line between the previous dot and the new one are
|
||||||
|
* selected first (e.g. connecting 0 -> 2 also captures 1). */
|
||||||
|
void pattern_add(struct att_pattern *p, int idx)
|
||||||
|
{
|
||||||
|
if (idx < 0 || idx >= DOT_COUNT || p->visited[idx])
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (p->len > 0) {
|
||||||
|
int last = p->seq[p->len - 1];
|
||||||
|
int r0 = last / GRID_N, c0 = last % GRID_N;
|
||||||
|
int r1 = idx / GRID_N, c1 = idx % GRID_N;
|
||||||
|
int dr = r1 - r0, dc = c1 - c0;
|
||||||
|
int g = igcd(dr, dc);
|
||||||
|
if (g > 1) {
|
||||||
|
int sr = dr / g, sc = dc / g;
|
||||||
|
for (int k = 1; k < g; k++) {
|
||||||
|
int mid = (r0 + sr * k) * GRID_N + (c0 + sc * k);
|
||||||
|
if (!p->visited[mid] && p->len < MAX_SEQ) {
|
||||||
|
p->visited[mid] = true;
|
||||||
|
p->seq[p->len++] = mid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!p->visited[idx] && p->len < MAX_SEQ) {
|
||||||
|
p->visited[idx] = true;
|
||||||
|
p->seq[p->len++] = idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Serialise the sequence to a stable canonical string, e.g. "0-1-2-5". */
|
||||||
|
int pattern_to_string(const struct att_pattern *p, char *buf, size_t n)
|
||||||
|
{
|
||||||
|
size_t off = 0;
|
||||||
|
for (int i = 0; i < p->len; i++) {
|
||||||
|
int w = snprintf(buf + off, n - off, "%s%d",
|
||||||
|
i ? "-" : "", p->seq[i]);
|
||||||
|
if (w < 0 || (size_t)w >= n - off)
|
||||||
|
return -1;
|
||||||
|
off += (size_t)w;
|
||||||
|
}
|
||||||
|
if (off == 0 && n > 0)
|
||||||
|
buf[0] = '\0';
|
||||||
|
return (int)off;
|
||||||
|
}
|
||||||
+286
@@ -0,0 +1,286 @@
|
|||||||
|
#include "att_lock.h"
|
||||||
|
|
||||||
|
#include <cairo.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
/* --- shm buffer pool ------------------------------------------------------ */
|
||||||
|
|
||||||
|
static void buffer_release(void *data, struct wl_buffer *wl_buffer)
|
||||||
|
{
|
||||||
|
(void)wl_buffer;
|
||||||
|
struct att_buffer *b = data;
|
||||||
|
b->busy = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const struct wl_buffer_listener buffer_listener = {
|
||||||
|
.release = buffer_release,
|
||||||
|
};
|
||||||
|
|
||||||
|
static void buffer_destroy(struct att_buffer *b)
|
||||||
|
{
|
||||||
|
if (b->wl_buffer)
|
||||||
|
wl_buffer_destroy(b->wl_buffer);
|
||||||
|
if (b->data && b->data != MAP_FAILED)
|
||||||
|
munmap(b->data, b->size);
|
||||||
|
memset(b, 0, sizeof(*b));
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool buffer_create(struct att *app, struct att_buffer *b, int w, int h)
|
||||||
|
{
|
||||||
|
int stride = w * 4;
|
||||||
|
size_t size = (size_t)stride * h;
|
||||||
|
|
||||||
|
int fd = memfd_create("att_lock-shm", MFD_CLOEXEC | MFD_ALLOW_SEALING);
|
||||||
|
if (fd < 0)
|
||||||
|
return false;
|
||||||
|
if (ftruncate(fd, size) < 0) {
|
||||||
|
close(fd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
||||||
|
if (data == MAP_FAILED) {
|
||||||
|
close(fd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct wl_shm_pool *pool = wl_shm_create_pool(app->shm, fd, size);
|
||||||
|
struct wl_buffer *wl_buffer = wl_shm_pool_create_buffer(
|
||||||
|
pool, 0, w, h, stride, WL_SHM_FORMAT_ARGB8888);
|
||||||
|
wl_shm_pool_destroy(pool);
|
||||||
|
close(fd);
|
||||||
|
|
||||||
|
b->wl_buffer = wl_buffer;
|
||||||
|
b->data = data;
|
||||||
|
b->size = size;
|
||||||
|
b->width = w;
|
||||||
|
b->height = h;
|
||||||
|
b->busy = false;
|
||||||
|
wl_buffer_add_listener(wl_buffer, &buffer_listener, b);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Return a non-busy buffer sized w*h, (re)creating as needed, or NULL. */
|
||||||
|
static struct att_buffer *buffer_get(struct att *app, struct att_output *o,
|
||||||
|
int w, int h)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
struct att_buffer *b = &o->buffers[i];
|
||||||
|
if (b->busy)
|
||||||
|
continue;
|
||||||
|
if (b->wl_buffer && (b->width != w || b->height != h))
|
||||||
|
buffer_destroy(b);
|
||||||
|
if (!b->wl_buffer && !buffer_create(app, b, w, h))
|
||||||
|
return NULL;
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- drawing -------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static void feedback_color(enum att_feedback fb, double *r, double *g, double *b)
|
||||||
|
{
|
||||||
|
switch (fb) {
|
||||||
|
case FB_ERROR: *r = 0.90; *g = 0.25; *b = 0.25; break;
|
||||||
|
case FB_OK: *r = 0.30; *g = 0.80; *b = 0.45; break;
|
||||||
|
default: *r = 0.30; *g = 0.62; *b = 0.98; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dot_center(const struct grid *gr, int idx, double *cx, double *cy)
|
||||||
|
{
|
||||||
|
int row = idx / GRID_N, col = idx % GRID_N;
|
||||||
|
*cx = gr->ox + gr->cell * (col + 0.5);
|
||||||
|
*cy = gr->oy + gr->cell * (row + 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void draw_background(cairo_t *cr, struct att *app, int w, int h)
|
||||||
|
{
|
||||||
|
cairo_surface_t *bg = app->bg_surface;
|
||||||
|
if (bg) {
|
||||||
|
int iw = cairo_image_surface_get_width(bg);
|
||||||
|
int ih = cairo_image_surface_get_height(bg);
|
||||||
|
if (iw > 0 && ih > 0) {
|
||||||
|
double s = fmax((double)w / iw, (double)h / ih);
|
||||||
|
cairo_save(cr);
|
||||||
|
cairo_translate(cr, (w - iw * s) / 2.0, (h - ih * s) / 2.0);
|
||||||
|
cairo_scale(cr, s, s);
|
||||||
|
cairo_set_source_surface(cr, bg, 0, 0);
|
||||||
|
cairo_paint(cr);
|
||||||
|
cairo_restore(cr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cairo_set_source_rgb(cr, 0.05, 0.05, 0.08);
|
||||||
|
cairo_paint(cr);
|
||||||
|
}
|
||||||
|
/* Dim overlay so the grid stays legible over any wallpaper. */
|
||||||
|
cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.45);
|
||||||
|
cairo_paint(cr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void draw_text(cairo_t *cr, int w, int h, const char *text, double cy_frac)
|
||||||
|
{
|
||||||
|
if (!text || !text[0])
|
||||||
|
return;
|
||||||
|
cairo_select_font_face(cr, "sans-serif",
|
||||||
|
CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
|
||||||
|
double size = (w < h ? w : h) * 0.045;
|
||||||
|
cairo_set_font_size(cr, size);
|
||||||
|
cairo_text_extents_t ext;
|
||||||
|
cairo_text_extents(cr, text, &ext);
|
||||||
|
cairo_move_to(cr, (w - ext.width) / 2.0 - ext.x_bearing, h * cy_frac);
|
||||||
|
cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 0.92);
|
||||||
|
cairo_show_text(cr, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void prompt_text(struct att *app, char *buf, size_t n)
|
||||||
|
{
|
||||||
|
int secs = att_lockout_secs(app);
|
||||||
|
if (secs > 0) {
|
||||||
|
if (secs >= 60)
|
||||||
|
snprintf(buf, n, "Too many attempts — try again in %d:%02d",
|
||||||
|
secs / 60, secs % 60);
|
||||||
|
else
|
||||||
|
snprintf(buf, n, "Too many attempts — try again in %ds", secs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *s;
|
||||||
|
switch (app->mode) {
|
||||||
|
case MODE_SETUP_FIRST:
|
||||||
|
s = app->feedback == FB_ERROR ? "Patterns didn't match — draw a new pattern"
|
||||||
|
: "Draw a new unlock pattern";
|
||||||
|
break;
|
||||||
|
case MODE_SETUP_CONFIRM:
|
||||||
|
s = "Draw the pattern again to confirm";
|
||||||
|
break;
|
||||||
|
case MODE_PASSWORD:
|
||||||
|
s = app->feedback == FB_ERROR ? "Authentication failed — type password, Enter to submit"
|
||||||
|
: "Password (Enter to submit, Esc to cancel)";
|
||||||
|
break;
|
||||||
|
case MODE_UNLOCK:
|
||||||
|
default:
|
||||||
|
s = app->feedback == FB_ERROR ? "Wrong pattern — try again"
|
||||||
|
: "Draw pattern to unlock";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
snprintf(buf, n, "%s", s);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void draw_password(cairo_t *cr, struct att *app, int w, int h)
|
||||||
|
{
|
||||||
|
int shown = app->password_len;
|
||||||
|
if (shown > 16)
|
||||||
|
shown = 16;
|
||||||
|
double r = (w < h ? w : h) * 0.012;
|
||||||
|
double gap = r * 3.0;
|
||||||
|
double total = shown > 0 ? (shown - 1) * gap : 0;
|
||||||
|
double x = w / 2.0 - total / 2.0;
|
||||||
|
double y = h / 2.0;
|
||||||
|
cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 0.9);
|
||||||
|
for (int i = 0; i < shown; i++) {
|
||||||
|
cairo_arc(cr, x + i * gap, y, r, 0, 2 * M_PI);
|
||||||
|
cairo_fill(cr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void draw_pattern(cairo_t *cr, struct att *app, struct att_output *o,
|
||||||
|
const struct grid *gr)
|
||||||
|
{
|
||||||
|
struct att_pattern *p = &app->pattern;
|
||||||
|
double r, g, b;
|
||||||
|
feedback_color(app->feedback, &r, &g, &b);
|
||||||
|
|
||||||
|
/* connecting lines */
|
||||||
|
if (p->len > 0) {
|
||||||
|
cairo_set_line_width(cr, gr->cell * 0.06);
|
||||||
|
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
|
||||||
|
cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
|
||||||
|
cairo_set_source_rgba(cr, r, g, b, 0.85);
|
||||||
|
|
||||||
|
double cx, cy;
|
||||||
|
dot_center(gr, p->seq[0], &cx, &cy);
|
||||||
|
cairo_move_to(cr, cx, cy);
|
||||||
|
for (int i = 1; i < p->len; i++) {
|
||||||
|
dot_center(gr, p->seq[i], &cx, &cy);
|
||||||
|
cairo_line_to(cr, cx, cy);
|
||||||
|
}
|
||||||
|
/* live segment to the fingertip, only on the active output */
|
||||||
|
if (p->drawing && o == app->active_output)
|
||||||
|
cairo_line_to(cr, p->cur_x, p->cur_y);
|
||||||
|
cairo_stroke(cr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* dots */
|
||||||
|
for (int i = 0; i < DOT_COUNT; i++) {
|
||||||
|
double cx, cy;
|
||||||
|
dot_center(gr, i, &cx, &cy);
|
||||||
|
|
||||||
|
cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 0.30);
|
||||||
|
cairo_arc(cr, cx, cy, gr->radius, 0, 2 * M_PI);
|
||||||
|
cairo_fill(cr);
|
||||||
|
|
||||||
|
if (p->visited[i]) {
|
||||||
|
cairo_set_source_rgba(cr, r, g, b, 1.0);
|
||||||
|
cairo_arc(cr, cx, cy, gr->radius * 1.9, 0, 2 * M_PI);
|
||||||
|
cairo_fill(cr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void render_output(struct att_output *o)
|
||||||
|
{
|
||||||
|
struct att *app = o->app;
|
||||||
|
if (!o->configured || o->width <= 0 || o->height <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int scale = o->scale > 0 ? o->scale : 1;
|
||||||
|
int pw = o->width * scale;
|
||||||
|
int ph = o->height * scale;
|
||||||
|
|
||||||
|
struct att_buffer *b = buffer_get(app, o, pw, ph);
|
||||||
|
if (!b)
|
||||||
|
return;
|
||||||
|
|
||||||
|
cairo_surface_t *surface = cairo_image_surface_create_for_data(
|
||||||
|
b->data, CAIRO_FORMAT_ARGB32, pw, ph, pw * 4);
|
||||||
|
cairo_t *cr = cairo_create(surface);
|
||||||
|
cairo_scale(cr, scale, scale); /* draw in logical coordinates */
|
||||||
|
|
||||||
|
draw_background(cr, app, o->width, o->height);
|
||||||
|
|
||||||
|
if (app->mode == MODE_PASSWORD) {
|
||||||
|
draw_password(cr, app, o->width, o->height);
|
||||||
|
} else {
|
||||||
|
struct grid gr;
|
||||||
|
att_grid(o, &gr);
|
||||||
|
draw_pattern(cr, app, o, &gr);
|
||||||
|
}
|
||||||
|
|
||||||
|
char prompt[128];
|
||||||
|
prompt_text(app, prompt, sizeof(prompt));
|
||||||
|
draw_text(cr, o->width, o->height, prompt, 0.15);
|
||||||
|
|
||||||
|
cairo_destroy(cr);
|
||||||
|
cairo_surface_flush(surface);
|
||||||
|
cairo_surface_destroy(surface);
|
||||||
|
|
||||||
|
b->busy = true;
|
||||||
|
wl_surface_set_buffer_scale(o->surface, scale);
|
||||||
|
wl_surface_attach(o->surface, b->wl_buffer, 0, 0);
|
||||||
|
wl_surface_damage_buffer(o->surface, 0, 0, pw, ph);
|
||||||
|
wl_surface_commit(o->surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
void render_all(struct att *app)
|
||||||
|
{
|
||||||
|
struct att_output *o;
|
||||||
|
wl_list_for_each(o, &app->outputs, link)
|
||||||
|
render_output(o);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user