blob: 79b37bd9d9a506192c7c6b53bdd12e5dc74e530e [file] [log] [blame]
// Copyright 2021 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 "content/browser/webauth/client_data_json.h"
#include "base/base64url.h"
#include "base/check.h"
#include "base/rand_util.h"
#include "base/strings/string_piece.h"
#include "base/strings/utf_string_conversion_utils.h"
namespace content {
namespace {
std::string Base64UrlEncode(const base::span<const uint8_t> input) {
std::string ret;
base::Base64UrlEncode(
base::StringPiece(reinterpret_cast<const char*>(input.data()),
input.size()),
base::Base64UrlEncodePolicy::OMIT_PADDING, &ret);
return ret;
}
// ToJSONString encodes |in| as a JSON string, using the specific escaping rules
// required by https://github.com/w3c/webauthn/pull/1375.
std::string ToJSONString(base::StringPiece in) {
std::string ret;
ret.reserve(in.size() + 2);
ret.push_back('"');
const char* const in_bytes = in.data();
// ICU uses |int32_t| for lengths.
const int32_t length = base::checked_cast<int32_t>(in.size());
int32_t offset = 0;
while (offset < length) {
const int32_t prior_offset = offset;
// Input strings must be valid UTF-8.
uint32_t codepoint;
CHECK(base::ReadUnicodeCharacter(in_bytes, 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], &in_bytes[offset]);
} else if (codepoint == 0x22) {
ret.append("\\\"");
} else if (codepoint == 0x5c) {
ret.append("\\\\");
} else {
static const char hextable[17] = "0123456789abcdef";
ret.append("\\u00");
ret.push_back(hextable[codepoint >> 4]);
ret.push_back(hextable[codepoint & 15]);
}
}
ret.push_back('"');
return ret;
}
} // namespace
std::string BuildClientDataJson(
ClientDataRequestType type,
const std::string& origin,
base::span<const uint8_t> challenge,
bool is_cross_origin,
blink::mojom::PaymentOptionsPtr payment_options /* = nullptr */,
const std::string& payment_rp /* = "" */,
const std::string& payment_top_origin /* = "" */) {
std::string ret;
ret.reserve(128);
// U2F uses "typ", while WebAuthn uses "type" for the type key.
switch (type) {
case ClientDataRequestType::kU2fRegister:
ret.append(R"({"typ":"navigator.id.finishEnrollment")");
break;
case ClientDataRequestType::kU2fSign:
ret.append(R"({"typ":"navigator.id.getAssertion")");
break;
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(Base64UrlEncode(challenge)));
ret.append(R"(,"origin":)");
ret.append(ToJSONString(origin));
if (is_cross_origin) {
ret.append(R"(,"crossOrigin":true)");
} else {
ret.append(R"(,"crossOrigin":false)");
}
if (payment_options) {
ret.append(R"(,"payment":{)");
ret.append(R"("rp":)");
ret.append(ToJSONString(payment_rp));
ret.append(R"(,"topOrigin":)");
ret.append(ToJSONString(payment_top_origin));
ret.append(R"(,"payeeOrigin":)");
ret.append(ToJSONString(payment_options->payee_origin.Serialize()));
ret.append(R"(,"total":{)");
ret.append(R"("value":)");
ret.append(ToJSONString(payment_options->total->value));
ret.append(R"(,"currency":)");
ret.append(ToJSONString(payment_options->total->currency));
ret.append(R"(},"instrument":{)");
ret.append(R"("icon":)");
ret.append(ToJSONString(payment_options->instrument->icon.spec()));
ret.append(R"(,"displayName":)");
ret.append(ToJSONString(payment_options->instrument->display_name));
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