Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions Codout.Framework.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
8 changes: 5 additions & 3 deletions Codout.Mailer.Razor/Configuration/ConfigureServices.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using Codout.Mailer.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
Expand All @@ -8,8 +8,8 @@ namespace Codout.Mailer.Razor.Configuration;
public static class ConfigureServices
{
/// <summary>
/// Registra o template engine Razor nativo do ASP.NET Core para renderização de templates de e-mail.
/// Deve ser chamado após <c>AddMailer()</c>.
/// Registra o template engine Razor nativo do ASP.NET Core para renderização de templates de e-mail.
/// Deve ser chamado após <c>AddMailer()</c>.
/// </summary>
public static IServiceCollection AddMailerRazor(
this IServiceCollection services,
Expand All @@ -32,3 +32,5 @@ public static IServiceCollection AddMailerRazor(
return services;
}
}


8 changes: 4 additions & 4 deletions Codout.Mailer.Razor/Configuration/RazorMailerOptions.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
using System;
using System;
using System.Reflection;

namespace Codout.Mailer.Razor.Configuration;

/// <summary>
/// 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
/// </summary>
public class RazorMailerOptions
{
/// <summary>
/// Assembly que contém os templates Razor embarcados como recursos
/// Assembly que contém os templates Razor embarcados como recursos
/// </summary>
public Assembly TemplateAssembly { get; set; }

Expand All @@ -19,7 +19,7 @@ public class RazorMailerOptions
public string RootNamespace { get; set; }

/// <summary>
/// Habilita o cache de templates compilados em memória
/// Habilita o cache de templates compilados em memória
/// </summary>
public bool EnableCache { get; set; } = true;

Expand Down
4 changes: 2 additions & 2 deletions Codout.Mailer.Razor/RazorViewTemplateEngine.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.Threading.Tasks;
using Codout.Mailer.Interfaces;
Expand Down Expand Up @@ -33,7 +33,7 @@ public async Task<string> RenderAsync<T>(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();
Expand Down
20 changes: 20 additions & 0 deletions src/Security/Codout.Security.Argon2/Argon2Extensions.cs
Original file line number Diff line number Diff line change
@@ -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<Argon2Options>? configure = null)
{
builder.Services.Configure<ImprovedPasswordHasherOptions>(options =>
{
options.Strength = builder.Options.Strength;
});

if (configure != null)
builder.Services.Configure(configure);

return builder.Services.AddScoped<IPasswordHasher, ArgonPasswordHash>();
}
}
17 changes: 17 additions & 0 deletions src/Security/Codout.Security.Argon2/Argon2Options.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Codout.Security.Argon2;

public class Argon2Options
{
/// <summary>
/// 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.
/// </summary>
public long? OpsLimit { get; set; }

/// <summary>
/// memlimit is the maximum amount of RAM that the function will use, in bytes.
/// When set along with OpsLimit, overrides the Strength setting.
/// </summary>
public int? MemLimit { get; set; }
}
89 changes: 89 additions & 0 deletions src/Security/Codout.Security.Argon2/ArgonPasswordHash.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using Codout.Security.Core;
using Microsoft.Extensions.Options;
using Sodium;

namespace Codout.Security.Argon2
{
public class ArgonPasswordHash(
IOptions<ImprovedPasswordHasherOptions>? optionsAccessor = null,
IOptions<Argon2Options>? 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)
};
}
}
}
18 changes: 18 additions & 0 deletions src/Security/Codout.Security.Argon2/Codout.Security.Argon2.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.3" />
<PackageReference Include="Sodium.Core" Version="1.4.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Codout.Security.Core\Codout.Security.Core.csproj" />
</ItemGroup>

</Project>
20 changes: 20 additions & 0 deletions src/Security/Codout.Security.Bcrypt/BcryptExtensions.cs
Original file line number Diff line number Diff line change
@@ -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<BcryptOptions>? configure = null)
{
builder.Services.Configure<ImprovedPasswordHasherOptions>(options =>
{
options.Strength = builder.Options.Strength;
});

if (configure != null)
builder.Services.Configure(configure);

return builder.Services.AddScoped<IPasswordHasher, BcryptPasswordHash>();
}
}
16 changes: 16 additions & 0 deletions src/Security/Codout.Security.Bcrypt/BcryptOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Codout.Security.Bcrypt;

public class BcryptOptions
{
/// <summary>
/// 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.
/// </summary>
public int WorkFactor { get; set; } = 12;

/// <summary>
/// The salt revision to use for BCrypt hashing.
/// </summary>
public BcryptSaltRevision SaltRevision { get; set; } = BcryptSaltRevision.Revision2B;
}
45 changes: 45 additions & 0 deletions src/Security/Codout.Security.Bcrypt/BcryptPasswordHash.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Codout.Security.Core;
using Microsoft.Extensions.Options;

namespace Codout.Security.Bcrypt
{
public class BcryptPasswordHash(IOptions<BcryptOptions>? 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))
};
}
}
10 changes: 10 additions & 0 deletions src/Security/Codout.Security.Bcrypt/BcryptSaltRevision.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Codout.Security.Bcrypt;

public enum BcryptSaltRevision
{
Revision2,
Revision2A,
Revision2B,
Revision2X,
Revision2Y
}
19 changes: 19 additions & 0 deletions src/Security/Codout.Security.Bcrypt/Codout.Security.Bcrypt.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Codout.Security.Core\Codout.Security.Core.csproj" />
</ItemGroup>

</Project>
13 changes: 13 additions & 0 deletions src/Security/Codout.Security.Core/Codout.Security.Core.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
</ItemGroup>

</Project>
Loading
Loading