ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
chacha20.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 chacha20.cpp
6 * @brief ChaCha20 (RFC 8439) - implementation. See pc_chacha20.h.
7 */
8
10#include "crypto/crypto_opt.h"
11#include "server/mmgr/secure.h" // the secure pool: nested-cipher working state, wiped on release
12
13// ChaCha20 is a hot, pure-integer (add/xor/rotate) keystream generator. The ESP32-S3 has no usable
14// vector path (its PIE unit has only a *saturating* 32-bit add, `ee.vadds.s32`; ChaCha needs modular
15// wrap-around, so it cannot be vectorized). The real lever is optimization level: the library ships at
16// the arduino framework's -Os, and ChaCha runs ~2.36x faster at -O2 (measured on-device, CCOUNT). It is
17// constant-time by structure (no secret-dependent branches), so forcing a higher level is side-channel
18// safe - see the caveats in crypto_opt.h. Byte-exact; the SIMD investigation is in docs/FEATURE_PERFORMANCE.md.
19//
20// Measured (crypto bench): ChaCha20's additional ~8.8% S3 win at -O3 is carried entirely by -funswitch-loops
21// (bisected on-device); pin just that transform on the -O2 floor. The P4 takes full -O3 via the per-die default
22// (its win is -O3's inline/unroll parameter budget, not one flag).
23#if defined(CONFIG_IDF_TARGET_ESP32S3) && CONFIG_IDF_TARGET_ESP32S3
25#else
27#endif
28
29namespace
30{
31uint32_t rd_le32(const uint8_t *p)
32{
33 return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
34}
35uint32_t rotl32(uint32_t v, int c)
36{
37 return (v << c) | (v >> (32 - c));
38}
39
40#define QR(a, b, c, d) \
41 a += b; \
42 d ^= a; \
43 d = rotl32(d, 16); \
44 c += d; \
45 b ^= c; \
46 b = rotl32(b, 12); \
47 a += b; \
48 d ^= a; \
49 d = rotl32(d, 8); \
50 c += d; \
51 b ^= c; \
52 b = rotl32(b, 7)
53
54// ChaCha20 working set (input state + keystream block + round state), borrowed from the secure pool
55// per op and wiped on release. It runs nested under chachapoly, whose own borrow is still live, so the
56// pool hands this one a separate address by construction - no region assignment needed.
58{
59 uint32_t st[16]; // input state (key / nonce / counter)
60 uint8_t ks[64]; // keystream block
61 uint32_t x[16]; // round state
62};
63static_assert(sizeof(Chacha20Work) <= PC_WORK_CHACHA20,
64 "Chacha20Work outgrew PC_WORK_CHACHA20 - raise it in protocore_config.h, which derives "
65 "PC_SECURE_ARENA_SIZE from it");
66
67// The ChaCha20 core: 20 rounds over w->st, add the original state, serialize LE into @p out (64 bytes).
68void chacha_core(Chacha20Work *w, uint8_t out[64])
69{
70 for (int i = 0; i < 16; i++)
71 {
72 w->x[i] = w->st[i];
73 }
74 for (int i = 0; i < 10; i++) // 10 double-rounds = 20 rounds
75 {
76 QR(w->x[0], w->x[4], w->x[8], w->x[12]);
77 QR(w->x[1], w->x[5], w->x[9], w->x[13]);
78 QR(w->x[2], w->x[6], w->x[10], w->x[14]);
79 QR(w->x[3], w->x[7], w->x[11], w->x[15]);
80 QR(w->x[0], w->x[5], w->x[10], w->x[15]);
81 QR(w->x[1], w->x[6], w->x[11], w->x[12]);
82 QR(w->x[2], w->x[7], w->x[8], w->x[13]);
83 QR(w->x[3], w->x[4], w->x[9], w->x[14]);
84 }
85 for (int i = 0; i < 16; i++)
86 {
87 uint32_t v = w->x[i] + w->st[i];
88 out[4 * i + 0] = (uint8_t)v;
89 out[4 * i + 1] = (uint8_t)(v >> 8);
90 out[4 * i + 2] = (uint8_t)(v >> 16);
91 out[4 * i + 3] = (uint8_t)(v >> 24);
92 }
93}
94
95// "expand 32-byte k"
96const uint32_t SIGMA0 = 0x61707865;
97const uint32_t SIGMA1 = 0x3320646e;
98const uint32_t SIGMA2 = 0x79622d32;
99const uint32_t SIGMA3 = 0x6b206574;
100} // namespace
101
102void pc_chacha20_xor(const uint8_t key[PC_CHACHA20_KEY_LEN], const uint8_t iv[8], uint64_t counter, const uint8_t *in,
103 uint8_t *out, size_t len)
104{
105 SecureBorrow ws_b(sizeof(Chacha20Work), alignof(Chacha20Work));
106 const pc_span &ws = ws_b.span();
107 if (!pc_span_ok(ws))
108 {
109 return; // pool exhausted: an empty borrow is the failure signal, as with malloc
110 }
111 Chacha20Work *w = reinterpret_cast<Chacha20Work *>(ws.buf);
112 w->st[0] = SIGMA0;
113 w->st[1] = SIGMA1;
114 w->st[2] = SIGMA2;
115 w->st[3] = SIGMA3;
116 for (int i = 0; i < 8; i++)
117 {
118 w->st[4 + i] = rd_le32(key + 4 * i);
119 }
120 w->st[14] = rd_le32(iv + 0); // OpenSSH layout: 64-bit nonce in words 14-15
121 w->st[15] = rd_le32(iv + 4);
122 size_t off = 0;
123 while (off < len)
124 {
125 w->st[12] = (uint32_t)(counter & 0xffffffffu); // 64-bit little-endian counter in words 12-13
126 w->st[13] = (uint32_t)(counter >> 32);
127 chacha_core(w, w->ks);
128 size_t n = (len - off < 64) ? (len - off) : 64;
129 for (size_t i = 0; i < n; i++)
130 {
131 out[off + i] = (uint8_t)((in ? in[off + i] : 0) ^ w->ks[i]);
132 }
133 off += n;
134 counter++;
135 }
136}
137
138void pc_chacha20_block_ietf(const uint8_t key[PC_CHACHA20_KEY_LEN], uint32_t counter, const uint8_t nonce[12],
139 uint8_t out[PC_CHACHA20_BLOCK_LEN])
140{
141 SecureBorrow ws_b(sizeof(Chacha20Work), alignof(Chacha20Work));
142 const pc_span &ws = ws_b.span();
143 if (!pc_span_ok(ws))
144 {
145 return; // pool exhausted: an empty borrow is the failure signal, as with malloc
146 }
147 Chacha20Work *w = reinterpret_cast<Chacha20Work *>(ws.buf);
148 w->st[0] = SIGMA0;
149 w->st[1] = SIGMA1;
150 w->st[2] = SIGMA2;
151 w->st[3] = SIGMA3;
152 for (int i = 0; i < 8; i++)
153 {
154 w->st[4 + i] = rd_le32(key + 4 * i);
155 }
156 w->st[12] = counter; // RFC 8439 layout: 32-bit counter, 96-bit nonce
157 w->st[13] = rd_le32(nonce + 0);
158 w->st[14] = rd_le32(nonce + 4);
159 w->st[15] = rd_le32(nonce + 8);
160 chacha_core(w, out);
161}
#define QR(a, b, c, d)
Definition chacha20.cpp:40
void pc_chacha20_block_ietf(const uint8_t key[PC_CHACHA20_KEY_LEN], uint32_t counter, const uint8_t nonce[12], uint8_t out[PC_CHACHA20_BLOCK_LEN])
One 64-byte ChaCha20 keystream block in the RFC 8439 layout (for the KAT).
Definition chacha20.cpp:138
void pc_chacha20_xor(const uint8_t key[PC_CHACHA20_KEY_LEN], const uint8_t iv[8], uint64_t counter, const uint8_t *in, uint8_t *out, size_t len)
XOR len bytes of in with the ChaCha20 keystream into out (OpenSSH layout).
Definition chacha20.cpp:102
ChaCha20 stream cipher (D. J. Bernstein; RFC 8439).
#define PC_CHACHA20_BLOCK_LEN
Definition chacha20.h:31
#define PC_CHACHA20_KEY_LEN
Definition chacha20.h:30
A secure borrow whose acquire and release are one call each.
Definition secure.h:153
const pc_span & span() const
The borrowed region; empty if the pool could not satisfy it.
Definition secure.h:161
Per-translation-unit optimization override for hot, pure-integer crypto.
#define PC_CRYPTO_HOT_UNSWITCH
Definition crypto_opt.h:95
const uint32_t SIGMA3
Definition chacha20.cpp:99
const uint32_t SIGMA1
Definition chacha20.cpp:97
uint32_t rotl32(uint32_t v, int c)
Definition chacha20.cpp:35
void chacha_core(Chacha20Work *w, uint8_t out[64])
Definition chacha20.cpp:68
const uint32_t SIGMA0
Definition chacha20.cpp:96
uint32_t rd_le32(const uint8_t *p)
Definition chacha20.cpp:31
const uint32_t SIGMA2
Definition chacha20.cpp:98
#define PC_WORK_CHACHA20
Secure pool accessor - borrows that hold key material.
bool pc_span_ok(const pc_span &s)
True when the span refers to real storage and every write so far has fit.
Definition span.h:114
A writable byte region: the storage, the capacity that belongs to it, and what has been produced into...
Definition span.h:65
uint8_t * buf
first byte, or nullptr when the region could not be obtained
Definition span.h:66