← 公开接口 /api/
XK-LAB // 你的密码在攻击者字典里吗 PASSWORD-CHECK ● 待检查(本地算 hash,只发 5 位前缀)

输入一个口令,这一页会在你的浏览器里算出它的 SHA-256,然后只把前 5 位发给我查一下: 攻击者在我们蜜罐里试过的口令里,有没有落在这个前缀上的。口令原文一个字节都不会离开这个页面 —— 不进步请求、不进控制台、不进浏览器存储、不进地址栏。

⚠ 不要在这里输入你的常用密码。 这一页虽然只发 5 位前缀,但"把真密码打进任何网页"本身就是个坏习惯: 浏览器插件、输入法、屏幕录制、共享屏幕都可能看到它。要试,请拿一个你想验证强度的旧口令 / 示例口令, 或者先用密码管理器生成一个再贴进来试。这一页不保存、不上传、不记录你输入的任何内容, 但它救不了"你自己把真密码交给一个网页"这件事。

查一次本地 SHA-256 · 只发 5 位 hex · 只读 GET

算式: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/ 公开端点清单

自己复核 · 三条 curl只读 · 不触发处置
# ① 查一个前缀(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。