如何使用Node.js Crypto创建HMAC-SHA1哈希?

238

我想创建一个带有密钥abcdegI love cupcakes 的哈希值。

如何使用 Node.js Crypto 创建该哈希值?

4个回答

418

加密相关文档:http://nodejs.org/api/crypto.html

const crypto = require('crypto')

const text = 'I love cupcakes'
const key = 'abcdeg'

crypto.createHmac('sha1', key)
  .update(text)
  .digest('hex')

“hex” 不是必需的,例如执行 Ruby 的 hmac 摘要等效操作时就不需要使用它。 - htafoya
9
为了验证一个哈希值,你应该使用crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b)):https://dev59.com/K10Z5IYBdhLWcg3wwyne#PrejEYcBWogLw_1bn4tn - baptx
1
圆圈已经完成:https://nodejs.org/api/crypto.html#crypto_crypto - Ricardo Tomasi
我该如何将密钥类型也设为十六进制? - kd12345

103
几年前,有人说update()digest()这两种方法是过时的,因此新的流API方法被引入。但现在文档表明这两种方法都可以使用。例如:
var crypto    = require('crypto');
var text      = 'I love cupcakes';
var secret    = 'abcdeg'; //make this your secret!!
var algorithm = 'sha1';   //consider using sha256
var hash, hmac;

// Method 1 - Writing to a stream
hmac = crypto.createHmac(algorithm, secret);    
hmac.write(text); // write in to the stream
hmac.end();       // can't read from the stream until you call end()
hash = hmac.read().toString('hex');    // read out hmac digest
console.log("Method 1: ", hash);

// Method 2 - Using update and digest:
hmac = crypto.createHmac(algorithm, secret);
hmac.update(text);
hash = hmac.digest('hex');
console.log("Method 2: ", hash);

测试过node v6.2.2和v7.7.2。

请参阅https://nodejs.org/api/crypto.html#crypto_class_hmac。提供了更多使用流式方法的示例。


2
我真的无论如何都做不到让它工作。hmac.read()返回一个"[object SlowBuffer]",如果我尝试使用hmac.read().toString('hex')读取内容,我得不到预期的值。如果我使用已弃用的update/digest方法,它会返回预期的字符串。我正在使用这个来验证第三方POST到我的服务器的签名。有什么想法是怎么回事吗? - AngraX
也许 hmac.read 是在数据被刷新到流之前发生的?或许 hmac.read 应该由流的完成事件驱动? - Dave
实际上,您发布的链接明确提到了使用 update 而不是 write。我很困惑,现在哪种做法是最佳实践?我找不到像您所提到的那样清晰明了的资源。 - SCBuergel
5
截至2016年11月,“digest”和“update”并未被弃用,并在文档中有相关说明:https://nodejs.org/api/crypto.html#crypto_class_hmac。如果您正在读取流,则建议仅使用流API。 - Ricardo Tomasi
@AdamGriffiths 两个都很好用。你推荐使用哪一个? - Raz
显示剩余2条评论

22

Gwerder的解决方案行不通,因为hash = hmac.read();在流被最终化之前发生。这就是AngraX的问题所在。此外,在这个例子中,hmac.write语句是不必要的。

改为这样做:

var crypto    = require('crypto');
var hmac;
var algorithm = 'sha1';
var key       = 'abcdeg';
var text      = 'I love cupcakes';
var hash;

hmac = crypto.createHmac(algorithm, key);

// readout format:
hmac.setEncoding('hex');
//or also commonly: hmac.setEncoding('base64');

// callback is attached as listener to stream's finish event:
hmac.end(text, function () {
    hash = hmac.read();
    //...do something with the hash...
});

更正式地说,如果您愿意,该行

hmac.end(text, function () {

可以被书写

hmac.end(text, 'utf8', function () {

因为在这个示例中,文本是一个UTF字符串


你错了,不需要添加回调函数。这个流是同步的,在调用end()之后就可以读取。最有趣的事情是它在官方文档中已经写明,但每个人都要发表自己的看法。 - stroncium
你是在恶意挑衅吗?也许你应该先阅读文档。如果你在完成事件之前尝试读取流,它会失败。 - Dave
1
它是一个既可读又可写的流。写入的数据用于计算hmac。一旦流的可写端结束,使用read()方法获取计算出的摘要。你可以在可写端结束时读取它,甚至不需要等待可读端变为可读状态(尽管它肯定会)。请阅读您的文档。 - stroncium
我已经在生产代码中使用它有一段时间了,它非常稳定。老实说,我不知道JSBin是什么,但我也尝试过将支持的代码复制粘贴到nodejs中,它也可以工作。您不应该想象文档中的其他含义。“ended”在文档中的任何地方都意味着“ended”。再次强调,您似乎误解了流具有两个方面的事实。并且在文档中明确说明人们可以在可写端结束时使用read(),而没有关于完成事件的任何内容。 - stroncium
甚至一些从世界各地读取hmac的抽象解决方案也不应该依赖于可写的finish事件,而是依赖于可读的readableend事件。 - stroncium
显示剩余2条评论

0

尽管有关于签名和验证哈希算法的所有示例代码,但我仍然需要进行实验和调整才能使其正常工作。这是我的工作示例,我相信它已经涵盖了所有边缘情况。

它是URL安全的(即不需要编码),它需要一个过期时间,并且不会意外地抛出异常。它依赖于Day.js,但您可以将其替换为另一个日期库或自己编写日期比较。

使用TypeScript编写:

// signature.ts
import * as crypto from 'crypto';
import * as dayjs from 'dayjs';

const key = 'some-random-key-1234567890';

const replaceAll = (
  str: string,
  searchValue: string,
  replaceValue: string,
) => str.split(searchValue).join(replaceValue);

const swap = (str: string, input: string, output: string) => {
  for (let i = 0; i < input.length; i++)
    str = replaceAll(str, input[i], output[i]);

  return str;
};

const createBase64Hmac = (message: string, expiresAt: Date) =>
  swap(
    crypto
      .createHmac('sha1', key)
      .update(`${expiresAt.getTime()}${message}`)
      .digest('hex'),
    '+=/', // Used to avoid characters that aren't safe in URLs
    '-_,',
  );

export const sign = (message: string, expiresAt: Date) =>
  `${expiresAt.getTime()}-${createBase64Hmac(message, expiresAt)}`;

export const verify = (message: string, hash: string) => {
  const matches = hash.match(/(.+?)-(.+)/);
  if (!matches) return false;

  const expires = matches[1];
  const hmac = matches[2];

  if (!/^\d+$/.test(expires)) return false;

  const expiresAt = dayjs(parseInt(expires, 10));
  if (expiresAt.isBefore(dayjs())) return false;

  const expectedHmac = createBase64Hmac(message, expiresAt.toDate());
  // Byte lengths must equal, otherwise crypto.timingSafeEqual will throw an exception
  if (hmac.length !== expectedHmac.length) return false;

  return crypto.timingSafeEqual(
    Buffer.from(hmac),
    Buffer.from(expectedHmac),
  );
};

你可以像这样使用它:

import { sign, verify } from './signature';

const message = 'foo-bar';
const expiresAt = dayjs().add(1, 'day').toDate();
const hash = sign(message, expiresAt);

const result = verify(message, hash);

expect(result).toBe(true);

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