Password pengguna adalah data yang paling sering dicuri sekaligus paling sensitif: orang yang sama sering memakai password serupa di banyak layanan. Karena itu password tidak boleh disimpan dalam bentuk aslinya — bukan di file, bukan di database, bukan di log. Chapter ini membahas mengapa fungsi hash cepat seperti md5 dan sha256 justru jadi kelemahan, lalu membangun penyimpanan password yang benar dengan hashlib.pbkdf2_hmac.

Hash Cepat Adalah Kelemahan #

Menyimpan plaintext berarti satu kebocoran database membocorkan semua akun. Solusi yang tampak jelas adalah meng-hash: fungsi satu arah mengubah password menjadi deretan byte yang tidak bisa dibalik. Masalahnya, penyerang tidak perlu membalik hash — cukup menebak. Kamus password populer di-hash lalu dicocokkan, dan hash cepat membuat pencocokan ini murah:

Kita buktikan sendiri dengan memecahkan PIN empat digit yang di-hash dengan MD5:

python
import hashlib

PIN_ASLI = "4826"
target = hashlib.md5(PIN_ASLI.encode()).hexdigest()
print("Hash MD5 target:", target)

ditemukan = None
for percobaan in range(10_000):
    kandidat = f"{percobaan:04d}"
    if hashlib.md5(kandidat.encode()).hexdigest() == target:
        ditemukan = kandidat
        break
print(f"PIN '{ditemukan}' ditemukan setelah {percobaan + 1} percobaan MD5")

Output yang Dihasilkan:

text
Hash MD5 target: a34e1ddbb4d329167f50992ba59fe45a
PIN '4826' ditemukan setelah 4827 percobaan MD5

Seluruh ruang PIN empat digit hanya berisi 10.000 kemungkinan, dan MD5 menghabiskan kurang dari sedetik untuk menguji semuanya. Password manusia yang lemah tidak jauh berbeda dari PIN panjang. Pertahanan yang benar adalah hash yang sengaja lambat dan unik per pengguna.

Salt dan Iterasi dengan pbkdf2_hmac #

Function hashlib.pbkdf2_hmac() dari standard library mengerjakan hash secara berulang — parameter iterasi mengatur ulang kecepatannya — dengan salt acak yang membuat dua pengguna ber-password sama tetap menghasilkan hash berbeda. Kita lihat dulu bentuk keluarannya dengan salt tetap agar hasilnya bisa dibandingkan:

python
import hashlib

salt_tetap = bytes.fromhex("00112233445566778899aabbccddeeff")
dk = hashlib.pbkdf2_hmac("sha256", b"kunci-rahasia-siti", salt_tetap, 600_000)
print("Panjang salt   :", len(salt_tetap), "bytes")
print("Derived key    :", dk.hex())

Output yang Dihasilkan:

text
Panjang salt   : 16 bytes
Derived key    : 34e2f108bbb06f161122975eec726acc4e87affb4e372bb9fe0299ac403b8ccf

Karena salt, password, dan jumlah iterasi kita tentukan sendiri, keluaran di atas bersifat deterministik — hash yang sama akan selalu terbentuk. Bandingkan dengan salt yang benar, yang dibuat acak setiap kali pengguna mendaftar:

python
import os

print("Salt acak identik dua kali?", os.urandom(16) == os.urandom(16))

Output yang Dihasilkan:

text
Salt acak identik dua kali? False

Fungsi os.urandom(16) menghasilkan 16 byte acak dari sumber sistem operasi; dua pemanggilan tidak akan menghasilkan nilai yang sama. Salt per pengguna meruntuhkan tabel hash pra-hitung milik penyerang — tebakan harus di-hash ulang per pengguna, satu per satu, dengan biaya iterasi penuh.

Verifikasi dengan compare_digest #

Saat pengguna login, kita meng-hash input dengan salt yang sama lalu membandingkannya. Perbandingannya memakai hmac.compare_digest(), bukan operator ==:

python
import hashlib
import hmac
import os


def buat_hash(password: bytes, salt: bytes, iterasi: int = 600_000) -> bytes:
    return hashlib.pbkdf2_hmac("sha256", password, salt, iterasi)


def verifikasi(password: bytes, salt: bytes, hash_tersimpan: bytes) -> bool:
    hash_baru = buat_hash(password, salt)
    return hmac.compare_digest(hash_baru, hash_tersimpan)


salt = os.urandom(16)
hash_tersimpan = buat_hash(b"kunci-rahasia-siti", salt)

print(verifikasi(b"kunci-rahasia-siti", salt, hash_tersimpan))
print(verifikasi(b"kunci-rahasia-budi", salt, hash_tersimpan))

Output yang Dihasilkan:

text
True
False

Mari kita bedah kode di atas:

  1. Baris 10-12 (verifikasi): input pengguna di-hash ulang dengan salt pengguna, lalu dibandingkan dengan hash tersimpan. Password asli tidak pernah disimpan di mana pun.
  2. Baris 12 (hmac.compare_digest(...)): fungsi ini membandingkan dua nilai byte dalam waktu konstan. Operator == berhenti di byte pertama yang berbeda, dan selisih waktu yang terukur bisa menjadi petunjuk bagi penyerang tentang jumlah byte awal yang sudah benar — teknik timing attack.
  3. Baris 16 (hash_tersimpan): dalam aplikasi nyata, kolom password_hash dan password_salt disimpan berdampingan di tabel pengguna; verifikasi cukup membaca keduanya.

Argon2, bcrypt, dan Kebijakan Password #

Ekosistem Python memiliki pilihan yang lebih baru dari PBKDF2: package argon2-cffi mengimplementasikan Argon2id (pemenang Password Hashing Competition, unggul melawan serangan GPU), dan package bcrypt menyediakan algoritma bcrypt yang sudah lama teruji. Keduanya perlu dipasang terpisah; pbkdf2_hmac tetap pilihan sah yang hanya butuh standard library. Panduan OWASP Password Storage Cheat Sheet merinci parameter yang direkomendasikan untuk masing-masing.

Terakhir, kebijakan password: penelitian dan panduan modern seperti NIST SP 800-63b menganjurkan panjang di atas aturan kerumitan — minta pengguna memakai frasa panjang, bukan password pendek penuh simbol wajib, dan sediakan pemeriksaan password yang umum dipakai. Alur penyimpanannya kita sudah pegang; sekarang kita hadapi serangan klasik yang menyasar cara aplikasi berbicara dengan database.