パスワードを忘れた場合にランダムトークンを生成するためのベストプラクティス


94

パスワードを忘れた場合の識別子を生成したい。私はmt_rand()でタイムスタンプを使用してそれを行うことができると読みましたが、一部の人々はタイムスタンプが毎回一意ではないかもしれないと言っています。だから私はここで少し混乱しています。これでタイムスタンプを使ってできますか?

質問
カスタムの長さのランダム/一意のトークンを生成するためのベストプラクティスは何ですか?

この辺りで多くの質問が寄せられることは承知していますが、さまざまな人々のさまざまな意見を読んだ後、さらに混乱します。


@AlmaDoMundo:コンピューターは時間を無制限に分割できません。
juergen d

@juergend-すみません、それを取得しないでください。
Alma Do

たとえばナノ秒単位で呼び出すと、同じタイムスタンプを取得します。たとえば、一部の時間関数は100nsステップでのみ時間を返すことができ、一部は秒ステップでのみ返します。
juergen d

@juergendああ、それ。はい。秒のみの「クラシック」タイムスタンプについて言及しました。しかし、あなたが言ったように行動する場合-はい(タイムマシンで非一意のタイムスタンプを取得するオプションのみが残ります)
Alma Do

1
おっしゃるとおり、受け入れられた回答はCSPRNGを活用していません。
Scott Arciszewski、2015

回答:


147

PHPでは、を使用しますrandom_bytes()。理由:パスワードリマインダトークンを取得する方法を探している場合、それが1回限りのログイン資格情報である場合は、実際に保護するデータ(ユーザーアカウント全体)があります。

したがって、コードは次のようになります。

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

更新この回答の以前のバージョンが参照していたものでuniqid()あり、一意性だけでなくセキュリティの問題がある場合は正しくありません。uniqid()本質的にmicrotime()は一部のエンコーディングのみです。microtime()サーバー上のの正確な予測を取得する簡単な方法があります。攻撃者はパスワードリセットリクエストを発行し、可能性のあるトークンをいくつか試すことができます。追加のエントロピーも同様に弱いため、more_entropyを使用する場合にもこれは可能です。これを指摘してくれた@NikiC@ScottArciszewskiに感謝します。

詳細については、


21
random_bytes()PHP7以降でのみ利用可能であることに注意してください。古いバージョンでは、@ yesitsmeによる回答が最良のオプションのようです。
ジェラルドシュナイダー

3
@GeraldSchneiderまたはrandom_compat。これは、最も多くのピアレビューを受けたこれらの機能のポリフィルです;)
Scott Arciszewski

このトークンを保存するために、SQLデータベースにvarchar(64)フィールドを作成しました。$ lengthを64に設定しましたが、返される文字列は128文字です。どうすれば固定サイズ(ここでは64)の文字列を取得できますか?
gordie

2
@gordie長さを32に設定します。各バイトは2
桁の

何をすべき$lengthですか?ユーザーのID?または何?
スタック

71

これは「最良のランダム」要求に答えます:

Security.StackExchangeからのAdiの回答1には、これに対するソリューションがあります。

OpenSSLをサポートしていることを確認してください。このワンライナーで問題が発生することはありません。

$token = bin2hex(openssl_random_pseudo_bytes(16));

1. Adi、Mon Nov 12 2018、Celeritas、「確認メール用の推測できないトークンの生成」、2013年9月20日7 06、https: //security.stackexchange.com/a/40314/


24
openssl_random_pseudo_bytes($length)-サポート:PHP 5> = 5.3.0、....................................... ...................(PHP 7以降の場合はを使用random_bytes($length))................................ ....................(5.3未満のPHPの場合
-5.3

54

受け入れられた回答の以前のバージョン(md5(uniqid(mt_rand(), true)))は安全ではなく、約2 ^ 60の可能な出力しか提供しません-低予算の攻撃者にとって約1週間の時間内にブルートフォース検索の範囲内です。

以来、56ビットDES鍵が約24時間でブルート強制することができ、平均ケースは、エントロピーの59約ビットを有することになる、我々は、8日間、約2 ^ 2分の59 ^ 56 =を計算することができます。このトークン検証の実装方法によっては、タイミング情報を実際にリークし、有効なリセットトークンの最初のNバイトを推測することが可能な場合があります

質問は「ベストプラクティス」に関するものであり、次のように始まります...

パスワードを忘れた場合の識別子を生成したい

...このトークンには暗黙のセキュリティ要件があると推測できます。また、乱数発生器にセキュリティ要件を追加する場合のベストプラクティスは、常に暗号で保護された疑似乱数発生器(省略形はCSPRNG)を使用することです。


CSPRNGの使用

PHP 7では、bin2hex(random_bytes($n))$nは15より大きい整数)を使用できます。

PHP 5では、を使用random_compatして同じAPIを公開できます。

または、インストールしたbin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))場合ext/mcrypt。別の良いワンライナーはbin2hex(openssl_random_pseudo_bytes($n))です。

ルックアップとバリデーターの分離

以前のPHPでの安全な "remember me" Cookieに関する作業から、前述のタイミングリーク(通常はデータベースクエリによって導入される)を軽減する唯一の効果的な方法は、ルックアップと検証を分離することです。

テーブルが次のようになっている場合(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)
);

...次のselectorように、列をもう1つ追加する必要があります。

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

CSPRNGを使用するパスワードリセットトークンが発行されたら、両方の値をユーザーに送信し、ランダムトークンのセレクターとSHA-256ハッシュをデータベースに保存します。セレクターを使用してハッシュとユーザーIDを取得し、ユーザーが提供するトークンのSHA-256ハッシュを計算しますhash_equals()

コード例

PDOを使用してPHP 7(またはrandom_compatを使用した5.6)でリセットトークンを生成する:

$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')
]);

ユーザー提供のリセットトークンの確認:

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

これらのコードスニペットは完全なソリューションではありませんが(入力の検証とフレームワークの統合を避けました)、何をすべきかの例として役立つはずです。


ユーザー提供のリセットトークンを検証するときに、ランダムトークンのバイナリ表現を使用するのはなぜですか?あなたはそれが可能になると思いますし(と安全な?):1)DBに格納してトークンのハッシュされた進値hash('sha256', bin2hex($token))、2)で確認してくださいif (hash_equals(hash('sha256', $validator), $results[0]['token'])) {...?ありがとう!
ギカラ

はい、16進文字列の比較も安全です。それは本当に好みの問題です。私はすべての暗号化操作をrawバイナリで実行することを好み、送信または保存のためにhex / base64にのみ変換します。
Scott Arciszewski、2015

こんにちはスコット、それは基本的にあなたの答えのためだけでなく、「私を覚えてください」機能に関する記事全体のための質問です。idセレクタとしてユニークを使用しないのはなぜですか?つまり、account_recoveryテーブルの主キーです。セレクターに追加のセキュリティレイヤーは必要ありません。ありがとう!
Andre Polykanine

id:secret大丈夫です。selector:secret大丈夫です。secretそれ自体はそうではありません。目標は、データベースクエリ(タイミングが漏れる)と認証プロトコル(一定時間である必要があります)を分離することです。
Scott Arciszewski、2017年

PHP 5.6を実行してopenssl_random_pseudo_bytesいるrandom_bytes場合、代わりに使用することに害はありますか?また、リンクのクエリ文字列にバリデータではなくセレクタだけを追加するべきではありませんか?
集計:

7

DEV_RANDOMを使用することもできます。128=生成されたトークンの長さの1/2。以下のコードは256トークンを生成します。

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

4
MCRYPT_DEV_URANDOM以上提案しMCRYPT_DEV_RANDOMます。
Scott Arciszewski、2015

2

これは、非常にランダムなトークンが必要な場合に役立ちます

<?php
   echo mb_strtoupper(strval(bin2hex(openssl_random_pseudo_bytes(16))));
?>
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.