backend phase 0: foundation, cross-cutting seams & starter cleanup
Remove the Order demo (entity/feature/repo/config/gRPC/proto) and the three pre-marketplace migrations; regenerate a fresh InitialBaseline migration. Stand up the REST surface (PingController + System/Ping CQRS) proving the Mediator -> behaviors -> OperationResult -> ApiResult envelope end to end. Close wiring gaps: register LoggingBehavior (outermost) and add the built-in rate limiter (per-IP global + otp/auth/sensitive policies), placed before authentication. Add current-user + audit plumbing: ICurrentUser (HttpContext + null impls), rename BaseEntity audit fields to CreatedAt/ModifiedAt (DateTimeOffset) + CreatedById/ModifiedById, stamped by a new AuditFieldInterceptor. Introduce five cross-cutting seams (IDateTimeProvider, IFieldEncryptor, ICacheService, IObjectStorage, INotificationDispatcher) with in-memory/local mocks registered via AddCrossCuttingSeams. Add Baya.Test.Foundation (encryptor, audit interceptor, ping handler) and update docs, contracts (swagger.v1.json), handoff, report, and mocks registry.
This commit is contained in:
+86
@@ -0,0 +1,86 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Baya.Application.Contracts.Common;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Baya.Infrastructure.CrossCutting.Seams;
|
||||
|
||||
/// <summary>
|
||||
/// Local symmetric-key implementation of <see cref="IFieldEncryptor"/> — AES-256-CBC with a random
|
||||
/// per-value IV (prepended to the ciphertext) for at-rest reversibility, and a keyed HMAC-SHA256 for
|
||||
/// deterministic lookup hashes. This is the mock seam: the real implementation swaps to a KMS / Key
|
||||
/// Vault provider behind the same interface. Plaintext is never logged.
|
||||
/// </summary>
|
||||
public sealed class SymmetricFieldEncryptor : IFieldEncryptor
|
||||
{
|
||||
private readonly byte[] _key;
|
||||
private readonly byte[] _hashKey;
|
||||
|
||||
public SymmetricFieldEncryptor(IOptions<SeamOptions> options)
|
||||
{
|
||||
var settings = options.Value.FieldEncryption;
|
||||
|
||||
// Derive a stable 32-byte AES key from whatever the operator configured (any length/format),
|
||||
// so a human-friendly secret still yields a valid key. SHA-256 of the configured material.
|
||||
_key = SHA256.HashData(Encoding.UTF8.GetBytes(Require(settings.Key, nameof(settings.Key))));
|
||||
|
||||
var hashMaterial = string.IsNullOrEmpty(settings.HashKey) ? settings.Key : settings.HashKey;
|
||||
_hashKey = SHA256.HashData(Encoding.UTF8.GetBytes(Require(hashMaterial, nameof(settings.HashKey))));
|
||||
}
|
||||
|
||||
public string Encrypt(string plaintext)
|
||||
{
|
||||
if (string.IsNullOrEmpty(plaintext))
|
||||
return plaintext;
|
||||
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = _key;
|
||||
aes.GenerateIV();
|
||||
|
||||
using var encryptor = aes.CreateEncryptor();
|
||||
var plainBytes = Encoding.UTF8.GetBytes(plaintext);
|
||||
var cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
|
||||
|
||||
var result = new byte[aes.IV.Length + cipherBytes.Length];
|
||||
Buffer.BlockCopy(aes.IV, 0, result, 0, aes.IV.Length);
|
||||
Buffer.BlockCopy(cipherBytes, 0, result, aes.IV.Length, cipherBytes.Length);
|
||||
|
||||
return Convert.ToBase64String(result);
|
||||
}
|
||||
|
||||
public string Decrypt(string ciphertext)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ciphertext))
|
||||
return ciphertext;
|
||||
|
||||
var cipherWithIv = Convert.FromBase64String(ciphertext);
|
||||
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = _key;
|
||||
|
||||
var ivLength = aes.BlockSize / 8;
|
||||
var iv = new byte[ivLength];
|
||||
Buffer.BlockCopy(cipherWithIv, 0, iv, 0, ivLength);
|
||||
aes.IV = iv;
|
||||
|
||||
using var decryptor = aes.CreateDecryptor();
|
||||
var cipherBytes = decryptor.TransformFinalBlock(cipherWithIv, ivLength, cipherWithIv.Length - ivLength);
|
||||
|
||||
return Encoding.UTF8.GetString(cipherBytes);
|
||||
}
|
||||
|
||||
public string Hash(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return value;
|
||||
|
||||
using var hmac = new HMACSHA256(_hashKey);
|
||||
var hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(value));
|
||||
return Convert.ToHexString(hashBytes);
|
||||
}
|
||||
|
||||
private static string Require(string value, string name) =>
|
||||
string.IsNullOrWhiteSpace(value)
|
||||
? throw new InvalidOperationException($"Seams:FieldEncryption:{name} must be configured.")
|
||||
: value;
|
||||
}
|
||||
Reference in New Issue
Block a user