¿Cómo descifrar archivos cifrados con OpenSSL AES en Python?

Resuelto Thijs van Dien asked hace 10 años • 7 respuestas

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.

Thijs van Dien avatar May 26 '13 23:05 Thijs van Dien
Aceptado

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.

Thijs van Dien avatar May 26 '2013 16:05 Thijs van Dien

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)
Gregor avatar Dec 08 '2013 18:12 Gregor

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(...)
Johnny Booy avatar Feb 11 '2014 17:02 Johnny Booy