diff --git a/BLIS-NG.csproj b/BLIS-NG.csproj index 968416f..7e8d8e2 100644 --- a/BLIS-NG.csproj +++ b/BLIS-NG.csproj @@ -1,4 +1,4 @@ - + WinExe @@ -51,10 +51,10 @@ diff --git a/Lang/Resources.fr.resx b/Lang/Resources.fr.resx index 142c70e..cc1a407 100644 --- a/Lang/Resources.fr.resx +++ b/Lang/Resources.fr.resx @@ -29,7 +29,7 @@ - + @@ -40,14 +40,13 @@ - + - text/microsoft-resx @@ -63,65 +62,106 @@ Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Système d'information de laboratoire de base (BLIS) - - - BLIS pour Windows {0} - - - Une initiative conjointe de C4G à Georgia Tech, du CDC et des pays participants. - - - C4G BLIS est sous licence GNU General Public License version 3. Pour plus d'informations, visitez blis.cc.gatech.edu. - - - Démarrer BLIS - - - Arrêter BLIS - - - Statut : Fonctionnel - - - Statut : Démarrage en cours - - - Statut : Échec de la vérification d'état d'Apache2. - - - Statut : Arrêt en cours - - - Statut : Arrêté - - - Langue : - - - Anglais - - - Français - - - Langue mise à jour. Redémarrez le lanceur pour appliquer tous les changements de texte. - - - Plus d'options - - - Mettre à jour avec un fichier ZIP - - - Réinitialiser le mot de passe - - - Sélectionner un fichier ZIP - - - Fichiers ZIP - + + + Système d'Information de Laboratoire de Base (BLIS) + + + Une initiative conjointe de C4G à Georgia Tech, le CDC et les pays participants. + + + C4G BLIS est distribué sous la licence publique générale GNU version 3. Pour plus d'informations, visitez blis.cc.gatech.edu. + + + Démarrer BLIS + + + Arrêter BLIS + + + + + Outils BLIS + + + Réinitialisation du mot de passe + + + Réinitialiser le mot de passe + + + Réinitialiser le mot de passe + + + Nom d'utilisateur à réinitialiser + + + Entrer le nom d'utilisateur + + + Exigences du mot de passe + + + • Au moins 20 caractères + + + • Au moins 2 parmi : majuscules, minuscules, chiffres, symboles + + + • Exemples de symboles : ! @ # $ % ^ & * + + + Nouveau mot de passe + + + Entrer le nouveau mot de passe (min. 20 caractères) + + + Confirmer le mot de passe + + + Confirmer le nouveau mot de passe + + + Un utilisateur de rang supérieur doit autoriser cette réinitialisation. + + + Nom d'utilisateur du superviseur + + + Entrer le nom du superviseur + + + Mot de passe du superviseur + + + Entrer le mot de passe du superviseur + + + Suivant → + + + ← Retour + + + Confirmer la réinitialisation + + + Réinitialiser le formulaire + + + + + Nom d'utilisateur + + + Entrer le nouveau mot de passe + + + Réinitialiser + + + Annuler + - + \ No newline at end of file diff --git a/Lang/Resources.resx b/Lang/Resources.resx index 11456e7..7b881ca 100644 --- a/Lang/Resources.resx +++ b/Lang/Resources.resx @@ -29,7 +29,7 @@ - + @@ -40,14 +40,13 @@ - + - text/microsoft-resx @@ -63,6 +62,7 @@ Culture=neutral, PublicKeyToken=b77a5c561934e089 + Basic Lab Information System (BLIS) @@ -83,15 +83,117 @@ For more information, visit blis.cc.gatech.edu. Stop BLIS + + + Basic Lab Information System (BLIS) + + + A Joint Initiative of C4G at Georgia Tech, the CDC, and participating countries. + + + C4G BLIS has been licensed under the GNU General Public License version 3. For more information, visit blis.cc.gatech.edu. + + + Start BLIS + + + Stop BLIS + + + + + BLIS Tools + + + Password Reset + + + Reset User Password + + + Reset Password + + + Username to Reset + + + Enter username + + + Password Requirements + + + • At least 20 characters + + + • At least 2 of: uppercase, lowercase, numbers, symbols + + + • Example symbol characters: ! @ # $ % ^ & * + + + New Password + + + Enter new password (min. 20 chars) + + + Confirm Password + + + Confirm new password + + + A higher-ranked user must authorize this password reset. + + + Supervisor Username + + + Enter supervisor username + + + Supervisor Password + + + Enter supervisor password + + + Next → + + + ← Back + + + Confirm Reset + + + Reset Form + + + + + Username + + + Enter new password + + + Reset + + + Cancel + + + + Status: Apache2 health check failed. + Status: Healthy Status: Starting - - Status: Apache2 health check failed. - Status: Stopping @@ -123,6 +225,7 @@ For more information, visit blis.cc.gatech.edu. Select ZIP File - ZIP Files + ZIP Files - + + \ No newline at end of file diff --git a/Server/Mysql.cs b/Server/Mysql.cs index 86587a6..91d4897 100644 --- a/Server/Mysql.cs +++ b/Server/Mysql.cs @@ -1,4 +1,8 @@ +using System.Security.Cryptography; +using System.Text; + using BLIS_NG.Config; + using Microsoft.Extensions.Logging; namespace BLIS_NG.Server; @@ -22,10 +26,18 @@ public override void Stop() return; } - public async Task ResetUserPassword(string username, string sha1Password) + // ── User password reset ─────────────────────────────────────────────── + + /// + /// Resets a user's password using the new SHA-256 scheme with a fresh random salt. + /// + public async Task ResetUserPassword(string username, string cleartextPassword) { var safeUsername = username.Replace("'", "\\'"); - var sql = $"UPDATE user SET password='{sha1Password}' WHERE username='{safeUsername}';"; + var newSalt = GenerateSalt(); + var newHash = HashPasswordV2(cleartextPassword, newSalt); + + var sql = $"UPDATE user SET password='{newHash}', salt='{newSalt}' WHERE username='{safeUsername}';"; var args = $"{baseArguments} blis_revamp -e \"{sql}\""; bool hasError = false; @@ -34,9 +46,11 @@ await Execute( (stdout) => logger.LogInformation("ResetUserPassword stdout: {Message}", stdout), (stderr) => { - logger.LogWarning("{Message}", stderr); if (!stderr.Contains("Using a password on the command line interface")) + { + logger.LogWarning("ResetUserPassword stderr: {Message}", stderr); hasError = true; + } } ); return !hasError; @@ -101,4 +115,123 @@ await Execute( ); return level; } + + + // ── Password hashing ────────────────────────────────────────────────── + + /// + /// Legacy SHA-1 hash with hardcoded salt. Still used to verify old passwords + /// that have not yet been upgraded, and for VerifyHigherRankedUser when the + /// supervisor account has not been upgraded yet. + /// + public static string HashPasswordLegacy(string password) + { + var salted = password + "This comment should suffice as salt."; + var bytes = SHA1.HashData(Encoding.UTF8.GetBytes(salted)); + return Convert.ToHexString(bytes).ToLowerInvariant(); + } + + /// + /// New SHA-256 hash with a caller-supplied per-user random salt. + /// + public static string HashPasswordV2(string password, string salt) + { + var salted = password + salt; + var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(salted)); + return Convert.ToHexString(bytes).ToLowerInvariant(); + } + + /// + /// Generates a cryptographically random 64-character hex salt. + /// + public static string GenerateSalt() + { + var bytes = new byte[32]; + RandomNumberGenerator.Fill(bytes); + return Convert.ToHexString(bytes).ToLowerInvariant(); + } + + // ── Supervisor verification ─────────────────────────────────────────── + + /// + /// Verifies that a supervisor exists, has sufficient rank, and that their + /// password matches — handling both the new SHA-256 scheme and the legacy + /// SHA-1 scheme for accounts not yet upgraded. + /// + /// TODO: confirm the rank column name in the `user` table with your team + /// (e.g. user_type, user_rank, level — check the BLIS schema). + /// Based on db_lib.php, 'level' is the column; level >= 2 = Admin. + /// + public async Task VerifyHigherRankedUser(string username, string cleartextPassword) + { + var safeUsername = username.Replace("'", "\\'"); + + // Fetch the supervisor's stored hash, salt, and level + string? storedHash = null; + string? storedSalt = null; + int storedLevel = 0; + + var selectSql = $"SELECT password, salt, level FROM user WHERE username='{safeUsername}' LIMIT 1;"; + var selectArgs = $"{baseArguments} blis_revamp -se \"{selectSql}\""; + + await Execute( + MysqlPath, selectArgs, null, + (stdout) => + { + logger.LogInformation("VerifyHigherRankedUser fetch stdout: {Message}", stdout); + // MySQL -se output: header line then data line, tab-separated + var lines = stdout.Split('\n', StringSplitOptions.RemoveEmptyEntries); + if (lines.Length >= 2) + { + var cols = lines[1].Split('\t'); + if (cols.Length >= 3) + { + storedHash = cols[0].Trim(); + storedSalt = cols[1].Trim() == "NULL" ? null : cols[1].Trim(); + int.TryParse(cols[2].Trim(), out storedLevel); + } + } + }, + (stderr) => + { + if (!stderr.Contains("Using a password on the command line interface")) + logger.LogWarning("VerifyHigherRankedUser fetch stderr: {Message}", stderr); + } + ); + + if (storedHash is null) + { + logger.LogWarning("VerifyHigherRankedUser: user '{Username}' not found.", username); + return false; + } + + // Level >= 2 = Admin or above (matches $LIS_ADMIN = 2 in db_lib.php) + if (storedLevel < 2) + { + logger.LogWarning("VerifyHigherRankedUser: user '{Username}' has insufficient rank ({Level}).", username, storedLevel); + return false; + } + + // Verify password using the appropriate scheme + bool passwordMatch; + if (!string.IsNullOrEmpty(storedSalt)) + { + // New scheme: SHA-256 with per-user salt + var candidate = HashPasswordV2(cleartextPassword, storedSalt); + passwordMatch = string.Equals(storedHash, candidate, StringComparison.OrdinalIgnoreCase); + } + else + { + // Legacy scheme: SHA-1 with hardcoded salt + var candidate = HashPasswordLegacy(cleartextPassword); + passwordMatch = string.Equals(storedHash, candidate, StringComparison.OrdinalIgnoreCase); + } + + if (!passwordMatch) + { + logger.LogWarning("VerifyHigherRankedUser: password mismatch for '{Username}'.", username); + } + + return passwordMatch; + } } diff --git a/Server/MysqlAdmin.cs b/Server/MysqlAdmin.cs index d588963..4152698 100644 --- a/Server/MysqlAdmin.cs +++ b/Server/MysqlAdmin.cs @@ -25,7 +25,6 @@ public override void Stop() public async Task Ping() { - // Not logging stdout here since it will just fill up logs. var result = await Execute(MysqlAdminPath, $"{baseArguments} ping", null, null, null); return result.ExitCode == 0; } @@ -36,6 +35,4 @@ await Execute(MysqlAdminPath, $"{baseArguments} shutdown", null, (stdout) => logger.LogInformation("{Message}", stdout), (stderr) => logger.LogWarning("{Message}", stderr)); } - - } diff --git a/Views/MainWindow.axaml b/Views/MainWindow.axaml index 2f3e826..19e1395 100644 --- a/Views/MainWindow.axaml +++ b/Views/MainWindow.axaml @@ -1,6 +1,7 @@ - - - - + Grid.ColumnSpan="5" + Text="{x:Static lang:Resources.AppTitle}" /> + Grid.ColumnSpan="5" /> - + Grid.ColumnSpan="5" + Text="{x:Static lang:Resources.AppJointInitiative}" /> - - - - + Grid.ColumnSpan="5" + Text="{x:Static lang:Resources.AppLicense}" /> - - - - - - - - + - - + + + IsVisible="{Binding IsStep1Visible}" + Content="{x:Static lang:Resources.ButtonNext}" /> + IsVisible="{Binding IsStep2Visible}" + Content="{x:Static lang:Resources.ButtonBack}" /> - + IsVisible="{Binding IsStep2Visible}" + Content="{x:Static lang:Resources.ButtonConfirmReset}" /> +