【问题标题】:How to decrypt the first message sent from Mifare Desfire EV1如何解密从 Mifare Desfire EV1 发送的第一条消息
【发布时间】:2025-12-03 02:25:01
【问题描述】:

有没有人知道如何解密从卡发送的第一条消息?我的意思是在身份验证成功之后,然后您发送一个命令(例如 0x51 (GetRealTagUID)。它返回 00+random32bits(总是不同)。我尝试使用以下命令对其进行解密:

        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 = 全零(16 个字节)

response = 0x51 命令后的 32 个随机位(只是去掉了两个零)

有人告诉我,IV 在第一次发送命令 (0x51) 后发生了变化。如何生成正确的 IV 来解密该响应?我认为全零是错误的,因为解密后的消息总是不同的,同一张卡应该总是相同的。

-编辑-

应用您的 (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));

-EDIT3-

仍然返回总是不同的值。我认为问题可能出在此处:

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

在创建用于解密响应的新 IV 时使用什么 IV?那里全是零。

【问题讨论】:

  • 嗨!你能在哪里完成这项工作?
  • 是的 :) 我记得尝试解决这个问题时的两周恐惧。
  • 我感觉到你了!很棒的帖子,这是一个救生员!谢谢!

标签: java encryption cryptography aes mifare


【解决方案1】:

身份验证后,IV 将重置为全零。当您使用 AES 身份验证时,您必须为每个后续命令计算 CMAC(即使 CMAC 实际上并未附加到命令中)。因此,您的命令的 CMAC 计算将导致正确的 IV 初始化以解码响应。 IE。命令的 CMAC 等于解密响应的 IV。同样,对于所有进一步的命令,IV 是来自先前加密/CMAC 的最后一个密码块。


更新:

如何计算 CMAC pad XOR 值

  • 使用会话密钥(使用零的 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]
  • rx[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 而不是整个字节
  • rrx[15] &amp; 0x01 == 0x01
  • 我不明白 :( 那么我该如何替换这个: if (rx[15] == 0x01) rx[15] = (byte) (rx[15] ^ 0x87); if (rx [15] == 0x00) rx[15] = (字节) (rx[15] ^ 0x01);
  • 我认为这是我的 LeftShift 功能!其他一切似乎都在工作。在 Java 中这种位移是如何工作的?如何将这些位向左移动?
【解决方案2】:

这不是 c/c++ 部分,而是进入我的位置,我正在查看 C 开发人员可能存在的错误。 我不能在这里证明 Java 的实现。但我认为@Michael Roland 的方式不正确!梅比!也许我错了!

对不起,我的英语很糟糕 :) 但这不是我的母语。我是俄罗斯人。

检查数组[0] 的第一个 MSB(7 位),然后将其向左移动。如果 MSB 7 位为 == 1,则异或; 或者保存 array[0] 的第一个 MSB 位,并在移位后将此位放在 array[15] 的末尾(LSB 位)。

只是证明它在这里: https://www.nxp.com/docs/en/application-note/AN10922.pdf

试试这个方法:

会话密钥

数据

首先你必须用 SesionKey 加密 16 个字节(零);

enc_aes_128_ecb(Zeros);

你会得到 EncryptedData。

加密数据

检查 EncryptedData[0] 的第 7 位 [MSB - LSB] == 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] == 1 的位 7 [MSB - LSB]?

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

然后将所有 ShiftedEncryptedData 移位到 1 位

ShiftLeft(ShiftedEncryptedData,16);

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

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)

正如迈克尔所说 - 使用 0x80 0x00 填充命令...

XOR 数据与 KEY_2 - 如果命令被填充,或者 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


会话密钥

Key_1

Key_2

数据

CMAC

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;
}   

祝你有美好的一天:)

【讨论】: