Implement secure key via KDF for transparent data encryption/decryption #60
This commit is contained in:
389
support/test/src/utils/base64_test.cpp
Normal file
389
support/test/src/utils/base64_test.cpp
Normal file
@@ -0,0 +1,389 @@
|
||||
/*
|
||||
Copyright <2018-2025> <scott.e.graves@protonmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
#include "test.hpp"
|
||||
|
||||
using macaron::Base64::Decode;
|
||||
using macaron::Base64::Encode;
|
||||
using macaron::Base64::EncodeUrlSafe;
|
||||
|
||||
namespace {
|
||||
[[nodiscard]] auto decode_to_string(std::string_view str) -> std::string {
|
||||
auto vec = Decode(str);
|
||||
return {vec.begin(), vec.end()};
|
||||
}
|
||||
|
||||
[[nodiscard]] auto standard_to_url_safe(std::string str, bool keep_padding)
|
||||
-> std::string {
|
||||
for (auto &cur_ch : str) {
|
||||
if (cur_ch == '+') {
|
||||
cur_ch = '-';
|
||||
} else if (cur_ch == '/') {
|
||||
cur_ch = '_';
|
||||
}
|
||||
}
|
||||
if (not keep_padding) {
|
||||
while (not str.empty() && str.back() == '=') {
|
||||
str.pop_back();
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(utils_base64, rfc4648_known_vectors_standard_padded) {
|
||||
struct vec_case {
|
||||
std::string_view in;
|
||||
std::string_view b64;
|
||||
};
|
||||
const std::array<vec_case, 7> vectors{{
|
||||
{"", ""},
|
||||
{"f", "Zg=="},
|
||||
{"fo", "Zm8="},
|
||||
{"foo", "Zm9v"},
|
||||
{"foob", "Zm9vYg=="},
|
||||
{"fooba", "Zm9vYmE="},
|
||||
{"foobar", "Zm9vYmFy"},
|
||||
}};
|
||||
|
||||
for (const auto &vec_entry : vectors) {
|
||||
const auto enc_str =
|
||||
Encode(reinterpret_cast<const unsigned char *>(vec_entry.in.data()),
|
||||
vec_entry.in.size(), /*url_safe=*/false, /*pad=*/true);
|
||||
EXPECT_EQ(enc_str, vec_entry.b64);
|
||||
|
||||
const auto dec_vec = Decode(vec_entry.b64);
|
||||
EXPECT_EQ(std::string(dec_vec.begin(), dec_vec.end()), vec_entry.in);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(utils_base64, url_safe_padded_and_unpadded_match_transformed_standard) {
|
||||
const std::string payload =
|
||||
std::string("This+/needs/URL-safe mapping and padding checks.") +
|
||||
std::string("\x00\x01\xFE\xFF", 4);
|
||||
|
||||
const auto std_padded =
|
||||
Encode(reinterpret_cast<const unsigned char *>(payload.data()),
|
||||
payload.size(), /*url_safe=*/false, /*pad=*/true);
|
||||
const auto url_padded =
|
||||
Encode(reinterpret_cast<const unsigned char *>(payload.data()),
|
||||
payload.size(), /*url_safe=*/true, /*pad=*/true);
|
||||
const auto url_unpadded =
|
||||
Encode(reinterpret_cast<const unsigned char *>(payload.data()),
|
||||
payload.size(), /*url_safe=*/true, /*pad=*/false);
|
||||
|
||||
const auto url_from_std_padded =
|
||||
standard_to_url_safe(std_padded, /*keep_padding=*/true);
|
||||
const auto url_from_std_unpadded =
|
||||
standard_to_url_safe(std_padded, /*keep_padding=*/false);
|
||||
|
||||
EXPECT_EQ(url_padded, url_from_std_padded);
|
||||
EXPECT_EQ(url_unpadded, url_from_std_unpadded);
|
||||
|
||||
const auto dec_one = Decode(url_padded);
|
||||
const auto dec_two = Decode(url_unpadded);
|
||||
EXPECT_EQ(std::string(dec_one.begin(), dec_one.end()), payload);
|
||||
EXPECT_EQ(std::string(dec_two.begin(), dec_two.end()), payload);
|
||||
}
|
||||
|
||||
TEST(utils_base64, empty_input) {
|
||||
const std::string empty_str;
|
||||
const auto enc_empty_std =
|
||||
Encode(reinterpret_cast<const unsigned char *>(empty_str.data()),
|
||||
empty_str.size(), /*url_safe=*/false, /*pad=*/true);
|
||||
const auto enc_empty_url =
|
||||
Encode(reinterpret_cast<const unsigned char *>(empty_str.data()),
|
||||
empty_str.size(), /*url_safe=*/true, /*pad=*/false);
|
||||
EXPECT_TRUE(enc_empty_std.empty());
|
||||
EXPECT_TRUE(enc_empty_url.empty());
|
||||
|
||||
const auto dec_empty = Decode("");
|
||||
EXPECT_TRUE(dec_empty.empty());
|
||||
}
|
||||
|
||||
TEST(utils_base64, remainder_boundaries_round_trip) {
|
||||
const std::string str_one = "A"; // rem 1
|
||||
const std::string str_two = "AB"; // rem 2
|
||||
const std::string str_thr = "ABC"; // rem 0
|
||||
const std::string str_fou = "ABCD"; // rem 1 after blocks
|
||||
const std::string str_fiv = "ABCDE"; // rem 2 after blocks
|
||||
|
||||
for (const auto *str_ptr :
|
||||
{&str_one, &str_two, &str_thr, &str_fou, &str_fiv}) {
|
||||
const auto enc_std =
|
||||
Encode(reinterpret_cast<const unsigned char *>(str_ptr->data()),
|
||||
str_ptr->size(), false, true);
|
||||
const auto dec_std = Decode(enc_std);
|
||||
EXPECT_EQ(std::string(dec_std.begin(), dec_std.end()), *str_ptr);
|
||||
|
||||
const auto enc_url_pad =
|
||||
Encode(reinterpret_cast<const unsigned char *>(str_ptr->data()),
|
||||
str_ptr->size(), true, true);
|
||||
const auto dec_url_pad = Decode(enc_url_pad);
|
||||
EXPECT_EQ(std::string(dec_url_pad.begin(), dec_url_pad.end()), *str_ptr);
|
||||
|
||||
const auto enc_url_nopad =
|
||||
Encode(reinterpret_cast<const unsigned char *>(str_ptr->data()),
|
||||
str_ptr->size(), true, false);
|
||||
const auto dec_url_nopad = Decode(enc_url_nopad);
|
||||
EXPECT_EQ(std::string(dec_url_nopad.begin(), dec_url_nopad.end()),
|
||||
*str_ptr);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(utils_base64, decode_accepts_standard_and_url_safe_forms) {
|
||||
const std::string input_str = "Man is distinguished, not only by his reason.";
|
||||
const auto std_padded =
|
||||
Encode(reinterpret_cast<const unsigned char *>(input_str.data()),
|
||||
input_str.size(), false, true);
|
||||
const auto url_padded =
|
||||
Encode(reinterpret_cast<const unsigned char *>(input_str.data()),
|
||||
input_str.size(), true, true);
|
||||
const auto url_unpadded =
|
||||
Encode(reinterpret_cast<const unsigned char *>(input_str.data()),
|
||||
input_str.size(), true, false);
|
||||
|
||||
const auto dec_std = Decode(std_padded);
|
||||
const auto dec_url_pad = Decode(url_padded);
|
||||
const auto dec_url_nopad = Decode(url_unpadded);
|
||||
|
||||
EXPECT_EQ(std::string(dec_std.begin(), dec_std.end()), input_str);
|
||||
EXPECT_EQ(std::string(dec_url_pad.begin(), dec_url_pad.end()), input_str);
|
||||
EXPECT_EQ(std::string(dec_url_nopad.begin(), dec_url_nopad.end()), input_str);
|
||||
}
|
||||
|
||||
TEST(utils_base64, all_byte_values_round_trip) {
|
||||
std::vector<unsigned char> byte_vec(256);
|
||||
for (size_t idx = 0; idx < byte_vec.size(); ++idx) {
|
||||
byte_vec[idx] = static_cast<unsigned char>(idx);
|
||||
}
|
||||
|
||||
const auto enc_std = Encode(byte_vec.data(), byte_vec.size(), false, true);
|
||||
const auto dec_std = Decode(enc_std);
|
||||
ASSERT_EQ(dec_std.size(), byte_vec.size());
|
||||
EXPECT_TRUE(std::equal(dec_std.begin(), dec_std.end(), byte_vec.begin()));
|
||||
|
||||
const auto enc_url = Encode(byte_vec.data(), byte_vec.size(), true, false);
|
||||
const auto dec_url = Decode(enc_url);
|
||||
ASSERT_EQ(dec_url.size(), byte_vec.size());
|
||||
EXPECT_TRUE(std::equal(dec_url.begin(), dec_url.end(), byte_vec.begin()));
|
||||
}
|
||||
|
||||
TEST(utils_base64, wrapper_encode_url_safe_equals_flagged_encode) {
|
||||
const std::string data_str = "wrap me!";
|
||||
const auto enc_wrap_a =
|
||||
EncodeUrlSafe(reinterpret_cast<const unsigned char *>(data_str.data()),
|
||||
data_str.size(), /*pad=*/false);
|
||||
const auto enc_wrap_b =
|
||||
Encode(reinterpret_cast<const unsigned char *>(data_str.data()),
|
||||
data_str.size(), /*url_safe=*/true, /*pad=*/false);
|
||||
EXPECT_EQ(enc_wrap_a, enc_wrap_b);
|
||||
|
||||
const auto enc_wrap_a2 = EncodeUrlSafe(data_str, /*pad=*/true);
|
||||
const auto enc_wrap_b2 = Encode(data_str, /*url_safe=*/true, /*pad=*/true);
|
||||
EXPECT_EQ(enc_wrap_a2, enc_wrap_b2);
|
||||
}
|
||||
|
||||
TEST(utils_base64, unpadded_length_rules) {
|
||||
const auto enc_one = Encode("f", /*url_safe=*/true, /*pad=*/false);
|
||||
const auto enc_two = Encode("fo", /*url_safe=*/true, /*pad=*/false);
|
||||
const auto enc_thr = Encode("foo", /*url_safe=*/true, /*pad=*/false);
|
||||
EXPECT_EQ(enc_one.size(), 2U);
|
||||
EXPECT_EQ(enc_two.size(), 3U);
|
||||
EXPECT_EQ(enc_thr.size(), 4U);
|
||||
|
||||
EXPECT_EQ(Decode(enc_one), std::vector<unsigned char>({'f'}));
|
||||
EXPECT_EQ(Decode(enc_two), std::vector<unsigned char>({'f', 'o'}));
|
||||
EXPECT_EQ(Decode(enc_thr), std::vector<unsigned char>({'f', 'o', 'o'}));
|
||||
}
|
||||
|
||||
TEST(utils_base64, errors_length_mod4_eq_1) {
|
||||
EXPECT_THROW(Decode("A"), std::runtime_error);
|
||||
EXPECT_THROW(Decode("AAAAA"), std::runtime_error);
|
||||
}
|
||||
|
||||
TEST(utils_base64, errors_invalid_characters) {
|
||||
EXPECT_THROW(Decode("Zm9v YmFy"), std::runtime_error);
|
||||
EXPECT_THROW(Decode("Zm9v*YmFy"), std::runtime_error);
|
||||
EXPECT_THROW(Decode("Z=g="), std::runtime_error);
|
||||
}
|
||||
|
||||
TEST(utils_base64, reject_whitespace_and_controls) {
|
||||
// newline, tab, and space should be rejected (decoder does not skip
|
||||
// whitespace)
|
||||
EXPECT_THROW(Decode("Zg==\n"), std::runtime_error);
|
||||
EXPECT_THROW(Decode("Zg==\t"), std::runtime_error);
|
||||
EXPECT_THROW(Decode("Z g=="), std::runtime_error);
|
||||
}
|
||||
|
||||
TEST(utils_base64, reject_padding_in_nonfinal_quartet) {
|
||||
// '=' cannot appear before the final quartet
|
||||
EXPECT_THROW(Decode("AAA=AAAA"), std::runtime_error);
|
||||
EXPECT_THROW(Decode("Zg==Zg=="), std::runtime_error);
|
||||
}
|
||||
|
||||
TEST(utils_base64, reject_padding_in_first_two_slots_of_final_quartet) {
|
||||
// '=' only allowed in slots 3 and/or 4 of the final quartet
|
||||
EXPECT_THROW(Decode("=AAA"), std::runtime_error);
|
||||
EXPECT_THROW(Decode("A=AA"), std::runtime_error);
|
||||
EXPECT_THROW(
|
||||
Decode("Z=g="),
|
||||
std::runtime_error); // already in your suite, kept for completeness
|
||||
}
|
||||
|
||||
TEST(utils_base64, reject_incorrect_padding_count_for_length) {
|
||||
// "f" must be "Zg==" (two '='). One '=' is invalid.
|
||||
EXPECT_THROW(Decode("Zg="), std::runtime_error);
|
||||
|
||||
// "foo" must be unpadded ("Zm9v"). Extra '=' is invalid.
|
||||
EXPECT_THROW(Decode("Zm9v="), std::runtime_error);
|
||||
|
||||
// "fo" must have exactly one '=' -> "Zm8="
|
||||
// Correct cases:
|
||||
EXPECT_NO_THROW(Decode("Zm8="));
|
||||
EXPECT_NO_THROW(Decode("Zm9v"));
|
||||
}
|
||||
|
||||
TEST(utils_base64, accept_unpadded_equivalents_when_legal) {
|
||||
EXPECT_EQ(decode_to_string("Zg"), "f");
|
||||
EXPECT_EQ(decode_to_string("Zm8"), "fo");
|
||||
EXPECT_EQ(decode_to_string("Zm9v"), "foo");
|
||||
EXPECT_EQ(decode_to_string("Zm9vYmE"), "fooba");
|
||||
}
|
||||
|
||||
TEST(utils_base64, mixed_alphabet_is_accepted) {
|
||||
const std::string input_str = "any+/mix_/of+chars/";
|
||||
const auto std_padded =
|
||||
Encode(reinterpret_cast<const unsigned char *>(input_str.data()),
|
||||
input_str.size(), /*url_safe=*/false, /*pad=*/true);
|
||||
|
||||
std::string mixed = std_padded;
|
||||
for (char &cur_ch : mixed) {
|
||||
if (cur_ch == '+') {
|
||||
cur_ch = '-';
|
||||
} else if (cur_ch == '/') {
|
||||
cur_ch = '_';
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_EQ(decode_to_string(mixed), input_str);
|
||||
}
|
||||
|
||||
TEST(utils_base64, invalid_non_ascii_octets_in_input) {
|
||||
// Extended bytes like 0xFF are not valid Base64 characters
|
||||
std::string bad = "Zg==";
|
||||
bad[1] = static_cast<char>(0xFF);
|
||||
EXPECT_THROW(Decode(bad), std::runtime_error);
|
||||
}
|
||||
|
||||
TEST(utils_base64, large_buffer_round_trip_and_sizes) {
|
||||
// Deterministic pseudo-random buffer
|
||||
const std::size_t byte_len = 1 << 20; // 1 MiB
|
||||
std::vector<unsigned char> data_vec(byte_len);
|
||||
unsigned int val = 0x12345678U;
|
||||
for (unsigned char &idx : data_vec) {
|
||||
val ^= val << 13;
|
||||
val ^= val >> 17;
|
||||
val ^= val << 5; // xorshift32
|
||||
idx = static_cast<unsigned char>(val & 0xFFU);
|
||||
}
|
||||
|
||||
// Padded encode length should be 4 * ceil(N/3)
|
||||
const auto enc_pad = Encode(data_vec.data(), data_vec.size(),
|
||||
/*url_safe=*/false, /*pad=*/true);
|
||||
const std::size_t expected_padded = 4U * ((byte_len + 2U) / 3U);
|
||||
EXPECT_EQ(enc_pad.size(), expected_padded);
|
||||
|
||||
// Unpadded encode length rule (RFC 4648 §5)
|
||||
const auto enc_nopad = Encode(data_vec.data(), data_vec.size(),
|
||||
/*url_safe=*/true, /*pad=*/false);
|
||||
const std::size_t rem = byte_len % 3U;
|
||||
const std::size_t expected_unpadded =
|
||||
4U * (byte_len / 3U) + (rem == 0U ? 0U : (rem == 1U ? 2U : 3U));
|
||||
EXPECT_EQ(enc_nopad.size(), expected_unpadded);
|
||||
|
||||
// Round-trips
|
||||
const auto dec_pad = Decode(enc_pad);
|
||||
const auto dec_nopad = Decode(enc_nopad);
|
||||
ASSERT_EQ(dec_pad.size(), data_vec.size());
|
||||
ASSERT_EQ(dec_nopad.size(), data_vec.size());
|
||||
EXPECT_TRUE(std::equal(dec_pad.begin(), dec_pad.end(), data_vec.begin()));
|
||||
EXPECT_TRUE(std::equal(dec_nopad.begin(), dec_nopad.end(), data_vec.begin()));
|
||||
}
|
||||
|
||||
TEST(utils_base64, url_safe_round_trip_various_lengths) {
|
||||
for (std::size_t len : {0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U, 32U, 33U, 64U, 65U}) {
|
||||
std::vector<unsigned char> buf(len);
|
||||
for (std::size_t i = 0; i < len; ++i) {
|
||||
buf[i] = static_cast<unsigned char>(i * 13U + 7U);
|
||||
}
|
||||
|
||||
const auto enc_unpadded =
|
||||
Encode(buf.data(), buf.size(), /*url_safe=*/true, /*pad=*/false);
|
||||
const auto enc_padded =
|
||||
Encode(buf.data(), buf.size(), /*url_safe=*/true, /*pad=*/true);
|
||||
|
||||
const auto dec_unpadded = Decode(enc_unpadded);
|
||||
const auto dec_padded = Decode(enc_padded);
|
||||
|
||||
ASSERT_EQ(dec_unpadded.size(), buf.size());
|
||||
ASSERT_EQ(dec_padded.size(), buf.size());
|
||||
EXPECT_TRUE(
|
||||
std::equal(dec_unpadded.begin(), dec_unpadded.end(), buf.begin()));
|
||||
EXPECT_TRUE(std::equal(dec_padded.begin(), dec_padded.end(), buf.begin()));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(utils_base64, reject_trailing_garbage_after_padding) {
|
||||
// Anything after final '=' padding is invalid
|
||||
EXPECT_THROW(Decode("Zg==A"), std::runtime_error);
|
||||
EXPECT_THROW(Decode("Zm8=A"), std::runtime_error);
|
||||
}
|
||||
|
||||
TEST(utils_base64, reject_three_padding_chars_total) {
|
||||
// Any string with total length %4==1 is invalid (e.g., "Zg===")
|
||||
EXPECT_THROW(Decode("Zg==="), std::runtime_error);
|
||||
}
|
||||
|
||||
TEST(utils_base64, standard_vs_url_safe_encoding_equivalence) {
|
||||
const std::string msg = "base64 / url-safe + cross-check";
|
||||
|
||||
const auto std_enc =
|
||||
Encode(reinterpret_cast<const unsigned char *>(msg.data()), msg.size(),
|
||||
/*url_safe=*/false, /*pad=*/true);
|
||||
const auto url_enc =
|
||||
Encode(reinterpret_cast<const unsigned char *>(msg.data()), msg.size(),
|
||||
/*url_safe=*/true, /*pad=*/true);
|
||||
|
||||
std::string transformed = std_enc;
|
||||
for (char &cur_ch : transformed) {
|
||||
if (cur_ch == '+') {
|
||||
cur_ch = '-';
|
||||
} else if (cur_ch == '/') {
|
||||
cur_ch = '_';
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_EQ(url_enc, transformed);
|
||||
|
||||
// decode once, then construct
|
||||
EXPECT_EQ(decode_to_string(url_enc), msg);
|
||||
}
|
Reference in New Issue
Block a user