Files
StrixScripts/HDD/erase-storage.sh
T

245 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# erase-storage.sh — overwrite a partition or securely erase a whole disk.
# Full documentation: see erase-storage.md in this same folder.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/common.sh
source "${SCRIPT_DIR}/../lib/common.sh"
SCRIPT_HELP=$(cat <<'HELP'
Usage: sudo ./HDD/erase-storage.sh DEVICE [zero|random]
For a partition, writes one full pass of zeros (default) or pseudorandom data.
For a whole disk, detects and runs a supported ATA Secure Erase or NVMe Sanitize
operation. Whole-disk erasure requires two separate confirmations.
At startup, asks you to confirm that you can keep the computer and drive
powered and connected for the hours the operation may take.
Arguments:
DEVICE Partition (for example /dev/sda1) or whole disk
zero Write zeros to a partition (default)
random Write pseudorandom data to a partition
Options:
-h, --help Show this help message and exit
Examples:
sudo ./HDD/erase-storage.sh /dev/sda1
sudo ./HDD/erase-storage.sh /dev/nvme0n1p2 random
sudo ./HDD/erase-storage.sh /dev/sda
sudo ./HDD/erase-storage.sh /dev/nvme0n1
HELP
)
common::show_help_if_requested "$@"
[[ $# -ge 1 && $# -le 2 ]] || common::die "expected DEVICE [zero|random]; run with --help for usage"
DEVICE_INPUT="$1"
if [[ "$DEVICE_INPUT" == dev/* ]]; then
DEVICE_INPUT="/$DEVICE_INPUT"
fi
[[ "$DEVICE_INPUT" == /dev/* ]] || common::die "DEVICE must be a path under /dev"
FILL_MODE="${2:-zero}"
case "$FILL_MODE" in
zero|random)
;;
*)
common::die "partition fill mode must be 'zero' or 'random'"
;;
esac
common::log "WARNING: Erasing data may take hours. Do not power off, reboot, or suspend the computer, or disconnect power or the drive."
common::confirm "Can the computer and drive remain powered and connected until the operation finishes?" || exit 0
for required_command in findmnt lsblk readlink swapon date dd sleep; do
common::require_cmd "$required_command"
done
[[ "$EUID" -eq 0 ]] || common::die "this script must be run as root (for example, with sudo)"
[[ -b "$DEVICE_INPUT" ]] || common::die "$DEVICE_INPUT does not exist or is not a block device"
DEVICE="$(readlink -f -- "$DEVICE_INPUT")" || common::die "cannot resolve device path: $DEVICE_INPUT"
DEVICE_TYPE="$(lsblk -dnro TYPE -- "$DEVICE")" || common::die "cannot determine device type"
[[ "$DEVICE_TYPE" == "part" || "$DEVICE_TYPE" == "disk" ]] \
|| common::die "only a partition or a whole disk is accepted (detected: $DEVICE_TYPE)"
ROOT_SOURCE="$(findmnt -n -o SOURCE /)" || common::die "cannot determine the root filesystem device"
ROOT_SOURCE="${ROOT_SOURCE%%\[*}"
[[ -b "$ROOT_SOURCE" ]] || common::die "cannot safely resolve the root filesystem device ($ROOT_SOURCE)"
ROOT_DEVICE="$(readlink -f -- "$ROOT_SOURCE")" || common::die "cannot resolve the root filesystem device"
ROOT_TREE="$(lsblk --inverse --list --noheadings --paths --output PATH -- "$ROOT_DEVICE")" \
|| common::die "cannot determine the devices backing the root filesystem"
while IFS= read -r root_device; do
[[ -n "$root_device" ]] || continue
if [[ "$(readlink -f -- "$root_device")" == "$DEVICE" ]]; then
common::die "$DEVICE backs the root filesystem; refusing to erase it"
fi
done <<< "$ROOT_TREE"
DEVICE_TREE="$(lsblk --list --noheadings --paths --output PATH -- "$DEVICE")" \
|| common::die "cannot inspect devices below $DEVICE"
ACTIVE_SWAP_DEVICES="$(swapon --show --noheadings --raw --output NAME)" \
|| common::die "cannot inspect active swap devices"
while IFS= read -r child_device; do
[[ -n "$child_device" ]] || continue
if findmnt -rn -S "$child_device" >/dev/null 2>&1; then
common::die "$child_device is mounted; unmount it before running this script"
fi
while IFS= read -r swap_device; do
[[ -n "$swap_device" ]] || continue
if [[ -b "$swap_device" ]]; then
swap_source="$swap_device"
else
swap_mount_source="$(findmnt -n -o SOURCE -T "$swap_device")" \
|| common::die "cannot determine the filesystem backing active swap file $swap_device"
swap_mount_source="${swap_mount_source%%\[*}"
[[ -b "$swap_mount_source" ]] \
|| common::die "cannot safely resolve the block device backing active swap file $swap_device"
swap_source="$swap_mount_source"
fi
swap_source_tree="$(lsblk --inverse --list --noheadings --paths --output PATH -- "$swap_source")" \
|| common::die "cannot determine the devices backing active swap $swap_device"
while IFS= read -r swap_backing_device; do
[[ -n "$swap_backing_device" ]] || continue
if [[ "$(readlink -f -- "$swap_backing_device")" == "$(readlink -f -- "$child_device")" ]]; then
common::die "$child_device backs active swap ($swap_device); disable it before running this script"
fi
done <<< "$swap_source_tree"
done <<< "$ACTIVE_SWAP_DEVICES"
done <<< "$DEVICE_TREE"
if [[ "$DEVICE_TYPE" == "part" ]]; then
[[ $# -eq 1 || "$FILL_MODE" == "zero" || "$FILL_MODE" == "random" ]] \
|| common::die "invalid fill mode; use 'zero' or 'random'"
DEVICE_SIZE="$(lsblk -dnro SIZE --bytes -- "$DEVICE")" || common::die "cannot determine partition size"
common::log "WARNING: this will irreversibly overwrite all data on partition $DEVICE ($DEVICE_SIZE bytes)."
read -r -p "Type the exact device path '$DEVICE' to continue: " CONFIRM
[[ "$CONFIRM" == "$DEVICE" ]] || common::die "operation cancelled; no data was changed"
common::log "Writing one full $FILL_MODE pass to $DEVICE."
if [[ "$FILL_MODE" == "zero" ]]; then
dd if=/dev/zero of="$DEVICE" bs=4M count="$DEVICE_SIZE" iflag=count_bytes conv=fsync status=progress
else
dd if=/dev/urandom of="$DEVICE" bs=4M count="$DEVICE_SIZE" iflag=count_bytes conv=fsync status=progress
fi
common::log "Partition overwrite completed successfully."
exit 0
fi
[[ $# -eq 1 ]] || common::die "a fill mode is only valid for a partition"
DEVICE_SIZE="$(lsblk -dnro SIZE --bytes -- "$DEVICE")" || common::die "cannot determine disk size"
DEVICE_NAME="${DEVICE##*/}"
ERASE_METHOD=""
ERASE_TOOL=""
NVME_ACTION=""
NVME_ACTION_NAME=""
ATA_ERASE_OPTION=""
if [[ "$DEVICE_NAME" =~ ^(nvme[0-9]+)n[0-9]+$ ]]; then
common::require_cmd nvme
common::require_cmd jq
NVME_CONTROLLER="/dev/${BASH_REMATCH[1]}"
NVME_IDENTIFY="$(nvme id-ctrl "$NVME_CONTROLLER" -o json)" \
|| common::die "could not read NVMe controller capabilities"
SANICAP="$(jq -er '.sanicap' <<< "$NVME_IDENTIFY")" \
|| common::die "NVMe controller did not report SANICAP"
[[ "$SANICAP" =~ ^[0-9]+$ ]] || common::die "invalid NVMe SANICAP value: $SANICAP"
SANICAP_VALUE=$((SANICAP))
if (( SANICAP_VALUE & 1 )); then
NVME_ACTION=4
NVME_ACTION_NAME="Crypto Erase"
elif (( SANICAP_VALUE & 2 )); then
NVME_ACTION=2
NVME_ACTION_NAME="Block Erase"
elif (( SANICAP_VALUE & 4 )); then
NVME_ACTION=3
NVME_ACTION_NAME="Overwrite"
else
common::die "NVMe controller does not advertise a supported Sanitize action"
fi
ERASE_METHOD="NVMe Sanitize: $NVME_ACTION_NAME"
ERASE_TOOL="nvme"
else
common::require_cmd hdparm
common::require_cmd awk
common::require_cmd grep
ATA_IDENTIFY="$(hdparm -I "$DEVICE" 2>&1)" || common::die "could not read ATA security capabilities"
ATA_SECURITY="$(awk '
/^[[:space:]]*Security:/ { in_security = 1; print; next }
in_security && /^[^[:space:]]/ { exit }
in_security { print }
' <<< "$ATA_IDENTIFY")"
[[ -n "$ATA_SECURITY" ]] || common::die "device did not report an ATA Security section"
grep -Eiq '^[[:space:]]+supported[[:space:]]*$' <<< "$ATA_SECURITY" \
|| common::die "device does not report ATA Security Erase support"
grep -Eiq '^[[:space:]]+not frozen[[:space:]]*$' <<< "$ATA_SECURITY" \
|| common::die "ATA security is frozen or its state could not be determined; refusing to continue"
grep -Eiq '^[[:space:]]+not enabled[[:space:]]*$' <<< "$ATA_SECURITY" \
|| common::die "ATA security is already enabled; refusing to risk leaving the disk locked"
if grep -Eiq 'supported:[[:space:]]+enhanced erase' <<< "$ATA_SECURITY"; then
ATA_ERASE_OPTION="--security-erase-enhanced"
ERASE_METHOD="ATA Enhanced Security Erase"
else
ATA_ERASE_OPTION="--security-erase"
ERASE_METHOD="ATA Security Erase"
fi
ERASE_TOOL="hdparm"
fi
common::log "WARNING: whole-disk operation on $DEVICE ($DEVICE_SIZE bytes)."
common::log "Detected supported method: $ERASE_METHOD. ALL DATA on this disk will be destroyed."
read -r -p "First confirmation: type ERASE to continue: " FIRST_CONFIRM
[[ "$FIRST_CONFIRM" == "ERASE" ]] || common::die "operation cancelled; no data was changed"
common::log "Final warning: $ERASE_METHOD will erase the entire disk $DEVICE, including every partition."
read -r -p "Final confirmation: type the exact device name '$DEVICE_NAME': " FINAL_CONFIRM
[[ "$FINAL_CONFIRM" == "$DEVICE_NAME" ]] || common::die "operation cancelled; no data was changed"
if [[ "$ERASE_TOOL" == "nvme" ]]; then
common::log "Starting $ERASE_METHOD on $DEVICE. The device may continue sanitizing after the command returns."
nvme sanitize "$DEVICE" --sanact="$NVME_ACTION"
while true; do
SANITIZE_LOG="$(nvme sanitize-log "$DEVICE" -o json)" \
|| common::die "could not read NVMe Sanitize status; the operation may still be running"
SANITIZE_STATUS="$(jq -er '.sstat' <<< "$SANITIZE_LOG")" \
|| common::die "NVMe Sanitize status was missing or invalid"
[[ "$SANITIZE_STATUS" =~ ^[0-9]+$ ]] || common::die "invalid NVMe Sanitize status: $SANITIZE_STATUS"
case $((SANITIZE_STATUS & 7)) in
1|4)
common::log "NVMe Sanitize completed successfully."
break
;;
2)
common::log "NVMe Sanitize is still in progress; checking again in 30 seconds."
sleep 30
;;
3)
common::die "NVMe Sanitize reported failure; consult the device sanitize log"
;;
*)
common::die "NVMe Sanitize is not running or reported an unknown status ($SANITIZE_STATUS)"
;;
esac
done
else
common::log "Starting $ERASE_METHOD on $DEVICE. Do not interrupt power or disconnect the drive."
ATA_PASSWORD="strix-erase"
hdparm --user-master u --security-set-pass "$ATA_PASSWORD" "$DEVICE"
if ! hdparm --user-master u "$ATA_ERASE_OPTION" "$ATA_PASSWORD" "$DEVICE"; then
common::die "ATA erase failed; the disk may remain security-locked with the temporary password '$ATA_PASSWORD'"
fi
common::log "$ERASE_METHOD completed successfully."
fi