blob: 0942796877ab395b3998f4a8839c5a83c93ec097 [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.
*/
#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