For English version click here.
neoatlantis-crypto-js是我写的一个功能比较全面的、在javascript下实现的加密库。本文将对这一库的各种功能进行讲解。
由于本库内容很多,并在不断开发和调试中,本文将不断更新。因此在标题中附加了这一说明的版本号。本说明还将以英文版撰写。
0 动机
0.1 enigma系统
本库可以说是开发enigma系统的必然结果。为了介绍本库,首先介绍下enigma系统。 enigma系统被设计为一个能运行在嵌入式计算机上的服务程序:
- 对于嵌入式计算机(之后称为内机)外、通过特定的手段(称为API)输入的请求(外面的计算机称为外机),它可进行如下操作:
- 将输入的请求展示给用户,在用户批准后,对请求中的数据进行加密和/或签名,并将结果返回给外机。
- 将输入的请求视为另一台enigma的密文(或签名的明文),解密内容并进行验证,将结果展示给用户,并在用户批准下将明文结果返回给外机。
- 根据请求和用户批准,将用户自身的某个身份公钥传送给外机。
- 根据请求和用户批准,导入一个其他用户的身份公钥。
- 此外,用户可以通过直接与内机上相连的输入输出设备,如键盘、鼠标、屏幕等,进行如下操作:
- 管理(包括列出、产生、删除、导入、导出)内机上存储的身份密钥(包括公钥和私钥)。
- 编辑可供发送的草稿。在执行批准外机的请求时,可以用草稿来代替外机给定的数据。
- 查看已经获取到的、存储的解密数据。
- 编辑、储存一类特殊的草稿,即对其他用户身份公钥的签署(称为令牌)。
enigma系统可以作为很多通讯工具或者协议的核心部件。 例如,它可以配合聊天软件的插件,实现在聊天软件中的端到端加密。 它也可能作为门禁之类的系统的部件,用户能通过签署给定的“挑战”数据(challenge),可以向门禁系统证明自己的身份。
0.2 neoatlantis-crypto-js库
在开发enigma系统的过程中,作者认识到,有必要将整个系统的涉及到密码学的部分分离出来,单独写成一个库。 在这个库中包含了安全的随机数发生器、对称加密算法、不对称加密算法和签名算法、散列函数等功能, 还包含了用于构建enigma系统的2个数据结构:身份密钥(identity)和消息(message)。
此外,本库的亮点在于,考虑到即使包含了前述这两个数据结构仍然需要大量精力构建一个enigma系统,例如需要处理存储、消息读取中的各种复杂的逻辑, 还包含了一个事实上已经接近完整的enigma系统的抽象实现。只需要根据这个抽象实现的要求,通过自定义的用户界面送入数据,就可以直接得到数据输出。 利用这一抽象实现,用户可以非常快速地构建一个具有enigma系统功能,或者有所缩减(例如自动化一些批准流程)的系统,并且保持其与本系统的兼容性。
简而言之,用户利用neoatlantis-crypto-js库,既可以完成基本的和密码相关的功能,又可以快速实现复杂的、用于端到端加密的逻辑。
0.3 功能列表
这里列出一个neoatlantis-crypto-js的完整的功能介绍。
- 散列
- 自带BLAKE2s、WHIRLPOOL、RIPEMD160的散列实现。
- 所有的散列算法都可以被用来进行MAC(消息认证值,Message Authentication Code)的计算。
- 所有的散列算法都可以被用来完成PBKDF2密钥推导算法。注意:速度上有劣势。
- 加密/签名
- 对称加密 本库自带了AES和Salsa20/20算法,但是不提供直接调用。 本库只提供一个基于如上算法构建的“复合”的对称加密算法。
- 不对称加密 本库的不对称加密算法不与任何现行标准兼容。这是一个将基于椭圆曲线密码构建的密钥交换算法(ECDH)和签名算法(ECDSA)共同利用起来的算法。 这一算法从一个基础的种子私钥开始,通过PBKDF2推导用于初始化ECDH和ECDSA的两个私钥。 然而特别注意的是,ECDH算法本意是用于密钥交换协议,本库中将ECDH给出的公钥作为公钥, 在向这一公钥加密的数据中附加临时的“响应”密钥(PeerKey)并藉此构建共享秘密(SharedSecret),供对称加密算法加密实际数据。
- enigma系统
- 一个身份密钥的数据结构。身份密钥是一个256(不含)字符以内的说明(subject)、一个公钥、一个对公钥和subject进行的签名构成。
- 一个消息数据结构。消息数据或者是由一个负荷直接构成,或者是一个加密的负荷(称为信封)附加解密信息。
- 负荷内包含签名者、签名数据和正文
- 信封内包含加密的负荷数据、解密者和供解密者恢复密钥的数据、以及压缩标记。
- 一个抽象的enigma系统实现
- 通过API名称调用功能
- 功能调用时通过问答形式和调用者交互,问答形式询问变量参数、抛出异常等等。
- 调用者只需很少的逻辑来将回答交给用户,或自动回答,即可完成特定任务。
- 自行管理存储(基于localStorage的接口),调用者只需维护一个localStorage即可。
- 加速模块
- 根据库所在平台不同,可以调用平台自带的接口实现对某些功能的加速或完善。
- 例如调用NodeJS的crypto库实现散列函数,或者调用自定义浏览器内核的接口实现更加安全的随机数发生器。
- 辅助工具
- 在Javascript的字符串、ASCII字符串、Base32、Base64、Hex编码之间转换。
- 操作ArrayBuffer,实现拼接、异或、判断相等、反转顺序等功能。
- 随机字节序列发生器。
- UUID发生器。
- 变量类型判断。
- (未完整实现的)LZW压缩。
- 一个序列化工具,根据预定义的数据结构将JSON格式的赋值序列化或逆序列化。支持多种类型的数据。
1 基本知识
1.1 初始化neoatlantis-crypto-js库
要初始化本库,可以在NodeJS中使用:
在浏览器中,使用RequireJS:
1.2 一些注意事项
在大多数时候,结果以ArrayBuffer
类型的数据输入输出。
要读取其内容,您需要使用类似如下代码:
要根据已有参数得到一个ArrayBuffer
:
2 使用辅助工具函数
在crypto.util
下提供了一些有用的工具函数。这些也被本库内部调用。
2.1 获得随机数据
最常用的功能是获取n个随机字节:
您可以通过如下代码让产生的随机数更加随机:
通过touch
方法,您将全局地(虽然您使用了new
来生成rand
)影响随机数发生器的一些内部状态。当它们变化后,产生的随机数就更加不可预料了。
您可以将这个方法与用户鼠标、键盘等各种输入结合起来。进行touch
的时机才是影响随机数发生器的参数。和您具体采用了什么输入无关。
2.2 处理ArrayBuffer
使用crypto.util.buffer
,可以对ArrayBuffer
类型的数据进行各类处理。
异或。要将两个byteLength
相等的ArrayBuffer
异或,使用如下代码:
拼接。要将多个ArrayBuffer
按次序拼接在一起,使用如下代码:
判断相等。判断两个ArrayBuffer
的内容是否一致。使用如下代码:
反序。将ArrayBuffer
内第一项和最后一项对调,第二项和倒数第二项对掉,以此类推。
2.3 判断变量类型
使用如下代码,可以判断一个变量是否属于特定的类型:
2.4 转换编码
使用如下代码,可以将字符串或ArrayBuffer
作为输入,得到改变编码后的输出。
2.5 UUID产生
使用如下代码,将会利用crypto.util.srand
产生一个UUID:
2.6 序列化/逆序列化工具
本库自带了一个序列化一个特定数据结构的工具。利用这个工具,您可以自定义一个数据结构,称为“模板”,然后填充数据,得到序列化的结果,当然也可以读取。
您可以通过类似给javascript的Object
的属性(Attribute)赋值一样的方式构建数据。在序列化之后的结果中,各属性的Key并不被包含。
所以逆序列化需要您已知目标数据属于哪个模板。
在一个数据结构中,您可以使用如下类型的数据给属性赋值:
- binary, 二进制字节流。具体支持3种:
shortBinary
,binary
,longBinary
shortBinary
占用1个字节标记字节流的长度,最大长度是255个字节,允许为空。binary
占用2个字节标记字节流的长度,最大长度是65535字节,允许为空。longBinary
占用4个字节标记字节流的长度,最大长度是2^32-1字节(约4GB),允许为空。
boolean
,一个布尔类型的变量,占用一个字节。constant
,将给定的ArrayBuffer
的数据放入序列化的结果中。根据ArrayBuffer
的长度确定占用的字节长度。在逆序列化时要求期望的结果与此一直,否则会抛出异常。enum
,允许输入数据为给定的字符串列表中的一项,最多可以定义255个项目的列表。占用1个字节。逆序列化时将根据输入字节,将输出表示为列表中的一项。datetime
,允许输入一个日期并记录。精度为1秒,占用7个字节。可以记录年份0-65535年。- array, 二进制字节流的数组。支持
shortArray
,array
,分别支持255、65535项记录。 注意:shortArray
支持的二进制字节流也必须是shortBinary
,而array
支持的二进制字节流也必须是binary
。
2.6.1 定义一个序列化模板
使用如下代码,构建一个序列化的模板:
template
是一个javascript的Object
,示例中定义了_
, subject
, algorithm
, public
, secret
, signature
等属性。
之后赋值时,利用这些名称即可。但是这些名称不会被包含在最终的序列化结果中。
注意:algorithm
是枚举(enum)类型。_
是一个常数类型。
在逆序列化时,至少如果在_
项目上发现结果不是定义的值,就会报错。由此可以快速简单区分一个正确的和不正确的输入。
2.6.2 序列化一段数据
使用如下代码,利用给定的模板序列化一段数据。
2.6.3 逆序列化一段数据
使用如下代码,将一段ArrayBuffer
类型的数据逆序列化。
请注意,逆序列化时可能抛出异常,请用try-catch处理。一般这标志着数据不符合给定的模板。
3 散列
本库提供一个散列函数的接口,可以进行对数据的散列、MAC(Message Authentication Code,消息认证码)的计算,以及一个速度很慢的PBKDF2实现。 散列函数只提供Whirlpool一种。输出长度可以从1字节到64字节之间选择。
3.1 计算一个ArrayBuffer的散列
利用如下代码,根据一个ArrayBuffer
,计算散列:
3.2 计算一个ArrayBuffer的MAC
MAC是一种在计算散列之前需要一个额外的密钥输入的算法。可以用来进行比如带密钥的验证消息完整性等操作。
3.3 进行PBKDF2密钥推导
PBKDF2密钥推导是一种消耗计算资源的算法,可用来将较弱的口令(用户的直接输入)通过消耗计算机时间而加强,可以抵御暴力破解密码,并提高结果的随机性。
PBKDF2的算法内部,需要调用由所选定的散列函数构成的MAC算法。这将由3.2所示的MAC算法构成。 在内部调用这些MAC算法时,MAC的结果并不被截断,即,使用的是它们的完整长度的输出。
4 对称加密
本库提供一个单一的对称加密接口。输入密钥和数据,就可以实现加密或者解密。加密的对象是任何已知长度的数据块。
这一对称加密算法包含如下步骤,以加密为例:
- 选取10比特的salt
- 由输入的密钥推导产生用于各实际加密算法的密钥
- 由散列算法计算明文的MAC(使用一个第2步产生的密钥),长度6字节,附加到明文开头。
- 对产生的明文首先进行补充长度至16字节的整数倍(RFC5652),然后先利用
Salsa20/20
算法,再利用AES-128-ECB
算法进行加密。
由于使用了Salsa20/20
算法,继续使用ECB模式的AES-128
并不会导致正常来说ECB模式的安全问题。本算法使用MAC验证解密的成功与否。
为了使用对称加密:
为了解密:
5 不对称加密和签名
本库提供的不对称算法有各种“套装”。这些套装的具体实现和输出格式不遵循现有的标准。
每套算法有一个名称,指定了这个名称,就指定了具体用来加密和签名的算法(和相关的参数),以及用来签名之前的散列函数等等。 由于这一定义,每套算法都可以用来加密和签名。此外,为了密钥管理上的方便,用户的私钥总是一个可以指定的一定字节长度的随机数据。 在私钥指定后,给定算法,可以推导出公钥。
目前支持的算法套装如下:
名称 | 加密算法 | 签名算法 | 散列算法 | 私钥长度(bit) |
---|---|---|---|---|
NECRAC256 | ECDH/secp521r1 | ECDSA/secp521r1 | WHIRLPOOL | 64 x 8 = 512 |
NECRAC192 | ECDH/secp384r1 | ECDSA/secp384r1 | BLAKE2s | 48 x 8 = 384 |
NECRAC128 | ECDH/secp256k1 | ECDSA/secp256r1 | BLAKE2s | 32 x 8 = 256 |
NECRAC112 | ECDH/secp224r1 | ECDSA/secp224r1 | RIPEMD160 | 28 x 8 = 224 |
NECRAC96 | ECDH/secp192r1 | ECDSA/secp192k1 | RIPEMD160 | 24 x 8 = 192 |
在选择了任何一个套装之后,得到的加密程序都能处理任意的输入,对其加密/解密/签名/验证签名。
5.1 初始化一个不对称加密对象
使用如下代码,根据选取的算法套装,初始化一个不对称加密的对象。
在初始化之后,asym
有2个方法可供进一步调用,即setPrivateKey
和setPublicKey
。
setPrivateKey
用来设定一个私钥。其参数是被随机的字节填充的ArrayBuffer
类型的数据,长度由上文表格确定。
setPublicKey
用来设定一个公钥,由下文所述的导出公钥的方式获取,其变量类型也是ArrayBuffer
。
调用这2个方法之一后,asym
将会暴露其他的方法以供进一步操作。
同时,asym
将删去已经暴露的setPrivateKey
和setPublicKey
方法,禁止重新进行初始化操作。
5.2 使用不对称加密对象
设定了公钥或者私钥之后的asym
对象,可以被用来进行如下操作:
5.2.1 推导公钥
在指定了一个任意的私钥之后,使用算法推导给出对应于这个私钥的公钥。 之后这个公钥就可以被公开,以便让他人藉此向私钥持有者加密发送消息,或者公钥持有者验证私钥持有者签署过的消息。
为此,asym
对象必须是通过setPrivateKey
方法初始化的。
5.2.2 加密
使用加密方法,可以在得到别人的公钥之后,向他发送加密的数据。只有持有和公钥对应的私钥的人才能解密数据,其他人不能。
使用setPrivateKey
和setPublicKey
方法通过输入私钥或者公钥初始化的asym
对象都可以调用这一功能。
5.2.3 解密
已知一个私钥,解密别人发来的数据。
只有使用setPrivateKey
方法通过私钥初始化的asym
对象可以调用这一功能。
5.2.4 签名
签名的功能是对给定的plaintextBuf
输入,生成一段数据,其他人拥有公钥的时候,可以据此确定确实是私钥的拥有者进行了“签名”这一操作,而不是任何其他人。
此外,签名者自己不能否定自己曾经签署过这段数据。
只有使用setPrivateKey
方法通过私钥初始化的asym
对象可以调用这一功能。
5.2.5 验证签名
在得到别人的公钥之后,可以验证此人所签署过的数据。需要输入声称被此人签署过的数据plaintextBuf2
和签名数据signatureBuf2
。
使用setPrivateKey
和setPublicKey
方法通过输入私钥或者公钥初始化的asym
对象都可以调用这一功能。
但是自然,一般在实际应用中,有意义的用法是通过输入公钥进行的初始化。