diff --git a/Codout.Framework.sln b/Codout.Framework.sln
index 7bd7290..4ef8528 100644
--- a/Codout.Framework.sln
+++ b/Codout.Framework.sln
@@ -58,6 +58,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codout.Framework.Data", "Co
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codout.Mailer.Razor", "Codout.Mailer.Razor\Codout.Mailer.Razor.csproj", "{B42C234C-CEB5-76D7-752D-DE71B2F1E9F7}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Security", "Security", "{F65D869E-54A1-41D2-A6C3-EAD78678ADC4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codout.Security.Core", "src\Security\Codout.Security.Core\Codout.Security.Core.csproj", "{6764EB74-2D58-45E8-8F5B-942B4FB1C47D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codout.Security.Argon2", "src\Security\Codout.Security.Argon2\Codout.Security.Argon2.csproj", "{3D7FBB69-1515-4A0E-BAED-D87C08C921EA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codout.Security.Bcrypt", "src\Security\Codout.Security.Bcrypt\Codout.Security.Bcrypt.csproj", "{732C166B-24BF-4A84-8C5F-4EDB6340CE45}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codout.Security.Scrypt", "src\Security\Codout.Security.Scrypt\Codout.Security.Scrypt.csproj", "{EF60A915-F3C8-4ABE-A6B3-5086F7093FB6}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -136,6 +146,22 @@ Global
{B42C234C-CEB5-76D7-752D-DE71B2F1E9F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B42C234C-CEB5-76D7-752D-DE71B2F1E9F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B42C234C-CEB5-76D7-752D-DE71B2F1E9F7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6764EB74-2D58-45E8-8F5B-942B4FB1C47D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6764EB74-2D58-45E8-8F5B-942B4FB1C47D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6764EB74-2D58-45E8-8F5B-942B4FB1C47D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6764EB74-2D58-45E8-8F5B-942B4FB1C47D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3D7FBB69-1515-4A0E-BAED-D87C08C921EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3D7FBB69-1515-4A0E-BAED-D87C08C921EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3D7FBB69-1515-4A0E-BAED-D87C08C921EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3D7FBB69-1515-4A0E-BAED-D87C08C921EA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {732C166B-24BF-4A84-8C5F-4EDB6340CE45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {732C166B-24BF-4A84-8C5F-4EDB6340CE45}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {732C166B-24BF-4A84-8C5F-4EDB6340CE45}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {732C166B-24BF-4A84-8C5F-4EDB6340CE45}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EF60A915-F3C8-4ABE-A6B3-5086F7093FB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EF60A915-F3C8-4ABE-A6B3-5086F7093FB6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EF60A915-F3C8-4ABE-A6B3-5086F7093FB6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EF60A915-F3C8-4ABE-A6B3-5086F7093FB6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -161,6 +187,10 @@ Global
{7A5F8194-FBD0-47C0-A8F8-1EB68662BE1E} = {B6832CA4-604B-4DC8-908B-06405133B6B6}
{52DE6A78-4383-E3EC-3109-2410414F8EEE} = {B6832CA4-604B-4DC8-908B-06405133B6B6}
{B42C234C-CEB5-76D7-752D-DE71B2F1E9F7} = {992EAEDC-B75F-45CA-8B2C-840E3F9B5133}
+ {6764EB74-2D58-45E8-8F5B-942B4FB1C47D} = {F65D869E-54A1-41D2-A6C3-EAD78678ADC4}
+ {3D7FBB69-1515-4A0E-BAED-D87C08C921EA} = {F65D869E-54A1-41D2-A6C3-EAD78678ADC4}
+ {732C166B-24BF-4A84-8C5F-4EDB6340CE45} = {F65D869E-54A1-41D2-A6C3-EAD78678ADC4}
+ {EF60A915-F3C8-4ABE-A6B3-5086F7093FB6} = {F65D869E-54A1-41D2-A6C3-EAD78678ADC4}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F077B358-8F0D-4791-8E85-DDED3550C27A}
diff --git a/Codout.Mailer.Razor/Configuration/ConfigureServices.cs b/Codout.Mailer.Razor/Configuration/ConfigureServices.cs
index 70c6f62..19c0ce3 100644
--- a/Codout.Mailer.Razor/Configuration/ConfigureServices.cs
+++ b/Codout.Mailer.Razor/Configuration/ConfigureServices.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Codout.Mailer.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
@@ -8,8 +8,8 @@ namespace Codout.Mailer.Razor.Configuration;
public static class ConfigureServices
{
///
- /// Registra o template engine Razor nativo do ASP.NET Core para renderiza��o de templates de e-mail.
- /// Deve ser chamado ap�s AddMailer().
+ /// Registra o template engine Razor nativo do ASP.NET Core para renderização de templates de e-mail.
+ /// Deve ser chamado após AddMailer().
///
public static IServiceCollection AddMailerRazor(
this IServiceCollection services,
@@ -32,3 +32,5 @@ public static IServiceCollection AddMailerRazor(
return services;
}
}
+
+
diff --git a/Codout.Mailer.Razor/Configuration/RazorMailerOptions.cs b/Codout.Mailer.Razor/Configuration/RazorMailerOptions.cs
index 98e6519..60a90be 100644
--- a/Codout.Mailer.Razor/Configuration/RazorMailerOptions.cs
+++ b/Codout.Mailer.Razor/Configuration/RazorMailerOptions.cs
@@ -1,15 +1,15 @@
-using System;
+using System;
using System.Reflection;
namespace Codout.Mailer.Razor.Configuration;
///
-/// Op��es de configura��o para o template engine Razor do ASP.NET Core
+/// Opções de configuração para o template engine Razor do ASP.NET Core
///
public class RazorMailerOptions
{
///
- /// Assembly que cont�m os templates Razor embarcados como recursos
+ /// Assembly que contém os templates Razor embarcados como recursos
///
public Assembly TemplateAssembly { get; set; }
@@ -19,7 +19,7 @@ public class RazorMailerOptions
public string RootNamespace { get; set; }
///
- /// Habilita o cache de templates compilados em mem�ria
+ /// Habilita o cache de templates compilados em memória
///
public bool EnableCache { get; set; } = true;
diff --git a/Codout.Mailer.Razor/RazorViewTemplateEngine.cs b/Codout.Mailer.Razor/RazorViewTemplateEngine.cs
index 002ef3d..529c32d 100644
--- a/Codout.Mailer.Razor/RazorViewTemplateEngine.cs
+++ b/Codout.Mailer.Razor/RazorViewTemplateEngine.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.IO;
using System.Threading.Tasks;
using Codout.Mailer.Interfaces;
@@ -33,7 +33,7 @@ public async Task RenderAsync(string templateKey, T model)
{
var searchedLocations = string.Join(", ", viewResult.SearchedLocations ?? []);
throw new InvalidOperationException(
- $"Template '{templateKey}' n�o encontrado. Locais pesquisados: {searchedLocations}");
+ $"Template '{templateKey}' não encontrado. Locais pesquisados: {searchedLocations}");
}
await using var writer = new StringWriter();
diff --git a/src/Security/Codout.Security.Argon2/Argon2Extensions.cs b/src/Security/Codout.Security.Argon2/Argon2Extensions.cs
new file mode 100644
index 0000000..f49da25
--- /dev/null
+++ b/src/Security/Codout.Security.Argon2/Argon2Extensions.cs
@@ -0,0 +1,20 @@
+using Codout.Security.Core;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Codout.Security.Argon2;
+
+public static class Argon2Extensions
+{
+ public static IServiceCollection UseArgon2(this IPasswordHashBuilder builder, Action? configure = null)
+ {
+ builder.Services.Configure(options =>
+ {
+ options.Strength = builder.Options.Strength;
+ });
+
+ if (configure != null)
+ builder.Services.Configure(configure);
+
+ return builder.Services.AddScoped();
+ }
+}
\ No newline at end of file
diff --git a/src/Security/Codout.Security.Argon2/Argon2Options.cs b/src/Security/Codout.Security.Argon2/Argon2Options.cs
new file mode 100644
index 0000000..f3c0452
--- /dev/null
+++ b/src/Security/Codout.Security.Argon2/Argon2Options.cs
@@ -0,0 +1,17 @@
+namespace Codout.Security.Argon2;
+
+public class Argon2Options
+{
+ ///
+ /// opslimit represents a maximum amount of computations to perform.
+ /// Raising this number will make the function require more CPU cycles to compute a key.
+ /// When set along with MemLimit, overrides the Strength setting.
+ ///
+ public long? OpsLimit { get; set; }
+
+ ///
+ /// memlimit is the maximum amount of RAM that the function will use, in bytes.
+ /// When set along with OpsLimit, overrides the Strength setting.
+ ///
+ public int? MemLimit { get; set; }
+}
diff --git a/src/Security/Codout.Security.Argon2/ArgonPasswordHash.cs b/src/Security/Codout.Security.Argon2/ArgonPasswordHash.cs
new file mode 100644
index 0000000..5274bc3
--- /dev/null
+++ b/src/Security/Codout.Security.Argon2/ArgonPasswordHash.cs
@@ -0,0 +1,89 @@
+using Codout.Security.Core;
+using Microsoft.Extensions.Options;
+using Sodium;
+
+namespace Codout.Security.Argon2
+{
+ public class ArgonPasswordHash(
+ IOptions? optionsAccessor = null,
+ IOptions? argon2OptionsAccessor = null) : IPasswordHasher
+ {
+ private readonly ImprovedPasswordHasherOptions _options = optionsAccessor?.Value ?? new ImprovedPasswordHasherOptions();
+ private readonly Argon2Options _argon2Options = argon2OptionsAccessor?.Value ?? new Argon2Options();
+
+ public string HashPassword(string password)
+ {
+ if (string.IsNullOrEmpty(password))
+ throw new ArgumentNullException(nameof(password), $"{nameof(password)} should not be null");
+
+ if (_argon2Options.OpsLimit.HasValue && _argon2Options.MemLimit.HasValue)
+ return PasswordHash.ArgonHashString(password, _argon2Options.OpsLimit.Value, _argon2Options.MemLimit.Value).TrimEnd('\0');
+
+ return _options.Strength switch
+ {
+ PasswordHasherStrength.Interactive => PasswordHash.ArgonHashString(password).TrimEnd('\0'),
+ PasswordHasherStrength.Moderate => PasswordHash.ArgonHashString(password, PasswordHash.StrengthArgon.Moderate).TrimEnd('\0'),
+ PasswordHasherStrength.Sensitive => PasswordHash.ArgonHashString(password, PasswordHash.StrengthArgon.Sensitive).TrimEnd('\0'),
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ }
+
+ public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
+ {
+ if (string.IsNullOrEmpty(hashedPassword))
+ throw new ArgumentNullException(nameof(hashedPassword), $"{nameof(hashedPassword)} should not be null");
+
+ if (string.IsNullOrEmpty(providedPassword))
+ throw new ArgumentNullException(nameof(providedPassword), $"{nameof(providedPassword)} should not be null");
+
+ if (!PasswordHash.ArgonHashStringVerify(hashedPassword, providedPassword))
+ return PasswordVerificationResult.Failed;
+
+ return NeedsRehash(hashedPassword)
+ ? PasswordVerificationResult.SuccessRehashNeeded
+ : PasswordVerificationResult.Success;
+ }
+
+ private bool NeedsRehash(string hashedPassword)
+ {
+ // Parse Argon2 hash format: $argon2id$v=19$m=65536,t=3,p=1$salt$hash
+ var parts = hashedPassword.Split('$');
+ if (parts.Length < 4)
+ return false;
+
+ var paramPart = parts[3];
+ var parameters = paramPart.Split(',');
+
+ long storedMemKib = 0;
+ long storedTimeCost = 0;
+
+ foreach (var param in parameters)
+ {
+ if (param.StartsWith("m=") && long.TryParse(param.AsSpan(2), out var m))
+ storedMemKib = m;
+ else if (param.StartsWith("t=") && long.TryParse(param.AsSpan(2), out var t))
+ storedTimeCost = t;
+ }
+
+ if (storedMemKib == 0 || storedTimeCost == 0)
+ return false;
+
+ var (expectedTimeCost, expectedMemKib) = GetExpectedParameters();
+ return storedTimeCost < expectedTimeCost || storedMemKib < expectedMemKib;
+ }
+
+ private (long timeCost, long memKib) GetExpectedParameters()
+ {
+ if (_argon2Options.OpsLimit.HasValue && _argon2Options.MemLimit.HasValue)
+ return (_argon2Options.OpsLimit.Value, _argon2Options.MemLimit.Value / 1024);
+
+ return _options.Strength switch
+ {
+ PasswordHasherStrength.Interactive => (2, 65536), // 64 MiB
+ PasswordHasherStrength.Moderate => (3, 262144), // 256 MiB
+ PasswordHasherStrength.Sensitive => (4, 1048576), // 1 GiB
+ _ => (4, 1048576)
+ };
+ }
+ }
+}
diff --git a/src/Security/Codout.Security.Argon2/Codout.Security.Argon2.csproj b/src/Security/Codout.Security.Argon2/Codout.Security.Argon2.csproj
new file mode 100644
index 0000000..3ba324a
--- /dev/null
+++ b/src/Security/Codout.Security.Argon2/Codout.Security.Argon2.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Security/Codout.Security.Bcrypt/BcryptExtensions.cs b/src/Security/Codout.Security.Bcrypt/BcryptExtensions.cs
new file mode 100644
index 0000000..cc20e89
--- /dev/null
+++ b/src/Security/Codout.Security.Bcrypt/BcryptExtensions.cs
@@ -0,0 +1,20 @@
+using Codout.Security.Core;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Codout.Security.Bcrypt;
+
+public static class BcryptExtensions
+{
+ public static IServiceCollection UseBcrypt(this IPasswordHashBuilder builder, Action? configure = null)
+ {
+ builder.Services.Configure(options =>
+ {
+ options.Strength = builder.Options.Strength;
+ });
+
+ if (configure != null)
+ builder.Services.Configure(configure);
+
+ return builder.Services.AddScoped();
+ }
+}
\ No newline at end of file
diff --git a/src/Security/Codout.Security.Bcrypt/BcryptOptions.cs b/src/Security/Codout.Security.Bcrypt/BcryptOptions.cs
new file mode 100644
index 0000000..281503e
--- /dev/null
+++ b/src/Security/Codout.Security.Bcrypt/BcryptOptions.cs
@@ -0,0 +1,16 @@
+namespace Codout.Security.Bcrypt;
+
+public class BcryptOptions
+{
+ ///
+ /// The log2 of the number of rounds of hashing to apply on BCrypt.
+ /// The work factor therefore increases as 2**workFactor.
+ /// Valid range: 4-31.
+ ///
+ public int WorkFactor { get; set; } = 12;
+
+ ///
+ /// The salt revision to use for BCrypt hashing.
+ ///
+ public BcryptSaltRevision SaltRevision { get; set; } = BcryptSaltRevision.Revision2B;
+}
diff --git a/src/Security/Codout.Security.Bcrypt/BcryptPasswordHash.cs b/src/Security/Codout.Security.Bcrypt/BcryptPasswordHash.cs
new file mode 100644
index 0000000..fe12a5d
--- /dev/null
+++ b/src/Security/Codout.Security.Bcrypt/BcryptPasswordHash.cs
@@ -0,0 +1,45 @@
+using Codout.Security.Core;
+using Microsoft.Extensions.Options;
+
+namespace Codout.Security.Bcrypt
+{
+ public class BcryptPasswordHash(IOptions? bcryptOptionsAccessor = null) : IPasswordHasher
+ {
+ private readonly BcryptOptions _bcryptOptions = bcryptOptionsAccessor?.Value ?? new BcryptOptions();
+
+ public string HashPassword(string password)
+ {
+ if (string.IsNullOrEmpty(password))
+ throw new ArgumentNullException(nameof(password), $"{nameof(password)} should not be null");
+
+ var salt = BCrypt.Net.BCrypt.GenerateSalt(_bcryptOptions.WorkFactor, GetSaltRevision());
+ return BCrypt.Net.BCrypt.HashPassword(password, salt);
+ }
+
+ public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
+ {
+ if (string.IsNullOrEmpty(hashedPassword))
+ throw new ArgumentNullException(nameof(hashedPassword), $"{nameof(hashedPassword)} should not be null");
+
+ if (string.IsNullOrEmpty(providedPassword))
+ throw new ArgumentNullException(nameof(providedPassword), $"{nameof(providedPassword)} should not be null");
+
+ if (!BCrypt.Net.BCrypt.Verify(providedPassword, hashedPassword))
+ return PasswordVerificationResult.Failed;
+
+ return BCrypt.Net.BCrypt.PasswordNeedsRehash(hashedPassword, _bcryptOptions.WorkFactor)
+ ? PasswordVerificationResult.SuccessRehashNeeded
+ : PasswordVerificationResult.Success;
+ }
+
+ private char GetSaltRevision() => _bcryptOptions.SaltRevision switch
+ {
+ BcryptSaltRevision.Revision2 => 'a',
+ BcryptSaltRevision.Revision2A => 'a',
+ BcryptSaltRevision.Revision2B => 'b',
+ BcryptSaltRevision.Revision2X => 'x',
+ BcryptSaltRevision.Revision2Y => 'y',
+ _ => throw new ArgumentOutOfRangeException(nameof(_bcryptOptions.SaltRevision))
+ };
+ }
+}
diff --git a/src/Security/Codout.Security.Bcrypt/BcryptSaltRevision.cs b/src/Security/Codout.Security.Bcrypt/BcryptSaltRevision.cs
new file mode 100644
index 0000000..df74353
--- /dev/null
+++ b/src/Security/Codout.Security.Bcrypt/BcryptSaltRevision.cs
@@ -0,0 +1,10 @@
+namespace Codout.Security.Bcrypt;
+
+public enum BcryptSaltRevision
+{
+ Revision2,
+ Revision2A,
+ Revision2B,
+ Revision2X,
+ Revision2Y
+}
diff --git a/src/Security/Codout.Security.Bcrypt/Codout.Security.Bcrypt.csproj b/src/Security/Codout.Security.Bcrypt/Codout.Security.Bcrypt.csproj
new file mode 100644
index 0000000..521c74e
--- /dev/null
+++ b/src/Security/Codout.Security.Bcrypt/Codout.Security.Bcrypt.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Security/Codout.Security.Core/Codout.Security.Core.csproj b/src/Security/Codout.Security.Core/Codout.Security.Core.csproj
new file mode 100644
index 0000000..5831e53
--- /dev/null
+++ b/src/Security/Codout.Security.Core/Codout.Security.Core.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/src/Security/Codout.Security.Core/IPasswordHashBuilder.cs b/src/Security/Codout.Security.Core/IPasswordHashBuilder.cs
new file mode 100644
index 0000000..6bad9b1
--- /dev/null
+++ b/src/Security/Codout.Security.Core/IPasswordHashBuilder.cs
@@ -0,0 +1,11 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Codout.Security.Core;
+
+public interface IPasswordHashBuilder
+{
+ IServiceCollection Services { get; }
+ ImprovedPasswordHasherOptions Options { get; }
+
+ IPasswordHashBuilder WithStrength(PasswordHasherStrength strength);
+}
\ No newline at end of file
diff --git a/src/Security/Codout.Security.Core/IPasswordHasher.cs b/src/Security/Codout.Security.Core/IPasswordHasher.cs
new file mode 100644
index 0000000..45930c9
--- /dev/null
+++ b/src/Security/Codout.Security.Core/IPasswordHasher.cs
@@ -0,0 +1,10 @@
+namespace Codout.Security.Core;
+
+public interface IPasswordHasher
+{
+ string HashPassword(string password);
+
+ PasswordVerificationResult VerifyHashedPassword(
+ string hashedPassword,
+ string providedPassword);
+}
\ No newline at end of file
diff --git a/src/Security/Codout.Security.Core/ImprovedPasswordHasherOptions.cs b/src/Security/Codout.Security.Core/ImprovedPasswordHasherOptions.cs
new file mode 100644
index 0000000..b57229d
--- /dev/null
+++ b/src/Security/Codout.Security.Core/ImprovedPasswordHasherOptions.cs
@@ -0,0 +1,9 @@
+namespace Codout.Security.Core;
+
+public class ImprovedPasswordHasherOptions
+{
+ ///
+ /// Password Strength. Used to configure the hashing algorithm's resource usage.
+ ///
+ public PasswordHasherStrength Strength { get; set; } = PasswordHasherStrength.Sensitive;
+}
\ No newline at end of file
diff --git a/src/Security/Codout.Security.Core/PasswordHasherBuilder.cs b/src/Security/Codout.Security.Core/PasswordHasherBuilder.cs
new file mode 100644
index 0000000..2df69f8
--- /dev/null
+++ b/src/Security/Codout.Security.Core/PasswordHasherBuilder.cs
@@ -0,0 +1,19 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Codout.Security.Core;
+
+public class PasswordHasherBuilder(IServiceCollection services) : IPasswordHashBuilder
+{
+ public ImprovedPasswordHasherOptions Options { get; } = new();
+
+ public IServiceCollection Services { get; } = services ?? throw new ArgumentNullException(nameof(services));
+
+ ///
+ /// Password Strength. Used to configure the hashing algorithm's resource usage.
+ ///
+ public IPasswordHashBuilder WithStrength(PasswordHasherStrength strength)
+ {
+ Options.Strength = strength;
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/src/Security/Codout.Security.Core/PasswordHasherServiceExtensions.cs b/src/Security/Codout.Security.Core/PasswordHasherServiceExtensions.cs
new file mode 100644
index 0000000..ebbb0f4
--- /dev/null
+++ b/src/Security/Codout.Security.Core/PasswordHasherServiceExtensions.cs
@@ -0,0 +1,18 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Codout.Security.Core
+{
+ public static class PasswordHasherServiceExtensions
+ {
+ public static IPasswordHashBuilder UseCustomHashPasswordBuilder(this IServiceCollection services)
+ {
+ return new PasswordHasherBuilder(services);
+ }
+
+ public static IPasswordHashBuilder UpgradePasswordSecurity(this IServiceCollection services)
+ {
+ return services.UseCustomHashPasswordBuilder();
+ }
+
+ }
+}
diff --git a/src/Security/Codout.Security.Core/PasswordHasherStrength.cs b/src/Security/Codout.Security.Core/PasswordHasherStrength.cs
new file mode 100644
index 0000000..0f9659e
--- /dev/null
+++ b/src/Security/Codout.Security.Core/PasswordHasherStrength.cs
@@ -0,0 +1,11 @@
+namespace Codout.Security.Core;
+
+public enum PasswordHasherStrength
+{
+ /// For interactive sessions (fast: uses 16MB of RAM).
+ Interactive,
+ /// For normal use (moderate: uses 128MB of RAM).
+ Moderate,
+ /// For highly sensitive data (slow: uses more than 1GB of RAM).
+ Sensitive
+}
\ No newline at end of file
diff --git a/src/Security/Codout.Security.Core/PasswordVerificationResult.cs b/src/Security/Codout.Security.Core/PasswordVerificationResult.cs
new file mode 100644
index 0000000..6b2d0cc
--- /dev/null
+++ b/src/Security/Codout.Security.Core/PasswordVerificationResult.cs
@@ -0,0 +1,8 @@
+namespace Codout.Security.Core;
+
+public enum PasswordVerificationResult
+{
+ Failed,
+ Success,
+ SuccessRehashNeeded
+}
\ No newline at end of file
diff --git a/src/Security/Codout.Security.Scrypt/Codout.Security.Scrypt.csproj b/src/Security/Codout.Security.Scrypt/Codout.Security.Scrypt.csproj
new file mode 100644
index 0000000..26f553a
--- /dev/null
+++ b/src/Security/Codout.Security.Scrypt/Codout.Security.Scrypt.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Security/Codout.Security.Scrypt/ScryptExtensions.cs b/src/Security/Codout.Security.Scrypt/ScryptExtensions.cs
new file mode 100644
index 0000000..2c78aae
--- /dev/null
+++ b/src/Security/Codout.Security.Scrypt/ScryptExtensions.cs
@@ -0,0 +1,20 @@
+using Codout.Security.Core;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Codout.Security.Scrypt;
+
+public static class ScryptExtensions
+{
+ public static IServiceCollection UseScrypt(this IPasswordHashBuilder builder, Action? configure = null)
+ {
+ builder.Services.Configure(options =>
+ {
+ options.Strength = builder.Options.Strength;
+ });
+
+ if (configure != null)
+ builder.Services.Configure(configure);
+
+ return builder.Services.AddScoped();
+ }
+}
\ No newline at end of file
diff --git a/src/Security/Codout.Security.Scrypt/ScryptOptions.cs b/src/Security/Codout.Security.Scrypt/ScryptOptions.cs
new file mode 100644
index 0000000..efafe77
--- /dev/null
+++ b/src/Security/Codout.Security.Scrypt/ScryptOptions.cs
@@ -0,0 +1,16 @@
+namespace Codout.Security.Scrypt;
+
+public class ScryptOptions
+{
+ ///
+ /// opslimit represents a maximum amount of computations to perform.
+ /// When set along with MemLimit, overrides the Strength setting.
+ ///
+ public long? OpsLimit { get; set; }
+
+ ///
+ /// memlimit is the maximum amount of RAM that the function will use, in bytes.
+ /// When set along with OpsLimit, overrides the Strength setting.
+ ///
+ public int? MemLimit { get; set; }
+}
diff --git a/src/Security/Codout.Security.Scrypt/ScryptPasswordHash.cs b/src/Security/Codout.Security.Scrypt/ScryptPasswordHash.cs
new file mode 100644
index 0000000..08f9c40
--- /dev/null
+++ b/src/Security/Codout.Security.Scrypt/ScryptPasswordHash.cs
@@ -0,0 +1,94 @@
+using Codout.Security.Core;
+using Microsoft.Extensions.Options;
+using Sodium;
+
+namespace Codout.Security.Scrypt
+{
+ public class ScryptPasswordHash(
+ IOptions? optionsAccessor = null,
+ IOptions? scryptOptionsAccessor = null) : IPasswordHasher
+ {
+ private readonly ImprovedPasswordHasherOptions _options = optionsAccessor?.Value ?? new ImprovedPasswordHasherOptions();
+ private readonly ScryptOptions _scryptOptions = scryptOptionsAccessor?.Value ?? new ScryptOptions();
+
+ public string HashPassword(string password)
+ {
+ if (string.IsNullOrEmpty(password))
+ throw new ArgumentNullException(nameof(password), $"{nameof(password)} should not be null");
+
+ if (_scryptOptions.OpsLimit.HasValue && _scryptOptions.MemLimit.HasValue)
+ return PasswordHash.ScryptHashString(password, _scryptOptions.OpsLimit.Value, _scryptOptions.MemLimit.Value);
+
+ return _options.Strength switch
+ {
+ PasswordHasherStrength.Interactive => PasswordHash.ScryptHashString(password),
+ PasswordHasherStrength.Moderate => PasswordHash.ScryptHashString(password, PasswordHash.Strength.MediumSlow),
+ PasswordHasherStrength.Sensitive => PasswordHash.ScryptHashString(password, PasswordHash.Strength.Sensitive),
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ }
+
+ public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
+ {
+ if (string.IsNullOrEmpty(hashedPassword))
+ throw new ArgumentNullException(nameof(hashedPassword), $"{nameof(hashedPassword)} should not be null");
+
+ if (string.IsNullOrEmpty(providedPassword))
+ throw new ArgumentNullException(nameof(providedPassword), $"{nameof(providedPassword)} should not be null");
+
+ if (!PasswordHash.ScryptHashStringVerify(hashedPassword, providedPassword))
+ return PasswordVerificationResult.Failed;
+
+ return NeedsRehash(hashedPassword)
+ ? PasswordVerificationResult.SuccessRehashNeeded
+ : PasswordVerificationResult.Success;
+ }
+
+ private bool NeedsRehash(string hashedPassword)
+ {
+ // Scrypt hash format (libsodium/escrypt): $7$$
+ // The first char after "$7$" encodes N_log2 using itoa64
+ if (!hashedPassword.StartsWith("$7$") || hashedPassword.Length < 4)
+ return false;
+
+ var storedNLog2 = DecodeItoa64(hashedPassword[3]);
+ if (storedNLog2 < 0)
+ return false;
+
+ var expectedNLog2 = GetExpectedNLog2();
+ return storedNLog2 < expectedNLog2;
+ }
+
+ private int GetExpectedNLog2()
+ {
+ if (_scryptOptions.OpsLimit.HasValue && _scryptOptions.MemLimit.HasValue)
+ {
+ // N ≈ memlimit / (r * 128), with default r=8 → N = memlimit / 1024
+ var estimatedN = _scryptOptions.MemLimit.Value / 1024;
+ return estimatedN > 0 ? (int)Math.Log2(estimatedN) : 14;
+ }
+
+ return _options.Strength switch
+ {
+ PasswordHasherStrength.Interactive => 14, // N=2^14 (16 MiB)
+ PasswordHasherStrength.Moderate => 17, // N=2^17 (128 MiB, MediumSlow)
+ PasswordHasherStrength.Sensitive => 20, // N=2^20 (1 GiB)
+ _ => 20
+ };
+ }
+
+ ///
+ /// Decodes a character from the itoa64 encoding used by libsodium's scrypt.
+ /// Table: ./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
+ ///
+ private static int DecodeItoa64(char c) => c switch
+ {
+ '.' => 0,
+ '/' => 1,
+ >= '0' and <= '9' => c - '0' + 2,
+ >= 'A' and <= 'Z' => c - 'A' + 12,
+ >= 'a' and <= 'z' => c - 'a' + 38,
+ _ => -1
+ };
+ }
+}
diff --git a/src/Security/README.md b/src/Security/README.md
new file mode 100644
index 0000000..83c41b0
--- /dev/null
+++ b/src/Security/README.md
@@ -0,0 +1,146 @@
+# Codout.Security
+
+Biblioteca para hashing seguro de senhas com suporte a múltiplos algoritmos via injeção de dependência.
+
+## Pacotes
+
+| Pacote | Algoritmo | NuGet |
+|---|---|---|
+| `Codout.Security.Core` | Abstrações e interfaces | Dependência obrigatória |
+| `Codout.Security.Argon2` | Argon2id (via libsodium) | Recomendado |
+| `Codout.Security.Bcrypt` | BCrypt | Amplamente suportado |
+| `Codout.Security.Scrypt` | Scrypt (via libsodium) | Alternativa |
+
+## Instalação
+
+Instale o pacote do algoritmo desejado (o Core é incluído automaticamente):
+
+```bash
+dotnet add package Codout.Security.Argon2
+# ou
+dotnet add package Codout.Security.Bcrypt
+# ou
+dotnet add package Codout.Security.Scrypt
+```
+
+## Configuração
+
+### Argon2 (recomendado)
+
+```csharp
+using Codout.Security.Argon2;
+using Codout.Security.Core;
+
+services.UpgradePasswordSecurity()
+ .WithStrength(PasswordHasherStrength.Sensitive)
+ .UseArgon2();
+```
+
+Com parâmetros customizados:
+
+```csharp
+services.UpgradePasswordSecurity()
+ .WithStrength(PasswordHasherStrength.Sensitive)
+ .UseArgon2(options =>
+ {
+ options.OpsLimit = 4; // Custo computacional
+ options.MemLimit = 1073741824; // 1 GB de RAM
+ });
+```
+
+### BCrypt
+
+```csharp
+using Codout.Security.Bcrypt;
+using Codout.Security.Core;
+
+services.UpgradePasswordSecurity()
+ .UseBcrypt();
+```
+
+Com parâmetros customizados:
+
+```csharp
+services.UpgradePasswordSecurity()
+ .UseBcrypt(options =>
+ {
+ options.WorkFactor = 12; // Padrão: 12
+ options.SaltRevision = BcryptSaltRevision.Revision2B; // Padrão: 2B
+ });
+```
+
+### Scrypt
+
+```csharp
+using Codout.Security.Scrypt;
+using Codout.Security.Core;
+
+services.UpgradePasswordSecurity()
+ .WithStrength(PasswordHasherStrength.Sensitive)
+ .UseScrypt();
+```
+
+Com parâmetros customizados:
+
+```csharp
+services.UpgradePasswordSecurity()
+ .WithStrength(PasswordHasherStrength.Sensitive)
+ .UseScrypt(options =>
+ {
+ options.OpsLimit = 4194304;
+ options.MemLimit = 1073741824;
+ });
+```
+
+## Uso
+
+Injete `IPasswordHasher` no seu serviço:
+
+```csharp
+public class AccountService(IPasswordHasher passwordHasher)
+{
+ public string CreateHash(string password)
+ {
+ return passwordHasher.HashPassword(password);
+ }
+
+ public bool VerifyPassword(string hashedPassword, string providedPassword)
+ {
+ var result = passwordHasher.VerifyHashedPassword(hashedPassword, providedPassword);
+
+ switch (result)
+ {
+ case PasswordVerificationResult.Success:
+ return true;
+
+ case PasswordVerificationResult.SuccessRehashNeeded:
+ // Senha válida, mas o hash precisa ser atualizado
+ var newHash = passwordHasher.HashPassword(providedPassword);
+ // Persistir newHash no banco de dados
+ return true;
+
+ case PasswordVerificationResult.Failed:
+ default:
+ return false;
+ }
+ }
+}
+```
+
+## Strength
+
+O enum `PasswordHasherStrength` controla o custo computacional para Argon2 e Scrypt:
+
+| Nível | Uso de RAM | Indicação |
+|---|---|---|
+| `Interactive` | ~16 MB | Sessões interativas, login rápido |
+| `Moderate` | ~128 MB | Uso geral |
+| `Sensitive` | ~1 GB | Dados altamente sensíveis |
+
+> **Nota:** O `Strength` não se aplica ao BCrypt, que usa `WorkFactor` para controlar o custo.
+
+## SuccessRehashNeeded
+
+Os providers Argon2, BCrypt e Scrypt detectam automaticamente quando um hash existente foi gerado com parâmetros mais fracos que os atuais. Quando isso acontece, `VerifyHashedPassword` retorna `SuccessRehashNeeded` em vez de `Success`.
+
+Isso permite migrar hashes antigos de forma transparente — basta regerar o hash quando esse resultado for retornado.