diff --git a/.licenserc.yaml b/.licenserc.yaml index 042769f140..a189f6bceb 100644 --- a/.licenserc.yaml +++ b/.licenserc.yaml @@ -59,6 +59,7 @@ header: - 'src/butil/atomic*' - 'src/butil/auto_reset.h' - 'src/butil/base64.*' + - 'src/butil/base64url.*' - 'src/butil/base_export.h' - 'src/butil/base_paths.cc' - 'src/butil/basictypes.h' 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/LICENSE b/LICENSE index feac1ef606..0efd09e42e 100644 --- a/LICENSE +++ b/LICENSE @@ -924,3 +924,38 @@ copyright (c) Google inc and (c) The Chromium Authors and licensed under the THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +src/butil/base64url.* +test/base64url_unittest.cc + +Some portions of these files are derived from code in the Chromium project, +copyright The Chromium Authors and licensed under the 3-clause BSD license: + + Copyright 2015 The Chromium Authors + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 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