martes, 1 de noviembre de 2011

Introducción a la criptografía moderna con Python (1): ARC4

Con este post damos comienzo a una serie para conocer algunos ejemplos de la criptografía moderna siguiendo un poco el proverbio chino de "Me lo contaron y lo olvidé; lo vi y lo entendí; lo hice y lo aprendí", así que aunque me conformo con que lo entendais, estaría genial que al menos lo intentarais hacer también :)

La primera parada en esta introducción a la criptografía moderna es ARC4 (a.k.a. RC4), un sencillo cifrado de flujo, posiblemente el más usado de su categoría, forma parte de WEP, WPA y del grupo de cifrados que puede usar la familia SSL/TLS.

Cifrados de flujo

Un cifrado de flujo se llama así por que genera un "flujo" de datos a partir de la contraseña, que se combina usando la operación XOR*1 con el "flujo" de texto plano*2 para generar un "flujo" texto cifrado, a diferencia del cifrado en bloque, que solo se aplica a una cierta cantidad de información a la vez, este tipo de cifrados no tienen un límite de cantidad a cifrar.

Nota: iré explicando algunos terminos por si acaso resultan desconocidos, esas palabras irán marcadas con un * y un superíndice para identificarlo, habrá información al final del post.





Dado que la operación XOR es simétrica, este cifrado es simétrico tambien, esto es, se cifra y descifra exactamente de la misma forma.

01
001
110

Por ejemplo, para cifrar "HxC" con el flujo "123", lo primero que tendríamos que hacer es obtener los valores que le asignaríamos a cada símbolo, normalmente se usan los códigos ASCII, por ser la opción más cómoda:

H -> 0x48
x -> 0x78
C -> 0x43

1 -> 0x31
2 -> 0x32
3 -> 0x33

Después haríamos XOR de cada símbolo (ahora representado por un byte) con su pareja:
0x48 ⊕ 0x31
0x78 ⊕ 0x32
0x43 ⊕ 0x33

0x48 -> 01001000
0x31 -> 00110001
---------------------------
              01111001 -> 0x79

Y repitiendo el proceso con las distintas parejas, se va cifrando el flujo.
Si aquí hacemos XOR de la clave con el valor cifrado, se puede ver la simetría al obtener de nuevo el valor original:

0x79 -> 01111001
0x31 -> 00110001
------------------------------
              01001000 -> 0x48

De lo que nos provee ARC4 es de las "parejas" con las que hacer XOR al texto plano.

ARC4: Inicialización de la clave

Volviendo al algoritmo, ARC4 se divide en dos partes, la inicialización de la clave (Key schedule), y la generación de números pseudoaleatorios*3, la inicialización, el que trataremos ahora se encarga de preparar todo para el segundo paso, y es el único que interactua con la clave original.

La inicialización se hace tradicionalmente en dos bucles, el primero inicializa en array "S" (de 256 elementos) con valores del 0 al 255, según su posición, es decir: 0, 1, 2, 3, ... 254, 255... pero como esto es una introducción con python, esto se puede resumir en una sola llamada :)
S = range(256)

Vamos entonces con el segundo bucle, de este no se puede escapar ;), pero primero hay que inicializar un par de variables, llamemoslas 'i' y 'j' . 'i' será un contador de 0 a 255, y en cada iteración 'j' tomará el valor "(j + posición i de S + valor de la posición i de la clave módulo*4 lóngitud de la clave) módulo 256", o en forma de código, que se entiende mejor, si la clave es k:

j = (j + S[i] + ord(k[i % len(k)])) % 256

Solo queda una cosa más que hacer en el bucle, intercambiar los valores de S entre las posiciones 'i' y 'j', con eso la parte de inicialización de la clave quedaría así:
def inicializacion(k):

    S = range(256)
    j = 0

    for i in xrange(256): # Contador de 0 a 255
        j = (j + S[i] + ord(k[i % len(k)])) % 256

        S[i], S[j] = S[j], S[i] # Intercambia S[i] y S[j]

    return S

ARC4: Generación de números pseudoaleatorios

Esta parte del algoritmo es la que se encarga de obtener los datos que se mezclaran con los originales, como a parte de 'S' hay que guardar otro par de variables, inicializadas a 0, llamemoslas de nuevo 'i' y 'j' (aunque no son las mismas que antes), crearemos una clase que lo encapsule todo, para simplificar, lo que llevamos hasta ahora quedaría así:
class ARC4:

    S = []
    i = 0
    j = 0

    def inicializacion(self, k): 
S = range(256)
        j = 0
 
        for i in xrange(256): # Contador de 0 a 255
            j = (j + S[i] + ord(k[i % len(k)])) % 256 
S[i], S[j] = S[j], S[i] # Intercambia S[i] y S[j]

        self.S = S

    def __init__(self, clave):
        self.inicializacion(clave)

Bien, escribamos entonces la función que devuelva el siguiente número pseudoaleatorio, cada vez que queramos tomar un número haremos esto:
  • Incrementar i módulo 256
  • j = ( j + posición i de S ) módulo 256
  • Intercambia S[i] y S[j]
  • K = posición ( posición i de S + posición j de S módulo 256 ) de S
  • El siguiente número es K
Escribamos entonces la función que lo hace:
def siguienteByte(self):

        self.i = (self.i + 1) % 256

        self.j = (self.j + self.S[self.i]) % 256

        self.S[self.i], self.S[self.j] = self.S[self.j], self.S[self.i]

        K = self.S[(self.S[self.i] + self.S[self.j]) % 256]

        return K

Entonces solo nos falta la última parte, que ni siquiera pertenece al algoritmo, sinó que es la común a todos los algoritmos de flujo, si recordamos como funciona, toma cada byte del "flujo" de texto plano y hace XOR con su correspondiente byte del "flujo" de clave, es decir:

def cifra(self, texto):

        cifrado = []
        for caracter in texto: # Por cada caracter

            byte_texto = ord(caracter) # Obtiene el codigo ASCII
            byte_clave = self.siguienteByte() # El numero con el que se mezcla

            byte_cifrado = byte_texto ^ byte_clave # XOR !

            caracter_cifrado = chr(byte_cifrado) # Lo pasamos a caracter

            cifrado.append(caracter_cifrado) # Y lo anhadimos

        return ''.join(cifrado) # Junta todos los caracteres


Y ya está, tenemos nuestro cifrado listo

>>> cifrador = ARC4("HxC")
>>> mensaje = "HackxCrack"
>>> mensaje_cifrado = cifrador.cifra(mensaje)
>>> mensaje_cifrado
'\x91XA\x8b\xb0=\x1c\xe7\xf4\xb5' 
>>> descifrador = ARC4("HxC")
>>> mensaje_descifrado = descifrador.cifra(mensaje_cifrado)
>>> mensaje_descifrado
'HackxCrack'
>>> 

Anexo: Consideraciones de seguridad 

ARC4 no se ganó precisamente fama de ser seguro, en parte por que no lo es por si mismo, y en parte por que no se lo trata como el algoritmo que es, uno ligero, la consecuencia más directa es que es debil si se reutiliza la clave, una forma de paliarlo  es aplicar una función hash a la clave y un nonce, que haría algo como el salt*5 de un hash, aquí hay que tener cuidado con que la clave no se utilice más veces que los posibles nonces, por que sinó volvemos al caso inicial, como pasa con WEP.

Otra cosa a tener en cuenta es que los primeros caracteres adolecen de ciertos problemas, son en cierto modo predecibles, y pueden desvelar la clave, así que es recomendable dejar pasar unos cuantos bytes, como mínimo 256 (un valor con cierto margen seria 3072), por ejemplo haciendo esto antes de cifrar y descifrar:
for i in xrange(3072):
    cifrador.siguienteByte()


Eso es todo por hoy, aquí está el código completo [ http://pastebin.com/wKZSr9eu ], ¿ alguien se anima a mandar su implementación ?

Otros posts de la serie:

*1Operación XOR: OR ( 'ò' en inglés ) exclusivo, dados dos bits, el resultado es 1 si los dos tienen distintos valores y 0 si tienen el mismo, se representa con ⊕.
*2Texto plano o claro: texto antes del cifrado, el que se desea enviar al receptor.
*3 Números pseudoaleatorio: secuencia de números que aunque parece aleatoria, se genera a través de un proceso determinista, las máquinas deterministas como los ordenadores están límitadas a este nivel de aleatoriedad. En este caso es necesario que no sean totalmente aleatorios, porque de serlo no se podría obtener otra secuencia igual y no sería posible descifrar la información.
*4Módulo: De entre todos sus significados, aquí quiere decir, el resto de la división.. sí, se podría haber usado esa palabra, pero en este ámbito se usa "módulo", así que cuanto antes se comprenda mejor ;).
*5 Sal de un hash: Es un número o una secuencia aleatoria que se añade a una contraseña a la que se va a aplicar una función hash para evitar el ataque a través de rainbow tables.

2 comentarios:

  1. ¿Por qué con Python?

    ResponderEliminar
    Respuestas
    1. Por que no es necesario escribir tanto código para hacer cosas como java y permite prestarle menos atención a la máquina y más al proceso que C, por ejemplo.

      Eliminar