diff --git a/db/umbrel-seed/.gitkeep b/db/umbrel-seed/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml index 6721f09..0613a73 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,8 @@ services: - ${PWD}/tor/torrc:/etc/tor/torrc - ${PWD}/tor/data:/var/lib/tor/ - ${PWD}/tor/run:/var/run/tor/ + ports: + - "127.0.0.1:9150:29050" networks: net: ipv4_address: 10.11.5.1 @@ -97,6 +99,7 @@ services: DOCKER_COMPOSE_DIRECTORY: $PWD DEVICE_HOST: ${DEVICE_HOST:-http://umbrel.local} MIDDLEWARE_API_URL: "http://10.11.2.2" + UMBREL_SEED_FILE: "/db/umbrel-seed/seed" UMBREL_DASHBOARD_HIDDEN_SERVICE_FILE: "/var/lib/tor/web/hostname" BITCOIN_P2P_HIDDEN_SERVICE_FILE: "/var/lib/tor/bitcoin-p2p/hostname" BITCOIN_P2P_PORT: $BITCOIN_P2P_PORT diff --git a/events/triggers/backup b/events/triggers/backup new file mode 100755 index 0000000..c6b5d33 --- /dev/null +++ b/events/triggers/backup @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../..)" + +"${UMBREL_ROOT}/scripts/backup/backup" diff --git a/scripts/backup/backup b/scripts/backup/backup new file mode 100755 index 0000000..a0418da --- /dev/null +++ b/scripts/backup/backup @@ -0,0 +1,126 @@ +#!/usr/bin/env bash + +set -euo pipefail + +UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../..)" +BACKUP_FOLDER="backup" +BACKUP_ROOT="${UMBREL_ROOT}/${BACKUP_FOLDER}" +BACKUP_FILE="${UMBREL_ROOT}/backup.tar.gz.pgp" + +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}" + umbrel_seed=$(cat "${UMBREL_ROOT}/db/umbrel-seed/seed") || true + + if [[ -z "$umbrel_seed" ]] || [[ -z "$identifier" ]]; then + >&2 echo "Missing derivation parameter, this is unsafe, exiting." + rm -f "${UMBREL_ROOT}/statuses/backup-in-progress" + exit 1 + fi + + # We need `sed 's/^.* //'` to trim the "(stdin)= " prefix from some versions of openssl + printf "%s" "${identifier}" | openssl dgst -sha256 -hmac "${umbrel_seed}" | sed 's/^.* //' +} + +# Make sure an update is not in progres +if [[ -f "${UMBREL_ROOT}/statuses/backup-in-progress" ]]; then + echo "A backup is already in progress. Exiting now." + exit 1 +fi + +echo "Creating lock..." +touch "${UMBREL_ROOT}/statuses/backup-in-progress" + +[[ -f "${UMBREL_ROOT}/.env" ]] && source "${UMBREL_ROOT}/.env" +BITCOIN_NETWORK=${BITCOIN_NETWORK:-mainnet} + +[[ -d "${BACKUP_ROOT}" ]] && rm -rf "${BACKUP_ROOT}" +[[ -f "${BACKUP_FILE}" ]] && rm -f "${BACKUP_FILE}" + +echo "Deriving keys..." + +backup_id=$(derive_entropy "umbrel_backup_id") +encryption_key=$(derive_entropy "umbrel_backup_encryption_key") + +echo "Creating backup..." + +mkdir -p "${BACKUP_ROOT}" + +cp --archive "${UMBREL_ROOT}/lnd/data/chain/bitcoin/${BITCOIN_NETWORK}/channel.backup" "${BACKUP_ROOT}/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 "${UMBREL_ROOT}/db/user.json" "${BACKUP_ROOT}/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_ROOT}/.padding" + +echo "Creating encrypted tarball..." + +tar \ + --create \ + --gzip \ + --verbose \ + --directory "${UMBREL_ROOT}" \ + "${BACKUP_FOLDER}" \ + | 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 + +BACKUP_API_URL="https://pvf3ozmmfl.execute-api.us-east-1.amazonaws.com/prod/v1/upload" + +if [[ $BITCOIN_NETWORK == "testnet" ]]; then + BACKUP_API_URL="https://as0ot0lg7h.execute-api.us-east-1.amazonaws.com/dev/v1/upload" +fi +if [[ $BITCOIN_NETWORK == "regtest" ]]; then + BACKUP_API_URL="https://5fxwqbum7g.execute-api.us-east-1.amazonaws.com/dev/v1/upload" +fi + +echo "Uploading backup..." +curl --socks5 localhost:9150 -F "file=@/${BACKUP_FILE}" "${BACKUP_API_URL}/${backup_id}" +echo + +rm -rf "${BACKUP_ROOT}" +rm -f "${BACKUP_FILE}" + +echo "Removing lock..." +rm -f "${UMBREL_ROOT}/statuses/backup-in-progress" + +echo "=============================" +echo "===== Backup successful =====" +echo "=============================" + +exit 0 diff --git a/scripts/backup/decoy-trigger b/scripts/backup/decoy-trigger new file mode 100755 index 0000000..97493b5 --- /dev/null +++ b/scripts/backup/decoy-trigger @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +set -euo pipefail + +UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../..)" + +check_if_not_already_running() { + if ps ax | grep $0 | grep -v $$ | grep bash | grep -v grep + then + echo "decoy trigger is already running" + exit 1 + fi +} + +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_if_not_already_running + +check_dependencies shuf + +main () { + while true; do + minutes_in_seconds="60" + hours_in_seconds="$((${minutes_in_seconds} * 10))" + max_interval="$((8 * ${hours_in_seconds}))" + delay="$(shuf -i 0-${max_interval} -n 1)" + echo "Sleeping for ${delay} seconds..." + sleep $delay + echo "Triggering decoy backup..." + touch "${UMBREL_ROOT}/events/signals/backup" + done +} + +main diff --git a/scripts/backup/monitor b/scripts/backup/monitor new file mode 100755 index 0000000..d201c0e --- /dev/null +++ b/scripts/backup/monitor @@ -0,0 +1,69 @@ +#!/usr/bin/env bash + +check_root () { + if [[ $UID != 0 ]]; then + echo "Error: This script must be run as root." + exit 1 + fi +} + +check_if_not_already_running() { + if ps ax | grep $0 | grep -v $$ | grep bash | grep -v grep + then + echo "backup monitor is already running" + exit 1 + fi +} + +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_root + +check_if_not_already_running + +check_dependencies fswatch readlink dirname + +UMBREL_ROOT="$(dirname $(readlink -f "${BASH_SOURCE[0]}"))/../.." + +monitor_file () { + local file_path="${1}" + echo "Monitoring $file_path" + echo + + if [[ ! -e "${file_path}" ]]; then + echo "$file_path doesn't exist, waiting for it to be created..." + echo + until [[ -e "${file_path}" ]]; do + echo "Nope, $file_path still doesn't exist..." + sleep 1 + done + touch "${UMBREL_ROOT}/events/signals/backup" + fi + + fswatch -0 --event Updated $file_path | xargs -0 -n 1 -I {} touch "${UMBREL_ROOT}/events/signals/backup" +} + +if [[ ! -d "${UMBREL_ROOT}" ]]; then + echo "Root dir does not exist '$UMBREL_ROOT'" + exit 1 +fi + +[[ -f "${UMBREL_ROOT}/.env" ]] && source "${UMBREL_ROOT}/.env" +BITCOIN_NETWORK=${BITCOIN_NETWORK:-mainnet} + +# Monitor LND channel.backup +monitor_file "${UMBREL_ROOT}/lnd/data/chain/bitcoin/${BITCOIN_NETWORK}/channel.backup" & + +# Monitor db/user.json +# 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. +# monitor_file "${UMBREL_ROOT}/db/user.json" & diff --git a/scripts/start b/scripts/start index 923e64e..7c644f1 100755 --- a/scripts/start +++ b/scripts/start @@ -63,6 +63,14 @@ echo "Starting karen..." echo ./karen & +echo "Starting backup monitor..." +echo +./scripts/backup/monitor & + +echo "Starting decoy backup trigger..." +echo +./scripts/backup/decoy-trigger & + echo echo "Starting Docker services..." echo