Implementacja Google Authenticator w Pythonie

104

Próbuję użyć haseł jednorazowych, które można wygenerować za pomocą aplikacji Google Authenticator .

Co robi Google Authenticator

Zasadniczo Google Authenticator implementuje dwa typy haseł:

  • HOTP - jednorazowe hasło oparte na HMAC, co oznacza, że ​​hasło jest zmieniane przy każdym połączeniu, zgodnie z RFC4226 , oraz
  • TOTP - jednorazowe hasło czasowe, które zmienia się co 30 sekund (o ile wiem).

Google Authenticator jest również dostępny jako Open Source tutaj: code.google.com/p/google-authenticator

Aktualny kod

Szukałem istniejących rozwiązań do generowania haseł HOTP i TOTP, ale niewiele znalazłem. Kod jaki posiadam to następujący fragment odpowiadający za generowanie 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

Problem polega na tym, że hasło wygenerowane za pomocą powyższego kodu nie jest tym samym, co wygenerowane za pomocą aplikacji Google Authenticator na Androida. Mimo że próbowałem wielu intervals_nowartości (dokładnie pierwszych 10000, zaczynając od intervals_no = 0), z secretrównością klucza dostarczonego w aplikacji GA.

Mam pytania

Moje pytania to:

  1. Co ja robię źle?
  2. Jak mogę wygenerować HOTP i / lub TOTP w Pythonie?
  3. Czy istnieją do tego jakieś biblioteki Pythona?

Podsumowując: proszę o wskazówki, które pomogą mi zaimplementować uwierzytelnianie Google Authenticator w moim kodzie Pythona.

Tadeck
źródło

Odpowiedzi:

153

Chciałem wyznaczyć nagrodę za moje pytanie, ale udało mi się stworzyć rozwiązanie. Mój problem wydawał się być związany z nieprawidłową wartością secretklucza (musi to być właściwy parametr dla base64.b32decode()funkcji).

Poniżej zamieszczam pełne działające rozwiązanie wraz z wyjaśnieniem, jak z niego korzystać.

Kod

Poniższy kod wystarczy. Wrzuciłem go również do GitHub jako oddzielny moduł o nazwie onetimepass (dostępny tutaj: 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)

Posiada dwie funkcje:

  • get_hotp_token() generuje jednorazowy token (który powinien zostać unieważniony po jednorazowym użyciu),
  • get_totp_token() generuje token na podstawie czasu (zmieniany co 30 sekund),

Parametry

Jeśli chodzi o parametry:

  • secret to tajna wartość znana serwerowi (powyższy skrypt) i klientowi (Google Authenticator, podając ją jako hasło w aplikacji),
  • intervals_no to liczba uzyskiwana po każdym wygenerowaniu tokena (prawdopodobnie należy to rozwiązać na serwerze, sprawdzając jakąś skończoną liczbę liczb całkowitych po ostatniej udanej sprawdzonej w przeszłości)

Jak tego użyć

  1. Generuj secret(musi to być poprawny parametr dla base64.b32decode()) - najlepiej 16-znakowy (bez =znaków), ponieważ z pewnością działał zarówno dla skryptu, jak i dla Google Authenticator.
  2. Użyj, get_hotp_token()jeśli chcesz, aby hasła jednorazowe były unieważniane po każdym użyciu. W Google Authenticator tego typu hasła wspomniałem jako oparte na liczniku. Aby sprawdzić to na serwerze, będziesz musiał sprawdzić kilka wartości intervals_no(ponieważ nie masz gwarancji, że użytkownik z jakiegoś powodu nie wygenerował przejścia między żądaniami), ale nie mniej niż ostatnia działająca intervals_nowartość (dlatego prawdopodobnie powinieneś ją zapisać gdzieś).
  3. Użyj get_totp_token(), jeśli chcesz, aby token działał w 30-sekundowych odstępach czasu. Musisz upewnić się, że oba systemy mają ustawiony prawidłowy czas (co oznacza, że ​​oba generują ten sam uniksowy znacznik czasu w dowolnym momencie).
  4. Upewnij się, że chronisz się przed atakiem brutalnej siły. Jeśli używane jest hasło czasowe, wypróbowanie wartości 1000000 w mniej niż 30 sekund daje 100% szansy na odgadnięcie hasła. W przypadku passowrdów opartych na HMAC (HOTPs) wygląda to jeszcze gorzej.

Przykład

W przypadku korzystania z następującego kodu jednorazowego hasła opartego na HMAC:

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

otrzymasz następujący wynik:

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

który odpowiada tokenom wygenerowanym przez aplikację Google Authenticator (z wyjątkiem przypadku, gdy jest krótszy niż 6 znaków, aplikacja dodaje zera na początku, aby uzyskać długość 6 znaków).

Tadeck
źródło
3
@burhan: Jeśli potrzebujesz kodu, wgrałem go również na GitHub (tutaj: https://github.com/tadeck/onetimepass ), więc powinno być dość łatwo używać go w projektach jako oddzielnego modułu. Cieszyć się!
Tadeck
1
Miałem problem z tym kodem, ponieważ „sekret” dostarczony przez usługę, do której próbuję się zalogować, był pisany małymi, a nie wielkimi literami. Zmiana wiersza 4 na „klucz = base64.b32decode (sekret, prawda)” rozwiązała problem.
Chris Moore,
1
@ChrisMoore: Zaktualizowałem kod, casefold=Truewięc ludzie nie powinni mieć teraz podobnych problemów. Dzięki za wkład.
Tadeck
3
Właśnie dostałem 23-znakowy sekret od strony. Twój kod kończy się niepowodzeniem z komunikatem „TypeError: Incorrect padding”, gdy podaję mu ten sekret. Uzupełnienie sekretu w ten sposób rozwiązało problem: klucz = base64.b32decode (sekret + '====' [: 3 - ((len (sekret) -1)% 4)], True)
Chris Moore
3
dla pythona 3: zmień: ord(h[19]) & 15na: o = h[19] & 15 Dzięki BTW
Orville
6

Chciałem, aby skrypt w Pythonie generował hasło TOTP. Więc napisałem skrypt w Pythonie. To jest moja realizacja. Mam te informacje na Wikipedii i trochę wiedzy o HOTP i TOTP, aby napisać ten skrypt.

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
źródło
Ciekawe, ale możesz chcieć uczynić to bardziej zrozumiałym dla czytelnika. Nadaj nazwom zmiennych bardziej zrozumiałe znaczenie lub dodaj ciągi dokumentów. Ponadto przestrzeganie PEP8 może zapewnić Ci większe wsparcie. Czy porównałeś wydajność między tymi dwoma rozwiązaniami? Ostatnie pytanie: czy Twoje rozwiązanie jest kompatybilne z Google Authenticator (ponieważ pytanie dotyczyło tego konkretnego rozwiązania)?
Tadeck
@Tadeck Dodałem kilka komentarzy. I zrobiłem swoje za pomocą tego skryptu. więc tak, powinno działać idealnie.
Anish Shah