• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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