From 59807809ec78687d147dcc0000cf0d20bae021f8 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Thu, 16 Jul 2026 23:13:36 +0200 Subject: [PATCH] Add end-to-end encryption support for channel attachments --- src/Decho/Services/ConnectionService.cs | 75 ++++++++++++++++++++++-- src/Decho/Views/MessageItemView.axaml.cs | 5 +- 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/src/Decho/Services/ConnectionService.cs b/src/Decho/Services/ConnectionService.cs index 18cb442..af7d821 100644 --- a/src/Decho/Services/ConnectionService.cs +++ b/src/Decho/Services/ConnectionService.cs @@ -7,6 +7,7 @@ using EchoHub.Core.Constants; using EchoHub.Core.DTOs; using EchoHub.Core.Models; using EchoHub.Core.Security; +using EchoHub.Core.Services; using System.Collections.ObjectModel; @@ -309,9 +310,41 @@ public sealed class ConnectionService : IDisposable return; } - await using FileStream stream = File.OpenRead(filePath); string fileName = Path.GetFileName(filePath); - OutgoingAttachment attachment = new OutgoingAttachment(stream, fileName); + OutgoingAttachment attachment; + + if (entry.Manager.RoomKeys.TryGetKey(channelName, out byte[]? roomKey)) + { + // End-to-end encrypted channel: encrypt the blob locally, declare its kind, + // and room-encrypt an image ASCII preview — the server stores the ciphertext as-is. + byte[] bytes = await File.ReadAllBytesAsync(filePath); + string declaredKind; + string? preview = null; + + await using (var ms = new MemoryStream(bytes)) + { + if (FileValidationHelper.IsValidImage(ms)) + { + declaredKind = "image"; + var (w, h) = ImageToAsciiService.GetDimensions(size); + ms.Position = 0; + preview = RoomCrypto.EncryptText(new ImageToAsciiService().ConvertToAscii(ms, w, h), roomKey); + } + else + { + declaredKind = FileValidationHelper.IsAudioFile(fileName) ? "audio" : "file"; + } + } + + byte[] encryptedBlob = RoomCrypto.EncryptBytes(bytes, roomKey); + attachment = new OutgoingAttachment(new MemoryStream(encryptedBlob), fileName, declaredKind, preview); + } + else + { + await using FileStream stream = File.OpenRead(filePath); + attachment = new OutgoingAttachment(stream, fileName); + } + _ = await entry.ApiClient.SendMessageWithAttachmentsAsync(channelName, "", [attachment], size); } @@ -362,7 +395,7 @@ public sealed class ConnectionService : IDisposable : null; } - public async Task DownloadAttachmentAsync(string serverUrl, string relativeUrl, string fileName) + public async Task DownloadAttachmentAsync(string serverUrl, string channelName, string relativeUrl, string fileName) { if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) { @@ -371,7 +404,26 @@ public sealed class ConnectionService : IDisposable try { - return await entry.ApiClient.DownloadFileToTempAsync(relativeUrl, fileName); + string? tempPath = await entry.ApiClient.DownloadFileToTempAsync(relativeUrl, fileName); + if (tempPath is null) + { + return null; + } + + if (entry.Manager.RoomKeys.TryGetKey(channelName, out byte[]? roomKey)) + { + try + { + byte[] encrypted = await File.ReadAllBytesAsync(tempPath); + await File.WriteAllBytesAsync(tempPath, RoomCrypto.DecryptBytes(encrypted, roomKey)); + } + catch + { + // Not room ciphertext — leave the downloaded bytes as-is. + } + } + + return tempPath; } catch { @@ -379,7 +431,7 @@ public sealed class ConnectionService : IDisposable } } - public async Task DownloadImageBytesAsync(string serverUrl, string relativeUrl) + public async Task DownloadImageBytesAsync(string serverUrl, string channelName, string relativeUrl) { if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) { @@ -403,6 +455,19 @@ public sealed class ConnectionService : IDisposable { // Ignore if deletion fails } + + if (entry.Manager.RoomKeys.TryGetKey(channelName, out byte[]? roomKey)) + { + try + { + bytes = RoomCrypto.DecryptBytes(bytes, roomKey); + } + catch + { + // Not room ciphertext — leave the downloaded bytes as-is. + } + } + return bytes; } catch diff --git a/src/Decho/Views/MessageItemView.axaml.cs b/src/Decho/Views/MessageItemView.axaml.cs index 273884c..2b21c0d 100644 --- a/src/Decho/Views/MessageItemView.axaml.cs +++ b/src/Decho/Views/MessageItemView.axaml.cs @@ -215,7 +215,7 @@ public partial class MessageItemView : UserControl } byte[]? bytes = await mainVm.ConnectionService.DownloadImageBytesAsync( - msg.ServerUrl ?? "", att.Url); + msg.ServerUrl ?? "", msg.Model.ChannelName, att.Url); cts.Token.ThrowIfCancellationRequested(); if (bytes is null || bytes.Length == 0) @@ -294,7 +294,8 @@ public partial class MessageItemView : UserControl return; } - string? tempPath = await mainVm.ConnectionService.DownloadAttachmentAsync(msg.ServerUrl ?? "", att.Url, att.FileName); + string? tempPath = await mainVm.ConnectionService.DownloadAttachmentAsync( + msg.ServerUrl ?? "", msg.Model.ChannelName, att.Url, att.FileName); if (tempPath is null) {