diff --git a/autorun/wipe-lib.sh b/autorun/wipe-lib.sh index fd8ae56..1e46336 100755 --- a/autorun/wipe-lib.sh +++ b/autorun/wipe-lib.sh @@ -115,6 +115,65 @@ unfreeze_drive() { return 0 } +# --- Overwrite pass count (opt-in multi-pass for random-overwrite methods) --- +# NIST SP 800-88 Rev. 1 treats a single random overwrite pass as sufficient, so +# the default is 1. Operators can raise it for stricter organizational policies +# (e.g. legacy DoD 5220.22-M expectations). Erase/crypto methods are unaffected. +# Resolution order (highest precedence first): +# 1. WIPE_OVERWRITE_ROUNDS environment variable +# 2. ssr.overwrite-rounds=N on the kernel cmdline +# 3. WIPE_OVERWRITE_ROUNDS=N in the baked wipe.env at the USB root +# 4. default: 1 +# Only positive integers are accepted; an invalid value at one source is warned +# about and the next source is tried, so a bad value never silently drops passes. +_valid_rounds() { [[ "$1" =~ ^[1-9][0-9]*$ ]]; } + +resolve_overwrite_rounds() { + local c + local cmdline_file="${WIPE_CMDLINE_FILE:-/proc/cmdline}" + local cfg_file="${WIPE_CONFIG_FILE:-$(dirname "${BASH_SOURCE[0]}")/../wipe.env}" + + # 1. environment + c="${WIPE_OVERWRITE_ROUNDS:-}" + if [[ -n "$c" ]]; then + if _valid_rounds "$c"; then printf '%s' "$c"; return 0; fi + log "WARN: ignoring invalid WIPE_OVERWRITE_ROUNDS='$c' (env)" >&2 + fi + + # 2. kernel cmdline + if [[ -r "$cmdline_file" ]]; then + c=$(tr ' ' '\n' < "$cmdline_file" 2>/dev/null | grep -E '^ssr\.overwrite-rounds=' | tail -1 || true) + c="${c#ssr.overwrite-rounds=}" + if [[ -n "$c" ]]; then + if _valid_rounds "$c"; then printf '%s' "$c"; return 0; fi + log "WARN: ignoring invalid ssr.overwrite-rounds='$c' (cmdline)" >&2 + fi + fi + + # 3. baked config file + if [[ -r "$cfg_file" ]]; then + c=$(grep -E '^[[:space:]]*WIPE_OVERWRITE_ROUNDS=' "$cfg_file" 2>/dev/null | tail -1 | cut -d'=' -f2 | tr -d '[:space:]' || true) + if [[ -n "$c" ]]; then + if _valid_rounds "$c"; then printf '%s' "$c"; return 0; fi + log "WARN: ignoring invalid WIPE_OVERWRITE_ROUNDS='$c' ($cfg_file)" >&2 + fi + fi + + # 4. default + printf '1' +} + +# Overwrite pass count for the audit log: the effective rounds for random- +# overwrite drive types, or the JSON literal "null" for erase/crypto types that +# do not overwrite. (A frozen sata-ssd falls back to overwrite at runtime; the +# audit classifies by nominal method, so that exceptional path records null.) +overwrite_rounds_for_type() { + case "$1" in + nvme-ssd | nvme-ssd-sed | sata-ssd | sata-ssd-sed) printf 'null' ;; + *) resolve_overwrite_rounds ;; + esac +} + wipe_drive() { local dev="$1" local dtype="$2" @@ -122,6 +181,8 @@ wipe_drive() { method_info=$(nist_method_for_type "$dtype") local method_desc method_desc=$(echo "$method_info" | cut -d'|' -f2) + local rounds + rounds=$(resolve_overwrite_rounds) log "Wiping /dev/$dev (type=$dtype, method=$method_desc)" @@ -155,8 +216,8 @@ wipe_drive() { # ref: hdparm --security-erase-enhanced (ATA secure erase); # falls back to nwipe random overwrite if the drive is frozen. if ! unfreeze_drive "$dev"; then - log "FALLBACK: frozen drive, using nwipe overwrite instead" - nwipe --autonuke --method=random --rounds=1 --verify=last "/dev/$dev" 2>&1 + log "FALLBACK: frozen drive, using nwipe overwrite instead ($rounds pass(es))" + nwipe --autonuke --method=random --rounds="$rounds" --verify=last "/dev/$dev" 2>&1 return $? fi local wipe_pass @@ -171,13 +232,13 @@ wipe_drive() { return $? ;; hdd) - # ref: nwipe -m random -r 1 --verify=last (random overwrite + verify) - nwipe --autonuke --method=random --rounds=1 --verify=last "/dev/$dev" 2>&1 + # ref: nwipe -m random -r --verify=last (random overwrite + verify) + nwipe --autonuke --method=random --rounds="$rounds" --verify=last "/dev/$dev" 2>&1 return $? ;; usb-flash|*) - # ref: nwipe -m random (best-effort random overwrite, no verify) - nwipe --autonuke --method=random "/dev/$dev" 2>&1 + # ref: nwipe -m random -r (best-effort random overwrite, no verify) + nwipe --autonuke --method=random --rounds="$rounds" "/dev/$dev" 2>&1 return $? ;; esac @@ -297,6 +358,9 @@ append_drive_audit() { local started="$9" completed="${10}" result="${11}" local verify_ok="${12:-}" errors="${13:-}" + local overwrite_rounds + overwrite_rounds=$(overwrite_rounds_for_type "$dtype") + local entry entry=$(cat << EOF { @@ -307,6 +371,7 @@ append_drive_audit() { "type": "${dtype}", "nist_tier": "${nist_tier}", "method": "${method_desc}", + "overwrite_rounds": ${overwrite_rounds}, "started_at": "${started}", "completed_at": "${completed}", "verify": {"sectors_sampled": 1024, "all_zero_or_random": ${verify_ok:-false}}, diff --git a/autorun/wipe-now.sh b/autorun/wipe-now.sh index 410a32d..6fc51d8 100755 --- a/autorun/wipe-now.sh +++ b/autorun/wipe-now.sh @@ -85,6 +85,7 @@ for entry in "${drives[@]}"; do fi completed=$(date -u +%Y-%m-%dT%H:%M:%SZ) + overwrite_rounds=$(overwrite_rounds_for_type "$dtype") # Write per-drive result to a temp file for the parent to collect cat > "$WIPE_TMP_DIR/wipe-result-${name}.json" << DRIVEEOF @@ -96,6 +97,7 @@ for entry in "${drives[@]}"; do "type": "${dtype}", "nist_tier": "${nist_tier}", "method": "${method_desc}", + "overwrite_rounds": ${overwrite_rounds}, "started_at": "${started}", "completed_at": "${completed}", "verify": {"sectors_sampled": 1024, "all_zero_or_random": ${verify_ok}}, diff --git a/bin/build-wipe-usb.sh b/bin/build-wipe-usb.sh index 389a6c7..d168aba 100755 --- a/bin/build-wipe-usb.sh +++ b/bin/build-wipe-usb.sh @@ -114,6 +114,7 @@ build_image() { sudo cp "${REPO_ROOT}/autorun/wipe-wizard.sh" "$mp/autorun/" sudo cp "${REPO_ROOT}/autorun/wipe-now.sh" "$mp/autorun/" sudo chmod +x "$mp/autorun/"*.sh + sudo cp "${REPO_ROOT}/config/wipe.env" "$mp/wipe.env" sudo touch "$mp/.simsys-wipe" sudo umount "$mp" rmdir "$mp" @@ -129,6 +130,7 @@ build_image() { mcopy -i "$fat_img" "${REPO_ROOT}/autorun/wipe-lib.sh" ::/autorun/ mcopy -i "$fat_img" "${REPO_ROOT}/autorun/wipe-wizard.sh" ::/autorun/ mcopy -i "$fat_img" "${REPO_ROOT}/autorun/wipe-now.sh" ::/autorun/ + mcopy -i "$fat_img" "${REPO_ROOT}/config/wipe.env" ::/wipe.env marker_file="$(mktemp)" mcopy -i "$fat_img" "$marker_file" ::/.simsys-wipe rm -f "$marker_file" diff --git a/config/wipe.env b/config/wipe.env new file mode 100644 index 0000000..8377085 --- /dev/null +++ b/config/wipe.env @@ -0,0 +1,17 @@ +# Tunable defaults for the NIST 800-88 wipe USB. +# Baked onto the FAT32 root of the wipe stick at build time. To change after +# flashing, edit /wipe.env on the FAT32 partition of the wipe stick. + +# Number of random-overwrite passes for overwrite-based methods (HDD, USB +# flash, and the frozen-SSD fallback). Only positive integers are accepted. +# +# The default of 1 is NIST SP 800-88 Rev. 1 compliant — a single random pass +# is sufficient for modern media. Raise this only if an organizational policy +# mandates stricter-than-NIST multi-pass overwrite (e.g. legacy DoD +# 5220.22-M). Extra passes cost proportionally more wall-clock time and do +# nothing for SSD erase/crypto methods (which ignore this setting). +# +# May also be overridden per boot via the kernel cmdline +# (ssr.overwrite-rounds=N) or the WIPE_OVERWRITE_ROUNDS environment variable, +# both of which take precedence over this file. +WIPE_OVERWRITE_ROUNDS=1 diff --git a/docs/usage.md b/docs/usage.md index e689146..c18bbc3 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -87,6 +87,28 @@ can't touch): > of Memtest86+ that adds `mt86p.cfg` config support, finite pass counts, and > ACPI auto-poweroff so this stage is also walk-away. +## Wipe USB — overwrite pass count + +Overwrite-based methods (HDD, USB flash, and the frozen-SSD fallback) default to +a **single** random-overwrite pass. This is compliant with NIST SP 800-88 Rev. 1 +— a single pass is sufficient for modern media, and additional passes are **not +required**. SSD erase/crypto methods ignore this setting entirely. + +If an organizational policy mandates stricter-than-NIST multi-pass overwrite +(e.g. legacy DoD 5220.22-M), raise the pass count via any of these — highest +precedence first: + +1. **Environment variable** (scripted/direct runs): `WIPE_OVERWRITE_ROUNDS=3` +2. **Kernel cmdline** (per boot): add `ssr.overwrite-rounds=3` to the boot entry +3. **Baked default**: edit `WIPE_OVERWRITE_ROUNDS` in `/wipe.env` on the FAT32 + partition of the wipe stick (set at build time from `config/wipe.env`) + +Only positive integers are accepted; an invalid value is ignored (with a warning) +and the next source is tried, so a bad value never silently reduces the pass +count. Each extra pass costs proportionally more wall-clock time. The pass count +actually used is recorded per drive in the JSON audit log as `overwrite_rounds` +(`null` for erase/crypto methods). + ## Troubleshooting - **Box didn't power off after 2.5h.** SystemRescue may have crashed mid-test. diff --git a/tests/test_overwrite_rounds.sh b/tests/test_overwrite_rounds.sh new file mode 100755 index 0000000..a45371f --- /dev/null +++ b/tests/test_overwrite_rounds.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# Tests for the opt-in overwrite pass-count resolver (resolve_overwrite_rounds) +# and the audit mapping (overwrite_rounds_for_type) in wipe-lib.sh. + +set -euo pipefail + +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +# shellcheck disable=SC1091 +source "$REPO_ROOT/autorun/wipe-lib.sh" + +fails=0 +check() { + local name="$1" expected="$2" actual="$3" + if [[ "$actual" == "$expected" ]]; then + echo "PASS: $name -> $actual" + else + echo "FAIL: $name — expected [$expected], got [$actual]" + fails=$((fails + 1)) + fi +} + +# Empty cmdline / config by default; point the source overrides at temp files. +EMPTY_CMDLINE="$TMP/cmdline.empty" +: > "$EMPTY_CMDLINE" +NO_CFG="$TMP/does-not-exist.env" + +rounds() { WIPE_CMDLINE_FILE="${1:-$EMPTY_CMDLINE}" WIPE_CONFIG_FILE="${2:-$NO_CFG}" resolve_overwrite_rounds 2>/dev/null; } + +echo "== precedence & default ==" + +unset WIPE_OVERWRITE_ROUNDS +check "all sources unset -> default 1" "1" "$(rounds)" + +# Baked config file only. +echo "WIPE_OVERWRITE_ROUNDS=3" > "$TMP/wipe.env" +check "baked file only -> 3" "3" "$(rounds "$EMPTY_CMDLINE" "$TMP/wipe.env")" + +# Kernel cmdline overrides the file. +echo "BOOT_IMAGE=/vmlinuz quiet ssr.overwrite-rounds=5 splash" > "$TMP/cmdline.5" +check "cmdline overrides file (5 vs 3)" "5" "$(rounds "$TMP/cmdline.5" "$TMP/wipe.env")" + +# Env overrides everything. +check "env overrides cmdline+file" "7" "$(WIPE_OVERWRITE_ROUNDS=7 rounds "$TMP/cmdline.5" "$TMP/wipe.env")" + +echo "== validation / fall-through ==" + +# Invalid env falls through to a valid cmdline. +check "invalid env -> cmdline" "5" "$(WIPE_OVERWRITE_ROUNDS=abc rounds "$TMP/cmdline.5" "$NO_CFG")" + +# Invalid env + invalid cmdline -> valid file. +echo "ssr.overwrite-rounds=-1" > "$TMP/cmdline.bad" +echo "WIPE_OVERWRITE_ROUNDS=4" > "$TMP/wipe4.env" +check "invalid env+cmdline -> file" "4" "$(WIPE_OVERWRITE_ROUNDS=0 rounds "$TMP/cmdline.bad" "$TMP/wipe4.env")" + +# Everything invalid -> default 1 (never 0 / never silently drops passes). +echo "WIPE_OVERWRITE_ROUNDS=nope" > "$TMP/wipe.bad" +check "all invalid -> default 1" "1" "$(WIPE_OVERWRITE_ROUNDS=x rounds "$TMP/cmdline.bad" "$TMP/wipe.bad")" + +# Zero is rejected (would be a dangerous no-op wipe). +check "zero rejected -> default 1" "1" "$(WIPE_OVERWRITE_ROUNDS=0 rounds)" + +echo "== overwrite_rounds_for_type (audit mapping) ==" + +unset WIPE_OVERWRITE_ROUNDS +otype() { WIPE_CMDLINE_FILE="$EMPTY_CMDLINE" WIPE_CONFIG_FILE="$NO_CFG" overwrite_rounds_for_type "$1" 2>/dev/null; } + +check "nvme-ssd -> null (erase)" "null" "$(otype nvme-ssd)" +check "nvme-ssd-sed -> null (crypto)" "null" "$(otype nvme-ssd-sed)" +check "sata-ssd -> null (erase)" "null" "$(otype sata-ssd)" +check "sata-ssd-sed -> null (crypto)" "null" "$(otype sata-ssd-sed)" +check "hdd -> 1 (overwrite, default)" "1" "$(otype hdd)" +check "usb-flash -> 1 (overwrite, default)" "1" "$(otype usb-flash)" +check "unknown type -> 1 (fallback overwrite)" "1" "$(otype weird-type)" +check "hdd honors env override" "2" "$(WIPE_OVERWRITE_ROUNDS=2 WIPE_CMDLINE_FILE="$EMPTY_CMDLINE" WIPE_CONFIG_FILE="$NO_CFG" overwrite_rounds_for_type hdd 2>/dev/null)" + +if [[ $fails -eq 0 ]]; then + echo "All overwrite-rounds tests passed." +else + echo "$fails test(s) failed." + /bin/false +fi