分类: 嵌入式
2010-07-30 16:25:27
标签: 无标签
2003 年 3 月 01 日
简介:在本系列的前两篇文章,我们讨论了智能卡与JAVA 卡的相关知识,此篇文章将着重介绍 JAVA 卡的 Applet一些基础知识,首先我们介绍了 JAVA 卡 Applet的开发环境和开发过程,并着重介绍一下 JAVA 卡 Applet的编译和转换过程。随后我们通过一个“电子钱包”例子的引入,进一步了解JAVA 卡 Applet开发的一些细节过程。为了便于介绍,在这篇文章中,我们只介绍在Windows 操作平台上的 Applet 的开发。
希望你在阅读这篇文章时,已经有了初步的 JAVA 语言的基础,并且已经阅读了本系列的前两篇篇文章:《 智能卡与安全》和 《 JAVA 卡概述》
硬件:PC(奔腾II 266 以上),读卡器,JAVA 卡
软件:Windows95/98/NT4.0, VJ++6.0, JDK1.2.2, Java Card 2.1.1 Development Kit
在上述软件清单中,Java Card 2.1.1 Development Kit(JAVA 卡开发工具)是开发 JAVA 智能卡 Applet 所特有的工具。我们在这对它进行简单的介绍。你能在 网址得到这个 JAVA 卡开发工具。 Java Card 2.1.1 Development Kit 主要包括了:
|
在介绍 JAVA 卡 Applet 生成过程之前,我们先来回顾一些 JAVA 智能卡的术语:
APDU(应用协议数据单元):用于智能卡与外界进行数据交换的基本命令单位。一个 APDU 要么包含一个指令消息,要么包含一个响应消息,这个消息发自智能卡或者读卡设备。它是智能卡与外界通信的基础。详细信息参照 ISO 7816-3 标准。
EEPROM (Electrically-Erasable Programmable Read-Only Memory):一种出厂后还能被写入数据的存储器
AID(Application identifier 应用ID号):ISO 7816-5 定义了 AID 的结构,为了能使每一个 Applet 都有一个唯一的ID身份号。JAVA 卡通过 AID 来确认 Applet。AID 是由ISO国际组织来管理的,所以它是唯一的。
JCRE(Java Card Runtime Environment JAVA 卡运行环境):它包括 JAVA 虚拟机,JAVA 卡的框架(Framework) 和一些基本函数(native functions)。
一些与生成 JAVA 卡 Applet 有关的重要的文件:
JAVA 卡的 Applet 生成大致可分为以下几个步骤:
在 JAVA 卡的开发过程中,编译和转换过程是两个非常关键的过程,现在我们来介绍一下这两个过程。在介绍之前,我们先来了解一下编译和转换时,一些文件在硬盘上存放的结构:
如下图所示。我们有三层目录:根目录,项目目录和 JavaCard 目录。根目录是存放所有项目目录的目录。项目目录存放着 JAVA 卡源代码文件和编译后的 class 文件。JavaCard 目录是由转换器自动生成的,它存放的是转换器的输出文件:Cap文件,Exp 文件和 Jca 文件。一般上述这些文件的文件名需与项目目录的目录名一致,正常情况下,我们就用项目名来命名项目目录和这些文件名,如下图所示:
编译命令行:
<编译执行程序的路径和程序名> <可选参数>
相信大家对 JAVA 编译命令行都较为熟悉了,不过请注意在 JAVA 卡 Applet 编译时我们只用“-g”参数,而不用“-O”参数,因为用“-g”参数时,我们能在 class 文件中产生“LocalVariableTable”属性,而这个属性在转换时(converter)要被使用到。但如果同时又使用了“-O”参数,“LocalVariableTable”属性就不会被产生了。这样转换时就会出错。另外 –d 参数指明生成的 class 文件放置的根目录,注意它是根目录,也就是说 class 文件位于此根目录下的项目目录中。
例如:
c:\JDK\bin\javac.exe –g –d c:\sample\ -classpath c:\jc211\bin\api21.jar c:\sample\Helloworld\Helloworld.java
如上命令行即完成了 HelloWorld.java 的编译,同时在编译时用到了 api21.jar 中的一些 class 文件
转换器(converter)是由 Java Card Development kit 提供的字节代码工具。作为字节代码工具,它需要 Java 解释器的帮助才能运行。它将 class 文件转换成一些输出文件。转换时,输入文件是:由编译器生成的 class 文件。输出文件是:Cap 文件,Export 文件,JCA 文件,它们的后缀分别是:*.cap,*.exp,*.jca,文件名与输入文件一致。它们将位于 Java 卡项目目录下的一个叫 Javacard 的子目录中。
转换器命令行:
<解释器> <-classpath> <被执行的 class> <可选参数> <包(package)名> <包 AID> <版本>
解释器:提供解释器的路径和文件名,如c:\JDK\bin\java.exe
被执行的 class :就是位于 converter.jar 中 com\sun\javacard\converter\converter\ 目录下的一些类。
在安装了 Java Card Development kit(Java 卡开发工具) 后,开发工具会提供给你一个批处理文件:converter.bat,它包含的就是命令行中:<解释器> <-classpath> <被执行的 class> 这三部分内容。也就是说,你在进行 Applet 转换时,对这三部分参数的可以不十分了解,而直接使用 converter.bat 进行文件转换。
命令行 的一些可选参数的介绍:
-classdir:项目的根目录
-exportpath:一些转换时要用到的 Exp 文件的父目录,
-d:输出的路径,它指明的是根目录
-applet [AID][classname]:指明缺省 Applet 的AID, 和含 Install() 方法的 class 文件名
-out [CAP][EXP][JCA]:说明要转换器生成什么文件,一般默认为生成 CAP 和 EXP 文件
-nobanner:信息使用标准输出
包(package)名:要被转换的包名
包 AID:5 到 16 十进制,十六进制或八进制数,表明 Applet 的 AID
版本:用户自定义的版本号
例如
c:\JDK\bin\java.exe –classpath c:\jc211\bin\converter.jar com.sun.javacard.converter.Converter -out EXP JCA CAP -exportpath c:\jc211\api21 -applet 0xa0:0x0:0x0:0x0:0x62:0x3:0x1:0xc:0x1:0x1 com.sun.javacard.samples.HelloWorld.HelloWorld com.sun.javacard.samples.HelloWorld 0xa0:0x0:0x0:0x0:0x62:0x3:0x1:0xc:0x1 1.0
或 converter.bat -out EXP JCA CAP -exportpath c:\jc211\api21 -applet 0xa0:0x0:0x0:0x0:0x62:0x3:0x1:0xc:0x1:0x1 com.sun.javacard.samples.HelloWorld.HelloWorld com.sun.javacard.samples.HelloWorld 0xa0:0x0:0x0:0x0:0x62:0x3:0x1:0xc:0x1 1.0
|
JAVA 卡 Applet 的开发过程与其他软件的开发过程是完全一样的,必须进行设计,实现,测试等过程。在这里我们简单介绍一下 JAVA 卡 Applet 的设计过程,并通过一个简单的例子的引入,进行每个过程的说明。
根据 JAVA 卡 Applet 的特点,一般它的设计过程有以下四个步骤:
功能定义是确定我们将要完成的 Applet 的功能,即定义 Applet 能做什么,而不能做什么。那我们这个 Applet 的功能是什么呢?
我们的例子是一个简单的“电子钱包”,它支持存款(credit),取款(debit), 和检查存款余额(get balance)等功能。当然,为了防治非法对“电子钱包”进行操作,这个简单的例子也包含了一些安全保护措施。它要求在使用此 “电子钱包”的某些功能前,如存款,取款等功能,用户的 PIN 码必须被验证。如果用户的 PIN 码连续三次验证都是错的,那么这个“电子钱包”就不能再被使用了。当然,真正的“电子钱包”的安全措施要复杂得多。同时,为了简化“电子钱包”存储过程,我们规定“电子钱包”的最大存储额为 32767(0X7FFF),每次的存取金额最多为 127(0X7F)。
其中 RID 是 ISO 分配给各个卡供应商的 ID 号,它们是唯一的。而 PIX 是由各个供应商自己来管理的 Applet 的 ID 号。AID 将会在转换过程中被使用。请参阅上文。当然,读者在编写自己的 Applet 时,AID 只需符合 ISO7816 的长度规定,而无须向 ISO 申请 RID 号。
JAVA 卡 Applet 必须从 javacard.framework.Applet 类扩展而来,并需要实现一些必须的方法。下面就是这些必须实现的方法,当 JAVA 卡收到终端发出的 APDU 命令后,这些方法就将被调用:
select ()
当 Applet 收到“Select”的 APDU 命令, 相对应的 Applet 的 select () 方法将被调用,在 select () 方法中,我们做一些初始化的工作。当 select () 方法返回 true, 则说明被选择的 Applet 已被选择,并准备好处理 APDU 命令。
deselect ()
当另一个 Applet 被选中,当前选中的 Applet 的 deselect () 方法将被调用,在 deselect () 方法中一般执行一些复位的工作,在本例子中,我们复位 PIN 码。
install (byte[] bArray, short bOffset, byte bLength)
Applet 必须用 install() 方法来创建一个 Applet 的实例,同时调用 register () 方法来注册这个实例
process (APDU apdu)
一旦 Applet 被选中,当 Applet 收到 APDU 指令时,process() 方法会被调用。在 process() 方法中,我们将分析 APDU 指令,从而进行相应处理,并返回相应的返回值。
register ()
调用register () 方法用来注册 Applet 的实例
还有一些经常被使用的方法:
getShareableInterfaceObject (AID client AID, byte parameter)
getShareableInterfaceObject() 方法是用来实现 Applet 之间相互通信的方法。
selectingApplet ()
由于当 Applet 被选中后,所有的 APDU 命令,包括 select 命令都会发送至 process() 方法, selectingApplet () 方法就是为了区别 select 命令与其他命令。一般 selectingApplet () 由 process() 方法调用,返回 true 说明是 select 命令。
Verify 指令
Verify APDU 指令 (验证 PIN 码指令 )- 终端发给卡 | ||||||
CLA |
INS |
P1 |
P2 |
Lc |
Data Field |
Le |
0xB0 |
0x20 |
0x0 |
0x0 |
PIN 码的长度 |
PIN 码 |
无 |
Verify APDU 响应 (验证 PIN 码指令响应)- 卡发给终端 | ||
Data Field | SW1 + SW2 (状态字) | 状态字含义 |
无 | 0x9000 | 命令执行成功 |
无 | 0x6300 | 验证 PIN 码失败 |
Data Field 在这里没有用
CREDIT 指令
CREDIT APDU 指令(存款指令)- 终端发给卡 | ||||||
CLA | INS | P1 | P2 | Lc | Data Field | Le |
0xB0 | 0x30 | 0x0 | 0x0 | 1 | 存款金额 | 无 |
为了方便介绍,我们规定每次的存取金额最多为 127(0X7F),所以存款金额(Data Field)用一个字节就能表示,而 Lc 一定为1
CREDIT APDU 响应 (存款指令响应)- 卡发给终端 | ||||||
Data Field | SW1 + SW2 (状态字) | 状态字含义 | ||||
无 | 0x9000 | 存款命令执行成功 | ||||
无 | 0x6A83 | 存款金额无效 | ||||
无 | 0x6A84 | 存储总金额超过最大值 | ||||
无 | 0x6301 | 存款前需验证 PIN 码 |
GetBalance 指令
GetBalance APDU 指令(余额查询指令)- 终端发给卡 | ||||||
CLA | INS | P1 | P2 | Lc | Data Field | Le |
0xB0 | 0x50 | 0x0 | 0x0 | 无 | 无 | 2 |
为了方便介绍,我们规定这张“电子钱包”卡的最大存储金额为 32767(0X7FFF),所以我们用两个字节就能得到存储金额的余额,所以 Le 为 2
GetBalance APDU 响应 (余额查询指令响应)- 卡发给终端 | ||||||
Data Field | SW1 + SW2 (状态字) | 状态字含义 | ||||
存款余额 | 0x9000 | 余额查询命令执行成功 |
除上面我们所提到的状态字(SW1+SW2),javacard.framework.ISO7816 中已定义了很多状态字可供我们使用,如 ISO7816.SW_INS_NOT_SUPPORTED 表明指令未知。我们可以直接使用这些状态字。
通过上文,我们已经知道,在 Applet 被选择(select)后,当 JAVA 卡收到 APDU 指令后,process() 方法会被调用,所有的有关 APDU 指令的处理都会在 process() 这个方法中被执行。一般我们执行以下五个过程来处理 APDU 指令:
以下就是这个“电子钱包”的例子代码:
package trans; //输入 javacardx.framework.*; import javacard.framework.*; public class trans extends Applet { //APDU的指令 final static byte VERIFY = (byte) 0x20;//验证PIN码命令 final static byte CREDIT = (byte) 0x30;//存款命令 final static byte DEBIT = (byte) 0x40;//取款命令 final static byte GETBALANCE = (byte) 0x50;//余额查询 //存储额最大值 final static short MAX_BALANCE = 0x7FFF; //存取金额最大值 final static byte MAX_TRANSACTION_AMOUNT = 127; //PIN码最多尝试值 final static byte PIN_TRY_LIMIT =(byte)0x03; //PIN最长的长度 final static byte MAX_PIN_SIZE =(byte)0x08; //一些返回值 //验证PIN 码失败 final static short SW_VERIFICATION_FAILED =0x6300; //此操作需验证PIN码 final static short SW_PIN_VERIFICATION_REQUIRED =0x6301; //存取金额值无效 final static short SW_INVALID_TRANSACTION_AMOUNT = 0x6A83; //存储额超过最大值 final static short SW_EXCEED_MAXIMUM_BALANCE =0x6A84; //存储额为负 final static short SW_NEGATIVE_BALANCE = 0x6A85; //PIN 码 OwnerPIN pin; //存款余额,最大值为0X7FFF = 32767 short balance; private trans (byte[] bArray,short bOffset,byte bLength){ //创建OwnerPIN,PIN 的最多尝试数为PIN_TRY_LIMIT, //长度最多为MAX_PIN_SIZE pin = new OwnerPIN(PIN_TRY_LIMIT, MAX_PIN_SIZE); //安装Applet 时,会传送PIN 的参数 pin.update(bArray, bOffset, bLength); //注册 register(); } public static void install(byte[] bArray, short bOffset, byte bLength){ //创建trans 实例,调用构造函数 new trans(bArray, bOffset, bLength); } public boolean select() { //如果PIN 码锁死,Applet 将不能被选择 if ( pin.getTriesRemaining() == 0 ) return false; return true; } public void deselect() { //重制PIN pin.reset(); } public void process(APDU apdu) { //用一个字节数组来处理APDU的头信息,和数据信息 // buffer 就是这个字节数组 byte[] buffer = apdu.getBuffer(); //cla为指令集 byte cla = buffer[ISO7816.OFFSET_CLA]; //ins为指令 byte ins = buffer[ISO7816.OFFSET_INS]; //检验指令集(cla)是否是正确,我们的Applet 指令 //集为”B0“,如果指令集错,我们将返回”指令 //未知“的错误响应 if (cla != 0xB0) ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED); //根据指令(ins),完成相应的命令 switch (ins) { case GETBALANCE: getBalance(apdu);//余额查询 return; case DEBIT: debit(apdu);//取款 return; case CREDIT: credit(apdu);//存款 return; case VERIFY: verify(apdu);//验证PIN码 return; default: ISOException.throwIt (ISO7816.SW_INS_NOT_SUPPORTED);//指令未知 } } private void credit(APDU apdu) { //检验PIN 码是否已被检验,若 //否,则无权存款,并返回相应错误代码 if ( ! pin.isValidated() ) ISOException.throwIt(SW_PIN_VERIFICATION_REQUIRED); //用一个字节数组来处理APDU的头信息,和数据信息 byte[] buffer = apdu.getBuffer(); //OFFSET_LC 用来得到APDU中数据信息的长度 byte numBytes = buffer[ISO7816.OFFSET_LC]; //APDU中实际得到的信息的长度 byte byteRead = (byte)(apdu.setIncomingAndReceive()); //如果长度不匹配,返回相应错误信息 if ( ( numBytes != 1 ) || (byteRead != 1) ) ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); //从缓冲的第六个字节得到存储金额 //ISO7816.OFFSET_CDATA=5 byte creditAmount = buffer[ISO7816.OFFSET_CDATA]; //验证存款金额是否超过最大值 if ( ( creditAmount > MAX_TRANSACTION_AMOUNT) || ( creditAmount < 0 ) ) ISOException.throwIt (SW_INVALID_TRANSACTION_AMOUNT); //验证存款后总金额是否超过最大值 if ( (short)( balance + creditAmount) > MAX_BALANCE ) ISOException.throwIt (SW_EXCEED_MAXIMUM_BALANCE); //存入存款金额 balance = (short)(balance + creditAmount); } private void debit(APDU apdu) { //检验PIN 码是否已被检验,若 //否,则无权取款,并返回相应错误代码 if ( ! pin.isValidated() ) ISOException.throwIt(SW_PIN_VERIFICATION_REQUIRED); //用一个字节数组来处理APDU的头信息,和数据信息 byte[] buffer = apdu.getBuffer(); //OFFSET_LC 用来得到APDU中数据信息的长度 byte numBytes = (byte)(buffer[ISO7816.OFFSET_LC]); //APDU中实际得到的信息的长度 byte byteRead = (byte)(apdu.setIncomingAndReceive()); //如果长度不匹配,返回相应错误信息 if ( ( numBytes != 1 ) || (byteRead != 1) ) ISOException.throwIt (ISO7816.SW_WRONG_LENGTH); //从缓冲的第六个字节获取取款数据 //ISO7816.OFFSET_CDATA=5 byte debitAmount = buffer[ISO7816.OFFSET_CDATA]; //判断取款数据是否大于每次允许的最大值 if ( ( debitAmount > MAX_TRANSACTION_AMOUNT) || ( debitAmount < 0 ) ) ISOException.throwIt (SW_INVALID_TRANSACTION_AMOUNT); //验证取款后余额是否为负 if ( (short)( balance - debitAmount ) < (short)0 ) ISOException.throwIt(SW_NEGATIVE_BALANCE); //取款 balance = (short) (balance - debitAmount); } private void getBalance(APDU apdu) { //用一个字节数组来处理APDU的头信息,和数据信息 byte[] buffer = apdu.getBuffer(); //setOutgoing()方法是告知终端Applet准备回传相应 //并得到期望响应长度 short le = apdu.setOutgoing(); //响应长度小于2,出错 if ( le < 2 ) ISOException.throwIt (ISO7816.SW_WRONG_LENGTH); //告知终端实际的响应长度 apdu.setOutgoingLength((byte)2); //将存储余额附给apdu的缓冲 buffer[0] = (byte)(balance >> 8); buffer[1] = (byte)(balance & 0xFF); //将apdu缓冲区中从0 位置后2 个字节长度的信息发出 apdu.sendBytes((short)0, (short)2); } private void verify(APDU apdu) { //用一个字节数组来处理APDU的头信息,和数据信息 byte[] buffer = apdu.getBuffer(); //APDU中实际得到的信息的长度,即PIN码长度 byte byteRead = (byte)(apdu.setIncomingAndReceive()); //验证PIN码,PIN码值在apdu缓冲中,从 //ISO7816.OFFSET_CDATA=5位置起, //长度=byteRead, if ( pin.check(buffer, ISO7816.OFFSET_CDATA, byteRead) == false ) ISOException.throwIt(SW_VERIFICATION_FAILED); } } |
当 JAVA 卡 Applet 开发完毕后,Cap 文件就可以被装载到 JAVA 智能卡上。安装时,我们需要使用读卡器。一般读卡器与 PC 之间的通信使用传统的串口,并口,或 USB 接口。PC 通过串口或 USB 接口向读卡器发送一定标准的指令,如微软的 PS/SC 接口指令,或读卡器制造商提供的接口指令。从而能让读卡器向智能卡发送装载(install)的 APDU 指令,完成 Applet 的装载。有关对读卡器的操作,超出了我们本次讨论的范围,如有兴趣,可与我联系。
通过本系列的三篇文章,我们介绍了智能卡,JAVA 卡和 JAVA 卡 Applet 的一些基本知识,希望读者通过阅读这一系列,能对 JAVA 卡和基于 JAVA 卡的一些 Applet 有所了解。如果你对 JAVA 卡和 JAVA 卡 Applet 有疑问,或对本系列有疑问,请 email 告诉我(sjbao@iname.com)。