Node.js 之 Crypto模块

node 的 crypto 模块

crypto 模块是对 OpenSSL 的封装,主要功能有 哈希、对称加密以及非对称加密。

一、 哈希

  • hash

    通常给数据签名,它是不可逆的。 hash 算法有很多种,取决于当前机器 OpenSSL 的版本。

    # 查看 openssl 版本(以 Ubuntu 18.04 为例)
    openssl version

常用的 hash 算法有 md5sha256sha512,下面是一些运算例子

const crypto = require("crypto");
​
function createHash(text, hashtype) {
  const hash = crypto.createHash(hashtype).update(text).digest("hex");
  console.log(hashtype, hash, hash.length);
}
​
['md5', 'sha256', 'sha512'].forEach((type) => {
  createHash("hello", type);
});
​
// out:
// md5 5d41402abc4b2a76b9719d911017c592 32
// sha256 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 64
// sha512 9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043 128

这里单独介绍一下MD5加密:

1、概述:

MD5消息摘要算法,属Hash算法一类。MD5算法对输入任意长度的消息进行运行,产生一个128位的消息摘要(32位的数字字母混合码)。

2、MD5主要特点:

不可逆,相同数据的MD5值肯定一样,不同数据的MD5值不一样

var crypto = require('crypto');  //引入crypto模块
var md5 = crypto.createHash('md5');  
var message = 'hello';
var digest = md5.update(message, 'utf8').digest('hex'); //hex转化为十六进制
console.log(digest);
// 输出如下:注意这里是16进制
// 5d41402abc4b2a76b9719d911017c592
  • MAC加密

MAC(Message Authentication Code):消息认证码,用以保证数据的完整性。运算结果取决于消息本身、秘钥。 MAC可以有多种不同的实现方式,比如HMAC。

HMAC(Hash-based Message Authentication Code):可以粗略地理解为带秘钥的hash函数。 MAC加密

const crypto = require('crypto');
// 参数一:摘要函数
// 参数二:秘钥
let hmac = crypto.createHmac('md5', '123456');
let ret = hmac.update('hello').digest('hex');
console.log(ret);
// 9c699d7af73a49247a239cb0dd2f8139

Hmac

Hmac算法也是一种哈希算法,它可以利用 MD5 或 SHA256 等哈希算法,不同的是,Hmac 还需要一个密钥(俗称加盐)。

const crypto = require("crypto");
​
const result = crypto.createHmac("md5", "123456").update("hello").digest("hex");
​
console.log(result);
​
// out:
// 9c699d7af73a49247a239cb0dd2f8139

为什么要加盐? 因为相同密码的hash值是相同的,所以很容易通过彩虹表来破解密码(彩虹表可以简单理解为 明文->密文 的键值对),加盐后,破解的概率几乎为零。

二、对称加密以及非对称

加密/解密:给定明文,通过一定的算法,产生加密后的密文,这个过程叫加密。反过来就是解密

秘钥:为了进一步增强加/解密算法的安全性,在加/解密的过程中引入了秘钥。秘钥可以视为加/解密算法的参数,在已知密文的情况下,如果不知道解密所用的秘钥,则无法将密文解开。

  • 对称加密

    对称加密和前面的 hash 不同,它是可逆的。

    加密、解密所用的秘钥是相同的,即encryptKey === decryptKey。

    常见的对称加密算法:DES、3DES、AES、Blowfish、RC5、IDEA。

    AES有很多不同的算法,如aes192,aes-128-ecb,aes-256-cbc等

    加、解密伪代码

    encryptedText = encrypt(plainText, key); // 加密
    plainText = decrypt(encryptedText, key); // 解密

    对称加密算法也有很多种,取决于当前机器 OpenSSL 的版本。

    使用 openssl list-cipher-commands 可以查看支持哪些对称加密算法.

    常见的对称加密算法有 aes-256-cbc

    crypto 模块中提供了 createCipherivcreateDecipheriv 来进行加密和解密的功能:

    • crypto.createCipheriv(algorithm, key, iv)

    • crypto.createDecipheriv(algorithm, key, iv)

      这两个方法都接收 3 个参数:

    • algorithm:加密解密的类型

    • key: 加密解密的密钥

    • iv: 初始向量

下面是一个例子:

const crypto = require("crypto");
​
// 生成符合规范长度的密钥
function genkey(secret, length = 32) {
  return crypto.createHash('sha256').update(String(secret)).digest('base64').substr(0, length);
}
​
// 加密字符串
function encryptByAes256(content, secretkey, iv) {
  const cipher = crypto.createCipheriv('aes-256-cbc', genkey(secretkey), genkey(iv, 16));
  let enc = cipher.update(content, 'utf8', 'hex');
  enc += cipher.final('hex');
  return enc;
}
​
// 解密字符串
function decryptByAes256(content, secretkey, iv) {
  const decipher = crypto.createDecipheriv('aes-256-cbc', genkey(secretkey), genkey(iv, 16));
  let dec = decipher.update(content, 'hex', 'utf8');
  dec += decipher.final('utf8');
  return dec;
}
​
const encContent = encryptByAes256('hello', 'a', 'b');
​
console.log(encContent);
​
const decContent = decryptByAes256(encContent, 'a', 'b');
​
console.log(decContent);

注意 createCipherivcreateDecipheriv 对 key 和 iv 的长度有要求,所以上例子中用 genkey 做了一次转换。下面是 AES 算法对长度的具体要求(以 key 和 iv 都是字符串类型为例):

算法类型 key iv
aes128 16 16
aes192 24 16
aes256 32 16

另外,细心的同学会看到 aes256 具体有 aes-256-cbcaes-256-ecb 两种算法。aes-256-cbc 算法相对更加安全

  • 非对称加密

又称公开秘钥加密。加密、解密所用的秘钥是不同的,即encryptKey !== decryptKey。

加密秘钥公开,称为公钥。解密秘钥保密,称为秘钥

非对称加密又避免了私钥的传输,大大增加了安全性。

常见的非对称加密算法:RSA、DSA、ElGamal。

加、解密伪代码

encryptedText = encrypt(plainText, publicKey); // 加密
plainText = decrypt(encryptedText, priviteKey); // 解密

对比与应用:

除了秘钥的差异,还有运算速度上的差异。通常来说:

1、对称加密速度要快于非对称加密。 2、非对称加密通常用于加密短文本,对称加密通常用于加密长文本。 3、两者可以结合起来使用,比如HTTPS协议,可以在握手阶段,通过RSA来交换生成对称秘钥。在之后的通讯阶段,可以使用对称加密算法对数据进行加密,秘钥则是握手阶段生成的。 备注:对称秘钥交换不一定通过RSA,还可以通过类似DH来完成

下面是 node 使用非对称性加密的例子:

1、创建一个私钥:

openssl genrsa -out rsa_private.key 1024

2、根据私钥创建对应的公钥:

openssl rsa -in rsa_private.key -pubout -out rsa_public.key

3、在 node 中使用:

const crypto = require("crypto");
const fs = require("fs");
​
const pub_key = fs.readFileSync("./rsa_public.key");
const priv_key = fs.readFileSync("./rsa_private.key");
​
const text = "hello";
​
const secret = crypto.publicEncrypt(pub_key, Buffer.from(text));
const result = crypto.privateDecrypt(priv_key, secret);
​
console.log(secret); // buffer格式
console.log(result.toString());

1、Diffie-Hellman

DH算法是一种密钥交换协议,它可以让双方在不泄漏密钥的情况下协商出一个密钥来。

2、数学基础

DH算法的数学基础如下:

  • 假设 A = K^X mod M,已知 X 的情况下,很容易算出 A;已知道 A 的情况下,很难算出X;

  • (K^X mod M)^Y mod M = K^(X * Y) mod M

用 node 模拟 HD 算法过程

X 先选一个素数和一个底数:素数 M=97,底数 K=31(底数可以任选),再选择一个秘密整数 X=5,计算 A=K^X mod M=86,然后告诉Y:K=31,M=97,A=86; Y 收到 X 发来的 K,M,A后,也选一个秘密整数 Y=7,然后计算 B=K^Y mod M=2,并告诉X:B=2;

X 自己计算出 key=B^Y mod M=32; Y 也自己计算出 key=A^Y mod M=32;

因此,最终协商的密钥 key 为 32。

const K = 31  // 底数
const M = 97  // 取模(需要是质数)
​
const X = 5   // X 的私钥
const A = Math.pow(K, X) % M; // 86
​
const Y = 7   // Y 的私钥
const B = Math.pow(K, Y) % M; // 2
​
console.log(Math.pow(B, X) % M); // X 计算出的密钥:32
console.log(Math.pow(A, Y) % M); // Y 计算出的密钥:32

使用 crypto 中的 DH 算法

const crypto = require('crypto');
​
// X 生成 key
const X = crypto.createDiffieHellman(512);
​
const prime = X.getPrime();                       // 公开的随机数
console.log('Prime: ' + prime.toString('hex'));
​
const X_PublicKey = X.generateKeys();             // X 生成用于交换的 key
const X_PrivateKey = X.getPrivateKey();           // X 自己的私钥
​
console.log('X_PublicKey', X_PublicKey.toString('hex'));
console.log('X_PrivateKey', X_PrivateKey.toString('hex'));
​
// Y 生成 keys
const Y = crypto.createDiffieHellman(prime);      // 根据公开的随机数创建 Y
const Y_PublicKey = Y.generateKeys();             // Y 生成用于交换的 key
const Y_PrivateKey = Y.getPrivateKey();           // Y 自己的私钥
​
console.log('Y_PublicKey', Y_PublicKey.toString('hex'));
console.log('Y_PrivateKey', Y_PrivateKey.toString('hex'));
​
// 交换生成协商密钥:
console.log('Secret of X: ' + X.computeSecret(Y_PublicKey).toString('hex'));
console.log('Secret of Y: ' + Y.computeSecret(X_PublicKey).toString('hex'));
​
// out:
// Prime: c17a5d01bf84632f164b45cea4061602f0bb9d37b34ab6fcd8afff029172ce1bf3b6459244be0fe54c31b59efca7a439d58da18e90471def8a5d4195c0aa9bab
// X_PublicKey a4612c019051615204760e62623b04fc4740f33575704314f3f4286c79ed3b87d1764391e03df3b783c3455b31afb5c24e5db69345a93159ac6b81ef49f5f0b5
// X_PrivateKey 44b54320bc5f3c3fd12183f2d4cdee0fe097f2a9f014c8fdec08e0510be3b68bf713dbdd58b7a689f72fa63f8d778d4d3f2756736ed626dc72d3c118171dc024
// Y_PublicKey 473ba37e71c54034aa98492825301b587e483845cb176d42f663b81598b685706d05bfe2b5531e6873f799de067ffb8281245c8461d0a4d5a8897bf1cd6252f2
// Y_PrivateKey 6e8940a33f92f69d9dca023958db8e10f5a39180b243c68e18d4efbfcdee1d795ac608fef29294affc777a80fe301ea8ba8a32c71318d4914a99f53d7d2e9b32
// Secret of X: bf26f9654e2ec0c0c504ed321ab41b55d7014aabff0d89bb4d3a542fcf179342c293627443c3141cfe04c34cac357977bae610175e889d4d8557bce89e431b31
// Secret of Y: bf26f9654e2ec0c0c504ed321ab41b55d7014aabff0d89bb4d3a542fcf179342c293627443c3141cfe04c34cac357977bae610175e889d4d8557bce89e431b31

可以看到 X 和 Y 在没有暴露自己的私钥的情况下,最后生成了相同的密钥

3、数字签名

数字签名基于非对称加密,过程与非对称加密的过程正好相反,是使用私钥进行加密签名,然后使用公钥进行解密的签名验证。

数字签名主要有如下作用:

  • 验证发送者的身份

  • 保证信息传输的完整性

想了解细节的同学可以看阮一峰的 数字签名是什么? 这篇文章。

下面是在 node 中使用数字签名的例子:

const crypto = require("crypto");
const fs = require("fs");
​
const pub_key = fs.readFileSync("./rsa_public.key");
const priv_key = fs.readFileSync("./rsa_private.key");
​
const text = "hello";
​
// 生成签名
const sign = crypto.createSign("RSA-SHA256");
sign.update(text);
const signed = sign.sign(priv_key, "hex");
​
// 验证签名
const verify = crypto.createVerify("RSA-SHA256");
verify.update(text);
const verifyResult = verify.verify(pub_key, signed, "hex");
​
console.log("sign", signed); // 244ea5f09e474380669351d605c6868625daee6f35734859c30d1de9a1bc06f16359b198b1eb860e2ae9b42ecf591b473511f4a0243eed9ca1ebea7cabfd3d270a352ed752bbb95bc190a8d539538e19cb759162009fb984b3eb504ee02e7a182531dee16cb134b7c416767ff666a9f370aa09d394e15347087cedb074b5b695
console.log("verifyResult", verifyResult); // true

三、其他注意点

node

const crypto = require('crypto');
​
const md5hash = crypto.createHash('md5')
  .update('hello')
  .digest('hex');
console.log(md5hash);

js

使用第三方模块 md5js-md5 或者 crypto-js

const md5_1 = require('md5')
const md5_2 = require('js-md5')
const md5_3 = require('crypto-js/md5')
​
console.log(md5_1('hello'));
console.log(md5_2('hello'));
console.log(md5_3('hello').toString());

你可能感兴趣的:(node,node.js)