Update: Full E2EE + Scripts

This commit is contained in:
2026-03-21 04:45:49 -04:00
parent cc31c4024a
commit 8a771220e4
21 changed files with 940 additions and 207 deletions

View File

@@ -2,37 +2,30 @@
public partial class App : Application
{
private bool _openedSecondWindow;
public App()
{
InitializeComponent();
var username = Environment.GetCommandLineArgs()
.Skip(1)
.Chunk(2)
.Where(x => x.Length == 2 && x[0] == "--user")
.Select(x => x[1])
.FirstOrDefault();
if (string.IsNullOrWhiteSpace(username))
{
throw new Exception("Missing required --user argument. Example: --user Keeper317");
}
ClientSession.Username = username;
}
protected override Window CreateWindow(IActivationState? activationState)
{
var keeperWindow = new Window(new MainPage("Keeper317"))
return new Window(new MainPage(ClientSession.Username))
{
Title = "Relay Client - Keeper317"
Title = $"Relay Client - {ClientSession.Username}"
};
keeperWindow.Created += KeeperWindow_Created;
return keeperWindow;
}
private void KeeperWindow_Created(object? sender, EventArgs e)
{
if (_openedSecondWindow)
return;
_openedSecondWindow = false;
var kiraWindow = new Window(new MainPage("Ru_Kira"))
{
Title = "Relay Client - Ru_Kira"
};
Current?.OpenWindow(kiraWindow);
}
}

View File

@@ -1,22 +1,5 @@
namespace RelayClient;
public static class ChatSimulator
{
public static event Action<ChatMessage>? MessageSent;
public static void Send(string senderUsername, string text)
{
var message = new ChatMessage
{
SenderUsername = senderUsername,
Text = text,
Timestamp = DateTime.Now
};
MessageSent?.Invoke(message);
}
}
public sealed class ChatMessage
{
public required string SenderUsername { get; set; }

View File

@@ -0,0 +1,6 @@
namespace RelayClient;
public static class ClientSession
{
public static string Username { get; set; } = "Unknown";
}

View File

@@ -9,35 +9,33 @@ public static class E2EeHelper
{
using var rsa = RSA.Create(2048);
var publicKey = Convert.ToBase64String(rsa.ExportSubjectPublicKeyInfo());
var privateKey = Convert.ToBase64String(rsa.ExportPkcs8PrivateKey());
return (publicKey, privateKey);
return (
Convert.ToBase64String(rsa.ExportSubjectPublicKeyInfo()),
Convert.ToBase64String(rsa.ExportPkcs8PrivateKey())
);
}
public static EncryptedMessagePayload EncryptForRecipient(string plainText, string recipientPublicKeyBase64)
public static EncryptedPayload EncryptForRecipient(string plainText, string recipientPublicKeyBase64)
{
var aesKey = RandomNumberGenerator.GetBytes(32);
var nonce = RandomNumberGenerator.GetBytes(12);
var plainBytes = Encoding.UTF8.GetBytes(plainText);
var cipherBytes = new byte[plainBytes.Length];
var tag = new byte[16];
byte[] aesKey = RandomNumberGenerator.GetBytes(32);
byte[] nonce = RandomNumberGenerator.GetBytes(12);
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
byte[] cipherBytes = new byte[plainBytes.Length];
byte[] tag = new byte[16];
using (var aes = new AesGcm(aesKey, 16))
{
aes.Encrypt(nonce, plainBytes, cipherBytes, tag);
}
var recipientPublicKey = Convert.FromBase64String(recipientPublicKeyBase64);
byte[] encryptedKey;
using (var rsa = RSA.Create())
{
rsa.ImportSubjectPublicKeyInfo(recipientPublicKey, out _);
rsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(recipientPublicKeyBase64), out _);
encryptedKey = rsa.Encrypt(aesKey, RSAEncryptionPadding.OaepSHA256);
}
return new EncryptedMessagePayload
return new EncryptedPayload
{
CipherText = Convert.ToBase64String(cipherBytes),
Nonce = Convert.ToBase64String(nonce),
@@ -46,34 +44,32 @@ public static class E2EeHelper
};
}
public static string DecryptForRecipient(EncryptedMessagePayload payload, string recipientPrivateKeyBase64)
public static string DecryptForRecipient(EncryptedPayload payload, string recipientPrivateKeyBase64)
{
var encryptedKey = Convert.FromBase64String(payload.EncryptedKey);
var privateKey = Convert.FromBase64String(recipientPrivateKeyBase64);
byte[] aesKey;
using (var rsa = RSA.Create())
{
rsa.ImportPkcs8PrivateKey(privateKey, out _);
aesKey = rsa.Decrypt(encryptedKey, RSAEncryptionPadding.OaepSHA256);
rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(recipientPrivateKeyBase64), out _);
aesKey = rsa.Decrypt(Convert.FromBase64String(payload.EncryptedKey), RSAEncryptionPadding.OaepSHA256);
}
var nonce = Convert.FromBase64String(payload.Nonce);
var tag = Convert.FromBase64String(payload.Tag);
var cipherBytes = Convert.FromBase64String(payload.CipherText);
var plainBytes = new byte[cipherBytes.Length];
byte[] plainBytes = new byte[Convert.FromBase64String(payload.CipherText).Length];
using (var aes = new AesGcm(aesKey, 16))
{
aes.Decrypt(nonce, cipherBytes, tag, plainBytes);
aes.Decrypt(
Convert.FromBase64String(payload.Nonce),
Convert.FromBase64String(payload.CipherText),
Convert.FromBase64String(payload.Tag),
plainBytes
);
}
return Encoding.UTF8.GetString(plainBytes);
}
}
public class EncryptedMessagePayload
public class EncryptedPayload
{
public required string CipherText { get; set; }
public required string Nonce { get; set; }

View File

@@ -2,19 +2,36 @@ namespace RelayClient.Crypto;
public static class KeyStorage
{
private static string GetKeyFolder()
{
var folder = Path.Combine(FileSystem.AppDataDirectory, "keys");
Directory.CreateDirectory(folder);
return folder;
}
public static void SavePrivateKey(string username, string privateKey)
{
Directory.CreateDirectory("keys");
File.WriteAllText(Path.Combine("keys", $"{username}.private.key"), privateKey);
File.WriteAllText(Path.Combine(GetKeyFolder(), $"{username}.private.key"), privateKey);
}
public static void SavePublicKey(string username, string publicKey)
{
File.WriteAllText(Path.Combine(GetKeyFolder(), $"{username}.public.key"), publicKey);
}
public static string LoadPrivateKey(string username)
{
return File.ReadAllText(Path.Combine("keys", $"{username}.private.key"));
return File.ReadAllText(Path.Combine(GetKeyFolder(), $"{username}.private.key"));
}
public static bool PrivateKeyExists(string username)
public static string LoadPublicKey(string username)
{
return File.Exists(Path.Combine("keys", $"{username}.private.key"));
return File.ReadAllText(Path.Combine(GetKeyFolder(), $"{username}.public.key"));
}
public static bool HasKeys(string username)
{
return File.Exists(Path.Combine(GetKeyFolder(), $"{username}.private.key")) &&
File.Exists(Path.Combine(GetKeyFolder(), $"{username}.public.key"));
}
}

View File

@@ -1,8 +1,15 @@
namespace RelayClient;
using RelayClient.Crypto;
using RelayClient.Models;
using WebSocketSharp;
using System.Text.Json;
namespace RelayClient;
public partial class MainPage : ContentPage
{
private readonly string _username;
private readonly WebSocket wsc;
private string? _serverPublicKey;
public MainPage(string username)
{
@@ -10,87 +17,188 @@ public partial class MainPage : ContentPage
_username = username;
UserLabel.Text = $"Logged in as: {_username}";
if (!KeyStorage.HasKeys(_username))
{
var keys = E2EeHelper.GenerateRsaKeyPair();
KeyStorage.SavePrivateKey(_username, keys.privateKey);
KeyStorage.SavePublicKey(_username, keys.publicKey);
}
ChatSimulator.MessageSent += OnMessageSent;
wsc = new WebSocket("ws://localhost:1337/");
wsc.OnMessage += WscOnMessage;
wsc.Connect();
var publicKey = KeyStorage.LoadPublicKey(_username);
wsc.Send($"REGISTER_KEY|{_username}|{publicKey}");
wsc.Send("GET_SERVER_KEY");
wsc.Send($"GET_HISTORY|{_username}");
}
private void SendButton_OnClicked(object? sender, EventArgs e)
{
SendMessage(sender, e);
SendMessage();
}
private void MessageEntry_OnCompleted(object? sender, EventArgs e)
{
SendMessage(sender, e);
SendMessage();
}
private void SendMessage(object? sender, EventArgs e)
private void SendMessage()
{
var text = MessageEntry.Text?.Trim();
if (string.IsNullOrWhiteSpace(text))
return;
MauiProgram.wsc.Send($"{_username}:{text}");
// ChatSimulator.Send(_username, text);
if (string.IsNullOrWhiteSpace(_serverPublicKey))
{
Console.WriteLine("Server public key not loaded yet.");
return;
}
Console.WriteLine($"[{_username}] sent message: {text}");
var encrypted = E2EeHelper.EncryptForRecipient(text, _serverPublicKey);
var payload = new SocketEncryptedMessage
{
Type = "client_encrypted_chat",
SenderUsername = _username,
CipherText = encrypted.CipherText,
Nonce = encrypted.Nonce,
Tag = encrypted.Tag,
EncryptedKey = encrypted.EncryptedKey
};
var json = JsonSerializer.Serialize(payload);
wsc.Send(json);
Console.WriteLine($"[{_username}] sent encrypted message.");
MessageEntry.Text = string.Empty;
MessageEntry.Focus();
}
private void OnMessageSent(ChatMessage message)
private void WscOnMessage(object? sender, MessageEventArgs e)
{
MainThread.BeginInvokeOnMainThread(async () =>
if (e.Data.StartsWith("SERVER:REGISTERED_KEY:"))
{
bool isOwnMessage = message.SenderUsername == _username;
Console.WriteLine(e.Data);
return;
}
var bubble = new Border
Console.WriteLine($"[{_username}] RAW WS DATA: {e.Data}");
try
{
using var doc = JsonDocument.Parse(e.Data);
var root = doc.RootElement;
if (!root.TryGetProperty("Type", out var typeElement))
return;
var type = typeElement.GetString();
if (type == "server_public_key")
{
StrokeThickness = 1,
Padding = 10,
Margin = isOwnMessage
? new Thickness(40, 0, 0, 0)
: new Thickness(0, 0, 40, 0),
HorizontalOptions = isOwnMessage
? LayoutOptions.End
: LayoutOptions.Start,
Content = new VerticalStackLayout
var serverKeyMessage = JsonSerializer.Deserialize<ServerPublicKeyMessage>(e.Data);
if (serverKeyMessage is not null)
{
Spacing = 2,
Children =
{
new Label
{
Text = message.SenderUsername,
FontAttributes = FontAttributes.Bold,
FontSize = 12
},
new Label
{
Text = message.Text,
FontSize = 14
},
new Label
{
Text = message.Timestamp.ToString("h:mm tt"),
FontSize = 10
}
}
_serverPublicKey = serverKeyMessage.PublicKey;
Console.WriteLine($"[{_username}] loaded server public key.");
}
return;
}
if (type != "encrypted_chat")
return;
var payload = JsonSerializer.Deserialize<SocketEncryptedMessage>(e.Data);
if (payload is null)
return;
if (payload.RecipientUsername != _username)
return;
Console.WriteLine($"[{_username}] received encrypted payload for {payload.RecipientUsername}");
var privateKey = KeyStorage.LoadPrivateKey(_username);
var decryptedText = E2EeHelper.DecryptForRecipient(
new EncryptedPayload
{
CipherText = payload.CipherText,
Nonce = payload.Nonce,
Tag = payload.Tag,
EncryptedKey = payload.EncryptedKey
},
privateKey
);
Console.WriteLine($"[{_username}] decrypted message from {payload.SenderUsername}: {decryptedText}");
var message = new ChatMessage
{
SenderUsername = payload.SenderUsername,
Text = decryptedText,
Timestamp = DateTime.Now
};
MessagesLayout.Children.Add(bubble);
await MessagesScrollView.ScrollToAsync(MessagesLayout, ScrollToPosition.End, true);
});
MainThread.BeginInvokeOnMainThread(async () =>
{
bool isOwnMessage = message.SenderUsername == _username;
var bubble = new Border
{
StrokeThickness = 1,
Padding = 10,
Margin = isOwnMessage
? new Thickness(40, 0, 0, 0)
: new Thickness(0, 0, 40, 0),
HorizontalOptions = isOwnMessage
? LayoutOptions.End
: LayoutOptions.Start,
Content = new VerticalStackLayout
{
Spacing = 2,
Children =
{
new Label
{
Text = message.SenderUsername,
FontAttributes = FontAttributes.Bold,
FontSize = 12
},
new Label
{
Text = message.Text,
FontSize = 14
},
new Label
{
Text = message.Timestamp.ToString("h:mm tt"),
FontSize = 10
}
}
}
};
MessagesLayout.Children.Add(bubble);
await MessagesScrollView.ScrollToAsync(MessagesLayout, ScrollToPosition.End, true);
});
}
catch (Exception ex)
{
Console.WriteLine($"[{_username}] failed to process websocket message: {ex.Message}");
}
}
protected override void OnDisappearing()
{
ChatSimulator.MessageSent -= OnMessageSent;
wsc.OnMessage -= WscOnMessage;
wsc.Close();
base.OnDisappearing();
}
}

View File

@@ -6,11 +6,10 @@ namespace RelayClient;
public static class MauiProgram
{
// public static event Action<ChatMessage>? MessageSent;
public static WebSocket wsc = new WebSocket("ws://localhost:1337");
public static MauiApp CreateMauiApp()
{
wsc.OnMessage += (sender, e) => OnWebSocketRecieved(sender, e);
wsc.Connect();
//wsc.OnMessage += (sender, e) => OnWebSocketRecieved(sender, e);
//wsc.Connect();
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>().ConfigureFonts(fonts =>
{
@@ -29,19 +28,18 @@ public static class MauiProgram
return builder.Build();
}
public static void OnWebSocketRecieved(object? sender, MessageEventArgs e)
{
Console.WriteLine(sender.ToString());
ChatSimulator.Send(e.Data.Split(":")[0], e.Data.Split(":")[1]);
// var message = new ChatMessage
// {
// SenderUsername = e.Data.Split(":")[0],
// Text = e.Data.Split(":")[1],
// Timestamp = DateTime.Now
// };
//
// MessageSent?.Invoke(message);
}
//public static void OnWebSocketRecieved(object? sender, MessageEventArgs e)
//{
// Console.WriteLine(sender.ToString());
//
// ChatSimulator.Send(e.Data.Split(":")[0], e.Data.Split(":")[1]);
// // var message = new ChatMessage
// // {
// // SenderUsername = e.Data.Split(":")[0],
// // Text = e.Data.Split(":")[1],
// // Timestamp = DateTime.Now
// // };
// //
// // MessageSent?.Invoke(message);
//}
}

View File

@@ -0,0 +1,7 @@
namespace RelayClient.Models;
public class ServerPublicKeyMessage
{
public required string Type { get; set; }
public required string PublicKey { get; set; }
}

View File

@@ -0,0 +1,12 @@
namespace RelayClient.Models;
public class SocketEncryptedMessage
{
public required string Type { get; set; }
public required string SenderUsername { get; set; }
public string? RecipientUsername { get; set; }
public required string CipherText { get; set; }
public required string Nonce { get; set; }
public required string Tag { get; set; }
public required string EncryptedKey { get; set; }
}