Source
stdlib/aead/aead.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 "aead.hpp"5
#include <cstdint>6
#include <cstring>8
#if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)9
# include <strings.h> // explicit_bzero10
#endif12
#include "aes_gcm_ni.hpp" // AES-NI + PCLMULQDQ fast path for AES-128-GCM (runtime-dispatched)14
// ChaCha20-Poly1305 AEAD (RFC 8439) from scratch. ChaCha20 is the 20-round ARX block15
// function keyed per RFC; Poly1305 is the one-time authenticator over r,s derived from16
// block 0. The AEAD construction MACs aad || pad || ciphertext || pad || lengths and17
// appends the 16-byte tag. The tag comparison on decrypt is constant-time.19
namespace cheatah::aead {20
namespace {22
using u32 = std::uint32_t;23
using u64 = std::uint64_t;25
/**26
* Erase secret bytes so they cannot outlive their use — and do it in a form the optimizer is not27
* permitted to delete.28
*29
* A plain `std::memset` over a local that is never read again is a dead store, and compilers really30
* do remove it; the expanded key would then stay on the stack for a core dump or ordinary stack31
* reuse to surface. Each platform spells the un-removable version differently, so this picks one:32
*33
* - `explicit_bzero` where the platform has it (glibc ≥ 2.25 and the BSDs).34
* - a `volatile` store loop everywhere else. `volatile` forbids eliding the writes, so this is35
* correct on any conforming compiler with no platform support whatsoever.36
*37
* Apple deliberately gets the second path rather than `memset_s`: that function is only declared38
* when `__STDC_WANT_LIB_EXT1__` is defined to 1 BEFORE the first `<string.h>` in the translation39
* unit, which is a fragile thing to depend on in a file that includes other headers — and it is40
* exactly what the first attempt at this fix got wrong ("no member named 'memset_s' in the global41
* namespace", caught by the macOS CI in 39 seconds). Wiping 32 bytes is not worth a platform axis.42
*/43
void secure_wipe(void* p, std::size_t n) {44
#if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)45
::explicit_bzero(p, n);46
#else47
volatile auto* q = static_cast<volatile unsigned char*>(p);48
for (std::size_t i = 0; i < n; ++i) q[i] = 0;49
#endif50
}52
u32 rotl(u32 x, int n) { return (x << n) | (x >> (32 - n)); }54
// One ChaCha quarter round on four state words. @complexity O(1) @alloc none55
// @test CheatahAead.Rfc8439Encrypt56
void quarter(u32& a, u32& b, u32& c, u32& d) {57
a += b; d ^= a; d = rotl(d, 16);58
c += d; b ^= c; b = rotl(b, 12);59
a += b; d ^= a; d = rotl(d, 8);60
c += d; b ^= c; b = rotl(b, 7);61
}63
// The ChaCha20 block function: 64 bytes of keystream for (key, counter, nonce).64
// @complexity O(1) — 20 rounds @alloc none @test CheatahAead.Rfc8439Encrypt65
void chacha_block(const u32 key[8], u32 counter, const u32 nonce[3], unsigned char out[64]) {66
u32 s[16] = {0x61707865, 0x3320646e, 0x79622d32, 0x6b206574, // "expand 32-byte k"67
key[0], key[1], key[2], key[3], key[4], key[5], key[6], key[7],68
counter, nonce[0], nonce[1], nonce[2]};69
u32 w[16];70
std::memcpy(w, s, sizeof w);71
for (int round = 0; round < 10; ++round) { // 10 double rounds = 20 rounds72
quarter(w[0], w[4], w[8], w[12]);73
quarter(w[1], w[5], w[9], w[13]);74
quarter(w[2], w[6], w[10], w[14]);75
quarter(w[3], w[7], w[11], w[15]);76
quarter(w[0], w[5], w[10], w[15]);77
quarter(w[1], w[6], w[11], w[12]);78
quarter(w[2], w[7], w[8], w[13]);79
quarter(w[3], w[4], w[9], w[14]);80
}81
for (int i = 0; i < 16; ++i) {82
const u32 v = w[i] + s[i];83
out[4 * i] = static_cast<unsigned char>(v);84
out[4 * i + 1] = static_cast<unsigned char>(v >> 8);85
out[4 * i + 2] = static_cast<unsigned char>(v >> 16);86
out[4 * i + 3] = static_cast<unsigned char>(v >> 24);87
}88
}90
// XOR `data` with the ChaCha20 keystream starting at block `counter0`.91
// @complexity O(n) @alloc the returned string @test CheatahAead.Rfc8439Encrypt92
std::string chacha_xor(const u32 key[8], const u32 nonce[3], u32 counter0, std::string_view data) {93
std::string out(data);94
unsigned char block[64];95
for (std::size_t off = 0; off < out.size(); off += 64) {96
chacha_block(key, counter0 + static_cast<u32>(off / 64), nonce, block);97
const std::size_t n = std::min<std::size_t>(64, out.size() - off);98
for (std::size_t i = 0; i < n; ++i) {99
out[off + i] = static_cast<char>(static_cast<unsigned char>(out[off + i]) ^ block[i]);100
}101
}102
return out;103
}105
// Poly1305, INCREMENTAL: keyed by (r, s) from ChaCha block 0, then fed 16-byte blocks. Splitting106
// the one-shot form into init/block/finish lets a caller authenticate a message that arrives in107
// pieces WITHOUT first concatenating it into one buffer — which is what makes the allocation-free108
// AEAD path below possible (the AEAD MAC input is aad || pad || ct || pad || lengths, and every109
// segment is 16-byte aligned, so no block ever straddles two segments).110
// 26-bit limbs in u64 lanes — the standard portable shape. @complexity O(n) @alloc none111
// @test CheatahAead.Rfc8439Encrypt / CheatahAead.IntoFormsMatchStringForms112
struct Poly1305 {113
u32 r0, r1, r2, r3, r4;114
u32 s1, s2, s3, s4;115
u32 h0 = 0, h1 = 0, h2 = 0, h3 = 0, h4 = 0;116
unsigned char rs_copy[32];118
// Load and clamp r (RFC 8439 §2.5) and keep s for the final addition.119
void init(const unsigned char rs[32]) {120
std::memcpy(rs_copy, rs, 32);121
u32 t[4];122
std::memcpy(t, rs, 16);123
t[0] &= 0x0fffffff; t[1] &= 0x0ffffffc; t[2] &= 0x0ffffffc; t[3] &= 0x0ffffffc;124
r0 = t[0] & 0x3ffffff;125
r1 = ((t[0] >> 26) | (t[1] << 6)) & 0x3ffffff;126
r2 = ((t[1] >> 20) | (t[2] << 12)) & 0x3ffffff;127
r3 = ((t[2] >> 14) | (t[3] << 18)) & 0x3ffffff;128
r4 = (t[3] >> 8) & 0x3ffffff;129
s1 = r1 * 5; s2 = r2 * 5; s3 = r3 * 5; s4 = r4 * 5;130
h0 = h1 = h2 = h3 = h4 = 0;131
}133
// Absorb ONE block: @p n bytes (n <= 16) zero-padded, with the high bit set per the RFC.134
void block(const unsigned char* data, std::size_t n) {135
unsigned char blk[17] = {0};136
std::memcpy(blk, data, n);137
blk[n] = 1;138
u32 t[4];139
std::memcpy(t, blk, 16);140
h0 += t[0] & 0x3ffffff;141
h1 += ((t[0] >> 26) | (t[1] << 6)) & 0x3ffffff;142
h2 += ((t[1] >> 20) | (t[2] << 12)) & 0x3ffffff;143
h3 += ((t[2] >> 14) | (t[3] << 18)) & 0x3ffffff;144
h4 += (t[3] >> 8) | (static_cast<u32>(blk[16]) << 24);146
const u64 d0 = (u64)h0 * r0 + (u64)h1 * s4 + (u64)h2 * s3 + (u64)h3 * s2 + (u64)h4 * s1;147
const u64 d1 = (u64)h0 * r1 + (u64)h1 * r0 + (u64)h2 * s4 + (u64)h3 * s3 + (u64)h4 * s2;148
const u64 d2 = (u64)h0 * r2 + (u64)h1 * r1 + (u64)h2 * r0 + (u64)h3 * s4 + (u64)h4 * s3;149
const u64 d3 = (u64)h0 * r3 + (u64)h1 * r2 + (u64)h2 * r1 + (u64)h3 * r0 + (u64)h4 * s4;150
u64 d4 = (u64)h0 * r4 + (u64)h1 * r3 + (u64)h2 * r2 + (u64)h3 * r1 + (u64)h4 * r0;152
u64 c = d0 >> 26; h0 = d0 & 0x3ffffff;153
const u64 e1 = d1 + c; c = e1 >> 26; h1 = e1 & 0x3ffffff;154
const u64 e2 = d2 + c; c = e2 >> 26; h2 = e2 & 0x3ffffff;155
const u64 e3 = d3 + c; c = e3 >> 26; h3 = e3 & 0x3ffffff;156
d4 += c; c = d4 >> 26; h4 = d4 & 0x3ffffff;157
h0 += static_cast<u32>(c * 5); c = h0 >> 26; h0 &= 0x3ffffff;158
h1 += static_cast<u32>(c);159
}161
// Absorb a whole segment plus its zero padding to a 16-byte boundary (the AEAD shape).162
void segment_padded(const unsigned char* data, std::size_t len) {163
std::size_t pos = 0;164
while (pos + 16 <= len) { block(data + pos, 16); pos += 16; }165
if (pos < len) {166
unsigned char pad[16] = {0};167
std::memcpy(pad, data + pos, len - pos);168
block(pad, 16); // the AEAD pads to a full block (NOT the one-shot partial rule)169
}170
}172
void finish(unsigned char tag[16]);173
};175
void poly1305_state_finish(Poly1305& st, unsigned char tag[16]);177
void Poly1305::finish(unsigned char tag[16]) { poly1305_state_finish(*this, tag); }179
// The one-shot form, now expressed through the incremental core so both paths are provably the180
// same arithmetic. Keeps the RFC's partial-final-block rule (pad with zeros, high bit after the181
// last real byte) which differs from the AEAD's whole-block padding.182
void poly1305(const unsigned char rs[32], std::string_view msg, unsigned char tag[16]) {183
Poly1305 st;184
st.init(rs);185
std::size_t pos = 0;186
while (pos < msg.size()) {187
const std::size_t n = std::min<std::size_t>(16, msg.size() - pos);188
st.block(reinterpret_cast<const unsigned char*>(msg.data()) + pos, n);189
pos += n;190
}191
st.finish(tag);192
}194
void poly1305_state_finish(Poly1305& state, unsigned char tag[16]) {195
const unsigned char* rs = state.rs_copy;196
u32 h0 = state.h0, h1 = state.h1, h2 = state.h2, h3 = state.h3, h4 = state.h4;197
// final reduction mod 2^130 - 5, then the trial subtraction (constant-time select)198
u32 c = h1 >> 26; h1 &= 0x3ffffff; h2 += c;199
c = h2 >> 26; h2 &= 0x3ffffff; h3 += c;200
c = h3 >> 26; h3 &= 0x3ffffff; h4 += c;201
c = h4 >> 26; h4 &= 0x3ffffff; h0 += c * 5;202
c = h0 >> 26; h0 &= 0x3ffffff; h1 += c;204
u32 g0 = h0 + 5; c = g0 >> 26; g0 &= 0x3ffffff;205
u32 g1 = h1 + c; c = g1 >> 26; g1 &= 0x3ffffff;206
u32 g2 = h2 + c; c = g2 >> 26; g2 &= 0x3ffffff;207
u32 g3 = h3 + c; c = g3 >> 26; g3 &= 0x3ffffff;208
const u32 g4 = h4 + c - (1u << 26);210
const u32 mask = (g4 >> 31) - 1; // all-ones when h >= p (take g), zero otherwise211
h0 = (h0 & ~mask) | (g0 & mask);212
h1 = (h1 & ~mask) | (g1 & mask);213
h2 = (h2 & ~mask) | (g2 & mask);214
h3 = (h3 & ~mask) | (g3 & mask);215
h4 = (h4 & ~mask) | (g4 & mask);217
// h += s (the second 16 bytes of rs), little-endian, then serialize218
const u64 f0 = ((h0) | (h1 << 26)) & 0xffffffffull;219
const u64 f1 = ((h1 >> 6) | (h2 << 20)) & 0xffffffffull;220
const u64 f2 = ((h2 >> 12) | (h3 << 14)) & 0xffffffffull;221
const u64 f3 = ((h3 >> 18) | (h4 << 8)) & 0xffffffffull;222
u32 s_part[4];223
std::memcpy(s_part, rs + 16, 16);224
const u64 f[4] = {f0, f1, f2, f3};225
u64 carry_word = 0;226
for (int i = 0; i < 4; ++i) {227
const u64 sum = f[i] + s_part[i] + carry_word; // 32-bit lanes with carry between them228
carry_word = sum >> 32;229
tag[4 * i] = static_cast<unsigned char>(sum);230
tag[4 * i + 1] = static_cast<unsigned char>(sum >> 8);231
tag[4 * i + 2] = static_cast<unsigned char>(sum >> 16);232
tag[4 * i + 3] = static_cast<unsigned char>(sum >> 24);233
}234
}236
// Assemble the AEAD MAC input (aad || pad16 || ct || pad16 || len(aad) || len(ct)) and tag it.237
// @complexity O(n) @alloc the assembled buffer @test CheatahAead.Rfc8439Encrypt238
void aead_tag(const u32 key[8], const u32 nonce[3], std::string_view aad, std::string_view ct,239
unsigned char tag[16]) {240
unsigned char block0[64];241
chacha_block(key, 0, nonce, block0); // rs = the first 32 bytes of block 0243
std::string mac_input;244
mac_input.reserve(aad.size() + ct.size() + 32);245
mac_input.append(aad);246
mac_input.append((16 - aad.size() % 16) % 16, '\0');247
mac_input.append(ct);248
mac_input.append((16 - ct.size() % 16) % 16, '\0');249
unsigned char lens[16];250
const u64 alen = aad.size(), clen = ct.size();251
for (int i = 0; i < 8; ++i) {252
lens[i] = static_cast<unsigned char>(alen >> (8 * i));253
lens[8 + i] = static_cast<unsigned char>(clen >> (8 * i));254
}255
mac_input.append(reinterpret_cast<const char*>(lens), 16);256
poly1305(block0, mac_input, tag);257
}259
// ChaCha20 keystream XOR into a CALLER buffer — the allocation-free twin of chacha_xor. in/out may260
// alias (encrypt in place). @complexity O(n) @alloc none @test CheatahAead.IntoFormsMatchStringForms261
void chacha_xor_into(const u32 key[8], const u32 nonce[3], u32 counter0, const unsigned char* in,262
std::size_t len, unsigned char* out) {263
unsigned char block[64];264
for (std::size_t off = 0; off < len; off += 64) {265
chacha_block(key, counter0 + static_cast<u32>(off / 64), nonce, block);266
const std::size_t n = std::min<std::size_t>(64, len - off);267
for (std::size_t i = 0; i < n; ++i) out[off + i] = static_cast<unsigned char>(in[off + i] ^ block[i]);268
}269
}271
// The AEAD tag WITHOUT assembling the MAC input: feed aad, ciphertext and the length block straight272
// into the incremental Poly1305. Same arithmetic as aead_tag, no buffer.273
// @complexity O(|aad| + |ct|) @alloc none @test CheatahAead.IntoFormsMatchStringForms274
void aead_tag_into(const u32 key[8], const u32 nonce[3], const unsigned char* aad, std::size_t aad_len,275
const unsigned char* ct, std::size_t ct_len, unsigned char tag[16]) {276
unsigned char block0[64];277
chacha_block(key, 0, nonce, block0);278
Poly1305 st;279
st.init(block0);280
st.segment_padded(aad, aad_len);281
st.segment_padded(ct, ct_len);282
unsigned char lens[16];283
const u64 alen = aad_len, clen = ct_len;284
for (int i = 0; i < 8; ++i) {285
lens[i] = static_cast<unsigned char>(alen >> (8 * i));286
lens[8 + i] = static_cast<unsigned char>(clen >> (8 * i));287
}288
st.block(lens, 16);289
st.finish(tag);290
}292
// hex -> n bytes (false on malformed). @complexity O(n) @alloc none @test CheatahAead.RejectsTamper293
bool hex_bytes(std::string_view hex, unsigned char* out, std::size_t n) {294
if (hex.size() != 2 * n) return false;295
for (std::size_t i = 0; i < n; ++i) {296
unsigned v = 0;297
for (int k = 0; k < 2; ++k) {298
const char ch = hex[2 * i + k];299
v <<= 4;300
if (ch >= '0' && ch <= '9') v |= static_cast<unsigned>(ch - '0');301
else if (ch >= 'a' && ch <= 'f') v |= static_cast<unsigned>(ch - 'a' + 10);302
else if (ch >= 'A' && ch <= 'F') v |= static_cast<unsigned>(ch - 'A' + 10);303
else return false;304
}305
out[i] = static_cast<unsigned char>(v);306
}307
return true;308
}310
bool load_key_nonce(std::string_view key_hex, std::string_view nonce_hex, u32 key[8], u32 nonce[3]) {311
unsigned char kb[32], nb[12];312
if (!hex_bytes(key_hex, kb, 32) || !hex_bytes(nonce_hex, nb, 12)) return false;313
std::memcpy(key, kb, 32); // little-endian words per RFC 8439314
std::memcpy(nonce, nb, 12);315
return true;316
}318
// ===================== AES-128-GCM (TLS_AES_128_GCM_SHA256) =====================319
// AES-128 (encrypt only — GCM never AES-decrypts) + GHASH over GF(2^128) + GCM mode. Byte-oriented320
// and correctness-first (no T-tables); the record cipher is exercised once per TLS record.322
// The AES S-box (FIPS-197).323
const unsigned char kSbox[256] = {324
0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,325
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,326
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,327
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,328
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,329
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,330
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,331
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,332
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,333
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,334
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,335
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,336
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,337
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,338
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,339
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16};341
// Expand a 16-byte AES-128 key into 11 round keys (176 bytes). State/round-key byte layout is342
// column-major: byte (row r, col c) at index 4*c + r.343
void aes128_key_expand(const unsigned char key[16], unsigned char rk[176]) {344
static const unsigned char rcon[10] = {0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36};345
std::memcpy(rk, key, 16);346
int r = 0;347
for (int i = 16; i < 176; i += 4) {348
unsigned char t[4] = {rk[i - 4], rk[i - 3], rk[i - 2], rk[i - 1]};349
if (i % 16 == 0) { // RotWord + SubWord + Rcon on the first word of each round key350
const unsigned char a0 = t[0];351
t[0] = static_cast<unsigned char>(kSbox[t[1]] ^ rcon[r++]);352
t[1] = kSbox[t[2]];353
t[2] = kSbox[t[3]];354
t[3] = kSbox[a0];355
}356
for (int j = 0; j < 4; ++j) rk[i + j] = static_cast<unsigned char>(rk[i - 16 + j] ^ t[j]);357
}358
}360
// Expand a 32-byte AES-256 key into 15 round keys (240 bytes). Nk = 8 words: every 8 words apply361
// RotWord+SubWord+Rcon to the first word, and — the AES-256 extra step — a plain SubWord to the 4th362
// word of each 8-word span (FIPS-197 §5.2).363
void aes256_key_expand(const unsigned char key[32], unsigned char rk[240]) {364
static const unsigned char rcon[7] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40};365
std::memcpy(rk, key, 32);366
int r = 0;367
for (int i = 32; i < 240; i += 4) {368
unsigned char t[4] = {rk[i - 4], rk[i - 3], rk[i - 2], rk[i - 1]};369
if (i % 32 == 0) { // RotWord + SubWord + Rcon370
const unsigned char a0 = t[0];371
t[0] = static_cast<unsigned char>(kSbox[t[1]] ^ rcon[r++]);372
t[1] = kSbox[t[2]];373
t[2] = kSbox[t[3]];374
t[3] = kSbox[a0];375
} else if (i % 32 == 16) { // SubWord only (AES-256-specific)376
for (int j = 0; j < 4; ++j) t[j] = kSbox[t[j]];377
}378
for (int j = 0; j < 4; ++j) rk[i + j] = static_cast<unsigned char>(rk[i - 32 + j] ^ t[j]);379
}380
}382
unsigned char xtime(unsigned char x) {383
return static_cast<unsigned char>((x << 1) ^ ((x >> 7) * 0x1b)); // ·2 in GF(2^8)384
}386
// Encrypt one 16-byte block with @p nr rounds (nr = 10 for AES-128, 14 for AES-256). @p rk holds387
// 16*(nr+1) round-key bytes.388
void aes_encrypt_block(const unsigned char* rk, int nr, const unsigned char in[16],389
unsigned char out[16]) {390
unsigned char s[16];391
for (int i = 0; i < 16; ++i) s[i] = static_cast<unsigned char>(in[i] ^ rk[i]); // round 0392
for (int round = 1; round <= nr; ++round) {393
for (int i = 0; i < 16; ++i) s[i] = kSbox[s[i]]; // SubBytes394
unsigned char t; // ShiftRows395
t = s[1]; s[1] = s[5]; s[5] = s[9]; s[9] = s[13]; s[13] = t;396
t = s[2]; s[2] = s[10]; s[10] = t; t = s[6]; s[6] = s[14]; s[14] = t;397
t = s[15]; s[15] = s[11]; s[11] = s[7]; s[7] = s[3]; s[3] = t;398
if (round != nr) { // MixColumns399
for (int c = 0; c < 4; ++c) {400
unsigned char* col = s + 4 * c;401
const unsigned char a0 = col[0], a1 = col[1], a2 = col[2], a3 = col[3];402
col[0] = static_cast<unsigned char>(xtime(a0) ^ (xtime(a1) ^ a1) ^ a2 ^ a3);403
col[1] = static_cast<unsigned char>(a0 ^ xtime(a1) ^ (xtime(a2) ^ a2) ^ a3);404
col[2] = static_cast<unsigned char>(a0 ^ a1 ^ xtime(a2) ^ (xtime(a3) ^ a3));405
col[3] = static_cast<unsigned char>((xtime(a0) ^ a0) ^ a1 ^ a2 ^ xtime(a3));406
}407
}408
const unsigned char* r_k = rk + 16 * round; // AddRoundKey409
for (int i = 0; i < 16; ++i) s[i] = static_cast<unsigned char>(s[i] ^ r_k[i]);410
}411
std::memcpy(out, s, 16);412
}414
// GF(2^128) multiply (SP 800-38D): out = X · Y, big-endian bit order, reduction poly R = 0xe1<<120.415
void gf_mult(const unsigned char X[16], const unsigned char Y[16], unsigned char out[16]) {416
unsigned char Z[16] = {0}, V[16];417
std::memcpy(V, Y, 16);418
for (int i = 0; i < 128; ++i) {419
if ((X[i / 8] >> (7 - (i % 8))) & 1)420
for (int j = 0; j < 16; ++j) Z[j] ^= V[j];421
const unsigned char lsb = V[15] & 1;422
for (int j = 15; j > 0; --j) V[j] = static_cast<unsigned char>((V[j] >> 1) | ((V[j - 1] & 1) << 7));423
V[0] >>= 1;424
if (lsb) V[0] ^= 0xe1;425
}426
std::memcpy(out, Z, 16);427
}429
// GHASH-accumulate the zero-padded `data` into Y (Y := (Y XOR block)·H per 16-byte block).430
void ghash_blocks(unsigned char Y[16], const unsigned char H[16], const unsigned char* p,431
std::size_t n) {432
for (std::size_t off = 0; off < n; off += 16) {433
unsigned char b[16] = {0};434
const std::size_t m = std::min<std::size_t>(16, n - off);435
std::memcpy(b, p + off, m);436
for (int i = 0; i < 16; ++i) Y[i] ^= b[i];437
unsigned char t[16];438
gf_mult(Y, H, t);439
std::memcpy(Y, t, 16);440
}441
}443
// The GCM tag: GHASH(AAD || pad || C || pad || [len(AAD)bits]_64 || [len(C)bits]_64) XOR E(J0).444
void gcm_tag(const unsigned char* rk, int nr, const unsigned char H[16], const unsigned char J0[16],445
std::string_view aad, std::string_view ct, unsigned char tag[16]) {446
unsigned char Y[16] = {0};447
ghash_blocks(Y, H, reinterpret_cast<const unsigned char*>(aad.data()), aad.size());448
ghash_blocks(Y, H, reinterpret_cast<const unsigned char*>(ct.data()), ct.size());449
unsigned char lb[16] = {0};450
const u64 abits = static_cast<u64>(aad.size()) * 8, cbits = static_cast<u64>(ct.size()) * 8;451
for (int i = 0; i < 8; ++i) {452
lb[7 - i] = static_cast<unsigned char>(abits >> (8 * i));453
lb[15 - i] = static_cast<unsigned char>(cbits >> (8 * i));454
}455
for (int i = 0; i < 16; ++i) Y[i] ^= lb[i];456
unsigned char t[16];457
gf_mult(Y, H, t);458
unsigned char ej0[16];459
aes_encrypt_block(rk, nr, J0, ej0);460
for (int i = 0; i < 16; ++i) tag[i] = static_cast<unsigned char>(t[i] ^ ej0[i]);461
}463
void inc32(unsigned char ctr[16]) { // increment the rightmost 32 bits (big-endian)464
for (int j = 15; j >= 12; --j)465
if (++ctr[j] != 0) break;466
}468
// GCTR: XOR `data` in place with the AES-CTR keystream starting at counter `ctr` (advanced).469
void gctr(const unsigned char* rk, int nr, unsigned char ctr[16], std::string& data) {470
unsigned char ks[16];471
for (std::size_t off = 0; off < data.size(); off += 16) {472
aes_encrypt_block(rk, nr, ctr, ks);473
const std::size_t n = std::min<std::size_t>(16, data.size() - off);474
for (std::size_t i = 0; i < n; ++i)475
data[off + i] = static_cast<char>(static_cast<unsigned char>(data[off + i]) ^ ks[i]);476
inc32(ctr);477
}478
}480
// Force the portable (non-AES-NI) AES-GCM path — a testing/determinism hook so the scalar481
// reference is exercised even on CPUs where the hardware path is the default.482
bool g_force_portable = false;484
} // namespace486
/**487
* Pin (or release) the portable scalar AES-GCM path — the test/determinism hook whose full488
* contract lives on the declaration in aead.hpp (kept out of the public docs via \\cond there).489
* @param on true to pin the portable scalar path; false to allow the hardware path again.490
* @complexity O(1).491
* @alloc none.492
* @test CheatahAead.AesGcmPortableMatchesHardware493
*/494
void set_force_portable_crypto(bool on) { g_force_portable = on; }496
namespace {497
bool aes_gcm_use_hw() { return accel::available() && !g_force_portable; }498
} // namespace500
bool crypto_hardware_active() { return aes_gcm_use_hw(); }502
// A single message this large would wrap the 32-bit block counter — ChaCha20's block index or503
// GCM's CTR — reusing keystream (and, for GCM, the E(J0) tag mask) WITHIN the one message, which504
// breaks confidentiality/integrity. The binding limit is GCM's 2^32 16-byte blocks (~64 GiB); cap505
// both constructions there. It is unreachable in practice (no TLS record is remotely this large,506
// and the message must fit in memory), but bounds the primitive against misuse. Cross-message nonce507
// uniqueness remains the caller's responsibility, as documented.508
constexpr std::uint64_t kMaxAeadMessage = std::uint64_t{1} << 36; // 64 GiB509
/**510
* Whether one AEAD message is under the 64 GiB counter-wrap cap above. Every encrypt/decrypt511
* checks it; the over-cap branch is unreachable in a test (the message would not fit in memory).512
* @param msg the plaintext or ciphertext.513
* @return true iff @p msg is within the single-message limit.514
* @complexity O(1).515
* @alloc none.516
* @test CheatahAead.Rfc8439Encrypt517
*/518
inline bool aead_len_ok(std::string_view msg) {519
return static_cast<std::uint64_t>(msg.size()) <= kMaxAeadMessage;520
}522
std::string chacha20poly1305_encrypt(std::string_view key_hex, std::string_view nonce_hex,523
std::string_view aad, std::string_view plaintext) {524
u32 key[8], nonce[3];525
if (!load_key_nonce(key_hex, nonce_hex, key, nonce) || !aead_len_ok(plaintext)) return "";526
std::string ct = chacha_xor(key, nonce, 1, plaintext); // counter starts at 1 (0 keys the MAC)527
unsigned char tag[16];528
aead_tag(key, nonce, aad, ct, tag);529
ct.append(reinterpret_cast<const char*>(tag), 16);530
return ct;531
}533
bool chacha20poly1305_encrypt_into(const unsigned char key[32], const unsigned char nonce[12],534
const unsigned char* aad, std::size_t aad_len,535
const unsigned char* plaintext, std::size_t plaintext_len,536
unsigned char* out) {537
if (key == nullptr || nonce == nullptr || out == nullptr ||538
(plaintext == nullptr && plaintext_len != 0) || (aad == nullptr && aad_len != 0) ||539
static_cast<std::uint64_t>(plaintext_len) > kMaxAeadMessage) {540
return false;541
}542
u32 k[8], n[3];543
std::memcpy(k, key, 32); // little-endian words per RFC 8439544
std::memcpy(n, nonce, 12);545
chacha_xor_into(k, n, 1, plaintext, plaintext_len, out); // counter 1 (0 keys the MAC)546
aead_tag_into(k, n, aad, aad_len, out, plaintext_len, out + plaintext_len);547
// Do not leave the expanded key on the stack: a later core dump, or ordinary stack reuse in a548
// process that keeps running, should not be able to surface it. See secure_wipe — a plain memset549
// to a dead local is legally removed by the optimizer, which is the whole point.550
secure_wipe(k, sizeof k);551
return true;552
}554
bool chacha20poly1305_decrypt_into(const unsigned char key[32], const unsigned char nonce[12],555
const unsigned char* aad, std::size_t aad_len,556
const unsigned char* ciphertext, std::size_t ciphertext_len,557
unsigned char* out) {558
if (key == nullptr || nonce == nullptr || ciphertext == nullptr || ciphertext_len < 16 ||559
(aad == nullptr && aad_len != 0) ||560
static_cast<std::uint64_t>(ciphertext_len) > kMaxAeadMessage) {561
return false;562
}563
const std::size_t ct_len = ciphertext_len - 16;564
if (out == nullptr && ct_len != 0) return false; // a tag-only message needs no out buffer565
u32 k[8], n[3];566
std::memcpy(k, key, 32);567
std::memcpy(n, nonce, 12);568
unsigned char tag[16];569
aead_tag_into(k, n, aad, aad_len, ciphertext, ct_len, tag);570
unsigned char diff = 0; // constant-time compare: never early-exit on a mismatching byte571
for (int i = 0; i < 16; ++i) diff |= tag[i] ^ ciphertext[ct_len + i];572
if (diff != 0) {573
secure_wipe(k, sizeof k);574
return false; // authentication failed: nothing is written to out575
}576
chacha_xor_into(k, n, 1, ciphertext, ct_len, out);577
secure_wipe(k, sizeof k);578
return true;579
}581
std::string chacha20poly1305_decrypt(std::string_view key_hex, std::string_view nonce_hex,582
std::string_view aad, std::string_view ciphertext) {583
u32 key[8], nonce[3];584
if (!load_key_nonce(key_hex, nonce_hex, key, nonce) || ciphertext.size() < 16 ||585
!aead_len_ok(ciphertext)) return "";586
const std::string_view ct = ciphertext.substr(0, ciphertext.size() - 16);587
const std::string_view given = ciphertext.substr(ciphertext.size() - 16);588
unsigned char tag[16];589
aead_tag(key, nonce, aad, ct, tag);590
unsigned char diff = 0; // constant-time compare: never early-exit on a mismatching byte591
for (int i = 0; i < 16; ++i) {592
diff |= tag[i] ^ static_cast<unsigned char>(given[i]);593
}594
if (diff != 0) return "";595
return chacha_xor(key, nonce, 1, ct);596
}598
namespace {599
// Portable AES-GCM encrypt over already-expanded round keys (@p nr rounds). Shared by AES-128/256.600
std::string gcm_encrypt_portable(const unsigned char* rk, int nr, const unsigned char nb[12],601
std::string_view aad, std::string_view plaintext) {602
unsigned char H[16], zero[16] = {0};603
aes_encrypt_block(rk, nr, zero, H); // hash subkey H = E(0)604
unsigned char J0[16] = {0};605
std::memcpy(J0, nb, 12);606
J0[15] = 1; // J0 = nonce || 0x00000001607
std::string ct(plaintext);608
unsigned char ctr[16];609
std::memcpy(ctr, J0, 16);610
inc32(ctr); // CTR starts at inc32(J0)611
gctr(rk, nr, ctr, ct);612
unsigned char tag[16];613
gcm_tag(rk, nr, H, J0, aad, ct, tag);614
ct.append(reinterpret_cast<const char*>(tag), 16);615
return ct;616
}618
// Portable AES-GCM decrypt (constant-time tag check; "" on mismatch). Shared by AES-128/256.619
std::string gcm_decrypt_portable(const unsigned char* rk, int nr, const unsigned char nb[12],620
std::string_view aad, std::string_view ciphertext) {621
unsigned char H[16], zero[16] = {0};622
aes_encrypt_block(rk, nr, zero, H);623
unsigned char J0[16] = {0};624
std::memcpy(J0, nb, 12);625
J0[15] = 1;626
const std::string_view ct = ciphertext.substr(0, ciphertext.size() - 16);627
const std::string_view given = ciphertext.substr(ciphertext.size() - 16);628
unsigned char tag[16];629
gcm_tag(rk, nr, H, J0, aad, ct, tag);630
unsigned char diff = 0; // constant-time tag compare631
for (int i = 0; i < 16; ++i) diff |= tag[i] ^ static_cast<unsigned char>(given[i]);632
if (diff != 0) return "";633
std::string pt(ct);634
unsigned char ctr[16];635
std::memcpy(ctr, J0, 16);636
inc32(ctr);637
gctr(rk, nr, ctr, pt);638
return pt;639
}640
} // namespace642
std::string aes128gcm_encrypt(std::string_view key_hex, std::string_view nonce_hex,643
std::string_view aad, std::string_view plaintext) {644
unsigned char kb[16], nb[12];645
if (!hex_bytes(key_hex, kb, 16) || !hex_bytes(nonce_hex, nb, 12) || !aead_len_ok(plaintext)) return "";646
if (aes_gcm_use_hw()) return accel::gcm_encrypt(kb, 16, nb, aad, plaintext);647
unsigned char rk[176];648
aes128_key_expand(kb, rk);649
return gcm_encrypt_portable(rk, 10, nb, aad, plaintext);650
}652
std::string aes128gcm_decrypt(std::string_view key_hex, std::string_view nonce_hex,653
std::string_view aad, std::string_view ciphertext) {654
unsigned char kb[16], nb[12];655
if (!hex_bytes(key_hex, kb, 16) || !hex_bytes(nonce_hex, nb, 12) || ciphertext.size() < 16 ||656
!aead_len_ok(ciphertext)) return "";657
if (aes_gcm_use_hw()) return accel::gcm_decrypt(kb, 16, nb, aad, ciphertext);658
unsigned char rk[176];659
aes128_key_expand(kb, rk);660
return gcm_decrypt_portable(rk, 10, nb, aad, ciphertext);661
}663
// AES-256-GCM — the record cipher of TLS_AES_256_GCM_SHA384. Same runtime dispatch as AES-128: the664
// AES-NI/PMULL hardware path when the CPU has it AND the power-on self-test reproduced the known-answer665
// vector (available()), otherwise the portable scalar reference. Both are KAT-tested and cross-checked666
// against each other (CheatahAead.Aes256GcmPortableMatchesHardware).667
std::string aes256gcm_encrypt(std::string_view key_hex, std::string_view nonce_hex,668
std::string_view aad, std::string_view plaintext) {669
unsigned char kb[32], nb[12];670
if (!hex_bytes(key_hex, kb, 32) || !hex_bytes(nonce_hex, nb, 12) || !aead_len_ok(plaintext)) return "";671
if (aes_gcm_use_hw()) return accel::gcm_encrypt(kb, 32, nb, aad, plaintext);672
unsigned char rk[240];673
aes256_key_expand(kb, rk);674
return gcm_encrypt_portable(rk, 14, nb, aad, plaintext);675
}677
std::string aes256gcm_decrypt(std::string_view key_hex, std::string_view nonce_hex,678
std::string_view aad, std::string_view ciphertext) {679
unsigned char kb[32], nb[12];680
if (!hex_bytes(key_hex, kb, 32) || !hex_bytes(nonce_hex, nb, 12) || ciphertext.size() < 16 ||681
!aead_len_ok(ciphertext)) return "";682
if (aes_gcm_use_hw()) return accel::gcm_decrypt(kb, 32, nb, aad, ciphertext);683
unsigned char rk[240];684
aes256_key_expand(kb, rk);685
return gcm_decrypt_portable(rk, 14, nb, aad, ciphertext);686
}688
} // namespace cheatah::aead