diff --git a/RelayServer/Models/Messages.cs b/RelayServer/Models/ChannelMessages.cs similarity index 61% rename from RelayServer/Models/Messages.cs rename to RelayServer/Models/ChannelMessages.cs index 1a91e52..ec85d1e 100644 --- a/RelayServer/Models/Messages.cs +++ b/RelayServer/Models/ChannelMessages.cs @@ -2,14 +2,12 @@ namespace RelayServer.Models; -public class Messages : Record +public class ChannelMessages : Record { - public required string ConversationId { get; set; } + public required string ChannelId { get; set; } public required string SenderUserId { get; set; } - public required string RecipientUserId { 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; } public required DateTime CreatedAt { get; set; } } \ No newline at end of file diff --git a/RelayServer/Models/Channels.cs b/RelayServer/Models/Channels.cs new file mode 100644 index 0000000..cc93855 --- /dev/null +++ b/RelayServer/Models/Channels.cs @@ -0,0 +1,9 @@ +using SurrealDb.Net.Models; + +namespace RelayServer.Models; + +public class Channels : Record +{ + public required string Name { get; set; } + public required DateTime CreatedAt { get; set; } +} \ No newline at end of file diff --git a/RelayServer/Models/Conversations.cs b/RelayServer/Models/Conversations.cs deleted file mode 100644 index 9788ea2..0000000 --- a/RelayServer/Models/Conversations.cs +++ /dev/null @@ -1,12 +0,0 @@ -using SurrealDb.Net.Models; - -namespace RelayServer.Models; - -public class Conversations : Record -{ - public required string CreatedByUserId { get; set; } - public required DateTime CreatedAt { get; set; } - public required DateTime UpdatedAt { get; set; } - public string? Title { get; set; } - public bool IsDirectMessage { get; set; } -} \ No newline at end of file diff --git a/RelayServer/Models/UserKeys.cs b/RelayServer/Models/ServerEncryptionKeys.cs similarity index 56% rename from RelayServer/Models/UserKeys.cs rename to RelayServer/Models/ServerEncryptionKeys.cs index 0dbf6fd..83abe08 100644 --- a/RelayServer/Models/UserKeys.cs +++ b/RelayServer/Models/ServerEncryptionKeys.cs @@ -2,10 +2,9 @@ namespace RelayServer.Models; -public class UserKeys : Record +public class ServerEncryptionKeys : Record { - public required string UserId { get; set; } - public required string PublicKey { get; set; } + public required string KeyBase64 { get; set; } public required DateTime CreatedAt { get; set; } public required DateTime UpdatedAt { get; set; } } \ No newline at end of file diff --git a/RelayServer/Models/ConversationMembers.cs b/RelayServer/Models/ServerMembers.cs similarity index 62% rename from RelayServer/Models/ConversationMembers.cs rename to RelayServer/Models/ServerMembers.cs index 27dd399..4871ae8 100644 --- a/RelayServer/Models/ConversationMembers.cs +++ b/RelayServer/Models/ServerMembers.cs @@ -2,9 +2,9 @@ namespace RelayServer.Models; -public class ConversationMembers : Record +public class ServerMembers : Record { - public required string ConversationId { get; set; } public required string UserId { get; set; } public required DateTime JoinedAt { get; set; } + public bool IsOwner { get; set; } } \ No newline at end of file diff --git a/RelayServer/Models/Servers.cs b/RelayServer/Models/Servers.cs new file mode 100644 index 0000000..3e34ead --- /dev/null +++ b/RelayServer/Models/Servers.cs @@ -0,0 +1,10 @@ +using SurrealDb.Net.Models; + +namespace RelayServer.Models; + +public class Servers : Record +{ + public required string Name { get; set; } + public required string OwnerUserId { get; set; } + public required DateTime CreatedAt { get; set; } +} \ No newline at end of file diff --git a/RelayServer/Program.cs b/RelayServer/Program.cs index 837131c..e868779 100644 --- a/RelayServer/Program.cs +++ b/RelayServer/Program.cs @@ -1 +1,152 @@ -Console.WriteLine("Hello, World!"); \ No newline at end of file +using System.Text.Json; +using RelayServer.Models; +using RelayServer.Services; + +var surrealService = new SurrealService(); +var coreClient = new CoreClientService(); +var cryptoService = new ChannelCryptoService(); + +await using var db = await surrealService.ConnectAsync(); + +var keeper = await coreClient.GetUserByUsernameAsync("Keeper317"); +var kira = await coreClient.GetUserByUsernameAsync("Ru_Kira"); + +if (keeper is null || kira is null) +{ + Console.WriteLine("One or more required users do not exist in RelayCore."); + return; +} + +if (!keeper.Licensed || !kira.Licensed) +{ + Console.WriteLine("One or more required users are not licensed."); + return; +} + +Console.WriteLine($"Core verified user: {keeper.Username}"); +Console.WriteLine($"Core verified user: {kira.Username}"); + +var server = await db.Create("servers", new Servers +{ + Name = "Test Server", + OwnerUserId = kira.Id, + CreatedAt = DateTime.UtcNow +}); + +Console.WriteLine($"Server created: {ToJsonString(server)}"); + +var keeperMember = await db.Create("server_members", new ServerMembers +{ + UserId = keeper.Id, + JoinedAt = DateTime.UtcNow, + IsOwner = true +}); + +var kiraMember = await db.Create("server_members", new ServerMembers +{ + UserId = kira.Id, + JoinedAt = DateTime.UtcNow, + IsOwner = false +}); + +Console.WriteLine("Server members created."); + +var channel = await db.Create("channels", new Channels +{ + Name = "general", + CreatedAt = DateTime.UtcNow +}); + +Console.WriteLine($"Channel created: {ToJsonString(channel)}"); + +var channelId = GetRecordId(channel.Id); +Console.WriteLine($"Resolved channelId: {channelId}"); + +Console.WriteLine($"Channel created: {ToJsonString(channel)}"); + +var keyBase64 = cryptoService.GenerateKey(); + +var serverKey = await db.Create("server_encryption_keys", new ServerEncryptionKeys +{ + KeyBase64 = keyBase64, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow +}); + +Console.WriteLine("Server encryption key created."); + +var encrypted = cryptoService.Encrypt("hello from Keeper317 in #general", keyBase64); + +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("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; + +static string ToJsonString(object? obj) +{ + return JsonSerializer.Serialize(obj, new JsonSerializerOptions + { + WriteIndented = true, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }); +} + +static string GetRecordId(object? id) +{ + if (id is null) + return string.Empty; + + var json = JsonSerializer.Serialize(id); + + using var doc = JsonDocument.Parse(json); + + var root = doc.RootElement; + + var recordId = root.GetProperty("Id").GetString() ?? string.Empty; + var table = root.GetProperty("Table").GetString() ?? string.Empty; + + return $"{table}:{recordId}"; +} \ No newline at end of file diff --git a/RelayServer/Services/ChannelCryptoService.cs b/RelayServer/Services/ChannelCryptoService.cs new file mode 100644 index 0000000..5f05d0f --- /dev/null +++ b/RelayServer/Services/ChannelCryptoService.cs @@ -0,0 +1,44 @@ +using System.Security.Cryptography; +using System.Text; + +namespace RelayServer.Services; + +public sealed class ChannelCryptoService +{ + public string GenerateKey() + { + return Convert.ToBase64String(RandomNumberGenerator.GetBytes(32)); + } + + public (string cipherText, string nonce, string tag) Encrypt(string plainText, string keyBase64) + { + var key = Convert.FromBase64String(keyBase64); + var nonce = RandomNumberGenerator.GetBytes(12); + var plainBytes = Encoding.UTF8.GetBytes(plainText); + var cipherBytes = new byte[plainBytes.Length]; + var tag = new byte[16]; + + using var aes = new AesGcm(key, 16); + aes.Encrypt(nonce, plainBytes, cipherBytes, tag); + + return ( + Convert.ToBase64String(cipherBytes), + Convert.ToBase64String(nonce), + Convert.ToBase64String(tag) + ); + } + + public string Decrypt(string cipherTextBase64, string nonceBase64, string tagBase64, string keyBase64) + { + var key = Convert.FromBase64String(keyBase64); + var nonce = Convert.FromBase64String(nonceBase64); + var tag = Convert.FromBase64String(tagBase64); + var cipherBytes = Convert.FromBase64String(cipherTextBase64); + var plainBytes = new byte[cipherBytes.Length]; + + using var aes = new AesGcm(key, 16); + aes.Decrypt(nonce, cipherBytes, tag, plainBytes); + + return Encoding.UTF8.GetString(plainBytes); + } +} \ No newline at end of file diff --git a/RelayServer/Services/ChannelMessageService.cs b/RelayServer/Services/ChannelMessageService.cs new file mode 100644 index 0000000..3aa8b3d --- /dev/null +++ b/RelayServer/Services/ChannelMessageService.cs @@ -0,0 +1,6 @@ +namespace RelayServer.Services; + +public class ChannelMessageService +{ + +} \ No newline at end of file diff --git a/RelayServer/Services/CoreClientService.cs b/RelayServer/Services/CoreClientService.cs new file mode 100644 index 0000000..3087bc7 --- /dev/null +++ b/RelayServer/Services/CoreClientService.cs @@ -0,0 +1,16 @@ +namespace RelayServer.Services; + +public sealed class CoreClientService +{ + public Task GetUserByUsernameAsync(string username) + { + return Task.FromResult(username switch + { + "Keeper317" => new CoreUser("users:keeper317", "Keeper317", true), + "Ru_Kira" => new CoreUser("users:ru_kira", "Ru_Kira", true), + _ => null + }); + } +} + +public sealed record CoreUser(string Id, string Username, bool Licensed); \ No newline at end of file diff --git a/RelayServer/Services/ServerBootstrapService.cs b/RelayServer/Services/ServerBootstrapService.cs new file mode 100644 index 0000000..45a0880 --- /dev/null +++ b/RelayServer/Services/ServerBootstrapService.cs @@ -0,0 +1,6 @@ +namespace RelayServer.Services; + +public class ServerBootstrapService +{ + +} \ No newline at end of file diff --git a/RelayServer/Services/SurrealService.cs b/RelayServer/Services/SurrealService.cs new file mode 100644 index 0000000..9408d6c --- /dev/null +++ b/RelayServer/Services/SurrealService.cs @@ -0,0 +1,21 @@ +using SurrealDb.Net; +using SurrealDb.Net.Models.Auth; + +namespace RelayServer.Services; + +public sealed class SurrealService +{ + public async Task ConnectAsync() + { + var db = new SurrealDbClient("ws://127.0.0.1:8000/rpc"); + + await db.SignIn(new RootAuth + { + Username = "root", + Password = "secret" + }); + + await db.Use("test", "test"); + return db; + } +} \ No newline at end of file