forked from michael.heier/citadel-core
171 lines
5.5 KiB
Bash
Executable File
171 lines
5.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# SPDX-FileCopyrightText: 2020 Umbrel. https://getumbrel.com
|
|
#
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
set -euo pipefail
|
|
|
|
CITADEL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../..)"
|
|
BACKUP_ROOT="${CITADEL_ROOT}/.backup/$RANDOM"
|
|
BACKUP_FOLDER_NAME="backup"
|
|
BACKUP_FOLDER_PATH="${BACKUP_ROOT}/${BACKUP_FOLDER_NAME}"
|
|
BACKUP_FILE="${BACKUP_ROOT}/backup.tar.gz.pgp"
|
|
BACKUP_STATUS_FILE="${CITADEL_ROOT}/statuses/backup-status.json"
|
|
|
|
check_dependencies () {
|
|
for cmd in "$@"; do
|
|
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
echo "This script requires \"${cmd}\" to be installed"
|
|
exit 1
|
|
fi
|
|
done
|
|
}
|
|
|
|
check_dependencies openssl tar gpg shuf curl
|
|
|
|
# Deterministically derives 128 bits of cryptographically secure entropy
|
|
derive_entropy () {
|
|
identifier="${1}"
|
|
citadel_seed=$(cat "${CITADEL_ROOT}/db/citadel-seed/seed") || true
|
|
|
|
if [[ -z "$citadel_seed" ]] || [[ -z "$identifier" ]]; then
|
|
>&2 echo "Missing derivation parameter, this is unsafe, exiting."
|
|
exit 1
|
|
fi
|
|
|
|
# We need `sed 's/^.* //'` to trim the "(stdin)= " prefix from some versions of openssl
|
|
printf "%s" "${identifier}" | openssl dgst -sha256 -hmac "${citadel_seed}" | sed 's/^.* //'
|
|
}
|
|
|
|
[[ -f "${CITADEL_ROOT}/.env" ]] && source "${CITADEL_ROOT}/.env"
|
|
BITCOIN_NETWORK=${BITCOIN_NETWORK:-mainnet}
|
|
|
|
echo "Deriving keys..."
|
|
|
|
backup_id=$(derive_entropy "citadel_backup_id")
|
|
encryption_key=$(derive_entropy "citadel_backup_encryption_key")
|
|
|
|
echo "Creating backup..."
|
|
|
|
if [[ ! -f "${CITADEL_ROOT}/lnd/data/chain/bitcoin/${BITCOIN_NETWORK}/channel.backup" ]]; then
|
|
echo "No channel.backup file found, skipping backup..."
|
|
exit 1
|
|
fi
|
|
|
|
mkdir -p "${BACKUP_FOLDER_PATH}"
|
|
|
|
cp --archive "${CITADEL_ROOT}/lnd/data/chain/bitcoin/${BITCOIN_NETWORK}/channel.backup" "${BACKUP_FOLDER_PATH}/channel.backup"
|
|
|
|
# We want to back up user settings too, however we currently store the encrypted
|
|
# mnemonic in this file which is not safe to backup remotely.
|
|
# Uncomment this in the future once we've ensured there's no critical data in
|
|
# this file.
|
|
# cp --archive "${CITADEL_ROOT}/db/user.json" "${BACKUP_FOLDER_PATH}/user.json"
|
|
|
|
echo "Adding random padding..."
|
|
|
|
# Up to 10KB of random binary data
|
|
# This prevents the server from being able to tell if the backup has increased
|
|
# decreased or stayed the sme size. Combined with random interval decoy backups
|
|
# this makes a (already very difficult) timing analysis attack to correlate backup
|
|
# activity with channel state changes practically impossible.
|
|
padding="$(shuf -i 0-10240 -n 1)"
|
|
dd if=/dev/urandom bs="${padding}" count=1 > "${BACKUP_FOLDER_PATH}/.padding"
|
|
|
|
echo "Creating encrypted tarball..."
|
|
|
|
tar \
|
|
--create \
|
|
--gzip \
|
|
--verbose \
|
|
--directory "${BACKUP_FOLDER_PATH}/.." \
|
|
"${BACKUP_FOLDER_NAME}" \
|
|
| gpg \
|
|
--batch \
|
|
--symmetric \
|
|
--cipher-algo AES256 \
|
|
--passphrase "${encryption_key}" \
|
|
--output "${BACKUP_FILE}"
|
|
|
|
# To decrypt:
|
|
# cat "${BACKUP_FILE}" | gpg \
|
|
# --batch \
|
|
# --decrypt \
|
|
# --passphrase "${encryption_key}" \
|
|
# | tar \
|
|
# --extract \
|
|
# --verbose \
|
|
# --gzip
|
|
|
|
# Check if a file exists on Firebase by checking if
|
|
# "https://firebasestorage.googleapis.com/v0/b/citadel-user-backups.appspot.com/o/backups%2F<THE_FILE_NAME>?alt=media"
|
|
# returns a 200 response code.
|
|
#
|
|
check_if_exists() {
|
|
curl -s -o /dev/null -w "%{http_code}" \
|
|
-X GET \
|
|
"https://firebasestorage.googleapis.com/v0/b/citadel-user-backups.appspot.com/o/backups%2F${1}?alt=media"
|
|
}
|
|
|
|
# Upload a file to Firebase Cloud Storage by uloading its name + a random ID.
|
|
# Before uploading, we need to check if a file with the same name already exists, and if it does, change the random ID.
|
|
# For example, if we want to upload a file named "<THE_FILE_NAME>" with the content AA, we'd use this command to upload:
|
|
# curl -X POST "https://firebasestorage.googleapis.com/v0/b/citadel-user-backups.appspot.com/o/backups%2F<THE_FILE_NAME>?alt=media" -d "AA" -H "Content-Type: text/plain"
|
|
# To check if a file exists, we can check if this endpoint returns an error 404:
|
|
# curl "https://firebasestorage.googleapis.com/v0/b/citadel-user-backups.appspot.com/o/backups%2F<THE_FILE_NAME>?alt=media"
|
|
# To download a file, we can the same endpoint
|
|
upload_file() {
|
|
local file_to_send="${1}"
|
|
local file_name="${2}"
|
|
# A random ID to avoid collisions
|
|
local random_id="$(tr -dc A-Za-z0-9 </dev/urandom | head -c 60 ; echo '')"
|
|
# Check if the file already exists
|
|
# While a file with the same name exists, we'll try to upload it with a different ID
|
|
while [[ $(check_if_exists "${file_name}-${random_id}") == "200" ]]; do
|
|
random_id="$(tr -dc A-Za-z0-9 </dev/urandom | head -c 60 ; echo '')"
|
|
done
|
|
# Upload the file
|
|
curl -X POST \
|
|
"https://firebasestorage.googleapis.com/v0/b/citadel-user-backups.appspot.com/o/backups%2F${file_name}-${random_id}?alt=media" \
|
|
-d @"${file_to_send}" \
|
|
-H "Content-Type: application/octet-stream" \
|
|
> /dev/null
|
|
}
|
|
|
|
if [[ $BITCOIN_NETWORK == "testnet" ]]; then
|
|
rm -rf "${BACKUP_ROOT}"
|
|
cat <<EOF > ${BACKUP_STATUS_FILE}
|
|
{"status": "skipped", "timestamp": $(date +%s000)}
|
|
EOF
|
|
exit
|
|
fi
|
|
if [[ $BITCOIN_NETWORK == "regtest" ]]; then
|
|
rm -rf "${BACKUP_ROOT}"
|
|
cat <<EOF > ${BACKUP_STATUS_FILE}
|
|
{"status": "skipped", "timestamp": $(date +%s000)}
|
|
EOF
|
|
exit
|
|
fi
|
|
|
|
echo "Uploading backup..."
|
|
if upload_file "${BACKUP_FILE}" "${backup_id}"; then
|
|
status="success"
|
|
else
|
|
status="failed"
|
|
fi
|
|
echo
|
|
|
|
rm -rf "${BACKUP_ROOT}"
|
|
|
|
# Update status file
|
|
cat <<EOF > ${BACKUP_STATUS_FILE}
|
|
{"status": "${status}", "timestamp": $(date +%s000)}
|
|
EOF
|
|
|
|
echo "============================="
|
|
echo "====== Backup ${status} ======="
|
|
echo "============================="
|
|
|
|
exit 0
|