mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
Server: - Add Let's Encrypt (ACME) TLS mode with autocert, HTTP-01 challenges on :80, and automatic certificate renewal (tls.mode: "acme" in config.yaml) - Add ASCII art startup banner with server info and endpoint URLs - Fix CSP blocking admin panel inline styles/scripts (per-route override) - Suppress TLS handshake error noise in console output - Fix TOCTOU race in invite consumption (atomic UPDATE with row-count check) - Fix sendMsg mutex race condition (hold lock for entire send) - Fix permission override formula (deny-first, allow-wins) - Fix voice join parsing channelID before permission check - Add session expiry check at WebSocket auth and periodic revalidation - Add message length limit (4000 chars) and emoji length validation (32 bytes) - Add file size enforcement in storage after io.Copy - Add checksum URL validation in updater - Add backup path traversal protection (BackupToSafe) - Add self-modification guard in admin handlePatchUser - Fix admin ownerOnlyMiddleware to use context user instead of re-auth - Remove redundant startup log lines (banner shows same info) - Add periodic expired session cleanup (15-min ticker) - Add permissions package with bitfield constants and EffectivePerms - Add rate limiter cleanup goroutine to prevent unbounded growth - Add auth helpers (IsEffectivelyBanned, IsSessionExpired) - Add WebSocket origin validation Client: - Add TOFU certificate trust service - Add receive loop error handling - Fix redundant else-if in OnChatMessage
160 lines
5.3 KiB
C#
160 lines
5.3 KiB
C#
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
|
|
namespace OwnCord.Client.Services;
|
|
|
|
/// <summary>
|
|
/// Trust-On-First-Use (TOFU) certificate pinning service.
|
|
/// Fingerprints are persisted as a JSON file in the application data directory so
|
|
/// that trust decisions survive application restarts.
|
|
///
|
|
/// Storage format: a flat JSON object mapping host strings to SHA-256 hex fingerprints,
|
|
/// e.g. { "server.local:8443": "AABBCC..." }
|
|
/// </summary>
|
|
public sealed class CertificateTrustService : ICertificateTrustService
|
|
{
|
|
private readonly string _dir;
|
|
private readonly string _filePath;
|
|
private readonly SemaphoreSlim _lock = new(1, 1);
|
|
|
|
// ── Constructors ──────────────────────────────────────────────────────────
|
|
|
|
public CertificateTrustService()
|
|
: this(Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
|
"OwnCord",
|
|
"certs")) { }
|
|
|
|
/// <summary>Internal constructor allowing an isolated directory for unit tests.</summary>
|
|
internal CertificateTrustService(string dir)
|
|
{
|
|
_dir = dir;
|
|
_filePath = Path.Combine(_dir, "trusted_certs.json");
|
|
}
|
|
|
|
// ── ICertificateTrustService ──────────────────────────────────────────────
|
|
|
|
/// <inheritdoc/>
|
|
public bool IsTrusted(string host, string fingerprint)
|
|
{
|
|
if (string.IsNullOrEmpty(host)) return false;
|
|
if (string.IsNullOrEmpty(fingerprint)) return false;
|
|
|
|
_lock.Wait();
|
|
try
|
|
{
|
|
var store = Load();
|
|
|
|
if (!store.TryGetValue(host, out var stored))
|
|
{
|
|
// First use — auto-trust (TOFU)
|
|
var updated = new Dictionary<string, string>(store, StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
[host] = fingerprint
|
|
};
|
|
Save(updated);
|
|
return true;
|
|
}
|
|
|
|
return string.Equals(stored, fingerprint, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
finally
|
|
{
|
|
_lock.Release();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void TrustFingerprint(string host, string fingerprint)
|
|
{
|
|
if (string.IsNullOrEmpty(host))
|
|
throw new ArgumentException("Host must not be null or empty.", nameof(host));
|
|
if (string.IsNullOrEmpty(fingerprint))
|
|
throw new ArgumentException("Fingerprint must not be null or empty.", nameof(fingerprint));
|
|
|
|
_lock.Wait();
|
|
try
|
|
{
|
|
var store = Load();
|
|
var updated = new Dictionary<string, string>(store, StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
[host] = fingerprint
|
|
};
|
|
Save(updated);
|
|
}
|
|
finally
|
|
{
|
|
_lock.Release();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void RemoveTrust(string host)
|
|
{
|
|
if (string.IsNullOrEmpty(host)) return;
|
|
|
|
_lock.Wait();
|
|
try
|
|
{
|
|
var store = Load();
|
|
if (!store.ContainsKey(host)) return;
|
|
|
|
var updated = new Dictionary<string, string>(store, StringComparer.OrdinalIgnoreCase);
|
|
updated.Remove(host);
|
|
Save(updated);
|
|
}
|
|
finally
|
|
{
|
|
_lock.Release();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public string? GetTrustedFingerprint(string host)
|
|
{
|
|
if (string.IsNullOrEmpty(host)) return null;
|
|
|
|
var store = Load();
|
|
return store.TryGetValue(host, out var fp) ? fp : null;
|
|
}
|
|
|
|
// ── Private helpers ───────────────────────────────────────────────────────
|
|
|
|
private static readonly JsonSerializerOptions JsonOpts = new()
|
|
{
|
|
WriteIndented = true
|
|
};
|
|
|
|
/// <summary>
|
|
/// Reads the trust store from disk. Returns an empty dictionary if the file does not exist.
|
|
/// Throws if the file exists but is corrupt/unreadable — this prevents a silent TOFU
|
|
/// downgrade where a corrupt store causes all hosts to be re-auto-trusted.
|
|
/// </summary>
|
|
private Dictionary<string, string> Load()
|
|
{
|
|
if (!File.Exists(_filePath))
|
|
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
var json = File.ReadAllText(_filePath);
|
|
var raw = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
|
return raw is null
|
|
? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
: new Dictionary<string, string>(raw, StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
/// <summary>Writes the trust store to disk atomically via a temp-file swap.</summary>
|
|
private void Save(Dictionary<string, string> store)
|
|
{
|
|
Directory.CreateDirectory(_dir);
|
|
|
|
var json = JsonSerializer.Serialize(store, JsonOpts);
|
|
|
|
// Write to a temp file first, then replace, to avoid corruption on crash
|
|
var tmp = _filePath + ".tmp";
|
|
File.WriteAllText(tmp, json);
|
|
File.Move(tmp, _filePath, overwrite: true);
|
|
}
|
|
}
|