Update: Full E2EE + Scripts
This commit is contained in:
@@ -2,37 +2,30 @@
|
|||||||
|
|
||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
private bool _openedSecondWindow;
|
|
||||||
|
|
||||||
public App()
|
public App()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
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)
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,5 @@
|
|||||||
namespace RelayClient;
|
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 sealed class ChatMessage
|
||||||
{
|
{
|
||||||
public required string SenderUsername { get; set; }
|
public required string SenderUsername { get; set; }
|
||||||
|
|||||||
6
RelayClient/ClientSession.cs
Normal file
6
RelayClient/ClientSession.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace RelayClient;
|
||||||
|
|
||||||
|
public static class ClientSession
|
||||||
|
{
|
||||||
|
public static string Username { get; set; } = "Unknown";
|
||||||
|
}
|
||||||
@@ -9,35 +9,33 @@ public static class E2EeHelper
|
|||||||
{
|
{
|
||||||
using var rsa = RSA.Create(2048);
|
using var rsa = RSA.Create(2048);
|
||||||
|
|
||||||
var publicKey = Convert.ToBase64String(rsa.ExportSubjectPublicKeyInfo());
|
return (
|
||||||
var privateKey = Convert.ToBase64String(rsa.ExportPkcs8PrivateKey());
|
Convert.ToBase64String(rsa.ExportSubjectPublicKeyInfo()),
|
||||||
|
Convert.ToBase64String(rsa.ExportPkcs8PrivateKey())
|
||||||
return (publicKey, privateKey);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static EncryptedMessagePayload EncryptForRecipient(string plainText, string recipientPublicKeyBase64)
|
public static EncryptedPayload EncryptForRecipient(string plainText, string recipientPublicKeyBase64)
|
||||||
{
|
{
|
||||||
var aesKey = RandomNumberGenerator.GetBytes(32);
|
byte[] aesKey = RandomNumberGenerator.GetBytes(32);
|
||||||
var nonce = RandomNumberGenerator.GetBytes(12);
|
byte[] nonce = RandomNumberGenerator.GetBytes(12);
|
||||||
var plainBytes = Encoding.UTF8.GetBytes(plainText);
|
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
|
||||||
var cipherBytes = new byte[plainBytes.Length];
|
byte[] cipherBytes = new byte[plainBytes.Length];
|
||||||
var tag = new byte[16];
|
byte[] tag = new byte[16];
|
||||||
|
|
||||||
using (var aes = new AesGcm(aesKey, 16))
|
using (var aes = new AesGcm(aesKey, 16))
|
||||||
{
|
{
|
||||||
aes.Encrypt(nonce, plainBytes, cipherBytes, tag);
|
aes.Encrypt(nonce, plainBytes, cipherBytes, tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
var recipientPublicKey = Convert.FromBase64String(recipientPublicKeyBase64);
|
|
||||||
byte[] encryptedKey;
|
byte[] encryptedKey;
|
||||||
|
|
||||||
using (var rsa = RSA.Create())
|
using (var rsa = RSA.Create())
|
||||||
{
|
{
|
||||||
rsa.ImportSubjectPublicKeyInfo(recipientPublicKey, out _);
|
rsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(recipientPublicKeyBase64), out _);
|
||||||
encryptedKey = rsa.Encrypt(aesKey, RSAEncryptionPadding.OaepSHA256);
|
encryptedKey = rsa.Encrypt(aesKey, RSAEncryptionPadding.OaepSHA256);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new EncryptedMessagePayload
|
return new EncryptedPayload
|
||||||
{
|
{
|
||||||
CipherText = Convert.ToBase64String(cipherBytes),
|
CipherText = Convert.ToBase64String(cipherBytes),
|
||||||
Nonce = Convert.ToBase64String(nonce),
|
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;
|
byte[] aesKey;
|
||||||
|
|
||||||
using (var rsa = RSA.Create())
|
using (var rsa = RSA.Create())
|
||||||
{
|
{
|
||||||
rsa.ImportPkcs8PrivateKey(privateKey, out _);
|
rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(recipientPrivateKeyBase64), out _);
|
||||||
aesKey = rsa.Decrypt(encryptedKey, RSAEncryptionPadding.OaepSHA256);
|
aesKey = rsa.Decrypt(Convert.FromBase64String(payload.EncryptedKey), RSAEncryptionPadding.OaepSHA256);
|
||||||
}
|
}
|
||||||
|
|
||||||
var nonce = Convert.FromBase64String(payload.Nonce);
|
byte[] plainBytes = new byte[Convert.FromBase64String(payload.CipherText).Length];
|
||||||
var tag = Convert.FromBase64String(payload.Tag);
|
|
||||||
var cipherBytes = Convert.FromBase64String(payload.CipherText);
|
|
||||||
var plainBytes = new byte[cipherBytes.Length];
|
|
||||||
|
|
||||||
using (var aes = new AesGcm(aesKey, 16))
|
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);
|
return Encoding.UTF8.GetString(plainBytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class EncryptedMessagePayload
|
public class EncryptedPayload
|
||||||
{
|
{
|
||||||
public required string CipherText { get; set; }
|
public required string CipherText { get; set; }
|
||||||
public required string Nonce { get; set; }
|
public required string Nonce { get; set; }
|
||||||
|
|||||||
@@ -2,19 +2,36 @@ namespace RelayClient.Crypto;
|
|||||||
|
|
||||||
public static class KeyStorage
|
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)
|
public static void SavePrivateKey(string username, string privateKey)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory("keys");
|
File.WriteAllText(Path.Combine(GetKeyFolder(), $"{username}.private.key"), privateKey);
|
||||||
File.WriteAllText(Path.Combine("keys", $"{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)
|
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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
public partial class MainPage : ContentPage
|
||||||
{
|
{
|
||||||
private readonly string _username;
|
private readonly string _username;
|
||||||
|
private readonly WebSocket wsc;
|
||||||
|
private string? _serverPublicKey;
|
||||||
|
|
||||||
public MainPage(string username)
|
public MainPage(string username)
|
||||||
{
|
{
|
||||||
@@ -11,39 +18,134 @@ public partial class MainPage : ContentPage
|
|||||||
_username = username;
|
_username = username;
|
||||||
UserLabel.Text = $"Logged in as: {_username}";
|
UserLabel.Text = $"Logged in as: {_username}";
|
||||||
|
|
||||||
ChatSimulator.MessageSent += OnMessageSent;
|
if (!KeyStorage.HasKeys(_username))
|
||||||
|
{
|
||||||
|
var keys = E2EeHelper.GenerateRsaKeyPair();
|
||||||
|
KeyStorage.SavePrivateKey(_username, keys.privateKey);
|
||||||
|
KeyStorage.SavePublicKey(_username, keys.publicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
private void SendButton_OnClicked(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
SendMessage(sender, e);
|
SendMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MessageEntry_OnCompleted(object? sender, EventArgs e)
|
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();
|
var text = MessageEntry.Text?.Trim();
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
MauiProgram.wsc.Send($"{_username}:{text}");
|
if (string.IsNullOrWhiteSpace(_serverPublicKey))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Server public key not loaded yet.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// ChatSimulator.Send(_username, text);
|
var encrypted = E2EeHelper.EncryptForRecipient(text, _serverPublicKey);
|
||||||
|
|
||||||
Console.WriteLine($"[{_username}] sent message: {text}");
|
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.Text = string.Empty;
|
||||||
MessageEntry.Focus();
|
MessageEntry.Focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnMessageSent(ChatMessage message)
|
private void WscOnMessage(object? sender, MessageEventArgs e)
|
||||||
{
|
{
|
||||||
|
if (e.Data.StartsWith("SERVER:REGISTERED_KEY:"))
|
||||||
|
{
|
||||||
|
Console.WriteLine(e.Data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
{
|
||||||
|
var serverKeyMessage = JsonSerializer.Deserialize<ServerPublicKeyMessage>(e.Data);
|
||||||
|
if (serverKeyMessage is not null)
|
||||||
|
{
|
||||||
|
_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
|
||||||
|
};
|
||||||
|
|
||||||
MainThread.BeginInvokeOnMainThread(async () =>
|
MainThread.BeginInvokeOnMainThread(async () =>
|
||||||
{
|
{
|
||||||
bool isOwnMessage = message.SenderUsername == _username;
|
bool isOwnMessage = message.SenderUsername == _username;
|
||||||
@@ -87,10 +189,16 @@ public partial class MainPage : ContentPage
|
|||||||
await MessagesScrollView.ScrollToAsync(MessagesLayout, ScrollToPosition.End, true);
|
await MessagesScrollView.ScrollToAsync(MessagesLayout, ScrollToPosition.End, true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[{_username}] failed to process websocket message: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnDisappearing()
|
protected override void OnDisappearing()
|
||||||
{
|
{
|
||||||
ChatSimulator.MessageSent -= OnMessageSent;
|
wsc.OnMessage -= WscOnMessage;
|
||||||
|
wsc.Close();
|
||||||
base.OnDisappearing();
|
base.OnDisappearing();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,11 +6,10 @@ namespace RelayClient;
|
|||||||
public static class MauiProgram
|
public static class MauiProgram
|
||||||
{
|
{
|
||||||
// public static event Action<ChatMessage>? MessageSent;
|
// public static event Action<ChatMessage>? MessageSent;
|
||||||
public static WebSocket wsc = new WebSocket("ws://localhost:1337");
|
|
||||||
public static MauiApp CreateMauiApp()
|
public static MauiApp CreateMauiApp()
|
||||||
{
|
{
|
||||||
wsc.OnMessage += (sender, e) => OnWebSocketRecieved(sender, e);
|
//wsc.OnMessage += (sender, e) => OnWebSocketRecieved(sender, e);
|
||||||
wsc.Connect();
|
//wsc.Connect();
|
||||||
var builder = MauiApp.CreateBuilder();
|
var builder = MauiApp.CreateBuilder();
|
||||||
builder.UseMauiApp<App>().ConfigureFonts(fonts =>
|
builder.UseMauiApp<App>().ConfigureFonts(fonts =>
|
||||||
{
|
{
|
||||||
@@ -29,19 +28,18 @@ public static class MauiProgram
|
|||||||
return builder.Build();
|
return builder.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void OnWebSocketRecieved(object? sender, MessageEventArgs e)
|
//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],
|
// Console.WriteLine(sender.ToString());
|
||||||
// Text = e.Data.Split(":")[1],
|
|
||||||
// Timestamp = DateTime.Now
|
|
||||||
// };
|
|
||||||
//
|
//
|
||||||
// MessageSent?.Invoke(message);
|
// 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);
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
7
RelayClient/Models/ServerPublicKeyMessage.cs
Normal file
7
RelayClient/Models/ServerPublicKeyMessage.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace RelayClient.Models;
|
||||||
|
|
||||||
|
public class ServerPublicKeyMessage
|
||||||
|
{
|
||||||
|
public required string Type { get; set; }
|
||||||
|
public required string PublicKey { get; set; }
|
||||||
|
}
|
||||||
12
RelayClient/Models/SocketEncryptedMessage.cs
Normal file
12
RelayClient/Models/SocketEncryptedMessage.cs
Normal 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; }
|
||||||
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
using SurrealDb.Net;
|
using SurrealDb.Net;
|
||||||
using SurrealDb.Net.Models;
|
|
||||||
using SurrealDb.Net.Models.Auth;
|
using SurrealDb.Net.Models.Auth;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using RelayCore;
|
|
||||||
using RelayCore.Enums;
|
using RelayCore.Enums;
|
||||||
using RelayCore.Models;
|
using RelayCore.Models;
|
||||||
|
|
||||||
@@ -13,9 +11,11 @@ await db.Use("test", "test");
|
|||||||
|
|
||||||
var keeper = await CreateUserAsync(db, "Keeper317", "Keeper317@gmail.com", "password");
|
var keeper = await CreateUserAsync(db, "Keeper317", "Keeper317@gmail.com", "password");
|
||||||
var kira = await CreateUserAsync(db, "Ru_Kira", "jduesling13@gmail.com", "password");
|
var kira = await CreateUserAsync(db, "Ru_Kira", "jduesling13@gmail.com", "password");
|
||||||
|
var test = await CreateUserAsync(db, "Test", "test@gmail.com", "password");
|
||||||
|
|
||||||
Console.WriteLine($"Keeper created: {ToJsonString(keeper)}");
|
Console.WriteLine($"Keeper created: {ToJsonString(keeper)}");
|
||||||
Console.WriteLine($"Kira created: {ToJsonString(kira)}");
|
Console.WriteLine($"Kira created: {ToJsonString(kira)}");
|
||||||
|
Console.WriteLine($"Test created: {ToJsonString(test)}");
|
||||||
Console.ReadKey(true);
|
Console.ReadKey(true);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
11
RelayServer/Models/ClientPublicKeys.cs
Normal file
11
RelayServer/Models/ClientPublicKeys.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using SurrealDb.Net.Models;
|
||||||
|
|
||||||
|
namespace RelayServer.Models;
|
||||||
|
|
||||||
|
public class ClientPublicKeys : Record
|
||||||
|
{
|
||||||
|
public required string Username { get; set; }
|
||||||
|
public required string PublicKey { get; set; }
|
||||||
|
public required DateTime CreatedAt { get; set; }
|
||||||
|
public required DateTime UpdatedAt { get; set; }
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ namespace RelayServer.Models;
|
|||||||
public class ServerEncryptionKeys : Record
|
public class ServerEncryptionKeys : Record
|
||||||
{
|
{
|
||||||
public required string KeyBase64 { get; set; }
|
public required string KeyBase64 { get; set; }
|
||||||
|
public required string PublicKey { get; set; }
|
||||||
|
public required string PrivateKey { get; set; }
|
||||||
public required DateTime CreatedAt { get; set; }
|
public required DateTime CreatedAt { get; set; }
|
||||||
public required DateTime UpdatedAt { get; set; }
|
public required DateTime UpdatedAt { get; set; }
|
||||||
}
|
}
|
||||||
7
RelayServer/Models/ServerPublicKeyMessage.cs
Normal file
7
RelayServer/Models/ServerPublicKeyMessage.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace RelayServer.Models;
|
||||||
|
|
||||||
|
public class ServerPublicKeyMessage
|
||||||
|
{
|
||||||
|
public required string Type { get; set; }
|
||||||
|
public required string PublicKey { get; set; }
|
||||||
|
}
|
||||||
12
RelayServer/Models/SocketEncryptedMessage.cs
Normal file
12
RelayServer/Models/SocketEncryptedMessage.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace RelayServer.Models;
|
||||||
|
|
||||||
|
public class SocketEncryptedMessage
|
||||||
|
{
|
||||||
|
public required string Type { get; set; }
|
||||||
|
public required string SenderUsername { get; set; }
|
||||||
|
public required 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; }
|
||||||
|
}
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System;
|
|
||||||
using WebSocketSharp.Server;
|
|
||||||
using WebSocketSharp;
|
|
||||||
|
|
||||||
using RelayServer.Models;
|
|
||||||
using RelayServer.Services;
|
using RelayServer.Services;
|
||||||
|
using WebSocketSharp.Server;
|
||||||
|
using RelayServer.Models;
|
||||||
|
|
||||||
var surrealService = new SurrealService();
|
var surrealService = new SurrealService();
|
||||||
var coreClient = new CoreClientService();
|
var coreClient = new CoreClientService();
|
||||||
@@ -12,23 +9,25 @@ var cryptoService = new ChannelCryptoService();
|
|||||||
|
|
||||||
await using var db = await surrealService.ConnectAsync();
|
await using var db = await surrealService.ConnectAsync();
|
||||||
|
|
||||||
|
ChatTest.ClientKeyService = new ClientKeyService(db);
|
||||||
|
ChatTest.Db = db;
|
||||||
|
|
||||||
var wssv = new WebSocketServer("ws://localhost:1337");
|
var wssv = new WebSocketServer("ws://localhost:1337");
|
||||||
wssv.AddWebSocketService<ChatTest>("/");
|
wssv.AddWebSocketService<ChatTest>("/");
|
||||||
wssv.Start();
|
wssv.Start();
|
||||||
Console.WriteLine("WebSocket server started");
|
Console.WriteLine("WebSocket server started");
|
||||||
Console.ReadKey(true);
|
|
||||||
wssv.Stop();
|
|
||||||
|
|
||||||
var keeper = await coreClient.GetUserByUsernameAsync("Keeper317");
|
var keeper = await coreClient.GetUserByUsernameAsync("Keeper317");
|
||||||
var kira = await coreClient.GetUserByUsernameAsync("Ru_Kira");
|
var kira = await coreClient.GetUserByUsernameAsync("Ru_Kira");
|
||||||
|
var test = await coreClient.GetUserByUsernameAsync("Test");
|
||||||
|
|
||||||
if (keeper is null || kira is null)
|
if (keeper is null || kira is null || test is null)
|
||||||
{
|
{
|
||||||
Console.WriteLine("One or more required users do not exist in RelayCore.");
|
Console.WriteLine("One or more required users do not exist in RelayCore.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!keeper.Licensed || !kira.Licensed)
|
if (!keeper.Licensed || !kira.Licensed || !test.Licensed)
|
||||||
{
|
{
|
||||||
Console.WriteLine("One or more required users are not licensed.");
|
Console.WriteLine("One or more required users are not licensed.");
|
||||||
return;
|
return;
|
||||||
@@ -36,11 +35,12 @@ if (!keeper.Licensed || !kira.Licensed)
|
|||||||
|
|
||||||
Console.WriteLine($"Core verified user: {keeper.Username}");
|
Console.WriteLine($"Core verified user: {keeper.Username}");
|
||||||
Console.WriteLine($"Core verified user: {kira.Username}");
|
Console.WriteLine($"Core verified user: {kira.Username}");
|
||||||
|
Console.WriteLine($"Core verified user: {test.Username}");
|
||||||
|
|
||||||
var server = await db.Create("servers", new Servers
|
var server = await db.Create("servers", new Servers
|
||||||
{
|
{
|
||||||
Name = "Test Server",
|
Name = "Test Server",
|
||||||
OwnerUserId = kira.Id,
|
OwnerUserId = keeper.Id,
|
||||||
CreatedAt = DateTime.UtcNow
|
CreatedAt = DateTime.UtcNow
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -60,6 +60,13 @@ var kiraMember = await db.Create("server_members", new ServerMembers
|
|||||||
IsOwner = false
|
IsOwner = false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var testMember = await db.Create("server_members", new ServerMembers
|
||||||
|
{
|
||||||
|
UserId = test.Id,
|
||||||
|
JoinedAt = DateTime.UtcNow,
|
||||||
|
IsOwner = false
|
||||||
|
});
|
||||||
|
|
||||||
Console.WriteLine("Server members created.");
|
Console.WriteLine("Server members created.");
|
||||||
|
|
||||||
var channel = await db.Create("channels", new Channels
|
var channel = await db.Create("channels", new Channels
|
||||||
@@ -72,66 +79,29 @@ Console.WriteLine($"Channel created: {ToJsonString(channel)}");
|
|||||||
|
|
||||||
var channelId = GetRecordId(channel.Id);
|
var channelId = GetRecordId(channel.Id);
|
||||||
Console.WriteLine($"Resolved channelId: {channelId}");
|
Console.WriteLine($"Resolved channelId: {channelId}");
|
||||||
|
ChatTest.DefaultChannelId = channelId;
|
||||||
|
|
||||||
var keyBase64 = cryptoService.GenerateKey();
|
var keyBase64 = cryptoService.GenerateKey();
|
||||||
|
var serverKeys = E2EeHelper.GenerateRsaKeyPair();
|
||||||
|
|
||||||
var serverKey = await db.Create("server_encryption_keys", new ServerEncryptionKeys
|
var serverKey = await db.Create("server_encryption_keys", new ServerEncryptionKeys
|
||||||
{
|
{
|
||||||
KeyBase64 = keyBase64,
|
KeyBase64 = keyBase64,
|
||||||
|
PublicKey = serverKeys.publicKey,
|
||||||
|
PrivateKey = serverKeys.privateKey,
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
UpdatedAt = DateTime.UtcNow
|
UpdatedAt = DateTime.UtcNow
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ChatTest.ServerPublicKey = serverKeys.publicKey;
|
||||||
|
ChatTest.ServerPrivateKey = serverKeys.privateKey;
|
||||||
|
ChatTest.ChannelDbKey = keyBase64;
|
||||||
|
|
||||||
Console.WriteLine("Server encryption key created.");
|
Console.WriteLine("Server encryption key created.");
|
||||||
|
|
||||||
var encrypted = cryptoService.Encrypt("hello from Keeper317 in #general", keyBase64);
|
Console.ReadKey(true);
|
||||||
|
wssv.Stop();
|
||||||
|
|
||||||
var savedMessage = await db.Create("channel_messages", new ChannelMessages
|
|
||||||
{
|
|
||||||
ChannelId = channelId,
|
|
||||||
SenderUserId = keeper.Id,
|
|
||||||
CipherText = encrypted.cipherText,
|
|
||||||
Nonce = encrypted.nonce,
|
|
||||||
Tag = encrypted.tag,
|
|
||||||
CreatedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
|
|
||||||
Console.WriteLine($"Encrypted message saved: {ToJsonString(savedMessage)}");
|
|
||||||
|
|
||||||
var decrypted = cryptoService.Decrypt(
|
|
||||||
savedMessage.CipherText,
|
|
||||||
savedMessage.Nonce,
|
|
||||||
savedMessage.Tag,
|
|
||||||
keyBase64
|
|
||||||
);
|
|
||||||
|
|
||||||
var storedMessages = await db.Select<ChannelMessages>("channel_messages");
|
|
||||||
|
|
||||||
Console.WriteLine("Stored DB messages:");
|
|
||||||
Console.WriteLine(ToJsonString(storedMessages));
|
|
||||||
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine($"Decrypted message: {decrypted}");
|
|
||||||
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine("Simulating Kira reading #general...");
|
|
||||||
|
|
||||||
var kiraVisibleMessages = storedMessages
|
|
||||||
.Where(m => m.ChannelId == channelId)
|
|
||||||
.OrderBy(m => m.CreatedAt)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
foreach (var msg in kiraVisibleMessages)
|
|
||||||
{
|
|
||||||
var plainText = cryptoService.Decrypt(
|
|
||||||
msg.CipherText,
|
|
||||||
msg.Nonce,
|
|
||||||
msg.Tag,
|
|
||||||
keyBase64
|
|
||||||
);
|
|
||||||
|
|
||||||
Console.WriteLine($"Kira reads message from {msg.SenderUserId}: {plainText}");
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
static string ToJsonString(object? obj)
|
static string ToJsonString(object? obj)
|
||||||
@@ -159,14 +129,3 @@ static string GetRecordId(object? id)
|
|||||||
|
|
||||||
return $"{table}:{recordId}";
|
return $"{table}:{recordId}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ChatTest : WebSocketBehavior
|
|
||||||
{
|
|
||||||
protected override void OnMessage(MessageEventArgs e)
|
|
||||||
{
|
|
||||||
// var msg = e.Data.Split(":")[1] == "PING" ? "SERVER:PONG" : "SERVER:RESPONSE";
|
|
||||||
var msg = e.Data;
|
|
||||||
Console.WriteLine(msg);
|
|
||||||
Send(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
274
RelayServer/Services/ChatTest.cs
Normal file
274
RelayServer/Services/ChatTest.cs
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using RelayServer.Models;
|
||||||
|
using WebSocketSharp;
|
||||||
|
using WebSocketSharp.Server;
|
||||||
|
|
||||||
|
namespace RelayServer.Services;
|
||||||
|
|
||||||
|
public class ChatTest : WebSocketBehavior
|
||||||
|
{
|
||||||
|
public static ClientKeyService? ClientKeyService { get; set; }
|
||||||
|
public static string? ServerPublicKey { get; set; }
|
||||||
|
public static string? ServerPrivateKey { get; set; }
|
||||||
|
public static string? ChannelDbKey { get; set; }
|
||||||
|
public static SurrealDb.Net.SurrealDbClient? Db { get; set; }
|
||||||
|
public static string? DefaultChannelId { get; set; }
|
||||||
|
|
||||||
|
protected override void OnMessage(MessageEventArgs e)
|
||||||
|
{
|
||||||
|
var msg = e.Data;
|
||||||
|
Console.WriteLine(msg);
|
||||||
|
|
||||||
|
if (msg.StartsWith("REGISTER_KEY|"))
|
||||||
|
{
|
||||||
|
HandleRegisterKey(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg == "GET_SERVER_KEY")
|
||||||
|
{
|
||||||
|
HandleGetServerKey();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.StartsWith("GET_HISTORY|"))
|
||||||
|
{
|
||||||
|
HandleGetHistory(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleEncryptedClientMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ExtractUsernameFromUserId(string senderUserId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(senderUserId))
|
||||||
|
return "Unknown";
|
||||||
|
|
||||||
|
var parts = senderUserId.Split(':', 2);
|
||||||
|
return parts.Length == 2 ? parts[1] : senderUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleRegisterKey(string msg)
|
||||||
|
{
|
||||||
|
var parts = msg.Split('|', 3);
|
||||||
|
|
||||||
|
if (parts.Length < 3)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Invalid REGISTER_KEY payload.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var username = parts[1];
|
||||||
|
var publicKey = parts[2];
|
||||||
|
|
||||||
|
if (ClientKeyService is null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("ClientKeyService is not initialized.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await ClientKeyService.RegisterOrUpdateKeyAsync(username, publicKey);
|
||||||
|
}).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Send($"SERVER:REGISTERED_KEY:{username}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleGetServerKey()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(ServerPublicKey))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Server public key is not initialized.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = new ServerPublicKeyMessage
|
||||||
|
{
|
||||||
|
Type = "server_public_key",
|
||||||
|
PublicKey = ServerPublicKey
|
||||||
|
};
|
||||||
|
|
||||||
|
Send(JsonSerializer.Serialize(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleEncryptedClientMessage(string msg)
|
||||||
|
{
|
||||||
|
SocketEncryptedMessage? clientPayload;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
clientPayload = JsonSerializer.Deserialize<SocketEncryptedMessage>(msg);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Console.WriteLine("Failed to parse encrypted client payload.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientPayload is null || clientPayload.Type != "client_encrypted_chat")
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (ClientKeyService is null ||
|
||||||
|
Db is null ||
|
||||||
|
string.IsNullOrWhiteSpace(ServerPrivateKey) ||
|
||||||
|
string.IsNullOrWhiteSpace(ChannelDbKey) ||
|
||||||
|
string.IsNullOrWhiteSpace(DefaultChannelId))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Server crypto/database dependencies are not initialized.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string plainText;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
plainText = E2EeHelper.DecryptForRecipient(
|
||||||
|
new EncryptedPayload
|
||||||
|
{
|
||||||
|
CipherText = clientPayload.CipherText,
|
||||||
|
Nonce = clientPayload.Nonce,
|
||||||
|
Tag = clientPayload.Tag,
|
||||||
|
EncryptedKey = clientPayload.EncryptedKey
|
||||||
|
},
|
||||||
|
ServerPrivateKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to decrypt client payload: {ex.Message}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Server decrypted message from {clientPayload.SenderUsername}: {plainText}");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var channelCrypto = new ChannelCryptoService();
|
||||||
|
var dbEncrypted = channelCrypto.Encrypt(plainText, ChannelDbKey);
|
||||||
|
|
||||||
|
var savedMessage = Task.Run(async () =>
|
||||||
|
await Db.Create("channel_messages", new ChannelMessages
|
||||||
|
{
|
||||||
|
ChannelId = DefaultChannelId,
|
||||||
|
SenderUserId = $"users:{clientPayload.SenderUsername.ToLower()}",
|
||||||
|
CipherText = dbEncrypted.cipherText,
|
||||||
|
Nonce = dbEncrypted.nonce,
|
||||||
|
Tag = dbEncrypted.tag,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
})
|
||||||
|
).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Console.WriteLine($"Live message saved to DB: {JsonSerializer.Serialize(savedMessage)}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to save live message to DB: {ex.Message}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var allKeys = Task.Run(async () => await ClientKeyService.GetAllAsync())
|
||||||
|
.GetAwaiter()
|
||||||
|
.GetResult();
|
||||||
|
|
||||||
|
foreach (var client in allKeys)
|
||||||
|
{
|
||||||
|
var encrypted = E2EeHelper.EncryptForRecipient(plainText, client.PublicKey);
|
||||||
|
|
||||||
|
Console.WriteLine($"Encrypting outbound message from {clientPayload.SenderUsername} for {client.Username}");
|
||||||
|
|
||||||
|
var outbound = new SocketEncryptedMessage
|
||||||
|
{
|
||||||
|
Type = "encrypted_chat",
|
||||||
|
SenderUsername = clientPayload.SenderUsername,
|
||||||
|
RecipientUsername = client.Username,
|
||||||
|
CipherText = encrypted.CipherText,
|
||||||
|
Nonce = encrypted.Nonce,
|
||||||
|
Tag = encrypted.Tag,
|
||||||
|
EncryptedKey = encrypted.EncryptedKey
|
||||||
|
};
|
||||||
|
|
||||||
|
Sessions.Broadcast(JsonSerializer.Serialize(outbound));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleGetHistory(string msg)
|
||||||
|
{
|
||||||
|
var parts = msg.Split('|', 2);
|
||||||
|
|
||||||
|
if (parts.Length < 2)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Invalid GET_HISTORY payload.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var username = parts[1];
|
||||||
|
|
||||||
|
if (ClientKeyService is null ||
|
||||||
|
Db is null ||
|
||||||
|
string.IsNullOrWhiteSpace(ChannelDbKey) ||
|
||||||
|
string.IsNullOrWhiteSpace(DefaultChannelId))
|
||||||
|
{
|
||||||
|
Console.WriteLine("History dependencies are not initialized.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetClient = Task.Run(async () => await ClientKeyService.GetByUsernameAsync(username))
|
||||||
|
.GetAwaiter()
|
||||||
|
.GetResult();
|
||||||
|
|
||||||
|
if (targetClient is null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"No public key found for history request user {username}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var allMessages = Task.Run(async () => await Db.Select<ChannelMessages>("channel_messages"))
|
||||||
|
.GetAwaiter()
|
||||||
|
.GetResult();
|
||||||
|
|
||||||
|
var channelMessages = allMessages
|
||||||
|
.Where(m => m.ChannelId == DefaultChannelId)
|
||||||
|
.OrderBy(m => m.CreatedAt)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Console.WriteLine($"Sending {channelMessages.Count} history messages to {username}");
|
||||||
|
|
||||||
|
var channelCrypto = new ChannelCryptoService();
|
||||||
|
|
||||||
|
foreach (var dbMessage in channelMessages)
|
||||||
|
{
|
||||||
|
string plainText;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
plainText = channelCrypto.Decrypt(
|
||||||
|
dbMessage.CipherText,
|
||||||
|
dbMessage.Nonce,
|
||||||
|
dbMessage.Tag,
|
||||||
|
ChannelDbKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to decrypt DB history row {dbMessage.Id}: {ex.Message}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var encrypted = E2EeHelper.EncryptForRecipient(plainText, targetClient.PublicKey);
|
||||||
|
|
||||||
|
var outbound = new SocketEncryptedMessage
|
||||||
|
{
|
||||||
|
Type = "encrypted_chat",
|
||||||
|
SenderUsername = ExtractUsernameFromUserId(dbMessage.SenderUserId),
|
||||||
|
RecipientUsername = username,
|
||||||
|
CipherText = encrypted.CipherText,
|
||||||
|
Nonce = encrypted.Nonce,
|
||||||
|
Tag = encrypted.Tag,
|
||||||
|
EncryptedKey = encrypted.EncryptedKey
|
||||||
|
};
|
||||||
|
|
||||||
|
Send(JsonSerializer.Serialize(outbound));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
61
RelayServer/Services/ClientKeyService.cs
Normal file
61
RelayServer/Services/ClientKeyService.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using RelayServer.Models;
|
||||||
|
using SurrealDb.Net;
|
||||||
|
|
||||||
|
namespace RelayServer.Services;
|
||||||
|
|
||||||
|
public sealed class ClientKeyService
|
||||||
|
{
|
||||||
|
private readonly SurrealDbClient _db;
|
||||||
|
|
||||||
|
public ClientKeyService(SurrealDbClient db)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RegisterOrUpdateKeyAsync(string username, string publicKey)
|
||||||
|
{
|
||||||
|
var allKeys = await _db.Select<ClientPublicKeys>("client_public_keys");
|
||||||
|
|
||||||
|
var existing = allKeys.FirstOrDefault(x => x.Username == username);
|
||||||
|
|
||||||
|
if (existing is null)
|
||||||
|
{
|
||||||
|
await _db.Create("client_public_keys", new ClientPublicKeys
|
||||||
|
{
|
||||||
|
Username = username,
|
||||||
|
PublicKey = publicKey,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
UpdatedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
|
||||||
|
Console.WriteLine($"Stored public key for {username}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.PublicKey = publicKey;
|
||||||
|
existing.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await _db.Merge<ClientPublicKeys, ClientPublicKeys>(new ClientPublicKeys
|
||||||
|
{
|
||||||
|
Id = existing.Id,
|
||||||
|
Username = existing.Username,
|
||||||
|
PublicKey = existing.PublicKey,
|
||||||
|
CreatedAt = existing.CreatedAt,
|
||||||
|
UpdatedAt = existing.UpdatedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
Console.WriteLine($"Updated public key for {username}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ClientPublicKeys?> GetByUsernameAsync(string username)
|
||||||
|
{
|
||||||
|
var allKeys = await _db.Select<ClientPublicKeys>("client_public_keys");
|
||||||
|
return allKeys.FirstOrDefault(x => x.Username == username);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<ClientPublicKeys>> GetAllAsync()
|
||||||
|
{
|
||||||
|
var allKeys = await _db.Select<ClientPublicKeys>("client_public_keys");
|
||||||
|
return allKeys.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ public sealed class CoreClientService
|
|||||||
{
|
{
|
||||||
"Keeper317" => new CoreUser("users:keeper317", "Keeper317", true),
|
"Keeper317" => new CoreUser("users:keeper317", "Keeper317", true),
|
||||||
"Ru_Kira" => new CoreUser("users:ru_kira", "Ru_Kira", true),
|
"Ru_Kira" => new CoreUser("users:ru_kira", "Ru_Kira", true),
|
||||||
|
"Test" => new CoreUser("users:test", "Test", true),
|
||||||
_ => null
|
_ => null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
78
RelayServer/Services/E2EeHelper.cs
Normal file
78
RelayServer/Services/E2EeHelper.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace RelayServer.Services;
|
||||||
|
|
||||||
|
public static class E2EeHelper
|
||||||
|
{
|
||||||
|
public static (string publicKey, string privateKey) GenerateRsaKeyPair()
|
||||||
|
{
|
||||||
|
using var rsa = RSA.Create(2048);
|
||||||
|
|
||||||
|
return (
|
||||||
|
Convert.ToBase64String(rsa.ExportSubjectPublicKeyInfo()),
|
||||||
|
Convert.ToBase64String(rsa.ExportPkcs8PrivateKey())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EncryptedPayload EncryptForRecipient(string plainText, string recipientPublicKeyBase64)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] encryptedKey;
|
||||||
|
using (var rsa = RSA.Create())
|
||||||
|
{
|
||||||
|
rsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(recipientPublicKeyBase64), out _);
|
||||||
|
encryptedKey = rsa.Encrypt(aesKey, RSAEncryptionPadding.OaepSHA256);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new EncryptedPayload
|
||||||
|
{
|
||||||
|
CipherText = Convert.ToBase64String(cipherBytes),
|
||||||
|
Nonce = Convert.ToBase64String(nonce),
|
||||||
|
Tag = Convert.ToBase64String(tag),
|
||||||
|
EncryptedKey = Convert.ToBase64String(encryptedKey)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string DecryptForRecipient(EncryptedPayload payload, string recipientPrivateKeyBase64)
|
||||||
|
{
|
||||||
|
byte[] aesKey;
|
||||||
|
using (var rsa = RSA.Create())
|
||||||
|
{
|
||||||
|
rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(recipientPrivateKeyBase64), out _);
|
||||||
|
aesKey = rsa.Decrypt(Convert.FromBase64String(payload.EncryptedKey), RSAEncryptionPadding.OaepSHA256);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] plainBytes = new byte[Convert.FromBase64String(payload.CipherText).Length];
|
||||||
|
|
||||||
|
using (var aes = new AesGcm(aesKey, 16))
|
||||||
|
{
|
||||||
|
aes.Decrypt(
|
||||||
|
Convert.FromBase64String(payload.Nonce),
|
||||||
|
Convert.FromBase64String(payload.CipherText),
|
||||||
|
Convert.FromBase64String(payload.Tag),
|
||||||
|
plainBytes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetString(plainBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EncryptedPayload
|
||||||
|
{
|
||||||
|
public required string CipherText { get; set; }
|
||||||
|
public required string Nonce { get; set; }
|
||||||
|
public required string Tag { get; set; }
|
||||||
|
public required string EncryptedKey { get; set; }
|
||||||
|
}
|
||||||
123
dev-run.ps1
Normal file
123
dev-run.ps1
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
Set-Location $root
|
||||||
|
|
||||||
|
$startedProcesses = @()
|
||||||
|
|
||||||
|
function Start-TrackedProcess {
|
||||||
|
param(
|
||||||
|
[string]$FilePath,
|
||||||
|
[string[]]$ArgumentList = @(),
|
||||||
|
[string]$WorkingDirectory = $root
|
||||||
|
)
|
||||||
|
|
||||||
|
$proc = Start-Process -FilePath $FilePath `
|
||||||
|
-ArgumentList $ArgumentList `
|
||||||
|
-WorkingDirectory $WorkingDirectory `
|
||||||
|
-PassThru
|
||||||
|
|
||||||
|
$script:startedProcesses += $proc
|
||||||
|
return $proc
|
||||||
|
}
|
||||||
|
|
||||||
|
function Stop-AllTrackedProcesses {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Stopping launched processes..."
|
||||||
|
|
||||||
|
foreach ($proc in $script:startedProcesses) {
|
||||||
|
try {
|
||||||
|
if ($proc -and -not $proc.HasExited) {
|
||||||
|
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
|
||||||
|
Write-Host "Stopped PID $($proc.Id)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$containerIds = docker ps --filter "ancestor=surrealdb/surrealdb:v2.2.1" --format "{{.ID}}"
|
||||||
|
foreach ($id in $containerIds) {
|
||||||
|
docker stop $id | Out-Null
|
||||||
|
Write-Host "Stopped Docker container $id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Register-EngineEvent PowerShell.Exiting -Action {
|
||||||
|
Stop-AllTrackedProcesses
|
||||||
|
} | Out-Null
|
||||||
|
|
||||||
|
Write-Host "Building RelayCore..."
|
||||||
|
dotnet build .\RelayCore\RelayCore.csproj
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "RelayCore build failed." }
|
||||||
|
|
||||||
|
Write-Host "Building RelayServer..."
|
||||||
|
dotnet build .\RelayServer\RelayServer.csproj
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "RelayServer build failed." }
|
||||||
|
|
||||||
|
Write-Host "Building RelayClient (Windows only)..."
|
||||||
|
dotnet build .\RelayClient\RelayClient.csproj -f net10.0-windows10.0.19041.0
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "RelayClient build failed." }
|
||||||
|
|
||||||
|
$coreDll = Join-Path $root "RelayCore\bin\Debug\net9.0\RelayCore.dll"
|
||||||
|
$serverDll = Join-Path $root "RelayServer\bin\Debug\net10.0\RelayServer.dll"
|
||||||
|
$clientExe = Join-Path $root "RelayClient\bin\Debug\net10.0-windows10.0.19041.0\win-x64\RelayClient.exe"
|
||||||
|
|
||||||
|
Write-Host "Starting SurrealDB..."
|
||||||
|
Start-TrackedProcess `
|
||||||
|
-FilePath "docker" `
|
||||||
|
-ArgumentList @(
|
||||||
|
"run",
|
||||||
|
"--rm",
|
||||||
|
"-p", "8000:8000",
|
||||||
|
"-v", "/mydata:/mydata",
|
||||||
|
"surrealdb/surrealdb:v2.2.1",
|
||||||
|
"start",
|
||||||
|
"--user", "root",
|
||||||
|
"--pass", "secret"
|
||||||
|
)
|
||||||
|
|
||||||
|
Start-Sleep -Seconds 5
|
||||||
|
|
||||||
|
Write-Host "Starting RelayCore..."
|
||||||
|
Start-TrackedProcess `
|
||||||
|
-FilePath "dotnet" `
|
||||||
|
-ArgumentList @($coreDll)
|
||||||
|
|
||||||
|
Start-Sleep -Seconds 3
|
||||||
|
|
||||||
|
Write-Host "Starting RelayServer..."
|
||||||
|
Start-TrackedProcess `
|
||||||
|
-FilePath "dotnet" `
|
||||||
|
-ArgumentList @($serverDll)
|
||||||
|
|
||||||
|
Start-Sleep -Seconds 3
|
||||||
|
|
||||||
|
Write-Host "Starting RelayClient (Keeper317)..."
|
||||||
|
Start-TrackedProcess `
|
||||||
|
-FilePath $clientExe `
|
||||||
|
-ArgumentList @("--user", "Keeper317")
|
||||||
|
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
|
||||||
|
Write-Host "Starting RelayClient (Ru_Kira)..."
|
||||||
|
Start-TrackedProcess `
|
||||||
|
-FilePath $clientExe `
|
||||||
|
-ArgumentList @("--user", "Ru_Kira")
|
||||||
|
|
||||||
|
Start-Sleep -Seconds 20
|
||||||
|
|
||||||
|
Write-Host "Starting RelayClient (Test)..."
|
||||||
|
Start-TrackedProcess `
|
||||||
|
-FilePath $clientExe `
|
||||||
|
-ArgumentList @("--user", "Test")
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Everything started."
|
||||||
|
Write-Host "Press Ctrl+C in this PowerShell to stop everything."
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
}
|
||||||
85
start-all.ps1
Normal file
85
start-all.ps1
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
Set-Location $root
|
||||||
|
|
||||||
|
$dockerExe = (Get-Command docker.exe).Source
|
||||||
|
$dotnetExe = (Get-Command dotnet.exe).Source
|
||||||
|
$ps = (Get-Command powershell.exe).Source
|
||||||
|
|
||||||
|
Write-Host "Building RelayCore..."
|
||||||
|
& $dotnetExe build .\RelayCore\RelayCore.csproj
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "RelayCore build failed." }
|
||||||
|
|
||||||
|
Write-Host "Building RelayServer..."
|
||||||
|
& $dotnetExe build .\RelayServer\RelayServer.csproj
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "RelayServer build failed." }
|
||||||
|
|
||||||
|
Write-Host "Building RelayClient (Windows only)..."
|
||||||
|
& $dotnetExe build .\RelayClient\RelayClient.csproj -f net10.0-windows10.0.19041.0
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "RelayClient build failed." }
|
||||||
|
|
||||||
|
$coreDll = Join-Path $root "RelayCore\bin\Debug\net9.0\RelayCore.dll"
|
||||||
|
$serverDll = Join-Path $root "RelayServer\bin\Debug\net10.0\RelayServer.dll"
|
||||||
|
$clientExe = Join-Path $root "RelayClient\bin\Debug\net10.0-windows10.0.19041.0\win-x64\RelayClient.exe"
|
||||||
|
|
||||||
|
$tempDir = Join-Path $env:TEMP "RelayTabs"
|
||||||
|
New-Item -ItemType Directory -Force -Path $tempDir | Out-Null
|
||||||
|
|
||||||
|
function New-TabScript {
|
||||||
|
param(
|
||||||
|
[string]$Name,
|
||||||
|
[string]$Content
|
||||||
|
)
|
||||||
|
|
||||||
|
$path = Join-Path $tempDir "$Name.ps1"
|
||||||
|
Set-Content -Path $path -Value $Content -Encoding UTF8
|
||||||
|
return $path
|
||||||
|
}
|
||||||
|
|
||||||
|
$dockerScript = New-TabScript -Name "SurrealDB" -Content @"
|
||||||
|
Set-Location '$root'
|
||||||
|
& '$dockerExe' run --rm -p 8000:8000 -v /mydata:/mydata surrealdb/surrealdb:v2.2.1 start --user root --pass secret
|
||||||
|
"@
|
||||||
|
|
||||||
|
$coreScript = New-TabScript -Name "RelayCore" -Content @"
|
||||||
|
Set-Location '$root'
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
& '$dotnetExe' '$coreDll'
|
||||||
|
"@
|
||||||
|
|
||||||
|
$serverScript = New-TabScript -Name "RelayServer" -Content @"
|
||||||
|
Set-Location '$root'
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
& '$dotnetExe' '$serverDll'
|
||||||
|
"@
|
||||||
|
|
||||||
|
$keeperScript = New-TabScript -Name "Keeper317" -Content @"
|
||||||
|
Set-Location '$root'
|
||||||
|
Start-Sleep -Seconds 5
|
||||||
|
& '$clientExe' --user Keeper317
|
||||||
|
"@
|
||||||
|
|
||||||
|
$kiraScript = New-TabScript -Name "Ru_Kira" -Content @"
|
||||||
|
Set-Location '$root'
|
||||||
|
Start-Sleep -Seconds 5
|
||||||
|
& '$clientExe' --user Ru_Kira
|
||||||
|
"@
|
||||||
|
|
||||||
|
$testScript = New-TabScript -Name "Test" -Content @"
|
||||||
|
Set-Location '$root'
|
||||||
|
Start-Sleep -Seconds 25
|
||||||
|
& '$clientExe' --user Test
|
||||||
|
"@
|
||||||
|
|
||||||
|
$wtArgs = @(
|
||||||
|
"new-tab --title `"SurrealDB`" `"$ps`" -NoExit -ExecutionPolicy Bypass -File `"$dockerScript`"",
|
||||||
|
"new-tab --title `"RelayCore`" `"$ps`" -NoExit -ExecutionPolicy Bypass -File `"$coreScript`"",
|
||||||
|
"new-tab --title `"RelayServer`" `"$ps`" -NoExit -ExecutionPolicy Bypass -File `"$serverScript`"",
|
||||||
|
"new-tab --title `"Keeper317`" `"$ps`" -NoExit -ExecutionPolicy Bypass -File `"$keeperScript`"",
|
||||||
|
"new-tab --title `"Ru_Kira`" `"$ps`" -NoExit -ExecutionPolicy Bypass -File `"$kiraScript`"",
|
||||||
|
"new-tab --title `"Test`" `"$ps`" -NoExit -ExecutionPolicy Bypass -File `"$testScript`""
|
||||||
|
) -join " ; "
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Everything started."
|
||||||
|
Write-Host "Close out terminal to end all applications."
|
||||||
|
Start-Process wt.exe -ArgumentList $wtArgs
|
||||||
Reference in New Issue
Block a user