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 <cstddef>6
#include <cstdint>7
#include <cstring>9
#if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)10
# include <strings.h> // explicit_bzero11
#endif13
#include "aes_gcm_ni.hpp" // AES-NI + PCLMULQDQ fast path for AES-128-GCM (runtime-dispatched)15
// ChaCha20-Poly1305 AEAD (RFC 8439) from scratch. ChaCha20 is the 20-round ARX block16
// function keyed per RFC; Poly1305 is the one-time authenticator over r,s derived from17
// block 0. The AEAD construction MACs aad || pad || ciphertext || pad || lengths and18
// appends the 16-byte tag. The tag comparison on decrypt is constant-time.20
namespace cheatah::aead {21
namespace {23
using u32 = std::uint32_t;24
using u64 = std::uint64_t;26
/**27
* Erase secret bytes so they cannot outlive their use — and do it in a form the optimizer is not28
* permitted to delete.29
*30
* A plain `std::memset` over a local that is never read again is a dead store, and compilers really31
* do remove it; the expanded key would then stay on the stack for a core dump or ordinary stack32
* reuse to surface. Each platform spells the un-removable version differently, so this picks one:33
*34
* - `explicit_bzero` where the platform has it (glibc ≥ 2.25 and the BSDs).35
* - a `volatile` store loop everywhere else. `volatile` forbids eliding the writes, so this is36
* correct on any conforming compiler with no platform support whatsoever.37
*38
* Apple deliberately gets the second path rather than `memset_s`: that function is only declared39
* when `__STDC_WANT_LIB_EXT1__` is defined to 1 BEFORE the first `<string.h>` in the translation40
* unit, which is a fragile thing to depend on in a file that includes other headers — and it is41
* exactly what the first attempt at this fix got wrong ("no member named 'memset_s' in the global42
* namespace", caught by the macOS CI in 39 seconds). Wiping 32 bytes is not worth a platform axis.43
*/44
void secure_wipe(void* p, std::size_t n) {45
#if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)46
::explicit_bzero(p, n);47
#else48
volatile auto* q = static_cast<volatile unsigned char*>(p);49
for (std::size_t i = 0; i < n; ++i) q[i] = 0;50
#endif51
}53
u32 rotl(u32 x, int n) { return (x << n) | (x >> (32 - n)); }55
// One ChaCha quarter round on four state words. @complexity O(1) @alloc none56
// @test CheatahAead.Rfc8439Encrypt57
void quarter(u32& a, u32& b, u32& c, u32& d) {58
a += b; d ^= a; d = rotl(d, 16);59
c += d; b ^= c; b = rotl(b, 12);60
a += b; d ^= a; d = rotl(d, 8);61
c += d; b ^= c; b = rotl(b, 7);62
}64
// The ChaCha20 block function: 64 bytes of keystream for (key, counter, nonce).65
// @complexity O(1) — 20 rounds @alloc none @test CheatahAead.Rfc8439Encrypt66
void chacha_block(const u32 key[8], u32 counter, const u32 nonce[3], unsigned char out[64]) {67
u32 s[16] = {0x61707865, 0x3320646e, 0x79622d32, 0x6b206574, // "expand 32-byte k"68
key[0], key[1], key[2], key[3], key[4], key[5], key[6], key[7],69
counter, nonce[0], nonce[1], nonce[2]};70
u32 w[16];71
std::memcpy(w, s, sizeof w);72
for (int round = 0; round < 10; ++round) { // 10 double rounds = 20 rounds73
quarter(w[0], w[4], w[8], w[12]);74
quarter(w[1], w[5], w[9], w[13]);75
quarter(w[2], w[6], w[10], w[14]);76
quarter(w[3], w[7], w[11], w[15]);77
quarter(w[0], w[5], w[10], w[15]);78
quarter(w[1], w[6], w[11], w[12]);79
quarter(w[2], w[7], w[8], w[13]);80
quarter(w[3], w[4], w[9], w[14]);81
}82
for (std::size_t i = 0; i < 16; ++i) {83
const u32 v = w[i] + s[i];84
out[4 * i] = static_cast<unsigned char>(v);85
out[4 * i + 1] = static_cast<unsigned char>(v >> 8);86
out[4 * i + 2] = static_cast<unsigned char>(v >> 16);87
out[4 * i + 3] = static_cast<unsigned char>(v >> 24);88
}89
}91
// XOR `data` with the ChaCha20 keystream starting at block `counter0`.92
// @complexity O(n) @alloc the returned string @test CheatahAead.Rfc8439Encrypt93
std::string chacha_xor(const u32 key[8], const u32 nonce[3], u32 counter0, std::string_view data) {94
std::string out(data);95
unsigned char block[64];96
for (std::size_t off = 0; off < out.size(); off += 64) {97
chacha_block(key, counter0 + static_cast<u32>(off / 64), nonce, block);98
const std::size_t n = std::min<std::size_t>(64, out.size() - off);99
for (std::size_t i = 0; i < n; ++i) {100
out[off + i] = static_cast<char>(static_cast<unsigned char>(out[off + i]) ^ block[i]);101
}102
}103
return out;104
}106
// Poly1305, INCREMENTAL: keyed by (r, s) from ChaCha block 0, then fed 16-byte blocks. Splitting107
// the one-shot form into init/block/finish lets a caller authenticate a message that arrives in108
// pieces WITHOUT first concatenating it into one buffer — which is what makes the allocation-free109
// AEAD path below possible (the AEAD MAC input is aad || pad || ct || pad || lengths, and every110
// segment is 16-byte aligned, so no block ever straddles two segments).111
// 26-bit limbs in u64 lanes — the standard portable shape. @complexity O(n) @alloc none112
// @test CheatahAead.Rfc8439Encrypt / CheatahAead.IntoFormsMatchStringForms113
struct Poly1305 {114
u32 r0{}, r1{}, r2{}, r3{}, r4{};115
u32 s1{}, s2{}, s3{}, s4{};116
u32 h0 = 0, h1 = 0, h2 = 0, h3 = 0, h4 = 0;117
unsigned char rs_copy[32]{};119
// Load and clamp r (RFC 8439 §2.5) and keep s for the final addition.120
void init(const unsigned char rs[32]) {121
std::memcpy(rs_copy, rs, 32);122
u32 t[4];123
std::memcpy(t, rs, 16);124
t[0] &= 0x0fffffff; t[1] &= 0x0ffffffc; t[2] &= 0x0ffffffc; t[3] &= 0x0ffffffc;125
r0 = t[0] & 0x3ffffff;126
r1 = ((t[0] >> 26) | (t[1] << 6)) & 0x3ffffff;127
r2 = ((t[1] >> 20) | (t[2] << 12)) & 0x3ffffff;128
r3 = ((t[2] >> 14) | (t[3] << 18)) & 0x3ffffff;129
r4 = (t[3] >> 8) & 0x3ffffff;130
s1 = r1 * 5; s2 = r2 * 5; s3 = r3 * 5; s4 = r4 * 5;131
h0 = h1 = h2 = h3 = h4 = 0;132
}134
// Absorb ONE block: @p n bytes (n <= 16) zero-padded, with the high bit set per the RFC.135
void block(const unsigned char* data, std::size_t n) {136
unsigned char blk[17] = {0};137
std::memcpy(blk, data, n);138
blk[n] = 1;139
u32 t[4];140
std::memcpy(t, blk, 16);141
h0 += t[0] & 0x3ffffff;142
h1 += ((t[0] >> 26) | (t[1] << 6)) & 0x3ffffff;143
h2 += ((t[1] >> 20) | (t[2] << 12)) & 0x3ffffff;144
h3 += ((t[2] >> 14) | (t[3] << 18)) & 0x3ffffff;145
h4 += (t[3] >> 8) | (static_cast<u32>(blk[16]) << 24);147
const u64 d0 = (u64)h0 * r0 + (u64)h1 * s4 + (u64)h2 * s3 + (u64)h3 * s2 + (u64)h4 * s1;148
const u64 d1 = (u64)h0 * r1 + (u64)h1 * r0 + (u64)h2 * s4 + (u64)h3 * s3 + (u64)h4 * s2;149
const u64 d2 = (u64)h0 * r2 + (u64)h1 * r1 + (u64)h2 * r0 + (u64)h3 * s4 + (u64)h4 * s3;150
const u64 d3 = (u64)h0 * r3 + (u64)h1 * r2 + (u64)h2 * r1 + (u64)h3 * r0 + (u64)h4 * s4;151
u64 d4 = (u64)h0 * r4 + (u64)h1 * r3 + (u64)h2 * r2 + (u64)h3 * r1 + (u64)h4 * r0;153
u64 c = d0 >> 26; h0 = d0 & 0x3ffffff;154
const u64 e1 = d1 + c; c = e1 >> 26; h1 = e1 & 0x3ffffff;155
const u64 e2 = d2 + c; c = e2 >> 26; h2 = e2 & 0x3ffffff;156
const u64 e3 = d3 + c; c = e3 >> 26; h3 = e3 & 0x3ffffff;157
d4 += c; c = d4 >> 26; h4 = d4 & 0x3ffffff;158
h0 += static_cast<u32>(c * 5); c = h0 >> 26; h0 &= 0x3ffffff;159
h1 += static_cast<u32>(c);160
}162
// Absorb a whole segment plus its zero padding to a 16-byte boundary (the AEAD shape).163
void segment_padded(const unsigned char* data, std::size_t len) {164
std::size_t pos = 0;165
while (pos + 16 <= len) { block(data + pos, 16); pos += 16; }166
if (pos < len) {167
unsigned char pad[16] = {0};168
std::memcpy(pad, data + pos, len - pos);169
block(pad, 16); // the AEAD pads to a full block (NOT the one-shot partial rule)170
}171
}173
void finish(unsigned char tag[16]);174
};176
void poly1305_state_finish(Poly1305& st, unsigned char tag[16]);178
void Poly1305::finish(unsigned char tag[16]) { poly1305_state_finish(*this, tag); }180
// The one-shot form, now expressed through the incremental core so both paths are provably the181
// same arithmetic. Keeps the RFC's partial-final-block rule (pad with zeros, high bit after the182
// last real byte) which differs from the AEAD's whole-block padding.183
void poly1305(const unsigned char rs[32], std::string_view msg, unsigned char tag[16]) {184
Poly1305 st;185
st.init(rs);186
std::size_t pos = 0;187
while (pos < msg.size()) {188
const std::size_t n = std::min<std::size_t>(16, msg.size() - pos);189
st.block(reinterpret_cast<const unsigned char*>(msg.data()) + pos, n);190
pos += n;191
}192
st.finish(tag);193
}195
void poly1305_state_finish(Poly1305& state, unsigned char tag[16]) {196
const unsigned char* rs = state.rs_copy;197
u32 h0 = state.h0, h1 = state.h1, h2 = state.h2, h3 = state.h3, h4 = state.h4;198
// final reduction mod 2^130 - 5, then the trial subtraction (constant-time select)199
u32 c = h1 >> 26; h1 &= 0x3ffffff; h2 += c;200
c = h2 >> 26; h2 &= 0x3ffffff; h3 += c;201
c = h3 >> 26; h3 &= 0x3ffffff; h4 += c;202
c = h4 >> 26; h4 &= 0x3ffffff; h0 += c * 5;203
c = h0 >> 26; h0 &= 0x3ffffff; h1 += c;205
u32 g0 = h0 + 5; c = g0 >> 26; g0 &= 0x3ffffff;206
u32 g1 = h1 + c; c = g1 >> 26; g1 &= 0x3ffffff;207
u32 g2 = h2 + c; c = g2 >> 26; g2 &= 0x3ffffff;208
u32 g3 = h3 + c; c = g3 >> 26; g3 &= 0x3ffffff;209
const u32 g4 = h4 + c - (1u << 26);211
const u32 mask = (g4 >> 31) - 1; // all-ones when h >= p (take g), zero otherwise212
h0 = (h0 & ~mask) | (g0 & mask);213
h1 = (h1 & ~mask) | (g1 & mask);214
h2 = (h2 & ~mask) | (g2 & mask);215
h3 = (h3 & ~mask) | (g3 & mask);216
h4 = (h4 & ~mask) | (g4 & mask);218
// h += s (the second 16 bytes of rs), little-endian, then serialize219
const u64 f0 = ((h0) | (h1 << 26)) & 0xffffffffull;220
const u64 f1 = ((h1 >> 6) | (h2 << 20)) & 0xffffffffull;221
const u64 f2 = ((h2 >> 12) | (h3 << 14)) & 0xffffffffull;222
const u64 f3 = ((h3 >> 18) | (h4 << 8)) & 0xffffffffull;223
u32 s_part[4];224
std::memcpy(s_part, rs + 16, 16);225
const u64 f[4] = {f0, f1, f2, f3};226
u64 carry_word = 0;227
for (std::size_t i = 0; i < 4; ++i) {228
const u64 sum = f[i] + s_part[i] + carry_word; // 32-bit lanes with carry between them229
carry_word = sum >> 32;230
tag[4 * i] = static_cast<unsigned char>(sum);231
tag[4 * i + 1] = static_cast<unsigned char>(sum >> 8);232
tag[4 * i + 2] = static_cast<unsigned char>(sum >> 16);233
tag[4 * i + 3] = static_cast<unsigned char>(sum >> 24);234
}235
}237
// Assemble the AEAD MAC input (aad || pad16 || ct || pad16 || len(aad) || len(ct)) and tag it.238
// @complexity O(n) @alloc the assembled buffer @test CheatahAead.Rfc8439Encrypt239
void aead_tag(const u32 key[8], const u32 nonce[3], std::string_view aad, std::string_view ct,240
unsigned char tag[16]) {241
unsigned char block0[64];242
chacha_block(key, 0, nonce, block0); // rs = the first 32 bytes of block 0244
std::string mac_input;245
mac_input.reserve(aad.size() + ct.size() + 32);246
mac_input.append(aad);247
mac_input.append((16 - aad.size() % 16) % 16, '\0');248
mac_input.append(ct);249
mac_input.append((16 - ct.size() % 16) % 16, '\0');250
unsigned char lens[16];251
const u64 alen = aad.size(), clen = ct.size();252
for (int i = 0; i < 8; ++i) {253
lens[i] = static_cast<unsigned char>(alen >> (8 * i));254
lens[8 + i] = static_cast<unsigned char>(clen >> (8 * i));255
}256
mac_input.append(reinterpret_cast<const char*>(lens), 16);257
poly1305(block0, mac_input, tag);258
}260
// ChaCha20 keystream XOR into a CALLER buffer — the allocation-free twin of chacha_xor. in/out may261
// alias (encrypt in place). @complexity O(n) @alloc none @test CheatahAead.IntoFormsMatchStringForms262
void chacha_xor_into(const u32 key[8], const u32 nonce[3], u32 counter0, const unsigned char* in,263
std::size_t len, unsigned char* out) {264
unsigned char block[64];265
for (std::size_t off = 0; off < len; off += 64) {266
chacha_block(key, counter0 + static_cast<u32>(off / 64), nonce, block);267
const std::size_t n = std::min<std::size_t>(64, len - off);268
for (std::size_t i = 0; i < n; ++i) out[off + i] = static_cast<unsigned char>(in[off + i] ^ block[i]);269
}270
}272
// The AEAD tag WITHOUT assembling the MAC input: feed aad, ciphertext and the length block straight273
// into the incremental Poly1305. Same arithmetic as aead_tag, no buffer.274
// @complexity O(|aad| + |ct|) @alloc none @test CheatahAead.IntoFormsMatchStringForms275
void aead_tag_into(const u32 key[8], const u32 nonce[3], const unsigned char* aad, std::size_t aad_len,276
const unsigned char* ct, std::size_t ct_len, unsigned char tag[16]) {277
unsigned char block0[64];278
chacha_block(key, 0, nonce, block0);279
Poly1305 st;280
st.init(block0);281
st.segment_padded(aad, aad_len);282
st.segment_padded(ct, ct_len);283
unsigned char lens[16];284
const u64 alen = aad_len, clen = ct_len;285
for (int i = 0; i < 8; ++i) {286
lens[i] = static_cast<unsigned char>(alen >> (8 * i));287
lens[8 + i] = static_cast<unsigned char>(clen >> (8 * i));288
}289
st.block(lens, 16);290
st.finish(tag);291
}293
// hex -> n bytes (false on malformed). @complexity O(n) @alloc none @test CheatahAead.RejectsTamper294
bool hex_bytes(std::string_view hex, unsigned char* out, std::size_t n) {295
if (hex.size() != 2 * n) return false;296
for (std::size_t i = 0; i < n; ++i) {297
unsigned v = 0;298
for (int k = 0; k < 2; ++k) {299
const char ch = hex[2 * i + k];300
v <<= 4;301
if (ch >= '0' && ch <= '9') v |= static_cast<unsigned>(ch - '0');302
else if (ch >= 'a' && ch <= 'f') v |= static_cast<unsigned>(ch - 'a' + 10);303
else if (ch >= 'A' && ch <= 'F') v |= static_cast<unsigned>(ch - 'A' + 10);304
else return false;305
}306
out[i] = static_cast<unsigned char>(v);307
}308
return true;309
}311
bool load_key_nonce(std::string_view key_hex, std::string_view nonce_hex, u32 key[8], u32 nonce[3]) {312
unsigned char kb[32], nb[12];313
if (!hex_bytes(key_hex, kb, 32) || !hex_bytes(nonce_hex, nb, 12)) return false;314
std::memcpy(key, kb, 32); // little-endian words per RFC 8439315
std::memcpy(nonce, nb, 12);316
return true;317
}319
// ===================== AES-128-GCM (TLS_AES_128_GCM_SHA256) =====================320
// AES-128 (encrypt only — GCM never AES-decrypts) + GHASH over GF(2^128) + GCM mode. Byte-oriented321
// and correctness-first (no T-tables); the record cipher is exercised once per TLS record.323
// The AES S-box (FIPS-197).324
const unsigned char kSbox[256] = {325
0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,326
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,327
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,328
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,329
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,330
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,331
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,332
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,333
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,334
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,335
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,336
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,337
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,338
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,339
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,340
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16};342
// Expand a 16-byte AES-128 key into 11 round keys (176 bytes). State/round-key byte layout is343
// column-major: byte (row r, col c) at index 4*c + r.344
void aes128_key_expand(const unsigned char key[16], unsigned char rk[176]) {345
static const unsigned char rcon[10] = {0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36};346
std::memcpy(rk, key, 16);347
int r = 0;348
for (int i = 16; i < 176; i += 4) {349
unsigned char t[4] = {rk[i - 4], rk[i - 3], rk[i - 2], rk[i - 1]};350
if (i % 16 == 0) { // RotWord + SubWord + Rcon on the first word of each round key351
const unsigned char a0 = t[0];352
t[0] = static_cast<unsigned char>(kSbox[t[1]] ^ rcon[r++]);353
t[1] = kSbox[t[2]];354
t[2] = kSbox[t[3]];355
t[3] = kSbox[a0];356
}357
for (int j = 0; j < 4; ++j) rk[i + j] = static_cast<unsigned char>(rk[i - 16 + j] ^ t[j]);358
}359
}361
// Expand a 32-byte AES-256 key into 15 round keys (240 bytes). Nk = 8 words: every 8 words apply362
// RotWord+SubWord+Rcon to the first word, and — the AES-256 extra step — a plain SubWord to the 4th363
// word of each 8-word span (FIPS-197 §5.2).364
void aes256_key_expand(const unsigned char key[32], unsigned char rk[240]) {365
static const unsigned char rcon[7] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40};366
std::memcpy(rk, key, 32);367
int r = 0;368
for (int i = 32; i < 240; i += 4) {369
unsigned char t[4] = {rk[i - 4], rk[i - 3], rk[i - 2], rk[i - 1]};370
if (i % 32 == 0) { // RotWord + SubWord + Rcon371
const unsigned char a0 = t[0];372
t[0] = static_cast<unsigned char>(kSbox[t[1]] ^ rcon[r++]);373
t[1] = kSbox[t[2]];374
t[2] = kSbox[t[3]];375
t[3] = kSbox[a0];376
} else if (i % 32 == 16) { // SubWord only (AES-256-specific)377
for (unsigned char & j : t) j = kSbox[j];378
}379
for (int j = 0; j < 4; ++j) rk[i + j] = static_cast<unsigned char>(rk[i - 32 + j] ^ t[j]);380
}381
}383
unsigned char xtime(unsigned char x) {384
return static_cast<unsigned char>((x << 1) ^ ((x >> 7) * 0x1b)); // ·2 in GF(2^8)385
}387
// Encrypt one 16-byte block with @p nr rounds (nr = 10 for AES-128, 14 for AES-256). @p rk holds388
// 16*(nr+1) round-key bytes.389
void aes_encrypt_block(const unsigned char* rk, int nr, const unsigned char in[16],390
unsigned char out[16]) {391
unsigned char s[16];392
for (int i = 0; i < 16; ++i) s[i] = static_cast<unsigned char>(in[i] ^ rk[i]); // round 0393
for (int round = 1; round <= nr; ++round) {394
for (unsigned char & i : s) i = kSbox[i]; // SubBytes395
unsigned char t = 0; // ShiftRows396
t = s[1]; s[1] = s[5]; s[5] = s[9]; s[9] = s[13]; s[13] = t;397
t = s[2]; s[2] = s[10]; s[10] = t; t = s[6]; s[6] = s[14]; s[14] = t;398
t = s[15]; s[15] = s[11]; s[11] = s[7]; s[7] = s[3]; s[3] = t;399
if (round != nr) { // MixColumns400
for (int c = 0; c < 4; ++c) {401
unsigned char* col = s + 4 * static_cast<std::ptrdiff_t>(c);402
const unsigned char a0 = col[0], a1 = col[1], a2 = col[2], a3 = col[3];403
col[0] = static_cast<unsigned char>(xtime(a0) ^ (xtime(a1) ^ a1) ^ a2 ^ a3);404
col[1] = static_cast<unsigned char>(a0 ^ xtime(a1) ^ (xtime(a2) ^ a2) ^ a3);405
col[2] = static_cast<unsigned char>(a0 ^ a1 ^ xtime(a2) ^ (xtime(a3) ^ a3));406
col[3] = static_cast<unsigned char>((xtime(a0) ^ a0) ^ a1 ^ a2 ^ xtime(a3));407
}408
}409
const unsigned char* r_k = rk + 16 * static_cast<std::ptrdiff_t>(round); // AddRoundKey410
for (int i = 0; i < 16; ++i) s[i] = static_cast<unsigned char>(s[i] ^ r_k[i]);411
}412
std::memcpy(out, s, 16);413
}415
// GF(2^128) multiply (SP 800-38D): out = X · Y, big-endian bit order, reduction poly R = 0xe1<<120.416
void gf_mult(const unsigned char X[16], const unsigned char Y[16], unsigned char out[16]) {417
unsigned char Z[16] = {0}, V[16];418
std::memcpy(V, Y, 16);419
for (int i = 0; i < 128; ++i) {420
if ((X[i / 8] >> (7 - (i % 8))) & 1)421
for (int j = 0; j < 16; ++j) Z[j] ^= V[j];422
const unsigned char lsb = V[15] & 1;423
for (int j = 15; j > 0; --j) V[j] = static_cast<unsigned char>((V[j] >> 1) | ((V[j - 1] & 1) << 7));424
V[0] >>= 1;425
if (lsb) V[0] ^= 0xe1;426
}427
std::memcpy(out, Z, 16);428
}430
// GHASH-accumulate the zero-padded `data` into Y (Y := (Y XOR block)·H per 16-byte block).431
void ghash_blocks(unsigned char Y[16], const unsigned char H[16], const unsigned char* p,432
std::size_t n) {433
for (std::size_t off = 0; off < n; off += 16) {434
unsigned char b[16] = {0};435
const std::size_t m = std::min<std::size_t>(16, n - off);436
std::memcpy(b, p + off, m);437
for (int i = 0; i < 16; ++i) Y[i] ^= b[i];438
unsigned char t[16];439
gf_mult(Y, H, t);440
std::memcpy(Y, t, 16);441
}442
}444
// The GCM tag: GHASH(AAD || pad || C || pad || [len(AAD)bits]_64 || [len(C)bits]_64) XOR E(J0).445
void gcm_tag(const unsigned char* rk, int nr, const unsigned char H[16], const unsigned char J0[16],446
std::string_view aad, std::string_view ct, unsigned char tag[16]) {447
unsigned char Y[16] = {0};448
ghash_blocks(Y, H, reinterpret_cast<const unsigned char*>(aad.data()), aad.size());449
ghash_blocks(Y, H, reinterpret_cast<const unsigned char*>(ct.data()), ct.size());450
unsigned char lb[16] = {0};451
const u64 abits = static_cast<u64>(aad.size()) * 8, cbits = static_cast<u64>(ct.size()) * 8;452
for (int i = 0; i < 8; ++i) {453
lb[7 - i] = static_cast<unsigned char>(abits >> (8 * i));454
lb[15 - i] = static_cast<unsigned char>(cbits >> (8 * i));455
}456
for (int i = 0; i < 16; ++i) Y[i] ^= lb[i];457
unsigned char t[16];458
gf_mult(Y, H, t);459
unsigned char ej0[16];460
aes_encrypt_block(rk, nr, J0, ej0);461
for (int i = 0; i < 16; ++i) tag[i] = static_cast<unsigned char>(t[i] ^ ej0[i]);462
}464
void inc32(unsigned char ctr[16]) { // increment the rightmost 32 bits (big-endian)465
for (int j = 15; j >= 12; --j)466
if (++ctr[j] != 0) break;467
}469
// GCTR: XOR `data` in place with the AES-CTR keystream starting at counter `ctr` (advanced).470
void gctr(const unsigned char* rk, int nr, unsigned char ctr[16], std::string& data) {471
unsigned char ks[16];472
for (std::size_t off = 0; off < data.size(); off += 16) {473
aes_encrypt_block(rk, nr, ctr, ks);474
const std::size_t n = std::min<std::size_t>(16, data.size() - off);475
for (std::size_t i = 0; i < n; ++i)476
data[off + i] = static_cast<char>(static_cast<unsigned char>(data[off + i]) ^ ks[i]);477
inc32(ctr);478
}479
}481
// Force the portable (non-AES-NI) AES-GCM path — a testing/determinism hook so the scalar482
// reference is exercised even on CPUs where the hardware path is the default.483
bool g_force_portable = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables): documented process-wide test knob set via set_force_portable_crypto()485
} // namespace487
/**488
* Pin (or release) the portable scalar AES-GCM path — the test/determinism hook whose full489
* contract lives on the declaration in aead.hpp (kept out of the public docs via \\cond there).490
* @param on true to pin the portable scalar path; false to allow the hardware path again.491
* @complexity O(1).492
* @alloc none.493
* @test CheatahAead.AesGcmPortableMatchesHardware494
*/495
void set_force_portable_crypto(bool on) { g_force_portable = on; }497
namespace {498
bool aes_gcm_use_hw() { return accel::available() && !g_force_portable; }499
} // namespace501
bool crypto_hardware_active() { return aes_gcm_use_hw(); }503
// A single message this large would wrap the 32-bit block counter — ChaCha20's block index or504
// GCM's CTR — reusing keystream (and, for GCM, the E(J0) tag mask) WITHIN the one message, which505
// breaks confidentiality/integrity. The binding limit is GCM's 2^32 16-byte blocks (~64 GiB); cap506
// both constructions there. It is unreachable in practice (no TLS record is remotely this large,507
// and the message must fit in memory), but bounds the primitive against misuse. Cross-message nonce508
// uniqueness remains the caller's responsibility, as documented.509
constexpr std::uint64_t kMaxAeadMessage = std::uint64_t{1} << 36; // 64 GiB510
/**511
* Whether one AEAD message is under the 64 GiB counter-wrap cap above. Every encrypt/decrypt512
* checks it; the over-cap branch is unreachable in a test (the message would not fit in memory).513
* @param msg the plaintext or ciphertext.514
* @return true iff @p msg is within the single-message limit.515
* @complexity O(1).516
* @alloc none.517
* @test CheatahAead.Rfc8439Encrypt518
*/519
inline bool aead_len_ok(std::string_view msg) {520
return static_cast<std::uint64_t>(msg.size()) <= kMaxAeadMessage;521
}523
std::string chacha20poly1305_encrypt(std::string_view key_hex, std::string_view nonce_hex,524
std::string_view aad, std::string_view plaintext) {525
u32 key[8], nonce[3];526
if (!load_key_nonce(key_hex, nonce_hex, key, nonce) || !aead_len_ok(plaintext)) return "";527
std::string ct = chacha_xor(key, nonce, 1, plaintext); // counter starts at 1 (0 keys the MAC)528
unsigned char tag[16];529
aead_tag(key, nonce, aad, ct, tag);530
ct.append(reinterpret_cast<const char*>(tag), 16);531
return ct;532
}534
bool chacha20poly1305_encrypt_into(const unsigned char key[32], const unsigned char nonce[12],535
const unsigned char* aad, std::size_t aad_len,536
const unsigned char* plaintext, std::size_t plaintext_len,537
unsigned char* out) {538
if (key == nullptr || nonce == nullptr || out == nullptr ||539
(plaintext == nullptr && plaintext_len != 0) || (aad == nullptr && aad_len != 0) ||540
static_cast<std::uint64_t>(plaintext_len) > kMaxAeadMessage) {541
return false;542
}543
u32 k[8], n[3];544
std::memcpy(k, key, 32); // little-endian words per RFC 8439545
std::memcpy(n, nonce, 12);546
chacha_xor_into(k, n, 1, plaintext, plaintext_len, out); // counter 1 (0 keys the MAC)547
aead_tag_into(k, n, aad, aad_len, out, plaintext_len, out + plaintext_len);548
// Do not leave the expanded key on the stack: a later core dump, or ordinary stack reuse in a549
// process that keeps running, should not be able to surface it. See secure_wipe — a plain memset550
// to a dead local is legally removed by the optimizer, which is the whole point.551
secure_wipe(k, sizeof k);552
return true;553
}555
bool chacha20poly1305_decrypt_into(const unsigned char key[32], const unsigned char nonce[12],556
const unsigned char* aad, std::size_t aad_len,557
const unsigned char* ciphertext, std::size_t ciphertext_len,558
unsigned char* out) {559
if (key == nullptr || nonce == nullptr || ciphertext == nullptr || ciphertext_len < 16 ||560
(aad == nullptr && aad_len != 0) ||561
static_cast<std::uint64_t>(ciphertext_len) > kMaxAeadMessage) {562
return false;563
}564
const std::size_t ct_len = ciphertext_len - 16;565
if (out == nullptr && ct_len != 0) return false; // a tag-only message needs no out buffer566
u32 k[8], n[3];567
std::memcpy(k, key, 32);568
std::memcpy(n, nonce, 12);569
unsigned char tag[16];570
aead_tag_into(k, n, aad, aad_len, ciphertext, ct_len, tag);571
unsigned char diff = 0; // constant-time compare: never early-exit on a mismatching byte572
for (int i = 0; i < 16; ++i) diff |= tag[i] ^ ciphertext[ct_len + i];573
if (diff != 0) {574
secure_wipe(k, sizeof k);575
return false; // authentication failed: nothing is written to out576
}577
chacha_xor_into(k, n, 1, ciphertext, ct_len, out);578
secure_wipe(k, sizeof k);579
return true;580
}582
std::string chacha20poly1305_decrypt(std::string_view key_hex, std::string_view nonce_hex,583
std::string_view aad, std::string_view ciphertext) {584
u32 key[8], nonce[3];585
if (!load_key_nonce(key_hex, nonce_hex, key, nonce) || ciphertext.size() < 16 ||586
!aead_len_ok(ciphertext)) return "";587
const std::string_view ct = ciphertext.substr(0, ciphertext.size() - 16);588
const std::string_view given = ciphertext.substr(ciphertext.size() - 16);589
unsigned char tag[16];590
aead_tag(key, nonce, aad, ct, tag);591
unsigned char diff = 0; // constant-time compare: never early-exit on a mismatching byte592
for (int i = 0; i < 16; ++i) {593
diff |= tag[i] ^ static_cast<unsigned char>(given[i]);594
}595
if (diff != 0) return "";596
return chacha_xor(key, nonce, 1, ct);597
}599
namespace {600
// Portable AES-GCM encrypt over already-expanded round keys (@p nr rounds). Shared by AES-128/256.601
std::string gcm_encrypt_portable(const unsigned char* rk, int nr, const unsigned char nb[12],602
std::string_view aad, std::string_view plaintext) {603
unsigned char H[16], zero[16] = {0};604
aes_encrypt_block(rk, nr, zero, H); // hash subkey H = E(0)605
unsigned char J0[16] = {0};606
std::memcpy(J0, nb, 12);607
J0[15] = 1; // J0 = nonce || 0x00000001608
std::string ct(plaintext);609
unsigned char ctr[16];610
std::memcpy(ctr, J0, 16);611
inc32(ctr); // CTR starts at inc32(J0)612
gctr(rk, nr, ctr, ct);613
unsigned char tag[16];614
gcm_tag(rk, nr, H, J0, aad, ct, tag);615
ct.append(reinterpret_cast<const char*>(tag), 16);616
return ct;617
}619
// Portable AES-GCM decrypt (constant-time tag check; "" on mismatch). Shared by AES-128/256.620
std::string gcm_decrypt_portable(const unsigned char* rk, int nr, const unsigned char nb[12],621
std::string_view aad, std::string_view ciphertext) {622
unsigned char H[16], zero[16] = {0};623
aes_encrypt_block(rk, nr, zero, H);624
unsigned char J0[16] = {0};625
std::memcpy(J0, nb, 12);626
J0[15] = 1;627
const std::string_view ct = ciphertext.substr(0, ciphertext.size() - 16);628
const std::string_view given = ciphertext.substr(ciphertext.size() - 16);629
unsigned char tag[16];630
gcm_tag(rk, nr, H, J0, aad, ct, tag);631
unsigned char diff = 0; // constant-time tag compare632
for (int i = 0; i < 16; ++i) diff |= tag[i] ^ static_cast<unsigned char>(given[i]);633
if (diff != 0) return "";634
std::string pt(ct);635
unsigned char ctr[16];636
std::memcpy(ctr, J0, 16);637
inc32(ctr);638
gctr(rk, nr, ctr, pt);639
return pt;640
}641
} // namespace643
std::string aes128gcm_encrypt(std::string_view key_hex, std::string_view nonce_hex,644
std::string_view aad, std::string_view plaintext) {645
unsigned char kb[16], nb[12];646
if (!hex_bytes(key_hex, kb, 16) || !hex_bytes(nonce_hex, nb, 12) || !aead_len_ok(plaintext)) return "";647
if (aes_gcm_use_hw()) return accel::gcm_encrypt(kb, 16, nb, aad, plaintext);648
unsigned char rk[176];649
aes128_key_expand(kb, rk);650
return gcm_encrypt_portable(rk, 10, nb, aad, plaintext);651
}653
std::string aes128gcm_decrypt(std::string_view key_hex, std::string_view nonce_hex,654
std::string_view aad, std::string_view ciphertext) {655
unsigned char kb[16], nb[12];656
if (!hex_bytes(key_hex, kb, 16) || !hex_bytes(nonce_hex, nb, 12) || ciphertext.size() < 16 ||657
!aead_len_ok(ciphertext)) return "";658
if (aes_gcm_use_hw()) return accel::gcm_decrypt(kb, 16, nb, aad, ciphertext);659
unsigned char rk[176];660
aes128_key_expand(kb, rk);661
return gcm_decrypt_portable(rk, 10, nb, aad, ciphertext);662
}664
// AES-256-GCM — the record cipher of TLS_AES_256_GCM_SHA384. Same runtime dispatch as AES-128: the665
// AES-NI/PMULL hardware path when the CPU has it AND the power-on self-test reproduced the known-answer666
// vector (available()), otherwise the portable scalar reference. Both are KAT-tested and cross-checked667
// against each other (CheatahAead.Aes256GcmPortableMatchesHardware).668
std::string aes256gcm_encrypt(std::string_view key_hex, std::string_view nonce_hex,669
std::string_view aad, std::string_view plaintext) {670
unsigned char kb[32], nb[12];671
if (!hex_bytes(key_hex, kb, 32) || !hex_bytes(nonce_hex, nb, 12) || !aead_len_ok(plaintext)) return "";672
if (aes_gcm_use_hw()) return accel::gcm_encrypt(kb, 32, nb, aad, plaintext);673
unsigned char rk[240];674
aes256_key_expand(kb, rk);675
return gcm_encrypt_portable(rk, 14, nb, aad, plaintext);676
}678
std::string aes256gcm_decrypt(std::string_view key_hex, std::string_view nonce_hex,679
std::string_view aad, std::string_view ciphertext) {680
unsigned char kb[32], nb[12];681
if (!hex_bytes(key_hex, kb, 32) || !hex_bytes(nonce_hex, nb, 12) || ciphertext.size() < 16 ||682
!aead_len_ok(ciphertext)) return "";683
if (aes_gcm_use_hw()) return accel::gcm_decrypt(kb, 32, nb, aad, ciphertext);684
unsigned char rk[240];685
aes256_key_expand(kb, rk);686
return gcm_decrypt_portable(rk, 14, nb, aad, ciphertext);687
}689
} // namespace cheatah::aead