From 0a6413d10652861d720690086c855f11afa4c787 Mon Sep 17 00:00:00 2001 From: LD-Reborn Date: Sat, 1 Nov 2025 23:11:26 +0100 Subject: [PATCH 1/2] Added user photo caching --- src/Program.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Program.cs b/src/Program.cs index 240cd7c..f952fd8 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -102,11 +102,20 @@ app.UseStaticFiles(new StaticFileOptions Public = true, MaxAge = TimeSpan.FromDays(365) }; + } else if (requestPath.Contains("UserPhoto")) + { + ctx.Context.Response.GetTypedHeaders().CacheControl = + new Microsoft.Net.Http.Headers.CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromMinutes(5) + }; } } }); app.UseRouting(); app.UseAuthorization(); +app.UseResponseCaching(); app.UseResponseCompression(); string[] supportedCultures = ["de", "en"]; From 5b5fcd6322a132661e6f6fdbf051adecce40181c Mon Sep 17 00:00:00 2001 From: LD-Reborn Date: Sun, 2 Nov 2025 13:39:28 +0100 Subject: [PATCH 2/2] Added settings model, added settings migration, added LDAP salted hashes --- src/Controllers/UsersController.cs | 13 +++-- src/Models/AdminSettingsModel.cs | 19 +++++++ src/Models/LdapConfigModel.cs | 1 + src/Services/LdapService.cs | 82 +++++++++++++++++++++++++----- src/Services/MigrationService.cs | 24 +++++++++ 5 files changed, 123 insertions(+), 16 deletions(-) create mode 100644 src/Models/AdminSettingsModel.cs diff --git a/src/Controllers/UsersController.cs b/src/Controllers/UsersController.cs index bdc7782..1228569 100644 --- a/src/Controllers/UsersController.cs +++ b/src/Controllers/UsersController.cs @@ -69,6 +69,7 @@ public class UsersController : Controller } try { + Task settingsTask = _ldap.GetAdminSettingsModelAsync(); string? jpegPhoto = requestModel.JpegPhoto; string? title = requestModel.Title; string userPassword = requestModel.UserPassword ?? ""; @@ -79,9 +80,10 @@ public class UsersController : Controller description ??= new() {Address = new(), BirthDate = "", Workplace = "", Groups = []}; if (!userPassword.StartsWith('{')) { + AdminSettingsModel settings = await settingsTask; byte[] passwordBytes = Encoding.UTF8.GetBytes(userPassword); - byte[] hashedPassword = SHA256.HashData(passwordBytes); - userPassword = "{SHA256}" + Convert.ToBase64String(hashedPassword); + byte[] hashedPassword = settings.hashAlgorithm?.ComputeHash(passwordBytes) ?? throw new Exception("Hash algorithm not instantiated yet"); + userPassword = $"{{{settings.DefaultHashAlgorithm.ToUpperInvariant()}}}{Convert.ToBase64String(hashedPassword)}"; } LdapAttributeSet attributeSet = @@ -115,6 +117,7 @@ public class UsersController : Controller } try { + Task settingsTask = _ldap.GetAdminSettingsModelAsync(); string uid = requestModel.Uid; UserModel? user = null; if (requestModel.NewUid is not null && requestModel.NewUid.Length > 0) @@ -136,7 +139,11 @@ public class UsersController : Controller } if (requestModel.UserPassword is not null && requestModel.UserPassword.Length > 0) { - await _ldap.UpdateUser(uid, "userPassword", "{SHA256}" + Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(requestModel.UserPassword)))); + AdminSettingsModel settings = await settingsTask; + byte[] passwordBytes = Encoding.UTF8.GetBytes(requestModel.UserPassword); + byte[] hashedPassword = settings.hashAlgorithm?.ComputeHash(passwordBytes) ?? throw new Exception("Hash algorithm not instantiated yet"); + requestModel.UserPassword = $"{{{settings.DefaultHashAlgorithm.ToUpperInvariant()}}}{Convert.ToBase64String(hashedPassword)}"; + await _ldap.UpdateUser(uid, "userPassword", requestModel.UserPassword); } string newUid = uid; diff --git a/src/Models/AdminSettingsModel.cs b/src/Models/AdminSettingsModel.cs new file mode 100644 index 0000000..39d2620 --- /dev/null +++ b/src/Models/AdminSettingsModel.cs @@ -0,0 +1,19 @@ +using System.Security.Cryptography; +using System.Text.Json; + +namespace Berufsschule_HAM.Models; + +public class AdminSettingsModel +{ + public required string DefaultHashAlgorithm { get; set; } + public int MaxDownloadableUserImageSize { get; set; } + public required string BarcodeType { get; set; } + public required string BarcodeText { get; set; } + public required Dictionary Presets { get; set; } + public HashAlgorithm? hashAlgorithm; +} + +public class Preset +{ + public required Dictionary Attribute { get; set; } +} \ No newline at end of file diff --git a/src/Models/LdapConfigModel.cs b/src/Models/LdapConfigModel.cs index 53a0d5f..4efc499 100644 --- a/src/Models/LdapConfigModel.cs +++ b/src/Models/LdapConfigModel.cs @@ -13,5 +13,6 @@ public class LdapConfig public string UsersOu { get; set; } = "ou=users"; public string GroupsOu { get; set; } = "ou=groups"; public string MigrationsOu { get; set; } = "ou=migrations"; + public string SettingsOu { get; set; } = "ou=settings"; public int ConnectionRetryCount { get; set; } = 10; } \ No newline at end of file diff --git a/src/Services/LdapService.cs b/src/Services/LdapService.cs index 1f58ce7..bb74a51 100644 --- a/src/Services/LdapService.cs +++ b/src/Services/LdapService.cs @@ -11,6 +11,7 @@ public partial class LdapService : IDisposable { private readonly LdapConfig _opts; private readonly LdapConnection _conn; + private AdminSettingsModel? adminSettingsModel; private ILogger _logger; public LdapService(IOptions options, ILogger logger) @@ -63,10 +64,12 @@ public partial class LdapService : IDisposable public string UsersBaseDn => string.IsNullOrEmpty(_opts.UsersOu) ? _opts.BaseDn : $"{_opts.UsersOu},{_opts.BaseDn}"; public string GroupsBaseDn => string.IsNullOrEmpty(_opts.GroupsOu) ? _opts.BaseDn : $"{_opts.GroupsOu},{_opts.BaseDn}"; public string MigrationsBaseDn => string.IsNullOrEmpty(_opts.MigrationsOu) ? _opts.BaseDn : $"{_opts.MigrationsOu},{_opts.BaseDn}"; + public string SettingsBaseDn => string.IsNullOrEmpty(_opts.SettingsOu) ? _opts.BaseDn : $"{_opts.SettingsOu},{_opts.BaseDn}"; public string[] UsersAttributes => ["cn", "sn", "title", "uid", "jpegPhoto", "userPassword", "description"]; public string[] AssetsAttributes => ["CN", "description", "l", "owner", "serialNumber", "name"]; public string[] LocationsAttributes => ["l", "description"]; public string[] GroupsAttributes => ["cn", "gidNumber", "description"]; + public Dictionary HashAlgorithms => new() { ["MD5"] = MD5.Create(), ["SHA1"] = SHA1.Create(), ["SHA256"] = SHA256.Create(), ["SHA384"] = SHA384.Create(), ["SHA512"] = SHA512.Create(), ["SSHA"] = SHA1.Create(), ["SSHA256"] = SHA256.Create(), ["SSHA384"] = SHA384.Create(), ["SSHA512"] = SHA512.Create()}; public async Task> ListLocationsAsync() { IEnumerable> locations = await ListObjectBy(LocationsBaseDn, "", LocationsAttributes); @@ -115,6 +118,53 @@ public partial class LdapService : IDisposable return true; } + public async Task GetAdminSettingsModelAsync() + { + if (adminSettingsModel is not null) return adminSettingsModel; + + Dictionary objects; + try + { + objects = (await ListObjectBy(_opts.BaseDn, _opts.SettingsOu, ["description"])).First(); + adminSettingsModel = JsonSerializer.Deserialize(objects["description"]) ?? throw new Exception(); + adminSettingsModel.hashAlgorithm = initHashAlgorithm(adminSettingsModel); + return adminSettingsModel; + } + catch (Exception) + { + _logger.LogError("Unable to retrieve Admin settings"); + throw new Exception("Unable to retrieve Admin settings"); + } + } + + public async Task SetAdminSettingsModelAsync(AdminSettingsModel adminSettingsModel) + { + adminSettingsModel.hashAlgorithm = initHashAlgorithm(adminSettingsModel); + this.adminSettingsModel = adminSettingsModel; + + await ConnectAndBind(); + try + { + string dn = SettingsBaseDn; + string targetText = JsonSerializer.Serialize(adminSettingsModel); + var modification = new LdapModification( + LdapModification.Replace, + new LdapAttribute("description", targetText) + ); + await _conn.ModifyAsync(dn, modification); + } + catch (Exception) + { + return false; + } + return true; + } + + private HashAlgorithm initHashAlgorithm(AdminSettingsModel adminSettingsModel) + { + return HashAlgorithms[adminSettingsModel.DefaultHashAlgorithm]; + } + public async Task GetAssetCounterAndIncrementAsync() { AssetsMetadataModel assetModel = await GetAssetCounterAsync(); @@ -292,7 +342,7 @@ public async Task CreateAsset(LdapAttributeSet attributeSet) { return new() { Success = false, AuthenticationState = UserNotAuthenticatedReason.InvalidCredentials }; } - if (CompareStringToSha256(password, user.UserPassword)) + if (CompareStringToHashed(password, user.UserPassword)) { return new() { Success = true, UserModel = user }; } @@ -309,22 +359,26 @@ public async Task CreateAsset(LdapAttributeSet attributeSet) } } - public bool CompareStringToSha256(string sourcePassword, string targetPasswordHashed) + public bool CompareStringToHashed(string sourcePassword, string targetPasswordHashed) { - byte[] sourcePasswordBytes = SHA256.HashData(Encoding.UTF8.GetBytes(sourcePassword)); + string algorithmName = CurlyBracesExtractor().Match(targetPasswordHashed).Groups[1].Value; + HashAlgorithm hashAlgorithm = HashAlgorithms[algorithmName.ToUpperInvariant()]; + byte[] sourcePasswordBytes = Encoding.UTF8.GetBytes(sourcePassword); byte[] targetPasswordHashedBytes = Convert.FromBase64String(CurlyBracesRemover().Replace(targetPasswordHashed, "")); - if (sourcePasswordBytes.Length != targetPasswordHashedBytes.Length) + int hashSize = hashAlgorithm.HashSize / 8; + if (targetPasswordHashedBytes.Length > hashSize) // Has salt { - return false; + int saltLength = targetPasswordHashedBytes.Length - hashSize; + byte[] salt = new byte[saltLength]; + Array.Copy(targetPasswordHashedBytes, hashSize, salt, 0, saltLength); + Array.Resize(ref targetPasswordHashedBytes, hashSize); + byte[] newSourcePasswordBytes = new byte[sourcePasswordBytes.Length + salt.Length]; + Array.Copy(sourcePasswordBytes, 0, newSourcePasswordBytes, 0, sourcePasswordBytes.Length); + Array.Copy(salt, 0, newSourcePasswordBytes, sourcePasswordBytes.Length, salt.Length); + sourcePasswordBytes = newSourcePasswordBytes; } - for (int i = 0; i < sourcePasswordBytes.Length; i++) - { - if (sourcePasswordBytes[i] != targetPasswordHashedBytes[i]) - { - return false; - } - } - return true; + sourcePasswordBytes = hashAlgorithm.ComputeHash(sourcePasswordBytes); + return CryptographicOperations.FixedTimeEquals(sourcePasswordBytes, targetPasswordHashedBytes); } private string PrependRDN(string rdn, string dn) @@ -466,4 +520,6 @@ public async Task CreateAsset(LdapAttributeSet attributeSet) [GeneratedRegex(@"\{.*?\}")] private static partial Regex CurlyBracesRemover(); + [GeneratedRegex(@"\{([^}]*)\}")] + private static partial Regex CurlyBracesExtractor(); } \ No newline at end of file diff --git a/src/Services/MigrationService.cs b/src/Services/MigrationService.cs index 8136c38..ecef3fc 100644 --- a/src/Services/MigrationService.cs +++ b/src/Services/MigrationService.cs @@ -105,6 +105,30 @@ public class MigrationService : IHostedService return 2; } + public static async Task UpdateFrom2Async(LdapService ldapService) + { + // Create the settings ou + AdminSettingsModel adminSettings = new() { + BarcodeText = "HAM", + BarcodeType = "code128", + MaxDownloadableUserImageSize = 256, + DefaultHashAlgorithm = "SSHA512", + Presets = [] + }; + string settingsString = JsonSerializer.Serialize(adminSettings); + LdapAttributeSet settingsAttributes = + [ + new("objectClass", "organizationalUnit"), + new("objectClass", "top"), + new("ou", "settings"), + new("description", settingsString) + ]; + await TryCreateObjectIgnoreAlreadyExists(ldapService, ldapService.SettingsBaseDn, settingsAttributes); + + return 3; + } + + private static async Task TryCreateObjectIgnoreAlreadyExists(LdapService ldapService, string dn, LdapAttributeSet ldapAttributes) { try