praktik terbaik untuk menghasilkan token acak untuk kata sandi yang terlupa

94

Saya ingin membuat pengenal untuk kata sandi yang lupa. Saya membaca bahwa saya dapat melakukannya dengan menggunakan stempel waktu dengan mt_rand (), tetapi beberapa orang mengatakan bahwa stempel waktu mungkin tidak selalu unik. Jadi saya agak bingung disini. Bisakah saya melakukannya dengan menggunakan cap waktu dengan ini?

Question
Apa praktik terbaik untuk menghasilkan token acak / unik dengan panjang kustom?

Saya tahu ada banyak pertanyaan yang diajukan di sekitar sini tetapi saya semakin bingung setelah membaca pendapat yang berbeda dari orang yang berbeda.

tajam
sumber
@AlmaDoMundo: Komputer tidak dapat membagi waktu tanpa batas.
juergen d
@juergend - maaf, jangan mengerti.
Alma Do
Anda akan mendapatkan stempel waktu yang sama jika Anda menyebutnya misalnya selisih satu nano detik. Beberapa fungsi waktu misalnya hanya dapat mengembalikan waktu dalam langkah 100ns, beberapa hanya dalam langkah detik.
juergen d
@juergend ah, itu. Iya. Saya menyebutkan cap waktu 'klasik' hanya dengan beberapa detik. Tetapi jika bertindak seperti yang Anda katakan - ya (itu hanya memberi kita opsi dengan mesin waktu untuk mendapatkan stempel waktu yang tidak unik)
Alma Do
1
Perhatian , jawaban yang diterima tidak memanfaatkan CSPRNG .
Scott Arciszewski

Jawaban:

148

Di PHP, gunakan random_bytes(). Alasan: Anda sedang mencari cara untuk mendapatkan token pengingat kata sandi, dan, jika itu adalah kredensial login satu kali, maka Anda sebenarnya memiliki data untuk dilindungi (yaitu - seluruh akun pengguna)

Jadi, kodenya adalah sebagai berikut:

//$length = 78 etc
$token = bin2hex(random_bytes($length));

Pembaruan : versi sebelumnya dari jawaban ini merujuk uniqid()dan itu tidak benar jika ada masalah keamanan dan bukan hanya keunikan. uniqid()pada dasarnya hanya microtime()dengan beberapa pengkodean. Ada cara sederhana untuk mendapatkan prediksi akurat tentang microtime()di server Anda. Seorang penyerang dapat mengeluarkan permintaan pengaturan ulang kata sandi dan kemudian mencoba melalui beberapa token yang mungkin. Ini juga dimungkinkan jika more_entropy digunakan, karena entropi tambahan juga lemah. Terima kasih kepada @NikiC dan @ScottArciszewski karena telah menunjukkan hal ini.

Untuk lebih jelasnya lihat

Alma Do
sumber
21
Perhatikan bahwa random_bytes()hanya tersedia pada PHP7. Untuk versi yang lebih lama, jawaban dari @yesitsme sepertinya menjadi pilihan terbaik.
Gerald Schneider
3
@GeraldSchneider atau random_compat , yang merupakan polyfill untuk fitur-fitur ini yang paling banyak menerima tinjauan sejawat;)
Scott Arciszewski
Saya membuat bidang varchar (64) di database sql saya untuk menyimpan token ini. Saya mengatur $ length menjadi 64, tetapi string yang dikembalikan adalah 128 karakter. Bagaimana saya bisa mendapatkan string dengan ukuran tetap (di sini, 64 lalu)?
gordie
2
@gordie Atur panjang menjadi 32, setiap byte adalah 2 karakter hex
JohnHoulderUK
Apa yang seharusnya $length? ID pengguna? Atau apa?
tumpukan
71

Ini menjawab permintaan 'acak terbaik':

Jawaban Adi 1 dari Security.StackExchange memiliki solusi untuk ini:

Pastikan Anda memiliki dukungan OpenSSL, dan Anda tidak akan pernah salah dengan satu baris ini

$token = bin2hex(openssl_random_pseudo_bytes(16));

1. Adi, Sen 12 Nov 2018, Celeritas, "Menghasilkan token yang tidak dapat dinantikan untuk email konfirmasi", 20 Sep '13 pada 7:06, https://security.stackexchange.com/a/40314/

YesItsMe
sumber
24
openssl_random_pseudo_bytes($length)- dukungan: PHP 5> = 5.3.0, ....................................... ................... (Untuk PHP 7 ke atas, gunakan random_bytes($length)) ...................... .................... (Untuk PHP di bawah 5.3 - jangan gunakan PHP di bawah 5.3)
jave.web
54

Versi awal dari jawaban yang diterima ( md5(uniqid(mt_rand(), true))) tidak aman dan hanya menawarkan sekitar 2 ^ 60 kemungkinan keluaran - baik dalam kisaran pencarian brute force dalam waktu sekitar satu minggu untuk penyerang anggaran rendah:

Karena kunci DES 56-bit dapat dipaksakan secara brutal dalam waktu sekitar 24 jam , dan kasus rata-rata akan memiliki sekitar 59 bit entropi, kita dapat menghitung 2 ^ 59/2 ^ 56 = sekitar 8 hari. Bergantung pada bagaimana verifikasi token ini diimplementasikan, dimungkinkan untuk secara praktis membocorkan informasi pengaturan waktu dan menyimpulkan N byte pertama dari token reset yang valid .

Karena pertanyaannya adalah tentang "praktik terbaik" dan dibuka dengan ...

Saya ingin membuat pengenal untuk kata sandi yang lupa

... kita dapat menyimpulkan bahwa token ini memiliki persyaratan keamanan implisit. Dan saat Anda menambahkan persyaratan keamanan ke pembuat nomor acak, praktik terbaiknya adalah selalu menggunakan pembuat nomor pseudorandom yang aman secara kriptografis (disingkat CSPRNG).


Menggunakan CSPRNG

Di PHP 7, Anda dapat menggunakan bin2hex(random_bytes($n))(di mana $nbilangan bulat lebih besar dari 15).

Di PHP 5, Anda dapat menggunakan random_compatuntuk mengekspos API yang sama.

Atau, bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))jika Anda telah ext/mcryptmenginstal. Satu baris bagus lainnya adalah bin2hex(openssl_random_pseudo_bytes($n)).

Memisahkan Pencarian dari Validator

Menarik dari pekerjaan saya sebelumnya tentang cookie "ingat saya" yang aman di PHP , satu-satunya cara efektif untuk mengurangi kebocoran waktu yang disebutkan di atas (biasanya diperkenalkan oleh kueri database) adalah dengan memisahkan pencarian dari validasi.

Jika tabel Anda terlihat seperti ini (MySQL) ...

CREATE TABLE account_recovery (
    id INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT 
    userid INTEGER(11) UNSIGNED NOT NULL,
    token CHAR(64),
    expires DATETIME,
    PRIMARY KEY(id)
);

... Anda perlu menambahkan satu kolom lagi selector, seperti ini:

CREATE TABLE account_recovery (
    id INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT 
    userid INTEGER(11) UNSIGNED NOT NULL,
    selector CHAR(16),
    token CHAR(64),
    expires DATETIME,
    PRIMARY KEY(id),
    KEY(selector)
);

Gunakan CSPRNG Saat token setel ulang sandi diterbitkan, kirim kedua nilai kepada pengguna, simpan pemilih, dan hash SHA-256 dari token acak dalam database. Gunakan selektor untuk mengambil hash dan ID Pengguna, hitung hash SHA-256 dari token yang diberikan pengguna dengan yang disimpan dalam database menggunakan hash_equals().

Kode Contoh

Menghasilkan token reset di PHP 7 (atau 5.6 dengan random_compat) dengan PDO:

$selector = bin2hex(random_bytes(8));
$token = random_bytes(32);

$urlToEmail = 'http://example.com/reset.php?'.http_build_query([
    'selector' => $selector,
    'validator' => bin2hex($token)
]);

$expires = new DateTime('NOW');
$expires->add(new DateInterval('PT01H')); // 1 hour

$stmt = $pdo->prepare("INSERT INTO account_recovery (userid, selector, token, expires) VALUES (:userid, :selector, :token, :expires);");
$stmt->execute([
    'userid' => $userId, // define this elsewhere!
    'selector' => $selector,
    'token' => hash('sha256', $token),
    'expires' => $expires->format('Y-m-d\TH:i:s')
]);

Memverifikasi token setel ulang yang disediakan pengguna:

$stmt = $pdo->prepare("SELECT * FROM account_recovery WHERE selector = ? AND expires >= NOW()");
$stmt->execute([$selector]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!empty($results)) {
    $calc = hash('sha256', hex2bin($validator));
    if (hash_equals($calc, $results[0]['token'])) {
        // The reset token is valid. Authenticate the user.
    }
    // Remove the token from the DB regardless of success or failure.
}

Potongan kode ini bukanlah solusi lengkap (saya menghindari validasi input dan integrasi kerangka kerja), tetapi mereka harus berfungsi sebagai contoh tentang apa yang harus dilakukan.

Scott Arciszewski
sumber
Saat Anda memverifikasi token setel ulang yang disediakan pengguna, mengapa Anda menggunakan representasi biner dari token acak? Menurut Anda, apakah mungkin (dan aman?) Untuk: 1) menyimpan di DB nilai hex yang di-hash dari token dengan hash('sha256', bin2hex($token)), 2) verifikasi dengan if (hash_equals(hash('sha256', $validator), $results[0]['token'])) {...? Terima kasih!
Guicara
Ya, membandingkan string hex juga aman. Ini benar-benar masalah preferensi. Saya lebih suka melakukan semua operasi kripto pada biner mentah dan hanya mengonversi ke hex / base64 untuk transmisi atau penyimpanan.
Scott Arciszewski
Hai Scott, pada dasarnya ini adalah pertanyaan tidak hanya untuk jawaban Anda, tetapi untuk keseluruhan artikel tentang fitur "Ingat Saya". Mengapa tidak menggunakan unik idsebagai pemilih? Maksud saya, kunci utama account_recoverytabel. Kami tidak memerlukan lapisan keamanan tambahan untuk pemilih, bukan? Terima kasih!
Andre Polykanine
id:secretOK. selector:secretOK. secretitu sendiri tidak. Tujuannya adalah untuk memisahkan query database (yang merupakan waktu bocor) dari protokol otentikasi (yang seharusnya waktu yang konstan).
Scott Arciszewski
Apakah ada salahnya menggunakan openssl_random_pseudo_bytesbukannya random_bytesjika berjalan PHP 5.6? Selain itu, bukankah seharusnya Anda menambahkan pemilih saja dan bukan validator di querystring link?
greg
7

Anda juga dapat menggunakan DEV_RANDOM, di mana 128 = 1/2 panjang token yang dihasilkan. Kode di bawah menghasilkan 256 token.

$token = bin2hex(mcrypt_create_iv(128, MCRYPT_DEV_RANDOM));
Graham T
sumber
4
Saya akan menyarankan MCRYPT_DEV_URANDOMlebih MCRYPT_DEV_RANDOM.
Scott Arciszewski
2

Ini mungkin berguna kapan pun Anda membutuhkan token yang sangat acak

<?php
   echo mb_strtoupper(strval(bin2hex(openssl_random_pseudo_bytes(16))));
?>
Ir Calif
sumber