任务描述:
先上代码,然后再解析:
package test; //import HelloWorld; import javacard.framework.APDU; import javacard.framework.Applet; import javacard.framework.ISO7816; import javacard.framework.ISOException; import javacard.framework.Util; public class HelloWorld extends Applet { public static void install(byte[] bArray, short bOffset, byte bLength) { // GP-compliant JavaCard applet registration new HelloWorld().register(bArray, (short) (bOffset + 1), bArray[bOffset]); } public void process(APDU apdu) { // Good practice: Return 9000 on SELECT if (selectingApplet()) { return; } byte[] buf = apdu.getBuffer();//定义缓冲区的引用数组,也就是通过buf数组可以实时获取缓冲区内容 //把终端发过来的apdu命令存到缓冲区当中,返回数据(即apdu中的data)的长度 short lc = apdu.setIncomingAndReceive(); //byte[] src = {0};//取得buf数组中data部分 byte ins = buf[ISO7816.OFFSET_INS]; switch (ins) { case (byte) 0x00: //从缓冲区数组buf中取得数据然后复制到src中 //Util.arrayCopyNonAtomic(buf, ISO7816.OFFSET_CDATA, src, (short)0, (short)buf.length); apdu.setOutgoingAndSend((short)5, lc);//用这句命令直接把缓冲区的数据返回给终端, //其中第一个参数表示偏移量,也就是从缓冲区的第几个元素开始返回,后面一个参数表示返回数据的长度, //也就是说最终返回给终端的数据是从缓冲区的第offset个元素开始到offset+length个元素的内容 break; default: // good practice: If you don't know the INStruction, say so: ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); } } }
(1)首先,得理解一点最基础的,你这个代码是运行在哪里的,这个关乎从哪接受数据数据又将发送到何处去的问题!显然,都叫java card applet开发了,当然就是运行在card卡片一端的啦!偏偏自己刚开始时搞混了也就没法写代码了。对,这些代码都是以card为主人的,那终端(读写器)呢?终端不就是你在eclipse JCOP Debug页面的命令行输入命令句的那里么!你在命令行那写命令发送就是发送给card,然后card通过你写的applet代码获取到终端发送过来的命令,根据命令进行一系列的处理(也就是代码的运转啦)。
(2)既然代码要把终端发送过来的命令获取并处理,那要怎么获取呢?JCOP已经帮我们封装好了一个东西,叫apdu,这个东西(是一个class)里面存的就是从终端发送过来的命令,对的,终端发送过来的命令就是apdu命令,啥叫apdu?:APDU(应用协议数据单元 application protocol data units)命令。
(3)既然终端来的apdu命令就在apdu这个类当中,拿我们就可以通过使用这个apdu类来获取到终端发送过来的命令中的所有东西,那这个所谓的命令包括什么东西呢?上图:
对,这就是终端发送过来的apdu命令的几大模块,每块都如其名称(缩写),第一个字节(注意真的是字节不是两个比特!)表示的是class指令的类,第二个字节是ins指令编码,第三和第四个字节是参数1和参数2,第五个字节LC表示的是后面Data的长度(多少个字节),然后Data部分就是真正的数据了,最长可以是255个字节,最后的LE字节表示的是期望卡片发回来的字节长度,比如08就表示我要卡片你给我发回来8字节的数据(不包括9000这两个字节),如果是00就表示多多益善,卡片你能发回来多长数据给我就都发过来。
嗯,这是终端发送给card的apdu命令,那卡片发送给终端的apdu命令结构是怎样的呢?如下图:
第一块表示的是要返回的数据,长度不定,这里联系到上面刚刚说到的终端期望card要发回多长的数据,说的就是这个数据的长度。后面两个字节是两个状态字节,两个加起来是9000时表示正常完成了指令的处理,也就是搞掂没问题!
(4)嗯,上面的都是写代码之前必须了解的基础,然后就说回代码中吧:首先得把终端发送过来的apdu命令读取到卡片的缓冲区当中,有两个方法,一个是用函数:Short setIncomingAndReceive(),第二个是用函数public short receiveBytes(short bOff),只接受一次数据时用前面的函数就够了,后面的是用于多次接受apdu的。详细的解释在教材《JAVA智能卡原理与应用开发》的101页有。对,有教材啊!只要你愿意花时间啃教材,哪还怕不会敲代码?所以别从头到尾都是对着eclipse想怎样敲代码,把原理搞懂了代码就能秒杀!所以说你是不是越来越码农化了?危险啊!!戒之慎之!!!
(5)嗯,上面已经把apdu存储都卡片的缓冲区当中了,然后通过
byte[] buf = apdu.getBuffer();
(6)然后通过上面的buf数组,就可以获取到apdu命令的每一个模块了,这里需要了解个鬼东西叫ISO7816 接口,详细的解释在刚才说的那本教材的105页,回去翻翻看吧。总之,用
byte ins = buf[ISO7816.OFFSET_INS];
(7)然后怎样获取到data数据模块部分呢?这里需要用到一个特殊的函数:
Util.arrayCopyNonAtomic(buf, ISO7816.OFFSET_CDATA, src, (short)0, lc);
这句命令非常重要,因为如果要自定义一个字节数组src,想把它里面的内容返回给终端,就要也是通过这条命令把src的内容复制给buf缓冲区数组,只需要把这句命令的参数更改或调换下位置即可。
(8)然后就是将缓冲区的内容发送回给终端,注意,终端发送过来的apdu命令是先存在缓冲区当中,然后card要发送回去的apdu命令也是先放到缓冲区再发给终端的,所以就会产生覆盖,所以可以先把缓冲区的内容取出来放到另一个数组再覆盖。当然,上面的这个题只需要直接把原来缓冲区的部分内容直接发送回给终端就够了,如下代码:
apdu.setOutgoingAndSend((short)5, lc);
嗯,针对这题就这几点了,最后运行applet,在命令行发送命令,最后显示出card返回的内容,这题是卡片直接将终端发送的数据原封不动地返回给终端:
先在命令行发送select applet的id
然后就可以发送内容命令了:
看到那两个箭头没,没错,右向箭头表示终端发送给卡片,左向箭头表示从卡片回收。最终卡片发回的命令是:原data+9000。
补充一个改进版本的代码,这份代码可以把存在字节数组的"hello"复制到缓冲区返回给终端输出:
package test; //import HelloWorld; import javacard.framework.APDU; import javacard.framework.Applet; import javacard.framework.ISO7816; import javacard.framework.ISOException; import javacard.framework.Util; public class HelloWorld extends Applet { public static void install(byte[] bArray, short bOffset, byte bLength) { // GP-compliant JavaCard applet registration new HelloWorld().register(bArray, (short) (bOffset + 1), bArray[bOffset]); } public void process(APDU apdu) { // Good practice: Return 9000 on SELECT if (selectingApplet()) { return; } //定义缓冲区的引用数组,也就是通过buf数组可以实时获取缓冲区内容 byte[] buf = apdu.getBuffer(); //把终端发过来的apdu命令存到缓冲区当中,返回数据(即apdu中的data)的长度 short lc = apdu.setIncomingAndReceive(); byte[] src = {'h','e','l','l','o'};//取得buf数组中data部分 byte ins = buf[ISO7816.OFFSET_INS]; switch (ins) { case (byte) 0x00: //从缓冲区数组buf中取得数据然后复制到src中 //Util.arrayCopyNonAtomic(buf, ISO7816.OFFSET_CDATA, src, (short)0, (short)buf.length); //把字节数组src中的数组复制到缓冲区数组中 Util.arrayCopyNonAtomic(src, (short)0, buf, (short)0, (short)src.length); apdu.setOutgoingAndSend((short)0, (short)src.length);//用这句命令直接把缓冲区的数据返回给终端, //其中第一个参数表示偏移量,也就是从缓冲区的第几个元素开始返回,后面一个参数表示返回数据的长度, //也就是说最终返回给终端的数据是从缓冲区的第offset个元素开始到offset+length个元素的内容 //apdu.setOutgoingAndSend((short)0, lc);//这样改一改就会返回8字节的data break; default: // good practice: If you don't know the INStruction, say so: ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); } } }
再来一份代码,这份代码实现了返回Hello world!的功能,和上面类似,但是用了new的方法新建一个缓存数组,new的方法不需要像byte[] src……这样的方法必须要初始化才能用(这里说的用主要是指在Util函数那用)。并且教材也是这样用new的方法定义一个新的缓存数组的。
/*author:lvlang date:2016-4-1*/ package test; //import HelloWorld; import javacard.framework.APDU; import javacard.framework.Applet; import javacard.framework.ISO7816; import javacard.framework.ISOException; import javacard.framework.Util; public class HelloWorld extends Applet { //定义一个数据缓冲区以及数据最大长度的常量 private static final short DATA_MAX= 256; private byte[] src; public static void install(byte[] bArray, short bOffset, byte bLength) { // GP-compliant JavaCard applet registration new HelloWorld().register(bArray, (short) (bOffset + 1), bArray[bOffset]); } public void process(APDU apdu) { // Good practice: Return 9000 on SELECT if (selectingApplet()) { return; } src = new byte[DATA_MAX];//创建相应数据缓冲区 byte[] str = {'H','e','l','l','o',' ','w','o','r','l','d','!'}; short str_len = (short)str.length; Util.arrayCopyNonAtomic(str, (short)0, src, (short)0, str_len); //定义缓冲区的引用数组,也就是通过buf数组可以实时获取缓冲区内容 byte[] buffer = apdu.getBuffer(); //把终端发过来的apdu命令存到缓冲区当中,返回数据(即apdu中的data)的长度 short lc = apdu.setIncomingAndReceive(); //获取ins段 byte ins = buffer[ISO7816.OFFSET_INS]; switch (ins) { case (byte) 0x00: //从缓冲区数组buf中取得数据然后复制到src中 //Util.arrayCopyNonAtomic(buffer, ISO7816.OFFSET_CDATA, src, (short)0, lc); //把字节数组src中的数组复制到缓冲区数组中 Util.arrayCopyNonAtomic(src, (short)0, buffer, ISO7816.OFFSET_CDATA, str_len); apdu.setOutgoingAndSend((short)5, str_len);//用这句命令直接把缓冲区的数据返回给终端, //其中第一个参数表示偏移量,也就是从缓冲区的第几个元素开始返回,后面一个参数表示返回数据的长度, //也就是说最终返回给终端的数据是从缓冲区的第offset个元素开始到offset+length个元素的内容 break; default: // good practice: If you don't know the INStruction, say so: ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); } } }
运行结果:
最后,说几个小技巧:
(1)运行直接点运行右边的三角形,在弹出窗口中选择项目名字单机就可以直接运行了,并不需要每次都run as这么麻烦
(2)打开了运行界面用上面的方法重新运行会实时更新代码带来的改变,不需要先close掉运行窗口