blob: eb04d50fb61a65152e770e3a9293ca5b3dd7367e [file] [log] [blame]
// Copyright 2020 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 "device/fido/client_data.h"
#include "base/base64url.h"
#include "base/json/json_reader.h"
#include "base/logging.h"
#include "base/numerics/safe_conversions.h"
#include "base/rand_util.h"
#include "base/strings/utf_string_conversion_utils.h"
#include "components/device_event_log/device_event_log.h"
#include "url/gurl.h"
namespace device {
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 SerializeCollectedClientDataToJson(
const std::string& type,
const std::string& origin,
base::span<const uint8_t> challenge,
bool is_cross_origin,
bool use_legacy_u2f_type_key /* = false */) {
std::string ret;
ret.reserve(128);
if (use_legacy_u2f_type_key) {
ret.append(R"({"typ":)");
} else {
ret.append(R"({"type":)");
}
ret.append(ToJSONString(type));
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 (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;
}
// static
base::Optional<AndroidClientDataExtensionInput>
AndroidClientDataExtensionInput::Parse(const cbor::Value& value) {
if (!value.is_map()) {
return base::nullopt;
}
const cbor::Value::MapValue& map = value.GetMap();
if (map.size() != 3) {
return base::nullopt;
}
AndroidClientDataExtensionInput ext;
for (const auto& pair : map) {
if (!pair.first.is_integer()) {
return base::nullopt;
}
switch (pair.first.GetInteger()) {
case 1:
if (!pair.second.is_string()) {
return base::nullopt;
}
ext.type = pair.second.GetString();
break;
case 2:
if (!pair.second.is_string()) {
return base::nullopt;
}
ext.origin = url::Origin::Create(GURL(pair.second.GetString()));
if (ext.origin.opaque() ||
ext.origin.Serialize() != pair.second.GetString()) {
return base::nullopt;
}
break;
case 3:
if (!pair.second.is_bytestring()) {
return base::nullopt;
}
ext.challenge = pair.second.GetBytestring();
break;
default:
return base::nullopt;
}
}
return ext;
}
AndroidClientDataExtensionInput::AndroidClientDataExtensionInput() = default;
AndroidClientDataExtensionInput::AndroidClientDataExtensionInput(
std::string type_,
url::Origin origin_,
std::vector<uint8_t> challenge_)
: type(type_), origin(origin_), challenge(challenge_) {}
AndroidClientDataExtensionInput::AndroidClientDataExtensionInput(
const AndroidClientDataExtensionInput&) = default;
AndroidClientDataExtensionInput::AndroidClientDataExtensionInput(
AndroidClientDataExtensionInput&&) = default;
AndroidClientDataExtensionInput& AndroidClientDataExtensionInput::operator=(
const AndroidClientDataExtensionInput&) = default;
AndroidClientDataExtensionInput& AndroidClientDataExtensionInput::operator=(
AndroidClientDataExtensionInput&&) = default;
AndroidClientDataExtensionInput::~AndroidClientDataExtensionInput() = default;
cbor::Value AsCBOR(const AndroidClientDataExtensionInput& ext) {
cbor::Value::MapValue map;
map[cbor::Value(1)] = cbor::Value(ext.type);
map[cbor::Value(2)] = cbor::Value(ext.origin.Serialize());
map[cbor::Value(3)] = cbor::Value(ext.challenge);
return cbor::Value(map);
}
bool IsValidAndroidClientDataJSON(
const device::AndroidClientDataExtensionInput& extension_input,
base::StringPiece android_client_data_json) {
base::Optional<base::Value> client_data =
base::JSONReader::Read(android_client_data_json);
if (!client_data || !client_data->is_dict()) {
FIDO_LOG(ERROR) << "Invalid androidClientData extension: "
<< android_client_data_json;
return false;
}
const base::DictionaryValue& client_data_dict =
base::Value::AsDictionaryValue(*client_data);
std::string type;
std::string challenge;
std::string origin;
std::string android_package_name;
if (client_data_dict.size() != 4 ||
!client_data_dict.GetString("type", &type) ||
type != extension_input.type ||
!client_data_dict.GetString("challenge", &challenge) ||
challenge != Base64UrlEncode(extension_input.challenge) ||
!client_data_dict.GetString("origin", &origin) ||
origin != extension_input.origin.Serialize() ||
!client_data_dict.GetString("androidPackageName",
&android_package_name)) {
FIDO_LOG(ERROR) << "Invalid androidClientData extension: "
<< android_client_data_json;
return false;
}
return true;
}
} // namespace device