随着网络应用进入成熟和发展期,信息安全的考虑在网络应用的开发中,也越来越重要,已经成为一种必备和基础的技术。典型的事件就是谷歌系统和服务的全面HTTPS化,将互联网信息安全大大向前推进了一步。
nodejs内置提供了crypto模块,实现了比较完善的密码学信息处理和操作功能。本文讨论的主题就是基于密码学套件的基本组成和功能,以及和crypto模块的结合和实现。
密码学套件
在具体讨论crypto库之前,我们先抛开这个具体实现,讨论一些通用的概念和方法论。我们已经知道,nodejs中的crypto是一个密码学套件的程序实现。而通常认为,一个成熟而完善的密码学套件,应当包括以下一些特性和功能,由于相关具体的内容比较多和篇幅的限制,这里只做简要的总结和说明,并假定读者有最基础的信息安全和密码学的概念和知识。
目标
密码学研究、实现和应用的基本目标是为了保障信息安全。这通常体现在以下几点:
- 完整性
需要保障信息在传输、交换的过程中,如果内容经过了修改,则有一种机制,可以发现这个修改和变化。这一需求通常通过检查内容摘要或者消息验证码的方式来实现。
- 机密性
只有得到授权的用户,才能够真正得到原始完整的信息。这一要求,通过加密和解密方法来实现。在相关的技术实现上,通常通过对密钥的持有和使用,来体现这一授权。
- 不可否认性
也被称为不可抵赖性,就是提供一种机制,可以证明信息的来源是特定(就是持有密钥的人,如果私钥,则可以确定只有这个人)的,而且它无法否认这一点。这一需求通常通过信息的签名和验证机制,结合密钥的持有和使用来实现。
除了信息安全的指标之外,在实际应用和工程实现上,可能还需要考虑一些其他的要素:
- 易用性
安全技术方案和操作容易使用,健壮可靠。
- 性能:
信息安全不是无代价的,引入密码和加密,无疑会增加计算工作和资源占用,这些都会对应用程序的性能造成影响。任何的加密解密或者相关的算法,都应当保证其可操作性和可实用性,不会对操作和应用过程产生过于明显的影响,或者完全超出计算机系统处理的能力。
- 成本:
信息安全会带来成本。这一点,经常被故意的忽视。因为成本并不简单的体现在开发上,还体现在持续的维护和监控工作,系统额外的负担,性能下降,资源占用增加等等很多方面,这些隐形的成本,就像海洋里的冰山,我们能看到的就是一角而已。
总之密码学和信息安全的工作内容,其实就是一个工程学的问题,它需要在各种可能冲突的要素中,寻址一个可行的做法,并试图达到一个动态的平衡。
编码解码(Encode/Decode)
严格的说来,这并不是密码学套件中应该包括的内容,但实际上确实是在编程实践中,一项非常常见和频繁的处理工作,一套良好的信息编解码工具,对于密码学操作的开发也是非常重要的。在密码学操作中,会频繁的在各种信息格式之间进行转换,从而适应各种信息使用和业务操作的场景,这些格式可能包括:
- (UTF8编码的)字符串
- JSON
- Base64/Base64url字符串
- Hex字符串
- Buffer/Uint8Array
- Interger/BigInterger
随机数(Random)
随机数是保证信息动态的重要技术。加解密使用的初始化向量和密钥衍生使用的随机盐都需要随机数算法生成。简单的随机数可以直接使用算数随机数算法,但要求较高的随机数最好使用密码学套件中的生成机制。
计算机内部的随机数其实都是伪随机数,完全真正的随机信息,应当基于外部干扰信息来实现,如网络抖动、实时摄像头影像等等。
摘要方法(Hash)
摘要(Digest)方式,一般也称为哈希(Hash)、杂凑等。它是一种算法,可以将任意长度的信息,编码压缩成为一个固定长度的字节数组,比如SHA1计算将会生成一个20个字节的Buffer。这个字节数组,如果用hex编码来表示,就是一个固定长度的hex字符串。
显然,摘要算法,试图在原始信息和编码信息之间建立一种映射关系,我们基本上可以认为编码信息就可以代表原始信息,是原始信息的另外一种形式。但是,编码信息的空间是有限的,它不能完全的体现一对一的关系,但由于编码信息空间巨大,如果摘要算法设计合理良好的话,要想找到两个原始信息的编码信息输出相同的摘要信息,也是非常困难的,就是在工程和实际上,我们可以认为它们是等价的。另外,由于这种不对称的特性,使得摘要计算方法也是不可逆的,就是从编码信息无法解码还原原始信息。这些都是摘要算法的一些特点,理解这些,可以让我们在实际应用中合理使用,来满足一些业务和安全性方法的需求。
业界常用的摘要算法包括MD5、SHA1、SHA-256、SHA-3等等,也是美国国家标准与技术研究院(NIST)推荐并认可的标准化的摘要算法。需要注意的是,随着算力和算法的发展,这些算法都有版本和时限问题,并且也在持续演进,如现在已经不建议使用MD5和SHA1了。
消息验证码(HMAC)
消息验证码(Message Authentication Code,MAC)是一种消息完整性保护机制,它使用密钥对消息进行处理,生成一个短小的信息片段(信息认证码)。其他人可以使用相同的密钥和算法也进行处理,如果MAC能够匹配,则说明原始信息是完整可信的。通常这个算法,是以摘要算法作为基础,也被称为H(Hash)MAC。
为什么要使用MAC? 因为摘要算法有一个特性,笔者不愿意将其称为缺陷,就是固定的原始信息的摘要值也是固定的。这个在某些场景下会被认为有安全隐患,因为可以使用预计算和字典匹配的方式来寻求破解。所以人们就将摘要函数稍加改进,通过引入干扰信息,来为固定的信息生成完全不同的摘要信息。
可能有人会想,HMAC的实现可以很简单啊,把内容和密钥进行拼接,然后摘要不就可以了? 道理上是这样的,但实际上MAC有其特别的处理方式,可能在理论上更安全吧。通常每种摘要算法都有其对应的MAC算法,所以我们常见的MAC算法也是SHA1、SHA256等等。
密钥衍生函数(Key Derivation Function, KDF)
这里先明确一下密码和密钥的差异。虽然看起来形式相同,都是连串随机的字符信息,但笔者认为,但密码和密钥,在密码学里面其实是两个概念。密码基本上是给用户使用的,需要记忆和输入,所以它不可能太复杂;而密钥是给程序和算法使用的,它可能会有一些格式和长度的要求,比如AES算法的密钥长度就是128/192/256。所以在程序实际进行加密操作的时候,需要将密码转换成为密码,即所谓的密钥衍生函数,它可以将密码转换成为密钥的形式。
"衍生",这个词的意思是,新的密钥,是以密码为基础计算出来的,在某种意义上是这个密码的另外一种形式或者等效物。但好处就是,这个等效物可以是变化的,并且可以保护原始信息。因为,为了提高安全性,衍生算法可以在转换的过程中,使用一些干扰的参数或者手段(如加入随机盐,增加迭代计算轮数等等),使这一过程更加随机和离散,同时使转换过程不可逆。
常见的KDF算法包括scrypt,hkdf,pbkdf2等等。
对称加密解密
对称加密,就是信息的加密和解密,都使用同一套密钥。
最经典和主流的对称加密算法,就应当是AES了。笔者有一篇文章非常细致的探讨了AES算法的原理、实现和相关细节,读者有兴趣也可以去看看,这里只说基础和要点。
简单来讲,AES的基本原理是先将要加密的信息分成数据块(每块16个比特),然后,使用块密钥对块中的每个字节进行一系列变换操作,得到一个加密后的块,将所有加密后的块组合起来,得到最后的加密结果。解密的过程基本上就是加密过程的逆运算,字节和块变换操作的可逆性保证了可以使用相同的密钥进行解密。
除了块内部操作之外,AES还提出了加密模式,来规定初始化块如何使用,块的密钥如何生成(可能从上一个块结果结合原始密钥衍生而来)等等。这样基于核心的AES块操作,还衍生出更安全更复杂的组合方式,来提高整体的安全性。并在此基础上,进一步提出了认证加密(AE)的概念和实现,即所谓CCM模式和GCM模式,它们在加密的同时,可以进一步保证和检查信息的完整性,即同时保证信息的机密性和完整性,也是更先进的建议的算法模式。
除了块加密技术之外,还有一种流加密的技术。流加密技术的原理和块加密有很大的差异。简单来讲,流加密是基于初始密钥,持续产生出和所加密的信息流相同的密钥位,并和原始信息流位做简单的异或操作,得到加密位和加密后的信息流。从原理上来看,流加密算法的操作和实现更简单直观,也更加灵活方便(从安全性角度差异不大),性能和资源利用也更好一点,所以被更广泛的应用在移动互联网的场景当中。并且日益扩展到更加通用的场景当中。
除了AES和chacha20之外,其实还有很多中对称加密算法,比如DES、RC4、chacha等等
非对称加密解密
非对称的意思就是加密和解密使用的不是一个密钥,而是要配合使用公钥和私钥。比如Charlie要向Steve发送信息,为了保障机密,他可以使用Steve的公钥进行加密,这时只有Steve可以使用自己的私钥,才能够正确解密。
从某种意义上而言,非对称加解密不仅仅简单的是一种算法和操作,而是一个体系,它基于非对称加密的数学理论,可以衍生出信息加密解密,密钥协商,签名和验证等各种应用方式和场景,来满足不同层面信息安全的要求。
这部分内容比较多,这里只作为一个摘要,笔者另外规划了相关的文章,结合操作和代码,来详细展开说明。
一次性密码(OTP)
OTP并不属于标准的密码学套件中的内容,看可以看成是一种衍生的应用方式。笔者另有一篇文章进行了详细的阐述。这里只做简单的说明。OTP即一次性密码,它是一种算法,可以在一个固定密钥的基础之上,在实际使用的时候,生成动态的密码用于身份验证。动态的要素,可以是步进的方式,也可以是时间片的方式。OTP现在常用做多因子验证。笔者理解,OTP本质上就是用户密码或者识别密钥的另外一种体现形式,可以看作其等价物用于身份验证,但由于是动态的,所以可以提供更好的安全性。
编码解码操作
这一部分并不是标准的密码学操作的范畴,在nodejs中也不是在crypto模块中实现的,所以我们在这里单独讨论。
nodejs本身就提供了一个强大的编码转换机制,就是Buffer,它支持很多种格式到Buffer之间的相互转换。实际上,在nodejs中,可以以buffer格式为中心,实现任意两种编码格式之间的转换。下面是一些代码示例:
js
// json转为Base64
let o = { id:1, title: "中国" };
let b64 = Buffer.from(JSON.stringify(o)).toString("base64");
// Base64转为JSON
let o2 = JSON.parse( Buffer.from(b64,"base64").toString());
在信息编码操作过程中,有一些经验和技巧:
- Base64和Base64URL格式,Buffer都是可以识别的
- atob/btoa方法慎用,虽然它在nodejs和浏览器中都可以直接使用,主要问题是字符集的支持问题
- 在前端没有buffer,可以考虑使用TextEncoder,前后端都可以使用
crypto模块
nodejs提供了crypto的相对比较完整的实现。当然这个实现也不是完全重新构建的,而是基于一个应用非常广泛的密码学软件包和库-OpenSSL。nodejs在其上面做了一些封装工作,从而能够保证开发者可以通过JS API来使用它,并且在不同的平台上有类似的开发和应用体验。
crypto模块的结构如下:
js
> crypto
{
createCipheriv: [Function: createCipheriv],
createDecipheriv: [Function: createDecipheriv],
createDiffieHellman: [Function: createDiffieHellman],
createDiffieHellmanGroup: [Function: createDiffieHellmanGroup],
createECDH: [Function: createECDH],
createHash: [Function: createHash],
createHmac: [Function: createHmac],
createPrivateKey: [Function: createPrivateKey],
createPublicKey: [Function: createPublicKey],
createSecretKey: [Function: createSecretKey],
createSign: [Function: createSign],
createVerify: [Function: createVerify],
diffieHellman: [Function: diffieHellman],
getCiphers: [Function (anonymous)],
getCurves: [Function (anonymous)],
getDiffieHellman: [Function: createDiffieHellmanGroup],
getHashes: [Function (anonymous)],
pbkdf2: [Function: pbkdf2],
pbkdf2Sync: [Function: pbkdf2Sync],
generateKeyPair: [Function: generateKeyPair],
generateKeyPairSync: [Function: generateKeyPairSync],
privateDecrypt: [Function (anonymous)],
privateEncrypt: [Function (anonymous)],
publicDecrypt: [Function (anonymous)],
publicEncrypt: [Function (anonymous)],
randomBytes: [Function: randomBytes],
randomFill: [Function: randomFill],
randomFillSync: [Function: randomFillSync],
randomInt: [Function: randomInt],
scrypt: [Function: scrypt],
scryptSync: [Function: scryptSync],
sign: [Function: signOneShot],
setEngine: [Function: setEngine],
timingSafeEqual: [Function: timingSafeEqual],
getFips: [Function: getFipsDisabled],
setFips: [Function: setFipsDisabled],
verify: [Function: verifyOneShot],
Certificate: [Function: Certificate] {
exportChallenge: [Function: exportChallenge],
exportPublicKey: [Function: exportPublicKey],
verifySpkac: [Function: verifySpkac]
},
Cipher: [Function: Cipher],
Cipheriv: [Function: Cipheriv],
Decipher: [Function: Decipher],
Decipheriv: [Function: Decipheriv],
DiffieHellman: [Function: DiffieHellman],
DiffieHellmanGroup: [Function: DiffieHellmanGroup],
ECDH: [Function: ECDH] { convertKey: [Function: convertKey] },
Hash: [Function: Hash],
Hmac: [Function: Hmac],
KeyObject: [class KeyObject],
Sign: [Function: Sign],
Verify: [Function: Verify],
constants: [Object: null prototype] {
OPENSSL_VERSION_NUMBER: 269488303,
SSL_OP_ALL: 2147485780,
SSL_OP_ALLOW_NO_DHE_KEX: 1024,
SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION: 262144,
SSL_OP_CIPHER_SERVER_PREFERENCE: 4194304,
SSL_OP_CISCO_ANYCONNECT: 32768,
SSL_OP_COOKIE_EXCHANGE: 8192,
SSL_OP_CRYPTOPRO_TLSEXT_BUG: 2147483648,
SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS: 2048,
SSL_OP_EPHEMERAL_RSA: 0,
SSL_OP_LEGACY_SERVER_CONNECT: 4,
SSL_OP_MICROSOFT_BIG_SSLV3_BUFFER: 0,
SSL_OP_MICROSOFT_SESS_ID_BUG: 0,
SSL_OP_MSIE_SSLV2_RSA_PADDING: 0,
SSL_OP_NETSCAPE_CA_DN_BUG: 0,
SSL_OP_NETSCAPE_CHALLENGE_BUG: 0,
SSL_OP_NETSCAPE_DEMO_CIPHER_CHANGE_BUG: 0,
SSL_OP_NETSCAPE_REUSE_CIPHER_CHANGE_BUG: 0,
SSL_OP_NO_COMPRESSION: 131072,
SSL_OP_NO_ENCRYPT_THEN_MAC: 524288,
SSL_OP_NO_QUERY_MTU: 4096,
SSL_OP_NO_RENEGOTIATION: 1073741824,
SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION: 65536,
SSL_OP_NO_SSLv2: 0,
SSL_OP_NO_SSLv3: 33554432,
SSL_OP_NO_TICKET: 16384,
SSL_OP_NO_TLSv1: 67108864,
SSL_OP_NO_TLSv1_1: 268435456,
SSL_OP_NO_TLSv1_2: 134217728,
SSL_OP_NO_TLSv1_3: 536870912,
SSL_OP_PKCS1_CHECK_1: 0,
SSL_OP_PKCS1_CHECK_2: 0,
SSL_OP_PRIORITIZE_CHACHA: 2097152,
SSL_OP_SINGLE_DH_USE: 0,
SSL_OP_SINGLE_ECDH_USE: 0,
SSL_OP_SSLEAY_080_CLIENT_DH_BUG: 0,
SSL_OP_SSLREF2_REUSE_CERT_TYPE_BUG: 0,
SSL_OP_TLS_BLOCK_PADDING_BUG: 0,
SSL_OP_TLS_D5_BUG: 0,
SSL_OP_TLS_ROLLBACK_BUG: 8388608,
ENGINE_METHOD_RSA: 1,
ENGINE_METHOD_DSA: 2,
ENGINE_METHOD_DH: 4,
ENGINE_METHOD_RAND: 8,
ENGINE_METHOD_EC: 2048,
ENGINE_METHOD_CIPHERS: 64,
ENGINE_METHOD_DIGESTS: 128,
ENGINE_METHOD_PKEY_METHS: 512,
ENGINE_METHOD_PKEY_ASN1_METHS: 1024,
ENGINE_METHOD_ALL: 65535,
ENGINE_METHOD_NONE: 0,
DH_CHECK_P_NOT_SAFE_PRIME: 2,
DH_CHECK_P_NOT_PRIME: 1,
DH_UNABLE_TO_CHECK_GENERATOR: 4,
DH_NOT_SUITABLE_GENERATOR: 8,
ALPN_ENABLED: 1,
RSA_PKCS1_PADDING: 1,
RSA_SSLV23_PADDING: 2,
RSA_NO_PADDING: 3,
RSA_PKCS1_OAEP_PADDING: 4,
RSA_X931_PADDING: 5,
RSA_PKCS1_PSS_PADDING: 6,
RSA_PSS_SALTLEN_DIGEST: -1,
RSA_PSS_SALTLEN_MAX_SIGN: -2,
RSA_PSS_SALTLEN_AUTO: -2,
defaultCoreCipherList: 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA',
TLS1_VERSION: 769,
TLS1_1_VERSION: 770,
TLS1_2_VERSION: 771,
TLS1_3_VERSION: 772,
POINT_CONVERSION_COMPRESSED: 2,
POINT_CONVERSION_UNCOMPRESSED: 4,
POINT_CONVERSION_HYBRID: 6
}
}
我们可以按照前面密码学通用方法论的方式,来对这些内容进行分类,结合简单的代码示例,就可以很好的理解了。虽然面的细节和信息很多,但大致可以分为以下几类:
- 常数: 包括各种算法、设置的标识等
- 能力: 当前可用的算法和设置列表(基于OpenSSL)
- 随机算法:随机数,随机数组
- Hash和Hmac: 摘要和消息验证码
- 密钥衍生方法(KDF)
- 加密解密器: 用于加密和解密的功能对象和实例
- 密钥对象: KeyObject相关,包括格式,类型等等
- 公钥私钥:密钥对的创建和操作
- 公私钥加解密
- 密钥协商(DH): 包括DH和ECDH
- 签名和验证
- 证书相关: 包括x509等
crypto应用示例
编码解码
前面已经提到,在nodejs中,可以使用buffer作为中转,进行各种编码形式之间的转换,常见的有:
js
// 随机生成密钥并转换为hex字符串
let hkey = crypto.randomBytes(8).toString("hex");
// hex转换为buffer
let bkey = Buffer.from(hkey,"hex");
// json base64编码
let b = Buffer.from(JSON.stringify({})).toString("base64").replace(/\=/g,"");
// base64转json
let o = JSON.parse(Buffer.from(b,"base64").toString());
随机信息
可以使用如Math.random, 还有crypto random系列方法来生成随机信息:
js
// 256内的随机整数
let r1 = 0 | Math.random() * 0xFF;
// 使用cryptoe生成随机整数
r1 = crypto.randomInt(256);
// 随机16位buffer
let r3 = crypto.randomBytes(16);
// 随机填充,用于重置已有buffer
crypto.randomFill(Buffer.alloc(16), (err,d)=> err? console.log("Error") :console.log("Buffer:",d));
// 随机8位hex字符串
let r2 = Math.random().toString(16).slice(2,10);
// 随机UUID, 使用UUIDv4
let r5 = crypto.randomUUID();
在进行密码学操作计算中,应优先选择crypto提供的随机信息方法。
Hash和MAC
在nodejs的crypto模块进行Hash和MAC操作,是非常简单的:
js
let hash = crypto.createHash("SHA1").update("content").digest("hex");
let hmac = crypto.createHmac("SHA1","key").update("content").digest("hex");
相关要点包括:
- 可以使用链式操作
- 计算结果默认为buffer,可以方便的转为常用的hex字符串
- 通过参数来控制所选择的算法家族
- key本身也应该是buffer,提供字符串时程序将会自动将其编码为buffer
对称加密
这里主要讨论使用AES进行对称加密操作。在实践中和生产环境,一个比较建议的模式是AES-256-GCM。一个相对完整的操作参考代码如下:
js
const crypto = require("crypto");
const CONFIG = {
ALGO: "AES-256-GCM",
LEN_KEY: 32, // key length for AES-256
ITER: 2048, // iter of key derive
LEN_SALT : 16, //
LEN_IV : 16,
LEN_TAG : 16, // default value
HASH : "SHA256" // pbkdf2 hash
};
const otext = "China中国";
// aes-256-gcm
const passwd = "TopSecrec";
console.log("=========== Encrypt ===============");
let salt = crypto.randomBytes(CONFIG.LEN_SALT);
console.log("Salt", salt);
const key = crypto.pbkdf2Sync(passwd, salt, CONFIG.ITER, CONFIG.LEN_KEY, CONFIG.HASH);
console.log("Key", key);
let iv = crypto.randomBytes(CONFIG.LEN_IV);
console.log("IV", iv);
const cipher = crypto.createCipheriv(CONFIG.ALGO, key, iv);
let etext = Buffer.concat([
cipher.update(otext,"utf8"),
cipher.final(),
cipher.getAuthTag(), // default 32 bit
iv,
salt
]).toString("base64");
console.log("AuthTag:", cipher.getAuthTag());
console.log("Encrypted:", etext, Buffer.from(etext,"base64"));
// decrypt
let
bufAll = Buffer.from(etext,"base64"),
lenBuf = bufAll.length,
psalt = lenBuf - CONFIG.LEN_SALT,
piv = psalt - CONFIG.LEN_IV,
ptag = piv - CONFIG.LEN_TAG,
bufSalt = bufAll.subarray(psalt, lenBuf),
bufIV = bufAll.subarray(piv, psalt),
bufTag = bufAll.subarray(ptag, piv),
bufCont = bufAll.subarray(0,ptag);
console.log("=========== Decrypt ===============");
console.log("Salt", bufSalt);
console.log("IV", bufIV);
console.log("AuthTag", bufTag);
console.log("Content", bufCont);
try {
const decipher = crypto.createDecipheriv(CONFIG.ALGO, key, bufIV);
// set AuthTRag
decipher.setAuthTag(bufTag);
let otext2 =Buffer.concat([
decipher.update(bufCont),
decipher.final()
]);
console.log("Original:", otext2.toString("utf8"));
} catch (error) {
};
这里其实展示了很多细节,相对而言时比较完整并且建议使用的,相关要点包括:
- 核心配置信息包括加密模式、相关可配置参数和默认参数的值
- GCM模式集成完整性检查,推荐使用
- 设置随机的初始化向量可以增加加密安全性
- 此处为了操作方便,中间过程都使用Buffer,包括定长的参数,都统一置入和封装到一个整体Buffer中
- 解密时,将各部分从密文Buffer中拆解出来
- 这里省却了在解密端,密钥的产生过程,因为其和在加密端是完全相同的
这里再简单分析一下crypto加解密的具体实现过程和一些要点,帮助大家更好的理解其应用方式。
1 加密器和解密器(ciphter/decipher)
crypto加密使用一个加密器(cipher)实例对象,当然对应有解密器(decipher),它们都有对应的create创建程序。这里iv是带有初始化向量的加解密器,需要用对应的创建方法。
nodejs crypto的很多类都是这种使用模式:create-update-final。就是创建处理器,更新数据,结束操作输出结果,明确和理解这些,就可以熟悉相关代码的编写。
2 初始化向量 Initial Vector(iv)
初始化向量(IV)对于加密安全比较重要,可以保证加密结果的随机性,不建议忽略。这个向量可以由randomBytes方法随机生成。而Salt(盐)和IV虽然都是随机信息,但它们的使用场景和作用稍有区别。Salt是给密码衍生密钥时做混淆的,是密码安全的一部分;而IV是AES加密算法的一部分,在创建加解密器的时候提供,IV的长度应该和AES块大小相同,16比特。
3 加密模式和认证加密
AES还有一个重要的选项和设置是加密模式,这里选择的是GCM。简单而言,GCM包括CCM都是一种认证加密(Authenticated encryption,AE) 的模式,和传统模式相比,它可以在加密时同时保证数据的完整性和真实性,从而提供更高但操作相对简便的安全性。
使用AE,对于应用过程的影响,就是需要额外处理两个信息:AuthTag和ADD(附加数据)。AuthTag是一个加密过程中产生的附加信息,默认情况下,这个信息(Buffer)的长度是16比特,加密完成后,可以使用getAuthTag方法提取,可以简单的理解为加密信息的签名。而在解密时,在创建解密器后,实际解密操作之前,需要先设置相同的信息。
ADD则是可以自己定义的附加信息。这里为了操作简单,没有使用ADD。如果读者感兴趣的话,可以自行查阅技术文档,应该和AuthTag的处理方式类似,只不过是可以作为明文使用,提供了更高的业务灵活性。
4 密钥长度
至于密钥的长度,使用的是256位,从AES的原理上来看,它会影响到一些操作的过程(如密钥衍生过程不同,多几轮计算等等),但从实践角度而言,这几种密钥长度的安全性几乎没有太大的差别。没有特别的要求的话,建议都使用256这个设置。
5 密钥衍生
这里还演示了密钥衍生的算法。这里选择的是pbkdf2(Password-Based Derivation Function,基于密码的密钥衍生函数2),这是开放的标准算法。它通过加入随机信息(盐)和多次迭代,基于一个固定密码,生成动态的密钥提供给实际加密使用。这个衍生算法,使用一个标准哈希函数,这里选择的是SHA256。迭代计算次数不应该少于1000。盐的长度应该不小于16比特。对于AES-256算法,所需生成密钥的长度为32比特。
非对称加密
这部分的内容比较丰富,包括密钥对生成和加载,公私钥加解密,密钥协商和签名验证等等。笔者另行撰文叙述讨论。
Web Cryptography API
在比较新的nodejs版本中,增加了一个模块: Web Cryptography API。这是一个W3C加密标准的nodejs实现。
W3C加密标准的API规范链接如下: www.w3.org/TR/WebCrypt...
笔者理解,这个标准本来是为浏览器环境设计的,在nodejs中,基本上就是简单的移植了一下其在浏览器前端的实现,这样nodejs可以和浏览器使用完全相同的密码学编码和代码实现,从而能够保证最大的兼容能力,并且简化开发工作。
笔者认为,作为主要面向系统后端开发的人员,在这一阶段,只需要知晓和掌握这个情况就可以了。所以本文不再展开详细讨论相关内容。
浏览器兼容
我们前面讨论的都是在nodejs内部的技术实现。但在真正的Web应用中,我们还必须要考虑和浏览器环境,甚至第三方Web应用兼容的问题。这里不会再详细的阐述密码学相关操作在浏览器环境中实现的细节,只例举一些和nodejs程序开放和互动相关的要点:
- nodejs应用经常使用buffer作为数据的基本形式,而浏览器环境通常使用Uint8Array(字节数组)
- 同样,文字信息的编解码方式,在浏览器环境中可以使用TextEncoder/TextDecoder
- 早期的浏览器需要使用第三方密码库程序,如cryptoJS、SJCL等,它们一般是纯JS实现
- 比较新版本的浏览器可以使用WebCryptoAPI,并且和新版的nodejs对应模块兼容
- crypto操作比较适合使用WebAssembly实现,也许我们会看到OpenSSL的WA版本?
其他技术选项
通用的密码学相关算法和实现,基本上已经完全的公开化和标准化了。因为它基于以下一些的现代化的信息安全和密码学理念:
- 密文的安全不依赖于算法和实现方法的机密性,而主要是密钥的机密性
- 开放的算法可能更安全,因为有更多的实践和检验机会
- 封闭的算法有一个安全隐患,就是可能被破解而不自知
nodejs的crypto模块,主要也是基于一个广泛部署应用的开源密码学软件库-OpenSSL。所以它提供的所有基础功能和设置方式,也是和它是相同的。而且从原理上讲,所有主流的密码学算法也都是公开的,可以使用各种语言和工具来实现,OpenSSL也只是其中之一。所以,市场上也是有很多JS体系的密码学算法的实现,也许一些处理的细节可能有差异,但理论它们都是可以相互兼容的。这里简单的例举几个:
- CryptoJS:老牌JS密码学算法实现
- SJCL(Standford Javascript Crypto Library): 斯坦福出品的JS密码学程序库,独特有趣
- NaCL:专注于网络通信安全,简单易用,性能突出
至于其他语言体系,则需要看其具体的实现方式来进行具体的分析,但基本上也遵循相同的方式和逻辑,但确实要注意其技术实现细节的差异。
小结
本文围绕着nodejs的密码学实现-crypto模块,探讨了密码学相关概念、组成和基础的方法论,并结合crypto的实现,提供和分析了相关测试和实例代码,以帮助开发者理解和使用crypto模块完成系统应用中有关信息安全的处理操作和实现。
由于篇幅限制,本文的内容非完全,有关非对称加密相关的内容另行撰文讨论。