diff --git a/configs/airootfs/root/configurator b/configs/airootfs/root/configurator index 4cd3db3d..c79331c2 100644 --- a/configs/airootfs/root/configurator +++ b/configs/airootfs/root/configurator @@ -435,6 +435,21 @@ disk_abort_hook() { abort "$1" } +# Argon2id --pbkdf-memory (KiB) for live-ISO luksFormat. +# Requires omarchy-luks-pbkdf-memory to exit 0 with a numeric value; do not +# probe with `command -v` (bash reports non-executables as found). +luks_pbkdf_memory_kib() { + local out + if ! out=$(omarchy-luks-pbkdf-memory); then + return 1 + fi + if ! [[ $out =~ ^[0-9]+$ ]]; then + echo "omarchy-luks-pbkdf-memory produced non-numeric output: ${out}" >&2 + return 1 + fi + printf '%s\n' "$out" +} + # Save the configurator answers the installer consumes: git identity and # encryption choice as plain files, account details as archinstall credentials. # Deferred-provisioning installs defer the user to first boot: no credentials, and on encrypted @@ -730,11 +745,18 @@ run_partition_execute() { if [[ "$encrypt_installation" == "true" ]]; then step "Setting up LUKS2 on $root_partition_device" + local pbkdf_memory + if ! pbkdf_memory=$(luks_pbkdf_memory_kib); then + disk_abort_hook "Could not choose a safe LUKS Argon2id memory cost" + fi + say --foreground 8 "LUKS Argon2id memory cost: ${pbkdf_memory} KiB" # Kept as pipes rather than routed through disk_step: the passphrase must # not become an argv the process table can show. Folding stderr into # stdout still lands any error in the install log. if ! printf "%s" "$password" | - cryptsetup luksFormat --type luks2 --batch-mode "$root_partition_device" - 2>&1; then + cryptsetup luksFormat --type luks2 --batch-mode \ + --pbkdf argon2id --pbkdf-memory "$pbkdf_memory" --iter-time 2000 \ + "$root_partition_device" - 2>&1; then disk_abort_hook "Formatting LUKS2 on $root_partition_device failed" fi if ! printf "%s" "$password" | diff --git a/configs/airootfs/usr/local/bin/omarchy-iso-install b/configs/airootfs/usr/local/bin/omarchy-iso-install index 59aca95b..3f21e32a 100755 --- a/configs/airootfs/usr/local/bin/omarchy-iso-install +++ b/configs/airootfs/usr/local/bin/omarchy-iso-install @@ -23,5 +23,134 @@ while [[ $# -gt 0 ]]; do esac done +# Patch archinstall's Luks2.encrypt before Python imports it. +# +# Full-disk encrypted installs still go through archinstall's cryptsetup +# luksFormat. Without --pbkdf-memory, argon2id defaults to ~1 GiB and the +# kernel OOM-kills cryptsetup on constrained live/PXE boots (SIGKILL: 9). +# Locate luks.py via sys.path (do not importlib.util.find_spec: that imports +# archinstall as a side effect). Fail closed on the encrypted path: a missed +# patch would reintroduce the SIGKILL. +# Ask disk_config.disk_encryption, the same field archinstall_adapter.is_encrypted +# reads to decide whether luksFormat runs at all. The flag file is only a +# fallback: README calls it the autologin signal, not the encryption one. +install_is_encrypted() { + local encryption_type=no_encryption encrypt=true + + if [[ -n ${OMARCHY_INSTALL_CONFIG:-} && -r $OMARCHY_INSTALL_CONFIG ]]; then + encryption_type=$(jq -r ' + .disk_config.disk_encryption + | if . == null then "no_encryption" else (.encryption_type // "luks") end + ' "$OMARCHY_INSTALL_CONFIG" 2>/dev/null) || encryption_type=luks + fi + if [[ $encryption_type != no_encryption ]]; then + return 0 + fi + + if [[ -n ${OMARCHY_INSTALL_ENCRYPT_FILE:-} && -r $OMARCHY_INSTALL_ENCRYPT_FILE ]]; then + encrypt=$(tr -d '[:space:]' <"$OMARCHY_INSTALL_ENCRYPT_FILE") + fi + # The same values context.py accepts for the same file. + [[ ${encrypt,,} == true || ${encrypt,,} == yes || ${encrypt,,} == 1 ]] +} + +read_pbkdf_memory() { + local out + if ! out=$(omarchy-luks-pbkdf-memory); then + echo "omarchy-iso-install: omarchy-luks-pbkdf-memory failed" >&2 + return 1 + fi + if ! [[ $out =~ ^[0-9]+$ ]]; then + echo "omarchy-iso-install: omarchy-luks-pbkdf-memory produced non-numeric output: ${out}" >&2 + return 1 + fi + printf '%s\n' "$out" +} + +patch_archinstall_luks_pbkdf_memory() { + local luks_py pbkdf_memory + + if ! install_is_encrypted; then + return 0 + fi + + # 4.x moved Luks2 to lib/disk/luks.py; 3.x kept it at lib/luks.py. Prefer + # 4.x (what the ISO ships) then 3.x so a locator miss does not abort an + # encrypted install on an older tree. Fail closed either way. + luks_py=$(python - <<'PY' +import sys +from pathlib import Path + +relatives = ( + Path("archinstall") / "lib" / "disk" / "luks.py", # 4.x + Path("archinstall") / "lib" / "luks.py", # 3.x +) +for entry in sys.path: + for rel in relatives: + candidate = Path(entry) / rel + if candidate.is_file(): + print(candidate) + sys.exit(0) +sys.exit(1) +PY + ) || { + echo "omarchy-iso-install: could not locate archinstall luks.py on sys.path (tried lib/disk/luks.py and lib/luks.py)" >&2 + exit 1 + } + + if [[ ! -f $luks_py ]]; then + echo "omarchy-iso-install: archinstall luks.py is not a file: ${luks_py}" >&2 + exit 1 + fi + if [[ ! -w $luks_py ]]; then + echo "omarchy-iso-install: archinstall luks.py is not writable: ${luks_py}" >&2 + exit 1 + fi + + pbkdf_memory=$(read_pbkdf_memory) || exit 1 + + echo "omarchy-iso-install: capping archinstall LUKS Argon2id memory to ${pbkdf_memory} KiB" >&2 + + # Insert --pbkdf-memory after the --iter-time value in the cmd list. + # Same encrypt argv in 3.x (lib/luks.py) and 4.x (lib/disk/luks.py): + # '--iter-time', str(iter_time), *key_file_arg, '--use-urandom', + python - "$luks_py" "$pbkdf_memory" <<'PY' +from pathlib import Path +import re +import sys + +path = Path(sys.argv[1]) +memory = sys.argv[2] +text = path.read_text() + +# Already patched: refresh the value rather than keep the first run's. A retry +# in the same live session re-reads MemAvailable and can land on the other side +# of the floor, and the log would name a cost archinstall never used. +already = re.compile( + r"(['\"]--iter-time['\"],\s*str\(iter_time\),\s*['\"]--pbkdf-memory['\"],\s*)['\"][^'\"]*['\"]", +) +refreshed, n = already.subn(rf"\g<1>'{memory}'", text, count=1) +if n == 1: + path.write_text(refreshed) + sys.exit(0) + +pattern = re.compile( + r"(['\"]--iter-time['\"],\s*str\(iter_time\),)", + re.MULTILINE, +) +replacement = rf"\1\n\t\t\t'--pbkdf-memory',\n\t\t\t'{memory}'," +new_text, n = pattern.subn(replacement, text, count=1) +if n != 1: + sys.stderr.write( + f"omarchy-iso-install: could not patch {path} for --pbkdf-memory " + f"(matches={n})\n" + ) + sys.exit(1) +path.write_text(new_text) +PY +} + +patch_archinstall_luks_pbkdf_memory + cd /usr/share/omarchy-iso exec python -m orchestrator.main diff --git a/configs/airootfs/usr/local/bin/omarchy-luks-pbkdf-memory b/configs/airootfs/usr/local/bin/omarchy-luks-pbkdf-memory new file mode 100755 index 00000000..8fa1307e --- /dev/null +++ b/configs/airootfs/usr/local/bin/omarchy-luks-pbkdf-memory @@ -0,0 +1,52 @@ +#!/bin/bash +# Print Omarchy's Argon2id --pbkdf-memory (KiB) for live-ISO LUKS formatting. +# +# cryptsetup's argon2id default (~1 GiB) is OOM-killed on constrained live/PXE +# boots (cryptsetup dies with SIGKILL 9). Omarchy uses a fixed 256 MiB default +# so every install gets the same KDF parameters. We only step down to the +# RFC 9106 64 MiB floor when that default cannot fit, and we refuse to format +# if even the floor cannot. +# +# Argon2id attacker cost scales with memory squared times passes. Lowering +# --pbkdf-memory is a real hardness reduction; --iter-time does not make up +# for it. That is why the default is a fixed Omarchy value rather than a +# function of whatever MemAvailable happens to be at format time. +# +# OMARCHY_LUKS_MEMINFO may point at an alternate meminfo file (used by tests). + +set -euo pipefail + +default_kib=262144 # 256 MiB — Omarchy Argon2id memory cost +floor_kib=65536 # 64 MiB — RFC 9106 memory-constrained recommendation + +# cryptsetup will try to lock --pbkdf-memory KiB. Leave at least as much +# again for the rest of the live session so the allocation cannot consume +# all of MemAvailable. +fits() { + local need=$1 avail=$2 + ((avail >= need * 2)) +} + +meminfo=${OMARCHY_LUKS_MEMINFO:-/proc/meminfo} +avail_kib=0 +if [[ -r $meminfo ]]; then + avail_kib=$(awk '/^MemAvailable:/ { print $2; exit }' "$meminfo" 2>/dev/null || echo 0) +fi + +if ! [[ $avail_kib =~ ^[0-9]+$ ]]; then + avail_kib=0 +fi + +if fits "$default_kib" "$avail_kib"; then + printf '%s\n' "$default_kib" + exit 0 +fi + +if fits "$floor_kib" "$avail_kib"; then + echo "omarchy-luks-pbkdf-memory: MemAvailable ${avail_kib} KiB cannot fit the ${default_kib} KiB Omarchy default; using ${floor_kib} KiB floor" >&2 + printf '%s\n' "$floor_kib" + exit 0 +fi + +echo "omarchy-luks-pbkdf-memory: MemAvailable ${avail_kib} KiB is too small to fit even the ${floor_kib} KiB Argon2id floor; refusing LUKS format" >&2 +exit 1 diff --git a/configs/profiledef.sh b/configs/profiledef.sh index 268a3cea..9bdc0d31 100644 --- a/configs/profiledef.sh +++ b/configs/profiledef.sh @@ -40,6 +40,7 @@ file_permissions=( ["/usr/local/bin/omarchy-iso-cleanup-disk"]="0:0:755" ["/usr/local/bin/omarchy-install-dashboard"]="0:0:755" ["/usr/local/bin/omarchy-iso-install"]="0:0:755" + ["/usr/local/bin/omarchy-luks-pbkdf-memory"]="0:0:755" ["/usr/local/bin/omarchy-upload-log"]="0:0:755" ["/var/cache/omarchy/mirror/offline/"]="0:0:775" ) diff --git a/test/unit/luks-pbkdf-memory-test.sh b/test/unit/luks-pbkdf-memory-test.sh new file mode 100755 index 00000000..43f9b8ba --- /dev/null +++ b/test/unit/luks-pbkdf-memory-test.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# +# Drive omarchy-luks-pbkdf-memory against fake meminfo files. The helper must +# be invoked for every assertion; inlining the formula would let a stubbed +# helper (printf '65536\n') pass. + +set -euo pipefail + +ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) +helper="$ROOT/configs/airootfs/usr/local/bin/omarchy-luks-pbkdf-memory" + +pass() { + printf 'ok - %s\n' "$1" +} + +fail() { + local description="$1" + local detail="${2:-}" + printf 'not ok - %s\n' "$description" >&2 + [[ -n $detail ]] && printf '%s\n' "$detail" >&2 + exit 1 +} + +[[ -x $helper ]] || fail "helper is executable" "missing $helper" + +fake_meminfo=$(mktemp) +trap 'rm -f "$fake_meminfo"' EXIT + +run_helper() { + OMARCHY_LUKS_MEMINFO="$fake_meminfo" "$helper" +} + +# 4 GiB available: 2x the 256 MiB default fits. +printf 'MemAvailable: 4194304 kB\n' >"$fake_meminfo" +out=$(run_helper) || fail "default on plenty of RAM" "helper exited $?" +[[ $out == 262144 ]] || fail "default on plenty of RAM" "expected 262144, got $out" +pass "default 256 MiB when MemAvailable is 4 GiB" + +# 512 MiB available: 256 MiB default needs 512 MiB free (2x), so this is the +# exact threshold for the default. +printf 'MemAvailable: 524288 kB\n' >"$fake_meminfo" +out=$(run_helper) || fail "default at 2x threshold" "helper exited $?" +[[ $out == 262144 ]] || fail "default at 2x threshold" "expected 262144, got $out" +pass "default 256 MiB when MemAvailable is exactly 2x 256 MiB" + +# Just under 2x default (511 MiB): cannot fit 256 MiB, can fit 64 MiB floor. +printf 'MemAvailable: 523264 kB\n' >"$fake_meminfo" +out=$(run_helper 2>/dev/null) || fail "floor when default cannot fit" "helper exited $?" +[[ $out == 65536 ]] || fail "floor when default cannot fit" "expected 65536, got $out" +pass "64 MiB floor when MemAvailable cannot fit 256 MiB" + +# 128 MiB available: exact 2x floor threshold. +printf 'MemAvailable: 131072 kB\n' >"$fake_meminfo" +out=$(run_helper 2>/dev/null) || fail "floor at 2x threshold" "helper exited $?" +[[ $out == 65536 ]] || fail "floor at 2x threshold" "expected 65536, got $out" +pass "64 MiB floor when MemAvailable is exactly 2x 64 MiB" + +# 64 MiB available: floor itself cannot fit (would consume all free RAM). +printf 'MemAvailable: 65536 kB\n' >"$fake_meminfo" +if out=$(run_helper 2>/dev/null); then + fail "refuse when floor cannot fit" "helper printed $out instead of failing" +fi +pass "refuses to format when MemAvailable cannot fit the 64 MiB floor" + +# Unreadable / missing MemAvailable: treat as 0 and refuse. +printf 'MemTotal: 16777216 kB\n' >"$fake_meminfo" +if out=$(run_helper 2>/dev/null); then + fail "refuse without MemAvailable" "helper printed $out instead of failing" +fi +pass "refuses to format when meminfo has no MemAvailable" + +echo "luks-pbkdf-memory-test: ok"