Implementación de Google Authenticator en Python

104

Estoy intentando utilizar contraseñas de un solo uso que se pueden generar mediante la aplicación Google Authenticator .

Qué 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 acuerdo con RFC4226 , y
  • TOTP - Contraseña de un solo uso basada en el tiempo, que cambia cada 30 segundos (hasta donde yo sé).

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

Código actual

Estaba buscando soluciones existentes para generar contraseñas HOTP y TOTP, pero no encontré muchas. El código que tengo es el siguiente fragmento 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. Aunque probé varios intervals_novalores (exactamente los primeros 10000, comenzando con intervals_no = 0), secretsiendo 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. ¿Existen bibliotecas de Python para esto?

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

Tadeck
fuente

Respuestas:

152

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

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 un 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 invalidarse después de un solo uso),
  • get_totp_token() genera 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, proporcionándolo como contraseña dentro de la aplicación),
  • intervals_no es el número incrementado después de cada generación del token (esto probablemente debería resolverse en el servidor verificando un número finito de enteros después de la última verificación exitosa en el pasado)

Cómo usarlo

  1. Genere secret(debe ser el parámetro correcto para base64.b32decode()), preferiblemente de 16 caracteres (sin =signos), ya que seguramente funcionó tanto para el script como para Google Authenticator.
  2. Úselo 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, deberá verificar varios valores de intervals_no(ya que no tiene garantía de que el usuario no generó el pase entre las solicitudes por alguna razón), pero no menos que el último trabajointervals_no valor de (por lo tanto, probablemente debería almacenarlo algun lado).
  3. Úselo get_totp_token()si desea que un token funcione en intervalos de 30 segundos. Debe asegurarse de que ambos sistemas tengan la hora configurada correctamente (lo que significa que ambos generan la misma marca de tiempo Unix en cualquier momento dado).
  4. Asegúrate de protegerte de los ataques de fuerza bruta. Si se utiliza una contraseña basada en el tiempo, probar 1000000 valores en menos de 30 segundos da un 100% de posibilidades de adivinar la contraseña. En el caso de passowrds basados ​​en HMAC (HOTP) parece ser aún peor.

Ejemplo

Cuando utilice el siguiente código para una contraseña única basada en HMAC:

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 tienen menos de 6 signos, la aplicación agrega ceros al principio para alcanzar una longitud de 6 caracteres).

Tadeck
fuente
3
@burhan: Si necesita el código, lo he subido también a GitHub (aquí: https://github.com/tadeck/onetimepass ), por lo que debería ser bastante fácil de usar dentro de proyectos como módulo separado. ¡Disfrutar!
Tadeck
1
Tuve un problema con este código porque el 'secreto' que me proporcionó el servicio al que estoy tratando de iniciar sesión estaba en minúsculas, no en mayúsculas. Cambiar la línea 4 para leer "key = base64.b32decode (secret, True)" solucionó el problema por mí.
Chris Moore
1
@ChrisMoore: He actualizado el código casefold=Truepara que la gente no tenga problemas similares ahora. Gracias por tu contribución.
Tadeck
3
Un sitio me acaba de dar un secreto de 23 caracteres. Su código falla con un "TypeError: relleno incorrecto" cuando le doy ese secreto. Al rellenar el secreto, así, se solucionó el problema: key = base64.b32decode (secret + '====' [: 3 - ((len (secret) -1)% 4)], True)
Chris Moore
3
para Python 3: cambio: ord(h[19]) & 15en: o = h[19] & 15 Gracias BTW
Orville
6

Quería un script de Python para generar una contraseña TOTP. Entonces, escribí el script 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)
Anish Shah
fuente
Interesante, pero es posible que desee hacerlo más comprensible para el lector. Haga que los nombres de las variables sean más significativos o agregue cadenas de documentos. Además, seguir PEP8 puede brindarle más apoyo. ¿Comparó el rendimiento entre estas dos soluciones? Última pregunta: ¿su solución es compatible con Google Authenticator (ya que la pregunta era sobre esta solución específica)?
Tadeck
@Tadeck He agregado algunos comentarios. Y terminé mis cosas usando este script. así que sí, debería funcionar perfectamente.
Anish Shah