phương pháp hay nhất để tạo mã thông báo ngẫu nhiên khi quên mật khẩu


94

Tôi muốn tạo mã định danh cho trường hợp quên mật khẩu. Tôi đọc rằng tôi có thể làm điều đó bằng cách sử dụng dấu thời gian với mt_rand (), nhưng một số người nói rằng dấu thời gian có thể không phải là duy nhất mọi lúc. Vì vậy, tôi hơi bối rối ở đây. Tôi có thể làm điều đó với việc sử dụng tem thời gian với cái này không?

Câu hỏi Phương
pháp hay nhất để tạo mã thông báo ngẫu nhiên / duy nhất có độ dài tùy chỉnh là gì?

Tôi biết có rất nhiều câu hỏi được đặt ra xung quanh đây nhưng tôi ngày càng bối rối hơn sau khi đọc những ý kiến ​​khác nhau từ những người khác nhau.


@AlmaDoMundo: Máy tính không thể phân chia thời gian không giới hạn.
ngày

@juergend - xin lỗi, đừng hiểu.
Alma Do

Bạn sẽ nhận được cùng một dấu thời gian nếu bạn gọi nó chẳng hạn như cách nhau một nano giây. Ví dụ, một số hàm thời gian chỉ có thể trả về thời gian trong 100ns bước, một số chỉ trong vài giây.
juergen d

@juergend ah, đó. Đúng. Tôi đã đề cập đến dấu thời gian 'cổ điển' chỉ với giây. Nhưng nếu hành động như bạn đã nói - có (mà chỉ có lá chúng tôi một tùy chọn với cỗ máy thời gian để có được dấu thời gian không duy nhất)
Alma Đỗ

1
Lưu ý, câu trả lời được chấp nhận không sử dụng CSPRNG .
Scott Arciszewski

Câu trả lời:


147

Trong PHP, sử dụng random_bytes(). Lý do: bạn đang tìm cách lấy mã thông báo nhắc nhở mật khẩu và, nếu đó là thông tin xác thực đăng nhập một lần, thì bạn thực sự có dữ liệu cần bảo vệ (nghĩa là - toàn bộ tài khoản người dùng)

Vì vậy, mã sẽ như sau:

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

Cập nhật : các phiên bản trước của câu trả lời này đã đề cập đến uniqid()và điều đó không chính xác nếu có vấn đề về bảo mật và không chỉ là tính duy nhất. uniqid()về cơ bản chỉ microtime()với một số mã hóa. Có nhiều cách đơn giản để có được dự đoán chính xác microtime()về máy chủ của bạn. Kẻ tấn công có thể đưa ra yêu cầu đặt lại mật khẩu và sau đó thử thông qua một vài mã thông báo có thể xảy ra. Điều này cũng có thể xảy ra nếu more_entropy được sử dụng, vì entropy bổ sung cũng yếu tương tự. Cảm ơn @NikiC@ScottArciszewski đã chỉ ra điều này.

Để biết thêm chi tiết xem


21
Lưu ý rằng random_bytes()chỉ có sẵn cho PHP7. Đối với các phiên bản cũ hơn, câu trả lời của @yesitsme dường như là lựa chọn tốt nhất.
Gerald Schneider,

3
@GeraldSchneider hoặc random_compat , là polyfill cho các tính năng này đã nhận được nhiều đánh giá ngang hàng nhất;)
Scott Arciszewski

Tôi đã tạo một trường varchar (64) trong cơ sở dữ liệu sql của mình để lưu trữ mã thông báo này. Tôi đặt $ length thành 64, nhưng chuỗi trả về dài 128 ký tự. Làm cách nào để lấy một chuỗi có kích thước cố định (ở đây là 64)?
gordie

2
@gordie Set chiều dài đến 32, mỗi byte là 2 ký tự hex
JohnHoulderUK

Nên là $lengthgì? Id của người dùng? Hay cái gì?
ngăn xếp

71

Điều này trả lời yêu cầu 'ngẫu nhiên tốt nhất':

Câu trả lời 1 của Adi từ Security.StackExchange có một giải pháp cho điều này:

Đảm bảo rằng bạn có hỗ trợ OpenSSL và bạn sẽ không bao giờ gặp lỗi với một lớp lót này

$token = bin2hex(openssl_random_pseudo_bytes(16));

1. Adi, Thứ Hai, ngày 12 tháng 11 năm 2018, Celeritas, "Tạo mã thông báo không thể nhận dạng cho email xác nhận", ngày 20 tháng 9 '13 lúc 7:06, https://security.stackexchange.com/a/40314/


24
openssl_random_pseudo_bytes($length)- hỗ trợ: PHP 5> = 5.3.0, ....................................... ................... (Đối với PHP 7 trở lên, sử dụng random_bytes($length)) ...................... .................... (Đối với PHP dưới 5.3 - không sử dụng PHP dưới 5.3)
jave.web

54

Phiên bản trước đó của câu trả lời được chấp nhận ( md5(uniqid(mt_rand(), true))) là không an toàn và chỉ cung cấp khoảng 2 ^ 60 kết quả đầu ra có thể có - nằm trong phạm vi của tìm kiếm bạo lực trong khoảng thời gian một tuần đối với kẻ tấn công ngân sách thấp:

khóa DES 56 bit có thể bị cưỡng bức trong khoảng 24 giờ và một trường hợp trung bình sẽ có khoảng 59 bit entropy, chúng ta có thể tính 2 ^ 59/2 ^ 56 = khoảng 8 ngày. Tùy thuộc vào cách xác minh mã thông báo này được triển khai, thực tế có thể bị rò rỉ thông tin thời gian và suy ra N byte đầu tiên của mã thông báo đặt lại hợp lệ .

Vì câu hỏi là về "các phương pháp hay nhất" và mở đầu bằng ...

Tôi muốn tạo mã định danh cho trường hợp quên mật khẩu

... chúng ta có thể suy ra rằng mã thông báo này có các yêu cầu bảo mật ngầm. Và khi bạn thêm các yêu cầu bảo mật vào trình tạo số ngẫu nhiên, cách tốt nhất là luôn sử dụng trình tạo số giả ngẫu nhiên an toàn bằng mật mã (viết tắt là CSPRNG).


Sử dụng CSPRNG

Trong PHP 7, bạn có thể sử dụng bin2hex(random_bytes($n))(ở đâu$n là một số nguyên lớn hơn 15).

Trong PHP 5, bạn có thể sử dụng random_compatđể hiển thị cùng một API.

Ngoài ra, bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))nếu bạn đã ext/mcryptcài đặt. Một lớp lót tốt khác là bin2hex(openssl_random_pseudo_bytes($n)).

Tách Tra cứu khỏi Trình xác thực

Lấy từ công việc trước đây của tôi về cookie "nhớ tôi" an toàn trong PHP , cách hiệu quả duy nhất để giảm thiểu rò rỉ thời gian nói trên (thường được giới thiệu bởi truy vấn cơ sở dữ liệu) là tách việc tra cứu khỏi xác thực.

Nếu bảng của bạn trông giống như thế này (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)
);

... bạn cần thêm một cột nữa selector, như sau:

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)
);

Sử dụng CSPRNG Khi mã thông báo đặt lại mật khẩu được phát hành, hãy gửi cả hai giá trị cho người dùng, lưu trữ bộ chọn và hàm băm SHA-256 của mã thông báo ngẫu nhiên trong cơ sở dữ liệu. Sử dụng bộ chọn để lấy mã băm và ID người dùng, tính toán mã băm SHA-256 của mã thông báo mà người dùng cung cấp với mã được lưu trữ trong cơ sở dữ liệu bằng cách sử dụng hash_equals().

Mã mẫu

Tạo mã thông báo đặt lại trong PHP 7 (hoặc 5.6 với random_compat) với 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')
]);

Xác minh mã thông báo đặt lại do người dùng cung cấp:

$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.
}

Các đoạn mã này không phải là giải pháp hoàn chỉnh (tôi đã tránh xác thực đầu vào và tích hợp khung), nhưng chúng sẽ đóng vai trò là một ví dụ về những việc cần làm.


Khi bạn xác minh mã thông báo đặt lại do người dùng cung cấp, tại sao bạn lại sử dụng biểu diễn nhị phân của mã thông báo ngẫu nhiên? Bạn có nghĩ rằng có thể (và an toàn không?) Để: 1) lưu trữ trong DB giá trị hex được băm của mã thông báo với hash('sha256', bin2hex($token)), 2) xác minh với if (hash_equals(hash('sha256', $validator), $results[0]['token'])) {...? Cảm ơn!
Guicara

Có, việc so sánh các chuỗi hex cũng rất an toàn. Đó thực sự là một vấn đề của sở thích. Tôi thích thực hiện tất cả các hoạt động tiền điện tử trên nhị phân thô và chỉ chuyển đổi sang hex / base64 để truyền hoặc lưu trữ.
Scott Arciszewski

Xin chào Scott, về cơ bản đây là một câu hỏi không chỉ dành cho câu trả lời của bạn mà còn cho toàn bộ bài viết về tính năng "Ghi nhớ". Tại sao không sử dụng duy nhất idlàm bộ chọn? Ý tôi là, khóa chính của account_recoverybảng. Chúng ta không cần lớp bảo mật bổ sung cho bộ chọn phải không? Cảm ơn!
Andre Polykanine,

id:secretđược. selector:secretđược. secretchính nó không phải là. Mục đích là tách truy vấn cơ sở dữ liệu (bị rò rỉ về thời gian) khỏi giao thức xác thực (phải là thời gian không đổi).
Scott Arciszewski

Có tác hại gì khi sử dụng openssl_random_pseudo_bytesthay thế random_bytesnếu đang chạy PHP 5.6 không? Ngoài ra, bạn không nên chỉ thêm bộ chọn chứ không phải bộ xác thực trong chuỗi truy vấn của liên kết?
greg

7

Bạn cũng có thể sử dụng DEV_RANDOM, trong đó 128 = 1/2 chiều dài mã thông báo được tạo. Mã bên dưới tạo 256 mã thông báo.

$token = bin2hex(mcrypt_create_iv(128, MCRYPT_DEV_RANDOM));

4
Tôi sẽ đề nghị MCRYPT_DEV_URANDOMhơn MCRYPT_DEV_RANDOM.
Scott Arciszewski,

2

Điều này có thể hữu ích bất cứ khi nào bạn cần một mã thông báo rất ngẫu nhiên

<?php
   echo mb_strtoupper(strval(bin2hex(openssl_random_pseudo_bytes(16))));
?>
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.