Cifrar y descifrar usando PyCrypto AES 256

171

Estoy tratando de construir dos funciones usando PyCrypto que acepten dos parámetros: el mensaje y la clave, y luego cifrar / descifrar el mensaje.

Encontré varios enlaces en la web para ayudarme, pero cada uno de ellos tiene fallas:

Este en codekoala usa os.urandom, que es desaconsejado por PyCrypto.

Además, no se garantiza que la clave que le doy a la función tenga la longitud exacta esperada. ¿Qué puedo hacer para que eso suceda?

Además, hay varios modos, ¿cuál se recomienda? No sé qué usar: /

Finalmente, ¿qué es exactamente el IV? ¿Puedo proporcionar un IV diferente para cifrar y descifrar, o esto devolverá un resultado diferente?

Editar : se eliminó la parte del código porque no era segura.

Cyril N.
fuente
12
Se recomienda os.urandom en el sitio web de PyCrypto . Utiliza la función CryptGenRandom de Microsoft que es un CSPRNG
Joel Vroom
55
o /dev/urandomen Unix
Joel Vroom
2
Solo para aclarar, en este ejemplo, la frase de contraseña es la clave que puede ser de 128, 192 o 256 bits (16, 24 o 32 bytes)
Marque el
44
Vale la pena mencionar que PyCrypto es un proyecto muerto . La última confirmación es de 2014. PyCryptodome parece un buen reemplazo directo
Overdrivr
1
Esta pregunta es antigua, pero me gustaría señalar (a partir de 2020) que el cifrado probablemente esté desactualizado y ya no sea compatible. Mirando su página de github ( github.com/pycrypto/pycrypto ), parece que su última confirmación fue en 2014. Tendría miedo de usar software criptográfico que ya no está en desarrollo
irritable_phd_syndrom

Respuestas:

151

Aquí está mi implementación y funciona para mí con algunas correcciones y mejora la alineación de la clave y la frase secreta con 32 bytes y iv a 16 bytes:

import base64
import hashlib
from Crypto import Random
from Crypto.Cipher import AES

class AESCipher(object):

    def __init__(self, key): 
        self.bs = AES.block_size
        self.key = hashlib.sha256(key.encode()).digest()

    def encrypt(self, raw):
        raw = self._pad(raw)
        iv = Random.new().read(AES.block_size)
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return base64.b64encode(iv + cipher.encrypt(raw.encode()))

    def decrypt(self, enc):
        enc = base64.b64decode(enc)
        iv = enc[:AES.block_size]
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8')

    def _pad(self, s):
        return s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs)

    @staticmethod
    def _unpad(s):
        return s[:-ord(s[len(s)-1:])]
mnótico
fuente
14
Sé que esto ha estado sucediendo por un tiempo, pero creo que esta respuesta puede generar cierta confusión. Esta función usa un tamaño de bloque de 32 bytes (256 bytes) para rellenar los datos de entrada, pero AES usa un tamaño de bloque de 128 bits. En AES256, la clave es de 256 bits, pero no el tamaño del bloque.
Tannin
13
para decirlo de otra manera, "self.bs" debería eliminarse y reemplazarse por "AES.block_size"
Alexis
2
¿Por qué estás haciendo hash la clave? Si espera que esto sea algo así como una contraseña, entonces no debería usar SHA256; es mejor usar una función de derivación de clave, como PBKDF2, que proporciona PyCrypto.
tweaksp
55
@Chris: SHA256 emite un hash de 32 bytes, una clave de tamaño perfecto para AES256. Se supone que la generación / derivación de una clave es aleatoria / segura y debe estar fuera del alcance del código de cifrado / descifrado; el hash es solo una garantía de que la clave se puede usar con el cifrado seleccionado.
zwer
2
en _pad self.bs se necesita acceso y en _unpad no se necesita
mnothic
149

Es posible que necesite las dos funciones siguientes: pad- para rellenar (cuando se realiza el cifrado) y unpad- para desempaquetar (cuando se realiza el descifrado) cuando la longitud de entrada no es un múltiplo de BLOCK_SIZE.

BS = 16
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS) 
unpad = lambda s : s[:-ord(s[len(s)-1:])]

Entonces, ¿estás preguntando la longitud de la clave? Puede usar el md5sum de la clave en lugar de usarlo directamente.

Además, según mi poca experiencia en el uso de PyCrypto, el IV se usa para mezclar la salida de un cifrado cuando la entrada es la misma, por lo que el IV se elige como una cadena aleatoria, y se usa como parte de la salida de cifrado, y luego Úselo para descifrar el mensaje.

Y aquí está mi implementación, espero que sea útil para usted:

import base64
from Crypto.Cipher import AES
from Crypto import Random

class AESCipher:
    def __init__( self, key ):
        self.key = key

    def encrypt( self, raw ):
        raw = pad(raw)
        iv = Random.new().read( AES.block_size )
        cipher = AES.new( self.key, AES.MODE_CBC, iv )
        return base64.b64encode( iv + cipher.encrypt( raw ) ) 

    def decrypt( self, enc ):
        enc = base64.b64decode(enc)
        iv = enc[:16]
        cipher = AES.new(self.key, AES.MODE_CBC, iv )
        return unpad(cipher.decrypt( enc[16:] ))
Marcus
fuente
1
¿Qué sucede si tiene una entrada que es exactamente un múltiplo de BLOCK_SIZE? Creo que la función de unpad se confundiría un poco ...
Kjir
2
@Kjir, luego se agregará una secuencia de valor chr (BS) en longitud BLOCK_SIZE a los datos de origen.
Marcus
1
@Marcus la padfunción está rota (al menos en Py3), reemplácela s[:-ord(s[len(s)-1:])]para que funcione en todas las versiones.
Torxed
2
La función @Torxed pad está disponible en CryptoUtil.Padding.pad () con pycryptodome (seguimiento de pycrypto)
comte
2
¿Por qué no simplemente tener un carácter constante como el personaje de relleno?
Inaimathi
16

Déjame abordar tu pregunta sobre los "modos". AES256 es una especie de cifrado de bloque . Toma como entrada una clave de 32 bytes y una cadena de 16 bytes, llamada bloque, y genera un bloque. Utilizamos AES en un modo de operación para encriptar. Las soluciones anteriores sugieren usar CBC, que es un ejemplo. Otro se llama CTR, y es algo más fácil de usar:

from Crypto.Cipher import AES
from Crypto.Util import Counter
from Crypto import Random

# AES supports multiple key sizes: 16 (AES128), 24 (AES192), or 32 (AES256).
key_bytes = 32

# Takes as input a 32-byte key and an arbitrary-length plaintext and returns a
# pair (iv, ciphtertext). "iv" stands for initialization vector.
def encrypt(key, plaintext):
    assert len(key) == key_bytes

    # Choose a random, 16-byte IV.
    iv = Random.new().read(AES.block_size)

    # Convert the IV to a Python integer.
    iv_int = int(binascii.hexlify(iv), 16) 

    # Create a new Counter object with IV = iv_int.
    ctr = Counter.new(AES.block_size * 8, initial_value=iv_int)

    # Create AES-CTR cipher.
    aes = AES.new(key, AES.MODE_CTR, counter=ctr)

    # Encrypt and return IV and ciphertext.
    ciphertext = aes.encrypt(plaintext)
    return (iv, ciphertext)

# Takes as input a 32-byte key, a 16-byte IV, and a ciphertext, and outputs the
# corresponding plaintext.
def decrypt(key, iv, ciphertext):
    assert len(key) == key_bytes

    # Initialize counter for decryption. iv should be the same as the output of
    # encrypt().
    iv_int = int(iv.encode('hex'), 16) 
    ctr = Counter.new(AES.block_size * 8, initial_value=iv_int)

    # Create AES-CTR cipher.
    aes = AES.new(key, AES.MODE_CTR, counter=ctr)

    # Decrypt and return the plaintext.
    plaintext = aes.decrypt(ciphertext)
    return plaintext

(iv, ciphertext) = encrypt(key, 'hella')
print decrypt(key, iv, ciphertext)

Esto a menudo se conoce como AES-CTR. Aconsejaría precaución al usar AES-CBC con PyCrypto . La razón es que requiere que especifique el esquema de relleno , como lo demuestran las otras soluciones dadas. En general, si no tienes mucho cuidado con el relleno, ¡ hay ataques que rompen completamente el cifrado!

Ahora, es importante tener en cuenta que la clave debe ser una cadena aleatoria de 32 bytes ; Una contraseña no es suficiente. Normalmente, la clave se genera así:

# Nominal way to generate a fresh key. This calls the system's random number
# generator (RNG).
key1 = Random.new().read(key_bytes)

También se puede derivar una clave de una contraseña :

# It's also possible to derive a key from a password, but it's important that
# the password have high entropy, meaning difficult to predict.
password = "This is a rather weak password."

# For added # security, we add a "salt", which increases the entropy.
#
# In this example, we use the same RNG to produce the salt that we used to
# produce key1.
salt_bytes = 8 
salt = Random.new().read(salt_bytes)

# Stands for "Password-based key derivation function 2"
key2 = PBKDF2(password, salt, key_bytes)

Algunas soluciones anteriores sugieren usar SHA256 para derivar la clave, pero esto generalmente se considera una mala práctica criptográfica . Consulte wikipedia para obtener más información sobre los modos de operación.

tweaksp
fuente
iv_int = int (binascii.hexlify (iv), 16) no funciona, reemplácelo con iv_int = int (binascii.hexlify (iv), 16) más el 'import binascii' y debería funcionar (en Python 3.x ), de lo contrario, un gran trabajo!
Valmond
Tenga en cuenta que es mejor utilizar los modos de cifrado Autehnticated como AES-GCM. GCM usa internamente el modo CTR.
kelalaka
Este código causa "TypeError: el tipo de objeto <clase 'str'> no se puede pasar al código C"
Da Woon Jung
7

Para alguien a quien le gustaría usar urlsafe_b64encode y urlsafe_b64decode, aquí está la versión que me funciona (después de pasar un tiempo con el problema Unicode)

BS = 16
key = hashlib.md5(settings.SECRET_KEY).hexdigest()[:BS]
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
unpad = lambda s : s[:-ord(s[len(s)-1:])]

class AESCipher:
    def __init__(self, key):
        self.key = key

    def encrypt(self, raw):
        raw = pad(raw)
        iv = Random.new().read(AES.block_size)
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return base64.urlsafe_b64encode(iv + cipher.encrypt(raw)) 

    def decrypt(self, enc):
        enc = base64.urlsafe_b64decode(enc.encode('utf-8'))
        iv = enc[:BS]
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return unpad(cipher.decrypt(enc[BS:]))
Hoang HUA
fuente
6

Puede obtener una frase de contraseña de una contraseña arbitraria mediante el uso de una función hash criptográfica ( NO de Python incorporada hash) como SHA-1 o SHA-256. Python incluye soporte para ambos en su biblioteca estándar:

import hashlib

hashlib.sha1("this is my awesome password").digest() # => a 20 byte string
hashlib.sha256("another awesome password").digest() # => a 32 byte string

Puede truncar un valor hash criptográfico simplemente usando [:16]o [:24]y retendrá su seguridad hasta la longitud que especifique.

nneonneo
fuente
13
No debe usar una función hash de la familia SHA para generar una clave a partir de una contraseña; consulte el ensayo de Coda Hale sobre el tema . Considere usar una función de derivación de clave real como scrypt en su lugar. (El ensayo de Coda Hale fue escrito antes de la publicación de scrypt.)
Benjamin Barenblat
77
Para futuros lectores, si está buscando derivar una clave de una frase de contraseña, busque PBKDF2. Es bastante fácil de usar en python ( pypi.python.org/pypi/pbkdf2 ). Sin embargo, si está buscando contraseñas hash, bcrypt es una mejor opción.
C Fairweather
6

Agradecido por las otras respuestas que inspiraron pero no funcionaron para mí.

Después de pasar horas tratando de averiguar cómo funciona, se me ocurrió la implementación a continuación con la biblioteca PyCryptodomex más nueva (es otra historia de cómo logré configurarlo detrás del proxy, en Windows, en un virtualenv .. phew)

Trabajando en su implementación, recuerde anotar los pasos de relleno, codificación y cifrado (y viceversa). Tienes que empacar y desempacar teniendo en cuenta el pedido.

importar base64
importar hashlib
de Cryptodome.Cipher import AES
from Cryptodome.Random import get_random_bytes

__key__ = hashlib.sha256 (b'16-key key '). digest ()

cifrado def (sin formato):
    BS = AES.block_size
    pad = lambda s: s + (BS - len (s)% BS) * chr (BS - len (s)% BS)

    raw = base64.b64encode (pad (raw) .encode ('utf8'))
    iv = get_random_bytes (AES.block_size)
    cifrado = AES.new (clave = __key__, modo = AES.MODE_CFB, iv = iv)
    return base64.b64encode (iv + cipher.encrypt (raw))

descifrar def (enc):
    unpad = lambda s: s [: - ord (s [-1:])]

    enc = base64.b64decode (enc)
    iv = enc [: AES.block_size]
    cifrado = AES.new (__ clave__, AES.MODE_CFB, iv)
    return unpad (base64.b64decode (cipher.decrypt (enc [AES.block_size:])). decode ('utf8'))
cenkarioz
fuente
Gracias amablemente por un ejemplo funcional de esto con las bibliotecas PyCryptodomeX. Eso es muy útil!
Ygramul
5

Para el beneficio de otros, aquí está mi implementación de descifrado a la que llegué combinando las respuestas de @Cyril y @Marcus. Esto supone que esto llega a través de Solicitud HTTP con el texto cifrado citado y codificado en base64.

import base64
import urllib2
from Crypto.Cipher import AES


def decrypt(quotedEncodedEncrypted):
    key = 'SecretKey'

    encodedEncrypted = urllib2.unquote(quotedEncodedEncrypted)

    cipher = AES.new(key)
    decrypted = cipher.decrypt(base64.b64decode(encodedEncrypted))[:16]

    for i in range(1, len(base64.b64decode(encodedEncrypted))/16):
        cipher = AES.new(key, AES.MODE_CBC, base64.b64decode(encodedEncrypted)[(i-1)*16:i*16])
        decrypted += cipher.decrypt(base64.b64decode(encodedEncrypted)[i*16:])[:16]

    return decrypted.strip()
scottmrogowski
fuente
5

Otra versión de esto (fuertemente derivada de las soluciones anteriores) pero

  • utiliza nulo para el relleno
  • no usa lambda (nunca ha sido fan)
  • probado con python 2.7 y 3.6.5

    #!/usr/bin/python2.7
    # you'll have to adjust for your setup, e.g., #!/usr/bin/python3
    
    
    import base64, re
    from Crypto.Cipher import AES
    from Crypto import Random
    from django.conf import settings
    
    class AESCipher:
        """
          Usage:
          aes = AESCipher( settings.SECRET_KEY[:16], 32)
          encryp_msg = aes.encrypt( 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppp' )
          msg = aes.decrypt( encryp_msg )
          print("'{}'".format(msg))
        """
        def __init__(self, key, blk_sz):
            self.key = key
            self.blk_sz = blk_sz
    
        def encrypt( self, raw ):
            if raw is None or len(raw) == 0:
                raise NameError("No value given to encrypt")
            raw = raw + '\0' * (self.blk_sz - len(raw) % self.blk_sz)
            raw = raw.encode('utf-8')
            iv = Random.new().read( AES.block_size )
            cipher = AES.new( self.key.encode('utf-8'), AES.MODE_CBC, iv )
            return base64.b64encode( iv + cipher.encrypt( raw ) ).decode('utf-8')
    
        def decrypt( self, enc ):
            if enc is None or len(enc) == 0:
                raise NameError("No value given to decrypt")
            enc = base64.b64decode(enc)
            iv = enc[:16]
            cipher = AES.new(self.key.encode('utf-8'), AES.MODE_CBC, iv )
            return re.sub(b'\x00*$', b'', cipher.decrypt( enc[16:])).decode('utf-8')
MIkee
fuente
Esto no funcionará si el byte de entrada [] tiene valores nulos finales porque en la función descifrar () comerá sus nulos de relleno MÁS los nulos finales.
Buzz Moschetti
Sí, como afirmo anteriormente, esta lógica rellena con nulos. Si los elementos que desea codificar / decodificar pueden tener nulos finales, mejor use una de las otras soluciones aquí
MIkee
3

He usado ambos Cryptoy la PyCryptodomexbiblioteca y es increíblemente rápido ...

import base64
import hashlib
from Cryptodome.Cipher import AES as domeAES
from Cryptodome.Random import get_random_bytes
from Crypto import Random
from Crypto.Cipher import AES as cryptoAES

BLOCK_SIZE = AES.block_size

key = "my_secret_key".encode()
__key__ = hashlib.sha256(key).digest()
print(__key__)

def encrypt(raw):
    BS = cryptoAES.block_size
    pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
    raw = base64.b64encode(pad(raw).encode('utf8'))
    iv = get_random_bytes(cryptoAES.block_size)
    cipher = cryptoAES.new(key= __key__, mode= cryptoAES.MODE_CFB,iv= iv)
    a= base64.b64encode(iv + cipher.encrypt(raw))
    IV = Random.new().read(BLOCK_SIZE)
    aes = domeAES.new(__key__, domeAES.MODE_CFB, IV)
    b = base64.b64encode(IV + aes.encrypt(a))
    return b

def decrypt(enc):
    passphrase = __key__
    encrypted = base64.b64decode(enc)
    IV = encrypted[:BLOCK_SIZE]
    aes = domeAES.new(passphrase, domeAES.MODE_CFB, IV)
    enc = aes.decrypt(encrypted[BLOCK_SIZE:])
    unpad = lambda s: s[:-ord(s[-1:])]
    enc = base64.b64decode(enc)
    iv = enc[:cryptoAES.block_size]
    cipher = cryptoAES.new(__key__, cryptoAES.MODE_CFB, iv)
    b=  unpad(base64.b64decode(cipher.decrypt(enc[cryptoAES.block_size:])).decode('utf8'))
    return b

encrypted_data =encrypt("Hi Steven!!!!!")
print(encrypted_data)
print("=======")
decrypted_data = decrypt(encrypted_data)
print(decrypted_data)
Smack Alpha
fuente
2

Es un poco tarde, pero creo que esto será muy útil. Nadie menciona el esquema de uso como el relleno PKCS # 7. En su lugar, puede usar las funciones anteriores para rellenar (cuando realiza el cifrado) y desempaquetar (cuando realiza el descifrado) .i proporcionará el código fuente completo a continuación.

import base64
import hashlib
from Crypto import Random
from Crypto.Cipher import AES
import pkcs7
class Encryption:

    def __init__(self):
        pass

    def Encrypt(self, PlainText, SecurePassword):
        pw_encode = SecurePassword.encode('utf-8')
        text_encode = PlainText.encode('utf-8')

        key = hashlib.sha256(pw_encode).digest()
        iv = Random.new().read(AES.block_size)

        cipher = AES.new(key, AES.MODE_CBC, iv)
        pad_text = pkcs7.encode(text_encode)
        msg = iv + cipher.encrypt(pad_text)

        EncodeMsg = base64.b64encode(msg)
        return EncodeMsg

    def Decrypt(self, Encrypted, SecurePassword):
        decodbase64 = base64.b64decode(Encrypted.decode("utf-8"))
        pw_encode = SecurePassword.decode('utf-8')

        iv = decodbase64[:AES.block_size]
        key = hashlib.sha256(pw_encode).digest()

        cipher = AES.new(key, AES.MODE_CBC, iv)
        msg = cipher.decrypt(decodbase64[AES.block_size:])
        pad_text = pkcs7.decode(msg)

        decryptedString = pad_text.decode('utf-8')
        return decryptedString

import StringIO
import binascii


def decode(text, k=16):
    nl = len(text)
    val = int(binascii.hexlify(text[-1]), 16)
    if val > k:
        raise ValueError('Input is not padded or padding is corrupt')

    l = nl - val
    return text[:l]


def encode(text, k=16):
    l = len(text)
    output = StringIO.StringIO()
    val = k - (l % k)
    for _ in xrange(val):
        output.write('%02x' % val)
    return text + binascii.unhexlify(output.getvalue())

Panagiotis Drakatos
fuente
No sé quién rechazó la respuesta, pero me gustaría saber por qué. ¿Quizás este método no es seguro? Una explicación sería genial.
Cyril N.
1
@CyrilN. Esta respuesta sugiere que cifrar la contraseña con una sola invocación de SHA-256 es suficiente. No lo es Realmente debería usar PBKDF2 o similar para la derivación de clave de una contraseña utilizando un gran recuento de iteraciones.
Artjom B.
Gracias por el detalle @ArtjomB.!
Cyril N.
Tengo una llave y también una llave iv con 44 de longitud. ¿Cómo puedo usar sus funciones? todos los algoritmos en el Internet que he encontrado, tiene un problema con la longitud de la llave del vector
mahshid.r
1
from Crypto import Random
from Crypto.Cipher import AES
import base64

BLOCK_SIZE=16
def trans(key):
     return md5.new(key).digest()

def encrypt(message, passphrase):
    passphrase = trans(passphrase)
    IV = Random.new().read(BLOCK_SIZE)
    aes = AES.new(passphrase, AES.MODE_CFB, IV)
    return base64.b64encode(IV + aes.encrypt(message))

def decrypt(encrypted, passphrase):
    passphrase = trans(passphrase)
    encrypted = base64.b64decode(encrypted)
    IV = encrypted[:BLOCK_SIZE]
    aes = AES.new(passphrase, AES.MODE_CFB, IV)
    return aes.decrypt(encrypted[BLOCK_SIZE:])
yuen
fuente
10
Proporcione no solo el código, sino que también explique lo que está haciendo y por qué esto es mejor / cuál es la diferencia con las respuestas existentes.
Florian Koch
Reemplace md5.new (key) .digest () por md5 (key) .digest (), ¡y funciona de maravilla!
UN STEFANI