Belajar 2FA Authentication - Menyimpan Secret dengan Aman
Episode 7 of 23

Belajar 2FA Authentication - Menyimpan Secret dengan Aman

Episode ini menjelaskan mengapa secret TOTP tidak boleh disimpan plaintext, cara mengenkripsinya dengan AES-256-GCM memakai kunci dari environment MFA_ENCRYPTION_KEY, serta praktik satu secret per user dan logging yang tidak pernah menyertakan secret atau kode.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Pendahuluan

Secret TOTP adalah kunci digital akun pengguna. Jika bocor, penyerang bisa menghitung kode yang sama persis dengan Google Authenticator pengguna — perbedaan dengan bocornya password adalah secret tidak bisa diganti pengguna, dan bahkan lebih jarang kesadaran pengguna untuk menanganinya. Episode 7 membahas penyimpanan secret yang aman.

Kalian akan belajar mengenkripsi secret Base32 dengan AES-256-GCM, mengelola kunci enkripsi lewat variabel environment MFA_ENCRYPTION_KEY, dan menerapkan praktik penyimpanan serta logging yang benar. Di akhir episode, secret di database tidak lagi dapat dibaca siapa pun tanpa kunci.

Mengapa Secret Tidak Boleh Plaintext

Risiko Kebocoran Database

Secret yang disimpan mentah sama bahayanya dengan password mentah. Jika database bocor — lewat SQL injection, backup yang hilang, atau akses tidak sah — penyerang langsung bisa memprovisioning akun di Google Authenticator miliknya dan login sebagai pengguna. Enkripsi memastikan bahwa bahkan data yang bocor tidak bisa dipakai tanpa kunci yang hanya dimiliki aplikasi.

Catatan penting: enkripsi melindungi saat data berhenti, bukan saat data sedang digunakan. Sekali secret didekripsi di memori untuk verifikasi, secret itu rentan terhadap kompromi proses — itulah mengapa secret juga tidak boleh muncul di log.

Pertahanan Berlapis

Enkripsi secret adalah satu lapis dalam pertahanan berlapis. Di atasnya masih ada rate limiting (episode 10), transport HTTPS (episode 13), dan sesi yang pendek (episode 15). Setiap lapis menaikkan biaya serangan; secret terenkripsi memastikan kebocoran database saja tidak cukup untuk masuk.

Enkripsi AES-256-GCM

Prinsip AES-256-GCM

AES-256-GCM adalah enkripsi symmetric dengan kunci 32 byte yang juga menyediakan autentikasi — ciphertext yang dirusak akan ditolak saat dekripsi, bukan menghasilkan data rusak yang tidak disadari. Mode GCM menghasilkan initial vector (IV) acak per enkripsi yang harus disimpan bersama ciphertext. Contoh fungsi enkripsi dan dekripsi:

JSEnkripsi dan dekripsi secret TOTP
const crypto = require('crypto');
 
const ALGO = 'aes-256-gcm';
const key = Buffer.from(process.env.MFA_ENCRYPTION_KEY, 'hex');
 
function encryptSecret(secret) {
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv(ALGO, key, iv);
  const enc = Buffer.concat([cipher.update(secret, 'utf8'), cipher.final()]);
  const tag = cipher.getAuthTag();
  return { iv: iv.toString('hex'), tag: tag.toString('hex'), data: enc.toString('hex') };
}
 
function decryptSecret(stored) {
  const decipher = crypto.createDecipheriv(ALGO, key, Buffer.from(stored.iv, 'hex'));
  decipher.setAuthTag(Buffer.from(stored.tag, 'hex'));
  const dec = Buffer.concat([decipher.update(Buffer.from(stored.data, 'hex')), decipher.final()]);
  return dec.toString('utf8');
}

Perhatikan bahwa crypto.createCipheriv memerlukan IV baru setiap enkripsi — memakai ulang IV dengan kunci yang sama adalah kesalahan kriptografi yang umum dan berbahaya. Simpan iv dan tag bersama data, karena keduanya diperlukan saat dekripsi.

Kunci dari Environment MFA_ENCRYPTION_KEY

Kunci enkripsi 32 byte disimpan sebagai hex di variabel environment:

Konfigurasi kunci enkripsi di .env.local
MFA_ENCRYPTION_KEY=9a3c2f4e1d0b7c6a5f8e9d0c1b2a3f4e5d6c7b8a9f0e1d2c3b4a5f6e7d8c9a0b
SESSION_SECRET=umumkanPanjangAcakTidakMudahDitebak
DATABASE_URL=postgresql://user:pass@localhost:5432/devvnull

Jangan pernah menaruh nilai ini di source control. Gunakan secret manager atau KMS di produksi (episode 20), dan simpan cadangan kunci di lokasi terpisah — kehilangan MFA_ENCRYPTION_KEY berarti semua secret 2FA tidak bisa didekripsi lagi.

Kunci Tidak Pernah di Source Control

Aturan sederhana: file berakhiran .env* masuk ke .gitignore, dan MFA_ENCRYPTION_KEY tidak boleh muncul di log, dokumentasi yang ter-commit, atau README. Putar kunci hanya dengan strategi yang mendukung dua kunci secara bersamaan, agar pengguna bisa dimigrasi satu per satu tanpa lockout massal.

Info

Rotasi kunci enkripsi memerlukan dua kunci aktif: kunci baru untuk data baru dan kunci lama untuk mendekripsi data yang belum dimigrasi. Proses migrasi berjalan bertahap, akun demi akun, sehingga tidak ada pengguna yang terkunci karena datanya dienkripsi dengan kunci yang sudah tidak dikenali.

Format Penyimpanan di Database

Kolom dan Struktur Data

Secret terenkripsi disimpan sebagai satu string yang menggabungkan IV, tag autentikasi, dan ciphertext. Contoh nilai kolom totp_secret_encrypted berbentuk tiga segmen hex yang dipisahkan titik dua — iv, tag, dan data. Ketiga nilai ini wajib disimpan utuh karena dekripsi membutuhkan semuanya sekaligus.

Gunakan tipe kolom TEXT atau VARCHAR yang cukup panjang. Hindari menyimpan secret dalam bentuk yang bisa dibaca saat proses debugging database, dan pastikan backup database ikut terenkripsi seperti data lainnya.

Praktik Penyimpanan yang Benar

Satu Secret per User

Setiap user memegang tepat satu secret aktif. Regenerasi secret — misalnya saat pengguna curiga perangkatnya hilang — harus mengganti secret lama dan membatalkan seluruh session autentikator sebelumnya. Alur reset dan rotasi ini dibahas penuh di episode 11.

Pastikan operasi reset 2FA selalu menuntut bukti identitas kuat, misalnya ulang password atau recovery code yang masih valid.

Log Tanpa Secret dan Kode

Log aplikasi sering menjadi sumber kebocoran yang paling diremehkan. Aturan yang wajib diingat: jangan pernah mencatat secret Base32, kode TOTP, atau URI provisioning lengkap ke log. Catat hanya peristiwa seperti mfa_enabled, mfa_disabled, dan mfa_failed tanpa data sensitif. Episode 20 akan merancang observability yang aman untuk ini.

Contoh baris log yang aman: mfa_enabled user=uuid — cukup untuk audit tanpa menyentuh data rahasia. Alur observability lengkapnya akan dibahas di episode 20.

Penutup

Episode 7 menjelaskan mengapa secret TOTP harus terenkripsi, cara kerja enkripsi AES-256-GCM dengan kunci dari MFA_ENCRYPTION_KEY, dan praktik penyimpanan satu secret per user dengan log yang bebas secret.

Inti yang harus dibawa pulang:

  • Secret plaintext di database sama berbahayanya dengan password plaintext.
  • AES-256-GCM mengenkripsi sekaligus mengautentikasi ciphertext.
  • Gunakan IV acak baru untuk setiap enkripsi.
  • MFA_ENCRYPTION_KEY 32 byte disimpan hanya di environment atau KMS.
  • Jangan pernah menyimpan kunci enkripsi di source control.
  • Jangan log secret, kode TOTP, atau URI provisioning.

Di episode 8 selanjutnya kita akan membahas login flow dengan 2FA challenge — memisahkan endpoint /login dan /login/mfa, menandai status pendingMfa, dan memastikan protected route tidak bisa dilewati sebelum tantangan 2FA selesai.

Belajar 2FA Authentication - Menyimpan Secret dengan Aman | Belajar 2FA Authentication