检查一个密码是否出现在泄露库里,天然要求你把密码交给某个「知道所有泄露密码」的机构。源钥选择了另一条路:让服务器先证明自己知道这个密码,再由你的设备判断答案。这篇拆解它的实现与取舍。
泄露检测到底在检测什么
所谓「泄露密码库」,是历年被攻破网站流出的凭证集合。它们的规模已经以十亿计,覆盖了大量真实用户密码,包括很多仍在使用中的邮箱与密码组合。
检测的本质问题因此变得清晰:你要拿自己的密码去查询一个「装着几亿条已知坏密码」的集合,而把密码直接发过去,等于把一份可能仍在使用的凭据亲手交出去。
源钥提供的方案是「不发送密码,只发送证明」。你的设备先对密码做一次哈希,与泄露库比对,但整个过程中密码本身不离开设备,服务端也无法从请求中还原出可用的明文。
- 泄露库包含数十亿条真实用户密码,规模持续增长
- 传统做法要求把密码发送给查询服务,等于交出凭据
- 我们的做法:密码不出设备,只发送不可逆的证明
k-匿名:把查询藏在人群里
k-匿名(k-anonymity)是隐私领域常用的分组技术:把一个人的查询拆成 N 份,每份都不足以定位到具体个人,只有把 N 份拼起来才能还原。
具体到密码检查,经典实现是「前缀方案」:客户端先算密码的 SHA-1,取其前 5 位十六进制(20 bit)作为前缀,向服务端请求该前缀下的全部哈希值(通常约 800 条),服务端原样返回这些哈希与后 40 位后缀。客户端在本地把每条哈希的 SHA-1 重算一遍,与你密码的完整哈希比对,命中即为泄露。
服务端在整个过程中只看到了一个 20 bit 的前缀,它无法从这批数据里判断你查的是哪一个密码——因为同一前缀下有几百上千个候选,你淹没在人群里。这就是 k-匿名的核心思想:服务器知道「有人在这堆密码里」,但不知道「是谁的密码」。
- 客户端发送 SHA-1 前 5 位(约 20 bit)作为前缀
- 服务端返回该前缀下全部哈希,约 800 条
- 命中判定完全在本地完成,服务端无法确定具体密码
hash = SHA1(password)
prefix = hash[0..5] // 发送
candidates = fetch(prefix) // 返回约 800 条
hit = candidates.some(h => h.suffix === hash[5..])我们为什么选择不上传
前缀方案已经相当安全,但它仍然要求把你的哈希前缀交给一个外部服务。源钥选择了更保守的方向:完全本地比对。
具体做法是:源钥维护一份经过处理的内置泄露密码集合,随客户端一起分发。为了让这份集合足够小又不失真,我们对泄露库做了去重与压缩处理,常见密码保留完整哈希,罕见密码以更紧凑的形式存储。你的设备在本地完成全部比对,网络请求一次都不发。
代价是这份集合需要随版本更新,客户端会定期从官方分发点获取更新后的集合(更新包本身经过签名校验)。收益是:即使我们的服务器被完全攻陷,攻击者也拿不到任何「谁查了什么」的流量——因为没有流量。
- 本地比对:密码与哈希都不离开设备
- 内置集合经过去重与压缩,兼顾体积与准确度
- 更新包经签名校验,客户端验证来源
实现细节:哈希、压缩与更新
泄露库里的密码基本都以哈希形式存在,常见的是 SHA-1(历史原因),近年也出现了 bcrypt 与 Argon2 的条目。由于 SHA-1 计算极快,它非常适合用来做批量匹配——这既是它出现在泄露库里的原因,也是我们在本地比对的效率来源。
为了控制体积,集合采用分层存储:出现频率高的密码以完整哈希保存,出现一次的低价值密码可以只保留一个短前缀(例如前 8 位十六进制)。这会引入极其有限的一次误报率——某个密码恰好与某个短前缀碰撞时会被标记为泄露——我们在界面上会用不同措辞区分「确认泄露」与「疑似出现在泄露库中」。
本地比对同样带来一个实际好处:离线也能用。在飞机上、在内网里、在断网的工位上,安全报告的泄露检测依然可以运行。
- SHA-1 条目可快速匹配,bcrypt / Argon2 条目按需校验
- 低频条目仅存短前缀,误报率极低但会明确区分措辞
- 离线可用:检测不依赖网络
误报与漏报
误报来自两处:一是短前缀碰撞造成的偶然命中,二是某些站点的「禁止使用的常见密码」列表被一并收录进了泄露集合——这类密码确实出现在泄露库里,但泄露并不意味着它已被盗用。
漏报则更现实:不是所有泄露都会公开数据,很多泄露只出现在暗网交易中,从未进入公开集合;此外,如果一个密码只在某个小站点泄露且从未出现在大型数据集中,我们的集合里也不会有它。
因此我们把泄露检测定位为「已知风险提示」,而不是「绝对安全证明」。命中之后该做的是立刻更换密码并开启二次验证;没有命中也不意味着可以放松其它习惯。
- 误报主要来自短前缀碰撞与「禁用密码」列表
- 漏报主要来自从未公开的暗网泄露
- 命中即更换,未命中也别放松其它习惯
命中之后该做什么
第一步,替换密码。直接丢弃旧密码,重新生成一个 20 位以上的随机密码,不要在原密码基础上做大小写或符号微调——微调后的密码在真实攻击里几乎等同于原密码。
第二步,检查这个密码是否被复用在其它条目。源钥的安全报告会把这部分列出来,让你一次改干净。
第三步,为这个账号开启二次验证,最好是通行密钥或硬件密钥,而不是短信——短信在 SIM 卡劫持场景下并不安全。
第四步,检查该账号是否留有自动登录会话。多数网站提供「退出所有设备」的选项,密码泄露后必须使用,否则旧的会话令牌依然有效。
- 直接换新密码,不要做微调式替换
- 同步修复复用了同一密码的其它条目
- 二次验证优先选通行密钥或硬件密钥
- 执行一次「退出所有设备」,清除旧会话
这项能力不做什么
泄露检测不判断你的密码是否会被在线爆破猜到,也不判断它是否在别的网站被复用过——后者由重复密码检测负责。它也不覆盖已经在使用通行密钥的账号,因为通行密钥本来就不存在「被猜中」这回事。
更重要的是,检测只覆盖已知集合。真正降低风险的是一整套做法:唯一密码、足够长度、通行密钥、二次验证、以及不把密码写在便利贴上。检测只是帮你把最危险的那一批先揪出来。
- 不判断在线爆破风险,也不判断复用(由重复检测负责)
- 只覆盖已知泄露集合,不等于绝对安全
- 真正的防线是唯一密码 + 通行密钥 + 二次验证
把这篇文章分享给同事
如果你正在推动团队迁移到通行密钥,这篇可能正好用得上。
苏未明
安全负责人
曾在安全厂商做攻防研究,负责漏洞响应、审计与安全白皮书。
关注方向:安全与合规