| // 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 <optional> |
| |
| #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> CookieCraving::Create( |
| const GURL& url, |
| const std::string& name, |
| const std::string& attributes, |
| base::Time creation_time, |
| std::optional<CookiePartitionKey> 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(); |
| } |
| CookieInclusionStatus ignored_status; |
| std::optional<std::string> domain = cookie_util::GetCookieDomainWithString( |
| url, domain_attribute_value, 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 (!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).value(), |
| 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<CookiePartitionKey> 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<CookiePartitionKey> 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<net::CookiePartitionKey::SerializedCookiePartitionKey, |
| std::string> |
| 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> 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<CookiePartitionKey> 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::optional<CookiePartitionKey>, 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 |