将 TOTP 实现与 Google 验证器相匹配
Matching TOTP implementation with Google Authenticator
(解决方案) TL;DR: Google 假定密钥字符串是 base32 编码的;将 1
替换为 I
,将 0
替换为 O
。这必须在散列之前解码。
原题
我很难让我的代码与 GA 匹配。我什至从当前时间步开始追逐 +/- ~100,000 的计数器,但一无所获。我很高兴看到我的函数通过了 RFC 6238 附录中的 SHA-1 测试,但是当应用于 "real life" 时它似乎失败了。
我什至在 Github (here) 上查看了 Google Authenticator 的开源代码。我使用密钥进行测试:"qwertyuiopasdfgh"
。根据 Github 代码:
/*
* Return key entered by user, replacing visually similar characters 1 and 0.
*/
private String getEnteredKey() {
String enteredKey = keyEntryField.getText().toString();
return enteredKey.replace('1', 'I').replace('0', 'O');
}
我相信我的密钥不会被修改。通过文件追踪,似乎密钥通过调用保持不变:AuthenticatorActivity.saveSecret() -> AccountDb.add() -> AccountDb.newContentValuesWith()
.
我比较了三个来源之间的时间:
- (erlang shell):
now()
- (bash):
date "+%s"
- (Google/bash):
pattern="\s*date\:\s*"; curl -I https://www.google.com 2>/dev/null | grep -iE $pattern | sed -e "s/$pattern//g" | xargs -0 date "+%s" -d
他们都是一样的。尽管如此,我的 phone 似乎与我的计算机有些偏差。它会改变与我的电脑不同步的步骤。然而,我试图通过 +/- 数千来追查正确的时间步长,但没有找到任何东西。根据 NetworkTimeProvider
class,这是应用程序的时间来源。
此代码适用于 RFC 中的所有 SHA-1 测试:
totp(Secret, Time) ->
% {M, S, _} = os:timestamp(),
Msg = binary:encode_unsigned(Time), %(M*1000000+S) div 30,
%% Create 0-left-padded 64-bit binary from Time
Bin = <<0:((8-size(Msg))*8),Msg/binary>>,
%% Create SHA-1 hash
Hash = crypto:hmac(sha, Secret, Bin),
%% Determine dynamic offset
Offset = 16#0f band binary:at(Hash,19),
%% Ignore that many bytes and store 4 bytes into THash
<<_:Offset/binary, THash:4/binary, _/binary>> = Hash,
%% Remove sign bit and create 6-digit code
Code = (binary:decode_unsigned(THash) band 16#7fffffff) rem 1000000,
%% Convert to text-string and 0-lead-pad if necessary
lists:flatten(string:pad(integer_to_list(Code),6,leading,[=12=])).
要使其真正匹配 RFC,需要针对上面的 8 位数字进行修改。我修改了它以尝试追查正确的步骤。目的是弄清楚我的时间是怎么错的。没有成功:
totp(_,_,_,0) ->
{ok, not_found};
totp(Secret,Goal,Ctr,Stop) ->
Msg = binary:encode_unsigned(Ctr),
Bin = <<0:((8-size(Msg))*8),Msg/binary>>,
Hash = crypto:hmac(sha, Secret, Bin),
Offset = 16#0f band binary:at(Hash,19),
<<_:Offset/binary, THash:4/binary, _/binary>> = Hash,
Code = (binary:decode_unsigned(THash) band 16#7fffffff) rem 1000000,
if Code =:= Goal ->
{ok, {offset, 2880 - Stop}};
true ->
totp(Secret,Goal,Ctr+1,Stop-1) %% Did another run with Ctr-1
end.
有什么明显的突出之处吗?
我很想制作自己的 Android 应用程序来为我的项目实施 TOTP。我确实继续查看 Java 代码。在下载 git 存储库和 grep -R
查找函数调用的帮助下,我发现了我的问题。要获得与 Google Authenticator 相同的 pin 代码,假定密钥是 base32 编码的,并且必须在将其传递给哈希算法之前对其进行解码。
getEnteredKey()
中通过替换 0
和 1
字符暗示了这一点,因为这些字符不存在于 base32 字母表中。
(解决方案) TL;DR: Google 假定密钥字符串是 base32 编码的;将 1
替换为 I
,将 0
替换为 O
。这必须在散列之前解码。
原题
我很难让我的代码与 GA 匹配。我什至从当前时间步开始追逐 +/- ~100,000 的计数器,但一无所获。我很高兴看到我的函数通过了 RFC 6238 附录中的 SHA-1 测试,但是当应用于 "real life" 时它似乎失败了。
我什至在 Github (here) 上查看了 Google Authenticator 的开源代码。我使用密钥进行测试:"qwertyuiopasdfgh"
。根据 Github 代码:
/*
* Return key entered by user, replacing visually similar characters 1 and 0.
*/
private String getEnteredKey() {
String enteredKey = keyEntryField.getText().toString();
return enteredKey.replace('1', 'I').replace('0', 'O');
}
我相信我的密钥不会被修改。通过文件追踪,似乎密钥通过调用保持不变:AuthenticatorActivity.saveSecret() -> AccountDb.add() -> AccountDb.newContentValuesWith()
.
我比较了三个来源之间的时间:
- (erlang shell):
now()
- (bash):
date "+%s"
- (Google/bash):
pattern="\s*date\:\s*"; curl -I https://www.google.com 2>/dev/null | grep -iE $pattern | sed -e "s/$pattern//g" | xargs -0 date "+%s" -d
他们都是一样的。尽管如此,我的 phone 似乎与我的计算机有些偏差。它会改变与我的电脑不同步的步骤。然而,我试图通过 +/- 数千来追查正确的时间步长,但没有找到任何东西。根据 NetworkTimeProvider
class,这是应用程序的时间来源。
此代码适用于 RFC 中的所有 SHA-1 测试:
totp(Secret, Time) ->
% {M, S, _} = os:timestamp(),
Msg = binary:encode_unsigned(Time), %(M*1000000+S) div 30,
%% Create 0-left-padded 64-bit binary from Time
Bin = <<0:((8-size(Msg))*8),Msg/binary>>,
%% Create SHA-1 hash
Hash = crypto:hmac(sha, Secret, Bin),
%% Determine dynamic offset
Offset = 16#0f band binary:at(Hash,19),
%% Ignore that many bytes and store 4 bytes into THash
<<_:Offset/binary, THash:4/binary, _/binary>> = Hash,
%% Remove sign bit and create 6-digit code
Code = (binary:decode_unsigned(THash) band 16#7fffffff) rem 1000000,
%% Convert to text-string and 0-lead-pad if necessary
lists:flatten(string:pad(integer_to_list(Code),6,leading,[=12=])).
要使其真正匹配 RFC,需要针对上面的 8 位数字进行修改。我修改了它以尝试追查正确的步骤。目的是弄清楚我的时间是怎么错的。没有成功:
totp(_,_,_,0) ->
{ok, not_found};
totp(Secret,Goal,Ctr,Stop) ->
Msg = binary:encode_unsigned(Ctr),
Bin = <<0:((8-size(Msg))*8),Msg/binary>>,
Hash = crypto:hmac(sha, Secret, Bin),
Offset = 16#0f band binary:at(Hash,19),
<<_:Offset/binary, THash:4/binary, _/binary>> = Hash,
Code = (binary:decode_unsigned(THash) band 16#7fffffff) rem 1000000,
if Code =:= Goal ->
{ok, {offset, 2880 - Stop}};
true ->
totp(Secret,Goal,Ctr+1,Stop-1) %% Did another run with Ctr-1
end.
有什么明显的突出之处吗?
我很想制作自己的 Android 应用程序来为我的项目实施 TOTP。我确实继续查看 Java 代码。在下载 git 存储库和 grep -R
查找函数调用的帮助下,我发现了我的问题。要获得与 Google Authenticator 相同的 pin 代码,假定密钥是 base32 编码的,并且必须在将其传递给哈希算法之前对其进行解码。
getEnteredKey()
中通过替换 0
和 1
字符暗示了这一点,因为这些字符不存在于 base32 字母表中。