mirror of
https://github.com/Stone-Red-Code/Decho.git
synced 2026-09-04 00:46:11 +02:00
Fix channel unlock crypto things
This commit is contained in:
@@ -141,14 +141,24 @@ public sealed class ConnectionService : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
RemoveFromLeftChannels(serverUrl, channelName);
|
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<MessageDto> existing = await entry.Manager.GetHistoryAsync(channelName);
|
||||||
List<MessageModel> hist = existing.Select(m => MessageModelFromDto(m, entry)).ToList();
|
List<MessageModel> hist = existing.Select(m => MessageModelFromDto(m, entry)).ToList();
|
||||||
|
|
||||||
// Check if we have the key for an already-tracked encrypted channel
|
return new ChannelJoinResult(hist, encFlag && !hasKeyFlag, null, null);
|
||||||
bool enc = entry.Manager.RoomKeys.IsChannelEncrypted(channelName);
|
|
||||||
bool hasK = entry.Manager.RoomKeys.HasKey(channelName);
|
|
||||||
|
|
||||||
return new ChannelJoinResult(hist, enc && !hasK, null, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ChannelJoinResult> UnlockRoomKeyAsync(string serverUrl, string channelName, string passphrase, string encryptionSalt, string wrappedRoomKey)
|
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);
|
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);
|
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))
|
if (!entry.Manager.RoomKeys.TryStoreFromEnvelope(channelName, wrappedRoomKey, derived.KeyEncryptionKey))
|
||||||
{
|
{
|
||||||
@@ -249,6 +269,36 @@ public sealed class ConnectionService : IDisposable
|
|||||||
entry.Manager.UntrackChannel(channelName);
|
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)
|
public async Task KickUserAsync(string serverUrl, string username, string? reason)
|
||||||
{
|
{
|
||||||
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
@@ -377,12 +427,12 @@ public sealed class ConnectionService : IDisposable
|
|||||||
string declaredKind;
|
string declaredKind;
|
||||||
string? preview = null;
|
string? preview = null;
|
||||||
|
|
||||||
await using (var ms = new MemoryStream(bytes))
|
await using (MemoryStream ms = new MemoryStream(bytes))
|
||||||
{
|
{
|
||||||
if (FileValidationHelper.IsValidImage(ms))
|
if (FileValidationHelper.IsValidImage(ms))
|
||||||
{
|
{
|
||||||
declaredKind = "image";
|
declaredKind = "image";
|
||||||
var (w, h) = ImageToAsciiService.GetDimensions(size);
|
(int w, int h) = ImageToAsciiService.GetDimensions(size);
|
||||||
ms.Position = 0;
|
ms.Position = 0;
|
||||||
preview = RoomCrypto.EncryptText(new ImageToAsciiService().ConvertToAscii(ms, w, h), roomKey);
|
preview = RoomCrypto.EncryptText(new ImageToAsciiService().ConvertToAscii(ms, w, h), roomKey);
|
||||||
}
|
}
|
||||||
@@ -784,7 +834,7 @@ public sealed class ConnectionService : IDisposable
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var model = new ChannelModel(
|
ChannelModel model = new ChannelModel(
|
||||||
channel.Id.ToString(),
|
channel.Id.ToString(),
|
||||||
channel.Name,
|
channel.Name,
|
||||||
[],
|
[],
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using EchoHub.Client.Services;
|
|||||||
using EchoHub.Core.Constants;
|
using EchoHub.Core.Constants;
|
||||||
using EchoHub.Core.DTOs;
|
using EchoHub.Core.DTOs;
|
||||||
using EchoHub.Core.Models;
|
using EchoHub.Core.Models;
|
||||||
|
using EchoHub.Core.Security;
|
||||||
|
|
||||||
using MsBox.Avalonia;
|
using MsBox.Avalonia;
|
||||||
using MsBox.Avalonia.Base;
|
using MsBox.Avalonia.Base;
|
||||||
@@ -142,51 +143,55 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
|
|
||||||
private async Task<string?> ShowPromptWindowAsync(string title, string message, string buttonText = "OK", bool isPassword = true)
|
private async Task<string?> ShowPromptWindowAsync(string title, string message, string buttonText = "OK", bool isPassword = true)
|
||||||
{
|
{
|
||||||
Window window = new Avalonia.Controls.Window
|
|
||||||
{
|
|
||||||
Title = title,
|
|
||||||
Width = 400,
|
|
||||||
Height = 200,
|
|
||||||
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
|
||||||
SizeToContent = SizeToContent.Height,
|
|
||||||
CanResize = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
TextBox inputBox = new Avalonia.Controls.TextBox { Watermark = isPassword ? "Password" : "Passphrase", PasswordChar = '*' };
|
|
||||||
string? result = null;
|
string? result = null;
|
||||||
|
await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(async () =>
|
||||||
Button okBtn = new Avalonia.Controls.Button { Content = buttonText, IsDefault = true };
|
|
||||||
Button cancelBtn = new Avalonia.Controls.Button { Content = "Cancel", IsCancel = true };
|
|
||||||
|
|
||||||
okBtn.Click += (_, _) =>
|
|
||||||
{
|
{
|
||||||
result = inputBox.Text;
|
Window window = new Avalonia.Controls.Window
|
||||||
window.Close();
|
|
||||||
};
|
|
||||||
cancelBtn.Click += (_, _) => window.Close();
|
|
||||||
|
|
||||||
StackPanel buttons = new Avalonia.Controls.StackPanel
|
|
||||||
{
|
|
||||||
Orientation = Avalonia.Layout.Orientation.Horizontal,
|
|
||||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right,
|
|
||||||
Spacing = 8,
|
|
||||||
Children = { cancelBtn, okBtn },
|
|
||||||
};
|
|
||||||
|
|
||||||
StackPanel panel = new Avalonia.Controls.StackPanel
|
|
||||||
{
|
|
||||||
Margin = new Avalonia.Thickness(12),
|
|
||||||
Spacing = 8,
|
|
||||||
Children =
|
|
||||||
{
|
{
|
||||||
new Avalonia.Controls.TextBlock { Text = message },
|
Title = title,
|
||||||
inputBox,
|
Width = 400,
|
||||||
buttons,
|
Height = 200,
|
||||||
},
|
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
||||||
};
|
SizeToContent = SizeToContent.Height,
|
||||||
|
CanResize = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
TextBox inputBox = new Avalonia.Controls.TextBox { Watermark = isPassword ? "Password" : "Passphrase", PasswordChar = '*' };
|
||||||
|
|
||||||
|
Button okBtn = new Avalonia.Controls.Button { Content = buttonText, IsDefault = true };
|
||||||
|
Button cancelBtn = new Avalonia.Controls.Button { Content = "Cancel", IsCancel = true };
|
||||||
|
|
||||||
|
okBtn.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
result = inputBox.Text;
|
||||||
|
window.Close();
|
||||||
|
};
|
||||||
|
cancelBtn.Click += (_, _) => window.Close();
|
||||||
|
|
||||||
|
StackPanel buttons = new Avalonia.Controls.StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Avalonia.Layout.Orientation.Horizontal,
|
||||||
|
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right,
|
||||||
|
Spacing = 8,
|
||||||
|
Children = { cancelBtn, okBtn },
|
||||||
|
};
|
||||||
|
|
||||||
|
StackPanel panel = new Avalonia.Controls.StackPanel
|
||||||
|
{
|
||||||
|
Margin = new Avalonia.Thickness(12),
|
||||||
|
Spacing = 8,
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
new Avalonia.Controls.TextBlock { Text = message },
|
||||||
|
inputBox,
|
||||||
|
buttons,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
window.Content = panel;
|
||||||
|
await window.ShowDialog(_mainWindow!);
|
||||||
|
});
|
||||||
|
|
||||||
window.Content = panel;
|
|
||||||
await window.ShowDialog(_mainWindow!);
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,22 +221,52 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ChannelJoinResult result = await ConnectionService.JoinChannelAsync(serverUrl, channelName, password);
|
ChannelCryptoDto? crypto = await ConnectionService.GetChannelCryptoAsync(serverUrl, channelName);
|
||||||
EnsureChannelInList(serverUrl, channelName);
|
bool isEncrypted = crypto is not null && crypto.IsEncrypted;
|
||||||
|
|
||||||
// Handle E2EE unlock if needed
|
string? wirePassword = password;
|
||||||
if (result.IsEncrypted && result.EncryptionSalt is not null && result.WrappedRoomKey is not null)
|
|
||||||
|
if (isEncrypted)
|
||||||
{
|
{
|
||||||
string? passphrase = await ShowPromptWindowAsync("Unlock Channel", "Enter the passphrase to unlock messages:", "Unlock");
|
ServerConnection entry = ConnectionService.Connections[serverUrl];
|
||||||
if (string.IsNullOrEmpty(passphrase))
|
entry.Manager.RoomKeys.MarkChannelEncrypted(channelName, true);
|
||||||
|
|
||||||
|
if (!entry.Manager.RoomKeys.HasKey(channelName) && password is null)
|
||||||
{
|
{
|
||||||
return;
|
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)
|
||||||
if (unlockResult.History.Count > 0)
|
|
||||||
{
|
{
|
||||||
result = unlockResult;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,28 +913,57 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
try
|
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
|
string? wirePassword = password;
|
||||||
if (joinResult.IsEncrypted && joinResult.EncryptionSalt is not null && joinResult.WrappedRoomKey is not null)
|
|
||||||
|
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 (string.IsNullOrEmpty(passphrase))
|
if (!entry.Manager.RoomKeys.HasKey(channel.Name) && password is null)
|
||||||
{
|
{
|
||||||
// User cancelled - leave channel locked
|
string? passphrase = await ShowPromptWindowAsync("Unlock Channel", "Enter the passphrase to unlock messages:", "Unlock");
|
||||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
if (string.IsNullOrEmpty(passphrase))
|
||||||
{
|
{
|
||||||
channel.IsLocked = true;
|
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||||
Chat.Composer.IsConnected = false;
|
{
|
||||||
});
|
channel.IsLocked = true;
|
||||||
break;
|
Chat.Composer.IsConnected = false;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
password = passphrase;
|
||||||
}
|
}
|
||||||
|
|
||||||
ChannelJoinResult unlockResult = await ConnectionService.UnlockRoomKeyAsync(serverUrl, channel.Name, passphrase, joinResult.EncryptionSalt, joinResult.WrappedRoomKey);
|
if (password is not null)
|
||||||
if (unlockResult.History.Count > 0)
|
|
||||||
{
|
{
|
||||||
joinResult = unlockResult; // Use decrypted history
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user