blob: 14c8845529b33a822a2516f0578b0177bd34c8fd [file] [log] [blame]
// 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 "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();
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, 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
ClientDataJsonParams::ClientDataJsonParams(ClientDataRequestType type,
url::Origin origin,
std::vector<uint8_t> challenge,
bool is_cross_origin_iframe)
: type(type),
origin(std::move(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) {
std::string ret;
ret.reserve(128);
// U2F uses "typ", while WebAuthn uses "type" for the type key.
switch (params.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(params.challenge)));
ret.append(R"(,"origin":)");
ret.append(ToJSONString(params.origin.Serialize()));
if (params.is_cross_origin_iframe) {
ret.append(R"(,"crossOrigin":true)");
} else {
ret.append(R"(,"crossOrigin":false)");
}
if (params.payment_options) {
ret.append(R"(,"payment":{)");
ret.append(R"("rpId":)");
ret.append(ToJSONString(params.payment_rp));
// TODO(crbug.com/1356224): Remove legacy 'rp' parameter.
ret.append(R"(,"rp":)");
ret.append(ToJSONString(params.payment_rp));
ret.append(R"(,"topOrigin":)");
ret.append(ToJSONString(params.payment_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()));
}
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));
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