initial commit

This commit is contained in:
2026-07-26 21:19:04 +02:00
commit 44aca6d476
12 changed files with 2281 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
/build/
/result
/result-*
*.o
+116
View File
@@ -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.
+159
View File
@@ -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
View File
@@ -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
}
+54
View File
@@ -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
View File
@@ -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)
+175
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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, &registry_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
View File
@@ -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
View File
@@ -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);
}