1

(Solution) TL;DR: Google assumes the key string is base32 encoded; replacing any 1 with I and 0 with O. This must be decoded prior to hashing.

Original Question

I'm having difficulty having my code match up with GA. I even went chasing down counters +/- ~100,000 from the current time step and found nothing. I was very excited to see my function pass the SHA-1 tests in the RFC 6238 Appendix, however when applied to "real life" it seems to fail.

I went so far as to look at the open source code for Google Authenticator at Github (here). I used the key for testing: "qwertyuiopasdfgh". According to the Github code:

  /*
   * 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');
  }

I believe my key would not be modified. Tracing through the files it seems the key remains unchanged through calls: AuthenticatorActivity.saveSecret() -> AccountDb.add() -> AccountDb.newContentValuesWith().

I compared my time between three sources:

  • (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

They are all the same. Despite that, it appears my phone is a bit off from my computer. It will change steps not in sync with my computer. However me trying to chase down the proper time step by +/- thousands didn't find anything. According to the NetworkTimeProvider class, that is the time source for the app.

This code worked with all the SHA-1 tests in the RFC:

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,$0)).

For it to truly match the RFC it would need to be modified for 8-digit numbers above. I modified it to try and chase down the proper step. The goal was to figure out how my time was wrong. Didn't work out:

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.

Anything obvious stick out?

Nolan Robidoux
  • 395
  • 2
  • 17

1 Answers1

1

I was tempted to make my own Android application to implement TOTP for my project. I did continue looking at the Java code. With aid of downloading the git repository and grep -R to find function calls I discovered my problem. To get the same pin codes as Google Authenticator the key is assumed to be base32 encoded and must be decoded prior to passing it to the hash algorithm.

There was a hint of this in getEnteredKey() by replacing the 0 and 1 characters as these are not present in the base32 alphabet.

Nolan Robidoux
  • 395
  • 2
  • 17