1. Mengapa Penyimpanan Kata Sandi Membutuhkan Algoritma Khusus (Argon2id & Bcrypt)?
Jika basis data sebuah aplikasi bocor (data breach), kata sandi pengguna adalah aset paling berharga yang diincar penyerang—karena banyak pengguna memakai kata sandi yang sama di berbagai layanan lain.
Algoritma hash kriptografis umum seperti MD5, SHA-1, atau SHA-256 dirancang untuk memverifikasi integritas berkas secara sangat cepat. Kecepatan itulah yang justru menjadi bencana bagi kata sandi: satu kartu grafis (GPU) konsumen dapat menguji puluhan miliar tebakan SHA-256 per detik!
Sebaliknya, algoritma penyimpanan kata sandi modern harus memenuhi tiga syarat:
- **Memiliki Cryptographic Salt Acak yang Unik untuk Setiap Pengguna:** Agar dua pengguna yang kebetulan memiliki kata sandi yang sama menghasilkan string hash yang 100% berbeda, serta menggagalkan penggunaan tabel pra-komputasi (Rainbow Tables).
- **Sengaja Dibuat Lambat secara Terukur (Work Factor / Cost):** Membutuhkan waktu sekitar
100–350 milidetikuntuk satu kali verifikasi—tidak terasa bagi pengguna jujur yang login satu kali, tetapi memperlambat upaya tebakan massal penyerang hingga jutaan kali lipat! - **Membutuhkan Memori RAM (Memory-Hard pada Argon2id):** Mencegah penyerang memparalelkan ribuan tebakan sekaligus di atas chip GPU/ASIC murah!
2. Menggunakan password_hash(), password_verify(), dan password_needs_rehash()
PHP memiliki API pengamanan kata sandi bawaan terbaik di kelasnya:
<?php
declare(strict_types=1);
final class PengelolaKataSandi
{
private const array OPSI_ARGON2ID = [
'memory_cost' => 65536, // 64 MB RAM per perhitungan hash
'time_cost' => 4, // 4 iterasi putaran
'threads' => 1,
];
public function buatHash(string $kataSandiPolos): string
{
if (mb_strlen($kataSandiPolos) < 8) {
throw new InvalidArgumentException('Kata sandi minimal harus terdiri dari 8 karakter.');
}
// Gunakan PASSWORD_ARGON2ID (rekomendasi utama OWASP),
// dengan fallback otomatis ke PASSWORD_DEFAULT (Bcrypt) jika ekstensi belum tersedia
if (defined('PASSWORD_ARGON2ID')) {
return password_hash($kataSandiPolos, PASSWORD_ARGON2ID, self::OPSI_ARGON2ID);
}
return password_hash($kataSandiPolos, PASSWORD_DEFAULT, ['cost' => 12]);
}
public function verifikasi(string $kataSandiMasukan, string $hashDariDatabase): bool
{
// password_verify() secara otomatis membaca algoritma, parameter cost, dan salt
// langsung dari dalam string $hashDariDatabase!
return password_verify($kataSandiMasukan, $hashDariDatabase);
}
public function apakahPerluRehash(string $hashDariDatabase): bool
{
if (defined('PASSWORD_ARGON2ID')) {
return password_needs_rehash($hashDariDatabase, PASSWORD_ARGON2ID, self::OPSI_ARGON2ID);
}
return password_needs_rehash($hashDariDatabase, PASSWORD_DEFAULT, ['cost' => 12]);
}
}
Anatomi String Hasil password_hash()
Ketika password_hash('Rahasia!2026', PASSWORD_ARGON2ID) dijalankan, string keluarannya terlihat seperti ini:
$argon2id$v=19$m=65536,t=4,p=1$c2FsdHUnbmlrMTIzNDU2$9f8e7d6c5b4a... └───┬───┘ └─┬┘ └──────┬──────┘ └─────────┬────────┘ └──────┬──────┘ Algoritma Versi Parameter Cost Salt Acak (Base64) Hash Akhir
Karena seluruh parameter dan salt sudah menyatu di dalam string tersebut (panjangnya sekitar 95–100 karakter), pastikan kolom password_hash di tabel basis data Anda bertipe VARCHAR(255) atau TEXT, dan Anda tidak perlu membuat kolom salt terpisah!
3. Pertahanan Tambahan: Mencegah Serangan Timing Enumeration Saat Login
Perhatikan celah halus pada halaman login berikut:
- Jika pengguna memasukkan alamat surel yang terdaftar, server menjalankan
password_verify()yang memakan waktu250 milidetiksebelum menjawab "Surel atau kata sandi salah". - Namun jika pengguna memasukkan alamat surel yang tidak terdaftar, server langsung menjawab "Surel atau kata sandi salah" dalam waktu
2 milidetik(karenapassword_verify()tidak dijalankan)!
Penyerang dapat mengukur selisih 250 ms vs 2 ms tersebut untuk mendata alamat surel mana saja yang sudah terdaftar di situs Anda! Solusinya: Jika surel tidak ditemukan di database, tetap jalankan password_verify($sandiMasukan, $hashPalsuTetap) terhadap sebuah konstanta hash tiruan (dummy hash) agar waktu respons server selalu seragam!
<?php
declare(strict_types=1);
// Hash tiruan untuk menyamakan waktu eksekusi ketika surel tidak ditemukan.
// Catatan: pada produksi, buat hash tiruan ini dengan algoritma dan parameter yang SAMA
// dengan hash asli (misalnya hasil password_hash() Argon2id yang disimpan sebagai konstanta);
// contoh Bcrypt di bawah hanya ilustrasi format dan waktunya berbeda dari Argon2id.
const HASH_TIRUAN = '$2y$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW';
function autentikasiPengguna(PDO $pdo, string $surel, string $sandi): ?array
{
$stmt = $pdo->prepare('SELECT id, surel, password_hash FROM pengguna WHERE surel = :surel LIMIT 1');
$stmt->execute(['surel' => strtolower(trim($surel))]);
$pengguna = $stmt->fetch();
// Jika pengguna tidak ditemukan, tetap jalankan password_verify terhadap HASH_TIRUAN!
$hashTarget = is_array($pengguna) ? (string) $pengguna['password_hash'] : HASH_TIRUAN;
$sandiCocok = password_verify($sandi, $hashTarget);
if (!is_array($pengguna) || !$sandiCocok) {
return null;
}
return $pengguna;
}
4. Merancang Alur "Lupa Kata Sandi" (Password Reset) yang Aman
Jangan pernah mengirimkan kata sandi baru lewat surel! Ikuti standar keamanan berikut saat membuat fitur Reset Kata Sandi:
- Buat token acak satu kali pakai menggunakan
$tokenPolos = bin2hex(random_bytes(32));. - Jangan menyimpan
$tokenPolosmentah-mentah di tabel database! Simpan hasil hash SHA-256-nya ($tokenHash = hash('sha256', $tokenPolos);) beserta waktu kedaluwarsa singkat (misalnya 30 menit:time() + 1800) di database. (Mengapa? Agar jika tabel database bocor dibaca pihak luar, mereka tetap tidak mengetahui$tokenPolosyang aktif untuk mengambil alih akun!). - Kirimkan tautan yang berisi
$tokenPoloske alamat surel pengguna. - Saat pengguna membuka tautan tersebut dan mengirim kata sandi baru, hitung
hash('sha256', $tokenDariUrl), cari di database yang belum kedaluwarsa, perbaruipassword_hashpengguna, langsung hapus token reset tersebut dari database, dan cabut sesi-sesi lama pengguna!
Latihan Praktik: Layanan Pembuatan dan Verifikasi Token Reset Kata Sandi
Buatlah berkas ResetSandiLayanan.php:
- Buat metode
buatTokenReset(): arrayyang mengembalikan['token_url' => $tokenPolos, 'token_db_hash' => hash('sha256', $tokenPolos), 'kedaluwarsa_pada' => time() + 1800]. - Buat metode
verifikasiTokenReset(string $tokenDariUrl, string $hashDariDb, int $kedaluwarsaPada): boolyang memeriksa waktu kedaluwarsa terlebih dahulu lalu membandingkan hash menggunakanhash_equals().