MFRC522与FM1208进行通信(可连续发送)

前言📚


有部分内容来自复旦FM1208 CPU卡调试-CSDN博客,若侵则删,之前对于使用RC522对FM1208进行连续发送毫无头绪,收集资料也没有很好的答案,故在写下此博客进行记录。

什么是FM1208🗃️


FM1208是一款由上海复旦微电子股份有限公司开发的单界面非接触CPU卡芯片,具有以下特点和功能:

基本特性

  • 芯片架构:内置8位CPU,指令兼容通用8051指令,还配备了硬件DES协处理器,支持Triple-DES加密。
  • 存储容量:数据存储器为8K字节的EEPROM,部分型号(如FM1208-10)采用7K+1M的存储组合。
  • 通信协议:支持ISO 14443-A协议,工作频率为13.56MHz,数据传输速率可达106Kbps。

安全性能

  • 加密技术:内置硬件DES协处理器,支持Triple-DES加密,还具备反电源分析模块和高低频检测复位模块。
  • 数据保护:EEPROM满足10万次擦写指标和10年数据保存指标。

应用场景

FM1208广泛应用于金融支付、身份验证、门禁控制、电子政务、健康医疗等领域。例如:

  • 金融支付:支持PBOC 2.0标准,可用于电子钱包。
  • 交通领域:适用于城市公交、高速公路卡、校园一卡通等。
  • 身份认证:可用于门禁系统、电梯控制等。

优势

  • 兼容性:符合ISO 14443 Type A标准,兼容市场上广泛应用的非接触逻辑加密卡。
  • 多功能性:支持多应用环境,可实现一卡多用。
  • 高效率:典型交易过程小于350ms。

FM1208凭借其高性能、高安全性和广泛的兼容性,成为智能卡领域的重要产品。

写卡流程✍️


  1. 选择主目录(MF 3F00)
  2. 外部认证
  3. 删除主目录下数据
  4. 创建主秘钥(MF)文件(0000)
  5. 写入秘钥文件
  6. 创建应用目录(ADF 3F01)
  7. 创建ADF的秘钥文件(0000)
  8. 写入秘钥文件
  9. 创建应用文件
  10. 写入数据

写卡具体命令📃


1.选择主目录

<- 00 A4 00 00 02 3F 00(发送指令)
-> 6F 15 84 0E 31 50 41 59 2E 53 59 53 2E 44 44 46 30 31 A5 03 88 01 01 90 00(指令回复)

2.外部认证流程:

获取随机数:

<- 00 84 00 00 04
-> 06CEE4F29000

加密:

06CEE4F200000000 与秘钥FFFFFFFFFFFFFFFF进行单倍长DES加密得到FE C6 31 84 B3 07 AE FD

认证:

<- 0082000008FEC63184B307AEFD
-> 9000

3.擦除数据:

擦除卡片里面的所有数据,擦除成功之后,卡片即成为一张空白卡片

发送指令:80 0E 00 00 00
指令回复:9000(擦除成功)
指令说明:80(CLA)0E(INS)00(P1)00(P2)00(Lc)

4.建立主目录下密钥文件:

<- 80 E0 00 00 07 3F 00 6C 01 FA FF FF
-> 9000

指令说明:80(CLA)E0(INS)0000(P1P2文件标识)07(Lc)3F(文件类型)006C(文件空间)01(DF文件短标识符)FA(增加权限)FF(默认)FF(默认)

5.写入密钥文件:

写入3F00目录下外部认证密钥(单倍长DES加密的秘钥必须为8字节,故新建的秘钥为8个字节;若用3DES,则秘钥长度为16个字节,此处的秘钥为16个字节)

<- 80 D4 01 00 0D F9 F0 F0 AA 88 11 22 33 44 55 66 77 88
-> 9000

指令说明:80(CLA)D4(INS)01(P1)00(P2)0D(Lc)F9(密钥标识)F0(使用权)F0(更改权)AA(后续状态)88(错误计数器)1122334455667788(密钥)

6.建立应用目录(ADF,3F01):

<- 80E03F010D380600F0FA95FFFF4144463031
-> 9000

指令说明:80(CLA)E0(INS)3F01(P1 P2文件标识)0D(Lc)38(文件类型(目录文件))0600(文件空间)F0(建立权限)FA(擦除权限)95(应用文件标识)FFFF(保留字段)4144463031(DF名称 AID)

7.选择应用目录3F01:

<- 00A40000023F01
-> 6F07840541444630319000

8.建立ADF目录下密钥文件:

<- 80E00000073F010095FAFFFF
-> 9000

指令说明:80(CLA)E0(INS)0000(P1P2文件标识)07(Lc)3F(文件类型)0100(文件空间)95(DF文件短标识符)FA(增加权限)FF(默认)FF(默认)

9.在ADF目录写入秘钥文件:

写入3F00目录下外部认证密钥(单倍长DES加密的秘钥必须为8字节,故新建的秘钥为8个字节;若用3DES,则秘钥长度为16个字节,此处的秘钥为16个字节)

<- 80D401000DF9F0F0AA881122334455667788
-> 9000

指令说明:80(CLA)D4(INS)01(P1)00(P2)0D(Lc)F9(密钥标识)F0(使用权)F0(更改权)AA(后续状态)88(错误计数器)1122334455667788(密钥)

10.建立应用目录下二进制文件:

<- 80E0000307280010FAFAFFFF
<- 80E0000307280005F0F0FFFF
-> 9000

指令说明:80(CLA)E0(INS)0003(P1 P2 文件标识)07(Lc)28(明文MAC 28) 0010 (文件空间 , 这里是16个字节 ) FA(读权限)FA(增加权限)FF(默认FF)FF(默认FF)

11.写入二进制文件内容

<- 00D60000081122334455667788
-> 9000

指令说明:00(CLA)D6(INS)00(P1)00(P2)08( Lc ,这里 写入8字节长度的数据,Lc是8,如果写入10字节长度的数据,Lc是0A )1122334455667788(数据)

读卡流程📖


  1. 选择读的目录
  2. 外部认证
    1. 获取随机数
    2. DES加密
    3. 外部认证
  3. 读取文件

具体命令如下:

1.选择应用目录3F01:

<- 00A40000023F01
-> 9000

2.外部认证流程:

获取自由数:

<- 0084000004
-> 06CEE4F29000

加密:

06CEE4F200000000 与秘钥1122334455667788进行单倍长DES加密得到E7415FB27FB8035A

外部认证:

<- 0082000008 E7415FB27FB8035A
-> 9000

3.选择二进制文件03:

<- 00B0830000
-> 112233445566778800000000000000009000

再次写卡

1.选择应用目录3F01:

<- 00A40000023F01
-> 6F07840541444630319000

2.外部认证流程:

获取自由数:

<- 0084000004
-> 06CEE4F29000

加密:

06CEE4F200000000 与秘钥1122334455667788进行单倍长DES加密得到E7415FB27FB8035A

外部认证:

<- 0082000008E7415FB27FB8035A
-> 9000

3.选择文件0003:

<- 00A40000020003
-> 9000

4.写入内容

<- 00D600000A112233445566778899aa
-> 9000

指令说明:00(CLA)D6(INS)00(P1)00(P2) 0A (Lc , 写入10字节长度的数据 ) 112233445566778899aa(数据)

关于FM1208的一些问题🤔


首先在此感谢这个博客复旦FM1208 CPU卡调试-CSDN博客

获取随机数加密后必须立刻使用随机数

我之前获取随机数之后选择主文件再进行外部认证,这样是不行的,这样会导致FM1208内部的随机数会更改,获得随机数并加密之后就要立刻使用,不要跨命令使用

❌错误流程:获取随机数 -> 任意不使用随机数操作 -> 外部认证
✔️正确流程:获取随机数 -> 外部认证 -> 任意不使用随机数操作

关于COS指令的构建

看了很多的博客都是直接发APDU指令,并没有提及到APDU前面的开始域构建,这也是我看了文档和上文提及的博客才知道的

image

image

image

image

PCB 用于传送控制数据传输所需要的信息,在FMCOS用户手册3.2.1.1.1有说明,我是用的是I块,也就是高4位为0,每次发送COS指令,最低位都要变化从0变1或者从1变0,我的PCB是0x0A和0x0B来回变化

0x0A和0x0B化成二进制则是0000 101x所以是要跟随CID的,CID设置这块就需要在卡复位中进行操作至于为什么,可以看3.1.1的选择应答请求的内容,后续的COS操作都需要根据复位时候设置的CID进行传输

APDU指令属于信息域,CRC校验出的两个字节就是结束域

下文是我获取随机数构造的指令

    // 构建 COS 指令

    cmdBuffer[0] = Pcb;             // PCB
    cmdBuffer[1] = 0x01;            // CID
    cmdBuffer[2] = 0x00;            // CLA
    cmdBuffer[3] = 0x84;            // INS: Get Challenge
    cmdBuffer[4] = 0x00;            // P1
    cmdBuffer[5] = 0x00;            // P2
    cmdBuffer[6] = Len;             // Le: 请求的随机数长度

    // 添加 CRC 校验
    CalculateCRC(cmdBuffer, 7, &cmdBuffer[7]);
  
    // 发送指令并接收响应
    status = PcdComMF522(PCD_TRANSCEIVE, cmdBuffer, 9, cmdBuffer, &recvLen);

如何传输大于64字节的内容(连续传输)

通过不断查阅资料发现在FMCOS手册中有提及到这个内容,当RC522发送给FM1208一个I块并且这个I块的PCB部分的链接位需要置1,如果FM1208回复R块则可以继续传输,重复此过程链接位置位为1和接收到R块直到要发送的最后一块让链接位置位为0,告诉FM1208是最后一块数据块即可。

接收FM1208大于64字节的数据也是同样的思路,只需要构造好响应的R块即可接收到FM1208返回的大于64字节的数据。

注意FM1208是不会回复R(NAK),也就是空响应。

image

所以整体的思路就是,在发送前需要重复构造I块和接收的时候重复构造R块,对数据进行拆分即可完成连续传输

/**
 * @brief 构造T=CL协议帧头(Prologue)
 *
 * 根据给定的PCB类型构造I-Block/S-Block/R-Block的帧头,
 * 包括toggle bit管理、CID/NAD扩展字段的添加。
 *
 * @param handle 指向TCL协议句柄的指针
 * @param prlg 输出缓冲区,用于保存构造好的帧头
 * @param prlg_len 输出参数,表示帧头长度
 * @param pcb 帧控制字节(Protocol Control Byte),指定帧类型(I/S/R-block)
 * @return int 返回操作结果,0表示成功
 *
 * @note 主要处理以下协议规范:
 * - ISO/IEC 14443-4:2016 帧头结构
 * - Toggle Bit 管理(bit0)
 * - CID 扩展支持(bit3)
 * - NAD 支持(bit2,在I-block中使用)
 */
static unsigned char Build_Prologue(struct ATS_Handle *handle, unsigned char *prlg, unsigned int *prlg_len, unsigned char pcb)
{
	*prlg_len = 1;

	*prlg = pcb;

	if (IS_I_BLOCK(pcb) | IS_R_BLOCK(pcb)) 
	{
		if (handle->toggle) 
		{
			handle->toggle = 0;
		} 
		else 
		{
			handle->toggle = 1;
			*prlg |= 0x01;
		}

	}

	if ((handle->flags & TCL_HANDLE_F_CID_SUPPORTED) == 0x02) 
	{
		*prlg |= TCL_PCB_CID_FOLLOWING;  // 设置 CID 跟随标志位(bit3)
		(*prlg_len)++;
		prlg[*prlg_len - 1] = handle->cid;  // 存储CID值(低4位)
	}


	return PROTOCOL_OK;
}

static unsigned char Build_Prologue_I(struct ATS_Handle *handle, unsigned char *prlg, unsigned int *prlg_len)
{
	return Build_Prologue(handle, prlg, prlg_len, 0x02);
}

static unsigned char Build_Prologue_R(struct ATS_Handle *handle, unsigned char *prlg, unsigned int *prlg_len, unsigned int nak)
{
	unsigned char pcb = 0xa2;

	if (nak)
		pcb |= 0x10;

	return Build_Prologue(handle, prlg, prlg_len, pcb);
}

static unsigned char Build_Prologue_S(struct ATS_Handle *handle, unsigned char *prlg, unsigned int *prlg_len)
{
	return Build_Prologue(handle, prlg, prlg_len, 0xc2);
}


static unsigned char Prologue_Len(struct ATS_Handle *handle)
{
	unsigned char prlg_len = 1;

	if (handle->flags & TCL_HANDLE_F_CID_SUPPORTED)
		prlg_len++;

	if (handle->flags & TCL_HANDLE_F_NAD_SUPPORTED)
		prlg_len++;

	return prlg_len;
}



/**
 *
 * @brief 填充transBuff缓冲区并构造I-Block帧
 *
 * 该函数负责将待发送的数据封装成一个完整的I-Block帧,
 * 并准备好transBuff缓冲区以供后续的物理层传输。
 *
 * @param transBuff 指向transBuff缓冲区的指针
 * @param ctx 指向TCL传输上下文的指针
 * @return int 返回操作结果,0表示成功,负值表示错误
 *
 * @note 主要处理以下协议规范:
 * - ISO/IEC 14443-4:2016 I-Block帧结构
 * - 数据分片传输机制
 * - 链路标志位设置(bit4)
 * - toggle bit管理(bit0)
 */
int Channing_Transceive(const unsigned char *tx_data, unsigned int tx_len, unsigned char *rx_data, unsigned int *rx_len)
{
    int ret;
    unsigned int net_payload_len;
    struct rfid_trans_buff transBuff;
    struct tx_context ctx;
    struct ATS_Handle *th = &ats_Handle;

    unsigned char ack[10];
    unsigned int ack_len;

    /* 初始化传输上下文环境 */
    ctx.next_tx_byte = ctx.tx = tx_data;
    ctx.next_rx_byte = ctx.rx = rx_data;
    ctx.rx_len = *rx_len;
    ctx.tx_len = tx_len;

    /* 初始化XCVRB缓冲区 */
    transBuff.timeout = th->fwt;

    // 初始化传输缓冲区
    if (Refill_TransBuff(&transBuff, &ctx) < 0) {
        ret = -1;
        goto out;
    }

    do {
        // 设置接收缓冲区大小
        transBuff.rx.frame_len = sizeof(transBuff.rx.data);
        // 执行底层物理层数据收发
        ret = RC522_Transceive_RegularFrame(transBuff.tx.data, transBuff.tx.frame_len, transBuff.rx.data, &transBuff.rx.frame_len);

        printf("l2 transceive finished\n");
        if (ret < 0)
            break;

        // 处理返回的R-Block响应
        if (IS_R_BLOCK(transBuff.rx.data[0])) 
        {
            printf("R-Block\n");
            // 验证toggle位有效性
            if ((transBuff.rx.data[0] & 0x01) != ats_Handle.toggle) 
            {
                printf("response with wrong toggle bit\n");
                ret = -1;
                break;
            }

            // CID校验处理
            if (!check_cid(th, &transBuff)) 
            {
                ret = -1;
                break;
            }

            // 重新填充传输缓冲区
            if (Refill_TransBuff(&transBuff, &ctx) < 0) 
            {
                ret = -1;
                continue;
            }
        }
        // 处理S-Block(状态块)
        else if (IS_S_BLOCK(transBuff.rx.data[0])) 
        {
            unsigned char inf;
            unsigned int prlg_len;
            
            printf("S-Block\n");
            
            // CID校验
            if (!check_cid(th, &transBuff)) 
            {
                ret = -1;
                break;
            }
            
            // 获取信息字段
            if (transBuff.rx.data[0] & TCL_PCB_CID_FOLLOWING) 
            {
                if (transBuff.rx.frame_len < 3)
                {
                    printf("S-Block with CID but short len\n");
                    ret = -1;
                    break;
                }
                inf = transBuff.rx.data[2];
            } 
            else 
            {
	            inf = transBuff.rx.data[1];
            }

            // 验证是否为WTX扩展
            if ((transBuff.rx.data[0] & 0x30) != 0x30) 
            {
                printf("S-Block but not WTX?\n");
                ret = -1;
                break;
            }

            // 提取WTXM参数
            inf &= 0x3f;    /* 只保留低6位作为WTXM */
            // 验证WTXM有效性
            if (inf == 0 || (inf >= 60 && inf <= 63)) 
            {
                printf("WTXM %u is RFU!\n", inf);
                ret = -1;
                break;
            }

            // 更新WTX超时时间
            Fill_TransBuff_WTXM(th, &transBuff, inf);
            // 开始下一次收发
            continue; 
        } 
        // 处理I-Block(信息块)
        else if (IS_I_BLOCK(transBuff.rx.data[0])) 
        {
            /* 正在接收有效载荷数据 */
            printf("I-Block: ");
            // 验证toggle位有效性
            if ((transBuff.rx.data[0] & 0x01) != ats_Handle.toggle) 
            {
                printf("response with wrong toggle bit\n");
                ret = -1;
                break;
            }
            
            // 设置默认头部长度
            transBuff.rx.head_len = 1;
            
            // CID校验
            if (!check_cid(th, &transBuff)) 
            {
                ret = -1;
                break;
            }

            // 计算实际头部长度(考虑CID/NAD标志位)
            if (transBuff.rx.data[0] & TCL_PCB_CID_FOLLOWING)
                transBuff.rx.head_len++;
            if (transBuff.rx.data[0] & TCL_PCB_NAD_FOLLOWING)
                transBuff.rx.head_len++;

            // 计算有效数据长度
            net_payload_len = transBuff.rx.frame_len - transBuff.rx.head_len - 2; // 减去CRC校验
            printf("%u bytes\n", net_payload_len);
            // 拷贝有效数据到接收缓冲区
            memcpy(ctx.next_rx_byte, &transBuff.rx.data[transBuff.rx.head_len], net_payload_len);
            ctx.next_rx_byte += net_payload_len;
            
            // 判断是否为链路传输的最后一帧
            if (transBuff.rx.data[0] & 0x10) {
                /* 不是链路最后一帧,需要继续接收 */
                printf("not the last frame in the chain, continue\n");
                ack_len = sizeof(ack);
                // 构造R-Block应答
                Build_Prologue_R(th, transBuff.tx.data, &transBuff.tx.frame_len,0);
                // 重置超时时间
                transBuff.timeout = th->fwt;
                continue;
            } 
            else 
            {
                // 如果是最后一帧,退出循环
                break;
            }
        }
    } while (1); // 模拟原始逻辑的无限循环

out:
    // 返回实际接收的数据长度
    *rx_len = ctx.next_rx_byte - ctx.rx;
    return ret;
}

测试卡片

测试FM1208的时候可以不用写入密钥也就不用进行外部认证了方便对应用进行测试。

posted @ 2025-08-20 22:33  M1nd3zz7z  阅读(74)  评论(0)    收藏  举报