Overview
Audio fingerprinting uses the Web Audio API (AudioContext) to generate a unique identifier based on how the device's audio hardware and software process a generated signal.
Mechanism
- Create an
AudioContext - Generate an oscillator signal (sine wave at a specific frequency)
- Process through
DynamicsCompressorNode - Extract the processed signal via
getChannelData() - Hash the resulting floating-point array
const ctx = new AudioContext()
const osc = ctx.createOscillator()
const compressor = ctx.createDynamicsCompressor()
osc.connect(compressor)
compressor.connect(ctx.destination)
// Compressor settings
compressor.threshold.value = -50
compressor.knee.value = 40
compressor.ratio.value = 12
compressor.reduction.value = -20
osc.frequency.value = 1000
osc.type = 'sawtooth'
// Process and extract
const analyser = ctx.createAnalyser()
const buffer = new Float32Array(analyser.fftSize)
analyser.getFloatFrequencyData(buffer)
const hash = md5(buffer) // unique per audio stack
Entropy Sources
| Source | Variability |
|---|---|
| Audio driver and hardware | High |
| Sample rate support | Medium |
| Dynamics processing implementation | Medium |
| OS audio stack (PulseAudio, ALSA, CoreAudio, WASAPI) | High |
| Audio channel count | Low |
Mitigation
At the proxy
The AudioContext runs entirely in the browser the proxy cannot modify its output.
Via injected JS
Override AudioContext.prototype.getChannelData or AnalyserNode.prototype.getFloatFrequencyData to return generic/flat data.
// Injected override
const originalGetFloatFrequencyData = AnalyserNode.prototype.getFloatFrequencyData
AnalyserNode.prototype.getFloatFrequencyData = function(array) {
originalGetFloatFrequencyData.call(this, array)
// Flat/noise the output
for (let i = 0; i < array.length; i++) {
array[i] = -100 // flat signal
}
}
Problem: The site can detect tampering by checking if the output is too uniform vs. real hardware output.
Conclusions
- Audio fingerprinting is CPU/OS/hardware specific
- Cannot be spoofed at proxy level
- JS injection can blunt it but not fully
- Best approach: override + consistent noise via injected JS