Penggunaan fungsi HASHABLE pada PHP

Indonesian (Bahasa Indonesia) translation by ⚡ Rova Rindrata (you can also view the original English article)

Dari waktu ke waktu, server dan database dicuri atau dikompromikan.Dengan pemikiran ini, penting untuk memastikan bahwa beberapa data penting pengguna, seperti password, tidak boleh dipulihkan. Hari ini, kita akan mempelajari dasar-dasar di balik hashing dan apa yang diperlukan untuk melindungi password dalam aplikasi web Anda.

Tutorial yang Dipublikasikan Ulang

Setiap beberapa minggu, kami mengunjungi kembali beberapa posting favorit pembaca kami dari sepanjang sejarah situs ini.Tutorial ini pertama kali terbit pada bulan Januari tahun 2011.


1. Penolakan

Kriptologi adalah subjek yang cukup rumit, dan saya sama sekali bukan ahlinya.Ada penelitian yang terus-menerus terjadi di bidang ini, di banyak universitas dan lembaga keamanan.

Pada artikel ini, saya akan mencoba untuk menjaga hal-hal sesederhana mungkin, sambil menyajikan kepada Anda metode penyimpan password yang cukup aman dalam aplikasi web.


2. Apa yang Dilakukan "Hashing"?

Hashing mengubah sepotong data (kecil atau besar), menjadi potongan data yang relatif singkat seperti string atau integer.

Hal ini dilakukan dengan menggunakan fungsi hash satu arah."Satu arah" berarti sangat sulit (atau hampir tidak mungkin) membalikkannya.

Contoh umum dari fungsi hash adalah md5(), yang cukup populer di banyak bahasa dan sistem yang berbeda.

$data = "Hello World";
$hash = md5($data);
echo $hash; // b10a8db164e0754105b7a99be72e3fe5

Dengan md5(), hasilnya akan selalu menjadi string sepanjang 32 karakter. Tapi, itu hanya berisi karakter heksadesimal; secara teknis juga dapat digambarkan sebagai integer 128-bit (16 byte). Anda mungkin md5() string dan data lebih panjang lagi, dan Anda masih akan berakhir dengan hash sepanjang ini. Fakta ini saja mungkin memberi petunjuk mengapa ini dianggap sebagai fungsi "satu arah".


3. Menggunakan Fungsi Hash untuk Menyimpan Password

Proses yang biasanya selama pendaftaran pengguna:

  • Pengguna mengisi form pendaftaran, termasuk field password.
  • Script web menyimpan semua informasi ke dalam database.
  • Namun, password dijalankan melalui fungsi hash, sebelum disimpan.
  • Versi asli password belum disimpan di manapun, sehingga secara teknis ia dibuang.

Dan proses login:

  • Pengguna memasukkan username (atau e-mail) dan password.
  • Skrip menjalankan password melalui fungsi hashing yang sama.
  • Skrip menemukan record pengguna dari database, dan membaca password hash yang tersimpan.
  • Kedua nilai ini dibandingkan, dan akses diberikan jika cocok.

Begitu kita memutuskan metode yang layak untuk memasukkan password-nya, kita akan menerapkan proses ini nanti di artikel ini.

Perhatikan bahwa password asli tidak pernah disimpan dimanapun. Jika database dicuri, user login tidak dapat dikompromikan, bukan? Nah, jawabannya adalah "tergantung." Mari kita lihat beberapa masalah potensial.


4. Masalah #1: Tabrakan Hash

"Tabrakan" hash terjadi ketika dua input data yang berbeda menghasilkan hash yang sama. Kemungkinan terjadinya hal ini tergantung pada fungsi mana yang Anda gunakan.

Bagaimana ini bisa dieksploitasi?

Sebagai contoh, saya telah melihat beberapa skrip lama yang menggunakan crc32() untuk hash password. Fungsi ini menghasilkan integer 32-bit sebagai hasilnya. Ini berarti hanya ada 2^32 (yaitu 4.294.967.296) kemungkinan hasilnya.

Mari hash sebuah password:

echo crc32('supersecretpassword');
// outputs: 323322056

Sekarang, mari kita asumsikan peran seseorang yang telah mencuri database, dan memiliki nilai hash. Kita mungkin tidak dapat mengubah 323322056 menjadi 'supersecretpassword', namun kita dapat menemukan kata kunci lain yang akan dikonversi ke nilai hash yang sama, dengan skrip sederhana:

set_time_limit(0);
$i = 0;
while (true) {

  if (crc32(base64_encode($i)) == 323322056) {
		echo base64_encode($i);
		exit;
	}

	$i++;
}

Ini mungkin berjalan untuk sementara waktu, meskipun, akhirnya, itu seharusnya mengembalikan sebuah string. Kita bisa menggunakan string yang dikembalikan ini -- alih-alih 'supersecretpassword' -- dan ini akan memungkinkan kita berhasil masuk ke akun orang itu.

Misalnya, setelah menjalankan skrip tepat ini beberapa saat di komputer saya, saya diberi 'MTIxMjY5MTAwNg=='. Mari kita mengujinya:

echo crc32('supersecretpassword');
// outputs: 323322056

echo crc32('MTIxMjY5MTAwNg==');
// outputs: 323322056

Bagaimana ini bisa dicegah?

Saat ini, PC rumahan yang kuat dapat digunakan untuk menjalankan fungsi hash hampir satu miliar kali per detik. Jadi kita butuh fungsi hash yang memiliki rentang sangat besar.

Misalnya, md5() mungkin cocok, karena menghasilkan hash 128-bit. Ini berarti 340.282.366.920.938.463.463.374.607.431.768.211.456 hasil yang mungkin. Tidak mungkin untuk melewati banyak iterasi untuk menemukan tabrakan. Namun beberapa orang masih menemukan cara untuk melakukan ini (lihat di sini).

Sha1

Sha1() adalah alternatif yang lebih baik, dan menghasilkan nilai hash 160-bit yang lebih panjang.


5. Masalah #2: Tabel Pelangi

Bahkan jika kita memperbaiki masalah tabrakan, kita masih belum aman.

Sebuah tabel pelangi dibangun dengan menghitung nilai hash dari kata-kata yang umum digunakan dan kombinasinya.

Tabel ini bisa memiliki jutaan atau bahkan milyaran baris.

Misalnya, Anda bisa membaca kamus, dan menghasilkan nilai hash untuk setiap kata. Anda juga bisa mulai menggabungkan kata-kata bersama-sama, dan menghasilkan hash untuk mereka juga. Itu tidak semuanya; Anda bahkan bisa mulai menambahkan angka sebelum/sesudah/di antara kata-kata, dan menyimpannya di tabel juga.

Mengingat betapa murahnya penyimpanan saat ini, Tabel Pelangi raksasa dapat diproduksi dan digunakan.

Bagaimana ini bisa dieksploitasi?

Mari kita bayangkan bahwa database besar dicuri, bersama dengan 10 juta hash kata sandi. Cukup mudah untuk mencari tabel pelangi untuk masing-masing. Tidak semua dari mereka akan ditemukan, tentu saja, tapi tetap saja...beberapa dari mereka akan ditemukan!

Bagaimana ini bisa dicegah?

Kita dapat mencoba menambahkan "salt". Berikut adalah contohnya:

$password = "easypassword";

// this may be found in a rainbow table
// because the password contains 2 common words
echo sha1($password); // 6c94d3b42518febd4ad747801d50a8972022f956

// use bunch of random characters, and it can be longer than this
$salt = "f#@V)Hu^%Hgfds";

// this will NOT be found in any pre-built rainbow table
echo sha1($salt . $password); // cd56a16759623378628c0d9336af69b74d9d71a5

Apa yang kita lakukan pada dasarnya adalah menggabungkan string "salt" dengan password sebelum meng-hash. String yang dihasilkan jelas tidak akan ada pada tabel pelangi yang pre-built. Tapi, kita masih belum aman dulu!


6. Masalah #3: Tabel Pelangi (lagi)

Ingat bahwa Tabel Pelangi dapat dibuat dari awal, setelah database dicuri.

Bagaimana ini bisa dieksploitasi?

Bahkan jika salt digunakan, ini mungkin telah dicuri bersama dengan database. Yang harus mereka lakukan hanyalah menghasilkan Tabel Pelangi baru dari nol, tapi kali ini mereka menggabungkan salt ke setiap kata yang mereka masukkan ke dalam tabel.

Misalnya, dalam Tabel Pelangi generik, "easypassword" mungkin ada. Tapi di Tabel Pelangi baru ini, mereka juga memiliki "f#@V)Hu^%Hgfdseasypassword" juga. Ketika mereka menjalankan semua dari 10 juta hash yang di-salt yang dicuri di tabel ini, mereka akan kembali dapat menemukan beberapa kecocokan.

Bagaimana ini bisa dicegah?

Kita bisa menggunakan "unique salt" sebagai gantinya, yang berubah untuk setiap pengguna.

Kandidat untuk jenis salt ini adalah nilai id pengguna dari database:

$hash = sha1($user_id . $password);

Ini mengasumsikan bahwa nomor id pengguna tidak pernah berubah, yang biasanya terjadi.

Kita juga bisa menghasilkan string acak untuk setiap pengguna dan menggunakannya sebagai unique salt. Tapi kita perlu memastikan bahwa kita menyimpannya di record pengguna di suatu tempat.

// generates a 22 character long random string
function unique_salt() {

	return substr(sha1(mt_rand()),0,22);
}

$unique_salt = unique_salt();

$hash = sha1($unique_salt . $password);

// and save the $unique_salt with the user record
// ...

Metode ini melindungi kita terhadap Tabel Pelangi, karena sekarang setiap password tunggal telah di-salt dengan nilai yang berbeda. Penyerang harus menghasilkan 10 juta Tabel Pelangi terpisah, yang sama sekali tidak praktis.


7. Masalah #4: Kecepatan Hash

Sebagian besar fungsi hashing telah dirancang dengan kecepatan dalam pikiran, karena sering digunakan untuk menghitung nilai checksum untuk kumpulan data dan file yang besar, untuk memeriksa integritas data.

Bagaimana ini bisa dieksploitasi?

Seperti yang saya sebutkan sebelumnya, PC modern dengan kartu GPU yang hebat (ya, kartu video) dapat diprogram untuk menghitung kira-kira satu miliar hash per detik. Dengan cara ini, mereka bisa menggunakan serangan brute force untuk mencoba setiap kemungkinan password.

Anda mungkin berpikir bahwa membutuhkan panjang password minimal 8 karakter mungkin akan menjaganya tetap aman dari serangan brute force, namun mari kita tentukan apakah memang benar masalahnya:

  • Jika password dapat berisi huruf kecil, huruf besar dan angka, yaitu 62 (26+26+10) karakter yang mungkin.
  • String sepanjang 8 karakter memiliki 62^8 versi yang mungkin.Itu sedikit di atas 218 triliun.
  • Pada tingkat 1 miliar hash per detik, itu bisa dipecahkan sekitar 60 jam.

Dan untuk password panjang 6 karakter, yang juga cukup umum, akan memakan waktu kurang dari 1 menit.

Jangan ragu untuk meminta password sepanjang 9 atau 10 karakter, namun Anda mungkin mulai mengganggu beberapa pengguna Anda.

Bagaimana ini bisa dicegah?

Gunakan fungsi hash yang lebih lambat.

Bayangkan bahwa Anda menggunakan fungsi hash yang hanya bisa berjalan 1 juta kali per detik pada perangkat keras yang sama, bukan 1 miliar kali per detik. Ini kemudian akan membawa penyerang 1000 kali lebih lama untuk brute force sebuah hash. 60 jam akan berubah menjadi hampir 7 tahun!

Salah satu cara untuk melakukannya adalah dengan menerapkannya sendiri:

function myhash($password, $unique_salt) {

	$salt = "f#@V)Hu^%Hgfds";
	$hash = sha1($unique_salt . $password);

	// make it take 1000 times longer
	for ($i = 0; $i < 1000; $i++) {
		$hash = sha1($hash);
	}

	return $hash;
}

Atau Anda bisa menggunakan algoritma yang mendukung "parameter biaya", seperti BLOWFISH. Di PHP, ini bisa dilakukan dengan menggunakan fungsi crypt().

function myhash($password, $unique_salt) {

	// the salt for blowfish should be 22 characters long

	return crypt($password, '$2a$10$'.$unique_salt);

}

Parameter kedua ke fungsi crypt() berisi beberapa nilai yang dipisahkan oleh tanda dollar ($).

Nilai pertama adalah '$2a', yang mengindikasikan bahwa kita akan menggunakan algoritma BLOWFISH.

Nilai kedua, dalam kasus ini '$ 10', adalah "parameter biaya". Ini adalah logaritma base-2 dari berapa banyak iterasi yang akan dijalankan (10 => 2^10 = 1024 iterasi.) Jumlah ini dapat berkisar antara 04 dan 31.

Mari kita jalankan sebuah contoh:

function myhash($password, $unique_salt) {
	return crypt($password, '$2a$10$'.$unique_salt);

}
function unique_salt() {
	return substr(sha1(mt_rand()),0,22);
}


$password = "verysecret";

echo myhash($password, unique_salt());
// result: $2a$10$dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC

Hasil hash yang dihasilkan mengandung algoritma ($2a), parameter biaya ($10), dan 22 karakter salt yang digunakan. Sisanya adalah hash yang dihitung. Mari kita uji:

// assume this was pulled from the database
$hash = '$2a$10$dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC';

// assume this is the password the user entered to log back in
$password = "verysecret";

if (check_password($hash, $password)) {
	echo "Access Granted!";
} else {
	echo "Access Denied!";
}


function check_password($hash, $password) {

	// first 29 characters include algorithm, cost and salt
	// let's call it $full_salt
	$full_salt = substr($hash, 0, 29);

	// run the hash function on $password
	$new_hash = crypt($password, $full_salt);

	// returns true or false
	return ($hash == $new_hash);
}

Ketika kita menjalankan ini, kita melihat "Access Granted!"


8. Menempatkannya Bersama-sama

Dengan semua hal di atas, mari kita tulis kelas utilitas berdasarkan apa yang telah kita pelajari sejauh ini:

class PassHash {

	// blowfish
	private static $algo = '$2a';

	// cost parameter
	private static $cost = '$10';


	// mainly for internal use
	public static function unique_salt() {
		return substr(sha1(mt_rand()),0,22);
	}

	// this will be used to generate a hash
	public static function hash($password) {

		return crypt($password,
					self::$algo .
					self::$cost .
					'$' . self::unique_salt());

	}


	// this will be used to compare a password against a hash
	public static function check_password($hash, $password) {

		$full_salt = substr($hash, 0, 29);

		$new_hash = crypt($password, $full_salt);

		return ($hash == $new_hash);

	}

}

Berikut adalah penggunaan saat pendaftaran pengguna:

// include the class
require ("PassHash.php");

// read all form input from $_POST
// ...

// do your regular form validation stuff
// ...

// hash the password
$pass_hash = PassHash::hash($_POST['password']);

// store all user info in the DB, excluding $_POST['password']
// store $pass_hash instead
// ...

Dan inilah penggunaan selama proses login pengguna:

// include the class
require ("PassHash.php");

// read all form input from $_POST
// ...

// fetch the user record based on $_POST['username']  or similar
// ...

// check the password the user tried to login with
if (PassHash::check_password($user['pass_hash'], $_POST['password']) {
	// grant access
	// ...
} else {
	// deny access
	// ...
}

9. Catatan tentang Ketersediaan Blowfish

Algoritma Blowfish mungkin tidak diimplementasikan di semua sistem, meski sekarang sudah cukup populer. Anda dapat memeriksa sistem Anda dengan kode ini:

if (CRYPT_BLOWFISH == 1) {
	echo "Yes";
} else {
	echo "No";
}

Namun, seperti pada PHP 5.3, Anda tidak perlu khawatir; PHP dikirimkan dengan implementasi ini sebagai bawaannya.


Kesimpulan

Metode password hashing ini cukup solid untuk sebagian besar aplikasi web. Yang katanya, jangan lupa: Anda juga dapat meminta agar anggota Anda menggunakan password yang lebih kuat, dengan menerapkan panjang minimum, karakter campuran, angka & karakter khusus.

Sebuah pertanyaan untuk Anda, pembaca: bagaimana Anda meng-hash password Anda? Dapatkah Anda merekomendasikan perbaikan atas penerapan ini?