Java Card
小应用程序结构
Sun
提供了两个模型用来设计
JavaCard
应用程序(
javacard.framework.Applet
):传统的
JavaCard API
和
JavaCard Remote Method Invocation
(
Java Card
远程方法调用,
JCRMI
)编程接口。我们可以使用其中任何一个来编写
Java Card
小应用程序,开发
Java Card
小应用程序是一个两步的过程:
Ø
1.
定义负责主应用程序和小应用程序之间接口的命令和响应
APDU
。
Ø
2.
编写
Java Card
小应用程序本身
JavaCard小应用程序结构
首先,让我们看一下
Java Card
小应用程序的结构。
列表
1
说明了一个典型的
JavaCard
小应用程序是如何构造的:
import javacard.framework.*
...
public class MyApplet extends Applet {
// Definitions of APDU-related instruction codes
...
MyApplet() {...} // Constructor
// Life-cycle methods
install() {...}
select() {...}
deselect() {...}
process() {...}
// Private methods
...
}
一个
JavaCard
小应用程序通常定义它的
APDU
相关指令、它的构造器,然后是
Java Card
小应用程序的生命周期方法:
install ()
、
select ()
、
deselect ()
和
process ()
。最后,它定义任何合适的私有方法。
定义APDU指令
不同的Java Card应用程序有不同的接口(APDU)需求。一个信用卡小应用程序可能支持验证PIN号码的方法,产生信用和借记事务,并且核对帐目余额。一个健康保险小应用程序可能提供访问健康保险信息、保险总额限制、医生、病人信息等等信息的权限。你定义的精确的APDU全依赖你的应用程序需求。
举例来说,让我们亲身感受一下如何开发经典的
Wallet
信用卡示例。你可以在
Sun Java Card Development
工具箱的
samples
目录下得到这个及其他示例的完整的代码。
我们将开始定义一个
APDU
命令来查询保存在
Java Card
设备上的当前余额数。注意,在一个实际信用卡应用程序中,我们还将定义信用并且借记命令。我们将分配我们的
Get Balance APDU
一个
0x80
指令类和一个
0x30
指令。
Get Balance APDU
不需要任何指令参数或者数据区,并且预期的响应由包含余额的两个字节组成。下一个表格描述
Get Balance APDU
命令:
表
1 - Get Balance APDU
命令
Name
|
CLA
|
INS
|
P1
|
P2
|
Lc
|
Data Field
|
Le (size of response)
|
Get
Balance
|
0x80
|
0x30
|
0
|
0
|
N/A
|
N/A
|
2
|
虽然
Get Balance
命令未定义输入数据,但是有一些命令
APDU
将定义输入数据。举例来说,让我们定义验证从卡片读取器中传递来的
PIN
号码的
Verify PIN APDU
命令。下一个表格定义
Verify APDU
:
表格
2- Verify APDU
命令
Name
|
CLA
|
INS
|
P1
|
P2
|
Lc
|
Data Field
|
Le (size of response)
|
Verify PIN
|
0x80
|
0x20
|
0
|
0
|
PIN Len
|
PIN Value
|
N/A
|
注意
Le
字段,响应的大小是
N/A
。这是因为没有到
Verify PIN
的应用程序特定响应;成功或者失败通过响应
APDU
中的状态字标明。
为了简化
APDU
过程,
javacard.framework.ISO7816
接口定义了许多常数,我们可以用来从
process ()
方法传送到小应用程序中的输入缓冲器中检索各个的
APDU
字段:
...
byte cla = buf[ISO7816.OFFSET_CLA];
byte ins = buf[ISO7816.OFFSET_INS];
byte p1 = buf[ISO7816.OFFSET_P1];
byte p2 = buf[ISO7816.OFFSET_P2];
byte lc = buf[ISO7816.OFFSET_LC];
...
// Get APDU data, by copying lc bytes from OFFSET_CDATA, into
// reusable buffer databuf.
Util.arrayCopy(buf, ISO7816.OFFSET_CDATA, databuf, 0, lc);
...
列表
2
、使用
ISO-7816-4
常数
现在我们将定义用于
Get Balance
和
Verify
命令的类
(CLA)
和指令
(INS)
,
Get Balance
响应的大小,以及在如果
PIN
验证失败后的出错返回代码。
// MyApplet APDU definitions
final static byte MyAPPLET_CLA = (byte)0x80;
final static byte VERIFY_INS = (byte)0x20;
final static byte GET_BALANCE_INS = (byte) 0x30;
final static short GET_BALANCE_RESPONSE_SZ = 2;
// Exception (return code) if PIN verify fails.
final static short SW_PINVERIFY_FAILED = (short)0x6900;
...
接下来,让我们定义小应用程序构造器和生命循环方法。
构造器
定义一个初始化这个对象的状态的私有构造器。这个构造器被从install()方法调用;换句话说,构造器只在小应用程序的生命周期期间被调用:
/**
* Private Constructor.
*/
private MyApplet() {
super();
// ... Allocate all objects needed during the applet's
// lifetime.
ownerPin = new OwnerPIN(PIN_TRY_LIMIT, MAX_PIN_SIZE);
...
// Register this applet instance with the JCRE.
register();
}
列表
4
、小应用程序构造器
在这个示例中,我们使用一个
javacard.framework.OwnerPIN
,一个描述个人识别号码的对象;这个对象将存在于
Java Card
小应用程序的一生。回忆一下本文第一部分中的
"
管理内存和对象
"
,在一个
Java Card
环境中,数组和基本类型将在对象声明中被声明,而且你应该最小化对象实例,以利于对象重用。在小应用程序生命周期期间,以创建对象一次。做到这点的一个简易的方法是在构造器中创建对象,并且从
install()
方法中调用这个构造器
-- install()
本身在小应用程序生命周期中只被调用一次。为了利于再使用,对象应该保持在范围中或者适当的引用中,用于小应用程序的生命周期,并且它们的成员的值在再使用之前适当的重置。因为一个垃圾收集程序并不总是可用,一个应用程序可能从不回收被分配给对象的存储空间。
install ()
方法
JCRE
在安装过程期间调用
install()
。你必须覆盖这个从
javacard.framework.Applet
类继承来的方法,并且你的
install ()
方法必须实例化这个小应用程序,如下:
/**
* Installs the Applet. Creates an instance of MyApplet. The
* JCRE calls this static method during applet installation.
* @param bArray install parameter array.
* @param bOffset where install data begins.
* @param bLength install parameter data length.
* @throw ISOException if the install method fails.
*/
public static void install(byte[] bArray, short bOffset, byte bLength)
throws ISOException {
// Instantiate MyApplet
new MyApplet();
...
}
列表
5
、
install ()
小应用程序生命周期方法
install ()
方法必须直接或者间接地调用
register ()
方法来完成安装;如果这步失败将导致安装失败。在我们的范例中,构造器调用
register()
。
select()
方法
JCRE
调用
select()
来通知已经被选作
APDU
过程的小应用程序。你不必实现这个方法,除非你想提供会话初始化或者个性化。
select()
方法必须返回
true
来指明它即将处理进入的
APDU
,或者返回
false
来拒绝选择。
javacard.framework.Applet
类的默认实现返回
true
。
/**
* Called by the JCRE to inform this applet that it has been
* selected. Perform any initialization that may be required to
* process APDU commands. This method returns a boolean to
* indicate whether it is ready to accept incoming APDU commands
* via its process() method.
* @return If this method returns false, it indicates to the JCRE
* that this Applet declines to be selected.
*/
public boolean select() {
// Perform any applet-specific session initialization.
return true;
}
deselect()
方法
JCRE
调用
deselect()
来通知小应用程序,它已经被取消选定了。你不必实现这个方法,除非你想提供会话清除。
javacard.framework.Applet
类的默认实现什么都不做。
/**
* Called by the JCRE to inform this currently selected applet
* it is being deselected on this logical channel. Performs
* the session cleanup.
*/
public void deselect() {
// Perform appropriate cleanup.
ownerPin.reset();
}
在我们的示例中,我们重置了
PIN
(个人识别号码)。
process()
方法
--
感受
APDU
的全过程
一旦一个小应用程序已经被选择,它将准备接收命令
APDUs
,如在本文第一部分中
"Java Card
小应用程序的生命周期
"
描写的。
回想一下被从主机端(客户端)应用程序发送到卡片的
APDU
命令,如下面的说明:
Figure 3. APDU
指令和响应流程
每次
JCRE
接收一个
APDU
命令(通过卡片读取器从主应用程序,或者如果使用
Sun Java Card Development
工具箱就通过
apdutool
),它调用小应用程序的
process()
方法,把输入命令当作一个参数传送给它(
APDU
命令输入缓冲中的参数)。
process()
方法然后:
Ø
1.
摘录
APDU CLA
和
INS
字段
Ø
2.
检索应用程序特定的
P1
、
P2
和数据字段
Ø
3.
处理
APDU
数据
Ø
4.
生成并发送一个响应
Ø
5.
优雅地返回,或者抛出相应的
ISO
异常
在此时,
JCRE
发送合适的状态字回到主应用程序,通过读卡器。
列表
8
显示一个样本
process()
方法。
/**
* Called by the JCRE to process an incoming APDU command. An
* applet is expected to perform the action requested and return
* response data if any to the terminal.
*
* Upon normal return from this method the JCRE sends the ISO-
* 7816-4-defined success status (90 00) in the APDU response. If
* this method throws an ISOException the JCRE sends the
* associated reason code as the response status instead.
* @param apdu is the incoming APDU.
* @throw ISOException if the process method fails.
*/
public void process(APDU apdu) throws ISOException {
// Get the incoming APDU buffer.
byte[] buffer = apdu.getBuffer();
// Get the CLA; mask out the logical-channel info.
buffer[ISO7816.OFFSET_CLA] =
(byte)(buffer[ISO7816.OFFSET_CLA] & (byte)0xFC);
// If INS is Select, return - no need to process select
// here.
if ((buffer[ISO7816.OFFSET_CLA] == 0) &&
(buffer[ISO7816.OFFSET_INS] == (byte)(0xA4)) )
return;
// If unrecognized class, return "unsupported class."
if (buffer[ISO7816.OFFSET_CLA] != MyAPPLET_CLA)
ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
// Process (application-specific) APDU commands aimed at
// MyApplet.
switch (buffer[ISO7816.OFFSET_INS]) {
case VERIFY_INS:
verify(apdu);
break;
case GET_BALANCE_INS:
getBalance(apdu);
break;
default:
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
break;
}
}
我们的
process()
方法调用
getBalance()
和
verify()
方法。列表
9
显示
getBalance ()
方法,处理
get balance APDU
并且返回保存在卡片中的余额。
/**
* Retrieves and returns the balance stored in this card.
* @param apdu is the incoming APDU.
*/
private void getBalance(APDU apdu) {
// Get the incoming APDU buffer.
byte[] buffer = apdu.getBuffer();
// Set the data transfer direction to outbound and obtain
// the expected length of response (Le).
short le = apdu.setOutgoing();
// If the expected size is incorrect, send a wrong-length
// status word.
if (le != GET_BALANCE_RESPONSE_SZ)
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
// Set the actual number of bytes in the response data field.
apdu.setOutgoingLength((byte)GET_BALANCE_RESPONSE_SZ);
// Set the response data field; split the balance into 2
// separate bytes.
buffer[0] = (byte)(balance >> 8);
buffer[1] = (byte)(balance & 0xFF);
// Send the 2-byte balance starting at the offset in the APDU
// buffer.
apdu.sendBytes((short)0, (short)GET_BALANCE_RESPONSE_SZ);
}
getBalance ()
方法通过调用
APDU.getBuffer ()
方法取得一个引用到
APDU
缓冲。在返回响应(当前余额)之前,小应用程序必须设置
JCRE
模式通过调用
APDU.setOutgoing()
方法来发送,方便地返回期望的响应大小。我们还必须设置响应数据字段中的字节的实际数字,通过调用
APDU.setOutgoingLenth()
。
APDU
缓冲中的响应事实上通过调用
APDU.sendBytes ()
发送
小应用程序不直接发送返回码(状态字);一旦小应用程序调用
APDU.setOutgoing ()
并且提供任何请求的信息,
JCRE
注意这个状态字。状态字的值依靠
process()
方法如何使返回到
JCRE
来变化。如果所有的已经正常运行,
JCRE
将返回
9000
,指明无错。你的小应用程序可以通过抛出一个定义在
ISO7816
接口中的异常返回一个错误代码。
在列表
9
中,如果期望响应的大小不正确,方法
getBalance()
抛出一个
ISO7816.SW_WRONG_LENGTH
代码。对于有效的状态码值,请参阅
ISO7816
接口的定义,或者回到本文第一部分的
"
响应
APDU"
。
现在让我们看看在列表
10
中的
verify()
方法。因为我们定义的验证
PIN APDU
命令包含数据,
verify()
方法必须调用
APDU.setIncomingAndReceive ()
方法,设置
JCRE
为接收模式,然后接收输入数据。
**
* Validates (verifies) the Owner's PIN number.
* @param apdu is the incoming APDU.
*/
private void verify(APDU apdu) {
// Get the incoming APDU buffer.
byte[] buffer = apdu.getBuffer();
// Get the PIN data.
byte bytesRead = (byte)apdu.setIncomingAndReceive();
// Check/verify the PIN number. Read bytesRead number of PIN
// bytes into the APDU buffer at the offset
// ISO7816.OFFSET_CDATA.
if (ownerPin.check(buffer, ISO7816.OFFSET_CDATA, byteRead)
== false )
ISOException.throwIt(SW_PINVERIFY_FAILED);
}
这个方法通过调用
APDU.getBuffer()
取得一个到
APDU
缓冲的引用,调用
APDU.setIncomingAndReceive()
来接收命令数据,从输入的
APDU
缓冲中取得
PIN
数据,并且验证
PIN
。一个验证失败导致状态码
6900
被发送回主应用程序。
有时输入的数据比填充到
APDU
缓冲中的数据要多,并且小应用程序必须大块的读取数据知道没有数据可以读取。在此情况下,我们必须首先调用
APDU.setIncomingAndReceive()
,然后重复地调用
APDU.receiveBytes()
,直到不再有数据可用。列表
11
显示如何读取大量输入数据。
...
byte[] buffer = apdu.getBuffer();
short bytes_left = (short) buffer[ISO.OFFSET_LC];
short readCount = apdu.setIncomingAndReceive();
while (bytes_left > 0) {
// Process received data in buffer; copy chunk to temp buf.
Util.arrayCopy(buffer, ISO.OFFSET_CDATA, tbuf, 0, readCount);
bytes_left -= readCount;
// Get more data
readCount = apdu.receiveBytes(ISO.OFFSET_CDDATA);
}
...
由于每个大块被读取,小应用程序可以把它添加到另一个缓冲中,否则仅仅处理它。