Updated with Antigravity.
This commit is contained in:
@@ -45,6 +45,19 @@ public static class AuthHelper
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsServerTokenValid(HttpContext context, PeerCache peerCache)
|
||||
{
|
||||
var serverUrl = context.Request.Headers["X-Server-Url"].ToString();
|
||||
var serverToken = context.Request.Headers["X-Server-Token"].ToString();
|
||||
|
||||
if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(serverToken))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return peerCache.VerifySessionToken(serverUrl, serverToken);
|
||||
}
|
||||
|
||||
// Helper to generate a server token for outgoing sync requests
|
||||
public static string GenerateServerToken(CertificateManager certificateManager)
|
||||
{
|
||||
|
||||
@@ -6,16 +6,19 @@ namespace SNote.Server.Security;
|
||||
|
||||
public class PeerCache
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, byte> _downstreamPeers = new(StringComparer.OrdinalIgnoreCase);
|
||||
// Mapping: PeerUrl (normalized) -> PeerDetails (SessionToken, PublicKeyPem)
|
||||
private readonly ConcurrentDictionary<string, PeerDetails> _downstreamPeers = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<string, DateTime> _recentBroadcastIds = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public void RegisterPeer(string peerUrl)
|
||||
public void RegisterPeer(string peerUrl, string sessionToken, string publicKeyPem)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(peerUrl)) return;
|
||||
if (string.IsNullOrWhiteSpace(peerUrl) || string.IsNullOrWhiteSpace(sessionToken)) return;
|
||||
|
||||
var cleanUrl = peerUrl.Trim().TrimEnd('/');
|
||||
_downstreamPeers.TryAdd(cleanUrl, 0);
|
||||
Console.WriteLine($"[PeerCache] Registered downstream peer node: {cleanUrl}");
|
||||
var details = new PeerDetails(sessionToken, publicKeyPem.Trim());
|
||||
|
||||
_downstreamPeers[cleanUrl] = details;
|
||||
Console.WriteLine($"[PeerCache] Registered downstream peer node: {cleanUrl} with secure session token.");
|
||||
}
|
||||
|
||||
public void RemovePeer(string peerUrl)
|
||||
@@ -34,6 +37,40 @@ public class PeerCache
|
||||
return new List<string>(_downstreamPeers.Keys);
|
||||
}
|
||||
|
||||
public string? GetToken(string peerUrl)
|
||||
{
|
||||
var cleanUrl = peerUrl.Trim().TrimEnd('/');
|
||||
return _downstreamPeers.TryGetValue(cleanUrl, out var details) ? details.SessionToken : null;
|
||||
}
|
||||
|
||||
public string? GetPublicKey(string peerUrl)
|
||||
{
|
||||
var cleanUrl = peerUrl.Trim().TrimEnd('/');
|
||||
return _downstreamPeers.TryGetValue(cleanUrl, out var details) ? details.PublicKeyPem : null;
|
||||
}
|
||||
|
||||
public bool VerifySessionToken(string peerUrl, string token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(peerUrl) || string.IsNullOrEmpty(token)) return false;
|
||||
|
||||
var cleanUrl = peerUrl.Trim().TrimEnd('/');
|
||||
if (_downstreamPeers.TryGetValue(cleanUrl, out var details))
|
||||
{
|
||||
return string.Equals(details.SessionToken, token, StringComparison.Ordinal);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public List<(string Url, string Token)> GetActivePeers()
|
||||
{
|
||||
var list = new List<(string Url, string Token)>();
|
||||
foreach (var kvp in _downstreamPeers)
|
||||
{
|
||||
list.Add((kvp.Key, kvp.Value.SessionToken));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public bool TryProcessBroadcast(string broadcastId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(broadcastId)) return false;
|
||||
@@ -51,3 +88,5 @@ public class PeerCache
|
||||
return _recentBroadcastIds.TryAdd(broadcastId, DateTime.UtcNow);
|
||||
}
|
||||
}
|
||||
|
||||
public record PeerDetails(string SessionToken, string PublicKeyPem);
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace SNote.Server.Security;
|
||||
|
||||
public class RsaKeyManager
|
||||
{
|
||||
private readonly string _publicKeyPath;
|
||||
private readonly string _privateKeyPath;
|
||||
private RSA? _privateKeyRsa;
|
||||
private RSA? _publicKeyRsa;
|
||||
private string _publicKeyPem = string.Empty;
|
||||
private string _privateKeyPem = string.Empty;
|
||||
|
||||
public RsaKeyManager(IConfiguration configuration)
|
||||
{
|
||||
_publicKeyPath = Path.Combine(AppContext.BaseDirectory, "snote-public.pem");
|
||||
_privateKeyPath = Path.Combine(AppContext.BaseDirectory, "snote-private.pem");
|
||||
|
||||
var configPublicKey = configuration["Sync:PublicKey"];
|
||||
var configPrivateKey = configuration["Sync:PrivateKey"];
|
||||
|
||||
if (!string.IsNullOrEmpty(configPublicKey) && !string.IsNullOrEmpty(configPrivateKey))
|
||||
{
|
||||
_publicKeyPem = configPublicKey;
|
||||
_privateKeyPem = configPrivateKey;
|
||||
InitializeKeys();
|
||||
}
|
||||
else
|
||||
{
|
||||
LoadOrGenerateKeys();
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeKeys()
|
||||
{
|
||||
try
|
||||
{
|
||||
_publicKeyRsa = RSA.Create();
|
||||
_publicKeyRsa.ImportFromPem(_publicKeyPem);
|
||||
|
||||
_privateKeyRsa = RSA.Create();
|
||||
_privateKeyRsa.ImportFromPem(_privateKeyPem);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[RsaKeyManager] Error initializing configured keys: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadOrGenerateKeys()
|
||||
{
|
||||
if (File.Exists(_privateKeyPath) && File.Exists(_publicKeyPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
_publicKeyPem = File.ReadAllText(_publicKeyPath);
|
||||
_privateKeyPem = File.ReadAllText(_privateKeyPath);
|
||||
InitializeKeys();
|
||||
Console.WriteLine("[RsaKeyManager] Loaded existing RSA keys from disk.");
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[RsaKeyManager] Error loading RSA keys from disk: {ex.Message}. Re-generating.");
|
||||
}
|
||||
}
|
||||
|
||||
// Generate new keypair
|
||||
Console.WriteLine("[RsaKeyManager] Generating new secure 2048-bit RSA keypair...");
|
||||
using var rsa = RSA.Create(2048);
|
||||
|
||||
_publicKeyPem = rsa.ExportSubjectPublicKeyInfoPem();
|
||||
_privateKeyPem = rsa.ExportPkcs8PrivateKeyPem();
|
||||
|
||||
File.WriteAllText(_publicKeyPath, _publicKeyPem);
|
||||
File.WriteAllText(_privateKeyPath, _privateKeyPem);
|
||||
|
||||
InitializeKeys();
|
||||
Console.WriteLine("[RsaKeyManager] Secure RSA keypair generated and saved to disk.");
|
||||
}
|
||||
|
||||
public RSA GetPrivateKey()
|
||||
{
|
||||
if (_privateKeyRsa == null) throw new InvalidOperationException("Private key is not loaded.");
|
||||
return _privateKeyRsa;
|
||||
}
|
||||
|
||||
public RSA GetPublicKey()
|
||||
{
|
||||
if (_publicKeyRsa == null) throw new InvalidOperationException("Public key is not loaded.");
|
||||
return _publicKeyRsa;
|
||||
}
|
||||
|
||||
public string GetPublicKeyPem()
|
||||
{
|
||||
return _publicKeyPem;
|
||||
}
|
||||
|
||||
public string GetPrivateKeyPem()
|
||||
{
|
||||
return _privateKeyPem;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user