| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/webauth/client_data_json.h" |
| |
| #include <string_view> |
| |
| #include "base/base64url.h" |
| #include "base/check.h" |
| #include "base/containers/span.h" |
| #include "base/rand_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/utf_string_conversion_utils.h" |
| #include "content/browser/webauth/common_utils.h" |
| #include "content/public/common/content_features.h" |
| |
| namespace content { |
| namespace { |
| |
| // ToJSONString encodes |in| as a JSON string, using the specific escaping rules |
| // required by https://github.com/w3c/webauthn/pull/1375. |
| std::string ToJSONString(std::string_view in) { |
| std::string ret; |
| ret.reserve(in.size() + 2); |
| ret.push_back('"'); |
| |
| base::span<const char> in_bytes = base::span(in); |
| const size_t length = in.size(); |
| size_t offset = 0; |
| |
| while (offset < length) { |
| const size_t prior_offset = offset; |
| // Input strings must be valid UTF-8. |
| base_icu::UChar32 codepoint; |
| CHECK(base::ReadUnicodeCharacter(in_bytes.data(), length, &offset, |
| &codepoint)); |
| // offset is updated by |ReadUnicodeCharacter| to index the last byte of the |
| // codepoint. Increment it to index the first byte of the next codepoint for |
| // the subsequent iteration. |
| offset++; |
| |
| if (codepoint == 0x20 || codepoint == 0x21 || |
| (codepoint >= 0x23 && codepoint <= 0x5b) || codepoint >= 0x5d) { |
| ret.append(&in_bytes[prior_offset], offset - prior_offset); |
| } else if (codepoint == 0x22) { |
| ret.append("\\\""); |
| } else if (codepoint == 0x5c) { |
| ret.append("\\\\"); |
| } else { |
| ret.append("\\u00"); |
| base::AppendHexEncodedByte(static_cast<uint8_t>(codepoint), ret, false); |
| } |
| } |
| |
| ret.push_back('"'); |
| return ret; |
| } |
| |
| } // namespace |
| |
| ClientDataJsonParams::ClientDataJsonParams( |
| ClientDataRequestType type, |
| url::Origin origin, |
| url::Origin top_origin, |
| std::optional<std::vector<uint8_t>> challenge, |
| bool is_cross_origin_iframe) |
| : type(type), |
| origin(std::move(origin)), |
| top_origin(std::move(top_origin)), |
| challenge(std::move(challenge)), |
| is_cross_origin_iframe(is_cross_origin_iframe) {} |
| ClientDataJsonParams::ClientDataJsonParams(ClientDataJsonParams&&) = default; |
| ClientDataJsonParams& ClientDataJsonParams::operator=(ClientDataJsonParams&&) = |
| default; |
| ClientDataJsonParams::~ClientDataJsonParams() = default; |
| |
| std::string BuildClientDataJson(ClientDataJsonParams params) { |
| CHECK(params.challenge.has_value()); |
| |
| std::string ret; |
| ret.reserve(128); |
| |
| switch (params.type) { |
| case ClientDataRequestType::kWebAuthnCreate: |
| ret.append(R"({"type":"webauthn.create")"); |
| break; |
| case ClientDataRequestType::kWebAuthnGet: |
| ret.append(R"({"type":"webauthn.get")"); |
| break; |
| case ClientDataRequestType::kPaymentGet: |
| ret.append(R"({"type":"payment.get")"); |
| break; |
| } |
| |
| ret.append(R"(,"challenge":)"); |
| ret.append(ToJSONString(Base64UrlEncodeOmitPadding(*params.challenge))); |
| |
| ret.append(R"(,"origin":)"); |
| ret.append(ToJSONString(params.origin.Serialize())); |
| |
| std::string serialized_top_origin = |
| ToJSONString(params.top_origin.Serialize()); |
| if (params.is_cross_origin_iframe) { |
| ret.append(R"(,"crossOrigin":true)"); |
| ret.append(R"(,"topOrigin":)"); |
| ret.append(serialized_top_origin); |
| } else { |
| ret.append(R"(,"crossOrigin":false)"); |
| } |
| |
| if (params.payment_options && |
| params.type == ClientDataRequestType::kPaymentGet) { |
| ret.append(R"(,"payment":{)"); |
| |
| ret.append(R"("rpId":)"); |
| ret.append(ToJSONString(params.payment_rp)); |
| |
| ret.append(R"(,"topOrigin":)"); |
| ret.append(serialized_top_origin); |
| |
| if (params.payment_options->payee_name.has_value()) { |
| ret.append(R"(,"payeeName":)"); |
| ret.append(ToJSONString(params.payment_options->payee_name.value())); |
| } |
| if (params.payment_options->payee_origin.has_value()) { |
| ret.append(R"(,"payeeOrigin":)"); |
| ret.append( |
| ToJSONString(params.payment_options->payee_origin->Serialize())); |
| } |
| |
| if (params.payment_options->payment_entities_logos.has_value()) { |
| const std::vector<blink::mojom::ShownPaymentEntityLogoPtr>& logos = |
| *params.payment_options->payment_entities_logos; |
| ret.append(R"(,"paymentEntitiesLogos":[)"); |
| for (auto logo_iterator = logos.begin(); logo_iterator != logos.end(); |
| ++logo_iterator) { |
| ret.append(R"({"url":)"); |
| if ((*logo_iterator)->url.is_empty()) { |
| ret.append(R"("")"); |
| } else { |
| ret.append(ToJSONString((*logo_iterator)->url.spec())); |
| } |
| ret.append(R"(,"label":)"); |
| ret.append(ToJSONString((*logo_iterator)->label)); |
| ret.append("}"); |
| if ((logo_iterator + 1) != logos.end()) { |
| ret.append(","); |
| } |
| } |
| ret.append("]"); |
| } |
| |
| ret.append(R"(,"total":{)"); |
| |
| ret.append(R"("value":)"); |
| ret.append(ToJSONString(params.payment_options->total->value)); |
| |
| ret.append(R"(,"currency":)"); |
| ret.append(ToJSONString(params.payment_options->total->currency)); |
| |
| ret.append(R"(},"instrument":{)"); |
| |
| ret.append(R"("icon":)"); |
| ret.append(ToJSONString(params.payment_options->instrument->icon.spec())); |
| |
| ret.append(R"(,"displayName":)"); |
| ret.append(ToJSONString(params.payment_options->instrument->display_name)); |
| |
| if (params.payment_options->instrument->details.has_value()) { |
| // SPC calls should have been rejected if the details field was present |
| // but empty. |
| CHECK(!params.payment_options->instrument->details->empty()); |
| |
| ret.append(R"(,"details":)"); |
| ret.append(ToJSONString(*params.payment_options->instrument->details)); |
| } |
| |
| ret.append("}"); |
| if (params.payment_options->browser_bound_public_key.has_value()) { |
| ret.append(R"(,"browserBoundPublicKey":)"); |
| ret.append(ToJSONString(Base64UrlEncodeOmitPadding( |
| *params.payment_options->browser_bound_public_key))); |
| } |
| ret.append("}"); |
| } else if (params.payment_options && |
| params.payment_options->browser_bound_public_key.has_value() && |
| params.type == ClientDataRequestType::kWebAuthnCreate) { |
| ret.append(R"(,"payment":{"browserBoundPublicKey":)"); |
| ret.append(ToJSONString(Base64UrlEncodeOmitPadding( |
| *params.payment_options->browser_bound_public_key))); |
| ret.append("}"); |
| } |
| |
| if (base::RandDouble() < 0.2) { |
| // An extra key is sometimes added to ensure that RPs do not make |
| // unreasonably specific assumptions about the clientData JSON. This is |
| // done in the fashion of |
| // https://tools.ietf.org/html/draft-ietf-tls-grease |
| ret.append(R"(,"other_keys_can_be_added_here":")"); |
| ret.append( |
| "do not compare clientDataJSON against a template. See " |
| "https://goo.gl/yabPex\""); |
| } |
| |
| ret.append("}"); |
| return ret; |
| } |
| |
| } // namespace content |