mirror of
https://github.com/gadgethd/ukmesh.git
synced 2026-09-01 17:08:17 +00:00
109 lines
4.2 KiB
Bash
Executable File
109 lines
4.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
umask 077
|
|
|
|
source_dir="${BACKUP_SOURCE_DIR:-/home/ben/ukmesh/backups}"
|
|
target_dir="${BACKUP_SYNC_TARGET_DIR:?BACKUP_SYNC_TARGET_DIR must name the mounted off-host publication target}"
|
|
verify_key="${BACKUP_RECEIPT_VERIFY_KEY:?BACKUP_RECEIPT_VERIFY_KEY is required}"
|
|
allow_local="${BACKUP_SYNC_ALLOW_LOCAL_TEST:-false}"
|
|
|
|
for command in openssl jq sha256sum findmnt find sort sed awk sync; do
|
|
command -v "$command" >/dev/null || {
|
|
echo "required command is unavailable: $command" >&2
|
|
exit 69
|
|
}
|
|
done
|
|
test -d "$source_dir" || { echo "backup source is missing: $source_dir" >&2; exit 66; }
|
|
test -r "$verify_key" || { echo "receipt verify key is unreadable" >&2; exit 66; }
|
|
mkdir -p "$target_dir"
|
|
target_dir="$(cd "$target_dir" && pwd)"
|
|
test -f "$target_dir/.meshcore-offsite-target" || {
|
|
echo "sync target is not attested (.meshcore-offsite-target missing)" >&2
|
|
exit 65
|
|
}
|
|
if [ "$allow_local" != "true" ] \
|
|
&& [ "$(findmnt -T "$source_dir" -n -o SOURCE)" = "$(findmnt -T "$target_dir" -n -o SOURCE)" ]; then
|
|
echo "sync target shares the backup source filesystem" >&2
|
|
exit 65
|
|
fi
|
|
|
|
receipt_path="$(
|
|
find "$source_dir" -maxdepth 1 -type f -name 'backup-*.receipt.json' -printf '%T@ %p\n' \
|
|
| sort -nr \
|
|
| sed -n '1 { s/^[^ ]* //; p; }'
|
|
)"
|
|
test -n "$receipt_path" && test -f "$receipt_path" || {
|
|
echo "no completed backup receipt found" >&2
|
|
exit 66
|
|
}
|
|
signature_path="${receipt_path}.sig"
|
|
test -f "$signature_path" || { echo "backup receipt signature is missing" >&2; exit 66; }
|
|
openssl dgst -sha256 -verify "$verify_key" \
|
|
-signature "$signature_path" "$receipt_path" >/dev/null
|
|
jq -e '.format == "meshcore-backup-receipt-v1" and .status == "complete"' \
|
|
"$receipt_path" >/dev/null
|
|
archive_name="$(jq -er '.archive' "$receipt_path")"
|
|
case "$archive_name" in
|
|
*/*|'') echo "backup receipt archive name is unsafe" >&2; exit 65 ;;
|
|
esac
|
|
archive_path="$source_dir/$archive_name"
|
|
test -f "$archive_path" || { echo "backup archive is missing" >&2; exit 66; }
|
|
expected_archive_sha="$(jq -er '.archive_sha256' "$receipt_path")"
|
|
actual_archive_sha="$(sha256sum "$archive_path" | awk '{print $1}')"
|
|
test "$actual_archive_sha" = "$expected_archive_sha" || {
|
|
echo "backup archive checksum does not match receipt" >&2
|
|
exit 65
|
|
}
|
|
|
|
publication_id="$(jq -er '.backup_id' "$receipt_path")"
|
|
case "$publication_id" in
|
|
backup-[0-9A-Za-z._-]*) ;;
|
|
*) echo "backup id is unsafe" >&2; exit 65 ;;
|
|
esac
|
|
partial_dir="$target_dir/.${publication_id}.partial"
|
|
test ! -e "$partial_dir" || { echo "partial publication already exists" >&2; exit 73; }
|
|
mkdir "$partial_dir"
|
|
cleanup() {
|
|
if [ -d "$partial_dir" ]; then
|
|
find "$partial_dir" -maxdepth 1 -type f -delete
|
|
rmdir "$partial_dir"
|
|
fi
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
cp -- "$archive_path" "$partial_dir/$archive_name"
|
|
cp -- "$receipt_path" "$partial_dir/latest.receipt.json"
|
|
cp -- "$signature_path" "$partial_dir/latest.receipt.json.sig"
|
|
test "$(sha256sum "$partial_dir/$archive_name" | awk '{print $1}')" = "$expected_archive_sha"
|
|
openssl dgst -sha256 -verify "$verify_key" \
|
|
-signature "$partial_dir/latest.receipt.json.sig" \
|
|
"$partial_dir/latest.receipt.json" >/dev/null
|
|
(
|
|
cd "$partial_dir"
|
|
sha256sum "$archive_name" latest.receipt.json latest.receipt.json.sig \
|
|
>latest.sha256
|
|
sha256sum --check latest.sha256 >/dev/null
|
|
)
|
|
sync "$partial_dir/$archive_name" "$partial_dir/latest.receipt.json" \
|
|
"$partial_dir/latest.receipt.json.sig" "$partial_dir/latest.sha256"
|
|
|
|
mv "$partial_dir/$archive_name" "$target_dir/$archive_name"
|
|
mv "$partial_dir/latest.receipt.json" "$target_dir/latest.receipt.json"
|
|
mv "$partial_dir/latest.receipt.json.sig" "$target_dir/latest.receipt.json.sig"
|
|
mv "$partial_dir/latest.sha256" "$target_dir/latest.sha256"
|
|
rmdir "$partial_dir"
|
|
sync "$target_dir/$archive_name" "$target_dir/latest.receipt.json" \
|
|
"$target_dir/latest.receipt.json.sig" "$target_dir/latest.sha256"
|
|
trap - EXIT
|
|
|
|
(
|
|
cd "$target_dir"
|
|
sha256sum --check latest.sha256 >/dev/null
|
|
)
|
|
openssl dgst -sha256 -verify "$verify_key" \
|
|
-signature "$target_dir/latest.receipt.json.sig" \
|
|
"$target_dir/latest.receipt.json" >/dev/null
|
|
jq -n --arg backup_id "$publication_id" --arg target "$target_dir" \
|
|
--arg archive "$archive_name" \
|
|
'{backup_id: $backup_id, target: $target, archive: $archive, status: "verified"}'
|