如何生成短的固定长度加密哈希?

信息安全 哈希
2021-08-26 00:59:45

我正在尝试使用没有状态的 node.js 服务器实现一种电子邮件验证系统。

战略

  1. 用户将他的电子邮件发送到服务器
  2. 服务器根据电子邮件地址生成一个 4 位代码,并通过电子邮件将其发送给用户。
  3. 用户通过电子邮件+电子邮件地址将收到的代码发送回服务器
  4. 服务器根据电子邮件重新生成 4 位代码,并将其与用户发送的代码进行比较。

我生成 4 位数代码的实现

  1. 使用 HMAC SHA-256 哈希函数创建 HEX 摘要
  2. 取摘要的前 3 个字符
  3. 将它们转换为整数
  4. 如果长度 < 4,0则在末尾连接一个或多个
const crypto = require('crypto')

const get4DigitsCode = (message) => {
  const hash = crypto
    .createHmac('sha256', Buffer.from(SECRET_KEY, 'hex'))
    .update(message)
    .digest('hex')

  const first3HexCharacters = hash.slice(0, 3)

  const int = parseInt(first3HexCharacters, 16)

  let code = int.toString()
  code =
    Array(4 - code.length)
      .fill(0)
      .join("") + code

  return code
}

在为 8293 个电子邮件地址生成代码后,我注意到我有 4758 个重复项。像这种类型的代码有这么多重复是否正常?我的策略和我的实施是否安全(猜测代码的能力)?

该服务是一个移动应用程序,基于电子邮件(“给自己的邮件”应用程序)。出于用户体验的原因,我想要一个 4 位数的代码。用户可以从电子邮件客户端通知中读取代码,轻松记住并在应用程序中输入(他永远不会离开)。无需繁琐的复制和粘贴,无需离开应用程序,只需阅读和输入即可。我知道多封电子邮件会生成相同的代码,但这并不重要,因为它只是用于验证电子邮件。我还将保护 API 免受暴力破解。

有人可以用这种策略猜出代码(有什么风险或可能的攻击?),我目前的实现是否正确?

更新

感谢@duskwuff的回答,一个更好的实现:

const crypto = require('crypto')

const get4DigitsCode = (message) => {
    const hash = crypto
        .createHmac('sha256', Buffer.from(SECRET, 'hex'))
        .update(message)
        .digest('hex');
    const first4HexCharacters = hash.slice(0, 4);
    const int = parseInt(first4HexCharacters, 16) % 10000;
    let code = int.toString();
    code =
        Array(4 - code.length)
            .fill(0)
            .join('') + code;
    return code;
};
2个回答

由于生日悖论,输出大小的密码散列函数有50% 的概率n发生√n冲突。

你取了 3 个十六进制数字,这意味着你得到了 12 位。在 2 6 =64 个哈希生成之后,您将看到 50% 的概率发生冲突。

由于固定大小的输出空间但输入空间的任意长度,哈希函数中的冲突是不可避免的。鸽巢原理告诉我们,碰撞是不可避免的。

如果你想要低碰撞概率,你必须使用更大的输出作为 128 位。对于 128 位,我们预计 2 64 个哈希中至少发生一次冲突,概率为 50%。

我们不接受系统是秘密的,我们假设它是已知的。电子邮件不是秘密,任何攻击者都可以使用您的源代码创建他的代码。由于短令牌,一个攻击者或一组攻击者可以尝试攻击您的系统。缓解措施正在增加令牌大小。

对于大多数应用程序来说,使用read("/dev/urandom", 16)会很好,或者您可以HMAC($email, $key)在不修剪的情况下使用。复制和粘贴对用户来说应该不是问题。

如果您需要更高的抵抗力,您可以使用数字签名;首先对电子邮件进行哈希处理,然后签名。

这种方法有几个非常严重的问题,这将增加碰撞风险,超出随机 4 位值通常预期的风险。

  1. 取摘要的前 3 个字符
  2. 将它们转换为整数

三个十六进制数字为您提供 0 – 4095 ( 0x000 - 0xfff) 的范围。对于 4 位数字,这会给您带来比预期更多的冲突,因为您只使用了 40% 的可能值。

使用摘要的较大摘录并使用模运算 ( % 10000) 将结果强制为四位十进制数字。出于数字原因,使用前 10 个十六进制数字(40 位)非常接近理想值。(10 位的倍数为您提供 1000 的粗略幂,40 位很大但低于最大安全整数 2 53。)

  1. 如果长度 < 4,则在末尾连接一个或多个 0

这引入了更多的碰撞。1 - 4、10 - 40 和 100 - 400 的值都映射到相同的 1000 - 4000 范围内,对于 5、50 和 500 等值集存在一些二次冲突。

如果您需要用零填充,请在左侧而不是右侧进行。