129 votes

Implémentation de Google Authenticator en Python

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

Ce que fait Google Authenticator

Fondamentalement, Google Authenticator met en œuvre deux types de mots de passe :

  • HOTP - Mot de passe unique basé sur le code HMAC, ce qui signifie que le mot de passe est changé à chaque appel, conformément à la directive sur la protection des données. RFC4226 et
  • TOTP - Mot de passe 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 dont je dispose est le snippet 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 que je rencontre est que le mot de passe que je génère en utilisant le code ci-dessus n'est pas le même que celui généré par l'application Google Authenticator pour Android. Même si j'ai essayé plusieurs fois intervals_no (exactement les 10000 premières valeurs, en commençant par intervals_no = 0 ), avec secret étant égale à la clé fournie dans l'application GA.

Questions que je me pose

Mes questions sont les suivantes :

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

En résumé, veuillez me donner tout indice qui m'aidera à mettre en œuvre l'authentification Google Authenticator dans mon code Python.

188voto

Tadeck Points 37046

Je voulais mettre une prime sur ma question, mais j'ai réussi à créer une solution. Mon problème semble être lié à une valeur incorrecte de secret (il doit s'agir du paramètre correct pour base64.b32decode() fonction).

Ci-dessous, j'affiche une solution complète et fonctionnelle avec des explications sur la façon de l'utiliser.

Code

Le code suivant est suffisant. Je l'ai également téléchargé sur GitHub en tant que module séparé appelé laissez-passer unique (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 à usage unique (qui doit être invalidé après une seule utilisation),
  • get_totp_token() génère un jeton en fonction du temps (modifié par intervalles de 30 secondes),

Paramètres

Quand il s'agit de paramètres :

  • secret est une valeur secrète connue du serveur (le script ci-dessus) et du client (Google Authenticator, en le fournissant comme mot de passe dans l'application),
  • intervals_no est le nombre incrémenté après chaque génération du jeton (ceci 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 (pas de = signes), car il a sûrement fonctionné à la fois pour script et Google Authenticator.
  2. Utilice get_hotp_token() si vous voulez que les mots de passe à usage unique soient invalidés après chaque utilisation. Dans Google Authenticator ce type de mots de passe est mentionné comme étant 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 laissez-passer entre les requêtes pour une raison quelconque), mais pas moins que le dernier laissez-passer en vigueur. intervals_no (vous devriez donc probablement la stocker quelque part).
  3. Utilice get_totp_token() si vous voulez un jeton fonctionnant par intervalles de 30 secondes. Vous devez vous assurer que les deux systèmes sont correctement réglés sur l'heure (ce qui signifie qu'ils génèrent tous 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 chance de deviner le mot de passe. Dans le cas de mots de passe basés sur HMAC (HOTP), cela semble être encore pire.

Ejemplo

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

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'ils sont plus courts que 6 signes, l'application ajoute des zéros au début pour atteindre une longueur de 6 caractères).

3 votes

@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é. Bonne lecture !

1 votes

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. En modifiant la ligne 4 pour lire "key = base64.b32decode(secret, True)", j'ai résolu le problème.

1 votes

@ChrisMoore : J'ai mis à jour le code avec casefold=True donc les gens ne devraient pas avoir de problèmes similaires maintenant. Merci pour votre contribution.

8voto

Anish Shah Points 734

Je voulais un python script pour générer le mot de passe TOTP. Donc, j'ai écrit le script en python. Voici mon implémentation. J'ai ceci info 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)

0 votes

Intéressant, mais vous pourriez le rendre plus compréhensible pour le lecteur. Veuillez rendre les noms de variables plus significatifs, ou ajouter des docstrings. De plus, si vous suivez la PEP8, vous obtiendrez plus de soutien. Avez-vous comparé les performances entre ces deux solutions ? Dernière question : votre solution est-elle compatible avec Google Authenticator (puisque la question portait sur cette solution spécifique) ?

0 votes

@Tadeck J'ai ajouté quelques commentaires. Et j'ai fait mes choses en utilisant ce script. Donc oui, ça devrait fonctionner parfaitement.

4voto

Metal3d Points 395

En suivant la réponse correcte de @tadeck et @Anish-Shah, il existe une méthode plus simple pour obtenir le code sans utiliser struct et éviter les importations supplémentaires :

""" TOTP """
import hmac
import time

def totp(key: bytes):
    """ Calculate TOTP using time and key """
    now = int(time.time() // 30)
    msg = now.to_bytes(8, "big")
    digest = hmac.new(key, msg, "sha1").digest()
    offset = digest[19] & 0xF
    code = digest[offset : offset + 4]
    code = int.from_bytes(code, "big") & 0x7FFFFFFF
    code = code % 1000000
    return "{:06d}".format(code)

Cela fonctionne avec Python 3.

Vous pouvez obtenir le code TOTP actuel en appelant totp(key) où la "clé" est un bytes (généralement la clé décodée en base 32).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X