#!/usr/bin/env bash # The reverse of backup.sh: open an igneum-keys-.dmg and either check it against the live folder or copy its # files back. # # tools/keys/restore.sh --check # every file in the image against ~/.config/igneum, by sha256; # # prints match / DIFFERS / missing, never a value; exit 1 on any difference # tools/keys/restore.sh --to # copies the files into (0700 dirs, 0600 files; .pub 0644), # # refuses to overwrite an existing file unless --force, then runs the check # tools/keys/restore.sh --list # names, sizes and modes inside the image # # Options: --source (the live folder for --check, default ~/.config/igneum), --agent (passphrase through the # macOS dialog), --stdinpass (test harness only: a NUL-terminated passphrase on standard input). The image is attached # read-only at a private mount point and detached at exit, also on failure. README.txt and keys.md inside the image are # documentation and are not copied or compared. set -euo pipefail IMG=""; ACTION=""; DEST=""; LIVE="$HOME/.config/igneum"; MODE="tty"; FORCE=0 die() { printf 'restore: %s\n' "$*" >&2; exit 1; } say() { printf '%s\n' "$*"; } while [ $# -gt 0 ]; do case "$1" in --check) ACTION="check" ;; --list) ACTION="list" ;; --to) ACTION="restore"; DEST="${2:?--to needs a folder}"; shift ;; --source) LIVE="${2:?--source needs a folder}"; shift ;; --force) FORCE=1 ;; --agent) MODE="agent" ;; --stdinpass) MODE="stdin" ;; -h|--help) sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;; -*) die "unknown argument $1" ;; *) [ -z "$IMG" ] && IMG="$1" || die "one image only" ;; esac shift done [ -n "$IMG" ] || die "which image? tools/keys/restore.sh --check|--list|--to " [ -f "$IMG" ] || die "no file at $IMG" [ -n "$ACTION" ] || die "pick --check, --list or --to " command -v hdiutil >/dev/null || die "hdiutil is not available (macOS only)" if [ "$MODE" = "tty" ] && [ ! -t 0 ]; then die "no terminal for the passphrase prompt; run from a terminal, or --agent"; fi PASS="" if [ "$MODE" = "stdin" ]; then IFS= read -r -d '' PASS || true; [ -n "$PASS" ] || die "--stdinpass: no passphrase on standard input"; fi MNT="$(mktemp -d "${TMPDIR:-/tmp}/igneum-keys-mnt.XXXXXX")" MNT="$(cd "$MNT" && pwd -P)" # the physical path: $TMPDIR is a symlink on macOS and `mount` prints the real one attached() { mount | grep -qF " on $MNT "; } cleanup() { if attached; then hdiutil detach "$MNT" -quiet >/dev/null 2>&1 || hdiutil detach "$MNT" -force -quiet >/dev/null 2>&1 || true; fi attached || rmdir "$MNT" 2>/dev/null || true PASS="" } trap cleanup EXIT case "$MODE" in tty) hdiutil attach "$IMG" -readonly -nobrowse -noautoopen -mountpoint "$MNT" -quiet ;; agent) hdiutil attach "$IMG" -readonly -nobrowse -noautoopen -agentpass -mountpoint "$MNT" -quiet ;; stdin) printf '%s\0' "$PASS" | hdiutil attach "$IMG" -readonly -nobrowse -noautoopen -stdinpass -mountpoint "$MNT" -quiet ;; esac PASS="" attached || die "the image did not attach at $MNT (wrong passphrase, or the image is already attached: hdiutil info)" # The secret files inside the image: everything except the documentation. FILES="$(cd "$MNT" && find . -type f ! -name .DS_Store ! -name README.txt ! -name keys.md -print | sed 's#^\./##' | LC_ALL=C sort)" COUNT="$(printf '%s\n' "$FILES" | grep -c . || true)" [ "$COUNT" -gt 0 ] || die "the image holds no files" say "image $IMG" say "files $COUNT (plus README.txt)" sha() { shasum -a 256 "$1" | cut -c1-64; } check_against() { local live="$1" bad=0 rel while IFS= read -r rel; do if [ ! -f "$live/$rel" ]; then printf ' %-40s %8s bytes MISSING in %s\n' "$rel" "$(stat -f '%z' "$MNT/$rel")" "$live"; bad=1 elif [ "$(sha "$MNT/$rel")" = "$(sha "$live/$rel")" ]; then printf ' %-40s %8s bytes match\n' "$rel" "$(stat -f '%z' "$MNT/$rel")" else printf ' %-40s %8s bytes DIFFERS (live %s bytes, mtime %s)\n' "$rel" "$(stat -f '%z' "$MNT/$rel")" "$(stat -f '%z' "$live/$rel")" "$(date -u -r "$(stat -f '%m' "$live/$rel")" +%Y-%m-%dT%H:%MZ)"; bad=1 fi done <<< "$FILES" # live files the image does not carry (the two settings files and the pip folder are expected) local extra extra="$(cd "$live" && find . -type f ! -name .DS_Store -print | sed 's#^\./##' | LC_ALL=C sort | grep -v -x -F -f <(printf '%s\n' "$FILES") | grep -v -E '^(build-slots|dlsite-dir|pytools/.*)$' || true)" if [ -n "$extra" ]; then say " live files not in the image (a newer key? back up again):" printf '%s\n' "$extra" | sed 's/^/ /' bad=1 fi return $bad } case "$ACTION" in list) while IFS= read -r rel; do printf ' %s %8s bytes %s\n' "$(stat -f '%Sp' "$MNT/$rel")" "$(stat -f '%z' "$MNT/$rel")" "$rel"; done <<< "$FILES" ;; check) [ -d "$LIVE" ] || die "no live folder at $LIVE" say "against $LIVE" if check_against "$LIVE"; then say "check every file in the image matches the live folder"; else say "check DIFFERENCES found (see above)"; exit 1; fi ;; restore) mkdir -p "$DEST"; chmod 700 "$DEST" if [ $FORCE -eq 0 ]; then while IFS= read -r rel; do [ -e "$DEST/$rel" ] && die "$DEST/$rel exists; --force to overwrite (it is replaced by the image's copy)"; done <<< "$FILES" fi while IFS= read -r rel; do mkdir -p "$DEST/$(dirname "$rel")"; chmod 700 "$DEST/$(dirname "$rel")" cp -p "$MNT/$rel" "$DEST/$rel" case "$rel" in *.pub) chmod 644 "$DEST/$rel" ;; *) chmod 600 "$DEST/$rel" ;; esac done <<< "$FILES" say "restored $COUNT files into $DEST" if check_against "$DEST"; then say "check every restored file matches the image"; else die "the restored files do not match the image"; fi ;; esac