From 0b354988fdbdc58aafdf1ec9bca41383546b0d01 Mon Sep 17 00:00:00 2001 From: csimonapastore Date: Tue, 11 Mar 2025 23:36:49 +0100 Subject: [PATCH] Adding SHA256 password encryption and password verify --- .../Controllers/AuthController_Tests.cs | 5 +- MainProject.Tests/JsonData/appsettings.json | 3 +- .../JsonData/invalidCryptAppsettings.json | 3 +- .../Common/User/AuthenticatedUser_Tests.cs | 5 +- .../Auth/AuthenticateResponse_Tests.cs | 5 +- MainProject.Tests/Utils/CryptoUtils_Tests.cs | 61 +++++++ .../20250311195750_AlterTableUser.Designer.cs | 154 ++++++++++++++++++ .../20250311195750_AlterTableUser.cs | 84 ++++++++++ .../SqlServerContextModelSnapshot.cs | 24 ++- MainProject/Models/Database/SqlServer/User.cs | 5 +- .../Models/Settings/EncryptionSettings.cs | 1 + MainProject/Utils/CryptoUtils.cs | 52 +++++- MainProject/Utils/JwtTokenUtils.cs | 2 +- MainProject/appsettings.json | 3 +- 14 files changed, 389 insertions(+), 18 deletions(-) create mode 100644 MainProject/Migrations/20250311195750_AlterTableUser.Designer.cs create mode 100644 MainProject/Migrations/20250311195750_AlterTableUser.cs diff --git a/MainProject.Tests/Controllers/AuthController_Tests.cs b/MainProject.Tests/Controllers/AuthController_Tests.cs index ede9785..26beb87 100644 --- a/MainProject.Tests/Controllers/AuthController_Tests.cs +++ b/MainProject.Tests/Controllers/AuthController_Tests.cs @@ -59,10 +59,13 @@ public class AuthController_Tests LastName = "test", Email = "test", PasswordHash = "test", + PasswordSalt = "test", + Password = "test", Role = new DatabaseSqlServer.Role() { Name = "test" - } + }, + IsTestUser = true }; AuthenticatedUser authenticatedUser = new AuthenticatedUser(user); diff --git a/MainProject.Tests/JsonData/appsettings.json b/MainProject.Tests/JsonData/appsettings.json index ec7201c..a8fa93d 100644 --- a/MainProject.Tests/JsonData/appsettings.json +++ b/MainProject.Tests/JsonData/appsettings.json @@ -35,7 +35,8 @@ "ExpiredAfterMinsOfInactivity": 15 }, "EncryptionSettings": { - "Salt": "S7VIidfXQf1tOQYX" + "Salt": "S7VIidfXQf1tOQYX", + "Pepper": "" } } diff --git a/MainProject.Tests/JsonData/invalidCryptAppsettings.json b/MainProject.Tests/JsonData/invalidCryptAppsettings.json index 6e1ad5d..bb290e6 100644 --- a/MainProject.Tests/JsonData/invalidCryptAppsettings.json +++ b/MainProject.Tests/JsonData/invalidCryptAppsettings.json @@ -35,7 +35,8 @@ "ExpiredAfterMinsOfInactivity": 15 }, "EncryptionSettings": { - "Salt": "AAAAA" + "Salt": "AAAAA", + "Pepper": "" } } diff --git a/MainProject.Tests/Models/Api/Common/User/AuthenticatedUser_Tests.cs b/MainProject.Tests/Models/Api/Common/User/AuthenticatedUser_Tests.cs index 9979492..b94ab6d 100644 --- a/MainProject.Tests/Models/Api/Common/User/AuthenticatedUser_Tests.cs +++ b/MainProject.Tests/Models/Api/Common/User/AuthenticatedUser_Tests.cs @@ -29,10 +29,13 @@ public class AuthenticatedUser_Tests LastName = "test", Email = "test", PasswordHash = "test", + PasswordSalt = "test", + Password = "test", Role = new DatabaseSqlServer.Role() { Name = "test" - } + }, + IsTestUser = true }; AuthenticatedUser authenticatedUser = new AuthenticatedUser(user); diff --git a/MainProject.Tests/Models/Api/Response/Auth/AuthenticateResponse_Tests.cs b/MainProject.Tests/Models/Api/Response/Auth/AuthenticateResponse_Tests.cs index e47dd3d..a80e288 100644 --- a/MainProject.Tests/Models/Api/Response/Auth/AuthenticateResponse_Tests.cs +++ b/MainProject.Tests/Models/Api/Response/Auth/AuthenticateResponse_Tests.cs @@ -74,10 +74,13 @@ public class AuthenticateResponse_Tests LastName = "test", Email = "test", PasswordHash = "test", + PasswordSalt = "test", + Password = "test", Role = new DatabaseSqlServer.Role() { Name = "test" - } + }, + IsTestUser = true }; AuthenticatedUser data = new AuthenticatedUser(user); var authenticateResponse = new AuthenticateResponse(200, "This is a test message", data); diff --git a/MainProject.Tests/Utils/CryptoUtils_Tests.cs b/MainProject.Tests/Utils/CryptoUtils_Tests.cs index 8e255fb..a0f7a6c 100644 --- a/MainProject.Tests/Utils/CryptoUtils_Tests.cs +++ b/MainProject.Tests/Utils/CryptoUtils_Tests.cs @@ -101,6 +101,67 @@ public class CryptoUtils_Tests } } + [TestMethod] + public void GenerateSalt() + { + try + { + var salt = CryptUtils.GenerateSalt(); + Assert.IsTrue(!String.IsNullOrEmpty(salt)); + } + catch (Exception ex) + { + Console.WriteLine(ex.InnerException); + Assert.Fail($"An exception was thrown: {ex.Message}"); + } + } + + [TestMethod] + public void ComputeHash_Hashed() + { + try + { + var password = "P4ssw0rd@1!"; + var salt = CryptUtils.GenerateSalt(); + Assert.IsTrue(!String.IsNullOrEmpty(salt)); + + WebApplicationBuilder builder = WebApplication.CreateBuilder(Array.Empty()); + AppSettings appSettings = ProgramUtils.AddConfiguration(ref builder, System.AppDomain.CurrentDomain.BaseDirectory + "/JsonData"); + CryptUtils cryptoUtils = new CryptUtils(appSettings); + var encryptedPassword = cryptoUtils.GeneratePassword(password, salt, 0); + Assert.IsTrue(password != encryptedPassword); + } + catch (Exception ex) + { + Console.WriteLine(ex.InnerException); + Assert.Fail($"An exception was thrown: {ex.Message}"); + } + } + + [TestMethod] + public void VerifyPassword_True() + { + try + { + var password = "P4ssw0rd@1!"; + var salt = "Afi7PQYgEL2sPbNyVzduvg=="; + var hashedPassword = "2lMeySZ9ciH1KtSg1Z7oSJRmJEjHMeDvdaNRcJcGutM="; + + WebApplicationBuilder builder = WebApplication.CreateBuilder(Array.Empty()); + AppSettings appSettings = ProgramUtils.AddConfiguration(ref builder, System.AppDomain.CurrentDomain.BaseDirectory + "/JsonData"); + CryptUtils cryptoUtils = new CryptUtils(appSettings); + var verified = cryptoUtils.VerifyPassword(password, salt, 0, hashedPassword); + Console.WriteLine(cryptoUtils.GeneratePassword(password, salt, 0)); + + Assert.IsTrue(verified); + } + catch (Exception ex) + { + Console.WriteLine(ex.InnerException); + Assert.Fail($"An exception was thrown: {ex.Message}"); + } + } + } diff --git a/MainProject/Migrations/20250311195750_AlterTableUser.Designer.cs b/MainProject/Migrations/20250311195750_AlterTableUser.Designer.cs new file mode 100644 index 0000000..c887e28 --- /dev/null +++ b/MainProject/Migrations/20250311195750_AlterTableUser.Designer.cs @@ -0,0 +1,154 @@ +// +using System; +using BasicDotnetTemplate.MainProject.Core.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace MainProject.Migrations +{ + [DbContext(typeof(SqlServerContext))] + [Migration("20250311195750_AlterTableUser")] + partial class AlterTableUser + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("BasicDotnetTemplate.MainProject.Models.Database.SqlServer.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreationTime") + .HasColumnType("datetime2"); + + b.Property("CreationUserId") + .HasColumnType("int"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("DeletionUserId") + .HasColumnType("int"); + + b.Property("Guid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdateTime") + .HasColumnType("datetime2"); + + b.Property("UpdateUserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Role"); + }); + + modelBuilder.Entity("BasicDotnetTemplate.MainProject.Models.Database.SqlServer.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreationTime") + .HasColumnType("datetime2"); + + b.Property("CreationUserId") + .HasColumnType("int"); + + b.Property("DeletionTime") + .HasColumnType("datetime2"); + + b.Property("DeletionUserId") + .HasColumnType("int"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Guid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Password") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.Property("UpdateTime") + .HasColumnType("datetime2"); + + b.Property("UpdateUserId") + .HasColumnType("int"); + + b.Property("Username") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("BasicDotnetTemplate.MainProject.Models.Database.SqlServer.User", b => + { + b.HasOne("BasicDotnetTemplate.MainProject.Models.Database.SqlServer.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MainProject/Migrations/20250311195750_AlterTableUser.cs b/MainProject/Migrations/20250311195750_AlterTableUser.cs new file mode 100644 index 0000000..26df65b --- /dev/null +++ b/MainProject/Migrations/20250311195750_AlterTableUser.cs @@ -0,0 +1,84 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MainProject.Migrations +{ + /// + public partial class AlterTableUser : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Guid", + table: "Users", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "IsDeleted", + table: "Users", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "Password", + table: "Users", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "PasswordSalt", + table: "Users", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Guid", + table: "Role", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "IsDeleted", + table: "Role", + type: "bit", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Guid", + table: "Users"); + + migrationBuilder.DropColumn( + name: "IsDeleted", + table: "Users"); + + migrationBuilder.DropColumn( + name: "Password", + table: "Users"); + + migrationBuilder.DropColumn( + name: "PasswordSalt", + table: "Users"); + + migrationBuilder.DropColumn( + name: "Guid", + table: "Role"); + + migrationBuilder.DropColumn( + name: "IsDeleted", + table: "Role"); + } + } +} diff --git a/MainProject/Migrations/SqlServerContextModelSnapshot.cs b/MainProject/Migrations/SqlServerContextModelSnapshot.cs index 90248ac..8368bbd 100644 --- a/MainProject/Migrations/SqlServerContextModelSnapshot.cs +++ b/MainProject/Migrations/SqlServerContextModelSnapshot.cs @@ -17,7 +17,7 @@ namespace MainProject.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("ProductVersion", "9.0.2") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -42,6 +42,13 @@ namespace MainProject.Migrations b.Property("DeletionUserId") .HasColumnType("int"); + b.Property("Guid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + b.Property("Name") .IsRequired() .HasColumnType("nvarchar(max)"); @@ -85,14 +92,29 @@ namespace MainProject.Migrations .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("Guid") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + b.Property("LastName") .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("Password") + .IsRequired() + .HasColumnType("nvarchar(max)"); + b.Property("PasswordHash") .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("PasswordSalt") + .IsRequired() + .HasColumnType("nvarchar(max)"); + b.Property("RoleId") .HasColumnType("int"); diff --git a/MainProject/Models/Database/SqlServer/User.cs b/MainProject/Models/Database/SqlServer/User.cs index 5f24212..2c300f2 100644 --- a/MainProject/Models/Database/SqlServer/User.cs +++ b/MainProject/Models/Database/SqlServer/User.cs @@ -8,10 +8,13 @@ namespace BasicDotnetTemplate.MainProject.Models.Database.SqlServer public required string FirstName { get; set; } public required string LastName { get; set; } public required string Email { get; set; } + public required string PasswordSalt { get; set; } + public required string PasswordHash { get; set; } public required Role Role { get; set; } + public required bool IsTestUser { get; set; } [JsonIgnore] - public required string PasswordHash { get; set; } + public required string Password { get; set; } } } diff --git a/MainProject/Models/Settings/EncryptionSettings.cs b/MainProject/Models/Settings/EncryptionSettings.cs index d157fe9..8d40260 100644 --- a/MainProject/Models/Settings/EncryptionSettings.cs +++ b/MainProject/Models/Settings/EncryptionSettings.cs @@ -4,5 +4,6 @@ public class EncryptionSettings { #nullable enable public string? Salt { get; set; } + public string? Pepper { get; set; } #nullable disable } \ No newline at end of file diff --git a/MainProject/Utils/CryptoUtils.cs b/MainProject/Utils/CryptoUtils.cs index 55910ce..67c0664 100644 --- a/MainProject/Utils/CryptoUtils.cs +++ b/MainProject/Utils/CryptoUtils.cs @@ -6,34 +6,36 @@ using BasicDotnetTemplate.MainProject.Models.Settings; namespace BasicDotnetTemplate.MainProject.Utils; public class CryptUtils { - private readonly string secretKey; - private const int M = 16; - private const int N = 32; + private readonly string _secretKey; + private readonly string _pepper; + private const int _M = 16; + private const int _N = 32; public CryptUtils(AppSettings appSettings) { - secretKey = appSettings.EncryptionSettings?.Salt ?? String.Empty; + _secretKey = appSettings.EncryptionSettings?.Salt ?? String.Empty; + _pepper = appSettings.EncryptionSettings?.Pepper ?? String.Empty; } public string Decrypt(string encryptedData) { var decrypted = String.Empty; - if (String.IsNullOrEmpty(this.secretKey) || this.secretKey.Length < M) + if (String.IsNullOrEmpty(this._secretKey) || this._secretKey.Length < _M) { throw new ArgumentException("Unable to proceed with decryption due to invalid settings"); } - if (!String.IsNullOrEmpty(encryptedData) && encryptedData.Length > N) + if (!String.IsNullOrEmpty(encryptedData) && encryptedData.Length > _N) { - var iv = encryptedData.Substring(0, M); + var iv = encryptedData.Substring(0, _M); - var cipherText = encryptedData.Substring(N); + var cipherText = encryptedData.Substring(_N); var fullCipher = Convert.FromBase64String(cipherText); using (var aes = Aes.Create()) { - aes.Key = Encoding.UTF8.GetBytes(this.secretKey); + aes.Key = Encoding.UTF8.GetBytes(this._secretKey); aes.IV = Encoding.UTF8.GetBytes(iv); using (var decryptor = aes.CreateDecryptor(aes.Key, aes.IV)) @@ -55,5 +57,37 @@ public class CryptUtils return decrypted; } + public static string GenerateSalt() + { + using var rng = RandomNumberGenerator.Create(); + var byteSalt = new byte[16]; + rng.GetBytes(byteSalt); + var salt = Convert.ToBase64String(byteSalt); + return salt; + } + + public string GeneratePassword(string password, string salt, int iteration) + { + string hashedPassword = password; + for(var i = 0; i <= iteration; i++) + { + using var sha256 = SHA256.Create(); + var passwordSaltPepper = $"{hashedPassword}{salt}{this._pepper}"; + var byteValue = Encoding.UTF8.GetBytes(passwordSaltPepper); + var byteHash = sha256.ComputeHash(byteValue); + hashedPassword = Convert.ToBase64String(byteHash); + } + + return hashedPassword; + } + + public bool VerifyPassword(string password, string salt, int iteration, string userPassword) + { + string hashedPassword = this.GeneratePassword(password, salt, iteration); + return hashedPassword.Equals(userPassword, StringComparison.OrdinalIgnoreCase); + } + + + } diff --git a/MainProject/Utils/JwtTokenUtils.cs b/MainProject/Utils/JwtTokenUtils.cs index 0b8de7d..7c427a0 100644 --- a/MainProject/Utils/JwtTokenUtils.cs +++ b/MainProject/Utils/JwtTokenUtils.cs @@ -99,7 +99,7 @@ public class JwtTokenUtils } catch(Exception exception) { - Logger.Error($"[JwtTokenUtils][ValidateToken] | {exception.Message}"); + Logger.Error($"[JwtTokenUtils][ValidateToken] | {exception}"); return guid; } } diff --git a/MainProject/appsettings.json b/MainProject/appsettings.json index acc5e8b..86a85b8 100644 --- a/MainProject/appsettings.json +++ b/MainProject/appsettings.json @@ -35,7 +35,8 @@ "ExpiredAfterMinsOfInactivity": 15 }, "EncryptionSettings": { - "Salt": "S7VIidfXQf1tOQYX" + "Salt": "S7VIidfXQf1tOQYX", + "Pepper": "" } } } \ No newline at end of file