Web Audio API 如何用于浏览器指纹识别
Web Audio API 的简要概述
您是否知道无需使用 cookie 或请求权限即可识别 Web 浏览器?
这被称为“浏览器指纹识别”,其工作原理是读取浏览器属性并将它们组合成一个标识符。该标识符是无状态的,在正常模式和隐身模式下都能正常工作。
生成浏览器标识符时,我们可以直接读取浏览器属性,也可以先使用属性处理技术。今天我们要讨论的一项创新技术是音频指纹识别。
音频指纹识别是一项非常有价值的技术,因为它相对独特且稳定。它的独特性源于Web Audio API内部的复杂性和精密性。之所以能够实现这种稳定性,是因为我们所使用的音频源是通过数学生成的数字序列。这些数字稍后将被组合成一个音频指纹值。
在深入研究技术实现之前,我们需要了解 Web Audio API 及其构建块的一些想法。
Web Audio API 的简要概述
Web Audio API 是一个功能强大的音频操作处理系统。它旨在AudioContext通过连接音频节点并构建音频图来在内部工作。单个音频图AudioContext可以处理插入其他节点并形成音频处理链的多种类型的音频源。
源可以是audio元素、流或通过 数学生成的内存源Oscillator。我们将使用Oscillator来实现我们的目的,然后将其连接到其他节点进行额外的处理。
在深入研究音频指纹实现细节之前,回顾一下我们将要使用的 API 的所有构建块会很有帮助。
音频上下文
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格式表示,每个样本由介于和32之间的-bit浮点数表示。它可以存储多个通道,但就我们的目的而言,我们只使用一个通道。-1.01.0.
振荡器
处理音频时,我们总是需要一个源。Anoscillator是一个不错的选择,因为它以数学方式生成样本,而不是播放音频文件。其最简单的形式是oscillator生成具有指定频率的周期性波形。
默认形状是正弦波。
我们做了一个现场演示!你可以在我们的博客上体验一下。
还可以生成其他类型的波,例如方波、锯齿波和三角波。
默认频率为440Hz,即标准 A4 纸频。
压缩机
Web Audio API 提供了一个DynamicsCompressorNode,它可以降低信号最响部分的音量并有助于防止失真或削波。
DynamicsCompressorNode有很多有趣的属性,我们会用到它们。这些属性将有助于在不同浏览器之间创建更多变化。
Threshold- 以分贝为单位的值,高于该值时压缩机将开始生效。Knee- 以分贝为单位的值,表示曲线平滑过渡到压缩部分的阈值以上范围。Ratio- 输出发生 1 dB 变化所需的输入变化量(以1dB 为单位)。Reduction- 浮点数,表示压缩器当前对信号应用的增益衰减量。Attack- 降低增益 dB 所需的时间(以秒为单位)10。该值可以是小数。Release- 增加 dB 增益所需的时间(以秒为单位)10。
我们做了一个现场演示!你可以在我们的博客上体验一下。
如何计算音频指纹
现在我们已经掌握了所有需要的概念,可以开始编写音频指纹代码了。
Safari 不支持无前缀的OfflineAudioContext,但支持webkitOfflineAudioContext,因此我们将使用此方法使其在 Chrome 和 Safari 中工作:
const AudioContext =
window.OfflineAudioContext ||
window.webkitOfflineAudioContex
现在我们创建一个AudioContext实例。我们将使用一个通道、一个44,100采样率和5,000总采样数,这将使其113长度约为 ms。
const context = new AudioContext(1, 5000, 44100)
接下来让我们创建一个声源——一个oscillator实例。它将生成一个三角形的声波,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到compressor,并将压缩器连接到上下文目标。
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 上运行它,得到了以下值:
101.45647543197447
就是这样。我们的音频指纹就是这个数字!
您可以在我们的开源浏览器指纹库中查看生产实现。
如果我尝试在 Safari 中执行代码,我会得到不同的数字:
79.58850509487092
在 Firefox 中得到另一个独特的结果:
80.95458510611206
我们测试笔记本电脑上的每个浏览器都会生成不同的值。这个值非常稳定,在隐身模式下也保持不变。
该值取决于底层硬件和操作系统,并且在您的情况下可能会有所不同。
为什么音频指纹因浏览器而异
让我们仔细看看为什么这些值在不同浏览器中会有所不同。我们将分别在 Chrome 和 Firefox 中检查单个振荡波。
首先,让我们将音频片段的持续时间缩短至1/2000th一秒,这对应于单个波,并检查构成该波的值。
我们需要将上下文持续时间改为23样本,大致相当于1/2000th一秒。我们暂时跳过压缩器,只检查未修改oscillator信号的差异。
const context = new AudioContext(1, 23, 44100)
现在,单个三角振荡在 Chrome 和 Firefox 中的外观如下:
3但是,这两个浏览器的底层值是不同的(为了简单起见,我仅显示第一个值):
Chrome: |
Firefox: |
|---|---|
0.08988945186138153 |
0.09155717492103577 |
0.18264609575271606 |
0.18603470921516418 |
0.2712443470954895 |
0.2762767672538757 |
让我们看一下这个演示来直观地看到这些差异。
我们做了一个现场演示!你可以在我们的博客上体验一下。
从历史上看,所有主流浏览器引擎(Blink、WebKit 和 Gecko)的 Web Audio API 实现都基于最初由 Google 在 WebKit 项目中开发的2011代码2012。
Google 对 Webkit 项目的贡献示例包括:
创建OfflineAudioContext、
创建OscillatorNode、创建 DynamicsCompressorNode。
从那时起,浏览器开发者就做了很多细微的改动。这些改动,加上大量的数学运算,导致了指纹识别的差异。音频信号处理使用浮点运算,这也导致了计算结果的差异。
您可以看到这些功能现在在三大浏览器引擎中是如何实现的:
此外,浏览器会针对不同的 CPU 架构和操作系统使用不同的实现,以利用SIMD等功能。例如,Chrome 在 macOS 上使用单独的快速傅里叶变换实现(产生不同的oscillator信号),并在不同的 CPU 架构上使用不同的矢量运算实现(这些实现用于 DynamicsCompressor 实现)。这些特定于平台的更改也会导致最终音频指纹的差异。
指纹结果还取决于 Android 版本(例如,在 Android 中9和在同一设备上是不同的)。10
根据浏览器源代码,音频处理不使用专用音频硬件或操作系统功能 - 所有计算都由 CPU 完成。
陷阱
当我们开始在生产环境中使用音频指纹识别时,我们的目标是实现良好的浏览器兼容性、稳定性和性能。为了实现更高的浏览器兼容性,我们还考虑了注重隐私的浏览器,例如 Tor 和 Brave。
离线音频上下文
正如您在caniuse.com上看到的,OfflineAudioContext它几乎可以在任何地方使用。但有些情况需要特殊处理。
第一种情况是 iOS11或更早版本。它支持OfflineAudioContext,但渲染仅在由用户操作(例如按钮点击)触发时启动。如果context.startRendering不是由用户操作触发,则context.state将会suspended,并且渲染将无限期挂起,除非您添加超时。目前仍在使用此 iOS 版本的用户并不多,因此我们决定为他们禁用音频指纹识别。
第二种情况是 iOS12或更新版本的浏览器。如果页面在后台运行,它们可能会拒绝启动音频处理。幸运的是,浏览器允许您在页面返回前台时恢复处理。
当页面激活时,我们会尝试context.startRendering()多次调用,直到context.state变为running。如果多次尝试后处理仍未启动,代码将停止。我们还会在重试策略的基础上使用常规方法,setTimeout以防出现意外错误或卡顿。您可以在此处查看代码示例。
Tor
在 Tor 浏览器中,一切都很简单。Web Audio API 在那里被禁用,因此无法进行音频指纹识别。
勇敢的
对于 Brave 来说,情况就更加微妙了。Brave 是一款基于 Blink 的注重隐私的浏览器。它以对音频样本值进行轻微随机化而闻名,它称之为“farbling”。
Brave 的术语“Farbling”是指对半识别性浏览器功能的输出进行轻微随机化,使其难以被网站检测到,但不会破坏良性的、为用户提供服务的网站。这些“farbled”值是使用每个会话、每个 eTLD +1 种子确定性生成的,因此,一个网站在同一会话中每次尝试指纹识别时都会获得完全相同的值,但不同的网站会获得不同的值,而同一个网站在下一次会话中也会获得不同的值。这项技术源于先前的隐私研究,包括PriVaricator(Nikiforakis 等人,WWW 2015)和FPRandom(Laperdrix 等人,ESSoS 2017)项目。
Brave 提供三个级别的 farbling(用户可以在设置中选择他们想要的级别):
- 已禁用 — 不应用任何 farbling。指纹与其他 Blink 浏览器(例如 Chrome)相同。
- 标准 — 这是默认值。音频信号值会乘以一个固定值,称为“模糊”因子,该值在用户会话中对于给定域是稳定的。实际上,这意味着音频波听起来和看起来都一样,但存在一些细微的变化,这使得其难以用于指纹识别。
- 严格——声波被伪随机序列取代。
farbling通过转换原始音频值来修改原始的 Blink 。AudioBuffer
恢复勇敢的标准
要恢复模糊数据,我们首先需要获取模糊因子。然后,我们可以通过将模糊值除以模糊因子来恢复原始缓冲区:
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% |
| Windows 10,Chrome 89 | 124.04347527516074 | 0.00000431% |
| Windows 10,Brave 1.21 | 浏览器重启后各种指纹: 124.04347610535537 124.04347187270707 124.04347220244154 124.04347384813703 |
0.00000364% – 0.00000679% |
| Android 11,Chrome 89 | 124.08075528279005 | 0.03% |
| Android 9,Chrome 89 | 124.08074500028306 | 0.03% |
| ChromeOS 89 | 124.04347721464 | 0.00000275% |
| macOS 11,Safari 14 | 35.10893232002854 | 71.7% |
| macOS 11,Firefox 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 Worker中不可用,因此我们无法在那里计算音频指纹。
不同浏览器的性能总结
下表显示了在不同浏览器和设备上获取指纹所需的时间。该时间是在冷页面加载后立即测量的。
| 设备、操作系统、浏览器 | 指纹采集时间 |
|---|---|
| MacBook Pro 2015(Core i7),macOS 11,Safari 14 | 5毫秒 |
| MacBook Pro 2015(Core i7),macOS 11,Chrome 89 | 7毫秒 |
| 宏碁 Chromebook 314,Chrome OS 89 | 7毫秒 |
| Pixel 5、Android 11、Chrome 89 | 7毫秒 |
| iPhone SE1、iOS 13、Safari 13 | 12毫秒 |
| Pixel 1,Android 7.1,Chrome 88 | 17毫秒 |
| Galaxy S4,Android 4.4,Chrome 80 | 40毫秒 |
| MacBook Pro 2015(Core i7),macOS 11,Firefox 86 | 50毫秒 |
音频指纹识别只是整个识别过程的一小部分。
音频指纹识别是我们开源库用来生成浏览器指纹的众多信号之一。然而,我们不会盲目地整合浏览器中所有可用的信号。相反,我们会分别分析每个信号的稳定性和独特性,以确定它们对指纹准确性的影响。
对于音频指纹,我们发现信号对独特性的贡献很小,但非常稳定,导致指纹准确度的净增长很小。
您可以在我们的浏览器指纹识别初学者指南中了解有关稳定性、独特性和准确性的更多信息。
亲自尝试浏览器指纹识别
浏览器指纹识别是各种反欺诈应用中一种有效的访客识别方法。它尤其适用于识别那些试图通过清除 Cookie、隐身模式浏览或使用 VPN 来规避追踪的恶意访客。
您可以尝试使用我们的开源库自行实现浏览器指纹识别。FingerprintJS 是最流行的浏览器指纹识别库,在 GitHub 上拥有超过 100 个12Kstar。
为了提高识别准确率,我们还开发了FingerprintJS Pro API,它利用机器学习将浏览器指纹识别与其他识别技术相结合。您可以免费试用 FingerprintJS Pro 数10日,没有任何使用限制。
联系我们
- 加星标、关注或 fork 我们的GitHub 项目
- 请将您的问题发送至oss@fingerprintJS.com
- 订阅我们的新闻通讯以获取最新信息
后端开发教程 - Java、Spring Boot 实战 - msg200.com




