起因

起因是 和桃子的讨论,一开始 blog 所用的 gravatar 是通过对 email 进行哈希得到链接,此处的哈希算法采取了 md5,而桃子发现他邮箱对应的 md5 已被彩虹表逆向,建议我也对头像做正向代理,避免出现被逆向导致隐私泄露的情况。

至于泄露邮箱有什么风险:鉴于 gravatar 是以邮箱作为唯一标识,一旦哈希值被逆向,别有用心之人就可以拿该邮箱冒充所有者,或是发邮件进行骚扰。

其实很早的时候就注意到他的 blog 已经对 gravatar 用的正向代理,当时他代理后的头像是和评论 ID 有关的。在他提出该情况以后,我了解到 gravatar 在近期启用了 sha256 作为新的哈希算法,但依然向下兼容了 md5。原本想着 sha256 应该比 md5 更安全,没想到将桃子的邮箱进行 sha256 哈希后再进行查表,发现这居然也被逆向了……逆天。

于是乎就有了本文。

架构

假设邮箱为 user@example.com,那么对应的原始哈希即为 b4c9a289323b21a01c3e940f150eb9b8c542587f1abfd8f0e1cc1ffc5e475514,将其拼接到 gravatar 获取 url 中即可获取对应头像。而我们依然需要生成对应的私有哈希,用以暴露在前端,并且在后端建立如下的对应关系:

graph LR A[邮箱] --自选哈希<br>算法--> B[私有哈希] B --建立<br>映射--> C A --SHA-256--> C[原始哈希]

既然邮箱和私有哈希、原始哈希都是一对一的关系,那么私有哈希和原始哈希自然也是一对一的关系。当访问类似于 https://qwq.me/api/v1/gravatar/私有哈希 的链接时,后端获取私有哈希,并根据一对一关系转换为原始哈希,而后根据原始哈希,通过 http 访问器获取 gravatar 头像返回前端,完成正向代理。

私有哈希

私有哈希的算法选择,决定了将其暴露在前端以后,被逆向的难度。此处我选择 SHA3-256 算法对加了盐的邮箱进行哈希。由于返回的字节长达 32 个,我将其折半后,将前半部分和后半部分按位异或,得到长度为 16 的字节数组,再通过 Sqids 生成短链接。

Sqids 接受若干个 unsigned long 类型的数据,并将其转换为 unique id。一个 unsigned long 占 8 字节,而上一步得到的私有哈希长度为 16 字节,正好能划分为两个 long,为将其转换为 sqids 提供了条件。

至于将 long 转换为 sqids 能接受的 unsigned long,可以借助额外的符号标识和绝对值来完成,譬如对于 {123, -456} 的数组,可以先转换为 {123, 0, 456} 的数组再进行 sqids 的转换,此时 0 即为符号标识,456 为 -456 的绝对值。最终即可生成暴露在前端的私有哈希。

缓存

一旦邮箱固定、算法固定,那么私有哈希和原始哈希都不会变更。为减少算法带来的开销,可将邮箱→私有哈希、私有哈希→原始哈希的对应关系放到缓存中。

正向代理获取到的头像,也可置于缓存中,并设置相应的 TTL。当命中缓存时,返回缓存中的头像;未命中时,重新获取头像返回,并再次置于缓存中。

默认头像

有时,邮箱所有者并未注册 gravatar 并上传头像,导致头像显示为 gravatar 的默认头像,可能会影响美观。此时可在做正向代理时,在 url 内拼接 d=404 参数,当邮箱所有者未设置头像时,返回 404 错误,而非默认头像。

当正向代理遇到 404 错误时,即可载入预先设置好的自定义默认头像。