Kamu adalah developer yang mengelola aplikasi Laravel dengan ribuan pengguna terdaftar. Suatu pagi, timnya mendapat laporan: database bocor. Tabel users yang berisi email dan password milik semua pengguna telah menjadi milik penyerang.
Di sini bedanya antara aplikasi yang bertahan dengan aplikasi yang hancur total tergantung pada satu keputusan yang dibuat lama: bagaimana password disimpan.
Jika password disimpan sebagai plaintext — teks asli tanpa pengubahan apa pun — penyerang segera memiliki akses ke semua akun. Bahkan lebih buruk, banyak pengguna menggunakan password yang sama di berbagai layanan, jadi kebocoran di aplikasi kamu bisa merusak akun email, media sosial, hingga rekening bank mereka di tempat lain.
Tetapi jika password disimpan sebagai hash, penyerang hanya mendapatkan rentetan karakter acak yang tidak dapat dikembalikan ke password aslinya. Tim kamu mendapat waktu untuk memaksa reset password, menutup celah, dan mengurangi kerusakan sebelum lebih parah.
Skenario ini bukan hipotesis kosong. Kebocoran database sudah menjadi risiko standar bagi setiap aplikasi yang menyimpan kredensial pengguna. Password hashing adalah salah satu baris pertahanan terakhir yang menentukan seberapa parah dampaknya.
Dalam artikel ini, kamu akan mempelajari apa itu password hashing, mengapa plaintext dan algoritma lama seperti MD5 serta SHA-1 berbahaya, perbedaan hashing dengan enkripsi, bagaimana bcrypt dan Argon2id bekerja, serta cara implementasi yang aman menggunakan password_hash() native PHP dan Facade Hash Laravel. Artikel juga membahas salt, pepper, cost factor, batasan 72 byte bcrypt, timing attack, dan rehash-on-login.
Apa Itu Password Hashing?
Password hashing adalah proses mengubah password teks asli (plaintext) menjadi representasi string tetap panjang yang disebut hash (atau digest) menggunakan fungsi hash satu arah. Dari hash, tidak ada cara untuk menyusun kembali password aslinya. Fungsi ini tidak reversible.
Verifikasi dilakukan di sisi lain. Saat pengguna login, aplikasi mengambil password yang dikirimkan, menghasilkan hash dari password tersebut dengan fungsi yang sama, lalu mencocokkan hasilnya dengan hash yang tersimpan di database. Jika cocok, password benar. Aplikasi tidak pernah perlu mengetahui — atau menyimpan — password asli pengguna.
Verifikasi ini merupakan bagian dari alur authentication (autentikasi) pada aplikasi. Jika kamu ingin memahami alur login secara keseluruhan, artikel authentication membahasnya lebih lengkap.
Tujuannya sederhana: jika database bocor, penyerang hanya mendapatkan hash. Memecahkan hash kembali menjadi password membutuhkan waktu dan biaya komputasi yang tinggi, terutama jika menggunakan algoritma modern seperti bcrypt atau Argon2id. Ini memberi waktu aplikasi dan pengguna untuk merespons, misalnya dengan memaksa ganti password.
Hashing Tidak Sama dengan Enkripsi
Kesalahan umum yang kerap dilakukan developer pemula adalah menganggap hashing dan enkripsi hal yang sama. Keduanya mengubah data menjadi acak, tetapi tujuan dan sifatnya berbeda.
Enkripsi adalah dua arah. Data asli diubah menjadi ciphertext dengan menggunakan kunci, dan data tersebut dapat dikembalikan ke bentuk asli (dekripsi) dengan kunci yang sama atau pasangan. Enkripsi cocok untuk data yang memang perlu dibaca kembali oleh sistem di kemudian hari, seperti nomor kartu kredit, token API, atau data identitas pribadi.
Password bukan data tersebut. Password tidak pernah perlu ditampilkan kembali ke siapa pun, termasuk administrator. Jika aplikasi bisa mendekripsi password kembali ke bentuk asli, maka siapa pun yang merampas kunci enkripsi — misalnya dari file .env, konfigurasi server, atau source code — bisa mendekripsi semua password pengguna. Dalam kebocoran database, enkripsi dua arah pada password hanya menunda waktu sampai kunci ditemukan.
Hashing adalah satu arah. Tidak ada kunci yang dapat mengubah hash kembali menjadi plaintext. Verifikasi hanya membandingkan hash baru dengan hash lama. Itulah sebabnya hashing, bukan enkripsi, adalah standar industri untuk penyimpanan password.
Contoh algoritma enkripsi: AES, RSA. Contoh algoritma hashing untuk password: bcrypt, Argon2id, PBKDF2, dan scrypt.
Mengapa Plaintext, MD5, SHA-1, dan Enkripsi Dua Arah Tidak Aman
Penyimpanan password plaintext adalah risiko langsung. Saat data breach, semua password pengguna langsung terekspos. Penyerang dapat login sebagai siapa saja atau mencoba credential stuffing ke layanan lain.
MD5 dan SHA-1 juga tidak aman untuk password. Keduanya adalah fast hash — dirancang untuk integritas data, bukan penyimpanan kredensial. GPU modern dapat menghitung miliaran hash MD5 atau SHA-1 per detik, sehingga serangan brute force dan rainbow table sangat efektif. Jika dua pengguna memiliki password yang sama, hash MD5-nya juga sama, memungkinkan penyerang menggunakan tabel hasil hash yang telah dicetak sebelumnya.
SHA-256 dan fungsi hash cepat lainnya memiliki masalah yang sama. Terlalu cepat. Untuk password, kita butuh fungsi yang sengaja memakan waktu dan sumber daya, dikenal sebagai key stretching.
Enkripsi dua arah seperti AES atau RSA tidak cocok untuk password karena ketergantungan pada kunci. Jika kunci server bocor, seluruh password dapat didekripsi. Administrator sistem juga seharusnya tidak memiliki kemampuan untuk melihat password asli pengguna.
Jadi, hindari menyimpan password sebagai plaintext, MD5, SHA-1, SHA-256, atau dengan enkripsi dua arah.
Algoritma Hashing Modern: bcrypt, Argon2id, dan Argon2i
bcrypt
bcrypt dibangun berdasarkan cipher Blowfish. Di PHP, output hash dimulai dengan $2y$ dan selalu berupa 60 karakter.
bcrypt memiliki cost factor, juga disebut work factor. Setiap peningkatan nilai cost menggandakan jumlah pekerjaan yang diperlukan untuk membuat dan memverifikasi hash. Semakin tinggi cost, semakin lama prosesnya, dan semakin sulit bagi penyerang melakukan brute force.
Rentang cost valid adalah 4 sampai 31. Default cost PHP adalah 10 sebelum versi 8.4, dan 12 sejak PHP 8.4. Laravel menggunakan default 12 melalui pengaturan BCRYPT_ROUNDS.
bcrypt juga memiliki batasan yang perlu kamu ketahui: input password dibatasi 72 byte. Karakter setelah 72 byte pertama diabaikan dalam sebagian besar implementasi, sehingga password yang sangat panjang dapat kehilangan sebagian entropinya. OWASP menempatkan bcrypt sebagai pilihan kuat setelah Argon2id untuk sistem yang tidak dapat menggunakan Argon2id.
Argon2id
Argon2id adalah pemenang Password Hashing Competition 2015 dan ditetapkan dalam RFC 9106. Ini adalah kombinasi dari Argon2i dan Argon2d, sehingga tahan terhadap serangan side-channel dan serangan berbasis GPU sekaligus. OWASP merekomendasikan Argon2id sebagai pilihan utama untuk penyimpanan password.
Argon2id adalah memory-hard. Menebak password memerlukan sejumlah memori yang besar, sehingga menjadikan serangan massal pada GPU atau hardware khusus menjadi mahal dan tidak efisien.
Tiga parameter biaya utamanya adalah:
- memory_cost (m): jumlah memori yang digunakan, dalam KiB.
- time_cost (t): jumlah pass atau iterasi.
- threads (p): jumlah lane atau paralelisme.
Contoh konfigurasi minimum OWASP adalah m=19456 (sekitar 19 MiB), t=2, dan p=1. Tersedia di PHP sejak versi 7.3.0 dengan konstanta PASSWORD_ARGON2ID, asalkan PHP dikompilasi dengan dukungan Argon2 atau libsodium.
Argon2i
Argon2i tersedia di PHP sejak 7.2.0 dengan konstanta PASSWORD_ARGON2I. Tahan terhadap side-channel, tetapi kurang tahan terhadap serangan GPU dibanding Argon2id. OWASP tidak merekomendasikan Argon2i untuk penyimpanan password. Jika memilih antara keduanya, gunakan Argon2id.
PBKDF2 dan scrypt (Singkat)
PBKDF2 adalah opsi jika diperlukan kompatibilitas FIPS-140. OWASP merekomendasikan work factor 600.000 iterasi atau lebih dengan HMAC-SHA-256. PBKDF2 bukan memory-hard.
scrypt adalah alternatif memory-hard jika Argon2id tidak tersedia di lingkungan kamu.
Memilih Algoritma
Untuk sistem baru, gunakan Argon2id jika tersedia di server kamu. bcrypt dengan cost minimal 10 (12 disarankan) adalah alternatif yang andal dan tersedia di semua build PHP. Hindari MD5, SHA-1, SHA-256, dan enkripsi dua arah untuk menyimpan password.
Komponen Keamanan: Salt dan Pepper
Salt
Salt adalah string acak yang digabungkan dengan password sebelum di-hash. Setiap pengguna memiliki salt yang berbeda, sehingga dua pengguna dengan password yang sama akan menghasilkan hash yang berbeda. Salt mencegah penggunaan tabel hasil hash tercetak sebelumnya, dikenal sebagai rainbow table.
password_hash() dan Hash::make() di Laravel menghasilkan salt secara otomatis menggunakan CSPRNG sistem, lalu menyematkannya dalam string hash itu sendiri. Salt tidak perlu — dan tidak disarankan — untuk disimpan di kolom terpisah atau diberikan secara manual. Opsi salt pada password_hash() sudah usang sejak PHP 7.0 dan diabaikan sepenuhnya sejak PHP 8.0.
Biarkan PHP atau Laravel yang mengurus salt. Jangan mencoba membuat salt sendiri.
Pepper
Pepper adalah nilai rahasia global tingkat aplikasi atau server, misalnya disimpan di .env, environment variable, atau KMS. Pepper digabungkan dengan password sebelum di-hash dan disimpan terpisah dari database.
Perbedaan kunci antara salt dan pepper adalah lokasi penyimpanan: salt disimpan bersama hash di database, sementara pepper disimpan di luar database. Fungsi pepper adalah defense-in-depth. Bahkan jika database bocor dan penyerang mendapatkan semua hash, penyerang tanpa pepper masih kesulitan mencrack password.
OWASP menyebut pepper sebagai lapisan pertahanan tambahan. Pepper sendiri tidak bisa menggantikan hashing yang baik, tetapi memberikan lapisan keamanan tambahan jika database terkompromi.
Implementasi Praktis di PHP Native
PHP menyediakan sejumlah fungsi bawaan untuk password hashing yang sudah menangani detail keamanan seperti salt dan cost factor secara otomatis.
Membuat Hash dengan password_hash()
<?php
$password = 'rahasia123'; // asumsikan input sudah divalidasi
// Menghasilkan hash bcrypt dengan cost default
$hash = password_hash($password, PASSWORD_DEFAULT);
// Simpan $hash ke kolom `password` di tabel pengguna
PASSWORD_DEFAULT menggunakan bcrypt. Kamu juga bisa menentukan algoritma secara eksplisit, misalnya PASSWORD_BCRYPT atau PASSWORD_ARGON2ID.
Opsi yang tersedia tergantung algoritma. Untuk bcrypt, opsi cost mengatur cost factor. Untuk Argon2, opsi memory_cost, time_cost, dan threads. Contoh:
<?php
$hash = password_hash($password, PASSWORD_ARGON2ID, [
'memory_cost' => 19456,
'time_cost' => 2,
'threads' => 1,
]);
Simpan hash di kolom database minimal 255 karakter. Panjang hash dapat bervariasi tergantung algoritma default di masa depan, jadi jangan gunakan kolom yang terlalu sempit.
Sebelum PHP 8.0, password_hash() mengembalikan false jika gagal. Sejak PHP 8.0, fungsi ini melempar ValueError.
Memverifikasi Password dengan password_verify()
<?php
$hash = $user->password; // diambil dari database
if (password_verify($password, $hash)) {
// Password cocok, login berhasil
} else {
// Password salah
}
password_verify() aman terhadap timing attack karena menggunakan perbandingan konstan waktu. Waktu respons tidak bergantung pada seberapa dekat password input dengan hash yang tersimpan. Jangan pernah membandingkan hash dengan == atau ===, karena itu membuka peluang penyerang menebak password berdasarkan selisih waktu respons server.
Hash yang dihasilkan password_hash() sudah mengandung semua informasi yang diperlukan untuk verifikasi — algoritma, cost, salt, dan hash itu sendiri — dalam satu string. Kamu tidak perlu menyimpan data tambahan saat memverifikasi.
Upgrade Hash Secara Transparan dengan password_needs_rehash()
Ketika cost factor dinaikkan atau algoritma default diubah, hash yang lama masih valid, tetapi tidak sesuai standar terbaru. Untuk menangani ini tanpa memaksa semua pengguna mengubah password sekaligus, gunakan teknik rehash-on-login.
<?php
if (password_verify($password, $hash) && password_needs_rehash($hash, PASSWORD_DEFAULT)) {
$newHash = password_hash($password, PASSWORD_DEFAULT);
// Simpan $newHash ke database, timpa hash lama
}
Alurnya: verifikasi password saat login. Jika valid dan password_needs_rehash() mengembalikan true, buat hash baru dengan parameter terbaru dan simpan ke database. Pengguna yang tidak aktif akan mendapatkan hash baru secara bertahap setiap kali mereka login.
Fungsi Pendukung
password_get_info($hash) mengembalikan array yang berisi algoritma, nama algoritma, dan opsi yang digunakan. password_algos() mengembalikan daftar algoritma yang didukung oleh build PHP saat ini. bcrypt selalu tersedia; Argon2 tergantung pada kompilasi PHP dan dukungan libsodium.
Implementasi Praktis di Laravel Framework
Laravel menyediakan Facade Hash yang mengelola bcrypt dan Argon2 dengan aman. Driver default adalah bcrypt, dikonfigurasi melalui config/hashing.php.
Membuat dan Memverifikasi Hash
<?php
namespace App\Http\Controllers\Auth;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class RegisteredUserController
{
public function store(Request $request)
{
$validated = $request->validate([
'email' => 'required|email|unique:users',
'password' => 'required|string|max:128|confirmed',
]);
$user = User::create([
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
]);
return redirect()->route('dashboard');
}
}
Hash::make() menghasilkan hash dengan driver yang dikonfigurasi (bcrypt secara default). Untuk verifikasi saat login:
<?php
$user = User::where('email', $request->email)->first();
if (! $user || ! Hash::check($request->password, $user->password)) {
// Kredensial salah
}
// Login berhasil
Hash::check() menggunakan perbandingan konstan waktu dan aman terhadap timing attack. Perlu diperhatikan: Hash::check() akan melempar RuntimeException jika algoritma hash tidak cocok dengan driver aplikasi. Ini adalah pencegahan terhadap manipulasi algoritma.
Konfigurasi config/hashing.php
Berikut adalah pengaturan default Laravel:
return [
'driver' => 'bcrypt',
'bcrypt' => [
'rounds' => env('BCRYPT_ROUNDS', 12),
],
'argon' => [
'memory' => env('ARGON_MEMORY', 65536),
'threads' => env('ARGON_THREADS', 1),
'time' => env('ARGON_TIME', 4),
],
'rehash_on_login' => true,
];
Variabel environment yang relevan: BCRYPT_ROUNDS, ARGON_MEMORY, ARGON_TIME, ARGON_THREADS, dan opsi rehash_on_login.
rehash_on_login ketika diaktifkan, biasanya mengotomatisasi rehash hash lama saat pengguna login berhasil, tergantung implementasi guard yang digunakan. Starter kit Laravel seperti Breeze, Jetstream, dan Fortify menggunakan bcrypt untuk registrasi dan autentikasi secara default.
Rehash di Laravel
<?php
if (Hash::check($password, $user->password) && Hash::needsRehash($user->password)) {
$user->update([
'password' => Hash::make($password),
]);
}
Hash::needsRehash() memeriksa apakah work factor telah berubah. Biasanya dipakai dalam alur login untuk rehash otomatis.
Timing Attack dan Rehash-on-Login
Timing attack adalah serangan di mana penyerang menganalisis selisih waktu respons server untuk menebak karakter password secara bertahap. password_verify() dan Hash::check() mencegah hal ini dengan perbandingan konstan waktu. Karena alasan itu, jangan pernah mengganti verifikasi hash dengan == atau ===.
Rehash-on-login adalah teknik untuk mengupgrade hash lama secara transparan. Saat pengguna login berhasil dan fungsi rehash mengembalikan true, buat hash baru dengan parameter terbaru dan timpa hash lama di database. Hash dari pengguna yang tidak aktif akan diupgrade seiring dengan login mereka ke aplikasi. Ini memungkinkan kamu meningkatkan cost factor atau mengganti algoritma tanpa mengganggu semua pengguna sekaligus.
Batasan Penting: 72 Byte bcrypt dan Panjang Password
bcrypt hanya memproses 72 byte pertama dari password. Sisa karakter diabaikan dalam sebagian besar implementasi. Ini berarti password yang lebih panjang dari 72 byte dapat kehilangan entropi di bagian akhirnya, dan situasinya lebih parah jika password mengandung karakter multibyte (UTF-8), karena satu karakter dapat menempati lebih dari satu byte.
Mitigasinya: batasi panjang password pada aplikasi, misalnya maksimal 64 hingga 128 karakter, atau gunakan Argon2id yang tidak memiliki batasan 72 byte. Jika kamu menggunakan bcrypt, pastikan validasi panjang password sudah diterapkan di sisi aplikasi sebelum hash dibuat.
Masalah Umum yang Harus Dihindari
Beberapa kesalahan fatal yang masih kerap ditemukan:
- Menyimpan password plaintext di database.
- Menggunakan
md5(),sha1(), atausha256()untuk password. - Menggunakan enkripsi dua arah seperti AES untuk menyimpan password.
- Membuat algoritma hash sendiri. Jangan pernah mengimplementasikan kriptografi sendiri.
- Memberikan salt secara manual, statis, atau hardcoded dalam source code.
- Cost factor terlalu rendah di bawah 10, atau terlalu tinggi sehingga menyebabkan Denial of Service karena kehabisan CPU.
- Melewati batas 72 byte bcrypt tanpa penanganan.
- Membandingkan hash dengan
==atau===. - Tidak melakukan rehash saat cost factor atau algoritma ditingkatkan.
- Menyimpan pepper di database bersama hash, sehingga menghilangkan nilai defense-in-depthnya.
- Menggunakan Argon2i sendirian untuk password. Pilih Argon2id.
Best Practices & Checklist Keamanan
Jika kamu ingin memeriksa aplikasi PHP atau Laravel yang sudah ada, berikut adalah daftar tindakan yang relevan:
- Gunakan Argon2id untuk sistem baru, atau bcrypt dengan cost minimal 10 (12 disarankan) sebagai alternatif andal.
- Biarkan runtime menghasilkan salt secara otomatis. Jangan simpan salt di kolom terpisah atau berikan salt manual.
- Simpan hash di kolom minimal 255 karakter.
- Gunakan
password_verify()atauHash::check()untuk verifikasi. Hindari perbandingan string biasa. - Terapkan rehash-on-login dengan
password_needs_rehash()atauHash::needsRehash(). - Batasi panjang password, misalnya 64 hingga 128 karakter, atau beralih ke Argon2id.
- Simpan pepper di luar database, misalnya
.envatau KMS, sebagai defense-in-depth. - Aktifkan rate limiting pada endpoint login dan registrasi (panduan rate limiting) untuk mencegah brute force.
- Jangan klaim sistem "100% tidak bisa dibobol". Asumsikan database dapat bocor dan siapkan rencana respons, seperti memaksa reset password.
Skenario Praktek
Berikut adalah enam situasi nyata yang mungkin kamu hadapi:
Registrasi user baru. Hash password dengan password_hash() atau Hash::make() sebelum menyimpan ke database. Simpan hash di kolom password minimal 255 karakter.
Login user. Ambil hash dari database, verifikasi dengan password_verify() atau Hash::check(). Jika valid dan perlu rehash, buat hash baru dan perbarui database.
Upgrade hash dari cost lama ke baru. Saat pengguna login berhasil, periksa password_needs_rehash() atau Hash::needsRehash(). Jika true, buat hash baru dan timpa yang lama. Pengguna yang tidak aktif akan diupgrade seiring login mereka.
Migrasi dari MD5 atau SHA-1. Tidak dapat langsung mengonversi hash MD5 ke bcrypt tanpa password asli. Tangkap login pengguna, verifikasi hash lama dengan cara yang sesuai, jika cocok buat hash modern dan simpan.
Password lebih dari 72 byte dengan bcrypt. Terapkan batasan panjang password atau gunakan Argon2id.
Respons setelah data breach. Asumsikan hash bocor. Naikkan cost factor, paksa reset password untuk akun yang rentan, dan aktifkan rate limiting pada login.
Penutup
Password hashing bukan sekadar fungsi teknis yang dipanggil saat registrasi. Ini adalah baris pertahanan terakhir yang menentukan seberapa parah dampak kebocoran database pada aplikasi kamu. Hash satu arah, algoritma modern seperti Argon2id atau bcrypt dengan cost yang cukup, salt otomatis, dan rehash-on-login adalah fondasi penyimpanan password yang aman di PHP dan Laravel.
Jika kamu masih menemukan md5(), sha1(), atau enkripsi dua arah untuk password di kode production, saatnya melakukan audit dan mengubahnya. Cek juga cost factor bcrypt, implementasi rehash, dan apakah rate limiting sudah aktif pada endpoint login.
Untuk detail implementasi fungsi native PHP, kamu bisa membaca dokumentasi resmi PHP tentang password_hash() dan panduan hashing Laravel. Untuk rekomendasi algoritma dan parameter keamanan, OWASP Password Storage Cheat Sheet adalah referensi yang berguna.
Jika kamu ingin mendalami topik keamanan dan autentikasi terkait, baca artikel tentang Authentication dan Cara Kerjanya.
Fitur komentar belum diaktifkan oleh administrator.