ngodingdulu
SecurityTutorialPemula

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

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).

password.mjs
import { scrypt, randomBytes, timingSafeEqual } from 'node:crypto';
import { promisify } from 'node:util';
const scryptAsync = promisify(scrypt);
// Parameter minimum yang direkomendasikan OWASP untuk scrypt
const 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:

contoh.mjs
import { hashPassword, verifyPassword } from './password.mjs';
const tersimpan = await hashPassword('kopi-susu-123'); // simpan string ini di database
console.log(await verifyPassword('kopi-susu-123', tersimpan)); // true
console.log(await verifyPassword('kopi-susu-124', tersimpan)); // false

Beberapa 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.
  • timingSafeEqual membandingkan 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:

Terminal window
pip install argon2-cffi
password.py
from argon2 import PasswordHasher
from 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 False

Hasil 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:

  1. Tambahkan kolom atau penanda untuk jenis hash.
  2. Saat pengguna login dan password-nya cocok dengan hash MD5 lama, langsung hash ulang password itu dengan Argon2id atau scrypt, lalu simpan hasilnya.
  3. 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.

Bagikan:WhatsAppTelegramXFacebook
Ilustrasi input email berisi tanda kutip OR 1=1 yang membuat query rentan membocorkan semua pengguna termasuk admin, dibandingkan query aman dengan placeholder yang mengembalikan nol baris
SecurityTutorial

SQL Injection: Cara Kerja dan Pencegahannya

Cukup satu tanda kutip di kolom login untuk membuka seluruh isi tabel. Lihat sendiri cara kerjanya, lalu tutup celahnya dengan satu kebiasaan sederhana.

9 Maret 20266 mnt

Ilustrasi kotak komentar berisi kode img onerror yang ditampilkan sebagai teks dengan textContent, dan form dari situs penyerang yang gagal mengirim cookie bertanda SameSite=Lax
SecurityTutorial

XSS dan CSRF dengan Contoh Nyata

Dua serangan yang memanfaatkan browser penggunamu sendiri. Kami mencobanya di Chrome sungguhan, supaya kamu tahu persis mana yang aman dan mana yang tidak.

23 Maret 20266 mnt

Enter bukaEsc tutupHalaman pencarian