¿Cómo funciona AES en CTR para Python con PyCrypto?

Estoy usando python 2.7.1 Quiero cifrar algo usando AES en modo CTR. Instalé la biblioteca PyCrypto para Python. Escribí el siguiente código:

secret = os.urandom(16) crypto = AES.new(os.urandom(32), AES.MODE_CTR, counter=lambda: secret) encrypted = crypto.encrypt("asdk") print crypto.decrypt(encrypted) 

Tengo que ejecutar crypto.decrypt tantas veces como el tamaño de byte de mi texto sin formato para obtener correctamente los datos descifrados. Es decir:

 encrypted = crypto.encrypt("test") print crypto.decrypt(encrypted) print crypto.decrypt(encrypted) print crypto.decrypt(encrypted) print crypto.decrypt(encrypted) 

La última llamada a descifrar me devolverá el texto sin formato. Las otras salidas de descifrar son algunas cadenas gibberish. Me pregunto si esto es normal o no? ¿Tengo que incluir en un bucle con un tamaño igual al de mi texto simple cada vez o me he equivocado de algo?

De acuerdo con @gertvdijk, AES_CTR es un cifrado de flujo que no necesita relleno. Así que he borrado los códigos relacionados.

Aquí hay algo que sé.

  1. AES.new(...) usar una misma clave (el primer parámetro en AES.new(...) ) en el cifrado y descifrado, y mantener la clave privada.

  2. Los métodos de cifrado / descifrado tienen estado , lo que significa que crypto.en(de)crypt("abcd")==crypto.en(de)crypt("abcd") no siempre es cierto. En su CTR, la respuesta de su contador siempre devuelve una misma cosa, por lo que se queda sin estado cuando se encripta (no estoy 100% seguro de que sea la razón), pero aún así encontramos que es algo descifrado. Como conclusión, siempre debemos usar un nuevo objeto para hacerlos.

  3. La función de counter callback tanto en el cifrado como en el descifrado debería comportarse de la misma manera. En tu caso, es hacer que ambos devuelvan el mismo secreto. Sin embargo, no creo que el secret sea ​​un “secreto”. Puede usar un "secret" generado aleatoriamente y pasarlo a través de los pares que se comunican sin ningún tipo de cifrado para que la otra parte pueda usarlo directamente, siempre que el secret no sea predecible .

Así que escribiría mi código así, espero que ofrezca ayuda.

 import os import hashlib import Crypto.Cipher.AES as AES class Cipher: @staticmethod def md5sum( raw ): m = hashlib.md5() m.update(raw) return m.hexdigest() BS = AES.block_size @staticmethod def pad( s ): """note that the padding is no necessary""" """return s + (Cipher.BS - len(s) % Cipher.BS) * chr(Cipher.BS - len(s) % Cipher.BS)""" return s @staticmethod def unpad( s ): """return s[0:-ord(s[-1])]""" return s def __init__(self, key): self.key = Cipher.md5sum(key) #the state of the counter callback self.cnter_cb_called = 0 self.secret = None def _reset_counter_callback_state( self, secret ): self.cnter_cb_called = 0 self.secret = secret def _counter_callback( self ): """ this function should be stateful """ self.cnter_cb_called += 1 return self.secret[self.cnter_cb_called % Cipher.BS] * Cipher.BS def encrypt(self, raw): secret = os.urandom( Cipher.BS ) #random choose a "secret" which is not secret self._reset_counter_callback_state( secret ) cipher = AES.new( self.key, AES.MODE_CTR, counter = self._counter_callback ) raw_padded = Cipher.pad( raw ) enc_padded = cipher.encrypt( raw_padded ) return secret+enc_padded #yes, it is not secret def decrypt(self, enc): secret = enc[:Cipher.BS] self._reset_counter_callback_state( secret ) cipher = AES.new( self.key, AES.MODE_CTR, counter = self._counter_callback ) enc_padded = enc[Cipher.BS:] #we didn't encrypt the secret, so don't decrypt it raw_padded = cipher.decrypt( enc_padded ) return Cipher.unpad( raw_padded ) 

Algunas pruebas:

 >>> from Cipher import Cipher >>> x = Cipher("this is key") >>> "a"==x.decrypt(x.encrypt("a")) True >>> "b"==x.decrypt(x.encrypt("b")) True >>> "c"==x.decrypt(x.encrypt("c")) True >>> x.encrypt("a")==x.encrypt("a") False #though the input is same, the outputs are different 

Referencia: http://packages.python.org/pycrypto/Crypto.Cipher.blockalgo-module.html#MODE_CTR

Comience con un nuevo objeto criptográfico para nuevas operaciones

La razón por la que esto se comporta como describió en la pregunta es porque su texto plano (4 bytes / 32 bits) es cuatro veces más pequeño que el tamaño en el que trabaja el motor criptográfico para el modo AES elegido (128 bits) y también reutilizando el mismo instancia del objeto criptográfico. Simplemente no reutilice el mismo objeto si está realizando una operación en un nuevo flujo de datos (u otra operación en él). Su problema se resolverá mediante la creación de una instancia de un nuevo objeto crypto para el descifrado, como este:

 # *NEVER* USE A FIXED LIKE COUNTER BELOW IN PRODUCTION CODE. READ THE DOCS. counter = os.urandom(16) key = os.urandom(32) # 256 bits key # Instantiate a crypto object first for encryption encrypto = AES.new(key, AES.MODE_CTR, counter=lambda: counter) encrypted = encrypto.encrypt("asdk") # Instantiate a new crypto object for decryption decrypto = AES.new(key, AES.MODE_CTR, counter=lambda: counter) print decrypto.decrypt(encrypted) # prints "asdk" 

Por qué no se trata de rellenar con AES-CTR

Esta respuesta comenzó como una respuesta a la respuesta de Marcus , en la que inicialmente indicó que el uso del relleno lo resolvería. Si bien entiendo que parecen los síntomas de un problema de relleno, ciertamente no lo es.

El punto central de AES-CTR es que no necesita relleno , ya que es un cifrado de flujo (a diferencia de ECB / CBC y así sucesivamente). Los cifrados de flujo funcionan en flujos de datos, en lugar de agrupar los datos en bloques y encadenarlos en el cálculo criptográfico real.

Voy a explicar la explicación de @ gertvdijk de por qué el cifrado se comportó como lo hizo en la pregunta original (mi edición fue rechazada), pero también señalaré que configurar el contador para devolver un valor estático es un defecto importante y mostrar Cómo configurarlo correctamente.

Restablecer el contador para nuevas operaciones.

La razón por la que esto se comporta como se describe en la pregunta es porque su texto sin formato (4 bytes / 32 bits) es cuatro veces más pequeño que el tamaño de los bloques de flujo de clave que el cifrado CTR genera para el cifrado (16 bytes / 128 bits) .

Debido a que está utilizando el mismo valor fijo una y otra vez en lugar de un contador real, el cifrado continúa escupiendo los mismos bloques de 16 bytes del flujo de claves. Puedes observar esto cifrando 16 bytes nulos repetidamente:

  >>> crypto.encrypt('\x00'*16) '?\\-\xdc\x16`\x05p\x0f\xa7\xca\x82\xdbE\x7f/' >>> crypto.encrypt('\x00'*16) '?\\-\xdc\x16`\x05p\x0f\xa7\xca\x82\xdbE\x7f/' 

Tampoco reinicia el estado del cifrado antes de realizar el descifrado, por lo que los 4 bytes del texto cifrado se descifran en los siguientes 4 bytes de la clave XOR del primer bloque de flujo de salida. Esto también se puede observar mediante el cifrado y descifrado de bytes nulos:

  >>> crypto.encrypt('\x00' * 4) '?\\-\xdc' >>> crypto.decrypt('\x00' * 4) '\x16`\x05p' 

Si esto fuera a funcionar como usted quería, el resultado de ambas operaciones debería ser el mismo. En su lugar, puede ver los primeros cuatro bytes del bloque de 16 bytes en el primer resultado y los segundos cuatro bytes en el segundo resultado.

Después de haber utilizado el bloque de 16 bytes de la clave XOR realizando cuatro operaciones en valores de cuatro bytes (para un total de 16 bytes), se genera un nuevo bloque de la clave XOR. Los primeros cuatro bytes (así como todos los demás) de cada bloque de clave XOR son los mismos, por lo que cuando llama a descifrar esta vez, le devuelve el texto sin formato.

¡Esto es realmente malo! No debe usar AES-CTR de esta manera; es equivalente a un cifrado XOR simple con una clave de repetición de 16 bytes, que puede romperse con bastante facilidad.

Solución

Debe restablecer el estado del cifrado antes de realizar una operación en un nuevo flujo de datos (u otra operación en él), ya que la instancia original ya no estará en el estado inicial correcto. Su problema se resolverá creando una instancia de un nuevo objeto crypto para el descifrado, así como restableciendo la posición del contador y la cadena de claves.

También debe usar una función de contador adecuada que combine un nonce con un valor de contador que aumente cada vez que se genere un nuevo bloque de secuencia de teclas. PyCrypto tiene una clase de contador que puede hacer esto por usted.

 from Crypto.Cipher import AES from Crypto.Util import Counter from Crypto import Random # Set up the counter with a nonce. # 64 bit nonce + 64 bit counter = 128 bit output nonce = Random.get_random_bytes(8) countf = Counter.new(64, nonce) key = Random.get_random_bytes(32) # 256 bits key # Instantiate a crypto object first for encryption encrypto = AES.new(key, AES.MODE_CTR, counter=countf) encrypted = encrypto.encrypt("asdk") # Reset counter and instantiate a new crypto object for decryption countf = Counter.new(64, nonce) decrypto = AES.new(key, AES.MODE_CTR, counter=countf) print decrypto.decrypt(encrypted) # prints "asdk" 

Además de lo que dice Marcus, la clase Crypto.Util.Counter puede usarse para construir su función de locking de contador.