如何使用Python / PyCrypto以OpenSSL兼容的方式对文件进行AESencryption/解密?
有很多方法使用AES,导致不同实现之间频繁的不兼容。 OpenSSL为AESencryption/解密提供了一个stream行的(但不安全 – 见下文!)命令行界面:
openssl aes-256-cbc -salt -in filename -out filename.enc openssl aes-256-cbc -d -in filename.enc -out filename
Python以PyCrypto包的forms支持AES,但它只提供工具。 如何使用Python / PyCrypto来encryption文件,使用OpenSSL来解密文件,解密使用OpenSSLencryption的文件?
警告
这个encryptionscheme不好。 它不提供充分的安全性。 不要使用它,除非你绝对需要向后兼容。
鉴于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 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) or 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]) 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: encrypt(in_file, out_file, password) with open(in_filename, 'rb') as in_file, open(out_filename, 'wb') as out_file: decrypt(in_file, out_file, password)
如果你有机会改进或扩展它,使其更加灵活(例如,使其无需工作,或提供Python 3的兼容性),请随时这样做。
警告
这个encryptionscheme不好。 它不提供充分的安全性。 不要使用它,除非你绝对需要向后兼容。
我重新发布您的代码与几个更正(我不想晦涩您的版本)。 当你的代码工作时,它不会检测到有关填充的错误。 特别是,如果提供的解密密钥不正确,你的填充逻辑可能会做一些奇怪的事情。 如果您同意我的更改,则可以更新您的解决scheme。
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 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)
下面的代码应该是Python 3与代码中logging的小变化兼容。 也想使用os.urandom而不是Crypto.Random。 'salted__'replace为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(...)
我知道这有点迟,但这里是我在2013年写博客的一个解决scheme,关于如何使用python pycrypto软件包以openssl兼容的方式进行encryption/解密。 它已经在python2.7和python3.x上testing过了。 源代码和testing脚本可以在这里find。
这个解决scheme和上面介绍的优秀解决scheme之间的一个主要区别在于,它区分了pipe道和文件I / O,这在某些应用中会导致问题。
该博客的主要function如下所示。
# ================================================================ # 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
注意:这个方法不是OpenSSL兼容的
但是,如果你想要做的只是encryption和解密文件,这是适合的。
我从这里复制一份自我答复。 我想这也许是一个更简单,更安全的select。 虽然我会对一些专家的意见感兴趣,因为它有多安全。
我用Python 3.6和SimpleCrypt来encryption文件,然后上传它。
我认为这是我用来encryption文件的代码:
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
variables
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')
请注意,Python 2.7中的UTF-8编码行为是不同的,所以代码会稍有不同。