恢复代码……还是只有一个恢复代码?
发布: (2026年2月12日 GMT+8 16:08)
5 分钟阅读
原文: Dev.to
Source: Dev.to
概述
身份验证的讨论通常聚焦于:
- Passkeys(通行密钥)
- TOTP(一次性密码)
- 硬件令牌
- 无密码登录
恢复机制很少得到同等严格的分析,然而恢复往往决定了实际的安全边界。如果登录很强但恢复很弱,系统就不安全。
备份代码列表(5–10 个一次性代码)
属性
- 多个静态令牌
- 一次性使用
- 可再生成的列表
常见问题
- 用户未妥善保存
- 被截图保存
- 代码被遗忘
- 未进行熵分析
- 因为“代码很多”而假设安全
安全考虑
- 熵才是关键。
- 恢复实际上是委托的身份验证。
- 安全性取决于:
- 电子邮件账户的保护
- SIM 卡的安全
- 外部攻击面
这会改变威胁模型,而不是强化它。
助记词(12–24 个单词)
特点
- 私钥的恢复
- 高熵
- 期望离线存储
可用性
- 加密学上严谨,但可用性成本高。
假设
- 攻击者拥有恢复端点的访问权限
- 没有数据泄露
- 没有内部妥协
- 攻击纯粹是在线进行
安全取决于
- 熵
- 生成质量
- 速率限制
而不是发行的代码数量。
测试配置
- 长度: 15 个字符
- 字母表大小: 31 个符号(排除
0/O、1/I/L等易混字符) - 生成方式: CSPRNG(
crypto.randomBytes)并使用拒绝抽样(无模运算偏差) - TTL: 无固定 TTL
- 速率限制: 严格;在数分钟内仅允许少量尝试
- 轮换: 成功使用后自动轮换
熵计算
- 字母表大小: 31
- 总搜索空间: (31^{15})
- 熵: ≈ 78 位
激进攻击场景
- 尝试次数: 大约 150 万次/年
- 成功概率: 可忽略不计(对在线攻击而言实际上不可行)
发行 10 个代码并不会使单次尝试的熵成倍增长;攻击者仍然一次只猜一个有效代码。
安全取决于
- 每账户的速率限制
- 每标识符的限流
- 每个代码的熵
- 正确的随机生成
- 安全的存储模型
而不是交给用户的可打印令牌数量。
随机性
// Use a cryptographically secure PRNG
const crypto = require('crypto');
function generateCode(length, alphabet) {
const bytes = crypto.randomBytes(length * 2); // oversample
let result = '';
let i = 0;
while (result.length < length && i < bytes.length) {
const idx = bytes[i] % alphabet.length;
// Rejection sampling to avoid modulo bias
if (bytes[i] < 256 - (256 % alphabet.length)) {
result += alphabet[idx];
}
i++;
}
return result;
}
- 避免使用
Math.random()。 - 在将字节映射到字母表时使用拒绝抽样,以消除模运算偏差。
速率限制
关键点
- 对每个恢复标识符进行限制(不仅仅是对 IP)。
- 结合每账户和每 IP 的限制。
- 如有必要,加入指数退避。
轮换
成功恢复后:
- 使先前的代码失效。
- 生成新代码。
- 避免长期使用静态令牌。
结论
恢复不是事后才考虑的用户体验;它是身份验证的原语。如果恢复熵和尝试控制得到恰当设计,单个恢复代码 在在线威胁模型下完全足够。
真正的问题不是 “需要多少代码?” 而是 “熵是多少,攻击面如何被控制?”