blob: 01f87348ab6a21a02e6036b258e8927260e3e718 [file] [log] [blame]
/*
* Copyright (C) 2019 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "RegistrableDomain.h"
#include <wtf/CompletionHandler.h>
#include <wtf/Forward.h>
#include <wtf/JSONValues.h>
#include <wtf/URL.h>
#include <wtf/WallTime.h>
#include <wtf/text/Base64.h>
#include <wtf/text/StringHash.h>
#include <wtf/text/WTFString.h>
#if PLATFORM(COCOA)
#include <wtf/RetainPtr.h>
#endif
#if PLATFORM(COCOA)
OBJC_CLASS RSABSSATokenReady;
OBJC_CLASS RSABSSATokenWaitingActivation;
OBJC_CLASS RSABSSATokenBlinder;
#endif
namespace WebCore {
class PrivateClickMeasurement {
public:
using PriorityValue = uint8_t;
enum class AttributionEphemeral : bool { No, Yes };
enum class PcmDataCarried : bool { NonPersonallyIdentifiable, PersonallyIdentifiable };
enum class AttributionReportEndpoint : bool { Source, Destination };
enum class IsRunningLayoutTest : bool { No, Yes };
struct SourceID {
static constexpr uint8_t MaxEntropy = 255;
explicit SourceID(uint8_t id)
: id { id }
{
}
uint8_t id { 0 };
};
struct SourceSite {
explicit SourceSite(const URL& url)
: registrableDomain { url }
{
}
explicit SourceSite(RegistrableDomain&& domain)
: registrableDomain { WTFMove(domain) }
{
}
SourceSite isolatedCopy() const { return SourceSite { registrableDomain.isolatedCopy() }; }
bool operator==(const SourceSite& other) const
{
return registrableDomain == other.registrableDomain;
}
bool operator!=(const SourceSite& other) const
{
return registrableDomain != other.registrableDomain;
}
bool matches(const URL& url) const
{
return registrableDomain.matches(url);
}
RegistrableDomain registrableDomain;
};
struct SourceSiteHash {
static unsigned hash(const SourceSite& sourceSite)
{
return sourceSite.registrableDomain.hash();
}
static bool equal(const SourceSite& a, const SourceSite& b)
{
return a == b;
}
static const bool safeToCompareToEmptyOrDeleted = false;
};
struct AttributionDestinationSite {
AttributionDestinationSite() = default;
explicit AttributionDestinationSite(const URL& url)
: registrableDomain { RegistrableDomain { url } }
{
}
explicit AttributionDestinationSite(RegistrableDomain&& domain)
: registrableDomain { WTFMove(domain) }
{
}
AttributionDestinationSite isolatedCopy() const { return AttributionDestinationSite { registrableDomain.isolatedCopy() }; }
bool operator==(const AttributionDestinationSite& other) const
{
return registrableDomain == other.registrableDomain;
}
bool operator!=(const AttributionDestinationSite& other) const
{
return registrableDomain != other.registrableDomain;
}
bool matches(const URL& url) const
{
return registrableDomain == RegistrableDomain { url };
}
RegistrableDomain registrableDomain;
};
struct AttributionDestinationSiteHash {
static unsigned hash(const AttributionDestinationSite& destinationSite)
{
return destinationSite.registrableDomain.hash();
}
static bool equal(const AttributionDestinationSite& a, const AttributionDestinationSite& b)
{
return a == b;
}
static const bool safeToCompareToEmptyOrDeleted = false;
};
struct Priority {
static constexpr uint8_t MaxEntropy = 63;
explicit Priority(PriorityValue value)
: value { value }
{
}
PriorityValue value;
};
struct EphemeralNonce {
String nonce;
EphemeralNonce isolatedCopy() const;
WEBCORE_EXPORT bool isValid() const;
template<class Encoder> void encode(Encoder&) const;
template<class Decoder> static std::optional<EphemeralNonce> decode(Decoder&);
};
struct UnlinkableToken {
#if PLATFORM(COCOA)
RetainPtr<RSABSSATokenBlinder> blinder;
RetainPtr<RSABSSATokenWaitingActivation> waitingToken;
RetainPtr<RSABSSATokenReady> readyToken;
#endif
String valueBase64URL;
UnlinkableToken isolatedCopy() const;
};
struct SourceUnlinkableToken : UnlinkableToken {
SourceUnlinkableToken isolatedCopy() const;
};
struct DestinationUnlinkableToken : UnlinkableToken {
DestinationUnlinkableToken isolatedCopy() const;
};
struct SecretToken {
String tokenBase64URL;
String signatureBase64URL;
String keyIDBase64URL;
SecretToken isolatedCopy() const;
bool isValid() const;
};
struct SourceSecretToken : SecretToken {
SourceSecretToken isolatedCopy() const;
};
struct DestinationSecretToken : SecretToken {
WEBCORE_EXPORT DestinationSecretToken isolatedCopy() const;
};
struct AttributionTriggerData {
static constexpr uint8_t MaxEntropy = 15;
enum class WasSent : bool { No, Yes };
AttributionTriggerData() = default;
AttributionTriggerData(uint8_t data, Priority priority, WasSent wasSent = WasSent::No, std::optional<EphemeralNonce> nonce = std::nullopt)
: data { data }
, priority { priority.value }
, wasSent { wasSent }
, ephemeralDestinationNonce { nonce }
{
}
bool isValid() const
{
return data <= MaxEntropy && priority <= Priority::MaxEntropy;
}
void setDestinationUnlinkableTokenValue(const String& value)
{
if (!destinationUnlinkableToken)
destinationUnlinkableToken = DestinationUnlinkableToken { };
destinationUnlinkableToken->valueBase64URL = value;
}
void setDestinationSecretToken(const DestinationSecretToken& token) { destinationSecretToken = token; }
const std::optional<const URL> tokenPublicKeyURL() const { return destinationSite ? PrivateClickMeasurement::tokenPublicKeyURL(*destinationSite) : URL(); }
const std::optional<const URL> tokenSignatureURL() const { return destinationSite ? PrivateClickMeasurement::tokenSignatureURL(*destinationSite) : URL(); }
WEBCORE_EXPORT Ref<JSON::Object> tokenSignatureJSON() const;
uint8_t data { 0 };
PriorityValue priority;
WasSent wasSent = WasSent::No;
std::optional<RegistrableDomain> sourceRegistrableDomain;
std::optional<EphemeralNonce> ephemeralDestinationNonce;
std::optional<DestinationUnlinkableToken> destinationUnlinkableToken;
std::optional<DestinationSecretToken> destinationSecretToken;
std::optional<RegistrableDomain> destinationSite;
template<class Encoder> void encode(Encoder&) const;
template<class Decoder> static std::optional<AttributionTriggerData> decode(Decoder&);
};
struct AttributionSecondsUntilSendData {
std::optional<Seconds> sourceSeconds;
std::optional<Seconds> destinationSeconds;
bool hasValidSecondsUntilSendValues()
{
return sourceSeconds && destinationSeconds;
}
std::optional<Seconds> minSecondsUntilSend()
{
if (!sourceSeconds && !destinationSeconds)
return std::nullopt;
if (sourceSeconds && destinationSeconds)
return std::min(sourceSeconds, destinationSeconds);
return sourceSeconds ? sourceSeconds : destinationSeconds;
}
template<class Encoder>
void encode(Encoder& encoder) const
{
encoder << sourceSeconds << destinationSeconds;
}
template<class Decoder>
static std::optional<AttributionSecondsUntilSendData> decode(Decoder& decoder)
{
std::optional<std::optional<Seconds>> sourceSeconds;
decoder >> sourceSeconds;
if (!sourceSeconds)
return std::nullopt;
std::optional<std::optional<Seconds>> destinationSeconds;
decoder >> destinationSeconds;
if (!destinationSeconds)
return std::nullopt;
return AttributionSecondsUntilSendData { WTFMove(*sourceSeconds), WTFMove(*destinationSeconds) };
}
};
struct AttributionTimeToSendData {
std::optional<WallTime> sourceEarliestTimeToSend;
std::optional<WallTime> destinationEarliestTimeToSend;
std::optional<WallTime> earliestTimeToSend()
{
if (!sourceEarliestTimeToSend && !destinationEarliestTimeToSend)
return std::nullopt;
if (sourceEarliestTimeToSend && destinationEarliestTimeToSend)
return std::min(sourceEarliestTimeToSend, destinationEarliestTimeToSend);
return sourceEarliestTimeToSend ? sourceEarliestTimeToSend : destinationEarliestTimeToSend;
}
std::optional<WallTime> latestTimeToSend()
{
if (!sourceEarliestTimeToSend && !destinationEarliestTimeToSend)
return std::nullopt;
if (sourceEarliestTimeToSend && destinationEarliestTimeToSend)
return std::max(sourceEarliestTimeToSend, destinationEarliestTimeToSend);
return sourceEarliestTimeToSend ? sourceEarliestTimeToSend : destinationEarliestTimeToSend;
}
std::optional<AttributionReportEndpoint> attributionReportEndpoint()
{
if (sourceEarliestTimeToSend && destinationEarliestTimeToSend) {
if (*sourceEarliestTimeToSend < *destinationEarliestTimeToSend)
return AttributionReportEndpoint::Source;
return AttributionReportEndpoint::Destination;
}
if (sourceEarliestTimeToSend)
return AttributionReportEndpoint::Source;
if (destinationEarliestTimeToSend)
return AttributionReportEndpoint::Destination;
return std::nullopt;
}
template<class Encoder>
void encode(Encoder& encoder) const
{
encoder << sourceEarliestTimeToSend << destinationEarliestTimeToSend;
}
template<class Decoder>
static std::optional<AttributionTimeToSendData> decode(Decoder& decoder)
{
std::optional<std::optional<WallTime>> sourceEarliestTimeToSend;
decoder >> sourceEarliestTimeToSend;
if (!sourceEarliestTimeToSend)
return std::nullopt;
std::optional<std::optional<WallTime>> destinationEarliestTimeToSend;
decoder >> destinationEarliestTimeToSend;
if (!destinationEarliestTimeToSend)
return std::nullopt;
return AttributionTimeToSendData { WTFMove(*sourceEarliestTimeToSend), WTFMove(*destinationEarliestTimeToSend) };
}
};
PrivateClickMeasurement(SourceID sourceID, const SourceSite& sourceSite, const AttributionDestinationSite& destinationSite, const String& sourceApplicationBundleID, WallTime timeOfAdClick, AttributionEphemeral isEphemeral)
: m_sourceID { sourceID }
, m_sourceSite { sourceSite }
, m_destinationSite { destinationSite }
, m_timeOfAdClick { timeOfAdClick }
, m_isEphemeral { isEphemeral }
, m_sourceApplicationBundleID { sourceApplicationBundleID }
{
}
WEBCORE_EXPORT static const Seconds maxAge();
WEBCORE_EXPORT bool isNeitherSameSiteNorCrossSiteTriggeringEvent(const RegistrableDomain& redirectDomain, const URL& firstPartyURL, const AttributionTriggerData&);
WEBCORE_EXPORT static Expected<AttributionTriggerData, String> parseAttributionRequest(const URL& redirectURL);
WEBCORE_EXPORT AttributionSecondsUntilSendData attributeAndGetEarliestTimeToSend(AttributionTriggerData&&, IsRunningLayoutTest);
WEBCORE_EXPORT bool hasHigherPriorityThan(const PrivateClickMeasurement&) const;
WEBCORE_EXPORT URL attributionReportClickSourceURL() const;
WEBCORE_EXPORT URL attributionReportClickDestinationURL() const;
WEBCORE_EXPORT Ref<JSON::Object> attributionReportJSON() const;
const SourceSite& sourceSite() const { return m_sourceSite; };
const AttributionDestinationSite& destinationSite() const { return m_destinationSite; };
WallTime timeOfAdClick() const { return m_timeOfAdClick; }
WEBCORE_EXPORT bool hasPreviouslyBeenReported();
AttributionTimeToSendData timesToSend() const { return m_timesToSend; };
void setTimesToSend(AttributionTimeToSendData data) { m_timesToSend = data; }
const SourceID& sourceID() const { return m_sourceID; }
const std::optional<AttributionTriggerData>& attributionTriggerData() const { return m_attributionTriggerData; }
void setAttribution(AttributionTriggerData&& attributionTriggerData) { m_attributionTriggerData = WTFMove(attributionTriggerData); }
const String& sourceApplicationBundleID() const { return m_sourceApplicationBundleID; }
WEBCORE_EXPORT void setSourceApplicationBundleIDForTesting(const String&);
bool isEphemeral() const { return m_isEphemeral == AttributionEphemeral::Yes; }
void setEphemeral(AttributionEphemeral isEphemeral) { m_isEphemeral = isEphemeral; }
// MARK: - Fraud Prevention
WEBCORE_EXPORT const std::optional<const URL> tokenPublicKeyURL() const;
WEBCORE_EXPORT static const std::optional<const URL> tokenPublicKeyURL(const RegistrableDomain&);
WEBCORE_EXPORT const std::optional<const URL> tokenSignatureURL() const;
WEBCORE_EXPORT static const std::optional<const URL> tokenSignatureURL(const RegistrableDomain&);
WEBCORE_EXPORT Ref<JSON::Object> tokenSignatureJSON() const;
WEBCORE_EXPORT void setEphemeralSourceNonce(EphemeralNonce&&);
std::optional<EphemeralNonce> ephemeralSourceNonce() const { return m_ephemeralSourceNonce; };
void clearEphemeralSourceNonce() { m_ephemeralSourceNonce.reset(); };
#if PLATFORM(COCOA)
WEBCORE_EXPORT std::optional<String> calculateAndUpdateSourceUnlinkableToken(const String& serverPublicKeyBase64URL);
WEBCORE_EXPORT static Expected<DestinationUnlinkableToken, String> calculateAndUpdateDestinationUnlinkableToken(const String& serverPublicKeyBase64URL);
WEBCORE_EXPORT std::optional<String> calculateAndUpdateSourceSecretToken(const String& serverResponseBase64URL);
WEBCORE_EXPORT static Expected<DestinationSecretToken, String> calculateAndUpdateDestinationSecretToken(const String& serverResponseBase64URL, DestinationUnlinkableToken&);
#endif
SourceUnlinkableToken& sourceUnlinkableToken() { return m_sourceUnlinkableToken; }
void setSourceUnlinkableTokenValue(const String& value) { m_sourceUnlinkableToken.valueBase64URL = value; }
const std::optional<SourceSecretToken>& sourceSecretToken() const { return m_sourceSecretToken; }
WEBCORE_EXPORT void setSourceSecretToken(SourceSecretToken&&);
WEBCORE_EXPORT void setDestinationSecretToken(DestinationSecretToken&&);
template<class Encoder> void encode(Encoder&) const;
template<class Decoder> static std::optional<PrivateClickMeasurement> decode(Decoder&);
WEBCORE_EXPORT PrivateClickMeasurement isolatedCopy() const;
private:
static Expected<AttributionTriggerData, String> parseAttributionRequestQuery(const URL&);
bool isValid() const;
#if PLATFORM(COCOA)
static std::optional<String> calculateAndUpdateUnlinkableToken(const String& serverPublicKeyBase64URL, UnlinkableToken&, const String& contextForLogMessage);
static std::optional<String> calculateAndUpdateSecretToken(const String& serverResponseBase64URL, UnlinkableToken&, SecretToken&, const String& contextForLogMessage);
#endif
SourceID m_sourceID;
SourceSite m_sourceSite;
AttributionDestinationSite m_destinationSite;
WallTime m_timeOfAdClick;
AttributionEphemeral m_isEphemeral;
std::optional<AttributionTriggerData> m_attributionTriggerData;
AttributionTimeToSendData m_timesToSend;
std::optional<EphemeralNonce> m_ephemeralSourceNonce;
SourceUnlinkableToken m_sourceUnlinkableToken;
std::optional<SourceSecretToken> m_sourceSecretToken;
String m_sourceApplicationBundleID;
};
template<class Encoder>
void PrivateClickMeasurement::encode(Encoder& encoder) const
{
encoder << m_sourceID.id
<< m_sourceSite.registrableDomain
<< m_destinationSite.registrableDomain
<< m_timeOfAdClick
<< m_ephemeralSourceNonce
<< m_isEphemeral
<< m_attributionTriggerData
<< m_sourceApplicationBundleID
<< m_timesToSend;
}
template<class Decoder>
std::optional<PrivateClickMeasurement> PrivateClickMeasurement::decode(Decoder& decoder)
{
std::optional<uint8_t> sourceID;
decoder >> sourceID;
if (!sourceID)
return std::nullopt;
std::optional<RegistrableDomain> sourceRegistrableDomain;
decoder >> sourceRegistrableDomain;
if (!sourceRegistrableDomain)
return std::nullopt;
std::optional<RegistrableDomain> destinationRegistrableDomain;
decoder >> destinationRegistrableDomain;
if (!destinationRegistrableDomain)
return std::nullopt;
std::optional<WallTime> timeOfAdClick;
decoder >> timeOfAdClick;
if (!timeOfAdClick)
return std::nullopt;
std::optional<std::optional<EphemeralNonce>> ephemeralSourceNonce;
decoder >> ephemeralSourceNonce;
if (!ephemeralSourceNonce)
return std::nullopt;
std::optional<AttributionEphemeral> isEphemeral;
decoder >> isEphemeral;
if (!isEphemeral)
return std::nullopt;
std::optional<std::optional<AttributionTriggerData>> attributionTriggerData;
decoder >> attributionTriggerData;
if (!attributionTriggerData)
return std::nullopt;
std::optional<String> sourceApplicationBundleID;
decoder >> sourceApplicationBundleID;
if (!sourceApplicationBundleID)
return std::nullopt;
std::optional<AttributionTimeToSendData> timesToSend;
decoder >> timesToSend;
if (!timesToSend)
return std::nullopt;
PrivateClickMeasurement attribution {
SourceID { WTFMove(*sourceID) },
SourceSite { WTFMove(*sourceRegistrableDomain) },
AttributionDestinationSite { WTFMove(*destinationRegistrableDomain) },
WTFMove(*sourceApplicationBundleID),
WTFMove(*timeOfAdClick),
WTFMove(*isEphemeral)
};
attribution.m_ephemeralSourceNonce = WTFMove(*ephemeralSourceNonce);
attribution.m_attributionTriggerData = WTFMove(*attributionTriggerData);
attribution.m_timesToSend = WTFMove(*timesToSend);
return attribution;
}
template<class Encoder>
void PrivateClickMeasurement::EphemeralNonce::encode(Encoder& encoder) const
{
encoder << nonce;
}
template<class Decoder>
std::optional<PrivateClickMeasurement::EphemeralNonce> PrivateClickMeasurement::EphemeralNonce::decode(Decoder& decoder)
{
std::optional<String> nonce;
decoder >> nonce;
if (!nonce)
return std::nullopt;
return EphemeralNonce { WTFMove(*nonce) };
}
template<class Encoder>
void PrivateClickMeasurement::AttributionTriggerData::encode(Encoder& encoder) const
{
encoder << data << priority << wasSent << sourceRegistrableDomain << ephemeralDestinationNonce << destinationSite;
}
template<class Decoder>
std::optional<PrivateClickMeasurement::AttributionTriggerData> PrivateClickMeasurement::AttributionTriggerData::decode(Decoder& decoder)
{
std::optional<uint8_t> data;
decoder >> data;
if (!data)
return std::nullopt;
std::optional<PriorityValue> priority;
decoder >> priority;
if (!priority)
return std::nullopt;
std::optional<WasSent> wasSent;
decoder >> wasSent;
if (!wasSent)
return std::nullopt;
std::optional<std::optional<RegistrableDomain>> sourceRegistrableDomain;
decoder >> sourceRegistrableDomain;
if (!sourceRegistrableDomain)
return std::nullopt;
std::optional<std::optional<EphemeralNonce>> ephemeralDestinationNonce;
decoder >> ephemeralDestinationNonce;
if (!ephemeralDestinationNonce)
return std::nullopt;
std::optional<std::optional<RegistrableDomain>> destinationSite;
decoder >> destinationSite;
if (!destinationSite)
return std::nullopt;
AttributionTriggerData attributionTriggerData { WTFMove(*data), Priority { *priority }, *wasSent };
attributionTriggerData.sourceRegistrableDomain = WTFMove(*sourceRegistrableDomain);
attributionTriggerData.ephemeralDestinationNonce = WTFMove(*ephemeralDestinationNonce);
attributionTriggerData.destinationSite = WTFMove(*destinationSite);
return attributionTriggerData;
}
} // namespace WebCore
namespace WTF {
template<typename T> struct DefaultHash;
template<> struct DefaultHash<WebCore::PrivateClickMeasurement::SourceSite> : WebCore::PrivateClickMeasurement::SourceSiteHash { };
template<> struct HashTraits<WebCore::PrivateClickMeasurement::SourceSite> : GenericHashTraits<WebCore::PrivateClickMeasurement::SourceSite> {
static WebCore::PrivateClickMeasurement::SourceSite emptyValue() { return WebCore::PrivateClickMeasurement::SourceSite(WebCore::RegistrableDomain()); }
static void constructDeletedValue(WebCore::PrivateClickMeasurement::SourceSite& slot) { new (NotNull, &slot.registrableDomain) WebCore::RegistrableDomain(WTF::HashTableDeletedValue); }
static bool isDeletedValue(const WebCore::PrivateClickMeasurement::SourceSite& slot) { return slot.registrableDomain.isHashTableDeletedValue(); }
};
template<> struct DefaultHash<WebCore::PrivateClickMeasurement::AttributionDestinationSite> : WebCore::PrivateClickMeasurement::AttributionDestinationSiteHash { };
template<> struct HashTraits<WebCore::PrivateClickMeasurement::AttributionDestinationSite> : GenericHashTraits<WebCore::PrivateClickMeasurement::AttributionDestinationSite> {
static WebCore::PrivateClickMeasurement::AttributionDestinationSite emptyValue() { return { }; }
static void constructDeletedValue(WebCore::PrivateClickMeasurement::AttributionDestinationSite& slot) { new (NotNull, &slot.registrableDomain) WebCore::RegistrableDomain(WTF::HashTableDeletedValue); }
static bool isDeletedValue(const WebCore::PrivateClickMeasurement::AttributionDestinationSite& slot) { return slot.registrableDomain.isHashTableDeletedValue(); }
};
}