起因
起因是 和桃子的讨论,一开始 blog 所用的 gravatar 是通过对 email 进行哈希得到链接,此处的哈希算法采取了 md5,而桃子发现他邮箱对应的 md5 已被彩虹表逆向,建议我也对头像做正向代理,避免出现被逆向导致隐私泄露的情况。
至于泄露邮箱有什么风险:鉴于 gravatar 是以邮箱作为唯一标识,一旦哈希值被逆向,别有用心之人就可以拿该邮箱冒充所有者,或是发邮件进行骚扰。
其实很早的时候就注意到他的 blog 已经对 gravatar 用的正向代理,当时他代理后的头像是和评论 ID 有关的。在他提出该情况以后,我了解到 gravatar 在近期启用了 sha256 作为新的哈希算法,但依然向下兼容了 md5。原本想着 sha256 应该比 md5 更安全,没想到将桃子的邮箱进行 sha256 哈希后再进行查表,发现这居然也被逆向了……逆天。
于是乎就有了本文。
架构
假设邮箱为 user@example.com
,那么对应的原始哈希即为 b4c9a289323b21a01c3e940f150eb9b8c542587f1abfd8f0e1cc1ffc5e475514
,将其拼接到 gravatar 获取 url 中即可获取对应头像。而我们依然需要生成对应的私有哈希,用以暴露在前端,并且在后端建立如下的对应关系:
既然邮箱和私有哈希、原始哈希都是一对一的关系,那么私有哈希和原始哈希自然也是一对一的关系。当访问类似于 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 错误时,即可载入预先设置好的自定义默认头像。
你这个私有哈希算法的考虑真的很深思熟虑🥵。
然后我就再想了一下我的算法,除非有人通过已有的邮箱列表再次对我的算法生成彩虹🌈表,否则应该是不能逆向了。
还有一点: 我算法中解决冲突时自增校验和的做法与加入缓存的顺序有关,所以最终的哈希值其实也不完全是重启和保持一致的。
不过没关系,谁没事引用我评论区头像的链接呢?
就像早期我还维护了几个版本的 API,后期基础设施搞好、自动升级搞好后,API 的版本就定格在
v3
了,看不惯的代码直接干掉,不向后兼容。我当时有看过你的算法,虽然不是很懂 Go 语言,但还是勉强看懂了。如果是我,我会考虑重启以后,从数据库中查询已经通过审核的邮箱,并按照时间排序、去重,再根据算法批量生成哈希置于缓存中。
终究是错付了🙃。
前面那条写错了,麻烦删一删🥵。
我刚改完去 jQuery 化,刚刚重启服务,就收到你的评论了🥵要是在早一点你应该会看到 502 Bad Gateway🥵🥵
哈哈哈哈,风风火火,恍恍惚惚。!