标签归档:one-time-password

Google Authenticator在Python中的实现

问题:Google Authenticator在Python中的实现

我正在尝试使用可以通过Google Authenticator应用程序生成的一次性密码。

Google身份验证器的功能

基本上,Google身份验证器实现两种类型的密码:

  • HOTP-基于HMAC的一次性密码,这意味着密码会在每次呼叫时更改,以符合RFC4226的要求,并且
  • TOTP-基于时间的一次性密码,每30秒更改一次(据我所知)。

Google身份验证器也可以在此处作为开源使用:code.google.com/p/google-authenticator

当前代码

我一直在寻找用于生成HOTP和TOTP密码的现有解决方案,但没有找到太多。我拥有的代码是负责生成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

我面临的问题是,使用上述代码生成的密码与使用Android的Google Authenticator应用生成的密码不同。即使我努力过多个intervals_no值(第一完全相同10000,开头intervals_no = 0),以secret等于在GA的应用程序内提供的密钥。

我有问题

我的问题是:

  1. 我究竟做错了什么?
  2. 如何在Python中生成HOTP和/或TOTP?
  3. 有没有现成的Python库?

总结一下:请给我提供一些线索,这些线索将有助于我在Python代码中实现Google Authenticator身份验证。

I am trying to use one-time passwords that can be generated using Google Authenticator application.

What Google Authenticator does

Basically, Google Authenticator implements two types of passwords:

  • HOTP – HMAC-based One-Time Password, which means the password is changed with each call, in compliance to RFC4226, and
  • TOTP – Time-based One-Time Password, which changes for every 30-seconds period (as far as I know).

Google Authenticator is also available as Open Source here: code.google.com/p/google-authenticator

Current code

I was looking for existing solutions to generate HOTP and TOTP passwords, but did not find much. The code I have is the following snippet responsible for generating 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

The problem I am facing is that the password I generate using the above code is not the same as generated using Google Authenticator app for Android. Even though I tried multiple intervals_no values (exactly first 10000, beginning with intervals_no = 0), with secret being equal to key provided within the GA app.

Questions I have

My questions are:

  1. What am I doing wrong?
  2. How can I generate HOTP and/or TOTP in Python?
  3. Are there any existing Python libraries for this?

To sum up: please give me any clues that will help me implement Google Authenticator authentication within my Python code.


回答 0

我想悬赏我的问题,但是我成功地找到了解决方案。我的问题似乎与不正确的secret键值有关(它必须是base64.b32decode()功能的正确参数)。

下面,我发布完整的工作解决方案,并说明如何使用它。

下面的代码就足够了。我也将它作为名为onetimepass的单独模块上传到GitHub (可在此处找到: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)

它具有两个功能:

  • get_hotp_token() 生成一次性令牌(单次使用后应失效),
  • get_totp_token() 根据时间生成令牌(每30秒更改一次),

参量

关于参数:

  • secret 是服务器(上述脚本)和客户端(Google身份验证器,通过在应用程序中将其作为密码提供)已知的秘密值,
  • intervals_no 是每代令牌生成后增加的数字(可能应该在服务器上通过检查过去一次成功的整数之后检查一些有限数量的整数来解决)

如何使用它

  1. 生成secret(必须是的正确参数base64.b32decode())-最好为16个字符(无=符号),因为它肯定适用于脚本和Google身份验证器。
  2. 使用get_hotp_token(),如果你想在每次使用后无效一次性密码。在Google Authenticator中,我提到了这种基于计数器的密码。为了在服务器上检查它,您将需要检查多个值intervals_no(因为您没有隔离该用户由于某种原因未在请求之间生成传递的密码),但不能小于最后一个工作intervals_no值(因此您可能应该存储它)某处)。
  3. get_totp_token()如果您希望令牌以30秒的间隔工作,请使用。您必须确保两个系统都设置了正确的时间(这意味着它们在任何给定的时间点都生成相同的Unix时间戳)。
  4. 确保保护自己免受暴力攻击。如果使用基于时间的密码,则在不到30秒的时间内尝试输入1000000个值将使您有100%的机会猜测密码。在基于HMAC的密码(HOTP)的情况下,情况甚至更糟。

当将以下代码用于基于HMAC的一次性密码时:

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

您将得到以下结果:

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

对应于Google Authenticator应用生成的令牌(除非少于6个符号,应用会在开头添加零,以达到6个字符的长度)。

I wanted to set a bounty on my question, but I have succeeded in creating solution. My problem seemed to be connected with incorrect value of secret key (it must be correct parameter for base64.b32decode() function).

Below I post full working solution with explanation on how to use it.

Code

The following code is enough. I have also uploaded it to GitHub as separate module called onetimepass (available here: 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)

It has two functions:

  • get_hotp_token() generates one-time token (that should invalidate after single use),
  • get_totp_token() generates token based on time (changed in 30-second intervals),

Parameters

When it comes to parameters:

  • secret is a secret value known to server (the above script) and client (Google Authenticator, by providing it as password within application),
  • intervals_no is the number incremeneted after each generation of the token (this should be probably resolved on the server by checking some finite number of integers after last successful one checked in the past)

How to use it

  1. Generate secret (it must be correct parameter for base64.b32decode()) – preferably 16-char (no = signs), as it surely worked for both script and Google Authenticator.
  2. Use get_hotp_token() if you want one-time passwords invalidated after each use. In Google Authenticator this type of passwords i mentioned as based on the counter. For checking it on the server you will need to check several values of intervals_no (as you have no quarantee that user did not generate the pass between the requests for some reason), but not less than the last working intervals_no value (thus you should probably store it somewhere).
  3. Use get_totp_token(), if you want a token working in 30-second intervals. You have to make sure both systems have correct time set (meaning that they both generate the same Unix timestamp in any given moment in time).
  4. Make sure to protect yourself from brute-force attack. If time-based password is used, then trying 1000000 values in less than 30 seconds gives 100% chance of guessing the password. In case of HMAC-based passowrds (HOTPs) it seems to be even worse.

Example

When using the following code for one-time HMAC-based password:

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

you will get the following result:

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

which is corresponding to the tokens generated by the Google Authenticator app (except if shorter than 6 signs, app adds zeros to the beginning to reach a length of 6 chars).


回答 1

我想要一个Python脚本来生成TOTP密码。因此,我编写了python脚本。这是我的实现。我在Wikipedia上有此信息,并且有一些有关HOTP和TOTP的知识可以编写此脚本。

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)

I wanted a python script to generate TOTP password. So, I wrote the python script. This is my implementation. I have this info on wikipedia and some knowledge about HOTP and TOTP to write this 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)