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