如何使用 Web Audio API 进行浏览器指纹识别
您是否知道无需使用 cookie 或请求权限即可识别 Web 浏览器?
这被称为“浏览器指纹”,它通过读取浏览器属性并将它们组合成一个标识符来工作。此标识符是无状态的,并且在正常和隐身模式下都能正常工作。

在生成浏览器标识符时,我们可以直接读取浏览器属性,也可以先使用属性处理技术。我们今天将讨论的一项创造性技术是音频指纹识别。
音频指纹识别是一项有价值的技术,因为它相对独特且稳定。它的独特性来自Web Audio API的内部复杂性和复杂性。之所以能够实现稳定性,是因为我们将使用的音频源是一个以数学方式生成的数字序列。这些数字稍后将组合成一个音频指纹值。
在深入研究技术实现之前,我们需要了解 Web Audio API 及其构建块的一些想法。
Web Audio API 的简要概述
Web Audio API 是一个处理音频操作的强大系统。它旨在通过将音频节点链接在一起并构建音频图来在AudioContext内工作。单个 AudioContext 可以处理插入其他节点并形成音频处理链的多种类型的音频源。

源可以是音频元素、流或使用振荡器以数学方式生成的内存源。我们将振荡器用于我们的目的,然后将其连接到其他节点以进行额外处理。
在深入了解音频指纹实现细节之前,回顾一下我们将使用的 API 的所有构建块会很有帮助。
AudioContext
AudioContext 表示由链接在一起的音频节点构建的整个链。它控制节点的创建和音频处理的执行。在执行任何其他操作之前,您总是先创建一个 AudioContext 的实例。创建单个 AudioContext 实例并将其重用于所有未来处理是一种很好的做法。
AudioContext 有一个目标属性,表示该上下文中所有音频的目标。
还有一种特殊的AudioContext:OfflineAudioContext。主要区别在于它不会将音频呈现给设备硬件。相反,它会尽可能快地生成音频并将其保存到AudioBuffer中。因此,OfflineAudioContext 的目标将是内存中的数据结构,而对于常规 AudioContext,目标将是音频渲染设备。
在创建 OfflineAudioContext 的实例时,我们传递 3 个参数:通道数、样本总数和以每秒样本为单位的采样率。
const AudioContext =
window.OfflineAudioContext ||
window.webkitOfflineAudioContext
const context = new AudioContext(1, 5000, 44100)
音频缓冲区
AudioBuffer 表示存储在内存中的音频片段。它旨在容纳小片段。数据在内部以线性 PCM 表示,每个样本由 -1.0 和 1.0 之间的 32 位浮点数表示。它可以容纳多个频道,但出于我们的目的,我们将只使用一个频道。

振荡器
在处理音频时,我们总是需要一个来源。振荡器是一个很好的候选者,因为它以数学方式生成样本,而不是播放音频文件。在其最简单的形式中,振荡器产生具有指定频率的周期性波形。
默认形状是正弦波。
也可以生成其他类型的波,例如方波、锯齿波和三角波。
默认频率为 440 Hz,这是标准 A4 纸币。
压缩机
Web Audio API 提供了一个 DynamicsCompressorNode,它可以降低信号中最响亮部分的音量,并有助于防止失真或削波。
DynamicsCompressorNode 有许多我们将使用的有趣属性。这些属性将有助于在浏览器之间创建更多可变性。
阈值 - 压缩机将开始生效的分贝值。
Knee - 以分贝为单位的值,表示曲线平滑过渡到压缩部分的阈值以上的范围。
比率 - 输出变化 1 dB 所需的输入变化量,以 dB 为单位。
Reduction - 浮点数,表示压缩器当前应用于信号的增益减少量。
起音 - 将增益降低 10 dB 所需的时间量(以秒为单位)。该值可以是小数。
释放 - 将增益增加 10 dB 所需的时间量(以秒为单位)。
音频指纹是如何计算的
现在我们已经有了我们需要的所有概念,我们可以开始处理我们的音频指纹代码了。
Safari 不支持无前缀 OfflineAudioContext,但支持
webkitOfflineAudioContext,所以我们将使用这个方法让它在 Chrome 和 Safari 中工作:
const AudioContext =
window.OfflineAudioContext ||
window.webkitOfflineAudioContex
现在我们创建一个 AudioContext 实例。我们将使用一个通道、44,100 个采样率和总共 5,000 个样本,这将使其长约 113 毫秒。
const context = new AudioContext(1, 5000, 44100)
接下来让我们创建一个声源——一个振荡器实例。它将产生每秒波动 1,000 次 (1,000 Hz) 的三角形声波。
const oscillator = context.createOscillator()
oscillator.type = "triangle"
oscillator.frequency.value = 1000
现在让我们创建一个压缩器来添加更多变化并转换原始信号。请注意,所有这些参数的值都是任意的,仅用于以有趣的方式更改源信号。我们可以使用其他值,它仍然可以工作。
const compressor = context.createDynamicsCompressor()
compressor.threshold.value = -50
compressor.knee.value = 40
compressor.ratio.value = 12
compressor.reduction.value = 20
compressor.attack.value = 0
compressor.release.value = 0.2
让我们将节点连接在一起:振荡器到压缩器,压缩器到上下文目的地。
oscillator.connect(compressor)
compressor.connect(context.destination);
是时候生成音频片段了。当它准备好时,我们将使用 oncomplete 事件来获取结果。
oscillator.start()
context.oncomplete = event => {
// We have only one channel, so we get it by index
const samples = event.renderedBuffer.getChannelData(0)
};
context.startRendering()
Samples 是代表未压缩声音的浮点值数组。现在我们需要从该数组中计算一个值。
让我们通过简单地总结数组值的一部分来做到这一点:
function calculateHash(samples) {
let hash = 0
for (let i = 0; i < samples.length; ++i) {
hash += Math.abs(samples[i])
}
return hash
}
console.log(getHash(samples))
现在我们准备生成音频指纹。当我在 MacOS 上的 Chrome 上运行它时,我得到了以下值:
zoz100027 101.45647543197447
这里的所有都是它的。我们的音频指纹就是这个数字!
您可以在我们的开源浏览器指纹库中查看生产实现。
如果我尝试在 Safari 中执行代码,我会得到一个不同的数字:
- 79.58850509487092
并在 Firefox 中获得另一个独特的结果:
- 80.95458510611206
我们测试笔记本电脑上的每个浏览器都会产生不同的价值。该值非常稳定,在隐身模式下保持不变。
此值取决于底层硬件和操作系统,在您的情况下可能会有所不同。
为什么音频指纹因浏览器而异
让我们仔细看看为什么不同浏览器中的值不同。我们将检查 Chrome 和 Firefox 中的单个振荡波。
首先,让我们将音频片段的持续时间减少到 1/2000 秒,这对应于单个波,并检查构成该波的值。
我们需要将上下文持续时间更改为 23 个样本,大致相当于 1/2000 秒。我们现在也将跳过压缩器,只检查未修改的振荡器信号的差异。
const context = new AudioContext(1, 23, 44100)
以下是现在 Chrome 和 Firefox 中单个三角形振荡的外观:

然而,两个浏览器之间的底层值是不同的(为了简单起见,我只显示了前 3 个值):
铬合金:
火狐:
0.08988945186138153
0.09155717492103577
0.18264609575271606
0.18603470921516418
0.2712443470954895
0.2762767672538757
让我们看一下这个演示,以直观地看到这些差异。
从历史上看,所有主要的浏览器引擎(Blink、WebKit 和 Gecko)都基于最初由 Google 在 2011 年和 2012 年为 WebKit 项目开发的代码实现 Web Audio API。
Google 对 Webkit 项目的贡献示例包括:创建 OfflineAudioContext,创建 OscillatorNode,创建 DynamicsCompressorNode。
从那时起,浏览器开发人员进行了许多小改动。这些变化,加上涉及的大量数学运算,导致指纹差异。音频信号处理使用浮点运算,这也导致了计算的差异。
你可以看到这些东西现在是如何在三大浏览器引擎中实现的:
-
闪烁:振荡器,动态压缩器
-
WebKit:振荡器,动态压缩器
-
Gecko:振荡器,动态压缩器
此外,浏览器针对不同的 CPU 架构和操作系统使用不同的实现来利用SIMD等功能。例如,Chrome 在 macOS 上使用一个单独的快速傅立叶变换实现(产生不同的振荡器信号),在不同的 CPU 架构上使用不同的矢量运算实现(在 DynamicsCompressor 实现中使用)。这些特定于平台的更改也导致最终音频指纹的差异。
指纹结果还取决于 Android 版本(例如,在同一设备上的 Android 9 和 10 中不同)。
根据浏览器源代码,音频处理不使用专用的音频硬件或操作系统功能——所有计算均由 CPU 完成。
陷阱
当我们开始在生产中使用音频指纹时,我们的目标是实现良好的浏览器兼容性、稳定性和性能。对于高浏览器兼容性,我们还研究了以隐私为中心的浏览器,例如 Tor 和 Brave。
离线音频上下文
正如您在caniuse.com上看到的那样,OfflineAudioContext 几乎可以在任何地方使用。但有些情况需要特殊处理。
第一种情况是 iOS 11 或更早版本。它确实支持 OfflineAudioContext,但只有当由用户操作触发时才会开始渲染,例如通过按钮单击。如果 context.startRendering 不是由用户操作触发的,则 context.state 将暂停并且渲染将无限期挂起,除非您添加超时。仍然使用此 iOS 版本的用户不多,因此我们决定为他们禁用音频指纹识别。
第二种情况是 iOS 12 或更新版本的浏览器。如果页面在后台,他们可以拒绝开始音频处理。幸运的是,浏览器允许您在页面返回到前台时恢复处理。当页面被激活时,我们尝试多次调用 context.startRendering() 直到 context.state 变为运行状态。如果多次尝试后处理仍未开始,则代码将停止。我们还在重试策略之上使用常规的 setTimeout,以防出现意外错误或冻结。您可以在此处查看代码示例。
托尔
对于 Tor 浏览器,一切都很简单。 Web Audio API 在那里被禁用,所以音频指纹识别是不可能.
勇敢
有了 Brave,情况就更加微妙了。 Brave 是一款基于 Blink 的注重隐私的浏览器。众所周知,音频样本值会稍微随机化,这被称为“farbling”。
Farbling 是 Brave 的术语,用于稍微随机化半识别浏览器功能的输出,以一种网站难以检测的方式,但不会破坏良性的、为用户服务的网站。这些“farbled”值是使用每个会话确定性生成的,每个 eTLD+1 种子,因此站点每次尝试在同一会话中进行指纹识别时都会获得完全相同的值,但不同的站点将获得不同的值,同一站点在下一次会话中将获得不同的值。该技术起源于先前的隐私研究,包括PriVaricator(Nikiforakis et al, WWW 2015) 和FPRandom(Laperdrix et al, ESSoS 2017) 项目。
Brave 提供了三个级别的游戏(用户可以在设置中选择他们想要的级别):
-
已禁用 — 不应用任何干扰。指纹与其他 Blink 浏览器(例如 Chrome)中的指纹相同。
-
标准 — 这是默认值。音频信号值乘以一个称为“fudge”因子的固定数字,该数字对于用户会话中的给定域是稳定的。在实践中,这意味着音频波的声音和外观相同,但有微小的变化,难以用于指纹识别。
-
Strict — 将声波替换为伪随机序列。
farbling[通过转换原始音频值来修改](https://github.com/brave/brave-core/blob/680b0d872e0a295ef94602fb5dc1907358d6a3ba/chromium_src/third_party/blink/renderer/modules/webaudio/audio_buffer.cc#L16)原始 Blink AudioBuffer。
恢复勇敢的标准
要恢复乱码,我们需要先获得 fudge 因子。然后我们可以通过将乱码值除以 fudge 因子来取回原始缓冲区:
async function getFudgeFactor() {
const context = new AudioContext(1, 1, 44100)
const inputBuffer = context.createBuffer(1, 1, 44100)
inputBuffer.getChannelData(0)[0] = 1
const inputNode = context.createBufferSource()
inputNode.buffer = inputBuffer
inputNode.connect(context.destination)
inputNode.start()
// See the renderAudio implementation
// at https://git.io/Jmw1j
const outputBuffer = await renderAudio(context)
return outputBuffer.getChannelData(0)[0]
}
const [fingerprint, fudgeFactor] = await Promise.all([
// This function is the fingerprint algorithm described
// in the “How audio fingerprint is calculated” section
getFingerprint(),
getFudgeFactor(),
])
const restoredFingerprint = fingerprint / fudgeFactor
不幸的是,浮点运算缺乏精确获取原始样本所需的精度。下表显示了不同情况下恢复的音频指纹,并显示了它们与原始值的接近程度:
操作系统、浏览器
指纹
目标指纹的绝对差异
macOS 11、Chrome 89(目标指纹)
124.0434806260746
不适用
macOS 11、Brave 1.21(相同的设备和操作系统)
浏览器重启后的各种指纹:
124.04347912294482
124.0434832855703
124.04347889351203
124.04348024313667
0.00000014% – 0.00000214%
视窗 10、铬 89
124.04347527516074
0.00000431%
Windows 10,勇敢 1.21
浏览器重启后的各种指纹:
124.04347610535537
124.04347187270707
124.04347220244154
124.04347384813703
0.00000364% - 0.00000679%
安卓 11、铬 89
124.08075528279005
0.03%
安卓 9,铬 89
124.08074500028306
0.03%
铬操作系统 89
124.04347721464
0.00000275%
标记 11,野生动物园 14
35.10893232002854
71.7%
macOS 11、火狐 86
35.7383295930922
71.2%
如您所见,与其他浏览器的指纹相比,恢复的 Brave 指纹更接近原始指纹。这意味着您可以使用模糊算法来匹配它们。例如,如果一对音频指纹数字之间的差异超过 0.0000022%,您可以假设它们是不同的设备或浏览器。
性能
Web Audio API 渲染
让我们来看看 Chrome 在音频指纹生成过程中发生了什么。在下面的截图中,横轴是时间,行是执行线程,条是浏览器繁忙时的时间片。您可以在这篇Chrome 文章中了解有关性能面板的更多信息。音频处理从 809.6 ms 开始,在 814.1 ms 完成:

在图像上标记为“Main”的主线程处理用户输入(鼠标移动、点击、轻敲等)和动画。当主线程忙时,页面冻结。避免在主线程上运行超过几毫秒的阻塞操作是一个好习惯。
如上图所示,浏览器将一些工作委托给 OfflineAudioRender 线程,从而释放主线程。 因此,在大多数音频指纹计算过程中,页面都保持响应状态。
Web Audio API 在web workers中不可用,所以我们不能在那里计算音频指纹。
不同浏览器的性能总结
下表显示了在不同浏览器和设备上获取指纹所需的时间。时间是在冷页面加载后立即测量的。
设备、操作系统、浏览器
指纹时间
MacBook Pro 2015(酷睿 i7)、macOS 11、Safari 14
5 毫秒
MacBook Pro 2015(酷睿 i7)、macOS 11、Chrome 89
7 毫秒
宏碁 Chromebook 314、Chrome 操作系统 89
7 毫秒
像素 5、Android 11、Chrome 89
7 毫秒
iPhone SE1、iOS 13、Safari 13
12 毫秒
像素 1,Android 7.1,Chrome 88
17 毫秒
银河 S4,安卓 4.4,Chrome 80
40 毫秒
MacBook Pro 2015(酷睿 i7)、macOS 11、Firefox 86
50 毫秒
音频指纹识别只是较大识别过程的一小部分。
音频指纹是我们的开源库用于生成浏览器指纹的众多信号之一。但是,我们不会盲目地合并浏览器中可用的每个信号。相反,我们分别分析每个信号的稳定性和唯一性,以确定它们对指纹准确性的影响。
对于音频指纹,我们发现信号对唯一性的贡献很小,但非常稳定,导致指纹准确度的净增加很小。
您可以在我们的浏览器指纹识别初学者指南中了解有关稳定性、唯一性和准确性的更多信息。
亲自尝试浏览器指纹识别
浏览器指纹识别是用于各种反欺诈应用的访客识别的有用方法。识别试图通过清除 cookie、以隐身模式浏览或使用 VPN 来规避跟踪的恶意访问者特别有用。
您可以尝试使用我们的开源库自己实现浏览器指纹识别。 FingerprintJS 是最流行的浏览器指纹库,拥有超过 12K 的 GitHub 星。
为了获得更高的识别精度,我们还开发了FingerprintJS Pro API,它使用机器学习将浏览器指纹识别与其他识别技术相结合。您可以[免费注册 FingerprintJS Pro。
取得联系
-
点赞、关注或分叉我们的GitHub 项目
-
将您的问题通过电子邮件发送至oss@fingerprintJS.com
-
订阅我们的时事通讯以获取更新
-
加入我们的团队,从事令人兴奋的在线安全研究:work@fingerprintjs.com
更多推荐


所有评论(0)