Implementación de Google Authenticator en Python

Estoy tratando de usar contraseñas de un solo uso que pueden generarse usando la aplicación Google Authenticator .

Lo que hace Google Authenticator

Básicamente, Google Authenticator implementa dos tipos de contraseñas:

  • HOTP : contraseña de un solo uso basada en HMAC, lo que significa que la contraseña se cambia con cada llamada, de conformidad con RFC4226 , y
  • TOTP : contraseña de un solo uso basada en el tiempo, que cambia por cada período de 30 segundos (que yo sepa).

Google Authenticator también está disponible como código abierto aquí: code.google.com/p/google-authenticator

Codigo actual

Estaba buscando soluciones existentes para generar contraseñas de HOTP y TOTP, pero no encontré mucho. El código que tengo es el siguiente fragmento de código responsable de generar HOTP:

import hmac, base64, struct, hashlib, time def get_token(secret, digest_mode=hashlib.sha1, intervals_no=None): if intervals_no == None: intervals_no = int(time.time()) // 30 key = base64.b32decode(secret) msg = struct.pack(">Q", intervals_no) h = hmac.new(key, msg, digest_mode).digest() o = ord(h[19]) & 15 h = (struct.unpack(">I", h[o:o+4])[0] & 0x7fffffff) % 1000000 return h 

El problema al que me enfrento es que la contraseña que genero con el código anterior no es la misma que la generada con la aplicación Google Authenticator para Android. A pesar de que intenté varios valores de intervals_no (exactamente los primeros 10000, comenzando con intervals_no = 0 ), con el secret es igual a la clave proporcionada dentro de la aplicación GA.

Preguntas que tengo

Mis preguntas son:

  1. ¿Qué estoy haciendo mal?
  2. ¿Cómo puedo generar HOTP y / o TOTP en Python?
  3. ¿Hay bibliotecas de Python existentes para esto?

Para resumir: por favor, dame alguna pista que me ayude a implementar la autenticación de Google Authenticator dentro de mi código Python.

Quería establecer una recompensa por mi pregunta, pero he logrado crear una solución. Mi problema parecía estar conectado con un valor incorrecto de la clave secret (debe ser el parámetro correcto para la función base64.b32decode() ).

A continuación publico una solución de trabajo completa con una explicación sobre cómo usarla.

Código

El siguiente código es suficiente. También lo he subido a GitHub como módulo separado llamado onetimepass (disponible aquí: https://github.com/tadeck/onetimepass ).

 import hmac, base64, struct, hashlib, time def get_hotp_token(secret, intervals_no): key = base64.b32decode(secret, True) msg = struct.pack(">Q", intervals_no) h = hmac.new(key, msg, hashlib.sha1).digest() o = ord(h[19]) & 15 h = (struct.unpack(">I", h[o:o+4])[0] & 0x7fffffff) % 1000000 return h def get_totp_token(secret): return get_hotp_token(secret, intervals_no=int(time.time())//30) 

Tiene dos funciones:

  • get_hotp_token() genera un token de una sola vez (que debería invalidar después de un solo uso),
  • get_totp_token() genera un token basado en el tiempo (cambiado en intervalos de 30 segundos),

Parámetros

Cuando se trata de parámetros:

  • secret es un valor secreto conocido por el servidor (el script anterior) y el cliente (Google Authenticator, al proporcionarlo como contraseña dentro de la aplicación),
  • intervals_no es el número que se incrementa después de cada generación del token (esto probablemente debería resolverse en el servidor al verificar un número finito de enteros después de que el último fue exitoso en el pasado)

Cómo usarlo

  1. Genere un secret (debe ser el parámetro correcto para base64.b32decode() ), preferiblemente 16 caracteres (no = signos), ya que seguramente funcionó tanto para el script como para Google Authenticator.
  2. Utilice get_hotp_token() si desea que las contraseñas de un solo uso se invaliden después de cada uso. En Google Authenticator, este tipo de contraseñas que mencioné están basadas en el contador. Para verificarlo en el servidor, tendrá que verificar varios valores de intervals_no (ya que no tiene garantía de que el usuario no haya generado el pase entre las solicitudes por alguna razón), pero no menos que el último valor de los intervals_no funciona (por lo tanto, probablemente debería almacenarlo en algún lugar).
  3. Utilice get_totp_token() , si desea que un token funcione en intervalos de 30 segundos. Debe asegurarse de que ambos sistemas tengan el tiempo correcto establecido (lo que significa que ambos generan la misma marca de tiempo Unix en un momento dado).
  4. Asegúrate de protegerte de los ataques de fuerza bruta. Si se utiliza una contraseña basada en el tiempo, al intentar 1000000 valores en menos de 30 segundos se obtiene un 100% de probabilidad de adivinar la contraseña. En el caso de las pasarelas basadas en HMAC (HOTPs) parece ser aún peor.

Ejemplo

Cuando se utiliza el siguiente código para una contraseña basada en HMAC de una sola vez:

 secret = 'MZXW633PN5XW6MZX' for i in xrange(1, 10): print i, get_hotp_token(secret, intervals_no=i) 

Obtendrás el siguiente resultado:

 1 448400 2 656122 3 457125 4 35022 5 401553 6 581333 7 16329 8 529359 9 171710 

que corresponde a los tokens generados por la aplicación Google Authenticator (excepto si tiene menos de 6 signos, la aplicación agrega ceros al principio para alcanzar una longitud de 6 caracteres).

Quería una secuencia de comandos de python para generar la contraseña TOTP. Por lo tanto, escribí el guión de python. Esta es mi implementación. Tengo esta información en wikipedia y algunos conocimientos sobre HOTP y TOTP para escribir este script.

 import hmac, base64, struct, hashlib, time, array def Truncate(hmac_sha1): """ Truncate represents the function that converts an HMAC-SHA-1 value into an HOTP value as defined in Section 5.3. http://tools.ietf.org/html/rfc4226#section-5.3 """ offset = int(hmac_sha1[-1], 16) binary = int(hmac_sha1[(offset * 2):((offset * 2) + 8)], 16) & 0x7fffffff return str(binary) def _long_to_byte_array(long_num): """ helper function to convert a long number into a byte array """ byte_array = array.array('B') for i in reversed(range(0, 8)): byte_array.insert(0, long_num & 0xff) long_num >>= 8 return byte_array def HOTP(K, C, digits=6): """ HOTP accepts key K and counter C optional digits parameter can control the response length returns the OATH integer code with {digits} length """ C_bytes = _long_to_byte_array(C) hmac_sha1 = hmac.new(key=K, msg=C_bytes, digestmod=hashlib.sha1).hexdigest() return Truncate(hmac_sha1)[-digits:] def TOTP(K, digits=6, window=30): """ TOTP is a time-based variant of HOTP. It accepts only key K, since the counter is derived from the current time optional digits parameter can control the response length optional window parameter controls the time window in seconds returns the OATH integer code with {digits} length """ C = long(time.time() / window) return HOTP(K, C, digits=digits)