我实施 2 因素授权有什么问题?
What's wrong with my implementation of 2 Factor Authorization?
我正在尝试实现我自己的 PHP 函数来为 Google 验证器生成代码。我这样做是为了好玩和学习新东西。这是我所做的:
function twoFactorAuthorizationCode(string $secretBase32, int $digitsCount): string {
$counter = (int) (time() / 30);
$secret = Base32::decode($secretBase32);
$hash = hash_hmac('sha1', $counter, $secret, true); // 20 binary characters
$hexHash = unpack('H*', $hash)[1]; // 40 hex characters
$offset = hexdec($hexHash[-1]); // last 4 bits of $hash
$truncatedHash = hexdec(substr($hexHash, $offset * 2, 8)) & 0x7fffffff; // last 31 bits
$code = $truncatedHash % (10 ** $digitsCount);
return str_pad($code, $digitsCount, '0', STR_PAD_LEFT);
}
我不确定哪一步是错误的,但它不会生成与 Google 身份验证器相同的结果。显然,我尝试使用时间偏移,以防我的时钟与 Google 验证器的时钟不同步。
我不确定的一些事情是:
- 秘密应该从 Base32 解码,还是应该保留 Base32 字符串?
- 计数器是 SHA1 哈希的值还是键?
我做了很多实验,但无法让我的算法生成有效结果。非常感谢任何建议。
我在反复试验中找到了答案。所以,问题出在我直接散列的 $counter
值中:
$hash = hash_hmac('sha1', $counter, $secret, true);
相反,它应该是由 $counter
:
组成的 64 位二进制字符串
$packedCounter = pack('J', $counter);
$hash = hash_hmac('sha1', $packedCounter, $secret, true);
说明
假设我们的 Unix 时间戳是 1578977176
.
这使得计数器如下:(int) (1578977176 / 30) = 52632572
.
用于散列的值必须是 64 位大端字节序字符串。这意味着我们需要用零填充它以使其成为 64 位。
52632572
在二进制中是 11001000110001101111111100
。那只是 26 位,所以我们还需要 38 位。我们现在拥有的是:
0000000000000000000000000000000000000011001000110001101111100010
.
每个字符都是一个字节,所以我们把它分成8个一组:
00000000 00000000 00000000 00000000 00000011 00100011 00011011 11100010
我们现在可以通过其代码将每个组转换为一个字符:
$packedCounter = chr(0b00000000)
. chr(0b00000000)
. chr(0b00000000)
. chr(0b00000000)
. chr(0b00000011)
. chr(0b00100011)
. chr(0b00011011)
. chr(0b11100010);
这就是我们要散列的字符串,这正是 pack('J', $string)
所做的。
瞧!
我正在尝试实现我自己的 PHP 函数来为 Google 验证器生成代码。我这样做是为了好玩和学习新东西。这是我所做的:
function twoFactorAuthorizationCode(string $secretBase32, int $digitsCount): string {
$counter = (int) (time() / 30);
$secret = Base32::decode($secretBase32);
$hash = hash_hmac('sha1', $counter, $secret, true); // 20 binary characters
$hexHash = unpack('H*', $hash)[1]; // 40 hex characters
$offset = hexdec($hexHash[-1]); // last 4 bits of $hash
$truncatedHash = hexdec(substr($hexHash, $offset * 2, 8)) & 0x7fffffff; // last 31 bits
$code = $truncatedHash % (10 ** $digitsCount);
return str_pad($code, $digitsCount, '0', STR_PAD_LEFT);
}
我不确定哪一步是错误的,但它不会生成与 Google 身份验证器相同的结果。显然,我尝试使用时间偏移,以防我的时钟与 Google 验证器的时钟不同步。
我不确定的一些事情是:
- 秘密应该从 Base32 解码,还是应该保留 Base32 字符串?
- 计数器是 SHA1 哈希的值还是键?
我做了很多实验,但无法让我的算法生成有效结果。非常感谢任何建议。
我在反复试验中找到了答案。所以,问题出在我直接散列的 $counter
值中:
$hash = hash_hmac('sha1', $counter, $secret, true);
相反,它应该是由 $counter
:
$packedCounter = pack('J', $counter);
$hash = hash_hmac('sha1', $packedCounter, $secret, true);
说明
假设我们的 Unix 时间戳是 1578977176
.
这使得计数器如下:(int) (1578977176 / 30) = 52632572
.
用于散列的值必须是 64 位大端字节序字符串。这意味着我们需要用零填充它以使其成为 64 位。
52632572
在二进制中是 11001000110001101111111100
。那只是 26 位,所以我们还需要 38 位。我们现在拥有的是:
0000000000000000000000000000000000000011001000110001101111100010
.
每个字符都是一个字节,所以我们把它分成8个一组:
00000000 00000000 00000000 00000000 00000011 00100011 00011011 11100010
我们现在可以通过其代码将每个组转换为一个字符:
$packedCounter = chr(0b00000000)
. chr(0b00000000)
. chr(0b00000000)
. chr(0b00000000)
. chr(0b00000011)
. chr(0b00100011)
. chr(0b00011011)
. chr(0b11100010);
这就是我们要散列的字符串,这正是 pack('J', $string)
所做的。
瞧!