说明

OTP动态密码常见是两种实现: HOTP(事件计数密码)与TOTP(基于时间密码).

分别对应着RFC 协议 RFC4266RFC6238

而实际上TOTP 也是由HOTP演变过来, 利用UNIX时间戳来作为计数输入.

HOTP算法的实现关键是有:

  • 密钥K, 最小长度是 128 位, 推荐160 位长度
  • 计数C, 8 字节的整数, 称为移动因子(moving factor)
  • HMAC哈希算法
  • 密码长度, 一般默认6位

“计数” 是指生成密码时有客户端或者服务器提供的的事件变量

OTP动态密码一般可以用作离线交易校验、动态密码二次验证、时间段动态密码等等.

本文将会用Python来简单实现算法逻辑后, 用例子来讲解如何利用OTP.

实现

算法本身可以用两条表达式来描述:

1
2
HOTP(K,C) = Truncate(HMAC-SHA-256(K,C))
PWD(K,C,digit) = HOTP(K,C) mod 10Digit

通过hmac计算摘要后截断, 将这个数对10的乘方(digit 指数范围 1-10)取模得到最终密码

代码实现

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
import hashlib
import hmac


class OTP(object):

def __init__(self, secret, digits=6, hash=hashlib.sha256):
self.secret = secret.encode("utf-8")
self.digits = digits
self.hash = hash

def generate(self, input):
# 生成8 bytes 整数作为计数器, 如果传入字符串则先计算md5
if isinstance(input, int):
c = (input).to_bytes(8, byteorder='big')
elif isinstance(input, str):
c = hashlib.md5(input.encode("UTF-8")).digest()
elif isinstance(input, bytes):
c = hashlib.md5(input).digest()
else:
raise Exception(TypeError)
# 以一个密钥和一个消息为输入,生成一个消息摘要作为输出
hmac_digest = hmac.new(self.secret, c, self.hash).digest()
# 得到摘要的bytes数组
hmac_hash = bytearray(hmac_digest)
# 取数组最后一位的bytes的低位数作为偏移值
offset = hmac_hash[-1] & 0xf
# 从偏移量开始取4位bytes组合作为基础数
base_str = "".join([hex(i)[2:] for i in hmac_hash[offset:offset + 4]])
base_num = int(base_str, 16)
# 取模得到密码
str_code = str(base_num % 10 ** self.digits)
# 根据长度返回密码, 不足则用0补全
if len(str_code) < self.digits:
str_code = '0' * (self.digits - len(str_code))+ str_code
return str_code

场景应用

以下所有场景均保持终端与服务端使用相同算法、相同密钥的前提

离线交易验证

在一些商业终端机设备(购物机、点唱机、自动按摩机、夹娃娃机等等)上进行交易动作, 通常这种类型的设备网络状态都不稳定, 用户扫码下单支付成功后, 支付服务器无法实时通知设备进行下一步.

这种场合可以在用户支付成功后根据订单信息来生成HTOP动态密码, 支付服务器返回密码给用户, 用户在设备验证该订单即可.

假设终端最新购物订单信息为: 苹果、价格200分、数量1个、下单时间 2017-04-11 12:33:45

生成密码实现

1
2
3
4
5
6
7
8
9
10
import OTP
otp = OTP("HJZY243BSLI26PVW")
order = {
"product": "apple",
"price": 200,
"number": 1,
"created_at": 1491885225
}
input_str = "".join([order[k] for k in sorted(order.keys())])
pwd = opt.generate(input_str)

动态密码验证

常见的使用是手机动态口令,U盾动态口令, 在一些账号密码认证后进行的二次验证确认. 这个动态口令一般有生存时间(默认30秒),在允许最长有效时间内均有效(注意: 这里用允许最长有效时间, 下面会说明)

首先明白OTP算法的外部变量就是”计数”

假设用户使用某账号demo 登录游戏, 这个使用游戏厂商的手机APP来生成动态口令, 触发时间为2017-04-11 12:33:45 (转为UNIX TIMESTAMP 即为1491885225)

计数C的值 1491885225 / 30 = 49729507

密码剩余有效时间为 1491885225 % 30 = 15

口令APP根据剩余有效时间来刷新密码, 服务器则使用当前时间戳计算密码来验证用户输入的密码.

注意: 这种验证了必须要求服务器与客户端的时间做NTP同步)

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import OTP
import time


class TOTP(OTP):

def __init__(self, *args, **kwargs):
# 密码最大允许有效时间
self.interval = kwargs.pop('interval', 30)
self.period = 0
super(TOTP, self).__init__(*args, **kwargs)

def timecode(self, unix_timestamp):
# 生成时间计数
return int(unix_timestamp / self.interval)

def period_life(self, unix_timestamp):
# 密码剩余生存时间
return self.interval - int(unix_timestamp % self.interval)

def now(self):
unix_timestamp = int(time.time())
self.period = self.period_life(unix_timestamp)
return self.generate(self.timecode(unix_timestamp))
1
2
3
4
5
6
7
import TOTP
import base64


ACCOUNT = "demo"
totp = TOTP(base64.b32encode(ACCOUNT.encode("utf-8")).decode("utf-8"))
print("dynamic password: {}, time to live: {}s".format(totp.now(), totp.period))