最近在分析PBOC卡的读写。手头有一些基于PBOC 2.0标准的ISO 14443A-4 CPU卡,其实就是我的校园卡。
介绍的话就不多说了,直接切入正题,讲解PBOC 2.0标准下的一卡通的圈存过程,并附上过程中所需密钥的相应算法(C#)。文中所有命令中的数据皆为16进制值。阅读本文需要对PBOC 2.0标准有一定的了解。
01 预备圈存
机器发送圈存预备命令到卡。命令格式为
字节[长度] 说明
80 50 00 圈存预备命令字
XX[1] 应用标识,01为电子存折(ED),02为电子钱包(EP)
0B
XX[1] 密钥索引号
XX[4] 圈存金额
XX[6] 设备编号
10
1字节应用标识,一般的非银行类消费应用基本都是电子钱包,这张校园卡亦然。故此处标识为02。
1字节密钥索引号。校园卡所用密钥索引号为01。
4字节圈存金额计算方法如下
int money;
// 金额
byte[] moneyArray = new byte[] {
((byte)(money / 256 / 256 / 256)),
((byte)(money / 256 / 256)),
((byte)(money / 256)),
((byte)(money % 256))
};
假设要充值的金额为12.34元,则money = 1234, 计算得四字节数据为00 00 04 D2
。
6字节设备编号。不同的一卡通系统所用的明文编号到字节编号的转换方式不尽相同。校园一卡通的转换方式为BCD码转换。若设备编号为229312324358,则6字节设备编号为22 93 12 32 43 58
。
综上,要使用编号为229312324358的设备给使用电子钱包、密钥索引为01的校园卡充值12.34元,完整命令为80 50 00 02 0B 01 00 00 04 D2 22 93 12 32 43 58
发送命令后,卡片返回数据。格式为
字节[长度] 说明
XX[4] ED或EP余额
XX[2] ED或EP交易序号
XX[1] 密钥版本号
XX[1] 算法标识
XX[4] 伪随机数
XX[4] MAC1
同样,返回的余额计算方法就是上面的逆过程。
此处的 MAC1 为 CPU 卡根据自身的伪随机数进行加密计算的,计算方法在后文会提到。
02 圈存
得到上面返回的值以后,圈存机按照密钥版本号,从 PSAM 卡中获取相应的密钥。圈存操作使用的密钥为 MLK,消费/取现所用密钥为 MPK。而后圈存机根据算法标识和伪随机数,计算得出自己的 MAC1,并与卡片返回的 MAC1 进行比较。若一致则视为合法卡片,进行圈存操作。
约定通过 MLK 和卡号一并计算得到 DLK,通过 MPK 和卡号计算得到 DPK。DLK 用于圈存,DPK 用于消费/取现,且仅对当前卡片有效。除此之外,还有仅对单次操作有效的 SESLK 和 SESPK。MLK/MPK、DLK/DPK、SESLK/SESPK 三者的关系如下:
对于圈存机,从 PSAM 卡中获取 MLK 后,将其与卡号一起作为输入计算出 DLK。
计算 DPK 或 DLK 的方法CalDPKOrDLK代码如下
public static byte[] CalDPKOrDLK(byte[] MPKOrMLK, byte[] cardNo)
{
byte[] left = new byte[8];
byte[] right = new byte[8];
byte[] DPKOrDLK = new byte[16];
// 生成 DPKOrDLK 的左半部分
left = TripleDES(MPKOrMLK, cardNo);
for (int i = 0; i < 8; i++)
{
cardNo[i] = (byte)~cardNo[i]; // 对卡号每一位求反
}
// 生成 DPKOrDLK 的右半部分
right = TripleDES(MPKOrMLK, cardNo);
Array.Copy(left, DPKOrDLK, 8);
Array.Copy(right, 0, DPKOrDLK, 8, 8);
return DPKOrDLK;
}
TripleDES代码如下
public static byte[] TripleDES(byte[] Key, byte[] inData)
{
SymmetricAlgorithm sa = new TripleDESCryptoServiceProvider();
byte[] _Key = new byte[16];
Array.Copy(Key, _Key, 16);
sa.Key = _Key;
sa.IV = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 };
sa.Mode = CipherMode.ECB;
sa.Padding = PaddingMode.Zeros;
ICryptoTransform ct = sa.CreateEncryptor();
using (MemoryStream ms = new MemoryStream())
{
using (CryptoStream cs = new CryptoStream(ms, ct, CryptoStreamMode.Write))
{
cs.Write(inData, 0, inData.Length);
cs.FlushFinalBlock();
}
return ms.ToArray();
}
}
请注意 TripleDES 所用的填充模式为 Zeros,加密模式为 ECB。这与后面计算 MAC 时所用的填充模式和加密模式会有不同。
获得了 DLK 之后,需要计算本次操作所需的长度为 8 字节的 SESLK。代码如下
byte[] SESLK = TripleDES(DLK, ucData);
其中byte字节数组变量 ucData 结构如下
字节[长度] 说明
XX[4] ED或EP余额,从准备圈存过程返回的数据中获得
XX[2] ED或EP交易序号,同上
80 00
获得本次操作所需的SESLK之后,即可计算本次所需的MAC1。计算MAC方法CalMAC代码如下。
public static byte[] CalMAC(byte[] ucData, int ucDataLen, byte[] SESLK)
{
byte[] macData = new byte[2048];
byte[] result = new byte[4];
Array.Clear(macData, 0, macData.Length);
Array.Copy(ucData, macData, ucDataLen);
// 开始进行数据填充
macData[ucDataLen] = 0x80;
if (ucDataLen % 8 != 0)
{
ucDataLen += 8 - ucDataLen % 8;
}
byte[] IV = new byte[8];
byte[] temp = new byte[8];
Array.Clear(IV, 0, IV.Length);
for (int i = 0; i < ucDataLen / 8; i++)
{
Array.Copy(macData, 8 * i, temp, 0, 8);
for (int j = 0; j < 8; j++)
{
IV[i] ^= temp[i];
}
temp = DES(SESLK, IV, CipherMode.CBC, PaddingMode.None, temp, true);
Array.Copy(temp, IV, 8);
}
Array.Copy(temp, result, 4);
return result;
}
以上方法的输入数据要经过填充处理。首先需要在输入数据后拼接0x80,若拼接后总长度不为8的整数倍,则一直往后拼接0x00直到数据长度为8的整数倍为止。由于此处macData已经预先填充了0,只需将ucDataLen的值增加到能被8整除即可。
运用DES计算得到的输出值实际上长度为8字节,而根据PBOC标准,MAC得到结果大于4字节的部分填充为0。由于卡片返回的MAC1和圈存所需的MAC2皆为4字节,故方法内直接对MAC进行截断,取前4字节。
所用DES算法如下
public static byte[] DES(byte[] Key, byte[] IV, CipherMode cm, PaddingMode pm, byte[] inData, bool encode)
{
if (Key.Length < 8 || IV.Length < 8)
return null;
byte[] _Key = new byte[8];
byte[] _IV = new byte[8];
Array.Copy(Key, _Key, 8);
Array.Copy(IV, _IV, 8);
try
{
SymmetricAlgorithm sa = new DESCryptoServiceProvider();
sa.Key = _Key;
sa.IV = _IV;
sa.Mode = cm;
sa.Padding = pm;
ICryptoTransform ct = null;
if (encode)
ct = sa.CreateEncryptor();
else
ct = sa.CreateDecryptor();
using (MemoryStream ms = new MemoryStream())
{
using (CryptoStream cs = new CryptoStream(ms, ct, CryptoStreamMode.Write))
{
cs.Write(inData, 0, inData.Length);
cs.FlushFinalBlock();
}
return ms.ToArray();
}
}
catch (Exception)
{
throw;
}
}
此处DES的填充模式为None,加密模式为CBC。当初在这里没少走弯路。
显然,有
byte[] MAC1 = CalMAC(ucData, 15, SESLK);
其中ucData格式为
字节[长度] 说明
XX[4] ED或EP余额,从准备圈存过程返回的数据中获得
XX[4] 交易金额
XX 交易类型标识,圈存为02
XX[6] 终端机编号
而后便可以进行MAC1的比对工作。
比对完MAC1之后,就要计算MAC2。类似地,有
byte[] MAC2 = CalMAC(ucData, 18, SESLK);
其中ucData格式为
字节[长度] 说明
XX[4] 交易金额
XX 交易类型标识,圈存为02
XX[6] 终端机编号
XX[4] 交易日期。如2008年2月27日,则为20 08 02 27
XX[3] 交易时间。如14时23分57秒,则为14 23 57
而后生成MAC2后,便可生成下一步的命令ucCMD,结构如下
字节[长度] 说明
80 52 00 00 0B 圈存指令
XX[4] 交易日期。同上
XX[3] 交易时间。同上
XX[4] MAC2
04
将其发送至卡片,卡片验证通过后会增加金额,同时返回TAC。鉴于学校圈存并不对TAC进行处理,便不再展开。