Skip to content

Commit 671ede3

Browse files
committed
chore: document encoding update
Add notes on base encoders and zeroization.
1 parent 39bfb2b commit 671ede3

4 files changed

Lines changed: 203 additions & 5 deletions

File tree

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,22 @@ bool v2 = hmac::is_token_valid(t2, secret_key, fingerprint, 60);
251251

252252
---
253253

254+
### Encoding helpers
255+
256+
`hmac_cpp::encoding` provides simple conversions:
257+
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".
264+
265+
Returned strings and buffers are not zeroized; if you store secrets, prefer
266+
`secure_buffer` and wipe explicitly.
267+
268+
---
269+
254270
## 📦 MQL5 Compatibility
255271

256272
Repository provides `sha256.mqh`, `sha512.mqh`, `hmac.mqh`, `hmac_utils.mqh` (MetaTrader 5).

include/hmac_cpp/encoding.hpp

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ namespace hmac_cpp {
4545
/// \param out Output byte vector (overwritten).
4646
/// \param alphabet Standard ("+/") or URL-safe ("-_") alphabet.
4747
/// \param require_padding If true, input must have proper '=' padding and length % 4 == 0.
48-
/// \param strict If true, disallow whitespaces and enforce '=' only in the last quartet.
49-
/// If false, ignore ASCII spaces and CR/LF/TAB and allow missing padding.
48+
/// \param strict If true, disallow whitespace and require '=' only in the last quartet.
49+
/// If false, ignore ASCII spaces/CR/LF/TAB, allow missing padding, and
50+
/// accept mixed "+/" and "-_" when \p alphabet == Url.
5051
/// \return true on success, false on invalid input.
5152
HMAC_CPP_API bool base64_decode(const std::string& in, std::vector<uint8_t>& out,
5253
Base64Alphabet alphabet = Base64Alphabet::Standard,
@@ -67,7 +68,7 @@ namespace hmac_cpp {
6768
/// \param data Pointer to input bytes.
6869
/// \param len Number of input bytes.
6970
/// \param pad If true, append '=' padding to a multiple of 8 chars.
70-
/// \return Encoded string (upper-case).
71+
/// \return Encoded string (upper-case alphabet).
7172
HMAC_CPP_API std::string base32_encode(const uint8_t* data, size_t len,
7273
bool pad = true);
7374

@@ -85,8 +86,8 @@ namespace hmac_cpp {
8586
/// \param in Input string (Base32; upper-case preferred).
8687
/// \param out Output byte vector (overwritten).
8788
/// \param require_padding If true, input must have proper '=' padding and length % 8 == 0.
88-
/// \param strict If true, disallow whitespaces and lower-case; enforce '=' only in the last block.
89-
/// If false, ignore ASCII spaces and CR/LF/TAB and accept lower-case letters.
89+
/// \param strict If true, disallow whitespace and lower-case; enforce '=' only in the last block.
90+
/// If false, ignore ASCII spaces/CR/LF/TAB and accept lower-case letters.
9091
/// \return true on success, false on invalid input.
9192
HMAC_CPP_API bool base32_decode(const std::string& in, std::vector<uint8_t>& out,
9293
bool require_padding = false,
@@ -97,6 +98,37 @@ namespace hmac_cpp {
9798
bool require_padding = false,
9899
bool strict = true) noexcept;
99100

101+
// -------------------------
102+
// Base36 — encode / decode
103+
// -------------------------
104+
105+
/// \brief Base36-encode a byte buffer (digits 0–9, letters A–Z).
106+
/// Leading zero bytes are preserved by prefixing '0'.
107+
/// Input of a single \x00 returns "0".
108+
/// \param data Pointer to input bytes (big-endian).
109+
/// \param len Number of input bytes.
110+
/// \return Encoded string.
111+
HMAC_CPP_API std::string base36_encode(const uint8_t* data, size_t len);
112+
113+
/// \brief Base36-encode a vector.
114+
inline std::string base36_encode(const std::vector<uint8_t>& v) {
115+
return base36_encode(v.data(), v.size());
116+
}
117+
118+
/// \brief Base36-encode a secure_buffer.
119+
inline std::string base36_encode(const secure_buffer<uint8_t>& v) {
120+
return base36_encode(v.data(), v.size());
121+
}
122+
123+
/// \brief Decode a Base36 string.
124+
/// \param in Input string (case-insensitive).
125+
/// \param out Output byte vector (overwritten).
126+
/// \return true on success, false on invalid input.
127+
HMAC_CPP_API bool base36_decode(const std::string& in, std::vector<uint8_t>& out) noexcept;
128+
129+
/// \brief Decode Base36 into secure_buffer.
130+
HMAC_CPP_API bool base36_decode(const std::string& in, secure_buffer<uint8_t>& out) noexcept;
131+
100132
} // namespace hmac_cpp
101133

102134
#endif // _HMAC_ENCODING_HPP_INCLUDED

src/encoding.cpp

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ static inline void b64_build_reverse(Base64Alphabet a, int8_t rev[256]) {
2929
for (int i = 0; i < 64; ++i) {
3030
rev[ static_cast<unsigned char>(alpha[i]) ] = static_cast<int8_t>(i);
3131
}
32+
if (a == Base64Alphabet::Url) {
33+
rev[ static_cast<unsigned char>('+') ] = 62;
34+
rev[ static_cast<unsigned char>('/') ] = 63;
35+
}
3236
rev[ static_cast<unsigned char>('=') ] = -2; // padding marker
3337
}
3438

@@ -474,5 +478,106 @@ bool base32_decode(const std::string& in, secure_buffer<uint8_t>& out,
474478
return true;
475479
}
476480

481+
// ======================
482+
// Base36
483+
// ======================
484+
485+
static inline const char* b36_alphabet() {
486+
return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
487+
}
488+
489+
std::string base36_encode(const uint8_t* data, size_t len) {
490+
if (len == 0) return std::string();
491+
if (len == 1 && data[0] == 0) return std::string("0");
492+
493+
size_t zeros = 0;
494+
while (zeros < len && data[zeros] == 0) ++zeros;
495+
496+
std::vector<uint8_t> tmp(data + zeros, data + len);
497+
std::string out;
498+
499+
while (!tmp.empty()) {
500+
uint32_t carry = 0;
501+
std::vector<uint8_t> next;
502+
next.reserve(tmp.size());
503+
for (size_t i = 0; i < tmp.size(); ++i) {
504+
uint32_t cur = (carry << 8) | tmp[i];
505+
uint8_t div = static_cast<uint8_t>(cur / 36);
506+
carry = cur % 36;
507+
if (!next.empty() || div != 0) next.push_back(div);
508+
}
509+
out.push_back(b36_alphabet()[carry]);
510+
tmp = std::move(next);
511+
}
512+
513+
if (out.empty()) out.push_back('0');
514+
std::reverse(out.begin(), out.end());
515+
out.insert(0, zeros, '0');
516+
return out;
517+
}
518+
519+
bool base36_decode(const std::string& in, std::vector<uint8_t>& out) noexcept {
520+
out.clear();
521+
if (in.empty()) return true;
522+
523+
bool all_zero = true;
524+
for (size_t i = 0; i < in.size(); ++i) {
525+
if (in[i] != '0') { all_zero = false; break; }
526+
}
527+
if (all_zero) {
528+
if (in.size() == 1) out.push_back(0);
529+
else out.assign(in.size() - 1, 0);
530+
return true;
531+
}
532+
533+
size_t zeros = 0;
534+
while (zeros < in.size() && in[zeros] == '0') ++zeros;
535+
536+
std::vector<uint8_t> b256(1, 0);
537+
for (size_t i = zeros; i < in.size(); ++i) {
538+
unsigned char c = static_cast<unsigned char>(in[i]);
539+
int val;
540+
if (c >= '0' && c <= '9') val = c - '0';
541+
else if (c >= 'A' && c <= 'Z') val = c - 'A' + 10;
542+
else if (c >= 'a' && c <= 'z') val = c - 'a' + 10;
543+
else return false;
544+
545+
int carry = val;
546+
for (size_t j = b256.size(); j-- > 0;) {
547+
int cur = b256[j] * 36 + carry;
548+
b256[j] = static_cast<uint8_t>(cur & 0xFF);
549+
carry = cur >> 8;
550+
}
551+
while (carry > 0) {
552+
b256.insert(b256.begin(), static_cast<uint8_t>(carry & 0xFF));
553+
carry >>= 8;
554+
}
555+
}
556+
557+
size_t start = 0;
558+
while (start < b256.size() && b256[start] == 0) ++start;
559+
out.assign(zeros, 0);
560+
out.insert(out.end(), b256.begin() + start, b256.end());
561+
return true;
562+
}
563+
564+
bool base36_decode(const std::string& in, secure_buffer<uint8_t>& out) noexcept {
565+
std::vector<uint8_t> tmp;
566+
bool ok = base36_decode(in, tmp);
567+
if (!ok) {
568+
out = secure_buffer<uint8_t>();
569+
return false;
570+
}
571+
out = secure_buffer<uint8_t>(tmp.size());
572+
if (out.size() != tmp.size()) {
573+
if (!tmp.empty()) std::memset(tmp.data(), 0, tmp.size());
574+
out = secure_buffer<uint8_t>();
575+
return false;
576+
}
577+
std::memcpy(out.data(), tmp.data(), tmp.size());
578+
if (!tmp.empty()) std::memset(tmp.data(), 0, tmp.size());
579+
return true;
580+
}
581+
477582
} // namespace hmac_cpp
478583

test_all.cpp

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,7 @@ TEST(TotpTimeErrorTest, ValidityNegativeTimeThrows) {
592592

593593
TEST(EncodingTest, Base64Vectors) {
594594
std::vector<std::pair<std::string,std::string>> cases = {
595+
{"", ""},
595596
{"f", "Zg=="},
596597
{"fo", "Zm8="},
597598
{"foo", "Zm9v"},
@@ -613,6 +614,16 @@ TEST(EncodingTest, Base64Vectors) {
613614
EXPECT_TRUE(hmac_cpp::base64_decode("Zm9v", sec));
614615
std::string foo(sec.begin(), sec.end());
615616
EXPECT_EQ(foo, "foo");
617+
std::vector<uint8_t> out;
618+
EXPECT_TRUE(hmac_cpp::base64_decode("Zg", out, hmac_cpp::Base64Alphabet::Standard, false, false));
619+
EXPECT_EQ(out, std::vector<uint8_t>({'f'}));
620+
EXPECT_TRUE(hmac_cpp::base64_decode("+__/", out, hmac_cpp::Base64Alphabet::Url));
621+
EXPECT_EQ(out, std::vector<uint8_t>({0xfb,0xff,0xff}));
622+
}
623+
624+
TEST(EncodingTest, Base64RejectsInvalid) {
625+
std::vector<uint8_t> out;
626+
EXPECT_FALSE(hmac_cpp::base64_decode("Zm9v$", out));
616627
}
617628

618629
TEST(EncodingTest, Base32Vectors) {
@@ -637,6 +648,40 @@ TEST(EncodingTest, Base32Vectors) {
637648
EXPECT_EQ(foo, "foo");
638649
}
639650

651+
TEST(EncodingTest, Base32CaseWhitespace) {
652+
std::vector<uint8_t> out;
653+
EXPECT_TRUE(hmac_cpp::base32_decode("my======", out, true, false));
654+
EXPECT_EQ(out, std::vector<uint8_t>({'f'}));
655+
EXPECT_TRUE(hmac_cpp::base32_decode("MY", out, false, false));
656+
EXPECT_EQ(out, std::vector<uint8_t>({'f'}));
657+
EXPECT_TRUE(hmac_cpp::base32_decode("MZXW6YTBOI======", out));
658+
EXPECT_EQ(std::string(out.begin(), out.end()), "foobar");
659+
EXPECT_TRUE(hmac_cpp::base32_decode("MZXW6 YTBO I======", out, true, false));
660+
EXPECT_EQ(std::string(out.begin(), out.end()), "foobar");
661+
}
662+
663+
TEST(EncodingTest, Base36Vectors) {
664+
std::vector<uint8_t> z2 = {0x00, 0x00};
665+
EXPECT_EQ(hmac_cpp::base36_encode(z2.data(), z2.size()), "000");
666+
std::vector<uint8_t> out;
667+
EXPECT_TRUE(hmac_cpp::base36_decode("000", out));
668+
EXPECT_EQ(out, z2);
669+
std::vector<uint8_t> one = {0x01};
670+
EXPECT_EQ(hmac_cpp::base36_encode(one.data(), one.size()), "1");
671+
EXPECT_TRUE(hmac_cpp::base36_decode("1", out));
672+
EXPECT_EQ(out, one);
673+
std::mt19937 rng(0);
674+
std::uniform_int_distribution<int> dist(0,255);
675+
for (int len = 0; len < 16; ++len) {
676+
std::vector<uint8_t> buf(len);
677+
for (int i = 0; i < len; ++i) buf[i] = static_cast<uint8_t>(dist(rng));
678+
std::string enc = hmac_cpp::base36_encode(buf.data(), buf.size());
679+
std::vector<uint8_t> dec;
680+
EXPECT_TRUE(hmac_cpp::base36_decode(enc, dec));
681+
EXPECT_EQ(dec, buf);
682+
}
683+
}
684+
640685
int main(int argc, char **argv) {
641686
::testing::InitGoogleTest(&argc, argv);
642687
return RUN_ALL_TESTS();

0 commit comments

Comments
 (0)