Replace argon2, add threadsafe SHA256 method, rename som variables
This commit is contained in:
parent
ea74cb899c
commit
e8131efc86
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user