diff --git a/tests/fixtures/luks/README.md b/tests/fixtures/luks/README.md new file mode 100644 index 0000000..3b52121 --- /dev/null +++ b/tests/fixtures/luks/README.md @@ -0,0 +1,44 @@ +# LUKS2 fixtures from real cryptsetup + +Other LUKS tests use images built by `tests/imagebuild_luks.py`, so reader and +builder share assumptions. These images are meant to be made by the real +`cryptsetup`. + +**Status:** `make_fixtures.sh` has been written but not yet run (no +cryptsetup in the authoring sandbox), so the `.img.xz` files and +`expected.json` are not committed yet. Run the script and commit its output. + +## Regenerate + + sh tests/fixtures/luks/make_fixtures.sh [output-dir] + +Needs `cryptsetup` (2.4+), `xz` and `python3`. It works on plain files with +`--disable-locks --force-offline-reencrypt`, so root should not be needed. +If rootless fails on your system, run the same script as root +(`sudo sh ...`); no loop devices are used either way. The cryptsetup version +used is recorded in `expected.json` (`cryptsetup` field). + +## Contents + +Plaintext: a 768 KiB file where each 512-byte sector reads +`STRATA-FIXTURE sector N` (NUL-padded). The first 640 KiB are replaced by +the LUKS header (`--reduce-device-size`), leaving 128 KiB of payload. + +| Image | KDF | Sector | Keyslots | +|---|---|---|---| +| `luks2-argon2id-4k.img.xz` | argon2id | 4096 | 0: passphrase 1 | +| `luks2-argon2i-512.img.xz` | argon2i | 512 | 0: passphrase 1 (argon2i), 1: passphrase 2 (pbkdf2, 1000 iterations) | + +Common parameters: `aes-xts-plain64`, 512-bit key, sha256, metadata 16k, +keyslots area 512k, `--pbkdf-force-iterations 4 --pbkdf-memory 64 +--pbkdf-parallel 1`. + +Passphrases (public, no trailing newline): `strata-fixture-1`, +`strata-fixture-2`. + +`expected.json` holds, per image, the `luksDump --dump-json-metadata` output +and the volume key (hex) from `--dump-volume-key`. + +## License + +MIT, same as the repository. The contents are public test data. diff --git a/tests/fixtures/luks/make_fixtures.sh b/tests/fixtures/luks/make_fixtures.sh new file mode 100644 index 0000000..0af54ad --- /dev/null +++ b/tests/fixtures/luks/make_fixtures.sh @@ -0,0 +1,81 @@ +#!/bin/sh +# Build the real-cryptsetup LUKS2 fixtures. See README.md. +# Usage: sh make_fixtures.sh [output-dir] (default: this script's directory) +# Needs cryptsetup >= 2.4, xz, python3. Tries rootless first. +set -eu + +here=$(cd "$(dirname "$0")" && pwd) +out=${1:-$here} +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT INT TERM + +# Public passphrases (no trailing newline: --key-file reads raw bytes). +printf '%s' 'strata-fixture-1' > "$work/pw1" +printf '%s' 'strata-fixture-2' > "$work/pw2" + +IMG_SIZE=786432 # 768 KiB plain file = 1536 sectors of 512 bytes +HDR_SIZE=655360 # 640 KiB reserved for header + keyslots area +HDR_SECTORS=$((HDR_SIZE / 512)) +COMMON="--type luks2 --disable-locks --batch-mode" +PBKDF="--pbkdf-force-iterations 4 --pbkdf-memory 64 --pbkdf-parallel 1" + +plain() { # plain : every 512-byte sector reads "STRATA-FIXTURE sector N" + python3 -I - "$1" "$IMG_SIZE" <<'PY' +import sys +path, size = sys.argv[1], int(sys.argv[2]) +with open(path, "wb") as f: + for n in range(size // 512): + f.write(("STRATA-FIXTURE sector %d" % n).encode().ljust(512, b"\0")) +PY +} + +encrypt() { # encrypt + # shellcheck disable=SC2086 + cryptsetup reencrypt --encrypt $COMMON --force-offline-reencrypt \ + --reduce-device-size "$HDR_SIZE" --offset "$HDR_SECTORS" \ + --luks2-metadata-size 16k --luks2-keyslots-size 512k \ + --cipher aes-xts-plain64 --key-size 512 --hash sha256 \ + --sector-size "$2" --pbkdf "$3" $PBKDF --key-file "$work/pw1" "$1" +} + +# A: argon2id, 4096-byte sectors +plain "$work/luks2-argon2id-4k.img" +encrypt "$work/luks2-argon2id-4k.img" 4096 argon2id + +# B: argon2i, 512-byte sectors, plus a pbkdf2 keyslot (slot 1, passphrase 2) +plain "$work/luks2-argon2i-512.img" +encrypt "$work/luks2-argon2i-512.img" 512 argon2i +# shellcheck disable=SC2086 +cryptsetup luksAddKey $COMMON --pbkdf pbkdf2 --pbkdf-force-iterations 1000 \ + --key-file "$work/pw1" "$work/luks2-argon2i-512.img" "$work/pw2" + +# expected.json: header metadata + volume key per image, tool version +for img in luks2-argon2id-4k luks2-argon2i-512; do + f=$work/$img.img + cryptsetup luksDump --disable-locks --dump-json-metadata "$f" > "$work/$img.json" + cryptsetup luksDump --disable-locks --dump-volume-key --batch-mode \ + --key-file "$work/pw1" "$f" > "$work/$img.vk" +done +cryptsetup --version > "$work/version" +python3 -I - "$work" "$out/expected.json" <<'PY' +import json, re, sys +work, dest = sys.argv[1], sys.argv[2] +res = {"cryptsetup": open(work + "/version").read().strip(), + "passphrases": {"1": "strata-fixture-1", "2": "strata-fixture-2"}, + "plaintext": "every 512-byte sector N = 'STRATA-FIXTURE sector N' NUL-padded", + "images": {}} +for name in ("luks2-argon2id-4k", "luks2-argon2i-512"): + meta = json.load(open("%s/%s.json" % (work, name))) + vk = open("%s/%s.vk" % (work, name)).read() + m = re.search(r"([0-9a-fA-F]{2}(?:\s+[0-9a-fA-F]{2})*)\s*$", vk) + hexkey = re.sub(r"\s+", "", m.group(1)).lower() if m else None + res["images"][name + ".img.xz"] = {"volume_key_hex": hexkey, "metadata": meta} +with open(dest, "w") as f: + json.dump(res, f, indent=2, sort_keys=True) + f.write("\n") +PY + +for img in luks2-argon2id-4k luks2-argon2i-512; do + xz -9e -c "$work/$img.img" > "$out/$img.img.xz" +done +echo "wrote fixtures to $out"