GA.php 7.0 KB

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