最近在分析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 三者的关系如下:
MDSES

对于圈存机,从 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进行处理,便不再展开。