GoogleAuthenticator.php 6.2 KB

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