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