User Tools

Site Tools


howtos:backup_and_restore

This script has two options, backup and restore, which you select during execution.

You need to specify the Bitwarden server and username (email) to use for the backup and restore. If you have an organization select at user which has the ownership of it to get that included as well.

#!/usr/bin/env bash

set -e

SERVER_ADDRESS="https://server.example.com"
EMAIL="me@example.com"

login() {
  if [ -z "$BW_SESSION" ]; then
    bw config server "$SERVER_ADDRESS"
    # -o: This option tells grep to show only the part of a line matching the pattern.
    # -P: This option enables Perl-Compatible Regular Expressions (PCRE) which allows some additional features like lookarounds.
    # (?<=export BW_SESSION=\"): This is a positive lookbehind assertion. It says that the desired match must be preceded by the string export BW_SESSION=".
    # [^\"]*: This matches any number of characters that are not a double quote.
    export BW_SESSION=$(bw login "$EMAIL" | grep -oP "(?<=export BW_SESSION=\")[^\"]*")
  fi
}

organization_exists() {
  bw list organizations | jq -r '.[0].id'
}

perform_backup() {
  EXPORT_NAME="bw-export-$(date "+%Y%m%d-%H%M%S")"
  bw sync --session $BW_SESSION

  bw export --session $BW_SESSION --output "./export/bitwarden.json" --format json
  if [ -n "$BW_ORGANIZATION" ]; then
    bw export --session $BW_SESSION --organizationid $BW_ORGANIZATION --output "./export/bitwarden_org.json" --format json
  fi

  # per entry, check if they contain attachments and then export them
  bash <(bw list items --session $BW_SESSION | jq -r '.[] | select(.attachments != null) | . as $parent | .attachments[] | "bw get attachment --session $BW_SESSION \(.id) --itemid \($parent.id) --output \"./export/attachments/\($parent.id)/\(.fileName)\""')
  if [ -n "$BW_ORGANIZATION" ]; then
    bash <(bw list items --session $BW_SESSION --organizationid $BW_ORGANIZATION| jq -r '.[] | select(.attachments != null) | . as $parent | .attachments[] | "bw get attachment --session $BW_SESSION --organizationid $BW_ORGANIZATION \(.id) --itemid \($parent.id) --output \"./export/attachments_org/\($parent.id)/\(.fileName)\""')
  fi

  # create manifest file
  find export -type f > manifest.txt

  # create an archive with all exported data and manifest
  tar czvf "$EXPORT_NAME.tar.gz" export manifest.txt
  rm -rf export/ manifest.txt

  echo "Backup complete. Archive: $EXPORT_NAME.tar.gz"

  echo "Validating backup..."
  ARCHIVE_NAME="$EXPORT_NAME.tar.gz"
  extract_backup
  validate_backup
  cleanup
  echo "Backup validation successful."
}

extract_backup() {
  TEMP_DIR=$(mktemp -d)
  chmod 700 "$TEMP_DIR"
  tar xzvf "$ARCHIVE_NAME" --directory "$TEMP_DIR" || {
    echo "Error extracting backup file. The archive may be corrupt."
    cleanup
    exit 1
  }
}

validate_backup() {
  if [ ! -f "$TEMP_DIR/export/bitwarden.json" ]; then
    echo "Backup validation failed: bitwarden.json is missing"
    exit 1
  fi

  if [ -n "$BW_ORGANIZATION" ] && [ ! -f "$TEMP_DIR/export/bitwarden_org.json" ]; then
    echo "Backup validation failed: bitwarden_org.json is missing"
    exit 1
  fi

  if ! jq -e . "$TEMP_DIR/export/bitwarden.json" >/dev/null 2>&1; then
    echo "Backup validation failed: bitwarden.json is not valid JSON"
    exit 1
  fi

  if [ -n "$BW_ORGANIZATION" ] && ! jq -e . "$TEMP_DIR/export/bitwarden_org.json" >/dev/null 2>&1; then
    echo "Backup validation failed: bitwarden_org.json is not valid JSON"
    exit 1
  fi

  # Validate manifest file
  if [ ! -f "$TEMP_DIR/manifest.txt" ]; then
    echo "Backup validation failed: manifest.txt is missing"
    exit 1
  fi

  while IFS= read -r line
  do
    if [ ! -f "$TEMP_DIR/$line" ]; then
      echo "Backup validation failed: $line is missing"
      exit 1
    fi
  done < "$TEMP_DIR/manifest.txt"

  # Validate attachments against manifest
  if [ -n "$BW_ORGANIZATION" ]; then
    for file in "$TEMP_DIR/export/attachments_org/"*; do
      for attachment in "$file"/*; do
        if ! grep -qF "export/attachments_org/$(basename "$file")/$(basename "$attachment")" "$TEMP_DIR/manifest.txt"; then
          echo "Backup validation failed: $attachment is missing in manifest"
          exit 1
        fi
      done
    done
  fi

  for file in "$TEMP_DIR/export/attachments/"*; do
    for attachment in "$file"/*; do
      if ! grep -qF "export/attachments/$(basename "$file")/$(basename "$attachment")" "$TEMP_DIR/manifest.txt"; then
        echo "Backup validation failed: $attachment is missing in manifest"
        exit 1
      fi
    done
  done
}

get_backup_file() {
  read -rp "Please enter the path to your backup file: " ARCHIVE_NAME
  if [ ! -f "$ARCHIVE_NAME" ]; then
    echo "File does not exist. Exiting."
    exit 1
  fi
}

import_attachments() {
  if [ -n "$BW_ORGANIZATION" ]; then
    for file in "$TEMP_DIR/export/attachments_org/"*; do
      ID=$(basename "$file")
      for attachment in "$file"/*; do
        bw create attachment "$attachment" --session $BW_SESSION --organizationid $BW_ORGANIZATION --itemid "$ID"
      done
    done
  fi
  for file in "$TEMP_DIR/export/attachments/"*; do
    ID=$(basename "$file")
    for attachment in "$file"/*; do
      bw create attachment "$attachment" --session $BW_SESSION --itemid "$ID"
    done
  done
}

import_entries() {
  bw import json "$TEMP_DIR/export/bitwarden.json" --session $BW_SESSION
  if [ -n "$BW_ORGANIZATION" ]; then
    bw import json "$TEMP_DIR/export/bitwarden_org.json" --session $BW_SESSION --organizationid $BW_ORGANIZATION
  fi
}

cleanup() {
  rm -rf "$TEMP_DIR"
}

logout() {
  bw logout
}

main() {
  echo "What do you want to do?"
  echo "1. Backup"
  echo "2. Restore"
  read -rp "Enter choice: " CHOICE
  login
  export BW_ORGANIZATION=$(organization_exists)

  if [ "$CHOICE" -eq 1 ]; then
    perform_backup
  elif [ "$CHOICE" -eq 2 ]; then
    get_backup_file
    extract_backup
    validate_backup
    import_entries
    import_attachments
    cleanup
  else
    echo "Invalid choice. Exiting."
    exit 1
  fi

  logout
}

main
howtos/backup_and_restore.txt · Last modified: 01/06/2023 18:14 by domingo