输入一个口令,这一页会在你的浏览器里算出它的 SHA-256,然后只把前 5 位发给我查一下: 攻击者在我们蜜罐里试过的口令里,有没有落在这个前缀上的。口令原文一个字节都不会离开这个页面 —— 不进步请求、不进控制台、不进浏览器存储、不进地址栏。
⚠ 不要在这里输入你的常用密码。 这一页虽然只发 5 位前缀,但"把真密码打进任何网页"本身就是个坏习惯: 浏览器插件、输入法、屏幕录制、共享屏幕都可能看到它。要试,请拿一个你想验证强度的旧口令 / 示例口令, 或者先用密码管理器生成一个再贴进来试。这一页不保存、不上传、不记录你输入的任何内容, 但它救不了"你自己把真密码交给一个网页"这件事。
算式:SHA-256(口令) → 64 位 hex → 取前 5 位 → GET /api/public/password?p=<5 位>。
后端只回「这个前缀桶里有哪些 hash 的后 59 位」,命中判定在你的浏览器里做完 ——
所以后端既拿不到你的口令,也拿不到"你查的是哪个 hash"。算完输入框会立刻被清空。
还没算过。输入口令后点「检查」。
还没查。上面输入一个口令后点「检查」—— 结果会告诉你:命中蜜罐真实爆破记录 / 只在公开弱口令榜上 / 这个前缀下没有记录 / 读不到(原因)。
| k-匿名:为什么只发 5 位 | SHA-256 是 64 位 hex、共 2²⁵⁶ 种可能;只发前 5 位 = 16⁵ = 1,048,576 个桶之一, 每个桶里平均有海量候选口令。后端看到的只是"有人问了 a1b2c 这个桶", 无法反推你输入的是哪一个 —— 这就是 k-匿名查询的通行做法(同类服务都这么做)。 代价写在明处:后 59 位确实会随响应下发,但那要求对方已经知道你的前缀,与直接猜口令是同一难度。 |
|---|---|
| bucketCount | 这 5 位前缀下所有口令的尝试次数合计 —— 不是"你这个口令被试了 N 次"。 一个桶里可能有几百个不同口令,页面文案一律写「同前缀桶合计」。 |
| 命中 / 榜上 / 未命中 | 命中:你的 hash 后 59 位出现在 hashes[] 里 —— 这个口令在蜜罐里被真实攻击者试过。榜上有名:出现在 listed[] 里 —— 它在我们收录的公开弱口令榜上,但蜜罐还没抓到过它。
这两件事不能混说:前者是"被真实试过",后者是"字典里有"。未命中:这个桶里没有它 —— 不代表绝对安全(见下面的数据来源)。 |
| truncated | 单个桶最多下发 200 条 hash(limits.maxHashesPerBucket)。桶更大时
truncated:true,页面会明确写「可能被截断,未命中不代表不在字典里」——
绝不把截断后的"没找到"说成"没被尝试过"。 |
| 限流与缓存 | 每 IP 每分钟 30 次(limits.perMinute),超了回 429 + 人话原因;
响应带 Cache-Control: public, max-age=300(limits.cacheSeconds),
同一个前缀 5 分钟内不会重复打到后端。限流是防刷,不是限制你检查自己的口令。 |
| 为什么会有 null | 字典索引没生成 / 格式坏 / 没配置 → 后端回 available:false +
notes 写清原因,页面写「读不到(原因)—— 不补数」。
读不到 ≠ 没命中 ≠ 安全,这三件事页面分得很开。 |
一句话口径:宁可说"读不到",也不给一个看起来像真的结论。
数据来源(两个):
① 自己蜜罐的真实爆破口令:最近 24 小时的 SSH / Web 登录尝试里,攻击者实际敲进来的那些
password 字段 —— 在日志所在的机器上就地被 SHA-256、按前 5 位分桶,
只把「hash + 次数 + 最后出现时间」写进索引文件;明文口令一个都不出那台机器,
本项目里也不存明文(见后端 scripts/password-dict-build.py 与
PasswordDictionaryService 的类注释)。
② 公开弱口令榜:业界公开的弱口令清单(rockyou / SecLists 一类),单独标成 listed[],
不混进「被真实尝试过」的计数里。
公开:某个 5 位前缀下有几条 hash、合计被尝试多少次、最后一次是什么时候、数据集生成时间与总量。
不公开:任何口令明文(本来就没有)、任何口令的完整 hash、哪个 IP 试过什么、真实账号与口令的对应关系
(蜜罐里没有真实账号)。
不做的:不记录你查了哪个前缀、不写任何文件、不触发任何处置动作;这个端点是一次纯读。
为什么不能说"你的密码已被泄露":我们只知道「这个口令字符串被攻击者试过」, 不知道它是谁的、也不知道它在哪个站还能用。反过来,「不在字典里」也只说明 它不在我们这两份数据里 —— 攻击者手里的私有字典比公开的大得多。
同一套诚实口径:/passwords/ 弱口令统计 · /ip/ 这个 IP 干过什么 · /api/ 公开端点清单
# ① 查一个前缀(a1b2c 换成你自己算出来的前 5 位) curl -s 'https://demo.xiaokeqing.cn/api/public/password?p=a1b2c' | head -c 800 # ② 对照:非法前缀 → 400 + 机读 reason(不是 500,也不是空对象) curl -s -o /dev/null -w '%{http_code}\n' 'https://demo.xiaokeqing.cn/api/public/password?p=A1B2C' # ③ 自己算前 5 位(口令只进本机的 sha256sum,不上网) printf '%s' 'example-password' | sha256sum | cut -c1-5
响应外层统一是 {code, message, data};data.available 为 false 时
bucketCount 是 null(不是 0),原因在 data.notes 里。
别名 /api/public/password.js 同一份数据,带 5 分钟缓存与 ETag。