Python 实现基于 HOTP/TOTP 的验证

基于TOTP的两步验证

最近很多云服务都开启了两步验证方式,其中使用基于 RFC 6238 标准的 TOTP(基于时间的一次性密码) 的服务非常多。当然标准都是开放的,也可以自己写一个玩玩啦~

查看基于 Python 的算法和 Google Authenticator 截图,请狂击:这里

验证客户端首选 Google Authenticator 咯,非常好用的客户端,效果见下:

程序截图

P.S. 什么时候能支持 iPhone 5 的高分屏幕就更好了……

程序在这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#!/usr/bin/env python
 
# Implementation demo of Google Authenticator in Python
# Bill Haofei Gong @ billgong.com
 
# Download Google Authenticator to your mobile:
#  iOS: https://itunes.apple.com/us/app/id388497605?mt=8
#  Android: https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en
#  Other mobile please refers to Google Code project: https://code.google.com/p/google-authenticator/
 
# * HOTP(RFC 4226) counter and 16-character secret is stored in file 'oauth.dat'. 
# * TOTP(RFC 6238) need to be synced with time.
# * Secret is case SENSITIVE!
 
# An example of 2-line preference file 'oauth.dat':
#
# MZXW633PN5XW6MZX
# 6
#
# Which will give HOTP token 581333 as it's 6th token of secret 'MZXW633PN5XW6MZX'. TOTP token is dynamic according to time. 
 
# Version History:
#  12 April 2013 Friday: First version
 
# P.S. HOTP integrity check value is... I don't know. I will figure out sometime later...
 
import sys, time, base64, struct, hmac, hashlib
 
# 从配置文件里读取密钥和 counter
try:
    file = open("oauth.dat", "r").readlines()
except (IOError):
    print 'Error: Cant\'t read preference file'
    exit()
 
secret = file[0].rstrip('\n')
 
# HOTP 使用16字符长的密钥,区分大小写
if len(secret) != 16:
    print 'Error: Preference: Illegal secret length!'
    exit()
 
try:
    hotp_counter = int(file[1])
except (ValueError):
    print 'Error: Preference: Illegal counter of HOTP!'
    exit()
 
# 精华在这里
def get_hotp_token(secret, interval):
    basedSecret = base64.b32decode(secret, True)
    structSecret = struct.pack(">Q", interval)
    hmacSecret = hmac.new(basedSecret, structSecret, hashlib.sha1).digest()
    ordSecret = ord(hmacSecret[19]) & 15
    tokenSecret = (struct.unpack(">I", hmacSecret[ordSecret:ordSecret+4])[0] & 0x7fffffff) % 1000000
    return tokenSecret
 
# TOTP 就是使用时间作为 HOTP 的 interval
def get_totp_token(secret):
    return get_hotp_token(secret, int(time.time())//30)
 
# HOTP 每个密码用完后需要在 counter 加 1,并将 counter 写入配置文件
def increase_hotp_counter():
    file[1] = str(hotp_counter + 1)
    try:
        open("oauth.dat", "w").writelines(file)
    except (IOError):
        print 'Error: Cant\'t write preference file'
        exit()
 
def appinfo():
    return "usage: totp.py [TOTP|HOTP] [TOKEN]\n\nSecret used: " + secret + "\n\nUse following URI to generate QRCode or open in Google Authenticator:\notpauth://hotp/Python:HOTP?secret=" + secret + "\notpauth://totp/Python:TOTP?secret=" + secret
 
try:
    tokenType = sys.argv[1]
    tokenInput = int(sys.argv[2])
except (IndexError):
    print 'Error: Console: Insufficient parameters!'
    print appinfo()
    exit()
except (ValueError): 
    print 'Error: Console: Given token is illegal!'
    print appinfo()
    exit()
 
# 当 token 作为 string 输出时,记得 padding 至 6 位数字
if tokenType in ('hotp', 'HOTP'):
    hotp_token = get_hotp_token(secret, hotp_counter)
    if hotp_token == tokenInput:
        print 'HOTP token is correct!\nCurrent token will become invalid.' 
        increase_hotp_counter()
    else:
        print 'HOTP token is invalid!\nToken should be ' + str(hotp_token).zfill(6)
elif tokenType in ('totp', 'TOTP'):
    totp_token = get_totp_token(secret)
    if totp_token == tokenInput:
        print 'TOTP token is correct!'
    else:
        print 'TOTP token is invalid!\nToken should be ' + str(totp_token).zfill(6)
else:
    print 'Error: Console: Illegal token type!'
    print appinfo()

运行前需要一个配置文件,命名为“oauth.dat”,放置在同一目录下,例如:

1
2
MZXW633PN5XW6MZX
10

格式为:第一行是密钥,第二行是 HOTP counter

运行示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$ ./oauth.py 
Error: Console: Insufficient parameters!
usage: totp.py [TOTP|HOTP] [TOKEN]
 
Secret used: MZXW633PN5XW6MZX
 
Use following URI to generate QRCode or open in Google Authenticator:
otpauth://hotp/Python:HOTP?secret=MZXW633PN5XW6MZX
otpauth://totp/Python:TOTP?secret=MZXW633PN5XW6MZX
 
# TOTP with correct token
$ ./oauth.py totp 879472
TOTP token is correct!
 
# TOTP with incorrect token
$ ./oauth.py totp 123123
TOTP token is invalid!
Token should be 879472
 
# HOTP with incorrect token
$ ./oauth.py hotp 123123
HOTP token is invalid!
Token should be 171710
 
# HOTP with correct token
$ ./oauth.py hotp 171710
HOTP token is correct!
Current token will become invalid.
 
# HOTP with used token
$ ./oauth.py hotp 171710
HOTP token is invalid!
Token should be 930616

Google Authenticator 可以使用 QRCode 添加密钥,下面的 QRCode 含有 TOTP,密钥为 AAAABBBBCCCCDDDD 的 URI:
QRTOTP4A4B4C4D

最后吐槽一句:这东西怎么放 Java 下面写就那么麻烦……看来我还是太笨了

2 Comments

  1. 比尔盖子 says:

    请问有客户端实现吗……

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.