the files

This commit is contained in:
niko
2026-10-14 06:59:51 +02:00
parent f6175893fc
commit 2040419525
1855 changed files with 6735668 additions and 0 deletions
@@ -0,0 +1,289 @@
using System.Buffers.Binary;
using System.Collections;
using System.Text;
namespace CampusNet.Protocol;
/// <summary>
/// A value the client sent as a custom type ('c'): PUN registers Vector3/Quaternion/Vector2/Player
/// etc. under one-letter codes. The server never interprets them - it stores and re-emits the bytes.
/// </summary>
public sealed record CustomValue(byte Code, byte[] Data);
/// <summary>An array the client sent with an explicit element type ('y'): re-emitted the same way.</summary>
public sealed record TypedArray(byte ElemType, object?[] Items, byte CustomCode = 0);
/// <summary>A Dictionary sent with typed keys/values ('D'); key/value type 0 = per-entry typed.</summary>
public sealed class TypedDictionary
{
public byte KeyType, ValueType;
public readonly List<KeyValuePair<object?, object?>> Entries = new();
}
/// <summary>
/// Photon GpBinary 1.6 - the wire format PUN uses on every Realtime connection (verified from a live
/// capture, see docs/photon-wire-findings.md). Big-endian throughout. Hashtables are the workhorse:
/// keys are typed values (bytes for system props, strings for custom props).
/// </summary>
public static class GpBinary16
{
public const byte Magic = 0xF3;
// Message types (the byte after 0xF3; bit 0x80 = encrypted body).
public const byte MsgInit = 0, MsgInitResponse = 1, MsgOperationRequest = 2, MsgOperationResponse = 3,
MsgEvent = 4, MsgInternalOperationRequest = 6, MsgInternalOperationResponse = 7,
MsgMessage = 8, MsgRawMessage = 9;
public const byte EncryptedFlag = 0x80;
// ---------------------------------------------------------------- reading
public ref struct Reader
{
private readonly ReadOnlySpan<byte> _b;
public int Pos;
public Reader(ReadOnlySpan<byte> b, int pos = 0) { _b = b; Pos = pos; }
public int Remaining => _b.Length - Pos;
public byte U8() => _b[Pos++];
public short I16() { var v = BinaryPrimitives.ReadInt16BigEndian(_b.Slice(Pos)); Pos += 2; return v; }
public int I32() { var v = BinaryPrimitives.ReadInt32BigEndian(_b.Slice(Pos)); Pos += 4; return v; }
public long I64() { var v = BinaryPrimitives.ReadInt64BigEndian(_b.Slice(Pos)); Pos += 8; return v; }
public float F32() { var v = BinaryPrimitives.ReadSingleBigEndian(_b.Slice(Pos)); Pos += 4; return v; }
public double F64() { var v = BinaryPrimitives.ReadDoubleBigEndian(_b.Slice(Pos)); Pos += 8; return v; }
public byte[] Bytes(int n) { var v = _b.Slice(Pos, n).ToArray(); Pos += n; return v; }
public string Str() { int n = I16(); var s = Encoding.UTF8.GetString(_b.Slice(Pos, n)); Pos += n; return s; }
public object? Value() => Value(U8());
public object? Value(byte t)
{
switch ((char)t)
{
case '*': return null;
case 'b': return U8();
case 'o': return U8() != 0;
case 'k': return I16();
case 'i': return I32();
case 'l': return I64();
case 'f': return F32();
case 'd': return F64();
case 's': return Str();
case 'x': { int n = I32(); return Bytes(n); }
case 'n': { int n = I32(); var a = new int[n]; for (int i = 0; i < n; i++) a[i] = I32(); return a; }
case 'a': { int n = I16(); var a = new string[n]; for (int i = 0; i < n; i++) a[i] = Str(); return a; }
case 'h':
{
int n = I16(); var h = new Hashtable(n);
for (int i = 0; i < n; i++) { var k = Value(); var v = Value(); if (k != null) h[k] = v; }
return h;
}
case 'D':
{
var d = new TypedDictionary { KeyType = U8(), ValueType = U8() };
int n = I16();
for (int i = 0; i < n; i++)
{
var k = d.KeyType == 0 ? Value() : Value(d.KeyType);
var v = d.ValueType == 0 ? Value() : Value(d.ValueType);
d.Entries.Add(new(k, v));
}
return d;
}
case 'z': { int n = I16(); var a = new object?[n]; for (int i = 0; i < n; i++) a[i] = Value(); return a; }
case 'y':
{
int n = I16(); byte et = U8();
if ((char)et == 'c')
{
byte code = U8(); var items = new object?[n];
for (int i = 0; i < n; i++) { int len = I16(); items[i] = new CustomValue(code, Bytes(len)); }
return new TypedArray(et, items, code);
}
var arr = new object?[n];
for (int i = 0; i < n; i++) arr[i] = Value(et);
return new TypedArray(et, arr);
}
case 'c': { byte code = U8(); int len = I16(); return new CustomValue(code, Bytes(len)); }
default: throw new InvalidDataException($"GpBinary16: unknown type 0x{t:x2} at {Pos - 1}");
}
}
public Dictionary<byte, object?> Params()
{
int n = I16(); var d = new Dictionary<byte, object?>(n);
for (int i = 0; i < n; i++) { byte k = U8(); d[k] = Value(); }
return d;
}
}
public sealed class OperationRequest { public byte Code; public Dictionary<byte, object?> Params = new(); public int PayloadBytes; /* wire size of the whole message, for budgets */ public Dictionary<byte, byte[]>? Raw; /* 1.8 only: each param's encoded value, type byte included - relayed verbatim (Photon Chat messages) */ }
public sealed class OperationResponse { public byte Code; public short ReturnCode; public string? Debug; public Dictionary<byte, object?> Params = new(); }
public sealed class EventData { public byte Code; public Dictionary<byte, object?> Params = new(); }
public static OperationRequest ReadOperationRequest(ReadOnlySpan<byte> body)
{
var r = new Reader(body);
var op = new OperationRequest { Code = r.U8() };
op.Params = r.Params();
return op;
}
public static OperationResponse ReadOperationResponse(ReadOnlySpan<byte> body)
{
var r = new Reader(body);
var op = new OperationResponse { Code = r.U8(), ReturnCode = r.I16() };
op.Debug = r.Value() as string;
op.Params = r.Params();
return op;
}
public static EventData ReadEvent(ReadOnlySpan<byte> body)
{
var r = new Reader(body);
var ev = new EventData { Code = r.U8() };
ev.Params = r.Params();
return ev;
}
// ---------------------------------------------------------------- writing
public sealed class Writer
{
private byte[] _b = new byte[512];
public int Length;
public Writer(int cap = 512) { _b = new byte[cap]; }
private void Ensure(int n) { if (Length + n > _b.Length) Array.Resize(ref _b, Math.Max(_b.Length * 2, Length + n)); }
public void U8(byte v) { Ensure(1); _b[Length++] = v; }
public void I16(short v) { Ensure(2); BinaryPrimitives.WriteInt16BigEndian(_b.AsSpan(Length), v); Length += 2; }
public void I32(int v) { Ensure(4); BinaryPrimitives.WriteInt32BigEndian(_b.AsSpan(Length), v); Length += 4; }
public void I64(long v) { Ensure(8); BinaryPrimitives.WriteInt64BigEndian(_b.AsSpan(Length), v); Length += 8; }
public void F32(float v) { Ensure(4); BinaryPrimitives.WriteSingleBigEndian(_b.AsSpan(Length), v); Length += 4; }
public void F64(double v) { Ensure(8); BinaryPrimitives.WriteDoubleBigEndian(_b.AsSpan(Length), v); Length += 8; }
public void Raw(ReadOnlySpan<byte> s) { Ensure(s.Length); s.CopyTo(_b.AsSpan(Length)); Length += s.Length; }
public void Str(string s) { var n = Encoding.UTF8.GetByteCount(s); I16((short)n); Ensure(n); Encoding.UTF8.GetBytes(s, _b.AsSpan(Length)); Length += n; }
public byte[] ToArray() => _b.AsSpan(0, Length).ToArray();
public ReadOnlySpan<byte> Span => _b.AsSpan(0, Length);
/// <summary>Write a typed value (type byte + data).</summary>
public void Value(object? v)
{
switch (v)
{
case null: U8((byte)'*'); break;
case byte b: U8((byte)'b'); U8(b); break;
case bool o: U8((byte)'o'); U8(o ? (byte)1 : (byte)0); break;
case short k: U8((byte)'k'); I16(k); break;
case int i: U8((byte)'i'); I32(i); break;
case long l: U8((byte)'l'); I64(l); break;
case float f: U8((byte)'f'); F32(f); break;
case double d: U8((byte)'d'); F64(d); break;
case string s: U8((byte)'s'); Str(s); break;
case byte[] x: U8((byte)'x'); I32(x.Length); Raw(x); break;
case int[] n: U8((byte)'n'); I32(n.Length); foreach (var e in n) I32(e); break;
case string[] a: U8((byte)'a'); I16((short)a.Length); foreach (var e in a) Str(e); break;
case Hashtable h:
U8((byte)'h'); I16((short)h.Count);
foreach (DictionaryEntry e in h) { Value(e.Key); Value(e.Value); }
break;
case TypedDictionary td:
U8((byte)'D'); U8(td.KeyType); U8(td.ValueType); I16((short)td.Entries.Count);
foreach (var e in td.Entries)
{
if (td.KeyType == 0) Value(e.Key); else ValueBody(e.Key, td.KeyType);
if (td.ValueType == 0) Value(e.Value); else ValueBody(e.Value, td.ValueType);
}
break;
case TypedArray ta:
U8((byte)'y'); I16((short)ta.Items.Length); U8(ta.ElemType);
if ((char)ta.ElemType == 'c')
{
U8(ta.CustomCode);
foreach (var e in ta.Items) { var c = (CustomValue)e!; I16((short)c.Data.Length); Raw(c.Data); }
}
else foreach (var e in ta.Items) ValueBody(e, ta.ElemType);
break;
case object?[] z: U8((byte)'z'); I16((short)z.Length); foreach (var e in z) Value(e); break;
case CustomValue c: U8((byte)'c'); U8(c.Code); I16((short)c.Data.Length); Raw(c.Data); break;
case Dictionary<byte, object?> dbo: // convenience: byte-keyed dictionary as Hashtable
U8((byte)'h'); I16((short)dbo.Count);
foreach (var e in dbo) { Value(e.Key); Value(e.Value); }
break;
case Dictionary<int, object?> dio: // actorNr -> props as Hashtable with int keys
U8((byte)'h'); I16((short)dio.Count);
foreach (var e in dio) { Value(e.Key); Value(e.Value); }
break;
case Dictionary<object, object?> doo:
U8((byte)'h'); I16((short)doo.Count);
foreach (var e in doo) { Value(e.Key); Value(e.Value); }
break;
default: throw new NotSupportedException($"GpBinary16: cannot serialize {v.GetType()}");
}
}
/// <summary>Write only the data of a value whose type byte was already written (typed arrays/dictionaries).</summary>
private void ValueBody(object? v, byte t)
{
// Re-use Value() and drop the leading type byte it wrote.
int start = Length;
Value(v);
if (_b[start] != t)
{
// Element type mismatch (e.g. int in a 'k' array) - fix up the type byte position.
throw new InvalidDataException($"GpBinary16: element type {(char)_b[start]} does not match array type {(char)t}");
}
Buffer.BlockCopy(_b, start + 1, _b, start, Length - start - 1);
Length--;
}
public void Params(IReadOnlyDictionary<byte, object?> p)
{
I16((short)p.Count);
foreach (var e in p) { U8(e.Key); Value(e.Value); }
}
}
public static byte[] OperationResponseMessage(byte opCode, short returnCode, string? debug, IReadOnlyDictionary<byte, object?> p, bool internalOp = false)
{
var w = new Writer();
w.U8(Magic); w.U8(internalOp ? MsgInternalOperationResponse : MsgOperationResponse);
w.U8(opCode); w.I16(returnCode); w.Value(debug); w.Params(p);
return w.ToArray();
}
public static byte[] EventMessage(byte eventCode, IReadOnlyDictionary<byte, object?> p)
{
var w = new Writer();
w.U8(Magic); w.U8(MsgEvent);
w.U8(eventCode); w.Params(p);
return w.ToArray();
}
/// <summary>Body (after the 0xF3 + type header) of an operation response - what gets encrypted when a reply must be.</summary>
public static byte[] OperationResponseBody(byte opCode, short returnCode, string? debug, IReadOnlyDictionary<byte, object?> p)
{
var w = new Writer();
w.U8(opCode); w.I16(returnCode); w.Value(debug); w.Params(p);
return w.ToArray();
}
public static byte[] InitResponse() => new byte[] { Magic, MsgInitResponse, 0 };
public static string Describe(object? v, int depth = 0)
{
switch (v)
{
case null: return "null";
case string s: return $"\"{s}\"";
case byte[] x: return $"byte[{x.Length}]";
case int[] n: return "[" + string.Join(",", n) + "]";
case string[] a: return "[" + string.Join(",", a.Select(s => "\"" + s + "\"")) + "]";
case Hashtable h:
if (depth > 2) return $"{{…{h.Count}}}";
return "{" + string.Join(", ", h.Cast<DictionaryEntry>().Select(e => Describe(e.Key, depth + 1) + ":" + Describe(e.Value, depth + 1))) + "}";
case TypedDictionary d: return $"D[{d.Entries.Count}]";
case TypedArray ta: return $"{(char)ta.ElemType}[{ta.Items.Length}]";
case object?[] z: return "(" + string.Join(", ", z.Select(e => Describe(e, depth + 1))) + ")";
case CustomValue c: return $"custom'{(char)c.Code}'[{c.Data.Length}]";
default: return v.ToString() ?? "?";
}
}
}
@@ -0,0 +1,211 @@
using System.Buffers.Binary;
using System.Collections;
using System.Text;
namespace CampusNet.Protocol;
/// <summary>
/// Photon GpBinary 1.8 - what the Photon Voice / newer client libraries speak (the PUN connections
/// use 1.6). Compact: counts are varints, ints are zig-zag varints, type codes are numeric.
/// Enough of it to run the name-server hop for the voice client and to hand it addresses.
/// </summary>
public static class GpBinary18
{
// GpType
private const byte TUnknown = 0, TBoolean = 2, TByte = 3, TShort = 4, TFloat = 5, TDouble = 6, TString = 7, TNull = 8,
TCompressedInt = 9, TCompressedLong = 10, TInt1 = 11, TInt1_ = 12, TInt2 = 13, TInt2_ = 14, TL1 = 15, TL1_ = 16,
TL2 = 17, TL2_ = 18, TCustom = 19, TDictionary = 20, THashtable = 21, TObjectArray = 23, TOperationRequest = 24,
TOperationResponse = 25, TEventData = 26, TBooleanFalse = 27, TBooleanTrue = 28, TShortZero = 29, TIntZero = 30,
TLongZero = 31, TFloatZero = 32, TDoubleZero = 33, TByteZero = 34, TArray = 64, TByteArray = 67, TCustomSlim = 128;
public ref struct Reader
{
private readonly ReadOnlySpan<byte> _b; public int Pos;
public Reader(ReadOnlySpan<byte> b, int pos = 0) { _b = b; Pos = pos; }
public byte U8() => _b[Pos++];
public uint VarUInt() { uint v = 0; int shift = 0; while (true) { byte b = U8(); v |= (uint)(b & 0x7F) << shift; if ((b & 0x80) == 0) return v; shift += 7; } }
public ulong VarULong() { ulong v = 0; int shift = 0; while (true) { byte b = U8(); v |= (ulong)(b & 0x7F) << shift; if ((b & 0x80) == 0) return v; shift += 7; } }
public int ZigInt() { uint u = VarUInt(); return (int)(u >> 1) ^ -(int)(u & 1); }
public long ZigLong() { ulong u = VarULong(); return (long)(u >> 1) ^ -(long)(u & 1); }
public short I16() { var v = BinaryPrimitives.ReadInt16BigEndian(_b.Slice(Pos)); Pos += 2; return v; }
public float F32() { var v = BinaryPrimitives.ReadSingleBigEndian(_b.Slice(Pos)); Pos += 4; return v; }
public double F64() { var v = BinaryPrimitives.ReadDoubleBigEndian(_b.Slice(Pos)); Pos += 8; return v; }
public byte[] Bytes(int n) { var v = _b.Slice(Pos, n).ToArray(); Pos += n; return v; }
public string Str() { int n = (int)VarUInt(); var s = Encoding.UTF8.GetString(_b.Slice(Pos, n)); Pos += n; return s; }
public object? Value() => Value(U8());
public object? Value(byte t)
{
switch (t)
{
case TNull: return null;
case TBoolean: return U8() != 0;
case TBooleanFalse: return false;
case TBooleanTrue: return true;
case TByte: return U8();
case TByteZero: return (byte)0;
case TShort: return I16();
case TShortZero: return (short)0;
case TFloat: return F32();
case TFloatZero: return 0f;
case TDouble: return F64();
case TDoubleZero: return 0d;
case TString: return Str();
case TCompressedInt: return ZigInt();
case TIntZero: return 0;
case TInt1: return (int)U8();
case TInt1_: return -(int)U8();
case TInt2: return (int)(ushort)I16();
case TInt2_: return -(int)(ushort)I16();
case TCompressedLong: return ZigLong();
case TLongZero: return 0L;
case TL1: return (long)U8();
case TL1_: return -(long)U8();
case TL2: return (long)(ushort)I16();
case TL2_: return -(long)(ushort)I16();
case TByteArray: { int n = (int)VarUInt(); return Bytes(n); }
case THashtable:
{
int n = (int)VarUInt(); var h = new Hashtable(n);
for (int i = 0; i < n; i++) { var k = Value(); var v = Value(); if (k != null) h[k] = v; }
return h;
}
case TDictionary:
{
byte kt = U8(), vt = U8(); int n = (int)VarUInt();
var d = new TypedDictionary { KeyType = kt, ValueType = vt };
for (int i = 0; i < n; i++) { var k = kt == TUnknown ? Value() : Value(kt); var v = vt == TUnknown ? Value() : Value(vt); d.Entries.Add(new(k, v)); }
return d;
}
case TObjectArray: { int n = (int)VarUInt(); var a = new object?[n]; for (int i = 0; i < n; i++) a[i] = Value(); return a; }
case TCustom: { byte code = U8(); int n = (int)VarUInt(); return new CustomValue(code, Bytes(n)); }
default:
if ((t & TCustomSlim) != 0) { byte code = (byte)(t & 0x7F); int n = (int)VarUInt(); return new CustomValue(code, Bytes(n)); }
if ((t & TArray) != 0)
{
int n = (int)VarUInt(); byte et = (byte)(t & ~TArray);
if (et == TBoolean)
{
// Packed: 8 per byte, least significant bit first.
var a = new bool[n]; int i = 0;
while (i < n) { byte bits = U8(); for (int k = 0; k < 8 && i < n; k++, i++) a[i] = (bits & (1 << k)) != 0; }
return a;
}
if (et == TString) { var a = new string[n]; for (int i = 0; i < n; i++) a[i] = Str(); return a; }
if (et == TCompressedInt) { var a = new int[n]; for (int i = 0; i < n; i++) a[i] = ZigInt(); return a; }
var arr = new object?[n];
for (int i = 0; i < n; i++) arr[i] = Value(et);
return new TypedArray(et, arr);
}
throw new InvalidDataException($"GpBinary18: unknown type {t} at {Pos - 1}");
}
}
public Dictionary<byte, object?> Params() => Params(null);
public Dictionary<byte, object?> Params(Dictionary<byte, byte[]>? raw)
{
int n = U8(); var d = new Dictionary<byte, object?>(n);
for (int i = 0; i < n; i++)
{
byte k = U8(); int start = Pos;
d[k] = Value();
if (raw != null) raw[k] = _b.Slice(start, Pos - start).ToArray();
}
return d;
}
}
public static GpBinary16.OperationRequest ReadOperationRequest(ReadOnlySpan<byte> body)
{
var r = new Reader(body);
var op = new GpBinary16.OperationRequest { Code = r.U8(), Raw = new() };
op.Params = r.Params(op.Raw);
return op;
}
public sealed class Writer
{
private byte[] _b = new byte[256]; public int Length;
private void Ensure(int n) { if (Length + n > _b.Length) Array.Resize(ref _b, Math.Max(_b.Length * 2, Length + n)); }
public void U8(byte v) { Ensure(1); _b[Length++] = v; }
public void Raw(ReadOnlySpan<byte> s) { Ensure(s.Length); s.CopyTo(_b.AsSpan(Length)); Length += s.Length; }
public void VarUInt(uint v) { while (v >= 0x80) { U8((byte)(v | 0x80)); v >>= 7; } U8((byte)v); }
public void I16(short v) { Ensure(2); BinaryPrimitives.WriteInt16BigEndian(_b.AsSpan(Length), v); Length += 2; }
public void Str(string s) { var n = Encoding.UTF8.GetByteCount(s); VarUInt((uint)n); Ensure(n); Encoding.UTF8.GetBytes(s, _b.AsSpan(Length)); Length += n; }
public byte[] ToArray() => _b.AsSpan(0, Length).ToArray();
public void Value(object? v)
{
switch (v)
{
case null: U8(TNull); break;
case bool o: U8(o ? TBooleanTrue : TBooleanFalse); break;
case byte b: if (b == 0) U8(TByteZero); else { U8(TByte); U8(b); } break;
case short k: if (k == 0) U8(TShortZero); else { U8(TShort); I16(k); } break;
case int i:
if (i == 0) U8(TIntZero);
else { U8(TCompressedInt); uint z = (uint)((i << 1) ^ (i >> 31)); VarUInt(z); }
break;
case long l:
if (l == 0) U8(TLongZero);
else { U8(TCompressedLong); ulong z = (ulong)((l << 1) ^ (l >> 63)); while (z >= 0x80) { U8((byte)(z | 0x80)); z >>= 7; } U8((byte)z); }
break;
case float f: if (f == 0) U8(TFloatZero); else { U8(TFloat); Ensure(4); BinaryPrimitives.WriteSingleBigEndian(_b.AsSpan(Length), f); Length += 4; } break;
case double d: if (d == 0) U8(TDoubleZero); else { U8(TDouble); Ensure(8); BinaryPrimitives.WriteDoubleBigEndian(_b.AsSpan(Length), d); Length += 8; } break;
case string s: U8(TString); Str(s); break;
case byte[] x: U8(TByteArray); VarUInt((uint)x.Length); Raw(x); break;
case string[] a: U8(TArray | TString); VarUInt((uint)a.Length); foreach (var e in a) Str(e); break;
case int[] n: U8(TArray | TCompressedInt); VarUInt((uint)n.Length); foreach (var e in n) { uint z = (uint)((e << 1) ^ (e >> 31)); VarUInt(z); } break;
case Hashtable h: U8(THashtable); VarUInt((uint)h.Count); foreach (DictionaryEntry e in h) { Value(e.Key); Value(e.Value); } break;
case object?[] z: U8(TObjectArray); VarUInt((uint)z.Length); foreach (var e in z) Value(e); break;
case bool[] ba:
{
U8(TArray | TBoolean); VarUInt((uint)ba.Length);
for (int i = 0; i < ba.Length; i += 8)
{
byte bits = 0;
for (int k = 0; k < 8 && i + k < ba.Length; k++) if (ba[i + k]) bits |= (byte)(1 << k);
U8(bits);
}
break;
}
case RawValue rv: Raw(rv.Bytes); break;
case CustomValue c: U8(TCustom); U8(c.Code); VarUInt((uint)c.Data.Length); Raw(c.Data); break;
default: throw new NotSupportedException($"GpBinary18: cannot serialize {v.GetType()}");
}
}
public void Params(IReadOnlyDictionary<byte, object?> p)
{
U8((byte)p.Count);
foreach (var e in p) { U8(e.Key); Value(e.Value); }
}
}
public static byte[] OperationResponseMessage(byte opCode, short returnCode, string? debug, IReadOnlyDictionary<byte, object?> p, bool internalOp = false)
{
var w = new Writer();
w.U8(GpBinary16.Magic); w.U8(internalOp ? GpBinary16.MsgInternalOperationResponse : GpBinary16.MsgOperationResponse);
w.U8(opCode); w.I16(returnCode); w.Value(debug); w.Params(p);
return w.ToArray();
}
public static byte[] EventMessage(byte eventCode, IReadOnlyDictionary<byte, object?> p)
{
var w = new Writer();
w.U8(GpBinary16.Magic); w.U8(GpBinary16.MsgEvent);
w.U8(eventCode); w.Params(p);
return w.ToArray();
}
}
/// <summary>An already-encoded 1.8 value (type byte included), written back out byte for byte. How
/// a Photon Chat message is relayed: whatever the sender serialised - custom types included -
/// reaches the subscribers exactly as sent, never decoded and re-encoded.</summary>
public sealed class RawValue
{
public readonly byte[] Bytes;
public RawValue(byte[] bytes) { Bytes = bytes; }
public override string ToString() => $"raw[{Bytes.Length}]";
}
@@ -0,0 +1,97 @@
using System.Globalization;
using System.Numerics;
using System.Security.Cryptography;
namespace CampusNet.Protocol;
/// <summary>
/// The client's InitEncryption handshake: Diffie-Hellman over the 768-bit Oakley group (the client's
/// public key is 96 bytes - see the capture), shared secret hashed with SHA-256 into an AES-256-CBC
/// key (zero IV, PKCS7). The exact byte order Photon feeds SHA-256 isn't verifiable from a capture, so
/// the first encrypted message is tried against every candidate key and the one that yields a
/// parseable body is kept (see TryDecryptAdaptive).
/// </summary>
public sealed class PhotonCrypto
{
// RFC 2409 Oakley group 1 (768-bit).
private static readonly BigInteger Prime = BigInteger.Parse(
"0FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DD" +
"EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A63A3620FFFFFFFFFFFFFFFF",
NumberStyles.HexNumber);
public static int Generator = int.TryParse(Environment.GetEnvironmentVariable("CAMPUSNET_DH_GENERATOR"), out var g) ? g : 22;
private readonly BigInteger _priv;
public readonly byte[] PublicKey; // 96 bytes, big-endian unsigned
private BigInteger _shared;
private byte[]? _aesKey; // locked in once a decrypt succeeds
private List<byte[]>? _candidates;
public PhotonCrypto()
{
var rnd = RandomNumberGenerator.GetBytes(64);
_priv = new BigInteger(rnd, isUnsigned: true, isBigEndian: true) % (Prime - 2) + 1;
var pub = BigInteger.ModPow(Generator, _priv, Prime);
PublicKey = ToFixed(pub, 96);
}
public bool Ready => _aesKey != null || _candidates != null;
/// <summary>Derive the candidate AES keys from the client's public key.</summary>
public void DeriveSharedKey(byte[] clientPublicKey)
{
var a = new BigInteger(clientPublicKey, isUnsigned: true, isBigEndian: true);
_shared = BigInteger.ModPow(a, _priv, Prime);
var be = _shared.ToByteArray(isUnsigned: true, isBigEndian: true);
var le = _shared.ToByteArray(isUnsigned: true, isBigEndian: false);
var leSigned = _shared.ToByteArray(isUnsigned: false, isBigEndian: false);
var be96 = ToFixed(_shared, 96);
_candidates = new List<byte[]>();
foreach (var bytes in new[] { be, be96, le, leSigned })
{
var k = SHA256.HashData(bytes);
if (!_candidates.Any(c => c.AsSpan().SequenceEqual(k))) _candidates.Add(k);
}
_aesKey = null;
}
private static byte[] ToFixed(BigInteger v, int len)
{
var raw = v.ToByteArray(isUnsigned: true, isBigEndian: true);
if (raw.Length == len) return raw;
var outp = new byte[len];
if (raw.Length > len) Buffer.BlockCopy(raw, raw.Length - len, outp, 0, len);
else Buffer.BlockCopy(raw, 0, outp, len - raw.Length, raw.Length);
return outp;
}
private static byte[] AesRun(byte[] key, byte[] data, bool encrypt)
{
using var aes = Aes.Create();
aes.Key = key; aes.IV = new byte[16]; aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7;
using var t = encrypt ? aes.CreateEncryptor() : aes.CreateDecryptor();
return t.TransformFinalBlock(data, 0, data.Length);
}
/// <summary>Decrypt an encrypted message body; on first use, pick the candidate key that yields a body `validate` accepts.</summary>
public byte[]? TryDecryptAdaptive(byte[] cipher, Func<byte[], bool> validate)
{
if (_aesKey != null)
{
try { return AesRun(_aesKey, cipher, false); } catch { return null; }
}
if (_candidates == null) return null;
foreach (var k in _candidates)
{
byte[] plain;
try { plain = AesRun(k, cipher, false); } catch { continue; }
if (validate(plain)) { _aesKey = k; return plain; }
}
return null;
}
public byte[]? Encrypt(byte[] plain)
{
var k = _aesKey ?? _candidates?.FirstOrDefault();
return k == null ? null : AesRun(k, plain, true);
}
}