如何解密来自Mifare Desfire EV1的第一条消息。

4

有人知道如何解密卡片发送的第一条消息吗? 我的意思是在身份验证成功后,您发送一个命令(例如0x51(GetRealTagUID)。它返回00 +随机32位数(始终不同)。我尝试使用以下方法进行解密:

        private byte[] decrypt(byte[] raw, byte[] encrypted, byte[] iv)
            throws Exception {
        IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
        SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
        cipher.init(Cipher.DECRYPT_MODE, skeySpec, ivParameterSpec);
        byte[] decrypted = cipher.doFinal(encrypted);
        return decrypted;
    }

使用decrypt(sessionKey, response, iv)进行调用。

IV = 全0(16字节)

response = 在0x51命令后的32个随机比特数(去除两个零)

有人告诉我,在第一个发送的命令(0x51)之后,IV会发生变化。如何生成正确的IV以解密响应?我认为全0是错误的,因为解密后的消息总是不同的,而且应该与相同的卡片相同。

-编辑-

按照Michael Roland的说明操作后,解密响应仍然只是随机位。这是我的代码(我认为我做错了一些事情):

            byte[] x = encrypt(sessionKey, iv, iv);

            byte[] rx = rotateBitsLeft(x);

            if ((rx[15] & 0x01) == 0x01)
                rx[15] = (byte) (rx[15] ^ 0x87);

            if ((rx[15] & 0x01) == 0x00)
                rx[15] = (byte) (rx[15] ^ 0x01);

            byte[] crc_k1 = rx;

            byte[] rrx = rotateBitsLeft(rx);

            if ((rrx[15] & 0x01) == 0x01)
                rrx[15] = (byte) (rrx[15] ^ 0x87);

            if ((rrx[15] & 0x01) == 0x00)
                rrx[15] = (byte) (rrx[15] ^ 0x01);

            byte[] crc_k2 = rrx;

            byte[] command = { (byte) 0x51, (byte) 0x80, (byte) 0x00,
                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
                    (byte) 0x00 };

            for (int i = 0; i < 16; i++){
                command[i] = (byte) (command[i] ^ crc_k2[i]);
            }

            byte[] iv2 = encrypt(sessionKey, command, iv);

            byte[] RealUID = decrypt(sessionKey, ReadDataParsed, iv2);

            Log.e("RealUID", ByteArrayToHexString(RealUID));

-编辑3-

仍然返回不同的值。我认为问题可能出在这里:

byte[] iv2 = encrypt(sessionKey, command, iv);

在创建新的IV以解密响应时,使用什么IV?它都是零。

你好!你能让这个工作起来吗? - Xyldrun Jacob
1
是的 :) 我记得当时为了解决这个问题,我经历了两周的噩梦。 - Stonde
我理解你的感受!这篇文章太棒了,真是救了我一命!谢谢! - Xyldrun Jacob
2个回答

8
经过身份验证后,IV将被重置为全零。使用AES认证时,您需要为每个随后的命令计算CMAC(即使实际上没有将CMAC附加到该命令)。因此,您的命令的CMAC计算会导致正确的IV初始化以解码响应。即对于解密响应而言,命令的CMAC等于IV。同样,对于所有后续命令,IV是前一个加密/CMAC的最后一块密文。
更新:
如何计算CMAC填充XOR值
- 用会话密钥(使用0的IV)加密零的一个块(0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00)-> x [0..15] - 将x [0..15]向左旋转一个位-> rx [0..15] - 如果最后一位(rx [15]中的第0位)为1:xor rx [15]和0x86。 - 将rx [0..15]存储为crc_k1 [0..15]。 - 将x [0..15]向左旋转一个位-> rrx [0..15] - 如果最后一位(rrx [15]中的第0位)为1:xor rrx [15]和0x86。 - 将rrx [0..15]存储为crc_k2 [0..15]。
如何计算CMAC
  • 将命令使用0x80 0x00 0x00 ...填充到密码块大小(AES为16字节)的长度。如果命令长度恰好是块大小的倍数,则不添加填充。
  • 对于您的命令 (0x51),这看起来像是: 0x51 0x80 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
  • 如果添加了填充,则将填充后命令的最后16个字节与crc_k2[0..15]进行异或操作。
  • 如果没有添加填充,则将命令的最后16个字节与crc_k1[0..15]进行异或操作。
  • 使用会话密钥进行加密(在发送模式下,即 enc(IV xor datablock),前一块的密文是新的 IV)。
  • 最后一个块的密文是 CMAC 和新的 IV。

更新 2:

如何向左旋转一位的位向量

public void rotateLeft(byte[] data) {
    byte t = (byte)((data[0] >>> 7) & 0x001);
    for (int i = 0; i < (data.length - 1); ++i) {
        data[i] = (byte)(((data[i] << 1) & 0x0FE) | ((data[i + 1] >>> 7) & 0x001));
    }
    data[data.length - 1] = (byte)(((data[data.length - 1] << 1) & 0x0FE) | t);
}

在检查(r)rx[15]时,您应该仅检查位0 = 1或0,而不是整个字节。 - Michael Roland
rrx[15] & 0x01 == 0x01 - Michael Roland
我认为这是我的 LeftShift 函数出了问题!其他方面好像都没问题。在Java中,那个位移操作是如何工作的?如何将这些位向左移动? - Stonde
谢谢你!但是它还是不起作用 :( 代码中还有其他错误吗? - Stonde
应该将 if ((rx[15] & 0x00) == 0x00) 更改为 if ((rx[15] & 0x01) == 0x00)(当然,rrx 也是一样的...)。 - Michael Roland
显示剩余9条评论

1

虽然不属于C/C++部分,但在我的职位上,我正在查看来自C开发人员的可能错误。我不能在这里证明Java实现。但我认为@Michael Roland的方法是不正确的!也许!也许我错了!

抱歉我的英语很糟糕 :) 但这不是我的母语。我是俄罗斯人。

先检查数组[0]的最高有效位(7位)然后将其向左移位。如果MSB 7位 == 1,则进行异或运算;或者保存数组[0]的第一个MSB位,在移位后将此位放在数组[15]的末尾(LSB位)。

这里有证据:

https://www.nxp.com/docs/en/application-note/AN10922.pdf

尝试这种方式:

Zeros <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

SessionKey <- 00 01 02 03 E3 27 64 0C 0C 0D 0E 0F 5C 5D B9 D5

Data <- 6F 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00

首先,您需要使用SessionKey加密16个字节的Zeros。

enc_aes_128_ecb(Zeros);

我会帮助您进行翻译。以下是相关内容:

并且您将获得加密数据。

加密数据 <- 3D 08 A2 49 D9 71 58 EA 75 73 18 F2 FA 6A 27 AC

检查加密数据[0]的第7位[最高位-最低位]是否等于1?如果是,则将i切换为true。

 bool i = false;
  if (EncryptedData[0] & 0x80){
    i = true;
  }

然后对所有的EncryptedData进行1位左移。

ShiftLeft(EncryptedData,16);

现在,当i == true时 - 将最后一个字节[15]与0x87异或

if (i){
    ShiftedEncryptedData[15] ^= 0x87;
  }

7A 11 44 93 B2 E2 B1 D4 EA E6 31 E5 F4 D4 4F 58

将其保存为 KEY_1。

尝试判断 ShiftedEncryptedData[0] 的第7位 [最高位 - 最低位] 是否等于1?

 i = false;
  if (ShiftedEncryptedData[0] & 0x80){
    i = true;
  }

然后将所有ShiftedEncryptedData向左位移1位。

ShiftLeft(ShiftedEncryptedData,16);

现在,当i == true时 - 将最后一个字节[15]与0x87进行XOR操作

if (i){
   ShiftedEncryptedData[15] ^= 0x87;
}

F4 22 89 27 65 C5 63 A9 D5 CC 63 CB E9 A8 9E B0

将其保存为KEY_2。

现在我们拿出数据(6F 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00

按照Michael的说法 - 用0x80 0x00填充命令...

用KEY_2异或Data - 如果命令被填充了,或者用KEY_1如果没有填充。 如果有16个字节以上(例如32个),只需对最后16个字节进行异或。

然后加密它:

enc_aes_128_ecb(Data);

现在您拥有了一个CMAC。

CD C0 52 62 6D F6 60 CA 9B C1 09 FF EF 64 1A E3


Zeros <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
SessionKey <- 00 01 02 03 E3 27 64 0C 0C 0D 0E 0F 5C 5D B9 D5
Key_1 <- 7A 11 44 93 B2 E2 B1 D4 EA E6 31 E5 F4 D4 4F 58
Key_2 <- F4 22 89 27 65 C5 63 A9 D5 CC 63 CB E9 A8 9E B0
Data <- 6F 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00
CMAC <- CD C0 52 62 6D F6 60 CA 9B C1 09 FF EF 64 1A E3

C/C++ 函数:

void ShiftLeft(byte *data, byte dataLen){
  for (int n = 0; n < dataLen - 1; n++) {
   data[n] = ((data[n] << 1) | ((data[n+1] >> 7)&0x01));
  }
  data[dataLen - 1] <<= 1;
}   

祝您拥有愉快的一天 :)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接