1 // Copyright 2020 The Chromium Authors. All rights reserved.
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 "device/fido/client_data.h"
6
7 #include "base/base64url.h"
8 #include "base/json/json_reader.h"
9 #include "base/logging.h"
10 #include "base/numerics/safe_conversions.h"
11 #include "base/rand_util.h"
12 #include "base/strings/utf_string_conversion_utils.h"
13 #include "components/device_event_log/device_event_log.h"
14 #include "url/gurl.h"
15
16 namespace device {
17
18 namespace {
19
Base64UrlEncode(const base::span<const uint8_t> input)20 std::string Base64UrlEncode(const base::span<const uint8_t> input) {
21 std::string ret;
22 base::Base64UrlEncode(
23 base::StringPiece(reinterpret_cast<const char*>(input.data()),
24 input.size()),
25 base::Base64UrlEncodePolicy::OMIT_PADDING, &ret);
26 return ret;
27 }
28
29 // ToJSONString encodes |in| as a JSON string, using the specific escaping rules
30 // required by https://github.com/w3c/webauthn/pull/1375.
ToJSONString(base::StringPiece in)31 std::string ToJSONString(base::StringPiece in) {
32 std::string ret;
33 ret.reserve(in.size() + 2);
34 ret.push_back('"');
35
36 const char* const in_bytes = in.data();
37 // ICU uses |int32_t| for lengths.
38 const int32_t length = base::checked_cast<int32_t>(in.size());
39 int32_t offset = 0;
40
41 while (offset < length) {
42 const int32_t prior_offset = offset;
43 // Input strings must be valid UTF-8.
44 uint32_t codepoint;
45 CHECK(base::ReadUnicodeCharacter(in_bytes, length, &offset, &codepoint));
46 // offset is updated by |ReadUnicodeCharacter| to index the last byte of the
47 // codepoint. Increment it to index the first byte of the next codepoint for
48 // the subsequent iteration.
49 offset++;
50
51 if (codepoint == 0x20 || codepoint == 0x21 ||
52 (codepoint >= 0x23 && codepoint <= 0x5b) || codepoint >= 0x5d) {
53 ret.append(&in_bytes[prior_offset], &in_bytes[offset]);
54 } else if (codepoint == 0x22) {
55 ret.append("\\\"");
56 } else if (codepoint == 0x5c) {
57 ret.append("\\\\");
58 } else {
59 static const char hextable[17] = "0123456789abcdef";
60 ret.append("\\u00");
61 ret.push_back(hextable[codepoint >> 4]);
62 ret.push_back(hextable[codepoint & 15]);
63 }
64 }
65
66 ret.push_back('"');
67 return ret;
68 }
69
70 } // namespace
71
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)72 std::string SerializeCollectedClientDataToJson(
73 const std::string& type,
74 const std::string& origin,
75 base::span<const uint8_t> challenge,
76 bool is_cross_origin,
77 bool use_legacy_u2f_type_key /* = false */) {
78 std::string ret;
79 ret.reserve(128);
80
81 if (use_legacy_u2f_type_key) {
82 ret.append(R"({"typ":)");
83 } else {
84 ret.append(R"({"type":)");
85 }
86 ret.append(ToJSONString(type));
87
88 ret.append(R"(,"challenge":)");
89 ret.append(ToJSONString(Base64UrlEncode(challenge)));
90
91 ret.append(R"(,"origin":)");
92 ret.append(ToJSONString(origin));
93
94 if (is_cross_origin) {
95 ret.append(R"(,"crossOrigin":true)");
96 } else {
97 ret.append(R"(,"crossOrigin":false)");
98 }
99
100 if (base::RandDouble() < 0.2) {
101 // An extra key is sometimes added to ensure that RPs do not make
102 // unreasonably specific assumptions about the clientData JSON. This is
103 // done in the fashion of
104 // https://tools.ietf.org/html/draft-ietf-tls-grease
105 ret.append(R"(,"extra_keys_may_be_added_here":")");
106 ret.append(
107 "do not compare clientDataJSON against a template. See "
108 "https://goo.gl/yabPex\"");
109 }
110
111 ret.append("}");
112 return ret;
113 }
114
115 // static
116 base::Optional<AndroidClientDataExtensionInput>
Parse(const cbor::Value & value)117 AndroidClientDataExtensionInput::Parse(const cbor::Value& value) {
118 if (!value.is_map()) {
119 return base::nullopt;
120 }
121 const cbor::Value::MapValue& map = value.GetMap();
122 if (map.size() != 3) {
123 return base::nullopt;
124 }
125 AndroidClientDataExtensionInput ext;
126 for (const auto& pair : map) {
127 if (!pair.first.is_integer()) {
128 return base::nullopt;
129 }
130 switch (pair.first.GetInteger()) {
131 case 1:
132 if (!pair.second.is_string()) {
133 return base::nullopt;
134 }
135 ext.type = pair.second.GetString();
136 break;
137 case 2:
138 if (!pair.second.is_string()) {
139 return base::nullopt;
140 }
141 ext.origin = url::Origin::Create(GURL(pair.second.GetString()));
142 if (ext.origin.opaque() ||
143 ext.origin.Serialize() != pair.second.GetString()) {
144 return base::nullopt;
145 }
146 break;
147 case 3:
148 if (!pair.second.is_bytestring()) {
149 return base::nullopt;
150 }
151 ext.challenge = pair.second.GetBytestring();
152 break;
153 default:
154 return base::nullopt;
155 }
156 }
157 return ext;
158 }
159
160 AndroidClientDataExtensionInput::AndroidClientDataExtensionInput() = default;
AndroidClientDataExtensionInput(std::string type_,url::Origin origin_,std::vector<uint8_t> challenge_)161 AndroidClientDataExtensionInput::AndroidClientDataExtensionInput(
162 std::string type_,
163 url::Origin origin_,
164 std::vector<uint8_t> challenge_)
165 : type(type_), origin(origin_), challenge(challenge_) {}
166 AndroidClientDataExtensionInput::AndroidClientDataExtensionInput(
167 const AndroidClientDataExtensionInput&) = default;
168 AndroidClientDataExtensionInput::AndroidClientDataExtensionInput(
169 AndroidClientDataExtensionInput&&) = default;
170
171 AndroidClientDataExtensionInput& AndroidClientDataExtensionInput::operator=(
172 const AndroidClientDataExtensionInput&) = default;
173 AndroidClientDataExtensionInput& AndroidClientDataExtensionInput::operator=(
174 AndroidClientDataExtensionInput&&) = default;
175
176 AndroidClientDataExtensionInput::~AndroidClientDataExtensionInput() = default;
177
AsCBOR(const AndroidClientDataExtensionInput & ext)178 cbor::Value AsCBOR(const AndroidClientDataExtensionInput& ext) {
179 cbor::Value::MapValue map;
180 map[cbor::Value(1)] = cbor::Value(ext.type);
181 map[cbor::Value(2)] = cbor::Value(ext.origin.Serialize());
182 map[cbor::Value(3)] = cbor::Value(ext.challenge);
183 return cbor::Value(map);
184 }
185
IsValidAndroidClientDataJSON(const device::AndroidClientDataExtensionInput & extension_input,base::StringPiece android_client_data_json)186 bool IsValidAndroidClientDataJSON(
187 const device::AndroidClientDataExtensionInput& extension_input,
188 base::StringPiece android_client_data_json) {
189 base::Optional<base::Value> client_data =
190 base::JSONReader::Read(android_client_data_json);
191 if (!client_data || !client_data->is_dict()) {
192 FIDO_LOG(ERROR) << "Invalid androidClientData extension: "
193 << android_client_data_json;
194 return false;
195 }
196 const base::DictionaryValue& client_data_dict =
197 base::Value::AsDictionaryValue(*client_data);
198 std::string type;
199 std::string challenge;
200 std::string origin;
201 std::string android_package_name;
202 if (client_data_dict.size() != 4 ||
203 !client_data_dict.GetString("type", &type) ||
204 type != extension_input.type ||
205 !client_data_dict.GetString("challenge", &challenge) ||
206 challenge != Base64UrlEncode(extension_input.challenge) ||
207 !client_data_dict.GetString("origin", &origin) ||
208 origin != extension_input.origin.Serialize() ||
209 !client_data_dict.GetString("androidPackageName",
210 &android_package_name)) {
211 FIDO_LOG(ERROR) << "Invalid androidClientData extension: "
212 << android_client_data_json;
213 return false;
214 }
215 return true;
216 }
217
218 } // namespace device
219