blob: ec4a6d9e1d4ef9879a479bc66820eb74c1fbf74f [file]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include <algorithm>
#include <memory>
#include <ostream>
#include <string>
#include <string_view>
#include "base/check.h"
#include "base/feature_list.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/types/optional_util.h"
#include "net/base/features.h"
#include "net/base/isolation_info.h"
#include "net/base/network_isolation_partition.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/cookies/cookie_partition_key.h"
#include "third_party/abseil-cpp/absl/strings/ascii.h"
#include "url/gurl.h"
namespace {
// This enum represents the different type of encodable partitioning
// attributes. These values are persisted to disk. Entries should not be
// renumbered and numeric values should never be reused.
enum class EncodedAttribute : uint8_t {
kTopLevelSite = 0,
kNonceHigh = 1,
kNonceLow = 2,
kAncestorChainBit = 3,
kTopLevelSiteOpaqueNonceHigh = 4,
kTopLevelSiteOpaqueNonceLow = 5,
kTopLevelSiteOpaquePrecursor = 6,
kMaxValue = kTopLevelSiteOpaquePrecursor,
};
// Converts the attribute type into the separator + uint8_t byte
// serialization. E.x.: kTopLevelSite becomes "^0"
std::string SerializeAttributeSeparator(const EncodedAttribute type) {
// Create a size 2 string, we'll overwrite the second char later.
std::string ret(2, '^');
char digit = static_cast<uint8_t>(type) + '0';
ret[1] = digit;
return ret;
}
// Converts the serialized separator into an EncodedAttribute enum.
// E.x.: "^0" becomes kTopLevelSite.
// Expects `in` to have a length of 2.
std::optional<EncodedAttribute> DeserializeAttributeSeparator(
const std::string_view& in) {
DCHECK_EQ(in.size(), 2U);
uint8_t number = in[1] - '0';
if (number > static_cast<uint8_t>(EncodedAttribute::kMaxValue)) {
// Bad input, return std::nullopt to indicate an issue.
return std::nullopt;
}
return static_cast<EncodedAttribute>(number);
}
// Returns true if there are at least 2 chars after the '^' in `in` and the
// second char is not '^'. Meaning that the substring is syntactically valid.
// This is to indicate that there is a valid separator with both a '^' and a
// uint8_t and some amount of encoded data. I.e.: "^09" has both a "^0" as the
// separator and '9' as the encoded data.
bool ValidSeparatorWithData(std::string_view in, size_t pos_of_caret) {
if (in.length() > pos_of_caret + 2 && in[pos_of_caret + 2] != '^')
return true;
return false;
}
} // namespace
namespace blink {
// static
std::optional<StorageKey> StorageKey::Deserialize(std::string_view in) {
// As per the Serialize() call, we have to expect one of the following
// structures:
// <StorageKey `key`.origin> + "/" + "^1" + <StorageKey
// `key`.nonce.High64Bits> + "^2" + <StorageKey `key`.nonce.Low64Bits>
// - or -
// <StorageKey `key`.origin> + "/"
// - or -
// <StorageKey `key`.origin> + "/" + "^3" + <StorageKey
// `key`.ancestor_chain_bit>
// - or -
// <StorageKey `key`.origin> + "/" + "^0" + <StorageKey `key`.top_level_site>
// - or -
// <StorageKey `key`.origin> + "/" + ^4" + <StorageKey
// `key`.top_level_site.nonce.High64Bits> + "^5" + <StorageKey
// `key`.top_level_site.nonce.Low64Bits> + "^6" + <StorageKey
// `key`.top_level_site.precursor>
//
// See Serialize() for more information.
// Let's check for a delimiting caret. The presence of a caret means this key
// is partitioned.
// More than three encoded attributes (delimited by carets) indicates a
// malformed input.
if (std::ranges::count(in, '^') > 3) {
return std::nullopt;
}
const size_t pos_first_caret = in.find_first_of('^');
const size_t pos_second_caret =
pos_first_caret == std::string::npos
? std::string::npos
: in.find_first_of('^', pos_first_caret + 1);
const size_t pos_third_caret =
pos_second_caret == std::string::npos
? std::string::npos
: in.find_first_of('^', pos_second_caret + 1);
url::Origin key_origin;
net::SchemefulSite key_top_level_site;
std::optional<base::UnguessableToken> nonce;
blink::mojom::AncestorChainBit ancestor_chain_bit;
if (pos_first_caret == std::string::npos) {
// Only the origin is serialized.
key_origin = url::Origin::Create(GURL(in));
// In this case the top_level_site is implicitly the same site as the
// origin.
key_top_level_site = net::SchemefulSite(key_origin);
// There is no nonce.
// The origin should not be opaque and the serialization should be
// reversible.
if (key_origin.opaque() || key_origin.GetURL().spec() != in) {
return std::nullopt;
}
return StorageKey(key_origin, key_top_level_site, nullptr,
blink::mojom::AncestorChainBit::kSameSite,
/*third_party_partitioning_allowed=*/false);
}
if (!ValidSeparatorWithData(in, pos_first_caret))
return std::nullopt;
// Otherwise the key is partitioned, let's see what it's partitioned by.
std::optional<EncodedAttribute> first_attribute =
DeserializeAttributeSeparator(in.substr(pos_first_caret, 2));
if (!first_attribute.has_value())
return std::nullopt;
switch (first_attribute.value()) {
case EncodedAttribute::kTopLevelSite: {
// Cross-Origin keys cannot be read if partitioning is off.
if (!IsThirdPartyStoragePartitioningEnabled()) {
return std::nullopt;
}
// A top-level site is serialized and has only one encoded attribute.
if (pos_second_caret != std::string::npos) {
return std::nullopt;
}
// The origin is the portion up to, but not including, the caret
// separator.
const std::string_view origin_substr = in.substr(0, pos_first_caret);
key_origin = url::Origin::Create(GURL(origin_substr));
// The origin should not be opaque and the serialization should be
// reversible.
if (key_origin.opaque() || key_origin.GetURL().spec() != origin_substr) {
return std::nullopt;
}
// The top_level_site is the portion beyond the first separator.
int length_of_site = pos_second_caret - (pos_first_caret + 2);
const std::string_view top_level_site_substr =
in.substr(pos_first_caret + 2, length_of_site);
key_top_level_site = net::SchemefulSite(GURL(top_level_site_substr));
// The top level site should not be opaque and the serialization should be
// reversible.
if (key_top_level_site.opaque() ||
key_top_level_site.Serialize() != top_level_site_substr) {
return std::nullopt;
}
// There is no nonce or ancestor chain bit.
// Neither should be opaque and they cannot match as that would mean
// we should have simply encoded the origin and the input is malformed.
if (key_origin.opaque() || key_top_level_site.opaque() ||
key_top_level_site.IsSameSiteWith(key_origin)) {
return std::nullopt;
}
// The ancestor chain bit must be CrossSite as that's an invariant
// when the origin and top level site don't match.
// TODO(crbug.com/1199077): Deserialize should always be able to make 3p
// keys and shouldn't depend on the state of partitioning (because we
// don't want to inadvertently turn two 3p keys into the same 1p key).
// Unfortunately, some tests (and potentially code) depend on this. Here,
// and below, should be changed to true and the dependencies on this
// behavior should be removed.
return StorageKey(key_origin, key_top_level_site, nullptr,
blink::mojom::AncestorChainBit::kCrossSite,
IsThirdPartyStoragePartitioningEnabled());
}
case EncodedAttribute::kAncestorChainBit: {
// Same-Origin kCrossSite keys cannot be read if partitioning is off.
if (!IsThirdPartyStoragePartitioningEnabled()) {
return std::nullopt;
}
// An ancestor chain bit is serialized and has only one encoded attribute.
if (pos_second_caret != std::string::npos) {
return std::nullopt;
}
// The origin is the portion up to, but not including, the caret
// separator.
const std::string_view origin_substr = in.substr(0, pos_first_caret);
key_origin = url::Origin::Create(GURL(origin_substr));
// The origin should not be opaque and the serialization should be
// reversible.
if (key_origin.opaque() || key_origin.GetURL().spec() != origin_substr) {
return std::nullopt;
}
// The ancestor_chain_bit is the portion beyond the first separator.
int raw_bit;
const std::string_view raw_bit_substr =
in.substr(pos_first_caret + 2, std::string::npos);
if (!base::StringToInt(raw_bit_substr, &raw_bit)) {
return std::nullopt;
}
// If the integer conversion results in a value outside the enumerated
// indices of [0,1] or trimmed leading 0s we must reject the key.
if (raw_bit < 0 || raw_bit > 1 || raw_bit_substr.size() > 1) {
return std::nullopt;
}
ancestor_chain_bit = static_cast<blink::mojom::AncestorChainBit>(raw_bit);
// There is no nonce or top level site.
// The origin shouldn't be opaque and ancestor chain bit must be CrossSite
// as otherwise should have simply encoded the origin and the input is
// malformed.
if (ancestor_chain_bit != blink::mojom::AncestorChainBit::kCrossSite) {
return std::nullopt;
}
// This format indicates the top level site matches the origin.
return StorageKey(key_origin, net::SchemefulSite(key_origin), nullptr,
ancestor_chain_bit,
IsThirdPartyStoragePartitioningEnabled());
}
case EncodedAttribute::kNonceHigh: {
// A nonce is serialized and has only two encoded attributes.
if (pos_third_caret != std::string::npos) {
return std::nullopt;
}
// Make sure we found the next separator, it's valid, that it's the
// correct attribute.
if (pos_second_caret == std::string::npos ||
!ValidSeparatorWithData(in, pos_second_caret))
return std::nullopt;
std::optional<EncodedAttribute> second_attribute =
DeserializeAttributeSeparator(in.substr(pos_second_caret, 2));
if (!second_attribute.has_value() ||
second_attribute.value() != EncodedAttribute::kNonceLow)
return std::nullopt;
// The origin is the portion up to, but not including, the first
// separator.
const std::string_view origin_substr = in.substr(0, pos_first_caret);
key_origin = url::Origin::Create(GURL(origin_substr));
// The origin should not be opaque and the serialization should be
// reversible.
if (key_origin.opaque() || key_origin.GetURL().spec() != origin_substr) {
return std::nullopt;
}
// The first high 64 bits of the nonce are next, between the two
// separators.
int length_of_high = pos_second_caret - (pos_first_caret + 2);
std::string_view high_digits =
in.substr(pos_first_caret + 2, length_of_high);
// The low 64 bits are last, after the second separator.
std::string_view low_digits = in.substr(pos_second_caret + 2);
uint64_t nonce_high = 0;
uint64_t nonce_low = 0;
if (!base::StringToUint64(high_digits, &nonce_high))
return std::nullopt;
if (!base::StringToUint64(low_digits, &nonce_low))
return std::nullopt;
// The key is corrupted if there are extra 0s in front of the nonce.
if (base::NumberToString(nonce_high) != high_digits ||
base::NumberToString(nonce_low) != low_digits) {
return std::nullopt;
}
nonce = base::UnguessableToken::Deserialize(nonce_high, nonce_low);
if (!nonce.has_value()) {
return std::nullopt;
}
// This constructor makes a copy of the nonce, so getting the raw pointer
// is safe.
// Note: The partitioning allowed value is irrelevant with a nonce,
// `false` was chosen arbitrarily.
return StorageKey(key_origin, net::SchemefulSite(key_origin),
&nonce.value(),
blink::mojom::AncestorChainBit::kCrossSite,
/*third_party_partitioning_allowed=*/false);
}
case EncodedAttribute::kTopLevelSiteOpaqueNonceHigh: {
// An opaque `top_level_site` is serialized.
// Cross-Origin keys cannot be read if partitioning is off.
if (!IsThirdPartyStoragePartitioningEnabled()) {
return std::nullopt;
}
// Make sure we found the next separator, it's valid, that it's the
// correct attribute.
if (pos_second_caret == std::string::npos ||
!ValidSeparatorWithData(in, pos_second_caret)) {
return std::nullopt;
}
std::optional<EncodedAttribute> second_attribute =
DeserializeAttributeSeparator(in.substr(pos_second_caret, 2));
if (!second_attribute.has_value() ||
second_attribute.value() !=
EncodedAttribute::kTopLevelSiteOpaqueNonceLow) {
return std::nullopt;
}
// The origin is the portion up to, but not including, the first
// separator.
const std::string_view origin_substr = in.substr(0, pos_first_caret);
key_origin = url::Origin::Create(GURL(origin_substr));
// The origin should not be opaque and the serialization should be
// reversible.
if (key_origin.opaque() || key_origin.GetURL().spec() != origin_substr) {
return std::nullopt;
}
// The first high 64 bits of the sites's nonce are next, between the first
// separators.
int length_of_high = pos_second_caret - (pos_first_caret + 2);
std::string_view high_digits =
in.substr(pos_first_caret + 2, length_of_high);
// The low 64 bits are next, after the second separator.
int length_of_low = pos_third_caret - (pos_second_caret + 2);
std::string_view low_digits =
in.substr(pos_second_caret + 2, length_of_low);
uint64_t nonce_high = 0;
uint64_t nonce_low = 0;
if (!base::StringToUint64(high_digits, &nonce_high)) {
return std::nullopt;
}
if (!base::StringToUint64(low_digits, &nonce_low)) {
return std::nullopt;
}
// The key is corrupted if there are extra 0s in front of the nonce.
if (base::NumberToString(nonce_high) != high_digits ||
base::NumberToString(nonce_low) != low_digits) {
return std::nullopt;
}
const std::optional<base::UnguessableToken> site_nonce =
base::UnguessableToken::Deserialize(nonce_high, nonce_low);
// The nonce must have content.
if (!site_nonce) {
return std::nullopt;
}
// Make sure we found the final separator, it's valid, that it's the
// correct attribute.
if (pos_third_caret == std::string::npos ||
(in.size() - pos_third_caret) < 2) {
return std::nullopt;
}
std::optional<EncodedAttribute> third_attribute =
DeserializeAttributeSeparator(in.substr(pos_third_caret, 2));
if (!third_attribute.has_value() ||
third_attribute.value() !=
EncodedAttribute::kTopLevelSiteOpaquePrecursor) {
return std::nullopt;
}
// The precursor is the rest of the input.
const std::string_view url_precursor_substr =
in.substr(pos_third_caret + 2);
const GURL url_precursor(url_precursor_substr);
const url::SchemeHostPort tuple_precursor(url_precursor);
// The precursor must be empty or valid, and the serialization should be
// reversible.
if ((!url_precursor.is_empty() && !tuple_precursor.IsValid()) ||
tuple_precursor.Serialize() != url_precursor_substr) {
return std::nullopt;
}
// This constructor makes a copy of the site's nonce, so getting the raw
// pointer is safe.
return StorageKey(
key_origin,
net::SchemefulSite(url::Origin(url::Origin::Nonce(site_nonce.value()),
tuple_precursor)),
nullptr, blink::mojom::AncestorChainBit::kCrossSite,
IsThirdPartyStoragePartitioningEnabled());
}
default: {
// Malformed input case. We saw a separator that we don't understand
// or one in the wrong order.
return std::nullopt;
}
}
}
// static
std::optional<StorageKey> StorageKey::DeserializeForLocalStorage(
std::string_view in) {
// We have to support the local storage specific variant that lacks the
// trailing slash.
const url::Origin maybe_origin = url::Origin::Create(GURL(in));
if (!maybe_origin.opaque()) {
if (maybe_origin.Serialize() == in) {
return StorageKey(maybe_origin, net::SchemefulSite(maybe_origin), nullptr,
blink::mojom::AncestorChainBit::kSameSite,
/*third_party_partitioning_allowed=*/false);
} else if (maybe_origin.GetURL().spec() == in) {
// This first party key was passed in with a trailing slash. This is
// required in Deserialize() but improper for DeserializeForLocalStorage()
// and must be rejected.
return std::nullopt;
}
}
// Otherwise we fallback on base deserialization.
return Deserialize(in);
}
// static
StorageKey StorageKey::CreateFromStringForTesting(const std::string& origin) {
return CreateFirstParty(url::Origin::Create(GURL(origin)));
}
// static
// Keep consistent with BlinkStorageKey::FromWire().
bool StorageKey::FromWire(
const url::Origin& origin,
const net::SchemefulSite& top_level_site,
const net::SchemefulSite& top_level_site_if_third_party_enabled,
const std::optional<base::UnguessableToken>& nonce,
blink::mojom::AncestorChainBit ancestor_chain_bit,
blink::mojom::AncestorChainBit ancestor_chain_bit_if_third_party_enabled,
StorageKey& out) {
// We need to build a different key to prevent overriding `out` if the result
// isn't valid.
StorageKey maybe_out;
maybe_out.origin_ = origin;
maybe_out.top_level_site_ = top_level_site;
maybe_out.top_level_site_if_third_party_enabled_ =
top_level_site_if_third_party_enabled;
maybe_out.nonce_ = nonce;
maybe_out.ancestor_chain_bit_ = ancestor_chain_bit;
maybe_out.ancestor_chain_bit_if_third_party_enabled_ =
ancestor_chain_bit_if_third_party_enabled;
if (maybe_out.IsValid()) {
out = maybe_out;
return true;
}
return false;
}
// static
bool StorageKey::IsThirdPartyStoragePartitioningEnabled() {
return base::FeatureList::IsEnabled(
net::features::kThirdPartyStoragePartitioning);
}
// static
StorageKey StorageKey::CreateFirstParty(const url::Origin& origin) {
return StorageKey(origin, net::SchemefulSite(origin), nullptr,
origin.opaque() ? blink::mojom::AncestorChainBit::kCrossSite
: blink::mojom::AncestorChainBit::kSameSite,
/*third_party_partitioning_allowed=*/false);
}
// static
StorageKey StorageKey::CreateWithNonce(const url::Origin& origin,
const base::UnguessableToken& nonce) {
// The AncestorChainBit is not applicable to StorageKeys with a non-empty
// nonce, so they are initialized to be kCrossSite.
// Note: The partitioning allowed value is irrelevant with a nonce, `false`
// was chosen arbitrarily.
return StorageKey(origin, net::SchemefulSite(origin), &nonce,
blink::mojom::AncestorChainBit::kCrossSite,
/*third_party_partitioning_allowed=*/false);
}
// static
StorageKey StorageKey::Create(const url::Origin& origin,
const net::SchemefulSite& top_level_site,
blink::mojom::AncestorChainBit ancestor_chain_bit,
bool third_party_partitioning_allowed) {
return StorageKey(origin, top_level_site, nullptr, ancestor_chain_bit,
third_party_partitioning_allowed);
}
// static
StorageKey StorageKey::CreateFromOriginAndIsolationInfo(
const url::Origin& origin,
const net::IsolationInfo& isolation_info) {
// Support for creating a StorageKey from IsolationInfos with special
// NetworkIsolationPartition is not implemented.
CHECK_EQ(isolation_info.GetNetworkIsolationPartition(),
net::NetworkIsolationPartition::kGeneral);
if (isolation_info.nonce()) {
// If the nonce is set we can use the simpler construction path.
return CreateWithNonce(origin, *isolation_info.nonce());
}
blink::mojom::AncestorChainBit ancestor_chain_bit =
blink::mojom::AncestorChainBit::kCrossSite;
net::SchemefulSite top_level_site =
net::SchemefulSite(isolation_info.top_frame_origin().value());
// If the origin or top_level_site is opaque the ancestor chain bit will be
// CrossSite. Otherwise if the top level site matches the new origin and the
// site for cookies isn't empty it must be SameSite.
if (!origin.opaque() && !top_level_site.opaque() &&
top_level_site.IsSameSiteWith(origin) &&
!isolation_info.site_for_cookies().IsNull()) {
ancestor_chain_bit = blink::mojom::AncestorChainBit::kSameSite;
}
return Create(origin, top_level_site, ancestor_chain_bit,
IsThirdPartyStoragePartitioningEnabled());
}
StorageKey StorageKey::WithOrigin(const url::Origin& origin) const {
net::SchemefulSite top_level_site = top_level_site_;
net::SchemefulSite top_level_site_if_third_party_enabled =
top_level_site_if_third_party_enabled_;
blink::mojom::AncestorChainBit ancestor_chain_bit = ancestor_chain_bit_;
blink::mojom::AncestorChainBit ancestor_chain_bit_if_third_party_enabled =
ancestor_chain_bit_if_third_party_enabled_;
if (nonce_) {
// If the nonce is set we have to update the top level site to match origin
// as that's an invariant.
top_level_site = net::SchemefulSite(origin);
top_level_site_if_third_party_enabled = top_level_site;
} else if (!top_level_site_.opaque()) {
// If `top_level_site_` is opaque then so is
// `top_level_site_if_third_party_enabled` and we don't need to explicitly
// check it. The ancestor chain bit also doesn't need to be changed in this
// case.
// Only adjust the ancestor chain bit if it's currently kSameSite but the
// new origin and top level site don't match. Note that the ACB might not
// necessarily be kSameSite if the TLS and origin do match, so we won't
// adjust the other way.
if (ancestor_chain_bit == blink::mojom::AncestorChainBit::kSameSite &&
!top_level_site_.IsSameSiteWith(origin)) {
ancestor_chain_bit = blink::mojom::AncestorChainBit::kCrossSite;
}
if (ancestor_chain_bit_if_third_party_enabled ==
blink::mojom::AncestorChainBit::kSameSite &&
!top_level_site_if_third_party_enabled.IsSameSiteWith(origin)) {
ancestor_chain_bit_if_third_party_enabled =
blink::mojom::AncestorChainBit::kCrossSite;
}
}
StorageKey out = *this;
out.origin_ = origin;
out.top_level_site_ = top_level_site;
out.top_level_site_if_third_party_enabled_ =
top_level_site_if_third_party_enabled;
out.ancestor_chain_bit_ = ancestor_chain_bit;
out.ancestor_chain_bit_if_third_party_enabled_ =
ancestor_chain_bit_if_third_party_enabled;
DCHECK(out.IsValid());
return out;
}
StorageKey::StorageKey(const url::Origin& origin,
const net::SchemefulSite& top_level_site,
const base::UnguessableToken* nonce,
blink::mojom::AncestorChainBit ancestor_chain_bit,
bool third_party_partitioning_allowed)
: origin_(origin),
top_level_site_(third_party_partitioning_allowed
? top_level_site
: net::SchemefulSite(origin)),
top_level_site_if_third_party_enabled_(top_level_site),
nonce_(base::OptionalFromPtr(nonce)),
ancestor_chain_bit_(third_party_partitioning_allowed ? ancestor_chain_bit
: (nonce || origin.opaque())
? blink::mojom::AncestorChainBit::kCrossSite
: blink::mojom::AncestorChainBit::kSameSite),
ancestor_chain_bit_if_third_party_enabled_(ancestor_chain_bit) {
DCHECK(IsValid());
}
std::string StorageKey::Serialize() const {
DCHECK(!origin_.opaque());
// If the storage key has a nonce, implying the top_level_site is the same as
// origin and ancestor_chain_bit is kCrossSite, then we need to serialize the
// key to fit the following scheme:
//
// Case 0: <StorageKey `key`.origin> + "/" + "^1" + <StorageKey
// `key`.nonce.High64Bits> + "^2" + <StorageKey `key`.nonce.Low64Bits>
//
// Note that we intentionally do not include the AncestorChainBit in
// serialization with nonce formats as that information is not applicable
// (similar to top-level-site).
if (nonce_.has_value()) {
return origin_.GetURL().spec() +
SerializeAttributeSeparator(EncodedAttribute::kNonceHigh) +
base::NumberToString(nonce_->GetHighForSerialization()) +
SerializeAttributeSeparator(EncodedAttribute::kNonceLow) +
base::NumberToString(nonce_->GetLowForSerialization());
}
// Else if storage partitioning is enabled we need to serialize the key to fit
// one of the following schemes:
//
// Case 1: If the ancestor_chain_bit is kSameSite or partitioning is disabled:
//
// <StorageKey `key`.origin> + "/"
//
// Case 2: If the origin matches top_level_site and the ancestor_chain_bit is
// kCrossSite:
//
// <StorageKey `key`.origin> + "/" + "^3" + <StorageKey
// `key`.ancestor_chain_bit>
//
// Case 3: If the origin doesn't match top_level_site (implying
// ancestor_chain_bit is kCrossSite):
//
// <StorageKey `key`.origin> + "/" + "^0" + <StorageKey `key`.top_level_site>
//
// Case 4: If the top_level_site is opaque (implying ancestor_chain_bit is
// kCrossSite):
//
// <StorageKey `key`.origin> + "/" + ^4" + <StorageKey
// `key`.top_level_site.nonce.High64Bits> + "^5" + <StorageKey
// `key`.top_level_site.nonce.Low64Bits> + "^6" + <StorageKey
// `key`.top_level_site.precursor>
if (IsThirdPartyStoragePartitioningEnabled() &&
ancestor_chain_bit_ == blink::mojom::AncestorChainBit::kCrossSite) {
if (top_level_site_.opaque()) {
// Case 4.
return base::StrCat({
origin_.GetURL().spec(),
SerializeAttributeSeparator(
EncodedAttribute::kTopLevelSiteOpaqueNonceHigh),
base::NumberToString(top_level_site_.internal_value()
.GetNonceForSerialization()
->GetHighForSerialization()),
SerializeAttributeSeparator(
EncodedAttribute::kTopLevelSiteOpaqueNonceLow),
base::NumberToString(top_level_site_.internal_value()
.GetNonceForSerialization()
->GetLowForSerialization()),
SerializeAttributeSeparator(
EncodedAttribute::kTopLevelSiteOpaquePrecursor),
top_level_site_.internal_value()
.GetTupleOrPrecursorTupleIfOpaque()
.Serialize(),
});
} else if (top_level_site_.IsSameSiteWith(origin_)) {
// Case 2.
return base::StrCat({
origin_.GetURL().spec(),
SerializeAttributeSeparator(EncodedAttribute::kAncestorChainBit),
base::NumberToString(static_cast<int>(ancestor_chain_bit_)),
});
} else {
// Case 3.
return base::StrCat({
origin_.GetURL().spec(),
SerializeAttributeSeparator(EncodedAttribute::kTopLevelSite),
top_level_site_.Serialize(),
});
}
}
// Case 1.
return origin_.GetURL().spec();
}
std::string StorageKey::SerializeForLocalStorage() const {
DCHECK(!origin_.opaque());
// If this is a third-party StorageKey we'll use the standard serialization
// scheme when partitioning is enabled or if there is a nonce.
if (IsThirdPartyContext()) {
return Serialize();
}
// Otherwise localStorage expects a slightly different scheme, so call that.
return origin_.Serialize();
}
std::string StorageKey::GetDebugString() const {
return base::StrCat(
{"{ origin: ", origin_.GetDebugString(),
", top-level site: ", top_level_site_.Serialize(),
", nonce: ", nonce_.has_value() ? nonce_->ToString() : "<null>",
", ancestor chain bit: ",
ancestor_chain_bit_ == blink::mojom::AncestorChainBit::kSameSite
? "Same-Site"
: "Cross-Site",
" }"});
}
std::string StorageKey::GetMemoryDumpString(size_t max_length) const {
std::string memory_dump_str = origin_.Serialize().substr(0, max_length);
if (max_length > memory_dump_str.length()) {
memory_dump_str.append(top_level_site_.Serialize().substr(
0, max_length - memory_dump_str.length()));
}
if (nonce_.has_value() && max_length > memory_dump_str.length()) {
memory_dump_str.append(
nonce_->ToString().substr(0, max_length - memory_dump_str.length()));
}
if (max_length > memory_dump_str.length()) {
std::string ancestor_full_string =
ancestor_chain_bit_ == blink::mojom::AncestorChainBit::kSameSite
? "Same-Site"
: "Cross-Site";
memory_dump_str.append(
ancestor_full_string.substr(0, max_length - memory_dump_str.length()));
}
std::ranges::replace_if(
memory_dump_str.begin(), memory_dump_str.end(),
[](char c) {
return !absl::ascii_isalnum(static_cast<unsigned char>(c));
},
'_');
return memory_dump_str;
}
const net::SiteForCookies StorageKey::ToNetSiteForCookies() const {
if (IsThirdPartyContext()) {
// If any of the ancestor frames are cross-site to `origin_` then the
// SiteForCookies should be null. The existence of `nonce_` means the same
// thing.
return net::SiteForCookies();
}
// Otherwise we are in a first party context.
return net::SiteForCookies(top_level_site_);
}
const net::IsolationInfo StorageKey::ToPartialNetIsolationInfo() const {
url::Origin top_frame_origin =
IsFirstPartyContext() ? origin_
: url::Origin::Create(top_level_site_.GetURL());
return net::IsolationInfo::Create(net::IsolationInfo::RequestType::kOther,
top_frame_origin, origin_,
ToNetSiteForCookies(), nonce_);
}
// static
bool StorageKey::ShouldSkipKeyDueToPartitioning(
const std::string& reg_key_string) {
// Don't skip anything if storage partitioning is enabled.
if (IsThirdPartyStoragePartitioningEnabled())
return false;
// Determine if there is a valid attribute encoded with a caret
size_t pos_first_caret = reg_key_string.find_first_of('^');
if (pos_first_caret != std::string::npos &&
ValidSeparatorWithData(reg_key_string, pos_first_caret)) {
std::optional<EncodedAttribute> attribute = DeserializeAttributeSeparator(
reg_key_string.substr(pos_first_caret, 2));
// Do skip if partitioning is disabled and we detect a top-level site
// serialization scheme (opaque or otherwise) or an ancestor chain bit:
if (attribute.has_value() &&
(attribute == EncodedAttribute::kTopLevelSite ||
attribute == EncodedAttribute::kAncestorChainBit ||
attribute == EncodedAttribute::kTopLevelSiteOpaqueNonceHigh)) {
return true;
}
}
// If otherwise first-party, nonce, or corrupted, don't skip.
return false;
}
const std::optional<net::CookiePartitionKey> StorageKey::ToCookiePartitionKey()
const {
return net::CookiePartitionKey::FromStorageKeyComponents(
top_level_site_,
net::CookiePartitionKey::BoolToAncestorChainBit(IsThirdPartyContext()),
nonce_);
}
bool StorageKey::MatchesOriginForTrustedStorageDeletion(
const url::Origin& origin) const {
// TODO(crbug.com/1382138): Address wss:// and https:// resulting in different
// SchemefulSites.
// TODO(crbug.com/1410196): Test that StorageKeys corresponding to anonymous
// iframes are handled appropriately here.
return IsFirstPartyContext() ? (origin_ == origin)
: (top_level_site_.IsSameSiteWith(origin));
}
bool StorageKey::MatchesRegistrableDomainForTrustedStorageDeletion(
std::string_view domain) const {
// TODO(crbug.com/1410196): Test that StorageKeys corresponding to anonymous
// iframes are handled appropriately here.
return top_level_site_.registrable_domain_or_host() == domain;
}
bool StorageKey::ExactMatchForTesting(const StorageKey& other) const {
return *this == other &&
this->ancestor_chain_bit_if_third_party_enabled_ ==
other.ancestor_chain_bit_if_third_party_enabled_ &&
this->top_level_site_if_third_party_enabled_ ==
other.top_level_site_if_third_party_enabled_;
}
std::ostream& operator<<(std::ostream& ostream, const StorageKey& sk) {
return ostream << sk.GetDebugString();
}
bool StorageKey::IsValid() const {
// If the key's origin is opaque ancestor_chain_bit* is always kCrossSite
// no matter the value of the other members.
if (origin_.opaque()) {
if (ancestor_chain_bit_ != blink::mojom::AncestorChainBit::kCrossSite) {
return false;
}
if (ancestor_chain_bit_if_third_party_enabled_ !=
blink::mojom::AncestorChainBit::kCrossSite) {
return false;
}
}
// If this key's "normal" members indicate a 3p key, then the
// *_if_third_party_enabled counterparts must match them.
if (!origin_.opaque() &&
(!top_level_site_.IsSameSiteWith(origin_) ||
ancestor_chain_bit_ != blink::mojom::AncestorChainBit::kSameSite)) {
if (top_level_site_ != top_level_site_if_third_party_enabled_) {
return false;
}
if (ancestor_chain_bit_ != ancestor_chain_bit_if_third_party_enabled_) {
return false;
}
}
// If top_level_site* is cross-site to origin, then ancestor_chain_bit* must
// indicate that. An opaque top_level_site* must have a cross-site
// ancestor_chain_bit*.
if (!top_level_site_.IsSameSiteWith(origin_)) {
if (ancestor_chain_bit_ != blink::mojom::AncestorChainBit::kCrossSite) {
return false;
}
}
if (!top_level_site_if_third_party_enabled_.IsSameSiteWith(origin_)) {
if (ancestor_chain_bit_if_third_party_enabled_ !=
blink::mojom::AncestorChainBit::kCrossSite) {
return false;
}
}
// If there is a nonce, all other values must indicate same-site to origin.
if (nonce_) {
if (nonce_->is_empty()) {
return false;
}
if (!top_level_site_.IsSameSiteWith(origin_)) {
return false;
}
if (!top_level_site_if_third_party_enabled_.IsSameSiteWith(origin_)) {
return false;
}
if (ancestor_chain_bit_ != blink::mojom::AncestorChainBit::kCrossSite) {
return false;
}
if (ancestor_chain_bit_if_third_party_enabled_ !=
blink::mojom::AncestorChainBit::kCrossSite) {
return false;
}
}
// If the state is not invalid, it must be valid!
return true;
}
} // namespace blink