NodeJS AES加解密简明实现和分析
Nodejs内置了强大的crypto密码学套件,可以非常方便简明的实现各种密码学计算和操作。本文探讨了一套基于crypto的对称信息加密的实现和过程。
需求和构想
这是一个基本的对称加密实现,但是在基本的AES加密技术的基础上,我们希望提供更好的安全性,所以我们的技术实现构想如下:
- 基于AES,密钥长度256,支持认证标签技术authTag(如CCM或GCM模式)
- 一次一密,加密结果动态
- 加密结果为单一的Base64
- 可以进行时效性检查
- 实现过程尽可能清楚、简单、高效
设置和参数
作为示例,我们选择的加密算法和设置包括(封装为一个常量,用于代码中的实现):
const AES = {
Aprefix : "U",
Mode: "aes-256-gcm",
Keylength: 32,
Taglength: 8,
Hash: "SHA256"
};
这些设置包括:
- 模式: AES-256-GCM, AES有很多模式,其中GCM是比较先进、安全和完整的,如支持初始化向量和认证标签,可以提供更好的安全性和完整性检查
- authTagLength:8bit, GCM模式要求有附加的信息,来保证加解密过程中信息的完整性
- keyLength: 32,密钥长度
- 密钥衍生算法基础为SHA256
加密
// require cryptoe
const {
randomBytes,
createHash, createHmac,
createCipheriv, createDecipheriv, hkdfSync
} = require("crypto");
// encrypt content string with a key buffer
const encrypt = (str, fkey) =>{
// prepare the params
let
// time in salt
btime = Buffer.from((0 | Date.now()/1000).toString(16),"hex"),
// add into salt
salt = Buffer.concat([randomBytes(AES.Taglength - btime.byteLength), btime ]),
// compute a int for iv
iter = salt.reduce((c,v)=>(c + v) % 256), // 512 ~ 1024
iv = salt.map(v=>v^(iter % 256)),
// deveris the key
key = hkdfSync(AES.Hash, fkey.slice(0,20), salt, fkey.slice(-20), AES.Keylength),
// content buffer
bufContent = Buffer.from(str);
// preare the ciphter
let cipher = createCipheriv( AES.Mode, key, iv, { authTagLength: AES.Taglength});
// concat update final auth salt
let rstr = Buffer.concat([
cipher.update(bufContent),
cipher.final(),
cipher.getAuthTag(),
salt
]).toString("base64");
// add prefix if needed
if ( AES.Aprefix) rstr = AES.Aprefix + rstr;
return rstr;
}
要点说明:
- 我们选择的加密算法为 aes-256-gcm (在浏览器环境中也可以支持)
- 混合时间和随机信息,生成加密用的盐
- 使用密钥派生函数,基于原始密钥和盐每次都可以生成新的加密密钥
- 使用盐生成初始向量
- 使用标准cipheriv方法,对明文进行加密
- 简单的使用Buffer连接加密内容、Tag和IV生成最后的加密结果,并输出为base64格式
解密
解密的函数如下,输入参数为加密后的base64字符串和密钥buffer:
// decrypt a encrypte base64 string from encrypt function with same key buffer
const decrypt = (str64, fkey)=> {
let
// content buf
buf = Buffer.from(AES.Aprefix ? str64.slice(AES.Aprefix.length): str64,"base64");
// salt segment
salt = buf.slice(-AES.Taglength),
// compute iv from salt
iter = salt.reduce((c,v)=>(c + v) % 256), // 512 ~ 1024
iv = salt.map(v=>v^(iter % 256)),
// buf tag
authTag = buf.slice(-2 * AES.Taglength, -AES.Taglength),
// buf content
bufContent = buf.slice(0, -2 * AES.Taglength),
// deveris the key
key = hkdfSync(AES.Hash, fkey.slice(0,20), salt, fkey.slice(-20), AES.Keylength);
// check time
let ctime = parseInt(salt.slice(-4).toString("hex"),16);
if (ctime + 600 < ( 0 | Date.now() / 1000)) { // 10 minites
console.log("DecryptContentTimeOut");
return null;
};
let rstr = null;
try {
// constuct deciphter
let decipher = createDecipheriv( AES.Mode, key, iv, { authTagLength: AES.Taglength});
decipher.setAuthTag(authTag);
rstr = Buffer.concat([
decipher.update(bufContent),
decipher.final()
]).toString();
} catch (error) {
console.log(error);
}
return rstr;
}
要点说明:
- 解密内容其实是一个预定格式的结合了密文、Tag和IV的综合体,可以转为Buffer方便处理
- 将Buffer按照格式分解
- key的计算方式同加密端
- 解密方式基本上就是加密的逆流程
- 解密的结果是一个buffer,也可以方便的解码为UTF8字符串或者JSON
调用方式
在上述函数实现的基础上,调用其进行加解密的过程如下:
let passwd = "一个机密";
let ptext = "China中国";
let key = createHash(AES.Hash).update(passwd).digest();
let r = encrypt(ptext, key);
console.log("Encrypt:", r);
let d = decrypt(r, key);
console.log("Decrypt:", d);
特点和优势
- 使用aes-256-gcm这一比较先进和安全的模式
- 直接将iv和tag加入到加密结果中,简单紧凑
- 可选的解密有效时限和检查
- 可选加密头标识算法和参数选项,提高扩展性和兼容性
- 真实的密钥基于随机盐衍生,一次一密更安全
- 使用Buffer进行加密和解密操作,简单高效
- 每次加密的结果都不同,增加了破解的难度
- 我们这里为了简单和方便说明,使用hmac对密码做了一个简单的变换。在生产和实际环境中,应当使用诸如hkdf或者pbkdf2等密钥衍生算法来从密码来生成密钥,更为安全
扩展
在深入的理解了这套对称加解密实现的基础上,我们可以轻易的对其进行扩展,来提高安全性,或者满足不同的业务场景。
- 密钥的派生其实和密码的形式和格式无关,也就是说是可以支持中文的
- 显然密钥也可以来自密钥协商的结果,如DH,从而和非对称加密很好的结合
- 使用时间检查,可以在一定程度上对抗重放攻击
- 加解密内容其实是不限的,因此可以直接加密和解密JSON对象
转载自:https://juejin.cn/post/7225525540249174074