비밀번호 분실에 대한 무작위 토큰을 생성하는 모범 사례


94

비밀번호 분실에 대한 식별자를 생성하고 싶습니다. mt_rand ()와 함께 타임 스탬프를 사용하여 할 수 있다고 읽었지만 일부 사람들은 타임 스탬프가 매번 고유하지 않을 수 있다고 말합니다. 그래서 나는 여기서 약간 혼란 스럽습니다. 타임 스탬프를 사용하여 할 수 있습니까?

질문
사용자 정의 길이의 무작위 / 고유 토큰을 생성하는 가장 좋은 방법은 무엇입니까?

여기에 많은 질문이 있다는 것을 알고 있지만 다른 사람들의 다른 의견을 읽은 후 더 혼란스러워집니다.


@AlmaDoMundo : 컴퓨터는 시간을 무제한으로 나눌 수 없습니다.
juergen d

@juergend-죄송합니다, 이해하지 마십시오.
알마 마

예를 들어 나노초 간격으로 호출하면 동일한 타임 스탬프를 얻을 수 있습니다. 예를 들어 일부 시간 함수는 100ns 단계로만 시간을 반환 할 수 있으며 일부는 초 단위로만 반환 할 수 있습니다.
juergen d

@juergend 아, 그. 예. 초만있는 '클래식'타임 스탬프를 언급했습니다. 당신 같은 행위가 말한하지만 - 예 (타임머신 만 잎 우리에게 옵션은 고유하지 않은 타임 스탬프를 얻을 수 있음)
알마 마에게

1
머리 위로, 허용되는 답변은 CSPRNG를 활용하지 않습니다 .
Scott Arciszewski

답변:


147

PHP에서는 random_bytes(). 이유 : 암호 알림 토큰을 얻을 수있는 방법을 찾고 있으며, 일회성 로그인 자격 증명 인 경우 실제로 보호 할 데이터 (즉, 전체 사용자 계정)가 있습니다.

따라서 코드는 다음과 같습니다.

//$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 , 가장 많은 피어 리뷰를받은 이러한 기능에 대한 polyfill입니다;)
Scott Arciszewski

이 토큰을 저장하기 위해 SQL 데이터베이스에 varchar (64) 필드를 만들었습니다. $ length를 64로 설정했지만 반환 된 문자열의 길이는 128 자입니다. 고정 크기 (여기서는 64)의 문자열을 어떻게 얻을 수 있습니까?
gordie

2
(32)에 설정을 길이를 @gordie, 각 바이트는 2 진수 자
JohnHoulderUK

무엇이어야 $length합니까? 사용자 ID? 또는 무엇을?
스택

71

이것은 '최상의 임의'요청에 응답합니다.

Security.StackExchange 의 Adi의 답변 1 에는 이에 대한 솔루션이 있습니다.

OpenSSL을 지원하는지 확인하세요.이 한 줄짜리로 잘못되지 않을 것입니다.

$token = bin2hex(openssl_random_pseudo_bytes(16));

1. Adi, Mon Nov 12 2018, Celeritas, "Generating an unguessable token for confirm e-mails", Sep 20 '13 at 7:06, https://security.stackexchange.com/a/40314/


24
openssl_random_pseudo_bytes($length)-지원 : PHP 5> = 5.3.0, ....................................... ................... (PHP 7 random_bytes($length)이상인 경우 ) ...................... .................... (PHP 5.3 이하-5.3 이하 PHP 사용 안 함)
jave.web

54

허용 된 답변 ( md5(uniqid(mt_rand(), true))) 의 이전 버전 은 안전하지 않으며 가능한 출력은 약 2 ^ 60 개뿐입니다. 저예산 공격자에 대한 약 1 주일 내에 무차별 대입 검색 범위 내에 있습니다.

때문에 56 비트 DES 키가 될 수있는 약 24 시간 동안 무차별 강제 하고 평균 케이스 엔트로피 (59)에 대한 비트를 가질 것이며, 우리 팔일 약 2 ^ 2분의 59 ^ 56 = 계산할 수있다. 이 토큰 확인이 구현되는 방법에 따라 실제로 타이밍 정보를 유출하고 유효한 재설정 토큰의 처음 N 바이트를 추론 할 수 있습니다.

질문은 "모범 사례"에 관한 것이며 다음으로 시작됩니다.

비밀번호 분실에 대한 식별자를 생성하고 싶습니다

...이 토큰에 암시 적 보안 요구 사항이 있다고 추론 할 수 있습니다. 그리고 난수 생성기에 보안 요구 사항을 추가 할 때 가장 좋은 방법은 항상 암호화 된 보안 의사 난수 생성기 (약칭 CSPRNG)를 사용하는 것입니다.


CSPRNG 사용

PHP 7에서는 사용할 수 있습니다 bin2hex(random_bytes($n))(여기서는 $n15보다 큰 정수).

PHP 5에서는을 사용 random_compat하여 동일한 API를 노출 할 수 있습니다 .

또한, bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))당신이 경우 ext/mcrypt설치. 또 다른 좋은 한 줄은 bin2hex(openssl_random_pseudo_bytes($n)).

유효성 검사기에서 조회 분리

PHP의 보안 "remember me"쿠키에 대한 이전 작업에서 가져온 , 앞서 언급 한 타이밍 누출 (일반적으로 데이터베이스 쿼리에 의해 도입 됨)을 완화하는 유일한 효과적인 방법은 조회와 유효성 검사를 분리하는 것입니다.

테이블이 다음과 같은 경우 (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과 같이 열을 하나 더 추가해야합니다 .

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를 가져오고 .NET을 사용하여 데이터베이스에 저장된 토큰과 함께 사용자가 제공하는 토큰의 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)를 사용하여 토큰의 해시 된 16 진수 값을 DB에 저장하고 hash('sha256', bin2hex($token)), 2) 인증을 사용하는 것이 가능하고 안전합니까 if (hash_equals(hash('sha256', $validator), $results[0]['token'])) {...? 감사!
Guicara

예, 16 진수 문자열을 비교하는 것도 안전합니다. 정말 선호도의 문제입니다. 나는 원시 바이너리에서 모든 암호화 작업을 수행하고 전송 또는 저장을 위해 hex / base64로만 변환하는 것을 선호합니다.
Scott Arciszewski

안녕하세요 Scott, 기본적으로 귀하의 답변뿐 아니라 "Remember Me"기능에 대한 전체 기사에 대한 질문입니다. 고유 항목 id을 선택기로 사용하지 않는 이유는 무엇 입니까? 내 말은, account_recovery테이블 의 기본 키입니다 . 선택기에 대한 추가 보안 계층이 필요하지 않습니까? 감사!
Andre Polykanine

id:secret괜찮습니다. selector:secret괜찮습니다. secret그 자체는 아닙니다. 목표는 인증 프로토콜 (일정 시간이어야 함)에서 데이터베이스 쿼리 (타이밍 누출)를 분리하는 것입니다.
Scott Arciszewski

PHP 5.6을 실행 openssl_random_pseudo_bytes하는 random_bytes경우 대신 사용하는 데 해가 있습니까? 또한 링크의 쿼리 문자열에 유효성 검사기가 아닌 선택 자만 추가하면 안됩니까?
greg

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

2

이것은 매우 임의의 토큰이 필요할 때 유용 할 수 있습니다.

<?php
   echo mb_strtoupper(strval(bin2hex(openssl_random_pseudo_bytes(16))));
?>
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.