mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 08:36:11 +02:00
feat: Implement message encryption and decryption support
- Added IMessageEncryptionService and its implementation MessageEncryptionService for handling message encryption. - Updated ChannelsController and ChatService to encrypt messages before storing and sending. - Introduced encryption key retrieval endpoint in ServerController. - Modified EchoHubDbContext to accommodate increased message content and embed JSON lengths for encrypted data. - Created migrations to support encryption-related database changes. - Enhanced FirstRunSetup to ensure encryption key is generated if not present. - Updated appsettings.example.json to include encryption configuration. - Added comprehensive unit tests for encryption service and compatibility tests between client and server encryption.
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
using System.Security.Cryptography;
|
||||
using EchoHub.Client.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace EchoHub.Tests;
|
||||
|
||||
public class ClientEncryptionServiceTests
|
||||
{
|
||||
private static string GenerateKey() =>
|
||||
Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
|
||||
|
||||
private static ClientEncryptionService CreateInitialized(string? key = null)
|
||||
{
|
||||
var service = new ClientEncryptionService();
|
||||
service.SetKey(key ?? GenerateKey());
|
||||
return service;
|
||||
}
|
||||
|
||||
// ── Initialization ───────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void IsInitialized_DefaultFalse()
|
||||
{
|
||||
var service = new ClientEncryptionService();
|
||||
Assert.False(service.IsInitialized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsInitialized_TrueAfterSetKey()
|
||||
{
|
||||
var service = CreateInitialized();
|
||||
Assert.True(service.IsInitialized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EncryptDatabaseEnabled_AlwaysFalse()
|
||||
{
|
||||
var service = CreateInitialized();
|
||||
Assert.False(service.EncryptDatabaseEnabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetKey_WrongLength_Throws()
|
||||
{
|
||||
var service = new ClientEncryptionService();
|
||||
var shortKey = Convert.ToBase64String(RandomNumberGenerator.GetBytes(16));
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => service.SetKey(shortKey));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetKey_InvalidBase64_Throws()
|
||||
{
|
||||
var service = new ClientEncryptionService();
|
||||
Assert.Throws<FormatException>(() => service.SetKey("not-valid-base64!!!"));
|
||||
}
|
||||
|
||||
// ── Encrypt before initialization ────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Encrypt_NotInitialized_PassesThrough()
|
||||
{
|
||||
var service = new ClientEncryptionService();
|
||||
var result = service.Encrypt("hello");
|
||||
|
||||
Assert.Equal("hello", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decrypt_NotInitialized_PassesThrough()
|
||||
{
|
||||
var service = new ClientEncryptionService();
|
||||
var result = service.Decrypt("$ENC$v1$something$else");
|
||||
|
||||
Assert.Equal("$ENC$v1$something$else", result);
|
||||
}
|
||||
|
||||
// ── Encrypt after initialization ─────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Encrypt_ProducesEncryptedFormat()
|
||||
{
|
||||
var service = CreateInitialized();
|
||||
var encrypted = service.Encrypt("Hello, world!");
|
||||
|
||||
Assert.StartsWith("$ENC$v1$", encrypted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Encrypt_DifferentNonceEachTime()
|
||||
{
|
||||
var service = CreateInitialized();
|
||||
var a = service.Encrypt("same message");
|
||||
var b = service.Encrypt("same message");
|
||||
|
||||
Assert.NotEqual(a, b);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Encrypt_EmptyString_Works()
|
||||
{
|
||||
var service = CreateInitialized();
|
||||
var encrypted = service.Encrypt("");
|
||||
|
||||
Assert.StartsWith("$ENC$v1$", encrypted);
|
||||
}
|
||||
|
||||
// ── Decrypt ──────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Decrypt_RoundTrip()
|
||||
{
|
||||
var service = CreateInitialized();
|
||||
var original = "Hello, world!";
|
||||
|
||||
var encrypted = service.Encrypt(original);
|
||||
var decrypted = service.Decrypt(encrypted);
|
||||
|
||||
Assert.Equal(original, decrypted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decrypt_EmptyString_RoundTrip()
|
||||
{
|
||||
var service = CreateInitialized();
|
||||
var encrypted = service.Encrypt("");
|
||||
var decrypted = service.Decrypt(encrypted);
|
||||
|
||||
Assert.Equal("", decrypted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decrypt_Unicode_RoundTrip()
|
||||
{
|
||||
var service = CreateInitialized();
|
||||
var original = "Hello 🌍 世界 مرحبا";
|
||||
|
||||
var encrypted = service.Encrypt(original);
|
||||
var decrypted = service.Decrypt(encrypted);
|
||||
|
||||
Assert.Equal(original, decrypted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decrypt_PlaintextPassthrough()
|
||||
{
|
||||
var service = CreateInitialized();
|
||||
var plaintext = "This is just plain text";
|
||||
|
||||
var result = service.Decrypt(plaintext);
|
||||
|
||||
Assert.Equal(plaintext, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decrypt_WrongKey_ReturnsFailureMessage()
|
||||
{
|
||||
var encryptor = CreateInitialized();
|
||||
var decryptor = CreateInitialized(); // different key
|
||||
|
||||
var encrypted = encryptor.Encrypt("secret message");
|
||||
var result = decryptor.Decrypt(encrypted);
|
||||
|
||||
Assert.Contains("decryption failed", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decrypt_MalformedContent_ReturnsOriginal()
|
||||
{
|
||||
var service = CreateInitialized();
|
||||
|
||||
// Malformed: prefix present but no valid separator after nonce
|
||||
var result = service.Decrypt("$ENC$v1$noseperatorhere");
|
||||
|
||||
Assert.Equal("$ENC$v1$noseperatorhere", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decrypt_TamperedCiphertext_ReturnsFailure()
|
||||
{
|
||||
var service = CreateInitialized();
|
||||
var encrypted = service.Encrypt("original message");
|
||||
|
||||
var tampered = encrypted[..^5] + "XXXXX";
|
||||
var result = service.Decrypt(tampered);
|
||||
|
||||
Assert.Contains("decryption failed", result);
|
||||
}
|
||||
|
||||
// ── Nullable helpers ─────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void EncryptNullable_Null_ReturnsNull()
|
||||
{
|
||||
var service = CreateInitialized();
|
||||
Assert.Null(service.EncryptNullable(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecryptNullable_Null_ReturnsNull()
|
||||
{
|
||||
var service = CreateInitialized();
|
||||
Assert.Null(service.DecryptNullable(null));
|
||||
}
|
||||
|
||||
// ── Key replacement ──────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void SetKey_CanBeCalledMultipleTimes()
|
||||
{
|
||||
var service = new ClientEncryptionService();
|
||||
|
||||
var key1 = GenerateKey();
|
||||
var key2 = GenerateKey();
|
||||
|
||||
service.SetKey(key1);
|
||||
var encrypted1 = service.Encrypt("test");
|
||||
|
||||
service.SetKey(key2);
|
||||
var encrypted2 = service.Encrypt("test");
|
||||
|
||||
// Both produce encrypted content
|
||||
Assert.StartsWith("$ENC$v1$", encrypted1);
|
||||
Assert.StartsWith("$ENC$v1$", encrypted2);
|
||||
|
||||
// Old key's content can't be decrypted with new key
|
||||
var result = service.Decrypt(encrypted1);
|
||||
Assert.Contains("decryption failed", result);
|
||||
|
||||
// New key's content decrypts fine
|
||||
Assert.Equal("test", service.Decrypt(encrypted2));
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\EchoHub.Client\EchoHub.Client.csproj" />
|
||||
<ProjectReference Include="..\EchoHub.Core\EchoHub.Core.csproj" />
|
||||
<ProjectReference Include="..\EchoHub.Server\EchoHub.Server.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
using System.Security.Cryptography;
|
||||
using EchoHub.Client.Services;
|
||||
using EchoHub.Server.Services;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
namespace EchoHub.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests that server and client encryption services are fully interoperable —
|
||||
/// content encrypted by one can be decrypted by the other using the same key.
|
||||
/// </summary>
|
||||
public class EncryptionCompatibilityTests
|
||||
{
|
||||
private static readonly string SharedKey =
|
||||
Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
|
||||
|
||||
private static MessageEncryptionService CreateServer(string? key = null)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Encryption:Key"] = key ?? SharedKey,
|
||||
["Encryption:EncryptDatabase"] = "false",
|
||||
})
|
||||
.Build();
|
||||
|
||||
var logger = NullLoggerFactory.Instance.CreateLogger<MessageEncryptionService>();
|
||||
return new MessageEncryptionService(config, logger);
|
||||
}
|
||||
|
||||
private static ClientEncryptionService CreateClient(string? key = null)
|
||||
{
|
||||
var service = new ClientEncryptionService();
|
||||
service.SetKey(key ?? SharedKey);
|
||||
return service;
|
||||
}
|
||||
|
||||
// ── Cross-service round trips ────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ClientEncrypt_ServerDecrypt()
|
||||
{
|
||||
var client = CreateClient();
|
||||
var server = CreateServer();
|
||||
var original = "Hello from client!";
|
||||
|
||||
var encrypted = client.Encrypt(original);
|
||||
var decrypted = server.Decrypt(encrypted);
|
||||
|
||||
Assert.Equal(original, decrypted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServerEncrypt_ClientDecrypt()
|
||||
{
|
||||
var server = CreateServer();
|
||||
var client = CreateClient();
|
||||
var original = "Hello from server!";
|
||||
|
||||
var encrypted = server.Encrypt(original);
|
||||
var decrypted = client.Decrypt(encrypted);
|
||||
|
||||
Assert.Equal(original, decrypted);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("a")]
|
||||
[InlineData("Hello, world!")]
|
||||
[InlineData("Hello 🌍 世界 مرحبا")]
|
||||
[InlineData("Line1\nLine2\nLine3")]
|
||||
public void CrossDecrypt_VariousMessages(string message)
|
||||
{
|
||||
var server = CreateServer();
|
||||
var client = CreateClient();
|
||||
|
||||
// Client → Server
|
||||
var clientEncrypted = client.Encrypt(message);
|
||||
Assert.Equal(message, server.Decrypt(clientEncrypted));
|
||||
|
||||
// Server → Client
|
||||
var serverEncrypted = server.Encrypt(message);
|
||||
Assert.Equal(message, client.Decrypt(serverEncrypted));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CrossDecrypt_LargeMessage()
|
||||
{
|
||||
var server = CreateServer();
|
||||
var client = CreateClient();
|
||||
var original = new string('X', 10000);
|
||||
|
||||
var encrypted = client.Encrypt(original);
|
||||
Assert.Equal(original, server.Decrypt(encrypted));
|
||||
}
|
||||
|
||||
// ── Key mismatch ─────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void DifferentKeys_ClientEncrypt_ServerCantDecrypt()
|
||||
{
|
||||
var clientKey = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
|
||||
var serverKey = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
|
||||
|
||||
var client = CreateClient(clientKey);
|
||||
var server = CreateServer(serverKey);
|
||||
|
||||
var encrypted = client.Encrypt("secret");
|
||||
var result = server.Decrypt(encrypted);
|
||||
|
||||
Assert.Contains("decryption failed", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentKeys_ServerEncrypt_ClientCantDecrypt()
|
||||
{
|
||||
var clientKey = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
|
||||
var serverKey = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
|
||||
|
||||
var server = CreateServer(serverKey);
|
||||
var client = CreateClient(clientKey);
|
||||
|
||||
var encrypted = server.Encrypt("secret");
|
||||
var result = client.Decrypt(encrypted);
|
||||
|
||||
Assert.Contains("decryption failed", result);
|
||||
}
|
||||
|
||||
// ── Plaintext passthrough ────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Server_DecryptsPlaintext_AsPassthrough()
|
||||
{
|
||||
var server = CreateServer();
|
||||
Assert.Equal("plain text", server.Decrypt("plain text"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Client_DecryptsPlaintext_AsPassthrough()
|
||||
{
|
||||
var client = CreateClient();
|
||||
Assert.Equal("plain text", client.Decrypt("plain text"));
|
||||
}
|
||||
|
||||
// ── Full E2E flow simulation ─────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void FullFlow_ClientSend_ServerProcess_BroadcastBack()
|
||||
{
|
||||
var client = CreateClient();
|
||||
var server = CreateServer();
|
||||
|
||||
// 1. Client encrypts and sends
|
||||
var originalMessage = "Hello everyone!";
|
||||
var clientEncrypted = client.Encrypt(originalMessage);
|
||||
Assert.StartsWith("$ENC$v1$", clientEncrypted);
|
||||
|
||||
// 2. Server decrypts for processing
|
||||
var serverPlaintext = server.Decrypt(clientEncrypted);
|
||||
Assert.Equal(originalMessage, serverPlaintext);
|
||||
|
||||
// 3. Server re-encrypts for broadcast (different nonce)
|
||||
var serverEncrypted = server.Encrypt(serverPlaintext);
|
||||
Assert.StartsWith("$ENC$v1$", serverEncrypted);
|
||||
Assert.NotEqual(clientEncrypted, serverEncrypted); // different nonce
|
||||
|
||||
// 4. Receiving client decrypts the broadcast
|
||||
var receivedPlaintext = client.Decrypt(serverEncrypted);
|
||||
Assert.Equal(originalMessage, receivedPlaintext);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullFlow_ServerGeneratedMessage_EncryptForBroadcast()
|
||||
{
|
||||
var server = CreateServer();
|
||||
var client = CreateClient();
|
||||
|
||||
// Server generates a system message (e.g. file upload notification)
|
||||
var systemMessage = "user uploaded file.png";
|
||||
|
||||
// Server encrypts for broadcast
|
||||
var encrypted = server.Encrypt(systemMessage);
|
||||
|
||||
// Client decrypts
|
||||
var decrypted = client.Decrypt(encrypted);
|
||||
Assert.Equal(systemMessage, decrypted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullFlow_HistoryRetrieve_ServerEncrypts_ClientDecrypts()
|
||||
{
|
||||
var server = CreateServer();
|
||||
var client = CreateClient();
|
||||
|
||||
// Simulate loading N messages from DB (plaintext) and encrypting for transport
|
||||
var messages = new[] { "msg1", "Hello 🌍", "msg with\nnewline" };
|
||||
|
||||
foreach (var original in messages)
|
||||
{
|
||||
var encrypted = server.Encrypt(original);
|
||||
var decrypted = client.Decrypt(encrypted);
|
||||
Assert.Equal(original, decrypted);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
using System.Security.Cryptography;
|
||||
using EchoHub.Server.Services;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
namespace EchoHub.Tests;
|
||||
|
||||
public class MessageEncryptionServiceTests
|
||||
{
|
||||
private static MessageEncryptionService CreateService(
|
||||
string? key = null, bool encryptDatabase = false)
|
||||
{
|
||||
key ??= Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Encryption:Key"] = key,
|
||||
["Encryption:EncryptDatabase"] = encryptDatabase.ToString(),
|
||||
})
|
||||
.Build();
|
||||
|
||||
var logger = NullLoggerFactory.Instance.CreateLogger<MessageEncryptionService>();
|
||||
return new MessageEncryptionService(config, logger);
|
||||
}
|
||||
|
||||
// ── Constructor validation ───────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Constructor_MissingKey_Throws()
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>())
|
||||
.Build();
|
||||
|
||||
var logger = NullLoggerFactory.Instance.CreateLogger<MessageEncryptionService>();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
new MessageEncryptionService(config, logger));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WrongKeyLength_Throws()
|
||||
{
|
||||
var shortKey = Convert.ToBase64String(RandomNumberGenerator.GetBytes(16));
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => CreateService(shortKey));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidKey_Succeeds()
|
||||
{
|
||||
var service = CreateService();
|
||||
Assert.NotNull(service);
|
||||
}
|
||||
|
||||
// ── EncryptDatabaseEnabled ───────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void EncryptDatabaseEnabled_DefaultFalse()
|
||||
{
|
||||
var service = CreateService();
|
||||
Assert.False(service.EncryptDatabaseEnabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EncryptDatabaseEnabled_WhenConfiguredTrue()
|
||||
{
|
||||
var service = CreateService(encryptDatabase: true);
|
||||
Assert.True(service.EncryptDatabaseEnabled);
|
||||
}
|
||||
|
||||
// ── Encrypt ──────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Encrypt_ProducesEncryptedFormat()
|
||||
{
|
||||
var service = CreateService();
|
||||
var encrypted = service.Encrypt("Hello, world!");
|
||||
|
||||
Assert.StartsWith("$ENC$v1$", encrypted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Encrypt_DifferentNonceEachTime()
|
||||
{
|
||||
var service = CreateService();
|
||||
var a = service.Encrypt("same message");
|
||||
var b = service.Encrypt("same message");
|
||||
|
||||
Assert.NotEqual(a, b);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Encrypt_EmptyString_Works()
|
||||
{
|
||||
var service = CreateService();
|
||||
var encrypted = service.Encrypt("");
|
||||
|
||||
Assert.StartsWith("$ENC$v1$", encrypted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Encrypt_Unicode_Works()
|
||||
{
|
||||
var service = CreateService();
|
||||
var encrypted = service.Encrypt("Hello 🌍 世界 مرحبا");
|
||||
|
||||
Assert.StartsWith("$ENC$v1$", encrypted);
|
||||
}
|
||||
|
||||
// ── Decrypt ──────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Decrypt_RoundTrip()
|
||||
{
|
||||
var service = CreateService();
|
||||
var original = "Hello, world!";
|
||||
|
||||
var encrypted = service.Encrypt(original);
|
||||
var decrypted = service.Decrypt(encrypted);
|
||||
|
||||
Assert.Equal(original, decrypted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decrypt_EmptyString_RoundTrip()
|
||||
{
|
||||
var service = CreateService();
|
||||
var encrypted = service.Encrypt("");
|
||||
var decrypted = service.Decrypt(encrypted);
|
||||
|
||||
Assert.Equal("", decrypted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decrypt_Unicode_RoundTrip()
|
||||
{
|
||||
var service = CreateService();
|
||||
var original = "Hello 🌍 世界 مرحبا";
|
||||
|
||||
var encrypted = service.Encrypt(original);
|
||||
var decrypted = service.Decrypt(encrypted);
|
||||
|
||||
Assert.Equal(original, decrypted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decrypt_LongMessage_RoundTrip()
|
||||
{
|
||||
var service = CreateService();
|
||||
var original = new string('A', 5000);
|
||||
|
||||
var encrypted = service.Encrypt(original);
|
||||
var decrypted = service.Decrypt(encrypted);
|
||||
|
||||
Assert.Equal(original, decrypted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decrypt_PlaintextPassthrough()
|
||||
{
|
||||
var service = CreateService();
|
||||
var plaintext = "This is just plain text";
|
||||
|
||||
var result = service.Decrypt(plaintext);
|
||||
|
||||
Assert.Equal(plaintext, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decrypt_WrongKey_ReturnsFailureMessage()
|
||||
{
|
||||
var key1 = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
|
||||
var key2 = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
|
||||
|
||||
var encryptor = CreateService(key1);
|
||||
var decryptor = CreateService(key2);
|
||||
|
||||
var encrypted = encryptor.Encrypt("secret message");
|
||||
var result = decryptor.Decrypt(encrypted);
|
||||
|
||||
Assert.Contains("decryption failed", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decrypt_MalformedContent_MissingSeparator_ReturnsFailure()
|
||||
{
|
||||
var service = CreateService();
|
||||
var malformed = "$ENC$v1$noseperatorhere";
|
||||
|
||||
var result = service.Decrypt(malformed);
|
||||
|
||||
Assert.Contains("decryption failed", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decrypt_MalformedContent_TooShort_ReturnsFailure()
|
||||
{
|
||||
var service = CreateService();
|
||||
var malformed = "$ENC$v1$AAAA$BBBB";
|
||||
|
||||
var result = service.Decrypt(malformed);
|
||||
|
||||
Assert.Contains("decryption failed", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decrypt_TamperedCiphertext_ReturnsFailure()
|
||||
{
|
||||
var service = CreateService();
|
||||
var encrypted = service.Encrypt("original message");
|
||||
|
||||
// Tamper with the ciphertext by flipping a character
|
||||
var tampered = encrypted[..^5] + "XXXXX";
|
||||
var result = service.Decrypt(tampered);
|
||||
|
||||
Assert.Contains("decryption failed", result);
|
||||
}
|
||||
|
||||
// ── Nullable helpers ─────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void EncryptNullable_Null_ReturnsNull()
|
||||
{
|
||||
var service = CreateService();
|
||||
Assert.Null(service.EncryptNullable(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EncryptNullable_Value_Encrypts()
|
||||
{
|
||||
var service = CreateService();
|
||||
var result = service.EncryptNullable("test");
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.StartsWith("$ENC$v1$", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecryptNullable_Null_ReturnsNull()
|
||||
{
|
||||
var service = CreateService();
|
||||
Assert.Null(service.DecryptNullable(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecryptNullable_EncryptedValue_Decrypts()
|
||||
{
|
||||
var service = CreateService();
|
||||
var encrypted = service.Encrypt("test");
|
||||
|
||||
var result = service.DecryptNullable(encrypted);
|
||||
|
||||
Assert.Equal("test", result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user