Google TOTP Two-factor Authentication for PHP

Published on by

At the beginning of the year Google released 2 Factor Authentication (2FA) for G-Mail providing an application for Android, IPhone and Blackberry called Google Authenticator to generate one time login tokens. This post will show how to implement Google 2FA to protect web applications from stolen credentials.

Google Authenticator is based on RFC 4226 - a Time based One Time Password (TOTP) which is initialized using a 16 digit base 32 (RFC 4648) encoded seed value. Initial seeds used for the TOTP can be entered into the Google Authenticator via a camera using QR codes or via the keyboard. Google has also provided a PAM module allowing users to integrate 2FA for sshd.

A module can be written to support the Google TOTP in any language - the only caveat with writing a library for PHP is a lack of an RFC 4648 compliant base 32 decoding function. A base 32 function is needed to decode the initial seed. This is probably the most tricky part of implementing Google's 2FA. The following function can be used:

function base32_decode($b32) {
  $lut = array("A" => 0,       "B" => 1,
               "C" => 2,       "D" => 3,
               "E" => 4,       "F" => 5,
               "G" => 6,       "H" => 7,
               "I" => 8,       "J" => 9,
               "K" => 10,      "L" => 11,
               "M" => 12,      "N" => 13,
               "O" => 14,      "P" => 15,
               "Q" => 16,      "R" => 17,
               "S" => 18,      "T" => 19,
               "U" => 20,      "V" => 21,
               "W" => 22,      "X" => 23,
               "Y" => 24,      "Z" => 25,
               "2" => 26,      "3" => 27,
               "4" => 28,      "5" => 29,
               "6" => 30,      "7" => 31
  );

  $b32    = strtoupper($b32);
  $l      = strlen($b32);
  $n      = 0;
  $j      = 0;
  $binary = "";

  for ($i = 0; $i < $l; $i++) {

       $n = $n << 5;
       $n = $n + $lut[$b32[$i]];       
       $j = $j + 5;

       if ($j >= 8) {
           $j = $j - 8;
           $binary .= chr(($n & (0xFF << $j)) >> $j);
       }
  }

  return $binary;
}

This binary seed value will be used in a SHA1 hash along with the current Unix time-stamp to generate one time tokens. The Unix time-stamp is divided by 30 so that the one time password changes every 30 seconds.

function get_timestamp() {
   return floor(microtime(true)/30);
}

Sadly you cant just pass the number from get_timestamp straight into the SHA1 function. The time-stamp first needs to be reduced into a binary string of 8 bytes. Since pack doesn't support 64bit integers we use two unsigned 32 bit integers to make up the binary form.

$binary_timestamp = pack('N*', 0) . pack('N*', $timestamp);

Once you have the binary seed and the binary timestamp you have to pass them into the "hash_hmac" function. This gives you a 20 byte SHA1 string.

$hash = hash_hmac ('sha1', $binary_timestamp, $binary_key, true);

This hash is then processed in accordance with RFC 4226 to obtain the one time password.

$offset = ord($hash[19]) & 0xf;

$OTP = (
   ((ord($hash[$offset+0]) & 0x7f) << 24 ) |
    ((ord($hash[$offset+1]) & 0xff) << 16 ) |
    ((ord($hash[$offset+2]) & 0xff) << 8 ) |
    (ord($hash[$offset+3]) & 0xff)
   ) % pow(10, 6);

Now $OTP should contain your one time password. There are however still a couple of small issues to overcome if you want to use this within an application:

  • Your client and server clocks may not be in sync - this could means that when you come to check your token generated by the user that it will fail. To combat this a you can either stipulate that the client and server clocks must be in perfect sync or you need to create a function which checks the tokens against those +/- 2 minutes of the current server time. This will allow your client and server to be up to 2 minutes out but obviously increases the chance that an attacker could correctly guess a one time token.

  • If there is no upper limit on the number of attempts a user can make at guessing a token it may be possible to brute-force the one-time token.

  • If the seed is too small and an attacker can intercept a few tokens it may be possible to brute-force the seed value allowing the attacker to generate new one-time tokens. For this reason Google enforces a minimum seed length of 16 characters or 80-bits.

  • If a token is not marked as invalid as soon as it has been used an attacker who has intercepted the token may be able to quickly replay it to obtain access.

Seed value 'PEHMPSDNLXIOG65U'

A class for PHP that implements Google TOTP can be downloaded here Its missing protection against brute-force attacks but otherwise fully functional.

You can check if its working by installing the Google Authenticator application and scanning the QR code to the right - codes generated by the application should match codes generated by the class. The function Google2FA::verify_key should be used to validate the users one time token as it allows the clients clock to drift either side of the server time by 2 minutes.

Custom QR codes can be generated using the Google QR generator at https://www.google.com/chart?chs=200x200&chld=M|0&cht=qr&chl=otpauth://totp/idontplaydarts?secret=SECRETVALUEHERE