Update: Full E2EE + Scripts
This commit is contained in:
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 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 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;
|
||||
using WebSocketSharp.Server;
|
||||
using WebSocketSharp;
|
||||
|
||||
using RelayServer.Models;
|
||||
using RelayServer.Services;
|
||||
using WebSocketSharp.Server;
|
||||
using RelayServer.Models;
|
||||
|
||||
var surrealService = new SurrealService();
|
||||
var coreClient = new CoreClientService();
|
||||
@@ -12,23 +9,25 @@ var cryptoService = new ChannelCryptoService();
|
||||
|
||||
await using var db = await surrealService.ConnectAsync();
|
||||
|
||||
ChatTest.ClientKeyService = new ClientKeyService(db);
|
||||
ChatTest.Db = db;
|
||||
|
||||
var wssv = new WebSocketServer("ws://localhost:1337");
|
||||
wssv.AddWebSocketService<ChatTest>("/");
|
||||
wssv.Start();
|
||||
Console.WriteLine("WebSocket server started");
|
||||
Console.ReadKey(true);
|
||||
wssv.Stop();
|
||||
|
||||
var keeper = await coreClient.GetUserByUsernameAsync("Keeper317");
|
||||
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.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!keeper.Licensed || !kira.Licensed)
|
||||
if (!keeper.Licensed || !kira.Licensed || !test.Licensed)
|
||||
{
|
||||
Console.WriteLine("One or more required users are not licensed.");
|
||||
return;
|
||||
@@ -36,11 +35,12 @@ if (!keeper.Licensed || !kira.Licensed)
|
||||
|
||||
Console.WriteLine($"Core verified user: {keeper.Username}");
|
||||
Console.WriteLine($"Core verified user: {kira.Username}");
|
||||
Console.WriteLine($"Core verified user: {test.Username}");
|
||||
|
||||
var server = await db.Create("servers", new Servers
|
||||
{
|
||||
Name = "Test Server",
|
||||
OwnerUserId = kira.Id,
|
||||
OwnerUserId = keeper.Id,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
@@ -60,6 +60,13 @@ var kiraMember = await db.Create("server_members", new ServerMembers
|
||||
IsOwner = false
|
||||
});
|
||||
|
||||
var testMember = await db.Create("server_members", new ServerMembers
|
||||
{
|
||||
UserId = test.Id,
|
||||
JoinedAt = DateTime.UtcNow,
|
||||
IsOwner = false
|
||||
});
|
||||
|
||||
Console.WriteLine("Server members created.");
|
||||
|
||||
var channel = await db.Create("channels", new Channels
|
||||
@@ -72,66 +79,29 @@ Console.WriteLine($"Channel created: {ToJsonString(channel)}");
|
||||
|
||||
var channelId = GetRecordId(channel.Id);
|
||||
Console.WriteLine($"Resolved channelId: {channelId}");
|
||||
ChatTest.DefaultChannelId = channelId;
|
||||
|
||||
var keyBase64 = cryptoService.GenerateKey();
|
||||
var serverKeys = E2EeHelper.GenerateRsaKeyPair();
|
||||
|
||||
var serverKey = await db.Create("server_encryption_keys", new ServerEncryptionKeys
|
||||
{
|
||||
KeyBase64 = keyBase64,
|
||||
PublicKey = serverKeys.publicKey,
|
||||
PrivateKey = serverKeys.privateKey,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
ChatTest.ServerPublicKey = serverKeys.publicKey;
|
||||
ChatTest.ServerPrivateKey = serverKeys.privateKey;
|
||||
ChatTest.ChannelDbKey = keyBase64;
|
||||
|
||||
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;
|
||||
|
||||
static string ToJsonString(object? obj)
|
||||
@@ -158,15 +128,4 @@ static string GetRecordId(object? id)
|
||||
var table = root.GetProperty("Table").GetString() ?? string.Empty;
|
||||
|
||||
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),
|
||||
"Ru_Kira" => new CoreUser("users:ru_kira", "Ru_Kira", true),
|
||||
"Test" => new CoreUser("users:test", "Test", true),
|
||||
_ => 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; }
|
||||
}
|
||||
Reference in New Issue
Block a user