citadel-core/scripts/backup/backup
2021-11-06 17:03:57 +00:00

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