15%

Encrypt at Rest: Menjaga Kerahasiaan Data hingga ke Level Disk

29-Jun-2025

Dalam dunia yang serba cloud dan API, keamanan bukan lagi fitur tambahan—ia adalah fondasi. Salah satu praktik keamanan yang dianggap sebagai baseline oleh OWASP adalah Encrypt at Rest: mengenkripsi data saat disimpan di disk atau database. Ini berbeda dari encrypt in transit (saat data dikirim via HTTPS). Tanpa encrypt at rest, semua backup database adalah harta karun yang siap dijarah jika jatuh ke tangan yang salah.

Analogi: Perang Dunia dan Pesan Radio

Saat Perang Dunia II, Jerman mengirim pesan militer lewat radio. Sekutu bisa menangkap sinyalnya, tapi tidak bisa membaca isinya.
Bukan karena mereka tidak bisa bahasa Jerman, tapi karena pesan tersebut dienkripsi.
Hanya tentara Jerman yang punya kunci untuk membacanya.

Inilah konsep dasar enkripsi: melindungi makna walaupun pesan jatuh ke tangan lawan. Dalam konteks aplikasi, backup SQL bisa disalin siapa saja, tapi hanya aplikasi (yang punya secret key) yang bisa membaca data aslinya.

Kenapa Encrypt at Rest Penting?

Tanpa enkripsi:

  • DevOps yang melakukan pg_dump bisa melihat semua data sensitif.

  • Database administrator bisa mengakses data pengguna tanpa otorisasi.

  • Attacker internal hanya perlu akses ke file .sql untuk melihat nomor KTP, email, dan password hash.

Dengan Encrypt at Rest:

  • Data disimpan dalam bentuk terenkripsi.

  • Hanya aplikasi — dengan secret key — yang dapat melakukan dekripsi.

  • Backup tetap aman, bahkan jika bocor.

Implementasi Encrypt at Rest dengan SQLAlchemy

Kita akan eksplorasi 3 pendekatan:

  1. Menggunakan @hybrid_property

  2. Menggunakan TypeDecorator

  3. Native PostgreSQL pgcrypto

1. Hybrid Property — Pythonik dan Transparan

from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.orm import declarative_base, sessionmaker
from cryptography.fernet import Fernet

Base = declarative_base()
secret_key = Fernet.generate_key()
cipher = Fernet(secret_key)

class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    _email = Column("email", String)

    @hybrid_property
    def email(self):
        return cipher.decrypt(self._email.encode()).decode()

    @email.setter
    def email(self, value):
        self._email = cipher.encrypt(value.encode()).decode()

Demo

engine = create_engine("sqlite:///:memory:")
Session = sessionmaker(bind=engine)
session = Session()
Base.metadata.create_all(engine)

user = User(email="mail@example.com")
session.add(user)
session.commit()

stored_user = session.query(User).first()
print("Email (decrypted):", stored_user.email)
print("Email (raw/encrypted):", stored_user._email)

Output

Email (decrypted): mail@example.com  
Email (raw/encrypted): gAAAAA...

2. TypeDecorator — Reusable untuk Banyak Kolom

Lihat kode sebelumnya. Dengan @hybrid_property, masalah enkripsi sudah selesai — kita bisa simpan data terenkripsi, lalu baca ulang dalam bentuk asli. Tapi bagaimana kalau kita ingin kolom lain seperti phone_number, address, atau bahkan kolom di tabel lain juga dienkripsi dengan cara yang sama?

Tentu saja kita harus menulis @hybrid_property dan setter satu per satu. Ini tidak praktis.

Nah, TypeDecorator membuat hal ini lebih sederhana. Kita cukup buat satu kelas enkripsi, dan tinggal pakai seperti tipe data biasa.

from sqlalchemy.types import TypeDecorator, String

class EncryptedString(TypeDecorator):
    impl = String

    def process_bind_param(self, value, dialect):
        if value is None:
            return None
        return cipher.encrypt(value.encode()).decode()

    def process_result_value(self, value, dialect):
        if value is None:
            return None
        return cipher.decrypt(value.encode()).decode()

Contoh Penggunaan:


class User2(Base):
    __tablename__ = 'user2'
    id = Column(Integer, primary_key=True)
    email = Column(EncryptedString(255))

Demo:

Base.metadata.create_all(engine)

user = User2(email="encrypted@example.com")
session.add(user)
session.commit()

stored_user = session.query(User2).first()
print("Email (decrypted):", stored_user.email)
print("Email (raw/encrypted):", session.execute("SELECT email FROM user2").fetchone()[0])

3. Native PostgreSQL: pgcrypto — Keamanan di Level SQL

TypeDecorator cukup keren, terutama kalau kita ingin kendali penuh di level aplikasi. Tapi... bagaimana kalau kita balik pertanyaannya:

Bagaimana jika kita tidak ingin aplikasi ikut-ikutan urus enkripsi?
Bagaimana jika enkripsi harus tetap jalan meskipun programmer frontend atau backend melakukan INSERT langsung dari DBeaver, pgadmin atau lainnya?
Bagaimana kalau kita ingin tanggung jawab enkripsi ini dipindahkan ke database itu sendiri?

Inilah saatnya kita gunakan PostgreSQL extension: pgcrypto.

Aktivasi

Pertama, aktifkan dulu ekstensi-nya:

CREATE EXTENSION IF NOT EXISTS pgcrypto;

Contoh Tabel dan Insert

CREATE TABLE users_pg (
    id SERIAL PRIMARY KEY,
    email BYTEA
);

Sekarang kita insert data, tapi bukan dengan plaintext. Kita pakai fungsi enkripsi simetris dari pgcrypto:

INSERT INTO users_pg (email)
VALUES (pgp_sym_encrypt('pgcrypto@example.com', 'my_secret_key'));

my_secret_key ini tentu jangan ditaruh hardcoded. Di production, key sebaiknya berasal dari environment variable yang di-inject ke koneksi.

Baca dan Dekripsi

SELECT
    pgp_sym_decrypt(email, 'my_secret_key') AS email
FROM users_pg;

 

Hasilnya:

email
------------------------
pgcrypto@example.com

Kenapa Ini Menarik?

  • Tidak peduli siapa yang melakukan INSERT — selama pakai pgp_sym_encrypt(), data pasti terenkripsi.

  • Tidak peduli siapa yang backup database — data tetap dalam bentuk ciphertext.

  • Bahkan DevOps, DBA, atau attacker internal tidak bisa membaca data tanpa key.

Dengan kata lain: enkripsi tidak tergantung pada kode aplikasi. Bahkan kalau backend-nya di-rewrite ulang dari Python ke Go, ke Node.js, atau ke Rust, asalkan key tetap sama, data tetap aman dan bisa didekripsi.

Kekurangan?

Tentu saja ada.

  • Enkripsi jadi melekat pada engine PostgreSQL. Portabilitas ke MySQL atau SQLite hilang.

  • Anda harus hati-hati dalam mengatur secret key di SET atau current_setting, karena kalau salah set, maka hasil dekripsi bisa NULL atau error.

  • Tidak semua query builder ORM mendukung fungsi pgp_sym_encrypt() secara eksplisit.

Ada artikel terkenal yang berjudul “6 Fungsi Enkripsi di PHP” dari PetaniKode yang membingungkan pembaca:

“Fungsi base64_encode() akan menghasilkan kode hash dari teks dan bisa dikembalikan ke bentuk semula dengan base64_decode().”

Ini salah kaprah:

  • base64 adalah encoding, bukan enkripsi.

  • md5() dan sha1() adalah hashing, bukan dua arah.

  • hash cocok untuk password, bukan untuk menyimpan data.

Gunakan enkripsi simetris seperti AES (Fernet) untuk data yang perlu dibaca ulang, dan hashing kuat (bcrypt/argon2) untuk password.

Encrypt at Rest bukan soal paranoid. Ini soal profesionalitas. Kalau Anda mengizinkan siapa saja yang punya akses ke database untuk membaca data sensitif, berarti Anda sedang membuka pintu ke insiden.

Hanya aplikasi yang memegang secret key yang boleh memahami isi data.
Sama seperti hanya Jerman yang bisa membaca pesan radio mereka di medan perang.

Referensi:

Topik : sql python
Similar Posts

Komentar (0)

Tinggalkan Komentar