GoogleAuthenticator.php 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. <?php
  2. namespace fast;
  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. *
  10. * @link http://www.phpgangsta.de/
  11. */
  12. class GoogleAuthenticator
  13. {
  14. protected $_codeLength = 6;
  15. /**
  16. * Create new secret.
  17. * 16 characters, randomly chosen from the allowed base32 characters.
  18. *
  19. * @param int $secretLength
  20. *
  21. * @return string
  22. */
  23. public function createSecret($secretLength = 16)
  24. {
  25. $validChars = $this->_getBase32LookupTable();
  26. // Valid secret lengths are 80 to 640 bits
  27. if ($secretLength < 16 || $secretLength > 128) {
  28. throw new \Exception('Bad secret length');
  29. }
  30. $secret = '';
  31. $rnd = false;
  32. if (function_exists('random_bytes')) {
  33. $rnd = random_bytes($secretLength);
  34. } elseif (function_exists('mcrypt_create_iv')) {
  35. $rnd = mcrypt_create_iv($secretLength, MCRYPT_DEV_URANDOM);
  36. } elseif (function_exists('openssl_random_pseudo_bytes')) {
  37. $rnd = openssl_random_pseudo_bytes($secretLength, $cryptoStrong);
  38. if (!$cryptoStrong) {
  39. $rnd = false;
  40. }
  41. }
  42. if ($rnd !== false) {
  43. for ($i = 0; $i < $secretLength; ++$i) {
  44. $secret .= $validChars[ord($rnd[$i]) & 31];
  45. }
  46. } else {
  47. throw new \Exception('No source of secure random');
  48. }
  49. return $secret;
  50. }
  51. /**
  52. * Calculate the code, with given secret and point in time.
  53. *
  54. * @param string $secret
  55. * @param int|null $timeSlice
  56. *
  57. * @return string
  58. */
  59. public function getCode($secret, $timeSlice = null)
  60. {
  61. if ($timeSlice === null) {
  62. $timeSlice = floor(time() / 30);
  63. }
  64. $secretkey = $this->_base32Decode($secret);
  65. // Pack time into binary string
  66. $time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice);
  67. // Hash it with users secret key
  68. $hm = hash_hmac('SHA1', $time, $secretkey, true);
  69. // Use last nipple of result as index/offset
  70. $offset = ord(substr($hm, -1)) & 0x0F;
  71. // grab 4 bytes of the result
  72. $hashpart = substr($hm, $offset, 4);
  73. // Unpak binary value
  74. $value = unpack('N', $hashpart);
  75. $value = $value[1];
  76. // Only 32 bits
  77. $value = $value & 0x7FFFFFFF;
  78. $modulo = pow(10, $this->_codeLength);
  79. return str_pad($value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT);
  80. }
  81. /**
  82. * Get QR-Code URL for image, from google charts.
  83. *
  84. * @param string $name
  85. * @param string $secret
  86. * @param string $title
  87. * @param array $params
  88. *
  89. * @return string
  90. */
  91. public function getQRCodeGoogleUrl($name, $secret, $title = null, $params = array())
  92. {
  93. $width = !empty($params['width']) && (int) $params['width'] > 0 ? (int) $params['width'] : 200;
  94. $height = !empty($params['height']) && (int) $params['height'] > 0 ? (int) $params['height'] : 200;
  95. $level = !empty($params['level']) && array_search($params['level'], array('L', 'M', 'Q', 'H')) !== false ? $params['level'] : 'M';
  96. $urlencoded = urlencode('otpauth://totp/'.$name.'?secret='.$secret.'');
  97. if (isset($title)) {
  98. $urlencoded .= urlencode('&issuer='.urlencode($title));
  99. }
  100. return "https://api.qrserver.com/v1/create-qr-code/?data=$urlencoded&size=${width}x${height}&ecc=$level";
  101. }
  102. /**
  103. * Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now.
  104. *
  105. * @param string $secret
  106. * @param string $code
  107. * @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after)
  108. * @param int|null $currentTimeSlice time slice if we want use other that time()
  109. *
  110. * @return bool
  111. */
  112. public function verifyCode($secret, $code, $discrepancy = 1, $currentTimeSlice = null)
  113. {
  114. if ($currentTimeSlice === null) {
  115. $currentTimeSlice = floor(time() / 30);
  116. }
  117. if (strlen($code) != 6) {
  118. return false;
  119. }
  120. for ($i = -$discrepancy; $i <= $discrepancy; ++$i) {
  121. $calculatedCode = $this->getCode($secret, $currentTimeSlice + $i);
  122. if ($this->timingSafeEquals($calculatedCode, $code)) {
  123. return true;
  124. }
  125. }
  126. return false;
  127. }
  128. /**
  129. * Set the code length, should be >=6.
  130. *
  131. * @param int $length
  132. *
  133. * @return PHPGangsta_GoogleAuthenticator
  134. */
  135. public function setCodeLength($length)
  136. {
  137. $this->_codeLength = $length;
  138. return $this;
  139. }
  140. /**
  141. * Helper class to decode base32.
  142. *
  143. * @param $secret
  144. *
  145. * @return bool|string
  146. */
  147. protected function _base32Decode($secret)
  148. {
  149. if (empty($secret)) {
  150. return '';
  151. }
  152. $base32chars = $this->_getBase32LookupTable();
  153. $base32charsFlipped = array_flip($base32chars);
  154. $paddingCharCount = substr_count($secret, $base32chars[32]);
  155. $allowedValues = array(6, 4, 3, 1, 0);
  156. if (!in_array($paddingCharCount, $allowedValues)) {
  157. return false;
  158. }
  159. for ($i = 0; $i < 4; ++$i) {
  160. if ($paddingCharCount == $allowedValues[$i] &&
  161. substr($secret, -($allowedValues[$i])) != str_repeat($base32chars[32], $allowedValues[$i])) {
  162. return false;
  163. }
  164. }
  165. $secret = str_replace('=', '', $secret);
  166. $secret = str_split($secret);
  167. $binaryString = '';
  168. for ($i = 0; $i < count($secret); $i = $i + 8) {
  169. $x = '';
  170. if (!in_array($secret[$i], $base32chars)) {
  171. return false;
  172. }
  173. for ($j = 0; $j < 8; ++$j) {
  174. $x .= str_pad(base_convert(@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT);
  175. }
  176. $eightBits = str_split($x, 8);
  177. for ($z = 0; $z < count($eightBits); ++$z) {
  178. $binaryString .= (($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48) ? $y : '';
  179. }
  180. }
  181. return $binaryString;
  182. }
  183. /**
  184. * Get array with all 32 characters for decoding from/encoding to base32.
  185. *
  186. * @return array
  187. */
  188. protected function _getBase32LookupTable()
  189. {
  190. return array(
  191. 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7
  192. 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15
  193. 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23
  194. 'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31
  195. '=', // padding char
  196. );
  197. }
  198. /**
  199. * A timing safe equals comparison
  200. * more info here: http://blog.ircmaxell.com/2014/11/its-all-about-time.html.
  201. *
  202. * @param string $safeString The internal (safe) value to be checked
  203. * @param string $userString The user submitted (unsafe) value
  204. *
  205. * @return bool True if the two strings are identical
  206. */
  207. private function timingSafeEquals($safeString, $userString)
  208. {
  209. if (function_exists('hash_equals')) {
  210. return hash_equals($safeString, $userString);
  211. }
  212. $safeLen = strlen($safeString);
  213. $userLen = strlen($userString);
  214. if ($userLen != $safeLen) {
  215. return false;
  216. }
  217. $result = 0;
  218. for ($i = 0; $i < $userLen; ++$i) {
  219. $result |= (ord($safeString[$i]) ^ ord($userString[$i]));
  220. }
  221. // They are only identical strings if $result is exactly 0...
  222. return $result === 0;
  223. }
  224. }