Replace argon2, add threadsafe SHA256 method, rename som variables

This commit is contained in:
GHXX 2024-01-14 23:12:37 +01:00
parent ea74cb899c
commit e8131efc86

View File

@ -1,73 +1,81 @@
using Konscious.Security.Cryptography; using Newtonsoft.Json;
using Newtonsoft.Json; using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
namespace SimpleHttpServer.Login; namespace SimpleHttpServer.Login;
internal struct SerialLoginData { internal struct SerialLoginData {
public string salt; public string passwordSalt;
public string extraDataSalt;
public string pwd; public string pwd;
public string additionalData; public string extraData;
public LoginData toPlainData() { public LoginData ToPlainData() {
return new LoginData { return new LoginData {
salt = Convert.FromBase64String(salt), passwordSalt = Convert.FromBase64String(passwordSalt),
password = Convert.FromBase64String(pwd) extraDataSalt = Convert.FromBase64String(extraDataSalt)
}; };
} }
} }
internal struct LoginData { internal struct LoginData {
public byte[] salt; public byte[] passwordSalt;
public byte[] password; public byte[] extraDataSalt;
public byte[] encryptedData; public byte[] passwordHash;
public byte[] encryptedExtraData;
public SerialLoginData toSerial() { public SerialLoginData ToSerial() {
return new SerialLoginData { return new SerialLoginData {
salt = Convert.ToBase64String(salt), passwordSalt = Convert.ToBase64String(passwordSalt),
pwd = Convert.ToBase64String(password), extraDataSalt = Convert.ToBase64String(extraDataSalt),
additionalData = Convert.ToBase64String(encryptedData) pwd = Convert.ToBase64String(passwordHash),
extraData = Convert.ToBase64String(encryptedExtraData)
}; };
} }
} }
internal struct LoginDataProviderConfig { internal struct LoginDataProviderConfig {
/// <summary>
/// Size of the password salt and the extradata salt. So each salt will be of size <see cref="SALT_SIZE"/>.
/// </summary>
public int SALT_SIZE = 32; public int SALT_SIZE = 32;
public int KEY_LENGTH = 256 / 8; public int KEY_LENGTH = 256 / 8;
public int A2_ITERATIONS = 5;
public int A2_MEMORY_SIZE = 500_000;
public int A2_PARALLELISM = 8;
public int A2_HASH_LENGTH = 256 / 8;
public int A2_MAX_CONCURRENT = 4;
public int PBKDF2_ITERATIONS = 600_000; public int PBKDF2_ITERATIONS = 600_000;
public LoginDataProviderConfig() { } public LoginDataProviderConfig() { }
} }
public class LoginProvider<T> { public class LoginProvider<TExtraData> {
private static readonly Func<T, byte[]> JsonSerialize = t => Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(t)); private static readonly Func<TExtraData, byte[]> JsonSerialize = t => Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(t));
private static readonly Func<byte[], T> JsonDeserialize = b => JsonConvert.DeserializeObject<T>(Encoding.UTF8.GetString(b))!; private static readonly Func<byte[], TExtraData> JsonDeserialize = b => JsonConvert.DeserializeObject<TExtraData>(Encoding.UTF8.GetString(b))!;
[ThreadStatic]
private static SHA256? _sha256PerThread;
private static SHA256 Sha256PerThread { get => _sha256PerThread ??= SHA256.Create(); }
private readonly LoginDataProviderConfig config; private readonly LoginDataProviderConfig config;
private readonly ReaderWriterLockSlim ldLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); private readonly ReaderWriterLockSlim ldLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
private readonly string ldPath; private readonly string ldPath;
private readonly Dictionary<string, LoginData> loginData; private readonly Dictionary<string, LoginData> loginDatas;
private readonly SemaphoreSlim argon2Limit;
private Func<TExtraData, byte[]> DataSerializer = JsonSerialize;
private Func<byte[], TExtraData> DataDeserializer = JsonDeserialize;
public void SetDataSerializers(Func<TExtraData, byte[]> serializer, Func<byte[], TExtraData> deserializer) {
DataSerializer = serializer ?? JsonSerialize;
DataDeserializer = deserializer ?? JsonDeserialize;
}
private Func<T, byte[]> DataSerializer = JsonSerialize;
private Func<byte[], T> DataDeserializer = JsonDeserialize;
public LoginProvider(string ldPath, string confPath) { public LoginProvider(string ldPath, string confPath) {
this.ldPath = ldPath; this.ldPath = ldPath;
loginData = LoadLoginData(ldPath); loginDatas = LoadLoginDatas(ldPath);
config = LoadArgon2Config(confPath); config = LoadLoginProviderConfig(confPath);
argon2Limit = new SemaphoreSlim(config.A2_MAX_CONCURRENT);
} }
private static Dictionary<string, LoginData> LoadLoginData(string path) { private static Dictionary<string, LoginData> LoadLoginDatas(string path) {
Dictionary<string, SerialLoginData> tempData; Dictionary<string, SerialLoginData> tempData;
if (!File.Exists(path)) { if (!File.Exists(path)) {
File.WriteAllText(path, "{}", Encoding.UTF8); File.WriteAllText(path, "{}", Encoding.UTF8);
@ -79,13 +87,26 @@ public class LoginProvider<T> {
} }
} }
var ld = new Dictionary<string, LoginData>(); var ld = new Dictionary<string, LoginData>();
foreach (var pair in tempData!) { foreach (var pair in tempData) {
ld.Add(pair.Key, pair.Value.toPlainData()); ld.Add(pair.Key, pair.Value.ToPlainData());
} }
return ld; return ld;
} }
private static LoginDataProviderConfig LoadArgon2Config(string path) { private void SaveLoginData() {
var serial = new Dictionary<string, SerialLoginData>();
ldLock.EnterWriteLock();
try {
foreach (var pair in loginDatas) {
serial.Add(pair.Key, pair.Value.ToSerial());
}
} finally {
ldLock.ExitWriteLock();
}
File.WriteAllText(ldPath, JsonConvert.SerializeObject(serial));
}
private static LoginDataProviderConfig LoadLoginProviderConfig(string path) {
if (!File.Exists(path)) { if (!File.Exists(path)) {
var conf = new LoginDataProviderConfig(); var conf = new LoginDataProviderConfig();
File.WriteAllText(path, JsonConvert.SerializeObject(conf)); File.WriteAllText(path, JsonConvert.SerializeObject(conf));
@ -94,39 +115,22 @@ public class LoginProvider<T> {
return JsonConvert.DeserializeObject<LoginDataProviderConfig>(File.ReadAllText(path)); return JsonConvert.DeserializeObject<LoginDataProviderConfig>(File.ReadAllText(path));
} }
public void SetDataSerialization(Func<T, byte[]> serializer, Func<byte[], T> deserializer) { public bool AddUser(string username, string password, TExtraData additional) {
DataSerializer = serializer ?? JsonSerialize;
DataDeserializer = deserializer ?? JsonDeserialize;
}
private void StoreLoginData() {
var serial = new Dictionary<string, SerialLoginData>();
ldLock.EnterWriteLock(); ldLock.EnterWriteLock();
try { try {
foreach (var pair in loginData!) { if (loginDatas.ContainsKey(username)) {
serial.Add(pair.Key, pair.Value.toSerial());
}
} finally {
ldLock.ExitWriteLock();
}
File.WriteAllText(ldPath, JsonConvert.SerializeObject(serial));
}
public bool AddUser(string username, string password, T additional) {
ldLock.EnterWriteLock();
try {
if (loginData.ContainsKey(username)) {
return false; return false;
} }
var salt = RandomNumberGenerator.GetBytes(config.SALT_SIZE); var passwordSalt = RandomNumberGenerator.GetBytes(config.SALT_SIZE);
var pwdHash = HashPwd(password, salt); var extraDataSalt = RandomNumberGenerator.GetBytes(config.SALT_SIZE);
LoginData ld = new LoginData() { LoginData ld = new LoginData() {
salt = salt, passwordSalt = passwordSalt,
password = pwdHash, extraDataSalt = extraDataSalt,
encryptedData = EncryptAdditionalData(password, salt, additional) passwordHash = ComputeSaltedSha256Hash(password, passwordSalt),
encryptedExtraData = EncryptExtraData(password, extraDataSalt, additional),
}; };
loginData.Add(username, ld); loginDatas.Add(username, ld);
StoreLoginData(); SaveLoginData();
} finally { } finally {
ldLock.ExitWriteLock(); ldLock.ExitWriteLock();
} }
@ -136,9 +140,9 @@ public class LoginProvider<T> {
public bool RemoveUser(string username) { public bool RemoveUser(string username) {
ldLock.EnterWriteLock(); ldLock.EnterWriteLock();
try { try {
var removed = loginData.Remove(username); var removed = loginDatas.Remove(username);
if (removed) { if (removed) {
StoreLoginData(); SaveLoginData();
} }
return removed; return removed;
} finally { } finally {
@ -146,64 +150,62 @@ public class LoginProvider<T> {
} }
} }
public bool ModifyUser(string username, string newPassword, T newAdditional) { public bool ModifyUser(string username, string newPassword, TExtraData newExtraData) {
ldLock.EnterWriteLock(); ldLock.EnterWriteLock();
try { try {
if (!loginData.ContainsKey(username)) { if (!loginDatas.ContainsKey(username)) {
return false; return false;
} }
loginData.Remove(username, out var data); loginDatas.Remove(username, out var data);
data.password = HashPwd(newPassword, data.salt); data.passwordHash = ComputeSaltedSha256Hash(newPassword, data.passwordSalt);
data.encryptedData = EncryptAdditionalData(newPassword, data.salt, newAdditional); data.encryptedExtraData = EncryptExtraData(newPassword, data.extraDataSalt, newExtraData);
loginData.Add(username, data); loginDatas.Add(username, data);
StoreLoginData(); SaveLoginData();
} finally { } finally {
ldLock.ExitWriteLock(); ldLock.ExitWriteLock();
} }
return true; return true;
} }
public (bool, T) Authenticate(string username, string password) { public bool TryAuthenticate(string username, string password, [MaybeNullWhen(false)] out TExtraData extraData) {
LoginData data; LoginData data;
ldLock.EnterReadLock(); ldLock.EnterReadLock();
try { try {
if (!loginData.TryGetValue(username, out data)) { if (!loginDatas.TryGetValue(username, out data)) {
return (false, default(T)!); extraData = default;
return false;
} }
} finally { } finally {
ldLock.ExitReadLock(); ldLock.ExitReadLock();
} }
var hash = HashPwd(password, data.salt); var hash = ComputeSaltedSha256Hash(password, data.passwordSalt);
if (!hash.SequenceEqual(data.password)) { if (!hash.SequenceEqual(data.passwordHash)) {
return (false, default(T)!); extraData = default;
return false;
} }
return (true, DecryptAdditionalData(password, data.salt, data.encryptedData)); extraData = DecryptExtraData(password, data.extraDataSalt, data.encryptedExtraData);
return true;
} }
private byte[] HashPwd(string pwd, byte[] salt) { /// <summary>
byte[] hash; /// Threadsafe as the SHA256 instance (<see cref="Sha256PerThread"/>) is per thread.
argon2Limit.Wait(); /// </summary>
try { /// <param name="data"></param>
using (var argon2 = new Argon2id(Encoding.UTF8.GetBytes(pwd))) { /// <param name="salt"></param>
argon2.Iterations = config.A2_ITERATIONS; /// <returns></returns>
argon2.MemorySize = config.A2_MEMORY_SIZE; private static byte[] ComputeSaltedSha256Hash(string data, byte[] salt) {
argon2.DegreeOfParallelism = config.A2_PARALLELISM; var dataBytes = Encoding.UTF8.GetBytes(data);
argon2.Salt = salt; var buf = new byte[data.Length + salt.Length];
hash = argon2.GetBytes(config.A2_HASH_LENGTH); Buffer.BlockCopy(dataBytes, 0, buf, 0, dataBytes.Length);
} Buffer.BlockCopy(salt, 0, buf, dataBytes.Length, salt.Length);
// force collection to reduce sustained memory usage if many hashes are done in close time proximity to each other return Sha256PerThread.ComputeHash(buf);
GC.Collect();
} finally {
argon2Limit.Release();
}
return hash;
} }
private byte[] EncryptAdditionalData(string pwd, byte[] salt, T data) { private byte[] EncryptExtraData(string pwd, byte[] salt, TExtraData extraData) {
var pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(pwd), salt, config.PBKDF2_ITERATIONS, HashAlgorithmName.SHA256); var pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(pwd), salt, config.PBKDF2_ITERATIONS, HashAlgorithmName.SHA256);
var key = pbkdf2.GetBytes(config.KEY_LENGTH / 8); var key = pbkdf2.GetBytes(config.KEY_LENGTH / 8);
var plainBytes = DataSerializer(data); var plainBytes = DataSerializer(extraData);
using var aes = Aes.Create(); using var aes = Aes.Create();
aes.KeySize = config.KEY_LENGTH; aes.KeySize = config.KEY_LENGTH;
aes.Key = key; aes.Key = key;
@ -219,7 +221,7 @@ public class LoginProvider<T> {
return encryptedBytes; return encryptedBytes;
} }
private T DecryptAdditionalData(string pwd, byte[] salt, byte[] encryptedData) { private TExtraData DecryptExtraData(string pwd, byte[] salt, byte[] encryptedData) {
var pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(pwd), salt, config.PBKDF2_ITERATIONS, HashAlgorithmName.SHA256); var pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(pwd), salt, config.PBKDF2_ITERATIONS, HashAlgorithmName.SHA256);
var key = pbkdf2.GetBytes(config.KEY_LENGTH / 8); var key = pbkdf2.GetBytes(config.KEY_LENGTH / 8);