前言
数据加密是前后端开发常用的技术,应用场景包括,信息通讯,敏感数据存储等,本文列举的几个常用的加密方案,并配合对应的场景讲解具体的应用。
密码加密存储
场景说明
一般软件系统用户名是明文存储在数据库中,密码是hash 处理后存在数据库中,对比验证时将用户输入的密码hash 处理后和数据库中的hash 比对,一致即为验证通过。 现在常用的加密方案中会做加盐hash ,用于对抗彩虹表暴力破解,尽量保证每个用户的盐值不相同,增加攻击方破解难度。
一个简单的示例
const crypto = require('crypto');
const random = () => {
return Math.random().toString().slice(5, 10);
}
const cryptPwd = (password, salt) => {
const saltPassword = `${password}:${salt}`;
const md5 = crypto.createHash('sha1');
const result = md5.update(saltPassword).digest('hex');
console.log("saltPassword", saltPassword)
console.log("result", result)
return result
}
const password = '123456';
cryptPwd(password, random());
/**
* saltPassword 123456:62130
* result ef8b60fe12179fd88dbe7304231d0bf1
*/
cryptPwd(password, random());
/**
* saltPassword 123456:77042
* result 79b05a3e6644cccaf9f57061588d13cd
*/
实际应用
crypto 的例子
加密生产salt 和hash,并将两条数据保存进数据库
const crypto = require('crypto');
function makeSalt() {
return crypto.randomBytes(10).toString('base64');
}
const salt = makeSalt()
const hash = crypto.pbkdf2Sync(password, salt, 10000, 16, 'sha1').toString('base64')
/**
* password 类型为 string | NodeJS.ArrayBufferView;
* salt 类型同上
* iterations 迭代次数,number 类型,数字越高越安全
* keylen 返回值的长度。这里是指返回buffer的长度,toString()里encoding不同,会导致字符串长度不同。
* digest 摘要算法名称 string 类型
*/
验证时,password 来源于前端输入,salt 来源于数据.。再次算出 hash 和数据库里字段做比对,相同即为通过验证。
bcrypt 的例子
加密时
const bcrypt = require('bcrypt');
const saltRounds = 10;
const myPlaintextPassword = '123456';
const hash = bcrypt.hashSync(myPlaintextPassword,saltRounds)
// 把hash 值存到数据库里
验证时:password 来源于前端输入,hash 来源于数据库,返回true 即为验证通过
const bcrypt = require('bcrypt');
bcrypt.compareSync(myPlaintextPassword, hash); // true
和上面的例子比起来,这里把salt 和hash 合并成一个字段了。
官方的说明
$2b$10$nOUIs5kJ7naTuTFkBy1veuK0kSxUFXfuaOKdOKf9xYT0KKIGSJwFa
| | | |
| | | hash-value = K0kSxUFXfuaOKdOKf9xYT0KKIGSJwFa
| | |
| | salt = nOUIs5kJ7naTuTFkBy1veu
| |
| cost-factor => 10 = 2^10 rounds
|
hash-algorithm identifier => 2b = BCrypt
签名
场景说明
签名的目的是防止数据被篡改,常用场景是在发送请求时,对body 里的数据做个签名放在header上,接收方拿到数据后,对body 进行签名并且和请求方给予的签名字段相对比,相同即为验证通过。
无私钥
var signature = crypto.createHmac('sha1', accessSecret )
signature.update(new Buffer(StringToSign, 'utf-8')).digest().toString('base64');
验证过程和加密过程一样。
有私钥
用私钥做签名
const crypto = require('crypto');
const fs = require('fs');
const privateKey = fs.readFileSync('./private-key.pem'); // 私钥
const publicKey = fs.readFileSync('./public-key.pem'); // 公钥
const algorithm = 'RSA-SHA256'; // 签名算法
const encoding = 'hex'
// 数字签名
function sign(text){
const sign = crypto.createSign(algorithm);
sign.update(text);
return sign.sign(privateKey, encoding);
}
// 对内容进行签名
const content = 'hello world';
const signature = sign(content);
console.log(signature);
公钥做验证
const crypto = require('crypto');
const fs = require('fs');
const publicKey = fs.readFileSync('./public-key.pem'); // 公钥
const algorithm = 'RSA-SHA256'; // 签名算法
const encoding = 'hex'
// 校验签名
function verify(oriContent, signature){
const verifier = crypto.createVerify(algorithm);
verifier.update(oriContent);
return verifier.verify(publicKey, signature, encoding);
}
// 校验签名,如果通过,返回true
const verified = verify(content, signature);
console.log(verified);
数据加密传输
场景说明
通常意义上的加密,即将发送的数据加密发送给对方,达到保密的目的。
对称加密
流程
- A 使用密钥加密
- A 并且把密钥给B
- B 使用密钥解密
const crypto = require("crypto")
const key = crypto.randomBytes(192 / 8)
const iv = crypto.randomBytes(128 / 8)
const algorithm = 'aes192'
const encoding = 'hex'
const encrypt = (text) => {
const cipher = crypto.createCipheriv(algorithm, key, iv)
cipher.update(text)
return cipher.final(encoding)
}
const content = 'Hello Node.js'
const crypted = encrypt(content)
console.log(crypted)
const decrypt = (encrypted) => {
const decipher = crypto.createDecipheriv(algorithm, key, iv)
decipher.update(encrypted, encoding)
return decipher.final('utf8')
}
// db75f3e9e78fba0401ca82527a0bbd62
const decrypted = decrypt(crypted)
console.log(decrypted)
// Hello Node.js
非对称加密
流程
- 由B 生成公钥和私钥
- B 把公钥给A
- A 使用B给的公钥加密数据,然后发给B
- B 使用私钥进行解密
const crypto = require("crypto");
const fs = require('fs');
const privateKey = fs.readFileSync('./private-key.pem'); // 私钥
const publicKey = fs.readFileSync('./public-key.pem'); // 公钥
const msg = "Hello Node.js"
const encodeData = crypto.publicEncrypt(publicKey,Buffer.from(msg))
console.log(encodeData.toString("base64"))
const decodedData = crypto.privateDecrpt(privateKey,encodeData)
console.log(decodeData.toString("utf8"))
混合加密
流程
- 由B 生成公钥和私钥
- B 把公钥给A
- A 生成一个 对称密钥 ,并用B 给的公钥加密,发送给B
- B 使用私钥解密,得到对称密钥
- A 和B 之间后续都使用对称密钥加密和解密
企业微信加密解密案例
场景说明
这里设定一个特定的场景,企业微信第三方套件,设置回调接口,以及从回调接口中拿到数据并解密。大体流程是 验证数据签名 -> 解密-> 解码
参考的是 官方文档
解码(编码)
这里使用 PKCS#7 加密标准,不过只涉及到了编码补全部分。
这个类仅充当Buffer补全功能,代替 createDecipheriv 的自动补全功能 setAutoPadding。
它的具体作用是设置一个块大小比如16,返回一个为它整数倍的Buffer。如果源数据能整除16 则添加一整个块的数据,否则将填充剩余空间的数据,填充的数据为源数据的长度。
例子1、buffer 长度 6,填充剩余10个位,每位都是6。
例子2 、buffer 长度20 ,填充剩余 16-(20-16)= 12 位,每位都是20
例子3 、buffer 长度16 ,填充 16-(16-16)= 16 位,每位都是16
class PKCS7Encoder {
constructor(blockSize) {
this.blockSize = blockSize
}
/**
* @param {Buffer} msg
*/
encode(msg) {
const that = this;
const msgLength = msg.length
const padding = that.blockSize - (msgLength % that.blockSize)
const pad = Buffer.alloc(padding, padding)
return Buffer.concat([msg, pad])
}
/**
* @param {Buffer} msg
* @returns
*/
decode(msg) {
const that = this;
const msgLength = msg.length
const padding = msg[msgLength - 1]
if (padding < 1 || padding > that.blockSize) {
throw new Error('Invalid padding');
}
return msg.subarray(0, msgLength - padding)
}
}
解密部分
class WXMsgCrypt {
/**
*
* @param {String} token
* @param {String} encodingAESKey
* @param {String} reciveId
*/
constructor(token, encodingAESKey, reciveId) {
if (!token || !encodingAESKey) {
throw new Error("arguments miss")
}
this.token = token;
this.encodingAESKey = encodingAESKey
let AESKey = Buffer.from(encodingAESKey + "=", "base64")
if (AESKey.length !== 32) {
throw new Error('encodingAESKey invalid');
}
this.key = AESKey;
this.iv = AESKey.subarray(0, 16)
this.pkcs7Encoder = new PKCS7Encoder(16)
this.reciveId = reciveId
}
/**
*
* @param {String} timestamp
* @param {String} nonce
* @param {String} encryptMsg
* @returns
*/
getSignature(timestamp, nonce, encryptMsg) {
let that = this;
const shasum = crypto.createHash("sha1")
const arr = [that.token, timestamp, nonce, encryptMsg]
shasum.update(arr.join(""))
return shasum.digest('hex')
}
/**
* @param {String} plainMsg
* @returns
*/
encrypt(plainMsg) {
let that = this;
const randomString = crypto.pseudoRandomBytes(16);
const msg = Buffer.from(plainMsg)
let msgLength = Buffer.alloc(4)
msgLength.writeUInt32BE(msg.length, 0)
let bufMsg = Buffer.concat([
randomString,
msgLength,
msg,
Buffer.from(that.reciveId)
])
let encoded = that.pkcs7Encoder.encode(bufMsg)
let cipher = crypto.createCipheriv('aes-256-cbc', that.key, that.iv);
cipher.setAutoPadding(false);// 这里用pkcs7 编码代替
let cipheredMsg = Buffer.concat([cipher.update(encoded), cipher.final()]);
return cipheredMsg.toString("base64")
}
/**
* @param {String} encryptMsg
* @returns
*/
decrypt(encryptMsg) {
let that = this;
let decipher = crypto.createDecipheriv("aes-256-cbc", that.key, that.iv)
decipher.setAutoPadding(false);
let decipheredMsg = Buffer.concat([
decipher.update(encryptMsg, "base64"),
decipher.final()
])
decipheredMsg = that.pkcs7Encoder.decode(decipheredMsg)
// 算法:AES_Encrypt[random(16B) + msg_len(4B) + msg + $reciveId]
let content = decipheredMsg.subarray(16)
const length = content.subarray(0, 4).readUInt32BE(0)
return {
message: content.subarray(4, length + 4).toString(),
reciveId: content.subarray(length + 4).toString()
}
}
}
实际调用
先拿timestamp,nonce,echostr 做签名和msg_signature 做比对,验证通过后对echostr 进行解密。 在回调接口验证的阶段,需要返回message 给企业微信验证,如果是接收数据阶段就是把解析出来的数据做进一步的处理
const qywxThirdCallback = function (req, res) {
// 省略部分代码
const _config = {
token: "",
encodingAESKey:"",
suiteid: ""
}
const { msg_signature, timestamp, nonce } = req.query
const echostr = decodeURIComponent(req.query.echostr);
let cryptor = new WXMsgCrypt(
_config.token,
_config.encodingAESKey,
_config.suiteId
);
if (msg_signature !== cryptor.getSignature(timestamp, nonce, echostr)) {
res.writeHead(401);
res.end('Invalid signature');
return;
}
let result = cryptor.decrypt(echostr);
res.writeHead(200);
res.end(result.message);
}
后语
本文简述nodejs 在各场景下的使用方法,属于一篇应用型的文章。
看完文章大家会发现
- 签名:私钥签名,公钥验证
- 数据加密 :公钥加密,私钥解密
这两个正好是反的,大家可以想一想背后的原理是什么?