概述
本文是笔者的系列博文 《Bun技术评估》 中的第八篇。
本文主要探讨的内容,是研究在bun的体系中,如何处理密码学相关的操作和内容。在信息安全越来越重要的情况下,这些方面的内容和支持,对于一个应用开发系统,也是非常重要的。而且,我们讨论的是一个全功能的密码学框架,而不是某个特定的功能或者算法。
在这方面,nodejs中的crypto模块,其实已经做得非常好了,无论在功能的完善、安全性、算法的丰富性、易用性和性能方面,都是非常成熟的。笔者原来也以为,bun可以直接应用和继承nodejs crypto的完整的生态,但在实际应用中,却发现事情好像没有那么简单。不知道是什么原因的限制,虽然bun crypto在架构上确实应用了nodejs的实现,但在一些细节上,却略有差异,而且在某些情况下,这个差异还不小,影响到应用开发的程度。这就是笔者对于bun crypto的初步印象,我们会在后面结合一些实例,展开讨论。
本文主要探讨的内容,同样主要来自bun官方技术文档的相关篇幅。比较奇怪的是,笔者觉得bun对于密码学模块的规划,并没有nodejs那么重视和完善,它甚至没有专门的模块和文档板块,而是分散在功能和函数的层面。就是笔者可以感受到,关于密码学操作,主要分为两个大的方面,第一是 bun本身提供和实现了一些密码学方法;其次,bun可以通过crypto模块,来实现更多的密码学操作。
内置密码学函数
Bun在bun应用程序中,是一个全局的静态类。它本身就包括一些常见的密码学操作实现。
Hash
js
Bun.hash("some data here", 1234);
// 15724820720172937558n
Bun.hash.wyhash("data", 1234); // equivalent to Bun.hash()
Bun.hash.crc32("data", 1234);
Bun.hash.adler32("data", 1234);
Bun.hash.cityHash32("data", 1234);
Bun.hash.cityHash64("data", 1234);
Bun.hash.xxHash32("data", 1234);
Bun.hash.xxHash64("data", 1234);
Bun.hash.xxHash3("data", 1234);
Bun.hash.murmur32v3("data", 1234);
Bun.hash.murmur32v2("data", 1234);
Bun.hash.murmur64v2("data", 1234);
Bun.hash.rapidhash("data", 1234);
这里的hash,确实是摘要算法,但不是严格密码学意义上的标准完整摘要。但它有个好处,就是效率和性能要高于标准的SHA等算法,特别适合于内部使用,因为在很多情况下,我们其实是不需要全功能的摘要方法的(比如完整性检查、临时签名等等)。
Bun.hash默认使用wyhash算法,当然从示例中,我们可以看到还有很多可选的实现。这类hash算法,都是可以处理一个字符串或者buffer,返回的结果,就是一个简单的32位或者64位的整数。一般执行时,还可以同时提供一个"种子"(整数),来混淆计算结果。如果把这个种子看成简单的密钥,那么这些hash算法就变成了简单的HMAC算法。
当然,标准的密码学hash算法,结果也是一个整数,但它们都是大整数,空间远超标准的32位或者64位整数。下面就是一个简单的示例:
js
> require("crypto").createHash("SHA256").update("data").digest("hex")
'3a6eb0790f39ac87c94f3856b2dd2c5d110e6811602261a9a923d3bb23adc8b7'
> Bun.hash("data", 12345678).toString(16);
'551d3d5d03460bfa'
CryptoHasher
标准密码学摘要算法,在bun中,被称为CryptoHasher(密码学摘要)。它几乎完整的支持你所能见到的主流摘要算法你包括MD5、各种SHA、blake、shake等等,它的使用方式也和nodejs crypto类似,下面是一个简单的示例:
js
> new Bun.CryptoHasher("sha256").update("data").digest("hex")
'3a6eb0790f39ac87c94f3856b2dd2c5d110e6811602261a9a923d3bb23adc8b7'
HMAC的实现:
js
const hasher = new Bun.CryptoHasher("sha256", "secret-key");
hasher.update("hello world");
console.log(hasher.digest("hex"));
// => "095d5a21fe6d0646db223fdf3de6436bb8dfb2fab0b51677ecf6441fcf5f2a67"
bun的cryptoHasher有一个有趣的特性,就是它支持实例复制,这样就不需要为了多个摘要计算,创建多个实例了(nodejs是这么操作的)。
js
const hasher = new Bun.CryptoHasher("sha256", "secret-key");
hasher.update("hello world");
const copy = hasher.copy();
copy.update("!");
console.log(copy.digest("hex"));
// => "3840176c3d8923f59ac402b7550404b28ab11cb0ef1fa199130a5c37864b5497"
console.log(hasher.digest("hex"));
// => "095d5a21fe6d0646db223fdf3de6436bb8dfb2fab0b51677ecf6441fcf5f2a67"
new Bun.CryptoHasher("sha256","secret-key").update("hello world!").digest("hex")
'3840176c3d8923f59ac402b7550404b28ab11cb0ef1fa199130a5c37864b5497'
这个示例中,要注意copy的时机,copy.update(),已经是在hello world的基础之上,又新增加了内容,而非重新摘要。
Password
Bun提供了一个基于argon2和bcrypt算法的密码编码和验证的算法。可以用于密码编码,存储和验证。
js
const password = "super-secure-pa$$word";
const hash = await Bun.password.hash(password);
// => $argon2id$v=19$m=65536,t=2,p=1$tFq+9AVr1bfPxQdh6E8DQRhEXg/M/SqYCNu6gVdRRNs$GzJ8PuBi+K+BVojzPfS5mjnC8OpLGtv8KJqF99eP6a4
const isMatch = await Bun.password.verify(password, hash);
// => true
// 算法可选bcrypt
await Bun.password.hash(pwd, { algorithm: "bcrypt" });
这个密码操作实现的想法很好(有salt生成和算法参数设置),但在实际应用环境中,例如标准的Web登录验证的过程,可能会遇到浏览器没有内置argon2算法支持的问题。笔者现在尚未找到和理解很好的应用这个模式的场景。
Crypto模块
前一个章节的内容,是Bun类直接内置的快速密码学操作函数,并不是完整的密码学套件。而这方面的内容,是通过另外一个crypto模块提供的,但是要注意的是,虽然它也叫crypto,但并不是直接继承和使用的nodejs的crypto,笔者感觉是基本上是重新用zig实现了一遍,当然它会尽力提供和nodejs兼容的接口和使用方式。所以,从开发者来看,这两者的开发使用是差异不大的。
按照密码学操作的框架,这里我们简单的检查一下,在bun中是如何操作和实现的,这里的内容较多,笔者只是例举一个框架和要点:
- 引用
bun使用crypto或者node:crypto来引用模块提供的方法。
js
const {
createHmac, createCipheriv, createDecipheriv, randomBytes , getCurves,
} = require("node:crypto");
- 编码和转换
bun支持标准的nodejs buffer和TextEncoder/Decoder等编码转换方式,这一点基本继承了nodejs相关的功能和特性。
- 随机信息
bun crypto提供的随机函数包括randomBytes,randomFill,randomint,randomUUID等等。可以用于生成各种随机信息。
- HASH/HMAC
除了再前面提到过的Bun类型提供的hash方法之外,也支持crypto相关的hash和hmac处理,使用方法和nodejs无异。
- 密钥衍生
bun crypto提供了scrypto,pbkdf2、hkdf等标准密钥衍生算法。这些算法基本上是比较常用和标准的。
- 对称加密解密
bun实现了标准的createCipherIV和createDecipher等加密器创建方法和使用方法。支持大部分常用的aes算法和参数(其实根本用不了那么多)。
- 非对称加密
bun crypte支持非对称加密的generateKeyPair,createPrivateKey,createPublickKey等方法,可以用于生成和处理各种密钥。密钥对象还支持各种格式的导入导出操作。
基于生成的密钥,提供了privateEncrypt/privateDecrypt,publicEncrypt/publicDecrypt等私钥公钥加解密方法。
- 签名和验证
bun中可以使用nodejs系统中标准的sign和verify方法。它们使用了类似的公钥和私钥对象。
- 密钥协商
bun提供了标准ECDH密钥协商机制和方式。
- OTP
bun没有内置OTP的实现,要使用OTP,需要开发者自行实现。
- 其他
bun提供了类似的timeSafeEqual函数。
- 建议的算法集
如果使用bun进行开发,笔者建议的密码学算法和参数组合是:
BASE64URL-WYHASH-SHA256-AES-256-GCM-SECP384R1-ECDH-PBKDF2
当然笔者理想中的算法和参数组合是:
BASE63URL-WYHASH-SHA256-CHACHA20-POLY1305-SECP384R1-ECDH-PBKDF2
性能
从笔者的评估经验来看,虽然表面上,bun继承实现了nodejs的crypto模块,但可能是由于内部的执行机制还是略有差异的,体现在性能测试上,结果差别还是比较大的。
下面是一个简单的测试过程,包括了hmac和aes加解密的操作,为了方便简单,甚至它们可以共享一套相同的代码:
js
const {
createHmac, createCipheriv, createDecipheriv, randomBytes
} = require("crypto");
const start = async()=>{
let key = randomBytes(32);
let t,c,d, tag, text, i = 100000;
console.time("CRYPTO");
while(i--) {
text = Math.random().toString(36).slice(2,10);
text = createHmac("SHA256",key).update(text).digest();
// encrypt
c = createCipheriv("aes-256-gcm",key,key.subarray(-8));
t = Buffer.concat([c.update(text), c.final()]);
tag = c.getAuthTag();
// decrypt
d = createDecipheriv("aes-256-gcm",key,key.subarray(-8));
d.setAuthTag(tag);
text = d.update(t).toString();
}
console.timeEnd("CRYPTO");
}; start();
// nodejs执行
node c2.js
CRYPTO: 1.825s
// bun 执行
bun c2.js
[869.76ms] CRYPTO
虽然测试体现了bun相对于nodejs而言在密码学操作方面的优势,但也应该看到,两者的密码学计算和操作的性能都非常高,所以如果业务系统对密码学操作的性能要求不是非常看重,这并不是一个很大的问题。
问题和不足
在笔者的评估和应用过程中,发现了一些问题和不足:
- 椭圆曲线的种类和数量
js
bun repl
> require("crypto").getCurves();
[ 'secp224r1', 'prime256v1', 'secp384r1', 'secp521r1' ]
node
> require("crypto").getCurves();
[
'Oakley-EC2N-3',
'Oakley-EC2N-4',
'SM2',
'brainpoolP160r1',
'brainpoolP160t1',
'brainpoolP192r1',
'brainpoolP192t1',
'brainpoolP224r1',
'brainpoolP224t1',
'brainpoolP256r1',
'brainpoolP256t1',
'brainpoolP320r1',
'brainpoolP320t1',
'brainpoolP384r1',
'brainpoolP384t1',
'brainpoolP512r1',
'brainpoolP512t1',
'c2pnb163v1',
'c2pnb163v2',
'c2pnb163v3',
'c2pnb176v1',
'c2pnb208w1',
'c2pnb272w1',
...
在现阶段,bun只支持区区4种椭圆曲线。虽然大多数ECC可能开发者都不会用到,这四种其实也是够用的,但有两个可能缺失的选择,包括SM2(国密算法)和secp256k1(区块链标准曲线)也是挺遗憾的。希望bun社区能够快速的弥补修复这个问题。
- 不支持chacha20-poly1305
这其实也是一个不小的问题。因为其实chacha20-poly1305也是一个应用非常广泛的数据流加密算法。这个问题笔者也不是特别理解。现在的弥补方案是引入其js实现。
- 其他不支持的算法
bun不支持的其他算法包括AES-ECB,DES/3DES,RC4等,但这些都是由于安全性的原因弃用的算法,这不是一个很大的问题。
- ECC密钥处理的兼容性
笔者在使用bun进行ECC加密的过程中,遇到了一个这个问题。在nodejs环境中,可以基于一个保存的私钥,恢复密钥对象。但相同的操作,在bun中是不能完成的。但后面在导入过程中,先计算私钥对应的公钥,补足后,也是可以恢复的,就是不如在nodejs环境中方便而已。这是一个兼容性的问题,但可以解决。
小结
本文评估和探讨了bun对于密码学操作的支持和功能,包括了bun内置的密码学方法包括hash/hmac,和通过crypto模块实现的密码学操作。简单例举了其配置和使用方式,支持的主要算法和不支持的部分。并简单的对比bun crypto和nodejs实现之间的性能差异。