中国X银行间联POS终端规范解读
一、标准POS报文设计思路解读
以下表格为标准POS报文结构,由TPDU+HEAD(报文头)+ISO8583MSG(8583报文数据)构成;其中8583报文体中包括”位元素”,代表后面的数据是哪几个域被用到,这样就最大化的缩小了发送报文的字节数。(详细见附录A)
TPDU | 报文头 | 应用数据 | ||||||
ISO8583Msg | ||||||||
ID | 目 的 地 址 | 源 地 址 | 应 用 类 别
| 软 件 版 本 号 | 终 端 状 态 | 处 理 要 求 | 保 留 使 用 | 交易数据 |
60H | N2 | N2 | N2 | N2 | N1 | N1 | N6 | 不定长度 |
XXX银行对此进行了改造,加入了双倍加密模式,由于3des算法对原文字节长度要求为8的倍数,所以需要加入一个“加密信息”区域了解原始报文长度,从而能在解密后得到原始报文。
二、报文结构解读
2.1X银行pos报文格式
TPDU | 报文头 | 加密信息 | 应用数据 | ||||||||||||
ISO8583Msg | |||||||||||||||
ID | 目 的 地 址 | 源 地 址 | 应 用 类 别
| 软 件 版 本 号 | 终 端 状 态 | 处 理 要 求 | 保 留 使 用 | 报文长度 | 算法标识 | 商户号 | 终端号 | 交易 标识 | 响应码 | 保留 | 交易数据 |
60H | N2 | N2 | N2 | N2 | N1 | N1 | N6 | A3 | A1 | A15 | A8 | A10 | A2 | A2 |
|
文档《X银行间联POS终端规范》中对各个字段已有详细描述,简单解释如下:
整个报文由TUDU+HEAD(报文头)+ISOPASS(加密信息)+ISO8583MSG(8583报文体00-64域)
签到(明文)格式: TPDU+HEAD+00-63域明文
交易(明文)格式: TPDU+HEAD+00-63域明文+64域 (64域来源于对02-63域的ECB加密)
如果想用明文方式发送报文,则需要先按照签到的明文格式进行签到,此时加密信息区域不需要传递,只需在发送交易时对02-63域进行ECB加密;
签到(密文)格式:TPDU+HEAD+ISOPASS+00-63域明文
交易(密文)格式: TPDU+HEAD+ISOPASS+全报文加密(对00-64域进行双倍加密处理后的结果)
如果想用密文方式发送报文,则需要先按照签到的密文格式进行签到,这时要添加加密信息区域(注意报文长度一定是3个字节长度,若报文长度小于100时则第一位补零),
全报文加密是先对02-63域进行ECB处理得到64域,然后将00-64域(如果不是8的倍数,则在后面补零)进行双倍(3DES)加密得到整个报文加密后的结果。
2.2手工解包文示例
下面以一段预授权返回报文进行解析,由于采用socket方式进行传输消息,故将其字节流转换为16进制可见文进行解析:
600000000660801001080231303332313035323930303534353130383338303030313638313430313030343232313238303046468e1ac77c445b27a7bc382059ece00931c60534c3cbd03055e980ee1f5ebcf7e344ba3a865e712bd2979ed0387c1d7bf62f8cb5b020a16b667473f5bde85ec080b6237335ee500a993acf095617aa8cb87cd1bfd58ce33b5bbc49f3dec793d1ccc2acddc73442f0aa
根据8583定义格式,TPDU长度为5个字节,报文头12个字节,用BCD码压缩后为6个字节长度,加密信息41个字节,剩下的就是报文体内容,由于1个字节可用2个16进制字符表示,故而得到以下解析内容
TPDU: 6000000006 (前十位)
HEAD: 608010010802 (紧接着12位)
ISOPASS(加密信息): 3130333231303532393030353435313038333830303031363831343031303034323231323830304646(紧接着82位)
报文体: 8e1ac77c445b27a7bc382059ece00931c60534c3cbd03055e980ee1f5ebcf7e344ba3a865e712bd2979ed0387c1d7bf62f8cb5b020a16b667473f5bde85ec080b6237335ee500a993acf095617aa8cb87cd1bfd58ce33b5bbc49f3dec793d1ccc2acddc73442f0aa
TPDU和HEAD一般都是固定值,就不再赘述;由于加密信息区域是ASCII值表示的,取出来的值根据ascii表转换得到原始报文内容,得到如下表格:
加密信息列 | 类型 | 响应值 | 转换后报文明文 | 含义 |
报文长度 | A3 | 313033 | 103 | 整个报文体的原始报文长度为103,即对后面的报文双倍解密后前103个字符即为报文明文; |
算法标识 | A1 | 32 | 2 | 代表加密算法为“3des” |
商户号 | A15 | 313035323930303534353130383338 | 105290054510838 |
|
终端号 | A8 | 3030303136383134 | 00016814 |
|
交易标识 | A10 | 30313030343232313238 | 0100422128 | 0100为预授权,422128为发送过去的流水号(应与11域相同) |
响应码 | A2 | 3030 | 00 | 对方解密成功 |
保留 | A2 | 4646 | FF |
|
首先对报文体进行3des解密(密钥=2afdbf46292a299e97da318f9d76e3ab)解密后结果为0110703800800ad0800316436745007172945903000000000000000142212809020901210635303231303939313830393039363030303136383134313035323930303534353130383338153031303530303030202020303030313135360000433342393531433200
剩下的就是按照文档标准对各个域进行解析;
消息类型:0110
位元素:703800800ad08003 (64位=8个字节=16个16进制字符)
转换为二进制后:1110000001110000000000010000000000010101101000010000000000000110,一共64位,第一位1代表域2有值,第二位为1代表域3有值,依次类推;
字段名称 | 属性 | 格式 | 类型 | 响应值 | 解析后明文 | 含义 |
域2 | n..19 | LLVAR | BCD | 164367450071729459 | 4367450071729459 | 域2为变长的字符类型,前两个字符16代表该字段的长度,故截取后16位长度字符即为该字段的值; |
域3 | n6 |
| BCD | 030000 | 030000 |
|
域4 | n12 |
| BCD | 000000000001 | 000000000001 |
|
域11 | n6 |
| BCD | 422128 | 422128 |
|
域12 | n6 | hhmmss | BCD | 90209 | 090209 |
|
域13 | n4 | MMDD | BCD | 0121 | 0121 |
|
域25 | n2 |
| BCD | 06 | 06 |
|
域37 | an12 |
| ASCII | 353032313039393138303930 | 502109918090 | 定长12个字节,ASCII形式标示 |
域39 | an2 |
| ASCII | 3936 | 96 |
|
域41 | ans8 |
| ASCII | 3030303136383134 | 00016814 |
|
域42 | ans15 |
| ASCII | 313035323930303534353130383338 | 105290054510838 |
|
域44 | ans..25 | LLVAR | ASCII | 15303130353030303020202030303031 | 0105000 0001 | 变长ASCII形式,前两个数字代表长度 |
域49 | an3 |
| ASCII | 313536 | 156 |
|
域63 | ans…96 |
| ASCII | 0000 | 0 | 长度为0,63域返回值为空 |
域64 | b64 |
| BINARY | 4333423935314332 | C3B951C2 | Mac校验值 |
关于pos终端的数据类型解释,请参考附录B
三、名词解释
3.1 BCD码
BCD码是用4位二进制码的组合代表十进制数的0,1,2,3,4,5,6,7,8,9 十个数符。
对应关系如下图表格所示:
十进制数 | BCD码(8421) |
0 | 0000 |
1 | 0001 |
2 | 0010 |
3 | 0011 |
4 | 0100 |
5 | 0101 |
6 | 0110 |
7 | 0111 |
8 | 1000 |
9 | 1001 |
示例: 对原文“113802”进行BCD转换
原文 | 1 | 1 | 3 | 8 | 0 | 2 | 原文六个字节 |
二进制 | 0000 0001 | 0000 0001 | 0000 0011 | 0000 1000 | 0000 0000 | 0000 0010 |
|
BCD码(8421) | 0001 | 0001 | 0011 | 1000 | 0000 | 0010 |
|
十进制 | 17 | 56 | 2 | 转换为BCD码后变成三个字节 | |||
十六进制 | 11 | 38 | 02 |
|
将字符串转换为BCD字节数组方法:
public static byte[]toBcd(String value) {
int charpos =0;
int bufpos =0;
byte[] buf = null;
int len =value.length() / 2;
if (value.length()% 2 == 0) {
buf = new byte[len];
}
if(value.length() % 2 == 1) {
buf = new byte[len + 1];
buf[0] = ((byte)(value.charAt(0) -'0'));
charpos = 1;
bufpos = 1;
}
while (charpos< value.length()) {
buf[bufpos] = ((byte)(value.charAt(charpos) -'0' << 4 | value
.charAt(charpos + 1) - '0'));
charpos += 2;
bufpos++;
}
return buf;
}
3.2单倍长密钥算法、双倍长密钥算法和ECB算法
单倍长密钥算法 和双倍长密钥算法分别对应java里的des和3des加密算法;
DES加密算法
public static byte[] des(byte[]reqBytes,byte[] key)
throwsInvalidKeyException, NoSuchAlgorithmException,
NoSuchPaddingException,IllegalBlockSizeException,
BadPaddingException,UnsupportedEncodingException,
ShortBufferException {
SecretKeySpec keySpec = null;
DESKeySpec deskey = null;
deskey = newDESKeySpec(key);
keySpec = newSecretKeySpec(deskey.getKey(),"DES");
Cipher cipher = Cipher.getInstance("DES/ECB/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE,keySpec);
byte[]cipherText = new byte[cipher.getOutputSize(reqBytes.length)];
cipherText = cipher.doFinal(reqBytes);
returncipherText;
}
DES解密算法
public static byte[]decryptByDes(byte[] reqBytes,byte[] key)
throwsInvalidKeyException, NoSuchAlgorithmException,
NoSuchPaddingException,IllegalBlockSizeException,
BadPaddingException,UnsupportedEncodingException,
ShortBufferException {
SecretKeySpec keySpec = null;
DESKeySpec deskey = null;
deskey = newDESKeySpec(key);
keySpec = newSecretKeySpec(deskey.getKey(),"DES");
Cipher cipher = Cipher.getInstance("DES/ECB/NoPadding");
cipher.init(Cipher.DECRYPT_MODE,keySpec);
byte[]cipherText = new byte[cipher.getOutputSize(reqBytes.length)];
cipherText = cipher.doFinal(reqBytes);
returncipherText;
}
3des加密算法:
private static final String DESede_ALG ="DESede/ECB/NOPADDING"; // 定义加密算法,可用DES,DESede,Blowfish
/**
* 3DES加密
*
* @param src
* @param key
* @return
*/
public static byte[]encryptDESede(byte[] src,byte[] key) {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
try {
Cipher c1 = Cipher.getInstance(DESede_ALG,"BC");
c1.init(Cipher.ENCRYPT_MODE,newSecretKeySpec(key, DESede_ALG));
returnc1.doFinal(src);
} catch(Exception e) {
logger.info("[3DES]双倍加密异常!Exception=[" +e.getMessage() +"]");
e.printStackTrace();
}
return null;
}
3des解密算法:
/**
* 3DES解密
*
* @param src
* @param key
* @return
*/
public static byte[]decryptDESede(byte[] src,byte[] key) {
Security.addProvider(neworg.bouncycastle.jce.provider.BouncyCastleProvider());
try {
Cipher c1 = Cipher.getInstance(DESede_ALG,"BC");
c1.init(Cipher.DECRYPT_MODE,newSecretKeySpec(key, DESede_ALG));
returnc1.doFinal(src);
} catch(Exception e) {
logger.info("[3DES]双倍解密异常!Exception=[" +e.getMessage() +"]");
e.printStackTrace();
}
return null;
}
ECB加密算法:
/**
* 建行Mac加密
* @param msg
* @param key
* @return
*/
public static StringccbEncrypt(byte[] macBytes,String key){
byte[]resultEncrypt=null;
//签名字符串
byte[]tmpBytes = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
//macBytes为 MAC原始字符串
int times =macBytes.length/8;
if(macBytes.length % 8 != 0) {
times ++;
}
for (int t = 0; t<= times; t++) {
for (int i = 0; i< 8; i++) {
if ((t*8 +i) < macBytes.length) {
tmpBytes[i] = (byte)(tmpBytes[i]^ macBytes[t*8 + i]);
}
}
}
System.out.println("b)MAC字符串=[" +byte2hex(macBytes)+ "],异或后[" + byte2hex(tmpBytes)+ "]");
//前四个字节
byte[] frontBytes= new byte[4];
//后四个字节
byte[]backBytes = new byte[4];
System.arraycopy(tmpBytes, 0,frontBytes, 0, 4);
System.arraycopy(tmpBytes, 4,backBytes, 0, 4);
try {
byte[]frontEncrypt =des(byte2hex(frontBytes).getBytes(), hex2byte(key));
System.out.println("c,d)前半段加密后结果=[" +byte2hex(frontEncrypt)+ "]");
byte[] tmp = byte2hex(backBytes).getBytes();
for (int i = 0; i< 8; i++) {
tmp[i] = (byte)(frontEncrypt[i]^ tmp[i]);
}
System.out.println("e)异或后结果=[" +byte2hex(tmp)+ "]");
resultEncrypt = des(tmp, hex2byte(key));
System.out.println("f)加密结果=[" +byte2hex(resultEncrypt)+ "]");
} catch(Exception e) {
// TODOAuto-generated catch block
e.printStackTrace();
}
byte[]macResult=newbyte[4];
System.arraycopy(resultEncrypt, 0,macResult, 0, 4);
System.out.println("g,h)ECB加密最终返回结果=[" +byte2hex(macResult)+ "]");
return byte2hex(macResult);
}
/**
* byte array to hex
*
* @param b bytearray
* @return hexstring
* 字节数组转为16进制码
*/
public static String byte2hex(byte[] b) {
StringBuffer hs = newStringBuffer();
String stmp;
for (int i = 0; i< b.length; i++) {
stmp = Integer.toHexString(b[i]& 0xFF).toUpperCase();
if (stmp.length()== 1) {
hs.append("0").append(stmp);
} else {
hs.append(stmp);
}
}
returnhs.toString();
}
四、附录
附录A:Socket报文设计
假设甲乙双方要通过Socket发送通信,双方约定了发送和接受的报文字段数不超过64,那么怎么设计才能达到所发送的报文长度最小的目的呢?
字段名称 | 类型 | 长度 | 备注 |
域1 | 变长字符 | 20 |
|
域2 | 定长字符 | 20 |
|
…. | … |
|
|
域64 | 数字 |
|
|
假设有一封报文需要发送
字段名称 | 类型 | 长度 | 值 |
域1 | 变长字符 | 20 | 123456 |
域2 | 定长字符 | 20 | ABCDEF |
.. | … |
|
|
域64 | 数字 |
| 369 |
方案一:不管某个字段是否有值,一律按照最大长度将其封装后发送,如果无值默认补空格或零,这样双方就能按照字段规定的长度来解析了。
发送报文的值=123456ABCDEF……369;(省略号为补足空格)
方案二:如果字段需要发送才做报文封装,否则舍弃,在整个报文前端加上哪些字段被发送的标识,这样双方也能先通过解析域值封装信息来解析后面的报文。
发送报文的值=c000000000000001123456ABCDE369;(前面16个字符转成二进制之后就能看到哪几个域被用到)
c000000000000001 –>1100000000000000000000000000000000000000000000000000000000000001
由于要发送的字段数目不会超过64,所以只需要添加固定长度为8个字节(64位)来表示报文体中哪几个域被用到,改区域被称作”位元素”;
由上所述,方案一的报文长度为最大值,方案二的报文长度最简短,故方案二为最佳模式。
附录B:Pos终端数据类型
类型名称 | 备注 | 实例说明 |
A | 字母向左靠,右部多余部分填空格。 |
|
AN | 字母和/或数字,左靠,右部多余部分填空格。
|
|
ANS
| 字母和/或特殊符号,左靠,右部多余部分填空格。 |
|
B | 二进制bit 位。
|
|
DD
| 日 |
|
hh | 时 |
|
LL | 可变长域的长度值(二位数) | 前面两位数字代表长度,按照此长度截取该字段的值; |
LLL | 可变长域的长度值(三位数) | 前面三位数字代表长度,按照此长度截取该字段的值; |
MM | 月 |
|
mm | 分 |
|
N | 数值,右靠,首位有效数字前充零。若表示金额,则最右二位为角分 |
|
S | 特殊符号 |
|
ss | 秒 |
|
VAR | 可变长域 |
|
X | 借贷符号,在数值之前,D 表示借,C 表示贷 |
|
YY | 年 |
|
Z | 由ISO 7811 和ISO 7813 制定的磁条卡第二、三磁道的数据类型 |
|
CN | BCD 压缩编码数值 |
|
|
|
|
|
|
|
对可变长数据元,以下例说明:
—— 变量XYZ 的数据类型为ANS...999(LLLVAR),则表示:该变量中可含字母、数字和特殊符号,
最长不超过999 个字符,长度由三位数字确定。
—— 变量XYZ 的数据类型为N...999(LLLVAR),则在压缩时,其长度位用右靠的BCD 码压缩,而
其后紧随的数字内容用左靠的BCD 码压缩。这是为了保证有效内容和其位数中间无缺省填充
值。若不为偶数位,左靠的数字内容后补零。由于有长度位表征该域有效内容的长度,因此后
补零不会改变该域的真实值。
注: 本文档中声明的压缩变量属性是针对POS终端与POS中心之间的消息,POS中心与任何金融机构之间的消息将全
部采用ASCII码且不压缩的格式。