JavaScript逆向Basics: 加密与安全
当我们学习使用爬虫时,我们发现大多数网站的动态数据都采用了加密的方式。通过源代码中进行调试,我们可以“扣”出执行加密的JavaScript代码,再在python中调用JavaScript即可完成加密。尽管如此,了解加密算法,仍能够让我们迅速地反应过来加密函数所用编码,从而能让我们对JavaScript用Python进行改写,或是应对源代码中的僵尸代码。
本文包含的算法有编码算法,哈希算法,对称加密算法,口令加密算法,密钥交换算法,非对称加密算法,签名算法,数字证书。我会首先介绍基础知识,如用途,使用什么JavaScript函数完成等等。紧接着再介绍该算法的流程。话不多说,我们开始吧!
编码算法
编码算法事实上并不是一种加密算法,他仅仅是将数据使用某种编码进行表示,因而我们首先要了解什么是编码。
我们最常见的编码是ASCII编码,如下表。
ASCII值 | 控制字符 | ASCII值 | 控制字符 | ASCII值 | 控制字符 | ASCII值 | 控制字符 | |
---|---|---|---|---|---|---|---|---|
0 | NUT | 32 | (space) | 64 | @ | 96 | 、 | |
1 | SOH | 33 | ! | 65 | A | 97 | a | |
2 | STX | 34 | “ | 66 | B | 98 | b | |
3 | ETX | 35 | # | 67 | C | 99 | c | |
4 | EOT | 36 | $ | 68 | D | 100 | d | |
5 | ENQ | 37 | % | 69 | E | 101 | e | |
6 | ACK | 38 | & | 70 | F | 102 | f | |
7 | BEL | 39 | , | 71 | G | 103 | g | |
8 | BS | 40 | ( | 72 | H | 104 | h | |
9 | HT | 41 | ) | 73 | I | 105 | i | |
10 | LF | 42 | * | 74 | J | 106 | j | |
11 | VT | 43 | + | 75 | K | 107 | k | |
12 | FF | 44 | , | 76 | L | 108 | l | |
13 | CR | 45 | - | 77 | M | 109 | m | |
14 | SO | 46 | . | 78 | N | 110 | n | |
15 | SI | 47 | / | 79 | O | 111 | o | |
16 | DLE | 48 | 0 | 80 | P | 112 | p | |
17 | DCI | 49 | 1 | 81 | Q | 113 | q | |
18 | DC2 | 50 | 2 | 82 | R | 114 | r | |
19 | DC3 | 51 | 3 | 83 | S | 115 | s | |
20 | DC4 | 52 | 4 | 84 | T | 116 | t | |
21 | NAK | 53 | 5 | 85 | U | 117 | u | |
22 | SYN | 54 | 6 | 86 | V | 118 | v | |
23 | TB | 55 | 7 | 87 | W | 119 | w | |
24 | CAN | 56 | 8 | 88 | X | 120 | x | |
25 | EM | 57 | 9 | 89 | Y | 121 | y | |
26 | SUB | 58 | : | 90 | Z | 122 | z | |
27 | ESC | 59 | ; | 91 | [ | 123 | { | |
28 | FS | 60 | < | 92 | \ | 124 | \ | |
29 | GS | 61 | = | 93 | ] | 125 | } | |
30 | RS | 62 | > | 94 | ^ | 126 | ` | |
31 | US | 63 | ? | 95 | _ | 127 | DEL |
上表中,ASCII值为十进制,但我们通常用16进制表示编码,故字母A
的编码是十六进制的0x41
,字母B
是0x42
,以此类推;
ASCII对常见的字符进行编码,但其仅仅只有128个字符,想要表示更多就需要使用Unicode
编码。
Unicode编码是一种字符编码方案,用于表示世界上几乎所有字符的标准化编码。它为每个字符分配了一个唯一的数值,称为码点(Code Point),并以十六进制表示。最常见的表示方式是使用前缀”\u”加上四个十六进制数字来表示一个字符的 Unicode 码点。例如,字符 “A” 的 Unicode 码点是 U+0041,可以表示为 “\u0041”。
Unicode 编码有不同的编码方式,其中最常见的是 UTF-8 和 UTF-16。UTF-8 是一种变长编码,使用 1 到 4 个字节来表示不同的字符。在 UTF-8 编码中,常见的 ASCII 字符使用一个字节表示,而非 ASCII 字符使用多个字节表示。UTF-16 是一种定长编码,使用 2 个字节或 4 个字节来表示不同的字符。大部分常见字符使用两个字节表示,而一些较罕见的字符使用四个字节表示。
Python 中的 ASCII 和 Unicode 处理:
ASCII 编码:
- 使用
ord()
函数将字符转换为对应的 ASCII 码值。 - 使用
chr()
函数将 ASCII 码值转换为对应的字符。1
2
3
4
5
6char = 'A'
ascii_value = ord(char)
print(ascii_value) # 输出:65
char = chr(65)
print(char) # 输出:A
- 使用
Unicode 编码:
- 使用
encode()
方法将字符串编码为指定的 Unicode 编码格式。 - 使用
decode()
方法将 Unicode 编码转换为字符串。1
2
3
4
5
6string = '你好'
unicode_string = string.encode('unicode_escape')
print(unicode_string) # 输出:b'\\u4f60\\u597d'
decoded_string = unicode_string.decode('unicode_escape')
print(decoded_string) # 输出:你好
- 使用
JavaScript 中的 ASCII 和 Unicode 处理:
ASCII 编码:
- 使用
charCodeAt()
方法获取字符串中指定位置字符的 ASCII 码值。 - 使用
String.fromCharCode()
方法将 ASCII 码值转换为对应的字符。1
2
3
4
5
6var char = 'A';
var asciiValue = char.charCodeAt(0);
console.log(asciiValue); // 输出:65
var char = String.fromCharCode(65);
console.log(char); // 输出:A
- 使用
Unicode 编码:
- 使用
\u
前缀将 Unicode 字符码直接插入字符串中。 - 使用
String.fromCharCode()
方法将 Unicode 字符码转换为对应的字符。1
2
3
4
5var string = '\u4F60\u597D';
console.log(string); // 输出:你好
var char = String.fromCharCode(0x4F60);
console.log(char); // 输出:你
- 使用
另外一种常见但可能绝大多数人不明白的是URL编码,他通常出现在我们浏览器的URL栏中。譬如,当我使用Bing进行一次检索“必应”,URL会变成“https://www.bing.com/search?q=%E5%BF%85%E5%BA%94&mkt=zh-CN”,如果你对爬虫比较熟悉的话应该知道?
后是字符串参数,使用&
进行分隔,”q=%E5%BF%85%E5%BA%94”即是我们这次检索的内容,q=
之后的即为URL编码后的“必应”。
URL的编码逻辑是:
- 如果字符是
A
~Z
,a
~z
,0
~9
以及-
、_
、.
、*
,则保持不变; - 如果是其他字符,先转换为UTF-8编码,然后对每个字节以
%XX
表示。
例如:字符中
的UTF-8编码是0xe4b8ad
,因此,它的URL编码是%E4%B8%AD
。URL编码总是大写。也即“去0x,改大写,每字节加%”。!
等特殊字符非-
、_
、.
、*
,虽然是ASCII字符,也要对其编码。
在 Python 中,可以使用 urllib.parse
模块中的 quote()
和 quote_plus()
函数来进行 URL 编码,以及使用 unquote()
和 unquote_plus()
函数来进行 URL 解码。1
2
3
4
5
6
7
8
9
10import urllib.parse
# URL 编码
url = 'https://www.example.com/?q=hello world'
encoded_url = urllib.parse.quote(url)
print(encoded_url) # 输出:https%3A//www.example.com/%3Fq%3Dhello%20world
# URL 解码
decoded_url = urllib.parse.unquote(encoded_url)
print(decoded_url) # 输出:https://www.example.com/?q=hello world
在 JavaScript 中,可以使用 encodeURIComponent()
函数进行 URL 编码,以及使用 decodeURIComponent()
函数进行 URL 解码。1
2
3
4
5
6
7
8// URL 编码
var url = 'https://www.example.com/?q=hello world';
var encodedUrl = encodeURIComponent(url);
console.log(encodedUrl); // 输出:https%3A%2F%2Fwww.example.com%2F%3Fq%3Dhello%20world
// URL 解码
var decodedUrl = decodeURIComponent(encodedUrl);
console.log(decodedUrl); // 输出:https://www.example.com/?q=hello world
URL编码是对字符进行编码,表示成%xx
的形式,而Base64编码是对二进制数据进行编码,表示成文本格式。它常用于在文本协议中传输或存储二进制数据,例如在电子邮件中传输附件或在网页中嵌入图像数据。
Base64编码可以把任意长度的二进制数据变为纯文本,且只包含A
~Z
、a
~z
、0
~9
、+
、/
、=
这些字符。它的原理是把3字节的二进制数据按6bit一组,用4个int整数表示,然后查表,把int整数用索引对应到字符,得到编码后的字符串。
码值 | 字符 | 码值 | 字符 | 码值 | 字符 | 码值 | 字符 |
---|---|---|---|---|---|---|---|
0 | A | 16 | Q | 32 | g | 48 | w |
1 | B | 17 | R | 33 | h | 49 | x |
2 | C | 18 | S | 34 | i | 50 | y |
3 | D | 19 | T | 35 | j | 51 | z |
4 | E | 20 | U | 36 | k | 52 | 0 |
5 | F | 21 | V | 37 | l | 53 | 1 |
6 | G | 22 | W | 38 | m | 54 | 2 |
7 | H | 23 | X | 39 | n | 55 | 3 |
8 | I | 24 | Y | 40 | o | 56 | 4 |
9 | J | 25 | Z | 41 | p | 57 | 5 |
10 | K | 26 | a | 42 | q | 58 | 6 |
11 | L | 27 | b | 43 | r | 59 | 7 |
12 | M | 28 | c | 44 | s | 60 | 8 |
13 | N | 29 | d | 45 | t | 61 | 9 |
14 | O | 30 | e | 46 | u | 62 | + |
15 | P | 31 | f | 47 | v | 63 | / |
需要注意的是,Base64 编码后的数据会比原始数据略长,因为每 3 个字节的数据会被编码为 4 个字符。Base64 编码的结果是由 A-Z、a-z、0-9、+ 和 / 组成的字符序列。有时,由于特定环境的限制(其实就是为了在URL中使用),会将字符 “+” 和 “/“ 分别替换为 “-“ 和 “_“。
当输入的二进制数组字节长度不是3的整数倍时,需要对输入的末尾补一个或两个0x00
,编码后,在结尾加一个=
表示补充了1个0x00
,加两个=
表示补充了2个0x00
,解码的时候,去掉末尾补充的一个或两个0x00
即可。
在 Python 中,可以使用 base64
模块来进行 Base64 编码和解码操作。该模块提供了 b64encode()
和 b64decode()
函数用于编码和解码操作。1
2
3
4
5
6
7
8
9
10import base64
# Base64 编码
data = b'Hello, World!'
encoded_data = base64.b64encode(data)
print(encoded_data) # 输出:b'SGVsbG8sIFdvcmxkIQ=='
# Base64 解码
decoded_data = base64.b64decode(encoded_data)
print(decoded_data) # 输出:b'Hello, World!'
在 JavaScript 中,可以使用 btoa()
函数进行 Base64 编码,以及使用 atob()
函数进行 Base64 解码。1
2
3
4
5
6
7
8// Base64 编码
var data = 'Hello, World!';
var encodedData = btoa(data);
console.log(encodedData); // 输出:SGVsbG8sIFdvcmxkIQ==
// Base64 解码
var decodedData = atob(encodedData);
console.log(decodedData); // 输出:Hello, World!
哈希算法
哈希算法(Hash Algorithm)是一种将任意长度的数据映射为固定长度哈希值(Hash Value)的算法。哈希值通常是一个较短的固定大小的字节数组,用于表示原始数据。
哈希算法的主要特点是:
- 固定输出长度:不论输入数据的大小,哈希算法都会生成一个固定长度的哈希值。常见的哈希长度有128位、256位或更长。
- 独特性:对于不同的输入数据,哈希算法应该生成不同的哈希值。这意味着即使输入数据发生细微的改变,生成的哈希值也会有较大的差异。
- 不可逆性:从哈希值无法还原出原始数据。即使对于稍微不同的输入,其哈希值也应该是完全不同的。
- 高效性:计算哈希值的速度应该很快,即使对于大量的数据也应该在合理的时间内完成。
哈希碰撞: 尽管哈希算法的目标是生成唯一的哈希值,但在实际应用中,由于输入数据的无限性和哈希值的有限性,哈希碰撞是可能发生的。哈希算法的输出空间是固定的,而输入空间则是无穷大的,因此存在多个不同的输入数据可能会映射到相同的哈希值上。
对于一个安全的哈希算法而言, 他必须要 1)碰撞概率低;2)不能推测出输出。碰撞概率高增需要加长输出字节,而不能推测输出指的是输出不能看出任何规律。
常见的哈希算法
MD5算法
算法流程:
- 填充数据:将输入数据进行填充,使其长度满足对512位(64字节)块的整数倍。填充方式是在数据末尾添加一个1比特,然后添加足够的零比特,以填充剩余的空间,并在最后添加一个64位的,表示原始数据长度的,值。即令其位长对512求余的结果等于448。
- 划分为块:将填充后的数据划分为多个512位(64字节)的数据块。
- 初始化变量:设置四个32位的初始变量A、B、C和D。这些变量作为中间计算结果的存储器。
- 处理每个块:对于每个512位的数据块,进行以下操作:
- 将块分为16个32位的子块。
- 初始化四个临时变量:a、b、c和d,其初始值与A、B、C和D相同。
- 进行四轮循环,每轮循环包括16个操作步骤。在每个步骤中,根据一定的位操作、逻辑函数和非线性函数,更新临时变量的值。
- 四个非线性函数:
- F( X ,Y ,Z ) = ( X & Y ) | ( (~X) & Z )
- G( X ,Y ,Z ) = ( X & Z ) | ( Y & (~Z) )
- H( X ,Y ,Z ) =X ^ Y ^ Z
- I( X ,Y ,Z ) =Y ^ ( X | (~Z) )
&
是与(And),|
是或(Or),~
是非(Not),^
是异或(Xor))
- 更新临时变量():
- FF(X ,Y ,Z ,V ,Mj ,s ,ti ) 操作为 X = Y + ( (X + F(Y,Z,V) + Mj + ti) << s)
- GG(X ,Y ,Z ,V ,Mj ,s ,ti ) 操作为 X = Y + ( (X + G(Y,Z,V) + Mj + ti) << s)
- HH(X ,Y ,Z ,V ,Mj ,s ,ti) 操作为 X = Y + ( (X + H(Y,Z,V) + Mj + ti) << s)
- II(X ,Y ,Z ,V ,Mj ,s ,ti) 操作为 X = Y + ( (X + I(Y,Z,V) + Mj + ti) << s)
<<
表示循环左移位,将左侧溢出的值移到右边。
- 每一轮中分别使用FF,GG,HH,II函数。
- 每次操作对a、b、c和d中的其中三个作一次非线性函数运算,然后将所得结果加上第四个变量,文本的一个子分组,即第i个子块Mj和一个常数ti。再将所得结果向左环移一个不定的数s,并加上a、b、c或d中之一。最后用该结果取代a、b、c或d中之一。
- 第一轮
- FF(a ,b ,c ,d ,M0 ,7 ,0xd76aa478 )
- FF(d ,a ,b ,c ,M1 ,12 ,0xe8c7b756 )
- FF(c ,d ,a ,b ,M2 ,17 ,0x242070db )
- FF(b ,c ,d ,a ,M3 ,22 ,0xc1bdceee )
- FF(a ,b ,c ,d ,M4 ,7 ,0xf57c0faf ) 发现规律了吗?abcd循环!
- 此后三轮不同之处在于
函数
和s
。
- 四个非线性函数:
- 将最终的临时变量的值与初始变量的值相加,得到新的A、B、C和D的值。
- 输出结果:最后,将A、B、C和D的值按照小端序连接起来,形成一个128位(16字节)的哈希值。这就是MD5算法的输出结果。
小端序(Little-endian)是一种字节序排列方式,用于表示多字节数据在存储器中的顺序。在小端序中,较低有效字节(低位字节)存储在内存的较低地址,而较高有效字节(高位字节)存储在内存的较高地址。
Python中,hashlib库完成:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import hashlib
# 定义要计算MD5哈希的数据
data = "Hello, World!"
# 创建MD5哈希对象
md5_hash = hashlib.md5()
# 更新哈希对象的数据
md5_hash.update(data.encode('utf-8'))
# 计算哈希值
hash_value = md5_hash.hexdigest()
# 打印哈希值
print(hash_value)
JavaScript中:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const crypto = require('crypto');
// 定义要计算MD5哈希的数据
const data = "Hello, World!";
// 创建MD5哈希对象
const md5Hash = crypto.createHash('md5');
// 更新哈希对象的数据
md5Hash.update(data, 'utf-8');
// 计算哈希值
const hashValue = md5Hash.digest('hex');
// 打印哈希值
console.log(hashValue);
SHA-1算法
算法流程:
- 初始化变量:SHA-1使用一组初始变量,称为哈希值(也称为中间哈希结果),共有5个32位的字,分别记为
h0
、h1
、h2
、h3
和h4
。 - 填充数据:将输入数据进行填充,使其长度满足对512位(64字节)块的整数倍。填充方式是在数据末尾添加一个1比特,然后添加足够的零比特,以填充剩余的空间,并在最后添加一个64位的表示原始数据长度的值。
- 划分为块:将填充后的数据划分为多个512位(64字节)的数据块。
- 处理每个块:对于每个512位的数据块,进行以下操作:
- 将块分为16个32位的子块。
- 初始化一个数组
w
,用于存储80个32位的字。 - 通过扩展机制,从子块中生成额外的64个字,填充到数组
w
中。- 将16个子块(32位)复制到数组
w
的前16个位置。 - 对于
i
从16到79的每个值,进行以下操作:- 通过进行位操作(如循环左移、异或等),从
w[i-3]
、w[i-8]
、w[i-14]
和w[i-16]
生成新的字w[i]
。 W(t) = S^1(W(t-3) XOR W(t-8) XOR W(t-14) XOR W(t-16))
- 这里的
t
表示当前的索引值(从0开始),W(t)
表示在数组w
中的第t
个字,XOR
表示按位异或操作,S^1
表示循环左移1位。
- 通过进行位操作(如循环左移、异或等),从
- 将16个子块(32位)复制到数组
- 进行80轮循环,每轮循环包括4个操作步骤,根据一定的位操作和非线性函数,更新
w
中的字。- 每轮循环:
- TEMP = S^5(A) + f(t;B,C,D) + E + W(t) + K(t);
- f(t;B,C,D)函数:
- f(t;B,C,D) = (B AND C) OR ((NOT B) AND D) ( 0 <= t <= 19)
- f(t;B,C,D) = B XOR C XOR D (20 <= t <= 39)
- f(t;B,C,D) = (B AND C) OR (B AND D) OR (C AND D) (40 <= t <= 59)
- f(t;B,C,D) = B XOR C XOR D (60 <= t <= 79)
- W(t)定义如上;
- K(t)函数:
- K(t) = 0x5A827999 ( 0 <= t <= 19)
- K(t) = 0x6ED9EBA1 (20 <= t <= 39)
- K(t) = 0x8F1BBCDC (40 <= t <= 59)
- K(t) = 0xCA62C1D6 (60 <= t <= 79)
- E = D;
- D = C;
- C = S^30(B);
- B = A;
- A = TEMP;
- 根据
w
中的字和当前的哈希值,进行一系列的位操作和运算,更新哈希值。
- 根据
- 每轮循环:
- 输出结果:最后,将最终的哈希值(
h0
、h1
、h2
、h3
和h4
)按照大端序(Big-endian)连接起来,形成一个160位(20字节)的哈希值。这就是SHA-1算法的输出结果。
在python中:1
2
3
4
5
6
7
8
9
10
11import hashlib
def calculate_sha1(data):
sha1_hash = hashlib.sha1()
sha1_hash.update(data.encode('utf-8')) # 将字符串编码为字节流并更新哈希对象
sha1_digest = sha1_hash.hexdigest() # 获取十六进制表示的哈希值
return sha1_digest
data = 'Hello, World!'
sha1 = calculate_sha1(data)
print(sha1)
发现规律了吗?hashlib
库在Python中提供了多种哈希算法的支持,除了SHA-1之外,以下是一些hashlib
支持的其他常用哈希算法:
- SHA-224:SHA-224是SHA-2系列中的一种哈希算法,生成224位(28字节)的哈希值。
- SHA-256:SHA-256是SHA-2系列中的一种哈希算法,生成256位(32字节)的哈希值。
- SHA-384:SHA-384是SHA-2系列中的一种哈希算法,生成384位(48字节)的哈希值。
- SHA-512:SHA-512是SHA-2系列中的一种哈希算法,生成512位(64字节)的哈希值。
- SHA-3系列:
hashlib
库也支持SHA-3系列的哈希算法,如SHA3-224、SHA3-256、SHA3-384和SHA3-512等。 - MD5:MD5是一种广泛使用的哈希算法,生成128位(16字节)的哈希值。然而,MD5已经不再被推荐在安全敏感的场景中使用。
- BLAKE2系列:BLAKE2是一系列高性能哈希函数,包括BLAKE2b和BLAKE2s等变种。它们提供了不同的输出长度和速度/安全性权衡。
你可以使用hashlib.new(algorithm)
方法来创建特定算法的哈希对象,其中algorithm
参数指定算法名称(例如,”sha256”、”md5”等)。然后,使用.update(data)
方法向哈希对象提供数据,并使用.hexdigest()
方法获取十六进制表示的哈希值。
在JavaScript中:1
2
3
4
5
6
7
8
9
10
11
12const crypto = require('crypto');
function calculateSHA1(data) {
const sha1Hash = crypto.createHash('sha1');
sha1Hash.update(data);
const sha1Digest = sha1Hash.digest('hex');
return sha1Digest;
}
const data = 'Hello, World!';
const sha1 = calculateSHA1(data);
console.log(sha1);
同样,也支持多种其他哈希算法
- SHA-256:
crypto.createHash('sha256')
- SHA-512:
crypto.createHash('sha512')
- MD5:
crypto.createHash('md5')
- SHA-3-256:
crypto.createHash('sha3-256')
- SHA-3-512:
crypto.createHash('sha3-512')
- RIPEMD-160:
crypto.createHash('ripemd160')
- Whirlpool:
crypto.createHash('whirlpool')
- Blake2b:
crypto.createHash('blake2b')
- Blake2s:
crypto.createHash('blake2s')
你可以使用以上名称作为crypto.createHash()
方法的参数来创建相应的哈希对象。然后,你可以使用.update()
方法将数据添加到哈希对象中,并使用.digest()
方法获取最终的哈希值。
拓展
加盐(Salting)和迭代(Iteration)是在密码哈希过程中使用的两种技术,旨在增强密码的安全性。
- 加盐(Salting):加盐是在密码哈希过程中引入一个随机的盐值(salt),将其与密码进行组合,然后再进行哈希计算。盐值是一个随机字符串,每个用户的盐值都是唯一的。通过将盐值添加到密码中,即使相同的密码在哈希过程中生成了相同的哈希值,但由于不同用户使用不同的盐值,最终的哈希值也会不同。这种技术可以防止使用彩虹表等预先计算的攻击手段进行密码破解。
- 迭代(Iteration):迭代是在哈希计算过程中进行多次重复操作。通过多次迭代哈希算法,可以增加破解密码所需的计算成本。迭代的次数越多,破解者需要投入的计算资源就越多。迭代次数应该根据计算资源和应用的性能要求进行平衡。增加迭代次数可以有效抵御暴力破解和大规模计算资源的攻击。
加盐和迭代通常一起使用,以提高密码哈希的安全性。在存储密码时,应该为每个用户生成一个随机的盐值,并将其与用户的密码组合后进行哈希计算。然后,将盐值和哈希值一起存储在数据库中。当用户验证密码时,再次使用相同的盐值和迭代次数对输入的密码进行哈希计算,并与存储的哈希值进行比较,以验证密码的正确性。
对称加密算法
对称加密算法指的是使用相同的密钥(称为密钥)进行加密和解密过程。在对称加密算法中,发送方使用密钥将明文数据加密成密文,接收方使用相同的密钥将密文解密回明文。
DES
基本流程:
- 密钥生成:从输入的密钥中生成16个子密钥,每个子密钥为48位。这些子密钥用于加密和解密过程中的轮函数。
- 初始置换(Initial Permutation):将输入的64位明文数据进行初始置换,重新排列数据位的顺序。
- 加密/解密轮函数(Encryption/Decryption Round Function):DES算法使用16个轮函数,每个轮函数的操作包括以下步骤:
- 扩展置换(Expansion Permutation):将32位数据扩展为48位,以便与子密钥进行异或运算。
- 子密钥与数据的异或运算:将扩展后的数据与当前轮的子密钥进行异或运算。
- S盒替换(S-Box Substitution):将48位数据分成8组,每组6位。通过8个S盒(每个S盒为4x16的置换表),将每组6位数据映射为4位输出。
- P置换(Permutation):对S盒输出进行P置换,重新排列4位数据的顺序。
- 轮函数输出:经过上述操作后,得到32位的轮函数输出。
- 轮交换(Round Swap):经过16个轮函数后,将最后一轮的左右32位数据交换位置。
- 逆初始置换(Final Permutation):对交换后的数据进行逆初始置换,恢复数据位的顺序。
Python中:1
2
3
4
5
6
7
8
9
10
11
12
13
14from Crypto.Cipher import DES
# 创建DES对象并指定密钥
key = b'abcdefgh' # 密钥长度必须是8个字节
cipher = DES.new(key, DES.MODE_ECB) # 使用ECB模式
# 加密
plaintext = b'Hello, DES!'
ciphertext = cipher.encrypt(plaintext)
print("加密后的密文:", ciphertext)
# 解密
decrypted = cipher.decrypt(ciphertext)
print("解密后的明文:", decrypted)
然而Crypto这个库装起来总是出错,另一方法是:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
import os
def des_encrypt(key, plaintext):
backend = default_backend()
iv = os.urandom(8) # 生成随机的初始向量
cipher = Cipher(algorithms.TripleDES(key), modes.CBC(iv), backend=backend)
encryptor = cipher.encryptor()
padder = padding.PKCS7(64).padder() # 使用PKCS7填充
padded_plaintext = padder.update(plaintext) + padder.finalize()
ciphertext = encryptor.update(padded_plaintext) + encryptor.finalize()
return iv + ciphertext
def des_decrypt(key, ciphertext):
backend = default_backend()
iv = ciphertext[:8]
cipher = Cipher(algorithms.TripleDES(key), modes.CBC(iv), backend=backend)
decryptor = cipher.decryptor()
unpadder = padding.PKCS7(64).unpadder()
padded_plaintext = decryptor.update(ciphertext[8:]) + decryptor.finalize()
plaintext = unpadder.update(padded_plaintext) + unpadder.finalize()
return plaintext
# 示例用法
key = b'sixteen byte key'
plaintext = b'This is a secret message'
ciphertext = des_encrypt(key, plaintext)
print("Ciphertext:", ciphertext)
decrypted_text = des_decrypt(key, ciphertext)
print("Decrypted text:", decrypted_text.decode())
JavaScript中:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36// 使用CryptoJS库(需要先引入CryptoJS库)
const crypto = require('crypto');
function desEncrypt(key, data) {
const cipher = crypto.createCipheriv('des-ecb', key, null);
let encrypted = cipher.update(data, 'utf8', 'hex');
encrypted += cipher.final('hex');
return encrypted;
}
function desDecrypt(key, encryptedData) {
const decipher = crypto.createDecipheriv('des-ecb', key, null);
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// 示例用法
const key = Buffer.from('abcdefgh', 'utf8'); // 8字节的密钥
const data = 'Hello, World!';
const encryptedData = desEncrypt(key, data);
const decryptedData = desDecrypt(key, encryptedData);
console.log('Encrypted data:', encryptedData);
console.log('Decrypted data:', decryptedData);
AES
基本流程:
- 密钥扩展(Key Expansion):
- 输入密钥(128位、192位或256位)被分成一系列的字(每个字为32位)。
- 通过密钥调度算法,根据输入密钥生成轮密钥(Round Keys)。这些轮密钥用于每轮的轮函数中。
- 初始轮密钥生成:
- 将输入的密钥拆分成一系列字(每个字为32位)。
- 这些字直接用作初始的轮密钥。
- 轮密钥扩展:
- 对于每个轮密钥的生成,算法执行以下步骤:
- 从上一轮的轮密钥中获取最后一个字,并进行特定的处理。
- 如果该字是4的倍数(0、4、8等),则进行密钥计算,涉及字的旋转、S盒替代和与轮常数的异或运算。
- 否则,如果该字不是4的倍数,则进行简单的字异或运算。
- 生成的字作为当前轮的轮密钥,并用于轮函数中的轮密钥加操作。
- 对于每个轮密钥的生成,算法执行以下步骤:
- 轮常数(Round Constants):
- 在密钥扩展过程中,每一轮都使用一个轮常数与某些字进行异或运算,以增加密钥的变化性。
- 轮常数是预定义的常量,与轮的顺序对应。
- 初始轮密钥生成:
- 初始轮(Initial Round):
- 将输入的明文数据(128位)与第一轮的轮密钥进行异或运算。
- 轮函数(Round Function):
- 字节替代(SubBytes):
- 将输入数据的每个字节(8位)通过一个称为S盒(Substitution Box)的查找表进行替代。
- S盒将每个输入字节映射到一个特定的输出字节,增加混淆效果。
- 行位移(ShiftRows):
- 对输入数据的每一行进行循环左移操作,以增加数据的扩散性。
- 第一行不进行位移操作,第二行左移一位,第三行左移两位,第四行左移三位。
- 列混淆(MixColumns):
- 对输入数据的每一列进行矩阵变换,通过一系列乘法和加法操作,增加数据的扩散性。
- 此操作使得每个字节的变化影响整个列。
- 轮密钥加(AddRoundKey):
- 将轮密钥与上一轮输出的数据进行逐位异或运算。
- 字节替代(SubBytes):
- 重复轮函数(Rounds):
- 根据AES密钥长度的不同,重复执行轮函数的步骤。一般来说,128位密钥执行9轮,192位或256位密钥执行11轮。
- 最后一轮(Final Round):
- 字节替代(SubBytes)
- 行位移(ShiftRows)
- 轮密钥加(AddRoundKey)
- 输出:
- 最终经过所有轮函数处理后的数据作为密文数据输出。
解密过程与加密过程相反,使用相同的密钥和相反顺序的操作来恢复明文数据。
- 最终经过所有轮函数处理后的数据作为密文数据输出。
Python中:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
def aes_encrypt(key, plaintext):
backend = default_backend()
iv = os.urandom(16) # 生成随机的初始向量
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=backend)
encryptor = cipher.encryptor()
padder = padding.PKCS7(128).padder() # 使用PKCS7填充
padded_plaintext = padder.update(plaintext) + padder.finalize()
ciphertext = encryptor.update(padded_plaintext) + encryptor.finalize()
return iv + ciphertext
def aes_decrypt(key, ciphertext):
backend = default_backend()
iv = ciphertext[:16]
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=backend)
decryptor = cipher.decryptor()
unpadder = padding.PKCS7(128).unpadder()
padded_plaintext = decryptor.update(ciphertext[16:]) + decryptor.finalize()
plaintext = unpadder.update(padded_plaintext) + unpadder.finalize()
return plaintext
# 示例用法
key = b'sixteen byte key'
plaintext = b'This is a secret message'
ciphertext = aes_encrypt(key, plaintext)
print("Ciphertext:", ciphertext)
decrypted_text = aes_decrypt(key, ciphertext)
print("Decrypted text:", decrypted_text.decode())
Javascript中:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const CryptoJS = require("crypto-js");
// 要加密的数据
var data = "Hello, AES!";
// 生成一个随机的密钥
var key = CryptoJS.enc.Utf8.parse(CryptoJS.lib.WordArray.random(16));
// 加密数据
var encrypted_data = CryptoJS.AES.encrypt(data, key).toString();
console.log("加密后的数据:", encrypted_data);
// 解密数据
var decrypted_data = CryptoJS.AES.decrypt(encrypted_data, key).toString(CryptoJS.enc.Utf8);
console.log("解密后的数据:", decrypted_data);