如何在 Python 中解密 OpenSSL AES 加密的文件?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 
原文地址: http://stackoverflow.com/questions/16761458/
Warning: these are provided under cc-by-sa 4.0 license.  You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to decrypt OpenSSL AES-encrypted files in Python?
提问by Thijs van Dien
OpenSSL provides a popular (but insecure – see below!) command line interface for AES encryption:
OpenSSL 为 AES 加密提供了一个流行的(但不安全 - 见下文!)命令行界面:
openssl aes-256-cbc -salt -in filename -out filename.enc
Python has support for AES in the shape of the PyCrypto package, but it only provides the tools. How to use Python/PyCrypto to decrypt files that have been encrypted using OpenSSL?
Python 以 PyCrypto 包的形式支持 AES,但它只提供工具。如何使用 Python/PyCrypto 解密使用 OpenSSL 加密的文件?
Notice
注意
This question used to also concern encryption in Python using the same scheme. I have since removed that part to discourage anyone from using it. Do NOT encrypt any more data in this way, because it is NOT secure by today's standards. You should ONLY use decryption, for no other reasons than BACKWARD COMPATIBILITY, i.e. when you have no other choice. Want to encrypt? Use NaCl/libsodium if you possibly can.
这个问题过去也涉及使用相同方案的 Python 加密。从那以后,我删除了该部分以阻止任何人使用它。不要再以这种方式加密任何数据,因为按照今天的标准,它是不安全的。您应该只使用解密,除了向后兼容性之外别无其他原因,即当您别无选择时。想加密?如果可能,请使用 NaCl/libsodium。
采纳答案by Thijs van Dien
Given the popularity of Python, at first I was disappointed that there was no complete answer to this question to be found. It took me a fair amount of reading different answers on this board, as well as other resources, to get it right. I thought I might share the result for future reference and perhaps review; I'm by no means a cryptography expert! However, the code below appears to work seamlessly:
鉴于 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)
Usage:
用法:
with open(in_filename, 'rb') as in_file, open(out_filename, 'wb') as out_file:
    decrypt(in_file, out_file, password)
If you see a chance to improve on this or extend it to be more flexible (e.g. make it work without salt, or provide Python 3 compatibility), please feel free to do so.
如果您看到有机会对此进行改进或将其扩展为更灵活(例如,让它在没有盐的情况下工作,或提供 Python 3 兼容性),请随时这样做。
Notice
注意
This answer used to also concern encryption in Python using the same scheme. I have since removed that part to discourage anyone from using it. Do NOT encrypt any more data in this way, because it is NOT secure by today's standards. You should ONLY use decryption, for no other reasons than BACKWARD COMPATIBILITY, i.e. when you have no other choice. Want to encrypt? Use NaCl/libsodium if you possibly can.
这个答案过去也涉及使用相同方案的 Python 加密。从那以后,我删除了该部分以阻止任何人使用它。不要再以这种方式加密任何数据,因为按照今天的标准,它是不安全的。您应该只使用解密,除了向后兼容性之外别无其他原因,即当您别无选择时。想加密?如果可能,请使用 NaCl/libsodium。
回答by Gregor
I am re-posting your code with a couple of corrections (I didn't want to obscure your version). While your code works, it does not detect some errors around padding. In particular, if the decryption key provided is incorrect, your padding logic may do something odd. If you agree with my change, you may update your solution.
我正在重新发布您的代码并进行一些更正(我不想掩盖您的版本)。当您的代码工作时,它不会检测到一些围绕填充的错误。特别是,如果提供的解密密钥不正确,您的填充逻辑可能会做一些奇怪的事情。如果您同意我的更改,您可以更新您的解决方案。
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)
回答by Johnny Booy
The code below should be Python 3 compatible with the small changes documented in the code. Also wanted to use os.urandom instead of Crypto.Random. 'Salted__' is replaced with salt_header that can be tailored or left empty if needed.
下面的代码应该是 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(...)
回答by Joe Linoff
I know this is a bit late but hereis a solution that I blogged in 2013 about how to use the python pycrypto package to encrypt/decrypt in an openssl compatible way. It has been tested on python2.7 and python3.x. The source code and a test script can be found here.
我知道这有点晚了,但这里有一个解决方案,我在 2013 年发表了一篇关于如何使用 python pycrypto 包以兼容 openssl 的方式加密/解密的博客。已经在python2.7和python3.x上测试过。可以在此处找到源代码和测试脚本。
One of the key differences between this solution and the excellent solutions presented above is that it differentiates between pipe and file I/O which can cause problems in some applications.
此解决方案与上面介绍的优秀解决方案之间的主要区别之一是它区分管道和文件 I/O,这可能会在某些应用程序中引起问题。
The key functions from that blog are shown below.
该博客的主要功能如下所示。
# ================================================================
# 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'\n'.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 = '\n'
        re1 = r'^\s*$'
        re2 = r'^\s*#'
    else:
        filtered = b''
        nl = b'\n'
        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
回答by Harvs
Note: this method is not OpenSSL compatible
注意:此方法与 OpenSSL 不兼容
But it is suitable if all you want to do is encrypt and decrypt files.
但如果您只想加密和解密文件,它就很合适。
A self-answer I copied from here. I think this is, perhaps, a simpler and more secure option. Although I would be interested in some expert opinion on how secure it is.
我从这里复制的自我回答。我认为这也许是一个更简单、更安全的选择。虽然我会对一些关于它的安全性的专家意见感兴趣。
I used Python 3.6 and SimpleCryptto encrypt the file and then uploaded it.
我使用 Python 3.6 和SimpleCrypt来加密文件,然后上传它。
I thinkthis is the code I used to encrypt the file:
我认为这是我用来加密文件的代码:
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
This is the code I use to decrypt at runtime, I run getpass("password: ")as an argument so I don't have to store a passwordvariable in memory
这是我在运行时用来解密的代码,我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 \n 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')
Note, the UTF-8 encoding behaviour is different in python 2.7 so the code will be slightly different.
请注意,UTF-8 编码行为在 python 2.7 中有所不同,因此代码会略有不同。
回答by mti2935
This answer is based on openssl v1.1.1, which supports a stronger key derivation process for AES encryption, than that of previous versions of openssl.
此答案基于 openssl v1.1.1,与以前版本的 openssl 相比,它支持更强大的 AES 加密密钥派生过程。
This answer is based on the following command:
此答案基于以下命令:
echo -n 'Hello World!' | openssl aes-256-cbc -e -a -salt -pbkdf2 -iter 10000 
This command encrypts the plaintext 'Hello World!' using aes-256-cbc. The key is derived using pbkdf2 from the password and a random salt, with 10,000 iterations of sha256 hashing. When prompted for the password, I entered the password, 'p4$$w0rd'. The ciphertext output produced by the command was:
此命令加密明文“Hello World!” 使用 aes-256-cbc。密钥是使用 pbkdf2 从密码和随机盐派生出来的,经过 10,000 次 sha256 哈希迭代。当提示输入密码时,我输入了密码“p4$$w0rd”。命令产生的密文输出为:
U2FsdGVkX1/Kf8Yo6JjBh+qELWhirAXr78+bbPQjlxE=
The process for decrypting of the ciphertext above produced by openssl is as follows:
以上由openssl产生的密文解密过程如下:
- base64-decode the output from openssl, and utf-8 decode the password, so that we have the underlying bytes for both of these.
- The salt is bytes 8-15 of the base64-decoded openssl output.
- Derive a 48-byte key using pbkdf2 given the password bytes and salt with 10,000 iterations of sha256 hashing.
- The key is bytes 0-31 of the derived key, the iv is bytes 32-47 of the derived key.
- The ciphertext is bytes 16 through the end of the base64-decoded openssl output.
- Decrypt the ciphertext using aes-256-cbc, given the key, iv, and ciphertext.
- Remove PKCS#7 padding from plaintext. The last byte of plaintext indicates the number of padding bytes appended to the end of the plaintext. This is the number of bytes to be removed.
- 对 openssl 的输出进行 base64 解码,utf-8 对密码进行解码,这样我们就有了这两个的底层字节。
- 盐是 base64 解码的 openssl 输出的字节 8-15。
- 给定密码字节和盐,使用 pbkdf2 派生一个 48 字节的密钥,并使用 10,000 次 sha256 哈希迭代。
- 密钥是派生密钥的 0-31 字节,iv 是派生密钥的 32-47 字节。
- 密文是经过 base64 解码的 openssl 输出末尾的第 16 个字节。
- 给定密钥、iv 和密文,使用 aes-256-cbc 解密密文。
- 从纯文本中删除 PKCS#7 填充。明文的最后一个字节表示附加到明文末尾的填充字节数。这是要删除的字节数。
Below is a python3 implementation of the above process:
下面是上述过程的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'))
As expected, the above python3 script produces the following:
正如预期的那样,上面的 python3 脚本产生以下内容:
openssloutputb64: U2FsdGVkX1/Kf8Yo6JjBh+qELWhirAXr78+bbPQjlxE=
password: p4$$w0rd
salt: ca7fc628e898c187
key:  444ab886d5721fc87e58f86f3e7734659007bea7fbe790541d9e73c481d9d983
iv:  7f4597a18096715d7f9830f0125be8fd
ciphertext:  ea842d6862ac05ebefcf9b6cf4239711
plaintext:  Hello World!
Note: An equivalent/compatible implementation in javascript (using the web crypto api) can be found at https://github.com/meixler/web-browser-based-file-encryption-decryption.
注意:可以在https://github.com/meixler/web-browser-based-file-encryption-decryption 中找到javascript 中的等效/兼容实现(使用web 加密 api)。

