返回知识中心
指纹

CreepJS canvas 种子循环:多少轮之后仍不重复

CreepJS 每一轮把 canvas 种子乘以 15000 并对 2001000001 取模。本探针测量序列在开始重复之前有多少轮保持不同。

BotBrowser Team

文档中心

想直接进入 指纹 文档吗?

这篇文章属于知识中心的指南库。若你要步骤化配置、参考说明和持续更新,请直接进入对应 docs 分区。

BotBrowser 可以在页面自己的 canvas 夹具上重放声明好的 profile,并保存页面绘制了什么的有限记录;它不能让短图画代表长图画,也不能改变一个库生成自己种子的方式。CreepJS 的 canvas 模块每一轮都从一个状态出发,把它乘以一个常数再对另一个常数取模,而这条序列在重复之前能保持多久不同,决定了长图画里有多少是新内容。 CreepJS Emoji 度量分析 将代表性的文本几何与这条绘制序列、像素输出分开。

摘要

  • CreepJS 把 canvas 种子初始化为 500,乘以 15000 再对 2001000001 取模,默认画 10 轮。
  • 这些公开取值在前 8 次抽样里给出 8 个不同值,在 10 轮里给出 10 个不同整数位置。
  • 乘法序列恰好在 26940 次抽样后重复,所以默认的 10 轮图画从不回到旧状态。
  • 这份图画的前 5 轮是 10 轮图画的前缀:短页面与长页面共享最先的 5 个值。
  • 只改种子会移动序列起点,不会改变长度:种子 500、501、1000、2000 都在 26940 处重复。
  • 只记一个 digest、不记轮数、种子和源码版本的报告,说不出图画里哪一部分是新的。

目录

第一栏显示每一轮把种子乘以 15000 并对 2001000001 取模。第二栏显示前八次抽样互不相同,以及默认十轮全部落在不同位置。第三栏显示序列在 26940 次抽样后回到起点,第四栏显示五轮图画是十轮图画的前缀。

公开的生成器

生成器保留一个小于模数的当前值,每次调用乘以 multiplier=15000 再取余;offset=2001000001 是模数,两个常数一起决定序列能走多长。

这个模块定义了一个小生成器,以及把它的原始值映射到 canvas 坐标的办法: 这个模块是 canvas 指纹,所以它的第一件事是画出在不同机器上读起来不同、在同一台机器上保持一致的图画。生成器给出确定的那一半,渲染器给出另一半。

source -> https://raw.githubusercontent.com/abrahamjuliot/creepjs/master/src/canvas/index.ts
source_bytes -> 22274 sha256=6bb5e580122bb26343e1fd8108fb2db6e5a719ab8beebf04394779f06139047d
defaults -> seed=500 offset=2001000001 multiplier=15000
rounds_default -> 10

生成器保留一个小于模数的当前值。每次调用把它乘以 15000 再取余,映射则除以模数、按 canvas 尺寸缩放并向下取整。两步都是整数运算,抽样那一刻没有随机性,所以同一份源码版本、同一个种子会给出同样的轮位置。

这是刻意的设计。canvas 指纹希望在同一台机器、同一个库版本上保持稳定,所以它不能在每次调用时都抽新像素。它从一个种子出发,其余都从种子推出来,而种子正是机器或 profile 提供的那一部分。 所以报告保存的是源码版本而不只是一个网址:master 上的文件会变,常数也可能跟着变。

多少轮之后仍不重复

循环长度也把两个问题分开:序列在重复之前能长多远,以及默认图画用掉多少轮。

报告的 first_draws_distinct -> 8 of 8 与 round_positions -> distinct=10 of 10 rounds 是十个连续生成器状态,而不是一轮画了多少状态。

用这些默认值,前几次抽样全都不同: 下面八个值是生成器在除以模数、缩放成像素之前的原始输出,所以两份 canvas 尺寸不同的报告仍然可以用它们比较。

first_draws -> 7500000 443999944 671156672 319074969 1733532609 1995122006 1875075045 69660944
first_draws_distinct -> 8 of 8
round_positions -> distinct=10 of 10 rounds

八个原始值、十个整数位置,没有重复。默认图画因此用到十个不同状态,每个落到 canvas 上一个不同位置。默认运行里没有任何东西从同一个状态画两次。 这个数字只覆盖生成器的十个连续状态,并不等于 CreepJS 一轮消耗多少状态:模块在每一轮中为渐变、阴影和绘制方法多次调用 getNextSeed(),所以一轮会前进不止一个状态。

循环长度远大于默认轮数:

cycle -> first_repeat_index=0 length=26940
cycle_covers_rounds -> true

序列在回到起始值之前走过 26940 次抽样。默认的 10 轮只用了这 26940 个状态里的 10 个,所以这幅图画是一条长序列里极小的一段前缀。若某页要画 500 轮,仍不会重复状态;画到 27000 轮就会碰到循环的边。

前缀对比较意味着什么

参数相同的短图画与长图画并不是从一开始就不同,而是后者包含前者: 稳定种子与渲染表面之间的这个差别,正是 canvas 指纹在表面一侧记录的内容。 页面也可以让一幅图画一直活着并往上追加;这种形态下第二幅的前五个状态并不是序列的前五个,所以前缀性质只在页面重新设种子时成立。

没有渲染层的记录分不出「序列不同」和「画出来的不同」,所以两半要一起保存。 最小记录包含 seed、rounds、源码摘要和 canvas 尺寸,并注明是哪个渲染器应答的。

源码版本与 canvas 尺寸要跟 digest 一起保存,因为单看 digest 分不开序列与渲染。

跨机器比较按这个顺序:源码版本、种子、轮数、canvas 尺寸、渲染器,最后才是 digest。

映射是向下取整而不是四舍五入:落在 37.9 的状态进像素 37,落在 38.1 的进像素 38,所以两个相邻状态可能共用坐标,也可能在小尺寸下分开。

尺寸这一层也值得单独看:50 像素的 canvas 每个轴上只有 50 个整数位置,所以两个不同状态可能落到同一坐标;canvas 越大,这类碰撞越少。

canvas.toDataURL() 的 digest 掩盖了是否加了第十轮:两种情况下 seed=500 的前几轮都相同。

报告描述的是请求,所以字段反映请求而不是画面:换渲染器后画面可能看起来一样,换尺寸后请求可能看起来一样,只有把两组字段分开的记录才能区分。 这对只保存一个 digest 的比较有直接影响:种子相同、轮数不同的两页会给出不同 digest,报告应把它读成长度变化,而不是浏览器变化。

prefix -> five_rounds_are_prefix_of_ten=true

页面增加一轮时,同样的不对称出现。前五个状态没被碰过,所以只比较共享区域的像素找不到差别,而整幅图的 digest 变了。只留整图 digest 的报告说不出加的是哪一轮。

这个结构也解释了两个来自不同页面的 CreepJS 报告为什么可以不同、却在描述同一台机器。生成器状态是确定的,所以会移动的只有种子、轮数和源码版本。三者都相同的两页给出同样的轮位置,只共享种子的两页从第一轮多出来的地方就分岔。不知道轮数的比较分不清这两种情况。 让同一次访问之间的图画保持一致,属于 噪声种子可复现性的问题,只是这里要保住的状态是 canvas 生成器。 乘数 15000 与模数 2001000001 是两个要保留的数字,因为改动其中任何一个描述的都是另一份源码,而不是另一台机器。

计数从哪里开始失去信息

有一个能用的数字来自模数和乘数,而不是种子: 循环长度是模算术的性质,不是种子的性质:模数与乘数固定时,任何与模数互素的起点都走同一个循环,而有公因子的起点走更短的循环。

multiplier=15000 与模数 offset=2001000001 固定了这个数字,所以报告可以把两者都写出来,并在比较两次运行之前核对 gcd。

seed:500 -> cycle=26940 draws_8_distinct=1
seed:501 -> cycle=26940 draws_8_distinct=1
seed:1000 -> cycle=26940 draws_8_distinct=1
seed:2000 -> cycle=26940 draws_8_distinct=1

所有试过的种子都在 26940 次抽样处重复,因为模数和乘数对任何与模数互素的起点都固定了循环长度。种子选的是图画从序列的哪个位置开始,它不改变序列有多长。

这一行的第二列是故意误导的,它演示一个测量陷阱。探针在单个表达式里为八次抽样分别新建了生成器,于是每次调用都重新从种子开始,返回同一个首值。这样写的探针会报出一个不同值,然后断定生成器已经退化。正确读数正是前面测到的:在一个实例上得到八个不同值。 只依据 digest 的报告,会混淆「重置生成器」与「继续用同一个生成器」的页面。

这个陷阱值得点明,因为它是种子探针最常见的假阳性来源。在循环里新建生成器,或者在抽样之间重置它,测到的都是同一个首值,从来没碰到序列本身。 multiplier=15000 与 offset=2001000001 要跟种子一起保存,因为缺了它们,序列就无法离线重算。

画超过 26940 轮的页面会看到重复状态,而画几千轮的页面仍然稳稳在循环之内;默认的 rounds=10 离这个边界很远。

记录一次 canvas 读数

canvas 读数支撑一个窄断言:这个源码版本、这个种子、这个轮数,产生了这些不同位置。四个字段让它保持可核对:

  1. 源码版本与摘要,因为乘数或模数一改,每一次抽样都变。
  2. 种子与轮数,因为它们决定图画用了序列的哪一段。
  3. 不同状态数与不同位置数,因为单看 digest 分不出是加了一轮还是挪了种子。
  4. 生成器的存活范围,因为每次抽样都重建,会把序列变成重复的首值。

字体表文章同样阅读第三套公开表,其中的计数与分组与 canvas 种子循环放在一起。

公开来源

实践结论

每次比较 canvas 都记下种子、轮数和源码版本,因为单个 digest 分不出更长的图画和不同的图画。种子与源码版本一致时,把五轮当作十轮的前缀,并预期 digest 仍然不同。把 26940 次抽样的循环理解成图画在重复之前能长多远,而不是默认十轮的性质。关于更长 canvas 比较背后的算术,读CreepJS 如何给 canvas 信号分组;关于页面侧读数在哪里结束、连接侧读数从哪里开始,读页面脚本无法评估的网络层规则。

#CreepJS#Canvas 指纹#伪随机种子#信号分析#隐私

本文属于指南: 什么是浏览器指纹?原理、风险、信号与隐私边界

让 BotBrowser 从研究走向生产

先用这些指南理解模型,再进入跨平台验证、隔离上下文和面向规模化的浏览器部署。