diff --git a/README.md b/README.md index 0816941..c07c5c1 100644 --- a/README.md +++ b/README.md @@ -251,6 +251,22 @@ bool v2 = hmac::is_token_valid(t2, secret_key, fingerprint, 60); --- +### Encoding helpers + +`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". + +Returned strings and buffers are not zeroized; if you store secrets, prefer +`secure_buffer` and wipe explicitly. + +--- + ## 📦 MQL5 Compatibility Repository provides `sha256.mqh`, `sha512.mqh`, `hmac.mqh`, `hmac_utils.mqh` (MetaTrader 5). diff --git a/include/hmac_cpp/encoding.hpp b/include/hmac_cpp/encoding.hpp index edb1057..4661caf 100644 --- a/include/hmac_cpp/encoding.hpp +++ b/include/hmac_cpp/encoding.hpp @@ -45,8 +45,9 @@ namespace hmac_cpp { /// \param out Output byte vector (overwritten). /// \param alphabet Standard ("+/") or URL-safe ("-_") alphabet. /// \param require_padding If true, input must have proper '=' padding and length % 4 == 0. - /// \param strict If true, disallow whitespaces and enforce '=' only in the last quartet. - /// If false, ignore ASCII spaces and CR/LF/TAB and allow missing padding. + /// \param strict If true, disallow whitespace and require '=' only in the last quartet. + /// If false, ignore ASCII spaces/CR/LF/TAB, allow missing padding, and + /// accept mixed "+/" and "-_" when \p alphabet == Url. /// \return true on success, false on invalid input. HMAC_CPP_API bool base64_decode(const std::string& in, std::vector& out, Base64Alphabet alphabet = Base64Alphabet::Standard, @@ -67,7 +68,7 @@ namespace hmac_cpp { /// \param data Pointer to input bytes. /// \param len Number of input bytes. /// \param pad If true, append '=' padding to a multiple of 8 chars. - /// \return Encoded string (upper-case). + /// \return Encoded string (upper-case alphabet). HMAC_CPP_API std::string base32_encode(const uint8_t* data, size_t len, bool pad = true); @@ -85,8 +86,8 @@ namespace hmac_cpp { /// \param in Input string (Base32; upper-case preferred). /// \param out Output byte vector (overwritten). /// \param require_padding If true, input must have proper '=' padding and length % 8 == 0. - /// \param strict If true, disallow whitespaces and lower-case; enforce '=' only in the last block. - /// If false, ignore ASCII spaces and CR/LF/TAB and accept lower-case letters. + /// \param strict If true, disallow whitespace and lower-case; enforce '=' only in the last block. + /// If false, ignore ASCII spaces/CR/LF/TAB and accept lower-case letters. /// \return true on success, false on invalid input. HMAC_CPP_API bool base32_decode(const std::string& in, std::vector& out, bool require_padding = false, @@ -97,6 +98,37 @@ namespace hmac_cpp { bool require_padding = false, bool strict = true) noexcept; + // ------------------------- + // Base36 — encode / decode + // ------------------------- + + /// \brief Base36-encode a byte buffer (digits 0–9, letters A–Z). + /// Leading zero bytes are preserved by prefixing '0'. + /// Input of a single \x00 returns "0". + /// \param data Pointer to input bytes (big-endian). + /// \param len Number of input bytes. + /// \return Encoded string. + 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) { + return base36_encode(v.data(), v.size()); + } + + /// \brief Base36-encode a secure_buffer. + inline std::string base36_encode(const secure_buffer& v) { + return base36_encode(v.data(), v.size()); + } + + /// \brief Decode a Base36 string. + /// \param in Input string (case-insensitive). + /// \param out Output byte vector (overwritten). + /// \return true on success, false on invalid input. + HMAC_CPP_API bool base36_decode(const std::string& in, std::vector& out) noexcept; + + /// \brief Decode Base36 into secure_buffer. + HMAC_CPP_API bool base36_decode(const std::string& in, secure_buffer& out) noexcept; + } // namespace hmac_cpp #endif // _HMAC_ENCODING_HPP_INCLUDED diff --git a/src/encoding.cpp b/src/encoding.cpp index b6d20e1..f20bb5e 100644 --- a/src/encoding.cpp +++ b/src/encoding.cpp @@ -29,6 +29,10 @@ static inline void b64_build_reverse(Base64Alphabet a, int8_t rev[256]) { for (int i = 0; i < 64; ++i) { rev[ static_cast(alpha[i]) ] = static_cast(i); } + if (a == Base64Alphabet::Url) { + rev[ static_cast('+') ] = 62; + rev[ static_cast('/') ] = 63; + } rev[ static_cast('=') ] = -2; // padding marker } @@ -474,5 +478,106 @@ bool base32_decode(const std::string& in, secure_buffer& out, return true; } +// ====================== +// Base36 +// ====================== + +static inline const char* b36_alphabet() { + return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +} + +std::string base36_encode(const uint8_t* data, size_t len) { + if (len == 0) return std::string(); + if (len == 1 && data[0] == 0) return std::string("0"); + + size_t zeros = 0; + while (zeros < len && data[zeros] == 0) ++zeros; + + std::vector tmp(data + zeros, data + len); + std::string out; + + while (!tmp.empty()) { + uint32_t carry = 0; + std::vector next; + next.reserve(tmp.size()); + for (size_t i = 0; i < tmp.size(); ++i) { + uint32_t cur = (carry << 8) | tmp[i]; + uint8_t div = static_cast(cur / 36); + carry = cur % 36; + if (!next.empty() || div != 0) next.push_back(div); + } + out.push_back(b36_alphabet()[carry]); + tmp = std::move(next); + } + + if (out.empty()) out.push_back('0'); + std::reverse(out.begin(), out.end()); + out.insert(0, zeros, '0'); + return out; +} + +bool base36_decode(const std::string& in, std::vector& out) noexcept { + out.clear(); + if (in.empty()) return true; + + bool all_zero = true; + for (size_t i = 0; i < in.size(); ++i) { + if (in[i] != '0') { all_zero = false; break; } + } + if (all_zero) { + if (in.size() == 1) out.push_back(0); + else out.assign(in.size() - 1, 0); + return true; + } + + size_t zeros = 0; + while (zeros < in.size() && in[zeros] == '0') ++zeros; + + std::vector b256(1, 0); + for (size_t i = zeros; i < in.size(); ++i) { + unsigned char c = static_cast(in[i]); + int val; + if (c >= '0' && c <= '9') val = c - '0'; + else if (c >= 'A' && c <= 'Z') val = c - 'A' + 10; + else if (c >= 'a' && c <= 'z') val = c - 'a' + 10; + else return false; + + int carry = val; + for (size_t j = b256.size(); j-- > 0;) { + int cur = b256[j] * 36 + carry; + b256[j] = static_cast(cur & 0xFF); + carry = cur >> 8; + } + while (carry > 0) { + b256.insert(b256.begin(), static_cast(carry & 0xFF)); + carry >>= 8; + } + } + + size_t start = 0; + while (start < b256.size() && b256[start] == 0) ++start; + out.assign(zeros, 0); + out.insert(out.end(), b256.begin() + start, b256.end()); + return true; +} + +bool base36_decode(const std::string& in, secure_buffer& out) noexcept { + std::vector tmp; + bool ok = base36_decode(in, tmp); + if (!ok) { + out = secure_buffer(); + return false; + } + out = secure_buffer(tmp.size()); + if (out.size() != tmp.size()) { + if (!tmp.empty()) std::memset(tmp.data(), 0, tmp.size()); + out = secure_buffer(); + return false; + } + std::memcpy(out.data(), tmp.data(), tmp.size()); + if (!tmp.empty()) std::memset(tmp.data(), 0, tmp.size()); + return true; +} + } // namespace hmac_cpp diff --git a/test_all.cpp b/test_all.cpp index b3b5447..10ae882 100644 --- a/test_all.cpp +++ b/test_all.cpp @@ -592,6 +592,7 @@ TEST(TotpTimeErrorTest, ValidityNegativeTimeThrows) { TEST(EncodingTest, Base64Vectors) { std::vector> cases = { + {"", ""}, {"f", "Zg=="}, {"fo", "Zm8="}, {"foo", "Zm9v"}, @@ -613,6 +614,16 @@ TEST(EncodingTest, Base64Vectors) { EXPECT_TRUE(hmac_cpp::base64_decode("Zm9v", sec)); std::string foo(sec.begin(), sec.end()); EXPECT_EQ(foo, "foo"); + 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_EQ(out, std::vector({0xfb,0xff,0xff})); +} + +TEST(EncodingTest, Base64RejectsInvalid) { + std::vector out; + EXPECT_FALSE(hmac_cpp::base64_decode("Zm9v$", out)); } TEST(EncodingTest, Base32Vectors) { @@ -637,6 +648,40 @@ TEST(EncodingTest, Base32Vectors) { EXPECT_EQ(foo, "foo"); } +TEST(EncodingTest, Base32CaseWhitespace) { + std::vector out; + EXPECT_TRUE(hmac_cpp::base32_decode("my======", out, true, false)); + EXPECT_EQ(out, std::vector({'f'})); + EXPECT_TRUE(hmac_cpp::base32_decode("MY", out, false, false)); + EXPECT_EQ(out, std::vector({'f'})); + EXPECT_TRUE(hmac_cpp::base32_decode("MZXW6YTBOI======", out)); + EXPECT_EQ(std::string(out.begin(), out.end()), "foobar"); + EXPECT_TRUE(hmac_cpp::base32_decode("MZXW6 YTBO I======", out, true, false)); + EXPECT_EQ(std::string(out.begin(), out.end()), "foobar"); +} + +TEST(EncodingTest, Base36Vectors) { + std::vector z2 = {0x00, 0x00}; + EXPECT_EQ(hmac_cpp::base36_encode(z2.data(), z2.size()), "000"); + std::vector out; + EXPECT_TRUE(hmac_cpp::base36_decode("000", out)); + EXPECT_EQ(out, z2); + std::vector one = {0x01}; + EXPECT_EQ(hmac_cpp::base36_encode(one.data(), one.size()), "1"); + EXPECT_TRUE(hmac_cpp::base36_decode("1", out)); + EXPECT_EQ(out, one); + std::mt19937 rng(0); + std::uniform_int_distribution dist(0,255); + for (int len = 0; len < 16; ++len) { + std::vector buf(len); + for (int i = 0; i < len; ++i) buf[i] = static_cast(dist(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();