欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

在 Node.js 中玩转加密和解密:深入理解crypto模块的方法

最编程 2024-02-12 13:53:49
...
NodeJS 加解密之 crypto 模块.png

我们前端一般会很少接触加解密方面的知识,后端对这方面了解的可能比较多,但是作为一个不安分的前端,绝对有必要学习下加解密的知识,因为现在技术发展的方向,越来是往弱后端的方向发展的,一个血淋淋的事实就是,小程序的云开发,一个前端,同时做了前端后端和运维的活。

NodeJS 的加解密,不需要自己实现,也不需要调用第三方模块,有内部模块支持,这个模块就是 crypto。而且这些算法还不是使用 JS 写的,而是用 C/C++ 实现的,然后通过 cypto 这个模块暴露为 JavaScript 接口。那为什么不用 JS 写呢?因为用纯 JavaScript 代码实现这些功能速度会非常的慢。

好了,我们来看看 NodeJS 常用算法和如何通过 API 使用这些常用算法的。

一、哈希(hash)算法

哈希算法是摘要算法的一种,可以根据你提供的内容,生成一段哈希值,只要提供的内容不变,生成的哈希值就不会改变,而且从哈希值是不能反向推导出原始数据的(所以哈希算法也叫单向哈希算法)。

也就是说哈希算法只能加密不能反向解密。看下在 NodeJS 中如何使用:

const crypto = require("crypto");

const hash = crypto.createHash("md5");

hash.update("Condor");
hash.update("Hero");
const hashCode = hash.digest("hex");

console.log(hashCode); // 输出结果为: 9f29506741761b010f98f908ab8f9e04

createHash 方法传入需要加密的摘要算法,例如 MD5、SHA1、SHA256 和 SHA512。等。如果把案例的 MD5 改成 SHA1 ,加密之后的结果为 d17dfb160fce1ca89ecf94027a0bf39b6dda7f4e

update() 方法默认字符串编码为 UTF-8,也可以传入 Bufferupdate() 可以多次被调用,多次调用只是简单的把要加密的结果拼接起来,例如:

hash.update("CondorHero");

===

hash.update("Condor");
hash.update("Hero");

digest 表示加密之后的结果,以什么编码方式输出,比如:

  • latin1 可以认为是 ASCII 扩展
  • hex 十六进制
  • base64 前端更熟悉的 base64 编码方式

createHash 是根据原始文件,直接生成 MD5,这样完全可以使用 MD5 批量生成,然后存储起来撞库。为了防止这个现象,在生成 MD5 的时候,我们会给原始内容,手动加点东西,专业点叫加一个密钥,然后再生成 MD5。NodeJS 不需要我们这么干,因为有 createHmac,可以认为 Hmac 理解为用随机数「增强」的哈希算法,Hmac 是 Hash 的加强。createHmac 的使用很简单:

const crypto = require("crypto");

const hash = crypto.createHmac("md5", "customz_key");

hash.update("CondorHero");
const hashCode = hash.digest("hex");

console.log(hashCode); // bded9377f8766692d7c6ccdb38542d58

上面讲到了 base64 ,呐,我们知道浏览器默认支持 base64 式编码(btoa)和解码(atob),那么在 NodeJS 中如何实现呢?

二、NodeJS 中 base64 的编码和解码方式

我们需要一个引入一个新的 API 来完成这个。这个 API 就是 Buffer.from,它可把数据例如字符串转化成 buffer 。

  • Base64 编码,对应浏览器中的 btoa:
const name = "CondorHero";
const nameBuffer = Buffer.from(name); // 等同于 Buffer.from(name, "utf-8")
const enecodedName = nameBuffer.toString("base64");
console.log(enecodedName); // Q29uZG9ySGVybw==
  • Base64 解码:对应浏览器中的 atob
const base64Name = "Q29uZG9ySGVybw==";
const decodeBuffer = Buffer.from(base64Name, "base64"); // 第二个参数就不能省略了
const decodedName = decodeBuffer.toString("utf-8");
console.log(decodedName); // CondorHero

这里还有个知识要注意,Buffer 的 toString 和平常见到的 toString 方法使用不同,Buffer 的 toString 方法有三个参数,看下 toString 如何定义的:

(method) Buffer.toString(encoding?: BufferEncoding, start?: number, end?: number): string
  • BufferEncoding 编码
  • start 截取从 start 开始
  • end 截取到 end 结束

三、对称加密 DES / AES

先来看看 createCipheriv 的用法:

    crypto.createCipheriv(algorithm,key,iv [,options])
  • iv是初始化向量,可以 为空 或者 16 字节的字符串
  • key是加密密钥,根据选用的算法不同,密钥长度也不同,对应关系如下:
    1. des-cbc 对应 8 位长度密钥
    2. aes128 对应 16 位长度密钥
    3. aes192 对应 24 位长度秘钥
    4. aes256 对应 32 位长度密钥

DES 是 Data Encryption Standard(数据加密标准)的缩写。它是由 IBM 公司研制的一种对称密码算法,美国国家标准局于1977年公布把它作为非机要部门使用的数据加密标准,DES 是一种对称加密算法,密匙长度必须是8的整数倍,在一些简单的应用场景经常被使用。

为了网络上信息传输的安全(防止第三方窃取信息看到明文),发送发和接收方分别进行加密和解密,这样信息在网络上传输的时候就是相对安全的。

DES 加密模式有: Electronic Codebook (ECB) , Cipher Block Chaining (CBC) , Cipher Feedback (CFB) , Output Feedback (OFB)。这里以密文分组链接模式 CBC 为例,使用了相同的 key 和 iv (Initialization Vector)

const crypto = require("crypto");

// DES 加密
function desEncrypt(message, key) {
    const cipher = crypto.createCipheriv("des-cbc", key, key);
    let crypted = cipher.update(message, "utf8", "base64");
    crypted += cipher.final("base64");
    return crypted;
};

// DES 解密
function desDecrypt(text, key) {
    const cipher = crypto.createDecipheriv("des-cbc", key, key);
    let decrypted = cipher.update(text, "base64", "utf8");
    decrypted += cipher.final("utf8");
    return decrypted;
};

const enCode = desEncrypt("CondorHero", "01234567");
console.log(enCode); // /yAMqF2n0wIcXg5/HuTz8A==
const deCode = desDecrypt(enCode, "01234567");
console.log(deCode); // CondorHero

还有一个常见的对称加密算法 AES——高级加密标准(AES,Advanced Encryption Standard),微信小程序加密传输就是用这个加密算法的,详见 服务端获取开放数据

const crypto = require("crypto");

// AES 加密
function aesEncrypt(message, key) {
    const cipher = crypto.createCipheriv("aes128", key, key);
    let crypted = cipher.update(message, "utf8", "hex");
    crypted += cipher.final("hex");
    return crypted;
};

// AES 解密
function aesDecrypt(text, key) {
    const cipher = crypto.createDecipheriv("aes128", key, key);
    let decrypted = cipher.update(text, "hex", "utf8");
    decrypted += cipher.final("utf8");
    return decrypted;
};

const data = "CondorHero";
const key = "0123456789abcdef";
const encrypted = aesEncrypt(data, key);
const decrypted = aesDecrypt(encrypted, key);
console.log("Encrypted text: " + encrypted);
console.log("Decrypted text: " + decrypted);
// Encrypted text: 7fe8c79194fbdf8598c67323a4e7da9a
// Decrypted text: CondorHero

四、RSA 非对称加密

RSA 算法是一种非对称加密算法,即由一个私钥和一个公钥构成的密钥对,通过私钥加密,公钥解密,或者通过公钥加密,私钥解密。其中,公钥可以公开,私钥必须保密。

为提高保密强度,RSA 密钥至少为 500 位长,一般推荐使用 1024 位。这就使加密的计算量很大。为减少计算量,在传送信息时,常采用传统加密方法与公开密钥加密方法相结合的方式,即信息采用对称加密,对称加密的密钥使用 RSA 加密传输。

RSA 算法是 1977 年由 Ron Rivest、Adi Shamir 和 Leonard Adleman 共同提出的,所以以他们三人的姓氏的头字母命名。

简单的来个例子,公钥加密,私钥加密:

const crypto = require("crypto");
const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", {
    modulusLength: 2048,
});
// RSA 公钥加密
function rsaPublicDecrypt(pubKey, message) {
    const crypted = crypto.publicEncrypt(pubKey, Buffer.from(message, "utf8"));
    return crypted.toString("hex");
};

// RSA 私钥解密
function rsaPrivateDecrypt(priKet, enCrypted) {
    const decrypted = crypto.privateDecrypt(priKet, Buffer.from(enCrypted, "hex"));
    return decrypted.toString("utf8");
};

const data = "CondorHero";
const encrypted = rsaPublicDecrypt(publicKey, data);
const decrypted = rsaPrivateDecrypt(privateKey, encrypted);
console.log("Encrypted text: " + encrypted);
console.log("Decrypted text: " + decrypted);

看完之后你可以模仿着写 「私钥加密,公钥加密」,比如使用 ECDH 椭圆曲线仿写加解密函数,当你完成可以对照下面代码查看自己的结果:

const crypto = require("crypto");

// 生成 ECDH 密钥对
const ecdh = crypto.createECDH("secp256k1");
ecdh.generateKeys();

// 获取公钥和私钥
const publicKey = ecdh.getPublicKey(null, "compressed");
const privateKey = ecdh.getPrivateKey(null, "compressed");

// ECDH 公钥加密
function ecdhPublicEncrypt(pubKey, message) {
  const secret = crypto.createECDH("secp256k1").setPrivateKey(privateKey);
  const sharedSecret = secret.computeSecret(pubKey);

  // 生成一个随机的 IV
  const iv = crypto.randomBytes(16);

  // 在加密之前将 IV 与密文拼接在一起,以便在解密时分离它们。
  const cipher = crypto.createCipheriv(
    "aes-256-cbc",
    sharedSecret.slice(0, 32),
    iv
  );
  let encrypted = cipher.update(message, "utf8", "hex");
  encrypted += cipher.final("hex");

  return iv.toString("hex") + encrypted;
}

// ECDH 私钥解密
function ecdhPrivateDecrypt(priKey, enCrypted) {
  const secret = crypto.createECDH("secp256k1").setPrivateKey(priKey);
  const sharedSecret = secret.computeSecret(publicKey);

  // 从加密数据中分离出 IV 和密文。
  const iv = Buffer.from(enCrypted.slice(0, 32), "hex");
  const encrypted = enCrypted.slice(32);

  const decipher = crypto.createDecipheriv(
    "aes-256-cbc",
    sharedSecret.slice(0, 32),
    iv
  );
  let decrypted = decipher.update(encrypted, "hex", "utf8");
  decrypted += decipher.final("utf8");
  return decrypted;
}

const data = "CondorHero";
const encrypted = ecdhPublicEncrypt(publicKey, data);
const decrypted = ecdhPrivateDecrypt(privateKey, encrypted);
console.log(`Encrypted text: ${encrypted}`);
console.log(`Decrypted text: ${decrypted}`);

完~

当前时间 Sunday, January 31, 2021 23:40:34