Skip to content

Commit 0628011

Browse files
🔤 Support for Base64url with case insensitive padding (#219)
* stl algs for `alphabet::index` * refact `jwt::base` to a namespace instead of a class * recursive count passing * test complicated scenarios * count padding of variable length * new percent enconding helper * spelling * fix gcc 4.8 * trying to avoid the runtime problem on init list with dynamic values * :qRevert "trying to avoid the runtime problem on init list with dynamic values" This reverts commit b9be15e. * switch to vector * linter * missing header * fix gcc 4.8 more * more tests with new alphabet
1 parent 484bdb0 commit 0628011

File tree

2 files changed

+233
-120
lines changed

2 files changed

+233
-120
lines changed

include/jwt-cpp/base.h

Lines changed: 181 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
#ifndef JWT_CPP_BASE_H
22
#define JWT_CPP_BASE_H
33

4+
#include <algorithm>
45
#include <array>
56
#include <stdexcept>
67
#include <string>
8+
#include <vector>
79

810
#ifdef __has_cpp_attribute
911
#if __has_cpp_attribute(fallthrough)
@@ -21,7 +23,10 @@ namespace jwt {
2123
*/
2224
namespace alphabet {
2325
/**
24-
* \brief valid list of characted when working with [Base64](https://tools.ietf.org/html/rfc3548)
26+
* \brief valid list of character when working with [Base64](https://datatracker.ietf.org/doc/html/rfc4648#section-4)
27+
*
28+
* As directed in [X.509 Parameter](https://datatracker.ietf.org/doc/html/rfc7517#section-4.7) certificate chains are
29+
* base64-encoded as per [Section 4 of RFC4648](https://datatracker.ietf.org/doc/html/rfc4648#section-4)
2530
*/
2631
struct base64 {
2732
static const std::array<char, 64>& data() {
@@ -38,7 +43,13 @@ namespace jwt {
3843
}
3944
};
4045
/**
41-
* \brief valid list of characted when working with [Base64URL](https://tools.ietf.org/html/rfc4648)
46+
* \brief valid list of character when working with [Base64URL](https://tools.ietf.org/html/rfc4648#section-5)
47+
*
48+
* As directed by [RFC 7519 Terminology](https://datatracker.ietf.org/doc/html/rfc7519#section-2) set the definition of Base64URL
49+
* encoding as that in [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515#section-2) that states:
50+
*
51+
* > Base64 encoding using the URL- and filename-safe character set defined in
52+
* > [Section 5 of RFC 4648 RFC4648](https://tools.ietf.org/html/rfc4648#section-5), with all trailing '=' characters omitted
4253
*/
4354
struct base64url {
4455
static const std::array<char, 64>& data() {
@@ -54,155 +65,205 @@ namespace jwt {
5465
return fill;
5566
}
5667
};
68+
namespace helper {
69+
/**
70+
* @brief A General purpose base64url alphabet respecting the
71+
* [URI Case Normalization](https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.2.1)
72+
*
73+
* This is useful in situations outside of JWT encoding/decoding and is provided as a helper
74+
*/
75+
struct base64url_percent_encoding {
76+
static const std::array<char, 64>& data() {
77+
static constexpr std::array<char, 64> data{
78+
{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
79+
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
80+
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
81+
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_'}};
82+
return data;
83+
}
84+
static const std::initializer_list<std::string>& fill() {
85+
static std::initializer_list<std::string> fill{"%3D", "%3d"};
86+
return fill;
87+
}
88+
};
89+
} // namespace helper
90+
91+
inline uint32_t index(const std::array<char, 64>& alphabet, char symbol) {
92+
auto itr = std::find_if(alphabet.cbegin(), alphabet.cend(), [symbol](char c) { return c == symbol; });
93+
if (itr == alphabet.cend()) { throw std::runtime_error("Invalid input: not within alphabet"); }
94+
95+
return std::distance(alphabet.cbegin(), itr);
96+
}
5797
} // namespace alphabet
5898

5999
/**
60-
* \brief Alphabet generic methods for working with encoding/decoding the base64 family
100+
* \brief A collection of fellable functions for working with base64 and base64url
61101
*/
62-
class base {
63-
public:
64-
template<typename T>
65-
static std::string encode(const std::string& bin) {
66-
return encode(bin, T::data(), T::fill());
67-
}
68-
template<typename T>
69-
static std::string decode(const std::string& base) {
70-
return decode(base, T::data(), T::fill());
71-
}
72-
template<typename T>
73-
static std::string pad(const std::string& base) {
74-
return pad(base, T::fill());
75-
}
76-
template<typename T>
77-
static std::string trim(const std::string& base) {
78-
return trim(base, T::fill());
79-
}
102+
namespace base {
80103

81-
private:
82-
static std::string encode(const std::string& bin, const std::array<char, 64>& alphabet,
83-
const std::string& fill) {
84-
size_t size = bin.size();
85-
std::string res;
104+
namespace details {
105+
struct padding {
106+
size_t count = 0;
107+
size_t length = 0;
86108

87-
// clear incomplete bytes
88-
size_t fast_size = size - size % 3;
89-
for (size_t i = 0; i < fast_size;) {
90-
uint32_t octet_a = static_cast<unsigned char>(bin[i++]);
91-
uint32_t octet_b = static_cast<unsigned char>(bin[i++]);
92-
uint32_t octet_c = static_cast<unsigned char>(bin[i++]);
109+
padding() = default;
110+
padding(size_t count, size_t length) : count(count), length(length) {}
93111

94-
uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c;
112+
padding operator+(const padding& p) { return padding(count + p.count, length + p.length); }
95113

96-
res += alphabet[(triple >> 3 * 6) & 0x3F];
97-
res += alphabet[(triple >> 2 * 6) & 0x3F];
98-
res += alphabet[(triple >> 1 * 6) & 0x3F];
99-
res += alphabet[(triple >> 0 * 6) & 0x3F];
100-
}
114+
friend bool operator==(const padding& lhs, const padding& rhs) {
115+
return lhs.count == rhs.count && lhs.length == rhs.length;
116+
}
117+
};
118+
119+
inline padding count_padding(const std::string& base, const std::vector<std::string>& fills) {
120+
for (const auto& fill : fills) {
121+
if (base.size() < fill.size()) continue;
122+
// Does the end of the input exactly match the fill pattern?
123+
if (base.substr(base.size() - fill.size()) == fill) {
124+
return padding{1, fill.length()} +
125+
count_padding(base.substr(0, base.size() - fill.size()), fills);
126+
}
127+
}
101128

102-
if (fast_size == size) return res;
103-
104-
size_t mod = size % 3;
105-
106-
uint32_t octet_a = fast_size < size ? static_cast<unsigned char>(bin[fast_size++]) : 0;
107-
uint32_t octet_b = fast_size < size ? static_cast<unsigned char>(bin[fast_size++]) : 0;
108-
uint32_t octet_c = fast_size < size ? static_cast<unsigned char>(bin[fast_size++]) : 0;
109-
110-
uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c;
111-
112-
switch (mod) {
113-
case 1:
114-
res += alphabet[(triple >> 3 * 6) & 0x3F];
115-
res += alphabet[(triple >> 2 * 6) & 0x3F];
116-
res += fill;
117-
res += fill;
118-
break;
119-
case 2:
120-
res += alphabet[(triple >> 3 * 6) & 0x3F];
121-
res += alphabet[(triple >> 2 * 6) & 0x3F];
122-
res += alphabet[(triple >> 1 * 6) & 0x3F];
123-
res += fill;
124-
break;
125-
default: break;
129+
return {};
126130
}
127131

128-
return res;
129-
}
132+
inline std::string encode(const std::string& bin, const std::array<char, 64>& alphabet,
133+
const std::string& fill) {
134+
size_t size = bin.size();
135+
std::string res;
136+
137+
// clear incomplete bytes
138+
size_t fast_size = size - size % 3;
139+
for (size_t i = 0; i < fast_size;) {
140+
uint32_t octet_a = static_cast<unsigned char>(bin[i++]);
141+
uint32_t octet_b = static_cast<unsigned char>(bin[i++]);
142+
uint32_t octet_c = static_cast<unsigned char>(bin[i++]);
143+
144+
uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c;
145+
146+
res += alphabet[(triple >> 3 * 6) & 0x3F];
147+
res += alphabet[(triple >> 2 * 6) & 0x3F];
148+
res += alphabet[(triple >> 1 * 6) & 0x3F];
149+
res += alphabet[(triple >> 0 * 6) & 0x3F];
150+
}
151+
152+
if (fast_size == size) return res;
130153

131-
static std::string decode(const std::string& base, const std::array<char, 64>& alphabet,
132-
const std::string& fill) {
133-
size_t size = base.size();
134-
135-
size_t fill_cnt = 0;
136-
while (size > fill.size()) {
137-
if (base.substr(size - fill.size(), fill.size()) == fill) {
138-
fill_cnt++;
139-
size -= fill.size();
140-
if (fill_cnt > 2) throw std::runtime_error("Invalid input: too much fill");
141-
} else
154+
size_t mod = size % 3;
155+
156+
uint32_t octet_a = fast_size < size ? static_cast<unsigned char>(bin[fast_size++]) : 0;
157+
uint32_t octet_b = fast_size < size ? static_cast<unsigned char>(bin[fast_size++]) : 0;
158+
uint32_t octet_c = fast_size < size ? static_cast<unsigned char>(bin[fast_size++]) : 0;
159+
160+
uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c;
161+
162+
switch (mod) {
163+
case 1:
164+
res += alphabet[(triple >> 3 * 6) & 0x3F];
165+
res += alphabet[(triple >> 2 * 6) & 0x3F];
166+
res += fill;
167+
res += fill;
142168
break;
169+
case 2:
170+
res += alphabet[(triple >> 3 * 6) & 0x3F];
171+
res += alphabet[(triple >> 2 * 6) & 0x3F];
172+
res += alphabet[(triple >> 1 * 6) & 0x3F];
173+
res += fill;
174+
break;
175+
default: break;
176+
}
177+
178+
return res;
143179
}
144180

145-
if ((size + fill_cnt) % 4 != 0) throw std::runtime_error("Invalid input: incorrect total size");
181+
inline std::string decode(const std::string& base, const std::array<char, 64>& alphabet,
182+
const std::vector<std::string>& fill) {
183+
const auto pad = count_padding(base, fill);
184+
if (pad.count > 2) throw std::runtime_error("Invalid input: too much fill");
146185

147-
size_t out_size = size / 4 * 3;
148-
std::string res;
149-
res.reserve(out_size);
186+
const size_t size = base.size() - pad.length;
187+
if ((size + pad.count) % 4 != 0) throw std::runtime_error("Invalid input: incorrect total size");
150188

151-
auto get_sextet = [&](size_t offset) {
152-
for (size_t i = 0; i < alphabet.size(); i++) {
153-
if (alphabet[i] == base[offset]) return static_cast<uint32_t>(i);
189+
size_t out_size = size / 4 * 3;
190+
std::string res;
191+
res.reserve(out_size);
192+
193+
auto get_sextet = [&](size_t offset) { return alphabet::index(alphabet, base[offset]); };
194+
195+
size_t fast_size = size - size % 4;
196+
for (size_t i = 0; i < fast_size;) {
197+
uint32_t sextet_a = get_sextet(i++);
198+
uint32_t sextet_b = get_sextet(i++);
199+
uint32_t sextet_c = get_sextet(i++);
200+
uint32_t sextet_d = get_sextet(i++);
201+
202+
uint32_t triple =
203+
(sextet_a << 3 * 6) + (sextet_b << 2 * 6) + (sextet_c << 1 * 6) + (sextet_d << 0 * 6);
204+
205+
res += static_cast<char>((triple >> 2 * 8) & 0xFFU);
206+
res += static_cast<char>((triple >> 1 * 8) & 0xFFU);
207+
res += static_cast<char>((triple >> 0 * 8) & 0xFFU);
154208
}
155-
throw std::runtime_error("Invalid input: not within alphabet");
156-
};
157209

158-
size_t fast_size = size - size % 4;
159-
for (size_t i = 0; i < fast_size;) {
160-
uint32_t sextet_a = get_sextet(i++);
161-
uint32_t sextet_b = get_sextet(i++);
162-
uint32_t sextet_c = get_sextet(i++);
163-
uint32_t sextet_d = get_sextet(i++);
210+
if (pad.count == 0) return res;
211+
212+
uint32_t triple = (get_sextet(fast_size) << 3 * 6) + (get_sextet(fast_size + 1) << 2 * 6);
164213

165-
uint32_t triple = (sextet_a << 3 * 6) + (sextet_b << 2 * 6) + (sextet_c << 1 * 6) + (sextet_d << 0 * 6);
214+
switch (pad.count) {
215+
case 1:
216+
triple |= (get_sextet(fast_size + 2) << 1 * 6);
217+
res += static_cast<char>((triple >> 2 * 8) & 0xFFU);
218+
res += static_cast<char>((triple >> 1 * 8) & 0xFFU);
219+
break;
220+
case 2: res += static_cast<char>((triple >> 2 * 8) & 0xFFU); break;
221+
default: break;
222+
}
166223

167-
res += static_cast<char>((triple >> 2 * 8) & 0xFFU);
168-
res += static_cast<char>((triple >> 1 * 8) & 0xFFU);
169-
res += static_cast<char>((triple >> 0 * 8) & 0xFFU);
224+
return res;
170225
}
171226

172-
if (fill_cnt == 0) return res;
227+
inline std::string decode(const std::string& base, const std::array<char, 64>& alphabet,
228+
const std::string& fill) {
229+
return decode(base, alphabet, std::vector<std::string>{fill});
230+
}
173231

174-
uint32_t triple = (get_sextet(fast_size) << 3 * 6) + (get_sextet(fast_size + 1) << 2 * 6);
232+
inline std::string pad(const std::string& base, const std::string& fill) {
233+
std::string padding;
234+
switch (base.size() % 4) {
235+
case 1: padding += fill; JWT_FALLTHROUGH;
236+
case 2: padding += fill; JWT_FALLTHROUGH;
237+
case 3: padding += fill; JWT_FALLTHROUGH;
238+
default: break;
239+
}
175240

176-
switch (fill_cnt) {
177-
case 1:
178-
triple |= (get_sextet(fast_size + 2) << 1 * 6);
179-
res += static_cast<char>((triple >> 2 * 8) & 0xFFU);
180-
res += static_cast<char>((triple >> 1 * 8) & 0xFFU);
181-
break;
182-
case 2: res += static_cast<char>((triple >> 2 * 8) & 0xFFU); break;
183-
default: break;
241+
return base + padding;
184242
}
185243

186-
return res;
187-
}
188-
189-
static std::string pad(const std::string& base, const std::string& fill) {
190-
std::string padding;
191-
switch (base.size() % 4) {
192-
case 1: padding += fill; JWT_FALLTHROUGH;
193-
case 2: padding += fill; JWT_FALLTHROUGH;
194-
case 3: padding += fill; JWT_FALLTHROUGH;
195-
default: break;
244+
inline std::string trim(const std::string& base, const std::string& fill) {
245+
auto pos = base.find(fill);
246+
return base.substr(0, pos);
196247
}
248+
} // namespace details
197249

198-
return base + padding;
250+
template<typename T>
251+
std::string encode(const std::string& bin) {
252+
return details::encode(bin, T::data(), T::fill());
199253
}
200-
201-
static std::string trim(const std::string& base, const std::string& fill) {
202-
auto pos = base.find(fill);
203-
return base.substr(0, pos);
254+
template<typename T>
255+
std::string decode(const std::string& base) {
256+
return details::decode(base, T::data(), T::fill());
257+
}
258+
template<typename T>
259+
std::string pad(const std::string& base) {
260+
return details::pad(base, T::fill());
261+
}
262+
template<typename T>
263+
std::string trim(const std::string& base) {
264+
return details::trim(base, T::fill());
204265
}
205-
};
266+
} // namespace base
206267
} // namespace jwt
207268

208269
#endif

0 commit comments

Comments
 (0)