Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 171 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,125 @@ make every guess genuinely expensive on any silicon, and let the passphrase do
the rest — which is why the passphrase advice here is not decoration. Under
Linux it is the TPM you chose not to trust.

### pbkdf2 is a `for` loop — and two of the richest companies on Earth still ship it

Strip the acronym off and this is what PBKDF2 is:

```
u = HMAC(password, salt)
for i in 2..N:
u = HMAC(password, u) # keep a running XOR; that is the key
```

That is the whole function. One hash, fed back into itself N times, with a
few hundred bytes of state. It was standardized in September 2000 (PKCS #5
v2.0, RFC 2898), when the fastest graphics card you could buy had four pixel
pipelines and no programmable shaders. Its only knob is N, and N buys you
linear time on a machine that has nothing but time: a modern GPU holds
sixteen thousand cores, each of which runs this loop on its own password
with no need to talk to any other. Raising N by ten makes the attacker wait
ten times longer, and makes you wait ten times longer, and the attacker has
ten thousand times more cores than you do. That is the entire negotiating
position of a memory-free KDF, and it was lost before it started.

Password hashing moved on. The Password Hashing Competition ran from 2013
to 2015 precisely because the field had understood that the only cost a GPU
cannot parallelize away is **memory**: argon2 won, argon2id became RFC 9106,
and LUKS2 made it the default in cryptsetup 2.0. Every Linux distribution's
installer has been writing argon2 keyslots since 2018. It is not exotic. It
is not new. It is what a disk encryptor does now.

**Apple's FileVault** stretches your login password with PBKDF2-SHA256. The
iteration count was last seen in public in 2012, when three researchers had
to reverse-engineer it to find out (41,000 rounds, in *Infiltrate the Vault*);
Apple has not published a number since and does not document the function
at all. On an Intel Mac without a T2, that loop is the entire wall between
your data and a graphics card. On newer Macs the Secure Enclave takes over,
which is Apple's tacit admission of the same point: the function cannot be
allowed anywhere an attacker can run it, so it is hidden inside a chip and
you are asked to trust the chip.

**Microsoft's BitLocker** does not even reach PBKDF2. Its password and
recovery-key paths stretch with a home-grown chained SHA-256, 2^20 rounds,
another memory-free loop — and its *default* mode stretches nothing at all:
TPM-only BitLocker releases the volume key when the firmware measurements
match, and no passphrase is ever typed. The key then crosses a bus. Discrete
TPMs have had it sniffed off the SPI lines with a logic analyser in under a
minute, on camera, more than once. Microsoft's answer to "what if the
attacker has a GPU" was to move the problem into a chip and hope the wires
hold. They did not.

Both companies own more silicon than most governments. Both employ people
who know exactly what argon2 is. Both ship a `for` loop from the year 2000
and put a hardware chaperone in front of it, so that the loop is never
caught outside alone. Linux ships argon2id and lets it stand in the open,
because it can.

**Cosmic time.** `luks-tune.sh` states the cost of every keyslot it writes
as the years a thousand 24 GiB GPUs would need to search half the passphrase
space, anchored to what the universe is doing by then. Run the same model
over the three functions — the GPU rates for the two loops are a
[published RTX 4090 hashcat run](https://gist.github.com/Chick3nman/32e662a5bb63bc4f51b847bb422222fd)
(BitLocker: 10,025 guesses/s per card; PBKDF2-SHA256: 8.87 billion
iterations/s per card, so ~216,000 guesses/s at 41,000 rounds); argon2id is
LinuxLocker's `aggressive` profile at the reference machine's 16 s per guess,
six guesses per card because 24 GiB holds six 4 GiB working sets:

| Passphrase | BitLocker SHA-256 chain | FileVault PBKDF2 | LinuxLocker argon2id (4 GiB) |
|---|---|---|---|
| a typical human password (~40 bits) | **15 hours** | **42 minutes** | decades |
| 6 diceware words (77 bits) | 10^8 years — less than the age of the universe | 10^7 years — less than the age of the universe | 10^13 years — **long past the age of the universe** |
| 8 diceware words (103 bits) | 10^16 years — every star has burned out | 10^15 years — every star has burned out | 10^21 years — **galaxies have evaporated** |

Read the top row. That is the password most people actually have, and
against the two loops it is a lunch break. The passphrase rows are where the
loops look respectable — until you notice that on those rows argon2id is a
million times further out, and that the universe is 1.4 × 10^10 years old:
six good words behind a `for` loop still fall inside its lifetime; behind
argon2id they do not. The KDF sets the price of one guess; the passphrase
sets how many guesses. Microsoft and Apple chose to keep the price low and
guard the till with a chip. LinuxLocker charges 4 GiB at the door, on any
silicon, and the chip is not invited.

**And against an ASIC farm.** A graphics card is a general-purpose machine
being polite. The real adversary of a memory-free hash is the ASIC: a chip
that does one hash and nothing else, and the hash it does, by the
exahash, is SHA-256 — the exact primitive inside BitLocker's chain and
FileVault's PBKDF2. This is not hypothetical hardware. It is the most
mass-produced special-purpose silicon on the planet, and there is roughly a
zettahash per second of it running right now: the Bitcoin network sat at
about 1,000 EH/s of SHA-256d through 2026, which is two thousand billion
billion SHA-256 compressions every second, all day, for a reward that
happens to be a coin. One
[Antminer S21 XP](https://support.bitmain.com/hc/en-us/articles/35383015643673-S21-XP-Specifications)
does 270 TH/s from a 3,645 W wall socket; on BitLocker's 2^20-round chain
that is about 260 million guesses a second, or twenty-five thousand RTX 4090s
in a box the size of a shoebox. Point the network at the loops and the
table above collapses:

| Passphrase | BitLocker chain, all of Bitcoin's ASICs | FileVault PBKDF2, the same | argon2id 4 GiB, a thousand memory-bound ASICs* |
|---|---|---|---|
| a typical human password (~40 bits) | **0.6 milliseconds** | **45 microseconds** | two months |
| 6 diceware words (77 bits) | **2.5 years** | **72 days** | 10^10 years — long past the age of the universe |
| 8 diceware words (103 bits) | 10^8 years — less than the age of the universe | 10^7 years — less than the age of the universe | 10^18 years — **galaxies have evaporated** |

\* There is no argon2id ASIC to buy, and the reason is the point. argon2id
at 4 GiB and ten passes moves about 80 GiB through memory per guess, so a
chip built for nothing else is bounded by DRAM bandwidth and DRAM capacity,
and those cost the attacker exactly what they cost you. The column assumes a
thousand parts each with 8 TB/s of HBM — twice the best accelerator on sale
— and 400 GiB resident to keep a hundred guesses in flight, so a hundred
guesses a second each. That is a generous upper bound on a machine nobody
has built, and six words are still past the age of the universe behind it.

Six diceware words against Microsoft's function, on hardware that exists and
is switched on today, is a project of two and a half years. Against Apple's,
ten weeks. Nobody will redirect Bitcoin at your laptop; but the number says
what the function is worth on its own, which is why neither company lets it
stand on its own. argon2id's answer to the ASIC is not a bigger N. It is a
bill for 4 GiB of RAM per guess that no foundry can discount — the only
currency a hash can charge that special silicon does not devalue.

## Environment knobs (fleet / non-interactive use)

```
Expand All @@ -424,8 +543,33 @@ LUKS_GRUB_KDF_FACTOR=<x> unlock-time multiplier for a volume G
itself unlocks (default 8.5, measured)
LUKS_GRUB_ARGON2_MAX_KIB=<n> argon2id ceiling for such a volume
(default 1 GiB — the x86 UEFI heap)

LUKS_CONFIRM=ENCRYPT|CONFIGURE|CONVERT the typed point-of-no-return gate of the
mode the run reaches; a mismatch is fatal
LUKS_STALE_MAPPER=keep|close a mapper left open by an earlier run
LUKS_LIVE_OVERRIDE=LIVE run from a root that is not a live environment
LUKS_BATTERY_OVERRIDE=BATTERY run on battery below 50%
LUKS_UNMOUNT=yes|no unmount a target a live desktop mounted
LUKS_RESUME=yes|no finish an interrupted encryption
LUKS_EXISTING=tune|config|quit what to do with a finished LUKS2 volume
LUKS_PROFILE=...|skip also answers the re-costing menu after a
LUKS1 conversion ('skip' defers it —
pbkdf2 is never an option)
LUKS_DATA_PARTITION=yes|no encrypt a volume with no fstab as data
LUKS_MISMATCH_OVERRIDE=MISMATCH keep a pinned /boot or EFI the target's
fstab disagrees with
LUKS_CROSS_DISK=yes|no proceed with /boot or EFI on another disk
LUKS_SUBVOL_MISMATCH=yes|no proceed when BLS and fstab disagree on subvol
LUKS_FSCK=yes|no run the read-only integrity check first
LUKS_FSCK_FORCE=FORCE continue past filesystem errors
LUKS_ALREADY_SHRUNK=yes|no f2fs/vfat: shrunk by an interrupted run?
```

Without a terminal, a prompt that has no pin stops the run and names the
variable it wanted; nothing hangs, and nothing is answered by default. The one
prompt with no pin at all is the inner-UUID-changed override: that state means
the wrong device is open, and no fleet should answer it blind.

The UKI and Secure Boot knobs are documented in full, with detection order and
key-discovery paths, in [BOOTLOADERS.md](docs/BOOTLOADERS.md).

Expand Down Expand Up @@ -534,9 +678,14 @@ match the target.

### Why argon2id only, and never pbkdf2?

pbkdf2 is CPU-only, which is exactly what a GPU cracking fleet is good at.
argon2id is memory-hard, so an attacker has to buy RAM per guess, not just
cores. All three profiles are argon2id, and the cheapest of them is a hard
pbkdf2 is a simple `for` loop: hash the passphrase, feed the result back in,
repeat. It holds no state larger than one hash, so it needs no memory, and
that is exactly what a GPU cracking fleet is good at — every core runs its
own copy of the loop. argon2id is memory-hard, so an attacker has to buy RAM
per guess, not just cores. **pbkdf2 is never an option here.** LinuxLocker
never writes it, never offers it, and treats a keyslot still on it as not
yet hardened: the `skip` choice after a LUKS1 conversion only defers the
re-costing, and the script says so before it lets you leave. All three profiles are argon2id, and the cheapest of them is a hard
floor with no override flag — the tool exists to beat a bare `luksFormat`, not
to undercut it. `luks-tune.sh` also converts leftover pbkdf2 keyslots on
volumes you encrypted earlier. For what the same loop looks like in BitLocker
Expand Down Expand Up @@ -682,8 +831,12 @@ costs you nothing.

### Can I run it unattended across several machines?

Yes. Every prompt has an `LUKS_*` environment variable behind it — see
[Environment knobs](#environment-knobs-fleet--non-interactive-use). Pin
Yes. Every prompt has an `LUKS_*` environment variable behind it, except the
inner-UUID-changed override, which means the wrong device is open and is
never answered blind — see
[Environment knobs](#environment-knobs-fleet--non-interactive-use). Run
without a terminal, a prompt that has no pin stops the run and names its
variable rather than hanging or dying on a failed read. Pin
`LUKS_PBKDF_MEMORY` / `_ITER` / `_PARALLEL` for reproducible KDF cost across a
fleet rather than per-machine benchmark drift; pinned values below the floor are
fatal rather than silently raised, precisely so the numbers you pinned are the
Expand Down Expand Up @@ -817,6 +970,19 @@ recognition, the harden-only guarantee — and v1.5.0 marks the audited state.
Nothing in this tool has changed hands: the design decisions are the
author's, the audit checked that the code keeps them.

Audited again on 2026-09-21 by **Claude Fable 5.1**, against v1.6.0: every
script read in full, the three test suites run, and each finding reproduced
before it was reported. Five bugs came out of it and were fixed in the same
pass — the diagnostic script shipped without its execute bit, the fstab
device-path branch could never match a real line, the chroot phase was fed
to bash on stdin where any child that reads it would have swallowed the rest,
the summary claimed a `GRUB_ENABLE_CRYPTODISK=y` the tool never writes, and
the unattended-use claim was not true until every prompt was given a pin.
The design gaps it named that are still open — an end-to-end boot of the
Fedora, Ubuntu and Arch GRUB paths, a console-keymap check for the initramfs,
`sgdisk` in the dependency map — are recognized here so that nobody mistakes
the passing suites for a boot.

## License and contact

MIT — see [LICENSE](LICENSE).
Expand Down
Empty file modified bin/linuxlocker-diag.sh
100644 → 100755
Empty file.
Loading
Loading