Skip to content

Security Tools Release (main)

Choose a tag to compare

feat(scarecrow): three LUKS PINs — duress, decoy, and both at once

Scarecrow had one password. It raised a silent signal and opened a decoy
session, and it did not wipe anything, despite the docs and the wiki implying a
duress mechanism. Now there are three, each optional and independent:

  duress        erases the LUKS header of the configured device, silently
  decoy         a believable working session, erases nothing
  duress+decoy  erases the header AND opens the decoy, so the data is gone and
                the screen still shows a working system

Design decisions worth keeping:

- **All three are verified on every attempt, with no early exit.** Stopping at
  the first match would make the time taken depend on which slot matched, and
  three Argon2 verifications at m=64MiB are slow enough for that to be
  measurable. Two extra verifications on a machine about to be wiped is a cheap
  price for not leaking which PIN was used.
- **Most destructive interpretation wins** if one password is enrolled in more
  than one slot. Under duress is the wrong moment to resolve ambiguity in favour
  of doing less.
- **Nothing is erased until --set-duress-device names a device.** Refusing to
  guess is the point: erasing the header of a device nobody named is
  unrecoverable and might be the wrong disk. The value is re-validated at wipe
  time as well, so a mangled config file cannot become arbitrary arguments.
- **The wipe is the header, not the disk.** luksErase takes milliseconds and is
  irreversible; overwriting a whole disk cannot finish before someone notices,
  which defeats the purpose.
- **Every path is silent**, including the failures. No progress, no confirmation,
  no error — not on success, not when cryptsetup is missing, not when no device
  is set. An error mentioning cryptsetup on a login screen says exactly what was
  attempted. A duress-only match prints `Login incorrect`, the same as a wrong
  password, because a disk that will not unlock reads as a forgotten passphrase.

The README now states the limits plainly rather than implying more than the tool
does: the ciphertext survives on the platter and this is useless against someone
who imaged the disk first; it cannot help with a machine seized powered-on; the
decoy is a session, not disk-level deniability; and anyone who knows this tool
exists knows a duress PIN might. The deniability is that they cannot tell which
password you handed over — not that the mechanism is a secret, since it is in a
public repository.

fix(ci): --locked was being satisfied by a lockfile the previous step wrote

Three lockfiles — anti-ducky, arch-security-suite and scarecrow — had drifted
from their Cargo.toml: dependencies were declared and never resolved into the
lock. `cargo metadata --locked` fails on all three at the parent commit.

`cargo build --release --locked` should have caught that. It did not, because
the clippy step runs first *without* --locked, silently re-resolves, and
rewrites the lock — so --locked was checking a file generated moments earlier
rather than the one in the repository, and the released binaries were not built
from the committed lock at all. That is exactly the guarantee --locked exists to
provide, and it was not being provided.

Locks regenerated, and every cargo invocation before the build now carries
--locked, with an explicit `cargo metadata --locked` first so a stale lock fails
loudly at a step whose name says what is wrong.

Verified locally: `cargo clippy --target x86_64-unknown-linux-gnu --all-targets
--locked -- -D warnings` is clean for scarecrow, libre-otp, anti-ducky and
kernel-watcher. anti-evil-maid and arch-security-suite cannot be checked from
this machine — libudev-sys needs a system library that is not present when
cross-checking from Windows — so CI remains the check for those two.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>