首先要知道Java card里面的文件系统结构,也就是有哪些文件:
包括四大类文件,应用基本文件——也就是发卡方官方的一些信息,然后是持卡人个人信息的文件,再就是EP(电子钱包)文件,以及最重要的密钥文件。并且密钥文件必须先于其他三个文件之前创建,因为没密码你弄啥操作不科学呀。
几大文件反应到代码中就是几大数据结构:
理解了文件系统之后就要一一去看各个文件代码模块是怎么实现的:
举个例子:
这是二进制文件数据结构的部分代码,可以看到它的构造函数需要传进一个pdata参数,其实就是要传输apdu命令的data部分给它。由于这几大数据结构代码课上都给得差不多了,而且本篇只讲文件系统的创建,所以目前只需要知道这几大文件数据结构的接口就够了,主要还是看主程序代码,也就是purse.java。嗯,本次java card的开发就是以电子钱包的开发作为项目来学习的。
先直接上主程序purse.java的代码,其他几个java文件的代码在后面学习篇再放:
//主文件:电子钱包,调用了其他几个辅助类。 //注意这些所有的代码和应用都是运行在卡片中的! //而终端只是在命令行敲几句命令发送的那里. package purse; //import Purse; import javacard.framework.APDU; import javacard.framework.Applet; import javacard.framework.ISO7816; import javacard.framework.ISOException; import javacard.framework.Util; public class Purse extends Applet { //APDU Object private Papdu papdu; //文件系统,每次run的时候都相当于一张空白卡片,全部需要重新建立, //并且建立之后就归当前run的applet所有,某个文件可以随时被调用(keyfile) private KeyFile keyfile; //密钥文件 private BinaryFile cardfile; //应用基本文件 private BinaryFile personfile; //持卡人基本文件 private EPFile EPfile; //电子钱包文件 Electronic Purse public Purse(byte[] bArray, short bOffset, byte bLength){ papdu = new Papdu(); byte aidLen = bArray[bOffset]; if(aidLen == (byte)0x00) register(); else register(bArray, (short)(bOffset + 1), aidLen);//注册applet } //安装applet public static void install(byte[] bArray, short bOffset, byte bLength) { new Purse(bArray, bOffset, bLength); } //执行applet public void process(APDU apdu) { if (selectingApplet()) { return; } //步骤1:取APDU缓冲区数组引用并将之赋给新建数组 byte buffer[] = apdu.getBuffer(); //步骤2:取APDU缓冲区中数据放到变量papdu short lc = apdu.setIncomingAndReceive();//将apdu读取到卡片缓冲区当中并返回data段的长度 papdu.cla = buffer[ISO7816.OFFSET_CLA]; papdu.ins = buffer[ISO7816.OFFSET_INS]; papdu.p1 = buffer[ISO7816.OFFSET_P1]; papdu.p2 = buffer[ISO7816.OFFSET_P2]; Util.arrayCopyNonAtomic(buffer, ISO7816.OFFSET_CDATA, papdu.pdata, (short)0, lc); //步骤3:判断命令APDU是否包含数据段,有数据则获取数据长度,并对le赋值, //否则,即不需要lc和data,则获取缓冲区原本lc实际上是le //获取le的方法,因为不确定papdu有le部分,所以IOS7816下标可选项并没有le而是放在数据块中的. //如果有数据快,那le就是buffer[ISO7816.OFFSET_CDATA+lc] if(papdu.pdata.length != 0) { papdu.le = buffer[ISO7816.OFFSET_CDATA+lc]; papdu.lc = buffer[ISO7816.OFFSET_LC]; } else papdu.le = buffer[ISO7816.OFFSET_LC];//若没data部分则lc部分实际是le boolean rc = handleEvent(); //步骤4:判断是否需要返回数据,并设置apdu缓冲区 if(papdu.le != 0) { Util.arrayCopyNonAtomic(papdu.pdata, (short)0, buffer, ISO7816.OFFSET_CDATA, (short)papdu.pdata.length); apdu.setOutgoingAndSend((short)5, (short)papdu.pdata.length);//把缓冲区的数据返回给终端 } } /* * 功能:对命令的分析和处理 * 参数:无 * 返回:是否成功处理了命令 */ private boolean handleEvent(){ switch(papdu.ins){ case condef.INS_CREATE_FILE: return create_file(); //todo:完成写二进制命令,读二进制命令,写密钥命令 case condef.INS_WRITE_KEY: return write_key(); case condef.INS_WRITE_BIN: return write_bin(); case condef.INS_READ_BIN: return read_bin(); case condef.INS_NIIT_TRANS: if(papdu.p1 == (byte)0x00) return init_load(); if(papdu.p1 == (byte)0x01) return init_purchase(); ISOException.throwIt(ISO7816.SW_WRONG_P1P2);//else抛出异常 case condef.INS_LOAD: return load(); } ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); return false; } /* * 功能:创建文件 */ private boolean create_file() { switch(papdu.pdata[0]){ //data的第一位表示要创建文件的类型 case condef.EP_FILE: return EP_file(); //todo:完成创建密钥文件,持卡人基本文件和应用基本文件 case condef.KEY_FILE: return Key_file(); case condef.CARD_FILE: return Card_file(); case condef.PERSON_FILE: return Person_file(); default: ISOException.throwIt(ISO7816.SW_FUNC_NOT_SUPPORTED); } return true; } /* * 功能:创建电子钱包文件 */ private boolean EP_file() { if(papdu.cla != (byte)0x80) ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED); if(papdu.lc != (byte)0x07) ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); if(EPfile != null)//有文件了还重复创建则会报错 ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); if(keyfile == null)//都还没密钥文件(必须先于任何其他文件创建) ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); EPfile = new EPFile(keyfile); return true; } //创建密钥文件 private boolean Key_file() { if(papdu.cla != (byte)0x80) ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED); if(papdu.lc != (byte)0x07) ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); if(keyfile != null)//有文件了还重复创建则会报错 ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); keyfile = new KeyFile(); return true; } //创建应用基本文件 private boolean Card_file() { if(papdu.cla != (byte)0x80) ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED); if(papdu.lc != (byte)0x07) ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); if(cardfile != null)//有文件了还重复创建则会报错 ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); if(keyfile == null)//都还没密钥文件(必须先于任何其他文件创建) ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); cardfile = new BinaryFile(papdu.pdata);//传进的参数就是要写入的内容 return true; } //创建持卡人信息文件 private boolean Person_file() { if(papdu.cla != (byte)0x80) ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED); if(papdu.lc != (byte)0x07) ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); if(personfile != null)//有文件了还重复创建则会报错 ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); if(keyfile == null)//都还没密钥文件(必须先于任何其他文件创建) ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); personfile = new BinaryFile(papdu.pdata);//传进的参数就是要写入的内容 return true; } //写入各种密钥 private boolean write_key() { return true; } //写入二进制文件 private boolean write_bin() { return true; } //读取二进制文件 private boolean read_bin() { return true; } /* * 功能:圈存命令的实现 */ private boolean load() { short rc; if(papdu.cla != (byte)0x80) ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED); if(papdu.p1 != (byte)0x00 && papdu.p2 != (byte)0x00) ISOException.throwIt(ISO7816.SW_WRONG_P1P2); if(EPfile == null) ISOException.throwIt(ISO7816.SW_FILE_NOT_FOUND); if(papdu.lc != (short)0x0B) ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); rc = EPfile.load(papdu.pdata); if(rc == 1) ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); else if(rc == 2) ISOException.throwIt(condef.SW_LOAD_FULL); else if(rc == 3) ISOException.throwIt(ISO7816.SW_RECORD_NOT_FOUND); papdu.le = (short)4; return true; } /* * 功能:圈存初始化命令的实现 */ private boolean init_load() { short num,rc; if(papdu.cla != (byte)0x80) ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED); if(papdu.p1 != (byte)0x00 && papdu.p2 != (byte)0x02) ISOException.throwIt(ISO7816.SW_WRONG_P1P2); if(papdu.lc != (short)0x0B) ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); if(EPfile == null) ISOException.throwIt(ISO7816.SW_FILE_NOT_FOUND); num = keyfile.findkey(papdu.pdata[0]); if(num == 0x00) ISOException.throwIt(ISO7816.SW_RECORD_NOT_FOUND); rc = EPfile.init4load(num, papdu.pdata); if(rc == 2) ISOException.throwIt((condef.SW_LOAD_FULL)); papdu.le = (short)0x10; return true; } /* * 功能:消费命令的实现 */ private boolean purchase(){ return true; } /* * 功能:余额查询功能的实现 */ private boolean get_balance(){ return true; } /* * 功能:消费初始化的实现 */ private boolean init_purchase(){ return true; } }
case condef.INS_CREATE_FILE: return create_file();
//这个文件表示的是一些常量 package purse; /** * 已给出部分常数值,其他根据需要自行添加 */ //存储INS值文件,ins值表示命令的具体意思 public class condef { //----------------- INS Byte ------------------- final static byte INS_CREATE_FILE = (byte)0xE0; //文件建立命令的INS值 final static byte INS_WRITE_KEY = (byte)0xD4; //写入密钥命令的INS值 final static byte INS_WRITE_BIN = (byte)0xD6; //写入二进制命令的INS值 final static byte INS_READ_BIN = (byte)0xB0; //读取二进制文件的INS值 final static byte INS_NIIT_TRANS = (byte)0x50; //初始化圈存和初始化消费命令的INS值 final static byte INS_LOAD = (byte)0x52; //圈存命令的INS值 //-------------- FILE TYPE Byte --------------- final static byte KEY_FILE = (byte)0x3F; //密钥文件的文件类型 final static byte CARD_FILE = (byte)0x38; //应用基本文件的文件类型 final static byte PERSON_FILE = (byte)0x39; //持卡人基本文件 的文件类型 final static byte EP_FILE = (byte)0x2F; //电子钱包文件的文件类型 //------------------------ SW --------------------- final static short SW_LOAD_FULL = (short)0x9501; //圈存超额 }
所以condef.java里面也定义了这部分文件类型的常量。
既然已经判断出了apdu命令是要我们创建文件,同时又判断出了要创建的是什么文件,接下来就简单了,就是直接调用几大文件数据结构的接口去创建相应的文件,同时,这里要判断一些异常的情况,也就是apdu命令有输入错误的情况,然后返回相应的异常给终端。比如:
if(papdu.cla != (byte)0x80) ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED); if(papdu.lc != (byte)0x07) ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); if(EPfile != null)//有文件了还重复创建则会报错 ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); if(keyfile == null)//都还没密钥文件(必须先于任何其他文件创建) ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
最后,看看运行结果,由于终端要发送不少命令,所以可以把命令全部放到文件里面,然后直接在命令行中读取文件,然后就会在模拟器中执行文件里面的所有命令了。
首先,把命令脚本名称改一下,主要是改后缀名为jcsh:
然后运行之后再命令行中先输入:
/set-var path "文件路径(不包括文件名.jcsh)"
查找文件路径的快速方法(通用电脑小常识),查询文件属性,然后就可以看到文件所在的路径了。
输入上面那条路径命令之后,就再输入文件名(不包括后缀),比如这里是purse,然后回车就可以让模拟器执行命令脚本里面的所有命令了,注意,有时候脚本文件里面会有一些奇奇怪怪的未注释的中文字符,比如中文的空格,有很多软件开发工具都对这些非常敏感会导致执行脚本出错,我在执行脚本的时候就遇到了,解决办法是重新建一个txt文件,然后改下文件名,把脚本命令复制进来。
命令脚本内容:
运行结果:
可以看到都是No Error.也就把四大文件都给建立好了,后面的就是在文件里面进行添加密钥等等的操作了,这是后面学习篇的内容。值得注意的是,每次重新运行的时候,模拟器不会保存你之前创建的文件,毕竟只是个模拟器嘛。所以除非一直运行着保留着上面创建文件的状态,否则重新运行后应当重新建立文件。