将 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().

我比较了三个来源之间的时间:

他们都是一样的。尽管如此,我的 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() 中通过替换 01 字符暗示了这一点,因为这些字符不存在于 base32 字母表中。