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