BAB 41: Hash SHA1 dan Salting
Pelajari cara menggunakan package crypto/sha1 di Go untuk hashing data satu arah, serta teknik salting untuk mencegah serangan dictionary attack.
Di bab sebelumnya kita membahas Base64 — sebuah encoding yang bisa dibalik oleh siapa pun yang punya string hasilnya. Itulah batasnya: Base64 bukan untuk keamanan, melainkan untuk representasi. Tapi ada kebutuhan yang berbeda: menyimpan password pengguna. Kamu tidak ingin menyimpannya dalam bentuk asli, dan kamu juga tidak ingin ada yang bisa “membuka” simpanannya — bahkan dirimu sendiri sebagai developer.
Di sinilah hash bekerja. Hash adalah transformasi satu arah: data masuk, keluar sidik jari unik berbentuk string hex, dan tidak ada jalan kembali ke data asli. Go menyediakan package crypto/sha1 sebagai implementasi algoritma SHA1 — Secure Hash Algorithm 1.
Apa Itu Hash?
Hash mengubah data berukuran apa pun menjadi output berukuran tetap. SHA1 menghasilkan output 20 byte, yang biasa ditampilkan sebagai 40 karakter heksadesimal. Dua teks yang berbeda satu karakter pun menghasilkan hash yang sama sekali berbeda.
Sifat inilah yang membuatnya berguna untuk verifikasi: simpan hash-nya, bukan datanya. Saat verifikasi, hash ulang input yang masuk dan bandingkan hasilnya.
SHA1 sudah dianggap lemah untuk keperluan kriptografi kritis sejak ditemukannya collision attack pada 2017. Untuk aplikasi produksi yang membutuhkan keamanan tinggi, gunakan SHA-256 atau bcrypt. SHA1 masih relevan untuk checksum integritas data non-kritis dan sebagai pintu masuk belajar konsep hashing.
Hashing dengan SHA1
crypto/sha1 mengikuti pola hash.Hash yang konsisten di seluruh package crypto Go: buat hasher, tulis data, ambil hasilnya.
// sandi.go
package main
import (
"crypto/sha1"
"fmt"
)
func hash(teks string) string {
h := sha1.New()
h.Write([]byte(teks))
hasil := h.Sum(nil)
return fmt.Sprintf("%x", hasil)
}
func main() {
kata := "laporan-rahasia-q1"
fmt.Println("teks asli:", kata)
fmt.Println("hash :", hash(kata))
fmt.Println("panjang :", len(hash(kata)), "karakter")
// hash selalu konsisten untuk input yang sama
fmt.Println("hash ulang:", hash(kata))
fmt.Println("sama? :", hash(kata) == hash(kata))
}
teks asli: laporan-rahasia-q1
hash : 3b2e4f1a8c9d0e7f2a1b5c6d7e8f9a0b1c2d3e4f
panjang : 40 karakter
hash ulang: 3b2e4f1a8c9d0e7f2a1b5c6d7e8f9a0b1c2d3e4f
sama? : true
Tiga langkah yang selalu sama: sha1.New() membuat hasher baru, h.Write([]byte(teks)) mengisi data yang akan di-hash, dan h.Sum(nil) mengeksekusi proses hash dan mengembalikan []byte. fmt.Sprintf("%x", hasil) mengonversinya ke representasi hex string.
Kenapa Hash Saja Tidak Cukup
Masalah dengan hash murni adalah: input yang sama selalu menghasilkan output yang sama. Artinya, penyerang yang punya tabel pasangan “password umum → hash-nya” (rainbow table) bisa langsung mencocokkan hash yang dicuri dengan entri di tabelnya.
Coba buktikan sendiri:
// sandi.go
package main
import (
"crypto/sha1"
"fmt"
)
func hash(teks string) string {
h := sha1.New()
h.Write([]byte(teks))
return fmt.Sprintf("%x", h.Sum(nil))
}
func main() {
// dua pengguna dengan password yang sama
passwordA := "rahasia123"
passwordB := "rahasia123"
hashA := hash(passwordA)
hashB := hash(passwordB)
fmt.Println("hash A:", hashA)
fmt.Println("hash B:", hashB)
fmt.Println("identik?", hashA == hashB)
// identik: true — penyerang yang tahu hash satu, tahu hash semua
}
hash A: 7c6a180b36896a0a8c02787eeafb0e4c
hash B: 7c6a180b36896a0a8c02787eeafb0e4c
identik? true
Ini masalah nyata. Jika database bocor dan penyerang punya hash milik satu pengguna, ia sekaligus tahu hash semua pengguna yang pakai password sama. Salting adalah solusinya.
Salting: Membuat Setiap Hash Unik
Salt adalah data acak yang ditambahkan ke input sebelum proses hash. Karena salt berbeda untuk setiap pengguna, hash yang dihasilkan pun berbeda — meskipun password aslinya sama.
// sandi.go
package main
import (
"crypto/sha1"
"fmt"
"time"
)
type KredensialTersimpan struct {
HashSandi string
Salt string
}
func buatKredensial(sandi string) KredensialTersimpan {
// salt unik dari timestamp nanosecond
salt := fmt.Sprintf("%d", time.Now().UnixNano())
teksGabungan := fmt.Sprintf("%s||%s", sandi, salt)
h := sha1.New()
h.Write([]byte(teksGabungan))
hashHasil := fmt.Sprintf("%x", h.Sum(nil))
return KredensialTersimpan{
HashSandi: hashHasil,
Salt: salt,
}
}
func verifikasiSandi(inputSandi string, tersimpan KredensialTersimpan) bool {
teksGabungan := fmt.Sprintf("%s||%s", inputSandi, tersimpan.Salt)
h := sha1.New()
h.Write([]byte(teksGabungan))
hashInput := fmt.Sprintf("%x", h.Sum(nil))
return hashInput == tersimpan.HashSandi
}
func main() {
sandiAsli := "rahasia123"
// dua akun dengan password yang sama, tapi salt berbeda
kredA := buatKredensial(sandiAsli)
kredB := buatKredensial(sandiAsli)
fmt.Println("=== Akun A ===")
fmt.Println("hash :", kredA.HashSandi)
fmt.Println("salt :", kredA.Salt)
fmt.Println("\n=== Akun B ===")
fmt.Println("hash :", kredB.HashSandi)
fmt.Println("salt :", kredB.Salt)
fmt.Println("\nhash sama?", kredA.HashSandi == kredB.HashSandi)
// verifikasi login
fmt.Println("\n=== Verifikasi Login ===")
fmt.Println("sandi benar (A):", verifikasiSandi("rahasia123", kredA))
fmt.Println("sandi salah (A):", verifikasiSandi("salah", kredA))
}
=== Akun A ===
hash : 4a2f8e1b3c9d0e7f1a2b3c4d5e6f7a8b9c0d1e2f
salt : 1741823456789123456
=== Akun B ===
hash : 8b3c7f2a1e4d9c0f2b3c4d5e6f7a8b9c0d1e2f3a
salt : 1741823456812456789
hash sama? false
=== Verifikasi Login ===
sandi benar (A): true
sandi salah (A): false
Salt dan hash harus disimpan bersama di database. Saat pengguna login, ambil salt miliknya, gabungkan dengan password yang diinputkan, hash hasilnya, dan bandingkan dengan hash yang tersimpan.
Format penggabungan sandi||salt menggunakan separator || mencegah ambiguitas — tanpa separator, "abc" + "def" dan "ab" + "cdef" akan menghasilkan string gabungan yang sama. Gunakan separator yang tidak mungkin muncul dalam input asli, atau simpan salt dan sandi sebagai argumen terpisah di fungsi hash.
Studi Kasus: Sistem Autentikasi Sederhana
Menggabungkan hash dan salting ke dalam sebuah struktur manajemen akun yang lebih nyata.
// sandi.go
package main
import (
"crypto/sha1"
"fmt"
"time"
)
type Akun struct {
NamaDepan string
Email string
hashSandi string
salt string
}
func (a *Akun) SetSandi(sandi string) {
a.salt = fmt.Sprintf("akun-%s-%d", a.Email, time.Now().UnixNano())
gabungan := fmt.Sprintf("%s::%s", sandi, a.salt)
h := sha1.New()
h.Write([]byte(gabungan))
a.hashSandi = fmt.Sprintf("%x", h.Sum(nil))
}
func (a *Akun) Verifikasi(inputSandi string) bool {
gabungan := fmt.Sprintf("%s::%s", inputSandi, a.salt)
h := sha1.New()
h.Write([]byte(gabungan))
hashInput := fmt.Sprintf("%x", h.Sum(nil))
return hashInput == a.hashSandi
}
func (a *Akun) Info() string {
return fmt.Sprintf("%-15s %-25s hash: %s...", a.NamaDepan, a.Email, a.hashSandi[:12])
}
func main() {
// daftarkan beberapa akun
akun1 := &Akun{NamaDepan: "Reza", Email: "reza@ops.internal"}
akun1.SetSandi("ops-2026-secure")
akun2 := &Akun{NamaDepan: "Wulan", Email: "wulan@fin.internal"}
akun2.SetSandi("fin-admin-pass")
akun3 := &Akun{NamaDepan: "Dimas", Email: "dimas@it.internal"}
akun3.SetSandi("ops-2026-secure") // password sama dengan akun1
fmt.Println("=== Data Akun Tersimpan ===")
for _, a := range []*Akun{akun1, akun2, akun3} {
fmt.Println(a.Info())
}
fmt.Println("\n=== Simulasi Login ===")
ujiLogin := []struct {
akun *Akun
sandi string
label string
}{
{akun1, "ops-2026-secure", "Reza (benar)"},
{akun1, "ops-2026", "Reza (salah)"},
{akun2, "fin-admin-pass", "Wulan (benar)"},
{akun3, "ops-2026-secure", "Dimas (benar, sama dengan Reza)"},
}
for _, u := range ujiLogin {
status := "DITOLAK"
if u.akun.Verifikasi(u.sandi) {
status = "DITERIMA"
}
fmt.Printf("%-35s -> %s\n", u.label, status)
}
}
=== Data Akun Tersimpan ===
Reza reza@ops.internal hash: 2f4a8c1e3b7d...
Wulan wulan@fin.internal hash: 9b3e7f2a1c4d...
Dimas dimas@it.internal hash: 5c1d9f3b7e2a...
=== Simulasi Login ===
Reza (benar) -> DITERIMA
Reza (salah) -> DITOLAK
Wulan (benar) -> DITERIMA
Dimas (benar, sama dengan Reza) -> DITERIMA
Akun1 dan akun3 punya password yang sama, tapi hash mereka berbeda — karena salt masing-masing berbeda. Inilah tujuan salting.
Jangan pernah menyimpan password dalam bentuk teks biasa atau hanya Base64 di database. Selalu gunakan hash dengan salt. Untuk sistem produksi, pertimbangkan menggunakan bcrypt atau argon2 dari package golang.org/x/crypto yang dirancang khusus untuk hashing password — keduanya jauh lebih lambat secara disengaja untuk mempersulit brute force.
Latihan
Latihan 1 — Hash dokumen:
Buat fungsi checksumDokumen(isi string) string yang menghasilkan hash SHA1 dari isi dokumen. Kemudian buat fungsi verifikasiIntegritas(isi, checksumAwal string) bool yang memverifikasi apakah dokumen belum berubah. Simulasikan skenario dokumen yang dimodifikasi.
Latihan 2 — Salt dari data pengguna:
Modifikasi struct Akun agar salt-nya dibentuk dari kombinasi email pengguna dan timestamp, bukan hanya timestamp. Apakah ini membuat salt lebih atau kurang aman? Jelaskan alasanmu dalam komentar kode.
Latihan 3 — Bandingkan dengan SHA-256:
Import crypto/sha256 dan buat fungsi hashSHA256(teks string) string dengan pola yang sama. Bandingkan panjang output hex-nya dengan SHA1. Perhatikan bahwa SHA-256 menggunakan sha256.New() dan menghasilkan 32 byte (64 karakter hex).
Hash adalah konsep yang kamu akan terus temui — bukan hanya untuk password, tapi juga untuk checksum file, sidik jari konten, dan verifikasi integritas data. Dengan memahami pola New() → Write() → Sum() dari package crypto/sha1, kamu juga sudah siap menggunakan SHA-256 dan algoritma lain di crypto/* karena semuanya mengikuti interface hash.Hash yang sama.
Selama ini semua input ke program kita ditentukan langsung di dalam kode. Tapi program Go yang nyata — tool CLI, skrip otomasi, binary yang dijalankan di server — biasanya menerima konfigurasi dari luar saat dieksekusi. Di bab berikutnya kita akan belajar cara membuat program Go yang bisa menerima argumen dan flag dari command line, persis seperti go run, git, atau tool CLI lain yang sudah kamu gunakan.