Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
42 changes: 37 additions & 5 deletions include/hmac_cpp/encoding.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint8_t>& out,
Base64Alphabet alphabet = Base64Alphabet::Standard,
Expand All @@ -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);

Expand All @@ -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<uint8_t>& out,
bool require_padding = false,
Expand All @@ -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<uint8_t>& v) {
return base36_encode(v.data(), v.size());
}

/// \brief Base36-encode a secure_buffer.
inline std::string base36_encode(const secure_buffer<uint8_t>& 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<uint8_t>& out) noexcept;

/// \brief Decode Base36 into secure_buffer.
HMAC_CPP_API bool base36_decode(const std::string& in, secure_buffer<uint8_t>& out) noexcept;

} // namespace hmac_cpp

#endif // _HMAC_ENCODING_HPP_INCLUDED
105 changes: 105 additions & 0 deletions src/encoding.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<unsigned char>(alpha[i]) ] = static_cast<int8_t>(i);
}
if (a == Base64Alphabet::Url) {
rev[ static_cast<unsigned char>('+') ] = 62;
rev[ static_cast<unsigned char>('/') ] = 63;
}
rev[ static_cast<unsigned char>('=') ] = -2; // padding marker
}

Expand Down Expand Up @@ -474,5 +478,106 @@ bool base32_decode(const std::string& in, secure_buffer<uint8_t>& 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<uint8_t> tmp(data + zeros, data + len);
std::string out;

while (!tmp.empty()) {
uint32_t carry = 0;
std::vector<uint8_t> 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<uint8_t>(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<uint8_t>& 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<uint8_t> b256(1, 0);
for (size_t i = zeros; i < in.size(); ++i) {
unsigned char c = static_cast<unsigned char>(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<uint8_t>(cur & 0xFF);
carry = cur >> 8;
}
while (carry > 0) {
b256.insert(b256.begin(), static_cast<uint8_t>(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<uint8_t>& out) noexcept {
std::vector<uint8_t> tmp;
bool ok = base36_decode(in, tmp);
if (!ok) {
out = secure_buffer<uint8_t>();
return false;
}
out = secure_buffer<uint8_t>(tmp.size());
if (out.size() != tmp.size()) {
if (!tmp.empty()) std::memset(tmp.data(), 0, tmp.size());
out = secure_buffer<uint8_t>();
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

45 changes: 45 additions & 0 deletions test_all.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@ TEST(TotpTimeErrorTest, ValidityNegativeTimeThrows) {

TEST(EncodingTest, Base64Vectors) {
std::vector<std::pair<std::string,std::string>> cases = {
{"", ""},
{"f", "Zg=="},
{"fo", "Zm8="},
{"foo", "Zm9v"},
Expand All @@ -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<uint8_t> out;
EXPECT_TRUE(hmac_cpp::base64_decode("Zg", out, hmac_cpp::Base64Alphabet::Standard, false, false));
EXPECT_EQ(out, std::vector<uint8_t>({'f'}));
EXPECT_TRUE(hmac_cpp::base64_decode("+__/", out, hmac_cpp::Base64Alphabet::Url));
EXPECT_EQ(out, std::vector<uint8_t>({0xfb,0xff,0xff}));
}

TEST(EncodingTest, Base64RejectsInvalid) {
std::vector<uint8_t> out;
EXPECT_FALSE(hmac_cpp::base64_decode("Zm9v$", out));
}

TEST(EncodingTest, Base32Vectors) {
Expand All @@ -637,6 +648,40 @@ TEST(EncodingTest, Base32Vectors) {
EXPECT_EQ(foo, "foo");
}

TEST(EncodingTest, Base32CaseWhitespace) {
std::vector<uint8_t> out;
EXPECT_TRUE(hmac_cpp::base32_decode("my======", out, true, false));
EXPECT_EQ(out, std::vector<uint8_t>({'f'}));
EXPECT_TRUE(hmac_cpp::base32_decode("MY", out, false, false));
EXPECT_EQ(out, std::vector<uint8_t>({'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<uint8_t> z2 = {0x00, 0x00};
EXPECT_EQ(hmac_cpp::base36_encode(z2.data(), z2.size()), "000");
std::vector<uint8_t> out;
EXPECT_TRUE(hmac_cpp::base36_decode("000", out));
EXPECT_EQ(out, z2);
std::vector<uint8_t> 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<int> dist(0,255);
for (int len = 0; len < 16; ++len) {
std::vector<uint8_t> buf(len);
for (int i = 0; i < len; ++i) buf[i] = static_cast<uint8_t>(dist(rng));
std::string enc = hmac_cpp::base36_encode(buf.data(), buf.size());
std::vector<uint8_t> 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();
Expand Down
Loading