使用内置的`crypto`在Node.js中进行密码哈希处理

9

如何在node.js中只使用内置的crypto模块实现最佳的密码哈希和验证方法。

基本上需要做到以下几点:

function passwordHash(password) {} // => passwordHash
function passwordVerify(password, passwordHash) {} // => boolean

通常人们会使用bcrypt或其他第三方库来实现这个目的。我想知道内置的crypto模块是否已经足够强大,至少可以满足所有基本需求?

有一个叫做scrypt()的函数,看起来就是为此目的而生的,但没有经过验证的对应函数,而且好像没有人关心它

3个回答

14
import { scrypt, randomBytes, timingSafeEqual } from "crypto";
import { promisify } from "util";

// scrypt is callback based so with promisify we can await it
const scryptAsync = promisify(scrypt);

哈希过程有两种方法。第一种方法是将密码进行哈希,第二种方法是需要将新的登录密码与存储的密码进行比较。我使用TypeScript详细编写了所有内容。

export class Password {

  static async hashPassword(password: string) {
    const salt = randomBytes(16).toString("hex");
    const buf = (await scryptAsync(password, salt, 64)) as Buffer;
    return `${buf.toString("hex")}.${salt}`;
  }

  static async comparePassword(
    storedPassword: string,
    suppliedPassword: string
  ): Promise<boolean> {
    // split() returns array
    const [hashedPassword, salt] = storedPassword.split(".");
    // we need to pass buffer values to timingSafeEqual
    const hashedPasswordBuf = Buffer.from(hashedPassword, "hex");
    // we hash the new sign-in password
    const suppliedPasswordBuf = (await scryptAsync(suppliedPassword, salt, 64)) as Buffer;
    // compare the new supplied password with the stored hashed password
    return timingSafeEqual(hashedPasswordBuf, suppliedPasswordBuf);
  }
}

测试一下:

Password.hashPassword("123dafdas")
  .then((res) => Password.comparePassword(res, "123edafdas"))
  .then((res) => console.log(res));

4
你可以使用 crypto.timingSafeEqual 来进行比较。 - disfated
2
这不应该是 randomBytes(16) 而不是 8 吗?即使官方的 Node 文档也建议使用 16 字节(最小)的盐。 - machineghost
现在你可以使用scryptSync()而不是等待scryptAsync()了。 - undefined

2
这是一个非常有趣的讨论,针对不同的问题提供了不同的解决方案。在我的研究中,我基于Node.js应用程序的简单性能分析提出了一种解决方案。
请查看下面的示例代码。
// add new user
app.get('/newUser', (req, res) => {
  let username = req.query.username || '';
  const password = req.query.password || '';

  username = username.replace(/[!@#$%^&*]/g, '');

  if (!username || !password || users[username]) {
    return res.sendStatus(400);
  }

  const salt = crypto.randomBytes(128).toString('base64');
  const hash = crypto.pbkdf2Sync(password, salt, 10000, 512, 'sha512');

  users[username] = { salt, hash };

  res.sendStatus(200);
});

验证用户身份认证尝试

// validating user authentication attempts
app.get('/auth', (req, res) => {
  let username = req.query.username || '';
  const password = req.query.password || '';

  username = username.replace(/[!@#$%^&*]/g, '');

  if (!username || !password || !users[username]) {
    return res.sendStatus(400);
  }

  const { salt, hash } = users[username];
  const encryptHash = crypto.pbkdf2Sync(password, salt, 10000, 512, 'sha512');

  if (crypto.timingSafeEqual(hash, encryptHash)) {
    res.sendStatus(200);
  } else {
    res.sendStatus(401);
  }
});


请注意,这些处理程序不建议用于在Node.js应用程序中验证用户,并仅用于说明目的。一般情况下,您不应该尝试设计自己的加密身份验证机制。最好使用现有的、经过验证的身份验证解决方案。

0
const password = "my_password"; 

// Creating a unique salt for a particular user
const salt = crypto.randomBytes(16).toString('hex'); 
  
// Hash the salt and password with 1000 iterations, 64 length and sha512 digest 
const hash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex');

在数据库中存储用户的salthash

const re_entered_password = "my_password";

// To verify the same - salt (stored in DB) with same other parameters used while creating hash (1000 iterations, 64 length and sha512 digest)
const newHash = crypto.pbkdf2Sync(re_entered_password, salt, 1000, 64, 'sha512').toString('hex');

// check if hash (stored in DB) and newly generated hash (newHash) are the same
hash === newHash;

感谢您的贡献。我看到这里有两个缺点(虽然我不是专家)。1)需要存储盐,而我认为它已经以某种方式存储在哈希中了。2)验证函数不应该具有与哈希函数相同的计算复杂度。 - disfated
  1. 在我看来,我们无法直接从“哈希”中提取“盐”。我们可以像其他加密库一样将“盐”和“哈希”存储在一个字段中(盐+哈希)。
  2. 我不确定验证相同的快捷方法。据我所知,我们应该使用相同的计算复杂度重新创建“哈希”以验证它是否与数据库中存在的“哈希”相同。希望这个答案能为您解决疑惑。
- Prathap Reddy
嘿@disfated,你有找到任何简单的解决方案吗?我有一点好奇想知道。如果有的话,你能否帮忙发布答案。谢谢。 - Prathap Reddy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接