Source
stdlib/x25519/x25519.cpp
1
// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).2
// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.3
#include "x25519.hpp"5
#include <cstddef>6
#include <cstdint>8
// X25519 (RFC 7748) from scratch. Field elements of GF(2^255 - 19) are 16 limbs of 16 bits9
// in int64 lanes (the compact TweetNaCl-style representation): wide enough that schoolbook10
// multiplication cannot overflow before the carry pass, small enough to stay readable.11
// EVERYTHING here is constant-time in the secret scalar: fixed 255 ladder steps, arithmetic12
// conditional swaps (no branches on secret bits), and a fixed carry/reduce schedule.14
namespace cheatah::x25519 {15
namespace {17
using i64 = std::int64_t;18
using Fe = i64[16]; // one field element: 16 little-endian 16-bit limbs20
// Propagate carries so every limb fits 16 bits again; the top carry re-enters at limb 021
// multiplied by 38 (= 2 * 19, because 2^256 = 38 mod p). @complexity O(1) @alloc none22
// @test CheatahX25519.Rfc7748Vector123
void carry(Fe o) {24
for (std::size_t i = 0; i < 16; ++i) {25
o[i] += (1LL << 16);26
const i64 c = o[i] >> 16;27
o[(i + 1) * static_cast<std::size_t>(i < 15)] += c - 1 + 37 * (c - 1) * static_cast<i64>(i == 15);28
o[i] -= c << 16;29
}30
}32
// Constant-time conditional swap: when b == 1 swap (p, q), when b == 0 leave them — by33
// XOR masking, never by branching on b (b derives from a SECRET scalar bit).34
// @complexity O(1) @alloc none @test CheatahX25519.Rfc7748Vector235
void cswap(Fe p, Fe q, i64 b) {36
const i64 mask = ~(b - 1);37
for (int i = 0; i < 16; ++i) {38
const i64 t = mask & (p[i] ^ q[i]);39
p[i] ^= t;40
q[i] ^= t;41
}42
}44
// o = a + b (no carry; callers carry after multiplication). @complexity O(1) @alloc none45
// @test CheatahX25519.Rfc7748Vector146
void add(Fe o, const Fe a, const Fe b) {47
for (int i = 0; i < 16; ++i) o[i] = a[i] + b[i];48
}50
// o = a - b. @complexity O(1) @alloc none @test CheatahX25519.Rfc7748Vector151
void sub(Fe o, const Fe a, const Fe b) {52
for (int i = 0; i < 16; ++i) o[i] = a[i] - b[i];53
}55
// o = a * b mod p: schoolbook product, fold the high half back with *38, then two carry56
// passes restore the limb bounds. @complexity O(1) @alloc none @test CheatahX25519.Rfc7748Vector157
void mul(Fe o, const Fe a, const Fe b) {58
i64 t[31] = {0};59
for (int i = 0; i < 16; ++i)60
for (int j = 0; j < 16; ++j) t[i + j] += a[i] * b[j];61
for (int i = 0; i < 15; ++i) t[i] += 38 * t[i + 16];62
for (int i = 0; i < 16; ++i) o[i] = t[i];63
carry(o);64
carry(o);65
}67
// o = a^2 mod p. @complexity O(1) @alloc none @test CheatahX25519.Rfc7748Vector168
void sqr(Fe o, const Fe a) { mul(o, a, a); }70
// o = z^-1 mod p by Fermat: z^(p-2), the standard fixed square-and-multiply chain (the two71
// skipped indices 2 and 4 encode p-2's binary form). Constant-time: fixed 254 iterations.72
// @complexity O(1) @alloc none @test CheatahX25519.DiffieHellman73
void invert(Fe o, const Fe z) {74
Fe c;75
for (int i = 0; i < 16; ++i) c[i] = z[i];76
for (int i = 253; i >= 0; --i) {77
sqr(c, c);78
if (i != 2 && i != 4) mul(c, c, z);79
}80
for (int i = 0; i < 16; ++i) o[i] = c[i];81
}83
// Freeze to the canonical representative in [0, p) and serialize to 32 little-endian bytes.84
// The two trial subtractions of p use arithmetic selection (no data-dependent branch).85
// @complexity O(1) @alloc none @test CheatahX25519.Rfc7748Vector186
void pack(unsigned char out[32], const Fe n) {87
Fe t, m;88
for (int i = 0; i < 16; ++i) t[i] = n[i];89
carry(t);90
carry(t);91
carry(t);92
for (int rep = 0; rep < 2; ++rep) {93
m[0] = t[0] - 0xffed;94
for (int i = 1; i < 15; ++i) {95
m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1);96
m[i - 1] &= 0xffff;97
}98
m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);99
const i64 borrow = (m[15] >> 16) & 1;100
m[14] &= 0xffff;101
cswap(t, m, 1 - borrow);102
}103
for (std::size_t i = 0; i < 16; ++i) {104
out[2 * i] = static_cast<unsigned char>(t[i] & 0xff);105
out[2 * i + 1] = static_cast<unsigned char>(t[i] >> 8);106
}107
}109
// Parse 32 little-endian bytes into limbs; the top bit is MASKED OFF per RFC 7748.110
// @complexity O(1) @alloc none @test CheatahX25519.Rfc7748Vector1111
void unpack(Fe o, const unsigned char in[32]) {112
for (std::size_t i = 0; i < 16; ++i) o[i] = in[2 * i] + (static_cast<i64>(in[2 * i + 1]) << 8);113
o[15] &= 0x7fff;114
}116
// The X25519 scalar multiplication: clamp the scalar, then 255 Montgomery-ladder steps over117
// the u-coordinate only (x/z pairs), each step one cswap + the fixed add/sub/mul schedule118
// with a24 = 121665. @complexity O(1) @alloc none @test CheatahX25519.Rfc7748Vector1119
void scalarmult(unsigned char out[32], const unsigned char scalar[32], const unsigned char point[32]) {120
unsigned char z[32];121
for (int i = 0; i < 32; ++i) z[i] = scalar[i];122
z[0] &= 248; // clamp: clear the low 3 bits (cofactor),123
z[31] &= 127; // clear the top bit,124
z[31] |= 64; // set bit 254.126
Fe x, a, b, c, d, e, f;127
static const Fe k121665 = {0xDB41, 1};128
unpack(x, point);129
for (int i = 0; i < 16; ++i) {130
b[i] = x[i];131
a[i] = c[i] = d[i] = 0;132
}133
a[0] = d[0] = 1;135
for (int i = 254; i >= 0; --i) {136
const i64 bit = (z[i >> 3] >> (i & 7)) & 1;137
cswap(a, b, bit);138
cswap(c, d, bit);139
add(e, a, c);140
sub(a, a, c);141
add(c, b, d);142
sub(b, b, d);143
sqr(d, e);144
sqr(f, a);145
mul(a, c, a);146
mul(c, b, e);147
add(e, a, c);148
sub(a, a, c);149
sqr(b, a);150
sub(c, d, f);151
mul(a, c, k121665);152
add(a, a, d);153
mul(c, c, a);154
mul(a, d, f);155
mul(d, b, x);156
sqr(b, e);157
cswap(a, b, bit);158
cswap(c, d, bit);159
}160
invert(c, c);161
mul(a, a, c);162
pack(out, a);163
}165
// ---- byte/hex helpers (the ed25519 module's conventions) ----167
// 64-char lowercase/uppercase hex -> 32 bytes; false on any malformed input.168
// @complexity O(1) @alloc none @test CheatahX25519.RejectsMalformed169
bool hex32(std::string_view hex, unsigned char out[32]) {170
if (hex.size() != 64) return false;171
for (int i = 0; i < 32; ++i) {172
unsigned v = 0;173
for (int k = 0; k < 2; ++k) {174
const char ch = hex[2 * i + k];175
v <<= 4;176
if (ch >= '0' && ch <= '9') v |= static_cast<unsigned>(ch - '0');177
else if (ch >= 'a' && ch <= 'f') v |= static_cast<unsigned>(ch - 'a' + 10);178
else if (ch >= 'A' && ch <= 'F') v |= static_cast<unsigned>(ch - 'A' + 10);179
else return false;180
}181
out[i] = static_cast<unsigned char>(v);182
}183
return true;184
}186
// 32 bytes -> 64-char lowercase hex. @complexity O(1) @alloc the returned string187
// @test CheatahX25519.Rfc7748Vector1188
std::string hex_of(const unsigned char in[32]) {189
static constexpr char kHex[] = "0123456789abcdef";190
std::string out;191
out.resize(64);192
for (std::size_t i = 0; i < 32; ++i) {193
out[2 * i] = kHex[in[i] >> 4];194
out[2 * i + 1] = kHex[in[i] & 0xF];195
}196
return out;197
}199
} // namespace201
std::string x25519(std::string_view scalar_hex, std::string_view point_hex) {202
unsigned char scalar[32], point[32], out[32];203
if (!hex32(scalar_hex, scalar) || !hex32(point_hex, point)) return "";204
scalarmult(out, scalar, point);205
unsigned char acc = 0; // contributory check: an all-zero shared secret is rejected206
for (unsigned char i : out) acc |= i;207
if (acc == 0) return "";208
return hex_of(out);209
}211
std::string x25519_base(std::string_view scalar_hex) {212
unsigned char scalar[32], out[32];213
if (!hex32(scalar_hex, scalar)) return "";214
static const unsigned char kBase[32] = {9};215
scalarmult(out, scalar, kBase);216
return hex_of(out);217
}219
} // namespace cheatah::x25519