恢复代码……还是只有一个恢复代码?

发布: (2026年2月12日 GMT+8 16:08)
5 分钟阅读
原文: Dev.to

Source: Dev.to

概述

身份验证的讨论通常聚焦于:

  • Passkeys(通行密钥)
  • TOTP(一次性密码)
  • 硬件令牌
  • 无密码登录

恢复机制很少得到同等严格的分析,然而恢复往往决定了实际的安全边界。如果登录很强但恢复很弱,系统就不安全。

备份代码列表(5–10 个一次性代码)

属性

  • 多个静态令牌
  • 一次性使用
  • 可再生成的列表

常见问题

  • 用户未妥善保存
  • 被截图保存
  • 代码被遗忘
  • 未进行熵分析
  • 因为“代码很多”而假设安全

安全考虑

  • 熵才是关键。
  • 恢复实际上是委托的身份验证。
  • 安全性取决于:
    • 电子邮件账户的保护
    • SIM 卡的安全
    • 外部攻击面

这会改变威胁模型,而不是强化它。

助记词(12–24 个单词)

特点

  • 私钥的恢复
  • 高熵
  • 期望离线存储

可用性

  • 加密学上严谨,但可用性成本高。

假设

  • 攻击者拥有恢复端点的访问权限
  • 没有数据泄露
  • 没有内部妥协
  • 攻击纯粹是在线进行

安全取决于

  • 生成质量
  • 速率限制

而不是发行的代码数量。

测试配置

  • 长度: 15 个字符
  • 字母表大小: 31 个符号(排除 0/O1/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 的限制。
  • 如有必要,加入指数退避。

轮换

成功恢复后:

  1. 使先前的代码失效。
  2. 生成新代码。
  3. 避免长期使用静态令牌。

结论

恢复不是事后才考虑的用户体验;它是身份验证的原语。如果恢复熵和尝试控制得到恰当设计,单个恢复代码 在在线威胁模型下完全足够。

真正的问题不是 “需要多少代码?” 而是 “熵是多少,攻击面如何被控制?”

0 浏览
Back to Blog

相关文章

阅读更多 »

KAIzen — AI 时代对敏捷的需求

一家游戏公司的小团队如何将流效率从 32% 提升到 85%——通过改变我们提供给 AI 的内容。我们的团队严格遵循 Scrum:两周的 s...