PIN block算法
前言
金融加密算法专辑写到现在,已经是第3篇了,前2篇身份证号码的编码规则及校验、银行卡号的编码规则及校验,大家可以看到笔者喜欢“引经据典”,“经典”主要包括国际标准和国家标准,很多国际标准都有对应的国家标准,可以中英文对照互相印证进行学习,对于没有国家标准对应的国际标准,只能靠着笔者拙劣的英文水平勉强学习,如有错误请不吝指教。其实,文中提及的“经典”也只是大家工作学习中的一小部分,笔者查阅到的也未必是最新修订的版本,不过是想要告诉大家,有理有据才值得信服,源起于理论,行动于代码,知其然并知其所以然,是程序员应具备的素养之一。
笔者目前使用的jdk版本是1.6.0_29,Eclipse版本是Juno Release,Build id 20120614-1722。如无特殊说明,本文所有的Java代码都是基于此。
修订记录
版本号 | 修订日期 | 修订说明 |
---|---|---|
V0.1 | 2018/09/07 | 初稿 |
V1.0 | 2018/09/20 | 发布 |
参考资料
- 维基百科 https://en.wikipedia.org/wiki/Personal_identification_number
- 维基百科 https://en.wikipedia.org/wiki/Rainbow_table
- ISO 9564-1-2017 Financial services - Personal Identification Number (PIN) management and security - Part 1: Basic principles and requirements for PINs in card-based systems
- ANS X9.8-2003 BANKING - PERSONAL IDENTIFICATION NUMBER MANAGEMENT AND SECURITY - Part 1: PIN protection principles and techniques for online PIN verification in ATM & POS systems
- Q/CUP 006.4-2015 中国银联股份有限公司企业标准 中国银联银行卡交换系统技术规范(国际卷) 第4部分 数据安全传输控制规范
- 中国银联银行卡交换系统技术规范 2016年4月生效版本升级公告及实施指南
PIN
Personal Identification Number的缩写,ISO 9564中定义:string of numeric digits established as a shared secret between the cardholder and the issuer, for subsequent use to validate authorized card usage。
个人识别号码,作为持卡人和发行者之间的共享秘密而建立的数字字符串,用于随后验证授权卡的使用。从定义理解,专指持卡人
的密码,至于其他的密码,比如你的邮箱密码,严格来说并不能称之为PIN。
PIN规定为4~12位数字,越长越安全,但是从易用的角度考虑不应该超过6位,所以目前的银行卡密码都是6位。但是6位不是唯一的,银行卡密码完全可以支持更少或更多位数。
关于PIN的长度,Wiki上给我们讲了一个小故事:ATM的发明者John Shepherd-Barron最初设想是使用6位长度的密码,但是他的妻子只能记住4位数字,所以国外很多地方是使用4位长度PIN的。毛主席教育我们:妇女能顶半边天。女人是伟大的,女人总在不经意间改变历史。
PAN
Primary Account Number的缩写,ISO 9564中定义:assigned number, composed of an issuer identification number, an individual account identification and an accompanying check digit as specified in ISO/IEC 7812-1, which identifies the card issuer and cardholder。
主账户号码,发行者分配的号码,由发行者识别号码、个人账户标识和ISO/IEC 7812-1中规定的校验数字组成,用于识别发卡机构和持卡人。详见银行卡号的编码规则及校验。
PIN block
ISO 9564标准中定义了5种格式,Format 0,Format 1, Format 2, Format 3,Format 4。
Format 0 PIN block
由PIN域和PAN域异或得到。
- PIN域
共64bit,每4bit为1位十六进制数字,共16位十六进制数字。
- 第1位(1~4bit):固定值0x0(0000)
- 第2位(5~8bit):PIN长度,取值范围0x4(0100) ~ 0xC(1100)
- 第3~16位(9~64bit):PIN,不足14位右补0xF(1111),因为PIN最多12位,所以最后2位一定是0xFF(1111,1111)
- PAN域
共64bit,每4bit为1位十六进制数字,共16位十六进制数字。
- 第1~4位(1~16bit):固定值0x0000(0000,0000,0000,0000)
- 第5~16位(17~64bit):PAN,去掉最右边1位校验数字后,从右边数12位,不足12位左补0x0(0000)
以银行卡号6225760008219524,密码123456为例:
代码示例:
/**
* Format 0 PIN block
*
* @param PIN 密码
* @param PAN 账号
* @return Format 0 PIN block 十六进制字符串
*/
private static String format0(String PIN, String PAN) {
// PIN域,64bit,16位十六进制数字
// 固定值0x0 + PIN长度 + PIN(不足14位右补F)
String PINField = "0"
+ Integer.toHexString(PIN.length())
+ String.format("%-14s", PIN).replace(' ', 'F');
// PAN域,64bit,16位十六进制数字
// 固定值0x0000 + PAN,去掉校验数字,从右边数12位,不足12位左补0x0
String PANWithoutCheckDigit = PAN.substring(0, PAN.length() - 1);
String PANField = "0000"
+ (PANWithoutCheckDigit.length() > 12 ? PANWithoutCheckDigit
.substring(PANWithoutCheckDigit.length() - 12,
PANWithoutCheckDigit.length()) : String.format(
"%12s", PANWithoutCheckDigit).replace(' ', '0'));
// 十六进制转byte数组
byte[] PINFieldByteArray = hexString2ByteArr(PINField);
// 十六进制转byte数组
byte[] PANFieldByteArray = hexString2ByteArr(PANField);
// 异或
byte[] PINBlockByteArray = new byte[8];
for (int i = 0; i < 8; i++) {
PINBlockByteArray[i] = (byte) (PINFieldByteArray[i] ^ PANFieldByteArray[i]);
}
// 返回十六进制
return byteArr2HexString(PINBlockByteArray).toUpperCase();
}
Format 1 PIN block
用于PAN不可获取的场景,由PIN域拼接交易域得到,交易域在每次PIN block时应当是唯一的,其来源于交易序列号、时间戳、随机数或其他相似情况,取值范围是0x0(0000) ~ 0xF(1111)。PIN block上送时,交易域不必上送,因为PIN的长度已经知道了。
- PIN block
共64bit,每4bit为1位十六进制数字,共16位十六进制数字。
- 第1位(1~4bit):固定值0x1(0001)
- 第2位(5~8bit):PIN长度,取值范围0x4(0100) ~ 0xC(1100)
- 第3~16位(9~64bit):PIN,不足14位右补交易域
以密码123456为例,交易域使用随机数:
代码示例:
/**
* Format 1 PIN block
*
* @param PIN 密码
* @return Format 1 PIN block 十六进制字符串
*/
private static String format1(String PIN) {
// PIN block,64bit,16位十六进制数字
// 固定值0x1 + PIN长度 + PIN,不足14位右补交易域
String PINBlock = "1" + Integer.toHexString(PIN.length()) + PIN;
// 交易域使用随机数,取值范围是0x0~0xF
Random r = new Random();
for (int i = 0; i < 14 - PIN.length(); i++) {
PINBlock += Integer.toHexString(r.nextInt(16));
}
return PINBlock.toUpperCase();
}
Format 2 PIN block
指定IC卡使用,只能用于离线环境,不能用于线上PIN验证。
- PIN域
共64bit,每4bit为1位十六进制数字,共16位十六进制数字。
- 第1位(1~4bit):固定值0x2(0010)
- 第2位(5~8bit):PIN长度,取值范围0x4(0100) ~ 0xC(1100)
- 第3~16位(9~64bit):PIN,不足14位右补0xF(1111),因为PIN最多12位,所以最后2位一定是0xFF(1111,1111)
以密码123456为例:
代码示例:
/**
* Format 2 PIN block
*
* @param PIN 密码
* @return Format 2 PIN block 十六进制字符串
*/
private static String format2(String PIN) {
// PIN block,64bit,16位十六进制数字
// 固定值0x2 + PIN长度 + PIN(不足14位右补F)
return "2"
+ Integer.toHexString(PIN.length())
+ String.format("%-14s", PIN).replace(' ', 'F');
}
Format 3 PIN block
由PIN域和PAN域异或得到。
- PIN域
共64bit,每4bit为1位十六进制数字,共16位十六进制数字。
- 第1位(1~4bit):固定值0x3(0011)
- 第2位(5~8bit):PIN长度,取值范围0x4(0100) ~ 0xC(1100)
- 第3~16位(9~64bit):PIN,不足14位右补0xA(1010) ~ 0xF(1111)中随机的或顺序的数字
- PAN域
共64bit,每4bit为1位十六进制数字,共16位十六进制数字。
- 第1~4位(1~16bit):固定值0x0000(0000,0000,0000,0000)
- 第5~16位(17~64bit):PAN,去掉最右边1位校验数字后,从右边数12位,不足12位左补0x0(0000)
以银行卡号6225760008219524,密码123456为例:
代码示例:
/**
* Format 3 PIN block
*
* @param PIN 密码
* @param PAN 账号
* @return Format 3 PIN block 十六进制字符串
*/
private static String format3(String PIN, String PAN) {
// PIN域,64bit,16位十六进制数字
// 固定值0x3 + PIN长度 + PIN(不足14位右补随机数)
String PINField = "3" + Integer.toHexString(PIN.length()) + PIN;
// 随机数取值范围0xA~0xF
Random r = new Random();
for (int i = 0; i < 14 - PIN.length(); i++) {
PINField += Integer.toHexString(r.nextInt(6) + 10);
}
// PAN域,64bit,16位十六进制数字
// 固定值0x0000 + PAN,去掉校验数字,从右边数12位,不足12位左补0x0
String PANWithoutCheckDigit = PAN.substring(0, PAN.length() - 1);
String PANField = "0000"
+ (PANWithoutCheckDigit.length() > 12 ? PANWithoutCheckDigit
.substring(PANWithoutCheckDigit.length() - 12,
PANWithoutCheckDigit.length()) : String.format(
"%12s", PANWithoutCheckDigit).replace(' ', '0'));
// 十六进制转byte数组
byte[] PINFieldByteArray = hexString2ByteArr(PINField);
// 十六进制转byte数组
byte[] PANFieldByteArray = hexString2ByteArr(PANField);
// 异或
byte[] PINBlockByteArray = new byte[8];
for (int i = 0; i < 8; i++) {
PINBlockByteArray[i] = (byte) (PINFieldByteArray[i] ^ PANFieldByteArray[i]);
}
// 返回十六进制
return byteArr2HexString(PINBlockByteArray).toUpperCase();
}
Format 4 PIN block
- PIN域
共128bit,每4bit为1位十六进制数字,共32位十六进制数字。
- 第1位(1~4bit):固定值0x4(0100)
- 第2位(5~8bit):PIN长度,取值范围0x4(0100) ~ 0xC(1100)
- 第3~16位(9~64bit):PIN,不足14位右补0xA(1010)
- 第17~32位(65~128bit):随机数字,取值范围0x0(0000) ~ 0xF(1111)
- PAN域
共128bit,每4bit为1位十六进制数字,共32位十六进制数字。
- 第1位(1~4bit):PAN长度减12,因为PAN最大19位,所以取值范围0x0(0000) ~ 0x7(0111);PAN长度小于12时,取值0x0(0000)
- 第2~32位(5~128bit):PAN,不足12位左补0x0(0000),超过12位不足20位右补0x0(0000)
以银行卡号6225760008219524,密码123456为例:
Format 4 与0/1/2/3不同,不是先得到PIN block再加密,而是将PIN域和PAN域放到加密过程中,最终得到加密的PIN block。
- 加密过程
- 使用密钥K对PIN域加密,得到结果A;
- 结果A与PAN域做异或,得到结果B;
- 使用密钥K对结果B加密,得到加密的PIN block。
- 解密过程(加密过程的逆过程)
- 使用密钥K对加密的PIN block解密,得到结果B;
- 结果B与PAN域做异或,得到结果A;
- 使用密钥K对结果A解密,得到PIN域。
因Format 4 PIN block涉及密钥加解密过程,此处不提供代码示例,PIN域、PAN域、异或,可参照其他格式PIN block代码示例。
ANSI X9.8 Format
ANSI - American National Standards Institute,美国国家标准学会
ANS - American National Standards,美国国家标准
ANS X9.8是在ISO 9564-1标准的基础上增加几处笔记形成的,内容可以认为是一致的。
ANS X9.8中指出PIN block只有Format 0和3建议在此标准下使用,Format 3应当在多次PIN加密都使用相同的PIN加密密钥时使用。
Q/CUP 006.4
中国银联股份有限公司企业标准,Q表示企业,CUP表示中国银联(China UnionPay)
Q/CUP 006.4中规定了PIN block使用ANSI X9.8 Format(带主账号信息),通过标准比对,可以确定就是ANS X9.8中的Format 0,也就是ISO 9564-1中的Format 0。
但是,笔者未查找到ANSI X9.8 Format(不带主账号信息)的定义。不过,从2016年4月生效版的《中国银联银行卡交换系统技术规范》中,可以确定ANSI X9.8 Format(不带主账号信息)的PIN block的生成方式与ANSI X9.8 Format(带主账号信息)中PIN域的生成方式一致,因此,笔者有理由猜测银联标准定义的ANSI X9.8 Format(不带主账号信息)就是ANSI X9.8 Format(带主账号信息)的简化,但是无法猜测其应用场景,对此有了解的朋友可留言告知。
同时,银联标准也定义了互联网支付密码的PIN block计算方式。
- 互联网支付密码的长度必须在6到20个字符以内;
- 互联网支付密码均为ASCII码字符,既可以为字符,也可以为数字,或其他符号;
- PIN block
共24个字节,每个字节使用2位十六进制数字表示,共48位十六进制数字;
前2个字节,互联网支付密码的长度;
剩余22个字节,PIN,6~20位字符,每个字符占1个字节,不足右补0xFF。
以互联网支付密码Hello!123为例:
PIN长度为9,2个字节表示是09,ASCII码是48和57,转为十六进制是0x30和0x39,因此可以得到PIN block为:303948656C6C6F21313233FFFFFFFFFFFFFFFFFFFFFFFFFF
代码示例:
/**
* 银联定义互联网支付密码PIN block
*
* @param PIN 密码
* @return PIN block 十六进制字符串
*/
private static String formatQCUP(String PIN) {
StringBuilder PINBlock = new StringBuilder();
// 支付密码长度
String PINLength = String.format("%2s", Integer.toHexString(PIN.length()))
.replace(' ', '0');
char[] PINLengthArray = PINLength.toCharArray();
for (int i = 0; i < PINLengthArray.length; i++) {
PINBlock.append(Integer.toHexString((int) PINLengthArray[i]));
}
// 密码
char[] PINArray = PIN.toCharArray();
for (int i = 0; i < PINArray.length; i++) {
PINBlock.append(Integer.toHexString((int) PINArray[i]));
}
// 补0xFF
for (int i = 0; i < 22 - PINArray.length; i++) {
PINBlock.append("FF");
}
return PINBlock.toString().toUpperCase();
}
不安全的PIN
ISO 9564-1标准建议客户在设置或更换密码时遵循以下原则:
- 不包括客户信息,比如:姓氏、手机号码、生日等;
- 不包括账号中连续的数字;
- 不包括三个及以上的相同数字;
- 不包括三个及以上递增或递减的连续数字;
- 不包括历史上重要的日期。
除了这些,笔者从自身项目经验中总结出以下几点供大家参考(主要针对6位数字的密码):
- 两个及以上的相同数字;
- AABBCC格式,比如:112233
- AAABBB格式,比如:111222
- AAAABB格式,比如:111122
- AAAAAB格式,比如:111112
- AAAAAA格式,比如:111111
- 两个及以上递增或递减的连续数字;
- ABABAB格式,比如:121212、212121
- ABCABC格式,比如:123123、321321
- ABCDAB格式,比如:123412、432121
- ABCDEA格式,比如:123451、543211
- ABCDEF格式,比如:123456、654321
- 账号中任意连续的6个数字;
- 身份证号中任意连续的6个数字;
- 手机号中任意连续的6个数字;
- 不与之前设置密码相同;
- 建立弱密码表,位于弱密码表中的都不安全。
互联网支付密码一般会建议:包含大写字母、小写字母、数字、特殊符号中三种或三种以上的不小于8位的支付密码。
彩虹表
简单来说,彩虹表就是密码的合集,你的密码在人家的彩虹表里,就容易被破解;彩虹表很大,大到你的密码很容易出现在其中,但是彩虹表又很小,掌握了彩虹表的原理,你的密码就可以消失的无影无踪。
彩虹表如果展开内容较多,后续会开辟新篇专题交流。本篇主要从以下几点来探讨彩虹表破解的防范措施:
存储密码明文是大忌
前几年,存储密码明文的事件被多次曝光,不断刷新着IT从业人员的安全意识下限,好在经过多年的网络安全普及与国家对相关行业软件系统的严格监管,至少笔者所在的金融软件行业领域,存储密码明文的系统已经被淘汰在历史的洪流中了。
公开的哈希算法不安全
彩虹表正是针对公开的哈希算法的破解之法,尽管哈希算法是不可逆的,但是破解它其实更加简单,随着科技的发展,穷举的速度越来越快,有了彩虹表的加持,更加势如破竹。
哈希算法,越来越沦为完整性校验的工具。
为哈希算法加盐是个好主意
没错,我为自己带盐。给密码加盐后再进行哈希,即使被破解,也是加盐的密码,原始密码也是相对安全的。PIN block也可以算作一种加盐方法,只不过PIN block的盐包含在账号中,在不知道账号的前提下,得到了PIN block确实很难破解出密码。
足够聪明可以自定义加密算法
使用自定义的加密算法,彩虹表就失去了用武之地,在加密算法和密钥不泄露的前提下,破解基本是不可能的。但是,世界上聪明人那么多,切忌自以为是,须知人外有人,要时刻保持警惕。
谨防数据库泄露
如果数据库中密码数据泄露,聪明的黑客总能探索到破解之法,因此对数据库要做足够的安全防护。
如果可能
- 密码加密算法不可逆,算法不公开,限定于仅核心开发人员(A)知晓;
- 数据库加密防护,脱离数据库环境后无法打开,打开方式限定于仅核心运维人员(B)知晓;
- 网络防护,限定IP访问,使用堡垒机等手段,访问权限限定于仅核心运维人员(C)拥有;
- 以上,凑齐ABC才可能发生数据库泄露,此时发生撞库的概率就变的极低。
当然,如果代码中存在某个bug,也可能被高明的黑客利用,所谓道高一尺,魔高一丈,信息安全的攻防战没有绝对的赢家。
其他建议
- 使用密码控件,研发较困难,采购最直接;
- 不常登录地区/终端,进行短信验证码校验;
- 限制短信验证码失效时间;
- 限制短信验证码发送频率;
- 密码输入错误2次,进行图形验证码校验;
- 密码输入错误5次,锁定账号,并发短信通知客户。
最后
安全是每个金融软件系统的底线。