#!/usr/bin/env python3 """Restore Fernet-encrypted file backups from S3. Usage: # List available backups python restore_files.py --list # Restore a specific backup python restore_files.py emails.tar.gz.enc --output ./restored_files # Restore from local file python restore_files.py /path/to/backup.tar.gz.enc --output ./restored_files """ import argparse import base64 import hashlib import io import os import sys import tarfile from pathlib import Path import boto3 from cryptography.fernet import Fernet def get_cipher(password: str) -> Fernet: """Create Fernet cipher from password (same derivation as backup.py).""" key_bytes = hashlib.sha256(password.encode()).digest() key = base64.urlsafe_b64encode(key_bytes) return Fernet(key) def list_backups(bucket: str, prefix: str, region: str) -> list[str]: """List available encrypted file backups in S3.""" s3 = boto3.client("s3", region_name=region) try: response = s3.list_objects_v2(Bucket=bucket, Prefix=prefix) except Exception as e: print(f"Error listing S3 bucket: {e}", file=sys.stderr) return [] backups = [] for obj in response.get("Contents", []): key = obj["Key"] if key.endswith(".tar.gz.enc"): name = key.split("/")[-1] size_mb = obj["Size"] / (1024 * 1024) modified = obj["LastModified"].strftime("%Y-%m-%d %H:%M:%S") backups.append(f"{name:40} {size_mb:8.2f} MB {modified}") return backups def download_from_s3( bucket: str, prefix: str, filename: str, region: str ) -> bytes | None: """Download encrypted backup from S3.""" s3 = boto3.client("s3", region_name=region) key = f"{prefix}/{filename}" try: print(f"Downloading s3://{bucket}/{key}...") response = s3.get_object(Bucket=bucket, Key=key) return response["Body"].read() except Exception as e: print(f"Error downloading from S3: {e}", file=sys.stderr) return None def decrypt_and_extract( encrypted_data: bytes, password: str, output_dir: Path ) -> bool: """Decrypt Fernet-encrypted tarball and extract contents.""" cipher = get_cipher(password) try: print("Decrypting...") decrypted = cipher.decrypt(encrypted_data) except Exception as e: print(f"Decryption failed: {e}", file=sys.stderr) print("Check that BACKUP_ENCRYPTION_KEY is correct", file=sys.stderr) return False print(f"Decrypted {len(decrypted)} bytes") try: print(f"Extracting to {output_dir}...") output_dir.mkdir(parents=True, exist_ok=True) tar_buffer = io.BytesIO(decrypted) with tarfile.open(fileobj=tar_buffer, mode="r:gz") as tar: tar.extractall(output_dir) print("Extraction complete") return True except Exception as e: print(f"Extraction failed: {e}", file=sys.stderr) return False def main(): parser = argparse.ArgumentParser( description="Restore Fernet-encrypted file backups from S3" ) parser.add_argument( "backup", nargs="?", help="Backup filename (e.g., emails.tar.gz.enc) or local path", ) parser.add_argument( "--output", "-o", type=Path, default=Path("./restored_files"), help="Output directory for restored files", ) parser.add_argument("--list", "-l", action="store_true", help="List available backups") parser.add_argument( "--bucket", default=os.getenv("S3_BACKUP_BUCKET", "equistamp-memory-backup"), help="S3 bucket name", ) parser.add_argument( "--prefix", default=os.getenv("S3_BACKUP_PREFIX", "Daniel"), help="S3 prefix", ) parser.add_argument( "--region", default=os.getenv("S3_BACKUP_REGION", "eu-central-1"), help="AWS region", ) args = parser.parse_args() # Get encryption key password = os.getenv("BACKUP_ENCRYPTION_KEY") if not password and not args.list: print("Error: BACKUP_ENCRYPTION_KEY environment variable not set", file=sys.stderr) sys.exit(1) # List mode if args.list: print(f"Available backups in s3://{args.bucket}/{args.prefix}/:\n") backups = list_backups(args.bucket, args.prefix, args.region) if backups: print("Name Size Modified") print("-" * 70) for backup in backups: print(backup) else: print("No encrypted backups found") return # Restore mode if not args.backup: parser.print_help() sys.exit(1) # Check if it's a local file or S3 key local_path = Path(args.backup) if local_path.exists(): print(f"Reading local file: {local_path}") encrypted_data = local_path.read_bytes() else: # Download from S3 encrypted_data = download_from_s3( args.bucket, args.prefix, args.backup, args.region ) if not encrypted_data: sys.exit(1) # Decrypt and extract if decrypt_and_extract(encrypted_data, password, args.output): print(f"\nFiles restored to: {args.output.absolute()}") else: sys.exit(1) if __name__ == "__main__": main()