using System; using System.Globalization; using System.Security.Cryptography; using System.Text; using System.Web; namespace atakanozbancom.Models.classes { public static class TotpHelper { private const string Base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); public static string GenerateSecret(int byteLength = 20) { var bytes = new byte[byteLength]; using (var rng = RandomNumberGenerator.Create()) { rng.GetBytes(bytes); } return ToBase32(bytes); } public static bool VerifyCode(string base32Secret, string code, int window = 1) { if (string.IsNullOrWhiteSpace(base32Secret) || string.IsNullOrWhiteSpace(code)) return false; code = code.Trim().Replace(" ", ""); if (code.Length != 6) return false; long timestep; try { timestep = GetCurrentTimeStep(); } catch { return false; } for (var i = -window; i <= window; i++) { var expected = ComputeTotp(base32Secret, timestep + i); if (FixedTimeEquals(expected, code)) return true; } return false; } public static string BuildOtpAuthUri(string issuer, string accountName, string base32Secret) { var label = HttpUtility.UrlEncode(issuer + ":" + accountName); var issuerParam = HttpUtility.UrlEncode(issuer); return string.Format( CultureInfo.InvariantCulture, "otpauth://totp/{0}?secret={1}&issuer={2}&digits=6&period=30", label, base32Secret, issuerParam); } private static long GetCurrentTimeStep() { return (long)Math.Floor((DateTime.UtcNow - UnixEpoch).TotalSeconds / 30.0); } private static string ComputeTotp(string base32Secret, long timestep) { var key = FromBase32(base32Secret); var counter = BitConverter.GetBytes(timestep); if (BitConverter.IsLittleEndian) Array.Reverse(counter); byte[] hash; using (var hmac = new HMACSHA1(key)) { hash = hmac.ComputeHash(counter); } var offset = hash[hash.Length - 1] & 0x0F; var binary = ((hash[offset] & 0x7F) << 24) | ((hash[offset + 1] & 0xFF) << 16) | ((hash[offset + 2] & 0xFF) << 8) | (hash[offset + 3] & 0xFF); var otp = binary % 1000000; return otp.ToString("D6", CultureInfo.InvariantCulture); } private static string ToBase32(byte[] data) { if (data == null || data.Length == 0) return string.Empty; var sb = new StringBuilder((data.Length * 8 + 4) / 5); int buffer = data[0]; var next = 1; var bitsLeft = 8; while (bitsLeft > 0 || next < data.Length) { if (bitsLeft < 5) { if (next < data.Length) { buffer <<= 8; buffer |= data[next++] & 0xFF; bitsLeft += 8; } else { var pad = 5 - bitsLeft; buffer <<= pad; bitsLeft += pad; } } var index = (buffer >> (bitsLeft - 5)) & 0x1F; bitsLeft -= 5; sb.Append(Base32Alphabet[index]); } return sb.ToString(); } private static byte[] FromBase32(string input) { var cleaned = input.Trim().Replace(" ", "").Replace("=", "").ToUpperInvariant(); var output = new byte[cleaned.Length * 5 / 8]; var bitBuffer = 0; var bitsLeft = 0; var index = 0; foreach (var c in cleaned) { var val = Base32Alphabet.IndexOf(c); if (val < 0) throw new FormatException("Invalid Base32 character."); bitBuffer = (bitBuffer << 5) | val; bitsLeft += 5; if (bitsLeft >= 8) { output[index++] = (byte)((bitBuffer >> (bitsLeft - 8)) & 0xFF); bitsLeft -= 8; } } if (index != output.Length) { var trimmed = new byte[index]; Array.Copy(output, trimmed, index); return trimmed; } return output; } private static bool FixedTimeEquals(string a, string b) { if (a == null || b == null || a.Length != b.Length) return false; var diff = 0; for (var i = 0; i < a.Length; i++) diff |= a[i] ^ b[i]; return diff == 0; } } }