Skip to content

Commit 827ede3

Browse files
committed
docs(encoding): clarify Base64 padding behavior
Mention that non-strict Base64 decoder tolerates missing '=' padding.
1 parent 8edcb23 commit 827ede3

4 files changed

Lines changed: 106 additions & 18 deletions

File tree

README.md

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -255,15 +255,34 @@ bool v2 = hmac::is_token_valid(t2, secret_key, fingerprint, 60);
255255

256256
`hmac_cpp::encoding` provides simple conversions:
257257

258-
* **Base64** — standard `+/` and URL-safe `-_` alphabets; optional `strict` mode
259-
(rejects whitespace and mixed padding) and ability to decode without `=`.
260-
* **Base32** — RFC 4648 alphabet `A–Z2–7`; encoder outputs upper-case, decoder
261-
accepts lower-case and ignores spaces/CR/LF when `strict=false`.
262-
* **Base36** — non-standard human-readable IDs using `0–9A–Z`; keeps leading
263-
zero bytes by prefixing `'0'` and maps a single `\x00` to "0".
258+
* **Base64** — standard `+/` and URL-safe `-_` alphabets; `pad=true/false` toggles
259+
`=` padding. `strict=true` rejects whitespace, mixed padding and `+`/`/` when
260+
using the URL alphabet; `strict=false` ignores ASCII spaces, accepts these
261+
aliases and tolerates missing padding.
262+
* **Base32** — RFC 4648 alphabet `A–Z2–7`; encoder emits upper-case. Decoder
263+
with `strict=true` requires `=` padding and upper-case; with `strict=false`
264+
it tolerates lower-case and whitespace.
265+
* **Base36** — human‑friendly IDs using `0–9A–Z`; not a cryptographic format.
266+
Leading zero bytes are preserved (e.g. `{0,0,1}``"001"`).
267+
268+
#### Encoding
269+
270+
```cpp
271+
std::string b64 = hmac_cpp::base64_encode(buf.data(), buf.size(),
272+
hmac_cpp::Base64Alphabet::Url, /*pad=*/false);
273+
hmac_cpp::base64_decode(b64, raw, hmac_cpp::Base64Alphabet::Url,
274+
/*require_padding=*/false, /*strict=*/true);
275+
276+
std::string b32 = hmac_cpp::base32_encode(buf.data(), buf.size(), /*pad=*/true);
277+
hmac_cpp::base32_decode(b32, raw, /*require_padding=*/true, /*strict=*/false);
278+
279+
std::string b36 = hmac_cpp::base36_encode(buf.data(), buf.size());
280+
hmac_cpp::base36_decode(b36, raw);
281+
```
264282
265283
Returned strings and buffers are not zeroized; if you store secrets, prefer
266-
`secure_buffer` and wipe explicitly.
284+
`secure_buffer` and wipe explicitly. Zeroization is a best‑effort and may be
285+
removed by optimizations or the C++ runtime allocator.
267286
268287
---
269288

include/hmac_cpp/encoding.hpp

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ namespace hmac_cpp {
2727
bool pad = true);
2828

2929
/// \brief Base64-encode a vector.
30-
inline std::string base64_encode(const std::vector<uint8_t>& v,
30+
inline HMAC_CPP_API std::string base64_encode(const std::vector<uint8_t>& v,
3131
Base64Alphabet alphabet = Base64Alphabet::Standard,
3232
bool pad = true) {
3333
return base64_encode(v.data(), v.size(), alphabet, pad);
3434
}
3535

3636
/// \brief Base64-encode a secure_buffer.
37-
inline std::string base64_encode(const secure_buffer<uint8_t>& v,
37+
inline HMAC_CPP_API std::string base64_encode(const secure_buffer<uint8_t>& v,
3838
Base64Alphabet alphabet = Base64Alphabet::Standard,
3939
bool pad = true) {
4040
return base64_encode(v.data(), v.size(), alphabet, pad);
@@ -73,12 +73,12 @@ namespace hmac_cpp {
7373
bool pad = true);
7474

7575
/// \brief Base32-encode a vector.
76-
inline std::string base32_encode(const std::vector<uint8_t>& v, bool pad = true) {
76+
inline HMAC_CPP_API std::string base32_encode(const std::vector<uint8_t>& v, bool pad = true) {
7777
return base32_encode(v.data(), v.size(), pad);
7878
}
7979

8080
/// \brief Base32-encode a secure_buffer.
81-
inline std::string base32_encode(const secure_buffer<uint8_t>& v, bool pad = true) {
81+
inline HMAC_CPP_API std::string base32_encode(const secure_buffer<uint8_t>& v, bool pad = true) {
8282
return base32_encode(v.data(), v.size(), pad);
8383
}
8484

@@ -111,12 +111,12 @@ namespace hmac_cpp {
111111
HMAC_CPP_API std::string base36_encode(const uint8_t* data, size_t len);
112112

113113
/// \brief Base36-encode a vector.
114-
inline std::string base36_encode(const std::vector<uint8_t>& v) {
114+
inline HMAC_CPP_API std::string base36_encode(const std::vector<uint8_t>& v) {
115115
return base36_encode(v.data(), v.size());
116116
}
117117

118118
/// \brief Base36-encode a secure_buffer.
119-
inline std::string base36_encode(const secure_buffer<uint8_t>& v) {
119+
inline HMAC_CPP_API std::string base36_encode(const secure_buffer<uint8_t>& v) {
120120
return base36_encode(v.data(), v.size());
121121
}
122122

src/encoding.cpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55

66
namespace hmac_cpp {
77

8-
struct _wipe_string_guard {
8+
namespace {
9+
struct _wipe_string_guard final {
910
std::string& s;
1011
explicit _wipe_string_guard(std::string& ref) : s(ref) {}
1112
~_wipe_string_guard() {
1213
if (!s.empty()) std::fill(s.begin(), s.end(), '\0');
1314
}
1415
};
16+
} // anonymous namespace
1517

1618
// ======================
1719
// Helpers (Base64)
@@ -23,13 +25,13 @@ static inline const char* b64_alphabet(Base64Alphabet a) {
2325
: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
2426
}
2527

26-
static inline void b64_build_reverse(Base64Alphabet a, int8_t rev[256]) {
28+
static inline void b64_build_reverse(Base64Alphabet a, int8_t rev[256], bool allow_alias) {
2729
for (int i = 0; i < 256; ++i) rev[i] = -1;
2830
const char* alpha = b64_alphabet(a);
2931
for (int i = 0; i < 64; ++i) {
3032
rev[ static_cast<unsigned char>(alpha[i]) ] = static_cast<int8_t>(i);
3133
}
32-
if (a == Base64Alphabet::Url) {
34+
if (a == Base64Alphabet::Url && allow_alias) {
3335
rev[ static_cast<unsigned char>('+') ] = 62;
3436
rev[ static_cast<unsigned char>('/') ] = 63;
3537
}
@@ -115,7 +117,7 @@ bool base64_decode(const std::string& in, std::vector<uint8_t>& out,
115117
}
116118

117119
int8_t rev[256];
118-
b64_build_reverse(alphabet, rev);
120+
b64_build_reverse(alphabet, rev, !strict);
119121

120122
size_t approx = (L / 4) * 3 + 3;
121123
out.reserve(approx);

test_all.cpp

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,7 @@ TEST(EncodingTest, Base64Vectors) {
617617
std::vector<uint8_t> out;
618618
EXPECT_TRUE(hmac_cpp::base64_decode("Zg", out, hmac_cpp::Base64Alphabet::Standard, false, false));
619619
EXPECT_EQ(out, std::vector<uint8_t>({'f'}));
620-
EXPECT_TRUE(hmac_cpp::base64_decode("+__/", out, hmac_cpp::Base64Alphabet::Url));
620+
EXPECT_TRUE(hmac_cpp::base64_decode("+__/", out, hmac_cpp::Base64Alphabet::Url, false, false));
621621
EXPECT_EQ(out, std::vector<uint8_t>({0xfb,0xff,0xff}));
622622
}
623623

@@ -682,6 +682,73 @@ TEST(EncodingTest, Base36Vectors) {
682682
}
683683
}
684684

685+
TEST(EncodingPropertyTest, Base64RoundTrip) {
686+
std::mt19937 rng(123);
687+
std::uniform_int_distribution<int> len_dist(0,64);
688+
for (int i = 0; i < 50; ++i) {
689+
int len = len_dist(rng);
690+
std::vector<uint8_t> buf(len);
691+
for (int j = 0; j < len; ++j) buf[j] = static_cast<uint8_t>(rng());
692+
for (auto alpha : {hmac_cpp::Base64Alphabet::Standard, hmac_cpp::Base64Alphabet::Url}) {
693+
for (bool pad : {true, false}) {
694+
std::string enc = hmac_cpp::base64_encode(buf.data(), buf.size(), alpha, pad);
695+
std::vector<uint8_t> dec;
696+
EXPECT_TRUE(hmac_cpp::base64_decode(enc, dec, alpha, pad, true));
697+
EXPECT_EQ(dec, buf);
698+
std::string re = hmac_cpp::base64_encode(dec.data(), dec.size(), alpha, pad);
699+
EXPECT_EQ(re, enc);
700+
}
701+
}
702+
}
703+
}
704+
705+
TEST(EncodingPropertyTest, Base64StrictRejects) {
706+
std::vector<uint8_t> out;
707+
EXPECT_FALSE(hmac_cpp::base64_decode("+__/", out, hmac_cpp::Base64Alphabet::Url, false, true));
708+
EXPECT_FALSE(hmac_cpp::base64_decode("AAAAA", out, hmac_cpp::Base64Alphabet::Standard, false, true));
709+
}
710+
711+
TEST(EncodingPropertyTest, Base32RoundTrip) {
712+
std::mt19937 rng(321);
713+
std::uniform_int_distribution<int> len_dist(0,40);
714+
for (int i = 0; i < 50; ++i) {
715+
int len = len_dist(rng);
716+
std::vector<uint8_t> buf(len);
717+
for (int j = 0; j < len; ++j) buf[j] = static_cast<uint8_t>(rng());
718+
for (bool pad : {true, false}) {
719+
std::string enc = hmac_cpp::base32_encode(buf.data(), buf.size(), pad);
720+
std::vector<uint8_t> dec;
721+
EXPECT_TRUE(hmac_cpp::base32_decode(enc, dec, pad, true));
722+
EXPECT_EQ(dec, buf);
723+
std::string re = hmac_cpp::base32_encode(dec.data(), dec.size(), pad);
724+
EXPECT_EQ(re, enc);
725+
}
726+
}
727+
}
728+
729+
TEST(EncodingPropertyTest, Base32RejectsInvalid) {
730+
std::vector<uint8_t> out;
731+
EXPECT_FALSE(hmac_cpp::base32_decode("A", out));
732+
EXPECT_FALSE(hmac_cpp::base32_decode("AAA", out));
733+
EXPECT_FALSE(hmac_cpp::base32_decode("AAAAAA", out));
734+
EXPECT_FALSE(hmac_cpp::base32_decode("MZX=====MZXW6===", out));
735+
}
736+
737+
TEST(EncodingPropertyTest, Base36LeadingZeros) {
738+
std::mt19937 rng(777);
739+
std::uniform_int_distribution<int> len_dist(0,10);
740+
for (int i = 0; i < 20; ++i) {
741+
int zeros = len_dist(rng) % 5;
742+
int data_len = len_dist(rng);
743+
std::vector<uint8_t> buf(zeros + data_len);
744+
for (int j = 0; j < data_len; ++j) buf[zeros + j] = static_cast<uint8_t>(rng());
745+
std::string enc = hmac_cpp::base36_encode(buf.data(), buf.size());
746+
std::vector<uint8_t> dec;
747+
EXPECT_TRUE(hmac_cpp::base36_decode(enc, dec));
748+
EXPECT_EQ(dec, buf);
749+
}
750+
}
751+
685752
int main(int argc, char **argv) {
686753
::testing::InitGoogleTest(&argc, argv);
687754
return RUN_ALL_TESTS();

0 commit comments

Comments
 (0)