#!/usr/bin/env bash # ============================================================================ # WIPE SECURE ERASE - Linux Mint Live XFCE Client # # Served at: GET https://wipe.codez.cz/wipe # Run: curl -fsSL https://wipe.codez.cz/wipe | bash # # The "Usuario que entrega el equipo" is the OWNER of the machine whose data # is being sanitized. It is NOT the technician running this script. # ============================================================================ set -uo pipefail # --------------------------------------------------------------------------- # Configuration (filled by installer / test mode) # --------------------------------------------------------------------------- WIPE_SERVER="${WIPE_SERVER:-https://wipe.codez.cz}" # e.g. https://wipe.codez.cz WIPE_ENROLL_KEY="${WIPE_ENROLL_KEY:-3006eafa1df1a004e4689c2e32209fb4f7937cf75079c525}" # enrollment key (NOT a permanent secret) WIPE_TEST_MODE="${WIPE_TEST_MODE:-0}" PROG="WIPE SECURE ERASE" VERSION="1.9.7" # --------------------------------------------------------------------------- # Logging / display helpers # --------------------------------------------------------------------------- declare -a LOG_LINES=() log() { local msg="$*" LOG_LINES+=("[$(date '+%H:%M:%S')] $msg") } error() { echo "ERROR: $*" >&2 } die() { echo "FATAL: $*" >&2 exit 1 } say() { echo "$*" } step() { # step — phased, terminal-visible progress so the # screenshot (evidence.png) and wipe.log show every phase of the run. # NEVER pass secrets (enroll key, JWT, raw responses) to step/say. echo "[$(date '+%H:%M:%S')] [$1/$2] $3" log "[$1/$2] $3" } print_header() { echo echo "============================================================" echo " $PROG" echo "============================================================" echo } # --------------------------------------------------------------------------- # Interactive input # # The client is normally run as: # curl -fsSL https://wipe.codez.cz/wipe | bash # which means stdin is the SCRIPT pipe, not the terminal. To prompt the # technician we must read from /dev/tty when it exists (real terminal), # falling back to stdin when not (e.g. scripted tests piping input). # --------------------------------------------------------------------------- read_input() { local prompt="$1" var="$2" if [ -e /dev/tty ] && [ -r /dev/tty ] && [ -w /dev/tty ]; then read -r -p "$prompt" "$var" < /dev/tty || return 1 else read -r -p "$prompt" "$var" || return 1 fi } normalize_ml() { # Canonical ML form: "ML-" + digits (e.g. "ML-123456"). # Accepts bare numbers ("123456"), lowercase ("ml-123456"), missing # dash ("ML123456") and surrounding/inner spaces (" ml 123456 "). # Prints the canonical value; returns non-zero if not an ML code. # NEVER trim leading zeros: "00123456" stays "ML-00123456". local raw="$1" compact digits compact="$(printf '%s' "$raw" | tr -d '[:space:]' | tr '[:lower:]' '[:upper:]')" case "$compact" in ML-*) digits="${compact#ML-}" ;; ML*) digits="${compact#ML}" ;; *) digits="$compact" ;; esac digits="${digits#-}" case "$digits" in ""|*[!0-9]*) return 1 ;; esac [ "${#digits}" -gt 16 ] && return 1 printf 'ML-%s' "$digits" } # --------------------------------------------------------------------------- # Privileged execution # # Sanitization commands (nvme, hdparm, ...) and package installation need # root. Run them with sudo when the client is not root. # --------------------------------------------------------------------------- run_priv() { if [ "$(id -u)" -eq 0 ]; then "$@" else sudo "$@" fi } # Check a tool in PATH plus /sbin and /usr/sbin (some live images keep a # reduced PATH for non-login shells). have() { command -v "$1" >/dev/null 2>&1 || [ -x "/sbin/$1" ] || [ -x "/usr/sbin/$1" ] } # --------------------------------------------------------------------------- # Tools auto-install (no confirmation) # # Installs everything needed for a reliable sanitization: util-linux # (lsblk/findmnt), nvme-cli, hdparm, dmidecode (fabric serial) and a # screenshot tool. Non-interactive; safe to re-run. # --------------------------------------------------------------------------- ensure_tools() { local missing=() local t for t in lsblk findmnt dmidecode nvme hdparm smartctl pv; do have "$t" || missing+=("$t") done # Screenshot tool (only needed outside TEST mode) if [ "$WIPE_TEST_MODE" != "1" ] \ && ! have xfce4-screenshooter \ && ! have scrot \ && ! have import; then missing+=(xfce4-screenshooter) fi [ "${#missing[@]}" -eq 0 ] && return 0 say "Instalando herramientas necesarias: ${missing[*]} ..." log "Herramientas faltantes: ${missing[*]}" local apt_map=( "lsblk=util-linux" "findmnt=util-linux" "dmidecode=dmidecode" "nvme=nvme-cli" "hdparm=hdparm" "smartctl=smartmontools" "pv=pv" "xfce4-screenshooter=xfce4-screenshooter" "scrot=scrot" ) local pkgs=() bin pair apt_rc attempt=0 max_attempts=3 for bin in "${missing[@]}"; do for pair in "${apt_map[@]}"; do if [ "${pair%%=*}" = "$bin" ]; then pkgs+=("${pair#*=}") break fi done done # Retry loop with per-tool verification: some Live mirrors serve corrupt # .debs (Hash Sum mismatch) — apt-get clean purges them before retrying. # Bounded: after max_attempts the wipe continues with a clear warning. while [ "$attempt" -lt "$max_attempts" ]; do attempt=$(( attempt + 1 )) if [ "${#pkgs[@]}" -eq 0 ]; then say "AVISO: no hay paquete conocido para las herramientas faltantes; se continuará." break elif command -v apt-get >/dev/null 2>&1; then if [ "$attempt" -gt 1 ]; then say "Reintento ${attempt}/${max_attempts}: limpiando caché (apt-get clean) y actualizando..." run_priv apt-get clean >/dev/null 2>&1 || true else say "Actualizando listas de paquetes (apt-get update)..." fi run_priv env DEBIAN_FRONTEND=noninteractive apt-get update -qq 2>&1 | grep -viE 'cdrom|no tiene un fichero Release|does not have a Release file' apt_rc="${PIPESTATUS[0]}" if [ "$apt_rc" -ne 0 ]; then say "AVISO: apt-get update devolvió errores (típico: repo cdrom del Live); se intenta instalar igualmente." fi run_priv env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${pkgs[@]}" elif command -v apt >/dev/null 2>&1; then if [ "$attempt" -gt 1 ]; then say "Reintento ${attempt}/${max_attempts}: limpiando caché (apt clean) y actualizando..." run_priv apt clean >/dev/null 2>&1 || true else say "Actualizando listas de paquetes (apt update)..." fi run_priv env DEBIAN_FRONTEND=noninteractive apt update -qq 2>&1 | grep -viE 'cdrom|no tiene un fichero Release|does not have a Release file' apt_rc="${PIPESTATUS[0]}" if [ "$apt_rc" -ne 0 ]; then say "AVISO: apt update devolvió errores (típico: repo cdrom del Live); se intenta instalar igualmente." fi run_priv env DEBIAN_FRONTEND=noninteractive apt install -y -qq "${pkgs[@]}" else say "AVISO: gestor de paquetes no disponible; se continuará con las herramientas presentes." break fi # Per-tool verification: only missing tools trigger another round. local verify=() v for v in "${missing[@]}"; do have "$v" || verify+=("$v") done if [ "${#verify[@]}" -eq 0 ]; then break fi missing=("${verify[@]}") if [ "$attempt" -lt "$max_attempts" ]; then say "Aún faltan tras intento ${attempt}: ${missing[*]}; reintentando en 10s..." sleep 10 fi done local still=() for t in "${missing[@]}"; do have "$t" || still+=("$t") done if [ "${#still[@]}" -gt 0 ]; then say "AVISO: aún faltan: ${still[*]} (el resultado puede verse limitado)." log "Aún faltan herramientas: ${still[*]}" else say "Herramientas listas." log "Herramientas instaladas correctamente." fi } # --------------------------------------------------------------------------- # Suspend inhibition: a wipe can take hours (Plan C/D); the machine must not # sleep mid-run. Holds a systemd inhibitor for the whole execution plus # best-effort desktop power-manager tweaks (Live session: no restore needed, # reboot resets everything). # --------------------------------------------------------------------------- inhibit_suspend() { INHIBIT_PID="" if have systemd-inhibit; then systemd-inhibit --what=sleep:idle:handle-suspend-key:handle-lid-switch \ --who="WIPE SECURE ERASE" --why="borrado seguro en curso, no suspender" \ --mode=block sleep 86400 >/dev/null 2>&1 & INHIBIT_PID="$!" if kill -0 "$INHIBIT_PID" 2>/dev/null; then say "Anti-suspensión: inhibidor activo (suspend/sleep bloqueados durante el borrado)." log "Inhibitor PID $INHIBIT_PID" else INHIBIT_PID="" say "Anti-suspensión: systemd-inhibit no pudo activarse." fi else say "Anti-suspensión: systemd-inhibit no disponible." fi if have xfconf-query; then xfconf-query -c xfce4-power-manager -p /xfce4-power-manager/inactivity-on-ac -n -t int -s 0 >/dev/null 2>&1 || true xfconf-query -c xfce4-power-manager -p /xfce4-power-manager/inactivity-on-battery -n -t int -s 0 >/dev/null 2>&1 || true fi if have gsettings; then gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type 'nothing' >/dev/null 2>&1 || true gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-battery-type 'nothing' >/dev/null 2>&1 || true fi if have xset; then xset s off >/dev/null 2>&1 || true xset -dpms >/dev/null 2>&1 || true fi } release_suspend() { if [ -n "${INHIBIT_PID:-}" ]; then kill "$INHIBIT_PID" 2>/dev/null || true fi } # --------------------------------------------------------------------------- # Media classification: the wipe strategy depends on the drive kind. # NVMe SSD | SATA SSD | SATA HDD | desconocido. # Rationale: crypto-erase first on NVMe (instant when SED), normal # (zeros) secure erase on SATA so read-back verification holds — enhanced # erase may write vendor patterns that are not zeros. # --------------------------------------------------------------------------- media_kind() { case "$1" in NVMe) echo "NVMe SSD" ;; SATA|ATA) [ "$2" = "1" ] && echo "SATA HDD" || echo "SATA SSD" ;; *) echo "desconocido" ;; esac } media_eta_note() { case "${MEDIA_KIND:-desconocido}" in "NVMe SSD") say "Tiempo estimado: minutos (crypto/format es casi instantáneo si el firmware colabora)." ;; "SATA SSD") say "Tiempo estimado: minutos a decenas de minutos según método; no apagar." ;; "SATA HDD") say "Tiempo estimado: HORAS (secure erase ~1-3h/TB; overwrite más). No apagar." ;; *) say "Tiempo estimado: variable según método; no apagar hasta terminar." ;; esac } # --------------------------------------------------------------------------- # ATA frozen-state parsing. # hdparm -I ALWAYS prints a "frozen" line ("not frozen" or "frozen"), so a # naive grep cannot distinguish them. This helper echoes exactly one of: # frozen | not-frozen | unknown. # --------------------------------------------------------------------------- ata_frozen_state() { local sec="$1" line line="$(printf '%s\n' "$sec" | grep -i 'frozen' | head -1)" if [ -z "$line" ]; then echo "unknown" return 0 fi if printf '%s' "$line" | grep -qi 'not'; then echo "not-frozen" else echo "frozen" fi } # --------------------------------------------------------------------------- # Hardware detection # --------------------------------------------------------------------------- detect_hardware() { step "3" "7" "Detectando hardware..." log "Detectando hardware..." # Manufacturer / model from DMI local maker model svc_tag bios for f in /sys/class/dmi/id/sys_vendor /sys/devices/virtual/dmi/id/sys_vendor; do [ -r "$f" ] && maker="$(cat "$f" 2>/dev/null)" && break done [ -z "${maker:-}" ] && have dmidecode && maker="$(dmidecode -s system-manufacturer 2>/dev/null)" # Fallbacks maker="${maker:--}" model="$(cat /sys/class/dmi/id/product_name 2>/dev/null)"; model="${model:-}" [ -z "$model" ] && have dmidecode && model="$(dmidecode -s system-product-name 2>/dev/null)" model="${model:-Unknown}" # Fabric serial: prefer DMI product serial, fall back to board serial, # then dmidecode (correct -number keywords), then product UUID. # Reject known placeholders. svc_tag="" for f in /sys/class/dmi/id/product_serial /sys/devices/virtual/dmi/id/product_serial \ /sys/class/dmi/id/board_serial /sys/devices/virtual/dmi/id/board_serial; do [ -r "$f" ] || continue svc_tag="$(cat "$f" 2>/dev/null | tr -d '[:space:]')" [ -n "$svc_tag" ] && break done if [ -z "$svc_tag" ] && have dmidecode && [ "$WIPE_TEST_MODE" != "1" ]; then # dmidecode >= 3.x only accepts the "-number" keywords. # (in TEST MODE we skip the privileged dmidecode lookup) for key in system-serial-number baseboard-serial-number chassis-serial-number; do svc_tag="$(run_priv dmidecode -s "$key" 2>/dev/null | tr -d '[:space:]')" case "${svc_tag,,}" in ""|none|default*|to*be*filled*|system*serial*number|not*specified|"s0000000000") svc_tag="" ;; esac [ -n "$svc_tag" ] && break done fi case "${svc_tag,,}" in ""|none|default*|to*be*filled*|system*serial*number|not*specified|123456789|0123456789|00000000) svc_tag="" ;; esac if [ -z "$svc_tag" ]; then svc_tag="$(cat /sys/class/dmi/id/product_uuid 2>/dev/null | tr -d '[:space:]')" fi svc_tag="${svc_tag:-Unknown}" HARDWARE_MANUFACTURER="$maker" HARDWARE_MODEL="$model" HARDWARE_SERIAL="$svc_tag" log "Fabricante: $HARDWARE_MANUFACTURER" log "Modelo: $HARDWARE_MODEL" log "Serial: $HARDWARE_SERIAL" say "Equipo: $HARDWARE_MANUFACTURER $HARDWARE_MODEL (S/N $HARDWARE_SERIAL)" } # --------------------------------------------------------------------------- # Boot device detection - CRITICAL # --------------------------------------------------------------------------- detect_boot_device() { say "Detectando medio de arranque..." log "Detectando medio de arranque..." local root_dev="" root_dev="$(findmnt -no SOURCE / 2>/dev/null)" if [ -z "$root_dev" ]; then # Fallback: parse /proc/mounts root_dev="$(awk '$2==" / " {print $1; exit}' /proc/mounts 2>/dev/null)" fi # Resolve to base device, e.g. /dev/sda, /dev/nvme0n1 BOOT_BASE="" if [ -n "$root_dev" ]; then BOOT_BASE="$(lsblk -no PKNAME "$root_dev" 2>/dev/null)" # If the root device is itself a partitionless device, use it directly case "$root_dev" in /dev/nvme*[0-9]) BOOT_BASE="$BOOT_BASE" ;; # parent of nvme p1 /dev/sd*[0-9]|/dev/vd*[0-9]|/dev/mmcblk*[0-9]|/dev/hd*[0-9]) BOOT_BASE="$BOOT_BASE" ;; *) BOOT_BASE="${root_dev}" ;; esac # Normalize to /dev/X form BOOT_BASE="${BOOT_BASE#/dev/}" BOOT_BASE="/dev/$BOOT_BASE" fi log "Media de arranque (raiz): $root_dev" log "Dispositivo base de arranque: $BOOT_BASE" say "Medio Live (excluido siempre): $BOOT_BASE (raiz: $root_dev)" if [ -z "$BOOT_BASE" ]; then say "ERROR: No se pudo determinar el medio de arranque." say "ABORTANDO - no se puede garantizar la exclusión del medio Live." die "Medio de arranque no identificado." fi } # --------------------------------------------------------------------------- # Storage detection # --------------------------------------------------------------------------- detect_storage() { step "4" "7" "Detectando almacenamiento y seleccionando disco..." log "Detectando almacenamiento..." # Enumerate all disks via lsblk (top-level devices only) mapfile -t DISKS < <(lsblk -dn -o NAME,TYPE,TRAN 2>/dev/null | awk '$2=="disk" {print $1}') # For NVMe devices TYPE may be "disk" too; fall back to listing block devices if [ "${#DISKS[@]}" -eq 0 ]; then mapfile -t DISKS < <(lsblk -dn -o NAME 2>/dev/null) fi CANDIDATES=() EXCLUDED=() local dev for dev in "${DISKS[@]}"; do [ -z "$dev" ] && continue local path="/dev/$dev" local tran size model serial type # One lsblk call per attribute (single column): MODELS contain # spaces, so printing several columns together and splitting with # awk would misalign TRAN/SIZE/SERIAL. model="$(lsblk -dno MODEL "$path" 2>/dev/null)" tran="$(lsblk -dno TRAN "$path" 2>/dev/null)" size="$(lsblk -dno SIZE "$path" 2>/dev/null)" serial="$(lsblk -dno SERIAL "$path" 2>/dev/null)" # Determine transport type case "$tran" in nvme) type="NVMe" ;; sata|ata|sat) type="SATA" ;; usb|mmc|sdio) type="USB" ;; *sas*) type="SAS" ;; *) type="${tran:-Unknown}" ;; esac # Exclude boot device if [ "$path" = "$BOOT_BASE" ]; then EXCLUDED+=("$path|$model|$size|$type|BOOT") continue fi # ONLY internal SATA / NVMe are candidates. USB and any other # transport are excluded and NOT listed as candidates. case "$type" in SATA|NVMe) ;; *) EXCLUDED+=("$path|$model|$size|$type|NO INTERNO") continue ;; esac CANDIDATES+=("$path|$model|$size|$type|$serial") done # In TEST MODE, if no real candidate disk exists (e.g. only the boot # medium is present), add a clearly-virtual simulated drive so the full # flow can be exercised WITHOUT touching any real disk. if [ "$WIPE_TEST_MODE" = "1" ] && [ "${#CANDIDATES[@]}" -eq 0 ]; then say "[TEST MODE] No hay disco real que borrar: usando dispositivo simulado virtual." log "[TEST MODE] Dispositivo simulado virtual añadido como candidato." CANDIDATES+=("/dev/wipe-sim-sda|WIPE VIRTUAL TEST DISK|512 GB|NVMe|WIPE-TEST-SERIAL") fi } # --------------------------------------------------------------------------- # Disc selection # --------------------------------------------------------------------------- show_and_select_disk() { echo echo "DISPOSITIVOS DETECTADOS" echo "------------------------------------------------------------" local i=1 local entry for entry in "${CANDIDATES[@]}"; do [ -z "$entry" ] && continue IFS='|' read -r d m s t ser <<<"$entry" echo "$i. $d" echo " Modelo: $m" echo " Serial: $ser" echo " Capacidad: $s" echo " Tipo: $t" echo " CANDIDATO" echo i=$((i+1)) done # Show excluded devices for clarity if [ "${#EXCLUDED[@]}" -gt 0 ]; then echo "EXCLUIDOS (NO CANDIDATOS)" for entry in "${EXCLUDED[@]}"; do [ -z "$entry" ] && continue IFS='|' read -r d m s t reason <<<"$entry" echo " $d $m ($s) $t - EXCLUIDO - $reason" done echo fi # If no candidates if [ "${#CANDIDATES[@]}" -eq 0 ]; then echo "ERROR: No se encontraron discos candidatos (todos excluidos o ninguno detectado)." log "No se encontraron discos candidatos." die "Sin discos candidatos." fi # If more than one internal disk, require exact selection local selection_idx=1 if [ "${#CANDIDATES[@]}" -gt 1 ]; then echo "Se detectaron ${#CANDIDATES[@]} discos internos." echo "Seleccione el número del disco a borrar:" read_input "> " selection_idx if ! [[ "$selection_idx" =~ ^[0-9]+$ ]] || \ [ "$selection_idx" -lt 1 ] || [ "$selection_idx" -gt "${#CANDIDATES[@]}" ]; then die "Selección inválida. Abortando por seguridad." fi else echo "Un solo disco interno. Se procederá con el disco 1." fi local sel="${CANDIDATES[$((selection_idx-1))]}" IFS='|' read -r SEL_DEV SEL_MODEL SEL_SIZE SEL_TYPE SEL_SERIAL <<<"$sel" log "Dispositivo seleccionado: $SEL_DEV" log "Tipo: $SEL_TYPE" log "Capacidad: $SEL_SIZE" log "Serial: $SEL_SERIAL" say "Seleccionado: $SEL_DEV ($SEL_TYPE, $SEL_SIZE, S/N $SEL_SERIAL)" } # --------------------------------------------------------------------------- # Storage method selection + sanitization capabilities # --------------------------------------------------------------------------- detect_capabilities() { local dev="$1" CAP_METHOD="" CAP_CMDS="" CAP_AVAILABLE=() # TEST MODE: report plausible NVMe capabilities for the virtual device. if [ "$WIPE_TEST_MODE" = "1" ] && [ "${dev#/dev/wipe-sim}" != "$dev" ]; then CAP_AVAILABLE=("nvme-cli" "nvme-format-user-data") CAP_METHOD="NVMe Format (User Data Erase)" CAP_METHOD_CMD="nvme format --ses=1 (simulado)" log "Capacidades detectadas (TEST): ${CAP_AVAILABLE[*]}" log "Método seleccionado (TEST): $CAP_METHOD" return 0 fi if [ "$SEL_TYPE" = "NVMe" ] || ([ -n "$dev" ] && [ "${dev#/dev/nvme}" != "$dev" ]); then if have nvme; then # NVMe device: User Data Erase via nvme format --ses=1 CAP_AVAILABLE+=("nvme-cli") if run_priv nvme id-ctrl "$dev" >/dev/null 2>&1; then CAP_AVAILABLE+=("nvme-id-ctrl") fi CAP_AVAILABLE+=("nvme-format-user-data") else CAP_AVAILABLE+=("none-nvme-cli-missing") fi elif [ "$SEL_TYPE" = "SATA" ] || [ "$SEL_TYPE" = "ATA" ]; then if have hdparm; then CAP_AVAILABLE+=("hdparm") # Check ATA security local sec sec="$(run_priv hdparm -I "$dev" 2>/dev/null)" if echo "$sec" | grep -qi "Security:"; then case "$(ata_frozen_state "$sec")" in frozen) CAP_AVAILABLE+=("ata-frozen") ;; not-frozen) CAP_AVAILABLE+=("ata-not-frozen") ;; esac if echo "$sec" | grep -qi "supported"; then CAP_AVAILABLE+=("ata-security-supported") fi fi if echo "$sec" | grep -qi "Sanitize"; then CAP_AVAILABLE+=("ata-sanitize") fi else CAP_AVAILABLE+=("none-hdparm-missing") fi else # USB / unknown / other CAP_AVAILABLE+=("none-generic-usb") fi # Decide method if [ "$SEL_TYPE" = "NVMe" ]; then if printf '%s\n' "${CAP_AVAILABLE[@]}" | grep -q "nvme-format-user-data"; then CAP_METHOD="NVMe Format (User Data Erase)" CAP_METHOD_CMD="nvme format --force --ses=1" else CAP_METHOD="UNSUPPORTED" fi elif [ "$SEL_TYPE" = "SATA" ] || [ "$SEL_TYPE" = "ATA" ]; then if printf '%s\n' "${CAP_AVAILABLE[@]}" | grep -q "ata-security-supported"; then CAP_METHOD="ATA Secure Erase" CAP_METHOD_CMD="hdparm --user-master u --security-set-pass NULL && hdparm --user-master u --security-erase NULL " elif printf '%s\n' "${CAP_AVAILABLE[@]}" | grep -q "ata-sanitize"; then CAP_METHOD="ATA Sanitize" CAP_METHOD_CMD="hdparm --yes-i-know-what-i-am-doing --sanitize-block-erase " else CAP_METHOD="UNSUPPORTED" fi else CAP_METHOD="UNSUPPORTED" fi log "Capacidades detectadas: ${CAP_AVAILABLE[*]}" log "Método seleccionado: $CAP_METHOD" say "Capacidades: ${CAP_AVAILABLE[*]}" say "Método: $CAP_METHOD" say "Comando técnico: ${CAP_METHOD_CMD:-N/A}" } # --------------------------------------------------------------------------- # NVMe deep diagnostics (read-only): controller/namespace capabilities, # current sanitize state (a stuck sanitize aborts new commands with # Command Sequence Error), NVMe smart log and recent error entries. # Printed to terminal (photo + wipe.log). Best effort, capped in size. # --------------------------------------------------------------------------- collect_nvme_diagnostics() { local dev="$1" out have nvme || { say "NVMe diagnóstico: nvme-cli no disponible"; return 0; } # Same non-interactive rule as above: diagnostics never block on sudo. local -a _priv=() [ "$(id -u)" -ne 0 ] && _priv=(sudo -n) say "Diagnóstico NVMe de $dev (solo lectura):" out="$("${_priv[@]}" nvme list 2>/dev/null | head -12 || true)" if [ -n "$out" ]; then say "nvme list:"; printf '%s\n' "$out" | sed 's/^/ /'; else say " (sin datos nvme list)"; fi say "id-ctrl -H (completo):" out="$("${_priv[@]}" nvme id-ctrl "$dev" -H 2>/dev/null | head -150 || true)" if [ -n "$out" ]; then printf '%s\n' "$out" | sed 's/^/ /'; else say " (sin datos id-ctrl)"; fi say "id-ns $dev:" out="$("${_priv[@]}" nvme id-ns "$dev" 2>/dev/null | head -25 || true)" if [ -n "$out" ]; then printf '%s\n' "$out" | sed 's/^/ /'; else say " (sin datos id-ns)"; fi say "sanitize-log (estado actual):" out="$("${_priv[@]}" nvme sanitize-log "$dev" 2>/dev/null | head -15 || true)" if [ -n "$out" ]; then printf '%s\n' "$out" | sed 's/^/ /'; else say " (sin datos sanitize-log)"; fi # Explicit verdict: is a sanitize running RIGHT NOW? A busy controller # answers everything with 0x0C and reads may abort — must know first. case "$(nvme_sanitize_status "$dev" "$out")" in 1) say "Estado sanitize: EN CURSO (se esperará antes de intentar nada)." ;; 2) say "Estado sanitize: completado anteriormente." ;; 0) say "Estado sanitize: nunca ejecutado." ;; *) say "Estado sanitize: desconocido." ;; esac say "smart-log NVMe (completo):" out="$("${_priv[@]}" nvme smart-log "$dev" 2>/dev/null | head -40 || true)" if [ -n "$out" ]; then printf '%s\n' "$out" | sed 's/^/ /'; else say " (sin datos smart-log)"; fi say "error-log (últimas 4 entradas):" out="$("${_priv[@]}" nvme error-log "$dev" --log-entries=4 2>/dev/null | head -60 || true)" if [ -n "$out" ]; then printf '%s\n' "$out" | sed 's/^/ /'; else say " (sin datos error-log)"; fi log "Diagnóstico NVMe de $dev registrado." } # --------------------------------------------------------------------------- # Drive diagnostics (read-only): SATA link speed, recent kernel errors and # SMART. Printed to terminal (photo + wipe.log) and stored in the evidence. # Never modifies media. Best effort: missing tools/data degrade to notes. # --------------------------------------------------------------------------- collect_drive_diagnostics() { SATA_LINK="" SMART_HEALTH="" SMART_ATTRS="" # Non-interactive privilege for diagnostics only: diagnostics must NEVER # block the wipe on a sudo password prompt (sudo -n fails fast). local -a _priv=() [ "$(id -u)" -ne 0 ] && _priv=(sudo -n) say "Diagnóstico de $SEL_DEV (solo lectura):" # 1. SATA link speed. local spd spd="$(cat /sys/class/ata_link/link*/sata_spd 2>/dev/null | grep -ioE '[0-9.]+ *Gbps' | head -1)" if [ -z "$spd" ]; then spd="$("${_priv[@]}" dmesg 2>/dev/null | grep -ioE 'SATA link up [0-9.]+ Gbps[^,]*' | head -1 || true)" fi SATA_LINK="${spd:-desconocido}" say "Enlace SATA: $SATA_LINK" # 2. Recent kernel ATA/disk errors. say "Errores recientes del kernel (dmesg):" local derr derr="$("${_priv[@]}" dmesg 2>/dev/null | grep -iE 'ata[0-9]+|sd[a-z]|I/O error|failed|reset|retry|UNC|medium access' | tail -8 || true)" if [ -n "$derr" ]; then printf '%s\n' "$derr" | sed 's/^/ /' else say " (ninguno)" fi # 3. SMART (best effort). if have smartctl; then local hout hout_full aout iout hout_full="$("${_priv[@]}" smartctl -H "$SEL_DEV" 2>&1 || true)" hout="$(printf '%s\n' "$hout_full" | grep -i 'test result' | head -1 || true)" if printf '%s' "$hout" | grep -qi 'PASSED'; then SMART_HEALTH="PASSED" elif printf '%s' "$hout" | grep -qiE 'FAILED|FAILURE'; then SMART_HEALTH="FAILED" elif printf '%s' "$hout" | grep -qi 'UNKNOWN'; then SMART_HEALTH="UNKNOWN (el disco no informa salud)" else SMART_HEALTH="desconocido" fi say "SMART salud: $SMART_HEALTH" if [ "$SMART_HEALTH" = "desconocido" ]; then # Forensics: show the raw output so the cause is in the log. printf '%s\n' "$hout_full" | head -6 | sed 's/^/ smartctl: /' fi # Identity (model/firmware/serial as the drive reports them). iout="$("${_priv[@]}" smartctl -i "$SEL_DEV" 2>/dev/null | grep -iE '^(Model|Firmware|Serial|Capacity|Sector Size|Device Model)' | head -8 || true)" if [ -n "$iout" ]; then printf '%s\n' "$iout" | sed 's/^/ /' # Fill Firmware when lsblk could not provide it. if [ -z "${FIRMWARE:-}" ] || [ "$FIRMWARE" = "N/A" ]; then FIRMWARE="$(printf '%s\n' "$iout" | grep -i 'Firmware' | head -1 | sed 's/^[^:]*:[[:space:]]*//')" fi fi aout="$("${_priv[@]}" smartctl -A "$SEL_DEV" 2>/dev/null | grep -iE 'Reallocated|Pending|Uncorrectable|Wear_Leveling|Percent_Lifetime|Temperature_C|CRC_Error|UDMA_CRC|Power_On|Media_Wearout|Spin_Up_Time|Spin_Retry|Seek_Error|Calibration_Retry|Load_Cycle|Start_Stop|Power-Off_Retract' | head -14 || true)" if [ -n "$aout" ]; then printf '%s\n' "$aout" | sed 's/^/ /' SMART_ATTRS="$(printf '%s' "$aout" | tr '\n' ';')" fi else SMART_HEALTH="smartctl no disponible" say "SMART: smartctl no disponible" fi log "Diagnóstico: SATA=$SATA_LINK SMART=$SMART_HEALTH" # 4. ATA sanitize state (best effort; hdparm output varies by version). # Informational only: unlike NVMe, hdparm sanitize blocks, so no # previous-run state is expected after a reboot. if { [ "$SEL_TYPE" = "SATA" ] || [ "$SEL_TYPE" = "ATA" ]; } && have hdparm; then local sastat sastat="$("${_priv[@]}" hdparm --sanitize-status "$SEL_DEV" 2>&1 || true)" if [ -z "$sastat" ]; then say "Estado sanitize ATA: desconocido." elif printf '%s' "$sastat" | grep -qiE 'in progress|running'; then say "Estado sanitize ATA: EN CURSO." elif printf '%s' "$sastat" | grep -qiE 'idle|complet|success|never|not .*start|no sanitize'; then say "Estado sanitize ATA: inactivo." else say "Estado sanitize ATA: desconocido." printf '%s\n' "$sastat" | head -5 | sed 's/^/ sanitize-status: /' fi fi # 4. NVMe deep diagnostics when applicable (capabilities, sanitize # state, smart-log, error log) — key to diagnose firmware rejects. if [ "$SEL_TYPE" = "NVMe" ]; then collect_nvme_diagnostics "$SEL_DEV" fi } # --------------------------------------------------------------------------- # Device prep: unmount any mounted partitions of the target device # (a mounted namespace makes nvme format / ATA security fail with EBUSY). # --------------------------------------------------------------------------- unmount_partitions() { local dev="$1" local mps=() mapfile -t mps < <(lsblk -nro MOUNTPOINT "$dev" 2>/dev/null | grep -v '^$') [ "${#mps[@]}" -eq 0 ] && return 0 local mp for mp in "${mps[@]}"; do if run_priv umount "$mp" 2>/dev/null; then say "Desmontando partición $mp (en uso por la sesión)." log "Partición $mp desmontada antes de sanitizar." else say "AVISO: no se pudo desmontar $mp; el borrado puede fallar." log "AVISO: no se pudo desmontar $mp." fi done } # --------------------------------------------------------------------------- # Poll NVMe sanitize until it finishes or a timeout elapses. # sanitize-log SSTATUS 0x0 means success. Returns 0 on success. # --------------------------------------------------------------------------- nvme_sanitize_status() { # Echoes sanitize state from `nvme sanitize-log`: 0 never, 1 in # progress, 2 completed OK, 3 failed, unknown (unreadable). # Supports nvme-cli 2.8 ("Sanitize Status (SSTAT) : 0x101") and older # SSTATUS formats. local dev="$1" log_out="${2:-}" hex st if [ -z "$log_out" ]; then log_out="$(run_priv nvme sanitize-log "$dev" 2>/dev/null || true)" fi hex="$(printf '%s\n' "$log_out" | grep -ioE 'SSTAT[^0-9a-f]*0x[0-9a-f]+' | grep -oE '0x[0-9a-fA-F]+' | head -1)" if [ -z "$hex" ]; then hex="$(printf '%s\n' "$log_out" | grep -i 'SSTAT' | sed -n 's/.*SSTAT[^0-9a-fA-F]*\([0-9a-fA-F]\+\).*/0x\1/pI' | head -1)" fi if [ -z "$hex" ]; then hex="$(printf '%s\n' "$log_out" | grep -i 'SSTATUS' | sed -n 's/.*0x0*\([0-9a-fA-F]\+\).*/0x\1/p' | head -1)" fi if ! [[ "$hex" =~ ^0x[0-9a-fA-F]+$ ]]; then echo "unknown" return 0 fi st=$(( 16#${hex#0x} & 7 )) case "$st" in 0) echo "0" ;; 1) echo "1" ;; 2) echo "2" ;; *) echo "3" ;; esac } nvme_sanitize_wait() { local dev="$1" timeout_s="${2:-600}" local deadline="$(( $(date +%s) + timeout_s ))" local st="" say "Esperando sanitize (máx ${timeout_s}s)..." while [ "$(date +%s)" -lt "$deadline" ]; do st="$(nvme_sanitize_status "$dev")" case "$st" in 2) say "Sanitize completado [OK]"; return 0 ;; 1|0) sleep 5 ;; # in progress (or just started) 3) SANITIZE_DETAIL="Sanitize terminó con fallo"; say "Sanitize terminó con fallo."; return 1 ;; *) SANITIZE_DETAIL="No se pudo leer estado del sanitize"; say "No se pudo leer estado del sanitize."; return 1 ;; esac done SANITIZE_DETAIL="Timeout esperando sanitize ($timeout_s s)" say "Timeout esperando sanitize." return 1 } nvme_sanitize_precheck() { # If a sanitize is already in progress (e.g. started by a previous # run and still going), wait for it: hammering a busy controller only # produces 0x0C cascades and unreadable media. local dev="$1" st st="$(nvme_sanitize_status "$dev")" if [ "$st" = "1" ]; then say "Sanitize en curso detectado (¿corrida previa?) — esperando hasta 5min antes de intentar nada..." if nvme_sanitize_wait "$dev" 300; then say "Sanitize previo completado. Se continúa con el flujo normal (verificará el resultado)." else say "AVISO: sanitize sigue en curso tras la espera; se intenta igualmente (puede fallar con 0x0C o ir al Plan D)." fi fi } # --------------------------------------------------------------------------- # NVMe format helpers. # - nvme_format_once: runs format with stdin detached (/dev/null) so a stray # prompt can never hang on the script pipe; --force skips confirmation. # - nvme_format_retry: one extra attempt after 15s on Command Sequence Error # (0xc), which smells like transient controller state (manual runs of the # identical command succeed). Bounded: at most 2 tries per invocation. # --------------------------------------------------------------------------- nvme_format_once() { local dev="$1" shift run_priv nvme format "$dev" "$@" --force < /dev/null 2>&1 } nvme_format_retry() { local dev="$1" shift local _out _rc=1 _out="$(nvme_format_once "$dev" "$@")" _rc=$? if [ "$_rc" -ne 0 ] && printf '%s' "$_out" | grep -qi 'Command Sequence Error'; then # NOTE: to stderr on purpose — this function runs inside $(...), # so stdout would pollute the captured firmware output. echo "NVMe 0x0C (¿estado transitorio?): reintento en 15s..." >&2 log "Reintento de format tras 0x0C." sleep 15 _out="$(nvme_format_once "$dev" "$@")" _rc=$? fi printf '%s' "$_out" return "$_rc" } # --------------------------------------------------------------------------- # Post-wipe verification (what makes the erase certifiable) # # Fail-closed: a sanitize that does not pass verification is downgraded to # FAILED, so an unverified erase can never be certified as SUCCESS. # Checks: # 1. ATA security status re-read (password must be cleared: "not enabled"). # 2. Multi-point read-back sampling (0/50/100% of device, 100 MiB each) # must read as zeros (normal-erase semantics). # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # Post-wipe verification ladder (what makes the erase certifiable). # # A read-back can show three erased states, all acceptable: # zeros — block/user-data erase (or TRIM deallocate reads-as-zero). # ones — erased NAND state (0xFF) some firmwares leave behind. # random — crypto-erase output (keys discarded): uniform, high-entropy, # with NO recognizable on-disk structures. # Anything else (low-entropy data, known filesystem/container magics at # offset 0) means the media still holds data -> FAILED (fail-closed). # Known limitation, recorded in the evidence detail: user data that is # itself uniformly random (movies, archives) at mid/end points is # indistinguishable from crypto output; offset-0 structures (partition # tables, superblocks, LUKS headers) are the decisive discriminator and # are always checked. # --------------------------------------------------------------------------- fetch_sample() { # fetch_sample — dd a sample to tmp. # Tries direct read first (no sudo involved), then escalates; records # the reason in _FETCH_ERR. Fail-closed on short reads and I/O errors. local dev="$1" mb="$2" skip_mb="$3" tmp="$4" expected derr actual local dd_args=(bs=1M "count=$mb" status=none "of=$tmp") _FETCH_ERR="" if [ -n "$skip_mb" ] && [ "$skip_mb" != "0" ]; then dd_args+=(skip="$skip_mb") fi if derr="$(dd "if=$dev" "${dd_args[@]}" 2>&1)"; then : else local derr_direct="$derr" derr_sudo="" if derr_sudo="$(run_priv dd "if=$dev" "${dd_args[@]}" 2>&1)"; then log "Muestreo con sudo (lectura directa falló: $(printf '%s\n' "$derr_direct" | head -1))." else _FETCH_ERR="dd directo: $(printf '%s\n' "$derr_direct" | head -1) | sudo: $(printf '%s\n' "$derr_sudo" | head -1)" rm -f "$tmp" return 1 fi fi expected=$(( mb * 1048576 )) actual="$(stat -c%s "$tmp" 2>/dev/null || echo -1)" if [ "$actual" != "$expected" ]; then _FETCH_ERR="lectura corta: ${actual}B de ${expected}B" rm -f "$tmp" return 1 fi return 0 } classify_sample() { # classify_sample — echoes: zeros | ones | random | other. # random = all 256 byte values present with none above 1% in a 10 MiB # probe (uniform output typical of crypto-erase, not proof by itself). local f="$1" sz stats distinct maxfreq n sz="$(stat -c%s "$f" 2>/dev/null || echo 0)" if ! [[ "$sz" =~ ^[0-9]+$ ]] || [ "$sz" -eq 0 ]; then echo "other" return 0 fi if head -c "$sz" /dev/zero 2>/dev/null | cmp -s - "$f"; then echo "zeros" return 0 fi if head -c "$sz" /dev/zero 2>/dev/null | tr '\0' '\377' | cmp -s - "$f"; then echo "ones" return 0 fi stats="$(head -c 10485760 "$f" 2>/dev/null | od -A n -t u1 -v 2>/dev/null | awk '{for(i=1;i<=NF;i++){c[$i]++;n++}} END{d=0;m=0;for(k in c){d++;if(c[k]>m)m=c[k]}; printf "%d %d %d", d+0, m+0, n+0}')" distinct="${stats%% *}" n="${stats##* }" maxfreq="$(printf '%s' "${stats#* }" | cut -d' ' -f1)" if [ "${distinct:-0}" -ge 250 ] && [ "${n:-0}" -gt 0 ] \ && [ $(( maxfreq * 100 / n )) -lt 1 ]; then echo "random" else echo "other" fi } scan_structures() { # scan_structures — echoes the matched structure or empty. # Only meaningful at device offset 0 (partition tables, superblocks, # headers). Anchored short magics (offset 0) + long strings anywhere in # the first 2 MiB; false-positive rate on uniform data is negligible. local f="$1" head2m hex0 sig mag head2m="$(mktemp)" || return 1 head -c 2097152 "$f" > "$head2m" 2>/dev/null sig="$(dd if="$f" bs=1 skip=510 count=2 status=none 2>/dev/null | od -A n -t x1 -v | tr -d ' \n')" if [ "$sig" = "55aa" ]; then echo "MBR-boot-signature" rm -f "$head2m" return 0 fi sig="$(dd if="$f" bs=1 skip=1022 count=2 status=none 2>/dev/null | od -A n -t x1 -v | tr -d ' \n')" if [ "$sig" = "55aa" ]; then echo "GPT-protective-MBR" rm -f "$head2m" return 0 fi hex0="$(head -c 64 "$head2m" 2>/dev/null | od -A n -t x1 -v | tr -d ' \n')" case "$hex0" in ffd8ff*|89504e47*|47494638*|25504446*|504b0304*|52617221*|377abcaf271c*|494433*|424d*|52494646*|1a45dfa3*|4c554b53babe*|58465342*|5f4248525f66534d*|4d53444f532e30*) echo "container-magic@${hex0:0:8}" rm -f "$head2m" return 0 ;; esac mag="$(LC_ALL=C grep -a -m1 -F -e 'EFI PART' -e 'NTFS ' -e 'SWAPSPACE2' -e 'hsqs' -e 'FAT32' -e 'FAT16' -e 'FAT12' -e '-FVE-FS-' -e 'LVM2' -e 'VMFS' "$head2m" 2>/dev/null | tr -d '\000' | head -1)" if [ -n "$mag" ]; then echo "fs-magic:$(printf '%s' "$mag" | head -c 24 | tr -cd '[:alnum:] _+-')" rm -f "$head2m" return 0 fi rm -f "$head2m" return 1 } sample_points_once() { # One full 3-point pass. Sets _LADDER_OK (1/0) and _LADDER_DETAIL. local _pt=0 _skip _cls _mag _tmp _LADDER_OK=1 _LADDER_DETAIL="" # Sample files live in WORKDIR (known-writable), not system /tmp: # on some Live sessions root cannot open mktemp files elsewhere. local _tmpdir="${WORKDIR:-/tmp}" _tmp="$(mktemp "$_tmpdir/wipe-sample-XXXXXX")" || { _LADDER_OK=0; _LADDER_DETAIL="no se pudo crear temporal"; return 1; } for _skip in 0 "$SAMPLE_MID" "$SAMPLE_END"; do _pt=$((_pt + 1)) say "Muestreo [${_pt}/3]: ${VERIFY_SAMPLE_MB} MiB en offset ${_skip} MiB ..." _cls="" if ! fetch_sample "$SEL_DEV" "$VERIFY_SAMPLE_MB" "$_skip" "$_tmp"; then say "Muestreo [${_pt}/3]: ERROR DE LECTURA (${_FETCH_ERR:-sin detalle}) [FAIL]" _LADDER_OK=0 _LADDER_DETAIL="error de lectura en punto ${_pt} (${_FETCH_ERR:-sin detalle})" break fi _cls="$(classify_sample "$_tmp")" say "Muestreo [${_pt}/3]: ${_cls}" case "$_cls" in zeros|ones) _LADDER_DETAIL="${_LADDER_DETAIL:+$_LADDER_DETAIL }p${_pt}:${_cls}" ;; random) if [ "$_skip" = "0" ]; then if _mag="$(scan_structures "$_tmp")" && [ -n "$_mag" ]; then say "Muestreo [${_pt}/3]: ESTRUCTURA intacta (${_mag}) [FAIL]" _LADDER_OK=0 _LADDER_DETAIL="estructura intacta en offset 0: ${_mag}" break fi fi _LADDER_DETAIL="${_LADDER_DETAIL:+$_LADDER_DETAIL }p${_pt}:crypto-pattern" ;; *) say "Muestreo [${_pt}/3]: NO CONCLUYENTE [FAIL]" _LADDER_OK=0 _LADDER_DETAIL="punto ${_pt} no concluyente (ni ceros/unos ni uniforme)" break ;; esac done rm -f "$_tmp" [ "$_LADDER_OK" = "1" ] } verify_sanitize() { # verify_sanitize [force] — with "force", verify even when the current # result is not SUCCESS (used by the sanitize fallback after re-running). local force="${1:-}" VERIFY_RESULT="" VERIFY_DETAIL="" VERIFY_SAMPLE_MB="100" if [ "$WIPE_TEST_MODE" = "1" ]; then VERIFY_RESULT="PASSED" VERIFY_DETAIL="Simulated verification" say "Verificación post-borrado: PASSED (simulado)" return 0 fi if [ "$SANITIZE_RESULT" != "SUCCESS" ] && [ "$force" != "force" ]; then VERIFY_RESULT="SKIPPED" VERIFY_DETAIL="No successful sanitize to verify" say "Verificación post-borrado: SKIPPED (sin sanitización exitosa)" return 0 fi step "5" "7" "Verificación post-borrado de $SEL_DEV..." # 1. Re-read ATA security status when applicable. if [ "$SEL_TYPE" = "SATA" ] || [ "$SEL_TYPE" = "ATA" ]; then local sec sec="$(run_priv hdparm -I "$SEL_DEV" 2>/dev/null | grep -i -A9 'Security:' || true)" if [ -n "$sec" ]; then if echo "$sec" | grep -qiE 'not[[:space:]]+enabled'; then say "Seguridad ATA: not enabled (password limpiada) [OK]" else VERIFY_RESULT="FAILED" VERIFY_DETAIL="ATA security sigue activa tras el borrado" say "Seguridad ATA: sigue activa [FAIL]" fi else say "Seguridad ATA: sin datos (se omite esta comprobación)" fi fi # 2. Read-back sampling at 3 points: start, middle, end of device. # A single wiped-looking prefix is not enough evidence on SSDs (FTL). # Each point goes through the ladder: zeros/ones (erased), random # without offset-0 structures (crypto-erase output), else FAILED. if [ "${VERIFY_RESULT:-}" != "FAILED" ]; then VERIFY_COVERAGE="3x${VERIFY_SAMPLE_MB}MiB@0/50/100%+magics@0" local total_mb=0 mid_skip=0 end_skip=0 if [[ "${CAPACITY_BYTES:-}" =~ ^[0-9]+$ ]] \ && [ "$CAPACITY_BYTES" -gt $(( VERIFY_SAMPLE_MB * 1048576 * 2 )) ]; then total_mb=$(( CAPACITY_BYTES / 1048576 )) mid_skip=$(( total_mb / 2 - VERIFY_SAMPLE_MB / 2 )) end_skip=$(( total_mb - VERIFY_SAMPLE_MB )) [ "$mid_skip" -lt 0 ] && mid_skip=0 [ "$end_skip" -lt 0 ] && end_skip=0 fi SAMPLE_MID="$mid_skip" SAMPLE_END="$end_skip" if sample_points_once; then VERIFY_RESULT="PASSED" VERIFY_DETAIL="ladder OK [${_LADDER_DETAIL}]" say "Muestreo: evidencia de borrado [OK]" else # One delayed re-check: some firmwares return from format/sanitize # while the erase still runs in background. Wait until 60s after # the sanitize ended and sample once more before failing. # (Skipped in TEST mode and when the sanitize itself took >60s.) local _now_ms _san_end_ms _wait_s _now_ms="$(date +%s%3N)" _san_end_ms=$(( SAN_START_MS + ${SANITIZE_DURATION_MS:-0} )) if [ "$WIPE_TEST_MODE" != "1" ] \ && [ "${SANITIZE_DURATION_MS:-0}" -lt 60000 ] \ && [ "$_now_ms" -lt "$((_san_end_ms + 60000))" ]; then _wait_s=$(( (_san_end_ms + 60000 - _now_ms + 999) / 1000 )) say "Muestreo: ${_LADDER_DETAIL} [FAIL] — reintento en ${_wait_s}s por si el firmware aún trabaja en segundo plano..." sleep "$_wait_s" if sample_points_once; then VERIFY_RESULT="PASSED" VERIFY_DETAIL="ladder OK en re-muestreo (+60s) [${_LADDER_DETAIL}]" say "Re-muestreo: evidencia de borrado [OK]" else VERIFY_RESULT="FAILED" VERIFY_DETAIL="${_LADDER_DETAIL} (confirmado en re-muestreo)" say "Muestreo: ${_LADDER_DETAIL} [FAIL] (confirmado)" fi else VERIFY_RESULT="FAILED" VERIFY_DETAIL="$_LADDER_DETAIL" say "Muestreo: ${_LADDER_DETAIL} [FAIL]" fi fi fi log "Verificación: $VERIFY_RESULT - $VERIFY_DETAIL" if [ "$VERIFY_RESULT" = "FAILED" ]; then SANITIZE_RESULT="FAILED" SANITIZE_EXIT="1" SANITIZE_FINAL="VERIFICATION FAILED" SANITIZE_DETAIL="${SANITIZE_DETAIL:+$SANITIZE_DETAIL | }Verificación post-borrado: $VERIFY_DETAIL" say "Resultado final: FAILED (verificación post-borrado)" return 1 fi say "Verificación post-borrado: PASSED" return 0 } # --------------------------------------------------------------------------- # ATA fallback chain: when Secure Erase did not verify (or was skipped as # frozen), try in order before giving up: # Plan B: sanitize block erase (fast, firmware-dependent) # Plan C: sanitize overwrite (slow pattern write, ~1h/240GB, real media I/O) # Plan D: labeled dd overwrite (LAST resort, explicitly non-native) # Every plan re-runs verification; only a PASSED verification yields SUCCESS. # Runs in main flow, after the first verification. # --------------------------------------------------------------------------- ata_fallback_chain() { if [ "$SANITIZE_RESULT" = "SUCCESS" ]; then return 0 fi case "$CAP_METHOD" in "ATA Secure Erase") if printf '%s\n' "${CAP_AVAILABLE[@]}" | grep -qx "ata-sanitize"; then ata_plan_b_block_erase ata_plan_c_overwrite fi ;; NVMe*) # Sanitize already attempted inline in execute_sanitize; # only the labeled dd (Plan D) remains below. : ;; *) return 0 ;; esac ata_plan_d_dd } ata_plan_b_block_erase() { if [ "$SANITIZE_RESULT" = "SUCCESS" ]; then return 0 fi echo step "5" "7" "Plan B: ATA Sanitize Block Erase en $SEL_DEV..." CAP_METHOD="ATA Sanitize (Block Erase)" CAP_METHOD_CMD="hdparm --yes-i-know-what-i-am-doing --sanitize-block-erase $SEL_DEV" log "Fallback a: $CAP_METHOD_CMD" unmount_partitions "$SEL_DEV" say "Sanitize block erase en curso (puede tardar varios minutos, NO apagar el equipo)..." local out end_epoch end_epoch_ms sanitize_err if out="$(run_priv hdparm --yes-i-know-what-i-am-doing --sanitize-block-erase "$SEL_DEV" 2>&1)"; then say "Sanitize aceptado por el firmware (exit 0). Verificando..." end_epoch="$(date +%s)" end_epoch_ms="$(date +%s%3N)" SANITIZE_DURATION="$(( end_epoch - SAN_START_S ))" SANITIZE_DURATION_MS="$(( end_epoch_ms - SAN_START_MS ))" say "Duración total: ${SANITIZE_DURATION_MS}ms" SANITIZE_RESULT="SUCCESS" SANITIZE_EXIT="0" SANITIZE_FINAL="" if verify_sanitize force; then SANITIZE_DETAIL="Plan B verificado: sanitize block erase + verificación PASSED (secure erase previo: ${SANITIZE_DETAIL:-sin detalle})" say "Resultado final: SUCCESS ($CAP_METHOD, verificado)" fi else sanitize_err="$(printf '%s\n' "$out" | head -1)" SANITIZE_DETAIL="${SANITIZE_DETAIL:+$SANITIZE_DETAIL | }Plan B sanitize falló: ${sanitize_err:-sin salida}" say "Plan B sanitize falló: ${sanitize_err:-sin salida}" say "Resultado parcial: FAILED (se intenta Plan C)." log "Fallback sanitize block erase falló." fi } ata_plan_c_overwrite() { if [ "$SANITIZE_RESULT" = "SUCCESS" ]; then return 0 fi echo step "5" "7" "Plan C: ATA Sanitize Overwrite en $SEL_DEV..." CAP_METHOD="ATA Sanitize (Overwrite)" CAP_METHOD_CMD="hdparm --yes-i-know-what-i-am-doing --sanitize-overwrite-passes 1 --sanitize-overwrite hex:00000000 $SEL_DEV" log "Fallback a: $CAP_METHOD_CMD" unmount_partitions "$SEL_DEV" say "Overwrite de ceros en TODA el área de usuario, 1 pasada (lento: puede tardar ~1h en 240GB, NO apagar el equipo)..." local out end_epoch end_epoch_ms sanitize_err if out="$(run_priv hdparm --yes-i-know-what-i-am-doing --sanitize-overwrite-passes 1 --sanitize-overwrite hex:00000000 "$SEL_DEV" 2>&1)"; then say "Overwrite aceptado por el firmware (exit 0). Verificando..." end_epoch="$(date +%s)" end_epoch_ms="$(date +%s%3N)" SANITIZE_DURATION="$(( end_epoch - SAN_START_S ))" SANITIZE_DURATION_MS="$(( end_epoch_ms - SAN_START_MS ))" say "Duración total: ${SANITIZE_DURATION_MS}ms" SANITIZE_RESULT="SUCCESS" SANITIZE_EXIT="0" SANITIZE_FINAL="" if verify_sanitize force; then SANITIZE_DETAIL="Plan C verificado: sanitize overwrite + verificación PASSED (previo: ${SANITIZE_DETAIL:-sin detalle})" say "Resultado final: SUCCESS ($CAP_METHOD, verificado)" fi else sanitize_err="$(printf '%s\n' "$out" | head -1)" SANITIZE_DETAIL="${SANITIZE_DETAIL:+$SANITIZE_DETAIL | }Plan C overwrite falló: ${sanitize_err:-sin salida}" say "Plan C overwrite falló: ${sanitize_err:-sin salida}" say "Resultado parcial: FAILED (se intenta Plan D)." log "Fallback sanitize overwrite falló." fi } # --------------------------------------------------------------------------- # Plan D I/O helpers with progress bar (pv) and legacy fallback. # pv only counts bytes (unprivileged); the writing dd keeps conv=fsync and # privileges. All pipeline stages must exit 0 (fail-closed). Overhead is # negligible (splice); duration is unaffected. # --------------------------------------------------------------------------- plan_d_write_main() { # plan_d_write_main — 0 on success. local blocks="$1" bytes="$2" if have pv; then dd if=/dev/zero bs=4M "count=$blocks" status=none 2>/dev/null \ | pv -s "$bytes" -N "wipe-dd" \ | run_priv dd "of=$SEL_DEV" bs=4M conv=fsync status=none 2>&1 local -a _rc=("${PIPESTATUS[@]}") [ "${_rc[0]}" = "0" ] && [ "${_rc[1]}" = "0" ] && [ "${_rc[2]}" = "0" ] else run_priv dd if=/dev/zero "of=$SEL_DEV" bs=4M "count=$blocks" conv=fsync status=progress 2>&1 fi } plan_d_verify_full() { # 0 iff the whole device reads as zeros (with progress bar if pv). if have pv; then run_priv dd "if=$SEL_DEV" bs=4M status=none 2>/dev/null \ | pv -s "$CAPACITY_BYTES" -N "wipe-verify" \ | cmp -s - <(head -c "$CAPACITY_BYTES" /dev/zero) local -a _rc=("${PIPESTATUS[@]}") [ "${_rc[0]}" = "0" ] && [ "${_rc[1]}" = "0" ] && [ "${_rc[2]}" = "0" ] else cmp <(run_priv dd "if=$SEL_DEV" bs=4M status=none 2>/dev/null) \ <(head -c "$CAPACITY_BYTES" /dev/zero) >/dev/null 2>&1 fi } ata_plan_d_dd() { # LAST resort, explicitly labeled: full-device overwrite with dd. # This is NOT a native Secure Erase; the certificate and method name say # so. Full-device read-back proves every byte reads as zero. # Applies to internal ATA and NVMe block devices alike. if [ "$SANITIZE_RESULT" = "SUCCESS" ]; then return 0 fi case "$CAP_METHOD" in ATA*|NVMe*) : ;; *) return 0 ;; esac if ! [[ "${CAPACITY_BYTES:-}" =~ ^[0-9]+$ ]]; then SANITIZE_DETAIL="${SANITIZE_DETAIL:+$SANITIZE_DETAIL | }Plan D omitido: capacidad desconocida" say "Plan D omitido: capacidad del disco desconocida. Resultado final: FAILED." return 0 fi # Ask first: dd is very slow (~1h per 240GB). The technician may skip # it; the evidence (including this decision) is uploaded either way. if [ "$WIPE_TEST_MODE" = "1" ]; then say "Plan D omitido (TEST MODE)." SANITIZE_DETAIL="${SANITIZE_DETAIL:+$SANITIZE_DETAIL | }Plan D omitido (TEST MODE)" return 0 fi echo # Rough ETA at 100 MB/s from the detected capacity (dd is host-side). local _eta_min _eta_txt _eta_min=$(( CAPACITY_BYTES / 6000000000 )) if [ "$_eta_min" -ge 60 ]; then _eta_txt="~$(( (_eta_min + 30) / 60 ))h" elif [ "$_eta_min" -ge 1 ]; then _eta_txt="~${_eta_min}min" else _eta_txt="algunos minutos" fi say "Plan D: overwrite completo con dd (NO NATIVO). Lento: ${_eta_txt} para ${SEL_SIZE} (estimado a 100MB/s)." local dd_answer="" if ! read_input "¿Ejecutar el dd? [s/N]: " dd_answer; then dd_answer="n" fi case "${dd_answer:-n}" in [sSyY]*) say "Plan D confirmado por el técnico. No apagar el equipo." ;; *) say "Plan D omitido por el técnico. Se sube la evidencia tal cual." SANITIZE_DETAIL="${SANITIZE_DETAIL:+$SANITIZE_DETAIL | }Plan D omitido por el técnico" log "Plan D dd omitido por decisión del técnico." return 0 ;; esac echo step "5" "7" "Plan D (ÚLTIMO RECURSO, NO NATIVO): overwrite completo con dd..." say "ADVERTENCIA: el firmware no ejecuta borrado nativo; se sobrescribe" say "todo el dispositivo con ceros. Lento (~1h en 240GB). NO apagar." CAP_METHOD="Overwrite completo (dd, no nativo)" CAP_METHOD_CMD="dd if=/dev/zero of=$SEL_DEV bs=4M count= conv=fsync (+pv barra) + cola final bs=512 + lectura total de verificación" log "Fallback a: $CAP_METHOD_CMD" unmount_partitions "$SEL_DEV" # Exact block math: dd from /dev/zero is infinite, so an unbounded copy # only ends with ENOSPC (exit != 0) AND leaves the last partial block # unwritten. Write full blocks + the exact 512-sector tail instead. local end_epoch end_epoch_ms full_blocks remainder dd_err main_bytes full_blocks=$(( CAPACITY_BYTES / 4194304 )) remainder=$(( CAPACITY_BYTES % 4194304 )) main_bytes=$(( full_blocks * 4194304 )) say "Escribiendo ${full_blocks} bloques de 4MiB + cola de ${remainder}B ..." if plan_d_write_main "$full_blocks" "$main_bytes"; then if [ "$remainder" -gt 0 ]; then say "Escribiendo cola final (${remainder}B)..." local _tail_ok=0 _try _tail_start _tail_count _tail_start=$(( full_blocks * 8192 )) _tail_count=$(( remainder / 512 )) for _try in 1 2 3; do if dd_err="$(run_priv dd if=/dev/zero "of=$SEL_DEV" bs=512 "seek=$_tail_start" "count=$_tail_count" conv=fsync,notrunc status=none 2>&1)"; then _tail_ok=1 break fi say "Cola final: intento ${_try}/3 falló ($(printf '%s\n' "$dd_err" | head -1)); reintentando..." sleep 5 done if [ "$_tail_ok" != "1" ]; then SANITIZE_DETAIL="${SANITIZE_DETAIL:+$SANITIZE_DETAIL | }Plan D dd falló en la cola final tras 3 intentos (LBA ${_tail_start}-$((_tail_start + _tail_count - 1)), $(( _tail_count * 512 ))B): $(printf '%s\n' "$dd_err" | head -1)" say "Plan D dd falló en la cola final (LBA ${_tail_start}-$((_tail_start + _tail_count - 1))). Resultado final: FAILED." log "Fallback dd falló (cola)." return 0 fi fi say "Escritura completa. Verificación TOTAL del dispositivo..." end_epoch="$(date +%s)" end_epoch_ms="$(date +%s%3N)" SANITIZE_DURATION="$(( end_epoch - SAN_START_S ))" SANITIZE_DURATION_MS="$(( end_epoch_ms - SAN_START_MS ))" if plan_d_verify_full; then SANITIZE_RESULT="SUCCESS" SANITIZE_EXIT="0" SANITIZE_FINAL="" VERIFY_RESULT="PASSED" VERIFY_DETAIL="dispositivo completo (${SEL_SIZE}) releído a cero tras overwrite dd" VERIFY_COVERAGE="full-device ${SEL_SIZE}" VERIFY_SAMPLE_MB="full" SANITIZE_DETAIL="Plan D verificado: overwrite dd + lectura total a cero (previo: ${SANITIZE_DETAIL:-sin detalle})" say "Verificación TOTAL: dispositivo a cero [OK]" say "Resultado final: SUCCESS ($CAP_METHOD, verificado total)" else SANITIZE_DETAIL="${SANITIZE_DETAIL:+$SANITIZE_DETAIL | }Plan D: la lectura total NO es cero" say "Verificación TOTAL: DATOS DETECTADOS [FAIL]" say "Resultado final: FAILED." fi else SANITIZE_DETAIL="${SANITIZE_DETAIL:+$SANITIZE_DETAIL | }Plan D dd falló durante la escritura" say "Plan D dd falló durante la escritura. Resultado final: FAILED." log "Fallback dd falló." fi } # --------------------------------------------------------------------------- # Sanitization execution # --------------------------------------------------------------------------- execute_sanitize() { local start_epoch start_epoch_ms end_epoch end_epoch_ms start_epoch="$(date +%s)" start_epoch_ms="$(date +%s%3N)" # Global copies so the sanitize fallback can extend the timing. SAN_START_S="$start_epoch" SAN_START_MS="$start_epoch_ms" local start_ts="$(date '+%Y-%m-%dT%H:%M:%S%z')" echo step "5" "7" "Sanitización con método: $CAP_METHOD" media_eta_note log "Iniciando sanitización con método: $CAP_METHOD" SANITIZE_RESULT="" SANITIZE_EXIT="" SANITIZE_DURATION="" SANITIZE_DURATION_MS="" SANITIZE_FINAL="" SANITIZE_DETAIL="" VERIFY_RESULT="" VERIFY_DETAIL="" VERIFY_SAMPLE_MB="" VERIFY_COVERAGE="" # TEST MODE: simulate if [ "$WIPE_TEST_MODE" = "1" ]; then say "[TEST MODE] Simulando sanitización de $SEL_DEV ..." sleep 2 SANITIZE_RESULT="SUCCESS" SANITIZE_EXIT="0" SANITIZE_FINAL="Simulated" log "Resultado (TEST): $SANITIZE_RESULT" echo echo "Resultado: $SANITIZE_RESULT" end_epoch="$(date +%s)" end_epoch_ms="$(date +%s%3N)" SANITIZE_DURATION="$(( end_epoch - start_epoch ))" SANITIZE_DURATION_MS="$(( end_epoch_ms - start_epoch_ms ))" say "Duración: ${SANITIZE_DURATION_MS}ms (simulado)" return 0 fi case "$CAP_METHOD" in "NVMe Format (User Data Erase)") SANITIZE_DETAIL="" unmount_partitions "$SEL_DEV" # If a previous run left a sanitize in progress, wait for it # first: a busy controller answers everything with 0x0C. nvme_sanitize_precheck "$SEL_DEV" # Order: sanitize FIRST when supported (stronger per NIST 800-88; # crypto erase is instant on SED), then format, then overwrite. # Verification judges every step. --force is mandatory: without # it nvme-cli asks for interactive confirmation ("You are about # to format...") and aborts since stdin is the script pipe, not # the terminal (seen on ML-125512). # nvme-cli actions: 4 = Start crypto erase, 2 = Start block # erase, 3 = Start overwrite (nvme-cli 2.8 --help). local can_block can_crypto san_out can_block="$(run_priv nvme id-ctrl -H "$SEL_DEV" 2>/dev/null | grep -i 'Block Erase.*Supported' || true)" can_crypto="$(run_priv nvme id-ctrl -H "$SEL_DEV" 2>/dev/null | grep -i 'Crypto Erase.*Supported' || true)" # 1. Sanitize crypto erase (instant on SED). if [ -n "$can_crypto" ]; then say "Ejecutando: nvme sanitize --sanact=4 (crypto erase)" log "Comando técnico: nvme sanitize --sanact=4" if san_out="$(run_priv nvme sanitize "$SEL_DEV" --sanact=4 2>&1)"; then if nvme_sanitize_wait "$SEL_DEV"; then SANITIZE_RESULT="SUCCESS" SANITIZE_EXIT="0" CAP_METHOD="NVMe Sanitize (Crypto Erase)" CAP_METHOD_CMD="nvme sanitize --sanact=4" fi else _san_err="$(printf '%s\n' "$san_out" | head -1)" SANITIZE_DETAIL="${SANITIZE_DETAIL:+$SANITIZE_DETAIL | }sanitize crypto rechazado: ${_san_err:-sin salida}" say "sanitize crypto erase rechazado: ${_san_err:-sin salida}" log "nvme sanitize crypto erase falló: $san_out" fi fi # 2. Sanitize block erase. if [ "$SANITIZE_RESULT" != "SUCCESS" ] && [ -n "$can_block" ]; then say "Intentando: nvme sanitize --sanact=2 (block erase)" if san_out="$(run_priv nvme sanitize "$SEL_DEV" --sanact=2 2>&1)"; then if nvme_sanitize_wait "$SEL_DEV"; then SANITIZE_RESULT="SUCCESS" SANITIZE_EXIT="0" CAP_METHOD="NVMe Sanitize (Block Erase)" CAP_METHOD_CMD="nvme sanitize --sanact=2" SANITIZE_DETAIL="" fi else _san_err="$(printf '%s\n' "$san_out" | head -1)" SANITIZE_DETAIL="${SANITIZE_DETAIL:+$SANITIZE_DETAIL | }sanitize block rechazado: ${_san_err:-sin salida}" say "sanitize block erase rechazado: ${_san_err:-sin salida}" log "nvme sanitize block erase falló: $san_out" fi fi # 3. Format crypto erase. if [ "$SANITIZE_RESULT" != "SUCCESS" ]; then say "Intentando: nvme format --force --ses=2 (crypto erase)" log "Comando técnico: nvme format --force --ses=2" if out="$(nvme_format_retry "$SEL_DEV" --ses=2)"; then SANITIZE_RESULT="SUCCESS" SANITIZE_EXIT="0" CAP_METHOD="NVMe Format (Crypto Erase)" CAP_METHOD_CMD="nvme format --force --ses=2" SANITIZE_DETAIL="" else SANITIZE_DETAIL="${SANITIZE_DETAIL:+$SANITIZE_DETAIL | }$(printf '%s\n' "$out" | head -1)" log "nvme format --ses=2 falló." say "AVISO: crypto erase rechazado." fi fi # 4. Format user data erase (+ explicit NSID retry: some # controllers, e.g. Intel 670p with FNA bit0=0, reject format # without an explicit NSID with Command Sequence Error (0xc)). if [ "$SANITIZE_RESULT" != "SUCCESS" ]; then say "Intentando: nvme format --force --ses=1 (user data erase)" if out="$(nvme_format_retry "$SEL_DEV" --ses=1)"; then SANITIZE_RESULT="SUCCESS" SANITIZE_EXIT="0" CAP_METHOD="NVMe Format (User Data Erase)" CAP_METHOD_CMD="nvme format --force --ses=1" SANITIZE_DETAIL="" else SANITIZE_DETAIL="${SANITIZE_DETAIL:+$SANITIZE_DETAIL | }$(printf '%s\n' "$out" | head -1)" log "nvme format --ses=1 falló." for _ses in 2 1; do if [ "$SANITIZE_RESULT" = "SUCCESS" ]; then break fi say "Intentando: nvme format --force --ses=${_ses} --namespace-id=1 ..." if out="$(nvme_format_retry "$SEL_DEV" --ses="$_ses" --namespace-id=1)"; then SANITIZE_RESULT="SUCCESS" SANITIZE_EXIT="0" CAP_METHOD="NVMe Format (ses=${_ses}, nsid=1)" CAP_METHOD_CMD="nvme format --force --ses=${_ses} --namespace-id=1" SANITIZE_DETAIL="" else SANITIZE_DETAIL="$SANITIZE_DETAIL | nsid1/ses${_ses}: $(printf '%s\n' "$out" | head -1)" fi done fi fi # 5. Sanitize overwrite (slow pattern write, 5min wait: if it # does not finish, the flow continues to Plan D dd). if [ "$SANITIZE_RESULT" != "SUCCESS" ]; then say "Intentando: nvme sanitize --sanact=3 (overwrite, hasta 5min)..." if san_out="$(run_priv nvme sanitize "$SEL_DEV" --sanact=3 2>&1)"; then if nvme_sanitize_wait "$SEL_DEV" 300; then SANITIZE_RESULT="SUCCESS" SANITIZE_EXIT="0" CAP_METHOD="NVMe Sanitize (Overwrite)" CAP_METHOD_CMD="nvme sanitize --sanact=3" SANITIZE_DETAIL="" fi else _san_err="$(printf '%s\n' "$san_out" | head -1)" SANITIZE_DETAIL="${SANITIZE_DETAIL:+$SANITIZE_DETAIL | }sanitize overwrite rechazado: ${_san_err:-sin salida}" say "sanitize overwrite rechazado: ${_san_err:-sin salida}" log "nvme sanitize overwrite falló: $san_out" fi fi [ "$SANITIZE_RESULT" != "SUCCESS" ] \ && SANITIZE_DETAIL="${SANITIZE_DETAIL:-Firmware rechaza sanitize y format}" if [ "$SANITIZE_RESULT" = "SUCCESS" ]; then say "Resultado: SUCCESS ($CAP_METHOD)" else SANITIZE_RESULT="FAILED" SANITIZE_EXIT="1" say "Resultado: FAILED" say "Detalle: ${SANITIZE_DETAIL:-sin detalle}" fi ;; "ATA Secure Erase") CAP_METHOD_CMD="hdparm --user-master u --security-set-pass NULL $SEL_DEV && hdparm --user-master u --security-erase NULL $SEL_DEV" unmount_partitions "$SEL_DEV" # A frozen drive rejects Security commands: refuse early with # actionable instructions instead of a fake instant SUCCESS. if printf '%s\n' "${CAP_AVAILABLE[@]}" | grep -qx "ata-frozen"; then SANITIZE_DETAIL="disco en estado frozen: apagar TOTALMENTE (no reiniciar, no suspender), encender de nuevo y re-ejecutar" SANITIZE_RESULT="FAILED" SANITIZE_EXIT="1" say "AVISO: disco congelado (frozen). $SANITIZE_DETAIL" say "Si persiste tras apagado total: desactiva 'Freeze Lock' en la BIOS o prueba suspender (echo mem | sudo tee /sys/power/state) y re-ejecutar." else # Step 1/2 (mandatory per ATA spec): set the password first. # Without this, some firmwares ACK the erase in milliseconds # without touching the media (seen on BX500: exit 0, datos # intactos). NULL keeps a power-loss safe state. say "Paso 1/2: fijando password de seguridad (NULL)..." log "Comando técnico: hdparm --user-master u --security-set-pass NULL $SEL_DEV" if out="$(run_priv hdparm --user-master u --security-set-pass NULL "$SEL_DEV" 2>&1)"; then say "Password fijada [OK]" else _setpass_detail="$(printf '%s\n' "$out" | head -1)" say "AVISO: set-pass falló (${_setpass_detail:-sin salida}); se intenta el borrado igualmente." log "set-pass falló, se continúa al erase: ${_setpass_detail:-sin salida}" fi # Step 2/2: erase (takes minutes on real media — do NOT power off). say "Paso 2/2: ejecutando ATA Secure Erase (puede tardar varios minutos, NO apagar el equipo)..." log "Comando técnico: hdparm --user-master u --security-erase NULL $SEL_DEV" if out="$(run_priv hdparm --user-master u --security-erase NULL "$SEL_DEV" 2>&1)"; then SANITIZE_RESULT="SUCCESS" SANITIZE_EXIT="0" say "Erase aceptado por el firmware (exit 0). Verificando..." else SANITIZE_DETAIL="$(printf '%s\n' "$out" | head -1)" SANITIZE_RESULT="FAILED" SANITIZE_EXIT="1" say "Resultado: FAILED ($CAP_METHOD)" say "Detalle: ${SANITIZE_DETAIL:-sin detalle}" fi fi ;; "ATA Sanitize") say "Ejecutando ATA Sanitize (Block Erase)" CAP_METHOD_CMD="hdparm --yes-i-know-what-i-am-doing --sanitize-block-erase $SEL_DEV" log "Comando técnico: $CAP_METHOD_CMD" unmount_partitions "$SEL_DEV" say "Sanitize block erase en curso (puede tardar varios minutos, NO apagar el equipo)..." if out="$(run_priv hdparm --yes-i-know-what-i-am-doing --sanitize-block-erase "$SEL_DEV" 2>&1)"; then SANITIZE_RESULT="SUCCESS" SANITIZE_EXIT="0" say "Sanitize aceptado por el firmware (exit 0). Verificando..." else SANITIZE_DETAIL="$(printf '%s\n' "$out" | head -1)" SANITIZE_RESULT="FAILED" SANITIZE_EXIT="1" say "Resultado: FAILED ($CAP_METHOD)" say "Detalle: ${SANITIZE_DETAIL:-sin detalle}" fi ;; *) # No supported method SANITIZE_RESULT="NOT_SUPPORTED" SANITIZE_EXIT="1" SANITIZE_FINAL="NO SE PUDO REALIZAR BORRADO SEGURO" echo "NO SE PUDO REALIZAR BORRADO SEGURO" log "Resultado: $SANITIZE_RESULT - ${SANITIZE_FINAL}" ;; esac end_epoch="$(date +%s)" end_epoch_ms="$(date +%s%3N)" SANITIZE_DURATION="$(( end_epoch - start_epoch ))" SANITIZE_DURATION_MS="$(( end_epoch_ms - start_epoch_ms ))" say "Duración: ${SANITIZE_DURATION_MS}ms (exit ${SANITIZE_EXIT:-N/A})" } # --------------------------------------------------------------------------- # SHA-256 helper # --------------------------------------------------------------------------- compute_sha256() { sha256sum "$1" 2>/dev/null | awk '{print $1}' } # --------------------------------------------------------------------------- # JSON escaping for values interpolated into operation.json. # The heredoc is not JSON-aware, so free-text fields (user input, firmware # strings) must be escaped or a stray '"' would corrupt the whole file and # the server would receive unparseable evidence. # --------------------------------------------------------------------------- json_escape() { printf '%s' "$1" \ | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' \ | tr '\n\t' ' ' | tr -d '[:cntrl:]' } # --------------------------------------------------------------------------- # Certificate generation # --------------------------------------------------------------------------- generate_certificate() { local finished_ts="$(date '+%Y-%m-%dT%H:%M:%S%z')" local finished_readable="$(date '+%Y-%m-%d %H:%M:%S')" CERT_FILE="$WORKDIR/certificate.txt" local caps_display="${CAP_AVAILABLE[*]:-}" local cmd_display="${CAP_METHOD_CMD:-N/A}" local initial_state="${SANITIZE_INITIAL_STATE:-Detected}" local final_state="${SANITIZE_FINAL:-$SANITIZE_RESULT}" cat > "$CERT_FILE" < "$OP_FILE" </dev/null; then say "[WARN] operation.json no pasó validación JSON local (se sube igual; el servidor preserva el raw)." log "AVISO: operation.json local no parsea como JSON." fi fi } # --------------------------------------------------------------------------- # Evidence summary (printed right before the screenshot so a single photo # contains all essential data even if earlier output has scrolled away) # --------------------------------------------------------------------------- print_evidence_summary() { echo echo "------------------------------------------------------------" echo "RESUMEN DE EVIDENCIA (visible en captura)" echo "------------------------------------------------------------" echo "Usuario que entrega el equipo: $USER_NAME" echo "ML: $ML" echo "Equipo: $HARDWARE_MANUFACTURER $HARDWARE_MODEL (S/N $HARDWARE_SERIAL)" echo "Disco: $SEL_DEV ($SEL_TYPE, ${MEDIA_KIND:-?}, $SEL_SIZE)" echo "Diagnóstico: SATA ${SATA_LINK:-N/A} · SMART ${SMART_HEALTH:-N/A}" echo "Método: $CAP_METHOD" echo "Comando: ${CAP_METHOD_CMD:-N/A}" echo "Resultado: $SANITIZE_RESULT (duración ${SANITIZE_DURATION_MS}ms, exit ${SANITIZE_EXIT:-N/A})" echo "Verificación: $VERIFY_RESULT (${VERIFY_DETAIL:-sin detalle})" echo "Operation ID (local): $OPERATION_ID" [ -n "${CERT_FILE:-}" ] && [ -f "$CERT_FILE" ] && echo "certificate.txt sha256: $(compute_sha256 "$CERT_FILE")" [ -n "${OP_FILE:-}" ] && [ -f "$OP_FILE" ] && echo "operation.json sha256: $(compute_sha256 "$OP_FILE")" echo "------------------------------------------------------------" } # --------------------------------------------------------------------------- # Screenshot # --------------------------------------------------------------------------- capture_screenshot() { local shot shot="$WORKDIR/evidence.png" if [ "$WIPE_TEST_MODE" = "1" ]; then # In test mode we create a placeholder PNG (valid image) # so we can exercise the upload path without a real screenshot. # Build a minimal 1x1 PNG using printf of base64. printf '%s' \ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==' \ | base64 -d > "$shot" 2>/dev/null SCREENSHOT_FILE="$shot" log "Captura (TEST): $shot" return 0 fi # Preferred: xfce4-screenshooter (window capture) if have xfce4-screenshooter; then xfce4-screenshooter -w -s "$shot" >/dev/null 2>&1 || \ xfce4-screenshooter --window --save "$shot" >/dev/null 2>&1 fi # Fallback: scrot if [ ! -s "$shot" ] && have scrot; then scrot "$shot" 2>/dev/null fi # Fallback: import (ImageMagick) if [ ! -s "$shot" ] && have import; then import -window root "$shot" 2>/dev/null fi if [ ! -s "$shot" ]; then say "AVISO: no se pudo tomar una captura, se continuará sin ella." log "AVISO: captura no disponible." SCREENSHOT_FILE="" else SCREENSHOT_FILE="$shot" log "Captura tomada: $shot" fi } # --------------------------------------------------------------------------- # Upload # --------------------------------------------------------------------------- upload_evidence() { say step "7" "7" "Subiendo evidencia a $WIPE_SERVER..." # 1. Enroll to get a short-lived JWT (no jq dependency) local token="" token="$(curl -fsS -X POST "$WIPE_SERVER/api/v1/enroll" \ -H "Authorization: Bearer $WIPE_ENROLL_KEY" \ -H "Accept: application/json" 2>/dev/null \ | sed -n 's/.*"token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')" if [ -z "$token" ]; then say "[FAIL] No se pudo obtener token de autenticación." return 1 fi # 2. Upload files local curl_args=(-fsS -X POST "$WIPE_SERVER/api/v1/upload") curl_args+=(-H "Authorization: Bearer $token") curl_args+=(-F "ml=$ML") curl_args+=(-F "usuario=$USER_NAME") curl_args+=(-F "serial=$HARDWARE_SERIAL") curl_args+=(-F "certificate.txt=@$CERT_FILE") [ -n "$SCREENSHOT_FILE" ] && curl_args+=(-F "evidence.png=@$SCREENSHOT_FILE") curl_args+=(-F "operation.json=@$OP_FILE") # Terminal transcript (mirrored via tee since the run started). [ -n "${WIPE_LOG:-}" ] && [ -f "$WIPE_LOG" ] && curl_args+=(-F "wipe.log=@$WIPE_LOG") local resp if resp="$(curl "${curl_args[@]}" 2>&1)"; then local op_id op_id="$(echo "$resp" | sed -n 's/.*"operation_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')" if [ -n "$op_id" ]; then OPERATION_ID="$op_id" say "[OK] certificate.txt" [ -n "$SCREENSHOT_FILE" ] && say "[OK] evidence.png" say "[OK] operation.json" [ -n "${WIPE_LOG:-}" ] && [ -f "$WIPE_LOG" ] && say "[OK] wipe.log" say say "Operation ID: $op_id" say "Resultado: UPLOAD SUCCESS" return 0 else say "[WARN] Respuesta sin operation_id: $resp" return 1 fi else say "[FAIL] Fallo de subida: $resp" return 1 fi } # --------------------------------------------------------------------------- # Final display # --------------------------------------------------------------------------- show_completion() { local res_bar="BORRADO COMPLETADO" if [ "$SANITIZE_RESULT" != "SUCCESS" ]; then res_bar="OPERACION NO COMPLETADA" fi echo echo "============================================================" echo " $res_bar" echo "============================================================" echo echo "Usuario que entrega el equipo: $USER_NAME" echo "ML: $ML" echo "Serial: $HARDWARE_SERIAL" echo echo "Método: $CAP_METHOD" echo "Resultado: $SANITIZE_RESULT" echo echo "Operation ID: $OPERATION_ID" echo echo "Evidencia: ${UPLOAD_STATUS:-NO SUBIDA}" echo echo "============================================================" echo } # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- main() { print_header # Resolve defaults if not substituted if [ "$WIPE_SERVER" = "https://wipe.codez.cz" ]; then WIPE_SERVER="https://wipe.codez.cz" fi # Working directory FIRST so the full terminal transcript (wipe.log) # captures everything from the user prompts onward. WORKDIR="$(mktemp -d)" trap 'release_suspend; rm -rf "$WORKDIR"' EXIT WIPE_LOG="$WORKDIR/wipe.log" # Mirror all terminal output to wipe.log (uploaded as evidence). # Prompts read from /dev/tty, so redirecting stdout is safe. exec > >(tee "$WIPE_LOG") 2>&1 # Block sleep/suspend for the whole run (wipes can take hours). inhibit_suspend step "1" "7" "Inicio de $PROG $VERSION" say "Servidor: $WIPE_SERVER" # Auto-install needed tools (no confirmation) step "2" "7" "Verificando herramientas necesarias..." ensure_tools # Request user info. The ML is canonicalized to "ML-" so that # "123456", "ml-123456" or " ml 123456 " all become "ML-123456" # before anything is uploaded. echo "USUARIO QUE ENTREGA EL EQUIPO" echo "------------------------------------------------------------" while true; do read_input "Usuario que entrega el equipo: " USER_NAME read_input "ML (ej. ML-123456): " ML_RAW echo if [ -z "${USER_NAME:-}" ]; then say "ERROR: el usuario es obligatorio. Inténtelo de nuevo." continue fi if ML="$(normalize_ml "${ML_RAW:-}")"; then if [ "$ML" != "${ML_RAW:-}" ]; then say "ML normalizado: $ML" fi break fi say "ERROR: ML no válido ('${ML_RAW:-}'). Use el formato ML-; se acepta solo el número o en minúsculas." done # Timestamps STARTED_TS="$(date '+%Y-%m-%dT%H:%M:%S%z')" STARTED_READABLE="$(date '+%Y-%m-%d %H:%M:%S')" OPERATION_ID="local-$(cat /proc/sys/kernel/random/uuid 2>/dev/null | head -c8 || date +%s)" say "Operation ID (local): $OPERATION_ID" say "Inicio: $STARTED_READABLE" CAPACITY_BYTES="" SECTOR_SIZE="N/A" FIRMWARE="N/A" # Detect detect_hardware detect_boot_device # Determine selected disk sizing info detect_storage show_and_select_disk # Refresh detailed info for selected device CAPACITY_BYTES="$(lsblk -bdno SIZE "$SEL_DEV" 2>/dev/null | head -1)" SECTOR_SIZE="$(lsblk -dno LOG-SEC "$SEL_DEV" 2>/dev/null | head -1)" FIRMWARE="$(lsblk -dno FIRMWARE "$SEL_DEV" 2>/dev/null | head -1)" SEL_ROTA="$(lsblk -dno ROTA "$SEL_DEV" 2>/dev/null | head -1)" MEDIA_KIND="$(media_kind "$SEL_TYPE" "$SEL_ROTA")" say "Medio detectado: $MEDIA_KIND" log "Medio: $MEDIA_KIND (ROTA=${SEL_ROTA:-?})" detect_capabilities "$SEL_DEV" # Read-only drive diagnostics (SATA link, dmesg, SMART) into the log. collect_drive_diagnostics # Execute (no confirmation required by design; the boot medium is the # only guaranteed safe exclusion and SATA/NVMe candidates are selected # explicitly above) execute_sanitize # Post-wipe verification (fail-closed): a SUCCESS that does not pass # verification is downgraded to FAILED — never certify unverified erases. verify_sanitize # Cadenas fallback (una sola llamada: la cadena acepta ATA y NVMe; # no duplicar o el prompt del Plan D saldría dos veces). ata_fallback_chain # Generate evidence generate_certificate generate_operation_json # Summary box so a single screenshot holds all essential data. print_evidence_summary # Capture capture_screenshot # Let the tee'd transcript flush to wipe.log before uploading it. sleep 1 # Upload UPLOAD_STATUS="NO SUBIDO" if upload_evidence; then UPLOAD_STATUS="UPLOAD SUCCESS" else UPLOAD_STATUS="UPLOAD FAILED (evidencia local conservada)" say say "AVISO: la evidencia NO se subió. Se conserva localmente en:" say "$WORKDIR" say "Puede reintentar la subida manualmente." fi # Final display show_completion # Keep evidence on failed upload if [ "$UPLOAD_STATUS" != "UPLOAD SUCCESS" ]; then local keep="$HOME/wipe-evidence-$(date +%Y%m%d-%H%M%S)" mkdir -p "$keep" [ -f "$CERT_FILE" ] && cp "$CERT_FILE" "$keep/" [ -f "$OP_FILE" ] && cp "$OP_FILE" "$keep/" [ -n "${SCREENSHOT_FILE:-}" ] && [ -f "$SCREENSHOT_FILE" ] && cp "$SCREENSHOT_FILE" "$keep/" [ -n "${WIPE_LOG:-}" ] && [ -f "$WIPE_LOG" ] && cp "$WIPE_LOG" "$keep/" say "Evidencia copiada a: $keep" fi } main "$@"