cheatah
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_bzero
11#endif
13#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 block
16// function keyed per RFC; Poly1305 is the one-time authenticator over r,s derived from
17// block 0. The AEAD construction MACs aad || pad || ciphertext || pad || lengths and
18// appends the 16-byte tag. The tag comparison on decrypt is constant-time.
20namespace cheatah::aead {
21namespace {
23using u32 = std::uint32_t;
24using u64 = std::uint64_t;
26/**
27 * Erase secret bytes so they cannot outlive their use — and do it in a form the optimizer is not
28 * permitted to delete.
29 *
30 * A plain `std::memset` over a local that is never read again is a dead store, and compilers really
31 * do remove it; the expanded key would then stay on the stack for a core dump or ordinary stack
32 * 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 is
36 * 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 declared
39 * when `__STDC_WANT_LIB_EXT1__` is defined to 1 BEFORE the first `<string.h>` in the translation
40 * unit, which is a fragile thing to depend on in a file that includes other headers — and it is
41 * exactly what the first attempt at this fix got wrong ("no member named 'memset_s' in the global
42 * namespace", caught by the macOS CI in 39 seconds). Wiping 32 bytes is not worth a platform axis.
43 */
44void secure_wipe(void* p, std::size_t n) {
45#if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)
46 ::explicit_bzero(p, n);
47#else
48 volatile auto* q = static_cast<volatile unsigned char*>(p);
49 for (std::size_t i = 0; i < n; ++i) q[i] = 0;
50#endif
53u32 rotl(u32 x, int n) { return (x << n) | (x >> (32 - n)); }
55// One ChaCha quarter round on four state words. @complexity O(1) @alloc none
56// @test CheatahAead.Rfc8439Encrypt
57void 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);
64// The ChaCha20 block function: 64 bytes of keystream for (key, counter, nonce).
65// @complexity O(1) — 20 rounds @alloc none @test CheatahAead.Rfc8439Encrypt
66void 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 rounds
73 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 }
91// XOR `data` with the ChaCha20 keystream starting at block `counter0`.
92// @complexity O(n) @alloc the returned string @test CheatahAead.Rfc8439Encrypt
93std::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;
106// Poly1305, INCREMENTAL: keyed by (r, s) from ChaCha block 0, then fed 16-byte blocks. Splitting
107// the one-shot form into init/block/finish lets a caller authenticate a message that arrives in
108// pieces WITHOUT first concatenating it into one buffer — which is what makes the allocation-free
109// AEAD path below possible (the AEAD MAC input is aad || pad || ct || pad || lengths, and every
110// 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 none
112// @test CheatahAead.Rfc8439Encrypt / CheatahAead.IntoFormsMatchStringForms
113struct 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};
176void poly1305_state_finish(Poly1305& st, unsigned char tag[16]);
178void 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 the
181// same arithmetic. Keeps the RFC's partial-final-block rule (pad with zeros, high bit after the
182// last real byte) which differs from the AEAD's whole-block padding.
183void 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);
195void 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 otherwise
212 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 serialize
219 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 them
229 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 }
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.Rfc8439Encrypt
239void 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 0
244 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);
260// ChaCha20 keystream XOR into a CALLER buffer — the allocation-free twin of chacha_xor. in/out may
261// alias (encrypt in place). @complexity O(n) @alloc none @test CheatahAead.IntoFormsMatchStringForms
262void 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 }
272// The AEAD tag WITHOUT assembling the MAC input: feed aad, ciphertext and the length block straight
273// into the incremental Poly1305. Same arithmetic as aead_tag, no buffer.
274// @complexity O(|aad| + |ct|) @alloc none @test CheatahAead.IntoFormsMatchStringForms
275void 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);
293// hex -> n bytes (false on malformed). @complexity O(n) @alloc none @test CheatahAead.RejectsTamper
294bool 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;
311bool 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 8439
315 std::memcpy(nonce, nb, 12);
316 return true;
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-oriented
321// and correctness-first (no T-tables); the record cipher is exercised once per TLS record.
323// The AES S-box (FIPS-197).
324const 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 is
343// column-major: byte (row r, col c) at index 4*c + r.
344void 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 key
351 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 }
361// Expand a 32-byte AES-256 key into 15 round keys (240 bytes). Nk = 8 words: every 8 words apply
362// RotWord+SubWord+Rcon to the first word, and — the AES-256 extra step — a plain SubWord to the 4th
363// word of each 8-word span (FIPS-197 §5.2).
364void 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 + Rcon
371 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 }
383unsigned char xtime(unsigned char x) {
384 return static_cast<unsigned char>((x << 1) ^ ((x >> 7) * 0x1b)); // ·2 in GF(2^8)
387// Encrypt one 16-byte block with @p nr rounds (nr = 10 for AES-128, 14 for AES-256). @p rk holds
388// 16*(nr+1) round-key bytes.
389void 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 0
393 for (int round = 1; round <= nr; ++round) {
394 for (unsigned char & i : s) i = kSbox[i]; // SubBytes
395 unsigned char t = 0; // ShiftRows
396 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) { // MixColumns
400 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); // AddRoundKey
410 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);
415// GF(2^128) multiply (SP 800-38D): out = X · Y, big-endian bit order, reduction poly R = 0xe1<<120.
416void 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);
430// GHASH-accumulate the zero-padded `data` into Y (Y := (Y XOR block)·H per 16-byte block).
431void 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 }
444// The GCM tag: GHASH(AAD || pad || C || pad || [len(AAD)bits]_64 || [len(C)bits]_64) XOR E(J0).
445void 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]);
464void 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;
469// GCTR: XOR `data` in place with the AES-CTR keystream starting at counter `ctr` (advanced).
470void 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 }
481// Force the portable (non-AES-NI) AES-GCM path — a testing/determinism hook so the scalar
482// reference is exercised even on CPUs where the hardware path is the default.
483bool g_force_portable = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables): documented process-wide test knob set via set_force_portable_crypto()
485} // namespace
487/**
488 * Pin (or release) the portable scalar AES-GCM path — the test/determinism hook whose full
489 * 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.AesGcmPortableMatchesHardware
494 */
495void set_force_portable_crypto(bool on) { g_force_portable = on; }
497namespace {
498bool aes_gcm_use_hw() { return accel::available() && !g_force_portable; }
499} // namespace
501bool 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 or
504// GCM's CTR — reusing keystream (and, for GCM, the E(J0) tag mask) WITHIN the one message, which
505// breaks confidentiality/integrity. The binding limit is GCM's 2^32 16-byte blocks (~64 GiB); cap
506// 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 nonce
508// uniqueness remains the caller's responsibility, as documented.
509constexpr std::uint64_t kMaxAeadMessage = std::uint64_t{1} << 36; // 64 GiB
510/**
511 * Whether one AEAD message is under the 64 GiB counter-wrap cap above. Every encrypt/decrypt
512 * 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.Rfc8439Encrypt
518 */
519inline bool aead_len_ok(std::string_view msg) {
520 return static_cast<std::uint64_t>(msg.size()) <= kMaxAeadMessage;
523std::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;
534bool 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 8439
545 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 a
549 // process that keeps running, should not be able to surface it. See secure_wipe — a plain memset
550 // to a dead local is legally removed by the optimizer, which is the whole point.
551 secure_wipe(k, sizeof k);
552 return true;
555bool 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 buffer
566 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 byte
572 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 out
576 }
577 chacha_xor_into(k, n, 1, ciphertext, ct_len, out);
578 secure_wipe(k, sizeof k);
579 return true;
582std::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 byte
592 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);
599namespace {
600// Portable AES-GCM encrypt over already-expanded round keys (@p nr rounds). Shared by AES-128/256.
601std::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 || 0x00000001
608 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;
619// Portable AES-GCM decrypt (constant-time tag check; "" on mismatch). Shared by AES-128/256.
620std::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 compare
632 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;
641} // namespace
643std::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);
653std::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);
664// AES-256-GCM — the record cipher of TLS_AES_256_GCM_SHA384. Same runtime dispatch as AES-128: the
665// AES-NI/PMULL hardware path when the CPU has it AND the power-on self-test reproduced the known-answer
666// vector (available()), otherwise the portable scalar reference. Both are KAT-tested and cross-checked
667// against each other (CheatahAead.Aes256GcmPortableMatchesHardware).
668std::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);
678std::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);
689} // namespace cheatah::aead