Implémentation de Google Authenticator en Python

104

J'essaie d'utiliser des mots de passe à usage unique qui peuvent être générés à l'aide de l' application Google Authenticator .

Ce que fait Google Authenticator

Fondamentalement, Google Authenticator implémente deux types de mots de passe:

  • HOTP - Mot de passe à usage unique basé sur HMAC, ce qui signifie que le mot de passe est modifié à chaque appel, conformément à la RFC4226 , et
  • TOTP - Mot de passe à usage unique basé sur le temps, qui change toutes les 30 secondes (pour autant que je sache).

Google Authenticator est également disponible en Open Source ici: code.google.com/p/google-authenticator

Code actuel

Je cherchais des solutions existantes pour générer des mots de passe HOTP et TOTP, mais je n'ai pas trouvé grand-chose. Le code que j'ai est l'extrait de code suivant responsable de la génération de 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

Le problème auquel je suis confronté est que le mot de passe que je génère à l'aide du code ci-dessus n'est pas le même que celui généré à l'aide de l'application Google Authenticator pour Android. Même si j'ai essayé plusieurs intervals_novaleurs (exactement les 10000 premiers, en commençant par intervals_no = 0), secretétant égal à la clé fournie dans l'application GA.

Questions que j'ai

Mes questions sont:

  1. Qu'est-ce que je fais mal?
  2. Comment puis-je générer HOTP et / ou TOTP en Python?
  3. Existe-t-il des bibliothèques Python pour cela?

Pour résumer: veuillez me donner tous les indices qui m'aideront à mettre en œuvre l'authentification Google Authenticator dans mon code Python.

Tadeck
la source

Réponses:

153

Je voulais mettre une prime sur ma question, mais j'ai réussi à créer une solution. Mon problème semblait être lié à une valeur incorrecte de la secretclé (ce doit être un paramètre correct pour la base64.b32decode()fonction).

Ci-dessous, je poste une solution de travail complète avec des explications sur son utilisation.

Code

Le code suivant suffit. Je l'ai également téléchargé sur GitHub en tant que module séparé appelé onetimepass (disponible ici: 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)

Il a deux fonctions:

  • get_hotp_token() génère un jeton unique (qui devrait être invalidé après une seule utilisation),
  • get_totp_token() génère un jeton en fonction du temps (changé toutes les 30 secondes),

Paramètres

En ce qui concerne les paramètres:

  • secret est une valeur secrète connue du serveur (le script ci-dessus) et du client (Google Authenticator, en la fournissant comme mot de passe dans l'application),
  • intervals_no est le nombre incrémenté après chaque génération du jeton (cela devrait probablement être résolu sur le serveur en vérifiant un nombre fini d'entiers après le dernier succès vérifié dans le passé)

Comment l'utiliser

  1. Générer secret(il doit s'agir d'un paramètre correct pour base64.b32decode()) - de préférence 16 caractères (aucun =signe), car cela fonctionnait sûrement pour le script et Google Authenticator.
  2. À utiliser get_hotp_token()si vous souhaitez que les mots de passe à usage unique soient invalidés après chaque utilisation. Dans Google Authenticator, j'ai mentionné ce type de mots de passe comme basé sur le compteur. Pour le vérifier sur le serveur, vous devrez vérifier plusieurs valeurs de intervals_no(car vous n'avez aucune garantie que l'utilisateur n'a pas généré le passage entre les requêtes pour une raison quelconque), mais pas moins que la dernière intervals_novaleur de travail (vous devriez donc probablement le stocker quelque part).
  3. À utiliser get_totp_token()si vous souhaitez qu'un jeton fonctionne toutes les 30 secondes. Vous devez vous assurer que les deux systèmes ont l'heure correcte (ce qui signifie qu'ils génèrent tous les deux le même horodatage Unix à un moment donné).
  4. Assurez-vous de vous protéger contre les attaques par force brute. Si un mot de passe basé sur le temps est utilisé, essayer 1000000 valeurs en moins de 30 secondes donne 100% de chances de deviner le mot de passe. Dans le cas des passowrds basés sur HMAC (HOTP), cela semble être encore pire.

Exemple

Lorsque vous utilisez le code suivant pour un mot de passe HMAC à usage unique:

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

vous obtiendrez le résultat suivant:

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

qui correspond aux jetons générés par l'application Google Authenticator (sauf s'il est inférieur à 6 signes, l'application ajoute des zéros au début pour atteindre une longueur de 6 caractères).

Tadeck
la source
3
@burhan: Si vous avez besoin du code, je l'ai également téléchargé sur GitHub (ici: https://github.com/tadeck/onetimepass ), il devrait donc être assez facile de l'utiliser dans des projets en tant que module séparé. Prendre plaisir!
Tadeck
1
J'ai eu un problème avec ce code car le «secret» qui m'a été fourni par le service auquel j'essaie de me connecter était en minuscules et non en majuscules. Changer la ligne 4 pour lire "key = base64.b32decode (secret, True)" a résolu le problème pour moi.
Chris Moore
1
@ChrisMoore: J'ai mis à jour le code avec casefold=Truepour que les gens ne devraient plus avoir de problèmes similaires maintenant. Merci pour votre contribution.
Tadeck
3
Je viens de recevoir un secret de 23 caractères par un site. Votre code échoue avec un "TypeError: Incorrect padding" lorsque je lui donne ce secret. Remplir le secret, comme ceci, a résolu le problème: key = base64.b32decode (secret + '====' [: 3 - ((len (secret) -1)% 4)], True)
Chris Moore
3
pour python 3: changement: ord(h[19]) & 15en: o = h[19] & 15 Merci BTW
Orville
6

Je voulais un script python pour générer le mot de passe TOTP. Alors, j'ai écrit le script python. Ceci est ma mise en œuvre. J'ai ces informations sur wikipedia et quelques connaissances sur HOTP et TOTP pour écrire ce 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
la source
Intéressant, mais vous voudrez peut-être le rendre plus compréhensible pour le lecteur. Veuillez rendre les noms de variables plus significatifs ou ajouter des docstrings. En outre, suivre PEP8 peut vous apporter plus de soutien. Avez-vous comparé les performances entre ces deux solutions? Dernière question: votre solution est-elle compatible avec Google Authenticator (car la question portait sur cette solution spécifique)?
Tadeck
@Tadeck J'ai ajouté quelques commentaires. Et j'ai fait mes choses en utilisant ce script. alors oui, ça devrait fonctionner parfaitement.
Anish Shah