| /* |
| * 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 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) |
| : data { data } |
| , priority { priority.value } |
| , wasSent { wasSent } |
| { |
| } |
| |
| bool isValid() const |
| { |
| return data <= MaxEntropy && priority <= Priority::MaxEntropy; |
| } |
| |
| uint8_t data { 0 }; |
| PriorityValue priority; |
| WasSent wasSent = WasSent::No; |
| std::optional<RegistrableDomain> sourceRegistrableDomain; |
| |
| 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 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 URL tokenPublicKeyURL() const; |
| WEBCORE_EXPORT URL tokenSignatureURL() const; |
| |
| WEBCORE_EXPORT Ref<JSON::Object> tokenSignatureJSON() const; |
| |
| struct EphemeralSourceNonce { |
| String nonce; |
| |
| EphemeralSourceNonce isolatedCopy() const; |
| |
| WEBCORE_EXPORT bool isValid() const; |
| |
| template<class Encoder> void encode(Encoder&) const; |
| template<class Decoder> static std::optional<EphemeralSourceNonce> decode(Decoder&); |
| }; |
| |
| WEBCORE_EXPORT void setEphemeralSourceNonce(EphemeralSourceNonce&&); |
| std::optional<EphemeralSourceNonce> ephemeralSourceNonce() const { return m_ephemeralSourceNonce; }; |
| void clearEphemeralSourceNonce() { m_ephemeralSourceNonce.reset(); }; |
| |
| struct SourceSecretToken { |
| String tokenBase64URL; |
| String signatureBase64URL; |
| String keyIDBase64URL; |
| |
| SourceSecretToken isolatedCopy() const; |
| bool isValid() const; |
| }; |
| |
| #if PLATFORM(COCOA) |
| WEBCORE_EXPORT std::optional<String> calculateAndUpdateSourceUnlinkableToken(const String& serverPublicKeyBase64URL); |
| WEBCORE_EXPORT std::optional<String> calculateAndUpdateSourceSecretToken(const String& serverResponseBase64URL); |
| #endif |
| |
| void setSourceUnlinkableTokenValue(const String& value) { m_sourceUnlinkableToken.valueBase64URL = value; } |
| const std::optional<SourceSecretToken>& sourceSecretToken() const { return m_sourceSecretToken; } |
| WEBCORE_EXPORT void setSourceSecretToken(SourceSecretToken&&); |
| |
| 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; |
| |
| SourceID m_sourceID; |
| SourceSite m_sourceSite; |
| AttributionDestinationSite m_destinationSite; |
| WallTime m_timeOfAdClick; |
| AttributionEphemeral m_isEphemeral; |
| |
| std::optional<AttributionTriggerData> m_attributionTriggerData; |
| AttributionTimeToSendData m_timesToSend; |
| |
| struct SourceUnlinkableToken { |
| #if PLATFORM(COCOA) |
| RetainPtr<RSABSSATokenBlinder> blinder; |
| RetainPtr<RSABSSATokenWaitingActivation> waitingToken; |
| RetainPtr<RSABSSATokenReady> readyToken; |
| #endif |
| String valueBase64URL; |
| |
| SourceUnlinkableToken isolatedCopy() const; |
| }; |
| |
| std::optional<EphemeralSourceNonce> 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<EphemeralSourceNonce>> 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::EphemeralSourceNonce::encode(Encoder& encoder) const |
| { |
| encoder << nonce; |
| } |
| |
| template<class Decoder> |
| std::optional<PrivateClickMeasurement::EphemeralSourceNonce> PrivateClickMeasurement::EphemeralSourceNonce::decode(Decoder& decoder) |
| { |
| std::optional<String> nonce; |
| decoder >> nonce; |
| if (!nonce) |
| return std::nullopt; |
| |
| return EphemeralSourceNonce { WTFMove(*nonce) }; |
| } |
| |
| template<class Encoder> |
| void PrivateClickMeasurement::AttributionTriggerData::encode(Encoder& encoder) const |
| { |
| encoder << data << priority << wasSent << sourceRegistrableDomain; |
| } |
| |
| 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; |
| |
| AttributionTriggerData attributionTriggerData { WTFMove(*data), Priority { *priority }, *wasSent }; |
| attributionTriggerData.sourceRegistrableDomain = WTFMove(*sourceRegistrableDomain); |
| 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(); } |
| }; |
| } |