Melakukan Hashing Password Dengan Nonce di Sisi Client
Proses hashing untuk password di sisi frontend biasanya dilakukan supaya password tidak dikirimkan apa adanya (plain text) melalui jaringan. Secara umum, proses ini tidak begitu meningkatkan keamanan password karena website modern sudah menggunakan HTTPS sehingga password yang dikirim ke backend sudah ter-enkripsi. Proses hashing ini lebih berguna untuk serangan tertentu seperti MITM proxy dan mencegah password tidak sengaja tersimpan di log backend (misalnya di server NGINX yang men-log seluruh request body).
Salah satu kriteria penting agar hashing efektif adalah hasil hash harus dinamis. Bila hasil hash selalu sama, nilai hash secara tidak langsung akan menjadi password. Penyerang bisa dengan mudah menggunakan nilai hash untuk login tanpa perlu tahu password yang sesungguhnya. Oleh sebab itu, pendekatan yang menggunakan metode enkripsi/dekripsi dimana password yang sama akan menghasilkan hasil enkripsi yang sama, bukanlah solusi yang efektif. Pada tulisan ini, saya akan mencoba menambahkan nonce pada proses hashing untuk menghindary replay attack.
Kondisi Awal
Sebagai latihan, saya akan membuat sebuah backend dari Go dengan menggunakan framework Gin. Ia akan menyediakan endpoint untuk proses login dengan kode program seperti berikut ini:
Pada kode program Go di atas, endpoint /api/login
akan menerima request JSON yang mengandung email
dan password
. Ia kemudian
melakukan pemeriksaan untuk menentukan apakah email dan password-nya sesuai. Selain itu, kode program di atas juga melayani file
statis index.html
. File ini akan mewakili frontend yang diakses langsung dari browser. Sebagai latihan,
saya akan membuat file index.html
dengan isi seperti berikut ini:
Halaman HTML di atas menggunakan JavaScript biasa tanpa framework (untuk dibuka dari browser modern). Ia menggunakan Fetch API untuk memanggil REST API yang disediakan backend Go sebelumnya. Status hasil kembalian dari backend akan ditampilkan di halaman web seperti yang terlihat pada gambar berikut ini:
Pada contoh eksekusi di atas, terlihat bahwa nilai password dikirim apa adanya ke backend. Salah satu potensi celah keamanan disini adalah password tersebut tidak sengaja terekam di salah satu komponen backend seperti di log API gateway atau sejenisnya. Bila seandainya password tidak pernah meninggalkan halaman HTML, maka tidak akan ada kebocoran kata sandi yang mungkin terjadi di sisi backend (walaupun demikian, kebocoran dari sisi frontend seperti terekam di platform web session replay atau dibaca oleh keylogger di perangkat pengguna tetap bisa saja terjadi!).
Melakukan Hashing Password Dengan HMAC-SHA1
Satu langkah untuk meningkatkan keamanan adalah dengan melakukan proses hashing pada kode program di bagian sebelumnya. Sebagai contoh, saya dapat menggunakan algoritma HMAC-SHA1 dengan sebuah key yang statis seperti pada kode program Go berikut ini:
Pada kode program di atas, saya melakukan kalkulasi HMAC SHA1 dengan menggunakan sebuah STATIC_KEY
dengan nilai "sebuah-kunci-statis"
. Agar
halaman web dapat menghasilkan kalkulasi HMAC SHA1 yang sama, saya juga perlu menggunakan nilai yang sama di halaman HTML, misalnya
seperti yang terlihat pada kode program berikut ini:
Pada kode program JavaScript di atas, saya menggunakan crypto.subtle
yang merupakan bagian dari Web Cryptography API dan didukung oleh
browser modern. Saya menggunakan importKey()
untuk menghasilkan sebuah key statis dengan nilai yang sama dengan yang saya pakai
di backend. Saya kemudian melewatkan key tersebut di sign()
untuk menghasilkan hash dari password yang kemudian dikirim ke backend
sebagai array di JSON request body.
Sebagai contoh, bila saya membuka halaman ini dan mengirim password, saya akan memperoleh hasil seperti yang terlihat pada gambar berikut ini:
Walaupun password kini tidak terlihat lagi, teknik ini sama sekali tidak meningkatkan keamanan. Hal ini disebabkan oleh hasil hash yang selalu menghasilkan nilai yang sama (statis). Penyerang yang berhasil melihat nilai hash tetap dapat mengirim hash tersebut kapan saja untuk masuk ke dalam website! Dengan kata lain, nilai hash perannya tidak jauh berbeda dari nilai password (yang perlu dilindungi).
Menambahkan Nonce Angka Pada Proses Hashing
Agar proses hashing aman, saya dapat menambahkan nonce yang berupa angka acak. Dengan demikian, setiap request dengan nonce yang berbeda akan menghasilkan hash yang berbeda namun tetap dapat diverifikasi oleh backend. Sebagai contoh, saya akan mengubah kode program Go yang ada menjadi seperti berikut ini:
Pada kode program di atas, saya menggunakan rand.Uint64()
untuk menghasilkan sebuah angka 64-bit yang acak sebagai nonce. Angka ini
kemudian disimpan ke dalam session. Saya juga menambahkan endpoint /api/login/nonce
untuk mendapatkan nilai nonce yang tersimpan
pada session yang sedang aktif dalam bentuk string. Saya tidak menggunakan angka karena batas maksimum nilai angka literal
yang dapat diproses oleh JavaScript hanya 53-bit sementara angka nonce adalah 64-bit.
Selain itu, kode program di atas juga tidak memakai STATIC_KEY
lagi. Nilai hash kini dihitung dengan password sebagai key dan nonce
sebagai nilai pada kalkulasi HMAC. Agar perbandingan hash-nya sama, saya juga perlu mengubah kode program JavaScript di halaman HTML
menjadi seperti berikut ini:
Pada kode program JavaScript di atas, saya terlebih dahulu memanggil endpoint /api/login/nonce
untuk mendapatkan nilai nonce
yang aktif. Setelah itu, saya menggunakan DataView
untuk menerjemahkan nonce dalam bentuk string menjadi angka 64-bit big endian.
Sama seperti di Go, saya juga menggunakan password sebagai key dan nonce sebagai nilai pada kalkulasi HMAC.
Sekarang, bila saya menggunakan halaman web ini, setiap kali /api/login
dipanggil, nilai hash yang dikirim selalu berbeda setiap
kali di-eksekusi walaupun hash tersebut untuk password yang sama, seperti yang terlihat pada gambar berikut ini:
Dengan teknik ini, proses hashing lebih aman dari serangan replay attack. Walaupun penyerang berhasil mendapatkan nilai hash yang pernah terekam, nilai tersebut tidak bisa dipakai lagi untuk login.
Menambahkan Nonce Waktu Pada Proses Hashing
Salah satu kelemahan pada proses hashing di bagian sebelumnya adalah proses tersebut perlu menyimpan nilai nonce di sebuah tempat. Sebagai contoh, saya menyimpan nonce di session dan menyediakan endpoint untuk mendapatkan nilai nonce yang sedang aktif. Ini membuat proses login menjadi sedikit lebih kompleks dari biasanya. Sebagai alternatif, saya dapat menggunakan waktu sebagai nilai nonce karena waktu selalu unik dan tidak dapat diputar ulang.
Sebagai contoh, saya akan mengubah kode program Go yang saya buat menjadi seperti berikut ini:
Pada kode program di atas, saya menghasilkan nonce berdasarkan ekspresi time.Now().Unix() / 30
sehingga selama 30 detik akan
selalu menghasilkan nonce yang sama. Hal ini saya lakukan untuk mendukung selisih waktu maksimal 30 detik antara jam di browser
dengan jam di server.
Berikutnya, saya akan mengubah kode program JavaScript di HTML agar menggunakan proses yang sama dalam menghasilkan hash seperti yang terlihat pada contoh berikut ini:
Pada kode program di atas, saya juga menggunakan step 30
detik dalam menghitung nilai nonce. Nilai ini harus sama dengan nilai
yang saya pakai di server.
Sekarang, bila saya login berkali-kali di web, selama 30 detik, halaman web akan selalu mengirim hash yang sama. Setelah 30 detik berlalu, bila saya mencoba login lagi, hash yang dikirim ke server akan berbeda, seperti yang terlihat pada gambar berikut ini:
Walaupun teknik ini masih memungkinkan replay attack selama 30 detik, namun teknik ini membuat proses login menjadi lebih sederhana karena tidak melibatkan pertukaran angka nonce. Teknik ini merupakan alternatif yang lebih disarankan terutama bila jam di server dan jam di browser dapat dipastikan ter-sinkronisasi dengan baik.