原创内容,转载请注明来源https://songyue.wang
注意:此内容仅限用于检票终端,签到系统开发等合法用途,并仅供测试学习使用。如欲应用到生产环境,请务必咨询专业人士,并处理好隐私合规事宜!
之前在香港的CinemaCity看电影,发现他们有个很有意思的设计。客户在网上使用信用卡购买电影票后,到影院之后无需输入取票码,只需要在一个普通的磁卡阅读器(不是那种银行POS机)刷一下购票时使用的信用卡,就可以完成自助取票。
以付款信用卡识别客户,确实是一个非常高效的办法,这次经历之后我想到另外一个应用场景。现在很多演唱会深受“黄牛票”的困扰,虽然国内国外很多主办方都用实名认证来解决,但我们的目的只是想解决“购票人转卖门票”的问题,非要收集客户的legal ID显然没必要。另外,国外的legal ID不像国内一样统一(例如加拿大,各省的身份证,驾照,医保卡,持枪证都是legal ID),在检票场景只能使用机读+视读同时进行的方式,浪费人力。由于购票信用卡是每个人都有的,且无论发卡行和组织都有统一的机读方式,因此更适合。


前置条件
- 树莓派Raspberry Pi Model 3B+单片机(官方定价35美元),并已烧录Raspberry Pi OS进Mircro SD卡
- 磁卡阅读器(淘宝售价70-80人民币)
- (可选)微雪电子OLED显示屏扩展板(淘宝售价70-80人民币)
项目代码
https://github.com/songyuew/raspi-cc-reader
卡片解码
将磁卡阅读器连接电脑,打开文本编辑器刷一下卡,查看读取的磁信息。重复尝试不同发卡行和卡组织的信用卡,找到剖析的规律。
香港汇丰银联提款卡(BIN621443):
%B621443*************^WANG SONGYUE /^YYMM*******?;621443*************=***********?+01621443*************=****************************************=************==0=100020?
美国运通信用卡(BIN3727):
%B3727***********^WANG/SONGYUE ^YYMM************0000000000000000?;3727***********=****************00000?
中银香港万事达信用卡(BIN523984):
%B523984**********^WANG SONGYUE ^YYMM******** *** ?;523984**********=******0100000***?+92523984**********==**********************************************************************************?
通过观察发现卡号(PAN)最先出现,开头以%B标识,以^结尾。紧接着是持卡人姓名,同样以^结尾。之后的YYMM位是卡片有效期。除此之外发卡行还会在磁条内写入其它信息。
如果对解码磁道内其它信息感兴趣,可以阅读《GBT 19584-2010银行卡磁条信息格式和使用规范》国标文件。
剖析磁信息的代码cc_parser.py如下:
import getpass
# 使用卢氏算法检查卡号是否为有效
def validator(pan):
rd1 = list(int(i)*2 for i in pan[-2::-2])
rd2 = 0
for i in rd1:
if i < 10: rd2 += i
else: rd2 += (int(str(i)[0]) + int(str(i)[1]))
rd3 = list(int(i) for i in pan[-1::-2])
return (sum(rd3) + rd2) % 10 == 0
# 获取卡号
def getPan(raw,start,end):
pan = ''
for char in raw[start:]:
if char == end: break
pan += char
# 如果卡片为非银联卡且卡号长度大于16位,截取前16位(只有银联卡可能有超过16位的卡号)
if pan[0] != '6' and len(pan) > 16:
pan = pan[:16]
return pan
# 获取卡片有效期
def getExp(raw,start):
if raw[0] == '%':
exp = raw[raw.find(start,raw.find(start) + 1) + len(start) : raw.find(start,raw.find(start) + 1) + len(start) + 4]
else:
exp = raw[raw.find(start) + len(start) : raw.find(start) + len(start) + 4]
if exp == '0000': return 'N/A'
return exp
# 获取持卡人姓名
def getCH(raw):
if raw[0] == '%':
ch = raw[raw.find('^') + 1 : raw.find('^',raw.find('^') + 1)].strip()
if ch == '': return 'N/A'
return ch
else: return 'N/A'
def reader():
try:
# 在终端界面隐藏磁信息
raw = getpass.getpass(prompt='Swipe the card ==>', stream=None)
pan, exp, ch = 'N/A','N/A','N/A'
if raw[0] == '%':
pan = getPan(raw,2,'^')
exp = getExp(raw,'^')
ch = getCH(raw)
elif raw[0] == ';':
pan = getPan(raw,1,'=')
exp = getExp(raw,'=')
ch = getCH(raw)
elif raw[0] == '+':
pan = getPan(raw,3,'=')
exp = getExp(raw,'==')
ch = getCH(raw)
elif raw == "exit":
return {'status':'exit'}
else:
return {'status':'Invalid card'}
if validator(pan) == False:
return {'status':'Invalid card'}
return {'status':'OK','pan':pan,'exp':exp,'ch':ch}
except:
return {'status':'failure'}
读卡的结果为一个包含PAN,有效期和持卡人姓名的字典,之后可以用来显示在设备屏幕上或者和订单数据库进行比对。为了防止其它磁卡被误读为信用卡,我们在每次刷卡后调用validator(pan)这个函数验证PAN的编码符合卢氏算法(Luhn Algorithm)。
如果业务场景需要识别卡片发卡行或者卡品牌等级(例如Visa Infinite,Visa Signature等)用来会员匹配,可以调用https://bincheck.io/zh/api/subscribe的API,在读卡后将BIN查询结果加到返回的字典里:
import requests
def getBIN(card):
url = "https://bin-ip-checker.p.rapidapi.com/"
querystring = {"bin":card['pan'][:6]}
payload = {"bin":card['pan'][:6]}
# 替换成你的bincheck.io API密钥
headers = {
"x-rapidapi-key": "YOUR_BINCHECK_API_KEY",
"x-rapidapi-host": "bin-ip-checker.p.rapidapi.com",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers, params=querystring).json()
if(response['success'] == True):
for i in response['BIN'].keys():
card[i] = response['BIN'][i]
return card
# 测试卡片(中银香港Visa Infinite Cheers号段)
card1 = {"pan":"4863308888888888","exp":"1223","ch":"John Doe"}
card1 = getBIN(card1)
print(card1)
更新后的字典示例:
{'pan': '4863308888888888', 'exp': '2712', 'ch': 'John Doe', 'valid': True, 'number': 486330, 'length': 6, 'scheme': 'VISA', 'brand': 'VISA', 'type': 'CREDIT', 'level': 'INFINITE', 'is_commercial': 'false', 'is_prepaid': 'false', 'currency': 'HKD', 'issuer': {'name': 'BOC CREDIT CARD (INTERNATIONAL), LTD.', 'website': 'http://www.boci.com.hk', 'phone': ''}, 'country': {'name': 'HONG KONG', 'native': '香港', 'flag': '🇭🇰', 'numeric': '344', 'capital': 'Hong Kong', 'currency': 'HKD', 'currency_name': 'Hong Kong dollar', 'currency_symbol': : '$', 'region': 'Asia', 'subregion': 'Eastern Asia', 'idd': '852', 'alpha2': 'HK', 'alpha3': 'HKG', 'language': 'Chinese', 'language_code': 'ZH'}}
PCI-DSS并没有对BIN列出特别的安全要求,并明文允许在隐藏其它数字的情况下显示PAN前6位和后4位。因此使用在线第三方API查询卡BIN可以是合规的。
业务程序
我这边测试用,就写了一个简单的循环,读取卡片之后继续使用getpass等待刷下一张卡。在实际场景中,可以考虑把卡号hash后之后,与客户数据库进行比对,输出票卡一致/不一致的结果。
屏显模块(可选)
在需要快速采集的场景下,在树莓派上直接安装屏显模块提示读卡结果(例如客户姓名,检票是否通过)会更加方便。首先把微雪OLED显示屏扩展板插入树莓派GPIO接口,并启用树莓派SPI界面。
sudo raspi-config




按照微雪官方教程安装Python依赖包,并下载显示屏驱动和字体包:
sudo apt-get install p7zip-full wget http://www.waveshare.net/w/upload/c/c5/2.23inch-OLED-HAT-Code.7z 7z x 2.23inch-OLED-HAT-Code.7z sudo chmod 777 -R 2.23inch-OLED-HAT-Code cd 2.23inch-OLED-HAT-Code/
把官方样例stats.py(显示树莓派系统状态的程序)末尾的while True代码块改成函数updateScreen(line1,line2,line3,line4),即简单地显示给定的四行文字。每次读卡时调用这个函数更新屏幕即可,或者使用updateScreen("","","","")清空。
def updateScreen(line1,line2,line3,line4):
# Draw a black filled box to clear the image.
draw.rectangle((0,0,width,height), outline=0, fill=0)
draw.text((x,top),line1,font=font,fill=255)
draw.text((x,top+8),line2,font=font,fill=255)
draw.text((x,top+16),line3,font=font,fill=255)
draw.text((x,top+25),line4,font=font,fill=255)
# Display image.
disp.image(image)
disp.display()
如果有树莓派3原装外壳,可以把红色上盖靠近屏显一侧的卡扣用小锯子锯掉。这样在装上屏幕的时候仍然可以把树莓派放进外壳里。


思考
在生产环境应用这种方法的主要考虑为PCI-DSS合规。下表摘自PCI-DSS Quick Reference Guide,有几处跟使用自制机具和软件(非认证POS)读取银行卡相关。

首先需要注意,完整的磁信息在任何时候都不得储存。普通的磁卡阅读器模拟的是键盘输入,所以要特别确保我们在剖析出持卡人信息后,原始输入不会存在系统内,例如文本编辑器或者待命的命令行界面。对于储存卡号(PAN),PCI-DSS的要求原文是“render PAN unreadable”,例如哈希,加密,或者标识化(tokenization),显示PAN需要有“legitimate business need”。因此,在终端屏显(即使是自助机具内部的检修屏显)像测试那样显示没有masking的PAN是存在合规隐患的。如果业务需求是辅助柜员快速完成会员匹配,那么存储持卡人姓名+卡BIN/PAN with masking就已足够。如果业务需求是与识别现有客户,在剖析PAN之后立刻hash则是最便利的合规实践。
对于持卡人姓名和有效期,PCI-DSS并没给出“render unreadable”的要求,但是这两个信息可能适用于其它隐私和个人信息保护的法规,同样需要留意。
