/ lib / GoogleAuthenticator.php
GoogleAuthenticator.php
  1  <?php
  2  
  3  /**
  4   * PHP Class for handling Google Authenticator 2-factor authentication
  5   *
  6   * @author Michael Kliewe
  7   * @copyright 2012 Michael Kliewe
  8   * @license http://www.opensource.org/licenses/bsd-license.php BSD License
  9   * @link http://www.phpgangsta.de/
 10   */
 11  
 12  class PHPGangsta_GoogleAuthenticator
 13  {
 14      protected $_codeLength = 6;
 15  
 16      /**
 17       * Create new secret.
 18       * 16 characters, randomly chosen from the allowed base32 characters.
 19       *
 20       * @param int $secretLength
 21       * @return string
 22       */
 23      public function createSecret($secretLength = 16)
 24      {
 25          $validChars = $this->_getBase32LookupTable();
 26          unset($validChars[32]);
 27  
 28          $secret = '';
 29          for ($i = 0; $i < $secretLength; $i++) {
 30              $secret .= $validChars[array_rand($validChars)];
 31          }
 32          return $secret;
 33      }
 34  
 35      /**
 36       * Calculate the code, with given secret and point in time
 37       *
 38       * @param string $secret
 39       * @param int|null $timeSlice
 40       * @return string
 41       */
 42      public function getCode($secret, $timeSlice = null)
 43      {
 44          if ($timeSlice === null) {
 45              $timeSlice = floor(time() / 30);
 46          }
 47  
 48          $secretkey = $this->_base32Decode($secret);
 49  
 50          // Pack time into binary string
 51          $time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice);
 52          // Hash it with users secret key
 53          $hm = hash_hmac('SHA1', $time, $secretkey, true);
 54          // Use last nipple of result as index/offset
 55          $offset = ord(substr($hm, -1)) & 0x0F;
 56          // grab 4 bytes of the result
 57          $hashpart = substr($hm, $offset, 4);
 58  
 59          // Unpak binary value
 60          $value = unpack('N', $hashpart);
 61          $value = $value[1];
 62          // Only 32 bits
 63          $value = $value & 0x7FFFFFFF;
 64  
 65          $modulo = pow(10, $this->_codeLength);
 66          return str_pad($value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT);
 67      }
 68  
 69      /**
 70       * Get QR-Code URL for image, from google charts
 71       *
 72       * @param string $name
 73       * @param string $secret
 74       * @return string
 75       */
 76      public function getQRCodeGoogleUrl($name, $secret) {
 77          $urlencoded = urlencode('otpauth://totp/'.$name.'?secret='.$secret.'');
 78          return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl='.$urlencoded.'';
 79      }
 80  
 81      /**
 82       * Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now
 83       *
 84       * @param string $secret
 85       * @param string $code
 86       * @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after)
 87       * @return bool
 88       */
 89      public function verifyCode($secret, $code, $discrepancy = 1)
 90      {
 91          $currentTimeSlice = floor(time() / 30);
 92  
 93          for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
 94              $calculatedCode = $this->getCode($secret, $currentTimeSlice + $i);
 95              if ($calculatedCode == $code ) {
 96                  return true;
 97              }
 98          }
 99  
100          return false;
101      }
102  
103      /**
104       * Set the code length, should be >=6
105       *
106       * @param int $length
107       * @return PHPGangsta_GoogleAuthenticator
108       */
109      public function setCodeLength($length)
110      {
111          $this->_codeLength = $length;
112          return $this;
113      }
114  
115      /**
116       * Helper class to decode base32
117       *
118       * @param $secret
119       * @return bool|string
120       */
121      protected function _base32Decode($secret)
122      {
123          if (empty($secret)) return '';
124  
125          $base32chars = $this->_getBase32LookupTable();
126          $base32charsFlipped = array_flip($base32chars);
127  
128          $paddingCharCount = substr_count($secret, $base32chars[32]);
129          $allowedValues = array(6, 4, 3, 1, 0);
130          if (!in_array($paddingCharCount, $allowedValues)) return false;
131          for ($i = 0; $i < 4; $i++){
132              if ($paddingCharCount == $allowedValues[$i] &&
133                  substr($secret, -($allowedValues[$i])) != str_repeat($base32chars[32], $allowedValues[$i])) return false;
134          }
135          $secret = str_replace('=','', $secret);
136          $secret = str_split($secret);
137          $binaryString = "";
138          for ($i = 0; $i < count($secret); $i = $i+8) {
139              $x = "";
140              if (!in_array($secret[$i], $base32chars)) return false;
141              for ($j = 0; $j < 8; $j++) {
142                  $x .= str_pad(base_convert(@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT);
143              }
144              $eightBits = str_split($x, 8);
145              for ($z = 0; $z < count($eightBits); $z++) {
146                  $binaryString .= ( ($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48 ) ? $y:"";
147              }
148          }
149          return $binaryString;
150      }
151  
152      /**
153       * Helper class to encode base32
154       *
155       * @param string $secret
156       * @param bool $padding
157       * @return string
158       */
159      protected function _base32Encode($secret, $padding = true)
160      {
161          if (empty($secret)) return '';
162  
163          $base32chars = $this->_getBase32LookupTable();
164  
165          $secret = str_split($secret);
166          $binaryString = "";
167          for ($i = 0; $i < count($secret); $i++) {
168              $binaryString .= str_pad(base_convert(ord($secret[$i]), 10, 2), 8, '0', STR_PAD_LEFT);
169          }
170          $fiveBitBinaryArray = str_split($binaryString, 5);
171          $base32 = "";
172          $i = 0;
173          while ($i < count($fiveBitBinaryArray)) {
174              $base32 .= $base32chars[base_convert(str_pad($fiveBitBinaryArray[$i], 5, '0'), 2, 10)];
175              $i++;
176          }
177          if ($padding && ($x = strlen($binaryString) % 40) != 0) {
178              if ($x == 8) $base32 .= str_repeat($base32chars[32], 6);
179              elseif ($x == 16) $base32 .= str_repeat($base32chars[32], 4);
180              elseif ($x == 24) $base32 .= str_repeat($base32chars[32], 3);
181              elseif ($x == 32) $base32 .= $base32chars[32];
182          }
183          return $base32;
184      }
185  
186      /**
187       * Get array with all 32 characters for decoding from/encoding to base32
188       *
189       * @return array
190       */
191      protected function _getBase32LookupTable()
192      {
193          return array(
194              'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', //  7
195              'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15
196              'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23
197              'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31
198              '='  // padding char
199          );
200      }
201  }