← Semua pembelajaran / Go Nol → Enterprise
Fase 6 · Keamanan

Hashing sandi

Sandi tidak pernah disimpan, tidak pernah dienkripsi, dan tidak pernah di-hash dengan SHA-256. Yang dipakai adalah fungsi yang sengaja lambat — dan seberapa lambat adalah keputusan yang harus kamu ambil sadar.

Sumber asli pkg.go.dev Resmi Rangkuman ~6 menit baca

Intisari

  • bcrypt aman dan tersedia; Argon2id lebih kuat terhadap serangan GPU dan direkomendasikan untuk sistem baru.
  • MD5, SHA-1, SHA-256 bukan untuk sandi — terlalu cepat, jutaan tebakan per detik.
  • Salt dibuat otomatis dan disimpan di dalam hash. Kamu tidak perlu mengurusnya.
  • bcrypt memotong sandi di 72 byte — pra-hash dengan SHA-256 kalau frasa sandi panjang diizinkan.
  • Selalu jalankan pembandingan hash meski penggunanya tidak ada, supaya waktu responsnya tidak membocorkan siapa yang terdaftar.

bcrypt

import "golang.org/x/crypto/bcrypt"

func HashSandi(sandi string) (string, error) {
	if len(sandi) > 72 {
		return "", ErrSandiTerlaluPanjang   // bcrypt memotong diam-diam
	}
	b, err := bcrypt.GenerateFromPassword([]byte(sandi), 12)   // biaya 12
	if err != nil {
		return "", fmt.Errorf("hash sandi: %w", err)
	}
	return string(b), nil
}

func PeriksaSandi(hash, sandi string) bool {
	return bcrypt.CompareHashAndPassword([]byte(hash), []byte(sandi)) == nil
}
$2a$12$N9qo8uLOickgx2ZMRZoMye.IjPeCLQK6IbnCtHwHz7Kk8vY7Zg9Wm
│  │  │                      │
│  │  └─ salt (22 karakter)  └─ hash
│  └──── biaya (2^12 iterasi)
└─────── varian algoritma

Salt sudah ada di dalamnya. Karena itu dua pengguna dengan sandi identik tetap punya hash yang berbeda, dan tabel pelangi tidak berguna. Kamu tidak perlu kolom salt terpisah — kalau sebuah tutorial menyuruhmu membuatnya, tutorial itu bicara tentang algoritma lain.

Memilih biaya

BiayaWaktu per hashUntuk
10~50 msTerlalu rendah untuk sistem baru
12~250 msTitik awal yang wajar
14~1 dtkData sangat sensitif; perhatikan beban CPU

Biaya hashing adalah beban CPU yang nyata di jalur login. Pada task Fargate 0,5 vCPU, biaya 12 berarti sekitar empat login per detik per task — dan selama itu CPU-nya tidak melayani hal lain. Karena itu rate limit di endpoint login bukan opsional (materi terpisah di fase ini): tanpa itu, siapa pun bisa menghabiskan CPU-mu hanya dengan mengirim sandi salah.

Argon2id

import "golang.org/x/crypto/argon2"

type ParamArgon struct {
	Waktu   uint32   // iterasi
	Memori  uint32   // KiB
	Thread  uint8
	Panjang uint32
}

var argonBaku = ParamArgon{Waktu: 2, Memori: 64 * 1024, Thread: 4, Panjang: 32}

func HashArgon(sandi string) (string, error) {
	salt := make([]byte, 16)
	if _, err := rand.Read(salt); err != nil {   // crypto/rand, BUKAN math/rand
		return "", err
	}

	h := argon2.IDKey([]byte(sandi), salt,
		argonBaku.Waktu, argonBaku.Memori, argonBaku.Thread, argonBaku.Panjang)

	// Simpan parameternya bersama hash: tanpa itu, menaikkan
	// parameter nanti akan mematahkan semua sandi lama.
	return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s",
		argonBaku.Memori, argonBaku.Waktu, argonBaku.Thread,
		base64.RawStdEncoding.EncodeToString(salt),
		base64.RawStdEncoding.EncodeToString(h)), nil
}
bcryptArgon2id
Umur & keteruujianSejak 1999, sangat terujiPemenang kompetisi 2015
Tahan GPU/ASICSedangTinggi (memory-hard)
Batas panjang sandi72 byteTidak ada
ParameterSatu angkaTiga — harus dipilih dan disimpan
RekomendasiAman; wajar untuk yang sudah adaUntuk sistem baru

Login yang tidak membocorkan apa pun

// Hash palsu berbiaya sama, dipakai saat penggunanya tidak ada.
var hashUmpan, _ = bcrypt.GenerateFromPassword([]byte("umpan"), 12)

func (s *Layanan) Masuk(ctx context.Context, email, sandi string) (Pengguna, error) {
	u, err := s.simpan.DariEmail(ctx, email)
	if errors.Is(err, ErrTidakDitemukan) {
		// Tetap jalankan hashing supaya waktu responsnya sama —
		// tanpa ini, penyerang bisa menebak email mana yang terdaftar
		// hanya dengan mengukur waktu balasan.
		_ = bcrypt.CompareHashAndPassword(hashUmpan, []byte(sandi))
		return Pengguna{}, ErrKredensialSalah
	}
	if err != nil {
		return Pengguna{}, err
	}

	if !PeriksaSandi(u.Hash, sandi) {
		return Pengguna{}, ErrKredensialSalah   // pesan yang SAMA
	}
	return u, nil
}

Pesan error harus identik untuk "email tidak ada" dan "sandi salah". Membedakannya memberi penyerang daftar akun yang valid — bahan untuk credential stuffing. Dan pesan yang sama saja tidak cukup kalau waktunya berbeda; itulah gunanya hash umpan di atas.

Menaikkan biaya tanpa mengganggu pengguna

func (s *Layanan) Masuk(...) (Pengguna, error) {
	...
	if !PeriksaSandi(u.Hash, sandi) {
		return Pengguna{}, ErrKredensialSalah
	}

	// Sandinya benar — dan hanya di sini kita punya sandi polosnya.
	// Kalau hash lama memakai biaya rendah, hash ulang sekarang.
	if biaya, err := bcrypt.Cost([]byte(u.Hash)); err == nil && biaya < 12 {
		if baru, err := HashSandi(sandi); err == nil {
			_ = s.simpan.PerbaruiHash(ctx, u.ID, baru)
		}
	}
	return u, nil
}

Aturan sandi yang benar-benar berguna

  1. Minimal 8 karakter — dan jangan batasi maksimumnya di bawah 64.
  2. Jangan wajibkan campuran karakter. Ia menghasilkan Password1!, bukan sandi kuat.
  3. Tolak sandi yang sudah bocor — periksa terhadap daftar sandi umum saat pendaftaran.
  4. Izinkan tempel dan pengelola sandi. Menonaktifkannya membuat orang memakai sandi lebih lemah.
  5. Jangan wajibkan penggantian berkala tanpa indikasi kebocoran; ia menghasilkan sandi yang lebih buruk.
  6. Tawarkan 2FA untuk akun yang menyentuh uang atau data pribadi.

Latihan: ukur waktu bcrypt.GenerateFromPassword di mesinmu untuk biaya 10, 12, dan 14. Lalu tulis endpoint login dan ukur waktu responsnya untuk email yang ada versus tidak ada — tanpa hash umpan, lalu dengan. Perbedaan yang terukur itu adalah kebocoran informasi yang sungguhan.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.