你开了无痕模式。你清空了 cookie。可网站依然把你当成十分钟前见过的那个访客。这一切之所以还在发生,一个不太起眼的原因,就藏在你屏幕的角落里:那个电量图标。
在过去十年的大部分时间里,任何网站都能向你的浏览器问出:你的笔记本究竟还剩多少电——精确到一个可疑的小数位——以及它还能撑多少秒。只要在相隔片刻的两个不同网站上读到这些数字,你就有足够的把握判定这两个访客其实是同一个人。本页上的这个面板此刻正在向你的浏览器提出同样的问题,并把答案摆给你看。
Battery Status API 交出的四个数字
调用 navigator.getBattery(),你会拿回一个带有四个字段的对象:level,取值 0 到 1;charging,为真或为假;chargingTime,距离充满还有多少秒;以及 dischargingTime,距离耗尽还有多少秒。没有序列号,也没有任何看起来像 ID 的东西。这正是它在 2010 年代初几乎没遇到什么阻力就走进浏览器的原因。一个电量百分比,又能泄露出什么呢?
结果是,熵。dischargingTime 单单一项,就落在一个宽达数千秒的区间里的某处。再把它和一个曾经精确到许多位小数的 level 配成一对,你就得到了一个几乎没有其他网民会在那一秒同时持有的值。它不是一个名字。它只是一个足够罕见的数字,罕见到能在一小段时间里当作名字来用。
两个网站如何靠它认出同一个你
这套机制早在 2015 年就被 Olejnik、Acar、Castelluccia 和 Díaz 在一篇名为 The leaking battery 的论文里讲清楚了。这个读数是一个带有存活期限的重新识别句柄。网站 A 记下你的 level 和 dischargingTime;片刻之后,网站 B 记下几乎一模一样的一对数字;这两者无需交换任何 cookie 就能被拼接到一起。它能挺过被清空的 cookie,也能挺过一次无痕模式的切换,因为这一切根本没有存在浏览器里。每次你去问,它都是从硬件上现读出来的。
这个时间窗很短。随着电池放电,数字会不断漂移,所以这种匹配只能维持大约三十秒到几分钟。可三十秒已经足够把两次页面加载缝合成同一个访客了。论文发表一年后,一次对上百万个网站的普林斯顿爬取就发现,真实的追踪脚本早已在真实环境中读取这个 API,可见这种攻击那时早已走出了实验室。
容量把戏:凭空读出 mAh
接下来是其他文章略过的部分。操作系统把 level 算作 charge_now / charge_full,这是两个以电池自身单位计的整数——在一台典型的 Linux 笔记本上就是微安时。所以你的浏览器报出的这个分数并不是随意的。它是一个比值,而它的分母,正是你电池的满充容量。Linux 上的 Firefox 曾经以完整精度交回这个原始比值,类似 0.9301926 这样,而从一个如此精确的读数出发,你就能把这个分数倒推回去,把藏在底下的分母还原出来。
这个分母就是以 mAh 计的你的容量,而它比 level 一直以来都更好用作标识符,因为电池的满充容量会随着老化而一点点下滑。它专属于你那块实实在在的电芯,对应它当下的损耗程度。现代 Chromium 基本把这条路堵死了——它把 level 四舍五入到整数百分比,于是你能倒推出来的分母就只剩 100,真正的容量也随之消失。本页上的这个面板会对你的实时读数运行这套还原,而它几乎必然会把结果报成已量化。这次失败,正是防护措施在尽职尽责。
Firefox 为什么干脆把整个功能删掉
Mozilla 没有去给电池 API 打补丁。它直接把它移除了——在 2017 年的 Firefox 52 里,整个 navigator.getBattery 接口面就此消失。Apple 则从一开始就没在 Safari 里实现过它。对于一项网页特性来说,这是个相当粗暴的处置,而它也告诉你,标准界后来是怎么看待这一个的:一个对被追踪者毫无好处的追踪向量。Chrome 和 Edge 至今仍带着它,如今被限制在安全的顶层页面里,数值也被向下取整,而规范本身如今开篇就摆着一条 2012 年时还没有的隐私警告。如果你是在 Firefox 或 Safari 里读这篇文章,那么上方的测试会返回一个干净的结果,因为那里根本没有东西可供回答。在手机上,这道分野同样成立:Android 上的 Chrome 也会暴露这个读数,而 iOS 上的 Safari 和 Android 版 Firefox 则什么都不交出。
从来就不只是电池
电池只是一份更长清单上的一行,而你的浏览器会把这份清单读给任何来问的网站,全程没有任何授权提示。navigator.hardwareConcurrency 交出你 CPU 的逻辑核心数。navigator.deviceMemory 报告你的内存,被粗化成 2 的幂——在桌面端长期被封顶在 8,尽管如今的 Chrome 也会给出 16 和 32。maxTouchPoints 说明你有没有触摸屏。WebGPU 适配器则以明文字符串交出你 GPU 的供应商和架构。这些单拎出来,哪一个都锁不定你。可一旦叠加起来,它们就能把你藏身其中的人群缩小到寥寥几人——就跟 canvas 渲染和音频处理在我们的 canvas 指纹和音频指纹拆解里所做的一样。完整浏览器扫描里的“硬件”面板,会展示你的浏览器此刻正在交出的确切数值。
到底该怎么办
- 先看清你到底泄露了什么,别急着假定自己已经无虞。 运行一下浏览器扫描,读一读电池和硬件面板。猜自己泄露了什么,和亲眼去看,是两码事。
- 无痕模式在这里帮不上忙。 这种重新关联走的是硬件,而不是 cookie,所以一个电池和 GPU 都相同的无痕窗口,依旧是同一个访客。无论你在哪儿指望着它,这一点都值得记住。
- Firefox 和 Safari 已经堵上了电池这条路。 如果你用的是 Chrome,那么保持版本更新,才能让 level 保持在被量化的状态,而不是原始精度。
- 如果你要同时打理好几个身份,就让整套硬件叙事保持一致。 只伪造一个数值、其余却对不上,恰恰是指纹识别系统正在搜寻的那种自相矛盾。Incogniton 在搭建每一个配置文件时,都会让它所呈现的电池、GPU、核心数和内存彼此对齐,凑成一台可信的机器,而不是一堆自相矛盾。
就一个电量图标,却泄露了这么多,实在不成比例。把你自己的浏览器对准这个扫描,看看你的浏览器如今还在说些什么。