Fix channel unlock crypto things

This commit is contained in:
Stone_Red
2026-07-17 15:27:09 +02:00
parent e1366c0aea
commit e407791561
2 changed files with 188 additions and 74 deletions
+59 -9
View File
@@ -141,14 +141,24 @@ public sealed class ConnectionService : IDisposable
}
RemoveFromLeftChannels(serverUrl, channelName);
// For E2EE channels where the key hasn't been stored yet, always do a real join
// to obtain the wrapped room key (TrackChannel returned false on re-selection,
// so the first branch above skipped the actual join).
bool encFlag = entry.Manager.RoomKeys.IsChannelEncrypted(channelName);
bool hasKeyFlag = entry.Manager.RoomKeys.HasKey(channelName);
if (encFlag && !hasKeyFlag)
{
JoinOutcome outcome = await entry.Manager.JoinChannelAsync(channelName, password);
List<MessageModel> history = outcome.History.Select(m => MessageModelFromDto(m, entry)).ToList();
return new ChannelJoinResult(history, true, outcome.EncryptionSalt, outcome.WrappedRoomKey);
}
List<MessageDto> existing = await entry.Manager.GetHistoryAsync(channelName);
List<MessageModel> hist = existing.Select(m => MessageModelFromDto(m, entry)).ToList();
// Check if we have the key for an already-tracked encrypted channel
bool enc = entry.Manager.RoomKeys.IsChannelEncrypted(channelName);
bool hasK = entry.Manager.RoomKeys.HasKey(channelName);
return new ChannelJoinResult(hist, enc && !hasK, null, null);
return new ChannelJoinResult(hist, encFlag && !hasKeyFlag, null, null);
}
public async Task<ChannelJoinResult> UnlockRoomKeyAsync(string serverUrl, string channelName, string passphrase, string encryptionSalt, string wrappedRoomKey)
@@ -163,8 +173,18 @@ public sealed class ConnectionService : IDisposable
return new ChannelJoinResult([], false, null, null);
}
if (string.IsNullOrEmpty(encryptionSalt))
{
throw new InvalidOperationException("Encryption salt not available for this channel");
}
if (string.IsNullOrEmpty(wrappedRoomKey))
{
throw new InvalidOperationException("Wrapped room key not available. Re-join the channel to obtain it.");
}
byte[] salt = Convert.FromBase64String(encryptionSalt);
var derived = RoomCrypto.DeriveKeys(passphrase, salt);
RoomCrypto.DerivedKeys derived = RoomCrypto.DeriveKeys(passphrase, salt);
if (!entry.Manager.RoomKeys.TryStoreFromEnvelope(channelName, wrappedRoomKey, derived.KeyEncryptionKey))
{
@@ -249,6 +269,36 @@ public sealed class ConnectionService : IDisposable
entry.Manager.UntrackChannel(channelName);
}
public async Task<List<ChannelDto>> GetChannelsAsync(string serverUrl)
{
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
{
return [];
}
return await entry.ApiClient.GetChannelsAsync();
}
public async Task<ChannelCryptoDto?> GetChannelCryptoAsync(string serverUrl, string channelName)
{
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
{
return null;
}
return await entry.ApiClient.GetChannelCryptoAsync(channelName);
}
public void MarkChannelEncrypted(string serverUrl, string channelName, bool isEncrypted)
{
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
{
return;
}
entry.Manager.RoomKeys.MarkChannelEncrypted(channelName, isEncrypted);
}
public async Task KickUserAsync(string serverUrl, string username, string? reason)
{
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
@@ -377,12 +427,12 @@ public sealed class ConnectionService : IDisposable
string declaredKind;
string? preview = null;
await using (var ms = new MemoryStream(bytes))
await using (MemoryStream ms = new MemoryStream(bytes))
{
if (FileValidationHelper.IsValidImage(ms))
{
declaredKind = "image";
var (w, h) = ImageToAsciiService.GetDimensions(size);
(int w, int h) = ImageToAsciiService.GetDimensions(size);
ms.Position = 0;
preview = RoomCrypto.EncryptText(new ImageToAsciiService().ConvertToAscii(ms, w, h), roomKey);
}
@@ -784,7 +834,7 @@ public sealed class ConnectionService : IDisposable
}
else
{
var model = new ChannelModel(
ChannelModel model = new ChannelModel(
channel.Id.ToString(),
channel.Name,
[],
+79 -15
View File
@@ -10,6 +10,7 @@ using EchoHub.Client.Services;
using EchoHub.Core.Constants;
using EchoHub.Core.DTOs;
using EchoHub.Core.Models;
using EchoHub.Core.Security;
using MsBox.Avalonia;
using MsBox.Avalonia.Base;
@@ -141,6 +142,9 @@ public sealed class MainWindowViewModel : ViewModelBase
}
private async Task<string?> ShowPromptWindowAsync(string title, string message, string buttonText = "OK", bool isPassword = true)
{
string? result = null;
await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(async () =>
{
Window window = new Avalonia.Controls.Window
{
@@ -153,7 +157,6 @@ public sealed class MainWindowViewModel : ViewModelBase
};
TextBox inputBox = new Avalonia.Controls.TextBox { Watermark = isPassword ? "Password" : "Passphrase", PasswordChar = '*' };
string? result = null;
Button okBtn = new Avalonia.Controls.Button { Content = buttonText, IsDefault = true };
Button cancelBtn = new Avalonia.Controls.Button { Content = "Cancel", IsCancel = true };
@@ -187,6 +190,8 @@ public sealed class MainWindowViewModel : ViewModelBase
window.Content = panel;
await window.ShowDialog(_mainWindow!);
});
return result;
}
@@ -216,24 +221,54 @@ public sealed class MainWindowViewModel : ViewModelBase
return;
}
ChannelJoinResult result = await ConnectionService.JoinChannelAsync(serverUrl, channelName, password);
EnsureChannelInList(serverUrl, channelName);
ChannelCryptoDto? crypto = await ConnectionService.GetChannelCryptoAsync(serverUrl, channelName);
bool isEncrypted = crypto is not null && crypto.IsEncrypted;
// Handle E2EE unlock if needed
if (result.IsEncrypted && result.EncryptionSalt is not null && result.WrappedRoomKey is not null)
string? wirePassword = password;
if (isEncrypted)
{
string? passphrase = await ShowPromptWindowAsync("Unlock Channel", "Enter the passphrase to unlock messages:", "Unlock");
if (string.IsNullOrEmpty(passphrase))
ServerConnection entry = ConnectionService.Connections[serverUrl];
entry.Manager.RoomKeys.MarkChannelEncrypted(channelName, true);
if (!entry.Manager.RoomKeys.HasKey(channelName) && password is null)
{
password = await ShowPromptWindowAsync("Unlock Channel", "Enter the passphrase to unlock messages:", "Unlock");
if (string.IsNullOrEmpty(password))
{
return;
}
}
ChannelJoinResult unlockResult = await ConnectionService.UnlockRoomKeyAsync(serverUrl, channelName, passphrase, result.EncryptionSalt, result.WrappedRoomKey);
if (password is not null)
{
byte[] salt = Convert.FromBase64String(crypto!.EncryptionSalt!);
wirePassword = RoomCrypto.DeriveKeys(password, salt).AuthKeyHex;
}
}
ChannelJoinResult result = await ConnectionService.JoinChannelAsync(serverUrl, channelName, wirePassword);
EnsureChannelInList(serverUrl, channelName);
if (isEncrypted && !ConnectionService.Connections[serverUrl].Manager.RoomKeys.HasKey(channelName))
{
try
{
ChannelJoinResult unlockResult = await ConnectionService.UnlockRoomKeyAsync(
serverUrl, channelName, password, crypto!.EncryptionSalt!, result.WrappedRoomKey ?? "");
if (unlockResult.History.Count > 0)
{
result = unlockResult;
}
}
catch (Exception ex)
{
IMsBox<ButtonResult> box = MessageBoxManager.GetMessageBoxStandard(
"Decrypt Error", $"Decrypt failed: {ex.Message}", ButtonEnum.Ok);
_ = await box.ShowWindowDialogAsync(_mainWindow);
return;
}
}
ChannelModel? channelModel = FindChannel(serverUrl, channelName);
if (channelModel is not null)
@@ -878,16 +913,21 @@ public sealed class MainWindowViewModel : ViewModelBase
{
try
{
ChannelJoinResult joinResult = await ConnectionService.JoinChannelAsync(serverUrl, channel.Name, password);
ChannelCryptoDto? crypto = await ConnectionService.GetChannelCryptoAsync(serverUrl, channel.Name);
bool isEncrypted = crypto is not null && crypto.IsEncrypted;
// Handle E2EE unlock if needed
if (joinResult.IsEncrypted && joinResult.EncryptionSalt is not null && joinResult.WrappedRoomKey is not null)
string? wirePassword = password;
if (isEncrypted)
{
string? passphrase = await ShowPromptWindowAsync($"Unlock Channel", "This channel is encrypted. Please enter the passphrase to unlock it:", "Unlock");
ServerConnection entry = ConnectionService.Connections[serverUrl];
entry.Manager.RoomKeys.MarkChannelEncrypted(channel.Name, true);
if (!entry.Manager.RoomKeys.HasKey(channel.Name) && password is null)
{
string? passphrase = await ShowPromptWindowAsync("Unlock Channel", "Enter the passphrase to unlock messages:", "Unlock");
if (string.IsNullOrEmpty(passphrase))
{
// User cancelled - leave channel locked
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
{
channel.IsLocked = true;
@@ -895,11 +935,35 @@ public sealed class MainWindowViewModel : ViewModelBase
});
break;
}
password = passphrase;
}
ChannelJoinResult unlockResult = await ConnectionService.UnlockRoomKeyAsync(serverUrl, channel.Name, passphrase, joinResult.EncryptionSalt, joinResult.WrappedRoomKey);
if (password is not null)
{
byte[] salt = Convert.FromBase64String(crypto!.EncryptionSalt!);
wirePassword = RoomCrypto.DeriveKeys(password, salt).AuthKeyHex;
}
}
ChannelJoinResult joinResult = await ConnectionService.JoinChannelAsync(serverUrl, channel.Name, wirePassword);
if (isEncrypted && !ConnectionService.Connections[serverUrl].Manager.RoomKeys.HasKey(channel.Name))
{
try
{
ChannelJoinResult unlockResult = await ConnectionService.UnlockRoomKeyAsync(
serverUrl, channel.Name, password, crypto!.EncryptionSalt!, joinResult.WrappedRoomKey ?? "");
if (unlockResult.History.Count > 0)
{
joinResult = unlockResult; // Use decrypted history
joinResult = unlockResult;
}
}
catch (Exception ex)
{
IMsBox<ButtonResult> errBox = MessageBoxManager.GetMessageBoxStandard(
"Decrypt Error", $"Decrypt failed: {ex.Message}", ButtonEnum.Ok);
_ = await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(
() => errBox.ShowWindowDialogAsync(_mainWindow));
}
}