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);
}
}