| /* |
| * 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. |
| */ |
| |
| #include "config.h" |
| #include "PrivateClickMeasurementManager.h" |
| |
| #include "Logging.h" |
| #include "NetworkSession.h" |
| #include "PrivateClickMeasurementDebugInfo.h" |
| #include "PrivateClickMeasurementNetworkLoader.h" |
| #include <JavaScriptCore/ConsoleTypes.h> |
| #include <WebCore/FetchOptions.h> |
| #include <WebCore/FormData.h> |
| #include <WebCore/ResourceError.h> |
| #include <WebCore/ResourceRequest.h> |
| #include <WebCore/ResourceResponse.h> |
| #include <WebCore/RuntimeApplicationChecks.h> |
| #include <WebCore/RuntimeEnabledFeatures.h> |
| #include <pal/crypto/CryptoDigest.h> |
| #include <wtf/CryptographicallyRandomNumber.h> |
| #include <wtf/text/StringBuilder.h> |
| #include <wtf/text/StringHash.h> |
| |
| namespace WebKit { |
| using namespace WebCore; |
| |
| using SourceSite = PrivateClickMeasurement::SourceSite; |
| using AttributionDestinationSite = PrivateClickMeasurement::AttributionDestinationSite; |
| using AttributionTriggerData = PrivateClickMeasurement::AttributionTriggerData; |
| using EphemeralNonce = PrivateClickMeasurement::EphemeralNonce; |
| |
| constexpr Seconds debugModeSecondsUntilSend { 10_s }; |
| |
| PrivateClickMeasurementManager::PrivateClickMeasurementManager(UniqueRef<PCM::Client>&& client, const String& storageDirectory) |
| : m_firePendingAttributionRequestsTimer(RunLoop::main(), this, &PrivateClickMeasurementManager::firePendingAttributionRequests) |
| , m_storageDirectory(storageDirectory) |
| , m_client(WTFMove(client)) |
| { |
| // We should send any pending attributions on session-start in case their |
| // send delay has expired while the session was closed. Waiting 5 seconds accounts for the |
| // delay in database startup. When running in the daemon, the xpc activity does this instead. |
| if (!m_client->runningInDaemon()) |
| startTimer(5_s); |
| } |
| |
| PrivateClickMeasurementManager::~PrivateClickMeasurementManager() |
| { |
| if (m_store) |
| m_store->close([] { }); |
| } |
| |
| void PrivateClickMeasurementManager::storeUnattributed(PrivateClickMeasurement&& measurement, CompletionHandler<void()>&& completionHandler) |
| { |
| if (!featureEnabled()) |
| return completionHandler(); |
| |
| clearExpired(); |
| |
| if (m_privateClickMeasurementAppBundleIDForTesting) |
| measurement.setSourceApplicationBundleIDForTesting(*m_privateClickMeasurementAppBundleIDForTesting); |
| |
| if (measurement.ephemeralSourceNonce()) { |
| auto measurementCopy = measurement; |
| // This is guaranteed to be close in time to the navigational click which makes it likely to be personally identifiable. |
| getTokenPublicKey(WTFMove(measurementCopy), PrivateClickMeasurement::AttributionReportEndpoint::Source, PrivateClickMeasurement::PcmDataCarried::PersonallyIdentifiable, [weakThis = WeakPtr { *this }, this] (PrivateClickMeasurement&& measurement, const String& publicKeyBase64URL) { |
| if (!weakThis) |
| return; |
| |
| if (publicKeyBase64URL.isEmpty()) |
| return; |
| |
| if (m_fraudPreventionValuesForTesting) |
| measurement.setSourceUnlinkableTokenValue(m_fraudPreventionValuesForTesting->unlinkableTokenForSource); |
| #if PLATFORM(COCOA) |
| else { |
| if (auto errorMessage = measurement.calculateAndUpdateSourceUnlinkableToken(publicKeyBase64URL)) { |
| RELEASE_LOG_INFO(PrivateClickMeasurement, "Got the following error in calculateAndUpdateSourceUnlinkableToken(): '%{public}s", errorMessage->utf8().data()); |
| m_client->broadcastConsoleMessage(MessageLevel::Error, makeString("[Private Click Measurement] "_s, *errorMessage)); |
| return; |
| } |
| } |
| #endif |
| |
| getSignedUnlinkableTokenForSource(WTFMove(measurement)); |
| return; |
| }); |
| } |
| |
| m_client->broadcastConsoleMessage(MessageLevel::Log, "[Private Click Measurement] Storing a click."_s); |
| |
| insertPrivateClickMeasurement(WTFMove(measurement), PrivateClickMeasurementAttributionType::Unattributed, WTFMove(completionHandler)); |
| } |
| |
| void PrivateClickMeasurementManager::getTokenPublicKey(PrivateClickMeasurement&& attribution, PrivateClickMeasurement::AttributionReportEndpoint attributionReportEndpoint, PrivateClickMeasurement::PcmDataCarried pcmDataCarried, Function<void(PrivateClickMeasurement&& attribution, const String& publicKeyBase64URL)>&& callback) |
| { |
| if (!featureEnabled()) |
| return; |
| |
| URL tokenPublicKeyURL; |
| if (m_tokenPublicKeyURLForTesting) { |
| if (attributionReportEndpoint == PrivateClickMeasurement::AttributionReportEndpoint::Destination) |
| return; |
| tokenPublicKeyURL = *m_tokenPublicKeyURLForTesting; |
| // FIXME(225364) |
| pcmDataCarried = PrivateClickMeasurement::PcmDataCarried::NonPersonallyIdentifiable; |
| } else { |
| if (auto optURL = attribution.tokenPublicKeyURL()) |
| tokenPublicKeyURL = *optURL; |
| else |
| return; |
| } |
| |
| if (tokenPublicKeyURL.isEmpty() || !tokenPublicKeyURL.isValid()) |
| return; |
| |
| if (debugModeEnabled()) |
| pcmDataCarried = PrivateClickMeasurement::PcmDataCarried::PersonallyIdentifiable; |
| |
| RELEASE_LOG_INFO(PrivateClickMeasurement, "About to fire a token public key request."); |
| m_client->broadcastConsoleMessage(MessageLevel::Log, "[Private Click Measurement] About to fire a token public key request."_s); |
| |
| PCM::NetworkLoader::start(WTFMove(tokenPublicKeyURL), nullptr, pcmDataCarried, [weakThis = WeakPtr { *this }, this, attribution = WTFMove(attribution), callback = WTFMove(callback)] (auto& errorDescription, auto& jsonObject) mutable { |
| if (!weakThis) |
| return; |
| |
| if (!errorDescription.isNull()) { |
| m_client->broadcastConsoleMessage(MessageLevel::Error, makeString("[Private Click Measurement] Received error: '"_s, errorDescription, "' for token public key request."_s)); |
| return; |
| } |
| |
| if (!jsonObject) { |
| m_client->broadcastConsoleMessage(MessageLevel::Error, makeString("[Private Click Measurement] JSON response is empty for token public key request."_s)); |
| return; |
| } |
| |
| m_client->broadcastConsoleMessage(MessageLevel::Log, makeString("[Private Click Measurement] Got JSON response for token public key request."_s)); |
| |
| callback(WTFMove(attribution), jsonObject->getString("token_public_key"_s)); |
| }); |
| } |
| |
| void PrivateClickMeasurementManager::getTokenPublicKey(AttributionTriggerData&& attributionTriggerData, PrivateClickMeasurement::AttributionReportEndpoint attributionReportEndpoint, PrivateClickMeasurement::PcmDataCarried pcmDataCarried, Function<void(AttributionTriggerData&& attributionTriggerData, const String& publicKeyBase64URL)>&& callback) |
| { |
| if (!featureEnabled()) |
| return; |
| |
| URL tokenPublicKeyURL; |
| if (m_tokenPublicKeyURLForTesting) { |
| if (attributionReportEndpoint == PrivateClickMeasurement::AttributionReportEndpoint::Source) |
| return; |
| tokenPublicKeyURL = *m_tokenPublicKeyURLForTesting; |
| // FIXME(225364) |
| pcmDataCarried = PrivateClickMeasurement::PcmDataCarried::NonPersonallyIdentifiable; |
| } else { |
| if (auto optURL = attributionTriggerData.tokenPublicKeyURL()) |
| tokenPublicKeyURL = *optURL; |
| else |
| return; |
| } |
| |
| if (tokenPublicKeyURL.isEmpty() || !tokenPublicKeyURL.isValid()) |
| return; |
| |
| if (debugModeEnabled()) |
| pcmDataCarried = PrivateClickMeasurement::PcmDataCarried::PersonallyIdentifiable; |
| |
| RELEASE_LOG_INFO(PrivateClickMeasurement, "About to fire a token public key request."); |
| m_client->broadcastConsoleMessage(MessageLevel::Log, "[Private Click Measurement] About to fire a token public key request."_s); |
| |
| PCM::NetworkLoader::start(WTFMove(tokenPublicKeyURL), nullptr, pcmDataCarried, [weakThis = WeakPtr { *this }, this, attributionTriggerData = WTFMove(attributionTriggerData), callback = WTFMove(callback)] (auto& errorDescription, auto& jsonObject) mutable { |
| if (!weakThis) |
| return; |
| |
| if (!errorDescription.isNull()) { |
| m_client->broadcastConsoleMessage(MessageLevel::Error, makeString("[Private Click Measurement] Received error: '"_s, errorDescription, "' for token public key request."_s)); |
| return; |
| } |
| |
| if (!jsonObject) { |
| m_client->broadcastConsoleMessage(MessageLevel::Error, makeString("[Private Click Measurement] JSON response is empty for token public key request."_s)); |
| return; |
| } |
| |
| m_client->broadcastConsoleMessage(MessageLevel::Log, makeString("[Private Click Measurement] Got JSON response for token public key request."_s)); |
| |
| callback(WTFMove(attributionTriggerData), jsonObject->getString("token_public_key"_s)); |
| }); |
| } |
| |
| void PrivateClickMeasurementManager::configureForTokenSigning(PrivateClickMeasurement::PcmDataCarried& pcmDataCarried, URL& tokenSignatureURL, std::optional<URL> givenTokenSignatureURL) |
| { |
| // This is guaranteed to be close in time to a page load which can identify the user, either the source or the destination. |
| pcmDataCarried = PrivateClickMeasurement::PcmDataCarried::PersonallyIdentifiable; |
| if (m_tokenSignatureURLForTesting) { |
| tokenSignatureURL = *m_tokenSignatureURLForTesting; |
| // FIXME(225364) |
| if (!debugModeEnabled()) |
| pcmDataCarried = PrivateClickMeasurement::PcmDataCarried::NonPersonallyIdentifiable; |
| } else if (givenTokenSignatureURL) |
| tokenSignatureURL = *givenTokenSignatureURL; |
| } |
| |
| std::optional<String> PrivateClickMeasurementManager::getSignatureBase64URLFromTokenSignatureResponse(const String& errorDescription, const RefPtr<JSON::Object>& jsonObject) |
| { |
| if (!errorDescription.isNull()) { |
| m_client->broadcastConsoleMessage(MessageLevel::Error, makeString("[Private Click Measurement] Received error: '"_s, errorDescription, "' for token signing request."_s)); |
| return std::nullopt; |
| } |
| |
| if (!jsonObject) { |
| m_client->broadcastConsoleMessage(MessageLevel::Error, makeString("[Private Click Measurement] JSON response is empty for token signing request."_s)); |
| return std::nullopt; |
| } |
| |
| auto signatureBase64URL = jsonObject->getString("unlinkable_token"_s); |
| if (signatureBase64URL.isEmpty()) { |
| m_client->broadcastConsoleMessage(MessageLevel::Error, makeString("[Private Click Measurement] JSON response doesn't have the key 'unlinkable_token' for token signing request."_s)); |
| return std::nullopt; |
| } |
| |
| return signatureBase64URL; |
| } |
| |
| void PrivateClickMeasurementManager::getSignedUnlinkableTokenForSource(PrivateClickMeasurement&& measurement) |
| { |
| if (!featureEnabled()) |
| return; |
| |
| PrivateClickMeasurement::PcmDataCarried pcmDataCarried; |
| URL tokenSignatureURL; |
| configureForTokenSigning(pcmDataCarried, tokenSignatureURL, measurement.tokenSignatureURL()); |
| |
| if (tokenSignatureURL.isEmpty() || !tokenSignatureURL.isValid()) |
| return; |
| |
| RELEASE_LOG_INFO(PrivateClickMeasurement, "About to fire a unlinkable token signing request for the click source."); |
| m_client->broadcastConsoleMessage(MessageLevel::Log, "[Private Click Measurement] About to fire a unlinkable token signing request for the click source."_s); |
| |
| PCM::NetworkLoader::start(WTFMove(tokenSignatureURL), measurement.tokenSignatureJSON(), pcmDataCarried, [weakThis = WeakPtr { *this }, this, measurement = WTFMove(measurement)] (auto& errorDescription, auto& jsonObject) mutable { |
| if (!weakThis) |
| return; |
| |
| auto signatureBase64URL = getSignatureBase64URLFromTokenSignatureResponse(errorDescription, jsonObject); |
| if (!signatureBase64URL) |
| return; |
| |
| if (m_fraudPreventionValuesForTesting) { |
| PrivateClickMeasurement::SourceSecretToken sourceSecretToken; |
| sourceSecretToken.tokenBase64URL = m_fraudPreventionValuesForTesting->secretTokenForSource; |
| sourceSecretToken.signatureBase64URL = m_fraudPreventionValuesForTesting->signatureForSource; |
| sourceSecretToken.keyIDBase64URL = m_fraudPreventionValuesForTesting->keyIDForSource; |
| measurement.setSourceSecretToken(WTFMove(sourceSecretToken)); |
| #if PLATFORM(COCOA) |
| } else { |
| if (auto errorMessage = measurement.calculateAndUpdateSourceSecretToken(*signatureBase64URL)) { |
| RELEASE_LOG_INFO(PrivateClickMeasurement, "Got the following error in calculateAndUpdateSourceSecretToken(): '%{public}s", errorMessage->utf8().data()); |
| m_client->broadcastConsoleMessage(MessageLevel::Error, makeString("[Private Click Measurement] "_s, *errorMessage)); |
| return; |
| } |
| #endif |
| } |
| |
| m_client->broadcastConsoleMessage(MessageLevel::Log, "[Private Click Measurement] Storing a secret token."_s); |
| |
| insertPrivateClickMeasurement(WTFMove(measurement), PrivateClickMeasurementAttributionType::Unattributed, [] { }); |
| }); |
| |
| } |
| |
| void PrivateClickMeasurementManager::getSignedUnlinkableTokenForDestination(SourceSite&& sourceSite, AttributionDestinationSite&& destinationSite, AttributionTriggerData&& attributionTriggerData, const ApplicationBundleIdentifier& applicationBundleIdentifier) |
| { |
| if (!featureEnabled()) |
| return; |
| |
| PrivateClickMeasurement::PcmDataCarried pcmDataCarried; |
| URL tokenSignatureURL; |
| configureForTokenSigning(pcmDataCarried, tokenSignatureURL, attributionTriggerData.tokenSignatureURL()); |
| |
| if (tokenSignatureURL.isEmpty() || !tokenSignatureURL.isValid()) |
| return; |
| |
| RELEASE_LOG_INFO(PrivateClickMeasurement, "About to fire a unlinkable token signing request for the click destination."); |
| m_client->broadcastConsoleMessage(MessageLevel::Log, "[Private Click Measurement] About to fire a unlinkable token signing request for the click destination."_s); |
| |
| PCM::NetworkLoader::start(WTFMove(tokenSignatureURL), attributionTriggerData.tokenSignatureJSON(), pcmDataCarried, [weakThis = WeakPtr { *this }, this, sourceSite = WTFMove(sourceSite), destinationSite = WTFMove(destinationSite), attributionTriggerData = WTFMove(attributionTriggerData), applicationBundleIdentifier = applicationBundleIdentifier.isolatedCopy()] (auto& errorDescription, auto& jsonObject) mutable { |
| if (!weakThis) |
| return; |
| |
| auto signatureBase64URL = getSignatureBase64URLFromTokenSignatureResponse(errorDescription, jsonObject); |
| if (!signatureBase64URL) |
| return; |
| |
| #if PLATFORM(COCOA) |
| if (!attributionTriggerData.destinationUnlinkableToken) { |
| RELEASE_LOG_INFO(PrivateClickMeasurement, "Destination unlinkable token is missing."); |
| m_client->broadcastConsoleMessage(MessageLevel::Error, "[Private Click Measurement] Destination unlinkable token is missing."_s); |
| return; |
| } |
| |
| auto result = PrivateClickMeasurement::calculateAndUpdateDestinationSecretToken(*signatureBase64URL, *attributionTriggerData.destinationUnlinkableToken); |
| if (!result) { |
| auto errorMessage = result.error().isEmpty() ? "Unknown" : result.error(); |
| RELEASE_LOG_INFO(PrivateClickMeasurement, "Got the following error in calculateAndUpdateSourceSecretToken(): '%{public}s", errorMessage.utf8().data()); |
| m_client->broadcastConsoleMessage(MessageLevel::Error, makeString("[Private Click Measurement] "_s, errorMessage)); |
| return; |
| } |
| |
| m_client->broadcastConsoleMessage(MessageLevel::Log, "[Private Click Measurement] Storing a secret token."_s); |
| |
| attributionTriggerData.destinationSecretToken = result.value(); |
| attribute(WTFMove(sourceSite), WTFMove(destinationSite), WTFMove(attributionTriggerData), m_privateClickMeasurementAppBundleIDForTesting ? *m_privateClickMeasurementAppBundleIDForTesting : applicationBundleIdentifier); |
| #endif |
| }); |
| } |
| |
| void PrivateClickMeasurementManager::insertPrivateClickMeasurement(PrivateClickMeasurement&& measurement, PrivateClickMeasurementAttributionType type, CompletionHandler<void()>&& completionHandler) |
| { |
| store().insertPrivateClickMeasurement(WTFMove(measurement), type, WTFMove(completionHandler)); |
| } |
| |
| void PrivateClickMeasurementManager::migratePrivateClickMeasurementFromLegacyStorage(PrivateClickMeasurement&& measurement, PrivateClickMeasurementAttributionType type) |
| { |
| store().insertPrivateClickMeasurement(WTFMove(measurement), type, [] { }); |
| } |
| |
| void PrivateClickMeasurementManager::setDebugModeIsEnabled(bool enabled) |
| { |
| // This doesn't maintain global state, it just broadcasts a message when debug mode enabled changes. |
| // The state is either stored in NetworkSession when not using the daemon |
| // or in DaemonConnectionSet per-connection when using the daemon. |
| |
| auto message = enabled ? "[Private Click Measurement] Turned Debug Mode on."_s : "[Private Click Measurement] Turned Debug Mode off."_s; |
| m_client->broadcastConsoleMessage(MessageLevel::Info, message); |
| } |
| |
| void PrivateClickMeasurementManager::handleAttribution(AttributionTriggerData&& attributionTriggerData, const URL& requestURL, WebCore::RegistrableDomain&& redirectDomain, const URL& firstPartyURL, const ApplicationBundleIdentifier& applicationBundleIdentifier) |
| { |
| if (!featureEnabled()) |
| return; |
| |
| if (!redirectDomain.matches(requestURL)) { |
| m_client->broadcastConsoleMessage(MessageLevel::Warning, "[Private Click Measurement] Triggering event was not accepted because the HTTP redirect was not same-site."_s); |
| return; |
| } |
| |
| RegistrableDomain sourceDomain; |
| if (redirectDomain.matches(firstPartyURL)) { |
| if (!attributionTriggerData.sourceRegistrableDomain) { |
| m_client->broadcastConsoleMessage(MessageLevel::Warning, "[Private Click Measurement] Triggering event was not accepted because it was requested in an HTTP redirect that is same-site as the first-party and no attributionSource query parameter was provided."_s); |
| return; |
| } |
| sourceDomain = *attributionTriggerData.sourceRegistrableDomain; |
| } else if (attributionTriggerData.sourceRegistrableDomain) { |
| m_client->broadcastConsoleMessage(MessageLevel::Warning, "[Private Click Measurement] Triggering event was not accepted because it was requested in an HTTP redirect that is cross-site from the first-party but an attributionSource query parameter was still provided."_s); |
| return; |
| } else |
| sourceDomain = WTFMove(redirectDomain); |
| |
| m_client->broadcastConsoleMessage(MessageLevel::Log, "[Private Click Measurement] Triggering event accepted."_s); |
| |
| // Signing of a destination token must be done unconditionally. Otherwise it becomes a way to discover that there is a pending attribution. |
| if (attributionTriggerData.ephemeralDestinationNonce) { |
| auto attributionTriggerDataCopy = attributionTriggerData; |
| // This is guaranteed to be close in time to the triggering event which makes it likely to be personally identifiable. |
| getTokenPublicKey(WTFMove(attributionTriggerDataCopy), PrivateClickMeasurement::AttributionReportEndpoint::Destination, PrivateClickMeasurement::PcmDataCarried::PersonallyIdentifiable, [weakThis = WeakPtr { *this }, this, sourceSite = SourceSite { WTFMove(sourceDomain) }, destinationSite = AttributionDestinationSite { firstPartyURL }, applicationBundleIdentifier = applicationBundleIdentifier.isolatedCopy()] (AttributionTriggerData&& attributionTriggerData, const String& publicKeyBase64URL) mutable { |
| if (!weakThis) |
| return; |
| |
| if (publicKeyBase64URL.isEmpty()) { |
| RELEASE_LOG_INFO(PrivateClickMeasurement, "The public key URL was empty."); |
| m_client->broadcastConsoleMessage(MessageLevel::Error, "[Private Click Measurement] The public key URL was empty."); |
| return; |
| } |
| |
| if (m_fraudPreventionValuesForTesting) |
| attributionTriggerData.setDestinationUnlinkableTokenValue(m_fraudPreventionValuesForTesting->unlinkableTokenForDestination); |
| #if PLATFORM(COCOA) |
| else { |
| auto result = PrivateClickMeasurement::calculateAndUpdateDestinationUnlinkableToken(publicKeyBase64URL); |
| if (result) { |
| attributionTriggerData.destinationUnlinkableToken = result.value(); |
| getSignedUnlinkableTokenForDestination(WTFMove(sourceSite), WTFMove(destinationSite), WTFMove(attributionTriggerData), m_privateClickMeasurementAppBundleIDForTesting ? *m_privateClickMeasurementAppBundleIDForTesting : applicationBundleIdentifier); |
| return; |
| } |
| |
| auto errorMessage = result.error().isEmpty() ? "Unknown" : result.error(); |
| RELEASE_LOG_INFO(PrivateClickMeasurement, "Got the following error in calculateAndUpdateDestinationUnlinkableToken(): '%{public}s", errorMessage.utf8().data()); |
| m_client->broadcastConsoleMessage(MessageLevel::Error, makeString("[Private Click Measurement] "_s, errorMessage)); |
| return; |
| } |
| #endif |
| }); |
| return; |
| } |
| |
| attribute(SourceSite { WTFMove(sourceDomain) }, AttributionDestinationSite { firstPartyURL }, WTFMove(attributionTriggerData), m_privateClickMeasurementAppBundleIDForTesting ? *m_privateClickMeasurementAppBundleIDForTesting : applicationBundleIdentifier); |
| } |
| |
| void PrivateClickMeasurementManager::startTimerImmediatelyForTesting() |
| { |
| startTimer(0_s); |
| } |
| |
| void PrivateClickMeasurementManager::setPrivateClickMeasurementAppBundleIDForTesting(ApplicationBundleIdentifier&& appBundleID) |
| { |
| if (appBundleID.isEmpty()) |
| m_privateClickMeasurementAppBundleIDForTesting = std::nullopt; |
| else |
| m_privateClickMeasurementAppBundleIDForTesting = WTFMove(appBundleID); |
| } |
| |
| void PrivateClickMeasurementManager::startTimer(Seconds seconds) |
| { |
| m_firePendingAttributionRequestsTimer.startOneShot(seconds); |
| } |
| |
| void PrivateClickMeasurementManager::attribute(SourceSite&& sourceSite, AttributionDestinationSite&& destinationSite, AttributionTriggerData&& attributionTriggerData, const ApplicationBundleIdentifier& applicationBundleIdentifier) |
| { |
| if (!featureEnabled()) |
| return; |
| |
| store().attributePrivateClickMeasurement(WTFMove(sourceSite), WTFMove(destinationSite), applicationBundleIdentifier, WTFMove(attributionTriggerData), m_isRunningTest ? WebCore::PrivateClickMeasurement::IsRunningLayoutTest::Yes : WebCore::PrivateClickMeasurement::IsRunningLayoutTest::No, [this, weakThis = WeakPtr { *this }] (auto attributionSecondsUntilSendData, auto debugInfo) { |
| if (!weakThis) |
| return; |
| |
| if (!attributionSecondsUntilSendData) |
| return; |
| |
| if (UNLIKELY(debugModeEnabled())) { |
| for (auto& message : debugInfo.messages) |
| m_client->broadcastConsoleMessage(message.messageLevel, message.message); |
| } |
| |
| if (attributionSecondsUntilSendData.value().hasValidSecondsUntilSendValues()) { |
| auto minSecondsUntilSend = attributionSecondsUntilSendData.value().minSecondsUntilSend(); |
| |
| ASSERT(minSecondsUntilSend); |
| if (!minSecondsUntilSend) |
| return; |
| |
| if (m_firePendingAttributionRequestsTimer.isActive() && m_firePendingAttributionRequestsTimer.secondsUntilFire() < *minSecondsUntilSend) |
| return; |
| |
| if (UNLIKELY(debugModeEnabled())) { |
| m_client->broadcastConsoleMessage(MessageLevel::Log, makeString("[Private Click Measurement] Setting timer for firing attribution request to the debug mode timeout of "_s, debugModeSecondsUntilSend.seconds(), " seconds where the regular timeout would have been "_s, minSecondsUntilSend.value().seconds(), " seconds."_s)); |
| minSecondsUntilSend = debugModeSecondsUntilSend; |
| } else |
| m_client->broadcastConsoleMessage(MessageLevel::Log, makeString("[Private Click Measurement] Setting timer for firing attribution request to the timeout of "_s, minSecondsUntilSend.value().seconds(), " seconds."_s)); |
| |
| startTimer(*minSecondsUntilSend); |
| } |
| }); |
| } |
| |
| void PrivateClickMeasurementManager::fireConversionRequest(const PrivateClickMeasurement& attribution, PrivateClickMeasurement::AttributionReportEndpoint attributionReportEndpoint) |
| { |
| if (!featureEnabled() || !attribution.attributionTriggerData()) |
| return; |
| |
| if (!attribution.sourceSecretToken() && !attribution.attributionTriggerData()->destinationSecretToken) { |
| fireConversionRequestImpl(attribution, attributionReportEndpoint); |
| return; |
| } |
| |
| auto attributionCopy = attribution; |
| // This happens out of webpage context and with a long delay and is thus unlikely to be personally identifiable. |
| if (attribution.sourceSecretToken()) { |
| getTokenPublicKey(WTFMove(attributionCopy), attributionReportEndpoint, PrivateClickMeasurement::PcmDataCarried::NonPersonallyIdentifiable, [weakThis = WeakPtr { *this }, this, attributionReportEndpoint] (PrivateClickMeasurement&& attribution, const String& publicKeyBase64URL) { |
| if (!weakThis) |
| return; |
| |
| auto publicKeyData = base64URLDecode(publicKeyBase64URL); |
| if (!publicKeyData) |
| return; |
| |
| auto crypto = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA_256); |
| crypto->addBytes(publicKeyData->data(), publicKeyData->size()); |
| auto publicKeyDataHash = crypto->computeHash(); |
| |
| auto keyID = base64URLEncodeToString(publicKeyDataHash.data(), publicKeyDataHash.size()); |
| if (keyID != attribution.sourceSecretToken()->keyIDBase64URL) |
| return; |
| |
| if (!attribution.attributionTriggerData()) |
| return; |
| |
| if (attribution.attributionTriggerData()->destinationSecretToken) { |
| getTokenPublicKey(WTFMove(attribution), attributionReportEndpoint, PrivateClickMeasurement::PcmDataCarried::NonPersonallyIdentifiable, [weakThis = WeakPtr { *this }, this, attributionReportEndpoint] (PrivateClickMeasurement&& attribution, const String& publicKeyBase64URL) { |
| if (!weakThis) |
| return; |
| |
| auto publicKeyData = base64URLDecode(publicKeyBase64URL); |
| if (!publicKeyData) |
| return; |
| |
| auto crypto = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA_256); |
| crypto->addBytes(publicKeyData->data(), publicKeyData->size()); |
| auto publicKeyDataHash = crypto->computeHash(); |
| |
| auto keyID = base64URLEncodeToString(publicKeyDataHash.data(), publicKeyDataHash.size()); |
| if (keyID != attribution.attributionTriggerData()->destinationSecretToken->keyIDBase64URL) |
| return; |
| |
| fireConversionRequestImpl(attribution, attributionReportEndpoint); |
| }); |
| return; |
| } |
| |
| fireConversionRequestImpl(attribution, attributionReportEndpoint); |
| }); |
| return; |
| } |
| |
| getTokenPublicKey(WTFMove(attributionCopy), attributionReportEndpoint, PrivateClickMeasurement::PcmDataCarried::NonPersonallyIdentifiable, [weakThis = WeakPtr { *this }, this, attributionReportEndpoint] (PrivateClickMeasurement&& attribution, const String& publicKeyBase64URL) { |
| if (!weakThis) |
| return; |
| |
| auto publicKeyData = base64URLDecode(publicKeyBase64URL); |
| if (!publicKeyData) |
| return; |
| |
| auto crypto = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA_256); |
| crypto->addBytes(publicKeyData->data(), publicKeyData->size()); |
| auto publicKeyDataHash = crypto->computeHash(); |
| |
| auto keyID = base64URLEncodeToString(publicKeyDataHash.data(), publicKeyDataHash.size()); |
| if (!attribution.attributionTriggerData()->destinationSecretToken || keyID != attribution.attributionTriggerData()->destinationSecretToken->keyIDBase64URL) |
| return; |
| |
| fireConversionRequestImpl(attribution, attributionReportEndpoint); |
| }); |
| } |
| |
| void PrivateClickMeasurementManager::fireConversionRequestImpl(const PrivateClickMeasurement& attribution, PrivateClickMeasurement::AttributionReportEndpoint attributionReportEndpoint) |
| { |
| URL attributionURL; |
| switch (attributionReportEndpoint) { |
| case PrivateClickMeasurement::AttributionReportEndpoint::Source: |
| attributionURL = m_attributionReportTestConfig ? m_attributionReportTestConfig->attributionReportClickSourceURL : attribution.attributionReportClickSourceURL(); |
| break; |
| case PrivateClickMeasurement::AttributionReportEndpoint::Destination: |
| attributionURL = m_attributionReportTestConfig ? m_attributionReportTestConfig->attributionReportClickDestinationURL : attribution.attributionReportClickDestinationURL(); |
| } |
| |
| if (attributionURL.isEmpty() || !attributionURL.isValid()) |
| return; |
| |
| auto pcmDataCarried = debugModeEnabled() ? PrivateClickMeasurement::PcmDataCarried::PersonallyIdentifiable : PrivateClickMeasurement::PcmDataCarried::NonPersonallyIdentifiable; |
| |
| RELEASE_LOG_INFO(PrivateClickMeasurement, "About to fire an attribution request."); |
| m_client->broadcastConsoleMessage(MessageLevel::Log, "[Private Click Measurement] About to fire an attribution request."_s); |
| |
| PCM::NetworkLoader::start(WTFMove(attributionURL), attribution.attributionReportJSON(), pcmDataCarried, [weakThis = WeakPtr { *this }, this](auto& errorDescription, auto&) { |
| if (!weakThis) |
| return; |
| |
| if (!errorDescription.isNull()) |
| m_client->broadcastConsoleMessage(MessageLevel::Error, makeString("[Private Click Measurement] Received error: '"_s, errorDescription, "' for ad click attribution request."_s)); |
| }); |
| } |
| |
| void PrivateClickMeasurementManager::clearSentAttribution(PrivateClickMeasurement&& sentConversion, PrivateClickMeasurement::AttributionReportEndpoint attributionReportEndpoint) |
| { |
| if (!featureEnabled()) |
| return; |
| |
| store().clearSentAttribution(WTFMove(sentConversion), attributionReportEndpoint); |
| } |
| |
| Seconds PrivateClickMeasurementManager::randomlyBetweenFifteenAndThirtyMinutes() const |
| { |
| if (m_isRunningTest) |
| return 0_s; |
| |
| return debugModeEnabled() ? debugModeSecondsUntilSend : 15_min + Seconds(cryptographicallyRandomNumber() % 900); |
| } |
| |
| void PrivateClickMeasurementManager::firePendingAttributionRequests() |
| { |
| if (!featureEnabled()) |
| return; |
| |
| store().allAttributedPrivateClickMeasurement([this, weakThis = WeakPtr { *this }] (auto&& attributions) { |
| if (!weakThis) |
| return; |
| auto nextTimeToFire = Seconds::infinity(); |
| bool hasSentAttribution = false; |
| |
| for (auto& attribution : attributions) { |
| std::optional<WallTime> earliestTimeToSend = attribution.timesToSend().earliestTimeToSend(); |
| std::optional<WebCore::PrivateClickMeasurement::AttributionReportEndpoint> attributionReportEndpoint = attribution.timesToSend().attributionReportEndpoint(); |
| |
| if (!earliestTimeToSend || !attributionReportEndpoint) { |
| ASSERT_NOT_REACHED(); |
| continue; |
| } |
| |
| auto now = WallTime::now(); |
| if (*earliestTimeToSend <= now || m_isRunningTest || debugModeEnabled()) { |
| if (hasSentAttribution) { |
| // We've already sent an attribution this round. We should send additional overdue attributions at |
| // a random time between 15 and 30 minutes to avoid a burst of simultaneous attributions. If debug |
| // mode is enabled, this should be much shorter for easy testing. |
| startTimer(randomlyBetweenFifteenAndThirtyMinutes()); |
| return; |
| } |
| |
| auto laterTimeToSend = attribution.timesToSend().latestTimeToSend(); |
| fireConversionRequest(attribution, *attributionReportEndpoint); |
| clearSentAttribution(WTFMove(attribution), *attributionReportEndpoint); |
| hasSentAttribution = true; |
| |
| // Update nextTimeToFire in case the later report time for this attribution is sooner than the scheduled next time to fire. |
| // Or, if debug mode is enabled, we should send the second report on a much shorter delay for easy testing. |
| if (laterTimeToSend) { |
| Seconds laterTimeToSendInSecondsFromNow = (*laterTimeToSend - WallTime::now()); |
| // Avoid sending expired attributions in bursts by using a random 15-30 minute interval if laterTimeToSend is expired. |
| laterTimeToSendInSecondsFromNow = laterTimeToSendInSecondsFromNow.value() < 0 ? randomlyBetweenFifteenAndThirtyMinutes() : laterTimeToSendInSecondsFromNow; |
| nextTimeToFire = debugModeEnabled() ? debugModeSecondsUntilSend : std::min(nextTimeToFire, laterTimeToSendInSecondsFromNow); |
| } |
| continue; |
| } |
| |
| auto seconds = *earliestTimeToSend - now; |
| nextTimeToFire = std::min(nextTimeToFire, seconds); |
| |
| // Attributions are sorted by earliestTimeToSend, so the first time we hit this there can be no further attributions |
| // due for reporting, and nextTimeToFire is the minimum earliestTimeToSend value for any pending attribution. |
| break; |
| } |
| |
| if (nextTimeToFire < Seconds::infinity()) |
| startTimer(nextTimeToFire); |
| }); |
| } |
| |
| void PrivateClickMeasurementManager::clear(CompletionHandler<void()>&& completionHandler) |
| { |
| m_firePendingAttributionRequestsTimer.stop(); |
| m_privateClickMeasurementAppBundleIDForTesting = std::nullopt; |
| |
| if (!featureEnabled()) |
| return completionHandler(); |
| |
| store().clearPrivateClickMeasurement(WTFMove(completionHandler)); |
| } |
| |
| void PrivateClickMeasurementManager::clearForRegistrableDomain(const RegistrableDomain& domain, CompletionHandler<void()>&& completionHandler) |
| { |
| if (!featureEnabled()) |
| return completionHandler(); |
| |
| store().clearPrivateClickMeasurementForRegistrableDomain(domain, WTFMove(completionHandler)); |
| } |
| |
| void PrivateClickMeasurementManager::clearExpired() |
| { |
| if (!featureEnabled()) |
| return; |
| |
| store().clearExpiredPrivateClickMeasurement(); |
| } |
| |
| void PrivateClickMeasurementManager::toStringForTesting(CompletionHandler<void(String)>&& completionHandler) const |
| { |
| if (!featureEnabled()) |
| return completionHandler("\nNo stored Private Click Measurement data.\n"_s); |
| |
| store().privateClickMeasurementToStringForTesting(WTFMove(completionHandler)); |
| } |
| |
| void PrivateClickMeasurementManager::setTokenPublicKeyURLForTesting(URL&& testURL) |
| { |
| if (testURL.isEmpty()) |
| return; |
| m_tokenPublicKeyURLForTesting = WTFMove(testURL); |
| } |
| |
| void PrivateClickMeasurementManager::setTokenSignatureURLForTesting(URL&& testURL) |
| { |
| if (testURL.isEmpty()) |
| m_tokenSignatureURLForTesting = { }; |
| else |
| m_tokenSignatureURLForTesting = WTFMove(testURL); |
| } |
| |
| void PrivateClickMeasurementManager::setAttributionReportURLsForTesting(URL&& sourceURL, URL&& destinationURL) |
| { |
| if (sourceURL.isEmpty() || destinationURL.isEmpty()) |
| m_attributionReportTestConfig = std::nullopt; |
| else |
| m_attributionReportTestConfig = AttributionReportTestConfig { WTFMove(sourceURL), WTFMove(destinationURL) }; |
| } |
| |
| void PrivateClickMeasurementManager::markAllUnattributedAsExpiredForTesting() |
| { |
| if (!featureEnabled()) |
| return; |
| |
| store().markAllUnattributedPrivateClickMeasurementAsExpiredForTesting(); |
| } |
| |
| void PrivateClickMeasurementManager::setPCMFraudPreventionValuesForTesting(String&& unlinkableToken, String&& secretToken, String&& signature, String&& keyID) |
| { |
| if (unlinkableToken.isEmpty() || secretToken.isEmpty() || signature.isEmpty() || keyID.isEmpty()) |
| return; |
| m_fraudPreventionValuesForTesting = TestingFraudPreventionValues { WTFMove(unlinkableToken), WTFMove(secretToken), WTFMove(signature), WTFMove(keyID), emptyString(), emptyString(), emptyString(), emptyString() }; |
| } |
| |
| bool PrivateClickMeasurementManager::featureEnabled() const |
| { |
| return m_client->featureEnabled(); |
| } |
| |
| bool PrivateClickMeasurementManager::debugModeEnabled() const |
| { |
| return m_client->debugModeEnabled(); |
| } |
| |
| void PrivateClickMeasurementManager::markAttributedPrivateClickMeasurementsAsExpiredForTesting(CompletionHandler<void()>&& completionHandler) |
| { |
| if (!featureEnabled()) |
| return completionHandler(); |
| |
| store().markAttributedPrivateClickMeasurementsAsExpiredForTesting(WTFMove(completionHandler)); |
| } |
| |
| PCM::Store& PrivateClickMeasurementManager::store() |
| { |
| if (!m_store) |
| m_store = PCM::Store::create(m_storageDirectory); |
| return *m_store; |
| } |
| |
| const PCM::Store& PrivateClickMeasurementManager::store() const |
| { |
| if (!m_store) |
| m_store = PCM::Store::create(m_storageDirectory); |
| return *m_store; |
| } |
| |
| void PrivateClickMeasurementManager::destroyStoreForTesting(CompletionHandler<void()>&& completionHandler) |
| { |
| if (!m_store) |
| return completionHandler(); |
| m_store->close([weakThis = WeakPtr { *this }, completionHandler = WTFMove(completionHandler)] () mutable { |
| if (weakThis) |
| weakThis->m_store = nullptr; |
| return completionHandler(); |
| }); |
| } |
| |
| void PrivateClickMeasurementManager::allowTLSCertificateChainForLocalPCMTesting(const WebCore::CertificateInfo& certificateInfo) |
| { |
| PCM::NetworkLoader::allowTLSCertificateChainForLocalPCMTesting(certificateInfo); |
| } |
| |
| } // namespace WebKit |