¿Cómo descifrar archivos cifrados con OpenSSL AES en Python?
OpenSSL proporciona una interfaz de línea de comandos popular (pero insegura, ¡consulte a continuación!) para el cifrado AES:
openssl aes-256-cbc -salt -in filename -out filename.enc
Python admite AES en forma del paquete PyCrypto, pero solo proporciona las herramientas. ¿Cómo usar Python/PyCrypto para descifrar archivos que han sido cifrados usando OpenSSL?
Aviso
Esta pregunta también se refería al cifrado en Python usando el mismo esquema. Desde entonces, eliminé esa parte para disuadir a cualquiera de usarla. NO cifre más datos de esta manera, porque NO es seguro según los estándares actuales. SÓLO debe utilizar el descifrado, únicamente por la COMPATIBILIDAD HACIA ATRÁS, es decir, cuando no tenga otra opción. ¿Quieres cifrar? Utilice NaCl/libsodio si es posible.
Dada la popularidad de Python, al principio me decepcionó que no hubiera una respuesta completa a esta pregunta. Me tomó bastante leer diferentes respuestas en este foro, así como otros recursos, para hacerlo bien. Pensé que podría compartir el resultado para referencia futura y tal vez revisarlo; ¡De ninguna manera soy un experto en criptografía! Sin embargo, el siguiente código parece funcionar perfectamente:
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)
Uso:
with open(in_filename, 'rb') as in_file, open(out_filename, 'wb') as out_file:
decrypt(in_file, out_file, password)
Si ve la posibilidad de mejorar esto o ampliarlo para que sea más flexible (por ejemplo, hacerlo funcionar sin sal o proporcionar compatibilidad con Python 3), no dude en hacerlo.
Aviso
Esta respuesta también se refería al cifrado en Python usando el mismo esquema. Desde entonces, eliminé esa parte para disuadir a cualquiera de usarla. NO cifre más datos de esta manera, porque NO es seguro según los estándares actuales. SÓLO debe utilizar el descifrado, únicamente por la COMPATIBILIDAD HACIA ATRÁS, es decir, cuando no tenga otra opción. ¿Quieres cifrar? Utilice NaCl/libsodio si es posible.
Estoy volviendo a publicar su código con un par de correcciones (no quería ocultar su versión). Si bien su código funciona, no detecta algunos errores relacionados con el relleno. En particular, si la clave de descifrado proporcionada es incorrecta, su lógica de relleno puede hacer algo extraño. Si está de acuerdo con mi cambio, puede actualizar su solución.
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)
El siguiente código debe ser compatible con Python 3 con los pequeños cambios documentados en el código. También quería usar os.urandom en lugar de Crypto.Random. 'Salted__' se reemplaza por salt_header que se puede adaptar o dejar vacío si es necesario.
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(...)