98 lines
2.8 KiB
Bash
98 lines
2.8 KiB
Bash
#!/usr/bin/env bash
|
|
# restore_backup.sh — Restore an encrypted Hermes backup archive
|
|
# Usage: restore_backup.sh /path/to/hermes-backup-YYYYmmdd-HHMMSS.tar.gz.enc /restore/root
|
|
set -euo pipefail
|
|
|
|
ARCHIVE_PATH="${1:-}"
|
|
RESTORE_ROOT="${2:-}"
|
|
STAGE_DIR="$(mktemp -d "${TMPDIR:-/tmp}/timmy-restore.XXXXXX")"
|
|
PLAINTEXT_ARCHIVE="${STAGE_DIR}/restore.tar.gz"
|
|
PASSFILE_CLEANUP=""
|
|
|
|
cleanup() {
|
|
rm -f "$PLAINTEXT_ARCHIVE"
|
|
rm -rf "$STAGE_DIR"
|
|
if [[ -n "$PASSFILE_CLEANUP" && -f "$PASSFILE_CLEANUP" ]]; then
|
|
rm -f "$PASSFILE_CLEANUP"
|
|
fi
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
fail() {
|
|
echo "ERROR: $1" >&2
|
|
exit 1
|
|
}
|
|
|
|
resolve_passphrase_file() {
|
|
if [[ -n "${BACKUP_PASSPHRASE_FILE:-}" ]]; then
|
|
[[ -f "$BACKUP_PASSPHRASE_FILE" ]] || fail "BACKUP_PASSPHRASE_FILE does not exist: $BACKUP_PASSPHRASE_FILE"
|
|
echo "$BACKUP_PASSPHRASE_FILE"
|
|
return
|
|
fi
|
|
|
|
if [[ -n "${BACKUP_PASSPHRASE:-}" ]]; then
|
|
PASSFILE_CLEANUP="${STAGE_DIR}/backup.passphrase"
|
|
printf '%s' "$BACKUP_PASSPHRASE" > "$PASSFILE_CLEANUP"
|
|
chmod 600 "$PASSFILE_CLEANUP"
|
|
echo "$PASSFILE_CLEANUP"
|
|
return
|
|
fi
|
|
|
|
fail "Set BACKUP_PASSPHRASE_FILE or BACKUP_PASSPHRASE before restoring a backup."
|
|
}
|
|
|
|
sha256_file() {
|
|
local path="$1"
|
|
if command -v shasum >/dev/null 2>&1; then
|
|
shasum -a 256 "$path" | awk '{print $1}'
|
|
elif command -v sha256sum >/dev/null 2>&1; then
|
|
sha256sum "$path" | awk '{print $1}'
|
|
else
|
|
python3 - <<'PY' "$path"
|
|
import hashlib
|
|
import pathlib
|
|
import sys
|
|
path = pathlib.Path(sys.argv[1])
|
|
h = hashlib.sha256()
|
|
with path.open('rb') as f:
|
|
for chunk in iter(lambda: f.read(1024 * 1024), b''):
|
|
h.update(chunk)
|
|
print(h.hexdigest())
|
|
PY
|
|
fi
|
|
}
|
|
|
|
[[ -n "$ARCHIVE_PATH" ]] || fail "Usage: restore_backup.sh /path/to/archive.tar.gz.enc /restore/root"
|
|
[[ -n "$RESTORE_ROOT" ]] || fail "Usage: restore_backup.sh /path/to/archive.tar.gz.enc /restore/root"
|
|
[[ -f "$ARCHIVE_PATH" ]] || fail "Archive not found: $ARCHIVE_PATH"
|
|
|
|
if [[ "$ARCHIVE_PATH" == *.tar.gz.enc ]]; then
|
|
MANIFEST_PATH="${ARCHIVE_PATH%.tar.gz.enc}.json"
|
|
else
|
|
MANIFEST_PATH=""
|
|
fi
|
|
|
|
if [[ -n "$MANIFEST_PATH" && -f "$MANIFEST_PATH" ]]; then
|
|
EXPECTED_SHA="$(python3 - <<'PY' "$MANIFEST_PATH"
|
|
import json
|
|
import sys
|
|
with open(sys.argv[1], 'r', encoding='utf-8') as handle:
|
|
manifest = json.load(handle)
|
|
print(manifest['archive_sha256'])
|
|
PY
|
|
)"
|
|
ACTUAL_SHA="$(sha256_file "$ARCHIVE_PATH")"
|
|
[[ "$EXPECTED_SHA" == "$ACTUAL_SHA" ]] || fail "Archive SHA256 mismatch: expected $EXPECTED_SHA got $ACTUAL_SHA"
|
|
fi
|
|
|
|
PASSFILE="$(resolve_passphrase_file)"
|
|
mkdir -p "$RESTORE_ROOT"
|
|
|
|
openssl enc -d -aes-256-cbc -salt -pbkdf2 -iter 200000 \
|
|
-pass "file:${PASSFILE}" \
|
|
-in "$ARCHIVE_PATH" \
|
|
-out "$PLAINTEXT_ARCHIVE"
|
|
|
|
tar -xzf "$PLAINTEXT_ARCHIVE" -C "$RESTORE_ROOT"
|
|
echo "Restored backup into $RESTORE_ROOT"
|