extended LoginProvider with additional data
This commit is contained in:
parent
05b722e513
commit
8d95693c02
|
|
@ -1,4 +1,6 @@
|
||||||
namespace SimpleHttpServer;
|
using SimpleHttpServer.Internal;
|
||||||
|
|
||||||
|
namespace SimpleHttpServer;
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||||
public class HttpEndpoint<T> : Attribute where T : IAuthorizer
|
public class HttpEndpoint<T> : Attribute where T : IAuthorizer
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using SimpleHttpServer.Internal;
|
||||||
|
|
||||||
namespace SimpleHttpServer;
|
namespace SimpleHttpServer;
|
||||||
|
|
||||||
|
|
@ -34,6 +35,9 @@ public sealed class HttpServer {
|
||||||
PrintErrorOrThrow(error, endpoint, throwOnInvalidEndpoint, "HttpEndpointAttribute is only valid on static methods!");
|
PrintErrorOrThrow(error, endpoint, throwOnInvalidEndpoint, "HttpEndpointAttribute is only valid on static methods!");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (!endpoint.IsPublic) {
|
||||||
|
PrintErrorOrThrow(error, endpoint, throwOnInvalidEndpoint, $"{GetFancyMethodName(endpoint)} needs to be public!");
|
||||||
|
}
|
||||||
var myParams = endpoint.GetParameters();
|
var myParams = endpoint.GetParameters();
|
||||||
if (myParams.Length <= 0 || !myParams[0].GetType().IsAssignableFrom(typeof(HttpListenerContext))) {
|
if (myParams.Length <= 0 || !myParams[0].GetType().IsAssignableFrom(typeof(HttpListenerContext))) {
|
||||||
PrintErrorOrThrow(error, endpoint, throwOnInvalidEndpoint, $"{GetFancyMethodName(endpoint)} needs to have a HttpListenerContext as its first argument!");
|
PrintErrorOrThrow(error, endpoint, throwOnInvalidEndpoint, $"{GetFancyMethodName(endpoint)} needs to have a HttpListenerContext as its first argument!");
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
namespace SimpleHttpServer;
|
namespace SimpleHttpServer.Internal;
|
||||||
|
|
||||||
public sealed class DefaultAuthorizer : IAuthorizer
|
public sealed class DefaultAuthorizer : IAuthorizer {
|
||||||
{
|
|
||||||
public (bool auth, object? data) IsAuthenticated(HttpListenerContext contect) => (true, null);
|
public (bool auth, object? data) IsAuthenticated(HttpListenerContext contect) => (true, null);
|
||||||
}
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace SimpleHttpServer;
|
namespace SimpleHttpServer.Internal;
|
||||||
|
|
||||||
internal class HttpEndpointHandler {
|
internal class HttpEndpointHandler {
|
||||||
private static readonly DefaultAuthorizer defaultAuth = new();
|
private static readonly DefaultAuthorizer defaultAuth = new();
|
||||||
|
|
@ -37,7 +37,9 @@ internal class HttpEndpointHandler {
|
||||||
var invokeParams = new object?[_params.Count + 1];
|
var invokeParams = new object?[_params.Count + 1];
|
||||||
var set = new BitArray(_params.Count);
|
var set = new BitArray(_params.Count);
|
||||||
invokeParams[0] = ctx;
|
invokeParams[0] = ctx;
|
||||||
|
|
||||||
// read pparams
|
// read pparams
|
||||||
|
|
||||||
// read qparams
|
// read qparams
|
||||||
var qst = ctx.Request.QueryString;
|
var qst = ctx.Request.QueryString;
|
||||||
foreach (var qelem in ctx.Request.QueryString.AllKeys) {
|
foreach (var qelem in ctx.Request.QueryString.AllKeys) {
|
||||||
251
SimpleHttpServer/Login/LoginProvider.cs
Normal file
251
SimpleHttpServer/Login/LoginProvider.cs
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Konscious.Security.Cryptography;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace SimpleHttpServer.Login;
|
||||||
|
|
||||||
|
internal struct SerialLoginData {
|
||||||
|
public string uname;
|
||||||
|
public string salt;
|
||||||
|
public string pwd;
|
||||||
|
public string additionalData;
|
||||||
|
|
||||||
|
public LoginData toPlainData() {
|
||||||
|
return new LoginData {
|
||||||
|
username = uname,
|
||||||
|
salt = Convert.FromBase64String(salt),
|
||||||
|
password = Convert.FromBase64String(pwd)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal struct LoginData {
|
||||||
|
public string username;
|
||||||
|
public byte[] salt;
|
||||||
|
public byte[] password;
|
||||||
|
public byte[] encryptedData;
|
||||||
|
|
||||||
|
public SerialLoginData toSerial() {
|
||||||
|
return new SerialLoginData {
|
||||||
|
uname = username,
|
||||||
|
salt = Convert.ToBase64String(salt),
|
||||||
|
pwd = Convert.ToBase64String(password),
|
||||||
|
additionalData = Convert.ToBase64String(encryptedData)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal struct LoginDataProviderConfig {
|
||||||
|
|
||||||
|
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<T> {
|
||||||
|
|
||||||
|
private static readonly Func<T, 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 readonly LoginDataProviderConfig config;
|
||||||
|
private readonly ReaderWriterLockSlim ldLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
|
||||||
|
private readonly string ldPath;
|
||||||
|
private readonly Dictionary<string, LoginData> loginData;
|
||||||
|
private readonly SemaphoreSlim argon2Limit;
|
||||||
|
|
||||||
|
private Func<T, byte[]> DataSerializer = JsonSerialize;
|
||||||
|
private Func<byte[], T> DataDeserializer = JsonDeserialize;
|
||||||
|
|
||||||
|
public LoginProvider(string ldPath, string confPath) {
|
||||||
|
this.ldPath = ldPath;
|
||||||
|
loginData = LoadLoginData(ldPath);
|
||||||
|
config = LoadArgon2Config(confPath);
|
||||||
|
argon2Limit = new SemaphoreSlim(config.A2_MAX_CONCURRENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, LoginData> LoadLoginData(string path) {
|
||||||
|
Dictionary<string, SerialLoginData> tempData;
|
||||||
|
if (!File.Exists(path)) {
|
||||||
|
File.WriteAllText(path, "{}", Encoding.UTF8);
|
||||||
|
tempData = new();
|
||||||
|
} else {
|
||||||
|
tempData = JsonConvert.DeserializeObject<Dictionary<string, SerialLoginData>>(File.ReadAllText(path))!;
|
||||||
|
if (tempData == null) {
|
||||||
|
throw new InvalidDataException($"could not read login data from file {path}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var ld = new Dictionary<string, LoginData>();
|
||||||
|
foreach (var pair in tempData!) {
|
||||||
|
ld.Add(pair.Key, pair.Value.toPlainData());
|
||||||
|
}
|
||||||
|
return ld;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LoginDataProviderConfig LoadArgon2Config(string path) {
|
||||||
|
if (!File.Exists(path)) {
|
||||||
|
var conf = new LoginDataProviderConfig();
|
||||||
|
File.WriteAllText(path, JsonConvert.SerializeObject(conf));
|
||||||
|
return conf;
|
||||||
|
}
|
||||||
|
return JsonConvert.DeserializeObject<LoginDataProviderConfig>(File.ReadAllText(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetDataSerialization(Func<T, byte[]> serializer, Func<byte[], T> deserializer) {
|
||||||
|
DataSerializer = serializer ?? JsonSerialize;
|
||||||
|
DataDeserializer = deserializer ?? JsonDeserialize;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StoreLoginData() {
|
||||||
|
var serial = new Dictionary<string, SerialLoginData>();
|
||||||
|
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)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var salt = RandomNumberGenerator.GetBytes(config.SALT_SIZE);
|
||||||
|
var pwdHash = HashPwd(password, salt);
|
||||||
|
LoginData ld = new LoginData() {
|
||||||
|
username = username,
|
||||||
|
salt = salt,
|
||||||
|
password = pwdHash,
|
||||||
|
encryptedData = EncryptAdditionalData(password, salt, additional)
|
||||||
|
};
|
||||||
|
loginData.Add(username, ld);
|
||||||
|
StoreLoginData();
|
||||||
|
} finally {
|
||||||
|
ldLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RemoveUser(string username) {
|
||||||
|
ldLock.EnterWriteLock();
|
||||||
|
try {
|
||||||
|
var removed = loginData.Remove(username);
|
||||||
|
if (removed) {
|
||||||
|
StoreLoginData();
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
} finally {
|
||||||
|
ldLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ModifyUser(string username, string newPassword, T newAdditional) {
|
||||||
|
ldLock.EnterWriteLock();
|
||||||
|
try {
|
||||||
|
if (!loginData.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();
|
||||||
|
} finally {
|
||||||
|
ldLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public (bool, T) Authenticate(string username, string password) {
|
||||||
|
LoginData data;
|
||||||
|
ldLock.EnterReadLock();
|
||||||
|
try {
|
||||||
|
if (!loginData.TryGetValue(username, out data)) {
|
||||||
|
return (false, default(T)!);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
ldLock.ExitReadLock();
|
||||||
|
}
|
||||||
|
var hash = HashPwd(password, data.salt);
|
||||||
|
if (!hash.SequenceEqual(data.password)) {
|
||||||
|
return (false, default(T)!);
|
||||||
|
}
|
||||||
|
return (true, DecryptAdditionalData(password, data.salt, data.encryptedData));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] EncryptAdditionalData(string pwd, byte[] salt, T data) {
|
||||||
|
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);
|
||||||
|
using var aes = Aes.Create();
|
||||||
|
aes.KeySize = config.KEY_LENGTH;
|
||||||
|
aes.Key = key;
|
||||||
|
aes.Mode = CipherMode.CBC;
|
||||||
|
aes.Padding = PaddingMode.PKCS7;
|
||||||
|
ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
|
||||||
|
byte[] cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
|
||||||
|
|
||||||
|
var encryptedBytes = new byte[aes.IV.Length + cipherBytes.Length];
|
||||||
|
Array.Copy(aes.IV, 0, encryptedBytes, 0, aes.IV.Length);
|
||||||
|
Array.Copy(cipherBytes, 0, encryptedBytes, aes.IV.Length, cipherBytes.Length);
|
||||||
|
|
||||||
|
return encryptedBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private T DecryptAdditionalData(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);
|
||||||
|
|
||||||
|
using var aes = Aes.Create();
|
||||||
|
aes.KeySize = config.KEY_LENGTH;
|
||||||
|
aes.Key = key;
|
||||||
|
aes.Mode = CipherMode.CBC;
|
||||||
|
aes.Padding = PaddingMode.PKCS7;
|
||||||
|
var iv = new byte[aes.BlockSize / 8];
|
||||||
|
var cipherBytes = new byte[encryptedData.Length - iv.Length];
|
||||||
|
|
||||||
|
Array.Copy(encryptedData, 0, iv, 0, iv.Length);
|
||||||
|
Array.Copy(encryptedData, iv.Length, cipherBytes, 0, cipherBytes.Length);
|
||||||
|
|
||||||
|
aes.IV = iv;
|
||||||
|
ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
|
||||||
|
byte[] plainBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
|
||||||
|
|
||||||
|
return DataDeserializer(plainBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
|
@ -15,4 +15,8 @@
|
||||||
<PackageReference Include="coverlet.collector" Version="3.1.2" />
|
<PackageReference Include="coverlet.collector" Version="3.1.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SimpleHttpServer\SimpleHttpServer.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
15
SimpleHttpServerTest/SimpleServerTest.cs
Normal file
15
SimpleHttpServerTest/SimpleServerTest.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
using SimpleHttpServer;
|
||||||
|
using SimpleHttpServerTest.SimpleTestServer;
|
||||||
|
|
||||||
|
namespace SimpleHttpServerTest;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class SimpleServerTest {
|
||||||
|
[TestMethod]
|
||||||
|
public void RunTestServer() {
|
||||||
|
var server = HttpServer.Create(8833, "localhost", typeof(SimpleEndpointDefinition));
|
||||||
|
server.Start();
|
||||||
|
Console.WriteLine("press any key to exit");
|
||||||
|
Assert.IsTrue(server.Shutdown(10000), "server did not exit gracefully");
|
||||||
|
}
|
||||||
|
}
|
||||||
17
SimpleHttpServerTest/SimpleTestServer/LdAuthorizer.cs
Normal file
17
SimpleHttpServerTest/SimpleTestServer/LdAuthorizer.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using SimpleHttpServer.Login;
|
||||||
|
|
||||||
|
namespace SimpleHttpServerTest.SimpleTestServer;
|
||||||
|
internal class LdAuthorizer {
|
||||||
|
private readonly LoginProvider lprov;
|
||||||
|
|
||||||
|
internal LdAuthorizer(LoginProvider lprov) {
|
||||||
|
this.lprov = lprov;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace SimpleHttpServerTest.SimpleTestServer;
|
||||||
|
internal class SimpleEndpointDefinition {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
namespace SimpleHttpServerTest;
|
|
||||||
|
|
||||||
[TestClass]
|
|
||||||
public class UnitTest1 {
|
|
||||||
[TestMethod]
|
|
||||||
public void TestMethod1() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user