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:
-
Menggunakan
@hybrid_property
-
Menggunakan
TypeDecorator
-
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 melakukanINSERT
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 pakaipgp_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
ataucurrent_setting
, karena kalau salah set, maka hasil dekripsi bisaNULL
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 denganbase64_decode()
.”
Ini salah kaprah:
-
base64
adalah encoding, bukan enkripsi. -
md5()
dansha1()
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.