Skip to content

Repository files navigation

AsahiLocker logo: a faceted pink and silver polyhedron bearing the Fedora mark and the Asahi Linux 'A', standing on a dark angular platform lit with magenta and blue neon

AsahiLocker — in-place LUKS2 disk encryption for Fedora Asahi Remix on Apple Silicon

CI License: MIT Platform: Apple Silicon KDF: argon2id

Encrypt the root filesystem of an already-installed Fedora Asahi Remix system on Apple Silicon — in place, without reinstalling, without wiping macOS, and without a second copy of your data.

luks-deploy.sh converts your existing btrfs root partition into a LUKS2 container holding that same filesystem. Your files, subvolumes, snapshots and btrfs UUID all survive; the partition simply gains an encryption layer. It then rewrites every piece of boot configuration that has to change (crypttab, fstab, /etc/kernel/cmdline, GRUB defaults, all BLS entries, dracut config, all initramfs images) and refuses to let you reboot until a 12-point verification gate passes.

Works on every M-series Mac that Asahi supports — M1 / M1 Pro / M1 Max / M1 Ultra, M2 / M2 Pro / M2 Max, M3, M4. Nothing in the tooling is model-specific: partitions, subvolumes and boot layout are all auto-detected at runtime.

This is destructive-by-nature tooling. It rewrites a live root filesystem. Read docs/INSTALL.md before running anything, and have a verified backup. See Risks.


Quick start

# 1. On the installed system: get the kit, and build a Fedora Asahi live USB
#    to run it from  (see docs/LIVE-USB.md — a stock Fedora ISO will NOT boot)
git clone https://github.com/doug445/AsahiLocker.git

# 2. Boot the live USB. Easiest route, with the USB plugged in:
#      sudo grub2-mkconfig -o /boot/grub2/grub.cfg    # adds it to your GRUB menu
#      (-o must be exactly that path — NEVER the ESP grub.cfg; see docs/LIVE-USB.md)
#    then reboot and select it.  (see docs/LIVE-USB.md for the U-Boot routes)

# 3. From the live environment, encrypt the installed root:
sudo ./AsahiLocker/bin/luks-deploy.sh

# 4. Reboot, enter your passphrase, then finish up on the encrypted system:
sudo ./AsahiLocker/bin/post-encryption-setup.sh
sudo ./AsahiLocker/boot-guards/install.sh

The deploy script auto-detects your disk layout and shows you what it found. You confirm the selection, type ENCRYPT, and choose a passphrase. Everything after that is automated, including recovery if a step fails partway.

Full walkthrough: docs/INSTALL.md


What you get

  • In-place btrfs → LUKS2 conversion. No reinstall, no backup-and-restore round trip, no second disk. Subvolumes, snapshots and the btrfs UUID survive.
  • Pinned argon2id, never pbkdf2. AES-256-XTS with argon2id at 4 / 2 / 1 GiB memory cost — memory-hard by design, so GPU and ASIC cracking stays expensive.
  • Every profile is stronger than cryptsetup's own defaults — enforced, not asserted. The weakest profile on offer does 1.125x the work of a plain luksFormat on the same machine, and the strongest does 5x. Before it writes anything the installer benchmarks what cryptsetup would have chosen unaided and refuses to ship weaker: a named profile below that bar is raised past it, pinned parameters below it are fatal. There is no flag to opt out.
  • Benchmarked on your machine. The installer measures your hardware and shows real unlock-latency estimates before you pick a KDF profile.
  • Every boot file rewritten, then verified. crypttab, fstab, /etc/kernel/cmdline, GRUB defaults, all BLS entries, dracut config and all initramfs images — behind a 12-point gate that refuses to let you reboot into a broken system.
  • Resumable after any interruption. LUKS2 re-encryption is journaled with checksum resilience; re-run the script and it detects the interrupted state and finishes it.
  • A --dry-run that really is dry. The entire read-only half, including the exact cryptsetup reencrypt invocation it would issue, with nothing modified.
  • Recovery you can actually use. Optional 64-hex recovery key in a second keyslot, plus a labeled bundle with the LUKS header and every changed config.
  • Asahi-specific boot guards. Stops a stray grub2-mkconfig from bricking an encrypted boot, and clears U-Boot's stale EFI entries.
  • Tested in CI on every push, x86_64 and aarch64, against a real loop device.

What's in here

Path What it is
bin/luks-deploy.sh The main event. In-place LUKS2 encryption of the installed btrfs root, run from a live USB. Auto-detects everything, cross-checks the selected boot/EFI partitions against the target's own fstab, self-repairs failed initramfs/BLS steps, fixes SELinux labels, and gates the reboot behind 12 verification checks. Fully resumable: re-run it after any interruption and it finishes the encryption (--resume-only) or redoes just the config phase.
bin/post-encryption-setup.sh Run once on the newly-encrypted system. Saves a recovery bundle, creates snapper subvolumes on the encrypted volume, enables the boot guards, verifies the result. Idempotent.
bin/luks-tune.sh An ncurses front-end (dialog, falling back to whiptail) for inspecting and re-costing the argon2id parameters of keyslots on volumes that already exist. Shows the measured unlock time and what the cost buys against a GPU fleet before you commit, backs the header up first, and hands the passphrase prompt to cryptsetup rather than reading it. Never creates or destroys a keyslot, never changes a passphrase, never touches data. --dry-run prints the command and changes nothing.
bin/save-luks-recovery-bundle.sh Assembles a labeled recovery bundle (LUKS header + crypttab/fstab/cmdline/BLS entries + a plain-English recovery README) so you are not dependent on a second USB.
bin/post-encryption.conf.example Optional config for the above — snapper subvolumes and any extra units you want enabled post-encryption.
boot-guards/ Two small Asahi-specific boot guards, plus an installer: ESP stub guard (stops a stray grub2-mkconfig from bricking an encrypted boot) and stale EFI entry cleaner (removes U-Boot's leftover entries for unplugged USB installers).
extras/ Optional luks-fetch-cache: an aligned LUKS/BitLocker status readout for fastfetch. Public header metadata only, no key material.
tests/ loopback-core-test.sh: runs the exact encrypt/resume/recovery-key sequence against a throwaway file-backed loop device — including a hard-kill mid-reencrypt followed by cryptsetup repair + --resume-only. Runs in CI on every push (x86_64 + aarch64); safe to run locally with sudo.
docs/ INSTALL · LIVE-USB · RECOVERY · U-Boot bootflow · Internals · Fleet deployment · Encrypted /boot research
tools/boot-probe/ Research only, not part of any install. Builds throwaway LUKS containers and a self-contained GRUB 2.14 image to measure what argon2id can actually do inside GRUB under U-Boot. Touches no real volume. See BOOT-ENCRYPTION-STATUS.md.

How the encrypted boot actually works on Apple Silicon

iBoot → m1n1 stage 1 → m1n1 stage 2 → U-Boot → shim → GRUB → Linux
                                        │                 │
                       provides the UEFI environment      │
                                                          │
                        reads BLS entries from /boot/loader/entries/
                                                          ↓
                                            kernel + initramfs load
                                                          ↓
                                     initramfs reads rd.luks.uuid from the
                                     kernel cmdline, prompts for your
                                     passphrase, opens LUKS, mounts root

U-Boot is the firmware/UEFI layer on Apple Silicon; GRUB is the bootloader running on top of it. Both are in the chain on Fedora Asahi Remix.

/boot stays unencrypted (plain ext4) so GRUB can read kernels and initramfs images. The encrypted root is unlocked by the initramfs, not by GRUB — see docs/INTERNALS.md.

Encrypting /boot is being researched, and the work so far is written up in docs/BOOT-ENCRYPTION-STATUS.md — including measurements on real hardware showing GRUB's argon2id is 8.5× slower than the kernel's, and a reproducible hard reset past a certain computation length. It is not shipped, not enabled, and has no flag; every released version encrypts root only. Contributions and probe results from other Apple Silicon machines are wanted — the open questions are listed at the end of that document.

Partition layout, before and after

nvme0n1
  p1  APFS    iBootSystemContainer
  p2  APFS    macOS  ← untouched
  p3  APFS
  p4  vfat    EFI          → /boot/efi   ← stays plain
  p5  ext4    BOOT         → /boot       ← stays plain
  p6  btrfs   fedora       → / and /home ← becomes LUKS2(btrfs)
  p7  APFS    RecoveryOS

Partition numbers are auto-detected; this is just the common Asahi shape.


Crypto parameters — AES-256-XTS and argon2id

Pinned explicitly rather than left to cryptsetup's auto-benchmark, so every box you deploy to ends up identical instead of picking a machine-dependent memory cost and sha256.

Parameter Value
Cipher aes-xts-plain64, 512-bit key (AES-256-XTS)
KDF argon2id (always — no profile selects pbkdf2)
Memory cost 4 / 2 / 1 GiB, by profile
Iterations (time cost) 10 / 8 / 9 — aggressive / moderate / fast
Parallelism 4 threads
Hash sha512 — sets both the AF splitter hash and the LUKS2 volume-key digest

The KDF re-runs in the initramfs at every boot, so its memory cost must be allocatable there — and you pay its full cost as unlock latency on every boot.

Choosing an argon2id profile

There is no weak choice here. All three profiles are stronger than what cryptsetup picks for itself — that is the floor, not the target, and it is checked at run time on the machine being encrypted rather than assumed from a table. fast means fast relative to the other two, not "cheap": it is deliberately pinned just above stock, never below it.

Because you wait for it every time you start the machine, the installer asks you to pick one of three profiles. It benchmarks your machine first and shows an estimate for each from your own hardware, not someone else's. Above the memory cryptsetup benchmark can allocate on the spot, the figure is scaled from a smaller measurement rather than measured outright, and every figure shifts with system load — at boot the machine is idle, so real unlocks land at the fast end:

The luks-deploy.sh KDF profile prompt, showing three argon2id profiles with unlock times benchmarked on the running machine

Times shown are from an M2 Max. Your machine is benchmarked at run time, so the numbers you see will be your own.

Profile Memory Iterations Threads Unlock, M2 Max vs stock
aggressive 4 GiB 10 4 9.5 s (measured) 5x
moderate (default) 2 GiB 8 4 ~3.8 s 2x
fast 1 GiB 9 4 ~2.1 s 1.125x

Only aggressive is measured — from the boot journal, the gap between systemd-cryptsetup taking the passphrase and the volume opening. The other two scale from it at ~0.24 s per GiB-pass. The screenshot above shows a live cryptsetup benchmark run instead, which is a different basis and jitters with load, so its numbers sit slightly higher.

"vs stock" is work (memory x iterations) against what cryptsetup picks on this machine unaided — 1 GiB x 8, its --iter-time default of 2000 ms. Every profile is above 1.0x by construction, and a runtime guard enforces it (below).

All three are argon2id — none uses pbkdf2. Memory cost only has to be allocatable in the initramfs, which has the machine to itself, so any profile is safe on any Asahi-supported Mac including an 8 GiB M1.

argon2id is memory-bandwidth-bound, so a base M1 is slower than an M2 Max for identical parameters — which is exactly why the installer measures your hardware rather than assuming.

Non-interactive selection, for scripted or fleet deployments:

sudo LUKS_PROFILE=fast ./bin/luks-deploy.sh                     # a named profile
sudo LUKS_PBKDF_MEMORY=3145728 LUKS_PBKDF_ITER=8 ./bin/luks-deploy.sh   # fully custom (3 GiB x 8)

Setting any LUKS_PBKDF_* variable pins the parameters and skips the menu. The fast profile is a hard floor. Custom parameters below it — less than 1 GiB of memory, or less total work (memory x iterations) than 1 GiB x 9 — are refused outright. There is no acknowledgement flag, and the floor also catches the classic typo (LUKS_PBKDF_MEMORY=1048 for 1048576).

The two conditions are separate because they fail differently: dropping below 1 GiB loses the memory-hardness that is the entire point, and dropping total work below fast is cheaper per guess however you trade the two off.

If you genuinely want a cheaper KDF, run cryptsetup luksConvertKey yourself. This script will not write one for you.

And a runtime guard on top of that. Before formatting, the script reads what cryptsetup itself would have chosen on this machine (argon2id at its default memory with iterations auto-tuned to --iter-time, 2000 ms) and refuses to ship anything weaker:

  • a named profile below stock has its iteration count raised to 25% past stock, and says so;
  • pinned LUKS_PBKDF_* numbers below stock are fatal — the operator chose exact values, and silently changing them would break the fleet reproducibility that pinning exists to provide.

This matters because the profiles are fixed numbers while stock is a fixed time: on hardware faster than the profiles assume, stock climbs and a fixed profile can quietly fall behind it. The benchmark is sampled and the lowest reading wins, because reading stock too low only makes the guard a no-op, while reading it too high would inflate your unlock latency on every boot forever.

Why the floor is 1 GiB and not "whatever cryptsetup picks". cryptsetup sizes argon2id memory against the RAM it can allocate at that moment. Inside a distro installer's live environment that is not much, and the result is a header you keep for the life of the machine. Measured across several x86 installs, the graphical installers produced argon2id memory costs of roughly 350-600 MiB — and sha256 — which then had to be corrected by hand afterwards with luksConvertKey. AsahiLocker pins the parameters instead: --pbkdf argon2id, --hash sha512, --cipher aes-xts-plain64, --key-size 512, and an explicit --pbkdf-memory/--pbkdf-force-iterations, so the header never depends on how much RAM happened to be free while it was being written.

The partition menus can be pinned the same way as the KDF (each pinned device is still fstype-checked and cross-checked against the target's own fstab, and the typed ENCRYPT confirmation still applies):

sudo LUKS_TARGET_ROOT=/dev/nvme0n1p6 LUKS_TARGET_BOOT=/dev/nvme0n1p5 \
     LUKS_TARGET_EFI=/dev/nvme0n1p4 ./bin/luks-deploy.sh

Fully hands-off (fleet imaging, automated testing): LUKS_PASSPHRASE_FILE=<path> reads the passphrase from a file — its exact bytes, no trailing newline — and is used for encrypt, resume, unlock, and as the existing key when enrolling the recovery key. LUKS_MAPPER_NAME=<name> changes the device-mapper name (default fedora_crypt; the companion scripts auto-detect a custom name from the booted system).

Changing the KDF on a volume that already exists

Profiles apply at format time only. A header keeps whatever it was built with, so changing a profile does nothing to a disk you already encrypted — but you are not stuck with what you picked: cryptsetup luksConvertKey re-wraps a keyslot's key under new argon2id parameters in place. It does not change your passphrase, does not re-encrypt anything, and does not touch a single byte of filesystem data. It takes seconds.

Read what a volume actually uses first:

sudo cryptsetup luksDump /dev/nvme0n1p6

Under each keyslot you get PBKDF:, Memory: (in KiB), Time cost: (the iteration count) and Threads:. Every keyslot carries its own parameters — a passphrase slot and a keyfile slot on the same volume are commonly different.

There is an ncurses front-end for all of this if you would rather not hand-type the parameters — bin/luks-tune.sh offers the four tiers below plus a custom option, shows the measured unlock time and the strength graph below for whatever you pick before you commit, and takes the header backup for you:

sudo ./bin/luks-tune.sh              # interactive
sudo ./bin/luks-tune.sh --dry-run    # show the command, change nothing

To do it by hand instead: back the header up first, and keep the backup until a successful boot confirms the new parameters — a keyslot is the only copy of your key, wrapped:

sudo cryptsetup luksHeaderBackup /dev/nvme0n1p6 \
     --header-backup-file ~/luks-header-before.bin

Then pick a tier. Each command converts the keyslot your passphrase opens without changing the passphrase; add -S <n> to target a specific slot, and repeat per slot you want re-costed.

# fast — 1 GiB, 9 iterations          (~2.1 s on an M2 Max; 12.5% more
#                                      work than cryptsetup unaided here,
#                                      which picks 1 GiB x 8)
sudo cryptsetup luksConvertKey --pbkdf argon2id \
     --pbkdf-memory 1048576 --pbkdf-force-iterations 9 --pbkdf-parallel 4 \
     /dev/nvme0n1p6

# moderate — 2 GiB, 8 iterations      (~3.8 s)   ← the shipped default
sudo cryptsetup luksConvertKey --pbkdf argon2id \
     --pbkdf-memory 2097152 --pbkdf-force-iterations 8 --pbkdf-parallel 4 \
     /dev/nvme0n1p6

# aggressive — 4 GiB, 10 iterations   (~9.5 s, measured)
sudo cryptsetup luksConvertKey --pbkdf argon2id \
     --pbkdf-memory 4194304 --pbkdf-force-iterations 10 --pbkdf-parallel 4 \
     /dev/nvme0n1p6

# paranoid — 4 GiB, 12 iterations     (~11.4 s)
sudo cryptsetup luksConvertKey --pbkdf argon2id \
     --pbkdf-memory 4194304 --pbkdf-force-iterations 12 --pbkdf-parallel 4 \
     /dev/nvme0n1p6

Two things that trip people up:

  • --pbkdf-force-iterations disables cryptsetup's time benchmarking. Left off, cryptsetup auto-tunes the iteration count to land near --iter-time (2000 ms by default). That is why a stock 1 GiB volume unlocks in about two seconds while these profiles take longer — the memory figure is the same, the iteration count is not.
  • --pbkdf-memory is in KiB, not MiB or GiB — and 4194304 (4 GiB) is the hard maximum: cryptsetup refuses anything above it. Past that ceiling only the iteration count can raise the cost, which is the entire difference between aggressive and paranoid.

What paranoid actually buys you

Paired with a properly generated passphrase, it takes offline brute force off the table completely — not "makes it hard", removes it as an avenue. Against a fleet of a thousand top-end GPUs, each running the six concurrent guesses that 4 GiB per guess allows, at the per-guess cost measured on real hardware:

                                  time to search half the keyspace
                                  (log scale — each block ≈ 1.5 orders of magnitude)

 weak / reused password  ~30 bit  ▏                          12 days
 human-chosen "strong"   ~40 bit  █                          33 years
 6 diceware words         77 bit  ████████                   10^13 years
 8 diceware words        103 bit  ██████████████             10^20 years
 10 diceware words       129 bit  ███████████████████        10^28 years
 12 diceware words       155 bit  ████████████████████████   10^36 years

 the universe is         ~10^10 years old  ────────┤ everything below this line
                                                     already outlives it

No budget closes that gap. Money buys an attacker hardware, and hardware scales the cost linearly while your passphrase scales the keyspace exponentially — adding two diceware words costs you four seconds of typing and multiplies their work by roughly seven million. A national intelligence service with an unlimited budget is in exactly the same position as a laptop thief, several dozen orders of magnitude short, and no appropriation changes the arithmetic.

Which is precisely why nobody capable would try. An adversary at that level does not brute-force argon2id; they go around it. They take the passphrase from you, or from a keylogger, or from a camera above your desk. They image the machine while it is running, where the key sits in RAM. They find the backup you made to an unencrypted disk. Set your KDF so that brute force is hopeless — it already is, at every tier here — and then spend your remaining attention on the attacks that actually work. See Risks.

That top row is the one to look at twice. At ~30 bits, paranoid buys you twelve days. The KDF cannot rescue a weak passphrase, and no tier on this page tries to pretend otherwise — see Your passphrase is the other half.

Using the graph to choose

Read it in both directions. If your passphrase is 8 diceware words or better, every tier already puts you past 10^13 years, so dropping to fast costs you nothing that matters and saves eight seconds at every single boot — a real, daily saving against a difference measured in orders of magnitude you will never reach. If your passphrase is shorter than you would like and you are not ready to change it, moving up to paranoid buys you the largest factor still available, though the row above shows how little that is compared with simply adding words.

Never use pbkdf2

Not as a fallback. Not to save memory. Not to shave a second off your boot. luksFormat still accepts it and LUKS1 defaulted to it, so it is easy to end up with by accident — treat that as a defect to fix, not a setting to keep.

pbkdf2 is not broken. It is obsolete, which is worse, because broken things get ripped out and obsolete things linger in defaults for twenty-six years.

Understand what it actually is. pbkdf2 is a for loop. That is the entire idea: take something cheap, do it a great many times, and hope the attacker finds the repetition as tedious as you do. It was standardised in RFC 2898, in the year 2000, when the threat model was a person with a computer roughly like yours. That assumption died the day general-purpose GPUs shipped, and pbkdf2 has had no answer since, because it has nothing to spend except time — and time is the one resource an attacker buys at a bulk discount and you pay for at retail.

It has no memory requirement at all. None. That single omission is the whole catastrophe. Memory is what makes an attacker's silicon expensive; a KDF that asks for none is a KDF that fits thousands of copies of itself onto one graphics card, and asks each one for nothing but arithmetic — the exact thing that card was built to do ten thousand times over. argon2id at 4 GiB tells a 24 GB GPU it may run six guesses. pbkdf2 tells it to help itself.

So you are on a treadmill, and it is rigged. Every iteration you add costs you, personally, at every single boot, on the hardware you already own. It costs your attacker nothing they cannot buy back with next year's card — and they will, while your boot time stays exactly where it is. You are the only participant in this arrangement who pays more over time. Raising pbkdf2's iteration count is not a defence; it is a subscription.

The cryptographic community reached this conclusion publicly and unanimously over a decade ago. The Password Hashing Competition ran from 2013 to 2015 for precisely this reason — that what everyone was using had aged out — and Argon2 won it. That was eleven years ago. Every argument for pbkdf2 on a new volume in 2026 is an argument that has already been had, in public, and lost.

Concretely, at the parameters below and against a human-chosen password: 28 years with argon2id, ten days with pbkdf2. That is not a tuning preference. That is the difference between a stolen laptop that keeps your life private and one that hands it over inside a fortnight.

This tool selects argon2id and only argon2id. No profile offers pbkdf2, no environment variable exposes it, no menu hides it behind an "advanced" tab, and there is no supported configuration in which it is the right answer. If luksDump shows PBKDF: pbkdf2 on a volume you care about, that volume is running on a twenty-six-year-old assumption about who is attacking it. Re-cost that keyslot with the luksConvertKey command above, today.

Your passphrase is the other half

The KDF sets the price of a single guess. Your passphrase sets how many guesses are needed. Neither one carries the volume alone — a 4 GiB argon2id keyslot protecting hunter2 falls in an afternoon, and a magnificent passphrase behind a cheap KDF is a lot cheaper to attack than you would like.

The numbers below assume the aggressive profile (4 GiB, 10 iterations) and a well-funded attacker: 1,000 top-end GPUs, 24 GiB of VRAM each, each running as many concurrent guesses as 4 GiB per guess leaves room for — about six — at the same per-guess cost this tool measures on your own machine. That is roughly 630 guesses per second for the whole fleet. Times are to search half the keyspace, and they are orders of magnitude, not predictions.

Passphrase Entropy Time to break What has happened by then
6 diceware words 77 bits ~10^13 years A thousand times the present age of the universe. The last red dwarfs are still burning — just.
7 diceware words 90 bits ~10^16 years Star formation ended long ago. Nothing is left but cooling remnants.
8 diceware words 103 bits ~10^20 years Galaxies have dynamically evaporated; the remnants drift alone in the dark.
10 diceware words 129 bits ~10^28 years Approaching the era in which protons themselves may decay.
11 diceware words 142 bits ~10^32 years Ordinary matter is dissolving, if protons decay at all.

For scale, the universe is about 1.4 × 10^10 years old. Even the weakest row here outlives it by a factor of a thousand. This is why the argument is over passphrase generation, never over adding another symbol to a short one.

What a weak KDF costs you — the pbkdf2 column

Same fleet, same passphrases, but with the keyslot wrapped in pbkdf2 instead. pbkdf2 needs essentially no memory per guess, so VRAM stops limiting how many guesses run at once and the attacker's rate is bounded only by raw arithmetic. The column below assumes they get 1000× the guess rate — deliberately conservative for a GPU fleet, since the real gap grows with every new card:

Passphrase Bits argon2id 4 GiB pbkdf2
a human-chosen password 40 28 years 10 days
a good non-diceware passphrase 60 10^7 years 10^4 years
6 diceware words 77 10^13 years 10^10 years
8 diceware words 103 10^20 years 10^17 years
10 diceware words 129 10^28 years 10^25 years

Read the bottom rows and the top row differently, because they say different things. At high entropy both are past cosmic time — argon2id is not what saves you there, your passphrase is. The KDF decides the outcome in the top row, where most real passphrases actually live: twenty-eight years versus ten days is the difference between a laptop that stays private and one that does not.

A 1000× cheaper KDF is exactly equivalent to deleting log2(1000) ≈ 10 bits from your passphrase — near enough one whole diceware word, silently, after you chose it. That is the entire argument, and it is why there is no pbkdf2 option in this tool.

Quantum computing

Two algorithms matter, and only one of them applies here.

Shor's algorithm does not touch this. It breaks RSA and elliptic-curve cryptography by exploiting their algebraic structure. LUKS uses none of that — AES-XTS with a 512-bit key and argon2id have no structure for Shor to attack. Nothing in this tool is on the "harvest now, decrypt later" list in the way a TLS session key or an encrypted email is.

Grover's algorithm does apply, and it halves your effective entropy. It searches an unstructured keyspace in √N instead of N, so a 103-bit passphrase behaves like a 51-bit one against an idealised quantum attacker. Applying that worst case to the table above:

Passphrase Bits Effective vs. Grover Time at 4 GiB argon2id
6 diceware words 77 38 10 years
8 diceware words 103 51 10^5 years
10 diceware words 129 64 10^9 years

That is the case for eight words as a floor and ten or eleven for anything you expect to matter in thirty years. Six words is comfortable today and thin under a machine that does not exist yet.

Two honest caveats, because this table is a ceiling and not a forecast. Grover requires evaluating the entire KDF in superposition — a fault-tolerant quantum computer would have to run argon2id at 4 GiB coherently, and memory-hard functions are about the most hostile possible target for that; nothing close to it is on any roadmap. Grover also parallelises badly: its speedup is sequential, so a thousand quantum computers do not give you a thousandfold gain the way a thousand GPUs do. Treat the middle column as a reason to buy entropy headroom while it costs you two extra words, not as a prediction that anyone will do this.

Use diceware, and generate it randomly. The EFF long wordlist holds 7,776 words — exactly five six-sided dice per word, log2(7776) = 12.92 bits each. Roll real dice if you have them; five rolls select one word by lookup.

Without dice, use a CSPRNG explicitly — do not let a shell pick for you. Fetch the list, check it, then draw from it:

# 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' '

Two details in that command are load-bearing:

  • -r draws with replacement. Diceware specifies independent rolls, so a word may legitimately repeat and each one contributes the full 12.92 bits. Without -r, shuf samples without replacement — a different model, and one that cannot produce the repeat it sometimes should.
  • Check the list. A truncated or substituted wordlist lowers your entropy silently: the passphrase still looks like eight ordinary words. A list cut to its first 100 lines yields 53 bits, not 103, and nothing in the output says so.

Eight words is a sound default for a machine you use daily. Ten or eleven is appropriate for a volume you expect to outlive the hardware.

The rules that actually matter:

  • Generated, not chosen. Entropy counts only if the selection was random. Words you picked yourself because they were memorable carry a small fraction of the bits their length suggests, because an attacker models the same preferences you have.
  • Never reused. Not your login password, not your password manager's master passphrase, not a variation on either. A keyslot passphrase that appears in any breach corpus is worth zero bits regardless of its length.
  • Length beats complexity. correct horse battery staple style beats Xk7$q!2 — more entropy, and vastly easier to type correctly.
  • Stay in plain ASCII. You type this at a bare console before any keymap is loaded, so a layout-dependent or non-ASCII character may be impossible to enter at the boot prompt even though it worked when you set it. Lowercase words and spaces are safe everywhere, which is a real practical argument for diceware over symbol soup on a boot passphrase specifically.
  • Watch Caps Lock. cryptsetup confirms a new passphrase by asking twice, so an inverted one verifies happily and fails only at the next boot. The deploy script warns when the kernel reports Caps Lock on at the confirmation gate.
  • Write it down until it is memorised. The realistic way to lose a correctly-configured encrypted volume is forgetting the passphrase, not someone breaking it. Paper in a safe beats a fifth backup of the header. Enrol a recovery key in a second keyslot as well — see Recovery key.

Dry run

sudo ./bin/luks-deploy.sh --dry-run     # or LUKS_DRY_RUN=1

Runs the entire read-only half — detection, selection menus, fstab cross-checks, the KDF benchmark, the state backup to the deployment drive — prints exactly what a real run would do (including the full cryptsetup reencrypt invocation), and exits before the point of no return. Nothing on the target is modified.

Boot splash

The deploy strips rhgb quiet from the boot args, because with the splash active the first LUKS passphrase prompt hides behind it and the boot looks hung. post-encryption-setup.sh restores both tokens after the first encrypted boot (via a marker in /var/lib/asahilocker/). Opt out with LUKS_KEEP_SPLASH=1.

Recovery key

During deployment the script offers to enroll a recovery key: 64 random hex characters in a second LUKS keyslot, saved to the deployment drive (pin the choice with LUKS_RECOVERY_KEY=yes|no). If the passphrase is ever forgotten, the recovery key still unlocks the volume — type it at the boot prompt, or use it as a --key-file from a live USB. It is enrolled before the header backup is taken, so the backup contains the slot. Move it to secure offline storage after deployment — anyone holding it can unlock the disk.

Note on GRUB and argon2id: none of this constrains the root volume, because GRUB never unlocks it — the initramfs does. It only matters if you have some other volume that GRUB itself must unlock — for example the experimental encrypted-/boot research, which runs a self-built GRUB 2.14 with argon2id (see docs/BOOT-ENCRYPTION-STATUS.md).

For those volumes: never use 4 GiB — a 32-bit overflow in GRUB's argon2_init wraps the allocation size to zero, so it proceeds instead of rejecting the parameters. That rule is unconditional.

Below that, the usable maximum is set by the firmware, not by GRUB, and has to be measured per platform. On x86 vendor UEFI more than 1 GiB has never worked — that firmware leaves GRUB too little heap, which is where the usual "GRUB caps at 1 GiB" comes from. Under Asahi's U-Boot it is not the same number: 2 GiB is measured working on an M2 Max. Use 1 GiB as the portable default; treat more as opt-in and measured. An allocation failure here means the machine does not boot. GRUB 2.12 (current in Fedora 44) has no argon2 support at all.

Do not answer any of that by downgrading the volume to pbkdf2 — argon2id at 1 GiB is memory-hard, pbkdf2 is not, and the gap matters far more than the memory cost does.


How this differs from encrypting by hand

The manual route — cryptsetup reencrypt followed by editing crypttab, fstab, the kernel cmdline, GRUB defaults and the BLS entries yourself — works, and there are guides for it. What this kit adds is the part those guides leave to you:

Manual cryptsetup reencrypt AsahiLocker
Partition selection You identify root/boot/EFI yourself Auto-detected, fstype-checked, and cross-checked against the target's own fstab
KDF parameters cryptsetup auto-benchmarks — machine-dependent, and picks sha256 Pinned argon2id + sha512, identical on every box, chosen from a menu benchmarked on your hardware
Boot config You edit crypttab, fstab, cmdline, GRUB defaults and every BLS entry by hand All rewritten, including every BLS entry and every initramfs image
Did it work? You find out at reboot 12-point verification gate refuses the reboot until it passes
Interrupted run You debug the header state yourself Detected and resumed automatically; cryptsetup repair path handled
SELinux Relabel it yourself or boot to AVC denials Relabelled and verified
Undo / recovery Whatever you thought to save Header backups plus a labeled bundle of every changed file
The Asahi footguns grub2-mkconfig clobbering the ESP stub; stale U-Boot EFI entries Boot guards install to prevent both

If you want to understand what it changes before trusting it, --dry-run prints every action, and docs/INTERNALS.md documents each one.


Risks — read this

  • No TPM on Apple Silicon. There is nowhere to seal a key, so you type the passphrase at every boot. That is by design, not a limitation of this tooling.
  • Forget the passphrase and the data is gone — unless you enrolled the optional recovery key and can still find it. There is no backdoor. Back up the recovery bundle, keep the recovery key offline, and remember the passphrase.
  • Have a verified backup before you start. Not "a backup" — one you have actually restored from or browsed. In-place re-encryption rewrites every sector of the root partition.
  • You cannot brick the Mac. Apple Silicon DFU / System Recovery always works, and macOS is on separate APFS partitions this tooling never touches. An interrupted encryption is not fatal either: LUKS2 re-encryption is journaled with checksum resilience, and re-running the script detects the interrupted state and resumes it automatically. The header backups cover the remaining worst case of a damaged header.
  • LUKS protects data at rest, not boot integrity. Only m1n1 stage 1 is cryptographically verified on Asahi; /boot is unencrypted and unsigned. An attacker with repeated physical access could tamper with the initramfs.
  • The passphrase prompt can hide behind boot text. If the machine looks hung right after GRUB, it is probably waiting — type the passphrase and press Enter. (The deploy strips the boot splash, and post-encryption-setup.sh restores it, precisely so this prompt stays visible on the first encrypted boots.)

Requirements

  • An already-installed Fedora Asahi Remix system with a btrfs root.
  • A Fedora Asahi live USB to run the encryption from — the script refuses to encrypt the filesystem it is booted from. A stock Fedora ISO will not boot on Apple Silicon; build one with asahi-fedora-usb as described in docs/LIVE-USB.md.
  • AC power connected (the script enforces AC or >50% battery).
  • cryptsetup ≥ 2.4 (for reencrypt --encrypt), btrfs-progs, dracut, grubby — all present in the live environment.
  • 15–60 minutes, depending on partition size.

The core script also works on Fedora x86_64, Arch and Manjaro with btrfs roots; the boot guards and U-Boot documentation are Asahi-specific.


Frequently asked questions

Can I encrypt Fedora Asahi Remix after installing it?

Yes — that is exactly what this is for. The Asahi installer has no full-disk encryption option, so encrypting normally means starting over. luks-deploy.sh converts the existing btrfs root in place instead, so your installed system, subvolumes, snapshots and btrfs UUID all survive.

Does this touch or wipe macOS?

No. macOS lives on separate APFS partitions that this tooling never reads or writes. Only the btrfs Linux root partition is converted. Apple Silicon DFU and System Recovery remain available regardless.

Why do I have to type a passphrase at every boot? Can't it use the Secure Enclave?

There is no TPM on Apple Silicon, and Asahi has no interface to seal a key in the Secure Enclave, so there is nowhere to store an auto-unlock key that would still be safe. The KDF re-runs in the initramfs on every boot and you type the passphrase. That is a platform constraint, not a shortcoming of this kit.

Which KDF does this use, and can I change it?

argon2id, always — it is memory-hard, and that memory requirement is what caps how many guesses a GPU or ASIC can run at once. You can tune its memory, iteration and thread costs freely, at deploy time or on an existing volume; see Changing the KDF on a volume that already exists. What you cannot sensibly do is swap the algorithm out — see Never use pbkdf2.

Why is /boot left unencrypted?

GRUB has to read the kernel and initramfs before anything is unlocked. The encrypted root is opened by the initramfs, not by GRUB, so keeping /boot plain avoids putting GRUB on the unlock path at all — where the KDF is limited by however much heap the firmware grants it, and where GRUB 2.12 (current in Fedora 44) has no argon2 support whatsoever. With GRUB 2.14 (which does support argon2id), that firmware limit has always been 1 GiB on x86 vendor UEFI; under Asahi's U-Boot it is higher — this project's self-built GRUB 2.14 measured 2 GiB working on an M2 Max. Exceeding whatever the firmware affords does not mean a slow boot, it means no boot: GRUB fails to allocate. And at exactly 4 GiB it is worse than a failure — a 32-bit overflow in its argon2_init wraps the allocation to zero, so it proceeds instead of rejecting the parameters. The trade-off of keeping /boot plain is that it is unsigned; see Risks.

What happens if the encryption is interrupted — power loss, a crash, a closed lid?

Nothing fatal. LUKS2 re-encryption is journaled with checksum resilience. Re-run luks-deploy.sh and it detects the interrupted state and resumes it (--resume-only), running cryptsetup repair first if the journal is dirty. That exact sequence — including a hard kill mid-reencrypt — is what the CI loopback test exercises on every push.

Can I run it unattended across several machines?

Yes. LUKS_PROFILE or the LUKS_PBKDF_* variables pin the KDF, LUKS_TARGET_ROOT / _BOOT / _EFI pin the partitions, and LUKS_PASSPHRASE_FILE supplies the passphrase, so no menu appears. Read docs/FLEET.md first — there is a real UUID-uniqueness footgun when imaging several boxes from one source.

Will this work on an M1 with only 8 GB of RAM?

Yes, on any profile including aggressive at 4 GiB. The memory cost only has to be allocatable in the initramfs, which has the machine entirely to itself. A base M1 is slower than an M2 Max at identical parameters because argon2id is memory-bandwidth-bound — which is why the installer benchmarks your machine instead of quoting someone else's numbers.

Does it work on anything other than Asahi?

The core script also runs on Fedora x86_64, Arch and Manjaro with btrfs roots. The boot guards and the U-Boot documentation are Apple-Silicon-specific.

How do I check what I actually ended up with?

sudo cryptsetup luksDump /dev/nvme0n1p6

Look for Cipher: aes-xts-plain64, a 512-bit key, and PBKDF: argon2id with the memory and iteration figures from the profile you chose.

Changing your KDF after installation

You are not stuck with the profile you picked. luksConvertKey re-wraps a keyslot's key under new argon2id parameters in place — no passphrase change, no re-encryption, not a byte of filesystem data touched, seconds of work. The header-backup step, commands for all four tiers (including paranoid), and the strength graph are in Changing the KDF on a volume that already exists; the menu version is bin/luks-tune.sh.


Documentation

Doc Covers
INSTALL.md Step-by-step install, start to finish, with what each prompt means
LIVE-USB.md Building a Fedora Asahi live USB, and the three ways to boot it
RECOVERY.md Interrupted encryption, unbootable system, corrupt header, undoing a shrink
UBOOT-BOOTFLOW.md Getting to the U-Boot prompt and booting the live USB
INTERNALS.md Every config file changed, the 12-point gate, self-repair, why /boot stays plain
FLEET.md Deploying across several M-series boxes, and the UUID-uniqueness footgun
BOOT-ENCRYPTION-STATUS.md Research, not a feature. Encrypted /boot: what has been measured, what broke, what is still unknown — and where help is wanted
BOOT-ENCRYPTION-DESIGN.md Design only, nothing implemented. The intended encrypted-/boot architecture: the two unlock options and their trade-offs, retrofit rules, detached headers, release plan

Contributing

Bug reports and patches are welcome — open an issue or a pull request.

Because this tooling rewrites a live root filesystem and its bootloader, a description of what went wrong is rarely enough to act on. CONTRIBUTING.md has copy-pasteable commands for the things that are: a read-only diagnostic bundle, running the loopback suite and the CI lint checks locally, dry-running the deploy, and verifying the GRUB argon2 constraint on your own machine. It also covers building GRUB 2.14 with argon2 into a local prefix, for anyone working on the /boot question.

License and contact

MIT — see LICENSE.

Copyright (c) 2026 William MacKinnon spilled-bowline0j@icloud.com

About

In-place LUKS2 disk encryption utility for an installed Fedora Asahi Remix system or fleet on Apple Silicon (M1/M2/M3/M4): auto-detecting deploy script, boot guards, recovery bundle, fully idempotent and a 12-point verification gate.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages