From 1efbfb3056c2cc3def8bb07e7e298dbb0b1cce98 Mon Sep 17 00:00:00 2001 From: Ahson Khan Date: Fri, 8 Jan 2021 21:20:44 -0800 Subject: [PATCH] Add Base64 encoding and decoding utility APIs to the `Azure::Core` namespace available from `azure/core/base64.hpp`. (#1297) * Add Base64 encode and decode APIs to Azure::Core (from Azure::Storage). * Revert changes to crypt.hpp to leave callsite updates for auto-gen storage component for later. * Add entry to changelog. * Add basic unit tests. * Remove definition of NOMINMAX since it is unnecessary. * Include missing header from openssl. * Include missing open ssl buffer header. * Address feedback around angle brackets, and move implementation into a namespace block. * Remove redundant namespace qualifier in the signature. * Link openssl to azure-core for non-Windows platforms, to fix the MacOS linking issue. --- sdk/core/azure-core/CHANGELOG.md | 1 + sdk/core/azure-core/CMakeLists.txt | 8 ++ sdk/core/azure-core/inc/azure/core/base64.hpp | 33 +++++++ sdk/core/azure-core/src/base64.cpp | 96 +++++++++++++++++++ sdk/core/azure-core/test/ut/CMakeLists.txt | 1 + sdk/core/azure-core/test/ut/base64.cpp | 63 ++++++++++++ 6 files changed, 202 insertions(+) create mode 100644 sdk/core/azure-core/inc/azure/core/base64.hpp create mode 100644 sdk/core/azure-core/src/base64.cpp create mode 100644 sdk/core/azure-core/test/ut/base64.cpp diff --git a/sdk/core/azure-core/CHANGELOG.md b/sdk/core/azure-core/CHANGELOG.md index 105034b1c..bc2089b80 100644 --- a/sdk/core/azure-core/CHANGELOG.md +++ b/sdk/core/azure-core/CHANGELOG.md @@ -9,6 +9,7 @@ - Added support for long-running operations with `Operation`. - Added support for setting a custom transport adapter by implementing the method `std::shared_ptr ::AzureSdkGetCustomHttpTransport()`. - Added default constructor to `DateTime`. +- Added Base64 encoding and decoding utility APIs to the `Azure::Core` namespace available from `azure/core/base64.hpp`. ### Breaking Changes diff --git a/sdk/core/azure-core/CMakeLists.txt b/sdk/core/azure-core/CMakeLists.txt index 76bcf71b4..236724c4d 100644 --- a/sdk/core/azure-core/CMakeLists.txt +++ b/sdk/core/azure-core/CMakeLists.txt @@ -64,6 +64,7 @@ set( inc/azure/core/internal/contract.hpp inc/azure/core/internal/log.hpp inc/azure/core/logging/logging.hpp + inc/azure/core/base64.hpp inc/azure/core/context.hpp inc/azure/core/credentials.hpp inc/azure/core/datetime.hpp @@ -95,6 +96,7 @@ set( src/http/transport_policy.cpp src/http/url.cpp src/logging/logging.cpp + src/base64.cpp src/context.cpp src/datetime.cpp src/operation_status.cpp @@ -122,6 +124,12 @@ target_include_directories(azure-core PUBLIC ${CURL_INCLUDE_DIRS}) target_include_directories(azure-core INTERFACE ${nlohmann_json_INCLUDE_DIRS}) target_link_libraries(azure-core INTERFACE Threads::Threads nlohmann_json::nlohmann_json) +if(NOT MSVC) + message("Find the OpenSSL package on MacOS, used for Base64 encoding.") + find_package(OpenSSL REQUIRED) + target_link_libraries(azure-core PRIVATE OpenSSL::SSL) +endif() + if(BUILD_TRANSPORT_CURL) target_link_libraries(azure-core PRIVATE CURL::libcurl) endif() diff --git a/sdk/core/azure-core/inc/azure/core/base64.hpp b/sdk/core/azure-core/inc/azure/core/base64.hpp new file mode 100644 index 000000000..4907c6c41 --- /dev/null +++ b/sdk/core/azure-core/inc/azure/core/base64.hpp @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * @brief Utility functions to help convert between binary data and UTF-8 encoded text that is + * represented in base 64. + */ + +#pragma once + +#include +#include + +namespace Azure { namespace Core { + + /** + * @brief Encodes the vector of binary data into UTF-8 encoded text represented as base 64. + * + * @param data The input vector that contains binary data that needs to be encoded. + * @return The UTF-8 encoded text in base 64. + */ + std::string Base64Encode(const std::vector& data); + + /** + * @brief Decodes the UTF-8 encoded text represented as base 64 into binary data. + * + * @param text The input UTF-8 encoded text in base 64 that needs to be decoded. + * @return The decoded binary data. + */ + std::vector Base64Decode(const std::string& text); + +}} // namespace Azure::Core diff --git a/sdk/core/azure-core/src/base64.cpp b/sdk/core/azure-core/src/base64.cpp new file mode 100644 index 000000000..5ddb97131 --- /dev/null +++ b/sdk/core/azure-core/src/base64.cpp @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "azure/core/base64.hpp" +#include "azure/core/platform.hpp" + +#if defined(AZ_PLATFORM_WINDOWS) +#include +#elif defined(AZ_PLATFORM_POSIX) +#include +#include +#include +#endif + +#include +#include + +namespace Azure { namespace Core { + +#if defined(AZ_PLATFORM_WINDOWS) + + std::string Base64Encode(const std::vector& data) + { + std::string encoded; + // According to RFC 4648, the encoded length should be ceiling(n / 3) * 4 + DWORD encodedLength = static_cast((data.size() + 2) / 3 * 4); + encoded.resize(encodedLength); + + CryptBinaryToStringA( + reinterpret_cast(data.data()), + static_cast(data.size()), + CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, + static_cast(&encoded[0]), + &encodedLength); + + return encoded; + } + + std::vector Base64Decode(const std::string& text) + { + std::vector decoded; + // According to RFC 4648, the encoded length should be ceiling(n / 3) * 4, so we can infer an + // upper bound here + DWORD decodedLength = DWORD(text.length() / 4 * 3); + decoded.resize(decodedLength); + + CryptStringToBinaryA( + text.data(), + static_cast(text.length()), + CRYPT_STRING_BASE64 | CRYPT_STRING_STRICT, + reinterpret_cast(decoded.data()), + &decodedLength, + nullptr, + nullptr); + decoded.resize(decodedLength); + return decoded; + } + +#elif defined(AZ_PLATFORM_POSIX) + + std::string Base64Encode(const std::vector& data) + { + BIO* bio = BIO_new(BIO_s_mem()); + bio = BIO_push(BIO_new(BIO_f_base64()), bio); + BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL); + BIO_write(bio, data.data(), static_cast(data.size())); + BIO_flush(bio); + BUF_MEM* bufferPtr; + BIO_get_mem_ptr(bio, &bufferPtr); + BIO_set_close(bio, BIO_NOCLOSE); + BIO_free_all(bio); + + return std::string(bufferPtr->data, bufferPtr->length); + } + + std::vector Base64Decode(const std::string& text) + { + std::vector decoded; + // According to RFC 4648, the encoded length should be ceiling(n / 3) * 4, so we can infer an + // upper bound here + std::size_t maxDecodedLength = text.length() / 4 * 3; + decoded.resize(maxDecodedLength); + + BIO* bio = BIO_new_mem_buf(text.data(), -1); + bio = BIO_push(BIO_new(BIO_f_base64()), bio); + BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL); + int decodedLength = BIO_read(bio, &decoded[0], static_cast(text.length())); + BIO_free_all(bio); + + decoded.resize(decodedLength); + return decoded; + } + +#endif + +}} // namespace Azure::Core \ No newline at end of file diff --git a/sdk/core/azure-core/test/ut/CMakeLists.txt b/sdk/core/azure-core/test/ut/CMakeLists.txt index e17716861..c85aaec44 100644 --- a/sdk/core/azure-core/test/ut/CMakeLists.txt +++ b/sdk/core/azure-core/test/ut/CMakeLists.txt @@ -29,6 +29,7 @@ include(GoogleTest) add_executable ( azure-core-test + base64.cpp context.cpp ${CURL_CONNECTION_POOL_TESTS} ${CURL_OPTIONS_TESTS} diff --git a/sdk/core/azure-core/test/ut/base64.cpp b/sdk/core/azure-core/test/ut/base64.cpp new file mode 100644 index 000000000..eb27e98bf --- /dev/null +++ b/sdk/core/azure-core/test/ut/base64.cpp @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include + +using namespace Azure::Core; + +TEST(Base64, Basic) +{ + int maxLength = 7; + + std::vector data; + for (int i = 0; i < maxLength; i++) + { + data.push_back(i + 1); + } + + std::string result = Base64Encode(data); + EXPECT_PRED2( + [](std::string expect, std::string actual) { return expect == actual; }, + result, + "AQIDBAUGBw=="); + EXPECT_TRUE(std::equal(data.begin(), data.end(), Base64Decode(result).begin())); + + std::vector subsection = std::vector(data.begin(), data.begin() + 1); + result = Base64Encode(subsection); + EXPECT_PRED2( + [](std::string expect, std::string actual) { return expect == actual; }, result, "AQ=="); + EXPECT_TRUE(std::equal(subsection.begin(), subsection.end(), Base64Decode(result).begin())); + + subsection = std::vector(data.begin(), data.begin() + 2); + result = Base64Encode(subsection); + EXPECT_PRED2( + [](std::string expect, std::string actual) { return expect == actual; }, result, "AQI="); + EXPECT_TRUE(std::equal(subsection.begin(), subsection.end(), Base64Decode(result).begin())); + + subsection = std::vector(data.begin(), data.begin() + 3); + result = Base64Encode(subsection); + EXPECT_PRED2( + [](std::string expect, std::string actual) { return expect == actual; }, result, "AQID"); + EXPECT_TRUE(std::equal(subsection.begin(), subsection.end(), Base64Decode(result).begin())); + + subsection = std::vector(data.begin(), data.begin() + 4); + result = Base64Encode(subsection); + EXPECT_PRED2( + [](std::string expect, std::string actual) { return expect == actual; }, result, "AQIDBA=="); + EXPECT_TRUE(std::equal(subsection.begin(), subsection.end(), Base64Decode(result).begin())); + + subsection = std::vector(data.begin(), data.begin() + 5); + result = Base64Encode(subsection); + EXPECT_PRED2( + [](std::string expect, std::string actual) { return expect == actual; }, result, "AQIDBAU="); + EXPECT_TRUE(std::equal(subsection.begin(), subsection.end(), Base64Decode(result).begin())); + + subsection = std::vector(data.begin(), data.begin() + 6); + result = Base64Encode(subsection); + EXPECT_PRED2( + [](std::string expect, std::string actual) { return expect == actual; }, result, "AQIDBAUG"); + EXPECT_TRUE(std::equal(subsection.begin(), subsection.end(), Base64Decode(result).begin())); +}