From 827ede318a09ae39ee1dce89a912e9e7f6fe8d1d Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Sat, 6 Sep 2025 06:03:22 +0300 Subject: [PATCH] docs(encoding): clarify Base64 padding behavior Mention that non-strict Base64 decoder tolerates missing '=' padding. --- README.md | 33 +++++++++++++---- include/hmac_cpp/encoding.hpp | 12 +++--- src/encoding.cpp | 10 +++-- test_all.cpp | 69 ++++++++++++++++++++++++++++++++++- 4 files changed, 106 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index c07c5c1..79e8bd2 100644 --- a/README.md +++ b/README.md @@ -255,15 +255,34 @@ bool v2 = hmac::is_token_valid(t2, secret_key, fingerprint, 60); `hmac_cpp::encoding` provides simple conversions: -* **Base64** — standard `+/` and URL-safe `-_` alphabets; optional `strict` mode - (rejects whitespace and mixed padding) and ability to decode without `=`. -* **Base32** — RFC 4648 alphabet `A–Z2–7`; encoder outputs upper-case, decoder - accepts lower-case and ignores spaces/CR/LF when `strict=false`. -* **Base36** — non-standard human-readable IDs using `0–9A–Z`; keeps leading - zero bytes by prefixing `'0'` and maps a single `\x00` to "0". +* **Base64** — standard `+/` and URL-safe `-_` alphabets; `pad=true/false` toggles + `=` padding. `strict=true` rejects whitespace, mixed padding and `+`/`/` when + using the URL alphabet; `strict=false` ignores ASCII spaces, accepts these + aliases and tolerates missing padding. +* **Base32** — RFC 4648 alphabet `A–Z2–7`; encoder emits upper-case. Decoder + with `strict=true` requires `=` padding and upper-case; with `strict=false` + it tolerates lower-case and whitespace. +* **Base36** — human‑friendly IDs using `0–9A–Z`; not a cryptographic format. + Leading zero bytes are preserved (e.g. `{0,0,1}` → `"001"`). + +#### Encoding + +```cpp +std::string b64 = hmac_cpp::base64_encode(buf.data(), buf.size(), + hmac_cpp::Base64Alphabet::Url, /*pad=*/false); +hmac_cpp::base64_decode(b64, raw, hmac_cpp::Base64Alphabet::Url, + /*require_padding=*/false, /*strict=*/true); + +std::string b32 = hmac_cpp::base32_encode(buf.data(), buf.size(), /*pad=*/true); +hmac_cpp::base32_decode(b32, raw, /*require_padding=*/true, /*strict=*/false); + +std::string b36 = hmac_cpp::base36_encode(buf.data(), buf.size()); +hmac_cpp::base36_decode(b36, raw); +``` Returned strings and buffers are not zeroized; if you store secrets, prefer -`secure_buffer` and wipe explicitly. +`secure_buffer` and wipe explicitly. Zeroization is a best‑effort and may be +removed by optimizations or the C++ runtime allocator. --- diff --git a/include/hmac_cpp/encoding.hpp b/include/hmac_cpp/encoding.hpp index 4661caf..1b8b207 100644 --- a/include/hmac_cpp/encoding.hpp +++ b/include/hmac_cpp/encoding.hpp @@ -27,14 +27,14 @@ namespace hmac_cpp { bool pad = true); /// \brief Base64-encode a vector. - inline std::string base64_encode(const std::vector& v, + inline HMAC_CPP_API std::string base64_encode(const std::vector& v, Base64Alphabet alphabet = Base64Alphabet::Standard, bool pad = true) { return base64_encode(v.data(), v.size(), alphabet, pad); } /// \brief Base64-encode a secure_buffer. - inline std::string base64_encode(const secure_buffer& v, + inline HMAC_CPP_API std::string base64_encode(const secure_buffer& v, Base64Alphabet alphabet = Base64Alphabet::Standard, bool pad = true) { return base64_encode(v.data(), v.size(), alphabet, pad); @@ -73,12 +73,12 @@ namespace hmac_cpp { bool pad = true); /// \brief Base32-encode a vector. - inline std::string base32_encode(const std::vector& v, bool pad = true) { + inline HMAC_CPP_API std::string base32_encode(const std::vector& v, bool pad = true) { return base32_encode(v.data(), v.size(), pad); } /// \brief Base32-encode a secure_buffer. - inline std::string base32_encode(const secure_buffer& v, bool pad = true) { + inline HMAC_CPP_API std::string base32_encode(const secure_buffer& v, bool pad = true) { return base32_encode(v.data(), v.size(), pad); } @@ -111,12 +111,12 @@ namespace hmac_cpp { HMAC_CPP_API std::string base36_encode(const uint8_t* data, size_t len); /// \brief Base36-encode a vector. - inline std::string base36_encode(const std::vector& v) { + inline HMAC_CPP_API std::string base36_encode(const std::vector& v) { return base36_encode(v.data(), v.size()); } /// \brief Base36-encode a secure_buffer. - inline std::string base36_encode(const secure_buffer& v) { + inline HMAC_CPP_API std::string base36_encode(const secure_buffer& v) { return base36_encode(v.data(), v.size()); } diff --git a/src/encoding.cpp b/src/encoding.cpp index f20bb5e..d6c2579 100644 --- a/src/encoding.cpp +++ b/src/encoding.cpp @@ -5,13 +5,15 @@ namespace hmac_cpp { -struct _wipe_string_guard { +namespace { +struct _wipe_string_guard final { std::string& s; explicit _wipe_string_guard(std::string& ref) : s(ref) {} ~_wipe_string_guard() { if (!s.empty()) std::fill(s.begin(), s.end(), '\0'); } }; +} // anonymous namespace // ====================== // Helpers (Base64) @@ -23,13 +25,13 @@ static inline const char* b64_alphabet(Base64Alphabet a) { : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; } -static inline void b64_build_reverse(Base64Alphabet a, int8_t rev[256]) { +static inline void b64_build_reverse(Base64Alphabet a, int8_t rev[256], bool allow_alias) { for (int i = 0; i < 256; ++i) rev[i] = -1; const char* alpha = b64_alphabet(a); for (int i = 0; i < 64; ++i) { rev[ static_cast(alpha[i]) ] = static_cast(i); } - if (a == Base64Alphabet::Url) { + if (a == Base64Alphabet::Url && allow_alias) { rev[ static_cast('+') ] = 62; rev[ static_cast('/') ] = 63; } @@ -115,7 +117,7 @@ bool base64_decode(const std::string& in, std::vector& out, } int8_t rev[256]; - b64_build_reverse(alphabet, rev); + b64_build_reverse(alphabet, rev, !strict); size_t approx = (L / 4) * 3 + 3; out.reserve(approx); diff --git a/test_all.cpp b/test_all.cpp index 10ae882..9c98d82 100644 --- a/test_all.cpp +++ b/test_all.cpp @@ -617,7 +617,7 @@ TEST(EncodingTest, Base64Vectors) { std::vector out; EXPECT_TRUE(hmac_cpp::base64_decode("Zg", out, hmac_cpp::Base64Alphabet::Standard, false, false)); EXPECT_EQ(out, std::vector({'f'})); - EXPECT_TRUE(hmac_cpp::base64_decode("+__/", out, hmac_cpp::Base64Alphabet::Url)); + EXPECT_TRUE(hmac_cpp::base64_decode("+__/", out, hmac_cpp::Base64Alphabet::Url, false, false)); EXPECT_EQ(out, std::vector({0xfb,0xff,0xff})); } @@ -682,6 +682,73 @@ TEST(EncodingTest, Base36Vectors) { } } +TEST(EncodingPropertyTest, Base64RoundTrip) { + std::mt19937 rng(123); + std::uniform_int_distribution len_dist(0,64); + for (int i = 0; i < 50; ++i) { + int len = len_dist(rng); + std::vector buf(len); + for (int j = 0; j < len; ++j) buf[j] = static_cast(rng()); + for (auto alpha : {hmac_cpp::Base64Alphabet::Standard, hmac_cpp::Base64Alphabet::Url}) { + for (bool pad : {true, false}) { + std::string enc = hmac_cpp::base64_encode(buf.data(), buf.size(), alpha, pad); + std::vector dec; + EXPECT_TRUE(hmac_cpp::base64_decode(enc, dec, alpha, pad, true)); + EXPECT_EQ(dec, buf); + std::string re = hmac_cpp::base64_encode(dec.data(), dec.size(), alpha, pad); + EXPECT_EQ(re, enc); + } + } + } +} + +TEST(EncodingPropertyTest, Base64StrictRejects) { + std::vector out; + EXPECT_FALSE(hmac_cpp::base64_decode("+__/", out, hmac_cpp::Base64Alphabet::Url, false, true)); + EXPECT_FALSE(hmac_cpp::base64_decode("AAAAA", out, hmac_cpp::Base64Alphabet::Standard, false, true)); +} + +TEST(EncodingPropertyTest, Base32RoundTrip) { + std::mt19937 rng(321); + std::uniform_int_distribution len_dist(0,40); + for (int i = 0; i < 50; ++i) { + int len = len_dist(rng); + std::vector buf(len); + for (int j = 0; j < len; ++j) buf[j] = static_cast(rng()); + for (bool pad : {true, false}) { + std::string enc = hmac_cpp::base32_encode(buf.data(), buf.size(), pad); + std::vector dec; + EXPECT_TRUE(hmac_cpp::base32_decode(enc, dec, pad, true)); + EXPECT_EQ(dec, buf); + std::string re = hmac_cpp::base32_encode(dec.data(), dec.size(), pad); + EXPECT_EQ(re, enc); + } + } +} + +TEST(EncodingPropertyTest, Base32RejectsInvalid) { + std::vector out; + EXPECT_FALSE(hmac_cpp::base32_decode("A", out)); + EXPECT_FALSE(hmac_cpp::base32_decode("AAA", out)); + EXPECT_FALSE(hmac_cpp::base32_decode("AAAAAA", out)); + EXPECT_FALSE(hmac_cpp::base32_decode("MZX=====MZXW6===", out)); +} + +TEST(EncodingPropertyTest, Base36LeadingZeros) { + std::mt19937 rng(777); + std::uniform_int_distribution len_dist(0,10); + for (int i = 0; i < 20; ++i) { + int zeros = len_dist(rng) % 5; + int data_len = len_dist(rng); + std::vector buf(zeros + data_len); + for (int j = 0; j < data_len; ++j) buf[zeros + j] = static_cast(rng()); + std::string enc = hmac_cpp::base36_encode(buf.data(), buf.size()); + std::vector dec; + EXPECT_TRUE(hmac_cpp::base36_decode(enc, dec)); + EXPECT_EQ(dec, buf); + } +} + int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS();