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