From 97ef3e4345e663627cfbbace8060afebab122301 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 3 Jan 2026 08:16:13 -0500 Subject: [PATCH] feat(infrastructure): implement RsaKeyService with tests --- .../Security/RsaKeyService.cs | 50 ++++++++++ .../Security/RsaKeyServiceTests.cs | 96 +++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 NEW/src/JdeScoping.Infrastructure/Security/RsaKeyService.cs create mode 100644 NEW/tests/JdeScoping.Infrastructure.Tests/Security/RsaKeyServiceTests.cs diff --git a/NEW/src/JdeScoping.Infrastructure/Security/RsaKeyService.cs b/NEW/src/JdeScoping.Infrastructure/Security/RsaKeyService.cs new file mode 100644 index 0000000..609550e --- /dev/null +++ b/NEW/src/JdeScoping.Infrastructure/Security/RsaKeyService.cs @@ -0,0 +1,50 @@ +// NEW/src/JdeScoping.Infrastructure/Security/RsaKeyService.cs +using System.Security.Cryptography; +using JdeScoping.Core.Interfaces; + +namespace JdeScoping.Infrastructure.Security; + +/// +/// RSA key service that auto-generates and persists keys. +/// +public class RsaKeyService : IRsaKeyService, IDisposable +{ + private readonly RSA _rsa; + + /// + /// Creates a new RSA key service. + /// + /// Path to persist the private key + public RsaKeyService(string keyFilePath) + { + _rsa = RSA.Create(2048); + + if (File.Exists(keyFilePath)) + { + var keyBytes = File.ReadAllBytes(keyFilePath); + _rsa.ImportRSAPrivateKey(keyBytes, out _); + } + else + { + var privateKey = _rsa.ExportRSAPrivateKey(); + var directory = Path.GetDirectoryName(keyFilePath); + if (!string.IsNullOrEmpty(directory)) + Directory.CreateDirectory(directory); + File.WriteAllBytes(keyFilePath, privateKey); + } + } + + /// + public string GetPublicKeyPem() + => _rsa.ExportSubjectPublicKeyInfoPem(); + + /// + public byte[] Decrypt(byte[] ciphertext) + => _rsa.Decrypt(ciphertext, RSAEncryptionPadding.OaepSHA256); + + public void Dispose() + { + _rsa.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/NEW/tests/JdeScoping.Infrastructure.Tests/Security/RsaKeyServiceTests.cs b/NEW/tests/JdeScoping.Infrastructure.Tests/Security/RsaKeyServiceTests.cs new file mode 100644 index 0000000..9512a34 --- /dev/null +++ b/NEW/tests/JdeScoping.Infrastructure.Tests/Security/RsaKeyServiceTests.cs @@ -0,0 +1,96 @@ +// NEW/tests/JdeScoping.Infrastructure.Tests/Security/RsaKeyServiceTests.cs +using JdeScoping.Core.Interfaces; +using JdeScoping.Infrastructure.Security; +using Shouldly; + +namespace JdeScoping.Infrastructure.Tests.Security; + +public class RsaKeyServiceTests : IDisposable +{ + private readonly string _testKeyPath; + + public RsaKeyServiceTests() + { + _testKeyPath = Path.Combine(Path.GetTempPath(), $"test-rsa-key-{Guid.NewGuid()}.bin"); + } + + public void Dispose() + { + if (File.Exists(_testKeyPath)) + File.Delete(_testKeyPath); + } + + [Fact] + public void GetPublicKeyPem_ReturnsValidPemFormat() + { + // Arrange + var service = new RsaKeyService(_testKeyPath); + + // Act + var pem = service.GetPublicKeyPem(); + + // Assert + pem.ShouldStartWith("-----BEGIN PUBLIC KEY-----"); + pem.ShouldEndWith("-----END PUBLIC KEY-----"); + } + + [Fact] + public void Constructor_WhenKeyFileMissing_GeneratesAndPersistsKey() + { + // Arrange - ensure file doesn't exist + File.Exists(_testKeyPath).ShouldBeFalse(); + + // Act + _ = new RsaKeyService(_testKeyPath); + + // Assert + File.Exists(_testKeyPath).ShouldBeTrue(); + new FileInfo(_testKeyPath).Length.ShouldBeGreaterThan(0); + } + + [Fact] + public void Constructor_WhenKeyFileExists_LoadsSameKey() + { + // Arrange - create service to generate key + var service1 = new RsaKeyService(_testKeyPath); + var publicKey1 = service1.GetPublicKeyPem(); + + // Act - create new service instance + var service2 = new RsaKeyService(_testKeyPath); + var publicKey2 = service2.GetPublicKeyPem(); + + // Assert - same key loaded + publicKey2.ShouldBe(publicKey1); + } + + [Fact] + public void Decrypt_WithValidCiphertext_ReturnsPlaintext() + { + // Arrange + var service = new RsaKeyService(_testKeyPath); + var plaintext = "Hello, World!"u8.ToArray(); + + // Encrypt using public key (simulating what client does) + using var rsa = System.Security.Cryptography.RSA.Create(); + rsa.ImportFromPem(service.GetPublicKeyPem()); + var ciphertext = rsa.Encrypt(plaintext, System.Security.Cryptography.RSAEncryptionPadding.OaepSHA256); + + // Act + var decrypted = service.Decrypt(ciphertext); + + // Assert + decrypted.ShouldBe(plaintext); + } + + [Fact] + public void Decrypt_WithInvalidCiphertext_ThrowsCryptographicException() + { + // Arrange + var service = new RsaKeyService(_testKeyPath); + var invalidCiphertext = new byte[] { 1, 2, 3, 4, 5 }; + + // Act & Assert + Should.Throw( + () => service.Decrypt(invalidCiphertext)); + } +}