88 lines
2.4 KiB
PHP
88 lines
2.4 KiB
PHP
<?php
|
|
namespace App\Support\Security;
|
|
|
|
class TotpService
|
|
{
|
|
public function generateSecret(int $length = 32): string
|
|
{
|
|
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
$secret = '';
|
|
|
|
for ($i = 0; $i < $length; $i++) {
|
|
$secret .= $alphabet[random_int(0, strlen($alphabet) - 1)];
|
|
}
|
|
|
|
return $secret;
|
|
}
|
|
|
|
public function verify(string $base32Secret, string $code, int $window = 1): bool
|
|
{
|
|
$code = trim($code);
|
|
if (! preg_match('/^\d{6}$/', $code)) {
|
|
return false;
|
|
}
|
|
|
|
$timeSlice = (int) floor(time() / 30);
|
|
|
|
for ($offset = -$window; $offset <= $window; $offset++) {
|
|
if (hash_equals($this->totp($base32Secret, $timeSlice + $offset), $code)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function buildOtpAuthUri(string $issuer, string $label, string $secret): string
|
|
{
|
|
$issuerEncoded = rawurlencode($issuer);
|
|
$labelEncoded = rawurlencode($label);
|
|
|
|
return "otpauth://totp/{$issuerEncoded}:{$labelEncoded}?secret={$secret}&issuer={$issuerEncoded}&algorithm=SHA1&digits=6&period=30";
|
|
}
|
|
|
|
private function totp(string $base32Secret, int $timeSlice): string
|
|
{
|
|
$secret = $this->base32Decode($base32Secret);
|
|
if ($secret === '') {
|
|
return '000000';
|
|
}
|
|
|
|
$time = pack('N*', 0, $timeSlice);
|
|
$hash = hash_hmac('sha1', $time, $secret, true);
|
|
$offset = ord(substr($hash, -1)) & 0x0F;
|
|
$truncated = substr($hash, $offset, 4);
|
|
$value = unpack('N', $truncated)[1] & 0x7FFFFFFF;
|
|
|
|
return str_pad((string) ($value % 1000000), 6, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
private function base32Decode(string $input): string
|
|
{
|
|
$input = strtoupper(preg_replace('/[^A-Z2-7]/', '', $input) ?? '');
|
|
if ($input === '') {
|
|
return '';
|
|
}
|
|
|
|
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
$bits = '';
|
|
|
|
foreach (str_split($input) as $char) {
|
|
$index = strpos($alphabet, $char);
|
|
if ($index === false) {
|
|
continue;
|
|
}
|
|
$bits .= str_pad(decbin($index), 5, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
$output = '';
|
|
foreach (str_split($bits, 8) as $chunk) {
|
|
if (strlen($chunk) === 8) {
|
|
$output .= chr(bindec($chunk));
|
|
}
|
|
}
|
|
|
|
return $output;
|
|
}
|
|
}
|