加密简介

加密是以某种算法改变原有的信息数据,使得未授权用户即使获得了已加密信息,因不知解密的方法,无法得知信息真正的含义,通过这种方式提高网络数据传输的安全性,加密算法常见的有哈希算法、HMAC 算法、签名、对称性加密算法和非对称性加密算法,加密算法也分为可逆和不可逆,比如 md5 就是不可逆加密,只能暴力破解(撞库),我们在 NodeJS 开发中就是直接使用这些加密算法,crypto 模块提供了加密功能,包含对 OpenSSL 的哈希、HMAC、加密、解密、签名以及验证功能的一整套封装,核心模块,使用时不需安装。

哈希算法

哈希算法也叫散列算法,用来把任意长度的输入变换成固定长度的输出,常见的有 md5、sha1 等,这类算法实现对原数据的转化过程是否能被称为加密备受争议,为了后面叙述方便我们姑且先叫做加密。

  1. // 查看哈希加密算法的种类
  2. const crypto = require("crypto");
  3. // getHashes 方法用于查看支持的加密算法
  4. console.log(crypto.getHashes());
  5. // [ 'DSA', 'DSA-SHA', 'DSA-SHA1', 'DSA-cSHA1-old',
  6. // 'RSA-MD4', 'RSA-MD5', 'RSA-MDC2', 'RSA-RIPEMD160',
  7. // 'RSA-SHA', 'RSA-SHA1', 'RSA-SHA1-2', 'RSA-SHA224',
  8. // 'RSA-SHA256', 'RSA-SHA384', 'RSA-SHA512',
  9. // 'dsaEncryption', 'dsaWithSHA', 'dsaWithSHA1', 'dss1',
  10. // 'ecdsa-with-SHA1', 'md4', 'md4WithRSAEncryption',
  11. // 'md5', 'md5WithRSAEncryption', 'mdc2', 'mdc2WithRSA',
  12. // 'ripemd', 'ripemd160', 'ripemd160WithRSA', 'rmd160',
  13. // 'sha', 'sha1', 'sha1WithRSAEncryption', 'sha224',
  14. // 'sha224WithRSAEncryption', 'sha256',
  15. // 'sha256WithRSAEncryption', 'sha384',
  16. // 'sha384WithRSAEncryption', 'sha512',
  17. // 'sha512WithRSAEncryption', 'shaWithRSAEncryption',
  18. // 'ssl2-md5', 'ssl3-md5', 'ssl3-sha1', 'whirlpool' ]

md5 是开发中经常使用的算法之一,官方称为摘要算法,具有以下几个特点:

  • 不可逆;
  • 不管加密的内容多长,最后输出的结果长度都是相等的;
  • 内容不同输出的结果完全不同,内容相同输出的结果完全相同。

由于相同的输入经过 md5 加密后返回的结果完全相同,所以破解时通过 “撞库” 进行暴力破解,当连续被 md5 加密 3 次以上时就很难被破解了,所以使用 md5 一般会进行多次加密。

  1. // md5 加密 —— 返回 Buffer
  2. const crytpo = require("crytpo");
  3. let md5 = crytpo.createHash("md5"); // 创建 md5
  4. let md5Sum = md5.update("hello"); // update 加密
  5. let result = md5Sum.digest(); // 获取加密后结果
  6. console.log(result); // <Buffer 5d 41 40 2a bc 4b 2a 76 b9 71 9d 91 10 17 c5 92>

digest 方法参数用于指定加密后的返回值的格式,不传参默认返回加密后的 Buffer,常用的参数有 hex和 Base64,hex 代表十六进制,加密后长度为 32,Base64 的结果长度为 24,以 == 结尾。

  1. // md5 加密 —— 返回十六进制
  2. const crypto = require("crypto");
  3. let md5 = crypto.createHash("md5");
  4. let md5Sum = md5.update("hello");
  5. let result = md5Sum.digest("hex");
  6. console.log(result); // 5d41402abc4b2a76b9719d911017c592
  1. // md5 加密 —— 返回 Base64
  2. const crypto = require("crypto");
  3. let md5 = crypto.createHash("md5");
  4. let md5Sum = md5.update("hello");
  5. let result = md5Sum.digest("Base64");
  6. console.log(result); // XUFAKrxLKna5cZ2REBfFkg==

update 方法的返回值就是 this,即当前实例,所以支持链式调用,较长的信息也可以多次调用 update方法进行分段加密,调用 digest 方法同样会返回整个加密后的值。

  1. // 链式调用和分段加密
  2. const crypto = require("crypto");
  3. let result = crypto
  4. .createHash("md5")
  5. .update("he")
  6. .update("llo")
  7. .digest("hex");
  8. console.log(result); // 5d41402abc4b2a76b9719d911017c592

由于可以使用 update 进行分段加密,就可以结合流来使用,其实 crypto 的本质是创建 Transform 类型的转化流,可以将可读流转化成可写流。

  1. // 对可读流读取的数据进行 md5 加密
  2. const crypto = require("crypto");
  3. let fs = require("fs");
  4. let md5 = crypto.createHash("md5");
  5. let rs = fs.createReadSteam("./readme.txt", {
  6. highWaterMark: 3
  7. });
  8. // 读取数据并加密
  9. rs.on("data", data => md5.update(data));
  10. rs.on("end", () => {
  11. let result = md5.digest("hex");
  12. console.log(result);
  13. });

使用场景 1:经常被使用在数据的校验,比如服务器与服务器之间进行通信发送的明文摘要加** md5 加密摘要后的暗文,接收端拿到数据以后将明文摘要按照相同的 md5 **算法加密后与暗文摘要对比验证,目的是防止数据传输过程中被劫持并篡改。
使用场景 2:在浏览器缓存策略中,可以通过对静态资源的信息摘要使用** md5 **加密,每次向服务器发送加密后的密钥进行比对就可以了,不至于对整个文件内容进行比较。
缺点:由于规定使用** md5 **的哈希算法加密,别人可以使用同样的算法对信息进行伪造,安全性不高。

Hmac 算法

1、Hmac 算法的使用

Hmac 算法又称加盐算法,是将哈希算法与一个密钥结合在一起,用来阻止对签名完整性的破坏,同样具备 md5 加密的几个特点。

  1. // 使用加盐算法加密
  2. const crytpo = require("crytpo");
  3. let hmac = crytpo.createHmac("sha1", "panda");
  4. let result = hmac.update("hello").digest("Base64");
  5. console.log(result); // 7spMLxN8WJdcEtQ8Hm/LR9pUE3YsIGag9Dcai7lwioo=

crytpo.createHmac 第一个参数同 crytpo.createHash,为加密的算法,常用 sha1 和 sha256,第二个参数为密钥。
digest 方法生成的加密结果长度要大于 md5,hex 生成的结果长度为 64,Base64 生成的结果长度为 44,以 = 结尾。
安全性高于** md5,通过密钥来加密,不知道密钥无法破解,缺点是密钥传输的过程容易被劫持,可以通过一些生成随机密钥的方式避免。**

2、创建密钥的方法

可以安装 openSSH 客户端,并通过命令行生成存储密钥的文件,命令如下。
openssl genrsa -out rsa_private.key 1024
openssl genrsa 代表生成密钥,-out 代表输出文件,rsa_private.key 代表文件名,1024 代表输出密钥的大小。

  1. // 直接读取密钥文件配合加盐算法加密
  2. const fs = require("fs");
  3. const crytpo = require("crytpo");
  4. const path = require("path");
  5. let key = fs.readFileSync(path.join(__dirname, "/rsa_private.key"));
  6. let hmac = crytpo.createHmac("sha256", key);
  7. let result = hmac.update("hello").digest("Base64");
  8. console.log(result); // bmi2N+6kwgwt5b+U+zSgjL/NFs+GsUnZmcieqLKBy4M=

对称性加密

对称性加密是发送数据时使用密钥和加密算法进行加密,接收数据时需要使用相同的密钥和加密算法的逆算法(解密算法)进行解密,也就是说对称性加密的过程是可逆的,crytpo 中使用的算法为 blowfish。

  1. // 对称性加密
  2. const fs = require("fs");
  3. const crypto = require("crypto");
  4. const path = require("path");
  5. let key = fs.readFileSync(path.join(__dirname, "/rsa_private.key"));
  6. // 加密
  7. let cipher = crypto.createCipher("blowfish", key);
  8. cipher.update("hello");
  9. // final 方法不能链式调用
  10. let result = cipher.final("hex");
  11. console.log(result); // 3eb9943113c7aa1e
  12. // 解密
  13. let decipher = crypto.createDecipher("blowfish", key);
  14. decipher.update(result, "hex");
  15. let data = decipher.final("utf8");
  16. console.log(data); // hello

加密使用 crypto.createCipher 方法,解密使用 crypto.createDecipher 方法,但是使用的算法和密钥必须相同,需要注意的是解密过程中 update 中需要在第二个参数中指定加密时的格式,如 hex,在 final还原数据时需要指定加密字符的编码格式,如 utf8。
注意:使用对称性加密的字符串有长度限制,不得超过** 7 **个字符,否则虽然可以加密成功,但是无法解密。
缺点:密钥在传输过程中容易被截获,存在安全风险。

非对称性加密

非对称性加密相也是可逆的,较于对称性加密要更安全,消息传输方和接收方都会在本地创建一对密钥,公钥和私钥,互相将自己的公钥发送给对方,每次消息传递时使用对方的公钥加密,对方接收消息后使用他的的私钥解密,这样在公钥传递的过程中被截获也无法解密,因为公钥加密的消息只有配对的私钥可以解密。
接下来我们使用 openSSH 对之前生成的私钥 rsa_private.key 产生一个对应的公钥,命令如下。
openssl rsa -in rsa_private.key -pubout -out rsa_public.key
上面的命令意思根据一个私钥生成对应的公钥,-pubout -out 代表公钥输出,rsa_public.key 为公钥的文件名。

  1. // 非对称性加密
  2. const fs = require("fs");
  3. const crypto = require("crypto");
  4. const path = require("path");
  5. // 获取公钥和私钥
  6. let publicKey = fs.readFileSync(path.join(__dirname, "/rsa_public.key"));
  7. let privateKey = fs.readFileSync(path.join(__dirname, "/rsa_private.key"));
  8. // 加密
  9. let secret = crytpo.publicEncrypt(publicKey, Buffer.from("hello"));
  10. // 解密
  11. let result = crytpo.provateDecrypt(privateKey, secret);
  12. console.log(result); // hello

使用公钥加密的方法是 crytpo.publicEncrypt,第一个参数为公钥,第二个参数为加密信息(必须是 Buffer),使用私钥解密的方法是 crytpo.provateDecrypt,第一个参数为私钥,第二个参数为解密的信息。

签名

签名与非对称性加密非常类似,同样有公钥和私钥,不同的是使用私钥加密,对方使用公钥进行解密验证,以确保这段数据是私钥的拥有者所发出的原始数据,且在网络中的传输过程中未被修改。
image.png
我们还使用 rsa_public.key 和 rsa_private.key 作为公钥和私钥,crypto 实现签名代码如下。

  1. // 签名
  2. const fs = require("fs");
  3. const crypto = require("crypto");
  4. const path = require("path");
  5. // 获取公钥和私钥
  6. let publicKey = fs.readFileSync(path.join(__dirname, "rsa_public.key"), "ascii");
  7. let privateKey = fs.readFileSync(path.join(__dirname, "rsa_private.key"), "ascii");
  8. // 生成签名
  9. let sign = crypto.createSign("RSA-SHA256");
  10. sign.update("panda");
  11. let signed = sign.sign(privateKey, "hex");
  12. // 验证签名
  13. let verify = crypto.createVerify("RSA-SHA256");
  14. verify.update("panda");
  15. let verifyResult = verify.verify(publicKey, signed, "hex");
  16. console.log(verifyResult); // true

生成签名的 sign 方法有两个参数,第一个参数为私钥,第二个参数为生成签名的格式,最后返回的 signed 为生成的签名(字符串)。
验证签名的 verify 方法有三个参数,第一个参数为公钥,第二个参数为被验证的签名,第三个参数为生成签名时的格式,返回为布尔值,即是否通过验证。
使用场景:经常用于对 cookie 签名返回浏览器,当浏览器访问同域服务器将 cookie 带过来时再进行验证,防止 cookie 被篡改和 CSRF 跨站请求伪造。

总结

各种项目在数据传输时根据信息的敏感度以及用途进行不同的加密算法和加密方式,在 NodeJS 中,crypto 的 API 完全可以实现我们的加密需求,也可以将上面的加密方案组合使用实现更复杂的加密方案。