原创内容,转载请注明来源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”的要求,但是这两个信息可能适用于其它隐私和个人信息保护的法规,同样需要留意。