diff --git a/API/ROLAC.API.Tests/ROLAC.API.Tests.csproj b/API/ROLAC.API.Tests/ROLAC.API.Tests.csproj
index ffa3eaa..59db6a3 100644
--- a/API/ROLAC.API.Tests/ROLAC.API.Tests.csproj
+++ b/API/ROLAC.API.Tests/ROLAC.API.Tests.csproj
@@ -22,6 +22,7 @@
+
diff --git a/API/ROLAC.API.Tests/Services/TinProtectorTests.cs b/API/ROLAC.API.Tests/Services/TinProtectorTests.cs
new file mode 100644
index 0000000..44ba817
--- /dev/null
+++ b/API/ROLAC.API.Tests/Services/TinProtectorTests.cs
@@ -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));
+}
diff --git a/API/ROLAC.API/Program.cs b/API/ROLAC.API/Program.cs
index 8f701c3..4e2bbf5 100644
--- a/API/ROLAC.API/Program.cs
+++ b/API/ROLAC.API/Program.cs
@@ -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();
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
+builder.Services.AddDataProtection();
+builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
diff --git a/API/ROLAC.API/Services/Security/ITinProtector.cs b/API/ROLAC.API/Services/Security/ITinProtector.cs
new file mode 100644
index 0000000..2996bd3
--- /dev/null
+++ b/API/ROLAC.API/Services/Security/ITinProtector.cs
@@ -0,0 +1,8 @@
+namespace ROLAC.API.Services.Security;
+
+/// Reversible protection for taxpayer identification numbers (SSN/EIN).
+public interface ITinProtector
+{
+ string Protect(string plaintext);
+ string Unprotect(string ciphertext);
+}
diff --git a/API/ROLAC.API/Services/Security/TinProtector.cs b/API/ROLAC.API/Services/Security/TinProtector.cs
new file mode 100644
index 0000000..f29a399
--- /dev/null
+++ b/API/ROLAC.API/Services/Security/TinProtector.cs
@@ -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);
+
+ /// Last four digits of a TIN (ignoring dashes/spaces); null/empty in => null.
+ 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..];
+ }
+}