RFID实验三总结

一次debug到哭泣的经历。

龙云尧个人博客,转载请注明出处。

CSDN地址:

个人blog地址:http://123.207.243.115:81/rfidshi-yan-san-zong-jie/

在实验过程中,需要不断翻阅实验课PPT之《04 电子钱包的功能》,word之《实验3文档》,以及不知名大佬的课程总代码,CSDN大佬吕浪的Java card开发系列文章

本次实验和前两次实验相比,代码量多很多,并且实验思路稍有区别。实验之前可以不太懂实验流程(主要是因为流程本身就太复杂了),但是一定要一遍又一遍阅读源代码,只有在读源码的过程中,才能体会整个验证过程,对项目中涉及到的函数方法的使用才能有一个更加深入的了解。接着自己不断重写代码,理解整个实现过程,才能对这个课程实验有较为深入的了解。

最终的代码地址:https://github.com/LongyunYao/RFID_lab/

代码在未征得本人同意之前,请勿直接Ctrl+C,Ctrl+V,谢谢。

正式实验

实验分析

首先我们在PPT中知道本次实验的主要需要实现的功能是:

  • 圈存
  • 消费
  • 余额查询

接下来我们开始看ppt《04 电子钱包的功能》和《实验3文档》。

首先是圈存功能的流程图。

圈存流程

流程图中我们可以分析出圈存一共有4个步骤:

  • 终端发送消息初始化
  • IC响应初始化,并且发送MAC1验证
  • 终端验证MAC1,确认IC卡是否合法,然后发送包含MAC2的圈存命令
  • IC卡验证终端机的合法性,执行完成以后返回TAC响应操作完成

接下来我们将一步一步仔细分析圈存是如何实现的。

圈存初始化命令

step1:圈存机发送的初始信息如下所示。消息中包含了秘钥标识符交易金额终端机编号

IC卡响应圈存初始化命令

IC卡处理圈存初始化信息流程

step2:

  • IC卡根据秘钥标识符寻找圈存秘钥
  • 生成过程秘钥。输入数据为[伪随机数||电子钱包联机交易序号||8000],秘钥为圈存秘钥,使用3DES加密算法。
  • 生成MAC1。输入数据为[电子钱包余额(交易前)||交易金额||交易类型标识||终端机编号],秘钥为过程秘钥,使用我们在上一次实现的MAC生成函数gmac4,计算出MAC1用来表明身份。
  • IC卡返回[余额||联机交易序列号||秘钥版本号||算法标识||伪随机数||MAC1]。

圈存指令

圈存指令流程

step3:

  • 圈存机对IC卡发挥的MAC1信息进行校验,如果正确就说明IC卡信息合法。
  • 计算MAC2。输入信息为[交易金额||交易类型标识||终端机编号||交易日期(主机)||交易时间(主机)],秘钥为过程秘钥,加密算法为依然为gmac4。用来表明自己的身份。
  • 发送圈存指令。消息中包含[交易日期||交易时间||MAC2]。

圈存TAC

TAC

step4:IC使用同样的算法计算MAC2,如果计算结果和终端返回的MAC2一致,就说明终端的身份合法。IC卡就会执行圈存命令。同时返回TAC。其中TAC计算时,输入数据为[电子钱包余额(交易后)||电子钱包联机交易序号(加1前)||交易金额||交易类型标识||终端机编号||交易日期(主机)||交易时间(主机)],密钥为TAC密码最左8个字节与TAC密码最右8个字节异或的结果。

到这里整个圈存过程就结束了。消费以及查询和圈存的实现原理一致,这里就不赘述了。

读代码

前面的分析中,我们已经对本次实验有了大致的了解,接下来就是开始读源码的过程了。不过本次实验中,因为我们只需要对IC卡的系统进行编程实现,而对终端机需要靠人脑完成,所以我们重心就会放在圈存的初始化和圈存的执行上面了。

圈存初始化

还是老样子,我们先读项目给的源码中的Purse部分,里面有圈存初始化和圈存确认信息的处理函数。

在TA给的源代码中,init_loadload方法是已经给好了的,我们先读这两部分的源代码,理解整个设计思路。

首先我们需要修改Purse类,让其process方法里面增执行圈存初始化,圈存,消费初始化,消费,以及查询的入口

handleEvent

因为圈存和消费的init方法的ins都一样,所以我们还需要增加一个判断方法,利用两者p1参数不一样,来判断是init_load还是init _purchase

init_load_purchase

好了,入口写好了。我们开始看init_load的实现方法。

init_load

前面还是和读写数据时一样的操作,进行参数校对,这个部分已经很简单了。我们看到有一个findkey方法。点进去查看函数的具体内容。

find_key

查找方法比较简单,从Key数组中获取秘钥,存进pbuf中,如果存在就返回秘钥标识符,否则返回0。

回到init_load,在执行fendkey之后,是异常处理,验证是否查找失败。然后执行init4load方法。继续点进去查看。(前方高能预警

init4load

回忆一下初始化的过程中,IC的操作部分是怎么操作的。

  • IC卡根据秘钥标识符寻找圈存秘钥
  • 生成过程秘钥。输入数据为[伪随机数||电子钱包联机交易序号||8000],秘钥为圈存秘钥,使用3DES加密算法。
  • 生成MAC1。输入数据为[电子钱包余额(交易前)||交易金额||交易类型标识||终端机编号],秘钥为过程秘钥,使用我们在上一次实现的MAC生成函数gmac4,计算出MAC1用来表明身份。
  • IC卡返回[余额||联机交易序列号||秘钥版本号||算法标识||伪随机数||MAC1]。

按照这个思路看源代码。(看代码的时候一定要对着ppt的流程读,我第一次读源代码就是单纯对着IDE读,结果读的很爽,但是读完了只知道每个基本操作在干嘛,但是整个操作流程还是一脸懵逼。)

开始看代码。

首先从data中提取交易金额,终端编号,存进pTemp42pTemp81

然后判断是否超额。我们继续点进去看一下increase的源代码。

increase

EP_ balance中和data中依次取出一个字节,将其相加再和一个ads(进位标志符)相加,如果flag为真,就改写EP _balance中的值,然后更新ads。最终返回进位标志位ads。整个就是大数加法的思想方法。但是EP _balance是啥???我们再点进去看源代码。

EPFile参数

是当前电子钱包的余额。因此整个increase就是判断当前余额加上一个圈存值,如果超额(结果超过4bytes),就会返回1(那个进位标志位ads)。否则返回0。如果传入的第二个参数为false,就不会更新余额,否则会执行余额更新操作。

好,到这里我们知道了rc = increase(pTemp42, false);部分的意义了。再往下看密码取位部分。

首先使用readkey方法。点进去查看。

readkey

这里需要对Key的结构有一定的了解了。我们点进去查看Key这个类定义和实现。

Key

所以Key中存放的结构2bytes信息位(分别为1byte的pbuf,1byte的length),5bytes的秘钥头以及16bytes的秘钥值。其中length是addKey传入value的长度,为表头+秘钥的长度。

回到readkeyreadkey就是将秘钥取出来,然后将秘钥表头中的value值取出来(5bytes表头+16bytes秘钥),返回长度为秘钥的实际长度(减掉了表头长度)

回到init4load,所以这4行代码的意义就是

  • 按照秘钥获取部分就是按照秘钥标识符获取bytes秘钥头+16bytes秘钥存进pTemp32
  • 从秘钥头第4个bytes获取秘钥版本号,从第5个bytes获取算法标识符
  • 从获取pTemp32中将秘钥的实际值(从第5位开始读取秘钥长度个bytes),取出来存进pTemp16。

再往下读随机数产生的代码。

GenerateSecureRnd

调用rd.generateData方法,对传入的参数进行操作。我们在查看generateData的实现方法的时候,已经进入.class文件,没有发现有意义的信息。于是折返查看v和size的信息

v的意义

根据注释,我们可以大概知道GenerateSecureRnd的意义就是根据特定的随机数产生机制产生随机数,然后写进v

getRndValue

getRndValue方法就是将随机数v写入参数bf中,偏移位为bOff

回到init4load,产生随机数这两行的意义就是产生随机数,然后将随机数写进pTemp32的[0:3]位。

接下来,将EP_online(电子钱包脱机交易序号,之前分析图片中出现过)写入pTemp32[4:5],将0x8000写入pTemp32[6:7],调用上一次实验中实现的3des加密算法,秘钥为上面得到的圈存秘钥(存在pTemp16中)产生过程秘钥,写入pTemp82

回看前面分析的圈存初始化的第一步和第二步,①IC卡根据秘钥标识符寻找圈存秘钥;②生成过程秘钥。输入数据为[伪随机数||电子钱包联机交易序号||8000],秘钥为圈存秘钥,使用3DES加密算法。是不是一模一样?

好,我们再往下看。

产生MAC1。首先分别往pTemp32中写入EP_balance(余额),data[1:4](交易金融,对着那一页PPT找data结构就知道了),0x02,data[5:10](终端机编号,一样看ppt中的data结构),然后将pTemp32中的内容复制到data(不知道这里写入data有什么意义,因为在响应数据部分又会被写一次。)。然后使用上次实验实现的gmac4,输入数据为pTemp32,秘钥为上一步得到的存在pTemp82的过程秘钥,得到的mac1存在pTemp41

同样回看前面分析的圈存初始化的第三步。生成MAC1。输入数据为[电子钱包余额(交易前)||交易金额||交易类型标识||终端机编号],秘钥为过程秘钥,使用我们在上一次实现的MAC生成函数gmac4,计算出MAC1用来表明身份。是不是对上了?

然后将EP_balance(余额),EP _online(电子钱包脱机交易序号),keyID(秘钥版本号),algID(算法标识符),随机数,以及mac1写进data

回看圈存初始化的第四步,IC卡返回[余额||联机交易序列号||秘钥版本号||算法标识||伪随机数||MAC1]。一模一样。

到这里init4load也结束了,往上回到init_loadpapdu.pdata已经在init4load中设置完成,在papdu.le(理想的下一次指令中的数据长度)写为0x10。然后purse.init _load结束。

光分析这一步我写了一个半钟,当时在读源代码的时候花的时间更久。但是这个步骤不能跳过,它让我们对整个IC卡的业务逻辑和方法实现以及调用打下了基础。我们在实现其他方法的时候,才能更加得心应手。

再看purse.load方法吧。这一步实现了圈存

purse.load

前面的就不详说了,它调用了EPfile.load方法,我们点进去查看。

load实现

回想在一开始分析的时候,step4中,IC卡对接收到的终端机指令是怎么处理的

step4:IC使用同样的算法计算MAC2,如果计算结果和终端返回的MAC2一致,就说明终端的身份合法。IC卡就会执行圈存命令。同时返回TAC。

查看MAC2的产生方法:输入信息为[交易金额||交易类型标识||终端机编号||交易日期(主机)||交易时间(主机)],秘钥为过程秘钥,加密算法为为gmac4。

查看TAC的产生方法:输入数据为[电子钱包余额(交易后)||电子钱包联机交易序号(加1前)||交易金额||交易类型标识||终端机编号||交易日期(主机)||交易时间(主机)],密钥为TAC密码最左8个字节与TAC密码最右8个字节异或的结果。

通过源代码和步骤分析我们可以得出,EPFile.load整个过程就是将step4实现的过程,不过需要注意的是,EPFile.load中不少参数使用的是EPFile.init4load中存下来放在pTemp中的值,这也就从逻辑上说明为什么我们在执行消费命令前必须执行消费初始化命令,保证了安全性。返回purse.load,校验异常,设置papdu.lepurse.load也就结束了。

整个阅读过程需要不断跳转代码,也需要不断在代码和PPT《04 电子钱包的功能》和word《实验3文档》。同时因为pTemp实在太多,我们最好能够在一旁做笔记,记录下来每一个pTemp存放了哪些值,以及它们的作用,这样我们才能在实现消费和查询的时候,使用起来更加方便。

附上我记录的在init4load和load中各种变量的变化情况以及pTemp们的存在意义。

load中pTemp的意义

大部分pTemp其实是有固定意义的(结合PPT中传入的各种数据长度,我们就不难理解为什么了)。pTemp32一般作为中间变量,用来进行3des加密或者gmac加密。

谨记这一点,结合我们在上一步中总结下来的经验。我们就可以着手实现init_purchase和purchase以及get _balance了。

查看消费的流程图。

消费流程

我们可以总结出消费的如下流程

  • 终端发送消息初始化
  • IC响应初始化,发回随机数
  • 终端产生MAC1,证明自己的身份,将交易信息发给IC卡
  • IC卡验证终端机的合法性,计算MAC2和TAC,返回给终端作为身份凭证和消费凭证

然后一步一步详细分析。

消费初始化命令

step1:圈存机发送的初始信息如下所示。消息中包含了密钥标识符交易金额终端机编号。(整个消息串中,除了p1、le对比圈存初始化指令有区别以外,并没有其他区别)。

IC卡处理消费初始化信息

IC卡处理消费初始化指令流程

step2:

  • IC卡根据秘钥标识符寻找圈存秘钥
  • 生成随机数
  • 检查余额是否足够支付本次交易
  • 返回[余额||脱机交易序号||透支限额||秘钥版本号||算法标识||伪随机数]

消费指令

step3: 终端将命令响应数据传送给主机,主机利用消费主密钥产生消费子密钥,并生成MAC1。然后终端向IC发送[交易序列号||交易日期||交易时间||MAC1]。(和圈存中发送的信息中多了一个交易序列号)。

IC卡处理消费

IC卡处理消费流程

step4:

  • IC卡根据消费秘钥生成过程秘钥
  • 利用过程秘钥生成MAC1
  • 交易序列号+1,将消费金额从卡中扣除。
  • IC卡生成MAC2(证明自己身份),和TAC(证明工作完成)。
  • 返回TAC和MAC2给终端

首先我们填purse中的init_purchase函数。(这一步可以先从init _load中复制下来,然后稍微修改一下p1le的值,以及调用的函数改成init4purchase,原因我刚刚在step1中说过了)。

init_purchase

接着我们需要实现EPFile.init4purchase。同样的,我们只需要按照step2步骤,从init4load中,按需复制代码就行。

EPFile_init4purchase

同时我们需要对照并且记录下来,每一个pTemp使用是否正确,还要记录init4purchase中每一步执行完成以后pTemp中数据对应的值。

EPFile_init4purchase_参数

然后我们再实现EPFile.purchase方法。实现过程比较复杂,但是每执行一步,就更新所有参数的状态,按照我们之前整理出来的pTemp用法,使用复制粘贴大法,从EPFile.init4loadEPFile.load中可以提取大量的重复代码下来。

需要注意的是,load中是余额加上交易金额,purchase中就应该是减去交易金额。因此我们必须手动实现decrease方法。

decrease

实现方法和increase类似,从后向前,一次取1byte,进行减法,同时更新借位,最后返回借位即可。

这里有一个小插曲,在计算MAC2的时候,一般不会出什么问题,但是在计算TAC的时候,就会出问题,这个坑在我最后debug的时候才发现(为了找这个bug整整debug了一个多钟= =)。于是我重新改变策略,使用在MAC2生成并且存进data中以后,将pTemp32新开一个数组(JCSystem.makeTransientByteArray),然后重新作为中间变量进行操作,但是不知道为什么,这样操作的结果还是错误的。于是就按照网上流传的代码,在计算TAC的时候,新申明了一个temp数组用来作为临时变量。

最终实现的代码如图。

EPFile_purchase_part1
EPFile_purchase_part2

参数变化的记录如图。

EPFile_purchase_参数

最后一个就是get_balance了。

余额查询指令

直接从EP_balance里面读出来就行了,一行代码搞定。

EPFile_get_balance

到这里,整个实验中需要我们填写的功能函数全部填写完成了。试验过程中,了解业务逻辑是一个很重要的过程,阅读源码,了解源码中每一个函数的具体意义,以及每一个参数中,存放的信息有哪些,这些都最好都在一个txt或者什么地方器记录下来,方便自己查阅和提取(毕竟一个没有debug的IDE,你不能要求什么,只能人脑debug),另一方面,每实现一个功能,就尽可能写清楚这个部分的功能,同时写清楚那些信息在哪些位置,这样你在实现的过程中才不会头晕脑胀。也能够方便你最后debug找错误信息从哪来。

验证实验

验证之前,我们还需要添加新的参数信息。

condef新添加

以及一个的天坑的地方。。。。。。。这个地方我用throw 0x1234大法,从init_purchase的return 0之前,一直回退到Purse中的if(papdu.APDUContainData())才找到问题。

Papdu新增

其他的地方应该没什么需要添加或者修改了,按照之前上一次的试验方法,我们可以开始进行debug了。

首先还是新建一个txt,因为我们需要人脑担任终端机的角色,所以提前将必要的操作流程和模板写下来存在txt里面,写指令的时候也能迅速一点。

实验3脚本

首先实验一中的初始化脚本。

然后模仿终端机输入圈存初始化指令。/send 805000020B080000100000112233445510

使用[伪随机数||电子钱包联机交易序号||8000]作为数据,圈存秘钥作为秘钥,使用3DES获得过程秘钥

init4load过程秘钥

然后使用过程秘药作为秘钥,输入自定义好的初始向量,以及数据[电子钱包余额(交易前)||交易金额||交易类型标识(0x02)||终端机编号]作为输入,生成mac1,和IC卡返回的mac1校对,发现一致

验证mac1

接着使用[电子钱包余额(交易前)||交易金额||交易类型标识(0x02)||终端机编号]作为数据,过程秘钥作为秘钥,生成mac2。夹杂其他信息一起发出去。

init_load发送mac2

返回TAC+9000,所以我们认为init_loadload成功。

接下来执行查询指令,返回00 00 10 00 90 00。说明卡里面已经有了00001000,我们圈存确实成功了。查询也成功了。

查询结果

接下来执行消费指令,/send 805001020B07000010000011223344550F

消费初始化

返回得到伪随机数。接着我们将[伪随机数||电子钱包脱机交易序号||终端交易序号的最右两个字节]作为数据,消费秘钥作为秘钥,使用3DES得到圈存秘钥

使用[交易金额||交易类型标识(0x06)||终端机编号||交易日期(主机)||交易时间(主机)]作为数据,过程秘钥最为秘钥生成mac1

消费发送mac1

返回MAC2+TAC+9000。故而我们认为本次init_purchasepurchase成功。

为了防止意外,我们再次执行查询余额的指令。

再次查询余额

结果显示余额为00000000,返回9000。故而我们认为,查询消费圈存三大功能都已经能够正确执行了。

多次圈存两次冲入00001000+消费测试一次消费00000001

最后测试一下多次存取操作。没什么问题,故而我们认为,本次实验成功。

2017/4/25更新

线下已校验多次存取(复杂数据),以及TAC码(我喜欢称其工单码)检验。经检验,不存在明显问题。

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

请我喝杯咖啡吧~

支付宝
微信