using System; using System.Numerics; namespace axXez.TS3.Protocol { /// /// Minimal Edwards25519 point operations equivalent to libsodium's /// crypto_scalarmult_ed25519_noclamp and crypto_core_ed25519_add. /// Uses BigInteger — correct but not constant-time. Fine for per-connection use. /// internal static class Edwards25519 { // p = 2^255 - 19 private static readonly BigInteger P = (BigInteger.One << 255) - 19; // d = -121665/121666 mod p (the Edwards25519 curve constant) private static readonly BigInteger CurveD; // sqrt(-1) mod p = 2^((p-1)/4) mod p, used for point decompression private static readonly BigInteger SqrtMinus1; // Base point B in compressed form (RFC 8032 §5.1 — y = 4/5 mod p, x positive) private static readonly byte[] BasePointBytes = { 0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, }; static Edwards25519() { CurveD = Mod(BigInteger.Parse("-121665") * BigInteger.ModPow(121666, P - 2, P)); SqrtMinus1 = BigInteger.ModPow(2, (P - 1) / 4, P); } private struct ExtPoint { public BigInteger X, Y, Z, T; public static readonly ExtPoint Identity = new ExtPoint { X = 0, Y = 1, Z = 1, T = 0 }; } private static ExtPoint Decompress(byte[] data) { byte[] yBytes = (byte[])data.Clone(); int xSign = (yBytes[31] >> 7) & 1; yBytes[31] &= 0x7f; var y = new BigInteger(yBytes, isUnsigned: true, isBigEndian: false); // x² = (y² - 1) / (d·y² + 1) mod p var y2 = Mod(y * y); var u = Mod(y2 - 1); var v = Mod(CurveD * y2 + 1); var x2 = Mod(u * ModInverse(v)); if (x2.IsZero) { if (xSign != 0) throw new Exception("Invalid Edwards25519 point."); return new ExtPoint { X = 0, Y = y, Z = 1, T = 0 }; } // sqrt candidate via p ≡ 5 (mod 8): x = x2^((p+3)/8) var x = BigInteger.ModPow(x2, (P + 3) / 8, P); if (Mod(x * x - x2) != 0) x = Mod(x * SqrtMinus1); if (Mod(x * x - x2) != 0) throw new Exception("No square root — invalid Edwards25519 point."); if ((int)(x & 1) != xSign) x = P - x; return new ExtPoint { X = x, Y = y, Z = 1, T = Mod(x * y) }; } private static byte[] Compress(ExtPoint p) { var zi = ModInverse(p.Z); var x = Mod(p.X * zi); var y = Mod(p.Y * zi); byte[] result = y.ToByteArray(isUnsigned: true, isBigEndian: false); Array.Resize(ref result, 32); // zero-pad to 32 bytes (little-endian high bytes) if ((x & 1) != 0) result[31] |= 0x80; return result; } private static ExtPoint PointAdd(ExtPoint a, ExtPoint b) { var A = Mod(a.X * b.X); var B = Mod(a.Y * b.Y); var C = Mod(CurveD * a.T * b.T); var Dv = Mod(a.Z * b.Z); var E = Mod((a.X + a.Y) * (b.X + b.Y) - A - B); var F = Mod(Dv - C); var G = Mod(Dv + C); var H = Mod(B + A); // a = -1 → H = B - a·A = B + A return new ExtPoint { X = Mod(E * F), Y = Mod(G * H), Z = Mod(F * G), T = Mod(E * H), }; } private static ExtPoint ScalarMultInternal(BigInteger scalar, ExtPoint point) { var result = ExtPoint.Identity; var addend = point; while (scalar > 0) { if ((scalar & 1) != 0) result = PointAdd(result, addend); addend = PointAdd(addend, addend); scalar >>= 1; } return result; } /// /// Scalar multiplication on Edwards25519 without clamping. /// Equivalent to libsodium's crypto_scalarmult_ed25519_noclamp. /// /// 32-byte little-endian scalar. /// 32-byte compressed Edwards25519 point. /// 32-byte compressed result point. public static byte[] ScalarMult(byte[] scalar, byte[] point) { var s = new BigInteger(scalar, isUnsigned: true, isBigEndian: false); return Compress(ScalarMultInternal(s, Decompress(point))); } /// /// Scalar × base-point. /// Equivalent to libsodium's crypto_scalarmult_ed25519_base_noclamp. /// /// 32-byte little-endian scalar. /// 32-byte compressed result point. public static byte[] ScalarMultBase(byte[] scalar) { var s = new BigInteger(scalar, isUnsigned: true, isBigEndian: false); return Compress(ScalarMultInternal(s, Decompress(BasePointBytes))); } /// /// Adds two compressed Edwards25519 points. /// Equivalent to libsodium's crypto_core_ed25519_add. /// public static byte[] PointAdd(byte[] p1, byte[] p2) => Compress(PointAdd(Decompress(p1), Decompress(p2))); private static BigInteger Mod(BigInteger x) => ((x % P) + P) % P; private static BigInteger ModInverse(BigInteger x) => BigInteger.ModPow(Mod(x), P - 2, P); } }