cleanup some old auth stuff

This commit is contained in:
GHXX 2024-07-25 03:41:53 +02:00
parent 176c5e7197
commit a4ae359df0
5 changed files with 275 additions and 287 deletions

View File

@ -1,7 +0,0 @@
using System.Net;
namespace SimpleHttpServer;
public interface IAuthorizer {
public abstract (bool auth, object? data) IsAuthenticated(HttpListenerContext contect);
}

View File

@ -1,7 +0,0 @@
using System.Net;
namespace SimpleHttpServer.Internal;
public sealed class DefaultAuthorizer : IAuthorizer {
public (bool auth, object? data) IsAuthenticated(HttpListenerContext contect) => (true, null);
}

View File

@ -1,73 +1,73 @@
using Newtonsoft.Json; //using Newtonsoft.Json;
using System.Collections; //using System.Collections;
using System.Net; //using System.Net;
using System.Reflection; //using System.Reflection;
namespace SimpleHttpServer.Internal; //namespace SimpleHttpServer.Internal;
internal class HttpEndpointHandler { //internal class HttpEndpointHandler {
private static readonly DefaultAuthorizer defaultAuth = new(); // private static readonly DefaultAuthorizer defaultAuth = new();
private readonly IAuthorizer auth; // private readonly IAuthorizer auth;
private readonly MethodInfo handler; // private readonly MethodInfo handler;
private readonly Dictionary<string, (int pindex, Type type, int pparamIdx)> @params; // private readonly Dictionary<string, (int pindex, Type type, int pparamIdx)> @params;
private readonly Func<Exception, HttpResponseBuilder> errorPageBuilder; // private readonly Func<Exception, HttpResponseBuilder> errorPageBuilder;
public HttpEndpointHandler() { // public HttpEndpointHandler() {
auth = defaultAuth; // auth = defaultAuth;
} // }
public HttpEndpointHandler(IAuthorizer auth) { // public HttpEndpointHandler(IAuthorizer auth) {
} // }
public virtual void Handle(HttpListenerContext ctx) { // public virtual void Handle(HttpListenerContext ctx) {
try { // try {
var (isAuth, authData) = auth.IsAuthenticated(ctx); // var (isAuth, authData) = auth.IsAuthenticated(ctx);
if (!isAuth) { // if (!isAuth) {
throw new HttpHandlingException(401, "Authorization required!"); // throw new HttpHandlingException(401, "Authorization required!");
} // }
// collect parameters // // collect parameters
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) {
if (@params.ContainsKey(qelem!)) { // if (@params.ContainsKey(qelem!)) {
var (pindex, type, isPParam) = @params[qelem!]; // var (pindex, type, isPParam) = @params[qelem!];
if (type == typeof(string)) { // if (type == typeof(string)) {
invokeParams[pindex] = ctx.Request.QueryString[qelem!]; // invokeParams[pindex] = ctx.Request.QueryString[qelem!];
set.Set(pindex - 1, true); // set.Set(pindex - 1, true);
} else { // } else {
var elem = JsonConvert.DeserializeObject(ctx.Request.QueryString[qelem!]!, type); // var elem = JsonConvert.DeserializeObject(ctx.Request.QueryString[qelem!]!, type);
if (elem != null) { // if (elem != null) {
invokeParams[pindex] = elem; // invokeParams[pindex] = elem;
set.Set(pindex - 1, true); // set.Set(pindex - 1, true);
} // }
} // }
} // }
} // }
// fill with defaults // // fill with defaults
foreach (var p in @params) { // foreach (var p in @params) {
if (!set.Get(p.Value.pindex)) { // if (!set.Get(p.Value.pindex)) {
invokeParams[p.Value.pindex] = p.Value.type.IsValueType ? Activator.CreateInstance(p.Value.type) : null; // invokeParams[p.Value.pindex] = p.Value.type.IsValueType ? Activator.CreateInstance(p.Value.type) : null;
} // }
} // }
var builder = handler.Invoke(null, invokeParams) as HttpResponseBuilder; // var builder = handler.Invoke(null, invokeParams) as HttpResponseBuilder;
builder!.SendResponse(ctx.Response); // builder!.SendResponse(ctx.Response);
} catch (Exception e) { // } catch (Exception e) {
if (e is TargetInvocationException tex) { // if (e is TargetInvocationException tex) {
e = tex.InnerException!; // e = tex.InnerException!;
} // }
errorPageBuilder(e).SendResponse(ctx.Response); // errorPageBuilder(e).SendResponse(ctx.Response);
} // }
} // }
} //}

View File

@ -1,245 +1,245 @@
using Newtonsoft.Json; //using Newtonsoft.Json;
using System.Diagnostics.CodeAnalysis; //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 passwordSalt; // public string passwordSalt;
public string extraDataSalt; // public string extraDataSalt;
public string pwd; // public string pwd;
public string extraData; // public string extraData;
public LoginData ToPlainData() { // public LoginData ToPlainData() {
return new LoginData { // return new LoginData {
passwordSalt = Convert.FromBase64String(passwordSalt), // passwordSalt = Convert.FromBase64String(passwordSalt),
extraDataSalt = Convert.FromBase64String(extraDataSalt) // extraDataSalt = Convert.FromBase64String(extraDataSalt)
}; // };
} // }
} //}
internal struct LoginData { //internal struct LoginData {
public byte[] passwordSalt; // public byte[] passwordSalt;
public byte[] extraDataSalt; // public byte[] extraDataSalt;
public byte[] passwordHash; // public byte[] passwordHash;
public byte[] encryptedExtraData; // public byte[] encryptedExtraData;
public SerialLoginData ToSerial() { // public SerialLoginData ToSerial() {
return new SerialLoginData { // return new SerialLoginData {
passwordSalt = Convert.ToBase64String(passwordSalt), // passwordSalt = Convert.ToBase64String(passwordSalt),
extraDataSalt = Convert.ToBase64String(extraDataSalt), // extraDataSalt = Convert.ToBase64String(extraDataSalt),
pwd = Convert.ToBase64String(passwordHash), // pwd = Convert.ToBase64String(passwordHash),
extraData = Convert.ToBase64String(encryptedExtraData) // extraData = Convert.ToBase64String(encryptedExtraData)
}; // };
} // }
} //}
internal struct LoginDataProviderConfig { //internal struct LoginDataProviderConfig {
/// <summary> // /// <summary>
/// Size of the password salt and the extradata salt. So each salt will be of size <see cref="SALT_SIZE"/>. // /// Size of the password salt and the extradata salt. So each salt will be of size <see cref="SALT_SIZE"/>.
/// </summary> // /// </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 PBKDF2_ITERATIONS = 600_000; // public int PBKDF2_ITERATIONS = 600_000;
public LoginDataProviderConfig() { } // public LoginDataProviderConfig() { }
} //}
public class LoginProvider<TExtraData> { //public class LoginProvider<TExtraData> {
private static readonly Func<TExtraData, 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[], TExtraData> JsonDeserialize = b => JsonConvert.DeserializeObject<TExtraData>(Encoding.UTF8.GetString(b))!; // private static readonly Func<byte[], TExtraData> JsonDeserialize = b => JsonConvert.DeserializeObject<TExtraData>(Encoding.UTF8.GetString(b))!;
[ThreadStatic] // [ThreadStatic]
private static SHA256? _sha256PerThread; // private static SHA256? _sha256PerThread;
private static SHA256 Sha256PerThread { get => _sha256PerThread ??= SHA256.Create(); } // 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> loginDatas; // private readonly Dictionary<string, LoginData> loginDatas;
private Func<TExtraData, byte[]> DataSerializer = JsonSerialize; // private Func<TExtraData, byte[]> DataSerializer = JsonSerialize;
private Func<byte[], TExtraData> DataDeserializer = JsonDeserialize; // private Func<byte[], TExtraData> DataDeserializer = JsonDeserialize;
public void SetDataSerializers(Func<TExtraData, byte[]> serializer, Func<byte[], TExtraData> deserializer) { // public void SetDataSerializers(Func<TExtraData, byte[]> serializer, Func<byte[], TExtraData> deserializer) {
DataSerializer = serializer ?? JsonSerialize; // DataSerializer = serializer ?? JsonSerialize;
DataDeserializer = deserializer ?? JsonDeserialize; // DataDeserializer = deserializer ?? JsonDeserialize;
} // }
public LoginProvider(string ldPath, string confPath) { // public LoginProvider(string ldPath, string confPath) {
this.ldPath = ldPath; // this.ldPath = ldPath;
loginDatas = LoadLoginDatas(ldPath); // loginDatas = LoadLoginDatas(ldPath);
config = LoadLoginProviderConfig(confPath); // config = LoadLoginProviderConfig(confPath);
} // }
private static Dictionary<string, LoginData> LoadLoginDatas(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);
tempData = new(); // tempData = new();
} else { // } else {
tempData = JsonConvert.DeserializeObject<Dictionary<string, SerialLoginData>>(File.ReadAllText(path))!; // tempData = JsonConvert.DeserializeObject<Dictionary<string, SerialLoginData>>(File.ReadAllText(path))!;
if (tempData == null) { // if (tempData == null) {
throw new InvalidDataException($"could not read login data from file {path}"); // throw new InvalidDataException($"could not read login data from file {path}");
} // }
} // }
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 void SaveLoginData() { // private void SaveLoginData() {
var serial = new Dictionary<string, SerialLoginData>(); // var serial = new Dictionary<string, SerialLoginData>();
ldLock.EnterWriteLock(); // ldLock.EnterWriteLock();
try { // try {
foreach (var pair in loginDatas) { // foreach (var pair in loginDatas) {
serial.Add(pair.Key, pair.Value.ToSerial()); // serial.Add(pair.Key, pair.Value.ToSerial());
} // }
} finally { // } finally {
ldLock.ExitWriteLock(); // ldLock.ExitWriteLock();
} // }
File.WriteAllText(ldPath, JsonConvert.SerializeObject(serial)); // File.WriteAllText(ldPath, JsonConvert.SerializeObject(serial));
} // }
private static LoginDataProviderConfig LoadLoginProviderConfig(string path) { // 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));
return conf; // return conf;
} // }
return JsonConvert.DeserializeObject<LoginDataProviderConfig>(File.ReadAllText(path)); // return JsonConvert.DeserializeObject<LoginDataProviderConfig>(File.ReadAllText(path));
} // }
public bool AddUser(string username, string password, TExtraData additional) { // public bool AddUser(string username, string password, TExtraData additional) {
ldLock.EnterWriteLock(); // ldLock.EnterWriteLock();
try { // try {
if (loginDatas.ContainsKey(username)) { // if (loginDatas.ContainsKey(username)) {
return false; // return false;
} // }
var passwordSalt = RandomNumberGenerator.GetBytes(config.SALT_SIZE); // var passwordSalt = RandomNumberGenerator.GetBytes(config.SALT_SIZE);
var extraDataSalt = RandomNumberGenerator.GetBytes(config.SALT_SIZE); // var extraDataSalt = RandomNumberGenerator.GetBytes(config.SALT_SIZE);
LoginData ld = new LoginData() { // LoginData ld = new LoginData() {
passwordSalt = passwordSalt, // passwordSalt = passwordSalt,
extraDataSalt = extraDataSalt, // extraDataSalt = extraDataSalt,
passwordHash = ComputeSaltedSha256Hash(password, passwordSalt), // passwordHash = ComputeSaltedSha256Hash(password, passwordSalt),
encryptedExtraData = EncryptExtraData(password, extraDataSalt, additional), // encryptedExtraData = EncryptExtraData(password, extraDataSalt, additional),
}; // };
loginDatas.Add(username, ld); // loginDatas.Add(username, ld);
SaveLoginData(); // SaveLoginData();
} finally { // } finally {
ldLock.ExitWriteLock(); // ldLock.ExitWriteLock();
} // }
return true; // return true;
} // }
public bool RemoveUser(string username) { // public bool RemoveUser(string username) {
ldLock.EnterWriteLock(); // ldLock.EnterWriteLock();
try { // try {
var removed = loginDatas.Remove(username); // var removed = loginDatas.Remove(username);
if (removed) { // if (removed) {
SaveLoginData(); // SaveLoginData();
} // }
return removed; // return removed;
} finally { // } finally {
ldLock.ExitWriteLock(); // ldLock.ExitWriteLock();
} // }
} // }
public bool ModifyUser(string username, string newPassword, TExtraData newExtraData) { // public bool ModifyUser(string username, string newPassword, TExtraData newExtraData) {
ldLock.EnterWriteLock(); // ldLock.EnterWriteLock();
try { // try {
if (!loginDatas.ContainsKey(username)) { // if (!loginDatas.ContainsKey(username)) {
return false; // return false;
} // }
loginDatas.Remove(username, out var data); // loginDatas.Remove(username, out var data);
data.passwordHash = ComputeSaltedSha256Hash(newPassword, data.passwordSalt); // data.passwordHash = ComputeSaltedSha256Hash(newPassword, data.passwordSalt);
data.encryptedExtraData = EncryptExtraData(newPassword, data.extraDataSalt, newExtraData); // data.encryptedExtraData = EncryptExtraData(newPassword, data.extraDataSalt, newExtraData);
loginDatas.Add(username, data); // loginDatas.Add(username, data);
SaveLoginData(); // SaveLoginData();
} finally { // } finally {
ldLock.ExitWriteLock(); // ldLock.ExitWriteLock();
} // }
return true; // return true;
} // }
public bool TryAuthenticate(string username, string password, [MaybeNullWhen(false)] out TExtraData extraData) { // public bool TryAuthenticate(string username, string password, [MaybeNullWhen(false)] out TExtraData extraData) {
LoginData data; // LoginData data;
ldLock.EnterReadLock(); // ldLock.EnterReadLock();
try { // try {
if (!loginDatas.TryGetValue(username, out data)) { // if (!loginDatas.TryGetValue(username, out data)) {
extraData = default; // extraData = default;
return false; // return false;
} // }
} finally { // } finally {
ldLock.ExitReadLock(); // ldLock.ExitReadLock();
} // }
var hash = ComputeSaltedSha256Hash(password, data.passwordSalt); // var hash = ComputeSaltedSha256Hash(password, data.passwordSalt);
if (!hash.SequenceEqual(data.passwordHash)) { // if (!hash.SequenceEqual(data.passwordHash)) {
extraData = default; // extraData = default;
return false; // return false;
} // }
extraData = DecryptExtraData(password, data.extraDataSalt, data.encryptedExtraData); // extraData = DecryptExtraData(password, data.extraDataSalt, data.encryptedExtraData);
return true; // return true;
} // }
/// <summary> // /// <summary>
/// Threadsafe as the SHA256 instance (<see cref="Sha256PerThread"/>) is per thread. // /// Threadsafe as the SHA256 instance (<see cref="Sha256PerThread"/>) is per thread.
/// </summary> // /// </summary>
/// <param name="data"></param> // /// <param name="data"></param>
/// <param name="salt"></param> // /// <param name="salt"></param>
/// <returns></returns> // /// <returns></returns>
private static byte[] ComputeSaltedSha256Hash(string data, byte[] salt) { // private static byte[] ComputeSaltedSha256Hash(string data, byte[] salt) {
var dataBytes = Encoding.UTF8.GetBytes(data); // var dataBytes = Encoding.UTF8.GetBytes(data);
var buf = new byte[data.Length + salt.Length]; // var buf = new byte[data.Length + salt.Length];
Buffer.BlockCopy(dataBytes, 0, buf, 0, dataBytes.Length); // Buffer.BlockCopy(dataBytes, 0, buf, 0, dataBytes.Length);
Buffer.BlockCopy(salt, 0, buf, dataBytes.Length, salt.Length); // Buffer.BlockCopy(salt, 0, buf, dataBytes.Length, salt.Length);
return Sha256PerThread.ComputeHash(buf); // return Sha256PerThread.ComputeHash(buf);
} // }
private byte[] EncryptExtraData(string pwd, byte[] salt, TExtraData extraData) { // 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(extraData); // 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;
aes.Mode = CipherMode.CBC; // aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7; // aes.Padding = PaddingMode.PKCS7;
ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV); // ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
byte[] cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length); // byte[] cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
var encryptedBytes = new byte[aes.IV.Length + cipherBytes.Length]; // var encryptedBytes = new byte[aes.IV.Length + cipherBytes.Length];
Array.Copy(aes.IV, 0, encryptedBytes, 0, aes.IV.Length); // Array.Copy(aes.IV, 0, encryptedBytes, 0, aes.IV.Length);
Array.Copy(cipherBytes, 0, encryptedBytes, aes.IV.Length, cipherBytes.Length); // Array.Copy(cipherBytes, 0, encryptedBytes, aes.IV.Length, cipherBytes.Length);
return encryptedBytes; // return encryptedBytes;
} // }
private TExtraData DecryptExtraData(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);
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;
aes.Mode = CipherMode.CBC; // aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7; // aes.Padding = PaddingMode.PKCS7;
var iv = new byte[aes.BlockSize / 8]; // var iv = new byte[aes.BlockSize / 8];
var cipherBytes = new byte[encryptedData.Length - iv.Length]; // var cipherBytes = new byte[encryptedData.Length - iv.Length];
Array.Copy(encryptedData, 0, iv, 0, iv.Length); // Array.Copy(encryptedData, 0, iv, 0, iv.Length);
Array.Copy(encryptedData, iv.Length, cipherBytes, 0, cipherBytes.Length); // Array.Copy(encryptedData, iv.Length, cipherBytes, 0, cipherBytes.Length);
aes.IV = iv; // aes.IV = iv;
ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV); // ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
byte[] plainBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length); // byte[] plainBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
return DataDeserializer(plainBytes); // return DataDeserializer(plainBytes);
} // }
} //}

View File

@ -19,9 +19,11 @@ public class RequestContext : IDisposable {
/// </summary> /// </summary>
public TextWriter RespWriter => respWriter ??= TextWriter.Synchronized(new StreamWriter(ListenerContext.Response.OutputStream) { NewLine = "\n" }); public TextWriter RespWriter => respWriter ??= TextWriter.Synchronized(new StreamWriter(ListenerContext.Response.OutputStream) { NewLine = "\n" });
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
public RequestContext(HttpListenerContext listenerContext) { public RequestContext(HttpListenerContext listenerContext) {
ListenerContext = listenerContext; ListenerContext = listenerContext;
} }
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
public async Task WriteLineToRespAsync(string resp) => await RespWriter.WriteLineAsync(resp); public async Task WriteLineToRespAsync(string resp) => await RespWriter.WriteLineAsync(resp);
public async Task WriteToRespAsync(string resp) => await RespWriter.WriteAsync(resp); public async Task WriteToRespAsync(string resp) => await RespWriter.WriteAsync(resp);