在一些业务场景下,我们经常需要实现一些hash摘要来进行浏览器到服务端的验证逻辑,但是如果载入第三方库,我们又需要避免该库被攻击,而nodejs自带的crypto模块可以实现加密解密,却无法在浏览器端找到对等的实现。其实,浏览器端早就提供了 Web Crypto API,我们就可以利用浏览器原生的接口来实现摘要hash啦,这样无论是在性能上,还是安全性上,都是最优解。
从caniuse反应的兼容性看,大部分浏览器都已经支持了,只要不使用低版本浏览器,都是可以放心使用的。当然,如果一定要支持,可以使用第三方库兜底。
让我们来认识一下 Web Crypto API。
Web Crypto API
Web Crypto API 是一组以构建密码学系统为目标的让脚本可以使用原生加密算法的接口。在浏览器端,它主要提供了两套和密码学关联的体系:random 和 subtle。从名字就可以看出,random负责随机算法,也就是说,基于Web Crypto API我们可以在浏览器端实现真正的随机,而不是Math.random这种伪随机。subtle则负责像hash、签名、加密、解密等密码学处理。
于此同时,由于它非常靠近原生底层,因此,它的接口使用时,大部分都会以ArrayBuffer作为参数。因此,如果你要使用它,你最好还了解ArrayBuffer相关的使用方法,以在使用时,可以更熟练的实现字符串、数值和buffer之间的转换。而buffer又是可以在网络间传输的,因此,我们又可以把buffer发送到服务端保存,实现各种签名与验证。
我把相关知识点梳理为一张知识导图,方便你梳理:
可以看到,要全面掌握Web Crypto API也并不难,因为它只提供了底层的实现,而要设计出完整的密码学系统,则需要有更丰富的密码学知识,简单靠这些API是不可能真正做到安全的。既然如此,那我们用它们有什么用呢?当然有用,因为设计密码学系统的,往往是后端的安全侧的工程师,当他们需要前端同学完成某些密码学处理时,我们有了这部分知识,才能快速实现我们的需求,如果没有掌握这些API,没有理解其中的规律,那么很难快速完成业务需求。
两端对齐的HASH摘要实现
回到我们的题目中,我们题目的使用场景是前端需要将摘要hash发送给后端,后端对该hash进行验证,验证通过后才予以后续处理。如果我们设计一套密码学系统,那么这里不仅需要使用密钥、签名、导出、加密等等,还要在这些基础的API使用之上,设计一套前后端对齐的加密协议,否则不可能做到真正安全的加密验证。要完成如此复杂的设计,对于前端同学来说,是很难的,因此,我们能够通过hash进行验证,就是不错的一种尝试了。
市面上比较多情况下,会习惯使用md5摘要,但是Web Crypto API中没有提供直接的md5摘要算法,因此,我们只能从众多SHA算法中挑一个。
此外,我们还必须对ArrayBuffer有较多的了解,因为这些API要实现算法,都需要借助ArrayBuffer来进行内存级别的运算,它们都是较低级别的接口。因此,想得到我们习惯的使用方式,还得进行封装。接下来,我们来实现一个简易的hash函数:
async function sha(str) { const encoder = new TextEncoder(); const data = encoder.encode(str); const hashBuffer = await crypto.subtle.digest("SHA-256", data); const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array const hashHex = hashArray .map((b) => b.toString(16).padStart(2, "0")) .join(""); // convert bytes to hex string return hashHex; }
我们用SHA-256算法实现了一个摘要函数sha,这样,我们就可以在浏览器端对我们想要的文本获取它的hash。
接下来,我们来到nodejs这一端。
由于Web Crypto API是底层原生实现,因此它可以被移植(,类似的可以被移植到原生模块,其实有很多,就看nodejs官方愿不愿意去做)。nodejs通过crypto模块暴露了webcrypto接口,而该接口就提供了和浏览器端相同的实现。接下来,我们就来实现一个与上面的sha函数具有相同功能的nodejs函数:
const { webcrypto } = require('crypto'); const { TextEncoder } = require('util'); async function sha(str) { const encoder = new TextEncoder(); const data = encoder.encode(str); const hashBuffer = await webcrypto.subtle.digest("SHA-256", data); const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array const hashHex = hashArray .map((b) => b.toString(16).padStart(2, "0")) .join(""); // convert bytes to hex string return hashHex; }
你会发现,我们几乎可以原封不动的把浏览器端的代码移植过来,如果做好封装,甚至可以实现代码同构,只要把subtle对象作为参数传入即可。
如此一来,我们就可以做到,当后端同学需要我们在前端处理并发送一个hash时,可以用相同的实现来处理了。而且由于我们使用了原生接口,无论是性能,还是安全性上,都比使用第三方纯代码实现的库要好。
结语
本文带你了解了Web Crypto API,让你知道可以通过nodejs的原生模块实现浏览器和服务端完全相同的摘要算法。不过,本文仅仅是一个知识的抛砖引玉,在实际业务中,我们需要去学习密码学知识,去研究优秀的第三方库和开源项目,了解业界是怎么利用密码学设计来保障系统的安全的。实际上,在其他语言中,往往提供了丰富的密码学模块,例如我们经常遇到带盐(salt)的摘要或加密,例如我们需要在客户端和服务端之间交换公钥,例如我们需要设计自己的session,诸如此类,就目前而言,JS在这一块还是很弱的,性能上也不大行,如果真正想用,我们会考虑使用webassembly在浏览器端提供由底层语言编译的加密模块,或者在nodejs端使用bind能力调用c/c++模块来实现。总而言之,JS的生态还比较脆弱,我们还有很长的路要走。
2023-08-15 1424