python 与 openssl 对应的 AES-CBC
chenzuoqing Lv3

python 实现与 openssl 对应的 AES-CBC

可对应 openssl 的 AES-CBC 加密,参考来源

奇怪的需求又增加了,项目大量使用的 shell 脚本,有需求加密某内容,让脚本解密后使用。一番尝试后,发现 python 中的 AES 还与操作系统中 openssl 工具加解密不对应。又一番面向 Stack Overflow 编程后,调试出了合适的 demo,以下是处理内容。

需要注意的是这种方式不够安全!!!

先安装加密库:

1
pip3 install pycryptodome

对应的 AES-CBC 加密类封装,使用了内存中的 BytesIO,所以别加密太大的内容。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import io
import base64
from os import urandom
from hashlib import md5
from Crypto.Cipher import AES

"""pip3 install pycryptodome"""


class AESCipher:
"""
NOTE 这种方法现在已经不是很安全,只是为了与 `openssl` 命令相兼容,方便脚本解密
使用内存 BytesIO 写入加密、解密内容,模拟文件进行加密、解密,主要用到 AES-CBC 算法,加 salt
参考: https://stackoverflow.com/questions/16761458/how-to-decrypt-openssl-aes-encrypted-files-in-python
"""
block_size = AES.block_size

def __init__(self, content, password, salt_header='Salted__', key_length=32):
"""
注意: 若需要支持 `openssl` 命令,必须指定 salt_header='Salted__'
如果只是 python 内部使用 salt_header 可以为空
"""
self.content = content
self.password = password
self.salt_header = salt_header
self.key_length = key_length

def derive_key_and_iv(self, salt):
"""计算得到 `key` 和 `iv` """
d = d_i = b''
while len(d) < self.key_length + self.block_size:
d_i = md5(d_i + str.encode(self.password) + salt).digest()
d += d_i
return d[:self.key_length], d[self.key_length:self.key_length + self.block_size]

def encrypt(self) -> bytes:
"""
返回的是加密后的 `base64` 二进制字符串,bytes
对应 `openssl` 命令解法:
从文件:openssl aes-256-cbc -salt -in secret.txt -d -a -k 'password'
从输入:echo "U2FsdGVkX1/5sFe6z6+H4CfQvnTZgCEV4yget0PI8XM=" | openssl aes-256-cbc -salt -d -a -k 'password'
"""
content = self.content.encode() if not isinstance(self.content, bytes) else self.content
# 字节IO模拟文件
in_file = io.BytesIO(content)
out_file = io.BytesIO()
salt = urandom(self.block_size - len(self.salt_header))
key, iv = self.derive_key_and_iv(salt)
cipher = AES.new(key, AES.MODE_CBC, iv)
out_file.write(str.encode(self.salt_header) + salt)
finished = False
while not finished:
chunk = in_file.read(1024 * self.block_size)
if len(chunk) == 0 or len(chunk) % self.block_size != 0:
padding_length = (self.block_size - len(chunk) % self.block_size) or self.block_size
chunk += str.encode(
padding_length * chr(padding_length))
finished = True
out_file.write(cipher.encrypt(chunk))
return base64.b64encode(out_file.getvalue())

def decrypt(self, content=None) -> bytes:
"""
密文 content 可以传入,使用实例对象的密码,返回解密后的明文 bytes
错误的密码、salt_header、key_size解密将报错
openssl 加密命令
从文件:openssl aes-256-cbc -salt -in text.txt -a -k 'password'
从输入:echo "abc" | openssl aes-256-cbc -salt -a -k 'password'
"""
content = content if content else self.content
content = content.encode() if not isinstance(content, bytes) else content
text = base64.b64decode(content)
# 字节IO模拟文件
in_file = io.BytesIO(text)
out_file = io.BytesIO()
salt = in_file.read(self.block_size)[len(self.salt_header):]
key, iv = self.derive_key_and_iv(salt)
cipher = AES.new(key, AES.MODE_CBC, iv)
next_chunk = b''
finished = False
while not finished:
chunk, next_chunk = next_chunk, cipher.decrypt(in_file.read(1024 * self.block_size))
if len(next_chunk) == 0:
padding_length = chunk[-1]
if padding_length < 1 or padding_length > self.block_size:
# 触发此错误原因可能是密码错误
raise ValueError("bad decrypt pad (%d)" % padding_length)
chunk = chunk[:-padding_length]
finished = True
out_file.write(chunk)
# 若为空可能是密码错误了,或者触发上面的异常
return out_file.getvalue()


if __name__ == '__main__':
print("++++++++++++++++++++++++++++++++++++++++++")
aes = AESCipher("hello world2223", '111111')
secret = aes.encrypt()
raw = aes.decrypt(secret)
print("密文:", secret)
print("明文:", raw)
print("++++++++++++++++++++++++++++++++++++++++++")
try:
# 错误的密码解密将报错
aes2 = AESCipher("", '222', salt_header='')
res = aes2.decrypt(secret)
print(res)
except ValueError:
print("密码错了!")

 Comments