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