Skip to content

Commit

Permalink
AudioBuffer noise injection in Private Browsing can be negated using …
Browse files Browse the repository at this point in the history
…a looping audio buffer source

https://bugs.webkit.org/show_bug.cgi?id=270767
rdar://124156971

Reviewed by Chris Dumez, Charlie Wolfe and Matthew Finkel.

Implement several mitigations to make it impractical to reverse noise injection by looping a single
audio sample many times in a single audio buffer and averaging the results.

1.  Adjust noise injection to use normally-distributed noise, instead of a uniform random
    distribution. This raises the bar for "averaging-style" attacks, which can currently converge on
    a stable result by averaging the min/max values in the random distribution. A similar attack
    will now require more iterations to converge on the original value.

2.  Store previously-generated random values while applying noise, and reapply these random values
    to the values that are encountered repeatedly. This ensures that an attacker does not gain more
    information about the original value, by causing it to be computed repeatedly in the same audio
    buffer.

3.  Instead of uniformly applying a fixed noise level (0.001) for all readback using
    `OfflineAudioContext`, allow certain node types that are known to expose hardware or OS
    differences (i.e. `DynamicsCompressorNode` and `OscillatorNode`) to increase the amount of
    injected noise beyond the baseline of 0.1%. `AudioBufferSourceNode`, in particular, will amplify
    the noise level more, depending on the number of times the audio buffer is looped.

* Source/WebCore/Modules/webaudio/AudioBasicProcessorNode.h:
* Source/WebCore/Modules/webaudio/AudioBuffer.cpp:
(WebCore::AudioBuffer::releaseMemory):

Replace the single boolean flag (`m_needsAdditionalNoise`) with a `m_noiseInjectionMultiplier`,
which indicates the magnitude of noise injection (the standard deviation of the normal distribution
used to inject noise).

(WebCore::AudioBuffer::copyToChannel):
(WebCore::AudioBuffer::zero):
(WebCore::AudioBuffer::copyTo const):
(WebCore::AudioBuffer::applyNoiseIfNeeded):
* Source/WebCore/Modules/webaudio/AudioBuffer.h:
(WebCore::AudioBuffer::increaseNoiseInjectionMultiplier):
(WebCore::AudioBuffer::noiseInjectionMultiplier const):
(WebCore::AudioBuffer::setNeedsAdditionalNoise): Deleted.
* Source/WebCore/Modules/webaudio/AudioBufferSourceNode.cpp:
(WebCore::AudioBufferSourceNode::noiseInjectionMultiplier const):

Increase the noise injection level for an audio buffer, if it's downstream from an
`AudioBufferSourceNode` that loops many times. For an audio buffer source that loops more than 200
times, this boosts the existing noise level for the audio buffer by a factor of 0.005 per loop,
leading to a massive amount of noise in the case where a tiny sample is looped back-to-back in a
large buffer.

* Source/WebCore/Modules/webaudio/AudioBufferSourceNode.h:
* Source/WebCore/Modules/webaudio/AudioNode.h:
(WebCore::AudioNode::noiseInjectionMultiplier const):

Add a subclassing hook that allows each `AudioNode` subclass to inject additional noise when reading
back the final `AudioBuffer`. This allows us to selectively increase the amount of injected noise
when using specific types of audio nodes, which are known to expose larger differences w.r.t. the
underlying OS or CPU architecture.

* Source/WebCore/Modules/webaudio/AudioNodeOutput.cpp:
(WebCore::AudioNodeOutput::forEachInputNode const):

Add a helper method to iterate over each input node (i.e. the next destination in the processing
graph) that's attached to this output. Note that this must be called from underneath the context's
graph lock.

* Source/WebCore/Modules/webaudio/AudioNodeOutput.h:
* Source/WebCore/Modules/webaudio/AudioWorkletNode.cpp:
(WebCore::AudioWorkletNode::process):

Increase the noise level when passing raw data into worklets, to adjust for the new normally-
distributed noise injection.

* Source/WebCore/Modules/webaudio/BaseAudioContext.h:
(WebCore::BaseAudioContext::referencedSourceNodes const):

Add a helper method to iterate over all source nodes in the audio context; must be called only when
the context's graph lock is held.

* Source/WebCore/Modules/webaudio/DynamicsCompressorNode.h:

Add additional buffer readback noise when using certain audio node types.

* Source/WebCore/Modules/webaudio/OfflineAudioContext.cpp:
(WebCore::OfflineAudioContext::OfflineAudioContext):
(WebCore::OfflineAudioContext::lazyInitialize):
(WebCore::OfflineAudioContext::increaseNoiseMultiplierIfNeeded):

Upon initialization, traverse the audio processing graph in search for audio nodes that warrant
additional noise injection, and accumulate this extra noise on the target buffer.

* Source/WebCore/Modules/webaudio/OfflineAudioContext.h:
* Source/WebCore/Modules/webaudio/OscillatorNode.h:
* Source/WebCore/platform/audio/AudioUtilities.cpp:
(WebCore::AudioUtilities::applyNoise):

Switch to normally-distributed noise injection, rather than uniformally random noise. Additionally,
ensure that if a value appears again in the same buffer, it'll use the same, previously computed
noise multiplier value instead of a newly generated random value.

* Source/WebCore/platform/audio/AudioUtilities.h:
* Tools/TestWebKitAPI/Tests/WebKit/AdvancedPrivacyProtections.mm:
(TestWebKitAPI::TEST):
* Tools/TestWebKitAPI/Tests/WebKitCocoa/audio-fingerprinting.html:

Add a new test case to exercise these mitigations.

Originally-landed-as: 272448.707@safari-7618-branch (3c7dd17). rdar://128089250
Canonical link: https://commits.webkit.org/278815@main
  • Loading branch information
whsieh authored and robert-jenner committed May 15, 2024
1 parent 7fc383d commit 775bac3
Show file tree
Hide file tree
Showing 18 changed files with 222 additions and 17 deletions.
2 changes: 2 additions & 0 deletions Source/WebCore/Modules/webaudio/AudioBasicProcessorNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ class AudioBasicProcessorNode : public AudioNode {
AudioProcessor* processor() { return m_processor.get(); }
const AudioProcessor* processor() const { return m_processor.get(); }

float noiseInjectionMultiplier() const override { return 0.01; }

std::unique_ptr<AudioProcessor> m_processor;
};

Expand Down
14 changes: 7 additions & 7 deletions Source/WebCore/Modules/webaudio/AudioBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ void AudioBuffer::releaseMemory()
Locker locker { m_channelsLock };
m_channels = { };
m_channelWrappers = { };
m_needsAdditionalNoise = false;
m_noiseInjectionMultiplier = 0;
}

ExceptionOr<JSC::JSValue> AudioBuffer::getChannelData(JSDOMGlobalObject& globalObject, unsigned channelIndex)
Expand Down Expand Up @@ -264,7 +264,7 @@ ExceptionOr<void> AudioBuffer::copyToChannel(Ref<Float32Array>&& source, unsigne
ASSERT(dst);

memmove(dst + bufferOffset, src, count * sizeof(*dst));
m_needsAdditionalNoise = false;
m_noiseInjectionMultiplier = 0;
return { };
}

Expand All @@ -273,7 +273,7 @@ void AudioBuffer::zero()
for (auto& channel : m_channels)
channel->zeroFill();

m_needsAdditionalNoise = false;
m_noiseInjectionMultiplier = 0;
}

size_t AudioBuffer::memoryCost() const
Expand Down Expand Up @@ -315,7 +315,7 @@ bool AudioBuffer::copyTo(AudioBuffer& other) const
for (unsigned channelIndex = 0; channelIndex < numberOfChannels(); ++channelIndex)
memcpy(other.rawChannelData(channelIndex), m_channels[channelIndex]->data(), length() * sizeof(float));

other.m_needsAdditionalNoise = m_needsAdditionalNoise;
other.m_noiseInjectionMultiplier = m_noiseInjectionMultiplier;
return true;
}

Expand All @@ -335,13 +335,13 @@ WebCoreOpaqueRoot root(AudioBuffer* buffer)

void AudioBuffer::applyNoiseIfNeeded()
{
if (!m_needsAdditionalNoise)
if (!m_noiseInjectionMultiplier)
return;

for (auto& channel : m_channels)
AudioUtilities::applyNoise(channel->data(), channel->length(), 0.001);
AudioUtilities::applyNoise(channel->data(), channel->length(), m_noiseInjectionMultiplier);

m_needsAdditionalNoise = false;
m_noiseInjectionMultiplier = 0;
}

} // namespace WebCore
Expand Down
5 changes: 3 additions & 2 deletions Source/WebCore/Modules/webaudio/AudioBuffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ class AudioBuffer : public RefCounted<AudioBuffer> {

bool topologyMatches(const AudioBuffer&) const;

void setNeedsAdditionalNoise() { m_needsAdditionalNoise = true; }
void increaseNoiseInjectionMultiplier(float amount = 0.001) { m_noiseInjectionMultiplier += amount; }
float noiseInjectionMultiplier() const { return m_noiseInjectionMultiplier; }

private:
AudioBuffer(unsigned numberOfChannels, size_t length, float sampleRate, LegacyPreventDetaching = LegacyPreventDetaching::No);
Expand All @@ -109,7 +110,7 @@ class AudioBuffer : public RefCounted<AudioBuffer> {
FixedVector<JSValueInWrappedObject> m_channelWrappers;
bool m_isDetachable { true };
mutable Lock m_channelsLock;
bool m_needsAdditionalNoise { false };
float m_noiseInjectionMultiplier { 0 };
};

WebCoreOpaqueRoot root(AudioBuffer*);
Expand Down
16 changes: 16 additions & 0 deletions Source/WebCore/Modules/webaudio/AudioBufferSourceNode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,22 @@ bool AudioBufferSourceNode::propagatesSilence() const
return !m_buffer;
}

float AudioBufferSourceNode::noiseInjectionMultiplier() const
{
Locker locker { m_processLock };

if (!m_buffer)
return 0;

auto multiplier = m_buffer->noiseInjectionMultiplier();
if (m_isLooping && m_loopStart < m_loopEnd) {
static constexpr auto noiseMultiplierPerLoop = 0.005;
auto loopCount = m_buffer->duration() / (m_loopEnd - m_loopStart);
multiplier *= std::max(1.0, noiseMultiplierPerLoop * loopCount);
}
return multiplier;
}

} // namespace WebCore

#endif // ENABLE(WEB_AUDIO)
2 changes: 2 additions & 0 deletions Source/WebCore/Modules/webaudio/AudioBufferSourceNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ class AudioBufferSourceNode final : public AudioScheduledSourceNode {
double tailTime() const final { return 0; }
double latencyTime() const final { return 0; }

float noiseInjectionMultiplier() const final;

ExceptionOr<void> startPlaying(double when, double grainOffset, std::optional<double> grainDuration);
void adjustGrainParameters() WTF_REQUIRES_LOCK(m_processLock);

Expand Down
1 change: 1 addition & 0 deletions Source/WebCore/Modules/webaudio/AudioNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ class AudioNode
void setIsTailProcessing(bool isTailProcessing) { m_isTailProcessing = isTailProcessing; }

NoiseInjectionPolicy noiseInjectionPolicy() const;
virtual float noiseInjectionMultiplier() const { return 0; }

protected:
// Inputs and outputs must be created before the AudioNode is initialized.
Expand Down
8 changes: 8 additions & 0 deletions Source/WebCore/Modules/webaudio/AudioNodeOutput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,14 @@ void AudioNodeOutput::removeInput(AudioNodeInput* input)
m_inputs.remove(input);
}

void AudioNodeOutput::forEachInputNode(Function<void(AudioNode&)>&& callback) const
{
for (auto& node : m_inputs.values()) {
if (node)
callback(*node);
}
}

void AudioNodeOutput::disconnectAllInputs()
{
ASSERT(context().isGraphOwner());
Expand Down
1 change: 1 addition & 0 deletions Source/WebCore/Modules/webaudio/AudioNodeOutput.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class AudioNodeOutput {

// Must be called with the context's graph lock.
void disconnectAll();
void forEachInputNode(Function<void(AudioNode&)>&&) const;

void setNumberOfChannels(unsigned);
unsigned numberOfChannels() const { return m_numberOfChannels; }
Expand Down
2 changes: 1 addition & 1 deletion Source/WebCore/Modules/webaudio/AudioWorkletNode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ void AudioWorkletNode::process(size_t framesToProcess)
if (auto& input = m_inputs[inputIndex]) {
for (unsigned channelIndex = 0; channelIndex < input->numberOfChannels(); ++channelIndex) {
auto* channel = input->channel(channelIndex);
AudioUtilities::applyNoise(channel->mutableData(), channel->length(), 0.001);
AudioUtilities::applyNoise(channel->mutableData(), channel->length(), 0.01);
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions Source/WebCore/Modules/webaudio/BaseAudioContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,10 @@ class BaseAudioContext

void clear();

protected:
// Only accessed when the graph lock is held.
const Vector<AudioConnectionRefPtr<AudioNode>>& referencedSourceNodes() const { return m_referencedSourceNodes; }

private:
void scheduleNodeDeletion();
void workletIsReady();
Expand Down
2 changes: 2 additions & 0 deletions Source/WebCore/Modules/webaudio/DynamicsCompressorNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ class DynamicsCompressorNode final : public AudioNode {
double latencyTime() const final;
bool requiresTailProcessing() const final;

float noiseInjectionMultiplier() const final { return 0.01; }

std::unique_ptr<DynamicsCompressor> m_dynamicsCompressor;
Ref<AudioParam> m_threshold;
Ref<AudioParam> m_knee;
Expand Down
39 changes: 38 additions & 1 deletion Source/WebCore/Modules/webaudio/OfflineAudioContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ OfflineAudioContext::OfflineAudioContext(Document& document, const OfflineAudioC
if (!renderTarget())
document.addConsoleMessage(MessageSource::JS, MessageLevel::Warning, makeString("Failed to construct internal AudioBuffer with "_s, options.numberOfChannels, " channel(s), a sample rate of "_s, options.sampleRate, " and a length of "_s, options.length, '.'));
else if (noiseInjectionPolicy() == NoiseInjectionPolicy::Minimal)
renderTarget()->setNeedsAdditionalNoise();
renderTarget()->increaseNoiseInjectionMultiplier();
}

ExceptionOr<Ref<OfflineAudioContext>> OfflineAudioContext::create(ScriptExecutionContext& context, const OfflineAudioContextOptions& options)
Expand All @@ -76,6 +76,43 @@ ExceptionOr<Ref<OfflineAudioContext>> OfflineAudioContext::create(ScriptExecutio
return create(context, { numberOfChannels, length, sampleRate });
}

void OfflineAudioContext::lazyInitialize()
{
BaseAudioContext::lazyInitialize();

increaseNoiseMultiplierIfNeeded();
}

void OfflineAudioContext::increaseNoiseMultiplierIfNeeded()
{
if (noiseInjectionPolicy() == NoiseInjectionPolicy::None)
return;

Locker locker { graphLock() };

RefPtr target = renderTarget();
if (!target)
return;

Vector<AudioConnectionRefPtr<AudioNode>, 1> remainingNodes;
for (auto& node : referencedSourceNodes())
remainingNodes.append(node.copyRef());

while (!remainingNodes.isEmpty()) {
auto node = remainingNodes.takeLast();
target->increaseNoiseInjectionMultiplier(node->noiseInjectionMultiplier());
for (unsigned i = 0; i < node->numberOfOutputs(); ++i) {
auto* output = node->output(i);
if (!output)
continue;

output->forEachInputNode([&](auto& inputNode) {
remainingNodes.append(&inputNode);
});
}
}
}

void OfflineAudioContext::uninitialize()
{
if (!isInitialized())
Expand Down
3 changes: 3 additions & 0 deletions Source/WebCore/Modules/webaudio/OfflineAudioContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ class OfflineAudioContext final : public BaseAudioContext {
private:
OfflineAudioContext(Document&, const OfflineAudioContextOptions&);

void lazyInitialize() final;
void increaseNoiseMultiplierIfNeeded();

AudioBuffer* renderTarget() const { return destination().renderTarget(); }

// ActiveDOMObject
Expand Down
2 changes: 2 additions & 0 deletions Source/WebCore/Modules/webaudio/OscillatorNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ class OscillatorNode final : public AudioScheduledSourceNode {

bool propagatesSilence() const final;

float noiseInjectionMultiplier() const final { return 0.01; }

// One of the waveform types defined in the enum.
OscillatorType m_type; // Only used on the main thread.

Expand Down
26 changes: 23 additions & 3 deletions Source/WebCore/platform/audio/AudioUtilities.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#if ENABLE(WEB_AUDIO)

#include "AudioUtilities.h"
#include <random>
#include <wtf/CryptographicallyRandomNumber.h>
#include <wtf/MathExtras.h>
#include <wtf/WeakRandom.h>
Expand Down Expand Up @@ -77,11 +78,30 @@ size_t timeToSampleFrame(double time, double sampleRate, SampleFrameRounding rou
return static_cast<size_t>(frame);
}

void applyNoise(float* values, size_t numberOfElementsToProcess, float magnitude)
void applyNoise(float* values, size_t numberOfElementsToProcess, float standardDeviation)
{
WeakRandom generator;
std::random_device device;
std::mt19937 generator(device());
std::normal_distribution<float> distribution(1, standardDeviation);

HashMap<float, float> noiseMultipliers;
auto computeNoiseMultiplier = [&](float rawValue) {
if (!noiseMultipliers.isValidKey(rawValue))
return distribution(generator);

auto result = noiseMultipliers.ensure(rawValue, [&] {
return distribution(generator);
}).iterator->value;

static constexpr auto maxNoiseMultiplierMapSize = 250000;
if (noiseMultipliers.size() >= maxNoiseMultiplierMapSize)
noiseMultipliers.remove(noiseMultipliers.random());

return result;
};

for (size_t i = 0; i < numberOfElementsToProcess; ++i)
values[i] *= 1 + magnitude * (2 * generator.get() - 1);
values[i] *= computeNoiseMultiplier(values[i]);
}

} // AudioUtilites
Expand Down
2 changes: 1 addition & 1 deletion Source/WebCore/platform/audio/AudioUtilities.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ inline float decibelsToLinear(float decibels)
return powf(10, 0.05f * decibels);
}

void applyNoise(float* values, size_t numberOfElementsToProcess, float magnitude);
void applyNoise(float* values, size_t numberOfElementsToProcess, float standardDeviation);

} // AudioUtilites

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,7 @@ HTTPServer server({
checkFingerprintForNoise(@"testOscillatorCompressor");
checkFingerprintForNoise(@"testOscillatorCompressorWorklet");
checkFingerprintForNoise(@"testOscillatorCompressorAnalyzer");
checkFingerprintForNoise(@"testLoopingOscillatorCompressorBiquadFilter");
}

// FIXME when rdar://115137641 is resolved.
Expand Down
Loading

0 comments on commit 775bac3

Please sign in to comment.