Add support page, admin features, and auth hardening.
Rename public affiliate route to /support with localized nav labels, and include affiliate/social/wallpaper admin, 2FA login, and related site updates. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
202dc16a52
commit
fc45c35dbe
@@ -0,0 +1,176 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user