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