在Node.js中使用crypto进行SALT和HASH密码

55

我正在尝试使用crypto模块在nodejs中对密码进行加盐哈希处理。我能够通过以下方式创建哈希密码:

UserSchema.pre('save', function(next) {
  var user = this;

  var salt = crypto.randomBytes(128).toString('base64');
  crypto.pbkdf2(user.password, salt, 10000, 512, function(err, derivedKey) {
    user.password = derivedKey;
    next();
  });
});

但是我对如何验证密码感到困惑。

UserSchema.methods.validPassword = function(password) {    
  // need to salt and hash this password I think to compare
  // how to I get the salt?
}
6个回答

78
无论您使用的是哪种持久化机制(数据库),都应该将生成的哈希值与盐和迭代次数一起存储,而这两者都应该是明文。如果每个密码使用不同的盐(建议如此),您还必须保存该信息。
然后,您将比较新的纯文本密码,使用相同的盐(和迭代次数)对其进行哈希处理,然后将字节序列与存储的密码进行比较。
要生成密码(伪代码):
function hashPassword(password) {
    var salt = crypto.randomBytes(128).toString('base64');
    var iterations = 10000;
    var hash = pbkdf2(password, salt, iterations);

    return {
        salt: salt,
        hash: hash,
        iterations: iterations
    };
}

验证密码(伪)

function isPasswordCorrect(savedHash, savedSalt, savedIterations, passwordAttempt) {
    return savedHash == pbkdf2(passwordAttempt, savedSalt, savedIterations);
}

啊,所以我必须要存储为每个用户创建的盐和哈希密码吗? - lostintranslation
6
如果你不知道生成散列值时使用的盐,就无法使用相同的明文重构出相同的散列值。 - Matthew
@Matthew 你好。你能给一个验证密码的代码示例吗?盐值让我有些困惑。我手动将盐值和哈希字符串连接起来并保存。我知道盐值是10个字符。我从保存的salt+hash中获取前10个字符以获取盐值。我使用pbkdf2对即将验证的密码进行哈希,使用相同的盐值。如果“原始盐值+哈希”=“待验证盐值+哈希”,那么我就让用户进入了?就这样吗? - slevin
在2022年,请使用 crypto.pbkdf2(password, salt, iterations, 128, 'sha256') 生成哈希值,并使用 crypto.timingSafeEqual() 进行比较。 - Francisco Gomes
你到底为什么要发布伪代码作为答案呢?我需要的是实际的代码,可以直接复制粘贴到我的项目中,包括导入和npm install/pnpm install。另外,为什么你要使用128字节的盐呢?256位/64字节的盐已经足够长,可以有效防止字典攻击。如果要将密码存储在SQL数据库中,最好将表与内存对齐,以优化从磁盘-RAM缓存加载的时间,所以你应该使用62字节的盐。256位/64字节的密码有一个空终止字符,所以65+63=128,这样就能保证内存对齐。 - Cale McCollough
1
@CaleMcCollough我发布了伪代码,这样人们就可以专注于逻辑而不是库。这个答案是十多年前发布的,当时的库可能与今天的库不具有相同的签名。答案还没有对哈希值的存储方式做出任何假设,也许你可以提交自己的答案来进一步扩展你所提到的问题。 - Matthew

32
根据http://nodejs.org/api/crypto.html上的Node.js文档,似乎没有特定的方法可以为您验证密码。要手动验证它,您需要计算当前提供的密码的哈希值,并将其与存储的密码进行比较以确定它们是否相等。基本上,您将使用数据库中存储的盐来处理挑战密码,而不是生成一个新的盐,并比较两个哈希值。
如果您并没有太过于依赖内置的crypto库,我可能会建议使用bcrypt。在安全方面,这两者大致相当,但我认为bcrypt拥有更加用户友好的界面。以下是如何使用它的示例(直接取自上面链接的bcrypt文档):
创建哈希:
var bcrypt = require('bcrypt');
var salt = bcrypt.genSaltSync(10);
var hash = bcrypt.hashSync("B4c0/\/", salt);
// Store hash in your password DB.

检查密码:

// Load hash from your password DB.
bcrypt.compareSync("B4c0/\/", hash); // true
bcrypt.compareSync("not_bacon", hash); // false

补充说明:

bcrypt的另一个优点是,genSalt函数的输出包含哈希和盐的字符串。这意味着您只需在数据库中存储单个项,而不是两个。还提供了一个方法,可以在哈希同时生成盐,因此您无需担心管理盐。

更新说明:

针对Peter Lyons的评论:您是100%正确的。我假设我推荐的bcrypt模块是JavaScript实现,因此异步使用不会在node的单线程模型上加速。事实证明这并非如此;bcrypt模块使用本地c ++代码进行计算,将异步运行得更快。Peter Lyons是正确的,您应该首先使用异步版本的方法,并仅在必要时选择同步方法。异步方法可能与同步方法一样慢,但同步方法始终很慢。


6
我认为在node.js问题中发布同步调用的示例是误导和令人困惑的。在几乎肯定是通过mongoose构建的网络服务中,您不能使用这样的函数。除非确定同步适合该情况,否则请只发布异步示例,但实际上它几乎从不适用。 - Peter Lyons
1
似乎最新的NIST建议使用pbkdf2而不是bcrypt。虽然不是要引发一场宗教战争。 - lostintranslation
你说的没错,NIST确实推荐使用pbkdf2。然而,由于这个问题中解释得非常清楚,在某些情况下bcrypt可以更安全。 - TwentyMiles
6
@TwentyMiles,这不是关于速度的问题,而是单线程的问题。在像Web应用程序这样的多用户网络服务中,如果您使用同步API,则整个节点进程将停止并且除了CPU密集型加密操作之外什么都不会做。这将导致所有其他网络客户端体验到服务器冻结。您不能在网络服务器中使用任何同步调用。这基本上违反了Node能够正常运行的最基本概念。参见:http://stackoverflow.com/questions/16827373/should-i-use-the-async-file-io-methods-over-their-synchronous-equivalents-for-lo/16827473#16827473 - Peter Lyons
3
@JonasDrotleff,你没有理解上下文。我在谈论异步与同步之间的区别,这是让一个用户等待缓慢加密(异步)与让所有客户端等待任何加密(同步)之间的区别。关键是“你不得在node.js网络服务器中使用同步调用”。事实上,这种调用在本例中是加密的只是偶然性。 - Peter Lyons
显示剩余2条评论

12

要么将密码和盐分别存储在您的数据库中的不同列中,或者(我更喜欢的方法),以与RFC 2307第5.3节兼容的格式将密码存储在您的数据库中。例如:{X-PBKDF2}base64salt:base64digest。您还可以在其中存储迭代次数,这样可以在将来为新帐户和更新密码的帐户增加迭代次数,而不会破坏其他人的登录。

来自我自己的Perl PBKDF2模块的示例哈希值如下所示:
{X-PBKDF2}HMACSHA1:AAAD6A:8ODUPA==:1HSdSVVwlWSZhbPGO7GIZ4iUbrk=,其中包括使用的特定哈希算法,迭代次数,盐和生成的密钥。


12

这是@Matthew的答案的修改版本,使用TypeScript

import * as crypto from "crypto";

const PASSWORD_LENGTH = 256;
const SALT_LENGTH = 64;
const ITERATIONS = 10000;
const DIGEST = "sha256";
const BYTE_TO_STRING_ENCODING = "hex"; // this could be base64, for instance

/**
 * The information about the password that is stored in the database
 */
interface PersistedPassword {
  salt: string;
  hash: string;
  iterations: number;
}

/**
 * Generates a PersistedPassword given the password provided by the user.
 * This should be called when creating a user or redefining the password
 */
export function generateHashPassword(
  password: string
): Promise<PersistedPassword> {
  return new Promise<PersistedPassword>((accept, reject) => {
    const salt = crypto
      .randomBytes(SALT_LENGTH)
      .toString(BYTE_TO_STRING_ENCODING);
    crypto.pbkdf2(
      password,
      salt,
      ITERATIONS,
      PASSWORD_LENGTH,
      DIGEST,
      (error, hash) => {
        if (error) {
          return reject(error);
        }

        accept({
          salt,
          hash: hash.toString(BYTE_TO_STRING_ENCODING),
          iterations: ITERATIONS,
        });
      }
    );
  });
}

/**
 * Verifies the attempted password against the password information saved in
 * the database. This should be called when
 * the user tries to log in.
 */
export function verifyPassword(
  persistedPassword: PersistedPassword,
  passwordAttempt: string
): Promise<boolean> {
  return new Promise<boolean>((accept, reject) => {
    crypto.pbkdf2(
      passwordAttempt,
      persistedPassword.salt,
      persistedPassword.iterations,
      PASSWORD_LENGTH,
      DIGEST,
      (error, hash) => {
        if (error) {
          return reject(error);
        }

        accept(
          persistedPassword.hash === hash.toString(BYTE_TO_STRING_ENCODING)
        );
      }
    );
  });
}

7

4

此场景涉及两个主要步骤。

1)创建和存储密码

在这里,您需要执行以下操作:

  • 获取用户密码
  • 生成随机字符串(盐)
  • 将盐与用户输入的密码组合
  • 哈希组合字符串。
  • 将哈希和盐存储在数据库中。

2)验证用户密码

此步骤需要对用户进行身份验证。

  • 用户将输入用户名/电子邮件和密码。

  • 基于输入的用户名获取哈希和盐

  • 将盐与用户密码组合

  • 使用相同的哈希算法对组合进行哈希处理。

  • 比较结果。

本教程详细解释了如何使用NodeJS crypto完成此操作。正是你所需要的。 使用NodeJS crypto盐散列密码


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