DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
ssh_aes256ctr.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file ssh_aes256ctr.cpp
6 * @brief AES-256-CTR implementation.
7 *
8 * Arduino path: delegates block encryption to mbedtls_aes_crypt_ecb(), which
9 * the ESP32 mbedtls port routes to the hardware AES accelerator.
10 *
11 * Native path: compact software AES-256 using only the 256-byte forward
12 * S-box and GF(2^8) multiply-by-2/3 for MixColumns. No large lookup tables.
13 */
14
16#include <string.h>
17
18// ============================================================================
19// ARDUINO - hardware-accelerated path via mbedtls
20// ============================================================================
21
22#ifdef ARDUINO
23
24void ssh_aes256ctr_init(SshAesCtrCtx *ctx, const uint8_t key[32], const uint8_t iv[16])
25{
26 mbedtls_aes_init(&ctx->_mbed);
27 mbedtls_aes_setkey_enc(&ctx->_mbed, key, 256);
28 memcpy(ctx->counter, iv, 16);
29 memset(ctx->keystream, 0, 16);
30 ctx->pos = 0;
31}
32
33void ssh_aes256ctr_crypt(SshAesCtrCtx *ctx, const uint8_t *in, uint8_t *out, size_t len)
34{
35 // Encrypt the whole buffer in one mbedtls call: it acquires the HW AES
36 // engine / loads the key once and manages the big-endian counter, keystream
37 // block, and intra-block offset itself (our fields map 1:1 to its
38 // nonce_counter / stream_block / nc_off). This replaces the previous
39 // per-16-byte-block mbedtls_aes_crypt_ecb() loop, whose per-block setup
40 // dominated bulk throughput on ESP32.
41 size_t nc_off = ctx->pos;
42 mbedtls_aes_crypt_ctr(&ctx->_mbed, len, &nc_off, ctx->counter, ctx->keystream, in, out);
43 ctx->pos = (uint8_t)nc_off; // 0..15
44}
45
47{
48 mbedtls_aes_free(&ctx->_mbed);
49 volatile uint8_t *p = (volatile uint8_t *)ctx;
50 for (size_t i = 0; i < sizeof(SshAesCtrCtx); i++)
51 p[i] = 0;
52}
53
54// ============================================================================
55// NATIVE - software AES-256 (for host-side unit tests only)
56// ============================================================================
57
58#else
59
60// ---------------------------------------------------------------------------
61// AES S-box (FIPS 197 Figure 7)
62// ---------------------------------------------------------------------------
63
65
66// AES round constants for key expansion (Rcon[1..10], index 0 unused)
67static const uint8_t RCON[11] = {0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36};
68
69// GF(2^8) multiply by 2 (x * 0x02 mod 0x1b irreducible polynomial)
70static inline uint8_t xtime(uint8_t a)
71{
72 return (uint8_t)((a << 1) ^ ((a >> 7) ? 0x1bu : 0x00u));
73}
74
75// ---------------------------------------------------------------------------
76// AES-256 key schedule expansion (FIPS 197 §5.2)
77// AES-256 has Nk=8, Nr=14, producing 15 round keys × 4 words = 60 words.
78// ---------------------------------------------------------------------------
79
80// SubWord: apply the S-box to each of the four bytes of a word.
81static uint32_t aes_sub_word(uint32_t w)
82{
83 return ((uint32_t)DET_AES_SBOX[w >> 24] << 24) | ((uint32_t)DET_AES_SBOX[(w >> 16) & 0xff] << 16) |
84 ((uint32_t)DET_AES_SBOX[(w >> 8) & 0xff] << 8) | (uint32_t)DET_AES_SBOX[w & 0xff];
85}
86// RotWord: cyclically rotate a word one byte left ([a,b,c,d] -> [b,c,d,a]).
87static uint32_t aes_rot_word(uint32_t w)
88{
89 return (w << 8) | (w >> 24);
90}
91
92static void aes256_key_expand(const uint8_t key[32], uint32_t rk[60])
93{
94 // The first Nk=8 words of the schedule are the cipher key verbatim, read as
95 // big-endian 32-bit words.
96 for (int i = 0; i < 8; i++)
97 {
98 rk[i] = ((uint32_t)key[4 * i] << 24) | ((uint32_t)key[4 * i + 1] << 16) | ((uint32_t)key[4 * i + 2] << 8) |
99 (uint32_t)key[4 * i + 3];
100 }
101
102 // Each later word is the word Nk positions back XOR a transform of the
103 // previous word. For AES-256 (Nk=8) there are two special positions per
104 // group of 8 words (FIPS 197 §5.2):
105 for (int i = 8; i < 60; i++)
106 {
107 uint32_t t = rk[i - 1];
108 if (i % 8 == 0)
109 // Start of a new key: RotWord, SubWord, then XOR the round constant.
110 t = aes_sub_word(aes_rot_word(t)) ^ ((uint32_t)RCON[i / 8] << 24);
111 else if (i % 8 == 4)
112 // Mid-key SubWord step unique to 256-bit keys.
113 t = aes_sub_word(t);
114 rk[i] = rk[i - 8] ^ t;
115 }
116}
117
118// ---------------------------------------------------------------------------
119// AES block encrypt (FIPS 197 §5.1) - CTR mode only needs Cipher (encrypt).
120// State is column-major: s[col*4 + row].
121// ---------------------------------------------------------------------------
122
123static void aes256_encrypt_block(const uint32_t rk[60], const uint8_t in[16], uint8_t out[16])
124{
125 uint8_t s[16];
126
127 // AddRoundKey (round 0)
128 for (int i = 0; i < 16; i++)
129 s[i] = in[i] ^ (uint8_t)(rk[i / 4] >> (24 - (i % 4) * 8));
130
131 // Rounds 1–13: SubBytes + ShiftRows + MixColumns + AddRoundKey
132 for (int r = 1; r <= 13; r++)
133 {
134 // SubBytes
135 for (int i = 0; i < 16; i++)
136 s[i] = DET_AES_SBOX[s[i]];
137
138 // ShiftRows: the state is column-major (s[col*4 + row]), so row r is
139 // {s[r], s[4+r], s[8+r], s[12+r]}. Row 0 is unchanged; rows 1/2/3 are
140 // cyclically left-rotated by 1/2/3 bytes respectively.
141 uint8_t t;
142 t = s[1]; // row 1 <<< 1
143 s[1] = s[5];
144 s[5] = s[9];
145 s[9] = s[13];
146 s[13] = t;
147 t = s[2]; // row 2 <<< 2 (swap the two pairs)
148 s[2] = s[10];
149 s[10] = t;
150 t = s[6];
151 s[6] = s[14];
152 s[14] = t;
153 t = s[15]; // row 3 <<< 3 (== >>> 1)
154 s[15] = s[11];
155 s[11] = s[7];
156 s[7] = s[3];
157 s[3] = t;
158
159 // MixColumns: multiply each column by the fixed AES polynomial matrix
160 // over GF(2^8). Using e = a^b^c^d, the standard identity gives each
161 // output byte as in ^ e ^ xtime(in ^ next), which needs only one
162 // GF multiply-by-2 (xtime) per term.
163 for (int c = 0; c < 4; c++)
164 {
165 uint8_t a = s[c * 4];
166 uint8_t b = s[c * 4 + 1];
167 uint8_t cc = s[c * 4 + 2];
168 uint8_t d = s[c * 4 + 3];
169 uint8_t e = a ^ b ^ cc ^ d;
170 s[c * 4] = a ^ e ^ xtime(a ^ b);
171 s[c * 4 + 1] = b ^ e ^ xtime(b ^ cc);
172 s[c * 4 + 2] = cc ^ e ^ xtime(cc ^ d);
173 s[c * 4 + 3] = d ^ e ^ xtime(d ^ a);
174 }
175
176 // AddRoundKey
177 for (int i = 0; i < 16; i++)
178 s[i] ^= (uint8_t)(rk[r * 4 + i / 4] >> (24 - (i % 4) * 8));
179 }
180
181 // Final round: SubBytes + ShiftRows + AddRoundKey (no MixColumns)
182 for (int i = 0; i < 16; i++)
183 s[i] = DET_AES_SBOX[s[i]];
184
185 uint8_t t;
186 t = s[1];
187 s[1] = s[5];
188 s[5] = s[9];
189 s[9] = s[13];
190 s[13] = t;
191 t = s[2];
192 s[2] = s[10];
193 s[10] = t;
194 t = s[6];
195 s[6] = s[14];
196 s[14] = t;
197 t = s[15];
198 s[15] = s[11];
199 s[11] = s[7];
200 s[7] = s[3];
201 s[3] = t;
202
203 for (int i = 0; i < 16; i++)
204 s[i] ^= (uint8_t)(rk[56 + i / 4] >> (24 - (i % 4) * 8));
205
206 memcpy(out, s, 16);
207}
208
209// ---------------------------------------------------------------------------
210// Public API (native software path)
211// ---------------------------------------------------------------------------
212
213void ssh_aes256ctr_init(SshAesCtrCtx *ctx, const uint8_t key[32], const uint8_t iv[16])
214{
215 aes256_key_expand(key, ctx->rk);
216 memcpy(ctx->counter, iv, 16);
217 memset(ctx->keystream, 0, 16);
218 ctx->pos = 0;
219}
220
221static void aes_block_sw(SshAesCtrCtx *ctx)
222{
223 aes256_encrypt_block(ctx->rk, ctx->counter, ctx->keystream);
224 for (int j = 15; j >= 0; j--)
225 if (++ctx->counter[j])
226 break;
227}
228
229void ssh_aes256ctr_crypt(SshAesCtrCtx *ctx, const uint8_t *in, uint8_t *out, size_t len)
230{
231 for (size_t i = 0; i < len; i++)
232 {
233 if (ctx->pos == 0)
234 aes_block_sw(ctx);
235 out[i] = in[i] ^ ctx->keystream[ctx->pos];
236 ctx->pos = (uint8_t)((ctx->pos + 1u) & 0x0fu);
237 }
238}
239
241{
242 volatile uint8_t *p = (volatile uint8_t *)ctx;
243 for (size_t i = 0; i < sizeof(SshAesCtrCtx); i++)
244 p[i] = 0;
245}
246
247#endif // ARDUINO
The AES forward S-box (FIPS 197 Figure 7) - one shared copy.
void ssh_aes256ctr_init(SshAesCtrCtx *ctx, const uint8_t key[32], const uint8_t iv[16])
Initialize an AES-256-CTR context.
void ssh_aes256ctr_wipe(SshAesCtrCtx *ctx)
Zero the key schedule and all context fields.
void ssh_aes256ctr_crypt(SshAesCtrCtx *ctx, const uint8_t *in, uint8_t *out, size_t len)
Encrypt or decrypt len bytes in-place (or src→dst).
AES-256-CTR stream cipher context and API.
uint8_t counter[16]
Current CTR block (big-endian 128-bit counter).
uint8_t pos
Next byte position within keystream[].
mbedtls_aes_context _mbed
mbedtls context with pre-expanded key schedule.
uint8_t keystream[16]
Buffered keystream from last AES-ECB call.