概述
在对nodejs的crypto密码学库有了相当的了解和使用经验之后,笔者在其基础上,开发了一个相对比较完善的封装,可以方便的应用在相关应用系统当中。为什么笔者认为这个实现相对完善,更加接近完美呢? 因为以下的一些优势和考量,读者应该能够在后续的深入分析中有所理解和体会:
- 应用更加简单方便
- 安全性完备,更加完善的密钥协商、使用和加密校验
- 性能更好,包括流加密的工作方式,和集成的对称加密,改进了原有的非对称加密大型信息的性能问题,适用性也更广(例如有更好的ARM平台支持,这对手机系统非常重要)
- 除内置的crypto模块外,没有外部依赖,具体实现也比较简洁,相对的兼容性、易实现性、可修改性更好
- 更透明,可定制的签名和验证机制
- 使用的组件(ECC/DH/Chacha/Poly/SHA256)和原来的常用组件(RSA/AES/SHA1)相比,更现代更先进也更安全
这里先提供本实现的封装和相关测试代码,然后在后面,关于技术选择和实现设计,有更深入的分析和讨论:
基本功能和流程
本实例库提供的基本功能非常基础,包括公钥加密和解密,公钥签名和验证,密钥加密和解密。但经过适当的组织和封装,非常容易使用,并在安全性方面具有很好的保障。
计算类和实例
实现的设计,基于类-实例化的应用模式。在实际操作之前,需要基于计算类,创建计算实例,然后调用实例的方法来实现计算操作。
实例构造方式,可以选择预置私钥和动态密钥对。创建实例时,如果使用一个已存在的私钥,则会使用这个私钥进行计算;而如果不使用任何密钥,则会在实例内部动态的创建密钥对。显然后者更加安全。在对安全要求更高的场合,密钥对都应该是动态生成的。使用时在进行公钥的交换,来支持后续的加解密操作。
公钥加密
系统默认的公钥加密,私钥解密的方式,有一个不足之处,在于这个操作的计算效率较低,计算性能和处理信息的规模都有所限制,所以通常不会用于常规信息的加密,而是用于密钥的加密和交换。然后用这个交换后的密钥,进行对称加密计算来处理实际业务数据。
但实际上,通过密钥协商机制,可以将这两个操作合二为一。具体方法和过程是(加密方):
- 获取解密方的公钥
- 基于私钥和对方的公钥,计算协商密钥
- 基于随机信息,对协商密钥进行衍生计算,生成加密密钥
- 使用加密密钥对信息进行对称加密
解密的方法就是加密的逆操作(解密方):
- 获取加密方的公钥
- 基于解密方的私钥,和加密方的公钥,计算协商密钥
- 解构加密信息,获取其中的随机信息
- 基于随机信息,计算衍生的解密密钥(同加密密钥)
- 使用解密密钥,对信息进行解密
在本实现中,密钥协商只使用了crypto的ecdh模块。但并没有直接使用这个协商的密钥,而是增加使用了pkdf2方法对这个密钥进行了衍生,从而实现了加密计算的"一次一密",提高了算法安全性。
对称加密
本例中,加解密的核心还是对称加密算法,其选择的对称加密算法是 chacha20-poly1305。关于这个算法,笔者另外有文详细讨论,这里只列举和AES相比的优势和特性:
- 这是一个流式对称加密算法,并且其算法结构的理论性能更好
- 虽然AES可能有硬件加速,但在移动设备上chacha20更有优势
- 和poly1305结合,提供信息校验和更好的安全性
- 实现相对AES更简单,也不需要那么多模式和编码参数,广泛性和适用性更好
签名和验证
在crypto原生实现中,签名和验证的计算,都是完全封装起来的,而且需要使用特别的算法和对应的密钥对,使用其实是挺不方便的。笔者经过研究,发现完全可以利用公钥加密的特性和机制,利用现有的公钥体系,在外部实现安全的信息签名和验证,并且有更好的扩展性和可理解性。在签名方的基本过程如下:
- 签名方创建算法实例,带有签名方的私钥
- 签名方创建一个临时的ecdh对象
- 签名方使用实例私钥和临时ecdh的公钥,协商一个共享密钥
- 考虑到ecdh本身就是临时随机的,所以不再进行密钥衍生混淆计算
- 使用共享密钥,对签名数据,进行HMAC计算,得到加密HASH
- 将加密HASH和临时ecdh的私钥进行合并,作为签名信息
在验证方的相关操作,输入信息是原始数据、签名信息和签名方的公钥,输出信息是验证的结果(是或者否),具体如下:
- 验证方,从签名信息中,分离出加密HASH和临时ecdh的私钥
- 基于签名方公钥和临时私钥计算逻辑上共享密钥
- 使用此共享密钥,对签名的原始数据,进行HMAC计算,得到计算HASH
- 将计算HASH和签名中的HASH进行比较,确认验证结果
- 由于理论上只能从签名方的公钥,才可能计算出正确的共享密钥,所以可以确保此签名确实来自签名方,并且和原始信息相关
简单加密
本实现还支持简单的对称加密解密方式。可以用在自己加密自己解的场合。基本原理其实还是基于非对称加密,只不过使用一个固定内置的密钥对来实现而已。只要加密方的私钥保证安全,这样的操作不会破坏逻辑上的安全性。
使用方式也很简单,就是加密解密时,不传入公钥参数(就使用了预先的内置公钥)即可。
实现相关技术选择和考量
上一章节,我们已经明确了相关的结构,功能设计和基本的操作流程,本章节会着重探讨相关的技术选型和实现的细节。下面我们分别进行深入的阐述,读者最好结合示例代码来阅读和理解。
配置信息
本实现不是一个通用的密码学套件,而是一个预先选择好的,笔者认为相对合理,比较安全并且兼顾性能的特定参数和算法的算法组合和封装,在实际工程实践中,约定使用相同的配置,可以简化开发和部署。这在这个类的配置信息中,我们可以看到:
vbnet
static Config = {
NAME: "chacha20-poly1305",
TAG_LENGTH: 16,
KEY_LENGTH: 32,
NONCE_LENGTH: 12,
CURVE: "secp256k1",
ITER: 100,
HASH: "SHA256",
DEFAULT_KEY : "Some Public Key" // default public key
DEFAULT_KEY2 : "Private Key", // default private key
}
这个套件中,笔者的选择包括:
- 对称加密: chacha20-poly1305
- ECC曲线: secp256k1
- 摘要算法: sha256
- 密钥长度: 32byte (chacha20的密钥长度)
- tag长度: 16byte (crypto默认,但在低版本nodejs中需要设置)
- 随机信息长度: 12byte,chacha20的设置
- 衍生迭代次数: 100
- 标准外部信息交换和呈现: base64
crypto模块
发展到2024年,其实nodejs的crypto已经相当的成熟和强大。完全基于nodejs crypto,意味着没有外部的第三方依赖。这对于程序的可维护性,可扩展性和安全性都有了很好的保障。而且,在本例中,我们只应用了其强大功能特性的有必要的很小一部分,降低了再次开发和移植的难度。
相关涉及到的crypto函数包括:
- randomBytes: 随机信息生成
- createCipheriv, createDecipheriv: 加密解密器实例
- createECDH: ecdh实例
- createHmac: Hmac实例
- pbkdf2Sync: 密码衍生
- timingSafeEqual: 字节数组比较器
ecdh
ecdh是crypto的内置模块。从名字上我们就可以了解,它是基于ECC(Eclipse Circle Curve椭圆曲线)非对称加密算法的密钥协商(Diffie-Hellman)算法。和经典的非对称加密体系RSA相比,ECC技术更加先进,选项和变化更多,相同长度密钥的操作更安全,但算法复杂度较高。已经逐渐成为现代密码学非对称加密的事实标准。
crypto中提供的ecdh功能比较完善而且容易使用。我们可以使用它创建ECC的密钥对、导出和加载密钥,计算共享密钥等等。在本例中,ecdh是被封装到工具类中的内置对象,在使用的时候,不会感觉它的存在。但它会在类被实例化的时候,就被创建出来,并参与后续的操作和计算。
内置的ecdh类主要有两个作用,一个在加解密过程中,存储和承载本方的私钥;另一个是在这个基础上,基于一个外部的公钥,来计算共享密钥,来参与实际的加解密操作。
除了动态生成密钥对之外,工具类提供了可以从外部加载一个已保存的私钥,来进行实例化,这对于很多静态配置的场景比较有用。实际上,保存的内容只需要一个有效的私钥,因为配套的公钥可以很方便的根据私钥计算出来。
相关的核心函数和方法包括:
- createECDH: 创建ECDH示例,参数是所使用的曲线名称,此处是secp256k1
- setPrivateKey: 如果是预保存的私钥,可以为ECDH进行设置
- getPublicKey: 获取当前ECDH实例的公钥,这个信息应该公布给加密的对方
- computeSecret: 基于当前实例和外部公钥,计算共享密钥,这个概念是本实现的核心,就是共享密钥,不是通过信息的传输,而是通过计算得到
对称加密chacha20-ploy1305
使用标准的createCipheriv和createDecipheriv方法。算法名称参数就是"chacha20-poly1305"。这个算法要求12个字节的初始化向量(Initial Vector, IV),可以使用randomeBytes函数生成。
作为一个支持信息校验的算法,会使用一个附加信息authTag标签(就是一段字节代码)来参与信息完整性检查。poly1305的标准要求这个tag的长度是16字节,这个参数在比较新的nodejs环境中是可以忽略(使用默认)的,但在早期的版本中,需要通过authTagLength这个算法参数进行设置,这个我们在示例代码中可以看到。
支持authTag的算法,在加密完成后,一般可以通过getAuthTag来获取这个tag信息,并且在解密的时候,在创建解密器之后,实际进行解密之前,需要使用setAuthTag方法,将这个信息设置到解密器实例当中,才能进行解密和校验。
基于安全和操作方便的考虑,笔者并没有设计复杂的数据结构,来存储加密后的密文、IV、Tag等信息,而是基于nodejs的buffer结构,将它们简单的连接在一起。因为IT和Tag的长度其实都是固定的,可以很方便的分离和解析。最后,将这个合成信息转换成为base64,来进行传输和交换。
除了设置算法之外,在crypto中,所有的对称加密和解密的使用方式,都是相同的,所以很容易进行更换和升级。
密钥衍生
ECDH的密钥协商,有一个小小的不足,就是如果对于固定的密钥对,协商出来的密钥,都是一样的。改进的方式也很简单,就是再增加一个密钥衍生计算。比如本例中,就直接使用了crypto内置的pkdfk2算法。
这个算法需要几个参数,一个密钥,这里就使用协商出来的密钥;一个随机信息,我们就使用对称加密中的随机IV;一个迭代次数,选择100;密钥长度,此处为chacha20需要的32字节;和摘要算法,就是SHA256。
这里的随机信息,保证了虽然都使用同一个协商密钥,但每次实际加密时,使用的衍生密钥都是不同的。这样就实现了事实上的"一次一密",大大提高了加解密过程的安全性。
信息签名和验证
在逻辑上而言,前面所讨论的信息加解密过程,由于要使用到双方的私钥,才能正确的操作,所以算法本身是能够保证信息在加密双方之间操作的确定性的,就是解密方,可以保证这个信息确实由加密方加密的,也就是说只能来自加密方,具有不可否认性。
但在其他更多的场景中,可能需要第三方也有这种检查信息完整性和不可否认性的能力,这就会涉及到除了加密和解密之外,信息的签名和验证操作了。
在业务和逻辑上而言,信息的加密解密,和信息的签名验证,是两类不同的操作。但在技术角度而言,它们在底层,是有一定的联系的。所以,在本实现中,工具类基于相同的底层实现,同时也可以提供信息的签名和验证的功能。
前面我们已经讨论过这个实现的基本原理和流程,这里展开讨论一些细节问题。
首先是签名信息的产生和封装。这里的签名信息,本质上就是两个部分组成。第一个信息是使用一个临时协商出来的密钥,对原始信息进行HMAC的计算,就是加密的摘要;然后,还是使用nodejs的buffer将它和临时ECDH的私钥连接在一起;因为摘要的长度是固定的,所以这个签名的原始结构也是固定的,很容易进行分离和解析。最后,将这个合成的信息,编码成为base64字符串,来进行传输和交换。
验证的时候,其实是不需要验证方的实例和私钥的,因为验证信息本身已经承载了用于验证的临时ECDH信息,拿到完整信息(原文、签名、签名公钥)的任何人都可以进行验证。
验证过程,就是基于临时ECDH的私钥和签名公钥,再计算一遍共享密钥,并使用这个密钥对原始信息进行HMAC摘要计算,然后比较签名的信息,就可以完成验证了。
验证的比较操作其实是检查两个字节数组(Buffer)是否完全相等,使用了crypto提供的内置方法timeSaftEquire,保证性能的同时,安全性也比较好。
由于签名的时候,所使用的ECDH也是临时生成的,所以衍生的HMAC密钥可以保证随机性,也保证了签名的随机性安全性。这里就不再需要对密钥进行衍生计算了。
简单加密
对于一些特殊的场景,比如自己加密自己解,运行条件限制等情况中,本例中的工具类使用还是有点复杂,比如需要进行公钥的交换等,为此工具类提供了简单加密解密的模式。
原理前面也简单解释过,就是使用内置的ECDH对象,其他都不需要修改。实现的方式已经内置在加解密函数当中,如果判断没有传入公钥信息,就使用预置的公钥来协商共享密钥。从这个角度来看,实际的加密,也可以做到一次一密,相对普通的对称加密,安全性更高。
显然,简单加密的使用有一些限制,如只能使用静态或者共享的实例私钥,这对安全性是有一定影响的,并不是安全计算的完全体。
应用场景
结合实际的应用需求,如果不考量网络层级的安全的话,笔者认为这个密码学实现可以适用于以下的应用场景:
- 网络服务组件间的数据交换
这是一个相对静态的应用环境。各个网络应用组件之间,需要一个比较安全的数据交换机制。就可以向对方共享自己的公钥,然后在需要通信的两方之间,使用本例中的加解密机制和信息的签名和验证,来保证信息交换的安全性。
- 客户端工具和服务
这种场景用于一些临时的客户端工具,要和应用服务交换数据的场景。客户端工具可以先获取和配置服务端的公钥,然后在工作时,临时生成工作的密钥对来对通信进行加密(认证时提供工作公钥,或者在加密数据提交时同时提交公钥)。这种情况下,还可以同时使用公钥来对对方的数据和签名进行验证。
- 浏览器客户端和应用接口
这个场景可能会有一些问题。就是现在的浏览器环境,无法直接支持nodejs的crypto模块(它应该是基于OpenSSL组件的),所以要使用相同的机制,可能需要兼容的JS库代码。这部分内容已经超出的本文要探讨的范围,笔者有机会会另行撰文讨论。
小结
本文研究和探讨了一个相对比较完善的信息加解密和签名验证的工具的实现。通过分析技术细节,阐述和证明了它的优势和特点,包括方便使用、功能和安全性的完备、性能保证等等。