netgescon-day0/app/Support/Security/TotpService.php

88 lines
2.4 KiB
PHP
Executable File

<?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;
}
}