From f5645bf7dc8e63f7279e4064ec7418c43437dc99 Mon Sep 17 00:00:00 2001 From: RuKira Date: Sat, 14 Mar 2026 12:27:50 -0400 Subject: [PATCH] Added E2EE stuff. --- E2EeHelper.cs | 82 +++++++++++++++++++++ Program.cs | 197 +++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 243 insertions(+), 36 deletions(-) create mode 100644 E2EeHelper.cs diff --git a/E2EeHelper.cs b/E2EeHelper.cs new file mode 100644 index 0000000..c4d5578 --- /dev/null +++ b/E2EeHelper.cs @@ -0,0 +1,82 @@ +using System.Security.Cryptography; +using System.Text; + +namespace RelayCore; + +public static class E2EeHelper +{ + public static (string publicKey, string privateKey) GenerateRsaKeyPair() + { + using var rsa = RSA.Create(2048); + + var publicKey = Convert.ToBase64String(rsa.ExportSubjectPublicKeyInfo()); + var privateKey = Convert.ToBase64String(rsa.ExportPkcs8PrivateKey()); + + return (publicKey, privateKey); + } + + public static EncryptedMessagePayload 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]; + + 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 _); + encryptedKey = rsa.Encrypt(aesKey, RSAEncryptionPadding.OaepSHA256); + } + + return new EncryptedMessagePayload + { + CipherText = Convert.ToBase64String(cipherBytes), + Nonce = Convert.ToBase64String(nonce), + Tag = Convert.ToBase64String(tag), + EncryptedKey = Convert.ToBase64String(encryptedKey) + }; + } + + public static string DecryptForRecipient(EncryptedMessagePayload 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); + } + + var nonce = Convert.FromBase64String(payload.Nonce); + 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)) + { + aes.Decrypt(nonce, cipherBytes, tag, plainBytes); + } + + return Encoding.UTF8.GetString(plainBytes); + } +} + +public class EncryptedMessagePayload +{ + 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/Program.cs b/Program.cs index 33ea9c4..4b45823 100644 --- a/Program.cs +++ b/Program.cs @@ -1,58 +1,146 @@ -using System.ComponentModel.Design; -using SurrealDb.Net; +using SurrealDb.Net; using SurrealDb.Net.Models; using SurrealDb.Net.Models.Auth; using System.Text.Json; using PasswordHasher; +using RelayCore; using 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"); -var user = new Users +var keeper = await CreateUserAsync(db, "Keeper317", "Keeper317@gmail.com", "password"); +var kira = await CreateUserAsync(db, "Ru_Kira", "jduesling13@gmail.com", "password"); + +Console.WriteLine($"Keeper created: {ToJsonString(keeper)}"); +Console.WriteLine($"Kira created: {ToJsonString(kira)}"); + +var keeperKeys = E2EeHelper.GenerateRsaKeyPair(); +var kiraKeys = E2EeHelper.GenerateRsaKeyPair(); + +KeyStorage.SavePrivateKey("Keeper317", keeperKeys.privateKey); +KeyStorage.SavePrivateKey("Ru_Kira", kiraKeys.privateKey); + +await db.Create("user_keys", new UserKeys { - Username = "Keeper317", - Email = "Keeper317@gmail.com", - CreatedAt = DateTime.Now, - UpdatedAt = DateTime.Now, - LastLogin = DateTime.Now, - TwoFactorEnabled = false, - EmailVerified = false, - AccountStatus = (int) AccountStatuses.Active, - OnlineStatus = (int) OnlineStatuses.Online, -}; -var created = await db.Create("users", user); -Console.WriteLine($"Created Person: {ToJsonString(created)}"); -var hasher = new PasswordHasher.PasswordHasher(); -user.Password = hasher.HashPassword(created.Id + "password"); -user.UpdatedAt = DateTime.Now; -user.LastLogin = DateTime.Now; -user.Id = created.Id; + UserId = keeper.Id.ToString(), + PublicKey = keeperKeys.publicKey, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow +}); -bool isValid = hasher.VerifyPassword(created.Id + "password", user.Password); -Console.WriteLine($"Password match: {isValid}"); +await db.Create("user_keys", new UserKeys +{ + UserId = kira.Id.ToString(), + PublicKey = kiraKeys.publicKey, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow +}); -var updated = await db.Merge("users", new() { Id = created.Id, Password = user.Password }); +Console.WriteLine("Public keys stored for both users."); -// var updated = await db.Merge( -// new() { Id = (TABLE, "8b4nwczy6x8f8zd5sslq"), Marketing = true } -// ); -Console.WriteLine($"Updated Person: {ToJsonString(updated)}"); -// -var people = await db.Select("users"); -Console.WriteLine($"Select Person: {ToJsonString(people)}"); -// -// var queryResponse = await db.Query($"SELECT Marketing, count() AS Count FROM type::table({TABLE}) GROUP BY Marketing"); -// var groups = queryResponse.GetValue>(0); -// Console.WriteLine($"Get Value as group: {ToJsonString(groups)}"); +var conversation = await db.Create("conversations", new Conversations +{ + CreatedByUserId = keeper.Id.ToString(), + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + Title = "Keeper317 + Ru_Kira", + IsDirectMessage = true +}); -await db.Delete("users"); +Console.WriteLine($"Conversation created: {ToJsonString(conversation)}"); +await db.Create("conversation_members", new ConversationMembers +{ + ConversationId = conversation.Id.ToString(), + UserId = keeper.Id.ToString(), + JoinedAt = DateTime.UtcNow +}); + +await db.Create("conversation_members", new ConversationMembers +{ + ConversationId = conversation.Id.ToString(), + UserId = kira.Id.ToString(), + JoinedAt = DateTime.UtcNow +}); + +Console.WriteLine("Conversation members added."); + +var encrypted = E2EeHelper.EncryptForRecipient("hello from Keeper317", kiraKeys.publicKey); + +var savedMessage = await db.Create("messages", new Messages +{ + ConversationId = conversation.Id.ToString(), + SenderUserId = keeper.Id.ToString(), + RecipientUserId = kira.Id.ToString(), + CipherText = encrypted.CipherText, + Nonce = encrypted.Nonce, + Tag = encrypted.Tag, + EncryptedKey = encrypted.EncryptedKey, + CreatedAt = DateTime.UtcNow +}); + +Console.WriteLine($"Encrypted message saved: {ToJsonString(savedMessage)}"); + +var decrypted = E2EeHelper.DecryptForRecipient(encrypted, kiraKeys.privateKey); +Console.WriteLine($"Decrypted for Ru_Kira: {decrypted}"); + +return; static string ToJsonString(object? o) { - return JsonSerializer.Serialize(o, new JsonSerializerOptions { WriteIndented = true, }); + return JsonSerializer.Serialize(o, new JsonSerializerOptions { WriteIndented = true }); +} + +static async Task CreateUserAsync(SurrealDbClient db, string username, string email, string rawPassword) +{ + var now = DateTime.UtcNow; + + var user = new Users + { + Username = username, + Email = email, + CreatedAt = now, + UpdatedAt = now, + LastLogin = now, + TwoFactorEnabled = false, + EmailVerified = false, + AccountStatus = (int)AccountStatuses.Active, + OnlineStatus = (int)OnlineStatuses.Online, + }; + + var created = await db.Create("users", user); + + var hasher = new PasswordHasher.PasswordHasher(); + var passwordHash = hasher.HashPassword(created.Id.ToString() + rawPassword); + + var updated = await db.Merge(new PasswordHash + { + Id = created.Id, + Password = passwordHash + }); + + return updated; +} + +public static class KeyStorage +{ + public static void SavePrivateKey(string username, string privateKey) + { + Directory.CreateDirectory("keys"); + File.WriteAllText(Path.Combine("keys", $"{username}.private.key"), privateKey); + } + + public static string LoadPrivateKey(string username) + { + return File.ReadAllText(Path.Combine("keys", $"{username}.private.key")); + } + + public static bool PrivateKeyExists(string username) + { + return File.Exists(Path.Combine("keys", $"{username}.private.key")); + } } public class ResponsibilityMerge : Record @@ -128,6 +216,43 @@ public class AuthAudits : Record public required string Details { get; set; } public required DateTime CreatedAt { get; set; } } + +public class UserKeys : Record +{ + public required string UserId { get; set; } + public required string PublicKey { get; set; } + public required DateTime CreatedAt { get; set; } + public required DateTime UpdatedAt { get; set; } +} + +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; } +} + +public class ConversationMembers : Record +{ + public required string ConversationId { get; set; } + public required string UserId { get; set; } + public required DateTime JoinedAt { get; set; } +} + +public class Messages : Record +{ + public required string ConversationId { 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; } +} + enum AccountStatuses { Active,