当我们学习使用爬虫时,我们发现大多数网站的动态数据都采用了加密的方式。通过源代码中进行调试,我们可以“扣”出执行加密的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,字母B0x42,以此类推;

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 处理:

  1. ASCII 编码:

    • 使用 ord() 函数将字符转换为对应的 ASCII 码值。
    • 使用 chr() 函数将 ASCII 码值转换为对应的字符。
      1
      2
      3
      4
      5
      6
      char = 'A'
      ascii_value = ord(char)
      print(ascii_value) # 输出:65

      char = chr(65)
      print(char) # 输出:A
  2. Unicode 编码:

    • 使用 encode() 方法将字符串编码为指定的 Unicode 编码格式。
    • 使用 decode() 方法将 Unicode 编码转换为字符串。
      1
      2
      3
      4
      5
      6
      string = '你好'
      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 处理:

  1. ASCII 编码:

    • 使用 charCodeAt() 方法获取字符串中指定位置字符的 ASCII 码值。
    • 使用 String.fromCharCode() 方法将 ASCII 码值转换为对应的字符。
      1
      2
      3
      4
      5
      6
      var char = 'A';
      var asciiValue = char.charCodeAt(0);
      console.log(asciiValue); // 输出:65

      var char = String.fromCharCode(65);
      console.log(char); // 输出:A
  2. Unicode 编码:

    • 使用 \u 前缀将 Unicode 字符码直接插入字符串中。
    • 使用 String.fromCharCode() 方法将 Unicode 字符码转换为对应的字符。
      1
      2
      3
      4
      5
      var 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~Za~z0~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
10
import 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~Za~z0~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
10
import 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)的算法。哈希值通常是一个较短的固定大小的字节数组,用于表示原始数据。

哈希算法的主要特点是:

  1. 固定输出长度:不论输入数据的大小,哈希算法都会生成一个固定长度的哈希值。常见的哈希长度有128位、256位或更长。
  2. 独特性:对于不同的输入数据,哈希算法应该生成不同的哈希值。这意味着即使输入数据发生细微的改变,生成的哈希值也会有较大的差异。
  3. 不可逆性:从哈希值无法还原出原始数据。即使对于稍微不同的输入,其哈希值也应该是完全不同的。
  4. 高效性:计算哈希值的速度应该很快,即使对于大量的数据也应该在合理的时间内完成。

哈希碰撞: 尽管哈希算法的目标是生成唯一的哈希值,但在实际应用中,由于输入数据的无限性哈希值的有限性,哈希碰撞是可能发生的。哈希算法的输出空间是固定的,而输入空间则是无穷大的,因此存在多个不同的输入数据可能会映射到相同的哈希值上。

对于一个安全的哈希算法而言, 他必须要 1)碰撞概率低;2)不能推测出输出。碰撞概率高增需要加长输出字节,而不能推测输出指的是输出不能看出任何规律。

常见的哈希算法

MD5算法

算法流程:

  1. 填充数据:将输入数据进行填充,使其长度满足对512位(64字节)块的整数倍。填充方式是在数据末尾添加一个1比特,然后添加足够的零比特,以填充剩余的空间,并在最后添加一个64位的,表示原始数据长度的,值。即令其位长对512求余的结果等于448。
  2. 划分为块:将填充后的数据划分为多个512位(64字节)的数据块。
  3. 初始化变量:设置四个32位的初始变量A、B、C和D。这些变量作为中间计算结果的存储器。
  4. 处理每个块:对于每个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的值。
  5. 输出结果:最后,将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
16
import 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
16
const 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算法

算法流程:

  1. 初始化变量:SHA-1使用一组初始变量,称为哈希值(也称为中间哈希结果),共有5个32位的字,分别记为h0h1h2h3h4
  2. 填充数据:将输入数据进行填充,使其长度满足对512位(64字节)块的整数倍。填充方式是在数据末尾添加一个1比特,然后添加足够的零比特,以填充剩余的空间,并在最后添加一个64位的表示原始数据长度的值。
  3. 划分为块:将填充后的数据划分为多个512位(64字节)的数据块。
  4. 处理每个块:对于每个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位。
    • 进行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中的字和当前的哈希值,进行一系列的位操作和运算,更新哈希值。
  5. 输出结果:最后,将最终的哈希值(h0h1h2h3h4)按照大端序(Big-endian)连接起来,形成一个160位(20字节)的哈希值。这就是SHA-1算法的输出结果。

在python中:

1
2
3
4
5
6
7
8
9
10
11
import 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
12
const 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)是在密码哈希过程中使用的两种技术,旨在增强密码的安全性。

  1. 加盐(Salting):加盐是在密码哈希过程中引入一个随机的盐值(salt),将其与密码进行组合,然后再进行哈希计算。盐值是一个随机字符串,每个用户的盐值都是唯一的。通过将盐值添加到密码中,即使相同的密码在哈希过程中生成了相同的哈希值,但由于不同用户使用不同的盐值,最终的哈希值也会不同。这种技术可以防止使用彩虹表等预先计算的攻击手段进行密码破解。
  2. 迭代(Iteration):迭代是在哈希计算过程中进行多次重复操作。通过多次迭代哈希算法,可以增加破解密码所需的计算成本。迭代的次数越多,破解者需要投入的计算资源就越多。迭代次数应该根据计算资源和应用的性能要求进行平衡。增加迭代次数可以有效抵御暴力破解和大规模计算资源的攻击。
    加盐和迭代通常一起使用,以提高密码哈希的安全性。在存储密码时,应该为每个用户生成一个随机的盐值,并将其与用户的密码组合后进行哈希计算。然后,将盐值和哈希值一起存储在数据库中。当用户验证密码时,再次使用相同的盐值和迭代次数对输入的密码进行哈希计算,并与存储的哈希值进行比较,以验证密码的正确性。

对称加密算法

对称加密算法指的是使用相同的密钥(称为密钥)进行加密和解密过程。在对称加密算法中,发送方使用密钥将明文数据加密成密文,接收方使用相同的密钥将密文解密回明文。

DES

基本流程:

  1. 密钥生成:从输入的密钥中生成16个子密钥,每个子密钥为48位。这些子密钥用于加密和解密过程中的轮函数。
  2. 初始置换(Initial Permutation):将输入的64位明文数据进行初始置换,重新排列数据位的顺序。
  3. 加密/解密轮函数(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位的轮函数输出。
  4. 轮交换(Round Swap):经过16个轮函数后,将最后一轮的左右32位数据交换位置。
  5. 逆初始置换(Final Permutation):对交换后的数据进行逆初始置换,恢复数据位的顺序。

Python中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from 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
34
from 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

基本流程:

  1. 密钥扩展(Key Expansion):
    • 输入密钥(128位、192位或256位)被分成一系列的字(每个字为32位)。
    • 通过密钥调度算法,根据输入密钥生成轮密钥(Round Keys)。这些轮密钥用于每轮的轮函数中。
      • 初始轮密钥生成:
        • 将输入的密钥拆分成一系列字(每个字为32位)。
        • 这些字直接用作初始的轮密钥。
      • 轮密钥扩展:
        • 对于每个轮密钥的生成,算法执行以下步骤:
          • 从上一轮的轮密钥中获取最后一个字,并进行特定的处理。
          • 如果该字是4的倍数(0、4、8等),则进行密钥计算,涉及字的旋转、S盒替代和与轮常数的异或运算。
          • 否则,如果该字不是4的倍数,则进行简单的字异或运算。
          • 生成的字作为当前轮的轮密钥,并用于轮函数中的轮密钥加操作。
      • 轮常数(Round Constants):
        • 在密钥扩展过程中,每一轮都使用一个轮常数与某些字进行异或运算,以增加密钥的变化性。
        • 轮常数是预定义的常量,与轮的顺序对应。
  2. 初始轮(Initial Round):
    • 将输入的明文数据(128位)与第一轮的轮密钥进行异或运算。
  3. 轮函数(Round Function):
    • 字节替代(SubBytes):
      • 将输入数据的每个字节(8位)通过一个称为S盒(Substitution Box)的查找表进行替代。
      • S盒将每个输入字节映射到一个特定的输出字节,增加混淆效果。
    • 行位移(ShiftRows):
      • 对输入数据的每一行进行循环左移操作,以增加数据的扩散性。
      • 第一行不进行位移操作,第二行左移一位,第三行左移两位,第四行左移三位。
    • 列混淆(MixColumns):
      • 对输入数据的每一列进行矩阵变换,通过一系列乘法和加法操作,增加数据的扩散性。
      • 此操作使得每个字节的变化影响整个列。
    • 轮密钥加(AddRoundKey):
      • 将轮密钥与上一轮输出的数据进行逐位异或运算。
  4. 重复轮函数(Rounds):
    • 根据AES密钥长度的不同,重复执行轮函数的步骤。一般来说,128位密钥执行9轮,192位或256位密钥执行11轮。
  5. 最后一轮(Final Round):
    • 字节替代(SubBytes)
    • 行位移(ShiftRows)
    • 轮密钥加(AddRoundKey)
  6. 输出:
    • 最终经过所有轮函数处理后的数据作为密文数据输出。
      解密过程与加密过程相反,使用相同的密钥和相反顺序的操作来恢复明文数据。

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
33
from 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
15
const 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);