Avi Drissman | 4e1b7bc3 | 2022-09-15 14:03:50 | [diff] [blame] | 1 | // Copyright 2021 The Chromium Authors |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | #include "content/browser/webauth/client_data_json.h" |
| 6 | |
Md Hasibul Hasan | a963a934 | 2024-04-03 10:15:14 | [diff] [blame] | 7 | #include <string_view> |
| 8 | |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 9 | #include "base/base64url.h" |
| 10 | #include "base/check.h" |
Adem Derinel | 620520b4 | 2024-11-04 15:45:44 | [diff] [blame] | 11 | #include "base/containers/span.h" |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 12 | #include "base/rand_util.h" |
Peter Kasting | abc2bc3 | 2023-10-27 22:30:09 | [diff] [blame] | 13 | #include "base/strings/string_number_conversions.h" |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 14 | #include "base/strings/utf_string_conversion_utils.h" |
Adem Derinel | 620520b4 | 2024-11-04 15:45:44 | [diff] [blame] | 15 | #include "content/browser/webauth/common_utils.h" |
Stephen McGruer | a80677c | 2023-01-12 22:32:24 | [diff] [blame] | 16 | #include "content/public/common/content_features.h" |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 17 | |
| 18 | namespace content { |
| 19 | namespace { |
| 20 | |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 21 | // ToJSONString encodes |in| as a JSON string, using the specific escaping rules |
| 22 | // required by https://github.com/w3c/webauthn/pull/1375. |
Md Hasibul Hasan | a963a934 | 2024-04-03 10:15:14 | [diff] [blame] | 23 | std::string ToJSONString(std::string_view in) { |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 24 | std::string ret; |
| 25 | ret.reserve(in.size() + 2); |
| 26 | ret.push_back('"'); |
| 27 | |
Adem Derinel | 620520b4 | 2024-11-04 15:45:44 | [diff] [blame] | 28 | base::span<const char> in_bytes = base::span(in); |
Peter Kasting | 8bb45c2 | 2022-06-16 19:39:27 | [diff] [blame] | 29 | const size_t length = in.size(); |
| 30 | size_t offset = 0; |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 31 | |
| 32 | while (offset < length) { |
Peter Kasting | 8bb45c2 | 2022-06-16 19:39:27 | [diff] [blame] | 33 | const size_t prior_offset = offset; |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 34 | // Input strings must be valid UTF-8. |
Peter Kasting | 5565d867 | 2022-05-31 18:19:10 | [diff] [blame] | 35 | base_icu::UChar32 codepoint; |
Adem Derinel | 620520b4 | 2024-11-04 15:45:44 | [diff] [blame] | 36 | CHECK(base::ReadUnicodeCharacter(in_bytes.data(), length, &offset, |
| 37 | &codepoint)); |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 38 | // offset is updated by |ReadUnicodeCharacter| to index the last byte of the |
| 39 | // codepoint. Increment it to index the first byte of the next codepoint for |
| 40 | // the subsequent iteration. |
| 41 | offset++; |
| 42 | |
| 43 | if (codepoint == 0x20 || codepoint == 0x21 || |
| 44 | (codepoint >= 0x23 && codepoint <= 0x5b) || codepoint >= 0x5d) { |
Adem Derinel | 620520b4 | 2024-11-04 15:45:44 | [diff] [blame] | 45 | ret.append(&in_bytes[prior_offset], offset - prior_offset); |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 46 | } else if (codepoint == 0x22) { |
| 47 | ret.append("\\\""); |
| 48 | } else if (codepoint == 0x5c) { |
| 49 | ret.append("\\\\"); |
| 50 | } else { |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 51 | ret.append("\\u00"); |
Peter Kasting | abc2bc3 | 2023-10-27 22:30:09 | [diff] [blame] | 52 | base::AppendHexEncodedByte(static_cast<uint8_t>(codepoint), ret, false); |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 53 | } |
| 54 | } |
| 55 | |
| 56 | ret.push_back('"'); |
| 57 | return ret; |
| 58 | } |
| 59 | |
| 60 | } // namespace |
| 61 | |
Ken Buchanan | be8629f | 2025-01-11 03:37:16 | [diff] [blame] | 62 | ClientDataJsonParams::ClientDataJsonParams( |
| 63 | ClientDataRequestType type, |
| 64 | url::Origin origin, |
| 65 | url::Origin top_origin, |
| 66 | std::optional<std::vector<uint8_t>> challenge, |
| 67 | bool is_cross_origin_iframe) |
Martin Kreichgauer | e255af06 | 2022-04-18 19:40:56 | [diff] [blame] | 68 | : type(type), |
| 69 | origin(std::move(origin)), |
Ken Buchanan | 4fb3ef1 | 2024-08-19 20:19:32 | [diff] [blame] | 70 | top_origin(std::move(top_origin)), |
Martin Kreichgauer | e255af06 | 2022-04-18 19:40:56 | [diff] [blame] | 71 | challenge(std::move(challenge)), |
| 72 | is_cross_origin_iframe(is_cross_origin_iframe) {} |
| 73 | ClientDataJsonParams::ClientDataJsonParams(ClientDataJsonParams&&) = default; |
| 74 | ClientDataJsonParams& ClientDataJsonParams::operator=(ClientDataJsonParams&&) = |
| 75 | default; |
| 76 | ClientDataJsonParams::~ClientDataJsonParams() = default; |
| 77 | |
| 78 | std::string BuildClientDataJson(ClientDataJsonParams params) { |
Ken Buchanan | be8629f | 2025-01-11 03:37:16 | [diff] [blame] | 79 | CHECK(params.challenge.has_value()); |
| 80 | |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 81 | std::string ret; |
| 82 | ret.reserve(128); |
| 83 | |
Martin Kreichgauer | e255af06 | 2022-04-18 19:40:56 | [diff] [blame] | 84 | switch (params.type) { |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 85 | case ClientDataRequestType::kWebAuthnCreate: |
| 86 | ret.append(R"({"type":"webauthn.create")"); |
| 87 | break; |
| 88 | case ClientDataRequestType::kWebAuthnGet: |
| 89 | ret.append(R"({"type":"webauthn.get")"); |
| 90 | break; |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 91 | case ClientDataRequestType::kPaymentGet: |
| 92 | ret.append(R"({"type":"payment.get")"); |
| 93 | break; |
| 94 | } |
| 95 | |
| 96 | ret.append(R"(,"challenge":)"); |
Ken Buchanan | be8629f | 2025-01-11 03:37:16 | [diff] [blame] | 97 | ret.append(ToJSONString(Base64UrlEncodeOmitPadding(*params.challenge))); |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 98 | |
| 99 | ret.append(R"(,"origin":)"); |
Martin Kreichgauer | e255af06 | 2022-04-18 19:40:56 | [diff] [blame] | 100 | ret.append(ToJSONString(params.origin.Serialize())); |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 101 | |
Ken Buchanan | 4fb3ef1 | 2024-08-19 20:19:32 | [diff] [blame] | 102 | std::string serialized_top_origin = |
| 103 | ToJSONString(params.top_origin.Serialize()); |
Martin Kreichgauer | e255af06 | 2022-04-18 19:40:56 | [diff] [blame] | 104 | if (params.is_cross_origin_iframe) { |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 105 | ret.append(R"(,"crossOrigin":true)"); |
Ken Buchanan | 4fb3ef1 | 2024-08-19 20:19:32 | [diff] [blame] | 106 | ret.append(R"(,"topOrigin":)"); |
| 107 | ret.append(serialized_top_origin); |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 108 | } else { |
| 109 | ret.append(R"(,"crossOrigin":false)"); |
| 110 | } |
| 111 | |
Slobodan Pejic | 7996c95 | 2025-04-29 16:30:49 | [diff] [blame] | 112 | if (params.payment_options && |
| 113 | params.type == ClientDataRequestType::kPaymentGet) { |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 114 | ret.append(R"(,"payment":{)"); |
| 115 | |
Stephen McGruer | 3472dda | 2022-08-25 17:48:47 | [diff] [blame] | 116 | ret.append(R"("rpId":)"); |
| 117 | ret.append(ToJSONString(params.payment_rp)); |
| 118 | |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 119 | ret.append(R"(,"topOrigin":)"); |
Ken Buchanan | 4fb3ef1 | 2024-08-19 20:19:32 | [diff] [blame] | 120 | ret.append(serialized_top_origin); |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 121 | |
Martin Kreichgauer | e255af06 | 2022-04-18 19:40:56 | [diff] [blame] | 122 | if (params.payment_options->payee_name.has_value()) { |
Nick Burris | f0b1a99a | 2022-03-21 20:10:57 | [diff] [blame] | 123 | ret.append(R"(,"payeeName":)"); |
Martin Kreichgauer | e255af06 | 2022-04-18 19:40:56 | [diff] [blame] | 124 | ret.append(ToJSONString(params.payment_options->payee_name.value())); |
Nick Burris | f0b1a99a | 2022-03-21 20:10:57 | [diff] [blame] | 125 | } |
Martin Kreichgauer | e255af06 | 2022-04-18 19:40:56 | [diff] [blame] | 126 | if (params.payment_options->payee_origin.has_value()) { |
Nick Burris | f0b1a99a | 2022-03-21 20:10:57 | [diff] [blame] | 127 | ret.append(R"(,"payeeOrigin":)"); |
Martin Kreichgauer | e255af06 | 2022-04-18 19:40:56 | [diff] [blame] | 128 | ret.append( |
| 129 | ToJSONString(params.payment_options->payee_origin->Serialize())); |
Nick Burris | f0b1a99a | 2022-03-21 20:10:57 | [diff] [blame] | 130 | } |
Rouslan Solomakhin | 58679256 | 2021-08-31 15:50:16 | [diff] [blame] | 131 | |
Slobodan Pejic | 44df0c1 | 2025-06-04 16:20:11 | [diff] [blame] | 132 | if (params.payment_options->payment_entities_logos.has_value()) { |
| 133 | const std::vector<blink::mojom::ShownPaymentEntityLogoPtr>& logos = |
| 134 | *params.payment_options->payment_entities_logos; |
| 135 | ret.append(R"(,"paymentEntitiesLogos":[)"); |
| 136 | for (auto logo_iterator = logos.begin(); logo_iterator != logos.end(); |
| 137 | ++logo_iterator) { |
| 138 | ret.append(R"({"url":)"); |
Slobodan Pejic | adc9ad2 | 2025-07-22 15:30:50 | [diff] [blame] | 139 | if ((*logo_iterator)->url.is_empty()) { |
| 140 | ret.append(R"("")"); |
| 141 | } else { |
| 142 | ret.append(ToJSONString((*logo_iterator)->url.spec())); |
| 143 | } |
Slobodan Pejic | 44df0c1 | 2025-06-04 16:20:11 | [diff] [blame] | 144 | ret.append(R"(,"label":)"); |
| 145 | ret.append(ToJSONString((*logo_iterator)->label)); |
| 146 | ret.append("}"); |
| 147 | if ((logo_iterator + 1) != logos.end()) { |
| 148 | ret.append(","); |
| 149 | } |
| 150 | } |
| 151 | ret.append("]"); |
| 152 | } |
| 153 | |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 154 | ret.append(R"(,"total":{)"); |
| 155 | |
| 156 | ret.append(R"("value":)"); |
Martin Kreichgauer | e255af06 | 2022-04-18 19:40:56 | [diff] [blame] | 157 | ret.append(ToJSONString(params.payment_options->total->value)); |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 158 | |
| 159 | ret.append(R"(,"currency":)"); |
Martin Kreichgauer | e255af06 | 2022-04-18 19:40:56 | [diff] [blame] | 160 | ret.append(ToJSONString(params.payment_options->total->currency)); |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 161 | |
| 162 | ret.append(R"(},"instrument":{)"); |
| 163 | |
| 164 | ret.append(R"("icon":)"); |
Martin Kreichgauer | e255af06 | 2022-04-18 19:40:56 | [diff] [blame] | 165 | ret.append(ToJSONString(params.payment_options->instrument->icon.spec())); |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 166 | |
Rouslan Solomakhin | da502724 | 2021-07-23 20:40:21 | [diff] [blame] | 167 | ret.append(R"(,"displayName":)"); |
Martin Kreichgauer | e255af06 | 2022-04-18 19:40:56 | [diff] [blame] | 168 | ret.append(ToJSONString(params.payment_options->instrument->display_name)); |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 169 | |
Stephen McGruer | 5853231 | 2025-06-27 14:13:38 | [diff] [blame] | 170 | if (params.payment_options->instrument->details.has_value()) { |
| 171 | // SPC calls should have been rejected if the details field was present |
| 172 | // but empty. |
| 173 | CHECK(!params.payment_options->instrument->details->empty()); |
| 174 | |
Slobodan Pejic | 78ffb05 | 2025-06-16 23:16:41 | [diff] [blame] | 175 | ret.append(R"(,"details":)"); |
Stephen McGruer | 5853231 | 2025-06-27 14:13:38 | [diff] [blame] | 176 | ret.append(ToJSONString(*params.payment_options->instrument->details)); |
Slobodan Pejic | 78ffb05 | 2025-06-16 23:16:41 | [diff] [blame] | 177 | } |
| 178 | |
Slobodan Pejic | 435c11ef | 2024-12-09 18:28:24 | [diff] [blame] | 179 | ret.append("}"); |
| 180 | if (params.payment_options->browser_bound_public_key.has_value()) { |
| 181 | ret.append(R"(,"browserBoundPublicKey":)"); |
| 182 | ret.append(ToJSONString(Base64UrlEncodeOmitPadding( |
| 183 | *params.payment_options->browser_bound_public_key))); |
| 184 | } |
| 185 | ret.append("}"); |
Slobodan Pejic | 7996c95 | 2025-04-29 16:30:49 | [diff] [blame] | 186 | } else if (params.payment_options && |
| 187 | params.payment_options->browser_bound_public_key.has_value() && |
| 188 | params.type == ClientDataRequestType::kWebAuthnCreate) { |
Slobodan Pejic | 2b3eb13 | 2025-06-09 19:54:56 | [diff] [blame] | 189 | ret.append(R"(,"payment":{"browserBoundPublicKey":)"); |
Slobodan Pejic | 7996c95 | 2025-04-29 16:30:49 | [diff] [blame] | 190 | ret.append(ToJSONString(Base64UrlEncodeOmitPadding( |
| 191 | *params.payment_options->browser_bound_public_key))); |
| 192 | ret.append("}"); |
Liquan (Max) Gu | 292fc3d | 2021-07-19 19:03:03 | [diff] [blame] | 193 | } |
| 194 | |
| 195 | if (base::RandDouble() < 0.2) { |
| 196 | // An extra key is sometimes added to ensure that RPs do not make |
| 197 | // unreasonably specific assumptions about the clientData JSON. This is |
| 198 | // done in the fashion of |
| 199 | // https://tools.ietf.org/html/draft-ietf-tls-grease |
| 200 | ret.append(R"(,"other_keys_can_be_added_here":")"); |
| 201 | ret.append( |
| 202 | "do not compare clientDataJSON against a template. See " |
| 203 | "https://goo.gl/yabPex\""); |
| 204 | } |
| 205 | |
| 206 | ret.append("}"); |
| 207 | return ret; |
| 208 | } |
| 209 | |
| 210 | } // namespace content |