DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
base64.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 base64.cpp
6 * @brief Base64 encoder/decoder implementation.
7 *
8 * **Encode** is a single portable software codec (RFC 4648) on every target: it is byte-identical to
9 * mbedTLS's but ~20x faster on the ESP32-S3 (docs/FEATURE_PERFORMANCE.md section 2), and it only ever
10 * encodes public data here (the SHA-1 in the WebSocket accept), so a data-dependent table lookup is fine.
11 *
12 * **Decode** is split by target. It is the only path that touches a secret (the Basic-auth credentials it
13 * decodes, RFC 7617), so on the ESP32 it delegates to mbedTLS's **constant-time** base64 decoder
14 * (`mbedtls_ct_base64_dec_value`, which evaluates every alphabet range with branchless masks so the timing
15 * / cache pattern does not leak the bytes). On the native test target it uses the portable software
16 * decoder. mbedTLS decode is ~8x slower but runs once per authenticated request, not in a byte loop.
17 * (JWT / OIDC use base64url below, which has always been the software codec.)
18 */
19
20#include "base64.h"
21
22#include <stddef.h>
23
24static const char B64_TABLE[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
25
26void base64_encode(const uint8_t *src, size_t src_len, char *dst)
27{
28 size_t i = 0;
29 size_t j = 0;
30
31 // Full groups: pack 3 input bytes into a 24-bit value, emit as 4 x 6-bit
32 // characters (most-significant 6 bits first).
33 while (i + 2 < src_len)
34 {
35 uint32_t v = ((uint32_t)src[i] << 16) | ((uint32_t)src[i + 1] << 8) | (uint32_t)src[i + 2];
36 dst[j++] = B64_TABLE[(v >> 18) & 0x3F];
37 dst[j++] = B64_TABLE[(v >> 12) & 0x3F];
38 dst[j++] = B64_TABLE[(v >> 6) & 0x3F];
39 dst[j++] = B64_TABLE[(v) & 0x3F];
40 i += 3;
41 }
42
43 // Tail of 1 or 2 leftover bytes: emit 2 or 3 characters, '='-padded to 4.
44 if (i < src_len)
45 {
46 uint32_t v = (uint32_t)src[i] << 16;
47 if (i + 1 < src_len)
48 v |= (uint32_t)src[i + 1] << 8;
49
50 dst[j++] = B64_TABLE[(v >> 18) & 0x3F];
51 dst[j++] = B64_TABLE[(v >> 12) & 0x3F];
52 dst[j++] = (i + 1 < src_len) ? B64_TABLE[(v >> 6) & 0x3F] : '='; // 3rd char only if 2 input bytes
53 dst[j++] = '=';
54 }
55
56 dst[j] = '\0';
57}
58
59#ifdef ARDUINO
60
61// --- ESP32: mbedTLS's constant-time decode for the secret Basic-auth path -------------------------------
62#include "mbedtls/base64.h"
63#include <string.h>
64
65size_t base64_decode(const char *src, uint8_t *dst, size_t dst_cap)
66{
67 size_t src_len = strnlen(src, ((dst_cap + 2) / 3) * 4 + 4); // max base64 length decoding within dst_cap
68 size_t olen;
69 // Pass the caller's true capacity; mbedtls returns BUFFER_TOO_SMALL (mapped to 0) rather than overrun.
70 int ret = mbedtls_base64_decode(dst, dst_cap, &olen, (const unsigned char *)src, src_len);
71 return (ret == 0) ? olen : 0;
72}
73
74#else
75
76// --- Native / test: portable software decode ------------------------------------------------------------
77static int b64_val(char c)
78{
79 if (c >= 'A' && c <= 'Z')
80 return c - 'A';
81 if (c >= 'a' && c <= 'z')
82 return c - 'a' + 26;
83 if (c >= '0' && c <= '9')
84 return c - '0' + 52;
85 if (c == '+')
86 return 62;
87 if (c == '/')
88 return 63;
89 // '=' is NOT a value here - padding is validated positionally by the
90 // decoder (1-2 '=' only at the end of the final quad).
91 return -1;
92}
93
94size_t base64_decode(const char *src, uint8_t *dst, size_t dst_cap)
95{
96 size_t out = 0;
97
98 // Canonical Base64: process complete 4-character quads. A truncated final
99 // group, any non-base64 character, or misplaced '=' padding fails (return 0).
100 while (*src)
101 {
102 char c0 = src[0];
103 char c1 = src[1];
104 char c2 = src[2];
105 char c3 = src[3];
106 if (!c1 || !c2 || !c3)
107 return 0; // input length must be a multiple of 4
108
109 // The first two characters of every quad must be real Base64 (never pad).
110 int a = b64_val(c0);
111 int b = b64_val(c1);
112 if (a < 0 || b < 0)
113 return 0;
114
115 // '=' is permitted only as 1-2 trailing pad characters of the FINAL quad.
116 bool pad2 = (c2 == '=');
117 bool pad3 = (c3 == '=');
118 if (pad2 && !pad3)
119 return 0; // "xx=y" - a single pad must be in position 4, not 3
120 if ((pad2 || pad3) && src[4] != '\0')
121 return 0; // padding only allowed in the last quad
122
123 int c = pad2 ? 0 : b64_val(c2);
124 int d = pad3 ? 0 : b64_val(c3);
125 if (c < 0 || d < 0)
126 return 0;
127
128 // Reassemble the 24-bit group and slice it back into bytes. Each write is
129 // bounded by dst_cap; an over-capacity decode fails rather than overruns.
130 if (out >= dst_cap)
131 return 0;
132 dst[out++] = (uint8_t)((a << 2) | (b >> 4));
133 if (!pad2)
134 {
135 if (out >= dst_cap)
136 return 0;
137 dst[out++] = (uint8_t)((b << 4) | (c >> 2));
138 }
139 if (!pad3)
140 {
141 if (out >= dst_cap)
142 return 0;
143 dst[out++] = (uint8_t)((c << 6) | d);
144 }
145
146 src += 4;
147 }
148
149 return out;
150}
151
152#endif // ARDUINO
153
154// ---------------------------------------------------------------------------
155// Base64url (RFC 4648 section 5): '-' / '_' replace '+' / '/', no '=' padding.
156// Shared by JWT (HS256) and OIDC (RS256) so the alphabet lives in one place.
157// Platform-independent: encode builds on base64_encode then rewrites the two
158// differing characters in place; decode is a direct streaming decoder that
159// accepts the URL and standard alphabets and stops at '=', so it needs no temp
160// buffer or re-padding.
161// ---------------------------------------------------------------------------
162
163size_t base64url_encode(const uint8_t *src, size_t src_len, char *dst)
164{
165 base64_encode(src, src_len, dst); // standard base64, '='-padded, NUL-terminated
166 size_t n = 0;
167 for (size_t i = 0; dst[i]; i++)
168 {
169 char c = dst[i];
170 if (c == '=')
171 break; // base64url carries no padding
172 if (c == '+')
173 dst[i] = '-';
174 else if (c == '/')
175 dst[i] = '_';
176 n = i + 1;
177 }
178 dst[n] = '\0';
179 return n;
180}
181
182static int base64url_val(char c)
183{
184 if (c >= 'A' && c <= 'Z')
185 return c - 'A';
186 if (c >= 'a' && c <= 'z')
187 return c - 'a' + 26;
188 if (c >= '0' && c <= '9')
189 return c - '0' + 52;
190 // RFC 4648 section 5: the URL-safe alphabet uses '-'/'_'. The standard '+'/'/'
191 // are NOT valid here; a strict JWS/JWT decoder rejects them rather than
192 // silently treating two alphabets as one (RFC 7515).
193 if (c == '-')
194 return 62;
195 if (c == '_')
196 return 63;
197 return -1;
198}
199
200size_t base64url_decode(const char *src, size_t src_len, uint8_t *dst, size_t dst_cap)
201{
202 size_t o = 0;
203 uint32_t acc = 0;
204 int bits = 0;
205 for (size_t i = 0; i < src_len; i++)
206 {
207 if (src[i] == '=')
208 break; // optional padding ends the input
209 int v = base64url_val(src[i]);
210 if (v < 0)
211 return 0;
212 acc = (acc << 6) | (uint32_t)v;
213 bits += 6;
214 if (bits >= 8)
215 {
216 bits -= 8;
217 if (o >= dst_cap)
218 return 0;
219 dst[o++] = (uint8_t)((acc >> bits) & 0xFF);
220 }
221 }
222 return o;
223}
void base64_encode(const uint8_t *src, size_t src_len, char *dst)
Encode src_len bytes of src as Base64.
Definition base64.cpp:26
size_t base64url_decode(const char *src, size_t src_len, uint8_t *dst, size_t dst_cap)
Decode src_len characters of base64url (RFC 4648 section 5, '-'/'_' alphabet; an '=' ends the input).
Definition base64.cpp:200
size_t base64_decode(const char *src, uint8_t *dst, size_t dst_cap)
Decode a null-terminated Base64 string.
Definition base64.cpp:65
size_t base64url_encode(const uint8_t *src, size_t src_len, char *dst)
Encode src_len bytes as base64url (RFC 4648 section 5): '-' / '_' in place of '+' / '/',...
Definition base64.cpp:163
Base64 encoder/decoder.