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 <cstdint>
6#include <cstring>
8#if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)
9# include <strings.h> // explicit_bzero
10#endif
12#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 block
15// function keyed per RFC; Poly1305 is the one-time authenticator over r,s derived from
16// block 0. The AEAD construction MACs aad || pad || ciphertext || pad || lengths and
17// appends the 16-byte tag. The tag comparison on decrypt is constant-time.
19namespace cheatah::aead {
20namespace {
22using u32 = std::uint32_t;
23using u64 = std::uint64_t;
25/**
26 * Erase secret bytes so they cannot outlive their use — and do it in a form the optimizer is not
27 * permitted to delete.
28 *
29 * A plain `std::memset` over a local that is never read again is a dead store, and compilers really
30 * do remove it; the expanded key would then stay on the stack for a core dump or ordinary stack
31 * 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 is
35 * 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 declared
38 * when `__STDC_WANT_LIB_EXT1__` is defined to 1 BEFORE the first `<string.h>` in the translation
39 * unit, which is a fragile thing to depend on in a file that includes other headers — and it is
40 * exactly what the first attempt at this fix got wrong ("no member named 'memset_s' in the global
41 * namespace", caught by the macOS CI in 39 seconds). Wiping 32 bytes is not worth a platform axis.
42 */
43void secure_wipe(void* p, std::size_t n) {
44#if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)
45 ::explicit_bzero(p, n);
46#else
47 volatile auto* q = static_cast<volatile unsigned char*>(p);
48 for (std::size_t i = 0; i < n; ++i) q[i] = 0;
49#endif
52u32 rotl(u32 x, int n) { return (x << n) | (x >> (32 - n)); }
54// One ChaCha quarter round on four state words. @complexity O(1) @alloc none
55// @test CheatahAead.Rfc8439Encrypt
56void 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);
63// The ChaCha20 block function: 64 bytes of keystream for (key, counter, nonce).
64// @complexity O(1) — 20 rounds @alloc none @test CheatahAead.Rfc8439Encrypt
65void 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 rounds
72 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 }
90// XOR `data` with the ChaCha20 keystream starting at block `counter0`.
91// @complexity O(n) @alloc the returned string @test CheatahAead.Rfc8439Encrypt
92std::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;
105// Poly1305, INCREMENTAL: keyed by (r, s) from ChaCha block 0, then fed 16-byte blocks. Splitting
106// the one-shot form into init/block/finish lets a caller authenticate a message that arrives in
107// pieces WITHOUT first concatenating it into one buffer — which is what makes the allocation-free
108// AEAD path below possible (the AEAD MAC input is aad || pad || ct || pad || lengths, and every
109// 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 none
111// @test CheatahAead.Rfc8439Encrypt / CheatahAead.IntoFormsMatchStringForms
112struct 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};
175void poly1305_state_finish(Poly1305& st, unsigned char tag[16]);
177void 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 the
180// same arithmetic. Keeps the RFC's partial-final-block rule (pad with zeros, high bit after the
181// last real byte) which differs from the AEAD's whole-block padding.
182void 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);
194void 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 otherwise
211 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 serialize
218 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 them
228 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 }
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.Rfc8439Encrypt
238void 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 0
243 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);
259// ChaCha20 keystream XOR into a CALLER buffer — the allocation-free twin of chacha_xor. in/out may
260// alias (encrypt in place). @complexity O(n) @alloc none @test CheatahAead.IntoFormsMatchStringForms
261void 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 }
271// The AEAD tag WITHOUT assembling the MAC input: feed aad, ciphertext and the length block straight
272// into the incremental Poly1305. Same arithmetic as aead_tag, no buffer.
273// @complexity O(|aad| + |ct|) @alloc none @test CheatahAead.IntoFormsMatchStringForms
274void 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);
292// hex -> n bytes (false on malformed). @complexity O(n) @alloc none @test CheatahAead.RejectsTamper
293bool 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;
310bool 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 8439
314 std::memcpy(nonce, nb, 12);
315 return true;
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-oriented
320// and correctness-first (no T-tables); the record cipher is exercised once per TLS record.
322// The AES S-box (FIPS-197).
323const 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 is
342// column-major: byte (row r, col c) at index 4*c + r.
343void 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 key
350 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 }
360// Expand a 32-byte AES-256 key into 15 round keys (240 bytes). Nk = 8 words: every 8 words apply
361// RotWord+SubWord+Rcon to the first word, and — the AES-256 extra step — a plain SubWord to the 4th
362// word of each 8-word span (FIPS-197 §5.2).
363void 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 + Rcon
370 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 }
382unsigned char xtime(unsigned char x) {
383 return static_cast<unsigned char>((x << 1) ^ ((x >> 7) * 0x1b)); // ·2 in GF(2^8)
386// Encrypt one 16-byte block with @p nr rounds (nr = 10 for AES-128, 14 for AES-256). @p rk holds
387// 16*(nr+1) round-key bytes.
388void 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 0
392 for (int round = 1; round <= nr; ++round) {
393 for (int i = 0; i < 16; ++i) s[i] = kSbox[s[i]]; // SubBytes
394 unsigned char t; // ShiftRows
395 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) { // MixColumns
399 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; // AddRoundKey
409 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);
414// GF(2^128) multiply (SP 800-38D): out = X · Y, big-endian bit order, reduction poly R = 0xe1<<120.
415void 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);
429// GHASH-accumulate the zero-padded `data` into Y (Y := (Y XOR block)·H per 16-byte block).
430void 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 }
443// The GCM tag: GHASH(AAD || pad || C || pad || [len(AAD)bits]_64 || [len(C)bits]_64) XOR E(J0).
444void 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]);
463void 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;
468// GCTR: XOR `data` in place with the AES-CTR keystream starting at counter `ctr` (advanced).
469void 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 }
480// Force the portable (non-AES-NI) AES-GCM path — a testing/determinism hook so the scalar
481// reference is exercised even on CPUs where the hardware path is the default.
482bool g_force_portable = false;
484} // namespace
486/**
487 * Pin (or release) the portable scalar AES-GCM path — the test/determinism hook whose full
488 * 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.AesGcmPortableMatchesHardware
493 */
494void set_force_portable_crypto(bool on) { g_force_portable = on; }
496namespace {
497bool aes_gcm_use_hw() { return accel::available() && !g_force_portable; }
498} // namespace
500bool 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 or
503// GCM's CTR — reusing keystream (and, for GCM, the E(J0) tag mask) WITHIN the one message, which
504// breaks confidentiality/integrity. The binding limit is GCM's 2^32 16-byte blocks (~64 GiB); cap
505// 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 nonce
507// uniqueness remains the caller's responsibility, as documented.
508constexpr std::uint64_t kMaxAeadMessage = std::uint64_t{1} << 36; // 64 GiB
509/**
510 * Whether one AEAD message is under the 64 GiB counter-wrap cap above. Every encrypt/decrypt
511 * 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.Rfc8439Encrypt
517 */
518inline bool aead_len_ok(std::string_view msg) {
519 return static_cast<std::uint64_t>(msg.size()) <= kMaxAeadMessage;
522std::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;
533bool 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 8439
544 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 a
548 // process that keeps running, should not be able to surface it. See secure_wipe — a plain memset
549 // to a dead local is legally removed by the optimizer, which is the whole point.
550 secure_wipe(k, sizeof k);
551 return true;
554bool 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 buffer
565 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 byte
571 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 out
575 }
576 chacha_xor_into(k, n, 1, ciphertext, ct_len, out);
577 secure_wipe(k, sizeof k);
578 return true;
581std::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 byte
591 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);
598namespace {
599// Portable AES-GCM encrypt over already-expanded round keys (@p nr rounds). Shared by AES-128/256.
600std::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 || 0x00000001
607 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;
618// Portable AES-GCM decrypt (constant-time tag check; "" on mismatch). Shared by AES-128/256.
619std::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 compare
631 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;
640} // namespace
642std::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);
652std::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);
663// AES-256-GCM — the record cipher of TLS_AES_256_GCM_SHA384. Same runtime dispatch as AES-128: the
664// AES-NI/PMULL hardware path when the CPU has it AND the power-on self-test reproduced the known-answer
665// vector (available()), otherwise the portable scalar reference. Both are KAT-tested and cross-checked
666// against each other (CheatahAead.Aes256GcmPortableMatchesHardware).
667std::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);
677std::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);
688} // namespace cheatah::aead