// Copyright 2024 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "net/device_bound_sessions/cookie_craving.h" #include #include "base/strings/strcat.h" #include "net/base/url_util.h" #include "net/cookies/canonical_cookie.h" #include "net/cookies/cookie_constants.h" #include "net/cookies/cookie_inclusion_status.h" #include "net/cookies/cookie_util.h" #include "net/cookies/parsed_cookie.h" #include "net/device_bound_sessions/proto/storage.pb.h" #include "url/url_canon.h" namespace net::device_bound_sessions { namespace { // A one-character value suffices to be non-empty. We avoid using an // unnecessarily long placeholder so as to not eat into the 4096-char limit for // a cookie name-value pair. const char kPlaceholderValue[] = "v"; proto::CookieSameSite ProtoEnumFromCookieSameSite(CookieSameSite same_site) { switch (same_site) { case CookieSameSite::UNSPECIFIED: return proto::CookieSameSite::COOKIE_SAME_SITE_UNSPECIFIED; case CookieSameSite::NO_RESTRICTION: return proto::CookieSameSite::NO_RESTRICTION; case CookieSameSite::LAX_MODE: return proto::CookieSameSite::LAX_MODE; case CookieSameSite::STRICT_MODE: return proto::CookieSameSite::STRICT_MODE; } } CookieSameSite CookieSameSiteFromProtoEnum(proto::CookieSameSite proto) { switch (proto) { case proto::CookieSameSite::COOKIE_SAME_SITE_UNSPECIFIED: return CookieSameSite::UNSPECIFIED; case proto::CookieSameSite::NO_RESTRICTION: return CookieSameSite::NO_RESTRICTION; case proto::CookieSameSite::LAX_MODE: return CookieSameSite::LAX_MODE; case proto::CookieSameSite::STRICT_MODE: return CookieSameSite::STRICT_MODE; } } proto::CookieSourceScheme ProtoEnumFromCookieSourceScheme( CookieSourceScheme scheme) { switch (scheme) { case CookieSourceScheme::kUnset: return proto::CookieSourceScheme::UNSET; case CookieSourceScheme::kNonSecure: return proto::CookieSourceScheme::NON_SECURE; case CookieSourceScheme::kSecure: return proto::CookieSourceScheme::SECURE; } } CookieSourceScheme CookieSourceSchemeFromProtoEnum( proto::CookieSourceScheme proto) { switch (proto) { case proto::CookieSourceScheme::UNSET: return CookieSourceScheme::kUnset; case proto::CookieSourceScheme::NON_SECURE: return CookieSourceScheme::kNonSecure; case proto::CookieSourceScheme::SECURE: return CookieSourceScheme::kSecure; } } } // namespace // static std::optional CookieCraving::Create( const GURL& url, const std::string& name, const std::string& attributes, base::Time creation_time, std::optional cookie_partition_key) { if (!url.is_valid() || creation_time.is_null()) { return std::nullopt; } // Check the name first individually, otherwise the next step which cobbles // together a cookie line may mask issues with the name. if (!ParsedCookie::IsValidCookieName(name)) { return std::nullopt; } // Construct an imitation "Set-Cookie" line to feed into ParsedCookie. // Make up a value which is an arbitrary a non-empty string, because the // "value" of the ParsedCookie will be discarded anyway, and it is valid for // a cookie's name to be empty, but not for both name and value to be empty. std::string line_to_parse = base::StrCat({name, "=", kPlaceholderValue, ";", attributes}); ParsedCookie parsed_cookie(line_to_parse); if (!parsed_cookie.IsValid()) { return std::nullopt; } // `domain` is the domain key for storing the CookieCraving, determined // from the domain attribute value (if any) and the URL. A domain cookie is // marked by a preceding dot, as per CookieBase::Domain(), whereas a host // cookie has no leading dot. std::string domain_attribute_value; if (parsed_cookie.HasDomain()) { domain_attribute_value = parsed_cookie.Domain(); } std::string domain; CookieInclusionStatus ignored_status; // Note: This is a deviation from CanonicalCookie. Here, we also require that // domain is non-empty, which CanonicalCookie does not. See comment below in // IsValid(). if (!cookie_util::GetCookieDomainWithString(url, domain_attribute_value, ignored_status, &domain) || domain.empty()) { return std::nullopt; } std::string path = cookie_util::CanonPathWithString( url, parsed_cookie.HasPath() ? parsed_cookie.Path() : ""); CookiePrefix prefix = cookie_util::GetCookiePrefix(name); if (!cookie_util::IsCookiePrefixValid(prefix, url, parsed_cookie)) { return std::nullopt; } // TODO(chlily): Determine whether nonced partition keys should be supported // for CookieCravings. bool partition_has_nonce = CookiePartitionKey::HasNonce(cookie_partition_key); if (!cookie_util::IsCookiePartitionedValid(url, parsed_cookie, partition_has_nonce)) { return std::nullopt; } if (!parsed_cookie.IsPartitioned() && !partition_has_nonce) { cookie_partition_key = std::nullopt; } // Note: This is a deviation from CanonicalCookie::Create(), which allows // cookies with a Secure attribute to be created as if they came from a // cryptographic URL, even if the URL is not cryptographic, on the basis that // the URL might be trustworthy. CookieCraving makes the simplifying // assumption to ignore this case. CookieSourceScheme source_scheme = url.SchemeIsCryptographic() ? CookieSourceScheme::kSecure : CookieSourceScheme::kNonSecure; int source_port = url.EffectiveIntPort(); CookieCraving cookie_craving{parsed_cookie.Name(), std::move(domain), std::move(path), creation_time, parsed_cookie.IsSecure(), parsed_cookie.IsHttpOnly(), parsed_cookie.SameSite(), std::move(cookie_partition_key), source_scheme, source_port}; CHECK(cookie_craving.IsValid()); return cookie_craving; } // TODO(chlily): Much of this function is copied directly from CanonicalCookie. // Try to deduplicate it. bool CookieCraving::IsValid() const { if (ParsedCookie::ParseTokenString(Name()) != Name() || !ParsedCookie::IsValidCookieName(Name())) { return false; } if (CreationDate().is_null()) { return false; } url::CanonHostInfo ignored_info; std::string canonical_domain = CanonicalizeHost(Domain(), &ignored_info); // Note: This is a deviation from CanonicalCookie. CookieCraving does not // allow Domain() to be empty, whereas CanonicalCookie does (perhaps // erroneously). if (Domain().empty() || Domain() != canonical_domain) { return false; } if (Path().empty() || Path().front() != '/') { return false; } CookiePrefix prefix = cookie_util::GetCookiePrefix(Name()); switch (prefix) { case COOKIE_PREFIX_HOST: if (!SecureAttribute() || Path() != "/" || !IsHostCookie()) { return false; } break; case COOKIE_PREFIX_SECURE: if (!SecureAttribute()) { return false; } break; default: break; } if (IsPartitioned()) { if (CookiePartitionKey::HasNonce(PartitionKey())) { return true; } if (!SecureAttribute()) { return false; } } return true; } bool CookieCraving::IsSatisfiedBy( const CanonicalCookie& canonical_cookie) const { CHECK(IsValid()); CHECK(canonical_cookie.IsCanonical()); // Note: Creation time is not required to match. DBSC configs may be set at // different times from the cookies they reference. DBSC also does not require // expiry time to match, for similar reasons. Source scheme and port are also // not required to match. DBSC does not require the config and its required // cookie to come from the same URL (and the source host does not matter as // long as the Domain attribute value matches), so it doesn't make sense to // compare the source scheme and port either. // TODO(chlily): Decide more carefully how nonced partition keys should be // compared. auto make_required_members_tuple = [](const CookieBase& c) { return std::make_tuple(c.Name(), c.Domain(), c.Path(), c.SecureAttribute(), c.IsHttpOnly(), c.SameSite(), c.PartitionKey()); }; return make_required_members_tuple(*this) == make_required_members_tuple(canonical_cookie); } std::string CookieCraving::DebugString() const { auto bool_to_string = [](bool b) { return b ? "true" : "false"; }; return base::StrCat({"Name: ", Name(), "; Domain: ", Domain(), "; Path: ", Path(), "; SecureAttribute: ", bool_to_string(SecureAttribute()), "; IsHttpOnly: ", bool_to_string(IsHttpOnly()), "; SameSite: ", CookieSameSiteToString(SameSite()), "; IsPartitioned: ", bool_to_string(IsPartitioned())}); // Source scheme and port, and creation date omitted for brevity. } // static CookieCraving CookieCraving::CreateUnsafeForTesting( std::string name, std::string domain, std::string path, base::Time creation, bool secure, bool httponly, CookieSameSite same_site, std::optional partition_key, CookieSourceScheme source_scheme, int source_port) { return CookieCraving{std::move(name), std::move(domain), std::move(path), creation, secure, httponly, same_site, std::move(partition_key), source_scheme, source_port}; } CookieCraving::CookieCraving() = default; CookieCraving::CookieCraving(std::string name, std::string domain, std::string path, base::Time creation, bool secure, bool httponly, CookieSameSite same_site, std::optional partition_key, CookieSourceScheme source_scheme, int source_port) : CookieBase(std::move(name), std::move(domain), std::move(path), creation, secure, httponly, same_site, std::move(partition_key), source_scheme, source_port) {} CookieCraving::CookieCraving(const CookieCraving& other) = default; CookieCraving::CookieCraving(CookieCraving&& other) = default; CookieCraving& CookieCraving::operator=(const CookieCraving& other) = default; CookieCraving& CookieCraving::operator=(CookieCraving&& other) = default; CookieCraving::~CookieCraving() = default; bool CookieCraving::IsEqualForTesting(const CookieCraving& other) const { return Name() == other.Name() && Domain() == other.Domain() && Path() == other.Path() && SecureAttribute() == other.SecureAttribute() && IsHttpOnly() == other.IsHttpOnly() && SameSite() == other.SameSite() && SourceScheme() == other.SourceScheme() && SourcePort() == other.SourcePort() && CreationDate() == other.CreationDate() && PartitionKey() == other.PartitionKey(); } std::ostream& operator<<(std::ostream& os, const CookieCraving& cc) { os << cc.DebugString(); return os; } proto::CookieCraving CookieCraving::ToProto() const { CHECK(IsValid()); proto::CookieCraving proto; proto.set_name(Name()); proto.set_domain(Domain()); proto.set_path(Path()); proto.set_secure(SecureAttribute()); proto.set_httponly(IsHttpOnly()); proto.set_source_port(SourcePort()); proto.set_creation_time( CreationDate().ToDeltaSinceWindowsEpoch().InMicroseconds()); proto.set_same_site(ProtoEnumFromCookieSameSite(SameSite())); proto.set_source_scheme(ProtoEnumFromCookieSourceScheme(SourceScheme())); if (IsPartitioned()) { // TODO(crbug.com/356581003) The serialization below does not handle // nonced cookies. Need to figure out whether this is required. base::expected serialized_partition_key = net::CookiePartitionKey::Serialize(PartitionKey()); CHECK(serialized_partition_key.has_value()); proto.mutable_serialized_partition_key()->set_top_level_site( serialized_partition_key->TopLevelSite()); proto.mutable_serialized_partition_key()->set_has_cross_site_ancestor( serialized_partition_key->has_cross_site_ancestor()); } return proto; } // static std::optional CookieCraving::CreateFromProto( const proto::CookieCraving& proto) { if (!proto.has_name() || !proto.has_domain() || !proto.has_path() || !proto.has_secure() || !proto.has_httponly() || !proto.has_source_port() || !proto.has_creation_time() || !proto.has_same_site() || !proto.has_source_scheme()) { return std::nullopt; } // Retrieve the serialized cookie partition key if present. std::optional partition_key; if (proto.has_serialized_partition_key()) { const proto::SerializedCookiePartitionKey& serialized_key = proto.serialized_partition_key(); if (!serialized_key.has_top_level_site() || !serialized_key.has_has_cross_site_ancestor()) { return std::nullopt; } base::expected, std::string> restored_key = CookiePartitionKey::FromStorage( serialized_key.top_level_site(), serialized_key.has_cross_site_ancestor()); if (!restored_key.has_value() || *restored_key == std::nullopt) { return std::nullopt; } partition_key = std::move(*restored_key); } CookieCraving cookie_craving{ proto.name(), proto.domain(), proto.path(), base::Time::FromDeltaSinceWindowsEpoch( base::Microseconds(proto.creation_time())), proto.secure(), proto.httponly(), CookieSameSiteFromProtoEnum(proto.same_site()), std::move(partition_key), CookieSourceSchemeFromProtoEnum(proto.source_scheme()), proto.source_port()}; if (!cookie_craving.IsValid()) { return std::nullopt; } return cookie_craving; } } // namespace net::device_bound_sessions