using System.Security.Cryptography;
using System.Text;
namespace Campus_live_rewrite.api.server
{
///
/// RFC 6238 TOTP (the algorithm behind Google Authenticator / Authy / 1Password's codes) - standard HMAC-SHA1,
/// 30-second step, 6 digits. No NuGet package: it's ~20 lines on top of the BCL's own HMACSHA1.
///
public static class Totp
{
private const string Base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
private const int Digits = 6;
private const int StepSeconds = 30;
/// A fresh 160-bit secret, base32-encoded (what an authenticator app wants typed in or scanned).
public static string GenerateSecret()
{
var bytes = RandomNumberGenerator.GetBytes(20);
return Base32Encode(bytes);
}
public static string Base32Encode(byte[] data)
{
var sb = new StringBuilder((data.Length * 8 + 4) / 5);
int bits = 0, value = 0;
foreach (byte b in data)
{
value = (value << 8) | b; bits += 8;
while (bits >= 5) { sb.Append(Base32Alphabet[(value >> (bits - 5)) & 0x1F]); bits -= 5; }
}
if (bits > 0) sb.Append(Base32Alphabet[(value << (5 - bits)) & 0x1F]);
return sb.ToString();
}
public static byte[] Base32Decode(string s)
{
s = (s ?? "").Trim().TrimEnd('=').Replace(" ", "").ToUpperInvariant();
var outBytes = new List(s.Length * 5 / 8);
int bits = 0, value = 0;
foreach (char c in s)
{
int idx = Base32Alphabet.IndexOf(c);
if (idx < 0) continue; // skip anything an app/user might paste in (dashes, etc.)
value = (value << 5) | idx; bits += 5;
if (bits >= 8) { outBytes.Add((byte)((value >> (bits - 8)) & 0xFF)); bits -= 8; }
}
return outBytes.ToArray();
}
private static string Hotp(byte[] key, long counter)
{
var counterBytes = BitConverter.GetBytes(counter);
if (BitConverter.IsLittleEndian) Array.Reverse(counterBytes);
using var h = new HMACSHA1(key);
byte[] hash = h.ComputeHash(counterBytes);
int offset = hash[^1] & 0x0F;
int code = ((hash[offset] & 0x7F) << 24) | ((hash[offset + 1] & 0xFF) << 16) | ((hash[offset + 2] & 0xFF) << 8) | (hash[offset + 3] & 0xFF);
return (code % (int)Math.Pow(10, Digits)).ToString(new string('0', Digits));
}
/// True if `code` matches the secret at the current 30s step, or one step either side (clock drift).
public static bool Verify(string base32Secret, string? code)
{
code = (code ?? "").Trim();
if (code.Length != Digits || !code.All(char.IsDigit)) return false;
byte[] key = Base32Decode(base32Secret);
if (key.Length == 0) return false;
long step = DateTimeOffset.UtcNow.ToUnixTimeSeconds() / StepSeconds;
for (long d = -1; d <= 1; d++)
if (CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(Hotp(key, step + d)), Encoding.ASCII.GetBytes(code)))
return true;
return false;
}
/// otpauth:// URI for a QR code, if the admin panel ever adds one - also readable as plain text to copy.
public static string OtpAuthUri(string secret, string account, string issuer = "Campus Admin") =>
$"otpauth://totp/{Uri.EscapeDataString(issuer)}:{Uri.EscapeDataString(account)}?secret={secret}&issuer={Uri.EscapeDataString(issuer)}&digits={Digits}&period={StepSeconds}";
}
}