diff --git a/RelayClient/MainPage.xaml.cs b/RelayClient/MainPage.xaml.cs index b8fc136..3ea2921 100644 --- a/RelayClient/MainPage.xaml.cs +++ b/RelayClient/MainPage.xaml.cs @@ -15,7 +15,7 @@ public partial class MainPage : ContentPage private string? _currentChannelName; private readonly Dictionary> _messagesByChannel = new(); - private readonly List _channels = new(); + private readonly List _channels = []; public MainPage(string username) { @@ -41,6 +41,11 @@ public partial class MainPage : ContentPage _wsc.Send("GET_SERVER_KEY"); _wsc.Send("GET_CHANNELS"); hybridWebView.SetInvokeJavaScriptTarget(this); + + Loaded += async (_, _) => + { + await InitializeRtcPageAsync(); + }; } @@ -66,6 +71,12 @@ public partial class MainPage : ContentPage Console.WriteLine("Server public key not loaded yet."); return; } + + if (string.IsNullOrWhiteSpace(_currentChannelId)) + { + Console.WriteLine("No channel selected yet."); + return; + } var encrypted = E2EeHelper.EncryptForRecipient(text, _serverPublicKey); @@ -152,49 +163,79 @@ public partial class MainPage : ContentPage return; } + + if (type == "encrypted_rtc_signal") + { + var payload = JsonSerializer.Deserialize(e.Data); + if (payload is null) + return; + + if (payload.RecipientUsername != _username) + return; + + var privateKey = KeyStorage.LoadPrivateKey(_username); + + var decryptedJson = E2EeHelper.DecryptForRecipient( + new EncryptedPayload + { + CipherText = payload.CipherText, + Nonce = payload.Nonce, + Tag = payload.Tag, + EncryptedKey = payload.EncryptedKey + }, + privateKey + ); + + MainThread.BeginInvokeOnMainThread(async () => + { + await SendRtcSignalToJsAsync(decryptedJson); + }); + + return; + } if (type != "encrypted_chat") return; - var payload = JsonSerializer.Deserialize(e.Data); - if (payload is null) + var pyload = JsonSerializer.Deserialize(e.Data); + if (pyload is null) return; - if (payload.RecipientUsername != _username) + if (pyload.RecipientUsername != _username) return; - Console.WriteLine($"[{_username}] received encrypted payload for {payload.RecipientUsername}"); + Console.WriteLine($"[{_username}] received encrypted payload for {pyload.RecipientUsername}"); - var privateKey = KeyStorage.LoadPrivateKey(_username); + var privKey = KeyStorage.LoadPrivateKey(_username); var decryptedText = E2EeHelper.DecryptForRecipient( new EncryptedPayload { - CipherText = payload.CipherText, - Nonce = payload.Nonce, - Tag = payload.Tag, - EncryptedKey = payload.EncryptedKey + CipherText = pyload.CipherText, + Nonce = pyload.Nonce, + Tag = pyload.Tag, + EncryptedKey = pyload.EncryptedKey }, - privateKey + privKey ); - Console.WriteLine($"[{_username}] decrypted message from {payload.SenderUsername}: {decryptedText}"); + Console.WriteLine($"[{_username}] decrypted message from {pyload.SenderUsername}: {decryptedText}"); var message = new ChatMessage { - SenderUsername = payload.SenderUsername, + SenderUsername = pyload.SenderUsername, Text = decryptedText, Timestamp = DateTime.Now }; - if (!_messagesByChannel.ContainsKey(payload.ChannelId)) + if (!_messagesByChannel.ContainsKey(pyload.ChannelId)) { - _messagesByChannel[payload.ChannelId] = []; + _messagesByChannel[pyload.ChannelId] = []; } - _messagesByChannel[payload.ChannelId].Add(message); + _messagesByChannel[pyload.ChannelId].Add(message); - if (payload.ChannelId == _currentChannelId) + if (pyload.ChannelId == _currentChannelId) { MainThread.BeginInvokeOnMainThread(() => { @@ -296,14 +337,14 @@ public partial class MainPage : ContentPage { MessagesScrollView.IsVisible = true; RtcView.IsVisible = false; - ViewSwapped.Text = "Swap to Message View"; + ViewSwapped.Text = "Swap to Web View"; } else { MessagesScrollView.IsVisible = false; RtcView.IsVisible = true; - ViewSwapped.Text = "Swap to Web View"; + ViewSwapped.Text = "Swap to Message View"; } } @@ -316,62 +357,56 @@ public partial class MainPage : ContentPage { await DisplayAlertAsync("Raw Message Received", e.Message, "OK"); } - - #region syncs - public async void DoSyncWork() - { - await DisplayAlertAsync("Sync Work", "Sync Work", "OK"); - } - - public async void DoSyncWorkParams(int i, string s) - { - await DisplayAlertAsync("Sync Work", $"{i}:{s}", "OK"); - } - - public string DoSyncWorkReturn() - { - return "Hello from C#!"; - } - - public SyncReturn DoSyncWorkParamsReturn(int i, string s) - { - return new SyncReturn - { - Message = $"Hello from C#! {s}", - Value = i - }; - } - #endregion - #region asyncs - - public async Task DoAsyncWork() + public void SendRtcSignal(string json) { - await Task.Delay(1000); - } - public async Task DoAsyncWorkParams(int i, string s) - { - await DisplayAlertAsync("Sync Work", $"{i}:{s}", "OK"); - } - public async Task DoAsyncWorkReturn() - { - return "Hello from C#!"; - } - - public async Task DoAsyncWorkParamsReturn(int i, string s) - { - await Task.Delay(1000); - return new SyncReturn + if (string.IsNullOrWhiteSpace(_serverPublicKey)) { - Message = $"Hello from C# ASync! {s}", - Value = i - }; - } + Console.WriteLine("Server public key not loaded yet."); + return; + } - #endregion - public class SyncReturn + RtcSignalMessage? rtcSignal; + try + { + rtcSignal = JsonSerializer.Deserialize(json); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to parse RTC signal from JS: {ex.Message}"); + return; + } + + if (rtcSignal is null) + return; + + var encrypted = E2EeHelper.EncryptForRecipient(json, _serverPublicKey); + + var payload = new SocketRtcSignalMessage + { + Type = "encrypted_rtc_signal", + SenderUsername = _username, + RecipientUsername = rtcSignal.To, + CipherText = encrypted.CipherText, + Nonce = encrypted.Nonce, + Tag = encrypted.Tag, + EncryptedKey = encrypted.EncryptedKey + }; + + _wsc.Send(JsonSerializer.Serialize(payload)); + Console.WriteLine($"[{_username}] sent RTC signal: {rtcSignal.Type} -> {rtcSignal.To}"); + } + + private async Task SendRtcSignalToJsAsync(string rawJson) { - public string? Message { get; set; } - public int Value { get; set; } + var jsArg = JsonSerializer.Serialize(rawJson); + await hybridWebView.EvaluateJavaScriptAsync($"window.handleRtcSignal({jsArg})"); + } + + private async Task InitializeRtcPageAsync() + { + var jsArg = JsonSerializer.Serialize(_username); + await hybridWebView.EvaluateJavaScriptAsync($"window.currentUsername = {jsArg};"); + Console.WriteLine($"[{_username}] RTC page initialized."); } } \ No newline at end of file diff --git a/RelayClient/Models/RtcSignalMessage.cs b/RelayClient/Models/RtcSignalMessage.cs new file mode 100644 index 0000000..9f3113d --- /dev/null +++ b/RelayClient/Models/RtcSignalMessage.cs @@ -0,0 +1,13 @@ +namespace RelayClient.Models; + +public class RtcSignalMessage +{ + public required string Type { get; set; } // rtc_offer / rtc_answer / rtc_ice_candidate / rtc_call_request / rtc_call_accept / rtc_call_reject + public required string From { get; set; } + public required string To { get; set; } + + public string? Sdp { get; set; } + public string? Candidate { get; set; } + public string? SdpMid { get; set; } + public int? SdpMLineIndex { get; set; } +} \ No newline at end of file diff --git a/RelayClient/Models/SocketRtcSignalMessage.cs b/RelayClient/Models/SocketRtcSignalMessage.cs new file mode 100644 index 0000000..e3553f8 --- /dev/null +++ b/RelayClient/Models/SocketRtcSignalMessage.cs @@ -0,0 +1,13 @@ +namespace RelayClient.Models; + +public class SocketRtcSignalMessage +{ + public required string Type { get; set; } // encrypted_rtc_signal + 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; } +} \ No newline at end of file diff --git a/RelayClient/Resources/Raw/wwwroot/index.html b/RelayClient/Resources/Raw/wwwroot/index.html index 795408d..2e562c6 100644 --- a/RelayClient/Resources/Raw/wwwroot/index.html +++ b/RelayClient/Resources/Raw/wwwroot/index.html @@ -9,116 +9,190 @@ - -
- Hybrid sample! -
-
- -
-
- - - - -
-
- - - - -
-
- Log: -
-
- Consider checking out this PDF: sample.pdf -
- + +
+

Relay RTC Test

+
+ +
+ + + +
+ +
+ + +
+ +
+ +
+ \ No newline at end of file diff --git a/RelayServer/Models/RtcSignalMessage.cs b/RelayServer/Models/RtcSignalMessage.cs new file mode 100644 index 0000000..979a64b --- /dev/null +++ b/RelayServer/Models/RtcSignalMessage.cs @@ -0,0 +1,13 @@ +namespace RelayServer.Models; + +public class RtcSignalMessage +{ + public required string Type { get; set; } // rtc_offer / rtc_answer / rtc_ice_candidate / rtc_call_request / rtc_call_accept / rtc_call_reject + public required string From { get; set; } + public required string To { get; set; } + + public string? Sdp { get; set; } + public string? Candidate { get; set; } + public string? SdpMid { get; set; } + public int? SdpMLineIndex { get; set; } +} \ No newline at end of file diff --git a/RelayServer/Models/SocketRtcSignalMessage.cs b/RelayServer/Models/SocketRtcSignalMessage.cs new file mode 100644 index 0000000..47f04ad --- /dev/null +++ b/RelayServer/Models/SocketRtcSignalMessage.cs @@ -0,0 +1,13 @@ +namespace RelayServer.Models; + +public class SocketRtcSignalMessage +{ + public required string Type { get; set; } // encrypted_rtc_signal + 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; } +} \ No newline at end of file diff --git a/RelayServer/Services/ChatTest.cs b/RelayServer/Services/ChatTest.cs index e31f1b9..90821fa 100644 --- a/RelayServer/Services/ChatTest.cs +++ b/RelayServer/Services/ChatTest.cs @@ -41,6 +41,22 @@ public class ChatTest : WebSocketBehavior HandleGetHistory(msg); return; } + + SocketRtcSignalMessage? rtcProbe = null; + try + { + rtcProbe = JsonSerializer.Deserialize(msg); + } + catch + { + // ignored + } + + if (rtcProbe?.Type == "encrypted_rtc_signal") + { + HandleEncryptedRtcSignal(msg); + return; + } HandleEncryptedClientMessage(msg); } @@ -323,4 +339,74 @@ public class ChatTest : WebSocketBehavior return $"{table}:{recordId}"; } + + private void HandleEncryptedRtcSignal(string msg) + { + SocketRtcSignalMessage? clientPayload; + + try + { + clientPayload = JsonSerializer.Deserialize(msg); + } + catch + { + Console.WriteLine("Failed to parse encrypted RTC signal payload."); + return; + } + + if (clientPayload is null || clientPayload.Type != "encrypted_rtc_signal") + return; + + if (ClientKeyService is null || string.IsNullOrWhiteSpace(ServerPrivateKey)) + { + Console.WriteLine("Server RTC crypto dependencies are not initialized."); + return; + } + + string plainJson; + + try + { + plainJson = 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 RTC signal payload: {ex.Message}"); + return; + } + + var targetClient = Task.Run(async () => await ClientKeyService.GetByUsernameAsync(clientPayload.RecipientUsername)) + .GetAwaiter() + .GetResult(); + + if (targetClient is null) + { + Console.WriteLine($"No target RTC client key found for {clientPayload.RecipientUsername}"); + return; + } + + var encrypted = E2EeHelper.EncryptForRecipient(plainJson, targetClient.PublicKey); + + var outbound = new SocketRtcSignalMessage + { + Type = "encrypted_rtc_signal", + SenderUsername = clientPayload.SenderUsername, + RecipientUsername = clientPayload.RecipientUsername, + CipherText = encrypted.CipherText, + Nonce = encrypted.Nonce, + Tag = encrypted.Tag, + EncryptedKey = encrypted.EncryptedKey + }; + + Sessions.Broadcast(JsonSerializer.Serialize(outbound)); + } } \ No newline at end of file diff --git a/start-all.ps1 b/start-all.ps1 index ae62351..a258d13 100644 --- a/start-all.ps1 +++ b/start-all.ps1 @@ -64,19 +64,19 @@ Start-Sleep -Seconds 5 & '$clientExe' --user Ru_Kira "@ -$testScript = New-TabScript -Name "Test" -Content @" -Set-Location '$root' -Start-Sleep -Seconds 25 -& '$clientExe' --user Test -"@ +#$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`"" + "new-tab --title `"Ru_Kira`" `"$ps`" -NoExit -ExecutionPolicy Bypass -File `"$kiraScript`"" + #"new-tab --title `"Test`" `"$ps`" -NoExit -ExecutionPolicy Bypass -File `"$testScript`"" ) -join " ; " Write-Host ""