Implementazione di Google Authenticator in Python


104

Sto cercando di utilizzare password monouso che possono essere generate utilizzando l' applicazione Google Authenticator .

Cosa fa Google Authenticator

Fondamentalmente, Google Authenticator implementa due tipi di password:

  • HOTP - One-Time Password basata su HMAC, il che significa che la password viene modificata a ogni chiamata, in conformità a RFC4226 , e
  • TOTP : password monouso basata sul tempo, che cambia ogni 30 secondi (per quanto ne so).

Google Authenticator è anche disponibile come Open Source qui: code.google.com/p/google-authenticator

Codice corrente

Stavo cercando soluzioni esistenti per generare password HOTP e TOTP, ma non ho trovato molto. Il codice che ho è il seguente frammento responsabile della generazione di 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

Il problema che sto affrontando è che la password generata utilizzando il codice sopra non è la stessa generata utilizzando l'app Google Authenticator per Android. Anche se ho provato più intervals_novalori (esattamente il primo 10000, a partire da intervals_no = 0), con l' secretessere uguale alla chiave fornita all'interno dell'app GA.

Domande che ho

Le mie domande sono:

  1. Che cosa sto facendo di sbagliato?
  2. Come posso generare HOTP e / o TOTP in Python?
  3. Esistono librerie Python esistenti per questo?

Per riassumere: per favore dammi qualche indizio che mi aiuterà a implementare l'autenticazione di Google Authenticator all'interno del mio codice Python.

Risposte:


152

Volevo dare una ricompensa alla mia domanda, ma sono riuscito a creare una soluzione. Il mio problema sembrava essere collegato a un valore errato della secretchiave (deve essere un parametro corretto per la base64.b32decode()funzione).

Di seguito inserisco una soluzione funzionante completa con spiegazione su come usarla.

Codice

Il codice seguente è sufficiente. L'ho anche caricato su GitHub come modulo separato chiamato onetimepass (disponibile qui: 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)

Ha due funzioni:

  • get_hotp_token() genera un token una tantum (che dovrebbe essere invalidato dopo un singolo utilizzo),
  • get_totp_token() genera token in base al tempo (modificato a intervalli di 30 secondi),

parametri

Quando si tratta di parametri:

  • secret è un valore segreto noto al server (lo script sopra) e al client (Google Authenticator, fornendolo come password all'interno dell'applicazione),
  • intervals_no è il numero incrementato dopo ogni generazione del token (questo dovrebbe essere probabilmente risolto sul server controllando un numero finito di interi dopo l'ultimo controllo riuscito in passato)

Come usarlo

  1. Genera secret(deve essere un parametro corretto per base64.b32decode()) - preferibilmente 16 caratteri (nessun =segno), poiché sicuramente ha funzionato sia per lo script che per Google Authenticator.
  2. Utilizzare get_hotp_token()se si desidera che le password monouso vengano invalidate dopo ogni utilizzo. In Google Authenticator questo tipo di password ho menzionato come basato sul contatore. Per controllarlo sul server dovrai controllare diversi valori di intervals_no(dato che non hai la garanzia che l'utente non abbia generato il passaggio tra le richieste per qualche motivo), ma non meno dell'ultimo intervals_novalore funzionante (quindi dovresti probabilmente memorizzarlo da qualche parte).
  3. Utilizzare get_totp_token(), se si desidera che un token funzioni a intervalli di 30 secondi. Devi assicurarti che entrambi i sistemi abbiano impostato l'ora corretta (il che significa che entrambi generano lo stesso timestamp Unix in un dato momento nel tempo).
  4. Assicurati di proteggerti dagli attacchi di forza bruta. Se viene utilizzata una password basata sul tempo, provare 1000000 valori in meno di 30 secondi offre il 100% di possibilità di indovinare la password. In caso di passowrds basati su HMAC (HOTP) sembra essere anche peggio.

Esempio

Quando si utilizza il codice seguente per una password monouso basata su HMAC:

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

otterrai il seguente risultato:

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

che corrisponde ai token generati dall'app Google Authenticator (a meno che non sia inferiore a 6 caratteri, l'app aggiunge zeri all'inizio per raggiungere una lunghezza di 6 caratteri).


3
@burhan: Se hai bisogno del codice, l'ho caricato anche su GitHub (qui: https://github.com/tadeck/onetimepass ), quindi dovrebbe essere abbastanza facile usarlo all'interno dei progetti come modulo separato. Godere!
Tadeck

1
Ho avuto un problema con questo codice perché il "segreto" che mi è stato fornito dal servizio a cui sto tentando di accedere era in minuscolo, non maiuscolo. La modifica della riga 4 in "key = base64.b32decode (secret, True)" ha risolto il problema per me.
Chris Moore

1
@ ChrisMoore: ho aggiornato il codice con casefold=Truecosì le persone non dovrebbero avere problemi simili ora. Grazie per il tuo contributo.
Tadeck

3
Mi è stato appena dato un segreto di 23 caratteri da un sito. Il tuo codice non riesce con un "TypeError: riempimento errato" quando gli do quel segreto. Riempire il segreto, in questo modo, ha risolto il problema: key = base64.b32decode (secret + '====' [: 3 - ((len (secret) -1)% 4)], True)
Chris Moore

3
per Python 3: modifica: ord(h[19]) & 15in: o = h[19] & 15 Grazie BTW
Orville

6

Volevo uno script Python per generare la password TOTP. Quindi, ho scritto lo script Python. Questa è la mia implementazione. Ho queste informazioni su wikipedia e alcune conoscenze su HOTP e TOTP per scrivere questo 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)

Interessante, ma potresti renderlo più comprensibile per il lettore. Rendi i nomi delle variabili più significativi o aggiungi docstrings. Inoltre, seguendo PEP8 potresti ottenere maggiore supporto. Hai confrontato le prestazioni tra queste due soluzioni? Ultima domanda: la tua soluzione è compatibile con Google Authenticator (poiché la domanda riguardava questa soluzione specifica)?
Tadeck

@Tadeck ho aggiunto alcuni commenti. E ho fatto le mie cose usando questo script. quindi sì, dovrebbe funzionare perfettamente.
Anish Shah
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.