Skip to content

PIN lock screen, with one counted door for every PIN check - #4

Merged
kwsantiago merged 10 commits into
mainfrom
lock-sudo
Oct 20, 2026
Merged

kwsantiago merged 10 commits into
mainfrom
lock-sudo

Conversation

@kwsantiago

@kwsantiago kwsantiago commented Oct 20, 2026 •

Copy link
Copy Markdown

The phone now has a real lock: a 6 to 12 digit PIN, chosen at first boot, asked for on a keypad drawn on the lock surface. It locks at boot, on a power press and after the idle timeout. The PIN is the account's password, so every way of checking it goes through one counted door. Passwordless sudo is unchanged here; removing it is the next PR.

Change

Lock screen

  • default/omarchy/plugins/moarchy.lock: an ext-session-lock plugin built on upstream's lock service (late outputs, recovering a lock left by a crashed shell). It draws its own keypad, because the on-screen keyboard is hidden under a session lock. The decisions live in Lock.js and are unit-tested in tests/tst_lock.qml.
  • Setup mode on first boot sets the first PIN through moarchy-pin first. There is no way past it without choosing one.
  • A phone whose account already has a password set some other way does not lock until Settings > Change PIN sets a PIN, because a keypad may never be able to type that password.
  • Calls while locked show Answer and Decline, then Hang up, with the keypad still below. moarchy.phone reports every call-state change, so a lock taken mid-call still shows the call.
  • The plugin is required: enabled whatever a user shell.json says, and never offered as a switch (port-4x.patch).

When it locks

  • Boot: autostart.lua blanks the panel, switches touch off and puts the keyboard in a submap that swallows every key until the lock is secure.
  • Power button: a single press locks once the double-press window has passed, and only a real double press cancels that. While a lock is being taken, a press lights the panel but leaves touch off until the lock is up.
  • Idle: moarchy-idle-lock honors Stay Awake.
  • The lock state in $XDG_RUNTIME_DIR is tagged with the compositor it was written under, so a state left by an earlier Hyprland is ignored.

One door to the PIN

  • bin/moarchy-pin is the only place a PIN is checked: check from the lock screen through a narrow sudoers rule, and pam from system-auth through pam_exec for sudo, polkit and console logins.
  • It counts a guess before checking it, takes one check at a time, and pauses entry after every tenth wrong PIN, for two minutes and doubling to an hour.
  • The count is per account and only root can write it. The pause runs on time since boot, so no time server can move it, and a reboot restarts a pause rather than ending it.
  • Callers can't stop a guess being counted: signals are ignored and resource limits are lifted. Accounts with no password are refused without being counted.
  • bin/moarchy-harden-auth rewrites system-auth and strips setuid from unix_chkpwd, su, chfn, chsh, passwd and ksu, which would each check a password uncounted. It runs from the image build, a pacman hook and a boot-time unit, and --undo runs before the package is removed. It warns about any PAM stack added later that checks passwords outside the count.
  • polkit's sandboxed agent helper can write the count (ReadWritePaths=-/var/lib/moarchy/pin). logind starts no gettys on spare consoles.

Verification

  • image/verify.sh checks:
    • the sudoers rule byte for byte, and that no other sudoers file names moarchy-pin;
    • system-auth, and that no other PAM stack checks passwords outside the count;
    • the setuid bits, the hooks, the boot unit, the polkit drop-in and the console setting.
  • image/negative-test.sh plants six new defects, and the verifier catches each one.
  • New tests:
    • scripts/test-moarchy-pin.sh (44 cases), test-harden-auth.sh (14) and test-power-press.sh (18), run from scripts/style-check.sh;
    • scripts/test-qml.sh runs all 41 plugin QML test files;
    • scripts/test-pam-door.sh runs real sudo through PAM, with controls proving the closed doors were open before.
  • CI runs the QML and PAM tests in the Arch Linux ARM image container.

Code running as the user can still capture the PIN when it is typed, and can spend tries so that entry stays paused. That is the Unix user model, and only sandboxing apps changes it (docs/structure.md I8).

Testing

Built in an isolated VM: release and dev images, both verified (apart from the online resize2fs check, which needs a native arm64 kernel and runs in CI). The negative tests caught all 31 planted defects across both images. In the image container, all 41 QML test files and the real-PAM door test pass. The helper and hardening tests pass under both their bwrap and sudo backends. The lock plugin was driven offscreen in Quickshell through setup, wrong and right PINs, a pause, a late PIN and six startup cases. Every operation the helper performs was run on the phone under polkit's exact unit sandbox.

Flashed to a Pixel 3a. First boot came up dark, then on the setup keypad, and set the PIN through moarchy-pin first. With the phone locked, a wrong PIN through the helper was refused (exit 4) and counted, and the owner's PIN on the keypad unlocked the phone and reset the count. A single power press locked the session with the panel dark: the lock reported secure and the state file read locked. The next press brought up the keypad, which unlocked it. After a reboot the phone came up on the keypad, not setup, and unlocked. On the device: unix_chkpwd, su, chfn, chsh and passwd are not setuid; system-auth routes through moarchy-pin; polkit's agent helper has the count's directory writable; the boot-time hardening unit succeeded; Hyprland reports no config errors; the boot submap is released; no units failed. The device found one bug that offscreen testing could not: Quickshell does not announce WlSessionLock's release, so locked stayed true after an unlock, which would have stopped every later lock. It is fixed in the last commit, and that file was verified on the phone.

@kwsantiago
kwsantiago merged commit 1c9bc82 into main Oct 20, 2026
1 check passed
@kwsantiago
kwsantiago deleted the lock-sudo branch October 21, 2026 03:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant