说明
OTP动态密码常见是两种实现: HOTP
(事件计数密码)与TOTP
(基于时间密码).
分别对应着RFC 协议 RFC4266 和 RFC6238
而实际上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): 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() hmac_hash = bytearray(hmac_digest) offset = hmac_hash[-1] & 0xf 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) 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))
|