mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 08:36:11 +02:00
feat: enhance message decryption for IRC clients and improve attachment handling
This commit is contained in:
@@ -2,6 +2,12 @@ namespace EchoHub.Core.Contracts;
|
|||||||
|
|
||||||
public interface IMessageEncryptionService
|
public interface IMessageEncryptionService
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Prefix marking transport/at-rest encrypted content. <see cref="Decrypt"/> is a
|
||||||
|
/// pass-through for values without it.
|
||||||
|
/// </summary>
|
||||||
|
const string CiphertextPrefix = "$ENC$v1$";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether database content should be encrypted at rest (server setting).
|
/// Whether database content should be encrypted at rest (server setting).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -16,8 +16,16 @@ public class IrcBroadcaster : IChatBroadcaster
|
|||||||
|
|
||||||
public async Task SendMessageToChannelAsync(string channelName, MessageDto message)
|
public async Task SendMessageToChannelAsync(string channelName, MessageDto message)
|
||||||
{
|
{
|
||||||
// Decrypt content for IRC clients (they can't handle app-layer encryption)
|
// Decrypt content and attachment previews for IRC clients (they can't handle
|
||||||
var decryptedMessage = message with { Content = _encryption.Decrypt(message.Content) };
|
// app-layer encryption). E2E room ciphertext ($RC1$) passes through untouched;
|
||||||
|
// the formatter drops previews that are still ciphertext.
|
||||||
|
var decryptedMessage = message with
|
||||||
|
{
|
||||||
|
Content = _encryption.Decrypt(message.Content),
|
||||||
|
Attachments = message.Attachments?
|
||||||
|
.Select(a => a with { AsciiPreview = _encryption.DecryptNullable(a.AsciiPreview) })
|
||||||
|
.ToList(),
|
||||||
|
};
|
||||||
var lines = IrcMessageFormatter.FormatMessage(decryptedMessage);
|
var lines = IrcMessageFormatter.FormatMessage(decryptedMessage);
|
||||||
|
|
||||||
foreach (var conn in _gateway.GetConnectionsInChannel(channelName))
|
foreach (var conn in _gateway.GetConnectionsInChannel(channelName))
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using EchoHub.Core.Contracts;
|
||||||
using EchoHub.Core.DTOs;
|
using EchoHub.Core.DTOs;
|
||||||
using EchoHub.Core.Models;
|
using EchoHub.Core.Models;
|
||||||
|
using EchoHub.Core.Security;
|
||||||
|
|
||||||
namespace EchoHub.Server.Irc;
|
namespace EchoHub.Server.Irc;
|
||||||
|
|
||||||
@@ -34,7 +36,7 @@ public static partial class IrcMessageFormatter
|
|||||||
{
|
{
|
||||||
case AttachmentKind.Image:
|
case AttachmentKind.Image:
|
||||||
lines.Add($"{prefix} PRIVMSG {ircChannel} :[Image: {attachment.FileName}] {attachment.Url}");
|
lines.Add($"{prefix} PRIVMSG {ircChannel} :[Image: {attachment.FileName}] {attachment.Url}");
|
||||||
if (attachment.AsciiPreview is not null)
|
if (attachment.AsciiPreview is not null && !IsCiphertext(attachment.AsciiPreview))
|
||||||
{
|
{
|
||||||
foreach (var line in attachment.AsciiPreview.Split('\n'))
|
foreach (var line in attachment.AsciiPreview.Split('\n'))
|
||||||
{
|
{
|
||||||
@@ -66,6 +68,15 @@ public static partial class IrcMessageFormatter
|
|||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when a preview is still encrypted — transport ($ENC$v1$) if a broadcast path
|
||||||
|
/// forgot to decrypt it, or E2E room ciphertext ($RC1$) the server cannot decrypt.
|
||||||
|
/// Emitting it would flood IRC clients with a multi-KB base64 blob.
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsCiphertext(string text) =>
|
||||||
|
text.StartsWith(IMessageEncryptionService.CiphertextPrefix, StringComparison.Ordinal)
|
||||||
|
|| text.StartsWith(RoomCrypto.CiphertextPrefix, StringComparison.Ordinal);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Format a link embed as IRC PRIVMSG lines (text-only, no ASCII thumbnail).
|
/// Format a link embed as IRC PRIVMSG lines (text-only, no ASCII thumbnail).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace EchoHub.Server.Services;
|
|||||||
|
|
||||||
public class MessageEncryptionService : IMessageEncryptionService
|
public class MessageEncryptionService : IMessageEncryptionService
|
||||||
{
|
{
|
||||||
private const string EncryptionPrefix = "$ENC$v1$";
|
private const string EncryptionPrefix = IMessageEncryptionService.CiphertextPrefix;
|
||||||
private const int NonceSizeBytes = 12;
|
private const int NonceSizeBytes = 12;
|
||||||
private const int TagSizeBytes = 16;
|
private const int TagSizeBytes = 16;
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,27 @@ public class IrcBroadcasterTests
|
|||||||
Assert.DoesNotContain(output, l => l.Contains("$ENC$"));
|
Assert.DoesNotContain(output, l => l.Contains("$ENC$"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendMessage_DecryptsAttachmentAsciiPreview()
|
||||||
|
{
|
||||||
|
var (_, stream) = AddConnectionWithCapture("bob", "general");
|
||||||
|
|
||||||
|
var attachment = new AttachmentDto(
|
||||||
|
AttachmentKind.Image, "/api/files/abc", "photo.png", 1234,
|
||||||
|
_encryption.Encrypt("line1\nline2"));
|
||||||
|
var message = new MessageDto(
|
||||||
|
Guid.NewGuid(), _encryption.Encrypt("look at this"), "alice", null, "general",
|
||||||
|
DateTimeOffset.UtcNow, [attachment]);
|
||||||
|
|
||||||
|
await _broadcaster.SendMessageToChannelAsync("general", message);
|
||||||
|
|
||||||
|
var output = stream.GetOutputLines();
|
||||||
|
Assert.Contains(output, l => l.Contains("[Image: photo.png]"));
|
||||||
|
Assert.Contains(output, l => l.Contains("line1"));
|
||||||
|
Assert.Contains(output, l => l.Contains("line2"));
|
||||||
|
Assert.DoesNotContain(output, l => l.Contains("$ENC$"));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SendMessage_SkipsSender()
|
public async Task SendMessage_SkipsSender()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -138,6 +138,19 @@ public class IrcMessageFormatterTests
|
|||||||
Assert.Equal(2, asciiLines.Count);
|
Assert.Equal(2, asciiLines.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("$ENC$v1$abc123$def456")] // transport ciphertext a broadcast path forgot to decrypt
|
||||||
|
[InlineData("$RC1$abc123def456")] // E2E room ciphertext the server cannot decrypt
|
||||||
|
public void FormatMessage_ImageMessage_SkipsCiphertextPreview(string ciphertextPreview)
|
||||||
|
{
|
||||||
|
var msg = CreateImageMessage(ciphertextPreview, "photo.jpg", "https://example.com/photo.jpg");
|
||||||
|
var lines = IrcMessageFormatter.FormatMessage(msg);
|
||||||
|
|
||||||
|
// Only the [Image: ...] header line — never the ciphertext blob
|
||||||
|
Assert.Single(lines);
|
||||||
|
Assert.Contains("[Image: photo.jpg]", lines[0]);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FormatMessage_FileMessage_FormatsCorrectly()
|
public void FormatMessage_FileMessage_FormatsCorrectly()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user