Restaurer PostgreSQL depuis une sauvegarde sans Databasus

Sauvegarder, ce n'est pas seulement protéger les données. C'est aussi pouvoir les restaurer. Databasus veille à ce que vos backups restent récupérables même si le VPS qui héberge Databasus est supprimé, si vous avez perdu l'accès ou si l'interface est inaccessible pour une raison quelconque. Vous n'avez donc pas besoin de Databasus pour restaurer vos backups : ils sont stockés dans un format standard et il n'y a aucun verrouillage propriétaire.

Ce dont vous avez besoin

Pour restaurer manuellement une sauvegarde, il vous faut :

  • Le fichier de sauvegarde depuis votre stockage (stockage local, S3, Google Drive, etc.)
  • Le fichier de métadonnées depuis le même stockage. Il porte le même nom que le fichier de sauvegarde, avec l'extension .metadata en plus.
  • La clé secrète depuis ./databasus-data/secret.key (située dans le même répertoire que les fichiers de sauvegarde, généralement /opt/databasus/)

Structure des fichiers

Chaque sauvegarde se compose de deux fichiers placés dans votre stockage (local ou cloud) :

  • {database-name}-{timestamp}-{backup-id} : les données de sauvegarde chiffrées et compressées
  • {database-name}-{timestamp}-{backup-id}.metadata : un fichier JSON avec les détails de chiffrement

Le fichier de métadonnées contient le sel de chiffrement et l'IV (nonce) au format Base64 :

{
  "backupId": "550e8400-e29b-41d4-a716-446655440000",
  "encryptionSalt": "base64-encoded-salt",
  "encryptionIV": "base64-encoded-nonce",
  "encryption": "encrypted"
}

Déchiffrement

Databasus utilise le chiffrement AES-256-GCM avec dérivation de clé PBKDF2. Chaque sauvegarde a une clé de chiffrement unique dérivée de :

  • La clé maîtresse (depuis le fichier secret.key)
  • L'ID de la sauvegarde
  • Un sel aléatoire (stocké dans les métadonnées)

Utilisez ce script Python pour déchiffrer votre sauvegarde :

import json
import base64
import struct
import os
from Crypto.Cipher import AES
from Crypto.Protocol.KDF import PBKDF2
from Crypto.Hash import SHA256

# Constants from Databasus encryption
MAGIC_BYTES = b"PGRSUS01"
HEADER_LENGTH = 64
CHUNK_SIZE = 1024 * 1024
PBKDF2_ITERATIONS = 100000


def decrypt_backup(backup_file, metadata_file, master_key):
    """
    Decrypt a Databasus backup file using metadata and master key.

    Args:
        backup_file: Path to encrypted backup file
        metadata_file: Path to metadata JSON file
        master_key: Master key from ./databasus-data/secret.key
    """
    # Validate files exist
    if not os.path.exists(backup_file):
        print(f"Error: Backup file not found: {backup_file}")
        return

    if not os.path.exists(metadata_file):
        print(f"Error: Metadata file not found: {metadata_file}")
        return

    # Read metadata
    with open(metadata_file, "r") as f:
        metadata = json.load(f)

    # Check if file is encrypted (case-insensitive check)
    encryption_status = metadata.get("encryption", "").upper()
    if encryption_status != "ENCRYPTED":
        print(
            f"Error: Backup is not encrypted (encryption status: {metadata.get('encryption')})"
        )
        print("No decryption needed. You can decompress/restore the file directly.")
        return

    backup_id = metadata["backupId"]
    salt = base64.b64decode(metadata["encryptionSalt"])
    iv = base64.b64decode(metadata["encryptionIV"])

    # Generate output filename with decrypted_ prefix
    backup_dir = os.path.dirname(backup_file) or "."
    backup_name = os.path.basename(backup_file)
    output_file = os.path.join(backup_dir, f"decrypted_{backup_name}")

    # Derive encryption key using PBKDF2
    key_material = (master_key + backup_id).encode("utf-8")
    derived_key = PBKDF2(
        key_material, salt, dkLen=32, count=PBKDF2_ITERATIONS, hmac_hash_module=SHA256
    )

    try:
        with open(backup_file, "rb") as f_in, open(output_file, "wb") as f_out:
            # Read and validate header
            header = f_in.read(HEADER_LENGTH)

            # Validate magic bytes
            magic = header[:8]
            if magic != MAGIC_BYTES:
                raise ValueError(
                    f"Invalid magic bytes: expected {MAGIC_BYTES}, got {magic}"
                )

            # Decrypt chunks
            chunk_index = 0
            while True:
                # Read chunk length (4 bytes)
                length_bytes = f_in.read(4)
                if not length_bytes:
                    break

                chunk_length = struct.unpack(">I", length_bytes)[0]

                # Read encrypted chunk
                encrypted_chunk = f_in.read(chunk_length)
                if not encrypted_chunk:
                    break

                # Generate chunk nonce (base IV + chunk index)
                chunk_nonce = bytearray(iv)
                chunk_nonce[4:12] = struct.pack(">Q", chunk_index)

                # Create cipher for this chunk
                chunk_cipher = AES.new(derived_key, AES.MODE_GCM, nonce=bytes(chunk_nonce))

                # Decrypt chunk
                try:
                    decrypted_chunk = chunk_cipher.decrypt_and_verify(
                        encrypted_chunk[:-16],  # ciphertext
                        encrypted_chunk[-16:],  # auth tag
                    )
                except ValueError as e:
                    if "MAC check failed" in str(e):
                        print("\nError: Failed to decrypt backup (MAC check failed)")
                        print("This usually means:")
                        print("  - The master key is incorrect")
                        print("  - The backup file is corrupted")
                        print("  - The metadata doesn't match this backup file")
                        print(f"\nFailed at chunk {chunk_index}")
                        raise
                    raise

                # Write decrypted data
                f_out.write(decrypted_chunk)
                chunk_index += 1

        print(f"Successfully decrypted {chunk_index} chunks to {output_file}")

    except ValueError as e:
        # Clean up partial output file after files are closed
        if "MAC check failed" in str(e) and os.path.exists(output_file):
            os.remove(output_file)
        return


# Example usage:
if __name__ == "__main__":
    decrypt_backup(
        backup_file="./your-backup-file",             # <--- change this to your backup file
        metadata_file="./your-backup-file.metadata",  # <--- change this to your metadata file
        master_key="your-master-key-here",            # <--- change this to your master key
    )

Installez les dépendances requises :

pip install pycryptodome

Comment utiliser le script :

  1. Enregistrez le script ci-dessus dans un fichier (par exemple decrypt_backup.py)
  2. Modifiez les paramètres dans la section d'exemple d'utilisation en bas du script
  3. Lancez le script :
python decrypt_backup.py

Le script créera automatiquement le fichier de sortie avec le préfixe decrypted_. Par exemple, si votre fichier de sauvegarde est backup-id.dump, le fichier déchiffré sera decrypted_backup-id.dump.

Restaurer dans la base de données

Après le déchiffrement, restaurez avec les outils propres à chaque base :

PostgreSQL

Les backups PostgreSQL utilisent la compression intégrée et peuvent être restaurés directement :

Base de données locale :

# Restore to local database
pg_restore -d your_database decrypted-backup.dump

Base de données distante :

# Restore to remote database
pg_restore -h hostname -p 5432 -U username -d database_name decrypted-backup.dump

MySQL

Les backups MySQL sont compressés avec zstd niveau 5 et doivent être décompressés avant la restauration.

Étape 1 : décompresser la sauvegarde

Utilisez l'outil en ligne de commande zstd ou tout outil de décompression compatible (7-Zip, PeaZip, WinRAR, etc.) :

# Decompress with zstd command-line tool
zstd -d decrypted-backup.sql.zst -o decrypted-backup.sql

# Or use graphical tools like 7-Zip, PeaZip, or WinRAR

Étape 2 : restaurer dans la base de données

Base de données locale :

# Restore to local database
mysql your_database < decrypted-backup.sql

Base de données distante :

# Restore to remote database
mysql -h hostname -P 3306 -u username -p database_name < decrypted-backup.sql

MariaDB

Les backups MariaDB sont compressés avec zstd niveau 5 et doivent être décompressés avant la restauration.

Étape 1 : décompresser la sauvegarde

Utilisez l'outil en ligne de commande zstd ou tout outil de décompression compatible (7-Zip, PeaZip, WinRAR, etc.) :

# Decompress with zstd command-line tool
zstd -d decrypted-backup.sql.zst -o decrypted-backup.sql

# Or use graphical tools like 7-Zip, PeaZip, or WinRAR

Étape 2 : restaurer dans la base de données

Base de données locale :

# Restore to local database
mariadb your_database < decrypted-backup.sql

Base de données distante :

# Restore to remote database
mariadb -h hostname -P 3306 -u username -p database_name < decrypted-backup.sql

MongoDB

Les backups MongoDB utilisent la compression gzip intégrée et peuvent être restaurés directement :

Base de données locale :

# Restore to local database
mongorestore --archive=decrypted-backup.archive --gzip --db your_database

Base de données distante :

# Restore to remote database
mongorestore --host hostname:27017 --username username --password password \
  --archive=decrypted-backup.archive --gzip --db database_name

Que faire en cas de problème ?

Si vous rencontrez un problème pendant la restauration :

  • Demandez de l'aide à une IA. Les assistants IA comme ChatGPT, Claude ou Gemini sont très efficaces pour aider avec les outils de compression et les procédures de restauration de bases de données. Décrivez simplement votre problème et ils vous guideront pas à pas.
  • Rejoignez notre communauté. Nos développeurs et les membres de la communauté peuvent vous aider dans votre cas particulier.