From 49a5368b635bde335ee06702915ef6d9d8b1bd7c Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Sat, 6 Sep 2025 04:20:43 +0300 Subject: [PATCH] feat(encoding): add Base64 and Base32 helpers --- CHANGELOG.md | 4 + CMakeLists.txt | 6 + README-RU.md | 13 + README.md | 13 + example_encoding.cpp | 23 ++ include/hmac_cpp/encoding.hpp | 102 ++++++ include/hmac_cpp/secure_buffer.hpp | 1 + src/encoding.cpp | 478 +++++++++++++++++++++++++++++ test_all.cpp | 48 +++ 9 files changed, 688 insertions(+) create mode 100644 example_encoding.cpp create mode 100644 include/hmac_cpp/encoding.hpp create mode 100644 src/encoding.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 471c986..c05d261 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [Unreleased] +### Added +- Base64 and Base32 encoding/decoding utilities with secure_buffer overloads. + ## [0.3.0] - 2025-09-05 ### Added - PBKDF2 implementation. diff --git a/CMakeLists.txt b/CMakeLists.txt index 1a1f0f6..4728950 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,7 @@ set(HMAC_SOURCES src/sha512.cpp src/hmac.cpp src/hmac_utils.cpp + src/encoding.cpp ) set(HMAC_HEADERS @@ -21,6 +22,7 @@ set(HMAC_HEADERS include/hmac_cpp/sha256.hpp include/hmac_cpp/sha512.hpp include/hmac_cpp/secure_buffer.hpp + include/hmac_cpp/encoding.hpp ) add_library(hmac_cpp ${HMAC_SOURCES}) @@ -58,6 +60,10 @@ endif() add_executable(example_pbkdf2 example_pbkdf2.cpp) target_link_libraries(example_pbkdf2 PRIVATE hmac_cpp) target_include_directories(example_pbkdf2 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) + + add_executable(example_encoding example_encoding.cpp) + target_link_libraries(example_encoding PRIVATE hmac_cpp) + target_include_directories(example_encoding PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) endif() include(CMakePackageConfigHelpers) diff --git a/README-RU.md b/README-RU.md index 54752f4..4a9e33b 100644 --- a/README-RU.md +++ b/README-RU.md @@ -204,6 +204,19 @@ auto key = hmac::pbkdf2_hmac_sha256(password, salt, iters, 32); - Каждому паролю нужна уникальная случайная соль достаточной длины. - Соль, итерации и алгоритм не являются секретом — храните их вместе с хешем или шифротекстом. +### Base64 / Base32 + +Утилиты для кодирования/декодирования Base64 (обычный и URL-алфавит) и Base32. + +```cpp +#include + +std::vector key = {0xff, 0xee}; +std::string b64 = hmac_cpp::base64_encode(key, hmac_cpp::Base64Alphabet::Url, false); +hmac_cpp::secure_buffer raw; +hmac_cpp::base64_decode(b64, raw, hmac_cpp::Base64Alphabet::Url, false); +``` + ### 🕓 HOTP и TOTP токены Библиотека поддерживает генерацию одноразовых паролей по RFC 4226 и RFC 6238. diff --git a/README.md b/README.md index 23f79bf..f51800f 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,19 @@ auto prk = hmac::hkdf_extract_sha256(ikm, salt); auto okm = hmac::hkdf_expand_sha256(prk, /*info=*/{}, /*L=*/32); // L ≤ 255*HashLen ``` +### Base64 / Base32 + +Utility helpers for Base64 (standard or URL alphabet) and Base32. + +```cpp +#include + +std::vector key = {0xff, 0xee}; +std::string b64 = hmac_cpp::base64_encode(key, hmac_cpp::Base64Alphabet::Url, false); +hmac_cpp::secure_buffer raw; +hmac_cpp::base64_decode(b64, raw, hmac_cpp::Base64Alphabet::Url, false); +``` + ### 🕓 HOTP / TOTP OTP per RFC 4226/6238. **Secrets should be random** (not passwords). If you receive Base32 (otpauth URI), decode before calling. diff --git a/example_encoding.cpp b/example_encoding.cpp new file mode 100644 index 0000000..0e3493e --- /dev/null +++ b/example_encoding.cpp @@ -0,0 +1,23 @@ +#include +#include +#include +#include +#include + +int main() { + std::vector data = {'f','o','o','b','a','r'}; + std::string b64 = hmac_cpp::base64_encode(data); + std::cout << "Base64: " << b64 << std::endl; + + std::vector decoded; + hmac_cpp::base64_decode(b64, decoded); + std::cout << "Decoded: " << std::string(decoded.begin(), decoded.end()) << std::endl; + + std::string b32 = hmac_cpp::base32_encode(data); + std::cout << "Base32: " << b32 << std::endl; + + hmac_cpp::secure_buffer sec; + hmac_cpp::base32_decode(b32, sec); + std::cout << "Decoded secure size: " << sec.size() << std::endl; + return 0; +} diff --git a/include/hmac_cpp/encoding.hpp b/include/hmac_cpp/encoding.hpp new file mode 100644 index 0000000..edb1057 --- /dev/null +++ b/include/hmac_cpp/encoding.hpp @@ -0,0 +1,102 @@ +#ifndef _HMAC_ENCODING_HPP_INCLUDED +#define _HMAC_ENCODING_HPP_INCLUDED + +#include "api.hpp" +#include "secure_buffer.hpp" +#include +#include +#include + +namespace hmac_cpp { + + /// \brief Alphabet variant for Base64. + enum class Base64Alphabet { Standard, Url }; // Standard: "+/", Url: "-_" + + // ------------------------- + // Base64 — encode / decode + // ------------------------- + + /// \brief Base64-encode a byte buffer (RFC 4648). + /// \param data Pointer to input bytes. + /// \param len Number of input bytes. + /// \param alphabet Standard ("+/") or URL-safe ("-_") alphabet. + /// \param pad If true, append '=' padding to a multiple of 4 chars. + /// \return Encoded string. + HMAC_CPP_API std::string base64_encode(const uint8_t* data, size_t len, + Base64Alphabet alphabet = Base64Alphabet::Standard, + bool pad = true); + + /// \brief Base64-encode a vector. + inline 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, + Base64Alphabet alphabet = Base64Alphabet::Standard, + bool pad = true) { + return base64_encode(v.data(), v.size(), alphabet, pad); + } + + /// \brief Decode a Base64 string (RFC 4648). + /// \param in Input string (raw Base64 chars). + /// \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. + /// \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, + bool require_padding = false, + bool strict = true) noexcept; + + /// \brief Decode Base64 into secure_buffer. + HMAC_CPP_API bool base64_decode(const std::string& in, secure_buffer& out, + Base64Alphabet alphabet = Base64Alphabet::Standard, + bool require_padding = false, + bool strict = true) noexcept; + + // ------------------------- + // Base32 — encode / decode + // ------------------------- + + /// \brief Base32-encode a byte buffer (RFC 4648; alphabet A–Z, 2–7). + /// \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). + HMAC_CPP_API std::string base32_encode(const uint8_t* data, size_t len, + bool pad = true); + + /// \brief Base32-encode a vector. + inline 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) { + return base32_encode(v.data(), v.size(), pad); + } + + /// \brief Decode a Base32 string (RFC 4648). + /// \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. + /// \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, + bool strict = true) noexcept; + + /// \brief Decode Base32 into secure_buffer. + HMAC_CPP_API bool base32_decode(const std::string& in, secure_buffer& out, + bool require_padding = false, + bool strict = true) noexcept; + +} // namespace hmac_cpp + +#endif // _HMAC_ENCODING_HPP_INCLUDED diff --git a/include/hmac_cpp/secure_buffer.hpp b/include/hmac_cpp/secure_buffer.hpp index 1b260da..c6d04e3 100644 --- a/include/hmac_cpp/secure_buffer.hpp +++ b/include/hmac_cpp/secure_buffer.hpp @@ -2,6 +2,7 @@ #define HMAC_CPP_SECURE_BUFFER_HPP #include +#include #include #include #include diff --git a/src/encoding.cpp b/src/encoding.cpp new file mode 100644 index 0000000..b6d20e1 --- /dev/null +++ b/src/encoding.cpp @@ -0,0 +1,478 @@ +#include "hmac_cpp/encoding.hpp" +#include +#include +#include + +namespace hmac_cpp { + +struct _wipe_string_guard { + 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'); + } +}; + +// ====================== +// Helpers (Base64) +// ====================== + +static inline const char* b64_alphabet(Base64Alphabet a) { + return (a == Base64Alphabet::Standard) + ? "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; +} + +static inline void b64_build_reverse(Base64Alphabet a, int8_t rev[256]) { + 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); + } + rev[ static_cast('=') ] = -2; // padding marker +} + +std::string base64_encode(const uint8_t* data, size_t len, + Base64Alphabet alphabet, bool pad) { + if (len == 0) return std::string(); + + const char* alpha = b64_alphabet(alphabet); + + const size_t full = len / 3; + const size_t rem = len % 3; + const size_t out_len = full * 4 + (rem ? (pad ? 4 : (rem == 1 ? 2 : 3)) : 0); + + std::string out; + out.resize(out_len); + + size_t ip = 0; + size_t op = 0; + + for (size_t i = 0; i < full; ++i) { + const uint32_t n = (static_cast(data[ip]) << 16) | + (static_cast(data[ip + 1]) << 8) | + (static_cast(data[ip + 2])); + ip += 3; + + out[op++] = alpha[(n >> 18) & 0x3F]; + out[op++] = alpha[(n >> 12) & 0x3F]; + out[op++] = alpha[(n >> 6) & 0x3F]; + out[op++] = alpha[(n) & 0x3F]; + } + + if (rem == 1) { + uint32_t n = static_cast(data[ip]) << 16; + out[op++] = alpha[(n >> 18) & 0x3F]; + out[op++] = alpha[(n >> 12) & 0x3F]; + if (pad) { + out[op++] = '='; + out[op++] = '='; + } + } else if (rem == 2) { + uint32_t n = (static_cast(data[ip]) << 16) | + (static_cast(data[ip + 1]) << 8); + out[op++] = alpha[(n >> 18) & 0x3F]; + out[op++] = alpha[(n >> 12) & 0x3F]; + out[op++] = alpha[(n >> 6) & 0x3F]; + if (pad) { + out[op++] = '='; + } + } + + return out; +} + +static inline bool is_space(unsigned char c) { + return c == ' ' || c == '\n' || c == '\r' || c == '\t'; +} + +bool base64_decode(const std::string& in, std::vector& out, + Base64Alphabet alphabet, bool require_padding, bool strict) noexcept { + out.clear(); + if (in.empty()) return true; + + std::string filtered; + filtered.reserve(in.size()); + if (strict) { + filtered.assign(in.begin(), in.end()); + } else { + for (size_t i = 0; i < in.size(); ++i) { + unsigned char c = static_cast(in[i]); + if (!is_space(c)) filtered.push_back(static_cast(c)); + } + } + _wipe_string_guard wipe(filtered); + + const size_t L = filtered.size(); + if (require_padding) { + if ((L % 4) != 0) return false; + } else { + if ((L % 4) == 1) return false; + } + + int8_t rev[256]; + b64_build_reverse(alphabet, rev); + + size_t approx = (L / 4) * 3 + 3; + out.reserve(approx); + + size_t i = 0; + + while (i + 4 <= L) { + int8_t v0 = rev[ static_cast(filtered[i + 0]) ]; + int8_t v1 = rev[ static_cast(filtered[i + 1]) ]; + int8_t v2 = rev[ static_cast(filtered[i + 2]) ]; + int8_t v3 = rev[ static_cast(filtered[i + 3]) ]; + if (v0 < 0 || v1 < 0) return false; + + bool pad2 = (v2 == -2); + bool pad3 = (v3 == -2); + + if (pad2) { + if (i + 4 != L) return false; + if (v3 != -2) return false; + uint32_t n = (static_cast(v0) << 18) | + (static_cast(v1) << 12); + out.push_back(static_cast((n >> 16) & 0xFF)); + return true; + } else if (pad3) { + if (i + 4 != L) return false; + if (v2 < 0 || v2 == -2) return false; + uint32_t n = (static_cast(v0) << 18) | + (static_cast(v1) << 12) | + (static_cast(v2) << 6); + out.push_back(static_cast((n >> 16) & 0xFF)); + out.push_back(static_cast((n >> 8) & 0xFF)); + return true; + } else { + if (v2 < 0 || v3 < 0) return false; + uint32_t n = (static_cast(v0) << 18) | + (static_cast(v1) << 12) | + (static_cast(v2) << 6) | + (static_cast(v3)); + out.push_back(static_cast((n >> 16) & 0xFF)); + out.push_back(static_cast((n >> 8) & 0xFF)); + out.push_back(static_cast((n) & 0xFF)); + } + i += 4; + } + + size_t rem = L - i; + if (rem == 0) { + return true; + } + if (require_padding) { + return false; + } + if (rem == 2) { + int8_t v0 = rev[ static_cast(filtered[i + 0]) ]; + int8_t v1 = rev[ static_cast(filtered[i + 1]) ]; + if (v0 < 0 || v1 < 0) return false; + uint32_t n = (static_cast(v0) << 18) | + (static_cast(v1) << 12); + out.push_back(static_cast((n >> 16) & 0xFF)); + return true; + } else if (rem == 3) { + int8_t v0 = rev[ static_cast(filtered[i + 0]) ]; + int8_t v1 = rev[ static_cast(filtered[i + 1]) ]; + int8_t v2 = rev[ static_cast(filtered[i + 2]) ]; + if (v0 < 0 || v1 < 0 || v2 < 0) return false; + uint32_t n = (static_cast(v0) << 18) | + (static_cast(v1) << 12) | + (static_cast(v2) << 6); + out.push_back(static_cast((n >> 16) & 0xFF)); + out.push_back(static_cast((n >> 8) & 0xFF)); + return true; + } + return false; +} + +bool base64_decode(const std::string& in, secure_buffer& out, + Base64Alphabet alphabet, bool require_padding, bool strict) noexcept { + std::vector tmp; + bool ok = base64_decode(in, tmp, alphabet, require_padding, strict); + 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; +} + +// ====================== +// Base32 (RFC 4648) +// ====================== + +static inline const char* b32_alphabet() { + return "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; +} + +std::string base32_encode(const uint8_t* data, size_t len, bool pad) { + if (len == 0) return std::string(); + + const char* A = b32_alphabet(); + + size_t full = len / 5; + size_t rem = len % 5; + + size_t tail_chars = 0; + switch (rem) { + case 0: tail_chars = 0; break; + case 1: tail_chars = 2; break; + case 2: tail_chars = 4; break; + case 3: tail_chars = 5; break; + case 4: tail_chars = 7; break; + } + + size_t out_len = full * 8 + tail_chars; + if (pad && rem) out_len += (8 - tail_chars); + + std::string out; + out.resize(out_len); + + size_t ip = 0, op = 0; + + for (size_t i = 0; i < full; ++i) { + uint32_t b0 = data[ip + 0]; + uint32_t b1 = data[ip + 1]; + uint32_t b2 = data[ip + 2]; + uint32_t b3 = data[ip + 3]; + uint32_t b4 = data[ip + 4]; + ip += 5; + + out[op++] = A[( b0 >> 3 ) & 0x1F]; + out[op++] = A[( (b0 & 0x07) << 2 ) | ( (b1 >> 6) & 0x03 )]; + out[op++] = A[( (b1 >> 1) & 0x1F )]; + out[op++] = A[( (b1 & 0x01) << 4 ) | ( (b2 >> 4) & 0x0F )]; + out[op++] = A[( (b2 & 0x0F) << 1 ) | ( (b3 >> 7) & 0x01 )]; + out[op++] = A[( (b3 >> 2) & 0x1F )]; + out[op++] = A[( (b3 & 0x03) << 3 ) | ( (b4 >> 5) & 0x07 )]; + out[op++] = A[( b4 & 0x1F )]; + } + + if (rem) { + uint32_t b0 = 0, b1 = 0, b2 = 0, b3 = 0; + switch (rem) { + case 1: b0 = data[ip + 0]; break; + case 2: b0 = data[ip + 0]; b1 = data[ip + 1]; break; + case 3: b0 = data[ip + 0]; b1 = data[ip + 1]; b2 = data[ip + 2]; break; + case 4: b0 = data[ip + 0]; b1 = data[ip + 1]; b2 = data[ip + 2]; b3 = data[ip + 3]; break; + } + + if (rem == 1) { + out[op++] = A[( b0 >> 3 ) & 0x1F]; + out[op++] = A[( (b0 & 0x07) << 2 )]; + if (pad) { out[op++]='='; out[op++]='='; out[op++]='='; out[op++]='='; out[op++]='='; out[op++]='='; } + } else if (rem == 2) { + out[op++] = A[( b0 >> 3 ) & 0x1F]; + out[op++] = A[( (b0 & 0x07) << 2 ) | ( (b1 >> 6) & 0x03 )]; + out[op++] = A[( (b1 >> 1) & 0x1F )]; + out[op++] = A[( (b1 & 0x01) << 4 )]; + if (pad) { out[op++]='='; out[op++]='='; out[op++]='='; out[op++]='='; } + } else if (rem == 3) { + out[op++] = A[( b0 >> 3 ) & 0x1F]; + out[op++] = A[( (b0 & 0x07) << 2 ) | ( (b1 >> 6) & 0x03 )]; + out[op++] = A[( (b1 >> 1) & 0x1F )]; + out[op++] = A[( (b1 & 0x01) << 4 ) | ( (b2 >> 4) & 0x0F )]; + out[op++] = A[( (b2 & 0x0F) << 1 )]; + if (pad) { out[op++]='='; out[op++]='='; out[op++]='='; } + } else if (rem == 4) { + out[op++] = A[( b0 >> 3 ) & 0x1F]; + out[op++] = A[( (b0 & 0x07) << 2 ) | ( (b1 >> 6) & 0x03 )]; + out[op++] = A[( (b1 >> 1) & 0x1F )]; + out[op++] = A[( (b1 & 0x01) << 4 ) | ( (b2 >> 4) & 0x0F )]; + out[op++] = A[( (b2 & 0x0F) << 1 ) | ( (b3 >> 7) & 0x01 )]; + out[op++] = A[( (b3 >> 2) & 0x1F )]; + out[op++] = A[( (b3 & 0x03) << 3 )]; + if (pad) { out[op++]='='; } + } + } + + return out; +} + +static inline void b32_build_reverse(int8_t rev[256], bool accept_lower) { + for (int i = 0; i < 256; ++i) rev[i] = -1; + const char* A = b32_alphabet(); + for (int i = 0; i < 32; ++i) { + unsigned char uc = static_cast(A[i]); + rev[uc] = static_cast(i); + if (accept_lower) { + if (uc >= 'A' && uc <= 'Z') { + unsigned char lc = static_cast(uc - 'A' + 'a'); + rev[lc] = static_cast(i); + } + } + } + rev[ static_cast('=') ] = -2; +} + +bool base32_decode(const std::string& in, std::vector& out, + bool require_padding, bool strict) noexcept { + out.clear(); + if (in.empty()) return true; + + std::string filtered; + filtered.reserve(in.size()); + if (strict) { + filtered.assign(in.begin(), in.end()); + } else { + for (size_t i = 0; i < in.size(); ++i) { + unsigned char c = static_cast(in[i]); + if (!is_space(c)) filtered.push_back(static_cast(c)); + } + } + _wipe_string_guard wipe(filtered); + + const size_t L = filtered.size(); + if (require_padding) { + if ((L % 8) != 0) return false; + } else { + size_t rem = L % 8; + if (rem == 1 || rem == 3 || rem == 6) return false; + } + + int8_t rev[256]; + b32_build_reverse(rev, !strict); + + out.reserve((L / 8) * 5 + 5); + + size_t i = 0; + + while (i + 8 <= L) { + bool has_pad = (filtered[i+0] == '=') || (filtered[i+1] == '=') || + (filtered[i+2] == '=') || (filtered[i+3] == '=') || + (filtered[i+4] == '=') || (filtered[i+5] == '=') || + (filtered[i+6] == '=') || (filtered[i+7] == '='); + if (has_pad && (i + 8 != L)) return false; + + int8_t c0 = rev[ static_cast(filtered[i+0]) ]; + int8_t c1 = rev[ static_cast(filtered[i+1]) ]; + int8_t c2 = rev[ static_cast(filtered[i+2]) ]; + int8_t c3 = rev[ static_cast(filtered[i+3]) ]; + int8_t c4 = rev[ static_cast(filtered[i+4]) ]; + int8_t c5 = rev[ static_cast(filtered[i+5]) ]; + int8_t c6 = rev[ static_cast(filtered[i+6]) ]; + int8_t c7 = rev[ static_cast(filtered[i+7]) ]; + + if (c2 == -2) { + if (!(c0 >= 0 && c1 >= 0) || !(filtered[i+2] == '=' && filtered[i+3] == '=' && + filtered[i+4] == '=' && filtered[i+5] == '=' && + filtered[i+6] == '=' && filtered[i+7] == '=')) return false; + uint8_t b0 = static_cast(( (c0 << 3) & 0xF8 ) | ( (c1 >> 2) & 0x07 )); + out.push_back(b0); + return true; + } else if (c4 == -2) { + if (!(c0 >= 0 && c1 >= 0 && c2 >= 0 && c3 >= 0) || + !(filtered[i+4] == '=' && filtered[i+5] == '=' && + filtered[i+6] == '=' && filtered[i+7] == '=')) return false; + uint8_t b0 = static_cast(( (c0 << 3) & 0xF8 ) | ( (c1 >> 2) & 0x07 )); + uint8_t b1 = static_cast(( (c1 & 0x03) << 6 ) | ( (c2 << 1) & 0x7E ) | ( (c3 >> 4) & 0x01 )); + out.push_back(b0); out.push_back(b1); + return true; + } else if (c5 == -2) { + if (!(c0 >= 0 && c1 >= 0 && c2 >= 0 && c3 >= 0 && c4 >= 0) || + !(filtered[i+5] == '=' && filtered[i+6] == '=' && filtered[i+7] == '=')) return false; + uint8_t b0 = static_cast(( (c0 << 3) & 0xF8 ) | ( (c1 >> 2) & 0x07 )); + uint8_t b1 = static_cast(( (c1 & 0x03) << 6 ) | ( (c2 << 1) & 0x7E ) | ( (c3 >> 4) & 0x01 )); + uint8_t b2 = static_cast(( (c3 & 0x0F) << 4 ) | ( (c4 >> 1) & 0x0F )); + out.push_back(b0); out.push_back(b1); out.push_back(b2); + return true; + } else if (c7 == -2) { + if (!(c0 >= 0 && c1 >= 0 && c2 >= 0 && c3 >= 0 && c4 >= 0 && c5 >= 0 && c6 >= 0) || + !(filtered[i+7] == '=')) return false; + uint8_t b0 = static_cast(( (c0 << 3) & 0xF8 ) | ( (c1 >> 2) & 0x07 )); + uint8_t b1 = static_cast(( (c1 & 0x03) << 6 ) | ( (c2 << 1) & 0x7E ) | ( (c3 >> 4) & 0x01 )); + uint8_t b2 = static_cast(( (c3 & 0x0F) << 4 ) | ( (c4 >> 1) & 0x0F )); + uint8_t b3 = static_cast(( (c4 & 0x01) << 7 ) | ( (c5 << 2) & 0x7C ) | ( (c6 >> 3) & 0x03 )); + out.push_back(b0); out.push_back(b1); out.push_back(b2); out.push_back(b3); + return true; + } else { + if (c0 < 0 || c1 < 0 || c2 < 0 || c3 < 0 || c4 < 0 || c5 < 0 || c6 < 0 || c7 < 0) return false; + uint8_t b0 = static_cast(( (c0 << 3) & 0xF8 ) | ( (c1 >> 2) & 0x07 )); + uint8_t b1 = static_cast(( (c1 & 0x03) << 6 ) | ( (c2 << 1) & 0x7E ) | ( (c3 >> 4) & 0x01 )); + uint8_t b2 = static_cast(( (c3 & 0x0F) << 4 ) | ( (c4 >> 1) & 0x0F )); + uint8_t b3 = static_cast(( (c4 & 0x01) << 7 ) | ( (c5 << 2) & 0x7C ) | ( (c6 >> 3) & 0x03 )); + uint8_t b4 = static_cast(( (c6 & 0x07) << 5 ) | ( (c7) & 0x1F )); + out.push_back(b0); out.push_back(b1); out.push_back(b2); out.push_back(b3); out.push_back(b4); + } + i += 8; + } + + size_t rem = L - i; + if (rem == 0) return true; + if (require_padding) return false; + if (!(rem == 2 || rem == 4 || rem == 5 || rem == 7)) return false; + + int8_t c0 = rev[ static_cast(filtered[i + 0]) ]; + int8_t c1 = rev[ static_cast(filtered[i + 1]) ]; + if (c0 < 0 || c1 < 0) return false; + + if (rem == 2) { + uint8_t b0 = static_cast(( (c0 << 3) & 0xF8 ) | ( (c1 >> 2) & 0x07 )); + out.push_back(b0); + return true; + } + + int8_t c2 = rev[ static_cast(filtered[i + 2]) ]; + int8_t c3 = rev[ static_cast(filtered[i + 3]) ]; + if (c2 < 0 || c3 < 0) return false; + + if (rem == 4) { + uint8_t b0 = static_cast(( (c0 << 3) & 0xF8 ) | ( (c1 >> 2) & 0x07 )); + uint8_t b1 = static_cast(( (c1 & 0x03) << 6 ) | ( (c2 << 1) & 0x7E ) | ( (c3 >> 4) & 0x01 )); + out.push_back(b0); out.push_back(b1); + return true; + } + + int8_t c4 = rev[ static_cast(filtered[i + 4]) ]; + if (c4 < 0) return false; + + if (rem == 5) { + uint8_t b0 = static_cast(( (c0 << 3) & 0xF8 ) | ( (c1 >> 2) & 0x07 )); + uint8_t b1 = static_cast(( (c1 & 0x03) << 6 ) | ( (c2 << 1) & 0x7E ) | ( (c3 >> 4) & 0x01 )); + uint8_t b2 = static_cast(( (c3 & 0x0F) << 4 ) | ( (c4 >> 1) & 0x0F )); + out.push_back(b0); out.push_back(b1); out.push_back(b2); + return true; + } + + int8_t c5 = rev[ static_cast(filtered[i + 5]) ]; + int8_t c6 = rev[ static_cast(filtered[i + 6]) ]; + if (c5 < 0 || c6 < 0) return false; + + uint8_t b0 = static_cast(( (c0 << 3) & 0xF8 ) | ( (c1 >> 2) & 0x07 )); + uint8_t b1 = static_cast(( (c1 & 0x03) << 6 ) | ( (c2 << 1) & 0x7E ) | ( (c3 >> 4) & 0x01 )); + uint8_t b2 = static_cast(( (c3 & 0x0F) << 4 ) | ( (c4 >> 1) & 0x0F )); + uint8_t b3 = static_cast(( (c4 & 0x01) << 7 ) | ( (c5 << 2) & 0x7C ) | ( (c6 >> 3) & 0x03 )); + out.push_back(b0); out.push_back(b1); out.push_back(b2); out.push_back(b3); + return true; +} + +bool base32_decode(const std::string& in, secure_buffer& out, + bool require_padding, bool strict) noexcept { + std::vector tmp; + bool ok = base32_decode(in, tmp, require_padding, strict); + 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 53af966..b3b5447 100644 --- a/test_all.cpp +++ b/test_all.cpp @@ -11,6 +11,7 @@ #include "hmac_cpp/hmac.hpp" #include "hmac_cpp/hmac_utils.hpp" +#include "hmac_cpp/encoding.hpp" static std::time_t mock_time_value = 0; static int mock_errno_value = 0; @@ -589,6 +590,53 @@ TEST(TotpTimeErrorTest, ValidityNegativeTimeThrows) { mock_errno_value = 0; } +TEST(EncodingTest, Base64Vectors) { + std::vector> cases = { + {"f", "Zg=="}, + {"fo", "Zm8="}, + {"foo", "Zm9v"}, + {"foob", "Zm9vYg=="}, + {"fooba", "Zm9vYmE="}, + {"foobar", "Zm9vYmFy"} + }; + for (auto& c : cases) { + std::vector bin(c.first.begin(), c.first.end()); + EXPECT_EQ(hmac_cpp::base64_encode(bin), c.second); + std::vector out; + EXPECT_TRUE(hmac_cpp::base64_decode(c.second, out)); + EXPECT_EQ(out, bin); + } + std::vector special = {0xfb, 0xff}; + EXPECT_EQ(hmac_cpp::base64_encode(special), "+/8="); + EXPECT_EQ(hmac_cpp::base64_encode(special, hmac_cpp::Base64Alphabet::Url), "-_8="); + hmac_cpp::secure_buffer sec; + EXPECT_TRUE(hmac_cpp::base64_decode("Zm9v", sec)); + std::string foo(sec.begin(), sec.end()); + EXPECT_EQ(foo, "foo"); +} + +TEST(EncodingTest, Base32Vectors) { + std::vector> cases = { + {"f", "MY======"}, + {"fo", "MZXQ===="}, + {"foo", "MZXW6==="}, + {"foob", "MZXW6YQ="}, + {"fooba", "MZXW6YTB"}, + {"foobar", "MZXW6YTBOI======"} + }; + for (auto& c : cases) { + std::vector bin(c.first.begin(), c.first.end()); + EXPECT_EQ(hmac_cpp::base32_encode(bin), c.second); + std::vector out; + EXPECT_TRUE(hmac_cpp::base32_decode(c.second, out, /*require_padding=*/true)); + EXPECT_EQ(out, bin); + } + hmac_cpp::secure_buffer sec; + EXPECT_TRUE(hmac_cpp::base32_decode("MZXW6===", sec, true)); + std::string foo(sec.begin(), sec.end()); + EXPECT_EQ(foo, "foo"); +} + int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS();