Comment décrypter les fichiers cryptés OpenSSL AES en Python?

OpenSSL fournit un populaire (mais peu sûr-voir ci-dessous!) interface en ligne de commande pour le cryptage AES:

openssl aes-256-cbc -salt -in filename -out filename.enc

Python a le support pour AES sous la forme du paquet PyCrypto, mais il ne fournit que les outils. Comment utiliser Python/PyCrypto pour déchiffrer des fichiers qui ont été cryptés en utilisant OpenSSL?

Avis

cette question concernait aussi le cryptage en Python en utilisant le même schéma. Depuis, j'ai supprimé la partie à décourager quiconque de l'utiliser. Ne cryptez plus de données de cette façon, car elles ne sont pas sécurisées selon les normes actuelles. Vous ne devez utiliser le décryptage, pour aucune autre raison que la rétrocompatibilité, c.-à-d. quand vous n'avez pas d'autre choix. Voulez chiffrer? Utilisez NaCl / libsodium si vous le pouvez.

51
demandé sur Thijs van Dien 2013-05-26 20:47:41

5 réponses

étant donné la popularité de Python, j'ai d'abord été déçu qu'il n'y ait pas de réponse complète à cette question à trouver. Il m'a fallu lire un bon nombre de réponses différentes sur ce jury, ainsi que d'autres ressources, pour bien faire les choses. J'ai pensé que je pourrais partager le résultat pour référence future et peut-être examen; Je ne suis en aucun cas un expert en cryptographie! Toutefois, le code ci-dessous semble fonctionner de façon transparente:

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)

Utilisation:

with open(in_filename, 'rb') as in_file, open(out_filename, 'wb') as out_file:
    decrypt(in_file, out_file, password)

si vous voyez une chance de l'améliorer ou de l'étendre pour être plus flexible (par exemple, le faire fonctionner sans sel, ou fournir la compatibilité Python 3), s'il vous plaît n'hésitez pas à le faire.

Avis

cette réponse concernait aussi le cryptage en Python en utilisant le même schéma. J'ai depuis retiré cette partie pour décourager quiconque de l'utiliser. Ne cryptez plus de données de cette façon, car elles ne sont pas sécurisées selon les normes actuelles. Vous devez UNIQUEMENT utiliser décryptage, pour aucune autre raison que la rétrocompatibilité, c'est-à-dire lorsque vous n'avez pas d'autre choix. Voulez chiffrer? Utilisez NaCl / libsodium si vous le pouvez.

86
répondu Thijs van Dien 2017-12-06 15:51:16

je re-POS TE votre code avec quelques corrections (Je ne voulais pas obscurcir votre version). Bien que votre code fonctionne, il ne détecte pas certaines erreurs autour du padding. En particulier, si la clé de déchiffrement fournie est incorrecte, votre logique de remplissage peut faire quelque chose de bizarre. Si vous êtes d'accord avec mon changement, vous pouvez mettre à jour votre 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)
20
répondu Gregor 2017-12-11 13:47:46

le code ci-dessous doit être Python 3 compatible avec les petits changements documentés dans le code. Voulait aussi utiliser os.urandom au lieu de Crypto.Aléatoire. "Salée__" est remplacé par salt_header qui peuvent être adaptées ou laissé vide si nécessaire.

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(...)
12
répondu Johnny Booy 2017-06-14 14:56:45

je sais que c'est un peu tard mais ici est une solution que j'ai blogué en 2013 sur la façon d'utiliser le paquet python pycrypto pour chiffrer/déchiffrer d'une manière compatible openssl. Il a été testé sur python2.7 et python3.x. Le code source et un script de test peuvent être trouvés ici .

L'une des principales différences entre cette solution et les excellentes solutions présentées ci-dessus est qu'elle fait la différence entre les tuyaux et fichier E / S qui peut causer des problèmes dans certaines applications.

les principales fonctions de ce blog sont indiquées ci-dessous.

# ================================================================
# 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
0
répondu Joe Linoff 2017-03-13 20:51:36

Note: cette méthode n'est pas compatible OpenSSL

mais il est approprié si tout ce que vous voulez faire est de chiffrer et de déchiffrer les fichiers.

Une auto-réponse, j'ai copié de ici . Je pense que c'est peut-être plus simple et plus sûres. Bien que je serais intéressé par une opinion d'expert sur la façon dont il est sûr.

j'ai utilisé Python 3.6 et SimpleCrypt pour chiffrer le fichier et puis téléchargé.

je penser c'est le code que j'ai utilisé pour crypter le fichier:

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

C'est le code que j'utilise pour déchiffrer à l'exécution, j'exécute getpass("password: ") comme argument pour ne pas avoir à stocker une password variable en mémoire

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')

notez que le comportement d'encodage UTF-8 est différent en python 2.7 donc le code sera légèrement différent.

-2
répondu Harvs 2017-08-05 10:20:11