Files
Relay/RelayServer/Services/Chat/ChannelCryptoService.cs
2026-06-06 23:38:50 -04:00

63 lines
2.5 KiB
C#

using System.Security.Cryptography;
using System.Text;
namespace RelayServer.Services.Chat;
/// <summary>
/// AES-GCM-256 only (no RSA). Used exclusively for "at-rest" encryption of channel messages
/// in the SurrealDB channel_messages table.
///
/// Why a separate service from E2EeHelper:
/// - E2EeHelper is for *transit* between a specific sender and a specific recipient — it
/// wraps an ephemeral AES key with the recipient's RSA public key.
/// - ChannelCryptoService is for *storage* — the server is both the encryptor and the
/// decryptor, and it stores the symmetric channel key in server_encryption_keys.KeyBase64.
/// There's no recipient to wrap for.
///
/// Server flow for a chat message:
/// incoming SocketEncryptedMessage (encrypted with server's RSA public key, by client)
/// → E2EeHelper.DecryptForRecipient(serverPrivateKey) → plaintext
/// → ChannelCryptoService.Encrypt(channelDbKey) → stored ciphertext
/// → … later, on history fetch …
/// → ChannelCryptoService.Decrypt(channelDbKey) → plaintext
/// → E2EeHelper.EncryptForRecipient(clientPublicKey) → delivered ciphertext
/// </summary>
public sealed class ChannelCryptoService
{
public string GenerateKey()
{
return Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
}
public (string cipherText, string nonce, string tag) Encrypt(string plainText, string keyBase64)
{
var key = Convert.FromBase64String(keyBase64);
var nonce = RandomNumberGenerator.GetBytes(12);
var plainBytes = Encoding.UTF8.GetBytes(plainText);
var cipherBytes = new byte[plainBytes.Length];
var tag = new byte[16];
using var aes = new AesGcm(key, 16);
aes.Encrypt(nonce, plainBytes, cipherBytes, tag);
return (
Convert.ToBase64String(cipherBytes),
Convert.ToBase64String(nonce),
Convert.ToBase64String(tag)
);
}
public string Decrypt(string cipherTextBase64, string nonceBase64, string tagBase64, string keyBase64)
{
var key = Convert.FromBase64String(keyBase64);
var nonce = Convert.FromBase64String(nonceBase64);
var tag = Convert.FromBase64String(tagBase64);
var cipherBytes = Convert.FromBase64String(cipherTextBase64);
var plainBytes = new byte[cipherBytes.Length];
using var aes = new AesGcm(key, 16);
aes.Decrypt(nonce, cipherBytes, tag, plainBytes);
return Encoding.UTF8.GetString(plainBytes);
}
}