Criptografar e descriptografar usando PyCrypto AES 256

171

Estou tentando criar duas funções usando PyCrypto que aceitam dois parâmetros: a mensagem e a chave e, em seguida, criptografam / descriptografam a mensagem.

Encontrei vários links na web para me ajudar, mas cada um deles tem falhas:

Este em codekoala usa os.urandom, que é desencorajado por PyCrypto.

Além disso, não é garantido que a chave que dou à função tenha o comprimento exato esperado. O que posso fazer para que isso aconteça?

Além disso, existem vários modos, qual é o recomendado? Não sei o que usar: /

Finalmente, o que exatamente é o IV? Posso fornecer um IV diferente para criptografar e descriptografar, ou isso retornará com um resultado diferente?

Editar : Removida a parte do código, pois não era segura.

Cyril N.
fonte
12
o os.urandom é incentivado no site do PyCrypto . Ele usa o Microsoft CryptGenRandom função que é um CSPRNG
Joel Vroom
5
ou /dev/urandomno Unix
Joel Vroom 28/11
2
Apenas para clarificar, neste exemplo senha é a chave que pode ser 128, 192, ou 256 bits (16, 24, ou 32 bytes)
Mark
4
Vale a pena mencionar que PyCrypto é um projeto morto . Última cometer é de 2014. PyCryptodome parece um bom substituto
Overdrivr
1
Esta pergunta é antiga, mas eu gostaria de salientar (a partir de 2020) que o pycrypto provavelmente está desatualizado e não é mais suportado. Olhando para a página do github ( github.com/pycrypto/pycrypto ), parece que o último commit foi em 2014. Eu ficaria desconfiado ao usar software criptográfico que não está mais em desenvolvimento
irritable_phd_syndrom

Respostas:

151

Aqui está minha implementação e funciona para mim com algumas correções e aprimora o alinhamento da chave e da frase secreta com 32 bytes e 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
fonte
14
Sei que isso já existe há algum tempo, mas acho que essa resposta pode espalhar alguma confusão. Essa função usa um tamanho de bloco de 32 bytes (256 bytes) para preencher os dados de entrada, mas o AES usa o tamanho do bloco de 128 bits. No AES256, a chave é de 256 bits, mas não o tamanho do bloco.
quer
13
dito de outra forma, "self.bs" deve ser removido e substituído por "AES.block_size"
Alexis
2
Por que você está usando a chave? Se você espera que isso seja algo como uma senha, não deve usar o SHA256; melhor usar uma função de derivação de chave, como PBKDF2, que o PyCrypto fornece.
21717 Twilightp
5
@Chris - SHA256 fornece um hash de 32 bytes - uma chave de tamanho perfeito para o AES256. A geração / derivação de uma chave é assumida como aleatória / segura e deve estar fora do escopo do código de criptografia / descriptografia - o hash é apenas uma garantia de que a chave é utilizável com a cifra selecionada.
zwer
2
no acesso _pad self.bs é necessário e em _unpad não precisa
mnothic
149

Você pode precisar das duas funções a seguir: pad- para pad (ao fazer criptografia) e unpad- para unpad (para descriptografar) quando o comprimento da entrada não for 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:])]

Então você está perguntando o comprimento da chave? Você pode usar o md5sum da chave em vez de usá-lo diretamente.

Além disso, de acordo com minha pouca experiência no uso do PyCrypto, o IV é usado para misturar a saída de uma criptografia quando a entrada é a mesma; portanto, o IV é escolhido como uma sequência aleatória e usado como parte da saída de criptografia. use-o para descriptografar a mensagem.

E aqui está minha implementação, espero que seja útil para você:

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
fonte
1
O que acontece se você tiver uma entrada que seja exatamente um múltiplo de BLOCK_SIZE? Eu acho que a função unpad iria ficar um pouco confuso ...
Kjir
2
@Kjir, uma sequência do valor chr (BS) de comprimento BLOCK_SIZE será anexada aos dados de origem.
Marcus
1
@ Marcus a padfunção está quebrada (pelo menos no Py3), substitua por s[:-ord(s[len(s)-1:])]para que ela funcione entre as versões.
Torxed
2
Função almofada @Torxed é beneficie em CryptoUtil.Padding.pad () com pycryptodome (continuação PyCrypto)
Comte
2
Por que não apenas ter um caractere constante como o caractere de preenchimento?
Inaimathi 25/04
16

Deixe-me responder à sua pergunta sobre "modos". AES256 é um tipo de cifra de bloco . Ele recebe como entrada uma chave de 32 bytes e uma string de 16 bytes, chamada de bloco e gera um bloco. Usamos o AES em um modo de operação para criptografar. As soluções acima sugerem o uso de CBC, que é um exemplo. Outro é chamado CTR, e é um pouco mais 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)

Isso geralmente é chamado de AES-CTR. Eu recomendaria cautela ao usar o AES-CBC com o PyCrypto . O motivo é que requer que você especifique o esquema de preenchimento , conforme exemplificado pelas outras soluções fornecidas. Em geral, se você não tomar muito cuidado com o preenchimento, ataques que quebram completamente a criptografia!

Agora, é importante observar que a chave deve ser uma sequência aleatória de 32 bytes ; uma senha não é suficiente. Normalmente, a chave é gerada da seguinte maneira:

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

Uma chave também pode ser derivada de uma senha :

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

Algumas soluções acima sugerem o uso do SHA256 para derivar a chave, mas isso geralmente é considerado uma má prática criptográfica . Confira a Wikipedia para mais informações sobre os modos de operação.

tweaksp
fonte
iv_int = int (binascii.hexlify (iv), 16) não funciona, substitua-o por iv_int = int (binascii.hexlify (iv), 16) mais o 'import binascii' e deve funcionar (no Python 3.x ), caso contrário, um ótimo trabalho!
Valmond
Observe que é melhor usar os modos de criptografia automática como AES-GCM. O GCM usa internamente o modo CTR.
Kelalaka 15/09/19
Esse código causa "TypeError: o tipo de objeto <classe 'str'> não pode ser passado para o código C"
Da Woon Jung
7

Para alguém que gostaria de usar o urlsafe_b64encode e o urlsafe_b64decode, aqui está a versão que está funcionando para mim (depois de passar algum tempo com o 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
fonte
6

Você pode obter uma frase secreta de uma senha arbitrária usando uma função de hash criptográfica ( NÃO embutida no Python hash) como SHA-1 ou SHA-256. O Python inclui suporte para ambos em sua biblioteca padrão:

import hashlib

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

Você pode truncar um valor de hash criptográfico apenas usando [:16]ou [:24]e ele manterá sua segurança até o comprimento especificado.

nneonneo
fonte
13
Você não deve usar uma função de hash da família SHA para gerar uma chave a partir de uma senha - consulte o ensaio de Coda Hale sobre o tópico . Considere usar uma função de derivação de chave real , como scrypt . (Ensaio de Coda Hale foi escrito, antes da publicação do Scrypt.)
Benjamin Barenblat
7
Para futuros leitores, se você deseja derivar uma chave de uma senha, procure por PBKDF2. É bastante fácil de usar em python ( pypi.python.org/pypi/pbkdf2 ). Se você estiver procurando senhas de hash, no entanto, o bcrypt é uma opção melhor.
C # Fairweather #
6

Grato pelas outras respostas que inspiraram, mas não funcionaram para mim.

Depois de passar horas tentando descobrir como ele funciona, eu vim com a implementação abaixo com o mais novo PyCryptodomex biblioteca (que é outra história como eu consegui configurá-lo atrás de proxy, no Windows, em um virtualenv .. ufa)

Trabalho sobre sua implementação, lembre-se de anotar as etapas de preenchimento, codificação e criptografia (e vice-versa). Você deve embalar e desembalar tendo em mente o pedido.

import base64
hashlib de importação
Importação de criptografia AES
Importação aleatória get_random_bytes

__key__ = hashlib.sha256 (chave com 16 caracteres '). digest ()

def encrypt (bruto):
    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)
    cipher = AES.new (chave = __key__, modo = AES.MODE_CFB, iv = iv)
    retornar base64.b64encode (iv + cipher.encrypt (raw))

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

    enc = base64.b64decode (enc)
    iv = enc [: tamanho do bloco AES]
    cipher = AES.new (__ key__, AES.MODE_CFB, iv)
    retornar unpad (base64.b64decode (cipher.decrypt (enc [AES.block_size:])). decode ('utf8'))
cenkarioz
fonte
Obrigado por um exemplo funcional disso com as bibliotecas PyCryptodomeX. Isso é bastante útil!
Ygramul 31/03
5

Para o benefício de outros, eis a minha implementação de descriptografia, que eu consegui combinando as respostas do @Cyril e do @Marcus. Isso pressupõe que isso seja recebido via solicitação HTTP com o encryptedText citado e a base64 codificada.

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
fonte
5

Outra visão sobre isso (fortemente derivada das soluções acima), mas

  • usa nulo para preenchimento
  • não usa lambda (nunca foi um fã)
  • testado com python 2.7 e 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
fonte
Isso não funcionará se o byte de entrada [] tiver nulos à direita, porque na função decrypt () você irá comer seus nulos de preenchimento MAIS quaisquer nulos à direita.
Buzz Moschetti
Sim, como afirmei acima, essa lógica é preenchida com valores nulos. Se os itens que deseja codificar / decodificar pode ter valores nulos à direita, melhor utilizar uma das outras soluções aqui
mikee
3

Eu usei tanto Cryptoe PyCryptodomexbiblioteca e é incrivelmente 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
fonte
2

É um pouco tarde, mas acho que isso será muito útil. Ninguém menciona o esquema de uso como o preenchimento PKCS # 7. Em vez disso, você pode usá-lo nas funções anteriores para preencher (quando criptografar) e unpad (quando descriptografar) .i fornecerá o código-fonte completo abaixo.

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
fonte
Não sei quem diminuiu a resposta, mas ficaria curioso para saber o porquê. Talvez este método não seja seguro? Uma explicação seria ótima.
Cyril N.
1
@CyrilN. Esta resposta sugere que o hash da senha com uma única chamada do SHA-256 é suficiente. Não é. Você realmente deve usar PBKDF2 ou similar para derivação de chave de uma senha usando uma grande contagem de iterações.
precisa
Obrigado pelo detalhe @ArtjomB.!
Cyril N.
Eu tenho uma chave e também uma chave iv com comprimento 44. Como posso usar suas funções ?! todos os algoritmos na internet que eu encontrei, tem problema com o tempo de minha chave 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
fonte
10
Forneça não apenas código, mas também explique o que você está fazendo e por que isso é melhor / qual é a diferença para as respostas existentes.
Florian Koch
Substitua md5.new (key) .digest () por md5 (key) .digest () e ele funciona como um encanto!
A STEFANI