Add password hashing, user settings with 2FA, and project image lightbox.
Hardens auth with PBKDF2, lockouts, local QR setup, and safer embeds while moving account security under the username menu.
This commit is contained in:
@@ -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