nodejs 通过 crypto模块实现加密解密

7,997 阅读6分钟

crypto 加密模块

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

  2. 使用 require('crypto') 来访问该模块

  3. 查看nodejs支持的加密算法,

    • 使用 crypto.getCiphers(),如下所示
        const crypto = require('crypto')
        crypto.getCiphers()
      
    • 得到的是一个比较大的数组,这里列举其中的几个元素:

      [ 'aes-128-cbc', 'aes-128-ccm', 'aes-128-cfb', 'aes256', 'aes256-wrap', 'aria-128-cbc', 'aria-128-ccm', 'aria-128-cfb', 'aria-128-cfb1', 'bf-cbc', 'bf-cfb', 'bf-ecb', 'camellia192', 'camellia256', 'cast', 'cast-cbc', ... 71 more items ]

    • 这里重点介绍 ASE 加密
      1. ASE加密是高级加密标准,为美国联邦政府采用的一种区块加密标准,高级加密标准已然成为对称密钥加密中最流行的算法之一。

      2. ASE使用的密钥长度可以128位、192位或256位,所以你可以看到加密算法:aes-128/196/256,表示的都是密钥的位数。最后一段是AES的工作模式,最常用的是 ECB、CBC、CFB、和OFB四种。

      3. ASE 加密时的key(原始密钥) 和 iv(初始化向量的长度)

        长度 密钥长度 向量长度
        128位 16 16
        192位 24 16
        256位 32 16
  4. crypto.scrypt(password,salt,keylen[,options],callback) 方法:

    • scrypt() 是一个异步的密钥派生函数,被设计为在计算和内存方面的成本都非常高,目的是使它无法暴力破解。
    • salt: 盐值,应该尽可能独特。建议盐值是随机的并且至少16个字节长。
    • keylen: 生成的密钥的长度。
    • callback 回调函数有两个参数:errderivedKey, 当密钥派生失败时, err 是一个异常对象,否则 err 为 null。 derivedKey 会作为 Buffer 传给回调。
  5. crypto.randomBytes(size[,callback]) 方法:

    • 生成加密的强伪随机数据。size 参数是生成随机数的字节数。
    • callback 回调函数有两个参数:errbuf 。如果发生错误,则err 是一个 Error 对象,否则为nullbuf 参数是包含生成字节的 Buffer

###Cipher

  1. Cipher
    • Cipher 是密码的意思。Cipher` 类实例用来加密数据。使用方式有以下两种,任选一种即可:
    • 作为一个可读可写的stream流,这样可以将原生未加密的数据写入并在可读侧生成加密的数据。
    • 使用 cipher.update 和 cipher.final 方法来生成加密数据
  2. crypto.createCipheriv(algorithm, key, iv[, options]) 方法:
    • 使用给定的 algorithm(算法)、key 和初始化向量 iv 创建并返回一个 Cipher 对象。
    • algorithm 取决于 OpenSSL,列如:'aes-128-cbc'
    • keyalgorithm 使用的原始密钥, iv 是初始化向量。两个参数都必须是 utf8 编码的字符串、BufferTypedArrayDataView
    • 初始化向量应该是不可预测的且唯一的,理想情况下,它们在密码上是随机的。
  3. cipher.update(data[, inputEncoding][, outputEncoding]) 方法:
    • 加密data ,生产加密数据。 如果指定了 inputEncoding 参数,则 data 参数是使用了指定的字符编码的字符串。 如果未指定 inputEncoding 参数,则 data 必须是一个 BufferTypedArrayDataView。 如果 data 是一个 BufferTypedArrayDataView,则 inputEncoding 会被忽略。
    • outputEncoding 指定了加密的数据的输出格式。 如果指定了 outputEncoding,则返回使用了指定的字符编码的字符串。 如果未提供 outputEncoding,则返回 Buffer
    • 可以使用新数据多次调用 cipher.update() 方法,直到 cipher.final() 被调用。
  4. cipher.final([outputEncoding]) 方法:
    • 返回任何剩余的加密内容。如果指定了 outputEncoding,则返回一个字符串。如果未提供 outputEncoding,则返回 Buffer
    • 一旦调用了 cipher.final() 方法,则 Cipher 对象就不能再用于加密数据。 如果试图多次调用 cipher.final(),则将会导致抛出错误。
  5. 注意:加密时要将生成密钥的密码:password ,盐值:salt 以及初始化向量 iv 一同写入密文,解密时通过密文来获取 passwordsaltiv

###Decipher

  1. Decipher 类的实例用于解密数据。 该类可以通过以下两种方式之一使用:
    • 作为可读写的流,其中写入加密的数据以在可读侧生成未加密的数据。
    • 使用 decipher.update()decipher.final() 方法生成未加密的数据。
  2. crypto.createDecipheriv(algorithm, key, iv[, options])方法:
    • 使用给定的 algorithm(算法)、 key 和初始化向量(iv)创建并返回一个 Decipher 对象。
    • algorithm :算法
    • key: 是 algorithm使用的原始密钥
    • iv : 初始化向量
  3. decipher.update(data[, inputEncoding][, outputEncoding]) 方法:
    • 解密使用 Cipher 类实例加密后的密文
    • inputEncoding : 如果指定了 inputEncoding 参数,则 data 参数是使用了指定的字符编码的字符串。
    • outputEncoding 指定了解密的数据的输出格式
  4. decipher.final([outputEncoding]) 方法:
    • 返回任何剩余的解密内容
    • 如果指定了 outputEncoding,则返回一个字符串。如果未提供 outputEncoding,则返回 Buffer
  5. 注意:解密时的算法,密钥密码, 盐值,初始化向量必须要和加密时的一致,否则就会出现下面常见的错误
    06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt
    

加密解密模块

加密解密模块实现:

  1. 加密解密模块被放在了一个叫 tools 的工具文件夹下面
  2. cipherCode.js
//============ 加密/解密数据模块 ===========
'use strict';
const crypto = require('crypto');

//------------------ 加密数据 -------------------
let algorithm = 'aes-128-cbc'; // algorithm 是算法的意思

/**
 * @description 加密数据
 * @description params: data--要加密的数据,必须是utf8格式的字符串;callback--处理结果集的回调函数
 * @param data {string}
 * @param callback {function(string)}
 */
let encrypt = function (data, callback) {
	let password = crypto.randomBytes(16).toString('hex');  // password是用于生产密钥的密码
	let salt = crypto.randomBytes(16).toString('hex'); // 生成盐值
	let iv = crypto.randomBytes(8).toString('hex'); // 初始化向量

	crypto.scrypt(password, salt, 16, function (err, derivedKey) {
		if (err) {
			throw err;
		} else {
			let cipher = crypto.createCipheriv(algorithm, derivedKey, iv); 	// 创建 cipher 实例

			// 加密数据
			let cipherText = cipher.update(data, 'utf8', 'hex');
			cipherText += cipher.final('hex');
			cipherText += (password+salt + iv);

			callback(cipherText)
		}
	});

};

/**
 * @description 解密通过 encrypt(); 加密的数据
 * @description param: cipherText--通过 encrypt() 加密的数据; callback--处理结果集的回调函数。
 * @param cipherText {string}
 * @param callback {function(string)}
 */
let decrypt = function (cipherText, callback) {
	let iv = cipherText.slice(-16);  // 获取初始化向量
	let salt = cipherText.slice(-48, -16);  // 获取盐值
	let password = cipherText.slice(-80, -48); // 获取密钥密码
	let data = cipherText.slice(0, -80);  //获取密文

	crypto.scrypt(password, salt, 16, function (err, derivedKey) {
		if (err) {
			throw err;
		} else {
			let decipher = crypto.createDecipheriv(algorithm, derivedKey, iv); 	// 创建 decipher 实例

			// 解密数据
			let txt = decipher.update(data, 'hex', 'utf8');
			txt += decipher.final('utf8');
			callback(txt)
		}
	});
};

//----------- 导出 加密/解密数据模块 --------------
module.exports = {
	encrypt,
	decrypt
};


  1. index.js
//=========== 工具模块 ==========
const cipher = require('./cipherCode.js');

module.exports = {
	cipher
};

测试

// =========== 测试加/解密模块 =============
const fs = require('fs');
const path = require('path');
const tools = require('../tools');


let str = '今年(应是2003年)是我大学毕业满10年的日子,也是我投身IT技术的第10年。';
tools.cipher.encrypt(str, function (result) {

	fs.writeFile(path.join(__dirname, '1.txt'), result, function (err) {
		console.log('加密数据写入成功');
	});
});


fs.readFile(path.join(__dirname, '1.txt'), 'utf8', function (err, info) {
	tools.cipher.decrypt(info, function (result) {
		console.log(result);
		console.log(result===str);
	});

});

  1. 测试结果

加密数据写入成功 今年(应是2003年)是我大学毕业满10年的日子,也是我投身IT技术的第10年。 true