This commit is contained in:
masoodafar-web
2025-11-12 23:17:56 +03:30
parent 2fac0f4922
commit 0adb0713f3
7 changed files with 3205 additions and 2812 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
namespace CMSMicroservice.Application.Common.Interfaces;
public interface IHashService
{
// Computes a deterministic SHA-256 hex string for the given input (legacy helper)
string ComputeSha256Hex(string input);
// Compares plain input with a stored hex hash (case-insensitive) (legacy helper)
bool VerifySha256Hex(string input, string? expectedHexHash);
// Computes HMAC-SHA256 hex using a provided secret (for OTP and similar)
string ComputeHmacSha256Hex(string input, string secret);
// Verifies input against expected HMAC-SHA256 hex using the secret
bool VerifyHmacSha256Hex(string input, string? expectedHexHash, string secret);
// Creates a salted password hash using PBKDF2; returns an encoded string containing algorithm params and salt
string HashPassword(string password);
// Verifies a password against the stored hash; supports both PBKDF2 format and legacy SHA-256 hex
bool VerifyPassword(string password, string? storedHash);
}

View File

@@ -7,11 +7,13 @@ public class CreateNewOtpTokenCommandHandler : IRequestHandler<CreateNewOtpToken
{
private readonly IApplicationDbContext _context;
private readonly IConfiguration _cfg;
private readonly IHashService _hashService;
public CreateNewOtpTokenCommandHandler(IApplicationDbContext context, IConfiguration cfg)
public CreateNewOtpTokenCommandHandler(IApplicationDbContext context, IConfiguration cfg, IHashService hashService)
{
_context = context;
_cfg = cfg;
_hashService = hashService;
}
const int CodeLength = 6;
@@ -41,7 +43,8 @@ public class CreateNewOtpTokenCommandHandler : IRequestHandler<CreateNewOtpToken
// تولید کد
var code = GenerateNumericCode(CodeLength);
var codeHash = Hash(code);
var secret = _cfg["Otp:Secret"] ?? throw new InvalidOperationException("Otp:Secret not set");
var codeHash = _hashService.ComputeHmacSha256Hex(code, secret);
var entity = new OtpToken
{
@@ -75,13 +78,4 @@ public class CreateNewOtpTokenCommandHandler : IRequestHandler<CreateNewOtpToken
foreach (var b in bytes) sb.Append((b % 10).ToString());
return sb.ToString();
}
private string Hash(string code)
{
// HMAC با secret اپ (نیازی به ذخیره salt جدا نیست)
var secret = _cfg["Otp:Secret"] ?? throw new InvalidOperationException("Otp:Secret not set");
using var h = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = h.ComputeHash(Encoding.UTF8.GetBytes(code));
return Convert.ToHexString(hash);
}
}

View File

@@ -1,17 +1,17 @@
using CMSMicroservice.Domain.Events;
using Microsoft.Extensions.Configuration;
using System.Security.Cryptography;
using System.Text;
namespace CMSMicroservice.Application.OtpTokenCQ.Commands.VerifyOtpToken;
public class VerifyOtpTokenCommandHandler : IRequestHandler<VerifyOtpTokenCommand, VerifyOtpTokenResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly IConfiguration _cfg;
private readonly IHashService _hashService;
public VerifyOtpTokenCommandHandler(IApplicationDbContext context, IConfiguration cfg)
public VerifyOtpTokenCommandHandler(IApplicationDbContext context, IConfiguration cfg, IHashService hashService)
{
_context = context;
_cfg = cfg;
_hashService = hashService;
}
const int MaxAttempts = 5; // محدودیت تلاش
@@ -21,7 +21,6 @@ public class VerifyOtpTokenCommandHandler : IRequestHandler<VerifyOtpTokenComman
var purpose = request.Purpose?.ToLowerInvariant() ?? "signup";
var now = DateTime.Now;
var otp = await _context.OtpTokens
.Where(o => o.Mobile == mobile && o.Purpose == purpose && !o.IsUsed && o.ExpiresAt > now)
.OrderByDescending(o => o.Created)
@@ -33,7 +32,8 @@ public class VerifyOtpTokenCommandHandler : IRequestHandler<VerifyOtpTokenComman
otp.Attempts++;
if (!VerifyHash(request.Code, otp.CodeHash))
var secret = _cfg["Otp:Secret"] ?? throw new InvalidOperationException("Otp:Secret not set");
if (!_hashService.VerifyHmacSha256Hex(request.Code, otp.CodeHash, secret))
{
await _context.SaveChangesAsync(cancellationToken);
return new VerifyOtpTokenResponseDto() { Success = false, Message = "کد نادرست است." };
@@ -96,15 +96,4 @@ public class VerifyOtpTokenCommandHandler : IRequestHandler<VerifyOtpTokenComman
RemainingSeconds = (otp.ExpiresAt - now).Seconds
};
}
private string Hash(string code)
{
// HMAC با secret اپ (نیازی به ذخیره salt جدا نیست)
var secret = _cfg["Otp:Secret"] ?? throw new InvalidOperationException("Otp:Secret not set");
using var h = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = h.ComputeHash(Encoding.UTF8.GetBytes(code));
return Convert.ToHexString(hash);
}
private bool VerifyHash(string code, string hexHash) => Hash(code).Equals(hexHash, StringComparison.OrdinalIgnoreCase);
}

View File

@@ -2,15 +2,33 @@ namespace CMSMicroservice.Application.UserCQ.Queries.AdminGetJwtToken;
public class AdminGetJwtTokenQueryHandler : IRequestHandler<AdminGetJwtTokenQuery, AdminGetJwtTokenResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly IGenerateJwtToken _generateJwt;
private readonly IHashService _hashService;
public AdminGetJwtTokenQueryHandler(IApplicationDbContext context)
public AdminGetJwtTokenQueryHandler(IApplicationDbContext context, IGenerateJwtToken generateJwt, IHashService hashService)
{
_context = context;
_generateJwt = generateJwt;
_hashService = hashService;
}
public async Task<AdminGetJwtTokenResponseDto> Handle(AdminGetJwtTokenQuery request, CancellationToken cancellationToken)
{
//TODO: Implement your business logic
return new AdminGetJwtTokenResponseDto();
var user = await _context.Users
.Include(u => u.UserRoles)
.ThenInclude(ur => ur.Role)
.FirstOrDefaultAsync(f => f.Mobile == request.Username, cancellationToken);
if (user == null)
throw new Exception("Invalid username or password.");
// verify password (supports PBKDF2 or legacy SHA-256)
if (!_hashService.VerifyPassword(request.Password, user.HashPassword))
throw new Exception("Invalid username or password.");
return new AdminGetJwtTokenResponseDto()
{
Token = await _generateJwt.GenerateJwtToken(user),
};
}
}

View File

@@ -19,6 +19,7 @@ public static class ConfigureServices
services.AddScoped<AuditableEntitySaveChangesInterceptor>();
services.AddScoped<ApplicationDbContextInitialiser>();
services.AddScoped<IGenerateJwtToken, GenerateJwtTokenService>();
services.AddScoped<IHashService, HashService>();
services.AddScoped<IApplicationDbContext>(p => p.GetRequiredService<ApplicationDbContext>());
if (configuration.GetValue<bool>("UseInMemoryDatabase"))
{

View File

@@ -0,0 +1,88 @@
using CMSMicroservice.Application.Common.Interfaces;
using System.Security.Cryptography;
using System.Text;
namespace CMSMicroservice.Infrastructure.Services;
public class HashService : IHashService
{
// PBKDF2 parameters
private const int SaltSize = 16; // 128-bit
private const int KeySize = 32; // 256-bit
private const int Iterations = 100_000; // strong enough default
private const string Pbkdf2Prefix = "PBKDF2"; // version marker
public string ComputeSha256Hex(string input)
{
using var sha = SHA256.Create();
var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(bytes);
}
public bool VerifySha256Hex(string input, string? expectedHexHash)
{
if (string.IsNullOrEmpty(expectedHexHash)) return false;
var actual = ComputeSha256Hex(input);
return actual.Equals(expectedHexHash, StringComparison.OrdinalIgnoreCase);
}
public string ComputeHmacSha256Hex(string input, string secret)
{
using var h = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = h.ComputeHash(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(hash);
}
public bool VerifyHmacSha256Hex(string input, string? expectedHexHash, string secret)
{
if (string.IsNullOrEmpty(expectedHexHash)) return false;
var actual = ComputeHmacSha256Hex(input, secret);
return actual.Equals(expectedHexHash, StringComparison.OrdinalIgnoreCase);
}
public string HashPassword(string password)
{
// generate salt
Span<byte> salt = stackalloc byte[SaltSize];
RandomNumberGenerator.Fill(salt);
// derive key
var pbkdf2 = new Rfc2898DeriveBytes(password, salt.ToArray(), Iterations, HashAlgorithmName.SHA256);
var derived = pbkdf2.GetBytes(KeySize);
// encode: PBKDF2$<iter>$<saltBase64>$<hashBase64>
return string.Join('$', Pbkdf2Prefix, Iterations.ToString(), Convert.ToBase64String(salt.ToArray()), Convert.ToBase64String(derived));
}
public bool VerifyPassword(string password, string? storedHash)
{
if (string.IsNullOrWhiteSpace(storedHash)) return false;
// legacy: raw SHA-256 hex
if (!storedHash.Contains('$'))
{
return VerifySha256Hex(password, storedHash);
}
var parts = storedHash.Split('$');
if (parts.Length != 4 || !string.Equals(parts[0], Pbkdf2Prefix, StringComparison.Ordinal))
return false;
if (!int.TryParse(parts[1], out var iterations) || iterations <= 0) return false;
byte[] salt;
byte[] expectedKey;
try
{
salt = Convert.FromBase64String(parts[2]);
expectedKey = Convert.FromBase64String(parts[3]);
}
catch
{
return false;
}
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA256);
var actualKey = pbkdf2.GetBytes(expectedKey.Length);
// fixed-time comparison
return CryptographicOperations.FixedTimeEquals(actualKey, expectedKey);
}
}