WebOrbiton
v1.0.0.1

StocketBase

99 lines · 2.8 KB
  1. <?php
  2. ​
  3. declare(strict_types=1);
  4. ​
  5. final class Totp
  6. {
  7. private const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
  8. private const PERIOD = 30;
  9. ​
  10. public static function generateSecret(): string
  11. {
  12. return self::base32Encode(random_bytes(20));
  13. }
  14. ​
  15. public static function verify(string $secret, string $input, ?int $lastStep): ?int
  16. {
  17. $input = (string) preg_replace('/\s+/', '', $input);
  18. if (preg_match('/^\d{6}$/', $input) !== 1) {
  19. return null;
  20. }
  21. ​
  22. $current = intdiv(time(), self::PERIOD);
  23. ​
  24. for ($offset = -1; $offset <= 1; $offset++) {
  25. $step = $current + $offset;
  26. if ($lastStep !== null && $step <= $lastStep) {
  27. continue;
  28. }
  29. ​
  30. if (hash_equals(self::code($secret, $step), $input)) {
  31. return $step;
  32. }
  33. }
  34. ​
  35. return null;
  36. }
  37. ​
  38. public static function uri(string $secret, string $account, string $issuer): string
  39. {
  40. return 'otpauth://totp/' . rawurlencode($issuer . ':' . $account)
  41. . '?secret=' . $secret
  42. . '&issuer=' . rawurlencode($issuer)
  43. . '&algorithm=SHA1&digits=6&period=' . self::PERIOD;
  44. }
  45. ​
  46. public static function formatSecret(string $secret): string
  47. {
  48. return trim(chunk_split($secret, 4, ' '));
  49. }
  50. ​
  51. private static function code(string $secret, int $step): string
  52. {
  53. $key = self::base32Decode($secret);
  54. $hash = hash_hmac('sha1', pack('J', $step), $key, true);
  55. $offset = ord($hash[19]) & 0x0F;
  56. $value = ((ord($hash[$offset]) & 0x7F) << 24)
  57. | ((ord($hash[$offset + 1]) & 0xFF) << 16)
  58. | ((ord($hash[$offset + 2]) & 0xFF) << 8)
  59. | (ord($hash[$offset + 3]) & 0xFF);
  60. ​
  61. return str_pad((string) ($value % 1000000), 6, '0', STR_PAD_LEFT);
  62. }
  63. ​
  64. private static function base32Encode(string $data): string
  65. {
  66. $bits = '';
  67. foreach (str_split($data) as $char) {
  68. $bits .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT);
  69. }
  70. ​
  71. $encoded = '';
  72. foreach (str_split($bits, 5) as $chunk) {
  73. $encoded .= self::ALPHABET[bindec(str_pad($chunk, 5, '0', STR_PAD_RIGHT))];
  74. }
  75. ​
  76. return $encoded;
  77. }
  78. ​
  79. private static function base32Decode(string $secret): string
  80. {
  81. $bits = '';
  82. foreach (str_split(strtoupper((string) preg_replace('/[^A-Za-z2-7]/', '', $secret))) as $char) {
  83. $position = strpos(self::ALPHABET, $char);
  84. if ($position !== false) {
  85. $bits .= str_pad(decbin($position), 5, '0', STR_PAD_LEFT);
  86. }
  87. }
  88. ​
  89. $decoded = '';
  90. foreach (str_split($bits, 8) as $byte) {
  91. if (strlen($byte) === 8) {
  92. $decoded .= chr((int) bindec($byte));
  93. }
  94. }
  95. ​
  96. return $decoded;
  97. }
  98. }
  99. ​