使用消息摘要算法对消息体计算和验证摘要,可以防止消息传输过程中被篡改为非法消息值;使用加密算法加密消息体,可以防止消息传输过程中被拦截并读取。二者结合则可以实现较强的安全性消息交换。
1 保证消息交换正确性
消息传输过程中可能被中间人篡改。比如 A 发送消息转账给 B,中间人在消息转发过程中进行了拦截,解密消息体并篡改为转账给 C,而且篡改转账额度,由此造成可能不可逆转的损失。
消息摘要可以保证消息传输的正确性。主要思路比较简单:对要发送对消息体使用消息摘要算法(如 md5、sha256、sha1、hashMAC 等)计算出摘要值,接收方收到消息后,使用相同的摘要算法对消息体计算摘要值,将其与接收到的摘要值比对验证是否一致。
该过程中需要注意的重点是要保障摘要算法的保密性。
2 消息交换加密传输
对称加密算法使用共同的密文对消息体加密,其特点是速度快效率高,缺点是需发送和接收方共享秘钥,任何一方泄露密文和加密算法则加密失效。非对称加密算法采用公私钥对,发送方使用公钥对消息体加密、接收方使用私钥对消息体解密(反之亦可),接收方只需保管好自己独有的秘钥,即可保障加密消息不被泄露。但其加密速度慢效率低,故一般不用其进行大消息体的加密。
将对称加密算法和非对称加密算法结合使用,则可以达到相对可行有效的消息交换加密传输。下文中非对称加密算法以 RSA 为例,对称加密算法以 AES 为例。
主要思路为:通过 RSA 算法加密 AES 算法使用的密钥,通过 AES 算法加密消息体,然后将加密的密钥和消息体发送给对方;对方收到消息后,使用 RSA 算法解密 AES 密钥,用解密后的密钥解密消息体,由此得到解密后的消息;接收方在回复消息的过程中重复类似过程,由此实现消息交换加密传输。
我们以客户端与服务端消息通信为例,详细流程概述如下:
准备:
- 客户端生成本地的 RSA 公私钥 clientPublicKey 和 clientPrivateKey
- 服务端生成远端的 RSA 公私钥 remotePublicKey 和 remotePublicKey
消息交换流程:
- 公钥交换:客户端发起请求,发送本地的 RSA 公钥 localPublicKey,并获取服务端的 RSA 公钥 remotePublicKey,用于加密对称算法(AES)的密钥
- 客户端生成随机的 16 位字符 aesKey,用于对称算法(AES)的密钥
- 客户端使用从服务器获取的公钥 remotePublicKey 对 aesKey 进行 RSA 加密,得到加密的值 aesKeyEncrypted
- 客户端使用 aesKey 对要发送的消息体 body 加密,得到 bodyAesEncrypted
- 客户端使用 http post 方法发送消息体 {aesKeyEncrypted, bodyAesEncrypted }。其中 localPublicKey 用于服务器加密返回的消息
- 服务端接收到消息体后,使用其 RSA 私钥 remotePrivateKey 对 aesKeyEncrypted 解密得到 AES 算法密钥 aesKey
- 服务端使用 AES 密钥 aesKey 对 bodyAesEncrypted 解密,得到 body。至此客户端到服务端的加密消息传输完毕
- 服务端使用 localPublicKey 重复上述 1-5 过程对回复消息加密并发送,客户端重复上述 6-7 过程对接收到的消息解密
通过以上流程,使得整个传输过程中的消息都是加密无法被读取的,当然前提是需保证双方的 RSA 秘密未被泄露。
此外,如果加上消息摘要算法对消息体签名,中间人不知道摘要算法则无法伪造有效的消息。所以结合消息摘要算法和消息加密算法,则可以进一步增强消息传递过程。当然,在对等双方的消息传递中,对方不一定是可信的,还是存在摘要算法泄露的风险。
上述流程可以保证消息的安全性,但无法保证其真实性,因为中间人也可以不解密消息,使用拦截到的公钥加密消息,发送伪造的消息体(伪造消息体和发送方公钥)。
解决办法则是引入第三方权威中间人机构,消息接收方在收到消息时,先通过中间人机构对来自消息发送方的公钥进行可靠性认证。
上述流程默认客户端与服务端为对等的双方。其实该流程也可以进行简化:客户端不维护本地 RSA 公私钥,但缓存随机生成的 RES 秘钥;服务端在得到消息后,回复消息时使用相同的 RES 算法和解密得到的客户端 RES 秘钥加密消息体,客户端收到消息后使用 RES 秘钥解密。其实简化流程中再加上颁发证书的第三方权威机构认证流程,就是典型的 HTTPS 消息加密传输的基本原理。
3 基于 nodejs 的消息交换加密传输示例
3.1 生成非对称加密 RSA 算法公私钥
使用 crypto
模块:
import crypto from 'crypto';
/** 使用 crypto 模块生成 RSA 公私钥 */
export function genRsaKeyByCrypto(options?: crypto.RSAKeyPairOptions<"pem", "pem">) {
options = {
modulusLength: 1024,
publicKeyEncoding: {
type: 'pkcs1',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
cipher: 'aes-256-cbc',
passphrase: '',
},
...options,
};
const result = crypto.generateKeyPairSync('rsa', options);
return result;
}
使用 node-rsa
库:
import NodeRSA from 'node-rsa';
export function genRsaKeyPairByNodeRsa() {
const key = new NodeRSA({ b: 1024 });
key.setOptions({encryptionScheme: 'pkcs1'});
return { publicKey: key.exportKey('public'), privateKey: key.exportKey('private') };
}
生成并保存 RSA 公私钥:
export function saveRsaKey(publicKeyPath='public.pem', privateKeyPath='private.pem', isForce = false) {
publicKeyPath = path.resolve(publicKeyPath);
privateKeyPath = path.resolve(privateKeyPath);
if (existsSync(publicKeyPath) && !isForce) {
// console.log('公钥文件已存在');
return { publicKey: readFileSync(publicKeyPath), privateKey: readFileSync(privateKeyPath) };
} else {
console.log('重新生成公私钥文件');
}
// const { publicKey, privateKey } = await genRsaKeyByCrypto();
const { publicKey, privateKey } = genRsaKeyPairByNodeRsa();
if (!existsSync(path.dirname(publicKeyPath))) mkdirSync(path.dirname(publicKeyPath), {recursive: true});
writeFileSync(publicKeyPath, publicKey);
console.log('写入公钥文件:', publicKeyPath);
writeFileSync(privateKeyPath, privateKey);
console.log('写入私钥文件:', privateKeyPath);
return { publicKey, privateKey };
}
3.2 RSA 算法加密与解密
使用 Node.js 自带的 crypto
模块示例:
/**
* RSA最大加密明文大小
*/
const MAX_ENCRYPT_BLOCK = 117 - 31;
/**
* RSA最大解密密文大小
*/
const MAX_DECRYPT_BLOCK = 128;
/**
* rsa 公钥加密
*/
export function publicEncrypt(data, publicKey, outputEncoding: BufferEncoding = 'base64') {
// 加密信息用buf封装
const buf = Buffer.from(data, 'utf-8');
const inputLen = buf.byteLength;
const bufs = [];
let offSet = 0;
let endOffSet = MAX_ENCRYPT_BLOCK;
// 分段加密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
const bufTmp = buf.slice(offSet, endOffSet);
bufs.push(crypto.publicEncrypt({key: publicKey, passphrase: '', padding: crypto.constants.RSA_PKCS1_PADDING}, bufTmp));
} else {
const bufTmp = buf.slice(offSet, inputLen);
bufs.push(crypto.publicEncrypt({key: publicKey, passphrase: '', padding: crypto.constants.RSA_PKCS1_PADDING}, bufTmp));
}
offSet += MAX_ENCRYPT_BLOCK;
endOffSet += MAX_ENCRYPT_BLOCK;
}
const result = Buffer.concat(bufs).toString(outputEncoding);
return result;
}
/**
* rsa 私钥解密
*/
export function privateDecrypt(data, privateKey, inputEncoding: BufferEncoding = 'base64') {
// 经过base64编码的密文转成buf
const buf = data instanceof Buffer ? data : Buffer.from(data, inputEncoding);
const inputLen = buf.byteLength;
const bufs = [];
let offSet = 0;
let endOffSet = MAX_DECRYPT_BLOCK;
// 分段加密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
const bufTmp = buf.slice(offSet, endOffSet);
bufs.push(crypto.privateDecrypt({key: privateKey, passphrase: '', padding: crypto.constants.RSA_PKCS1_PADDING}, bufTmp));
} else {
const bufTmp = buf.slice(offSet, inputLen);
bufs.push(crypto.privateDecrypt({key: privateKey, passphrase: '', padding: crypto.constants.RSA_PKCS1_PADDING}, bufTmp));
}
offSet += MAX_DECRYPT_BLOCK;
endOffSet += MAX_DECRYPT_BLOCK;
}
const result = Buffer.concat(bufs).toString();
return result;
}
使用 node-rsa
库:
import NodeRSA from 'node-rsa';
/**
* rsa 公钥加密
*/
export function rsaEncrypt(data, publicKey, outputEncoding = 'base64') {
const key = new NodeRSA({ b: 1024 });
key.importKey(publicKey, 'public');
let encryData = key.encrypt(data, outputEncoding, 'utf8');
if (outputEncoding === 'hex' || outputEncoding === 'binary') encryData = Buffer.from(encryData, outputEncoding);
return encryData;
}
/**
* rsa 私钥解密
*/
export function rsaDecrypt(data, privateKey) {
const key = new NodeRSA({ b: 1024 });
key.importKey(privateKey, 'private');
const decryptData = key.decrypt(data, 'utf8');
return decryptData;
}
3.3 AES 算法加密与解密
AES 加密与解密相对比较简单,只需要传入对应的 data 数据与解密密文即可。
使用 Node.js 自带的 crypto
模块示例:
import crypto from 'crypto';
/** aes 加密 */
export aesEncrypt(data, passKey, outputEncoding = 'base64') {
if (typeof data !== 'string') data = JSON.stringify(data);
const cipherChunks = [];
// const key = Buffer.from(passKey, 'utf8');
// 对原始秘钥点加盐
const key = crypto.scryptSync(passKey, 'salt', 16);
const iv = key; // Buffer.alloc(16, 0);
const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
cipher.setAutoPadding(true);
cipherChunks.push(cipher.update(data, 'utf8', outputEncoding as any));
cipherChunks.push(cipher.final(outputEncoding as any));
return cipherChunks.join('');
}
/** aes 解密 */
export aesDecrypt(data, passKey, inputEncoding = 'base64') {
const cipherChunks = [];
// const key = Buffer.from(passKey, 'utf8');
const key = crypto.scryptSync(passKey, 'salt', 16);
const iv = key; // Buffer.alloc(16, 0);
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
decipher.setAutoPadding(true);
cipherChunks.push(decipher.update(data, inputEncoding as any, 'utf8'));
cipherChunks.push(decipher.final('utf8'));
return cipherChunks.join('');
}
使用 crypto-js
模块示例:
import CryptoJS from 'crypto-js';
const key = CryptoJS.enc.Utf8.parse("1234123412ABCDEF"); //十六位十六进制数作为密钥
const iv = CryptoJS.enc.Utf8.parse('ABCDEF1234123412'); //十六位十六进制数作为密钥偏移量
//解密方法
function Decrypt(data, passKey, iv?) {
if (!iv) iv = passKey;
passKey = CryptoJS.enc.Utf8.parse(passKey);
iv = CryptoJS.enc.Utf8.parse(iv);
const hex = CryptoJS.enc.Hex.parse(data);
const base64 = CryptoJS.enc.Base64.stringify(hex);
const decrypt = CryptoJS.AES.decrypt(base64, passKey, { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
return decrypt.toString(CryptoJS.enc.Utf8);
}
//加密方法
function Encrypt(data, passKey, iv?) {
if (!iv) iv = passKey;
passKey = CryptoJS.enc.Utf8.parse(passKey);
iv = CryptoJS.enc.Utf8.parse(iv);
const utf8 = CryptoJS.enc.Utf8.parse(data);
const encrypted = CryptoJS.AES.encrypt(utf8, key, { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
return encrypted.ciphertext.toString().toUpperCase();
}
3.4 结合 AES 和 RSA 加密算法对消息体加密与解密
数据加密与解密实现:
/** 生成指定长度的字符串 */
export function genRandomAesKey(len = 16) {
// return crypto.randomBytes(len).toString('utf-8');
const result = [];
for (let i = 0; i < len; i++) {
let code = Math.round(Math.random() * 126);
if (code < 33) code += 32;
result.push( String.fromCharCode(code));
}
return result.join('');
}
/** (使用对方的公钥)数据加密,返回加密后的 key、data 以及本地公钥(用于给对方加密回据使用) */
export function dataEncrypt(data, remotePublicKey) {
const aesKey = genRandomAesKey();
// const localPublicKey = fs.readFileSync(config.publicKeyPath, {encoding: 'utf-8'});
// if (!remotePublicKey) remotePublicKey = localPublicKey;
const aesKeyEncrypted = rsaEncrypt(aesKey, remotePublicKey);
const encryptedData = aesEncrypt(data, aesKey);
return {key: aesKeyEncrypted, data: encryptedData, publicKey: localPublicKey};
}
export function dataDecrypt(encryptedData, aesKeyEncrypted, localPrivateKey) {
// if (!localPrivateKey) localPrivateKey = fs.readFileSync(config.privateKeyPath, {encoding: 'utf-8'});
const aesKey = rsaDecrypt(aesKeyEncrypted, localPrivateKey);
const result = aesDecrypt(encryptedData, aesKey);
try {
return JSON.parse(result);
} catch (_e) {
return result;
}
}
消息发送方数据加密与接收方数据解密:
// 发送方加密发送消息
const data = { user: 'lzwme', pwd: '123456' };
const remotePublicKey = '...';
const encryptedInfo = dataEncrypt(data);
console.log('数据加密:', encryptedInfo);
// 接受方收到消息体后解密消息
const localPrivateKey = '...';
const deCryptedInfo = dataDecrypt(encryptedInfo.key, encryptedInfo.data, localPrivateKey);
console.log('数据解密:', deCryptedInfo);
4.在 node 中也有原生的 crypto 模块,该模块提供了 hash、hmac、加密解密、签名、验证功能等一整套的封装。
使用const crypto = require('crypto');
即可引入该模块。
1. hash 算法
hash 算法也被称为摘要算法,该算法可以将任意长度的数据,转换为固定长度的 hash 值,这种方式具有不可逆性。你可以把一本小说转换为 hash 数据,但无法从这 hash 数据再逆转回一本小说。因此,若要获取 hash 的原数据,只能靠字典碰撞。
该算法通常在文本校验、存储密码时用的比较多。虽然摘要算法会用于密码的存储,但严格来说,摘要算法不算做是加密算法。
使用getHashes()
方法,可以获取到所有支持的 hash 算法:
crypto.getHashes();
获取到一个数组:
[
"RSA-MD4",
"RSA-MD5",
"RSA-MDC2",
"RSA-RIPEMD160",
"RSA-SHA1",
"RSA-SHA1-2",
"RSA-SHA224",
"RSA-SHA256",
"RSA-SHA3-224",
"RSA-SHA3-256",
"RSA-SHA3-384",
"RSA-SHA3-512",
"RSA-SHA384",
"RSA-SHA512",
"RSA-SHA512/224",
"RSA-SHA512/256",
"RSA-SM3",
"blake2b512",
"blake2s256",
"id-rsassa-pkcs1-v1_5-with-sha3-224",
"id-rsassa-pkcs1-v1_5-with-sha3-256",
"id-rsassa-pkcs1-v1_5-with-sha3-384",
"id-rsassa-pkcs1-v1_5-with-sha3-512",
"md4",
"md4WithRSAEncryption",
"md5",
"md5-sha1",
"md5WithRSAEncryption",
"mdc2",
"mdc2WithRSA",
"ripemd",
"ripemd160",
"ripemd160WithRSA",
"rmd160",
"sha1",
"sha1WithRSAEncryption",
"sha224",
"sha224WithRSAEncryption",
"sha256",
"sha256WithRSAEncryption",
"sha3-224",
"sha3-256",
"sha3-384",
"sha3-512",
"sha384",
"sha384WithRSAEncryption",
"sha512",
"sha512-224",
"sha512-224WithRSAEncryption",
"sha512-256",
"sha512-256WithRSAEncryption",
"sha512WithRSAEncryption",
"shake128",
"shake256",
"sm3",
"sm3WithRSAEncryption",
"ssl3-md5",
"ssl3-sha1",
"whirlpool"
]
这么多 hash 算法,我们平时用的比较多的是md5
, sha1
, sha256
, sha512
。这里把同一个文本,按照不同的摘要算法来生成 hash 值:
// text 要摘要的文本
// hashtype 摘要的算法
function createHash(text, hashtype) {
const hash = crypto.createHash(hashtype).update(text).digest("hex");
console.log(hashtype, hash, hash.length);
}
hashes.forEach((type) => {
createHash("蚊子", type);
});
生成的结果:
md5 37725295ea78b626efcf77768be478cb 32
sha1 21f226b5a07ed3f74e6ae07e994f36d6a9bf6fac 40
sha256 a200ce289b67afbfb6fbc3d7dd33f7ef493daef64fb159c2e48e8534a0289a9b 64
sha512 b88bd9eac191f58e06c99c256bbcfdf2945aa94b47d5e0242be1f0739bf4adccebf4753e9f38f92603fe3f52f331121540c1dda2ed91796410abcfe49a677fba 128
不同的算法,生成的 hash 值的长度也不一样,碰撞成功的难度也越大。
同时,update
方法不止可以接收字符串,还可以接收 stream 流:
const filename = "./node-crypto.md";
const hash = crypto.createHash("sha1");
const fsStream = fs.createReadStream(filename);
fsStream.on("readable", () => {
// 哈希流只会生成一个元素。
const data = fsStream.read();
if (data) {
hash.update(data);
} else {
// 数据接收完毕后,输出hash值
console.log(`${hash.digest("hex")} ${filename}`);
}
});
既然可以接收 stream 流的格式,那么就使用 pipe 管道进行处理:
const filename = "./node-crypto.md";
const hash = crypto.createHash("sha1");
const fsStream = fs.createReadStream(filename);
fsStream.pipe(hash).pipe(process.stdout);
hash 后传给下个管道进行处理,不过这里输出的通常会是乱码,因此这里我们自己写一个可写流:
const { Writable } = require("stream");
const write = Writable();
write._write = function (data, enc, next) {
// 将流中的数据写入底层
process.stdout.write(hash.digest("hex") + "\n");
// 写入完成时,调用`next()`方法通知流传入下一个数据
process.nextTick(next);
};
fsStream.pipe(hash).pipe(write); // 正常输出hash值
2. hmac 算法
我们先看下 hmac 算法的用法:
const result = crypto.createHmac("sha1", "123456").update("蚊子").digest("hex");
console.log(result); // 0bdd6c1192e321e34887d965c1140be4361ada65
hmac 算法与 hash 算法的调用方式很像,但createHmac()
方法这里多了一个参数,这个参数相当于密钥。密钥不一样,即使要加密的文本一样,生成的结果也会不一样。
function createHmac() {
const text = "蚊子";
const key = Math.random().toString().slice(-6);
const result = crypto.createHmac("sha1", key).update(text).digest("hex");
console.log(text, key, result);
}
let n = 10;
while (n--) {
createHmac();
}
生成的结果:
蚊子 508028 486d1f539e4bb8adfd601fd6a3302fae74043bfe
蚊子 644233 dcd6501e6eee9e1462625b50c1ff91c613559b35
蚊子 479257 752945c62b87ce1edb24661103b65e612bb849b7
蚊子 445857 0c6399758a2348ea31bc778f87f503b050e036d5
蚊子 954174 a78ff9d4301bb09d249db9fa6c9a3a28c04acff7
蚊子 629736 b7fd4d3836363f029dd9009f51ad6c14280987c1
蚊子 343366 7a8cadf5dd620f8c82315f38de1f6dc60bfc5336
蚊子 168627 cc51e4531449642a5a10357cbf8f206319fb1b1f
蚊子 103054 49b1ad9dc2de5da2cd67dc892f51718aa9475a05
蚊子 477238 82615006638be235a220bcfdee0705b5cc6551fc
hmac 算法相当于加盐版的 hash 算法,但内部具体的实现原理,可自行查看:github-node-crypto-hmac。
这种算法实现密码存储就非常的合适,碰撞成功的概率大大减少。在数据库中,我们可以这样存储:
{
"username": "蚊子",
"password": "486d1f539e4bb8adfd601fd6a3302fae74043bfe",
"key": "508028"
}
即使脱库得到了这些数据,反向获取到原密码的机会也非常的低。
在 stream 流的操作上,hmac 算法和 hash 算法的用法一样。
3. 对称加密和解密算法
前面的两种方法都是不可逆的 hash 加密算法,这里我们介绍下可加密和可解密的算法。常见的对称加密算法有aes
和des
。
crypto 模块中提供了createCipheriv
和createDecipheriv
来进行加密和解密的功能。之前的 createCipher 和 createDecipher 在 10.0.0 版本已经废弃了,我们这里以新的方法为例,写下加密和解密的算法。
这两个方法都接收 3 个参数:
- algorithm:加密解密的类型;
- key: 加密解密的密钥:密钥必须是 8/16/32 位,如果加密算法是 128,则对应的密钥是 16 位,如果加密算法是 256,则对应的密钥是 32 位;
- iv: 初始向量,规则与 key 一样
key 和 iv 两个参数都必须是 'utf8' 编码的字符串、Buffer、 TypedArray 或 DataView。 key 可以是 secret 类型的 KeyObject。 如果密码不需要初始化向量,则 iv 可以为 null。
加密的算法:
function encode(src, key, iv) {
let sign = "";
const cipher = crypto.createCipheriv("aes-128-cbc", key, iv); // createCipher在10.0.0已被废弃
sign += cipher.update(src, "utf8", "hex");
sign += cipher.final("hex");
return sign;
}
解密的算法:
function decode(sign, key, iv) {
let src = "";
const cipher = crypto.createDecipheriv("aes-128-cbc", key, iv);
src += cipher.update(sign, "hex", "utf8");
src += cipher.final("utf8");
return src;
}
使用方法:
const key = "37725295ea78b626"; // Buffer.from('37725295ea78b626', 'utf8');
const iv = "efcf77768be478cb"; // Buffer.from('efcf77768be478cb', 'utf8');
// console.log(key, iv);
const src = "hello, my name is wenzi! my password is `etu^&&*(^123)`";
const sign = encode(src, key, iv);
const _src = decode(sign, key, iv);
console.log("key: ", key, "iv: ", iv);
console.log("原文:", src);
console.log("加密后: ", sign);
console.log("解密后: ", _src);
// key: 37725295ea78b626 iv: efcf77768be478cb
// 原文: hello, my name is wenzi! my password is `etu^&&*(^123)`
// 加密后: ce6dc873bfd5a5ae6fe0b2bb3f3de46fb9fc15e0ffc75d12286871dbfa3ed185b3ebf60b8e16dd0057eb0750e897347abeddf5a2741944d5a307ceb25c181276
// 解密后: hello, my name is wenzi! my password is `etu^&&*(^123)`
4. 非对称加密算法
刚才了解了下对称加密,即加密和解密用的都是相同的密钥。非对称加密相对来说,比对称加密更安全,用公钥加密的内容,必须通过对应的私钥才能解密。双方传输信息时,可以使用先使用对方的公钥进行加密,然后对方再使用自己的私钥解开即可。
先用创建一个私钥:
openssl genrsa -out rsa_private.key 1024
然后根据私钥创建对应的公钥:
openssl rsa -in rsa_private.key -pubout -out rsa_public.key
这里就可以进行非对称的加密和解密了:
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, my name is 蚊子";
const secret = crypto.publicEncrypt(pub_key, Buffer.from(text));
const result = crypto.privateDecrypt(priv_key, secret);
console.log(secret); // buffer格式
console.log(result.toString()); // hello, my name is 蚊子
使用publicEncrypt
进行公钥的加密过程,使用privateDecrypt
进行私钥的解密过程。
5. 签名
在网络中传输的数据,除可使用 Cipher 类进行数据加密外,还可以对数据生成数字签名,以防止在传输过程中对数据进行修改。
签名的过程与非对称加密的过程正好相反,是使用私钥进行加密签名,然后使用公钥进行解密的签名验证。
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, my name is 蚊子";
// 生成签名
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); // ca364a6e31c1f540737ba3efb1ddf7fa2a087c5c11efe52a9e1f2c88b1fd1e0e50f12da4f22362fdfc3d77f3f538995a27a8206d250dba3572510dfcb33064f48685b96f2b2393f56de4958448cec92a4299434aa3318efe418e166b38100bc3a1d1a9310a510087021da0f66a817043ddfd2fb88db76eb2ace480c17a7f732f
console.log("verifyResult", verifyResult); // true
生成签名的 sign 方法有两个参数,第一个参数为私钥,第二个参数为生成签名的格式,最后返回的 signed 为生成的签名(字符串)。
验证签名的 verify 方法有三个参数,第一个参数为公钥,第二个参数为被验证的签名,第三个参数为生成签名时的格式,返回为布尔值,即是否通过验证。
5.总结
本文主要介绍了结合使用对称加密算法(AES)和非对称加密算法(RSA)对消息交换双方消息加密传输的典型方案以及从简单的 hash 算法,到对称加密,最后到非对称加密和签名,都有个大致的了解。后续我们也会对 node 的其他模块进行深入的理解。文章中使用 nodejs 代码演示了主要流程的实现方式。在基于的浏览器/服务器(B/S)的服务中,浏览器端没有 nodejs
接口能力,可借助第三方库 crypto-js
、jsencrypt
等提供的 API 实现相关加密和解密算法。