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:
Atakan Doğan Özban
2026-07-17 18:49:34 +02:00
parent 8ec6e3fd50
commit e97a71e5b2
18 changed files with 1253 additions and 62 deletions
+104
View File
@@ -0,0 +1,104 @@
using System;
using System.Security.Cryptography;
using System.Text;
namespace atakanozbancom.Models.classes
{
public static class PasswordHasher
{
private const string Prefix = "pbkdf2";
private const int DefaultIterations = 100000;
private const int SaltSize = 16;
private const int HashSize = 32;
public static string Hash(string password)
{
if (password == null)
throw new ArgumentNullException("password");
var salt = new byte[SaltSize];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(salt);
}
var hash = Pbkdf2(password, salt, DefaultIterations, HashSize);
return string.Format(
"{0}${1}${2}${3}",
Prefix,
DefaultIterations,
Convert.ToBase64String(salt),
Convert.ToBase64String(hash));
}
public static bool IsHashed(string stored)
{
return !string.IsNullOrWhiteSpace(stored)
&& stored.StartsWith(Prefix + "$", StringComparison.Ordinal);
}
public static bool Verify(string password, string stored)
{
if (string.IsNullOrEmpty(password) || string.IsNullOrEmpty(stored))
return false;
if (!IsHashed(stored))
return FixedTimeEquals(password, stored);
var parts = stored.Split('$');
if (parts.Length != 4 || !string.Equals(parts[0], Prefix, StringComparison.Ordinal))
return false;
int iterations;
if (!int.TryParse(parts[1], out iterations) || iterations < 1000)
return false;
byte[] salt;
byte[] expected;
try
{
salt = Convert.FromBase64String(parts[2]);
expected = Convert.FromBase64String(parts[3]);
}
catch
{
return false;
}
var actual = Pbkdf2(password, salt, iterations, expected.Length);
return FixedTimeEquals(actual, expected);
}
private static byte[] Pbkdf2(string password, byte[] salt, int iterations, int length)
{
using (var derive = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA256))
{
return derive.GetBytes(length);
}
}
private static bool FixedTimeEquals(byte[] a, byte[] 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;
}
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;
}
}
}