- Heartbeat an die Website, Abmelden beim Beenden (--private, --public-address, --description, --master) - Spiel-Tickets pruefen (Signatur der Website), Name aus dem Konto, --no-auth fuer LAN - IPv4 und IPv6, Protokoll 2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
63 lines
2.8 KiB
C#
63 lines
2.8 KiB
C#
using System;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
|
|
namespace BSO.Net
|
|
{
|
|
/// <summary>
|
|
/// Spiel-Ticket der BSO-Website: base64url(JSON {u, id, iat, exp}) + "." + base64url(RSA-SHA256-Signatur). Der Launcher
|
|
/// holt es beim Spielen und schreibt es in BSO.json, die Mod schickt es im Hello mit. Die Mod liest es nur (Name, Ablauf),
|
|
/// pruefen kann es nur der Server mit dem oeffentlichen Schluessel der Website (Verify) - wer die Mod veraendert, kommt so
|
|
/// trotzdem nicht ohne gueltiges Konto auf einen Server.
|
|
/// </summary>
|
|
public sealed class Ticket
|
|
{
|
|
public string Username { get; private set; }
|
|
public string AccountId { get; private set; }
|
|
public DateTime Expires { get; private set; }
|
|
public bool Expired => DateTime.UtcNow >= Expires;
|
|
|
|
/// <summary>Inhalt lesen, ohne die Signatur zu pruefen. null = kein Ticket / kaputt.</summary>
|
|
public static Ticket Read(string ticket)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrEmpty(ticket) || ticket.Length > 4096) return null;
|
|
string[] parts = ticket.Split('.');
|
|
if (parts.Length != 2) return null;
|
|
using var doc = JsonDocument.Parse(FromB64(parts[0]));
|
|
var r = doc.RootElement;
|
|
return new Ticket
|
|
{
|
|
Username = r.GetProperty("u").GetString(),
|
|
AccountId = r.GetProperty("id").GetString(),
|
|
Expires = DateTimeOffset.FromUnixTimeSeconds(r.GetProperty("exp").GetInt64()).UtcDateTime
|
|
};
|
|
}
|
|
catch { return null; }
|
|
}
|
|
|
|
/// <summary>Signatur pruefen (Server). Gueltig und nicht abgelaufen -> Ticket, sonst null mit Grund.</summary>
|
|
public static Ticket Verify(string ticket, RSA publicKey, out string error)
|
|
{
|
|
error = null;
|
|
Ticket t = Read(ticket);
|
|
if (t == null) { error = "Kein gueltiges BSO-Konto - bitte ueber den BSO Launcher starten"; return null; }
|
|
string[] parts = ticket.Split('.');
|
|
bool ok;
|
|
try { ok = publicKey.VerifyData(Encoding.ASCII.GetBytes(parts[0]), FromB64(parts[1]), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); }
|
|
catch { ok = false; }
|
|
if (!ok) { error = "Ungueltiges Spiel-Ticket - bitte ueber den BSO Launcher starten"; return null; }
|
|
if (t.Expired) { error = "Spiel-Ticket abgelaufen - bitte im BSO Launcher neu auf SPIELEN druecken"; return null; }
|
|
return t;
|
|
}
|
|
|
|
static byte[] FromB64(string s)
|
|
{
|
|
s = s.Replace('-', '+').Replace('_', '/');
|
|
return Convert.FromBase64String(s.PadRight(s.Length + (4 - s.Length % 4) % 4, '='));
|
|
}
|
|
}
|
|
}
|