Intention
Dieser Artikel zeigt einen Beispielcode um über die Web Audio API den Fingerprint eines Gerätes zu identifzieren. Bitte beachten Sie, dass die Identifikation nicht mit den Richtlinien der DSGVO-konform ist, daher sollte sie nur zu lokalen Testzwecken genutzt werden.
Die Aufmerksamkeit auf diesen Sachverhalt lenkte der folgende Heise-Artikel auf mich, daher entschied ich mich hier näher damit zu befassen:
Grundidee: Was wir messen
Wir nutzen die Web Audio API, um:
- ein synthetisches Signal zu erzeugen (Oszillator),
- es durch einen DynamicsCompressorNode zu schicken,
- das Ergebnis in einem OfflineAudioContext zu rendern,
- aus dem fertigen Audiopuffer einen Zahlwert zu berechnen, der vom Gerät abhängt.
Dieser Zahlwert ist der Audio-Fingerprint für das Gerät und kann genutzt werden um das Geräte eindeutig zu identifizieren.
Warum das pro Gerät anders ist
- Floating‑Point‑Arithmetik unterscheidet sich je nach CPU/Instruction‑Set.
- Browser‑Engine (Chromium, Gecko, WebKit) implementiert DSP leicht unterschiedlich.
- Audio‑Stack des OS kann Einfluss haben, je nach Implementierung.
Das Ergebnis: derselbe Code → auf Rechner A ein anderer Wert als auf Rechner B, aber bei Rechner A stabil über viele Aufrufe.
Code
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>Audio-Fingerprint Test (OfflineAudioContext)</title>
<style>
body { font-family: system-ui, sans-serif; margin: 2rem; }
button { padding: 0.5rem 1rem; font-size: 1rem; }
#result { margin-top: 1rem; font-family: monospace; }
</style>
</head>
<body>
<h1>Audio-Fingerprint Test</h1>
<p>
Diese Seite erzeugt einen <strong>OfflineAudioContext</strong>, rendert ein festes Signal
und berechnet daraus einen Fingerprint-Wert. Es wird <strong>kein Ton abgespielt</strong>
und <strong>keine Daten übertragen</strong.
</p>
<button id="run">Fingerprint berechnen</button>
<div id="result"></div>
<script>
async function getAudioFingerprint() {
// 1 Kanal, 5000 Frames, 44100 Hz
const sampleRate = 44100;
const length = 5000;
const offlineCtx = new OfflineAudioContext(1, length, sampleRate);
// Oszillator als Testsignal
const osc = offlineCtx.createOscillator();
osc.type = 'triangle';
osc.frequency.value = 10000; // 10 kHz
// DynamicsCompressor verstärkt Floating-Point-Unterschiede
const comp = offlineCtx.createDynamicsCompressor();
comp.threshold.value = -24;
comp.knee.value = 30;
comp.ratio.value = 12;
comp.attack.value = 0.003;
comp.release.value = 0.25;
osc.connect(comp);
comp.connect(offlineCtx.destination);
osc.start(0);
// Offline rendern (kein Audio-Ausgang)
const buffer = await offlineCtx.startRendering();
const data = buffer.getChannelData(0);
// Aus einem festen Bereich eine kompakte Zahl ableiten
const start = 4500;
const end = 5000;
let sum = 0;
let hash = 0;
for (let i = start; i < end; i++) {
const v = Math.abs(data[i]);
sum += v;
// einfache Hash-Mischung
hash = (hash * 31 + Math.floor(v * 1e6)) >>> 0;
}
return {
rawSum: sum,
hash: hash.toString(16).padStart(8, '0')
};
}
document.getElementById('run').addEventListener('click', async () => {
const resultEl = document.getElementById('result');
resultEl.textContent = 'Berechne...';
try {
const fp = await getAudioFingerprint();
resultEl.innerHTML =
'<p><strong>Roh-Summe:</strong> ' + fp.rawSum.toFixed(6) + '</p>' +
'<p><strong>Hash (hex):</strong> ' + fp.hash + '</p>' +
'<p>Auf demselben Gerät sollte dieser Wert relativ stabil sein, ' +
'auf anderen Geräten/Browsers unterschiedlich.</p>';
} catch (e) {
resultEl.textContent = 'Fehler: ' + e;
}
});
</script>
</body>
</html>