Keamanan Password

Password adalah kunci utama keamanan aplikasi web. Jika password user bocor, seluruh data mereka dalam bahaya. Di bab ini, kamu akan belajar cara menyimpan dan memverifikasi password dengan benar menggunakan standar industri.

Kenapa Tidak Boleh Simpan Plain Text?

<?php
// ❌ FATAL: Simpan password sebagai teks biasa
$stmt = $pdo->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
$stmt->execute(['admin', 'rahasia123']);
// Database: | admin | rahasia123 |
// Jika database BOCOR → semua password langsung terbaca! 💀

Dampak menyimpan password plain text:

  1. Database bocor → hacker dapat semua password
  2. User sering pakai password sama di banyak situs → akun lain ikut terancam
  3. Karyawan/admin database bisa membaca password user
  4. Melanggar regulasi (GDPR, dll) → denda besar

Solusi: Hashing

Hashing mengubah password menjadi string acak yang tidak bisa dikembalikan ke bentuk aslinya:

"rahasia123"  →  hash()  →  "$2y$10$X3qYkL9mN7..."

Satu arah — tidak ada cara mengubah hash kembali menjadi "rahasia123". Inilah yang membuat hashing aman.

password_hash() — Membuat Hash

<?php
$password = 'rahasia123';

// Buat hash (gunakan PASSWORD_DEFAULT = bcrypt)
$hash = password_hash($password, PASSWORD_DEFAULT);

echo $hash;
// $2y$10$X3qYkL9mN7pBvFG8aMjKO.rK3HuXxKzJ2vwN5tIw6GmQ1L4YhV3Wy

// Setiap kali dijalankan, hasilnya BERBEDA (karena random salt)
$hash2 = password_hash($password, PASSWORD_DEFAULT);
echo $hash2;
// $2y$10$aB5cD6eF7gH8iJ9kL0mNoP1qR2sT3uV4wX5yZ...
NOTE

Kenapa Hasilnya Selalu Berbeda?password_hash() otomatis menambahkan salt (data acak) yang unik setiap kali. Ini mencegah serangan rainbow table — dimana hacker punya daftar hash yang sudah dihitung sebelumnya. Dengan salt yang berbeda, hash password yang sama menghasilkan output berbeda.

Anatomi Hash bcrypt

$2y$10$X3qYkL9mN7pBvFG8aMjKO.rK3HuXxKzJ2vwN5tIw6GmQ1L4YhV3Wy
 │  │  │                      │
 │  │  │                      └── Hash result (31 karakter)
 │  │  └── Salt (22 karakter, auto-generated)
 │  └── Cost factor (10 = 2^10 iterasi)
 └── Algoritma ($2y = bcrypt)

password_verify() — Memverifikasi Password

Saat user login, jangan hash ulang dan bandingkan string — gunakan password_verify():

<?php
$password_dari_form = 'rahasia123';
$hash_dari_database = '$2y$10$X3qYkL9mN7pBvFG8aMjKO.rK3HuXxKzJ2vwN5tIw6GmQ1L4YhV3Wy';

// Verifikasi
if (password_verify($password_dari_form, $hash_dari_database)) {
    echo "✅ Password cocok! Login berhasil.";
} else {
    echo "❌ Password salah!";
}
WARNING
// ❌ SALAH: hash hasil berbeda tiap kali (karena random salt)
if (password_hash($input, PASSWORD_DEFAULT) === $hash_dari_db) { ... }

// ✅ BENAR: password_verify() tahu cara membaca salt dari hash
if (password_verify($input, $hash_dari_db)) { ... }

Implementasi Lengkap: Registrasi & Login

Registrasi (Simpan Password)

<?php
// register.php
declare(strict_types=1);
require 'config/database.php';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = trim($_POST['username'] ?? '');
    $password = $_POST['password'] ?? '';
    $konfirmasi = $_POST['konfirmasi_password'] ?? '';

    $errors = [];

    // Validasi
    if (strlen($username) < 3) {
        $errors[] = 'Username minimal 3 karakter';
    }
    if (strlen($password) < 8) {
        $errors[] = 'Password minimal 8 karakter';
    }
    if ($password !== $konfirmasi) {
        $errors[] = 'Konfirmasi password tidak cocok';
    }

    // Cek username sudah terpakai
    $stmt = $pdo->prepare("SELECT id FROM users WHERE username = ?");
    $stmt->execute([$username]);
    if ($stmt->fetch()) {
        $errors[] = 'Username sudah digunakan';
    }

    if (empty($errors)) {
        // ✅ Hash password sebelum simpan
        $hash = password_hash($password, PASSWORD_DEFAULT);

        $stmt = $pdo->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
        $stmt->execute([$username, $hash]);

        header('Location: login.php?registered=1');
        exit;
    }
}

Login (Verifikasi Password)

<?php
// login.php
declare(strict_types=1);
session_start();
require 'config/database.php';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = trim($_POST['username'] ?? '');
    $password = $_POST['password'] ?? '';

    // 1. Cari user di database
    $stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
    $stmt->execute([$username]);
    $user = $stmt->fetch();

    // 2. Verifikasi password
    if ($user && password_verify($password, $user['password'])) {
        // ✅ Login berhasil
        $_SESSION['user_id'] = $user['id'];
        $_SESSION['username'] = $user['username'];

        // 3. Cek apakah hash perlu di-upgrade
        if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
            $newHash = password_hash($password, PASSWORD_DEFAULT);
            $stmt = $pdo->prepare("UPDATE users SET password = ? WHERE id = ?");
            $stmt->execute([$newHash, $user['id']]);
        }

        header('Location: dashboard.php');
        exit;
    } else {
        // ❌ Login gagal
        $error = 'Username atau password salah';
        // JANGAN beri tahu yang mana yang salah (keamanan)!
    }
}

password_needs_rehash() — Upgrade Hash

Algoritma hashing berkembang. password_needs_rehash() mengecek apakah hash lama perlu di-upgrade:

<?php
// Otomatis upgrade hash ketika user login
if (password_verify($password, $hash_lama)) {
    if (password_needs_rehash($hash_lama, PASSWORD_DEFAULT)) {
        // Hash menggunakan algoritma/cost yang lebih lama
        // Re-hash dengan standar terbaru
        $hash_baru = password_hash($password, PASSWORD_DEFAULT);
        // Update di database
        $stmt = $pdo->prepare("UPDATE users SET password = ? WHERE id = ?");
        $stmt->execute([$hash_baru, $user['id']]);
    }
}

Kebijakan Password yang Baik

<?php
function validasiKekuatanPassword(string $password): array {
    $errors = [];

    if (strlen($password) < 8) {
        $errors[] = 'Minimal 8 karakter';
    }
    if (!preg_match('/[A-Z]/', $password)) {
        $errors[] = 'Harus ada huruf besar';
    }
    if (!preg_match('/[a-z]/', $password)) {
        $errors[] = 'Harus ada huruf kecil';
    }
    if (!preg_match('/[0-9]/', $password)) {
        $errors[] = 'Harus ada angka';
    }

    return $errors;
}

$masalah = validasiKekuatanPassword('abc');
// ['Minimal 8 karakter', 'Harus ada huruf besar', 'Harus ada angka']

Rangkuman

[!IMPORTANT] Checklist Keamanan Password

  1. Selalu hash password dengan password_hash(PASSWORD_DEFAULT)
  2. Selalu verifikasi dengan password_verify()
  3. Jangan pernah simpan password plain text
  4. Jangan pernah buat hash sendiri (MD5, SHA1 = TIDAK AMAN)
  5. ✅ Cek password_needs_rehash() saat login untuk upgrade otomatis
  6. ✅ Pesan error login: "username atau password salah" (jangan spesifik)
  7. ✅ Terapkan kebijakan password minimal (panjang, kompleksitas)

Latihan

  1. Update project Toko Online: ganti penyimpanan password (jika masih plain text) ke password_hash()
  2. Buat fungsi validasiKekuatanPassword() dan tampilkan feedback real-time di form registrasi
  3. Implementasikan password_needs_rehash() di flow login

Selanjutnya

Password sudah aman! Lanjut ke Validasi & Sanitasi Input → untuk melindungi aplikasi dari serangan XSS dan injection.