refactor: UI file upload components and ephemeral RSA key service

Replace InputFile with RadzenUpload in filter panels for better UX,
switch to ephemeral RSA keys (safe for transport-only encryption),
and add test scripts and documentation files.
This commit is contained in:
Joseph Doherty
2026-01-28 17:22:30 -05:00
parent 5dd17cbab8
commit 04383d672c
32 changed files with 9901 additions and 280 deletions
@@ -0,0 +1,101 @@
using System.Security.Cryptography;
using System.Text;
using JdeScoping.Infrastructure.Security;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Shouldly;
namespace JdeScoping.Infrastructure.Tests.Security;
public class EphemeralRsaKeyServiceTests : IDisposable
{
private readonly ILogger<EphemeralRsaKeyService> _logger;
private readonly EphemeralRsaKeyService _service;
public EphemeralRsaKeyServiceTests()
{
_logger = Substitute.For<ILogger<EphemeralRsaKeyService>>();
_service = new EphemeralRsaKeyService(_logger);
}
[Fact]
public void Constructor_GeneratesNewKeyPair()
{
// Assert - service was created with a key that works
var publicKey = _service.GetPublicKeyPem();
publicKey.ShouldStartWith("-----BEGIN PUBLIC KEY-----");
publicKey.ShouldEndWith("-----END PUBLIC KEY-----");
}
[Fact]
public void GetPublicKeyPem_ReturnsValidPemFormat()
{
// Act
var publicKey = _service.GetPublicKeyPem();
// Assert - key can be imported
using var rsa = RSA.Create();
rsa.ImportFromPem(publicKey);
rsa.KeySize.ShouldBe(2048);
}
[Fact]
public void Decrypt_WithValidCiphertext_ReturnsPlaintext()
{
// Arrange
var plaintext = "Hello, World!"u8.ToArray();
var publicKeyPem = _service.GetPublicKeyPem();
// Encrypt with public key
using var rsa = RSA.Create();
rsa.ImportFromPem(publicKeyPem);
var ciphertext = rsa.Encrypt(plaintext, RSAEncryptionPadding.OaepSHA256);
// Act
var decrypted = _service.Decrypt(ciphertext);
// Assert
Encoding.UTF8.GetString(decrypted).ShouldBe("Hello, World!");
}
[Fact]
public void Decrypt_WithInvalidCiphertext_ThrowsCryptographicException()
{
// Arrange
var invalidCiphertext = new byte[256]; // Random bytes won't decrypt
// Act & Assert
Should.Throw<CryptographicException>(() => _service.Decrypt(invalidCiphertext));
}
[Fact]
public void MultipleInstances_HaveDifferentKeys()
{
// Arrange
using var service2 = new EphemeralRsaKeyService(_logger);
// Act
var key1 = _service.GetPublicKeyPem();
var key2 = service2.GetPublicKeyPem();
// Assert - each instance has its own unique key
key1.ShouldNotBe(key2);
}
[Fact]
public void Dispose_PreventsSubsequentOperations()
{
// Arrange
var service = new EphemeralRsaKeyService(_logger);
service.Dispose();
// Act & Assert
Should.Throw<ObjectDisposedException>(() => service.GetPublicKeyPem());
Should.Throw<ObjectDisposedException>(() => service.Decrypt(new byte[1]));
}
public void Dispose()
{
_service.Dispose();
}
}
@@ -1,153 +0,0 @@
using JdeScoping.Infrastructure.Security;
using JdeScoping.Infrastructure.Tests.Helpers;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
using System.Security.Cryptography;
namespace JdeScoping.Infrastructure.Tests.Security;
public class SecureStoreRsaKeyServiceTests : IDisposable
{
private readonly InMemorySecureStore _secureStore;
public SecureStoreRsaKeyServiceTests()
{
_secureStore = new InMemorySecureStore();
}
public void Dispose()
{
_secureStore.Dispose();
}
private SecureStoreRsaKeyService CreateService()
{
return new SecureStoreRsaKeyService(
_secureStore,
NullLogger<SecureStoreRsaKeyService>.Instance);
}
[Fact]
public void Constructor_WhenNoKeyInStore_GeneratesNewKeyAndStoresIt()
{
// Arrange - ensure no key exists
_secureStore.Contains(SecureStoreRsaKeyService.RsaPrivateKeyName).ShouldBeFalse();
// Act
using var service = CreateService();
// Assert - key should now be stored
_secureStore.Contains(SecureStoreRsaKeyService.RsaPrivateKeyName).ShouldBeTrue();
var storedKey = _secureStore.Get(SecureStoreRsaKeyService.RsaPrivateKeyName);
storedKey.ShouldStartWith("-----BEGIN RSA PRIVATE KEY-----");
}
[Fact]
public void Constructor_WhenKeyExistsInStore_LoadsExistingKey()
{
// Arrange - pre-store a key
using var originalRsa = RSA.Create(2048);
var originalPem = originalRsa.ExportRSAPrivateKeyPem();
var originalPublicKey = originalRsa.ExportSubjectPublicKeyInfoPem();
_secureStore.Set(SecureStoreRsaKeyService.RsaPrivateKeyName, originalPem);
// Act
using var service = CreateService();
// Assert - should load the same key
var loadedPublicKey = service.GetPublicKeyPem();
loadedPublicKey.ShouldBe(originalPublicKey);
}
[Fact]
public void GetPublicKeyPem_ReturnsValidPemFormat()
{
// Arrange
using var service = CreateService();
// Act
var pem = service.GetPublicKeyPem();
// Assert
pem.ShouldStartWith("-----BEGIN PUBLIC KEY-----");
pem.ShouldEndWith("-----END PUBLIC KEY-----");
}
[Fact]
public void Decrypt_WithValidCiphertext_ReturnsPlaintext()
{
// Arrange
using var service = CreateService();
var plaintext = "Hello, World!"u8.ToArray();
// Encrypt using public key (simulating what client does)
using var rsa = RSA.Create();
rsa.ImportFromPem(service.GetPublicKeyPem());
var ciphertext = rsa.Encrypt(plaintext, RSAEncryptionPadding.OaepSHA256);
// Act
var decrypted = service.Decrypt(ciphertext);
// Assert
decrypted.ShouldBe(plaintext);
}
[Fact]
public void Decrypt_WithInvalidCiphertext_ThrowsCryptographicException()
{
// Arrange
using var service = CreateService();
var invalidCiphertext = new byte[] { 1, 2, 3, 4, 5 };
// Act & Assert
Should.Throw<CryptographicException>(() => service.Decrypt(invalidCiphertext));
}
[Fact]
public void MultipleInstances_WithSameStore_UseSameKey()
{
// Arrange & Act
using var service1 = CreateService();
var publicKey1 = service1.GetPublicKeyPem();
// Create second instance with same store
using var service2 = new SecureStoreRsaKeyService(
_secureStore,
NullLogger<SecureStoreRsaKeyService>.Instance);
var publicKey2 = service2.GetPublicKeyPem();
// Assert
publicKey2.ShouldBe(publicKey1);
}
[Fact]
public void EncryptDecrypt_RoundTrip_Succeeds()
{
// Arrange
using var service = CreateService();
var originalMessage = "Sensitive password data 123!"u8.ToArray();
// Act - encrypt with public key
using var clientRsa = RSA.Create();
clientRsa.ImportFromPem(service.GetPublicKeyPem());
var encrypted = clientRsa.Encrypt(originalMessage, RSAEncryptionPadding.OaepSHA256);
// Act - decrypt with service (which has private key)
var decrypted = service.Decrypt(encrypted);
// Assert
decrypted.ShouldBe(originalMessage);
}
[Fact]
public void Operations_AfterDispose_ThrowObjectDisposedException()
{
// Arrange
var service = CreateService();
service.Dispose();
// Act & Assert
Should.Throw<ObjectDisposedException>(() => service.GetPublicKeyPem());
Should.Throw<ObjectDisposedException>(() => service.Decrypt(Array.Empty<byte>()));
}
}