请留意:本文所述的方法仅是经过资料收集和讨论所提出的一种原理假设,并非微信/支付宝官方披露的具体方法。
疑问
在信息时代,路边摊都能接受支付宝付款。于是,我一直有个疑问。
1. 微信/支付宝/Paypal 的条形码支付/二维码支付是如何实现的?它们安全吗?
2. 为什么用户不需要联网,也能完成支付?
条形码/二维码支付的特点
二维码支付主要有“用户扫码”和“商家扫码”(反扫)两种。“用户扫码”是商家提供二维码,用户手机客户端扫码,确认购物信息后进行支付。“商家扫码”(“刷卡支付”)则是用户出示二维码,商户扫描该二维码进行扣款。
“用户扫码”的二维码实际是个购物网站的链接,扫描后的流程与我们通常的网上购物差异不大。
“商家扫码”(“刷卡支付”)则是由用户的手机客户端生成一串“支付码”(如下图),商家读取支付码后,交由支付网关进行清算。据观察,它有如下特点:
1. 用户的手机客户端不需要联网,即可生成支付码并完成交易(第一次使用时,需要联网验证支付密码,来开启扫码支付功能);
2. 这串18位的支付码是动态变化的,大约30秒动态变化一次。
本文将探讨“商家扫码”(“刷卡支付”)背后的原理。
“刷卡支付”的流程
“刷卡支付”分为“免密支付”和“验密支付”两种,前者用户无须联网无须输入支付密码,后者需要用户联网输入密码以完成交易。本文接下来讨论的,是“刷卡支付”中的“免密支付”。
“免密刷卡支付”的流程大致如下:
1. 商家生成订单,告知用户消费金额;
2. 用户打开客户端,进入条码界面;
3. 商家使用扫码设备读取支付码,调用 API 将支付信息提交支付网关进行处理;
4. 支付网关验证商家和用户的信息,进行扣款后向商家返回支付结果;
5. 用户收到扣款消息(银行短信等),交易完成;
微信支付的流程如下图所示(来源 https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=5_4 )
另外,使用微信免密支付需要满足以下规则:
◆ 支付金额>1000元的交易需要验证用户支付密码
◆ 用户账号每天最多有5笔交易可以免密,超过后需要验证密码
◆ 微信支付后台判断用户支付行为有异常情况,符合免密规则的交易也会要求验证密码
来源 https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=5_2
一次性密码 (OTP)
在进一步讨论“刷卡支付”前,我们首先要了解一次性密码(One Time Password, OTP)。常用的一次性密码包括 HOTP (RFC 4226) 和 TOTP (RFC 6238)。
在使用 HOTP (HMAC-Based One-Time Password) 之前,双方要事先约定 shared_secret. 往后每次进行验证,HTOP 将根据 shared_secret(K) 和 计数器(C),生成一个6-8位的动态口令。
动态口令是一次性的,每次生成动态口令时,计数器C会加1,生成的新口令与之前的口令完全无关。使用 OTP 无需暴露 shared_secret 即可验证身份。假如第三方截获了某个动态口令,也无法反推出 shared_secret,更无法推导出下一个动态口令。
下面以用户登录网站为例,简述 HOTP 验证的大致方法:
1. 在最开始, 网站和用户约定一个 shared_secret(K)。
2. 往后,用户每次登录网站时,通过 HOTP 生成一个6位的动态口令(user_token),并提供给网站:
user_token = HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))
3. 因为网站与用户拥有同样的 shared_secret(K) 和计数器(C),所以网站也可以算出一个6位的动态口令:
web_token = HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))
4. 随后,网站可以通过对比动态口令校验用户身份:
if ( user_token == web_token ) accept(); else reject();
另一种算法 TOTP (Time-Based One-Time Password),是在 HOTP 基础上的实现。它用时间代替 HOTP 中的计数器, C = Unix_time / step,通常 step = 30s.
客户端如何生成支付码?
了解OTP后,我们可以猜想微信支付是如何生成支付码的。
支付码最少需要包含 [账户信息 + 动态密码],账户信息可以是一个静态的能映射到支付帐号的数字,动态密码可以通过 OTP 生成。然而从实际中,我们观察到微信/支付宝的支付码在每次刷新时,几乎每一位都在变化,那账户信息藏哪呢? 北邮人论坛的 hdyvip 提供了一个思路:
全部hash肯定不行,要能识别出用户来才可以。
我说的一部分标示用户,一部分是密码并不是指特定几位数的。
这个16位应该是编码之后的。比如:
提前商定一个数,至少要大于用户数,质数最好(全局唯一)
用一次性密码乘以这个数加上用户的数字id
当然这种方法并不能保证13是不变的,我只是举个例子。
按照这个思路,客户端可按如下方法生成支付码:
约定的质数 > 用户总数 6位动态口令 = TOTP(shared_secret) 支付码 = 账户ID + 6位动态口令 * 约定的质数
微信/支付宝如何验证账户?
确定好客户端如何生成密码,支付网关就可以从支付码中提取信息了。
账户ID = 支付码 mod 约定的质数 6位动态口令 = 支付码 / 约定的质数
因为 shared_secret 是用户和支付网关都知道的(没有第三方知道),所以支付网关也可以用同样算法得出6位动态口令进行对比,从而验证用户身份。
演示
我们可以虚构一个客户端和一个服务端进行尝试。客户端与服务端共享一个 shared_secret 和 shared_prime (约定的质数),客户端生成支付码,服务端接收并验证用户身份。
1. 客户端, client.py .
#!/usr/bin/python # # Demo for Barcode Payment client. # # Usage: python client.py # Input: null # Output: 11-digit barcode for payment. # # Note that share_secret and user_id is pre-set here # for the convenience of demostration. # In read world, they should not be set in program, # instead each user_id should map with its own unique # shared_secret. # # feichashao@gmail.com # # 2016.May.14 # import pyotp # pre-set variables shared_secret = u'NUC5JRHENP53CJK4' user_id = 66666 shared_prime = 99991 # Generate token totp = pyotp.TOTP(shared_secret) token = int(totp.now()) # Generate 11-digit barcode barcode = user_id + shared_prime * token print "%011d" % barcode
2. 服务端,server.py .
#!/usr/bin/python # # Demo for Barcode Payment server. # # Usage: python server.py barcode # Input: (int) 11-digit barcode for payment. # Output: (string) 'Accept' or 'Reject' # # Note that share_secret and user_id is pre-set here # for the convenience of demostration. # In read world, they should not be set in program, # instead each user_id should map with its own unique # shared_secret. # # feichashao@gmail.com # # 2016.May.14 # import sys import pyotp # pre-set variables shared_secret = u'NUC5JRHENP53CJK4' user_id = 66666 shared_prime = 99991 # Extract barcode try: barcode = int(sys.argv[1]) except: barcode = 0 income_uid = barcode % shared_prime token = barcode / shared_prime # Verify account totp = pyotp.TOTP(shared_secret) result = ( income_uid == user_id ) and totp.verify( str(token).zfill(6) ) # Print result if result: print 'Accept' else: print 'Reject'
3. 尝试一次有效的验证。
### 客户端生成支付码 ~/TOTP$ python client.py 06412089532 ### 商家获取支付码 06412089532,提交到服务端进行验证。 ~/TOTP$ python server.py 06412089532 Accept
4. 尝试使用一个虚假的支付码
~/TOTP$ python client.py 26972238952 ~/TOTP$ python server.py 88888888888 Reject
5. 验证支付码的时效性(默认每个支付码30秒有效)
### 获取支付码后马上进行验证 ~/TOTP$ barcode=`python client.py`; python server.py $barcode Accept ### 获取支付码后等待30s进行验证 ~/TOTP$ barcode=`python client.py`; sleep 30s; python server.py $barcode Reject
参考资料
[With Payment Code, PayPal Taps QR Codes And Existing Hardware For Large Retailer Mobile Payments]
http://techcrunch.com/2013/10/08/with-payment-code-paypal-taps-qr-codes-and-existing-hardware-for-retail-mobile-payments/
[ PayPal offers QR codes for retail-store purchases ]
http://www.cnet.com/news/paypal-offers-qr-codes-for-retail-store-purchases/
[微信支付开发者文档] https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=5_4