Skip to content

Commit

Permalink
utils: Added base64 URL decoding
Browse files Browse the repository at this point in the history
Added a utility function to perform Base64 URL decoding.  This
approach was selected because the base64 library we currently use
does not support encoding/decoding Base64URL messages.  The
only use case we have for this is in OIDC and happens infrequently
enough that performance is not a large concern.

Signed-off-by: Michael Boquard <michael@redpanda.com>
  • Loading branch information
michael-redpanda committed May 6, 2024
1 parent cb6cda2 commit 710dc18
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 1 deletion.
33 changes: 33 additions & 0 deletions src/v/utils/base64.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@

#include <libbase64.h>

const int decode_url_table[128] = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, 62, -1, -1, 52, 53, 54, 55, 56, 57, 58, 59, 60,
61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1,
63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
};

// Required length is ceil(4n/3) rounded up to 4 bytes
static inline size_t encode_capacity(size_t input_size) {
return (((4 * input_size) / 3) + 3) & ~0x3U;
Expand Down Expand Up @@ -110,3 +120,26 @@ ss::sstring iobuf_to_base64(const iobuf& input) {
output.resize(written);
return output;
}

bytes base64url_to_bytes(std::string_view data) {
bytes rv{bytes::initialized_later{}, data.size()};
unsigned int bits_collected = 0;
unsigned int accumulator = 0;
int pos = 0;
for (const unsigned char c : data) {
if (c > 127 || decode_url_table[c] < 0) {
throw base64_url_decoder_exception();
}
accumulator = (accumulator << 6)
+ static_cast<unsigned int>(decode_url_table[c]);
bits_collected += 6;
if (bits_collected >= 8) {
auto val = static_cast<bytes::value_type>(
(accumulator >> (bits_collected - 8)) & 0xffu);
rv[pos++] = val;
bits_collected -= 8;
}
}
rv.resize(pos);
return rv;
}
15 changes: 15 additions & 0 deletions src/v/utils/base64.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ class base64_decoder_exception final : public std::exception {
}
};

class base64_url_decoder_exception final : public std::exception {
public:
const char* what() const noexcept final {
return "error decoding base64url string";
}
};

// base64 <-> bytes
bytes base64_to_bytes(std::string_view);
ss::sstring bytes_to_base64(bytes_view);
Expand All @@ -31,3 +38,11 @@ ss::sstring base64_to_string(std::string_view);

// base64 <-> iobuf
ss::sstring iobuf_to_base64(const iobuf&);

/// \brief Used to decode URL encoded base64 values
///
/// URL encoded base64 values use '-' and '_' instead of
/// '+' and '/', respectively
/// \throws base64_url_decoder_exception if an invalid URL base64 encoded string
/// is provided
bytes base64url_to_bytes(std::string_view);
2 changes: 1 addition & 1 deletion src/v/utils/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ rp_test(
vint_test.cc
waiter_queue_test.cc
auto_fmt_test.cc
LIBRARIES v::seastar_testing_main v::utils v::bytes v::version absl::flat_hash_set
LIBRARIES v::seastar_testing_main v::utils v::bytes v::version absl::flat_hash_set cryptopp
ARGS "-- -c 1"
LABELS utils
)
Expand Down
70 changes: 70 additions & 0 deletions src/v/utils/tests/base64_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "utils/base64.h"

#include <boost/test/unit_test.hpp>
#include <cryptopp/base64.h>

BOOST_AUTO_TEST_CASE(bytes_type) {
auto encdec = [](const bytes& input, const auto expected) {
Expand Down Expand Up @@ -49,3 +50,72 @@ BOOST_AUTO_TEST_CASE(iobuf_type) {
auto decoded = base64_to_bytes(encoded);
BOOST_REQUIRE_EQUAL(decoded, iobuf_to_bytes(buf));
}

BOOST_AUTO_TEST_CASE(base64_url_decode_test_basic) {
auto dec = [](std::string_view input, const bytes& expected) {
auto decoded = base64url_to_bytes(input);
BOOST_REQUIRE_EQUAL(decoded, expected);
};

dec("UmVkcGFuZGEgUm9ja3M", "Redpanda Rocks");
// ChatGPT was asked to describe the Redpanda product
dec(
"UmVkcGFuZGEgaXMgYSBjdXR0aW5nLWVkZ2UgZGF0YSBzdHJlYW1pbmcgcGxhdGZvcm0gZGVz"
"aWduZWQgdG8gb2ZmZXIgYSBoaWdoLXBlcmZvcm1hbmNlIGFsdGVybmF0aXZlIHRvIEFwYWNo"
"ZSBLYWZrYS4gSXQncyBjcmFmdGVkIHRvIGhhbmRsZSB2YXN0IGFtb3VudHMgb2YgcmVhbC10"
"aW1lIGRhdGEgZWZmaWNpZW50bHksIG1ha2luZyBpdCBhbiBleGNlbGxlbnQgY2hvaWNlIGZv"
"ciBtb2Rlcm4gZGF0YS1kcml2ZW4gYXBwbGljYXRpb25zLiAgT3ZlcmFsbCwgUmVkcGFuZGEg"
"cmVwcmVzZW50cyBhIGNvbXBlbGxpbmcgb3B0aW9uIGZvciBvcmdhbml6YXRpb25zIHNlZWtp"
"bmcgYSBoaWdoLXBlcmZvcm1hbmNlLCBzY2FsYWJsZSwgYW5kIHJlbGlhYmxlIGRhdGEgc3Ry"
"ZWFtaW5nIHNvbHV0aW9uLiBXaGV0aGVyIHlvdSdyZSBidWlsZGluZyByZWFsLXRpbWUgYW5h"
"bHl0aWNzIGFwcGxpY2F0aW9ucywgcHJvY2Vzc2luZyBJb1QgZGF0YSBzdHJlYW1zLCBvciBt"
"YW5hZ2luZyBldmVudC1kcml2ZW4gbWljcm9zZXJ2aWNlcywgUmVkcGFuZGEgaGFzIHlvdSBj"
"b3ZlcmVkLg",
"Redpanda is a cutting-edge data streaming platform designed to offer a "
"high-performance alternative to Apache Kafka. It's crafted to handle "
"vast amounts of real-time data efficiently, making it an excellent "
"choice for modern data-driven applications. Overall, Redpanda "
"represents a compelling option for organizations seeking a "
"high-performance, scalable, and reliable data streaming solution. "
"Whether you're building real-time analytics applications, processing "
"IoT data streams, or managing event-driven microservices, Redpanda has "
"you covered.");

dec("", "");
dec("YQ", "a");
dec("YWI", "ab");
dec("YWJj", "abc");
dec("A", "");
}

BOOST_AUTO_TEST_CASE(base64_url_decode_test_random) {
const std::array<size_t, 5> test_sizes = {1, 10, 128, 256, 512};
auto dec = [](std::string_view input, const bytes& expected) {
auto decoded = base64url_to_bytes(input);
BOOST_REQUIRE_EQUAL(decoded, expected);
};

auto enc = [](const bytes& msg) {
CryptoPP::Base64URLEncoder encoder;
encoder.Put(msg.data(), msg.size());
encoder.MessageEnd();
auto size = encoder.MaxRetrievable();
BOOST_REQUIRE_NE(size, 0);
ss::sstring encoded(ss::sstring::initialized_later{}, size);
encoder.Get(
reinterpret_cast<CryptoPP::byte*>(encoded.data()), encoded.size());
return encoded;
};

for (auto s : test_sizes) {
auto val = random_generators::get_bytes(s);
auto encoded = enc(val);
dec(encoded, val);
}
}

BOOST_AUTO_TEST_CASE(base64_url_decode_invalid_character) {
const std::string invalid_encode = "abc+/";
BOOST_REQUIRE_THROW(
base64url_to_bytes(invalid_encode), base64_url_decoder_exception);
}

0 comments on commit 710dc18

Please sign in to comment.