如何在 Python 中解密 OpenSSL AES 加密文件?
- 2025-02-27 09:07:00
- admin 原创
- 70
问题描述:
OpenSSL 为 AES 加密提供了一个流行(但不安全 - 见下文!)的命令行界面:
openssl aes-256-cbc -salt -in filename -out filename.enc
Python 以 PyCrypto 包的形式支持 AES,但它仅提供工具。如何使用 Python/PyCrypto 解密使用 OpenSSL 加密的文件?
注意
这个问题以前也涉及使用相同方案的 Python 加密。我已经删除了该部分以阻止任何人使用它。不要再以这种方式加密任何数据,因为按照今天的标准它并不安全。您应该只使用解密,除了向后兼容之外没有其他原因,即当您别无选择时。想要加密?如果可能的话,使用 NaCl/libsodium。
解决方案 1:
鉴于 Python 的流行,一开始我很失望,因为这个问题没有完整的答案。我花了很多时间阅读这个论坛上的不同答案以及其他资源,才找到正确的答案。我想我可以分享结果以供将来参考和审查;我绝不是密码学专家!然而,下面的代码似乎可以无缝运行:
from hashlib import md5
from Crypto.Cipher import AES
from Crypto import Random
def derive_key_and_iv(password, salt, key_length, iv_length):
d = d_i = ''
while len(d) < key_length + iv_length:
d_i = md5(d_i + password + salt).digest()
d += d_i
return d[:key_length], d[key_length:key_length+iv_length]
def decrypt(in_file, out_file, password, key_length=32):
bs = AES.block_size
salt = in_file.read(bs)[len('Salted__'):]
key, iv = derive_key_and_iv(password, salt, key_length, bs)
cipher = AES.new(key, AES.MODE_CBC, iv)
next_chunk = ''
finished = False
while not finished:
chunk, next_chunk = next_chunk, cipher.decrypt(in_file.read(1024 * bs))
if len(next_chunk) == 0:
padding_length = ord(chunk[-1])
chunk = chunk[:-padding_length]
finished = True
out_file.write(chunk)
用法:
with open(in_filename, 'rb') as in_file, open(out_filename, 'wb') as out_file:
decrypt(in_file, out_file, password)
如果您发现有机会对其进行改进或者扩展,使其更加灵活(例如,使其在没有盐的情况下工作,或者提供 Python 3 兼容性),请随意这样做。
注意
这个答案过去也涉及使用相同方案的 Python 加密。我已经删除了该部分以阻止任何人使用它。不要再以这种方式加密任何数据,因为按照今天的标准它并不安全。您应该只使用解密,除了向后兼容之外没有其他原因,即当您别无选择时。想要加密?如果可能的话,使用 NaCl/libsodium。
解决方案 2:
我正在重新发布您的代码,并进行了一些更正(我不想掩盖您的版本)。虽然您的代码可以工作,但它无法检测到填充周围的一些错误。特别是,如果提供的解密密钥不正确,您的填充逻辑可能会出现一些奇怪的情况。如果您同意我的更改,您可以更新您的解决方案。
from hashlib import md5
from Crypto.Cipher import AES
from Crypto import Random
def derive_key_and_iv(password, salt, key_length, iv_length):
d = d_i = ''
while len(d) < key_length + iv_length:
d_i = md5(d_i + password + salt).digest()
d += d_i
return d[:key_length], d[key_length:key_length+iv_length]
# This encryption mode is no longer secure by today's standards.
# See note in original question above.
def obsolete_encrypt(in_file, out_file, password, key_length=32):
bs = AES.block_size
salt = Random.new().read(bs - len('Salted__'))
key, iv = derive_key_and_iv(password, salt, key_length, bs)
cipher = AES.new(key, AES.MODE_CBC, iv)
out_file.write('Salted__' + salt)
finished = False
while not finished:
chunk = in_file.read(1024 * bs)
if len(chunk) == 0 or len(chunk) % bs != 0:
padding_length = bs - (len(chunk) % bs)
chunk += padding_length * chr(padding_length)
finished = True
out_file.write(cipher.encrypt(chunk))
def decrypt(in_file, out_file, password, key_length=32):
bs = AES.block_size
salt = in_file.read(bs)[len('Salted__'):]
key, iv = derive_key_and_iv(password, salt, key_length, bs)
cipher = AES.new(key, AES.MODE_CBC, iv)
next_chunk = ''
finished = False
while not finished:
chunk, next_chunk = next_chunk, cipher.decrypt(in_file.read(1024 * bs))
if len(next_chunk) == 0:
padding_length = ord(chunk[-1])
if padding_length < 1 or padding_length > bs:
raise ValueError("bad decrypt pad (%d)" % padding_length)
# all the pad-bytes must be the same
if chunk[-padding_length:] != (padding_length * chr(padding_length)):
# this is similar to the bad decrypt:evp_enc.c from openssl program
raise ValueError("bad decrypt")
chunk = chunk[:-padding_length]
finished = True
out_file.write(chunk)
解决方案 3:
以下代码应与 Python 3 兼容,并且代码中记录了细微更改。还想使用 os.urandom 代替 Crypto.Random。'Salted__' 被 salt_header 替换,可以根据需要进行定制或留空。
from os import urandom
from hashlib import md5
from Crypto.Cipher import AES
def derive_key_and_iv(password, salt, key_length, iv_length):
d = d_i = b'' # changed '' to b''
while len(d) < key_length + iv_length:
# changed password to str.encode(password)
d_i = md5(d_i + str.encode(password) + salt).digest()
d += d_i
return d[:key_length], d[key_length:key_length+iv_length]
def encrypt(in_file, out_file, password, salt_header='', key_length=32):
# added salt_header=''
bs = AES.block_size
# replaced Crypt.Random with os.urandom
salt = urandom(bs - len(salt_header))
key, iv = derive_key_and_iv(password, salt, key_length, bs)
cipher = AES.new(key, AES.MODE_CBC, iv)
# changed 'Salted__' to str.encode(salt_header)
out_file.write(str.encode(salt_header) + salt)
finished = False
while not finished:
chunk = in_file.read(1024 * bs)
if len(chunk) == 0 or len(chunk) % bs != 0:
padding_length = (bs - len(chunk) % bs) or bs
# changed right side to str.encode(...)
chunk += str.encode(
padding_length * chr(padding_length))
finished = True
out_file.write(cipher.encrypt(chunk))
def decrypt(in_file, out_file, password, salt_header='', key_length=32):
# added salt_header=''
bs = AES.block_size
# changed 'Salted__' to salt_header
salt = in_file.read(bs)[len(salt_header):]
key, iv = derive_key_and_iv(password, salt, key_length, bs)
cipher = AES.new(key, AES.MODE_CBC, iv)
next_chunk = ''
finished = False
while not finished:
chunk, next_chunk = next_chunk, cipher.decrypt(
in_file.read(1024 * bs))
if len(next_chunk) == 0:
padding_length = chunk[-1] # removed ord(...) as unnecessary
chunk = chunk[:-padding_length]
finished = True
out_file.write(bytes(x for x in chunk)) # changed chunk to bytes(...)
解决方案 4:
这个答案基于 openssl v1.1.1,与以前版本的 openssl 相比,它支持更强大的 AES 加密密钥派生过程。
该答案基于以下命令:
echo -n 'Hello World!' | openssl aes-256-cbc -e -a -salt -pbkdf2 -iter 10000
此命令使用 aes-256-cbc 加密明文“Hello World!”。密钥是使用 pbkdf2 从密码和随机盐中派生出来的,并经过 10,000 次 sha256 哈希迭代。当提示输入密码时,我输入了密码“p4$$w0rd”。该命令生成的密文输出为:
U2FsdGVkX1/Kf8Yo6JjBh+qELWhirAXr78+bbPQjlxE=
openssl对上述密文进行解密的过程如下:
对 openssl 的输出进行 base64 解码,对密码进行 utf-8 解码,这样我们就有了这两个的底层字节。
盐是 base64 解码的 openssl 输出的第 8-15 个字节。
给定密码字节数并使用 10,000 次 sha256 哈希迭代计算盐值,使用 pbkdf2 得出一个 48 字节的密钥。
key 是派生密钥的 0-31 个字节,iv 是派生密钥的 32-47 个字节。
密文是从第 16 个字节到经过 base64 解码的 openssl 输出末尾的字节。
给定密钥、iv 和密文,使用 aes-256-cbc 解密密文。
从明文中删除 PKCS#7 填充。明文的最后一个字节表示附加到明文末尾的填充字节数。这是要删除的字节数。
下面是上述过程的python3实现:
import binascii
import base64
import hashlib
from Crypto.Cipher import AES #requires pycrypto
#inputs
openssloutputb64='U2FsdGVkX1/Kf8Yo6JjBh+qELWhirAXr78+bbPQjlxE='
password='p4$$w0rd'
pbkdf2iterations=10000
#convert inputs to bytes
openssloutputbytes=base64.b64decode(openssloutputb64)
passwordbytes=password.encode('utf-8')
#salt is bytes 8 through 15 of openssloutputbytes
salt=openssloutputbytes[8:16]
#derive a 48-byte key using pbkdf2 given the password and salt with 10,000 iterations of sha256 hashing
derivedkey=hashlib.pbkdf2_hmac('sha256', passwordbytes, salt, pbkdf2iterations, 48)
#key is bytes 0-31 of derivedkey, iv is bytes 32-47 of derivedkey
key=derivedkey[0:32]
iv=derivedkey[32:48]
#ciphertext is bytes 16-end of openssloutputbytes
ciphertext=openssloutputbytes[16:]
#decrypt ciphertext using aes-cbc, given key, iv, and ciphertext
decryptor=AES.new(key, AES.MODE_CBC, iv)
plaintext=decryptor.decrypt(ciphertext)
#remove PKCS#7 padding.
#Last byte of plaintext indicates the number of padding bytes appended to end of plaintext. This is the number of bytes to be removed.
plaintext = plaintext[:-plaintext[-1]]
#output results
print('openssloutputb64:', openssloutputb64)
print('password:', password)
print('salt:', salt.hex())
print('key: ', key.hex())
print('iv: ', iv.hex())
print('ciphertext: ', ciphertext.hex())
print('plaintext: ', plaintext.decode('utf-8'))
正如预期的那样,上面的 python3 脚本生成以下内容:
openssloutputb64: U2FsdGVkX1/Kf8Yo6JjBh+qELWhirAXr78+bbPQjlxE=
password: p4$$w0rd
salt: ca7fc628e898c187
key: 444ab886d5721fc87e58f86f3e7734659007bea7fbe790541d9e73c481d9d983
iv: 7f4597a18096715d7f9830f0125be8fd
ciphertext: ea842d6862ac05ebefcf9b6cf4239711
plaintext: Hello World!
注意:可以在https://github.com/meixler/web-browser-based-file-encryption-decryption找到javascript 中的等效/兼容实现(使用web 加密 api ) 。
解决方案 5:
我知道这有点晚了,但这是我在 2013 年博客中提出的解决方案,关于如何使用 python pycrypto 包以 openssl 兼容的方式加密/解密。它已在 python2.7 和 python3.x 上进行了测试。源代码和测试脚本可以在这里找到。
该解决方案与上面提出的优秀解决方案之间的一个主要区别是,它区分了管道和文件 I/O,这可能会在某些应用程序中导致问题。
该博客中的关键功能如下所示。
# ================================================================
# get_key_and_iv
# ================================================================
def get_key_and_iv(password, salt, klen=32, ilen=16, msgdgst='md5'):
'''
Derive the key and the IV from the given password and salt.
This is a niftier implementation than my direct transliteration of
the C++ code although I modified to support different digests.
CITATION: http://stackoverflow.com/questions/13907841/implement-openssl-aes-encryption-in-python
@param password The password to use as the seed.
@param salt The salt.
@param klen The key length.
@param ilen The initialization vector length.
@param msgdgst The message digest algorithm to use.
'''
# equivalent to:
# from hashlib import <mdi> as mdf
# from hashlib import md5 as mdf
# from hashlib import sha512 as mdf
mdf = getattr(__import__('hashlib', fromlist=[msgdgst]), msgdgst)
password = password.encode('ascii', 'ignore') # convert to ASCII
try:
maxlen = klen + ilen
keyiv = mdf(password + salt).digest()
tmp = [keyiv]
while len(tmp) < maxlen:
tmp.append( mdf(tmp[-1] + password + salt).digest() )
keyiv += tmp[-1] # append the last byte
key = keyiv[:klen]
iv = keyiv[klen:klen+ilen]
return key, iv
except UnicodeDecodeError:
return None, None
# ================================================================
# encrypt
# ================================================================
def encrypt(password, plaintext, chunkit=True, msgdgst='md5'):
'''
Encrypt the plaintext using the password using an openssl
compatible encryption algorithm. It is the same as creating a file
with plaintext contents and running openssl like this:
$ cat plaintext
<plaintext>
$ openssl enc -e -aes-256-cbc -base64 -salt \\
-pass pass:<password> -n plaintext
@param password The password.
@param plaintext The plaintext to encrypt.
@param chunkit Flag that tells encrypt to split the ciphertext
into 64 character (MIME encoded) lines.
This does not affect the decrypt operation.
@param msgdgst The message digest algorithm.
'''
salt = os.urandom(8)
key, iv = get_key_and_iv(password, salt, msgdgst=msgdgst)
if key is None:
return None
# PKCS#7 padding
padding_len = 16 - (len(plaintext) % 16)
if isinstance(plaintext, str):
padded_plaintext = plaintext + (chr(padding_len) * padding_len)
else: # assume bytes
padded_plaintext = plaintext + (bytearray([padding_len] * padding_len))
# Encrypt
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(padded_plaintext)
# Make openssl compatible.
# I first discovered this when I wrote the C++ Cipher class.
# CITATION: http://projects.joelinoff.com/cipher-1.1/doxydocs/html/
openssl_ciphertext = b'Salted__' + salt + ciphertext
b64 = base64.b64encode(openssl_ciphertext)
if not chunkit:
return b64
LINELEN = 64
chunk = lambda s: b'
'.join(s[i:min(i+LINELEN, len(s))]
for i in range(0, len(s), LINELEN))
return chunk(b64)
# ================================================================
# decrypt
# ================================================================
def decrypt(password, ciphertext, msgdgst='md5'):
'''
Decrypt the ciphertext using the password using an openssl
compatible decryption algorithm. It is the same as creating a file
with ciphertext contents and running openssl like this:
$ cat ciphertext
# ENCRYPTED
<ciphertext>
$ egrep -v '^#|^$' | \\
openssl enc -d -aes-256-cbc -base64 -salt -pass pass:<password> -in ciphertext
@param password The password.
@param ciphertext The ciphertext to decrypt.
@param msgdgst The message digest algorithm.
@returns the decrypted data.
'''
# unfilter -- ignore blank lines and comments
if isinstance(ciphertext, str):
filtered = ''
nl = '
'
re1 = r'^s*$'
re2 = r'^s*#'
else:
filtered = b''
nl = b'
'
re1 = b'^\\s*$'
re2 = b'^\\s*#'
for line in ciphertext.split(nl):
line = line.strip()
if re.search(re1,line) or re.search(re2, line):
continue
filtered += line + nl
# Base64 decode
raw = base64.b64decode(filtered)
assert(raw[:8] == b'Salted__' )
salt = raw[8:16] # get the salt
# Now create the key and iv.
key, iv = get_key_and_iv(password, salt, msgdgst=msgdgst)
if key is None:
return None
# The original ciphertext
ciphertext = raw[16:]
# Decrypt
cipher = AES.new(key, AES.MODE_CBC, iv)
padded_plaintext = cipher.decrypt(ciphertext)
if isinstance(padded_plaintext, str):
padding_len = ord(padded_plaintext[-1])
else:
padding_len = padded_plaintext[-1]
plaintext = padded_plaintext[:-padding_len]
return plaintext
解决方案 6:
尝试了上面的所有方法以及其他线程中的一些方法,这是对我有用的方法,相当于 openssl 中的这个:
不是最好的 encrpython,但这些是要求
解密:openssl enc -d -aes256 -md md5 -in {->path_in} -out {->path_out} -pass pass:{->pass}
加密:openssl enc -e -aes256 -md md5 -in {->path_in} -out {->path_out} -pass pass:{->pass}
Python:
from os import urandom
from hashlib import md5
from Crypto.Cipher import AES
import typer
def filecrypto(in_file, out_file, password, decrypt: bool = True):
salt_header = 'Salted__'
def derive_key_and_iv(password, salt, key_length, iv_length):
d = d_i = b'' # changed '' to b''
while len(d) < key_length + iv_length:
# changed password to str.encode(password)
d_i = md5(d_i + str.encode(password) + salt).digest()
d += d_i
return d[:key_length], d[key_length:key_length+iv_length]
def encrypt_f(in_file, out_file, password, salt_header=salt_header, key_length=32):
bs = AES.block_size
salt = urandom(bs - len(salt_header))
key, iv = derive_key_and_iv(password, salt, key_length, bs)
cipher = AES.new(key, AES.MODE_CBC, iv)
with open(out_file, 'wb') as f_out:
# write the first line or the salted header
f_out.write(str.encode(salt_header) + salt)
with open(in_file, 'rb') as f_in:
f_out.write(cipher.encrypt(f_in.read()))
def decrypt_f(in_file, out_file, password, salt_header=salt_header, key_length=32):
bs = AES.block_size
with open(in_file, 'rb') as f_in:
# retrieve the salted header
salt = f_in.read(bs)[len(salt_header):]
key, iv = derive_key_and_iv(password, salt, key_length, bs)
cipher = AES.new(key, AES.MODE_CBC, iv)
with open(out_file, 'wb') as f_out:
f_out.write(cipher.decrypt(f_in.read()))
return decrypt_f(in_file, out_file, password) if decrypt else encrypt_f(in_file, out_file, password)
if __name__ == "__filecrypto__":
typer.run(filecrypto)
解决方案 7:
注意:此方法与 OpenSSL 不兼容
但如果您所要做的只是加密和解密文件,它是合适的。
我从这里复制了一个自我回答。我认为这也许是一个更简单、更安全的选择。不过,我对专家关于它有多安全的意见很感兴趣。
我使用Python 3.6和SimpleCrypt加密文件然后上传。
我认为这是我用来加密文件的代码:
from simplecrypt import encrypt, decrypt
f = open('file.csv','r').read()
ciphertext = encrypt('USERPASSWORD',f.encode('utf8')) # I am not certain of whether I used the .encode('utf8')
e = open('file.enc','wb') # file.enc doesn't need to exist, python will create it
e.write(ciphertext)
e.close
这是我在运行时用来解密的代码,我将其getpass("password: ")
作为参数运行,因此不必password
在内存中存储变量
from simplecrypt import encrypt, decrypt
from getpass import getpass
# opens the file
f = open('file.enc','rb').read()
print('Please enter the password and press the enter key
Decryption may take some time')
# Decrypts the data, requires a user-input password
plaintext = decrypt(getpass("password: "), f).decode('utf8')
print('Data have been Decrypted')
请注意,UTF-8 编码行为在 python 2.7 中有所不同,因此代码会略有不同。
扫码咨询,免费领取项目管理大礼包!