feat(1099): add ITinProtector with Data Protection encryption + last-4 helper
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.11" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using ROLAC.API.Services.Security;
|
||||
using Xunit;
|
||||
|
||||
namespace ROLAC.API.Tests.Services;
|
||||
|
||||
public class TinProtectorTests
|
||||
{
|
||||
private static TinProtector Build() =>
|
||||
new TinProtector(DataProtectionProvider.Create("ROLAC.Tests"));
|
||||
|
||||
[Fact]
|
||||
public void Protect_then_Unprotect_round_trips()
|
||||
{
|
||||
var p = Build();
|
||||
var cipher = p.Protect("123-45-6789");
|
||||
Assert.NotEqual("123-45-6789", cipher);
|
||||
Assert.Equal("123-45-6789", p.Unprotect(cipher));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("123-45-6789", "6789")]
|
||||
[InlineData("12-3456789", "6789")]
|
||||
[InlineData("7", "7")]
|
||||
public void Last4_keeps_only_trailing_digits(string raw, string expected)
|
||||
=> Assert.Equal(expected, TinProtector.Last4(raw));
|
||||
|
||||
[Fact]
|
||||
public void Last4_of_null_is_null() => Assert.Null(TinProtector.Last4(null));
|
||||
}
|
||||
@@ -15,6 +15,7 @@ using ROLAC.API.Json;
|
||||
using ROLAC.API.Middleware;
|
||||
using ROLAC.API.Services;
|
||||
using ROLAC.API.Services.Logging;
|
||||
using ROLAC.API.Services.Security;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var config = builder.Configuration;
|
||||
@@ -157,6 +158,8 @@ builder.Services.AddScoped<IExpenseSnapshotService, ExpenseSnapshotService>();
|
||||
builder.Services.AddScoped<IMonthlyStatementService, MonthlyStatementService>();
|
||||
builder.Services.AddScoped<IFinanceDashboardService, FinanceDashboardService>();
|
||||
builder.Services.AddScoped<IForm990ReportService, Form990ReportService>();
|
||||
builder.Services.AddDataProtection();
|
||||
builder.Services.AddScoped<ITinProtector, TinProtector>();
|
||||
builder.Services.AddScoped<IChurchProfileService, ChurchProfileService>();
|
||||
builder.Services.AddScoped<ISettingsService, SettingsService>();
|
||||
builder.Services.AddScoped<IDisbursementService, DisbursementService>();
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ROLAC.API.Services.Security;
|
||||
|
||||
/// <summary>Reversible protection for taxpayer identification numbers (SSN/EIN).</summary>
|
||||
public interface ITinProtector
|
||||
{
|
||||
string Protect(string plaintext);
|
||||
string Unprotect(string ciphertext);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
|
||||
namespace ROLAC.API.Services.Security;
|
||||
|
||||
public class TinProtector : ITinProtector
|
||||
{
|
||||
private readonly IDataProtector _protector;
|
||||
|
||||
public TinProtector(IDataProtectionProvider provider)
|
||||
=> _protector = provider.CreateProtector("Payee1099.Tin");
|
||||
|
||||
public string Protect(string plaintext) => _protector.Protect(plaintext);
|
||||
public string Unprotect(string ciphertext) => _protector.Unprotect(ciphertext);
|
||||
|
||||
/// <summary>Last four digits of a TIN (ignoring dashes/spaces); null/empty in => null.</summary>
|
||||
public static string? Last4(string? raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw)) return null;
|
||||
var digits = new string(raw.Where(char.IsDigit).ToArray());
|
||||
return digits.Length <= 4 ? digits : digits[^4..];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user