Menyimpan Password dengan Benar: Hashing
Kalau database-mu bocor, password pengguna tidak boleh ikut terbaca. Begini cara menyimpannya supaya tetap aman, bahkan dalam skenario terburuk.
Oleh Afa Hernandes6 menit baca
Daftar isi
- Kenapa tidak dienkripsi saja?
- Tiga syarat hash password yang aman
- Node.js: scrypt bawaan, tanpa library tambahan
- Python: Argon2id dengan argon2-cffi
- Bagaimana dengan PHP?
- Memperbarui sistem lama yang memakai MD5
- Hal lain yang sama pentingnya
- Error yang sering muncul
- Pertanyaan yang sering diajukan
- Kalau hashing itu lambat, apakah server saya akan berat?
- Apakah salt harus dirahasiakan?
- Bolehkah saya memakai bcrypt?
- Kesimpulan
Bayangkan database aplikasimu bocor. Ini bukan skenario langka: setiap tahun ada saja layanan besar yang mengalaminya. Pertanyaannya: apa yang didapat penyerang dari kolom password?
Kalau password disimpan apa adanya, penyerang langsung mendapat password semua pengguna. Lebih parah lagi, banyak orang memakai password yang sama di banyak tempat, sehingga kebocoran di aplikasimu bisa membuka email dan rekening mereka. Kalau password disimpan dengan benar, yang didapat penyerang hanya deretan karakter yang tidak bisa dibalik menjadi password.
Di tutorial ini kamu akan memahami cara menyimpan password yang benar dan menerapkannya di Node.js dan Python, dengan kode yang sudah diuji.
Kenapa tidak dienkripsi saja?
Enkripsi bisa dibuka kembali (decrypt) dengan kuncinya. Kalau kuncinya ikut bocor, dan sering kali kunci itu tersimpan di server yang sama, semua password terbuka.
Password tidak perlu dibuka kembali. Saat pengguna login, kamu cukup memeriksa apakah password yang diketik cocok dengan yang tersimpan. Untuk itu dipakai hash: fungsi satu arah yang mengubah password menjadi deretan karakter tetap. Dari password ke hash itu mudah, dari hash kembali ke password praktis mustahil.
Saat login, password yang diketik di-hash dengan cara yang sama, lalu hasilnya dibandingkan dengan hash yang tersimpan.
Tiga syarat hash password yang aman
1. Pakai salt yang acak. Salt adalah nilai acak yang dicampurkan ke password sebelum di-hash, dan berbeda untuk setiap password. Tanpa salt, dua pengguna dengan password 123456 punya hash yang sama, dan penyerang bisa mencocokkan hash dengan daftar hash password populer yang sudah dihitung sebelumnya. Dengan salt, setiap hash unik.
2. Pakai algoritma yang sengaja lambat. Ini terdengar aneh, tapi justru intinya. Hash biasa seperti MD5 atau SHA-256 dirancang untuk cepat: komputer biasa bisa mencoba jutaan sampai miliaran tebakan per detik. Algoritma khusus password dirancang untuk lambat dan boros memori, sehingga setiap tebakan mahal. Bagi pengguna, selisih sepersekian detik saat login tidak terasa. Bagi penyerang yang mencoba miliaran tebakan, perbedaannya sangat besar.
3. Pakai algoritma yang sudah teruji, jangan bikin sendiri. Panduan OWASP (Password Storage Cheat Sheet) merekomendasikan, berurutan:
| Algoritma | Catatan |
|---|---|
| Argon2id | Pilihan pertama |
| scrypt | Kalau Argon2id tidak tersedia. Sudah ada bawaan di Node.js |
| bcrypt | Untuk sistem lama. Password hanya dihitung sampai 72 byte pertama |
Jangan pakai MD5, SHA-1, atau SHA-256 polos untuk password, walaupun ditambah salt. Algoritma itu terlalu cepat. Kalau kamu menemukan kode lama yang memakai md5($password), itu tanda kode tersebut perlu diperbarui.
Node.js: scrypt bawaan, tanpa library tambahan
Node.js punya scrypt di modul crypto. Parameternya mengikuti nilai minimum yang direkomendasikan OWASP (N = 2¹⁷, r = 8, p = 1).
import { scrypt, randomBytes, timingSafeEqual } from 'node:crypto';import { promisify } from 'node:util';
const scryptAsync = promisify(scrypt);// Parameter minimum yang direkomendasikan OWASP untuk scryptconst N = 2 ** 17, r = 8, p = 1;const MAXMEM = 256 * 1024 * 1024; // scrypt dengan N=2^17 butuh ±128 MB memori
export async function hashPassword(password) { const salt = randomBytes(16); // salt acak, berbeda untuk setiap password const hash = await scryptAsync(password.normalize('NFKC'), salt, 64, { N, r, p, maxmem: MAXMEM }); // simpan nama algoritma, parameter, salt, dan hash dalam satu string return ['scrypt', N, r, p, salt.toString('base64'), hash.toString('base64')].join('$');}
export async function verifyPassword(password, tersimpan) { const [alg, n, rr, pp, saltB64, hashB64] = tersimpan.split('$'); if (alg !== 'scrypt') return false; const salt = Buffer.from(saltB64, 'base64'); const hashLama = Buffer.from(hashB64, 'base64'); const opsi = { N: Number(n), r: Number(rr), p: Number(pp), maxmem: MAXMEM }; const hashBaru = await scryptAsync(password.normalize('NFKC'), salt, hashLama.length, opsi); return timingSafeEqual(hashBaru, hashLama); // bandingkan tanpa membocorkan info lewat waktu}Cara memakainya:
import { hashPassword, verifyPassword } from './password.mjs';
const tersimpan = await hashPassword('kopi-susu-123'); // simpan string ini di databaseconsole.log(await verifyPassword('kopi-susu-123', tersimpan)); // trueconsole.log(await verifyPassword('kopi-susu-124', tersimpan)); // falseBeberapa detail penting dari kode ini:
- Parameter dan salt ikut disimpan dalam satu string, misalnya
scrypt$131072$8$1$lLqq…$QJGi…. Kalau suatu saat parameternya dinaikkan, password lama tetap bisa diperiksa. timingSafeEqualmembandingkan dua hash dalam waktu yang sama, apa pun isinya. Perbandingan biasa (===) berhenti di karakter pertama yang berbeda, dan selisih waktunya bisa dimanfaatkan penyerang.normalize('NFKC')menyeragamkan karakter Unicode, sehingga huruf beraksen yang diketik dari perangkat berbeda tetap dianggap sama.
Diuji dengan Node 24: satu kali hash butuh sekitar 200 milidetik di laptop biasa. Dua hash dari password yang sama menghasilkan string berbeda (karena salt-nya berbeda), password benar menghasilkan true, dan password salah menghasilkan false.
Python: Argon2id dengan argon2-cffi
Untuk Python, pakai library argon2-cffi, yang menerapkan Argon2id dengan parameter bawaan yang aman:
pip install argon2-cffifrom argon2 import PasswordHasherfrom argon2.exceptions import VerifyMismatchError
ph = PasswordHasher() # Argon2id dengan parameter bawaan yang aman
def hash_password(password: str) -> str: return ph.hash(password) # salt acak sudah otomatis disertakan
def cek_password(password: str, hash_tersimpan: str) -> bool: try: ph.verify(hash_tersimpan, password) return True except VerifyMismatchError: return FalseHasil ph.hash() berbentuk seperti $argon2id$v=19$m=65536,t=3,p=4$…. Nama algoritma, parameter, salt, dan hash-nya tersimpan dalam satu string. Parameter bawaannya (memori 64 MiB, 3 iterasi, 4 jalur paralel) di atas batas minimum yang direkomendasikan OWASP.
Library ini juga punya ph.check_needs_rehash(hash_tersimpan). Fungsi ini memberi tahu apakah hash dibuat dengan parameter lama. Kalau hasilnya True, hash ulang password itu saat pengguna berhasil login, karena hanya saat itulah kamu memegang password aslinya.
Diuji dengan Python 3.13 dan argon2-cffi 25.1.
Bagaimana dengan PHP?
PHP sudah menyediakan fungsi yang tepat sejak lama: password_hash() dan password_verify(). Pakai password_hash($password, PASSWORD_ARGON2ID) kalau server-mu mendukungnya, atau PASSWORD_DEFAULT (bcrypt). Kalau kamu menemukan md5() atau sha1() untuk password di kode lama, ganti dengan fungsi ini.
Memperbarui sistem lama yang memakai MD5
Kamu tidak bisa mengubah hash MD5 yang sudah tersimpan menjadi Argon2id, karena password aslinya tidak diketahui. Cara yang umum dipakai:
- Tambahkan kolom atau penanda untuk jenis hash.
- Saat pengguna login dan password-nya cocok dengan hash MD5 lama, langsung hash ulang password itu dengan Argon2id atau scrypt, lalu simpan hasilnya.
- Untuk akun yang lama tidak login, minta mereka mengatur ulang password.
Hal lain yang sama pentingnya
Hashing yang benar hanya satu bagian. Lengkapi dengan:
- Batasi percobaan login, supaya password tidak bisa ditebak terus-menerus lewat form login.
- Wajibkan HTTPS, supaya password tidak bisa diintip saat dikirim. Lihat Domain, DNS, dan HTTPS.
- Jangan pernah mencatat password ke log, termasuk saat login gagal.
- Jangan batasi panjang password terlalu pendek. Izinkan password panjang dan frasa (passphrase), karena jauh lebih kuat daripada aturan “harus ada simbol”.
- Pertimbangkan layanan atau library autentikasi yang matang daripada membangun sistem login dari nol. Hashing hanyalah satu dari banyak hal yang harus benar di sistem login.
Error yang sering muncul
Node: RangeError: Invalid scrypt params: … memory limit exceeded.
Parameter scrypt dengan N = 2¹⁷ butuh sekitar 128 MB memori, sedangkan batas bawaan Node lebih kecil. Tambahkan opsi maxmem, seperti di contoh di atas.
Python: ModuleNotFoundError: No module named 'argon2'.
Nama paket yang dipasang argon2-cffi, tapi nama modul yang di-import argon2. Pastikan kamu memasangnya di virtual environment yang sama dengan yang dipakai untuk menjalankan aplikasi.
Login selalu gagal setelah migrasi. Pastikan kolom database cukup panjang untuk menampung seluruh string hash (minimal 255 karakter cukup aman), dan tidak ada spasi atau karakter yang terpotong saat disimpan.
Pertanyaan yang sering diajukan
Kalau hashing itu lambat, apakah server saya akan berat?
Hashing hanya terjadi saat mendaftar, login, dan mengganti password, bukan di setiap permintaan. Untuk aplikasi kecil sampai menengah, ini tidak masalah. Justru karena itulah percobaan login perlu dibatasi: supaya penyerang tidak bisa memakai form login untuk membebani server.
Apakah salt harus dirahasiakan?
Tidak. Salt disimpan bersama hash-nya dan boleh diketahui. Tugasnya bukan rahasia, tapi membuat setiap hash unik, sehingga penyerang harus menebak satu per satu.
Bolehkah saya memakai bcrypt?
Boleh, terutama kalau sistemmu sudah memakainya. Perhatikan batasnya: bcrypt hanya menghitung 72 byte pertama dari password. Untuk sistem baru, Argon2id atau scrypt lebih disarankan.
Kesimpulan
Password tidak dienkripsi, tapi di-hash: satu arah, dengan salt acak, memakai algoritma yang sengaja lambat seperti Argon2id atau scrypt. Simpan parameter bersama hash-nya, bandingkan dengan fungsi yang aman terhadap serangan waktu, dan jangan pernah memakai MD5 atau SHA polos.
Langkah berikutnya di jalur Security: celah yang paling sering membuat database bocor, SQL injection.



