JavaScript逆向爬虫教程-------基础篇之JavaScript密码学以及CryptoJS各种常用算法的实现
目录
- 一、密码学介绍
-
- 1.1 为什么要学密码学?
- 1.2 密码学里面学哪一些
- 二、字符编码
- 三、位运算
- 四、Hex 编码与 Base64 编码
-
- 4.1 Hex 编码
- 4.2 Base64 编码
- 五、消息摘要算法
-
- 5.1 简介
- 5.2 JS中的MD5、SHA、HMAC、SM3
- 六、对称加密算法
-
- 6.1 介绍
- 6.2 加密模式和填充方式
- 6.3 CryptoJS 中DES、DESede、AES算法实现
- 6.4 对称加密算法注意事项
- 6.5 CryptoJS(其他算法)
- 七、非对称加密算法
一、密码学介绍
1.1 为什么要学密码学?
数据请求中未知的参数可能是随机生成、标准算法加密的、魔改算法加密、自写算法加密的,如下图所示:
逆向中会接触到的标准算法加密,使用的语言一般在 JS、Java、C
中,JS
版的标准算法会应用于网页、H5
的 app
、小程序中,一般使用第三方库或者自己实现。Java
版的标准算法有现成的系统 API
调用,开发者想使用这些 API
,必须使用固定的方法名去访问。C/C++
没有现成的系统 API
调用,开发者要么自己去实现算法,要么调用别人写好的模块,算法的运行不依赖系统 API
,因此方法名可以混淆。我们要做的就是根据各种标准算法的特征、实现细节,去识别是否标准算法。
补充:
iOS
系统中有现成的C
实现的API
调用,开发者想使用这些API
,必须使用固定的方法名去访问iOS
系统也可以自己实现标准算法的,处理方式与安卓的so
相同。
密码学的学习非常地重要,后续我在安卓逆向的文章中会进一步加深讲解。
1.2 密码学里面学哪一些
消息摘要算法(散列函数、哈希函数): MD5、SHA、MAC、SM3
对称加密算法: DES、3DES、RC4、AES、SM4
非对称加密算法: RSA、SM2
数字签名算法: MD5withRSA、SHA1withRSA、SHA256withRSA
备注: 任何语言里面对于标准算法的实现都相同
二、字符编码
编码分为很多种: 字符编码、Hex
编码、URL
编码、Base64
编码…
字符编码学习笔记参考文章:https://blog.csdn.net/xw1680/article/details/126964362
备注: 字符和码值的对应关系是通过字符编码表决定的
ASCII :https://baike.baidu.com/item/ASCII/309296
UTF8: https://baike.baidu.com/item/UTF-8/481798
ANSI: https://blog.csdn.net/Liuqz2009/article/details/107861408
编码与解码:https://blog.csdn.net/u012485099/article/details/126037992
三、位运算
位操作符用于数值的底层操作,也就是操作内存中表示数据的比特(位)。ECMAScript 中的所有数值都以 IEEE 754 64
位格式存储,但位操作并不直接应用到64位表示,而是先把值转换为32位整数,再进行位操作,之后再把结果转换为64位。对开发者而言,就好像只有32位整数一样,因为64位整数存储格式是不可见的。既然知道了这些,就只需要考虑32位整数即可。
有符号整数使用32位的前31位表示整数值。第32位表示数值的符号,如0表示正,1表示负。这一位称为符号位(sign bit),它的值决定了数值其余部分的格式。正值以真正的二进制格式存储,即31位中的每一位都代表2的幂。第一位(称为第0位)表示2^0,第二位表示 2^1,依此类推。如果一个位是空的,则以0填充,相当于忽略不计。比如,数值18的二进制格式为 00000000000000000000000000010010,或更精简的10010。后者是用到的5个有效位,决定了实际的值。负值以一种称为二补数(或补码)的二进制编码存储。一个数值的二补数通过如下3个步骤计算得到:
- 确定绝对值的二进制表示(如,对于-18,先确定18的二进制表示);
- 找到数值的一补数(或反码),换句话说,就是每个0都变成1,每个1都变成0;
- 给结果加1。
按位非操作符用 波浪符(~)
表示,它的作用是返回数值的 一补数(反码)
。按位非是 ECMAScript 中为数不多的几个二进制数学操作符之一。看下面的例子:
let num1 = 25; //二进制 00000000000000000000000000011001 ==> 8421码计算或者直接除以2取余数
let num2 = ~num1; //二进制 11111111111111111111111111100110(补码)
==> 11111111111111111111111111100110
==>-1 11111111111111111111111111100101
==> 10000000000000000000000000011010
==>符号位 2^4+2^3+2^1 ==> 16+8+2 ==> 26 符号位为1 故结果为 -26
console.log(num2); // -26
//可以这样进行记忆:按位非的最终效果是对数值取反并减1
let num1 = 25;
let num2 = -num1 - 1;
console.log(num2)
//实际上,尽管两者返回的结果一样,但位操作的速度快得多。这是因为位操作是在数值的底层表示上完成的。
//总结:对一个数取反偶数次结果是它本身
按位与操作符用 和号(&)
表示,有两个操作数。本质上,按位与就是将两个数的每一个位对齐,然后基于真值表中的规则,对每一位执行相应的与操作。按位与操作在两个位都是1时返回1,在任何一位是0时返回0,可以用来取出指定的二进制位。 下面看一个例子:
let result = 25 & 3;
console.log(result); // 1
看下面的二进制计算过程:
25 = 0000 0000 0000 0000 0000 0000 0001 1001
3 = 0000 0000 0000 0000 0000 0000 0000 0011
---------------------------------------------
AND = 0000 0000 0000 0000 0000 0000 0000 0001
计算 let result = -5 & -3;?
-5 ==> 1000 0000 0000 0000 0000 0000 0000 0101
==> 1111 1111 1111 1111 1111 1111 1111 1010
==> 1111 1111 1111 1111 1111 1111 1111 1011
-3 ==> 1000 0000 0000 0000 0000 0000 0000 0011
==> 1111 1111 1111 1111 1111 1111 1111 1100
==> 1111 1111 1111 1111 1111 1111 1111 1101
-5 1111 1111 1111 1111 1111 1111 1111 1011 &
-3 1111 1111 1111 1111 1111 1111 1111 1101
1111 1111 1111 1111 1111 1111 1111 1001 补码
-1 1111 1111 1111 1111 1111 1111 1111 1000 反码
1000 0000 0000 0000 0000 0000 0000 0111 取反 ==> -7
按位或操作符用 管道符(|)
表示,同样有两个操作数。按位或操作在至少一位是1时返回1,两位都是0时返回0,可以用来将指定的二进制位拼接。 仍然用按位与的示例,如果对25和3执行按位或,代码如下所示:
let result = 25 | 3;
console.log(result); // 27
计算过程如下:
25 = 0000 0000 0000 0000 0000 0000 0001 1001
3 = 0000 0000 0000 0000 0000 0000 0000 0011
---------------------------------------------
OR = 0000 0000 0000 0000 0000 0000 0001 1011 ==> 2^4+2^3+2^1+2^0 ==> 16+8+2+1 ==> 27
在参与计算的两个数中,有4位都是1,因此它们直接对应到结果上。二进制码11011等于27。
-5 ==> 1000 0000 0000 0000 0000 0000 0000 0101
==> 1111 1111 1111 1111 1111 1111 1111 1010
==> 1111 1111 1111 1111 1111 1111 1111 1011
-3 ==> 1000 0000 0000 0000 0000 0000 0000 0011
==> 1111 1111 1111 1111 1111 1111 1111 1100
==> 1111 1111 1111 1111 1111 1111 1111 1101
-5 1111 1111 1111 1111 1111 1111 1111 1011 |
-3 1111 1111 1111 1111 1111 1111 1111 1101
1111 1111 1111 1111 1111 1111 1111 1111 补码
-1 1111 1111 1111 1111 1111 1111 1111 1110 反码
1000 0000 0000 0000 0000 0000 0000 0001 取反 ==> -1
按位异或用 脱字符(^)
表示,同样有两个操作数。按位异或与按位或的区别是,它只在一位上是1的时候返回1(两位都是1或0,则返回0)。 对数值25和3执行按位异或操作:
let result = 25 ^ 3;
console.log(result); // 26
计算过程如下:
25 = 0000 0000 0000 0000 0000 0000 0001 1001
3 = 0000 0000 0000 0000 0000 0000 0000 0011
-----------------------------------------------
XOR = 0000 0000 0000 0000 0000 0000 0001 1010 ==> 2^4+2^3+2^1 ==> 26
二进制码11010等于26。(注意,这比对同样两个值执行按位或操作得到的结果小1。)
计算 let result = 25 ^ -3;?
25 = 0000 0000 0000 0000 0000 0000 0001 1001
-3 = 1000 0000 0000 0000 0000 0000 0000 0011
= 1111 1111 1111 1111 1111 1111 1111 1101 补码 ^
= 0000 0000 0000 0000 0000 0000 0001 1001
= 1111 1111 1111 1111 1111 1111 1110 0100 补码 - 1
= 1111 1111 1111 1111 1111 1111 1110 0011 反码
= 1000 0000 0000 0000 0000 0000 0001 1100 ==> 2^4+2^3+2^2 ==> -28
计算 let result = -25 ^ -3;?
-25 = 1000 0000 0000 0000 0000 0000 0001 1001
= 1111 1111 1111 1111 1111 1111 1110 0110
= 1111 1111 1111 1111 1111 1111 1110 0111 补码
-3 = 1000 0000 0000 0000 0000 0000 0000 0011
= 1111 1111 1111 1111 1111 1111 1111 1101 补码 ^
= 1111 1111 1111 1111 1111 1111 1110 0111
= 0000 0000 0000 0000 0000 0000 0001 1010 补码 符号位为0 表示正数 正数原码、反码、补码相同
==> 故结果为:2^4+2^3+2^1 ==> 26
计算 let result = -5 ^ -3;?
-5 ==> 1000 0000 0000 0000 0000 0000 0000 0101
==> 1111 1111 1111 1111 1111 1111 1111 1010
==> 1111 1111 1111 1111 1111 1111 1111 1011
-3 ==> 1000 0000 0000 0000 0000 0000 0000 0011
==> 1111 1111 1111 1111 1111 1111 1111 1100
==> 1111 1111 1111 1111 1111 1111 1111 1101
-5 1111 1111 1111 1111 1111 1111 1111 1011 ^
-3 1111 1111 1111 1111 1111 1111 1111 1101
0000 0000 0000 0000 0000 0000 0000 0110 补码 符号位为0 表示正数 正数原码、反码、补码相同
结果==> 2^2+2^1 ==> 6
补充:^
的特点:一个数据对另一个数据按位异或两次,结果为该数本身。如下:
let a = 10
let b = 20
console.log(a ^ b ^ b) //10
console.log(a ^ b ^ a) //20
左移操作符用两个 小于号(<<)
表示,会按照指定的位数将数值的所有位向左移动。比如,如果数值2(二进制10)向左移5位,就会得到64(二进制1000000),如下所示:
let oldValue = 2; //等于二进制10
let newValue = oldValue << 5; //等于二进制1000000,即十进制64
注意在移位后,数值右端会空出5位。左移会以0填充这些空位,让结果是完整的32位数值.
注意,左移会保留它所操作数值的符号。比如,如果-2左移5位,将得到-64,而不是正64。
这个是:左边最高位丢弃,右边补齐0
面试题: 请用最有效率的方式写出计算2乘以8的结果?
有符号右移由两个 大于号(>>)
表示,会将数值的所有32位都向右移,同时保留符号(正或负)。有符号右移实际上是左移的逆运算。比如,如果将64右移5位,那就是 2:
let oldValue = 64; //等于二进制1000000
let newValue = oldValue >> 5; //等于二进制10,即十进制2
同样,移位后就会出现空位。不过,右移后空位会出现在左侧,且在符号位之后。ECMAScript会用符号位的值来填充这些空位,
以得到完整的数值。
同时保留符号(正或负)这句话的意思是:最高位是0,左边补齐0;最高为是1,左边补齐1
无符号右移用3个大于号表示(>>>),会将数值的所有32位都向右移。对于正数,无符号右移与有符号右移结果相同。仍然以前面有符号右移的例子为例,64向右移动5位,会变成2:
let oldValue = 64; //等于二进制1000000
let newValue = oldValue >>> 5; //等于二进制10,即十进制2
无符号右移 无论最高位是0还是1,左边补齐0
对于负数,有时候差异会非常大。与有符号右移不同,无符号右移会给空位补0,而不管符号位是什么。对正数来说,这跟有符号右移效果相同。但对负数来说,结果就差太多了。无符号右移操作符将负数的二进制表示当成正数的二进制表示来处理。因为负数是其绝对值的二补数,所以右移之后结果变得非常之大,如下面的例子所示:
let oldValue = -64; //等于二进制11111111111111111111111111000000
let newValue = oldValue >>> 5; //等于十进制134217726
在对-64无符号右移5位后,结果是134 217 726。这是因为-64的二进制表示是 11111111111111111111111111000000,无符号右移
却将它当成正值。
四、Hex 编码与 Base64 编码
4.1 Hex 编码
Hex 编码就是十六进制编码,是一种用16个字符表示任意二进制数据的方法,其实就是将字符所对应的码值转为十六进制后拼接。
Hex 编码的应用:1、密钥的编码 2、密文的编码
Hex 编码特点:
用 0-9、a-f 16个字符表示
字符编码是一个字节或几个字节对应一个字符,而Hex编码是4个bit对应一个字符
2个十六进制字符代表一个字节
在实际应用中,一定要分清楚得到的数据是哪种编码的,采用对应方式解析,才能得到正确的结果
编程中很多问题,需要从字节甚至二进制位的角度去考虑,才能明白
简单的 Hex 编码实现,参考代码如下:
let test_str = "AmoXiang666"
let table = "0123456789abcdef"
let result = ""
for (let i = 0; i < test_str.length; i++) {
// 使用charCodeAt()方法可以查看指定码元的字符编码。这个方法返回指定索引位置的码元值,索引以整数指定。
let ascii = test_str.charCodeAt(i)
// 65 ==> 0100 0001
let left = ascii >> 4
let right = ascii & 0xf
result += table[left] + table[right]
}
console.log(result)
// let oldValue = 64; //等于二进制1000000
// let newValue = oldValue >> 5; //等于二进制10,即十进制2
// console.log(newValue)
// 416d6f5869616e67363636
// 416d6f5869616e67363636
使用 CryptoJS 实现,参考代码如下:
let cryptoJs = require("./CryptoJS")
// { stringify: [Function: stringify], parse: [Function: parse] }
// stringify: 编码 parse: 解码
console.log(cryptoJs.enc.Hex)
let wordArray = cryptoJs.enc.Utf8.parse("AmoXiang666")
console.log(wordArray)
console.log(cryptoJs.enc.Hex.stringify(wordArray))
console.log(cryptoJs.enc.Hex.parse("416d6f5869616e67363636").toString(cryptoJs.enc.Utf8))
补充:
Hex 编码的实现比较简单,且不是所有的实现都会出现码表
一般 Hex 编码都是比较标准的,不会进行魔改
URL 编码:URL 编码是 GET 请求中比较常见的,是将请求地址中的参数进行编码,尤其是对于中文参数。(其实就是 Hex 编码,只不过在每一个字节前加了一个%)
编码与解码的方式是公开的,只要知道码表即可
4.2 Base64 编码
Base64 是一种用 64个字符 表示任意二进制数据的方法,Base64 的应用:RSA密钥的编码、密文的编码、图片的编码。Base64 码表如下图所示:
Base64 的代码实现:
let cryptoJs = require('./CryptoJS');
let wordArray = cryptoJs.enc.Utf8.parse('AmoXiang666');
console.log(wordArray)
console.log(cryptoJs.enc.Base64.stringify(wordArray));
Base64 码表的妙用: 为了传输数据安全,通常会对 Base64 数据进行 URL 编码,或者会把 +
和 /
替换成 -
和 _
。
Base64 编码细节:
- 每个 Base64 字符代表原数据中的 6bit
- Base64 编码后的字符数,是
4
的倍数 - 编码的字节数是
3
的倍数时,不需要填充
Base64 编码的特点:
- 用
A-Z、a-z、0-9、+/
64个字符表示,=
作为填充使用 - Base64 编码是编码,不是压缩,编码后只会增加字节数
- 标准的 Base64 每行为
76
个字符,行末添加换行符 - Base64 编码的码表可能会被魔改
Hex 和 Base64 的转换:
let CryptoJS = require('../CryptoJS')
let text = 'amo666'
// 64编码
let wordArray = CryptoJS.enc.Utf8.parse(text)
let b64 = CryptoJS.enc.Base64.stringify(wordArray)
console.log(b64)
let b64_wordArray = CryptoJS.enc.Base64.parse(b64)
console.log(CryptoJS.enc.Hex.stringify(b64_wordArray))
五、消息摘要算法
5.1 简介
消息摘要算法(Message Digest Algorithm)是一类密码学哈希函数,用于产生数据的摘要,通常是固定长度的二进制串。消息摘要算法接受任意长度的消息作为输入,并输出固定长度的摘要。消息摘要算法具有以下特点:
- 散列后的密文不可逆
- 散列后的结果唯一。一般用于校验数据完整性、签名sign,由于密文不可逆,所以服务端也无法解密,想要验证,就需要跟前端一样的方式去重新签名一遍,签名算法一般会把源数据和签名后的值一起提交到服务端,要保证在签名时候的数据和提交上去的源数据一致
- 哈希碰撞
常见的哈希算法包括但不限于:
- MD5(Message Digest Algorithm 5): MD5 是一种广泛使用的哈希函数,生成 128 位(16 字节)的哈希值。然而,由于其存在安全性漏洞,已不推荐用于加密目的,而主要用于校验数据完整性等非加密场景。
- SHA-1(Secure Hash Algorithm 1): SHA-1 生成 160 位(20 字节)的哈希值,被广泛应用于数字签名、证书签名等场景。但是,SHA-1 也已经被证明存在碰撞攻击的安全性问题,因此也不再推荐用于安全目的。
- SHA-256、SHA-384、SHA-512: 这些是安全哈希算法家族中的一部分,分别生成 256 位、384 位和 512 位长度的哈希值。它们是目前广泛应用于数据完整性验证、数字签名等安全领域的哈希算法。
- RIPEMD(RACE Integrity Primitives Evaluation Message Digest):RIPEMD 系列是一组哈希函数,分为 RIPEMD-128、RIPEMD-160、RIPEMD-256 和 RIPEMD-320,分别生成不同长度的哈希值。它们在一些特定的应用场景中有一定的使用。
- BLAKE2:BLAKE2 是一种高速、安全的哈希函数,能够生成不同长度的哈希值。它在性能方面优于许多其他哈希算法,并且在一些应用场景中取得了广泛的应用。
- Whirlpool:Whirlpool 是一种比较少见但仍在一些场景中使用的哈希函数,生成 512 位长度的哈希值,被认为具有较高的安全性。
- SHA-3(Secure Hash Algorithm 3):SHA-3 是美国国家标准与技术研究所(NIST)发布的一种哈希算法标准,与 SHA-2 不同,SHA-3 使用了基于 Keccak 构造的算法。SHA-3 提供了多种摘要长度的选择,包括 224 位、256 位、384 位和 512 位。
- HMAC 算法, 它是一种基于哈希函数的消息认证码算法。HMAC 通过将密钥与消息进行连续的哈希运算,结合了密钥的安全性和哈希函数的强度,从而提供了一种安全的消息认证方式。它通常使用的哈希函数包括 MD5、SHA-1、SHA-256 等,因此 HMAC 可以基于不同的哈希算法进行实现,如 HMAC-MD5、HMAC-SHA1、HMAC-SHA256 等。
对比:
这些哈希算法在不同的应用场景中具有不同的特点和适用性,选择合适的哈希算法取决于具体的需求以及安全性要求。
5.2 JS中的MD5、SHA、HMAC、SM3
CryptoJS 字符串解析: 如果加密函数传入的参数是 string 类型的数据,将使用默认的 Utf8.parse
来进行解析,示例代码:
let CryptoJS = require('../CryptoJS');
// ① string转wordArray
console.log(CryptoJS.enc.Utf8.parse('AmoXiang666'));
console.log(CryptoJS.enc.Hex.parse('416d6f5869616e67363636'));
console.log(CryptoJS.enc.Base64.parse('QW1vWGlhbmc2NjY='));
// ② wordArray转string
let wordArray = CryptoJS.enc.Utf8.parse('AmoXiang666');
console.log(wordArray.toString()); // Hex
console.log(wordArray + ''); // Hex
console.log(wordArray.toString(CryptoJS.enc.Base64));
console.log(wordArray.toString(CryptoJS.enc.Utf8));
// console.log(CryptoJS.enc.Utf8.stringify(wordArray));
// console.log(CryptoJS.enc.Base64.stringify(wordArray));
wordArray = CryptoJS.enc.Utf8.parse('AmoXiang666');
let hex = wordArray.toString();
console.log(hex);
console.log(CryptoJS.enc.Hex.parse(hex).toString(CryptoJS.enc.Base64));
console.log(CryptoJS.MD5('AmoXiang666').toString());
MD5: 加密后的字节数组可以编码成 Hex、Base64,CryptoJS 库默认输出 Hex,没有任何输入,也能计算 hash 值,碰到加 salt 的 MD5,可以直接输入空的值,得到结果去 CMD5 查询一下,有可能就得到 salt,示例代码:
let CryptoJS = require('../CryptoJS');
let wordArray = CryptoJS.enc.Utf8.parse('AmoXiang666');
console.log(CryptoJS.MD5(wordArray) + ''); //默认加密结果为hex编码
console.log(CryptoJS.MD5(wordArray).toString(CryptoJS.enc.Base64)); //转换为base64编码
let hexMd5 = CryptoJS.MD5(wordArray) + '';
console.log(hexMd5)
wordArray = CryptoJS.enc.Hex.parse(hexMd5); //字节数组
console.log(wordArray)
console.log(CryptoJS.enc.Base64.stringify(wordArray));
console.log(CryptoJS.MD5() + ''); //没有任何输入,也能计算 hash 值
let CryptoJS = require('../CryptoJS');
console.log(CryptoJS.MD5('AmoXiang666').toString()); //默认会使用CryptoJS.enc.Utf8.parse
let xiaArr = CryptoJS.enc.Utf8.parse('AmoXiang666');
console.log(CryptoJS.MD5(xiaArr).toString());
SHA:
// 简单写法
let CryptoJS = require('../CryptoJS');
let wordArray = CryptoJS.enc.Utf8.parse('AmoXiang666');
console.log(CryptoJS.SHA1(wordArray) + '');
console.log(CryptoJS.SHA256(wordArray) + '');
console.log(CryptoJS.SHA512(wordArray) + '');
console.log(CryptoJS.SHA224(wordArray) + '');
console.log(CryptoJS.SHA384(wordArray) + '');
console.log(CryptoJS.SHA3(wordArray) + '');
// 另外的写法
let SHA1 = CryptoJS.algo.SHA1.create();
SHA1.update('AmoXiang666');
let cipherText = SHA1.finalize() + '';
console.log(cipherText);
//SHA1.reset(); 重置
SHA1.update('jerry');
console.log(SHA1.finalize() + '');
HMAC算法: HMAC 算法与 MD 和 SHA 的区别是多了一个密钥,密钥可以随机给,HMAC 的密文长度与 MD 和 SHA 是一致的,同样加密后的字节数组可以编码成 Hex、Base64,CryptoJS 库默认输出 Hex,没有任何输入,也能计算 hash 值。
let CryptoJS = require('../CryptoJS');
console.log(CryptoJS.HmacMD5('', 'key') + '');
console.log(CryptoJS.HmacMD5('AmoXiang666', 'key') + '');
console.log(CryptoJS.HmacSHA1('AmoXiang666', 'key') + '');
console.log(CryptoJS.HmacSHA1('AmoXiang666', 'key').toString(CryptoJS.enc.Base64));
let hmacSHA1 = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA1.create(), 'key');
hmacSHA1.update('AmoXiang666');
console.log(hmacSHA1.finalize() + '');
SM3算法: 国密算法有很多种,其中 SM3 是类似于 SHA256 的消息摘要算法,SM3 的输入长度与 SHA 算法一致,最大为 264-1,SM3 的密文长度与 SHA256 一致,同样加密后的字节数组可以编码成 Hex、Base64,没有任何输入,也能计算 hash 值。
//这一步是先将输入数据转成utf-8编码的字节流,然后再转成16进制可见字符
var dataBy = Hex.utf8StrToBytes('AmoXiang666');
var sm3 = new SM3Digest();
sm3.update(dataBy,0,dataBy.length); //数据很多的话,可以分多次update
var sm3Hash = sm3.doFinal(); //得到的数据是个byte数组
var sm3HashHex = Hex.encode(sm3Hash,0,sm3Hash.length); //编码成16进制可见字符
console.log(sm3HashHex);
六、对称加密算法
6.1 介绍
对称加密算法是一种加密技术,使用 相同的密钥 对数据进行加密和解密。这意味着发送方和接收方必须共享相同的密钥,用于加密和解密数据。对称加密算法具有加密速度快、计算效率高的特点,适合对大量数据进行加密。一些常见的对称加密算法包括:
- DES(Data Encryption Standard): DES 是一种早期的对称加密算法,使用 56 位密钥对数据进行加密和解密。尽管 DES 在安全性上存在一些弱点,但它为后续的加密算法奠定了基础。(在实际使用中,DES 密钥通常由 64 位长度的密钥中的第 8 位用作奇偶校验位,因此实际上只有 56 位是用于加密和解密的密钥位数)
- 3DES(Triple DES): 3DES 是 DES 的增强版本,它多次对数据进行 DES 加密,通常使用两个或三个密钥。虽然 3DES 提供了更高的安全性,但由于其计算复杂度较高,已经逐渐被更先进的加密算法所取代。(在使用 3DES 进行加密时,通常会使用两个密钥(K1 和 K2)或三个密钥(K1、K2 和 K3)进行三次 DES 加密。如果使用两个密钥,则每个密钥长度为 56 位,总长度为 112 位。如果使用三个密钥,则每个密钥长度为 56 位,总长度为 168 位)
- AES(Advanced Encryption Standard): AES 是一种广泛使用的对称加密算法,设计用于取代 DES。它支持不同的密钥长度,包括 128 位、192 位和 256 位,具有较高的安全性和较快的加密速度,因此被广泛应用于各种安全领域。(AES(Advanced Encryption Standard)算法的密钥长度要求:AES-128:128 位(16 字节)、AES-192:192 位(24 字节)、AES-256:256 位(32 字节))
- Blowfish:Blowfish 是一种对称加密算法,支持变长密钥(32 至 448 位),并且具有高速和高度可配置性的特点。尽管 Blowfish 在许多场景下仍然被使用,但它已经逐渐被更先进的算法所取代。
- RC4(Rivest Cipher 4):RC4 是一种流密码(Stream Cipher)算法,具有简单、高效的特点。尽管 RC4 曾被广泛应用于 SSL/TLS、WEP 等协议中,但由于其存在一些安全性问题,如密钥漏洞和偏置攻击,已经逐渐被淘汰。
- SM4 算法, 也称为国密算法,是由中国密码学家提出的一种分组加密算法,被采纳为中国商用密码算法标准。它是一种对称加密算法,用于对数据进行加密和解密。SM4 算法的主要特点包括:①分组大小: SM4 使用 128 位(16 字节)的分组大小进行加密和解密操作。②密钥长度: SM4 算法支持密钥长度为 128 位(16 字节)。③轮数: SM4 算法采用了 32 轮的 Feistel 结构进行加密,每轮包括逐位的非线性变换和线性变换。④S 盒: SM4 使用了一个固定的 8x8 的 S 盒,用于非线性变换,增强了算法的安全性。⑤密钥扩展: SM4 算法对输入的密钥进行扩展,生成多轮加密过程中所需的轮密钥。⑥安全性: SM4 算法经过了严格的密码学分析和安全性评估,被认为具有较高的安全性和抗攻击能力。SM4 的代码实现细节,在专栏安卓逆向中做介绍。
6.2 加密模式和填充方式
在对称加密算法中,加密模式和填充模式是两个重要的概念,用于指定如何对数据进行加密和解密。
加密模式(Encryption Mode): 加密模式定义了在加密过程中如何处理数据块、处理块之间的关系以及如何处理最后一个块的方法。常见的加密模式包括:
ECB(Electronic Codebook)模式:将明文分成固定大小的块,并独立地对每个块进行加密。
CBC(Cipher Block Chaining)模式:每个明文块先与前一个密文块进行异或运算,然后再进行加密。
CFB(Cipher Feedback)模式:将前一个密文块作为加密器的输入,产生密文块。
OFB(Output Feedback)模式:将前一个密文块作为加密器的输入,产生密钥流,再与明文进行异或运算得到密文。
CTR(Counter)模式:使用一个计数器和密钥生成伪随机密钥流,再与明文进行异或运算得到密文。
填充模式(Padding Mode): 填充模式用于在加密前将不满足块大小要求的数据块填充到合适的长度,以便进行加密。常见的填充模式包括:
PKCS#5 和 PKCS#7:使用一定规则填充数据块,通常采用的填充值是缺少的字节数。
Zero Padding:填充的字节全部为零。
ANSI X.923:除了最后一个字节外,填充的字节为零,最后一个字节表示填充的字节数。
ISO 10126:填充的字节为随机值,最后一个字节表示填充的字节数。
6.3 CryptoJS 中DES、DESede、AES算法实现
DES: DES 密钥长度为 64bit(实际使用长度是 56bit,前面已经提到过),分组长度为 64bit,CryptoJS 中 DES 算法的实现:
let CryptoJS = require('../CryptoJS');
let plainText = CryptoJS.enc.Utf8.parse('AmoXiang666');
// 00110001 00110010 00110011 00110100 00110101 00110110 00110111 00111000 12345678
// 00110000 00110011 00110010 00110101 00110100 00110111 00110110 00111001 03254769
// let key = CryptoJS.enc.Utf8.parse('12345678'); //密钥
let key = CryptoJS.enc.Utf8.parse('03254769'); //两个完全不同的密钥可以加密得到相同的结果
let iv = CryptoJS.enc.Utf8.parse('88888888');
let cfg = {
iv: iv, //如果加密模式是ECB,则不需要加iv,加了也用不上
mode: CryptoJS.mode.CBC, //填充的模式
padding: CryptoJS.pad.Pkcs7 //cfg中没有传mode与padding,默认使用CBC的加密模式,Pkcs7的填充方式
};
let cipherObj = CryptoJS.DES.encrypt(plainText, key, cfg); //加密
console.log(cipherObj)
// 加密的结果cipherObj是一个对象,调用toString()方法默认转Base64编码的密文
console.log(cipherObj.toString());
// 转hex可以使用下面的方式
console.log(cipherObj.ciphertext.toString());
//6cfaefd865294e2970c639c3eedc4b4e
key = CryptoJS.enc.Utf8.parse('12345678');
//let key = CryptoJS.enc.Utf8.parse('03254769');
iv = CryptoJS.enc.Utf8.parse('88888888');
let cipherText = CryptoJS.enc.Hex.parse('6cfaefd865294e2970c639c3eedc4b4e')
.toString(CryptoJS.enc.Base64);
cfg = {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
};
let plainObj = CryptoJS.DES.decrypt(cipherText, key, cfg);
console.log(plainObj.toString(CryptoJS.enc.Utf8));
DESede: DESede 算法明文按 64 位进行分组加密,密钥长度为 24 字节,本质为三次 DES 加解密,DES 加密(使用密钥前8个字节),DES 解密(使用密钥中8个字节),DES 加密(使用密钥后8个字节),CryptoJS 中 DESede 算法的实现:
let CryptoJS = require('../CryptoJS');
let plainText = CryptoJS.enc.Utf8.parse('AmoXiang666');
let key = CryptoJS.enc.Utf8.parse('123456783333333388888888');
let iv = CryptoJS.enc.Utf8.parse('88888888');
var cfg = {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
};
let cipherObj = CryptoJS.TripleDES.encrypt(plainText, key, cfg);
console.log(cipherObj.toString());
console.log(cipherObj.ciphertext.toString());
AES: 根据密钥长度不同,分为 AES128、AES192、AES256
CryptoJS 中 AES 算法的实现:
let CryptoJS = require('../CryptoJS');
let plainText = CryptoJS.enc.Utf8.parse('AmoXiang666');
let key = CryptoJS.enc.Utf8.parse('1234567890abcdef12345678');
let iv = CryptoJS.enc.Utf8.parse('1234567890abcdef');
var cfg = {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
};
let cipherObj = CryptoJS.AES.encrypt(plainText, key, cfg);
console.log(cipherObj.toString());
console.log(cipherObj.ciphertext.toString());
6.4 对称加密算法注意事项
-
要复现一个对称加密算法,需要得到明文、key、iv、mode、padding
-
明文、key、iv 需要注意解析方式,而且不一定是字符串形式
-
如果明文中有两个分组的内容相同,ECB 会得到完全一样的密文,CBC 不会
-
加密算法的结果通常与明文等长或者更长,如果变短了,那可能是 gzip、protobuf
-
密文/明文的自定义输出/输入(cfg 中 format 的指定)
let CryptoJS = require('../CryptoJS'); let plainText = CryptoJS.enc.Utf8.parse('AmoXiang666'); let key = CryptoJS.enc.Utf8.parse('1234567890abcdef12345678'); let iv = CryptoJS.enc.Utf8.parse('1234567890abcdef'); // let cfg = { // iv: iv, // mode: CryptoJS.mode.CBC, // padding: CryptoJS.pad.Pkcs7, // format: CryptoJS.format.Hex // }; let format = { stringify: function (data) { let e = { ct: data.ciphertext.toString(CryptoJS.enc.Base64), miaoshu: "这是我们的自定义输出内容" }; return JSON.stringify(e) }, parse: function (data) { let json = JSON.parse(data); let newVar = CryptoJS.lib.CipherParams.create({ ciphertext: CryptoJS.enc.Base64.parse(json.ct) }); return newVar } }; // let cfg = { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, format: format }; // let cipherObj = CryptoJS.AES.encrypt(plainText, key, cfg); console.log(cipherObj.toString()); let cipherText = cipherObj.toString(); let plainObj = CryptoJS.AES.decrypt(cipherText, key, cfg); console.log(plainObj.toString(CryptoJS.enc.Utf8));
-
CryptoJS 自动生成 key、iv、salt。参考文章链接:https://www.jianshu.com/p/0689506403e7
let CryptoJS = require('../CryptoJS'); let format = { stringify: function (data){ let e = { ct: data.ciphertext.toString(), iv: data.iv.toString(), salt: data.salt.toString(), }; return JSON.stringify(e) }, parse: function (data){ let json = JSON.parse(data); return CryptoJS.lib.CipherParams.create({ ciphertext: CryptoJS.enc.Hex.parse(json.ct), iv: CryptoJS.enc.Hex.parse(json.iv), salt: CryptoJS.enc.Hex.parse(json.salt), }); } }; var cfg = { format: format }; let cipherObj = CryptoJS.AES.encrypt('AmoXiang666', '12345678123456781234567812345678', cfg); let cipherText = cipherObj.toString(); console.log(cipherText); let plainObj = CryptoJS.AES.decrypt(cipherText, '12345678123456781234567812345678', cfg); console.log(plainObj.toString(CryptoJS.enc.Utf8));
6.5 CryptoJS(其他算法)
示例代码:
let CryptoJS = require('../CryptoJS');
console.log(CryptoJS.RIPEMD160('AmoXiang666').toString());
console.log(CryptoJS.HmacRIPEMD160('AmoXiang666', 'keykeykey').toString());
console.log(CryptoJS.PBKDF2('AmoXiang666', 'keykeykey1234', {keySize: 4, iterations: 2000}).toString());
console.log(CryptoJS.EvpKDF('AmoXiang666', 'keykeykey1234', {keySize: 4, iterations: 2000}).toString());
console.log(CryptoJS.RC4);
console.log(CryptoJS.RC4Drop);
console.log(CryptoJS.Rabbit);
console.log(CryptoJS.RabbitLegacy);
SM4 算法, JS 实现:
//sm4-1.0.js
function sm4_encrypt_ecb() {
let inputBytes = Hex.decode('0123456789abcdeffedcba9876543210');
let key = Hex.decode('0123456789abcdeffedcba9876543210');
let sm4 = new SM4();
let cipher = sm4.encrypt_ecb(key, inputBytes);
console.log(Hex.encode(cipher, 0, cipher.length));
}
function sm4_encrypt_cbc() {
let inputBytes = Hex.decode('0123456789abcdeffedcba9876543210');
let key = Hex.decode('0123456789abcdeffedcba9876543210');
let iv = Hex.decode('0123456789abcdeffedcba9876543210');
let sm4 = new SM4();
let cipher = sm4.encrypt_cbc(key, iv, inputBytes);
console.log(Hex.encode(cipher, 0, cipher.length));
}
sm4_encrypt_ecb();
sm4_encrypt_cbc();
七、非对称加密算法
非对称加密算法: 加密解密使用不同密钥的算法,典型算法,RSA、SM2
非对称加密算法通常有一个密钥对,称为公钥和私钥
公钥加密的数据,私钥才能解密
私钥加密的数据,公钥才能解密
密钥对需要生成,不是随便写的,RSA 密钥对的生成 http://web.chacuo.net/netrsakeypair
私钥的格式:
PKCS1格式通常开头是 -----BEGIN RSA PRIVATE KEY-----
PKCS8格式通常开头是 -----BEGIN PRIVATE KEY-----Java中使用的私钥通常是PKCS8格式
RSA密钥的形式: Base64 编码形式(PEM格式)、Hex 编码形式。公钥是可以公开的,私钥保密,私钥包含公钥,从公钥无法推导出私钥,数字签名算法使用私钥签名,此时私钥会出现在客户端。
RSA 常见加密库的使用(jsencrypt.js、RSA.js): 加密后的字节数组可以编码成 Hex、Base64。
//jsencrypt.js
function getEncrypt(password, publickey){
var jsEncrypt = new JSEncrypt();
jsEncrypt.setPublicKey(publickey);
return jsEncrypt.encrypt(password);
}
var publicKeyBase64 = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDxRQHxL/8xZ1EaNmQBGZnpMiCY" +
"7gRzog6nDjfBJacytEiVJnJRuq1V/D+JKaXDwetsCnSUaz65LCFHU09OSEYee5oC" +
"iI0ql21EA306c91oT/fQpPngQGZHLUtDOUdJVlAKnicCvmR24NqyNKFuY8L0cnB1" +
"zcax73Rf+Ctf/lxAOwIDAQAB";
console.log(getEncrypt("AmoXiang666", publicKeyBase64));
RSA 加密处理安全,但是性能极差,单次加密长度有限制,一般用于加密较短的数据,需要加密较长的数据时,会和对称加密算法结合使用。RSA 填充细节:
NOPadding:明文最多字节数为密钥字节数,密文与密钥等长,填充字节0,加密后的密文不变
PKCS1Padding:明文最大字节数为密钥字节数-11,密文与密钥等长,每一次的填充不一样,使得加密后的密文会变
多种加密算法的常见结合套路:
① 随机生成 AES 密钥 AESKey
② AESKey 密钥用于 AES 加密数据,得到数据密文 cipherText
③ 使用 RSA 对 AESKey 加密,得到密钥密文 cipherKey
④ 提交密钥密文 cipherKey 和数据密文 cipherText 给服务器
JS 数字签名算法库的使用:
var signData = "AmoXiang666";
//PKCS1格式的密钥 前缀 -----BEGIN RSA PRIVATE KEY----- 后缀 -----END RSA PRIVATE KEY-----
//PKCS8格式的密钥 前缀 -----BEGIN PRIVATE KEY----- 后缀 -----END PRIVATE KEY-----
var privateKeyBase64 = "-----BEGIN PRIVATE KEY-----MIICdwIBADANBgkqhki" +
"G9w0BAQEFAASCAmEwggJdAgEAAoGBAPFFAfEv/zFnURo2\n" +
"ZAEZmekyIJjuBHOiDqcON8ElpzK0SJUmclG6rVX8P4kppcPB62wKdJRrPrksIUdT\n" +
"T05IRh57mgKIjSqXbUQDfTpz3WhP99Ck+eBAZkctS0M5R0lWUAqeJwK+ZHbg2rI0\n" +
"oW5jwvRycHXNxrHvdF/4K1/+XEA7AgMBAAECgYEAsGkDrYWps0bW7zKb1o4Qkojb\n" +
"etZ2HNJ+ojlsHObaJOHbPGs7JXU4bmmdTz5LfSIacAoJCciMuTqCLrPEhfmkghPq\n" +
"U2MjyjfqYdXALoP7l/vt6QmjY/g1IAsaZN9nFhyjJ2WzgOx1f7gZj4NBSvTdSj7H\n" +
"m5E24zkm+p7Qw1z6/mkCQQD7WSXAXcv2v3Vo6qi1FUlkzQgCQLFYqXNSOSPpno3y\n" +
"oohUFIkMj0bYGbVE1LzV30Rb6Z8e8yQAByw6l8RuGb2PAkEA9bwb2euyOe6CcqpE\n" +
"PNFc+7UlOJAy5epVFKHbu0aNivVpU0hsphqjIGXJGHYTspyEOLqtzILqKPZr6pru\n" +
"WvJUlQJBAJoImQUZtlyCGs7wN/G5mN/ocscGpGikd+Lk16hdHbqbdpaoexCyYYUf\n" +
"xCHpicw75mW5d2V9Ngu6WZWS2rNqnOsCQCoMK//X8sEy7KNOOyrk8DIpxtqs4eix\n" +
"dil3oK+k3OdgIsubYuvxNuR+RjCnU6uGWKGUX9TUudiUgda89/gb6xkCQFm8gD6n\n" +
"AyN+PPPKRq2M84+cAbnvjdIAY3OFHfkaoWCtEj5DR0UDuVv7jN7+re2D7id/GkAe\n" +
"FAmhvYQwwLnifrw=-----END PRIVATE KEY-----";
function doSign() {
var signature = KEYUTIL.getKey(privateKeyBase64);
var hSig = signature.signString(signData, "sha256");
return hex2b64(hSig);
}
console.log(doSign());
SM2 算法库的使用: 参考 gmjs-master
说明
文章转载自@Amo Xiang,2024最新版JavaScript逆向爬虫教程-------基础篇之JavaScript密码学以及CryptoJS各种常用算法的实现