From 74d9579da08ae472a0f07985cdc888d0e1cc8bbb Mon Sep 17 00:00:00 2001 From: Bright Chen Date: Tue, 1 Aug 2023 22:54:22 +0800 Subject: [PATCH] Support url safe base64 --- BUILD.bazel | 1 + CMakeLists.txt | 1 + Makefile | 1 + src/butil/base64url.cc | 96 ++++++++++++++++++++++++++++++++ src/butil/base64url.h | 54 ++++++++++++++++++ test/BUILD.bazel | 1 + test/CMakeLists.txt | 1 + test/Makefile | 1 + test/base64url_unittest.cc | 110 +++++++++++++++++++++++++++++++++++++ 9 files changed, 266 insertions(+) create mode 100644 src/butil/base64url.cc create mode 100644 src/butil/base64url.h create mode 100644 test/base64url_unittest.cc diff --git a/BUILD.bazel b/BUILD.bazel index 58e8863dd2..475b34c7e5 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -117,6 +117,7 @@ BUTIL_SRCS = [ "src/butil/at_exit.cc", "src/butil/atomicops_internals_x86_gcc.cc", "src/butil/base64.cc", + "src/butil/base64url.cc", "src/butil/big_endian.cc", "src/butil/cpu.cc", "src/butil/debug/alias.cc", diff --git a/CMakeLists.txt b/CMakeLists.txt index 9328f21b13..b746843d39 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -291,6 +291,7 @@ set(BUTIL_SOURCES ${PROJECT_SOURCE_DIR}/src/butil/at_exit.cc ${PROJECT_SOURCE_DIR}/src/butil/atomicops_internals_x86_gcc.cc ${PROJECT_SOURCE_DIR}/src/butil/base64.cc + ${PROJECT_SOURCE_DIR}/src/butil/base64url.cc ${PROJECT_SOURCE_DIR}/src/butil/big_endian.cc ${PROJECT_SOURCE_DIR}/src/butil/cpu.cc ${PROJECT_SOURCE_DIR}/src/butil/debug/alias.cc diff --git a/Makefile b/Makefile index 574c63bbfb..5dcaf43dc0 100644 --- a/Makefile +++ b/Makefile @@ -68,6 +68,7 @@ BUTIL_SOURCES = \ src/butil/at_exit.cc \ src/butil/atomicops_internals_x86_gcc.cc \ src/butil/base64.cc \ + src/butil/base64url.cc \ src/butil/big_endian.cc \ src/butil/cpu.cc \ src/butil/debug/alias.cc \ diff --git a/src/butil/base64url.cc b/src/butil/base64url.cc new file mode 100644 index 0000000000..f782f33dcc --- /dev/null +++ b/src/butil/base64url.cc @@ -0,0 +1,96 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "butil/base64.h" +#include "butil/base64url.h" + +#include "third_party/modp_b64/modp_b64_data.h" + +namespace butil { + +// Base64url maps {+, /} to {-, _} in order for the encoded content to be safe +// to use in a URL. These characters will be translated by this implementation. +#define BASE64_CHARS "+/" +#define BASE64_URL_SAFE_CHARS "-_" +#define URL_SAFE_CHAR62 '-' +#define URL_SAFE_CHAR63 '_' + +void Base64UrlEncode(const StringPiece& input, + Base64UrlEncodePolicy policy, + std::string* output) { + Base64Encode(input, output); + + std::replace(output->begin(), output->end(), CHAR62, URL_SAFE_CHAR62); + std::replace(output->begin(), output->end(), CHAR63, URL_SAFE_CHAR63); + + switch (policy) { + case Base64UrlEncodePolicy::INCLUDE_PADDING: + // The padding included in |*output| will not be amended. + break; + case Base64UrlEncodePolicy::OMIT_PADDING: + // The padding included in |*output| will be removed. + const size_t last_non_padding_pos = + output->find_last_not_of(CHARPAD); + if (last_non_padding_pos != std::string::npos) { + output->resize(last_non_padding_pos + 1); + } + break; + } +} + +bool Base64UrlDecode(const StringPiece& input, + Base64UrlDecodePolicy policy, + std::string* output) { + // Characters outside of the base64url alphabet are disallowed, which includes + // the {+, /} characters found in the conventional base64 alphabet. + if (input.find_first_of(BASE64_CHARS) != std::string::npos) + return false; + + const size_t required_padding_characters = input.size() % 4; + const bool needs_replacement = + input.find_first_of(BASE64_URL_SAFE_CHARS) != std::string::npos; + + switch (policy) { + case Base64UrlDecodePolicy::REQUIRE_PADDING: + // Fail if the required padding is not included in |input|. + if (required_padding_characters > 0) + return false; + break; + case Base64UrlDecodePolicy::IGNORE_PADDING: + // Missing padding will be silently appended. + break; + case Base64UrlDecodePolicy::DISALLOW_PADDING: + // Fail if padding characters are included in |input|. + if (input.find_first_of(CHARPAD) != std::string::npos) + return false; + break; + } + + // If the string either needs replacement of URL-safe characters to normal + // base64 ones, or additional padding, a copy of |input| needs to be made in + // order to make these adjustments without side effects. + if (required_padding_characters > 0 || needs_replacement) { + std::string base64_input; + + size_t base64_input_size = input.size(); + if (required_padding_characters > 0) + base64_input_size += 4 - required_padding_characters; + + base64_input.reserve(base64_input_size); + input.AppendToString(&base64_input); + + // Substitute the base64url URL-safe characters to their base64 equivalents. + std::replace(base64_input.begin(), base64_input.end(), URL_SAFE_CHAR62, CHAR62); + std::replace(base64_input.begin(), base64_input.end(), URL_SAFE_CHAR63, CHAR63); + + // Append the necessary padding characters. + base64_input.resize(base64_input_size, '='); + + return Base64Decode(base64_input, output); + } + + return Base64Decode(input, output); +} + +} // namespace butil diff --git a/src/butil/base64url.h b/src/butil/base64url.h new file mode 100644 index 0000000000..438f6b7fa4 --- /dev/null +++ b/src/butil/base64url.h @@ -0,0 +1,54 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef BASE_BASE64URL_H_ +#define BASE_BASE64URL_H_ + +#include + +#include "butil/base_export.h" +#include "butil/strings/string_piece.h" + +namespace butil { + +enum class Base64UrlEncodePolicy { + // Include the trailing padding in the output, when necessary. + INCLUDE_PADDING, + + // Remove the trailing padding from the output. + OMIT_PADDING +}; + +// Encodes the |input| string in base64url, defined in RFC 4648: +// https://tools.ietf.org/html/rfc4648#section-5 +// +// The |policy| defines whether padding should be included or omitted from the +// encoded |*output|. |input| and |*output| may reference the same storage. +BUTIL_EXPORT void Base64UrlEncode(const StringPiece& input, + Base64UrlEncodePolicy policy, + std::string* output); + +enum class Base64UrlDecodePolicy { + // Require inputs to contain trailing padding if non-aligned. + REQUIRE_PADDING, + + // Accept inputs regardless of whether they have the correct padding. + IGNORE_PADDING, + + // Reject inputs if they contain any trailing padding. + DISALLOW_PADDING +}; + +// Decodes the |input| string in base64url, defined in RFC 4648: +// https://tools.ietf.org/html/rfc4648#section-5 +// +// The |policy| defines whether padding will be required, ignored or disallowed +// altogether. |input| and |*output| may reference the same storage. +BUTIL_EXPORT bool Base64UrlDecode(const StringPiece& input, + Base64UrlDecodePolicy policy, + std::string* output) WARN_UNUSED_RESULT; + +} // namespace butil + +#endif // BASE_BASE64URL_H_ diff --git a/test/BUILD.bazel b/test/BUILD.bazel index 82dcd8828b..e493e6cdb8 100644 --- a/test/BUILD.bazel +++ b/test/BUILD.bazel @@ -45,6 +45,7 @@ TEST_BUTIL_SOURCES = [ "at_exit_unittest.cc", "atomicops_unittest.cc", "base64_unittest.cc", + "base64url_unittest.cc", "big_endian_unittest.cc", "bits_unittest.cc", "hash_tables_unittest.cc", diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9720a0fabe..d207d9841a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -80,6 +80,7 @@ SET(TEST_BUTIL_SOURCES ${PROJECT_SOURCE_DIR}/test/at_exit_unittest.cc ${PROJECT_SOURCE_DIR}/test/atomicops_unittest.cc ${PROJECT_SOURCE_DIR}/test/base64_unittest.cc + ${PROJECT_SOURCE_DIR}/test/base64url_unittest.cc ${PROJECT_SOURCE_DIR}/test/big_endian_unittest.cc ${PROJECT_SOURCE_DIR}/test/bits_unittest.cc ${PROJECT_SOURCE_DIR}/test/hash_tables_unittest.cc diff --git a/test/Makefile b/test/Makefile index 871a99ed88..f084d1f43d 100644 --- a/test/Makefile +++ b/test/Makefile @@ -50,6 +50,7 @@ TEST_BUTIL_SOURCES = \ at_exit_unittest.cc \ atomicops_unittest.cc \ base64_unittest.cc \ + base64url_unittest.cc \ big_endian_unittest.cc \ bits_unittest.cc \ hash_tables_unittest.cc \ diff --git a/test/base64url_unittest.cc b/test/base64url_unittest.cc new file mode 100644 index 0000000000..4995b162a0 --- /dev/null +++ b/test/base64url_unittest.cc @@ -0,0 +1,110 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "butil/base64url.h" + +#include + +namespace butil { + +TEST(Base64UrlTest, EncodeIncludePaddingPolicy) { + std::string output; + Base64UrlEncode("hello?world", Base64UrlEncodePolicy::INCLUDE_PADDING, + &output); + + // Base64 version: aGVsbG8/d29ybGQ= + EXPECT_EQ("aGVsbG8_d29ybGQ=", output); + + // Test for behavior for very short and empty strings. + Base64UrlEncode("??", Base64UrlEncodePolicy::INCLUDE_PADDING, &output); + EXPECT_EQ("Pz8=", output); + + Base64UrlEncode("", Base64UrlEncodePolicy::INCLUDE_PADDING, &output); + EXPECT_EQ("", output); +} + +TEST(Base64UrlTest, EncodeOmitPaddingPolicy) { + std::string output; + Base64UrlEncode("hello?world", Base64UrlEncodePolicy::OMIT_PADDING, &output); + + // base64 version: aGVsbG8/d29ybGQ= + EXPECT_EQ("aGVsbG8_d29ybGQ", output); + + // Test for behavior for very short and empty strings. + Base64UrlEncode("??", Base64UrlEncodePolicy::OMIT_PADDING, &output); + EXPECT_EQ("Pz8", output); + + Base64UrlEncode("", Base64UrlEncodePolicy::OMIT_PADDING, &output); + EXPECT_EQ("", output); +} + +TEST(Base64UrlTest, DecodeRequirePaddingPolicy) { + std::string output; + ASSERT_TRUE(Base64UrlDecode("aGVsbG8_d29ybGQ=", + Base64UrlDecodePolicy::REQUIRE_PADDING, &output)); + + EXPECT_EQ("hello?world", output); + + ASSERT_FALSE(Base64UrlDecode( + "aGVsbG8_d29ybGQ", Base64UrlDecodePolicy::REQUIRE_PADDING, &output)); + + // Test for behavior for very short and empty strings. + ASSERT_TRUE( + Base64UrlDecode("Pz8=", Base64UrlDecodePolicy::REQUIRE_PADDING, &output)); + EXPECT_EQ("??", output); + + ASSERT_TRUE( + Base64UrlDecode("", Base64UrlDecodePolicy::REQUIRE_PADDING, &output)); + EXPECT_EQ("", output); +} + +TEST(Base64UrlTest, DecodeIgnorePaddingPolicy) { + std::string output; + ASSERT_TRUE(Base64UrlDecode("aGVsbG8_d29ybGQ", + Base64UrlDecodePolicy::IGNORE_PADDING, &output)); + + EXPECT_EQ("hello?world", output); + + // Including the padding is accepted as well. + ASSERT_TRUE(Base64UrlDecode("aGVsbG8_d29ybGQ=", + Base64UrlDecodePolicy::IGNORE_PADDING, &output)); + + EXPECT_EQ("hello?world", output); +} + +TEST(Base64UrlTest, DecodeDisallowPaddingPolicy) { + std::string output; + ASSERT_FALSE(Base64UrlDecode( + "aGVsbG8_d29ybGQ=", Base64UrlDecodePolicy::DISALLOW_PADDING, &output)); + + // The policy will allow the input when padding has been omitted. + ASSERT_TRUE(Base64UrlDecode( + "aGVsbG8_d29ybGQ", Base64UrlDecodePolicy::DISALLOW_PADDING, &output)); + + EXPECT_EQ("hello?world", output); +} + +TEST(Base64UrlTest, DecodeDisallowsBase64Alphabet) { + std::string output; + + // The "/" character is part of the conventional base64 alphabet, but has been + // substituted with "_" in the base64url alphabet. + ASSERT_FALSE(Base64UrlDecode( + "aGVsbG8/d29ybGQ=", Base64UrlDecodePolicy::REQUIRE_PADDING, &output)); +} + +TEST(Base64UrlTest, DecodeDisallowsPaddingOnly) { + std::string output; + + ASSERT_FALSE(Base64UrlDecode( + "=", Base64UrlDecodePolicy::IGNORE_PADDING, &output)); + ASSERT_FALSE(Base64UrlDecode( + "==", Base64UrlDecodePolicy::IGNORE_PADDING, &output)); + ASSERT_FALSE(Base64UrlDecode( + "===", Base64UrlDecodePolicy::IGNORE_PADDING, &output)); + ASSERT_FALSE(Base64UrlDecode( + "====", Base64UrlDecodePolicy::IGNORE_PADDING, &output)); +} + +} // namespace butil