Add project files.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Wingnal.Protocol.Crypto;
|
||||
|
||||
namespace Wingnal.Protocol.Spqr;
|
||||
|
||||
/// <summary>
|
||||
/// The SPQR authenticator (ported from SparsePostQuantumRatchet v1.5.1 src/authenticator.rs). Keeps a
|
||||
/// rolling root key + MAC key, advanced each epoch via HKDF, and MACs the chunked header/ciphertext
|
||||
/// material so a man-in-the-middle can't tamper with the spread-out ML-KEM bytes. HKDF-SHA256 +
|
||||
/// HMAC-SHA256; the domain-separation strings must match Signal byte-for-byte.
|
||||
/// </summary>
|
||||
public sealed class Authenticator
|
||||
{
|
||||
public const int MacSize = 32;
|
||||
|
||||
private static readonly byte[] ZeroSalt = new byte[32];
|
||||
private static readonly byte[] UpdateInfo = "Signal_PQCKA_V1_MLKEM768:Authenticator Update"u8.ToArray();
|
||||
private static readonly byte[] CiphertextLabel = "Signal_PQCKA_V1_MLKEM768:ciphertext"u8.ToArray();
|
||||
private static readonly byte[] HeaderLabel = "Signal_PQCKA_V1_MLKEM768:ekheader"u8.ToArray();
|
||||
|
||||
private byte[] _rootKey = new byte[32];
|
||||
private byte[] _macKey = new byte[32];
|
||||
|
||||
public Authenticator(byte[] rootKey, ulong epoch) => Update(epoch, rootKey);
|
||||
|
||||
private Authenticator(byte[] rootKey, byte[] macKey, bool _)
|
||||
{
|
||||
_rootKey = (byte[])rootKey.Clone();
|
||||
_macKey = (byte[])macKey.Clone();
|
||||
}
|
||||
|
||||
public byte[] RootKey => _rootKey;
|
||||
public byte[] MacKey => _macKey;
|
||||
|
||||
/// <summary>Deep copy (used so a failed recv doesn't corrupt committed state).</summary>
|
||||
public Authenticator Clone() => new(_rootKey, _macKey, true);
|
||||
|
||||
internal void Write(System.IO.BinaryWriter w) { w.WriteBlob(_rootKey); w.WriteBlob(_macKey); }
|
||||
internal static Authenticator Read(System.IO.BinaryReader r) => new(r.ReadBlob(), r.ReadBlob(), true);
|
||||
|
||||
public void Update(ulong epoch, byte[] k)
|
||||
{
|
||||
byte[] ikm = Concat(_rootKey, k);
|
||||
byte[] info = Concat(UpdateInfo, Be64(epoch));
|
||||
byte[] okm = CryptoPrimitives.Hkdf(ikm, ZeroSalt, info, 64);
|
||||
_rootKey = okm[..32];
|
||||
_macKey = okm[32..];
|
||||
}
|
||||
|
||||
public byte[] MacCiphertext(ulong epoch, byte[] ciphertext) =>
|
||||
Mac(CiphertextLabel, epoch, ciphertext);
|
||||
|
||||
public byte[] MacHeader(ulong epoch, byte[] header) =>
|
||||
Mac(HeaderLabel, epoch, header);
|
||||
|
||||
public bool VerifyCiphertext(ulong epoch, byte[] ciphertext, byte[] expectedMac) =>
|
||||
CryptographicOperations.FixedTimeEquals(expectedMac, MacCiphertext(epoch, ciphertext));
|
||||
|
||||
public bool VerifyHeader(ulong epoch, byte[] header, byte[] expectedMac) =>
|
||||
CryptographicOperations.FixedTimeEquals(expectedMac, MacHeader(epoch, header));
|
||||
|
||||
private byte[] Mac(byte[] label, ulong epoch, byte[] data)
|
||||
{
|
||||
byte[] macData = Concat(label, Be64(epoch), data);
|
||||
return CryptoPrimitives.HmacSha256(_macKey, macData); // already 32 bytes
|
||||
}
|
||||
|
||||
private static byte[] Be64(ulong v)
|
||||
{
|
||||
var b = new byte[8];
|
||||
for (int i = 7; i >= 0; i--) { b[i] = (byte)(v & 0xFF); v >>= 8; }
|
||||
return b;
|
||||
}
|
||||
|
||||
private static byte[] Concat(params byte[][] parts)
|
||||
{
|
||||
int len = 0;
|
||||
foreach (byte[] p in parts) len += p.Length;
|
||||
var result = new byte[len];
|
||||
int off = 0;
|
||||
foreach (byte[] p in parts) { Buffer.BlockCopy(p, 0, result, off, p.Length); off += p.Length; }
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
using System.IO;
|
||||
using Wingnal.Protocol.Crypto;
|
||||
|
||||
namespace Wingnal.Protocol.Spqr;
|
||||
|
||||
public enum Direction { A2B, B2A }
|
||||
|
||||
public static class DirectionExtensions
|
||||
{
|
||||
public static Direction Switch(this Direction d) => d == Direction.A2B ? Direction.B2A : Direction.A2B;
|
||||
}
|
||||
|
||||
/// <summary>A shared secret for a given ratchet epoch.</summary>
|
||||
public sealed record EpochSecret(ulong Epoch, byte[] Secret);
|
||||
|
||||
/// <summary>Bounds for the symmetric chain (out-of-order tolerance / max forward jump).</summary>
|
||||
public sealed class ChainParams
|
||||
{
|
||||
public const uint DefaultMaxJump = 25_000;
|
||||
public const uint DefaultMaxOooKeys = 2_000;
|
||||
|
||||
public uint MaxJump { get; init; } = DefaultMaxJump;
|
||||
public uint MaxOooKeys { get; init; } = DefaultMaxOooKeys;
|
||||
|
||||
internal int TrimSize => (int)((long)MaxOooKeys * 11 / 10 + 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SPQR symmetric key chain (ported from SparsePostQuantumRatchet v1.5.1 src/chain.rs). Per epoch and
|
||||
/// direction it maintains a hash chain (HKDF) producing 32-byte keys; <see cref="SendKey"/> advances
|
||||
/// it and <see cref="RecvKey"/> retrieves keys by index, tolerating out-of-order delivery via a
|
||||
/// bounded key history. The A2B send chain matches the B2A receive chain (and vice versa).
|
||||
/// </summary>
|
||||
public sealed class Chain
|
||||
{
|
||||
private static readonly byte[] ZeroSalt = new byte[32];
|
||||
private static readonly byte[] StartInfo = "Signal PQ Ratchet V1 Chain Start"u8.ToArray(); // two spaces
|
||||
private static readonly byte[] NextInfo = "Signal PQ Ratchet V1 Chain Next"u8.ToArray();
|
||||
private static readonly byte[] AddEpochInfo = "Signal PQ Ratchet V1 Chain Add Epoch"u8.ToArray();
|
||||
private const int EpochsToKeepPriorToSendEpoch = 1;
|
||||
|
||||
private sealed class KeyHistory
|
||||
{
|
||||
private const int KeySize = 4 + 32;
|
||||
private readonly List<byte> _data = new();
|
||||
|
||||
/// <summary>Raw history bytes, for state serialization.</summary>
|
||||
public byte[] Data { get => _data.ToArray(); set { _data.Clear(); _data.AddRange(value); } }
|
||||
|
||||
public void Add(uint idx, byte[] key)
|
||||
{
|
||||
_data.AddRange(Be32(idx));
|
||||
_data.AddRange(key);
|
||||
}
|
||||
|
||||
public void Clear() => _data.Clear();
|
||||
|
||||
public void Gc(uint currentKey, ChainParams p)
|
||||
{
|
||||
if (_data.Count < p.TrimSize * KeySize) return;
|
||||
uint horizon = currentKey - p.MaxOooKeys;
|
||||
int i = 0;
|
||||
while (i < _data.Count)
|
||||
{
|
||||
uint entryIdx = ReadBe32(i);
|
||||
if (horizon > entryIdx) RemoveAt(i);
|
||||
else i += KeySize;
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveAt(int index)
|
||||
{
|
||||
int newEnd = _data.Count - KeySize;
|
||||
if (index + KeySize < _data.Count)
|
||||
for (int k = 0; k < KeySize; k++) _data[index + k] = _data[newEnd + k];
|
||||
_data.RemoveRange(newEnd, KeySize);
|
||||
}
|
||||
|
||||
public byte[] Get(uint at, uint currentCtr, ChainParams p)
|
||||
{
|
||||
if (at + p.MaxOooKeys < currentCtr)
|
||||
throw new SpqrException($"key trimmed: {at}");
|
||||
for (int i = 0; i < _data.Count; i += KeySize)
|
||||
if (ReadBe32(i) == at)
|
||||
{
|
||||
var outp = new byte[32];
|
||||
for (int k = 0; k < 32; k++) outp[k] = _data[i + 4 + k];
|
||||
RemoveAt(i);
|
||||
return outp;
|
||||
}
|
||||
throw new SpqrException($"key already requested: {at}");
|
||||
}
|
||||
|
||||
private uint ReadBe32(int i) =>
|
||||
(uint)((_data[i] << 24) | (_data[i + 1] << 16) | (_data[i + 2] << 8) | _data[i + 3]);
|
||||
}
|
||||
|
||||
private sealed class ChainEpochDirection
|
||||
{
|
||||
public uint Ctr;
|
||||
public byte[] Next;
|
||||
public readonly KeyHistory Prev = new();
|
||||
|
||||
public ChainEpochDirection(byte[] k) => Next = (byte[])k.Clone();
|
||||
|
||||
public (uint Idx, byte[] Key) NextKey()
|
||||
{
|
||||
Ctr += 1;
|
||||
byte[] info = Concat(Be32(Ctr), NextInfo);
|
||||
byte[] gen = CryptoPrimitives.Hkdf(Next, ZeroSalt, info, 64);
|
||||
Next = gen[..32];
|
||||
return (Ctr, gen[32..64]);
|
||||
}
|
||||
|
||||
public byte[] Key(uint at, ChainParams p)
|
||||
{
|
||||
if (at > Ctr)
|
||||
{
|
||||
if (at - Ctr > p.MaxJump) throw new SpqrException($"key jump {Ctr} -> {at}");
|
||||
}
|
||||
else if (at < Ctr)
|
||||
{
|
||||
return Prev.Get(at, Ctr, p);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new SpqrException($"key already requested: {at}");
|
||||
}
|
||||
|
||||
if (at > Ctr + p.MaxOooKeys) Prev.Clear();
|
||||
while (at > Ctr + 1)
|
||||
{
|
||||
(uint idx, byte[] k) = NextKey();
|
||||
if (Ctr + p.MaxOooKeys >= at) Prev.Add(idx, k);
|
||||
}
|
||||
Prev.Gc(Ctr, p);
|
||||
return NextKey().Key;
|
||||
}
|
||||
|
||||
public void ClearNext() => Next = Array.Empty<byte>();
|
||||
}
|
||||
|
||||
private sealed class ChainEpoch
|
||||
{
|
||||
public required ChainEpochDirection Send;
|
||||
public required ChainEpochDirection Recv;
|
||||
}
|
||||
|
||||
private readonly Direction _dir;
|
||||
private ulong _currentEpoch;
|
||||
private ulong _sendEpoch;
|
||||
private readonly LinkedList<ChainEpoch> _links = new();
|
||||
private byte[] _nextRoot;
|
||||
private readonly ChainParams _params;
|
||||
|
||||
public Chain(byte[] initialKey, Direction dir, ChainParams parameters)
|
||||
{
|
||||
_dir = dir;
|
||||
_params = parameters;
|
||||
byte[] gen = CryptoPrimitives.Hkdf(initialKey, ZeroSalt, StartInfo, 96);
|
||||
_nextRoot = gen[0..32];
|
||||
_links.AddLast(new ChainEpoch
|
||||
{
|
||||
Send = CedForDirection(gen, dir),
|
||||
Recv = CedForDirection(gen, dir.Switch()),
|
||||
});
|
||||
}
|
||||
|
||||
private static ChainEpochDirection CedForDirection(byte[] gen, Direction dir) =>
|
||||
new(dir == Direction.A2B ? gen[32..64] : gen[64..96]);
|
||||
|
||||
public void AddEpoch(EpochSecret epochSecret)
|
||||
{
|
||||
if (epochSecret.Epoch != _currentEpoch + 1)
|
||||
throw new SpqrException($"epoch must be {_currentEpoch + 1}, got {epochSecret.Epoch}");
|
||||
byte[] gen = CryptoPrimitives.Hkdf(epochSecret.Secret, _nextRoot, AddEpochInfo, 96);
|
||||
_currentEpoch = epochSecret.Epoch;
|
||||
_nextRoot = gen[0..32];
|
||||
_links.AddLast(new ChainEpoch
|
||||
{
|
||||
Send = CedForDirection(gen, _dir),
|
||||
Recv = CedForDirection(gen, _dir.Switch()),
|
||||
});
|
||||
}
|
||||
|
||||
private int EpochIdx(ulong epoch)
|
||||
{
|
||||
if (epoch > _currentEpoch) throw new SpqrException($"epoch out of range: {epoch}");
|
||||
int back = (int)(_currentEpoch - epoch);
|
||||
if (back >= _links.Count) throw new SpqrException($"epoch out of range: {epoch}");
|
||||
return _links.Count - 1 - back;
|
||||
}
|
||||
|
||||
public (uint Index, byte[] Key) SendKey(ulong epoch)
|
||||
{
|
||||
if (epoch < _sendEpoch) throw new SpqrException($"send key epoch decreased {_sendEpoch} -> {epoch}");
|
||||
int epochIndex = EpochIdx(epoch);
|
||||
if (_sendEpoch != epoch)
|
||||
{
|
||||
_sendEpoch = epoch;
|
||||
while (epochIndex > EpochsToKeepPriorToSendEpoch)
|
||||
{
|
||||
_links.RemoveFirst();
|
||||
epochIndex--;
|
||||
}
|
||||
int i = 0;
|
||||
foreach (ChainEpoch link in _links)
|
||||
{
|
||||
if (i >= epochIndex) break;
|
||||
link.Send.ClearNext();
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return LinkAt(epochIndex).Send.NextKey();
|
||||
}
|
||||
|
||||
public byte[] RecvKey(ulong epoch, uint index) => LinkAt(EpochIdx(epoch)).Recv.Key(index, _params);
|
||||
|
||||
private ChainEpoch LinkAt(int index)
|
||||
{
|
||||
LinkedListNode<ChainEpoch> node = _links.First!;
|
||||
for (int i = 0; i < index; i++) node = node.Next!;
|
||||
return node.Value;
|
||||
}
|
||||
|
||||
private static byte[] Be32(uint v) => new[] { (byte)(v >> 24), (byte)(v >> 16), (byte)(v >> 8), (byte)v };
|
||||
|
||||
private static byte[] Concat(byte[] a, byte[] b)
|
||||
{
|
||||
var r = new byte[a.Length + b.Length];
|
||||
Buffer.BlockCopy(a, 0, r, 0, a.Length);
|
||||
Buffer.BlockCopy(b, 0, r, a.Length, b.Length);
|
||||
return r;
|
||||
}
|
||||
|
||||
// ── serialization (local-only state persistence) ──
|
||||
|
||||
private Chain(Direction dir, ChainParams parameters)
|
||||
{
|
||||
_dir = dir;
|
||||
_params = parameters;
|
||||
_nextRoot = Array.Empty<byte>();
|
||||
}
|
||||
|
||||
internal void Write(BinaryWriter w)
|
||||
{
|
||||
w.Write((int)_dir);
|
||||
w.Write(_currentEpoch);
|
||||
w.Write(_sendEpoch);
|
||||
w.WriteBlob(_nextRoot);
|
||||
w.Write(_params.MaxJump);
|
||||
w.Write(_params.MaxOooKeys);
|
||||
w.Write(_links.Count);
|
||||
foreach (ChainEpoch link in _links) { WriteDir(w, link.Send); WriteDir(w, link.Recv); }
|
||||
}
|
||||
|
||||
private static void WriteDir(BinaryWriter w, ChainEpochDirection d)
|
||||
{
|
||||
w.Write(d.Ctr);
|
||||
w.WriteBlob(d.Next);
|
||||
w.WriteBlob(d.Prev.Data);
|
||||
}
|
||||
|
||||
internal static Chain Read(BinaryReader r)
|
||||
{
|
||||
var dir = (Direction)r.ReadInt32();
|
||||
ulong cur = r.ReadUInt64();
|
||||
ulong send = r.ReadUInt64();
|
||||
byte[] nextRoot = r.ReadBlob();
|
||||
var p = new ChainParams { MaxJump = r.ReadUInt32(), MaxOooKeys = r.ReadUInt32() };
|
||||
var chain = new Chain(dir, p);
|
||||
chain._currentEpoch = cur;
|
||||
chain._sendEpoch = send;
|
||||
chain._nextRoot = nextRoot;
|
||||
int n = r.ReadInt32();
|
||||
for (int i = 0; i < n; i++)
|
||||
chain._links.AddLast(new ChainEpoch { Send = ReadDir(r), Recv = ReadDir(r) });
|
||||
return chain;
|
||||
}
|
||||
|
||||
private static ChainEpochDirection ReadDir(BinaryReader r)
|
||||
{
|
||||
uint ctr = r.ReadUInt32();
|
||||
byte[] next = r.ReadBlob();
|
||||
byte[] prev = r.ReadBlob();
|
||||
var d = new ChainEpochDirection(next) { Ctr = ctr };
|
||||
d.Prev.Data = prev;
|
||||
return d;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Errors from the SPQR layer.</summary>
|
||||
public sealed class SpqrException : Exception
|
||||
{
|
||||
public SpqrException(string message) : base(message) { }
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
namespace Wingnal.Protocol.Spqr;
|
||||
|
||||
/// <summary>
|
||||
/// Arithmetic in GF(2^16) with reduction polynomial 0x1100b, ported from Signal's
|
||||
/// SparsePostQuantumRatchet (v1.5.1) <c>src/encoding/gf.rs</c>. This is the base field for the
|
||||
/// Reed-Solomon-style erasure coding that "sparsely" spreads ML-KEM-768 keys/ciphertexts across
|
||||
/// messages. Addition/subtraction are XOR; multiplication is carryless-multiply then reduce;
|
||||
/// inversion is a^(2^16-2) via square-and-multiply.
|
||||
/// </summary>
|
||||
public readonly struct Gf16 : IEquatable<Gf16>
|
||||
{
|
||||
public const uint Poly = 0x1100b;
|
||||
|
||||
public static readonly Gf16 Zero = new(0);
|
||||
public static readonly Gf16 One = new(1);
|
||||
|
||||
public ushort Value { get; }
|
||||
|
||||
public Gf16(ushort value) => Value = value;
|
||||
|
||||
public static Gf16 Add(Gf16 a, Gf16 b) => new((ushort)(a.Value ^ b.Value));
|
||||
public static Gf16 Sub(Gf16 a, Gf16 b) => new((ushort)(a.Value ^ b.Value));
|
||||
|
||||
public static Gf16 Mul(Gf16 a, Gf16 b) => new(PolyReduce(PolyMul(a.Value, b.Value)));
|
||||
|
||||
/// <summary>a / b = a * b^(2^16-2). Dividing by zero yields zero (matches the reference loop).</summary>
|
||||
public static Gf16 Div(Gf16 a, Gf16 b)
|
||||
{
|
||||
// out = self * other^(2+4+...+2^15) = self * other^(2^16-2) = self * inv(other).
|
||||
Gf16 square = b;
|
||||
Gf16 outp = a;
|
||||
for (int i = 1; i < 16; i++)
|
||||
{
|
||||
square = Mul(square, square);
|
||||
outp = Mul(outp, square);
|
||||
}
|
||||
return outp;
|
||||
}
|
||||
|
||||
public static Gf16 Inv(Gf16 a) => Div(One, a);
|
||||
|
||||
/// <summary>Carryless (polynomial) multiply of two 16-bit values into a 32-bit result.</summary>
|
||||
private static uint PolyMul(ushort a, ushort b)
|
||||
{
|
||||
uint acc = 0;
|
||||
uint me = a;
|
||||
for (int shift = 0; shift < 16; shift++)
|
||||
if ((b & (1 << shift)) != 0)
|
||||
acc ^= me << shift;
|
||||
return acc;
|
||||
}
|
||||
|
||||
/// <summary>Reduce a 32-bit carryless product modulo POLY (a 17-bit polynomial) to 16 bits.</summary>
|
||||
private static ushort PolyReduce(uint v)
|
||||
{
|
||||
for (int bit = 31; bit >= 16; bit--)
|
||||
if ((v & (1u << bit)) != 0)
|
||||
v ^= Poly << (bit - 16);
|
||||
return (ushort)v;
|
||||
}
|
||||
|
||||
public bool Equals(Gf16 other) => Value == other.Value;
|
||||
public override bool Equals(object? obj) => obj is Gf16 g && Equals(g);
|
||||
public override int GetHashCode() => Value;
|
||||
public override string ToString() => $"GF16({Value})";
|
||||
}
|
||||
@@ -0,0 +1,735 @@
|
||||
using Org.BouncyCastle.Crypto.Digests;
|
||||
|
||||
namespace Wingnal.Protocol.Spqr;
|
||||
|
||||
/// <summary>
|
||||
/// Pure-C# FIPS-203 ML-KEM-768 with the "incremental" ek/ciphertext split used by Signal's Sparse
|
||||
/// Post-Quantum Ratchet (libcrux <c>mlkem768::incremental</c>). The ring arithmetic (q=3329, NTT,
|
||||
/// zetas, Montgomery/Barrett, basemul, CBD-eta2, matrix gen) is identical to round-3 Kyber-1024
|
||||
/// (<see cref="Curve.Kyber1024"/>); the differences are k=3, d_u=10 / d_v=4 compression, the FIPS-203
|
||||
/// hashing (keygen G(d‖k), encaps (K,r)=G(m‖H(ek)), implicit reject J(z‖c)), and NO final KDF.
|
||||
///
|
||||
/// Incremental layout (byte-exact with libcrux, confirmed against cryspen/libcrux):
|
||||
/// keygen → hdr(64) = rho(32)‖H(ek)(32); ek/pk2(1152) = ByteEncode12(t̂); dk(2400) =
|
||||
/// dkPke(1152)‖ek(1184)‖H(ek)(32)‖z(32), where ek(1184) = ByteEncode12(t̂)‖rho.
|
||||
/// encaps1(hdr,m) → ct1(960) = Compress_{10}(u); es (local); ss(32) = K.
|
||||
/// encaps2(ek,es) → ct2(128) = Compress_{4}(v).
|
||||
/// decaps(dk,ct1,ct2) → ss(32): standard FIPS-203 decaps on ct = ct1‖ct2 (1088).
|
||||
/// The encapsulation state <c>es</c> is never transmitted, so its format is our own.
|
||||
/// </summary>
|
||||
internal static class MlKem768
|
||||
{
|
||||
public const int N = 256;
|
||||
public const int Q = 3329;
|
||||
public const int K = 3;
|
||||
public const int Eta = 2; // eta1 == eta2 == 2
|
||||
public const int SymBytes = 32;
|
||||
public const byte RankByte = 3; // FIPS-203 domain-separation byte k for ML-KEM-768
|
||||
|
||||
public const int PolyBytes = 384;
|
||||
public const int PolyVecBytes = K * PolyBytes; // 1152 = ByteEncode12(vec)
|
||||
private const int Du = 10;
|
||||
private const int Dv = 4;
|
||||
public const int PolyVecCompressedBytes = K * (N * Du / 8); // 960
|
||||
public const int PolyCompressedBytes = N * Dv / 8; // 128
|
||||
|
||||
public const int EkBytes = PolyVecBytes + SymBytes; // 1184 (full FIPS-203 encapsulation key)
|
||||
public const int Pk2Bytes = PolyVecBytes; // 1152 (incremental "encapsulation key")
|
||||
public const int HeaderBytes = 2 * SymBytes; // 64
|
||||
public const int CiphertextBytes = PolyVecCompressedBytes + PolyCompressedBytes; // 1088
|
||||
public const int Ct1Bytes = PolyVecCompressedBytes; // 960
|
||||
public const int Ct2Bytes = PolyCompressedBytes; // 128
|
||||
public const int DkBytes = PolyVecBytes + EkBytes + SymBytes + SymBytes; // 2400
|
||||
public const int SsBytes = 32;
|
||||
|
||||
private const short QINV = -3327; // q^-1 mod 2^16
|
||||
|
||||
private static readonly short[] Zetas =
|
||||
{
|
||||
-1044, -758, -359, -1517, 1493, 1422, 287, 202,
|
||||
-171, 622, 1577, 182, 962, -1202, -1474, 1468,
|
||||
573, -1325, 264, 383, -829, 1458, -1602, -130,
|
||||
-681, 1017, 732, 608, -1542, 411, -205, -1571,
|
||||
1223, 652, -552, 1015, -1293, 1491, -282, -1544,
|
||||
516, -8, -320, -666, -1618, -1162, 126, 1469,
|
||||
-853, -90, -271, 830, 107, -1421, -247, -951,
|
||||
-398, 961, -1508, -725, 448, -1065, 677, -1275,
|
||||
-1103, 430, 555, 843, -1251, 871, 1550, 105,
|
||||
422, 587, 177, -235, -291, -460, 1574, 1653,
|
||||
-246, 778, 1159, -147, -777, 1483, -602, 1119,
|
||||
-1590, 644, -872, 349, 418, 329, -156, -75,
|
||||
817, 1097, 603, 610, 1322, -1285, -1465, 384,
|
||||
-1215, -136, 1218, -1335, -874, 220, -1187, -1659,
|
||||
-1185, -1530, -1278, 794, -1510, -854, -870, 478,
|
||||
-108, -308, 996, 991, 958, -1460, 1522, 1628,
|
||||
};
|
||||
|
||||
// ---- reductions ----
|
||||
|
||||
private static short MontgomeryReduce(int a)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
short t = (short)((short)a * QINV);
|
||||
return (short)((a - (int)t * Q) >> 16);
|
||||
}
|
||||
}
|
||||
|
||||
private static short BarrettReduce(short a)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
const int v = ((1 << 26) + Q / 2) / Q;
|
||||
short t = (short)((v * a + (1 << 25)) >> 26);
|
||||
return (short)(a - (short)(t * Q));
|
||||
}
|
||||
}
|
||||
|
||||
private static short FqMul(short a, short b) => MontgomeryReduce(a * b);
|
||||
|
||||
// ---- NTT ----
|
||||
|
||||
private static void Ntt(short[] r)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int k = 1;
|
||||
for (int len = 128; len >= 2; len >>= 1)
|
||||
for (int start = 0; start < 256; start += 2 * len)
|
||||
{
|
||||
short zeta = Zetas[k++];
|
||||
for (int j = start; j < start + len; j++)
|
||||
{
|
||||
short t = FqMul(zeta, r[j + len]);
|
||||
r[j + len] = (short)(r[j] - t);
|
||||
r[j] = (short)(r[j] + t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void InvNtt(short[] r)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
const short f = 1441; // mont^2/128
|
||||
int k = 127;
|
||||
for (int len = 2; len <= 128; len <<= 1)
|
||||
for (int start = 0; start < 256; start += 2 * len)
|
||||
{
|
||||
short zeta = Zetas[k--];
|
||||
for (int j = start; j < start + len; j++)
|
||||
{
|
||||
short t = r[j];
|
||||
r[j] = BarrettReduce((short)(t + r[j + len]));
|
||||
r[j + len] = (short)(r[j + len] - t);
|
||||
r[j + len] = FqMul(zeta, r[j + len]);
|
||||
}
|
||||
}
|
||||
for (int j = 0; j < 256; j++) r[j] = FqMul(r[j], f);
|
||||
}
|
||||
}
|
||||
|
||||
private static void BaseMul(short[] r, int rOff, short[] a, int aOff, short[] b, int bOff, short zeta)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
r[rOff] = FqMul(a[aOff + 1], b[bOff + 1]);
|
||||
r[rOff] = FqMul(r[rOff], zeta);
|
||||
r[rOff] = (short)(r[rOff] + FqMul(a[aOff], b[bOff]));
|
||||
r[rOff + 1] = FqMul(a[aOff], b[bOff + 1]);
|
||||
r[rOff + 1] = (short)(r[rOff + 1] + FqMul(a[aOff + 1], b[bOff]));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- hashing ----
|
||||
|
||||
private static byte[] Sha3_256(byte[] data, int off, int len)
|
||||
{
|
||||
var d = new Sha3Digest(256);
|
||||
d.BlockUpdate(data, off, len);
|
||||
var o = new byte[32];
|
||||
d.DoFinal(o, 0);
|
||||
return o;
|
||||
}
|
||||
|
||||
private static byte[] Sha3_512(byte[] data)
|
||||
{
|
||||
var d = new Sha3Digest(512);
|
||||
d.BlockUpdate(data, 0, data.Length);
|
||||
var o = new byte[64];
|
||||
d.DoFinal(o, 0);
|
||||
return o;
|
||||
}
|
||||
|
||||
private static byte[] Shake256(byte[] data, int len)
|
||||
{
|
||||
var d = new ShakeDigest(256);
|
||||
d.BlockUpdate(data, 0, data.Length);
|
||||
var o = new byte[len];
|
||||
d.Output(o, 0, len);
|
||||
return o;
|
||||
}
|
||||
|
||||
// ---- CBD (eta = 2) ----
|
||||
|
||||
private static uint Load32Le(byte[] x, int off) =>
|
||||
(uint)(x[off] | (x[off + 1] << 8) | (x[off + 2] << 16) | (x[off + 3] << 24));
|
||||
|
||||
private static void Cbd2(short[] r, byte[] buf)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
for (int i = 0; i < N / 8; i++)
|
||||
{
|
||||
uint t = Load32Le(buf, 4 * i);
|
||||
uint d = t & 0x55555555u;
|
||||
d += (t >> 1) & 0x55555555u;
|
||||
for (int j = 0; j < 8; j++)
|
||||
{
|
||||
short a = (short)((d >> (4 * j + 0)) & 0x3);
|
||||
short b = (short)((d >> (4 * j + 2)) & 0x3);
|
||||
r[8 * i + j] = (short)(a - b);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static short[] GetNoise(byte[] seed, byte nonce)
|
||||
{
|
||||
var extkey = new byte[SymBytes + 1];
|
||||
Array.Copy(seed, extkey, SymBytes);
|
||||
extkey[SymBytes] = nonce;
|
||||
byte[] buf = Shake256(extkey, Eta * N / 4);
|
||||
var r = new short[N];
|
||||
Cbd2(r, buf);
|
||||
return r;
|
||||
}
|
||||
|
||||
// ---- poly (de)serialization: ByteEncode12 / ByteDecode12 ----
|
||||
|
||||
private static void PolyToBytes(byte[] r, int rOff, short[] a)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
for (int i = 0; i < N / 2; i++)
|
||||
{
|
||||
ushort t0 = (ushort)(a[2 * i] + ((a[2 * i] >> 15) & Q));
|
||||
ushort t1 = (ushort)(a[2 * i + 1] + ((a[2 * i + 1] >> 15) & Q));
|
||||
r[rOff + 3 * i + 0] = (byte)t0;
|
||||
r[rOff + 3 * i + 1] = (byte)((t0 >> 8) | (t1 << 4));
|
||||
r[rOff + 3 * i + 2] = (byte)(t1 >> 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void PolyFromBytes(short[] r, byte[] a, int aOff)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
for (int i = 0; i < N / 2; i++)
|
||||
{
|
||||
r[2 * i] = (short)(((a[aOff + 3 * i + 0] >> 0) | (a[aOff + 3 * i + 1] << 8)) & 0xFFF);
|
||||
r[2 * i + 1] = (short)(((a[aOff + 3 * i + 1] >> 4) | (a[aOff + 3 * i + 2] << 4)) & 0xFFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- generic d-bit compression (LSB-first bit packing, FIPS-203 ByteEncode/Compress) ----
|
||||
|
||||
private static void CompressPoly(byte[] outBuf, int outOff, short[] a, int d)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
uint mask = (1u << d) - 1;
|
||||
ulong acc = 0;
|
||||
int bits = 0, pos = outOff;
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
int u = a[i];
|
||||
u += (u >> 15) & Q;
|
||||
uint t = (uint)(((((ulong)u << d) + Q / 2) / Q) & mask);
|
||||
acc |= (ulong)t << bits;
|
||||
bits += d;
|
||||
while (bits >= 8) { outBuf[pos++] = (byte)acc; acc >>= 8; bits -= 8; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static short[] DecompressPoly(byte[] a, int aOff, int d)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var r = new short[N];
|
||||
uint mask = (1u << d) - 1;
|
||||
ulong acc = 0;
|
||||
int bits = 0, pos = aOff;
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
while (bits < d) { acc |= (ulong)a[pos++] << bits; bits += 8; }
|
||||
uint t = (uint)(acc & mask);
|
||||
acc >>= d;
|
||||
bits -= d;
|
||||
r[i] = (short)(((uint)t * Q + (1u << (d - 1))) >> d);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
private static void PolyFromMsg(short[] r, byte[] msg)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
for (int i = 0; i < N / 8; i++)
|
||||
for (int j = 0; j < 8; j++)
|
||||
{
|
||||
short mask = (short)(-(short)((msg[i] >> j) & 1));
|
||||
r[8 * i + j] = (short)(mask & ((Q + 1) / 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] PolyToMsg(short[] a)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var msg = new byte[SymBytes];
|
||||
for (int i = 0; i < N / 8; i++)
|
||||
{
|
||||
msg[i] = 0;
|
||||
for (int j = 0; j < 8; j++)
|
||||
{
|
||||
int t = a[8 * i + j];
|
||||
t += (t >> 15) & Q;
|
||||
t = (((t << 1) + Q / 2) / Q) & 1;
|
||||
msg[i] |= (byte)(t << j);
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
||||
private static void PolyNtt(short[] r) { Ntt(r); PolyReduce(r); }
|
||||
|
||||
private static void PolyBaseMulMont(short[] r, short[] a, short[] b)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
for (int i = 0; i < N / 4; i++)
|
||||
{
|
||||
BaseMul(r, 4 * i, a, 4 * i, b, 4 * i, Zetas[64 + i]);
|
||||
BaseMul(r, 4 * i + 2, a, 4 * i + 2, b, 4 * i + 2, (short)(-Zetas[64 + i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void PolyToMont(short[] r)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
const short f = (short)((1L << 32) % Q);
|
||||
for (int i = 0; i < N; i++) r[i] = MontgomeryReduce(r[i] * f);
|
||||
}
|
||||
}
|
||||
|
||||
private static void PolyReduce(short[] r) { for (int i = 0; i < N; i++) r[i] = BarrettReduce(r[i]); }
|
||||
private static void PolyAdd(short[] r, short[] a, short[] b) { unchecked { for (int i = 0; i < N; i++) r[i] = (short)(a[i] + b[i]); } }
|
||||
private static void PolySub(short[] r, short[] a, short[] b) { unchecked { for (int i = 0; i < N; i++) r[i] = (short)(a[i] - b[i]); } }
|
||||
|
||||
// ---- polyvec ----
|
||||
|
||||
private static short[][] NewPolyVec()
|
||||
{
|
||||
var v = new short[K][];
|
||||
for (int i = 0; i < K; i++) v[i] = new short[N];
|
||||
return v;
|
||||
}
|
||||
|
||||
private static void PolyVecToBytes(byte[] r, int rOff, short[][] a)
|
||||
{
|
||||
for (int i = 0; i < K; i++) PolyToBytes(r, rOff + i * PolyBytes, a[i]);
|
||||
}
|
||||
|
||||
private static short[][] PolyVecFromBytes(byte[] a, int aOff)
|
||||
{
|
||||
var r = NewPolyVec();
|
||||
for (int i = 0; i < K; i++) PolyFromBytes(r[i], a, aOff + i * PolyBytes);
|
||||
return r;
|
||||
}
|
||||
|
||||
private static void PolyVecCompress(byte[] r, int rOff, short[][] a)
|
||||
{
|
||||
for (int i = 0; i < K; i++) CompressPoly(r, rOff + i * (N * Du / 8), a[i], Du);
|
||||
}
|
||||
|
||||
private static short[][] PolyVecDecompress(byte[] a, int aOff)
|
||||
{
|
||||
var r = NewPolyVec();
|
||||
for (int i = 0; i < K; i++) r[i] = DecompressPoly(a, aOff + i * (N * Du / 8), Du);
|
||||
return r;
|
||||
}
|
||||
|
||||
private static void PolyVecNtt(short[][] r) { for (int i = 0; i < K; i++) PolyNtt(r[i]); }
|
||||
|
||||
private static void PolyVecBaseMulAccMont(short[] r, short[][] a, short[][] b)
|
||||
{
|
||||
var t = new short[N];
|
||||
PolyBaseMulMont(r, a[0], b[0]);
|
||||
for (int i = 1; i < K; i++) { PolyBaseMulMont(t, a[i], b[i]); PolyAdd(r, r, t); }
|
||||
PolyReduce(r);
|
||||
}
|
||||
|
||||
private static void PolyVecReduce(short[][] r) { for (int i = 0; i < K; i++) PolyReduce(r[i]); }
|
||||
private static void PolyVecAdd(short[][] r, short[][] a, short[][] b) { for (int i = 0; i < K; i++) PolyAdd(r[i], a[i], b[i]); }
|
||||
|
||||
// ---- matrix generation (identical to round-3 Kyber: SHAKE128 rejection sampling) ----
|
||||
|
||||
private const int XofBlockBytes = 168;
|
||||
private const int GenMatrixNBlocks = (12 * N / 8 * (1 << 12) / Q + XofBlockBytes) / XofBlockBytes;
|
||||
|
||||
private static int RejUniform(short[] r, int rOff, int len, byte[] buf, int buflen)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int ctr = 0, pos = 0;
|
||||
while (ctr < len && pos + 3 <= buflen)
|
||||
{
|
||||
ushort val0 = (ushort)(((buf[pos + 0] >> 0) | (buf[pos + 1] << 8)) & 0xFFF);
|
||||
ushort val1 = (ushort)(((buf[pos + 1] >> 4) | (buf[pos + 2] << 4)) & 0xFFF);
|
||||
pos += 3;
|
||||
if (val0 < Q) r[rOff + ctr++] = (short)val0;
|
||||
if (ctr < len && val1 < Q) r[rOff + ctr++] = (short)val1;
|
||||
}
|
||||
return ctr;
|
||||
}
|
||||
}
|
||||
|
||||
private static short[][][] GenMatrix(byte[] seed, bool transposed)
|
||||
{
|
||||
var a = new short[K][][];
|
||||
for (int i = 0; i < K; i++)
|
||||
{
|
||||
a[i] = NewPolyVec();
|
||||
for (int j = 0; j < K; j++)
|
||||
{
|
||||
var extseed = new byte[SymBytes + 2];
|
||||
Array.Copy(seed, extseed, SymBytes);
|
||||
extseed[SymBytes] = (byte)(transposed ? i : j);
|
||||
extseed[SymBytes + 1] = (byte)(transposed ? j : i);
|
||||
|
||||
var xof = new ShakeDigest(128);
|
||||
xof.BlockUpdate(extseed, 0, extseed.Length);
|
||||
|
||||
var buf = new byte[GenMatrixNBlocks * XofBlockBytes + 2];
|
||||
xof.Output(buf, 0, GenMatrixNBlocks * XofBlockBytes);
|
||||
int buflen = GenMatrixNBlocks * XofBlockBytes;
|
||||
int ctr = RejUniform(a[i][j], 0, N, buf, buflen);
|
||||
|
||||
while (ctr < N)
|
||||
{
|
||||
int off = buflen % 3;
|
||||
for (int k = 0; k < off; k++) buf[k] = buf[buflen - off + k];
|
||||
xof.Output(buf, off, XofBlockBytes);
|
||||
buflen = off + XofBlockBytes;
|
||||
ctr += RejUniform(a[i][j], ctr, N - ctr, buf, buflen);
|
||||
}
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
// ---- K-PKE keygen (FIPS-203) ----
|
||||
|
||||
private static void KPkeKeygen(byte[] d, out short[][] skpv, out short[][] pkpv, out byte[] rho)
|
||||
{
|
||||
var dk = new byte[SymBytes + 1];
|
||||
Array.Copy(d, dk, SymBytes);
|
||||
dk[SymBytes] = RankByte; // FIPS-203: (rho,sigma) = G(d || k)
|
||||
byte[] buf = Sha3_512(dk);
|
||||
rho = buf[..SymBytes];
|
||||
var sigma = buf[SymBytes..];
|
||||
|
||||
// Keygen samples A[i][j] from XOF(rho, j, i) (pq-crystals gen_a, transposed:false); encrypt
|
||||
// uses the transpose Aᵀ (transposed:true). Identical convention to round-3 Kyber.
|
||||
short[][][] a = GenMatrix(rho, transposed: false);
|
||||
|
||||
skpv = NewPolyVec();
|
||||
var e = NewPolyVec();
|
||||
byte nonce = 0;
|
||||
for (int i = 0; i < K; i++) skpv[i] = GetNoise(sigma, nonce++);
|
||||
for (int i = 0; i < K; i++) e[i] = GetNoise(sigma, nonce++);
|
||||
|
||||
PolyVecNtt(skpv);
|
||||
PolyVecNtt(e);
|
||||
|
||||
pkpv = NewPolyVec();
|
||||
for (int i = 0; i < K; i++)
|
||||
{
|
||||
PolyVecBaseMulAccMont(pkpv[i], a[i], skpv);
|
||||
PolyToMont(pkpv[i]);
|
||||
}
|
||||
PolyVecAdd(pkpv, pkpv, e);
|
||||
PolyVecReduce(pkpv);
|
||||
}
|
||||
|
||||
// ---- standard FIPS-203 API (for KAT) ----
|
||||
|
||||
/// <summary>FIPS-203 ML-KEM.KeyGen_internal(d, z) → (ek 1184, dk 2400).</summary>
|
||||
public static void KeyGen(byte[] d, byte[] z, out byte[] ek, out byte[] dk)
|
||||
{
|
||||
KPkeKeygen(d, out short[][] skpv, out short[][] pkpv, out byte[] rho);
|
||||
|
||||
ek = new byte[EkBytes];
|
||||
PolyVecToBytes(ek, 0, pkpv);
|
||||
Array.Copy(rho, 0, ek, PolyVecBytes, SymBytes);
|
||||
|
||||
dk = new byte[DkBytes];
|
||||
PolyVecToBytes(dk, 0, skpv); // dkPke = ByteEncode12(s)
|
||||
Array.Copy(ek, 0, dk, PolyVecBytes, EkBytes); // ek
|
||||
byte[] hek = Sha3_256(ek, 0, EkBytes);
|
||||
Array.Copy(hek, 0, dk, PolyVecBytes + EkBytes, SymBytes); // H(ek)
|
||||
Array.Copy(z, 0, dk, PolyVecBytes + EkBytes + SymBytes, SymBytes); // z
|
||||
}
|
||||
|
||||
/// <summary>K-PKE encryption split into the c1 (u, compressed du) and c2 (v, compressed dv) halves.
|
||||
/// Pass <paramref name="tHat"/> = null to skip c2 (incremental encaps1).</summary>
|
||||
private static void KPkeEncrypt(byte[] rho, short[][]? tHat, byte[] msg, byte[] coins,
|
||||
short[][] rOut, out short[] e2Out, byte[]? ct1, byte[]? ct2)
|
||||
{
|
||||
short[][][] at = GenMatrix(rho, transposed: true); // Aᵀ for encrypt (see KPkeKeygen note)
|
||||
|
||||
var sp = NewPolyVec();
|
||||
var ep = NewPolyVec();
|
||||
byte nonce = 0;
|
||||
for (int i = 0; i < K; i++) sp[i] = GetNoise(coins, nonce++);
|
||||
for (int i = 0; i < K; i++) ep[i] = GetNoise(coins, nonce++);
|
||||
short[] epp = GetNoise(coins, nonce);
|
||||
|
||||
PolyVecNtt(sp);
|
||||
for (int i = 0; i < K; i++) rOut[i] = (short[])sp[i].Clone(); // r̂ saved for encaps2
|
||||
e2Out = epp;
|
||||
|
||||
if (ct1 is not null)
|
||||
{
|
||||
var b = NewPolyVec();
|
||||
for (int i = 0; i < K; i++) PolyVecBaseMulAccMont(b[i], at[i], sp);
|
||||
for (int i = 0; i < K; i++) InvNtt(b[i]);
|
||||
PolyVecAdd(b, b, ep);
|
||||
PolyVecReduce(b);
|
||||
PolyVecCompress(ct1, 0, b);
|
||||
}
|
||||
|
||||
if (ct2 is not null && tHat is not null)
|
||||
{
|
||||
var v = new short[N];
|
||||
PolyVecBaseMulAccMont(v, tHat, sp);
|
||||
InvNtt(v);
|
||||
var k = new short[N];
|
||||
PolyFromMsg(k, msg);
|
||||
PolyAdd(v, v, epp);
|
||||
PolyAdd(v, v, k);
|
||||
PolyReduce(v);
|
||||
CompressPoly(ct2, 0, v, Dv);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>FIPS-203 ML-KEM.Encaps_internal(ek, m) → (c 1088, ss 32). For KAT / standard use.</summary>
|
||||
public static void Encaps(byte[] ek, byte[] m, out byte[] ct, out byte[] ss)
|
||||
{
|
||||
byte[] hek = Sha3_256(ek, 0, EkBytes);
|
||||
byte[] g = Sha3_512(Concat(m, hek));
|
||||
ss = g[..SymBytes];
|
||||
byte[] coins = g[SymBytes..];
|
||||
|
||||
byte[] rho = ek[PolyVecBytes..EkBytes];
|
||||
short[][] tHat = PolyVecFromBytes(ek, 0);
|
||||
|
||||
var rHat = new short[K][];
|
||||
ct = new byte[CiphertextBytes];
|
||||
var ct1 = new byte[Ct1Bytes];
|
||||
var ct2 = new byte[Ct2Bytes];
|
||||
KPkeEncrypt(rho, tHat, m, coins, rHat, out _, ct1, ct2);
|
||||
Array.Copy(ct1, 0, ct, 0, Ct1Bytes);
|
||||
Array.Copy(ct2, 0, ct, Ct1Bytes, Ct2Bytes);
|
||||
}
|
||||
|
||||
/// <summary>FIPS-203 ML-KEM.Decaps(dk, c) → ss 32 (c = ct1‖ct2).</summary>
|
||||
public static byte[] Decaps(byte[] dk, byte[] ct1, byte[] ct2)
|
||||
{
|
||||
var dkPke = dk[..PolyVecBytes];
|
||||
var ek = new byte[EkBytes];
|
||||
Array.Copy(dk, PolyVecBytes, ek, 0, EkBytes);
|
||||
var hek = new byte[SymBytes];
|
||||
Array.Copy(dk, PolyVecBytes + EkBytes, hek, 0, SymBytes);
|
||||
var z = new byte[SymBytes];
|
||||
Array.Copy(dk, PolyVecBytes + EkBytes + SymBytes, z, 0, SymBytes);
|
||||
|
||||
// K-PKE.Decrypt
|
||||
short[][] u = PolyVecDecompress(ct1, 0);
|
||||
short[] v = DecompressPoly(ct2, 0, Dv);
|
||||
short[][] skpv = PolyVecFromBytes(dkPke, 0);
|
||||
PolyVecNtt(u);
|
||||
var mp = new short[N];
|
||||
PolyVecBaseMulAccMont(mp, skpv, u);
|
||||
InvNtt(mp);
|
||||
PolySub(mp, v, mp);
|
||||
PolyReduce(mp);
|
||||
byte[] mPrime = PolyToMsg(mp);
|
||||
|
||||
// (K', r') = G(m' || H(ek))
|
||||
byte[] g = Sha3_512(Concat(mPrime, hek));
|
||||
byte[] kPrime = g[..SymBytes];
|
||||
byte[] coins = g[SymBytes..];
|
||||
|
||||
// re-encrypt and compare
|
||||
byte[] rho = ek[PolyVecBytes..EkBytes];
|
||||
short[][] tHat = PolyVecFromBytes(ek, 0);
|
||||
var rHat = new short[K][];
|
||||
var ct1Cmp = new byte[Ct1Bytes];
|
||||
var ct2Cmp = new byte[Ct2Bytes];
|
||||
KPkeEncrypt(rho, tHat, mPrime, coins, rHat, out _, ct1Cmp, ct2Cmp);
|
||||
|
||||
int fail = Verify(ct1, ct1Cmp) | Verify(ct2, ct2Cmp);
|
||||
|
||||
// implicit reject: K_bar = J(z || c)
|
||||
byte[] kBar = Shake256(Concat(z, Concat(ct1, ct2)), SsBytes);
|
||||
|
||||
var outSs = new byte[SsBytes];
|
||||
CMov(outSs, kPrime, kBar, (byte)fail);
|
||||
return outSs;
|
||||
}
|
||||
|
||||
// ---- incremental API ----
|
||||
|
||||
public sealed class Keys
|
||||
{
|
||||
public required byte[] Header { get; init; } // 64
|
||||
public required byte[] Ek { get; init; } // 1152 (pk2)
|
||||
public required byte[] Dk { get; init; } // 2400
|
||||
}
|
||||
|
||||
/// <summary>Incremental keygen → header (rho‖H(ek)), ek/pk2 (ByteEncode12(t̂)), dk.</summary>
|
||||
public static Keys Generate(byte[] d, byte[] z)
|
||||
{
|
||||
KeyGen(d, z, out byte[] ekFull, out byte[] dk);
|
||||
var header = new byte[HeaderBytes];
|
||||
Array.Copy(ekFull, PolyVecBytes, header, 0, SymBytes); // rho
|
||||
byte[] hek = Sha3_256(ekFull, 0, EkBytes);
|
||||
Array.Copy(hek, 0, header, SymBytes, SymBytes); // H(ek)
|
||||
var pk2 = new byte[Pk2Bytes];
|
||||
Array.Copy(ekFull, 0, pk2, 0, Pk2Bytes); // ByteEncode12(t̂)
|
||||
return new Keys { Header = header, Ek = pk2, Dk = dk };
|
||||
}
|
||||
|
||||
/// <summary>Validates that an encapsulation key (pk2) matches a header: H(pk2‖rho) == hdr hash.</summary>
|
||||
public static bool EkMatchesHeader(byte[] pk2, byte[] header)
|
||||
{
|
||||
if (pk2.Length != Pk2Bytes || header.Length != HeaderBytes) return false;
|
||||
var ekFull = new byte[EkBytes];
|
||||
Array.Copy(pk2, 0, ekFull, 0, Pk2Bytes);
|
||||
Array.Copy(header, 0, ekFull, PolyVecBytes, SymBytes); // rho from header
|
||||
byte[] hek = Sha3_256(ekFull, 0, EkBytes);
|
||||
return CryptographicEquals(hek, header.AsSpan(SymBytes, SymBytes));
|
||||
}
|
||||
|
||||
/// <summary>encaps1(hdr, m) → (ct1 960, es, ss 32). The shared secret is available immediately.</summary>
|
||||
public static void Encaps1(byte[] header, byte[] m, out byte[] ct1, out byte[] es, out byte[] ss)
|
||||
{
|
||||
byte[] rho = header[..SymBytes];
|
||||
byte[] hek = header[SymBytes..HeaderBytes];
|
||||
byte[] g = Sha3_512(Concat(m, hek));
|
||||
ss = g[..SymBytes];
|
||||
byte[] coins = g[SymBytes..];
|
||||
|
||||
var rHat = new short[K][];
|
||||
ct1 = new byte[Ct1Bytes];
|
||||
KPkeEncrypt(rho, tHat: null, m, coins, rHat, out short[] e2, ct1, ct2: null);
|
||||
es = SerializeState(rHat, e2, m);
|
||||
}
|
||||
|
||||
/// <summary>encaps2(ek/pk2, es) → ct2 128.</summary>
|
||||
public static byte[] Encaps2(byte[] pk2, byte[] es)
|
||||
{
|
||||
DeserializeState(es, out short[][] rHat, out short[] e2, out byte[] m);
|
||||
short[][] tHat = PolyVecFromBytes(pk2, 0);
|
||||
|
||||
var v = new short[N];
|
||||
PolyVecBaseMulAccMont(v, tHat, rHat);
|
||||
InvNtt(v);
|
||||
var k = new short[N];
|
||||
PolyFromMsg(k, m);
|
||||
PolyAdd(v, v, e2);
|
||||
PolyAdd(v, v, k);
|
||||
PolyReduce(v);
|
||||
var ct2 = new byte[Ct2Bytes];
|
||||
CompressPoly(ct2, 0, v, Dv);
|
||||
return ct2;
|
||||
}
|
||||
|
||||
/// <summary>decaps(dk, ct1, ct2) → ss 32.</summary>
|
||||
public static byte[] DecapsIncremental(byte[] dk, byte[] ct1, byte[] ct2) => Decaps(dk, ct1, ct2);
|
||||
|
||||
// ---- local encapsulation-state serialization (never transmitted; our own format) ----
|
||||
// es = r̂ (K*N int16 LE) || e2 (N int16 LE) || m (32)
|
||||
|
||||
private static byte[] SerializeState(short[][] rHat, short[] e2, byte[] m)
|
||||
{
|
||||
var es = new byte[(K * N + N) * 2 + SymBytes];
|
||||
int p = 0;
|
||||
for (int i = 0; i < K; i++)
|
||||
for (int j = 0; j < N; j++) { es[p++] = (byte)rHat[i][j]; es[p++] = (byte)(rHat[i][j] >> 8); }
|
||||
for (int j = 0; j < N; j++) { es[p++] = (byte)e2[j]; es[p++] = (byte)(e2[j] >> 8); }
|
||||
Array.Copy(m, 0, es, p, SymBytes);
|
||||
return es;
|
||||
}
|
||||
|
||||
private static void DeserializeState(byte[] es, out short[][] rHat, out short[] e2, out byte[] m)
|
||||
{
|
||||
rHat = NewPolyVec();
|
||||
e2 = new short[N];
|
||||
int p = 0;
|
||||
for (int i = 0; i < K; i++)
|
||||
for (int j = 0; j < N; j++) { rHat[i][j] = (short)(es[p] | (es[p + 1] << 8)); p += 2; }
|
||||
for (int j = 0; j < N; j++) { e2[j] = (short)(es[p] | (es[p + 1] << 8)); p += 2; }
|
||||
m = new byte[SymBytes];
|
||||
Array.Copy(es, p, m, 0, SymBytes);
|
||||
}
|
||||
|
||||
// ---- helpers ----
|
||||
|
||||
private static byte[] Concat(byte[] a, byte[] b)
|
||||
{
|
||||
var r = new byte[a.Length + b.Length];
|
||||
Buffer.BlockCopy(a, 0, r, 0, a.Length);
|
||||
Buffer.BlockCopy(b, 0, r, a.Length, b.Length);
|
||||
return r;
|
||||
}
|
||||
|
||||
private static int Verify(byte[] a, byte[] b)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
byte r = 0;
|
||||
for (int i = 0; i < a.Length; i++) r |= (byte)(a[i] ^ b[i]);
|
||||
return (int)((ulong)(0 - (ulong)r) >> 63);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CMov(byte[] dst, byte[] good, byte[] bad, byte fail)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
byte mask = (byte)(-(sbyte)fail); // fail==1 -> 0xFF (use bad), fail==0 -> 0x00 (use good)
|
||||
for (int i = 0; i < dst.Length; i++)
|
||||
dst[i] = (byte)(good[i] ^ (mask & (good[i] ^ bad[i])));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool CryptographicEquals(byte[] a, ReadOnlySpan<byte> b)
|
||||
{
|
||||
if (a.Length != b.Length) return false;
|
||||
int r = 0;
|
||||
for (int i = 0; i < a.Length; i++) r |= a[i] ^ b[i];
|
||||
return r == 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
using System.IO;
|
||||
|
||||
namespace Wingnal.Protocol.Spqr;
|
||||
|
||||
/// <summary>
|
||||
/// Polynomial fountain/erasure code over GF(2^16), ported from Signal's SparsePostQuantumRatchet
|
||||
/// v1.5.1 <c>src/encoding/polynomial.rs</c>. A message is split into <see cref="NumPolys"/>=16
|
||||
/// polynomials (one per 2-byte symbol position, round-robin); chunk <c>idx</c> carries each
|
||||
/// polynomial evaluated at x=idx (16 symbols = 32 bytes). The first ⌈M/16⌉ chunks are the message
|
||||
/// itself (systematic); later chunks are redundancy. The decoder Lagrange-interpolates each
|
||||
/// polynomial once it has enough points. Used to spread large ML-KEM-768 keys/ciphertexts across
|
||||
/// many ratchet messages.
|
||||
/// </summary>
|
||||
public static class Polynomial
|
||||
{
|
||||
public const int ChunkSize = 32;
|
||||
public const int NumPolys = ChunkSize / 2; // 16
|
||||
|
||||
/// <summary>A polynomial over GF(2^16), coefficients low-order first (coeff[0] = constant term).</summary>
|
||||
public sealed class Poly
|
||||
{
|
||||
public Gf16[] Coefficients { get; }
|
||||
public Poly(Gf16[] coefficients) => Coefficients = coefficients;
|
||||
|
||||
/// <summary>Evaluate f(x) via Horner's method.</summary>
|
||||
public Gf16 Evaluate(Gf16 x)
|
||||
{
|
||||
Gf16 acc = Gf16.Zero;
|
||||
for (int i = Coefficients.Length - 1; i >= 0; i--)
|
||||
acc = Gf16.Add(Gf16.Mul(acc, x), Coefficients[i]);
|
||||
return acc;
|
||||
}
|
||||
|
||||
/// <summary>The unique polynomial through the given (x,y) points (distinct x). Standard Lagrange.</summary>
|
||||
public static Poly Interpolate((Gf16 X, Gf16 Y)[] points)
|
||||
{
|
||||
int n = points.Length;
|
||||
var coeffs = new Gf16[n];
|
||||
for (int i = 0; i < n; i++) coeffs[i] = Gf16.Zero;
|
||||
if (n == 0) return new Poly(coeffs);
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
// basis_i(x) = PRODUCT_{m != i} (x - x_m) / (x_i - x_m), scaled by y_i.
|
||||
var basis = new Gf16[n];
|
||||
basis[0] = Gf16.One;
|
||||
int deg = 0;
|
||||
Gf16 denom = Gf16.One;
|
||||
for (int m = 0; m < n; m++)
|
||||
{
|
||||
if (m == i) continue;
|
||||
// multiply basis by (x - x_m): basis = basis*x + basis*(-x_m). (-x_m == x_m in GF(2^k))
|
||||
var next = new Gf16[n];
|
||||
for (int k = deg; k >= 0; k--)
|
||||
{
|
||||
// contribute basis[k] * x -> next[k+1]
|
||||
next[k + 1] = Gf16.Add(next[k + 1], basis[k]);
|
||||
// contribute basis[k] * x_m -> next[k]
|
||||
next[k] = Gf16.Add(next[k], Gf16.Mul(basis[k], points[m].X));
|
||||
}
|
||||
basis = next;
|
||||
deg++;
|
||||
denom = Gf16.Mul(denom, Gf16.Sub(points[i].X, points[m].X));
|
||||
}
|
||||
Gf16 scale = Gf16.Mul(points[i].Y, Gf16.Inv(denom));
|
||||
for (int k = 0; k < n; k++)
|
||||
coeffs[k] = Gf16.Add(coeffs[k], Gf16.Mul(basis[k], scale));
|
||||
}
|
||||
return new Poly(coeffs);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Encodes a message (even length) into an unbounded stream of 32-byte indexed chunks.</summary>
|
||||
public sealed class Encoder
|
||||
{
|
||||
private readonly List<Gf16>[] _data = new List<Gf16>[NumPolys];
|
||||
private readonly Poly?[] _polys = new Poly?[NumPolys];
|
||||
private uint _nextIdx;
|
||||
|
||||
public Encoder(byte[] message)
|
||||
{
|
||||
if (message.Length % 2 != 0)
|
||||
throw new ArgumentException("message length must be even", nameof(message));
|
||||
for (int j = 0; j < NumPolys; j++) _data[j] = new List<Gf16>();
|
||||
for (int i = 0; i < message.Length / 2; i++)
|
||||
{
|
||||
ushort v = (ushort)((message[2 * i] << 8) | message[2 * i + 1]);
|
||||
_data[i % NumPolys].Add(new Gf16(v));
|
||||
}
|
||||
}
|
||||
|
||||
public (ushort Index, byte[] Data) NextChunk()
|
||||
{
|
||||
ushort idx = (ushort)_nextIdx;
|
||||
_nextIdx++;
|
||||
return ChunkAt(idx);
|
||||
}
|
||||
|
||||
public (ushort Index, byte[] Data) ChunkAt(ushort idx)
|
||||
{
|
||||
var data = new byte[ChunkSize];
|
||||
for (int j = 0; j < NumPolys; j++)
|
||||
{
|
||||
Gf16 v = PointAt(j, idx);
|
||||
data[2 * j] = (byte)(v.Value >> 8);
|
||||
data[2 * j + 1] = (byte)v.Value;
|
||||
}
|
||||
return (idx, data);
|
||||
}
|
||||
|
||||
private Gf16 PointAt(int poly, int idx)
|
||||
{
|
||||
List<Gf16> pts = _data[poly];
|
||||
if (idx < pts.Count)
|
||||
return pts[idx]; // systematic: original data value
|
||||
|
||||
// Redundancy point: interpolate (cached) and evaluate.
|
||||
Poly p = _polys[poly] ??= BuildPoly(pts);
|
||||
return p.Evaluate(new Gf16((ushort)idx));
|
||||
}
|
||||
|
||||
private static Poly BuildPoly(List<Gf16> values)
|
||||
{
|
||||
var points = new (Gf16, Gf16)[values.Count];
|
||||
for (int x = 0; x < values.Count; x++)
|
||||
points[x] = (new Gf16((ushort)x), values[x]);
|
||||
return Poly.Interpolate(points);
|
||||
}
|
||||
|
||||
private Encoder() { for (int j = 0; j < NumPolys; j++) _data[j] = new List<Gf16>(); }
|
||||
|
||||
internal void Write(BinaryWriter w)
|
||||
{
|
||||
w.Write(_nextIdx);
|
||||
for (int j = 0; j < NumPolys; j++)
|
||||
{
|
||||
w.Write(_data[j].Count);
|
||||
foreach (Gf16 g in _data[j]) w.Write(g.Value);
|
||||
}
|
||||
}
|
||||
|
||||
internal static Encoder Read(BinaryReader r)
|
||||
{
|
||||
var e = new Encoder();
|
||||
e._nextIdx = r.ReadUInt32();
|
||||
for (int j = 0; j < NumPolys; j++)
|
||||
{
|
||||
int c = r.ReadInt32();
|
||||
for (int k = 0; k < c; k++) e._data[j].Add(new Gf16(r.ReadUInt16()));
|
||||
}
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Collects chunks until it can reconstruct the original <paramref name="lenBytes"/>-byte message.</summary>
|
||||
public sealed class Decoder
|
||||
{
|
||||
private readonly int _lenBytes;
|
||||
private readonly int _symbolCount; // M = lenBytes/2 GF16 symbols
|
||||
private readonly int[] _pointsPerPoly = new int[NumPolys];
|
||||
private readonly Dictionary<ushort, Gf16[]> _chunks = new(); // idx -> 16 symbols
|
||||
|
||||
public Decoder(int lenBytes)
|
||||
{
|
||||
if (lenBytes % 2 != 0) throw new ArgumentException("length must be even", nameof(lenBytes));
|
||||
_lenBytes = lenBytes;
|
||||
_symbolCount = lenBytes / 2;
|
||||
for (int i = 0; i < _symbolCount; i++)
|
||||
_pointsPerPoly[i % NumPolys]++;
|
||||
}
|
||||
|
||||
public void AddChunk(ushort index, byte[] data)
|
||||
{
|
||||
if (data.Length != ChunkSize) throw new ArgumentException("chunk must be 32 bytes", nameof(data));
|
||||
var symbols = new Gf16[NumPolys];
|
||||
for (int j = 0; j < NumPolys; j++)
|
||||
symbols[j] = new Gf16((ushort)((data[2 * j] << 8) | data[2 * j + 1]));
|
||||
_chunks[index] = symbols;
|
||||
}
|
||||
|
||||
public bool CanReconstruct()
|
||||
{
|
||||
for (int j = 0; j < NumPolys; j++)
|
||||
if (_chunks.Count < _pointsPerPoly[j])
|
||||
return false;
|
||||
// Every poly needs pointsPerPoly[j] distinct x; we have _chunks.Count distinct indices,
|
||||
// and max points-per-poly <= _chunks.Count is the binding constraint.
|
||||
return _chunks.Count >= MaxPoints();
|
||||
}
|
||||
|
||||
public byte[]? DecodedMessage()
|
||||
{
|
||||
if (!CanReconstruct()) return null;
|
||||
|
||||
// Sort received indices for determinism; use the first N for each poly.
|
||||
ushort[] indices = _chunks.Keys.OrderBy(k => k).ToArray();
|
||||
var msg = new byte[_lenBytes];
|
||||
|
||||
for (int j = 0; j < NumPolys; j++)
|
||||
{
|
||||
int need = _pointsPerPoly[j];
|
||||
if (need == 0) continue;
|
||||
var points = new (Gf16, Gf16)[need];
|
||||
for (int k = 0; k < need; k++)
|
||||
{
|
||||
ushort idx = indices[k];
|
||||
points[k] = (new Gf16(idx), _chunks[idx][j]);
|
||||
}
|
||||
Poly p = Poly.Interpolate(points);
|
||||
for (int x = 0; x < need; x++)
|
||||
{
|
||||
int pos = x * NumPolys + j; // message symbol position
|
||||
if (pos >= _symbolCount) break;
|
||||
Gf16 v = x < need ? p.Evaluate(new Gf16((ushort)x)) : Gf16.Zero;
|
||||
msg[2 * pos] = (byte)(v.Value >> 8);
|
||||
msg[2 * pos + 1] = (byte)v.Value;
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
private int MaxPoints()
|
||||
{
|
||||
int max = 0;
|
||||
for (int j = 0; j < NumPolys; j++) max = Math.Max(max, _pointsPerPoly[j]);
|
||||
return max;
|
||||
}
|
||||
|
||||
internal void Write(BinaryWriter w)
|
||||
{
|
||||
w.Write(_lenBytes);
|
||||
w.Write(_chunks.Count);
|
||||
foreach (KeyValuePair<ushort, Gf16[]> kv in _chunks)
|
||||
{
|
||||
w.Write(kv.Key);
|
||||
foreach (Gf16 g in kv.Value) w.Write(g.Value);
|
||||
}
|
||||
}
|
||||
|
||||
internal static Decoder Read(BinaryReader r)
|
||||
{
|
||||
var d = new Decoder(r.ReadInt32());
|
||||
int n = r.ReadInt32();
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
ushort idx = r.ReadUInt16();
|
||||
var syms = new Gf16[NumPolys];
|
||||
for (int j = 0; j < NumPolys; j++) syms[j] = new Gf16(r.ReadUInt16());
|
||||
d._chunks[idx] = syms;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
using System.IO;
|
||||
using Enc = Wingnal.Protocol.Spqr.Polynomial.Encoder;
|
||||
using Dec = Wingnal.Protocol.Spqr.Polynomial.Decoder;
|
||||
|
||||
namespace Wingnal.Protocol.Spqr;
|
||||
|
||||
/// <summary>A 32-byte fountain-code chunk with its index.</summary>
|
||||
public sealed class SpqrChunk
|
||||
{
|
||||
public ushort Index { get; }
|
||||
public byte[] Data { get; } // 32
|
||||
public SpqrChunk(ushort index, byte[] data) { Index = index; Data = data; }
|
||||
}
|
||||
|
||||
public enum SpqrMsgKind { None, Hdr, Ek, EkCt1Ack, Ct1Ack, Ct1, Ct2 }
|
||||
|
||||
/// <summary>The SPQR per-message payload (one of the V1Msg inner_msg variants).</summary>
|
||||
public sealed class SpqrPayload
|
||||
{
|
||||
public SpqrMsgKind Kind { get; }
|
||||
public SpqrChunk? Chunk { get; }
|
||||
public bool Ack { get; }
|
||||
|
||||
private SpqrPayload(SpqrMsgKind kind, SpqrChunk? chunk, bool ack) { Kind = kind; Chunk = chunk; Ack = ack; }
|
||||
|
||||
public static readonly SpqrPayload None = new(SpqrMsgKind.None, null, false);
|
||||
public static SpqrPayload Hdr(SpqrChunk c) => new(SpqrMsgKind.Hdr, c, false);
|
||||
public static SpqrPayload Ek(SpqrChunk c) => new(SpqrMsgKind.Ek, c, false);
|
||||
public static SpqrPayload EkCt1Ack(SpqrChunk c) => new(SpqrMsgKind.EkCt1Ack, c, false);
|
||||
public static SpqrPayload Ct1Ack(bool ack) => new(SpqrMsgKind.Ct1Ack, null, ack);
|
||||
public static SpqrPayload Ct1(SpqrChunk c) => new(SpqrMsgKind.Ct1, c, false);
|
||||
public static SpqrPayload Ct2(SpqrChunk c) => new(SpqrMsgKind.Ct2, c, false);
|
||||
}
|
||||
|
||||
/// <summary>An SCKA message: an epoch plus a payload.</summary>
|
||||
public sealed class SpqrMessage
|
||||
{
|
||||
public ulong Epoch { get; }
|
||||
public SpqrPayload Payload { get; }
|
||||
public SpqrMessage(ulong epoch, SpqrPayload payload) { Epoch = epoch; Payload = payload; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The SPQR v1 chunked SCKA state machine (ported from SparsePostQuantumRatchet v1.5.1
|
||||
/// src/v1/chunked/{states,send_ek,send_ct}.rs). It wraps the unchunked crypto states with polynomial
|
||||
/// encoders/decoders so the large ML-KEM-768 header/ek/ct blobs are spread across many messages.
|
||||
/// <see cref="Send"/> emits the next chunk (and possibly a new EpochSecret); <see cref="Recv"/> ingests
|
||||
/// a peer chunk and advances state. Header/ct chunks carry an authenticator MAC appended to the blob.
|
||||
/// </summary>
|
||||
public sealed class SckaStates
|
||||
{
|
||||
private const int HeaderSize = MlKem768.HeaderBytes; // 64
|
||||
private const int MacSize = Authenticator.MacSize; // 32
|
||||
private const int Ct1Size = MlKem768.Ct1Bytes; // 960
|
||||
private const int Ct2Size = MlKem768.Ct2Bytes; // 128
|
||||
private const int EkSize = MlKem768.Pk2Bytes; // 1152
|
||||
|
||||
public sealed class SendResult
|
||||
{
|
||||
public required SpqrMessage Msg { get; init; }
|
||||
public EpochSecret? Key { get; init; }
|
||||
public required SckaStates State { get; init; }
|
||||
}
|
||||
|
||||
public sealed class RecvResult
|
||||
{
|
||||
public EpochSecret? Key { get; init; }
|
||||
public required SckaStates State { get; init; }
|
||||
}
|
||||
|
||||
// Exactly one of these is non-null (mirrors the Rust States enum).
|
||||
private readonly object _inner;
|
||||
|
||||
private SckaStates(object inner) => _inner = inner;
|
||||
|
||||
public static SckaStates InitA(byte[] authKey) =>
|
||||
new(new CKeysUnsampled(UcKeysUnsampled.New(authKey)));
|
||||
|
||||
public static SckaStates InitB(byte[] authKey) =>
|
||||
new(new CNoHeaderReceived(UcNoHeaderReceived.New(authKey),
|
||||
new Dec(HeaderSize + MacSize)));
|
||||
|
||||
private static ulong EpochOf(object s) => s switch
|
||||
{
|
||||
CKeysUnsampled x => x.Uc.Epoch,
|
||||
CKeysSampled x => x.Uc.Epoch,
|
||||
CHeaderSent x => x.Uc.Epoch,
|
||||
CCt1Received x => x.Uc.Epoch,
|
||||
CEkSentCt1Received x => x.Uc.Epoch,
|
||||
CNoHeaderReceived x => x.Uc.Epoch,
|
||||
CHeaderReceived x => x.Uc.Epoch,
|
||||
CCt1Sampled x => x.Uc.Epoch,
|
||||
CEkReceivedCt1Sampled x => x.Uc.Epoch,
|
||||
CCt1Acknowledged x => x.Uc.Epoch,
|
||||
CCt2Sampled x => x.Uc.Epoch,
|
||||
_ => throw new SpqrException("unknown state"),
|
||||
};
|
||||
|
||||
// ───────────────────────── send ─────────────────────────
|
||||
|
||||
public SendResult Send()
|
||||
{
|
||||
switch (_inner)
|
||||
{
|
||||
// send_ek
|
||||
case CKeysUnsampled s:
|
||||
{
|
||||
ulong epoch = s.Uc.Epoch;
|
||||
(UcHeaderSent uc, byte[] hdr, byte[] mac) = s.Uc.SendHeader();
|
||||
var enc = new Enc(Cat(hdr, mac));
|
||||
SpqrChunk chunk = Next(enc);
|
||||
return new SendResult
|
||||
{
|
||||
Msg = new SpqrMessage(epoch, SpqrPayload.Hdr(chunk)),
|
||||
State = new SckaStates(new CKeysSampled(uc, enc)),
|
||||
};
|
||||
}
|
||||
case CKeysSampled s:
|
||||
{
|
||||
SpqrChunk chunk = Next(s.SendingHdr);
|
||||
return new SendResult
|
||||
{
|
||||
Msg = new SpqrMessage(s.Uc.Epoch, SpqrPayload.Hdr(chunk)),
|
||||
State = this,
|
||||
};
|
||||
}
|
||||
case CHeaderSent s:
|
||||
{
|
||||
SpqrChunk chunk = Next(s.SendingEk);
|
||||
return new SendResult
|
||||
{
|
||||
Msg = new SpqrMessage(s.Uc.Epoch, SpqrPayload.Ek(chunk)),
|
||||
State = this,
|
||||
};
|
||||
}
|
||||
case CCt1Received s:
|
||||
{
|
||||
SpqrChunk chunk = Next(s.SendingEk);
|
||||
return new SendResult
|
||||
{
|
||||
Msg = new SpqrMessage(s.Uc.Epoch, SpqrPayload.EkCt1Ack(chunk)),
|
||||
State = this,
|
||||
};
|
||||
}
|
||||
case CEkSentCt1Received s:
|
||||
{
|
||||
return new SendResult
|
||||
{
|
||||
Msg = new SpqrMessage(s.Uc.Epoch, SpqrPayload.Ct1Ack(true)),
|
||||
State = this,
|
||||
};
|
||||
}
|
||||
// send_ct
|
||||
case CNoHeaderReceived s:
|
||||
{
|
||||
return new SendResult
|
||||
{
|
||||
Msg = new SpqrMessage(s.Uc.Epoch, SpqrPayload.None),
|
||||
State = this,
|
||||
};
|
||||
}
|
||||
case CHeaderReceived s:
|
||||
{
|
||||
ulong epoch = s.Uc.Epoch;
|
||||
(UcCt1Sent uc, byte[] ct1, EpochSecret secret) = s.Uc.SendCt1();
|
||||
var enc = new Enc(ct1);
|
||||
SpqrChunk chunk = Next(enc);
|
||||
return new SendResult
|
||||
{
|
||||
Msg = new SpqrMessage(epoch, SpqrPayload.Ct1(chunk)),
|
||||
Key = secret,
|
||||
State = new SckaStates(new CCt1Sampled(uc, enc, s.ReceivingEk)),
|
||||
};
|
||||
}
|
||||
case CCt1Sampled s:
|
||||
{
|
||||
SpqrChunk chunk = Next(s.SendingCt1);
|
||||
return new SendResult
|
||||
{
|
||||
Msg = new SpqrMessage(s.Uc.Epoch, SpqrPayload.Ct1(chunk)),
|
||||
State = this,
|
||||
};
|
||||
}
|
||||
case CEkReceivedCt1Sampled s:
|
||||
{
|
||||
SpqrChunk chunk = Next(s.SendingCt1);
|
||||
return new SendResult
|
||||
{
|
||||
Msg = new SpqrMessage(s.Uc.Epoch, SpqrPayload.Ct1(chunk)),
|
||||
State = this,
|
||||
};
|
||||
}
|
||||
case CCt1Acknowledged s:
|
||||
{
|
||||
return new SendResult
|
||||
{
|
||||
Msg = new SpqrMessage(s.Uc.Epoch, SpqrPayload.None),
|
||||
State = this,
|
||||
};
|
||||
}
|
||||
case CCt2Sampled s:
|
||||
{
|
||||
SpqrChunk chunk = Next(s.SendingCt2);
|
||||
return new SendResult
|
||||
{
|
||||
Msg = new SpqrMessage(s.Uc.Epoch, SpqrPayload.Ct2(chunk)),
|
||||
State = this,
|
||||
};
|
||||
}
|
||||
default: throw new SpqrException("unknown state");
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────────── recv ─────────────────────────
|
||||
|
||||
public RecvResult Recv(SpqrMessage msg)
|
||||
{
|
||||
EpochSecret? key = null;
|
||||
object newState;
|
||||
ulong epoch = EpochOf(_inner);
|
||||
|
||||
switch (_inner)
|
||||
{
|
||||
// send_ek
|
||||
case CKeysUnsampled s:
|
||||
RequireNotGreater(msg.Epoch, epoch);
|
||||
newState = s; // Less or Equal: stay
|
||||
break;
|
||||
|
||||
case CKeysSampled s:
|
||||
RequireNotGreater(msg.Epoch, epoch);
|
||||
if (msg.Epoch == epoch && msg.Payload.Kind == SpqrMsgKind.Ct1)
|
||||
{
|
||||
UcEkSent uc; byte[] ek;
|
||||
(uc, ek) = s.Uc.SendEk();
|
||||
var ct1Dec = new Dec(Ct1Size);
|
||||
ct1Dec.AddChunk(msg.Payload.Chunk!.Index, msg.Payload.Chunk.Data);
|
||||
newState = new CHeaderSent(uc, new Enc(ek), ct1Dec);
|
||||
}
|
||||
else newState = s;
|
||||
break;
|
||||
|
||||
case CHeaderSent s:
|
||||
RequireNotGreater(msg.Epoch, epoch);
|
||||
if (msg.Epoch == epoch && msg.Payload.Kind == SpqrMsgKind.Ct1)
|
||||
{
|
||||
s.ReceivingCt1.AddChunk(msg.Payload.Chunk!.Index, msg.Payload.Chunk.Data);
|
||||
byte[]? decoded = s.ReceivingCt1.DecodedMessage();
|
||||
if (decoded is not null)
|
||||
{
|
||||
UcEkSentCt1Received uc = s.Uc.RecvCt1(msg.Epoch, decoded);
|
||||
newState = new CCt1Received(uc, s.SendingEk);
|
||||
}
|
||||
else newState = s;
|
||||
}
|
||||
else newState = s;
|
||||
break;
|
||||
|
||||
case CCt1Received s:
|
||||
RequireNotGreater(msg.Epoch, epoch);
|
||||
if (msg.Epoch == epoch && msg.Payload.Kind == SpqrMsgKind.Ct2)
|
||||
{
|
||||
var ct2Dec = new Dec(Ct2Size + MacSize);
|
||||
ct2Dec.AddChunk(msg.Payload.Chunk!.Index, msg.Payload.Chunk.Data);
|
||||
newState = new CEkSentCt1Received(s.Uc, ct2Dec);
|
||||
}
|
||||
else newState = s;
|
||||
break;
|
||||
|
||||
case CEkSentCt1Received s:
|
||||
RequireNotGreater(msg.Epoch, epoch);
|
||||
if (msg.Epoch == epoch && msg.Payload.Kind == SpqrMsgKind.Ct2)
|
||||
{
|
||||
s.ReceivingCt2.AddChunk(msg.Payload.Chunk!.Index, msg.Payload.Chunk.Data);
|
||||
byte[]? decoded = s.ReceivingCt2.DecodedMessage();
|
||||
if (decoded is not null)
|
||||
{
|
||||
byte[] ct2 = decoded[..Ct2Size];
|
||||
byte[] mac = decoded[Ct2Size..];
|
||||
(UcNoHeaderReceived uc, EpochSecret sec) = s.Uc.RecvCt2(ct2, mac);
|
||||
key = sec;
|
||||
newState = new CNoHeaderReceived(uc, new Dec(HeaderSize + MacSize));
|
||||
}
|
||||
else newState = s;
|
||||
}
|
||||
else newState = s;
|
||||
break;
|
||||
|
||||
// send_ct
|
||||
case CNoHeaderReceived s:
|
||||
RequireNotGreater(msg.Epoch, epoch);
|
||||
if (msg.Epoch == epoch && msg.Payload.Kind == SpqrMsgKind.Hdr)
|
||||
{
|
||||
s.ReceivingHdr.AddChunk(msg.Payload.Chunk!.Index, msg.Payload.Chunk.Data);
|
||||
byte[]? decoded = s.ReceivingHdr.DecodedMessage();
|
||||
if (decoded is not null)
|
||||
{
|
||||
byte[] hdr = decoded[..HeaderSize];
|
||||
byte[] mac = decoded[HeaderSize..];
|
||||
UcHeaderReceived uc = s.Uc.RecvHeader(msg.Epoch, hdr, mac);
|
||||
newState = new CHeaderReceived(uc, new Dec(EkSize));
|
||||
}
|
||||
else newState = s;
|
||||
}
|
||||
else newState = s;
|
||||
break;
|
||||
|
||||
case CHeaderReceived s:
|
||||
RequireNotGreater(msg.Epoch, epoch);
|
||||
newState = s; // no recv transition; we only send_ct1 from here
|
||||
break;
|
||||
|
||||
case CCt1Sampled s:
|
||||
RequireNotGreater(msg.Epoch, epoch);
|
||||
if (msg.Epoch == epoch)
|
||||
{
|
||||
SpqrChunk? chunk = null; bool ack = false;
|
||||
if (msg.Payload.Kind == SpqrMsgKind.Ek) { chunk = msg.Payload.Chunk; ack = false; }
|
||||
else if (msg.Payload.Kind == SpqrMsgKind.EkCt1Ack) { chunk = msg.Payload.Chunk; ack = true; }
|
||||
if (chunk is not null)
|
||||
{
|
||||
s.ReceivingEk.AddChunk(chunk.Index, chunk.Data);
|
||||
byte[]? decoded = s.ReceivingEk.DecodedMessage();
|
||||
if (decoded is not null)
|
||||
{
|
||||
UcCt1SentEkReceived uc = s.Uc.RecvEk(msg.Epoch, decoded);
|
||||
if (ack)
|
||||
{
|
||||
(UcCt2Sent uc2, byte[] ct2, byte[] mac) = uc.SendCt2();
|
||||
newState = new CCt2Sampled(uc2, new Enc(Cat(ct2, mac)));
|
||||
}
|
||||
else newState = new CEkReceivedCt1Sampled(uc, s.SendingCt1);
|
||||
}
|
||||
else if (ack)
|
||||
newState = new CCt1Acknowledged(s.Uc, s.ReceivingEk);
|
||||
else newState = s;
|
||||
}
|
||||
else newState = s;
|
||||
}
|
||||
else newState = s;
|
||||
break;
|
||||
|
||||
case CEkReceivedCt1Sampled s:
|
||||
RequireNotGreater(msg.Epoch, epoch);
|
||||
if (msg.Epoch == epoch &&
|
||||
((msg.Payload.Kind == SpqrMsgKind.Ct1Ack && msg.Payload.Ack) ||
|
||||
msg.Payload.Kind == SpqrMsgKind.EkCt1Ack))
|
||||
{
|
||||
(UcCt2Sent uc2, byte[] ct2, byte[] mac) = s.Uc.SendCt2();
|
||||
newState = new CCt2Sampled(uc2, new Enc(Cat(ct2, mac)));
|
||||
}
|
||||
else newState = s;
|
||||
break;
|
||||
|
||||
case CCt1Acknowledged s:
|
||||
RequireNotGreater(msg.Epoch, epoch);
|
||||
if (msg.Epoch == epoch)
|
||||
{
|
||||
SpqrChunk? chunk = msg.Payload.Kind is SpqrMsgKind.Ek or SpqrMsgKind.EkCt1Ack
|
||||
? msg.Payload.Chunk : null;
|
||||
if (chunk is not null)
|
||||
{
|
||||
s.ReceivingEk.AddChunk(chunk.Index, chunk.Data);
|
||||
byte[]? decoded = s.ReceivingEk.DecodedMessage();
|
||||
if (decoded is not null)
|
||||
{
|
||||
UcCt1SentEkReceived uc = s.Uc.RecvEk(msg.Epoch, decoded);
|
||||
(UcCt2Sent uc2, byte[] ct2, byte[] mac) = uc.SendCt2();
|
||||
newState = new CCt2Sampled(uc2, new Enc(Cat(ct2, mac)));
|
||||
}
|
||||
else newState = s;
|
||||
}
|
||||
else newState = s;
|
||||
}
|
||||
else newState = s;
|
||||
break;
|
||||
|
||||
case CCt2Sampled s:
|
||||
if (msg.Epoch > epoch)
|
||||
{
|
||||
if (msg.Epoch == epoch + 1)
|
||||
{
|
||||
UcKeysUnsampled uc = s.Uc.RecvNextEpoch(msg.Epoch);
|
||||
newState = new CKeysUnsampled(uc);
|
||||
}
|
||||
else throw new SpqrException($"epoch out of range: {msg.Epoch}");
|
||||
}
|
||||
else newState = s; // Less or Equal: stay
|
||||
break;
|
||||
|
||||
default: throw new SpqrException("unknown state");
|
||||
}
|
||||
|
||||
return new RecvResult { Key = key, State = newState == _inner ? this : new SckaStates(newState) };
|
||||
}
|
||||
|
||||
private static void RequireNotGreater(ulong msgEpoch, ulong stateEpoch)
|
||||
{
|
||||
if (msgEpoch > stateEpoch) throw new SpqrException($"epoch out of range: {msgEpoch}");
|
||||
}
|
||||
|
||||
private static SpqrChunk Next(Enc enc)
|
||||
{
|
||||
(ushort idx, byte[] data) = enc.NextChunk();
|
||||
return new SpqrChunk(idx, data);
|
||||
}
|
||||
|
||||
private static byte[] Cat(byte[] a, byte[] b)
|
||||
{
|
||||
var r = new byte[a.Length + b.Length];
|
||||
Buffer.BlockCopy(a, 0, r, 0, a.Length);
|
||||
Buffer.BlockCopy(b, 0, r, a.Length, b.Length);
|
||||
return r;
|
||||
}
|
||||
|
||||
// ── serialization (local-only state persistence) ──
|
||||
|
||||
internal void Write(BinaryWriter w)
|
||||
{
|
||||
switch (_inner)
|
||||
{
|
||||
case CKeysUnsampled s: w.Write((byte)1); WKeysUnsampled(w, s.Uc); break;
|
||||
case CKeysSampled s: w.Write((byte)2); WHeaderSent(w, s.Uc); s.SendingHdr.Write(w); break;
|
||||
case CHeaderSent s: w.Write((byte)3); WEkSent(w, s.Uc); s.SendingEk.Write(w); s.ReceivingCt1.Write(w); break;
|
||||
case CCt1Received s: w.Write((byte)4); WEkSentCt1Received(w, s.Uc); s.SendingEk.Write(w); break;
|
||||
case CEkSentCt1Received s: w.Write((byte)5); WEkSentCt1Received(w, s.Uc); s.ReceivingCt2.Write(w); break;
|
||||
case CNoHeaderReceived s: w.Write((byte)6); WNoHeaderReceived(w, s.Uc); s.ReceivingHdr.Write(w); break;
|
||||
case CHeaderReceived s: w.Write((byte)7); WHeaderReceived(w, s.Uc); s.ReceivingEk.Write(w); break;
|
||||
case CCt1Sampled s: w.Write((byte)8); WCt1Sent(w, s.Uc); s.SendingCt1.Write(w); s.ReceivingEk.Write(w); break;
|
||||
case CEkReceivedCt1Sampled s: w.Write((byte)9); WCt1SentEkReceived(w, s.Uc); s.SendingCt1.Write(w); break;
|
||||
case CCt1Acknowledged s: w.Write((byte)10); WCt1Sent(w, s.Uc); s.ReceivingEk.Write(w); break;
|
||||
case CCt2Sampled s: w.Write((byte)11); WCt2Sent(w, s.Uc); s.SendingCt2.Write(w); break;
|
||||
default: throw new SpqrException("unknown state");
|
||||
}
|
||||
}
|
||||
|
||||
internal static SckaStates Read(BinaryReader r)
|
||||
{
|
||||
byte tag = r.ReadByte();
|
||||
object inner = tag switch
|
||||
{
|
||||
1 => new CKeysUnsampled(RKeysUnsampled(r)),
|
||||
2 => new CKeysSampled(RHeaderSent(r), Enc.Read(r)),
|
||||
3 => new CHeaderSent(REkSent(r), Enc.Read(r), Dec.Read(r)),
|
||||
4 => new CCt1Received(REkSentCt1Received(r), Enc.Read(r)),
|
||||
5 => new CEkSentCt1Received(REkSentCt1Received(r), Dec.Read(r)),
|
||||
6 => new CNoHeaderReceived(RNoHeaderReceived(r), Dec.Read(r)),
|
||||
7 => new CHeaderReceived(RHeaderReceived(r), Dec.Read(r)),
|
||||
8 => new CCt1Sampled(RCt1Sent(r), Enc.Read(r), Dec.Read(r)),
|
||||
9 => new CEkReceivedCt1Sampled(RCt1SentEkReceived(r), Enc.Read(r)),
|
||||
10 => new CCt1Acknowledged(RCt1Sent(r), Dec.Read(r)),
|
||||
11 => new CCt2Sampled(RCt2Sent(r), Enc.Read(r)),
|
||||
_ => throw new SpqrException("unknown state tag"),
|
||||
};
|
||||
return new SckaStates(inner);
|
||||
}
|
||||
|
||||
// Unchunked-state field writers/readers (fields are public; Authenticator round-trips itself).
|
||||
private static void WKeysUnsampled(BinaryWriter w, UcKeysUnsampled u) { w.Write(u.Epoch); u.Auth.Write(w); }
|
||||
private static UcKeysUnsampled RKeysUnsampled(BinaryReader r) => new(r.ReadUInt64(), Authenticator.Read(r));
|
||||
|
||||
private static void WHeaderSent(BinaryWriter w, UcHeaderSent u) { w.Write(u.Epoch); u.Auth.Write(w); w.WriteBlob(u.Ek); w.WriteBlob(u.Dk); }
|
||||
private static UcHeaderSent RHeaderSent(BinaryReader r) => new(r.ReadUInt64(), Authenticator.Read(r), r.ReadBlob(), r.ReadBlob());
|
||||
|
||||
private static void WEkSent(BinaryWriter w, UcEkSent u) { w.Write(u.Epoch); u.Auth.Write(w); w.WriteBlob(u.Dk); }
|
||||
private static UcEkSent REkSent(BinaryReader r) => new(r.ReadUInt64(), Authenticator.Read(r), r.ReadBlob());
|
||||
|
||||
private static void WEkSentCt1Received(BinaryWriter w, UcEkSentCt1Received u) { w.Write(u.Epoch); u.Auth.Write(w); w.WriteBlob(u.Dk); w.WriteBlob(u.Ct1); }
|
||||
private static UcEkSentCt1Received REkSentCt1Received(BinaryReader r) => new(r.ReadUInt64(), Authenticator.Read(r), r.ReadBlob(), r.ReadBlob());
|
||||
|
||||
private static void WNoHeaderReceived(BinaryWriter w, UcNoHeaderReceived u) { w.Write(u.Epoch); u.Auth.Write(w); }
|
||||
private static UcNoHeaderReceived RNoHeaderReceived(BinaryReader r) => new(r.ReadUInt64(), Authenticator.Read(r));
|
||||
|
||||
private static void WHeaderReceived(BinaryWriter w, UcHeaderReceived u) { w.Write(u.Epoch); u.Auth.Write(w); w.WriteBlob(u.Hdr); }
|
||||
private static UcHeaderReceived RHeaderReceived(BinaryReader r) => new(r.ReadUInt64(), Authenticator.Read(r), r.ReadBlob());
|
||||
|
||||
private static void WCt1Sent(BinaryWriter w, UcCt1Sent u) { w.Write(u.Epoch); u.Auth.Write(w); w.WriteBlob(u.Hdr); w.WriteBlob(u.Es); w.WriteBlob(u.Ct1); }
|
||||
private static UcCt1Sent RCt1Sent(BinaryReader r) => new(r.ReadUInt64(), Authenticator.Read(r), r.ReadBlob(), r.ReadBlob(), r.ReadBlob());
|
||||
|
||||
private static void WCt1SentEkReceived(BinaryWriter w, UcCt1SentEkReceived u) { w.Write(u.Epoch); u.Auth.Write(w); w.WriteBlob(u.Es); w.WriteBlob(u.Ek); w.WriteBlob(u.Ct1); }
|
||||
private static UcCt1SentEkReceived RCt1SentEkReceived(BinaryReader r) => new(r.ReadUInt64(), Authenticator.Read(r), r.ReadBlob(), r.ReadBlob(), r.ReadBlob());
|
||||
|
||||
private static void WCt2Sent(BinaryWriter w, UcCt2Sent u) { w.Write(u.Epoch); u.Auth.Write(w); }
|
||||
private static UcCt2Sent RCt2Sent(BinaryReader r) => new(r.ReadUInt64(), Authenticator.Read(r));
|
||||
|
||||
// ── chunked state holders (mirror proto V1State.Chunked.*) ──
|
||||
private sealed class CKeysUnsampled { public UcKeysUnsampled Uc; public CKeysUnsampled(UcKeysUnsampled uc) => Uc = uc; }
|
||||
private sealed class CKeysSampled { public UcHeaderSent Uc; public Enc SendingHdr; public CKeysSampled(UcHeaderSent uc, Enc h) { Uc = uc; SendingHdr = h; } }
|
||||
private sealed class CHeaderSent { public UcEkSent Uc; public Enc SendingEk; public Dec ReceivingCt1; public CHeaderSent(UcEkSent uc, Enc e, Dec d) { Uc = uc; SendingEk = e; ReceivingCt1 = d; } }
|
||||
private sealed class CCt1Received { public UcEkSentCt1Received Uc; public Enc SendingEk; public CCt1Received(UcEkSentCt1Received uc, Enc e) { Uc = uc; SendingEk = e; } }
|
||||
private sealed class CEkSentCt1Received { public UcEkSentCt1Received Uc; public Dec ReceivingCt2; public CEkSentCt1Received(UcEkSentCt1Received uc, Dec d) { Uc = uc; ReceivingCt2 = d; } }
|
||||
|
||||
private sealed class CNoHeaderReceived { public UcNoHeaderReceived Uc; public Dec ReceivingHdr; public CNoHeaderReceived(UcNoHeaderReceived uc, Dec d) { Uc = uc; ReceivingHdr = d; } }
|
||||
private sealed class CHeaderReceived { public UcHeaderReceived Uc; public Dec ReceivingEk; public CHeaderReceived(UcHeaderReceived uc, Dec d) { Uc = uc; ReceivingEk = d; } }
|
||||
private sealed class CCt1Sampled { public UcCt1Sent Uc; public Enc SendingCt1; public Dec ReceivingEk; public CCt1Sampled(UcCt1Sent uc, Enc e, Dec d) { Uc = uc; SendingCt1 = e; ReceivingEk = d; } }
|
||||
private sealed class CEkReceivedCt1Sampled { public UcCt1SentEkReceived Uc; public Enc SendingCt1; public CEkReceivedCt1Sampled(UcCt1SentEkReceived uc, Enc e) { Uc = uc; SendingCt1 = e; } }
|
||||
private sealed class CCt1Acknowledged { public UcCt1Sent Uc; public Dec ReceivingEk; public CCt1Acknowledged(UcCt1Sent uc, Dec d) { Uc = uc; ReceivingEk = d; } }
|
||||
private sealed class CCt2Sampled { public UcCt2Sent Uc; public Enc SendingCt2; public CCt2Sampled(UcCt2Sent uc, Enc e) { Uc = uc; SendingCt2 = e; } }
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
using System.Security.Cryptography;
|
||||
using Wingnal.Protocol.Crypto;
|
||||
|
||||
namespace Wingnal.Protocol.Spqr;
|
||||
|
||||
/// <summary>
|
||||
/// The "unchunked" SPQR v1 SCKA crypto states (ported from SparsePostQuantumRatchet v1.5.1
|
||||
/// src/v1/unchunked/{send_ek,send_ct}.rs). These hold the raw ML-KEM-768 material + Authenticator and
|
||||
/// perform the actual KEM operations; the chunked layer (<see cref="SckaChunked"/>) spreads the large
|
||||
/// header/ek/ct byte blobs across many ratchet messages via the polynomial fountain code.
|
||||
///
|
||||
/// Two role tracks alternate per epoch:
|
||||
/// send_ek: KeysUnsampled → HeaderSent → EkSent → EkSentCt1Received → (switch to send_ct, epoch+1)
|
||||
/// send_ct: NoHeaderReceived → HeaderReceived → Ct1Sent → Ct1SentEkReceived → Ct2Sent → (switch, epoch+1)
|
||||
/// </summary>
|
||||
internal static class SckaKdf
|
||||
{
|
||||
private static readonly byte[] ZeroSalt = new byte[32];
|
||||
private static readonly byte[] SckaKeyLabel = "Signal_PQCKA_V1_MLKEM768:SCKA Key"u8.ToArray();
|
||||
|
||||
/// <summary>HKDF(salt=0^32, ikm=ss, info="…SCKA Key"‖BE64(epoch), 32) — turns a raw ML-KEM shared
|
||||
/// secret into the per-epoch secret mixed into the symmetric Chain.</summary>
|
||||
public static byte[] DeriveEpochSecret(byte[] ss, ulong epoch)
|
||||
{
|
||||
var info = new byte[SckaKeyLabel.Length + 8];
|
||||
Buffer.BlockCopy(SckaKeyLabel, 0, info, 0, SckaKeyLabel.Length);
|
||||
for (int i = 7; i >= 0; i--) { info[SckaKeyLabel.Length + i] = (byte)(epoch & 0xFF); epoch >>= 8; }
|
||||
return CryptoPrimitives.Hkdf(ss, ZeroSalt, info, 32);
|
||||
}
|
||||
|
||||
public static byte[] Random32()
|
||||
{
|
||||
var b = new byte[32];
|
||||
RandomNumberGenerator.Fill(b);
|
||||
return b;
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────────── send_ek track ─────────────────────────
|
||||
|
||||
internal sealed class UcKeysUnsampled
|
||||
{
|
||||
public ulong Epoch;
|
||||
public Authenticator Auth;
|
||||
|
||||
public UcKeysUnsampled(ulong epoch, Authenticator auth) { Epoch = epoch; Auth = auth; }
|
||||
public static UcKeysUnsampled New(byte[] authKey) => new(1, new Authenticator(authKey, 1));
|
||||
|
||||
/// <summary>Generates a fresh ML-KEM keypair, MACs its header, advances to HeaderSent.</summary>
|
||||
public (UcHeaderSent State, byte[] Hdr, byte[] Mac) SendHeader()
|
||||
{
|
||||
MlKem768.Keys keys = MlKem768.Generate(SckaKdf.Random32(), SckaKdf.Random32());
|
||||
byte[] mac = Auth.MacHeader(Epoch, keys.Header);
|
||||
return (new UcHeaderSent(Epoch, Auth, keys.Ek, keys.Dk), keys.Header, mac);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class UcHeaderSent
|
||||
{
|
||||
public ulong Epoch;
|
||||
public Authenticator Auth;
|
||||
public byte[] Ek; // 1152
|
||||
public byte[] Dk; // 2400
|
||||
|
||||
public UcHeaderSent(ulong epoch, Authenticator auth, byte[] ek, byte[] dk)
|
||||
{ Epoch = epoch; Auth = auth; Ek = ek; Dk = dk; }
|
||||
|
||||
public (UcEkSent State, byte[] Ek) SendEk() => (new UcEkSent(Epoch, Auth, Dk), Ek);
|
||||
}
|
||||
|
||||
internal sealed class UcEkSent
|
||||
{
|
||||
public ulong Epoch;
|
||||
public Authenticator Auth;
|
||||
public byte[] Dk;
|
||||
|
||||
public UcEkSent(ulong epoch, Authenticator auth, byte[] dk) { Epoch = epoch; Auth = auth; Dk = dk; }
|
||||
|
||||
public UcEkSentCt1Received RecvCt1(ulong epoch, byte[] ct1)
|
||||
{
|
||||
if (epoch != Epoch) throw new SpqrException("epoch mismatch");
|
||||
return new UcEkSentCt1Received(Epoch, Auth, Dk, ct1);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class UcEkSentCt1Received
|
||||
{
|
||||
public ulong Epoch;
|
||||
public Authenticator Auth;
|
||||
public byte[] Dk;
|
||||
public byte[] Ct1; // 960
|
||||
|
||||
public UcEkSentCt1Received(ulong epoch, Authenticator auth, byte[] dk, byte[] ct1)
|
||||
{ Epoch = epoch; Auth = auth; Dk = dk; Ct1 = ct1; }
|
||||
|
||||
/// <summary>Decapsulates ct1‖ct2, derives + mixes the epoch secret, verifies the ciphertext MAC,
|
||||
/// and switches into the send_ct track for the next epoch.</summary>
|
||||
public (UcNoHeaderReceived State, EpochSecret Secret) RecvCt2(byte[] ct2, byte[] mac)
|
||||
{
|
||||
byte[] ss = MlKem768.DecapsIncremental(Dk, Ct1, ct2);
|
||||
byte[] secret = SckaKdf.DeriveEpochSecret(ss, Epoch);
|
||||
Auth.Update(Epoch, secret);
|
||||
var full = new byte[Ct1.Length + ct2.Length];
|
||||
Buffer.BlockCopy(Ct1, 0, full, 0, Ct1.Length);
|
||||
Buffer.BlockCopy(ct2, 0, full, Ct1.Length, ct2.Length);
|
||||
if (!Auth.VerifyCiphertext(Epoch, full, mac))
|
||||
throw new SpqrException("ciphertext MAC verification failed");
|
||||
return (new UcNoHeaderReceived(Epoch + 1, Auth), new EpochSecret(Epoch, secret));
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────────── send_ct track ─────────────────────────
|
||||
|
||||
internal sealed class UcNoHeaderReceived
|
||||
{
|
||||
public ulong Epoch;
|
||||
public Authenticator Auth;
|
||||
|
||||
public UcNoHeaderReceived(ulong epoch, Authenticator auth) { Epoch = epoch; Auth = auth; }
|
||||
public static UcNoHeaderReceived New(byte[] authKey) => new(1, new Authenticator(authKey, 1));
|
||||
|
||||
public UcHeaderReceived RecvHeader(ulong epoch, byte[] hdr, byte[] mac)
|
||||
{
|
||||
if (epoch != Epoch) throw new SpqrException("epoch mismatch");
|
||||
if (!Auth.VerifyHeader(Epoch, hdr, mac)) throw new SpqrException("header MAC verification failed");
|
||||
return new UcHeaderReceived(Epoch, Auth, hdr);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class UcHeaderReceived
|
||||
{
|
||||
public ulong Epoch;
|
||||
public Authenticator Auth;
|
||||
public byte[] Hdr; // 64
|
||||
|
||||
public UcHeaderReceived(ulong epoch, Authenticator auth, byte[] hdr) { Epoch = epoch; Auth = auth; Hdr = hdr; }
|
||||
|
||||
/// <summary>encaps1 against the received header, derives + mixes the epoch secret.</summary>
|
||||
public (UcCt1Sent State, byte[] Ct1, EpochSecret Secret) SendCt1()
|
||||
{
|
||||
MlKem768.Encaps1(Hdr, SckaKdf.Random32(), out byte[] ct1, out byte[] es, out byte[] ss);
|
||||
byte[] secret = SckaKdf.DeriveEpochSecret(ss, Epoch);
|
||||
Auth.Update(Epoch, secret);
|
||||
return (new UcCt1Sent(Epoch, Auth, Hdr, es, ct1), ct1, new EpochSecret(Epoch, secret));
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class UcCt1Sent
|
||||
{
|
||||
public ulong Epoch;
|
||||
public Authenticator Auth;
|
||||
public byte[] Hdr; // 64
|
||||
public byte[] Es; // encaps state (local)
|
||||
public byte[] Ct1; // 960
|
||||
|
||||
public UcCt1Sent(ulong epoch, Authenticator auth, byte[] hdr, byte[] es, byte[] ct1)
|
||||
{ Epoch = epoch; Auth = auth; Hdr = hdr; Es = es; Ct1 = ct1; }
|
||||
|
||||
public UcCt1SentEkReceived RecvEk(ulong epoch, byte[] ek)
|
||||
{
|
||||
if (epoch != Epoch) throw new SpqrException("epoch mismatch");
|
||||
if (!MlKem768.EkMatchesHeader(ek, Hdr)) throw new SpqrException("erroneous data received");
|
||||
return new UcCt1SentEkReceived(Epoch, Auth, Es, ek, Ct1);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class UcCt1SentEkReceived
|
||||
{
|
||||
public ulong Epoch;
|
||||
public Authenticator Auth;
|
||||
public byte[] Es;
|
||||
public byte[] Ek; // 1152
|
||||
public byte[] Ct1; // 960
|
||||
|
||||
public UcCt1SentEkReceived(ulong epoch, Authenticator auth, byte[] es, byte[] ek, byte[] ct1)
|
||||
{ Epoch = epoch; Auth = auth; Es = es; Ek = ek; Ct1 = ct1; }
|
||||
|
||||
/// <summary>encaps2 to produce ct2, then MAC ct1‖ct2.</summary>
|
||||
public (UcCt2Sent State, byte[] Ct2, byte[] Mac) SendCt2()
|
||||
{
|
||||
byte[] ct2 = MlKem768.Encaps2(Ek, Es);
|
||||
var full = new byte[Ct1.Length + ct2.Length];
|
||||
Buffer.BlockCopy(Ct1, 0, full, 0, Ct1.Length);
|
||||
Buffer.BlockCopy(ct2, 0, full, Ct1.Length, ct2.Length);
|
||||
byte[] mac = Auth.MacCiphertext(Epoch, full);
|
||||
return (new UcCt2Sent(Epoch, Auth), ct2, mac);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class UcCt2Sent
|
||||
{
|
||||
public ulong Epoch;
|
||||
public Authenticator Auth;
|
||||
|
||||
public UcCt2Sent(ulong epoch, Authenticator auth) { Epoch = epoch; Auth = auth; }
|
||||
|
||||
public UcKeysUnsampled RecvNextEpoch(ulong nextEpoch)
|
||||
{
|
||||
if (nextEpoch != Epoch + 1) throw new SpqrException("epoch must advance by one");
|
||||
return new UcKeysUnsampled(Epoch + 1, Auth);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.IO;
|
||||
|
||||
namespace Wingnal.Protocol.Spqr;
|
||||
|
||||
/// <summary>Length-prefixed binary helpers for the SPQR/session state serialization. The format is
|
||||
/// local-only (never sent to a peer) — the ML-KEM Braid spec leaves state serialization
|
||||
/// implementation-defined — so this compact custom encoding is sufficient.</summary>
|
||||
internal static class Bin
|
||||
{
|
||||
public static void WriteBlob(this BinaryWriter w, byte[] b)
|
||||
{
|
||||
w.Write(b.Length);
|
||||
w.Write(b);
|
||||
}
|
||||
|
||||
public static byte[] ReadBlob(this BinaryReader r)
|
||||
{
|
||||
int n = r.ReadInt32();
|
||||
return r.ReadBytes(n);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
using System.IO;
|
||||
|
||||
namespace Wingnal.Protocol.Spqr;
|
||||
|
||||
public enum SpqrVersion { V0 = 0, V1 = 1 }
|
||||
|
||||
/// <summary>Parameters for <see cref="SpqrRatchet.InitialState"/> (mirrors lib.rs Params).</summary>
|
||||
public sealed class SpqrParams
|
||||
{
|
||||
public required Direction Direction { get; init; }
|
||||
public required SpqrVersion Version { get; init; }
|
||||
public required SpqrVersion MinVersion { get; init; }
|
||||
public required byte[] AuthKey { get; init; }
|
||||
public ChainParams ChainParams { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Top-level SPQR ratchet API (ported from SparsePostQuantumRatchet v1.5.1 src/lib.rs). Glues the
|
||||
/// chunked SCKA state machine to the symmetric <see cref="Chain"/>: <see cref="Send"/> emits the wire
|
||||
/// <c>pq_ratchet</c> bytes plus an optional 32-byte message key (the HKDF salt libsignal mixes into the
|
||||
/// per-message WhisperMessageKeys), and <see cref="Recv"/> consumes peer bytes and returns the matching
|
||||
/// salt. State is kept in-memory (object form); proto serialization for durable storage is layered on
|
||||
/// separately. Only V1 is implemented (libsignal mandates V1/min-V1 on both ends).
|
||||
/// </summary>
|
||||
public sealed class SpqrRatchet
|
||||
{
|
||||
private SckaStates? _inner; // null ⇒ V0 (disabled)
|
||||
private Chain? _chain;
|
||||
|
||||
// Version-negotiation block (present until the first recv); needed to lazily build the Chain.
|
||||
private bool _hasVn;
|
||||
private byte[]? _vnAuthKey;
|
||||
private Direction _vnDirection;
|
||||
private SpqrVersion _vnMinVersion;
|
||||
private ChainParams _vnChainParams = new();
|
||||
|
||||
private SpqrRatchet() { }
|
||||
|
||||
public SpqrVersion CurrentVersion => _inner is null ? SpqrVersion.V0 : SpqrVersion.V1;
|
||||
|
||||
public static SpqrRatchet InitialState(SpqrParams p)
|
||||
{
|
||||
var r = new SpqrRatchet();
|
||||
if (p.Version == SpqrVersion.V0) return r; // empty/disabled
|
||||
|
||||
r._inner = p.Direction == Direction.A2B
|
||||
? SckaStates.InitA(p.AuthKey)
|
||||
: SckaStates.InitB(p.AuthKey);
|
||||
r._hasVn = true;
|
||||
r._vnAuthKey = p.AuthKey;
|
||||
r._vnDirection = p.Direction;
|
||||
r._vnMinVersion = p.MinVersion;
|
||||
r._vnChainParams = p.ChainParams;
|
||||
return r;
|
||||
}
|
||||
|
||||
public sealed class SendOutput
|
||||
{
|
||||
public required byte[] Message { get; init; } // wire pq_ratchet bytes (empty for V0)
|
||||
public byte[]? Key { get; init; } // 32-byte HKDF salt, or null
|
||||
}
|
||||
|
||||
public SendOutput Send()
|
||||
{
|
||||
if (_inner is null) return new SendOutput { Message = Array.Empty<byte>(), Key = null };
|
||||
|
||||
SckaStates.SendResult sr = _inner.Send();
|
||||
|
||||
Chain? chain;
|
||||
if (_chain is not null) chain = _chain;
|
||||
else if (_hasVn) chain = _vnMinVersion > SpqrVersion.V0 ? new Chain(_vnAuthKey!, _vnDirection, _vnChainParams) : null;
|
||||
else throw new SpqrException("chain not available");
|
||||
|
||||
uint index; byte[] msgKey;
|
||||
if (chain is null) { index = 0; msgKey = Array.Empty<byte>(); }
|
||||
else
|
||||
{
|
||||
if (sr.Key is not null) chain.AddEpoch(sr.Key);
|
||||
(index, msgKey) = chain.SendKey(sr.Msg.Epoch - 1);
|
||||
}
|
||||
|
||||
byte[] wire = SerializeMessage(sr.Msg, index);
|
||||
_inner = sr.State;
|
||||
if (chain is not null) _chain = chain; // version_negotiation unchanged on send
|
||||
return new SendOutput { Message = wire, Key = msgKey.Length == 0 ? null : msgKey };
|
||||
}
|
||||
|
||||
/// <summary>Process a peer's pq_ratchet bytes; returns the 32-byte message-key salt (or null).</summary>
|
||||
public byte[]? Recv(byte[] message)
|
||||
{
|
||||
if (_inner is null) return null; // V0
|
||||
|
||||
// Version negotiation: libsignal uses V1/min-V1 both ways, so msg version (1) == our version (1).
|
||||
SpqrVersion? msgVer = MsgVersion(message);
|
||||
if (msgVer is null) return null; // unsupported higher version: ignore
|
||||
if (msgVer.Value < SpqrVersion.V1)
|
||||
throw new SpqrException("SPQR version downgrade not supported");
|
||||
|
||||
(SpqrMessage scka, uint index) = DeserializeMessage(message);
|
||||
SckaStates.RecvResult rr = _inner.Recv(scka);
|
||||
|
||||
ulong msgKeyEpoch = scka.Epoch - 1;
|
||||
Chain chain = _chain ?? (_hasVn
|
||||
? new Chain(_vnAuthKey!, _vnDirection, _vnChainParams)
|
||||
: throw new SpqrException("chain not available"));
|
||||
if (rr.Key is not null) chain.AddEpoch(rr.Key);
|
||||
byte[] msgKey = msgKeyEpoch == 0 && index == 0
|
||||
? Array.Empty<byte>()
|
||||
: chain.RecvKey(msgKeyEpoch, index);
|
||||
|
||||
_inner = rr.State;
|
||||
_chain = chain;
|
||||
_hasVn = false; // receiving clears version negotiation
|
||||
return msgKey.Length == 0 ? null : msgKey;
|
||||
}
|
||||
|
||||
// ── state serialization (local-only persistence) ──
|
||||
|
||||
public byte[] Serialize()
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
using var w = new BinaryWriter(ms);
|
||||
if (_inner is null)
|
||||
{
|
||||
w.Write((byte)0); // V0 / disabled
|
||||
}
|
||||
else
|
||||
{
|
||||
w.Write((byte)1);
|
||||
_inner.Write(w);
|
||||
w.Write(_chain is not null);
|
||||
_chain?.Write(w);
|
||||
w.Write(_hasVn);
|
||||
if (_hasVn)
|
||||
{
|
||||
w.WriteBlob(_vnAuthKey!);
|
||||
w.Write((int)_vnDirection);
|
||||
w.Write((int)_vnMinVersion);
|
||||
w.Write(_vnChainParams.MaxJump);
|
||||
w.Write(_vnChainParams.MaxOooKeys);
|
||||
}
|
||||
}
|
||||
w.Flush();
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
public static SpqrRatchet Deserialize(byte[] bytes)
|
||||
{
|
||||
var r = new SpqrRatchet();
|
||||
using var ms = new MemoryStream(bytes);
|
||||
using var rd = new BinaryReader(ms);
|
||||
if (rd.ReadByte() == 0) return r; // V0
|
||||
r._inner = SckaStates.Read(rd);
|
||||
if (rd.ReadBoolean()) r._chain = Chain.Read(rd);
|
||||
r._hasVn = rd.ReadBoolean();
|
||||
if (r._hasVn)
|
||||
{
|
||||
r._vnAuthKey = rd.ReadBlob();
|
||||
r._vnDirection = (Direction)rd.ReadInt32();
|
||||
r._vnMinVersion = (SpqrVersion)rd.ReadInt32();
|
||||
r._vnChainParams = new ChainParams { MaxJump = rd.ReadUInt32(), MaxOooKeys = rd.ReadUInt32() };
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
private static SpqrVersion? MsgVersion(byte[] msg)
|
||||
{
|
||||
if (msg.Length == 0) return SpqrVersion.V0;
|
||||
return msg[0] switch { 0 => SpqrVersion.V0, 1 => SpqrVersion.V1, _ => null };
|
||||
}
|
||||
|
||||
// ── wire message format (see v1/chunked/states/serialize.rs) ──
|
||||
// [version=1] [varint epoch] [varint index] [type:1] [optional: varint chunkIndex || 32B data]
|
||||
private enum MsgType : byte { None = 0, Hdr = 1, Ek = 2, EkCt1Ack = 3, Ct1Ack = 4, Ct1 = 5, Ct2 = 6 }
|
||||
|
||||
private static byte[] SerializeMessage(SpqrMessage msg, uint index)
|
||||
{
|
||||
var o = new List<byte>(40) { (byte)SpqrVersion.V1 };
|
||||
EncodeVarint(msg.Epoch, o);
|
||||
EncodeVarint(index, o);
|
||||
MsgType type = msg.Payload.Kind switch
|
||||
{
|
||||
SpqrMsgKind.None => MsgType.None,
|
||||
SpqrMsgKind.Hdr => MsgType.Hdr,
|
||||
SpqrMsgKind.Ek => MsgType.Ek,
|
||||
SpqrMsgKind.EkCt1Ack => MsgType.EkCt1Ack,
|
||||
SpqrMsgKind.Ct1Ack => MsgType.Ct1Ack,
|
||||
SpqrMsgKind.Ct1 => MsgType.Ct1,
|
||||
SpqrMsgKind.Ct2 => MsgType.Ct2,
|
||||
_ => throw new SpqrException("bad payload"),
|
||||
};
|
||||
o.Add((byte)type);
|
||||
if (msg.Payload.Chunk is { } chunk)
|
||||
{
|
||||
EncodeVarint(chunk.Index, o);
|
||||
o.AddRange(chunk.Data);
|
||||
}
|
||||
return o.ToArray();
|
||||
}
|
||||
|
||||
private static (SpqrMessage Msg, uint Index) DeserializeMessage(byte[] from)
|
||||
{
|
||||
if (from.Length == 0 || from[0] != (byte)SpqrVersion.V1) throw new SpqrException("message decode failed");
|
||||
int at = 1;
|
||||
ulong epoch = DecodeVarint(from, ref at);
|
||||
if (epoch == 0) throw new SpqrException("message decode failed");
|
||||
ulong indexU = DecodeVarint(from, ref at);
|
||||
if (indexU > uint.MaxValue) throw new SpqrException("message decode failed");
|
||||
if (at >= from.Length) throw new SpqrException("message decode failed");
|
||||
var type = (MsgType)from[at];
|
||||
at++;
|
||||
SpqrPayload payload = type switch
|
||||
{
|
||||
MsgType.None => SpqrPayload.None,
|
||||
MsgType.Ct1Ack => SpqrPayload.Ct1Ack(true),
|
||||
MsgType.Hdr => SpqrPayload.Hdr(DecodeChunk(from, ref at)),
|
||||
MsgType.Ek => SpqrPayload.Ek(DecodeChunk(from, ref at)),
|
||||
MsgType.EkCt1Ack => SpqrPayload.EkCt1Ack(DecodeChunk(from, ref at)),
|
||||
MsgType.Ct1 => SpqrPayload.Ct1(DecodeChunk(from, ref at)),
|
||||
MsgType.Ct2 => SpqrPayload.Ct2(DecodeChunk(from, ref at)),
|
||||
_ => throw new SpqrException("message decode failed"),
|
||||
};
|
||||
return (new SpqrMessage(epoch, payload), (uint)indexU);
|
||||
}
|
||||
|
||||
private static SpqrChunk DecodeChunk(byte[] from, ref int at)
|
||||
{
|
||||
ulong index = DecodeVarint(from, ref at);
|
||||
int start = at;
|
||||
at += 32;
|
||||
if (at > from.Length || index > 65535) throw new SpqrException("message decode failed");
|
||||
return new SpqrChunk((ushort)index, from[start..at]);
|
||||
}
|
||||
|
||||
private static void EncodeVarint(ulong a, List<byte> into)
|
||||
{
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
byte b = (byte)(a & 0x7F);
|
||||
if (a < 0x80) { into.Add(b); break; }
|
||||
into.Add((byte)(0x80 | b));
|
||||
a >>= 7;
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong DecodeVarint(byte[] from, ref int at)
|
||||
{
|
||||
ulong outv = 0;
|
||||
int start = at;
|
||||
if (start >= from.Length) throw new SpqrException("message decode failed");
|
||||
int max = Math.Min(10, from.Length - start);
|
||||
int i = 0;
|
||||
bool done = false;
|
||||
while (i < max && !done)
|
||||
{
|
||||
byte b = from[start + i];
|
||||
outv |= ((ulong)b & 0x7F) << (7 * i);
|
||||
i++;
|
||||
done = (b & 0x80) == 0;
|
||||
}
|
||||
if (!done) throw new SpqrException("message decode failed");
|
||||
at += i;
|
||||
return outv;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user