Implementação do Google Authenticator em Python

104

Estou tentando usar senhas de uso único que podem ser geradas usando o aplicativo Google Authenticator .

O que o Google Authenticator faz

Basicamente, o Google Authenticator implementa dois tipos de senhas:

  • HOTP - Senha de uso único baseada em HMAC, o que significa que a senha é alterada a cada chamada, em conformidade com RFC4226 , e
  • TOTP - Senha de uso único baseada em tempo, que muda a cada período de 30 segundos (até onde eu sei).

O Google Authenticator também está disponível como código aberto aqui: code.google.com/p/google-authenticator

Código atual

Eu estava procurando soluções existentes para gerar senhas HOTP e TOTP, mas não encontrei muito. O código que tenho é o seguinte snippet responsável por gerar 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

O problema que estou enfrentando é que a senha que gero usando o código acima não é a mesma que gerada usando o aplicativo Google Authenticator para Android. Embora eu tenha tentado vários intervals_novalores (exatamente os primeiros 10000, começando com intervals_no = 0), secretsendo igual à chave fornecida no aplicativo GA.

Perguntas que tenho

Minhas perguntas são:

  1. O que estou fazendo de errado?
  2. Como posso gerar HOTP e / ou TOTP em Python?
  3. Existe alguma biblioteca Python existente para isso?

Resumindo: dê-me alguma pista que me ajude a implementar a autenticação do Google Authenticator no meu código Python.

Tadeck
fonte

Respostas:

152

Queria lançar uma recompensa à minha pergunta, mas consegui criar uma solução. Meu problema parecia estar conectado com o valor incorreto da secretchave (deve ser o parâmetro correto para a base64.b32decode()função).

Abaixo eu posto uma solução completa de trabalho com explicação de como usá-la.

Código

O código a seguir é suficiente. Também fiz o upload para o GitHub como um módulo separado chamado onetimepass (disponível aqui: 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)

Tem duas funções:

  • get_hotp_token() gera token único (que deve ser invalidado após o uso único),
  • get_totp_token() gera token com base no tempo (alterado em intervalos de 30 segundos),

Parâmetros

Quando se trata de parâmetros:

  • secret é um valor secreto conhecido pelo servidor (o script acima) e pelo cliente (Google Authenticator, fornecendo-o como senha no aplicativo),
  • intervals_no é o número incrementado após cada geração do token (isso provavelmente deve ser resolvido no servidor verificando algum número finito de inteiros após o último bem-sucedido verificado no passado)

Como usá-lo

  1. Gerar secret(deve ser o parâmetro correto para base64.b32decode()) - de preferência 16 caracteres (sem =sinais), pois certamente funcionou para o script e o Google Authenticator.
  2. Use get_hotp_token()se quiser que as senhas de uso único sejam invalidadas após cada uso. No Google Authenticator, mencionei esse tipo de senha com base no contador. Para verificá-lo no servidor você precisará verificar vários valores de intervals_no(já que você não tem garantia de que o usuário não gerou a passagem entre as solicitações por algum motivo), mas não menos que o último intervals_novalor de trabalho (portanto, você provavelmente deve armazená-lo algum lugar).
  3. Use get_totp_token(), se desejar um token funcionando em intervalos de 30 segundos. Você deve certificar-se de que ambos os sistemas têm o conjunto de tempo correto (o que significa que ambos geram o mesmo carimbo de data / hora Unix em qualquer momento).
  4. Certifique-se de se proteger de ataques de força bruta. Se a senha baseada em tempo for usada, tentar 1.000.000 de valores em menos de 30 segundos oferece 100% de chance de adivinhar a senha. No caso de senhas baseadas em HMAC (HOTPs), parece ser ainda pior.

Exemplo

Ao usar o seguinte código para senha única baseada em HMAC:

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

você obterá o seguinte resultado:

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

que corresponde aos tokens gerados pelo aplicativo Google Authenticator (exceto se menor que 6 sinais, o aplicativo adiciona zeros ao início para atingir um comprimento de 6 caracteres).

Tadeck
fonte
3
@burhan: Se você precisar do código, eu carreguei também no GitHub (aqui: https://github.com/tadeck/onetimepass ), então deve ser bem fácil de usá-lo em projetos como um módulo separado. Aproveitar!
Tadeck
1
Tive um problema com este código porque o 'segredo' que me foi fornecido pelo serviço no qual estou tentando fazer login estava em letras minúsculas, não em maiúsculas. Alterar a linha 4 para ler "key = base64.b32decode (secret, True)" corrigiu o problema para mim.
Chris Moore
1
@ChrisMoore: Eu atualizei o código casefold=Truepara que as pessoas não tenham problemas semelhantes agora. Obrigado pela sua contribuição.
Tadeck
3
Acabo de receber um segredo de 23 caracteres de um site. Seu código falha com um "TypeError: Preenchimento incorreto" quando dou esse segredo. O preenchimento do segredo, assim, corrigiu o problema: key = base64.b32decode (secret + '====' [: 3 - ((len (secret) -1)% 4)], True)
Chris Moore
3
para python 3: mudança: ord(h[19]) & 15para: o = h[19] & 15 obrigado BTW
Orville
6

Eu queria um script python para gerar a senha TOTP. Então, eu escrevi o script python. Esta é minha implementação. Eu tenho essas informações na wikipedia e algum conhecimento sobre HOTP e TOTP para escrever 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
fonte
Interessante, mas você pode querer torná-lo mais compreensível para o leitor. Torne os nomes de variáveis ​​mais significativos ou adicione docstrings. Além disso, seguir o PEP8 pode lhe dar mais suporte. Você comparou o desempenho entre essas duas soluções? Última pergunta: a sua solução é compatível com o Google Authenticator (já que a pergunta era sobre esta solução específica)?
Tadeck de
@Tadeck Eu adicionei alguns comentários. E fiz minhas coisas usando este script. então sim, deve funcionar perfeitamente.
Anish Shah