GoogleAuthenticatorService.php 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. <?php
  2. namespace App\Services;
  3. class GoogleAuthenticatorService
  4. {
  5. const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
  6. public function generateSecret($length = 32)
  7. {
  8. $alphabet = self::BASE32_ALPHABET;
  9. $max = strlen($alphabet) - 1;
  10. $secret = '';
  11. for ($i = 0; $i < $length; $i++) {
  12. $secret .= $alphabet[random_int(0, $max)];
  13. }
  14. return $secret;
  15. }
  16. public function verifyCode($secret, $code, $window = 1, $period = 30, $digits = 6)
  17. {
  18. $code = trim((string) $code);
  19. if ($code === '' || !preg_match('/^\d{6}$/', $code)) {
  20. return false;
  21. }
  22. $timeSlice = (int) floor(time() / $period);
  23. for ($i = -$window; $i <= $window; $i++) {
  24. $calc = $this->getCode($secret, $timeSlice + $i, $digits);
  25. if (hash_equals($calc, $code)) {
  26. return true;
  27. }
  28. }
  29. return false;
  30. }
  31. public function getOtpAuthUrl($issuer, $account, $secret)
  32. {
  33. $label = rawurlencode($issuer . ':' . $account);
  34. $issuerParam = rawurlencode($issuer);
  35. return "otpauth://totp/{$label}?secret={$secret}&issuer={$issuerParam}&algorithm=SHA1&digits=6&period=30";
  36. }
  37. public function getQrCodeUrl($otpAuthUrl, $size = 200)
  38. {
  39. $encoded = rawurlencode($otpAuthUrl);
  40. return "https://api.qrserver.com/v1/create-qr-code/?size={$size}x{$size}&data={$encoded}";
  41. }
  42. protected function getCode($secret, $timeSlice, $digits)
  43. {
  44. $secretKey = $this->base32Decode($secret);
  45. if ($secretKey === '') {
  46. return '';
  47. }
  48. $time = pack('N*', 0) . pack('N*', $timeSlice);
  49. $hash = hash_hmac('sha1', $time, $secretKey, true);
  50. $offset = ord(substr($hash, -1)) & 0x0F;
  51. $truncatedHash = substr($hash, $offset, 4);
  52. $value = unpack('N', $truncatedHash)[1] & 0x7FFFFFFF;
  53. $modulo = pow(10, $digits);
  54. return str_pad((string) ($value % $modulo), $digits, '0', STR_PAD_LEFT);
  55. }
  56. protected function base32Decode($secret)
  57. {
  58. $secret = strtoupper($secret);
  59. $secret = preg_replace('/[^A-Z2-7]/', '', $secret);
  60. if ($secret === '') {
  61. return '';
  62. }
  63. $alphabet = array_flip(str_split(self::BASE32_ALPHABET));
  64. $binary = '';
  65. $buffer = 0;
  66. $bitsLeft = 0;
  67. foreach (str_split($secret) as $char) {
  68. if (!isset($alphabet[$char])) {
  69. continue;
  70. }
  71. $buffer = ($buffer << 5) | $alphabet[$char];
  72. $bitsLeft += 5;
  73. while ($bitsLeft >= 8) {
  74. $bitsLeft -= 8;
  75. $binary .= chr(($buffer >> $bitsLeft) & 0xFF);
  76. }
  77. }
  78. return $binary;
  79. }
  80. }