node.js_crypto 模块

crypto 模块提供了加密功能,实现了包括对 OpenSSL 的哈希、HMAC、加密、解密、签名、以及验证功能的一整套封装。

Hash 算法

Hash 类是用于创建数据哈希值的工具类。

查看 crypto 模块支持的 hash 函数:crypto.getHashes()

[ 'RSA-MD4',
  'RSA-MD5',
  'RSA-MDC2',
  'RSA-RIPEMD160',
  'RSA-SHA1',
  'RSA-SHA1-2',
  'RSA-SHA224',
  'RSA-SHA256',
  'RSA-SHA384',
  'RSA-SHA512',
  'blake2b512',
  'blake2s256',
  'md4',
  'md4WithRSAEncryption',
  'md5',
  'md5-sha1',
  'md5WithRSAEncryption',
  'mdc2',
  'mdc2WithRSA',
  'ripemd',
  'ripemd160',
  'ripemd160WithRSA',
  'rmd160',
  'sha1',
  'sha1WithRSAEncryption',
  'sha224',
  'sha224WithRSAEncryption',
  'sha256',
  'sha256WithRSAEncryption',
  'sha384',
  'sha384WithRSAEncryption',
  'sha512',
  'sha512WithRSAEncryption',
  'ssl3-md5',
  'ssl3-sha1',
  'whirlpool' ]

SHA-256

使用 hash.update() 方法将要计算的数据以流(stream)的方式写入,流输入结束后,使用 hash.digest() 方法计算数据的 hash 值。

const crypto = require('crypto');
// 创建哈希函数 sha256
const hash = crypto.createHash('sha256'); 

// 输入流编码:utf8、ascii、binary(默认)
hash.update('some data to hash', 'utf8');
// 输出编码:hex、binary、base64
console.log(hash.digest('hex'));

// 输出
// 6a2da20943931e9834fc12cfe5bb47bbd9ae43489a30726962b576f4e3993e50

PBKDF2

PBKDF2 是 Node.js 的 crypto 模块原生支持的标准方法。

const crypto = require('crypto');

function pbkdf2_encrypt(username, password) {
  // crypto.randomBytes()方法生成 32 字节的随机数 - 这里作为盐值
  crypto.randomBytes(32, (err, salt) => {
    if (err) throw err;

    // 参数列表:(密码,盐值,迭代次数,密钥长度,摘要函数)
    crypto.pbkdf2(password, salt, 4096, 512, 'sha256', (err, key) => {
      if (err) throw err;
      // 将用户名、密码哈希值和盐值存入数据库
      console.log(username, key.toString('hex'), salt.toString('hex'));
    });
  });
}

pbkdf2_encrypt('zhangSan', '123456');

同步函数示例:

const salt = crypto.randomBytes(32);
const result = crypto.pbkdf2Sync(password, salt, 4096, 512, 'sha256');

Hmac 算法

Hmac 类是用于创建加密 Hmac 摘要的工具。

示例一:

const crypto = require('crypto');
const key = 'BDvDYUmfdykkBLgX';
const hmac = crypto.createHmac('sha1', key.toString('ascii'));

hmac.update('data to crypt');
console.log(hmac.digest('hex')); 

// 输出
// a43bfb9f12b6f69ad9fcd4338a981efbed2569ae

示例二:

const crypto = require('crypto');

const secret = 'abcdefg';
const hash = crypto.createHmac('sha256', secret)
                   .update('I love cupcakes')
                   .digest('hex');
console.log(hash);

加盐(salt)

盐值就是随机数值,用于在计算密码的哈希值时加强数据的安全性,可以有效抵御诸如字典攻击、彩虹表攻击等密码攻击媒介。

常见的 Hash 算法使用示例:

const crypto = require('crypto');
const md5 = crypto.createHash('md5');

let password = '123456';
// 直接对密码原文进行 Hash
md5.update(password);
console.log(md5.digest('hex')); 

// 输出
// e10adc3949ba59abbe56e057f20f883e
// 可以通过 <https://www.cmd5.com/> 反向查询得到密码,不够安全。

加“盐”的 Hash 算法:

const crypto = require('crypto');
const md5 = crypto.createHash('md5');

let password = '123456';
let salt = 'hhug6dcKyCNBQ5sUC0i6hja5dCTqdSzV'; // 盐值
// 将密码拼接上任意长度的随机字符串后,再进行 Hash
md5.update(password+salt);
console.log(md5.digest('hex'));

// 输出
// b2d23b0443c319e574f1ea3f8bddc6e0

crypto.randomBytes() 方法可以生成随机数,用于作为密钥或者盐值:

const crypto = require('crypto');

// crypto.randomBytes() 生成强加密的伪随机数
crypto.randomBytes(16, (err, buf) => {
  if (err) throw err;
  console.log(`${buf.length} bytes of random data: ${buf.toString('hex')}`);
});

// 打印
// 16 bytes of random data: 411fd0a79188f2fde58a91ea0302f148

使用 Node crypto 模块为哈希函数生成随机的盐值:

const crypto = require('crypto');

// 1. 异步方法
crypto.randomBytes(32, (err, salt) => {
  if (err) throw err;

  // 记录盐值可读的字符串版本
  console.log('salt: ' + salt.toString('hex'));

  // 后续步骤:使用盐值
});

// 2. 同步方法
const buf = crypto.randomBytes(256);

加密(Cipher)和解密(Decipher)算法

对称加密算法

查看 Openssl 对称加密算法列表:

$ openssl list -cipher-algorithms
$ openssl list-cipher-algorithms # 旧版本命令

AES-256-GCM 示例1:

'use strict';

const crypto = require('crypto');

// 初始化参数
const text = 'Encryption Testing AES GCM mode'; // 要加密和解密的数据
const key = crypto.randomBytes(32); // 256 位的共享密钥
const iv = crypto.randomBytes(16); // 初始向量,16 字节
const algorithm = 'aes-256-gcm'; // 加密算法和操作模式

// 加密
const cipher = crypto.createCipheriv(algorithm, key, iv); // 初始化加密算法
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const tag = cipher.getAuthTag(); // 生成标签,用于验证密文的来源

// 解密
const decipher = crypto.createDecipheriv(algorithm, key, iv); // 初始化解密算法
decipher.setAuthTag(tag); // 传入验证标签,验证密文的来源
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');

console.log(decrypted); // Encryption Testing AES GCM mode

AES-256-GCM 示例2:

'use strict';

const crypto = require('crypto');

const aes256gcm = key => {
  const ALGO = 'aes-256-gcm'; // 加密算法和操作模式

  // encrypt returns base64-encoded ciphertext
  const encrypt = str => {
    // Hint: the 'iv' should be unique (but not necessarily random).
    // 'randomBytes' here are (relatively) slow but convenient for demonstration
    const iv = new Buffer.from(crypto.randomBytes(16), 'utf8'); // 初始向量,16 字节
    const cipher = crypto.createCipheriv(ALGO, key, iv); // 初始化加密算法

    // Hint: Larger inputs (it's GCM, after all!) should use the stream API
    let enc = cipher.update(str, 'utf8', 'base64');
    enc += cipher.final('base64');
    return [ enc, iv, cipher.getAuthTag() ];
  };

  // decrypt decodes base64-encoded ciphertext into a utf8-encoded string
  const decrpt = (enc, iv, authTag) => {
    const decipher = crypto.createDecipheriv(ALGO, key, iv);
    decipher.setAuthTag(authTag);
    let str = decipher.update(enc, 'base64', 'utf8');
    str += decipher.final('utf8');
    return str;
  };

  return { encrypt, decrpt };
};

// 加密密钥
// Note: a key size of 16 bytes will use AES-128, 24 => AES-192, 32 => AES-256
const KEY = new Buffer.from(crypto.randomBytes(32), 'utf8');

const aesCipher = aes256gcm(KEY);

// 加密
const [ encrypted, iv, authTag ] = aesCipher.encrypt('hello world');
// 解密
const decrypted = aesCipher.decrpt(encrypted, iv, authTag);

console.log(decrypted); // hello world

非对称加密算法的使用,1. 公钥加密-私钥解密;2. 私钥加密-公钥解密。

  • crypto.publicEncrypt(key, buffer)
  • crypto.publicDecrypt(key, buffer)
  • crypto.privateEncrypt(privateKey, buffer)
  • crypto.privateDecrypt(privateKey, buffer)

生成 RSA 密钥对

方法:crypto.generateKeyPair(type, options, callback) 生成给定类型的新非对称密钥对,目前仅支持的算法类型:RSA,DSA 和 EC。

参数:

  • type:<string> 类型,要生成的算法名称,必须是 'rsa''dsa' 或者 'ec' 之一;
  • options:<Object> 类型:
    • modulusLength:<number> 类型,密钥长度(RSA,DSA);
    • publicExponent:<number> 类型,公共指数(RSA),默认值:0x10001
    • divisorLength:<number> 类型,以比特为单位的 q 的大小(DSA);
    • namedCurve:<string> 类型,要使用的椭圆曲线名称(EC);
    • publicKeyEncoding:<Object> 类型,公钥编码格式:
      • type:<string> 类型,必须是 'pkcs1'(仅限 RSA)或 'spki' 之一;
      • format:<string> 类型,公钥输出格式,必须是 'pem''der' 之一;
    • privateKeyEncoding:<Object> 类型,私钥编码格式:
      • type:<string> 类型,必须是 'pkcs1'(仅限 RSA)或 'pkcs8''sec1'(仅限 EC) 之一;
      • format:<string> 类型,公钥输出格式,必须是 'pem''der' 之一;
      • cipher:<string> 类型,如果指定算法,将使用基于 PKCS#5 v2.0 规范的算法和密钥加密,使用给定的算法和密钥加密私钥;
      • passphrase:<string> 类型,用于加密的密钥;
  • callback:<Function> 类型:
    • err:<Error> 类型
    • publicKey:<string> 类型 | <Buffer> 类型,设置为 PEM 编码时返回 String 类型,DER 编码则返回 Buffer 类型
    • privateKey:<string> 类型 | <Buffer> 类型

建议将公钥编码为 'spki' 格式,将私钥编码为 'pkcs8' 格式并加密保存:

const { generateKeyPair } = require('crypto');
generateKeyPair('rsa', {
  modulusLength: 4096,
  publicKeyEncoding: {
    type: 'spki', // pkcs1、spki
    format: 'pem'
  },
  privateKeyEncoding: {
    type: 'pkcs8', // pkcs1、pkcs8、sec1
    format: 'pem',
    cipher: 'aes-256-cbc', // 对私钥使用 aes-256-cbc 算法加密后返回
    passphrase: 'top secret' // aes-256-cbc 算法密钥
  }
}, (err, publicKey, privateKey) => {
  // Handle errors and use the generated key pair.
});

使用 RSA 2048 进行公钥加密,私钥解密

使用 OpenSSL 生成 2048位 RSA 密钥:

# 使用 genrsa 命令生成密钥长度是 2048 比特的 RSA 密钥对
$ openssl genrsa -out mykey.pem 2048

# 从密钥对中分离出公钥
$ openssl rsa -in mykey.pem -pubout -out mypubkey.pem

使用公钥加密数据,再使用私钥解密数据:

const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const root = __dirname;

const publicKey = fs.readFileSync(path.join(root, './mypubkey.pem')).toString('ascii');
const privateKey = fs.readFileSync(path.join(root, './ mykey.pem')).toString('ascii');
const data = 'data to crypt';

// 公钥加密
const encryptData = crypto.publicEncrypt(publicKey, Buffer.from(data)).toString('base64');
console.log('encode', encryptData);

// 私钥解密
const decryptData = crypto.privateDecrypt(privateKey, Buffer.from(encryptData.toString('base64'), 'base64'));
console.log('decode', decryptData.toString());

签名(Sign)和验证(Verify)算法

签名算法示例:

const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const root = __dirname;

// 加载私钥文件
const pem = fs.readFileSync(path.join(root, './rsa_private_key.pem'));
const key = pem.toString('ascii');

const sign = crypto.createSign('RSA-SHA256'); // 创建签名算法
sign.update('data to sign'); // 更新待签名内容
const result = sign.sign(key, 'hex'); // 生成并返回签名
console.log('result:', result);

// 打印
// 3a26e0c3ec5ad1f517aad27f0f3a48cc0db156c2221800e4c2f995220f62a9077b4507a72ac6b0f1f50ee72386191d5ada4013945ff2afdb1ea6aa4e8d51a94f186089a67c2d6f4236150d59a1db134286fd7ba8deab0fc28f97eba7ad35820113f24dc6e30c6d11f0773527c4d81582c2b42016c9f972b5f4809e6d1f9115c2fb6db3168c68af3cfca986a458e3ea1c15606e7d33ee0bf5400ca460cf5371a51e800abe7ac943f16d9546239949bf7eec77488212d815b6a013e68fc2f456c4f2eb9d38c551e3f94168b076c3f761341afa3b724d8e73f486aacbace5a09412ef16c9b88182fcaeaaf97d96fdb402832530708a915d34c42131d7a26e4f04d4

验证算法示例:

const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const root = __dirname;

// 加载公钥文件
const publicKey = fs.readFileSync(path.join(root, './rsa_public_key.pem')).toString('ascii');

const verify = crypto.createVerify('RSA-SHA256'); // 创建验证算法
verify.update('data to sign');
console.log(verify.verify(publicKey, result, 'hex'));

// 打印
// true

DH 密钥交换算法

DiffieHellman 类是一个用来创建 Diffie-Hellman 键交换的工具。 DiffieHellman 类的实例可以使用 crypto.createDiffieHellman() 方法。

const crypto = require('crypto');
const assert = require('assert');

// Generate Alice's keys...
const alice = crypto.createDiffieHellman(2048);
const aliceKey = alice.generateKeys();

// Generate Bob's keys...
const bob = crypto.createDiffieHellman(alice.getPrime(), alice.getGenerator());
const bobKey = bob.generateKeys();

// Exchange and generate the secret...
const aliceSecret = alice.computeSecret(bobKey);
const bobSecret = bob.computeSecret(aliceKey);

// OK
assert.strictEqual(aliceSecret.toString('hex'), bobSecret.toString('hex'));

ECDH 密钥交换算法

ECDH 类是创建椭圆曲线 Diffie-Hellman(Elliptic Curve Diffie-Hellman (ECDH))键交换的实用工具。 ECDH 类的实例可以使用 crypto.createECDH() 方法。

crypto.getCurves() 方法会返回一个数组,其中包含支持的椭圆曲线的名称。

const crypto = require('crypto');
const assert = require('assert');

// Generate Alice's keys...
const alice = crypto.createECDH('secp521r1');
const aliceKey = alice.generateKeys();

// Generate Bob's keys...
const bob = crypto.createECDH('secp521r1');
const bobKey = bob.generateKeys();

// Exchange and generate the secret...
const aliceSecret = alice.computeSecret(bobKey);
const bobSecret = bob.computeSecret(aliceKey);

assert.strictEqual(aliceSecret.toString('hex'), bobSecret.toString('hex'));
// OK

参考

相关第三方库

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • Node.js是目前非常火热的技术,但是它的诞生经历却很奇特。 众所周知,在Netscape设计出JavaScri...
    Myselfyan阅读 4,061评论 2 58
  • Node.js是目前非常火热的技术,但是它的诞生经历却很奇特。 众所周知,在Netscape设计出JavaScri...
    w_zhuan阅读 3,607评论 2 41
  • https://nodejs.org/api/documentation.html 工具模块 Assert 测试 ...
    KeKeMars阅读 6,297评论 0 6
  • 2. NODE模块端实现 2.2 node模块的实现 引入模块: 路径分析 文件定位 编译执行 2.2.1 优先从...
    yozosann阅读 2,120评论 0 0
  • 当我想你的时候它是胡须总会不知不觉中长长当我想你的时候它是月亮总是在梦中滋生疯狂当我想你的时候它是迷茫总是毁灭所有...
    守海入梦阅读 1,118评论 29 34