1 // Copyright 2024 The Chromium Authors
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 "net/device_bound_sessions/cookie_craving.h"
6
7 #include <optional>
8
9 #include "base/strings/strcat.h"
10 #include "net/base/url_util.h"
11 #include "net/cookies/canonical_cookie.h"
12 #include "net/cookies/cookie_constants.h"
13 #include "net/cookies/cookie_inclusion_status.h"
14 #include "net/cookies/cookie_util.h"
15 #include "net/cookies/parsed_cookie.h"
16 #include "net/device_bound_sessions/proto/storage.pb.h"
17 #include "url/url_canon.h"
18
19 namespace net::device_bound_sessions {
20
21 namespace {
22
23 // A one-character value suffices to be non-empty. We avoid using an
24 // unnecessarily long placeholder so as to not eat into the 4096-char limit for
25 // a cookie name-value pair.
26 const char kPlaceholderValue[] = "v";
27
ProtoEnumFromCookieSameSite(CookieSameSite same_site)28 proto::CookieSameSite ProtoEnumFromCookieSameSite(CookieSameSite same_site) {
29 switch (same_site) {
30 case CookieSameSite::UNSPECIFIED:
31 return proto::CookieSameSite::COOKIE_SAME_SITE_UNSPECIFIED;
32 case CookieSameSite::NO_RESTRICTION:
33 return proto::CookieSameSite::NO_RESTRICTION;
34 case CookieSameSite::LAX_MODE:
35 return proto::CookieSameSite::LAX_MODE;
36 case CookieSameSite::STRICT_MODE:
37 return proto::CookieSameSite::STRICT_MODE;
38 }
39 }
40
CookieSameSiteFromProtoEnum(proto::CookieSameSite proto)41 CookieSameSite CookieSameSiteFromProtoEnum(proto::CookieSameSite proto) {
42 switch (proto) {
43 case proto::CookieSameSite::COOKIE_SAME_SITE_UNSPECIFIED:
44 return CookieSameSite::UNSPECIFIED;
45 case proto::CookieSameSite::NO_RESTRICTION:
46 return CookieSameSite::NO_RESTRICTION;
47 case proto::CookieSameSite::LAX_MODE:
48 return CookieSameSite::LAX_MODE;
49 case proto::CookieSameSite::STRICT_MODE:
50 return CookieSameSite::STRICT_MODE;
51 }
52 }
53
ProtoEnumFromCookieSourceScheme(CookieSourceScheme scheme)54 proto::CookieSourceScheme ProtoEnumFromCookieSourceScheme(
55 CookieSourceScheme scheme) {
56 switch (scheme) {
57 case CookieSourceScheme::kUnset:
58 return proto::CookieSourceScheme::UNSET;
59 case CookieSourceScheme::kNonSecure:
60 return proto::CookieSourceScheme::NON_SECURE;
61 case CookieSourceScheme::kSecure:
62 return proto::CookieSourceScheme::SECURE;
63 }
64 }
65
CookieSourceSchemeFromProtoEnum(proto::CookieSourceScheme proto)66 CookieSourceScheme CookieSourceSchemeFromProtoEnum(
67 proto::CookieSourceScheme proto) {
68 switch (proto) {
69 case proto::CookieSourceScheme::UNSET:
70 return CookieSourceScheme::kUnset;
71 case proto::CookieSourceScheme::NON_SECURE:
72 return CookieSourceScheme::kNonSecure;
73 case proto::CookieSourceScheme::SECURE:
74 return CookieSourceScheme::kSecure;
75 }
76 }
77
78 } // namespace
79
80 // static
Create(const GURL & url,const std::string & name,const std::string & attributes,base::Time creation_time,std::optional<CookiePartitionKey> cookie_partition_key)81 std::optional<CookieCraving> CookieCraving::Create(
82 const GURL& url,
83 const std::string& name,
84 const std::string& attributes,
85 base::Time creation_time,
86 std::optional<CookiePartitionKey> cookie_partition_key) {
87 if (!url.is_valid() || creation_time.is_null()) {
88 return std::nullopt;
89 }
90
91 // Check the name first individually, otherwise the next step which cobbles
92 // together a cookie line may mask issues with the name.
93 if (!ParsedCookie::IsValidCookieName(name)) {
94 return std::nullopt;
95 }
96
97 // Construct an imitation "Set-Cookie" line to feed into ParsedCookie.
98 // Make up a value which is an arbitrary a non-empty string, because the
99 // "value" of the ParsedCookie will be discarded anyway, and it is valid for
100 // a cookie's name to be empty, but not for both name and value to be empty.
101 std::string line_to_parse =
102 base::StrCat({name, "=", kPlaceholderValue, ";", attributes});
103
104 ParsedCookie parsed_cookie(line_to_parse);
105 if (!parsed_cookie.IsValid()) {
106 return std::nullopt;
107 }
108
109 // `domain` is the domain key for storing the CookieCraving, determined
110 // from the domain attribute value (if any) and the URL. A domain cookie is
111 // marked by a preceding dot, as per CookieBase::Domain(), whereas a host
112 // cookie has no leading dot.
113 std::string domain_attribute_value;
114 if (parsed_cookie.HasDomain()) {
115 domain_attribute_value = parsed_cookie.Domain();
116 }
117 std::string domain;
118 CookieInclusionStatus ignored_status;
119 // Note: This is a deviation from CanonicalCookie. Here, we also require that
120 // domain is non-empty, which CanonicalCookie does not. See comment below in
121 // IsValid().
122 if (!cookie_util::GetCookieDomainWithString(url, domain_attribute_value,
123 ignored_status, &domain) ||
124 domain.empty()) {
125 return std::nullopt;
126 }
127
128 std::string path = cookie_util::CanonPathWithString(
129 url, parsed_cookie.HasPath() ? parsed_cookie.Path() : "");
130
131 CookiePrefix prefix = cookie_util::GetCookiePrefix(name);
132 if (!cookie_util::IsCookiePrefixValid(prefix, url, parsed_cookie)) {
133 return std::nullopt;
134 }
135
136 // TODO(chlily): Determine whether nonced partition keys should be supported
137 // for CookieCravings.
138 bool partition_has_nonce = CookiePartitionKey::HasNonce(cookie_partition_key);
139 if (!cookie_util::IsCookiePartitionedValid(url, parsed_cookie,
140 partition_has_nonce)) {
141 return std::nullopt;
142 }
143 if (!parsed_cookie.IsPartitioned() && !partition_has_nonce) {
144 cookie_partition_key = std::nullopt;
145 }
146
147 // Note: This is a deviation from CanonicalCookie::Create(), which allows
148 // cookies with a Secure attribute to be created as if they came from a
149 // cryptographic URL, even if the URL is not cryptographic, on the basis that
150 // the URL might be trustworthy. CookieCraving makes the simplifying
151 // assumption to ignore this case.
152 CookieSourceScheme source_scheme = url.SchemeIsCryptographic()
153 ? CookieSourceScheme::kSecure
154 : CookieSourceScheme::kNonSecure;
155 int source_port = url.EffectiveIntPort();
156
157 CookieCraving cookie_craving{parsed_cookie.Name(),
158 std::move(domain),
159 std::move(path),
160 creation_time,
161 parsed_cookie.IsSecure(),
162 parsed_cookie.IsHttpOnly(),
163 parsed_cookie.SameSite(),
164 std::move(cookie_partition_key),
165 source_scheme,
166 source_port};
167
168 CHECK(cookie_craving.IsValid());
169 return cookie_craving;
170 }
171
172 // TODO(chlily): Much of this function is copied directly from CanonicalCookie.
173 // Try to deduplicate it.
IsValid() const174 bool CookieCraving::IsValid() const {
175 if (ParsedCookie::ParseTokenString(Name()) != Name() ||
176 !ParsedCookie::IsValidCookieName(Name())) {
177 return false;
178 }
179
180 if (CreationDate().is_null()) {
181 return false;
182 }
183
184 url::CanonHostInfo ignored_info;
185 std::string canonical_domain = CanonicalizeHost(Domain(), &ignored_info);
186 // Note: This is a deviation from CanonicalCookie. CookieCraving does not
187 // allow Domain() to be empty, whereas CanonicalCookie does (perhaps
188 // erroneously).
189 if (Domain().empty() || Domain() != canonical_domain) {
190 return false;
191 }
192
193 if (Path().empty() || Path().front() != '/') {
194 return false;
195 }
196
197 CookiePrefix prefix = cookie_util::GetCookiePrefix(Name());
198 switch (prefix) {
199 case COOKIE_PREFIX_HOST:
200 if (!SecureAttribute() || Path() != "/" || !IsHostCookie()) {
201 return false;
202 }
203 break;
204 case COOKIE_PREFIX_SECURE:
205 if (!SecureAttribute()) {
206 return false;
207 }
208 break;
209 default:
210 break;
211 }
212
213 if (IsPartitioned()) {
214 if (CookiePartitionKey::HasNonce(PartitionKey())) {
215 return true;
216 }
217 if (!SecureAttribute()) {
218 return false;
219 }
220 }
221
222 return true;
223 }
224
IsSatisfiedBy(const CanonicalCookie & canonical_cookie) const225 bool CookieCraving::IsSatisfiedBy(
226 const CanonicalCookie& canonical_cookie) const {
227 CHECK(IsValid());
228 CHECK(canonical_cookie.IsCanonical());
229
230 // Note: Creation time is not required to match. DBSC configs may be set at
231 // different times from the cookies they reference. DBSC also does not require
232 // expiry time to match, for similar reasons. Source scheme and port are also
233 // not required to match. DBSC does not require the config and its required
234 // cookie to come from the same URL (and the source host does not matter as
235 // long as the Domain attribute value matches), so it doesn't make sense to
236 // compare the source scheme and port either.
237 // TODO(chlily): Decide more carefully how nonced partition keys should be
238 // compared.
239 auto make_required_members_tuple = [](const CookieBase& c) {
240 return std::make_tuple(c.Name(), c.Domain(), c.Path(), c.SecureAttribute(),
241 c.IsHttpOnly(), c.SameSite(), c.PartitionKey());
242 };
243
244 return make_required_members_tuple(*this) ==
245 make_required_members_tuple(canonical_cookie);
246 }
247
DebugString() const248 std::string CookieCraving::DebugString() const {
249 auto bool_to_string = [](bool b) { return b ? "true" : "false"; };
250 return base::StrCat({"Name: ", Name(), "; Domain: ", Domain(),
251 "; Path: ", Path(),
252 "; SecureAttribute: ", bool_to_string(SecureAttribute()),
253 "; IsHttpOnly: ", bool_to_string(IsHttpOnly()),
254 "; SameSite: ", CookieSameSiteToString(SameSite()),
255 "; IsPartitioned: ", bool_to_string(IsPartitioned())});
256 // Source scheme and port, and creation date omitted for brevity.
257 }
258
259 // static
CreateUnsafeForTesting(std::string name,std::string domain,std::string path,base::Time creation,bool secure,bool httponly,CookieSameSite same_site,std::optional<CookiePartitionKey> partition_key,CookieSourceScheme source_scheme,int source_port)260 CookieCraving CookieCraving::CreateUnsafeForTesting(
261 std::string name,
262 std::string domain,
263 std::string path,
264 base::Time creation,
265 bool secure,
266 bool httponly,
267 CookieSameSite same_site,
268 std::optional<CookiePartitionKey> partition_key,
269 CookieSourceScheme source_scheme,
270 int source_port) {
271 return CookieCraving{std::move(name), std::move(domain),
272 std::move(path), creation,
273 secure, httponly,
274 same_site, std::move(partition_key),
275 source_scheme, source_port};
276 }
277
278 CookieCraving::CookieCraving() = default;
279
CookieCraving(std::string name,std::string domain,std::string path,base::Time creation,bool secure,bool httponly,CookieSameSite same_site,std::optional<CookiePartitionKey> partition_key,CookieSourceScheme source_scheme,int source_port)280 CookieCraving::CookieCraving(std::string name,
281 std::string domain,
282 std::string path,
283 base::Time creation,
284 bool secure,
285 bool httponly,
286 CookieSameSite same_site,
287 std::optional<CookiePartitionKey> partition_key,
288 CookieSourceScheme source_scheme,
289 int source_port)
290 : CookieBase(std::move(name),
291 std::move(domain),
292 std::move(path),
293 creation,
294 secure,
295 httponly,
296 same_site,
297 std::move(partition_key),
298 source_scheme,
299 source_port) {}
300
301 CookieCraving::CookieCraving(const CookieCraving& other) = default;
302
303 CookieCraving::CookieCraving(CookieCraving&& other) = default;
304
305 CookieCraving& CookieCraving::operator=(const CookieCraving& other) = default;
306
307 CookieCraving& CookieCraving::operator=(CookieCraving&& other) = default;
308
309 CookieCraving::~CookieCraving() = default;
310
IsEqualForTesting(const CookieCraving & other) const311 bool CookieCraving::IsEqualForTesting(const CookieCraving& other) const {
312 return Name() == other.Name() && Domain() == other.Domain() &&
313 Path() == other.Path() &&
314 SecureAttribute() == other.SecureAttribute() &&
315 IsHttpOnly() == other.IsHttpOnly() && SameSite() == other.SameSite() &&
316 SourceScheme() == other.SourceScheme() &&
317 SourcePort() == other.SourcePort() &&
318 CreationDate() == other.CreationDate() &&
319 PartitionKey() == other.PartitionKey();
320 }
321
operator <<(std::ostream & os,const CookieCraving & cc)322 std::ostream& operator<<(std::ostream& os, const CookieCraving& cc) {
323 os << cc.DebugString();
324 return os;
325 }
326
ToProto() const327 proto::CookieCraving CookieCraving::ToProto() const {
328 CHECK(IsValid());
329
330 proto::CookieCraving proto;
331 proto.set_name(Name());
332 proto.set_domain(Domain());
333 proto.set_path(Path());
334 proto.set_secure(SecureAttribute());
335 proto.set_httponly(IsHttpOnly());
336 proto.set_source_port(SourcePort());
337 proto.set_creation_time(
338 CreationDate().ToDeltaSinceWindowsEpoch().InMicroseconds());
339 proto.set_same_site(ProtoEnumFromCookieSameSite(SameSite()));
340 proto.set_source_scheme(ProtoEnumFromCookieSourceScheme(SourceScheme()));
341
342 if (IsPartitioned()) {
343 // TODO(crbug.com/356581003) The serialization below does not handle
344 // nonced cookies. Need to figure out whether this is required.
345 base::expected<net::CookiePartitionKey::SerializedCookiePartitionKey,
346 std::string>
347 serialized_partition_key =
348 net::CookiePartitionKey::Serialize(PartitionKey());
349 CHECK(serialized_partition_key.has_value());
350 proto.mutable_serialized_partition_key()->set_top_level_site(
351 serialized_partition_key->TopLevelSite());
352 proto.mutable_serialized_partition_key()->set_has_cross_site_ancestor(
353 serialized_partition_key->has_cross_site_ancestor());
354 }
355
356 return proto;
357 }
358
359 // static
CreateFromProto(const proto::CookieCraving & proto)360 std::optional<CookieCraving> CookieCraving::CreateFromProto(
361 const proto::CookieCraving& proto) {
362 if (!proto.has_name() || !proto.has_domain() || !proto.has_path() ||
363 !proto.has_secure() || !proto.has_httponly() ||
364 !proto.has_source_port() || !proto.has_creation_time() ||
365 !proto.has_same_site() || !proto.has_source_scheme()) {
366 return std::nullopt;
367 }
368
369 // Retrieve the serialized cookie partition key if present.
370 std::optional<CookiePartitionKey> partition_key;
371 if (proto.has_serialized_partition_key()) {
372 const proto::SerializedCookiePartitionKey& serialized_key =
373 proto.serialized_partition_key();
374 if (!serialized_key.has_top_level_site() ||
375 !serialized_key.has_has_cross_site_ancestor()) {
376 return std::nullopt;
377 }
378 base::expected<std::optional<CookiePartitionKey>, std::string>
379 restored_key = CookiePartitionKey::FromStorage(
380 serialized_key.top_level_site(),
381 serialized_key.has_cross_site_ancestor());
382 if (!restored_key.has_value() || *restored_key == std::nullopt) {
383 return std::nullopt;
384 }
385 partition_key = std::move(*restored_key);
386 }
387
388 CookieCraving cookie_craving{
389 proto.name(),
390 proto.domain(),
391 proto.path(),
392 base::Time::FromDeltaSinceWindowsEpoch(
393 base::Microseconds(proto.creation_time())),
394 proto.secure(),
395 proto.httponly(),
396 CookieSameSiteFromProtoEnum(proto.same_site()),
397 std::move(partition_key),
398 CookieSourceSchemeFromProtoEnum(proto.source_scheme()),
399 proto.source_port()};
400
401 if (!cookie_craving.IsValid()) {
402 return std::nullopt;
403 }
404
405 return cookie_craving;
406 }
407
408 } // namespace net::device_bound_sessions
409