Skip to content

Releases: doug445/AsahiLocker

v1.8.4 — Caps Lock gate at the prompt it protects

Choose a tag to compare

@doug445 doug445 released this 02 Sep 04:55
Immutable release. Only release title and notes can be modified.

Moves the Caps Lock gate from the ENCRYPT confirmation to immediately before the cryptsetup call, and requires 'yes' in lower case — which cannot be typed with Caps Lock on, so accepting it is the proof.

cryptsetup asks "Are you sure? (Type 'yes' in capital letters)" one prompt before it asks for the new passphrase. Reaching for Caps Lock to type that YES leaves it on for the passphrase, which cryptsetup takes twice and inverts both times — so its verification passes and the boot prompt is where you find out. The old gate warned about this before the btrfs shrink and integrity check, far too early, and read a keyboard LED that reports nothing over Bluetooth.

Also guards the stage-6d /etc/kernel/cmdline redirect with mkdir -p: unguarded, it is the same silent 'set -e' abort as the stage-6c bug fixed in v1.8.2 (#2).

v1.8.3

Choose a tag to compare

@doug445 doug445 released this 28 Aug 07:48
Immutable release. Only release title and notes can be modified.

Recovery-key default + doc fixes

v1.8.2 — fix silent stage-6 abort on Asahi Remix

Choose a tag to compare

@doug445 doug445 released this 28 Aug 07:14
Immutable release. Only release title and notes can be modified.

Bug fix release. Recommended for anyone deploying onto Fedora Asahi Remix.

Fixed

luks-deploy.sh aborted silently in stage 6 on Fedora Asahi Remix (#2)

The script read the existing kernel command line with a grep "^GRUB_CMDLINE_LINUX=" inside a command substitution. Under set -euo pipefail, a /etc/default/grub with no such line makes grep exit 1, which killed the script — with no error message, because nothing had printed one yet. The run stopped after Updating /etc/default/grub... and went straight to the cleanup trap.

Stock Fedora Asahi Remix ships GRUB_CMDLINE_LINUX_DEFAULT and no GRUB_CMDLINE_LINUX line at all, so every such install was affected. The bug has been present since the initial commit and only fires on targets that use the _DEFAULT name.

A second failure was hidden behind it: the sed -i that added the unlock arguments silently matched nothing when the line was absent, so even a run that survived could have dropped rd.luks.uuid / rd.luks.name without a word.

Stage 6c now updates GRUB_CMDLINE_LINUX and GRUB_CMDLINE_LINUX_DEFAULT — whichever exist, both if both do — appends GRUB_CMDLINE_LINUX when the file defines neither, creates the file if it is missing, and refuses to leave the stage unless rd.luks.uuid is actually on disk. Rewrites go through awk so a stray & or | in an existing command line cannot corrupt the replacement, and the file is written in place so its inode, mode, owner and SELinux label survive.

post-encryption-setup.sh reported a splash restore that never happened

Its splash-restore sed only matched GRUB_CMDLINE_LINUX=", so on Asahi it changed nothing and still printed [ok]sed exits 0 when it matches nothing. It now matches either variable and verifies the tokens landed before reporting success.

Recovering an interrupted run

If a deployment stopped at this point, no data is at risk: the root partition is already encrypted and /etc/crypttab and /etc/fstab are already correct. Re-run luks-deploy.sh on the same target — it detects the existing LUKS container and offers config-only mode, which redoes every configuration step idempotently without touching the data.

Testing

Stage 6c was tested against 10 shapes of /etc/default/grub: Asahi _DEFAULT-only, plain Fedora, both variables, neither, single-quoted, unquoted, values containing sed metacharacters, commented-out lines, a missing file, and duplicate definitions — plus an idempotent second run.

Thanks to @Larsvanzijl for the report and a diagnostic bundle complete enough to find this from.

v1.8.0 — audit release

Choose a tag to compare

@doug445 doug445 released this 25 Aug 10:09
Immutable release. Only release title and notes can be modified.

v1.8.0 — audit release

Full-repo audit: eight code defects fixed, stale and contradictory documentation corrected, README deduplicated. No change to encryption parameters, the KDF floor, or the verification gate. All scripts pass bash -n + shellcheck -S warning; the sed and awk fixes were verified against real LUKS2 headers.

Fixed

  • luks-deploy.sh: the splash strip missed rhgb/quiet at the start of a line or just inside the opening quote of GRUB_CMDLINE_LINUX; "GRUB config rebuilt" was printed even when grub2-mkconfig failed; an initramfs whose name yields no parseable kernel version could pass the cryptsetup gate silently (now an error); a typo at the KDF profile menu aborted the whole run — it re-prompts.
  • luks-tune.sh: pbkdf2 keyslots were invisible to the menu — the exact slots the README tells you to convert. They are now listed (- for memory/threads) and convertible to argon2id like any other slot.
  • post-encryption-setup.sh: the splash marker is kept when the grubby restore fails, so a re-run can retry; the "is root encrypted" check now inspects root's own mapper (the btrfs [/subvol] suffix silently broke cryptsetup status before); the initramfs check matches cryptsetup/dm-crypt instead of any string containing "crypt"; unknown arguments are refused instead of ignored.
  • save-luks-recovery-bundle.sh: the backing-device parse truncated device names containing -; it now takes cryptsetup status's device: line verbatim.

Changed

  • ESP stub guard caps drifted-copy forensics at the 5 newest .broken.* files; the stale-EFI cleaner also removes inactive (un-starred) entries; luks-fetch-cache.service dropped its unused [Install] section (could double-run the sweep at boot); the deploy prints LUKS2-format header fields and logs the KDF parameters the recovery keyslot is enrolled with.

Documentation

  • INSTALL: removed the false "there is no recovery key" claim — the deploy offers one, default yes; unlock-time figures now match the README (2.1 / 3.8 / 9.5 s); the prompt table is in true runtime order and gains the missing unmount prompt.
  • The last two retracted "1 GiB hard GRUB ceiling" claims (extras) now carry the measured position: the ceiling is the firmware's — 2 GiB works under U-Boot on an M2 Max, 4 GiB never, 1 GiB stays the portable default.
  • BOOT-ENCRYPTION-DESIGN reconciled with its own probe results (stale "open questions" resolved or reframed; keyfile size settled at 4096 bytes); the 2026-08-23 probe postmortem is marked superseded.
  • CONTRIBUTING: GRUB 2.14 build path now matches the probes' GRUB_PREFIX default; loopback pass counts explained for stage-5b SKIPs. LIVE-USB: caveat that Fedora's GRUB_DISABLE_OS_PROBER=true can hide Method A's USB entry.
  • README: the two duplicate "change your KDF" sections merged into one canonical section with all four tiers and the strength graph (the FAQ entry is now a pointer); the stranded fleet-pinning paragraphs moved next to the KDF pinning they refer to; strength figures made self-consistent (28 years / ten days at 40 bits); broken fragments and grammar fixed; BOOT-ENCRYPTION-DESIGN added to the docs table.

v1.7.2 — GRUB 2.14 + argon2id marked experimental

Choose a tag to compare

@doug445 doug445 released this 25 Aug 03:13
Immutable release. Only release title and notes can be modified.

A documentation release. No script changed, and nothing in the encryption path
moved — there is nothing to re-run if you are already on v1.7.1.

Added

An EXPERIMENTAL marker on unlocking /boot with GRUB 2.14 under argon2id.

The README's note on GRUB and argon2id already made the important distinction:
none of the GRUB memory limits constrain your root volume, because GRUB
never unlocks it — the initramfs does. They only bind a volume that GRUB itself
has to open, which in practice means an encrypted /boot.

That path is now labelled experimental. Fedora's aarch64 GRUB 2.12 builds ship
no argon2.mod at all, so an argon2id /boot keyslot is not something the
stock toolchain can unlock; GRUB 2.14 is where the capability appears, and it
has not been exercised widely enough here to be called supported. Treat it as
research, not as a deployment recommendation. docs/BOOT-ENCRYPTION-STATUS.md
and tools/boot-probe/ carry the measurements behind that judgement.

The unconditional rule alongside it is unchanged and still matters:

Never give GRUB a 4 GiB argon2id keyslot. A 32-bit overflow in GRUB's
argon2_init wraps the allocation size to zero, so it proceeds instead of
rejecting the parameters.

pbkdf2 keyslots work on unmodified Fedora aarch64 GRUB and remain the path that
is actually known to boot.

Note on tag ordering

The commit in this release (c612d49) predates the one in v1.7.1 (a041312) —
the two were authored on separate lines and only met at merge b2e4865, after
v1.7.1 was cut. The tag is later; the content is older. Nothing is missing from
either release.

Full changelog: v1.7.1...v1.7.2

v1.7.1 — diceware snippet fix

Choose a tag to compare

@doug445 doug445 released this 25 Aug 03:10
Immutable release. Only release title and notes can be modified.

A documentation fix. No script, no on-disk behaviour, and nothing in the
encryption path changed — if you are already running v1.7.0 there is nothing
here you need to re-run.

Fixed

The diceware passphrase recipe in the README did not work, and overstated its
own entropy.

The snippet carried the EFF wordlist URL as a comment and then went straight to
shuf, with no step that actually downloads the file. Anyone who had not
already fetched eff_large_wordlist.txt by hand got:

shuf: eff_large_wordlist.txt: No such file or directory

The second problem was quieter. The command used shuf -n 8, which samples
without replacement, while the paragraph directly above it computes strength
from log2(7776) = 12.92 bits per word. That figure describes independent
draws. Sampling without replacement is a different model, and it cannot produce
the repeated word that a genuine sequence of dice rolls sometimes should.

The recipe now fetches the list, verifies it against a published SHA-256, and
draws with shuf -r:

# EFF long wordlist: 7,776 lines of "11111<TAB>abacus"
curl -O https://www.eff.org/files/2016/07/18/eff_large_wordlist.txt

# Verify it before trusting it for entropy
echo 'addd35536511597a02fa0a9ff1e5284677b8883b83e986e43f15a3db996b903e  eff_large_wordlist.txt' \
    | sha256sum -c

# Eight words, drawn with replacement, from the kernel CSPRNG
shuf -r -n 8 --random-source=/dev/urandom eff_large_wordlist.txt | cut -f2 | paste -sd' '

The checksum is not ceremony. A truncated or substituted wordlist lowers your
entropy silently — the passphrase still reads as eight ordinary English words.
A list cut to its first 100 lines yields 53 bits instead of 103, and nothing in
the output tells you that happened.

Full changelog: v1.7.0...v1.7.1

v1.7.0 — drive permissions

Choose a tag to compare

@doug445 doug445 released this 24 Aug 10:00
Immutable release. Only release title and notes can be modified.

Security fix — chmod has been silently doing nothing on FAT deployment drives

chmod(2) on vfat, exfat and ntfs returns success and changes nothing. Those filesystems have no permission bits; the mode you see comes from the mount's fmask/dmask, not from the inode. So a script that runs chmod 0600 recovery-key.txt on a FAT stick is told it succeeded, logs nothing, and leaves the file readable by anyone who plugs the drive in.

The deployment drive is very often exactly such a stick. Measured on a vfat loop image:

chmod 0700 <dir>       rc=0   mode=755
install -m 600 <file>  rc=0   mode=755
chmod 0400 <file>      rc=0   mode=755

Three things were wrong, and they differ in severity depending on how the deployment drive is formatted.

  • The recovery key. install -m 600 was already being used, so on ext4/btrfs it was correct. On FAT it was a no-op and the key sat world-readable. This is the serious one: the recovery key is 64 plaintext hex characters that unlock the volume outright. There is nothing to crack.
  • The LUKS header backup copied to the drive was created with plain cp, which uses 0666 & ~umask0644 by default. It was world-readable on every filesystem, FAT or not. A header backup carries your keyslots. They are argon2id-protected rather than plaintext, but it hands someone an offline target that needs no further access to your machine.
  • pre-luks-state-*/ itself was created by a bare mkdir -p and left at 0755, so the directory holding both of the above was world-listable.
ext4 / btrfs drive FAT / exFAT drive
recovery-key.txt 0600 — correct 0755plaintext key exposed
luks-header-backup.img 0644exposed 0755exposed
pre-luks-state-*/ 0755 0755

What changed

  • New harden_path helper. It sets the mode, reads it back with stat, and warns once if the mode did not take. It never returns non-zero — a drive that cannot hold permissions is a reason to warn loudly, not to abort an encryption that is already under way.
  • The warning fires before encryption starts. The first harden_path call locks pre-luks-state-*/ to 0700, immediately after it is created, so an unsuitable deployment drive is reported while aborting still costs nothing. Locking the directory also covers the layout dumps, crypttab and the BLS entries copied into it.
  • The header backup is copied with install -m 0400, not cp. install sets the mode as it creates the destination, so the header is never briefly world-readable between the copy and a chmod — and never permanently so if that chmod is a no-op.
  • The recovery key and its README now verify their modes rather than assuming them.

The directory-plus-explicit-mode pattern is the one bin/save-luks-recovery-bundle.sh already used; this brings luks-deploy.sh in line with it.

A global umask 077 would have fixed all of it in one line. It was rejected: the same script writes /etc/fstab, the BLS entries and the initramfs config on the target, and mode-restricting those is a boot-breaking change hiding inside a tidy one-liner.

If you have already deployed

Check what your deployment drive actually holds:

ls -la <drive>/pre-luks-state-*/

If the modes are 0700 / 0600 / 0400, nothing was exposed beyond that drive. Move the recovery key and header backup to secure offline storage — which the deployment already told you to do — and you are done.

If they are 0755 or 0644, treat the contents as having been readable by anything with access to that drive, and judge the exposure by where the drive has been. A stick that never left a desk drawer is a very different situation from one that was plugged into a shared machine.

One thing worth knowing before you reach for the obvious fix. If a header backup genuinely leaked, changing your passphrase does not neutralise it. luksChangeKey re-wraps the same master key; a header backup taken beforehand still contains a keyslot that unwraps that master key, and restoring it re-enables the old passphrase against your current data. The only real remedy is a new master key — full re-encryption, or a reinstall. A leaked recovery key is different: revoking that keyslot with cryptsetup luksKillSlot is sufficient, provided no header backup from before the revocation is also loose.

Not affected

bin/save-luks-recovery-bundle.sh, which already set 0700 on its bundle directory and 0400 on every header it wrote.

v1.6.0 — KDF floor hardening + encrypted-/boot research (gated)

Choose a tag to compare

@doug445 doug445 released this 24 Aug 03:01
Immutable release. Only release title and notes can be modified.

Security fix — the fast profile was weaker than doing nothing

fast shipped 1 GiB × 4 through v1.5.0. cryptsetup's own default is argon2id at 1 GiB with iterations auto-tuned to --iter-time (2000 ms) — measured at t=8 on an M2 Max. So fast was exactly half the work of a plain luksFormat with no arguments, while the README claimed every profile beat stock.

  • fast → 1 GiB × 9, and it is now a hard floor. LUKS_PBKDF_ACK_WEAK is removed — there is no override. The floor tests two conditions because they fail differently: memory below 1 GiB loses memory-hardness, and total work below fast is cheaper per guess however the factors are traded.
  • moderate → 2 GiB × 8, restoring separation from fast.
  • New runtime guard. Before formatting, the installer reads what cryptsetup would choose unaided on that machine and refuses to ship weaker: a named profile below the bar is raised to 25% past it; pinned LUKS_PBKDF_* below it is fatal. The benchmark is sampled and the lowest reading wins, because it is load-sensitive and the errors are asymmetric.
  • The pre-flight summary now prints the measured multiple: = 2x the work of cryptsetup's own default here.
Profile Memory Iterations Unlock, M2 Max vs stock
aggressive 4 GiB 10 9.5 s (measured) 5x
moderate (default) 2 GiB 8 ~3.8 s 2x
fast 1 GiB 9 ~2.1 s 1.125x

Encrypted /boot — research published, still gated

Not shipped, not enabled, no flag. Every released version encrypts root only. Lifting the gate will be a major bump.

docs/BOOT-ENCRYPTION-STATUS.md is a laboratory notebook of what has been measured on real hardware, what broke, and what is still unknown. Highlights from an M2 Max running GRUB 2.14 under U-Boot:

  • argon2id allocates and decrypts at 512 / 1024 / 2048 MiB. The familiar "GRUB caps at 1 GiB" is an artefact of x86 vendor UEFI leaving GRUB a constrained heap — not a GRUB limit. The only unconditional rule is the u32 overflow at exactly 4 GiB.
  • GRUB's argon2 is 8.5× slower than the kernel's for identical work — 9.5 s in the initramfs versus ~80 s in GRUB — and linear at ~2.0 s per GiB-pass. Encrypted /boot would cost ~20 s at every boot, minimum.
  • 2 GiB × 20 hard-resets the machine, reproducibly, with no output. It allocates exactly what 2 GiB × 10 allocates, so the cause is time, not size: a wall between 40 s and 80 s of uninterrupted computation.
  • save_env to the FAT ESP returns success and writes nothing.

Contributions wanted — especially probe results from other Apple Silicon Macs, and a base M1 in particular. tools/boot-probe/ carries the harness; it builds throwaway LUKS containers holding nothing, chainloads a self-contained image, and leaves the installed bootloader alone. Open questions are listed at the end of the status doc.

v1.5.0 — two /boot unlock architectures, and a hard 1 GiB GRUB rule

Choose a tag to compare

@doug445 doug445 released this 23 Aug 03:55
Immutable release. Only release title and notes can be modified.

v1.5.0 — two /boot unlock architectures, and a hard 1 GiB GRUB rule

Documentation release. No behavioural change to the deploy script; the
/boot work it describes is still design-only and gated on hardware
testing.

Two unlock options for encrypted /boot, instead of one imposed choice

  • Option 1, one unlock: a passphrase at the GRUB prompt opens /boot, and
    a keyfile inside the encrypted initramfs opens root with no second
    prompt. Its cost is stated exactly - root's parameters are untouched
    but stop being load-bearing, since the keyfile opens root and the
    1 GiB /boot keyslot becomes the cheapest way in. If root is already at
    1 GiB there is no reduction at all.
  • Option 2, two unlocks: no keyfile is generated, and root is opened by
    its own passphrase at its full KDF. Carries the warning it cannot ship
    without - this only works with a DIFFERENT passphrase for root, since
    reusing one hands an attacker root the moment they break the weaker
    /boot keyslot.
  • /boot is pinned at 1 GiB / 10 iterations. Measured here, cryptsetup's
    own defaults with no KDF flags are argon2id 1 GiB / t=8, so fewer than
    8 iterations would ship a /boot weaker than a plain luksFormat.

The 1 GiB GRUB ceiling is now a prohibition, not a soft cap

  • Every mention read as a performance note, which invites treating it as
    a number to tune toward. All of them now state it as a rule: 1 GiB is
    the hard ceiling for any volume GRUB itself must unlock, root excepted.
  • Both failure modes are spelled out wherever the rule appears. Above
    1 GiB GRUB fails to allocate, which is not a slow boot but no boot. At
    exactly 4 GiB it is worse: argon2_init computes
    xtrymalloc (1024 * memory_blocks) in 32-bit unsigned arithmetic, so
    1024 * 4194304 wraps to 0 and GRUB proceeds instead of rejecting the
    parameters cleanly.

Also in this release

  • luks-tune.sh offers the paranoid tier (4 GiB / 12) and gained --help
    plus an argument guard. Previously a mistyped --dry-run was silently
    ignored, which would re-cost a keyslot for real while the caller
    believed it was a rehearsal.

v1.4.0 — KDF tuning after installation, and corrected cost figures

Choose a tag to compare

@doug445 doug445 released this 23 Aug 02:42

v1.4.0 — KDF tuning after installation, and corrected cost figures

Adds a way to change your mind about the KDF once the disk is already
encrypted, and fixes two things the previous release stated wrongly.

Post-installation tuning

  • New README section after the verification step: four ready-to-paste
    cryptsetup luksConvertKey commands covering fast, moderate, aggressive
    and a new documented-only paranoid tier at 4 GiB / 12 iterations. The
    installer still ships three profiles; 12 iterations is a deliberate
    choice, not a default. All four were verified against a loopback
    volume - each writes exactly the requested parameters, and the
    passphrase still opens the volume afterwards.
  • New bin/luks-tune.sh: an ncurses front end for the same operation.
    Backs the header up before any write, shows the measured unlock time
    and a strength table for whatever you pick before you commit, and
    hands the passphrase prompt to cryptsetup rather than reading it.
    Never creates or destroys a keyslot and never touches data.

Corrected claims

  • 4 GiB is argon2id's MAXIMUM memory cost, a hard cryptsetup limit that
    rejects anything above 4194304 KiB. v1.3.0 documentation implied it
    was a property of how much RAM an 8 GiB Mac can spare. It is not, and
    the wrong reason would have misled anyone with more memory into
    thinking they could go higher.
  • Unlock cost tracks memory TIMES iterations, not memory alone. Measured
    from the boot journal on an M2 Max, argon2id at 4 GiB / 10 iterations
    costs 9.5 s, consistent to +/-0.1 s across three boots - and only
    about 1 s of that is the 4 GiB. Documentation that called a large
    memory figure "deliberately expensive" has been corrected throughout.

Profile changes

  • aggressive 4 GiB t=10 (unchanged in effect; t=12 was tried during
    development and reverted), moderate 2 GiB t=8 -> t=6.

Guidance

  • New sections on passphrase generation with diceware, on why a
    memory-hard KDF is not optional, and on what Grover's algorithm does
    and does not change. Every figure in them is derived from the measured
    per-guess cost rather than asserted.