Pemulihan manual dari backup tanpa Databasus

Backup bukan hanya soal melindungi data, tetapi juga memulihkannya. Databasus dirancang agar backup tetap bisa dipulihkan meskipun VPS yang menjalankan Databasus terhapus, kamu kehilangan akses, atau antarmukanya tidak bisa dibuka. Kamu tidak memerlukan Databasus untuk memulihkan backup karena file disimpan dalam format standar, tanpa ketergantungan pada vendor.

Yang perlu disiapkan

Untuk memulihkan backup secara manual, siapkan:

  • File backup dari penyimpanan (penyimpanan lokal, S3, Azure Blob Storage, dan lainnya)
  • File metadata dari penyimpanan yang sama. Namanya sama dengan file backup, tetapi menggunakan ekstensi .metadata.
  • Secret key dari ./databasus-data/secret.key (berada di direktori yang sama dengan file backup, biasanya /opt/databasus/)

Struktur file

Setiap backup terdiri dari dua file di penyimpanan lokal atau cloud:

  • {database-name}-{timestamp}-{backup-id} - Data backup yang dienkripsi dan dikompresi
  • {database-name}-{timestamp}-{backup-id}.metadata - File JSON berisi detail enkripsi

File metadata berisi salt enkripsi dan IV (nonce) dalam format Base64:

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

Dekripsi

Databasus menggunakan enkripsi AES-256-GCM dengan derivasi kunci PBKDF2. Setiap backup memiliki kunci enkripsi unik yang diturunkan dari:

  • Master key (dari file secret.key)
  • ID backup
  • Salt acak (disimpan dalam metadata)

Gunakan skrip Python berikut untuk mendekripsi backup:

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
    )

Instal dependency yang diperlukan:

pip install pycryptodome

Cara menggunakan skrip:

  1. Simpan skrip di atas ke sebuah file (misalnya decrypt_backup.py)
  2. Ubah parameter pada bagian contoh penggunaan di paling bawah
  3. Jalankan skrip:
python decrypt_backup.py

Skrip otomatis membuat file hasil dengan awalan decrypted_. Misalnya, jika file backup bernama backup-id.dump, file yang sudah didekripsi akan bernama decrypted_backup-id.dump.

Restore ke database

Setelah dekripsi, lakukan restore menggunakan tool yang sesuai dengan database:

PostgreSQL

Backup PostgreSQL menggunakan kompresi bawaan dan bisa langsung di-restore:

Database lokal:

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

Database remote:

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

MySQL

Backup MySQL dikompresi dengan zstd level 5 dan wajib didekompresi sebelum restore.

Langkah 1: Dekompresi backup

Gunakan tool command-line zstd atau tool dekompresi yang kompatibel (7-Zip, PeaZip, WinRAR, dan lainnya):

# 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

Langkah 2: Restore ke database

Database lokal:

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

Database remote:

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

MariaDB

Backup MariaDB dikompresi dengan zstd level 5 dan wajib didekompresi sebelum restore.

Langkah 1: Dekompresi backup

Gunakan tool command-line zstd atau tool dekompresi yang kompatibel (7-Zip, PeaZip, WinRAR, dan lainnya):

# 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

Langkah 2: Restore ke database

Database lokal:

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

Database remote:

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

MongoDB

Backup MongoDB menggunakan kompresi gzip bawaan dan bisa langsung di-restore:

Database lokal:

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

Database remote:

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

Bagaimana jika ada kendala?

Jika menemui masalah selama pemulihan:

  • Minta bantuan AI. Asisten AI seperti ChatGPT, Claude, atau Gemini sangat membantu dalam penggunaan tool kompresi dan prosedur restore database. Jelaskan kendalanya agar mereka bisa memandu prosesnya.
  • Bergabung dengan komunitas. Developer dan anggota komunitas kami bisa membantu menangani kasusmu.