Java Card 技术(二)
2009-11-09 21:01:04 来源:WEB开发网核心提示:本系列文章的第 1 部分介绍了 java Card 技术的高级知识 —— 什么是智能卡、Java Card 应用程序的元素、通信和不同 Java Card 技术规范的汇总,在本部分中,Java Card 技术(二),我们将重点介绍 Java Card applet 的开发:开发 Java Card 应用程序、Sun J
本系列文章的第 1 部分介绍了 java Card 技术的高级知识 —— 什么是智能卡、Java Card 应用程序的元素、通信和不同 Java Card 技术规范的汇总。在本部分中,我们将重点介绍 Java Card applet 的开发:开发 Java Card 应用程序、Sun Java Card 开发工具箱、Java Card 和 Java Card RMI API 时所需的一般步骤。
<script src="/a_dir_d/ads_250x250.js"></script>
开发 Java Card 应用程序
创建 Java Card 应用程序的典型步骤如下:
编写 Java 源代码。
编译 源代码。
将类文件 转换 为 Converted Applet(CAP)文件。
验证 CAP 是否有效;此步骤为可选。
安装 CAP 文件。
使用 Java 程序设计语言开发传统程序时,前两个步骤是相同的:编写 .java 文件并将它们编译为 .class 文件。虽然您已经创建了 Java Card 类文件,但是该过程仍可改变。
Java Card Virtual Machine(JCVM)被划分为卡片外部 JVM 和卡片内部 JVM。这种划分移除了开销较大的卡片外部操作,并且考虑到了卡片内部的内存占用量较小,但是这会导致开发 Java Card 应用程序的步骤增加。
将 Java Card 类载入 Java Card 设备之前,必须将它们转换为标准的 CAP 文件格式,然后选择性地进行验证:
转换时必须将每个 Java 软件包转换为 CAP 文件,其中一个软件包中包含了类和接口的联合二进制表示法。转换是一项卡片外部的操作。
验证是一个可选过程,目的是验证 CAP 文件结构、有效的字节码子集、软件包之间的依赖关系。您可能想对要使用的第三方供应商的软件包进行验证,或者,如果您的转换器工具由第三方供应商提供,您希望对其进行验证。验证通常是一个卡片外部的操作,但是一些卡片产品可能包含机载的检验器。
完成验证之后,便可以将 CAP 安装到 Java Card 设备中了。
Sun Java Card 开发工具箱
您可以使用 Sun Java Card 开发工具箱编写 Java Card applet,甚至在没有智能卡或卡片读取器的情况下对它们进行测试。该工具箱包含了需要开发和测试 Java Card applet 的所有基本工具:
Java Card 工作站开发环境(JCWDE)是一个方便且易于使用的 Java Card 模拟工具,使开发人员无需转换和安装 CAP 文件而直接执行类文件。JCWDE 可以使用调试器和 IDE 进行集成。
从这个开发工具箱的2.2.1版本开始,JCWDE 支持 Java Card RMI(JCRMI)。请注意 JCWDE 不是一个成熟的 Java Card 仿真器。它还不支持 JCRE 的许多功能,例如软件包安装、applet 实例创建、防火墙和事务。要了解更多信息,请参考开发工具箱的《用户指南》。
C 语言 Java Card 运行时环境(C-JCRE)是一个用 C 语言编写的可执行参考实现。C-JCRE 是 Java Card API、VM 和运行时环境的完全兼容的实现。它能让开发人员在工作站环境下准确地测试 applet 的行为。
C-JCRE 的限制很少:在卡片会话期间,它支持多达 8 个可以返回的远程引用、多达 16 个可以同时导出的远程对象、在远程方法中多达 8 个数组类型的参数、多达 32 个受支持的 Java 软件包和多达 16 个的 Java Card applet。要了解关于限制方面的更多信息,请参考《 Java Card 开发工具箱用户指南》。
Java Card 转换工具,用于生成 CAP 文件。
Java Card 检验器,用于可选地检查 CAP 和导出文件的有效性。
一个用于发送和接收应用程序协议数据单元(APDU)的 APDU 工具(apdutool)。这就是测试期间如何将 APDU 发送给 Java Card applet 的过程。您可以创建 apdutool 读取的脚本文件,以便将 APDU 发送到 C-JCRE 或 JCWDE。
一个 capdump工具,用于转储 CAP 的内容和一个打印 EXP 文件的 exp2text。
一个 scriptgen 工具,用于将 CAP 文件转换为 APDU 脚本文件。该工具简称为卡片外部安装程序。
支持库(用于 Java Card API 的类文件和导出文件)、文档和示例。
虽然 Sun Java Card 开发工具箱允许编写并测试 Java Card applet,部署真正的端到端智能卡应用程序却要求这些工具不被包含在开发工具箱内,例如:像 OpenCard 和 Global Platform API 这样的终端 API 的使用。可能还要求使用像用户识别模块(Subscriber Identification Module,SIM)这样的工具箱来帮助你管理 SIM。
表 1 显示了工具箱的目录结构(Windows 版本),以及包含开发工具的 bin 目录内容。
图 1a. 开发工具箱目录结构
图 1b. Bin 目录的内容
现在让我们重新访问 Java Card 开发步骤,记住这次要使用 Sun Java Card 开发工具箱:
使用您喜欢的编辑器或 IDE 编写 Java 源代码。
使用您喜欢的编译器或 IDE 编译 源代码。
使用 JCWDE 仿真器测试 Java Card applet。这是一个可选的步骤。请记住 JCWDE 不是一个成熟的 Java Card 仿真器。
使用工具箱中 bin 目录中的 转换器 工具将类文件转换 为 Converted Applet(CAP)。请注意,除了类文件之外,向转换工具的另一个输入为 导出文件,提供由应用程序所导入软件包的相关信息;这些软件包还会被加载到卡片中。导出文件还是转换器工具的一个输出结果。
验证 CAP 的有效性。这一步是可选的。这一步包括使用 verifycap 脚本来验证 CAP 文件的有效性,使用 verifyexp 来验证导出文件,并且使用 verifyrev 验证软件包修订版本间的二进制兼容性。工具 verifycap、verifyexp 和 verifyrev 脚本都在 bin 目录中可以找到。
安装 CAP 文件。使用 scriptgen 工具将 CAP 文件转换为(安装) APDU 脚本文件。然后使用 apdutool 将脚本文件(安装 APDU 命令和 CAP 文件)发送到 Java Card 设备上的 C-JCRE 或 JCRE。JCRE 将 CAP 文件保存卡片内存中。
下列图总结了这些步骤。请注意,每个Java Card 供应商提供自己的工具,但是开发 Java Card applet 的步骤通常在开发工具箱之间是相同的:
图 2. Java Card 开发步骤
要了解关于如何使用 Sun 的 Java Card 开发工具箱方面的更多信息,请参见《 Java Card 开发工具箱用户指南 》,从中找到工具箱文档目录。另一个优秀的参考资料是文章“使用 Java Card 开发工具箱”。
编写卡片端 Java Card Applet
Sun 提供了两个模型用来编写 Java Card applet(javacard.framework.Applet):传统的 Java Card API 或者 Java Card 远程方法调用(JCRMI) API。我们可以使用其中任何一个模型来编写 Java Card applet。
使用 Java Card API
开发 Java Card applet 是一个两步完成的过程:
定义负责主机应用程序和 applet 之间接口的 APDU 命令和响应。
编写 Java Card applet 本身
首先,让我们看一下 Java Card applet 的结构。
Applet 结构
清单 1 说明了构造 Java Card applet 的方式:
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
...
}
清单 1. Java Card Applet 的结构
Java Card applet 通常定义它的 APDU 相关指令、它的构造函数和 Java Card applet 生命周期方法:install()、select()、deselect() 和 process()。最后,它还定义任意适当的专用方法。
定义 APDU 指令
不同的 Java Card 应用程序具有不同的接口(APDU)需求。信用卡 applet 可能支持验证 PIN 号的方式,完成信用和借记事务,并检查帐户的卡片余额。健康保险 applet 可能提供对健康保险信息、保险总额限制、医生、病人信息等内容的访问。需要根据应用程序的需求定义的准确的 APDU 。
举例来说,让我们完成经典的 Wallet 信用卡示例的一些部分。您能在 Sun Java Card 开发工具箱中的示例目录下发现本示例和其他示例的完整源代码。
首先,我们定义一个 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 命令会定义传入的数据。下面给出一个示例。让我们定义 Verify PIN APDU 命令,它验证从卡片读取器中传递来的 PIN 号。下一个表格定义了 Verify APDU:
表 2 — Verify APDU 命令
名称 CLA INS P1 P2 Lc Data Field Le (size of response)
验证 PIN 0x80 0x20 0 0 PIN Len PIN Value N/A
请注意,Le 字段、响应的大小是 N/A。这是由于不存在特定于应用程序的响应对 PIN 进行验证;成功或失败通过响应中 APDU 的状态字显示。
为了便于 APDU 处理,javacard.framework.ISO7816 接口定义了许多常量,我们可以用来从输入缓冲器中检索各个 APDU 字段,其中输入缓冲器通过 process() 方法被传递到 applet。
...
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 常量
现在,我们将定义类(CLA)和指令(INS),用于 Get Balance 和 Verify 命令,所获取卡片余额响应的大小,以及如果 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;
...
清单 3. Applet 的 APDU 定义
接下来,让我们定义一些 applet 构造函数和生命周期方法。
构造函数
定义一个初始化对象状态的专用构造函数。从 install() 方法中调用该构造函数;换句话说,构造函数在 applet 生命周期内只被调用一次。
/**
* 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. Applet 构造函数
在这个示例中,我们使用对象 javacard.framework.OwnerPIN表示个人识别号码;这个对象将存在于 Java Card Applet 的生命周期期间。回忆一下本系列文章的第 1 部分中的“管理内存和对象”部分,在 Java Card 环境中,数组和基本类型在对象声明中被声明,为了有利于对象的重用,应该最大限度减少对象实例化。在 applet 生命周期内只创建对象一次。轻松完成这一操作的一个简易的方法是在构造函数中创建对象,从 install() 方法(在 applet 生命周期内只创建一次)调用这个构造函数。为了利于重用,对象保留在原有存储范围内或在 applet 生命周期内被充分地引用,且这些成员变量的值在重用前应适当重置。由于垃圾收集器并不是总是可用的,应用程序可能从不回收分配给超出范围的对象的存储空间。
install() 方法
JCRE 在安装过程中调用 install()。您必须覆盖这个从 javacard.framework.Applet 类中继承的方法,且 install() 方法必须实例化 applet,如下所示:
/**
* 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() Applet 生命周期方法
install() 方法必须直接或间接调用 register() 方法来完成安装;如果这步失败将导致安装失败。在这个示例中,构造函数调用 register()。
select() 方法
JCRE 调用 select() 以便通知 applet 已经被选中用于 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;
}
清单 6. select() Applet 生命周期方法
deselect() 方法
JCRE 调用 deselect() 来通知 applet 已经被取消选中。除非想提供会话清除,否则不必实现该方法。通过 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();
}
清单 7. deselect() Applet 生命周期方法
在本示例中,我们将重置 PIN。
process() 方法 — 与 APDU 协同工作
一旦 applet 已经被选择,它就准备接收 APDU 命令,如 第 1 部分 中“Java Card Applet 的生命周期”一节中所述。
回想一下,APDU 命令从主机端(客户机端)应用程序中被发送到卡片上,如下所示:
图 3. APDU 命令、主机应用程序和 Java Card Applet 之间的响应流程
每当 JCRE 接收到 APDU 命令(从主机应用程序通过卡片读取器完成,或者如果使用 Sun Java Card 开发工具箱时通过 apdutool 完成)时,JCRE 调用 applet 的 process() 方法,将之作为一个参数传递给传入的命令(该参数位于 APDU 命令输入缓冲器中)。然后 process() 方法执行下列操作:
提取 APDU CLA 和 INS 字段
检索特定于应用程序的 P1、P2 和数据字段
处理 APDU 数据
生成并发送响应
适度返回或抛弃适当的 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;
}
}
清单 8. process() Applet 生命周期方法
process() 方法调用 getBalance() 和 verify() 方法。清单 9 显示了处理获取卡片余额 APDU 的方法 getBalance(),并将卡片余额返回并保存在卡片内。
/**
* 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);
}
清单 9. 处理 Get Balance APDU
getBalance() 方法通过调用 APDU.getBuffer() 方法获得对 APDU 缓冲器的引用。在返回响应(当前的卡片余额)前,applet 设置 JCRE 的模式以便通过调用方便返回预期响应大小的 APDU.setOutgoing() 方法发送。通过调用 APDU.setOutgoingLenth(),我们还必须设置在响应数据字段中字节的实际个数。在 APDU 缓冲器中的响应实际上通过调用 APDU.sendBytes() 发送。
Applet 并不直接返回代码(状态字);一旦 applet 调用 APDU.setOutgoing() 并提供所请求的信息,JCRE 便负责这项操作。状态字的值根据 process() 方法返回到 JCRE 的方式而发生变化。如果一切进展顺利,JCRE 将返回 9000,指示没有错误。Applet 可以通过抛出在 ISO7816 接口中所定义的一种异常或者应用程序制定的值来返回错误代码。在清单 9 中,如果预期响应不正确,方法 getBalance() 会抛出一个 ISO7816.SW_WRONG_LENGTH 代码。有效的状态代码值参考 ISO7816 接口的定义,或参考本文中第一部分中“响应 APDU”一节。
现在让我们查看一下清单 10 中的 verify() 方法。因为我们定义了 verify PIN APDU 命令要包含数据,因此 verify() 方法必须调用设置 JCRE 为接收模式的 APDU.setIncomingAndReceive() 方法,然后接收传入的数据。
/**
* 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);
}
清单 10. 处理 Verify APDU
这个方法通过调用 APDU.getBuffer() 获得对 APDU 缓冲器的引用,调用APDU.setIncomingAndReceive() 来接收命令数据,从传入的 APDU 缓冲器中获取 PIN 数据并验证 PIN。验证失败将引起状态代码 6900 被发送回主机应用程序。
有时所传入的数据会超过 APDU 缓冲器的数据容量,applet 必须以块的形式读取数据,直到不再有要读取的数据。在这种情况下,我们必须首先调用 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);
}
...
清单 11. 读取大量的传入数据
当每个块被读取时,applet 只得将其附加到另一个缓冲器上,否则只好处理它。
使用 Java Card RMI API
第二个可以进行 Java Card applet 编程的模型是基于 J2SE RMI 分布式对象模型的 Java Card RMI(JCRMI)。
该方法提供了以对象为中心的模型,其中 APDU 通信和处理在前面一节中被抽象化;而现在您需要处理这些对象。这一过程可以简化了基于 Java Card 技术的设备编程和集成。
在 RMI 模型中,服务器应用程序创建并生成可访问的远程对象,并且客户机应用程序获得对服务器的远程对象的远程引用,然后为这些对象调用它们的远程方法。在 JCRMI 中,Java Card applet 是服务器,主机应用程序是客户机。
Java Card RMI 简介
两个软件包提供了对 Java Card RMI 的支持:
java.rmi 定义了 Java 2 标准版 java.rmi 软件包的子集。它还定义了 Remote 接口和 RemoteException 类。除此之外,不包含其它传统的 java.rmi 类。
javacard.framework.service 定义了 Java Card applet 服务类,其中包括 RMI 服务类 CardRemoteObject 和 RMIService。
类 CardRemoteObject 定义了两种方法来启用和禁用对来自卡片外部的 对象 的远程访问。类 RMIService 处理 RMI 请求(将传入的 APDU 命令转换为远程方法调用)。
编写 JCRMI 应用程序类似于编写典型的基于 RMI 的应用程序:
将远程类的行为设为一个接口。
编写远程类的服务器实现和支持类。
编写一个使用远程服务的客户机程序和支持类。
请注意,正如您稍候将要看到的,JCRMI 不改变 applet 的基本结构和生命周期。
远程接口
创建远程服务的第一步是定义它的可见行为。远程接口定义 applet 提供的服务。正如标准 J2SE RMI 中规定的那样,所有 Java Card RMI 远程接口必须扩展 java.rmi.Remote 接口。要详细说明这一点,在此给出一个远程接口,公开获取卡片余额并将之保存在卡片里的方法。
import java.rmi.*;
import javacard.framework.*;
public interface MyRemoteInterface extends Remote {
...
public short getBalance() throws RemoteException;
...
// A complete credit card application would also define other
// methods such as credit() and debit() methods.
...
}
清单 12. 远程接口
MyRemoteInterface 定义了检索存储在智能卡中卡片余额的远程方法,在本示例中为 getBalance() 方法。请注意,除了特定于 Java Card 的导入之外,这个远程接口看起来完全象一个标准的 RMI 远程接口。
服务器实现
下一步是实现服务器的行为。服务器实现由 Java Card applet、已经定义的任意远程接口的实现和任意特定于应用程序的相关类组成。
Java Card Applet
Java Card applet 是一个 JCRMI 服务器,并且是可用于主机(客户机)应用程序的远程对象的所有者。在下面的图中解释了典型的 Java Card RMI applet 结构:
图 4. 典型的 Java Card RMI Applet 的结构
当与显式处理 APDU 消息的 applet 比较时,基于 JCRMI 的 applet 更像一个对象容器。正如您在图 4 中看到的,基于 JCRMI 的 applet 有一个或多个远程对象,一个 APDU Dispatcher 和一个接收 APDU 并将它们转换为远程方法调用的 RMIService。Java Card 远程类可以扩展 CardRemoteObject 类,以便自动导出对象,使得远程应用为可见状态。
JCRMI applet 必须扩展 javacard.framework.Applet,遵循标准的 applet 结构,并定义适当的生命周期方法。还必须安装和自我注册,并分发 APDU。下列代码片断说明一个基于 JCRMI 的 applet 的典型结构:
public class MyApplet extends javacard.framework.Applet {
private Dispatcher disp;
private RemoteService serv;
private Remote myRemoteInterface;
/**
* Construct the applet. Here instantiate the remote
* implementation(s), the APDU Dispatcher, and the
* RMIService. Before returning, register the applet.
*/
public MyApplet () {
// Create the implementation for my applet.
myRemoteInterface = new MyRemoteInterfaceImpl();
// Create a new Dispatcher that can hold a maximum of 1
// service, the RMIService.
disp = new Dispatcher((short)1);
// Create the RMIService
serv = new RMIService(myRemoteInterface);
disp.addService(serv, Dispatcher.PROCESS_COMMAND);
// Complete the registration process
register();
}
...
Applet 创建一个 Dispatcher 和 RMIService 来处理传入的 JCRMI APDU。
...
/**
* 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.
*/
public static void install(byte[] aid, short s, byte b) {
new MyApplet();
}
在 Java Card 环境下,JVM 的生命周期是物理卡片的生命周期,且不是所有的 Java Card 实现都提供垃圾收集器,因此有必要最大限度减少内存分配。对象在安装时创建,因此,只一次性向这些对象分配内存。
/**
* 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.
*
* This JCRMI version of the applet dispatches remote
* invocation APDUs by invoking the Dispatcher.
*
* 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 install method fails.
*/
public void process(APDU apdu) throws ISOException {
// Dispatch the incoming command APDU to the RMIService.
disp.process(apdu);
}
}
Applet 的 process() 接收了一个 APDU 命令并将该命令分发给 RMIService,RMIService 通过将该命令转换为 RMI 调用和后续响应而进行处理。
清单 13. Java Card RMI Applet
实现 Remote 对象
实现 JCRMI 远程对象类似于实现标准的 J2SE RMI 远程对象。二者的主要不同之处在于,在 JCRMI 中,远程对象有扩展 CardRemoteObject (除了实现远程接口之外)的选择。
CardRemoteObject 定义了两种方法,export() 和 unexport(),分别用于启用和禁用对来自卡片外部对象的访问。通过扩展 CardRemoteObject,可以自动导出远程对象的所有方法。如果决定不扩展 CardRemoteObject,您将负责通过调用 CardRemoteObject.export() 将它们导出。
import java.rmi.RemoteException;
import javacard.framework.service.CardRemoteObject;
import javacard.framework.Util;
import javacard.framework.UserException;
/**
* Provides the implementation for MyRemoteInterface.
*/
public class MyRemoteImpl extends CardRemoteObject implements MyRemoteInterface {
/** The balance. */
private short balance = 0;
/**
* The Constructor invokes the superclass constructor,
* which exports this remote implementation.
*/
public MyRemoteImpl() {
super(); // make this remote object visible
}
/**
* This method returns the balance.
* @return the stored balance.
* @throws RemoteException if a JCRMI exception is
* encountered
*/
public short getBalance() throws RemoteException {
return balance;
}
// Other methods
...
}
清单 14. 远程对象实现
完整的 Java Card RMI 应用程序流程
现在让我们总结一下 JCRMI 应用程序的流程图。客户机(主机)应用程序通过将 RMI APDU 传递给卡片内部的 JCRE,继而依次将这些 APDU 转发给适当的 JCRMI applet 来调用 RMI。这个 applet 将所接收到的 APDU 分发给 RMIService,继而依次处理 APDU 并将其转换为 RMI 调用。JCRMI applet 的典型流程如下所示:
图 5. 基于 Java Card RMI 的 Applet 的流程
简言之,熟练运用基于 APDU 的消息传递模型,JCRMI 提供了一个分发模型机制。JCRMI 消息被封装在被传递到 RMIService 的 APDU 消息内。RMIService负责解码 APDU 命令,将它们转换为方法调用和响应。这使得服务器往复地和客户机通信、传递方法信息、参数并返回值。
结束语
“Java Card 技术简介”的第 2 部分介绍了 Java Card applet 开发的方方面面:Java Card applet 的结构、Sun Java Card 开发工具箱、API 和可用于编写 applet 的编程模型:Java Card API 和 Java Card RMI API。
本系列文章的第 3 部分将介绍主机应用程序和可用于编写主机应用程序的 Java API:OpenCard Framework、Java Card RMI Client API 和用于 J2ME 的安全性和信任服务应用编程接口(SATSA)。
<script src="/a_dir_d/ads_250x250.js"></script>
开发 Java Card 应用程序
创建 Java Card 应用程序的典型步骤如下:
编写 Java 源代码。
编译 源代码。
将类文件 转换 为 Converted Applet(CAP)文件。
验证 CAP 是否有效;此步骤为可选。
安装 CAP 文件。
使用 Java 程序设计语言开发传统程序时,前两个步骤是相同的:编写 .java 文件并将它们编译为 .class 文件。虽然您已经创建了 Java Card 类文件,但是该过程仍可改变。
Java Card Virtual Machine(JCVM)被划分为卡片外部 JVM 和卡片内部 JVM。这种划分移除了开销较大的卡片外部操作,并且考虑到了卡片内部的内存占用量较小,但是这会导致开发 Java Card 应用程序的步骤增加。
将 Java Card 类载入 Java Card 设备之前,必须将它们转换为标准的 CAP 文件格式,然后选择性地进行验证:
转换时必须将每个 Java 软件包转换为 CAP 文件,其中一个软件包中包含了类和接口的联合二进制表示法。转换是一项卡片外部的操作。
验证是一个可选过程,目的是验证 CAP 文件结构、有效的字节码子集、软件包之间的依赖关系。您可能想对要使用的第三方供应商的软件包进行验证,或者,如果您的转换器工具由第三方供应商提供,您希望对其进行验证。验证通常是一个卡片外部的操作,但是一些卡片产品可能包含机载的检验器。
完成验证之后,便可以将 CAP 安装到 Java Card 设备中了。
Sun Java Card 开发工具箱
您可以使用 Sun Java Card 开发工具箱编写 Java Card applet,甚至在没有智能卡或卡片读取器的情况下对它们进行测试。该工具箱包含了需要开发和测试 Java Card applet 的所有基本工具:
Java Card 工作站开发环境(JCWDE)是一个方便且易于使用的 Java Card 模拟工具,使开发人员无需转换和安装 CAP 文件而直接执行类文件。JCWDE 可以使用调试器和 IDE 进行集成。
从这个开发工具箱的2.2.1版本开始,JCWDE 支持 Java Card RMI(JCRMI)。请注意 JCWDE 不是一个成熟的 Java Card 仿真器。它还不支持 JCRE 的许多功能,例如软件包安装、applet 实例创建、防火墙和事务。要了解更多信息,请参考开发工具箱的《用户指南》。
C 语言 Java Card 运行时环境(C-JCRE)是一个用 C 语言编写的可执行参考实现。C-JCRE 是 Java Card API、VM 和运行时环境的完全兼容的实现。它能让开发人员在工作站环境下准确地测试 applet 的行为。
C-JCRE 的限制很少:在卡片会话期间,它支持多达 8 个可以返回的远程引用、多达 16 个可以同时导出的远程对象、在远程方法中多达 8 个数组类型的参数、多达 32 个受支持的 Java 软件包和多达 16 个的 Java Card applet。要了解关于限制方面的更多信息,请参考《 Java Card 开发工具箱用户指南》。
Java Card 转换工具,用于生成 CAP 文件。
Java Card 检验器,用于可选地检查 CAP 和导出文件的有效性。
一个用于发送和接收应用程序协议数据单元(APDU)的 APDU 工具(apdutool)。这就是测试期间如何将 APDU 发送给 Java Card applet 的过程。您可以创建 apdutool 读取的脚本文件,以便将 APDU 发送到 C-JCRE 或 JCWDE。
一个 capdump工具,用于转储 CAP 的内容和一个打印 EXP 文件的 exp2text。
一个 scriptgen 工具,用于将 CAP 文件转换为 APDU 脚本文件。该工具简称为卡片外部安装程序。
支持库(用于 Java Card API 的类文件和导出文件)、文档和示例。
虽然 Sun Java Card 开发工具箱允许编写并测试 Java Card applet,部署真正的端到端智能卡应用程序却要求这些工具不被包含在开发工具箱内,例如:像 OpenCard 和 Global Platform API 这样的终端 API 的使用。可能还要求使用像用户识别模块(Subscriber Identification Module,SIM)这样的工具箱来帮助你管理 SIM。
表 1 显示了工具箱的目录结构(Windows 版本),以及包含开发工具的 bin 目录内容。
图 1a. 开发工具箱目录结构
图 1b. Bin 目录的内容
现在让我们重新访问 Java Card 开发步骤,记住这次要使用 Sun Java Card 开发工具箱:
使用您喜欢的编辑器或 IDE 编写 Java 源代码。
使用您喜欢的编译器或 IDE 编译 源代码。
使用 JCWDE 仿真器测试 Java Card applet。这是一个可选的步骤。请记住 JCWDE 不是一个成熟的 Java Card 仿真器。
使用工具箱中 bin 目录中的 转换器 工具将类文件转换 为 Converted Applet(CAP)。请注意,除了类文件之外,向转换工具的另一个输入为 导出文件,提供由应用程序所导入软件包的相关信息;这些软件包还会被加载到卡片中。导出文件还是转换器工具的一个输出结果。
验证 CAP 的有效性。这一步是可选的。这一步包括使用 verifycap 脚本来验证 CAP 文件的有效性,使用 verifyexp 来验证导出文件,并且使用 verifyrev 验证软件包修订版本间的二进制兼容性。工具 verifycap、verifyexp 和 verifyrev 脚本都在 bin 目录中可以找到。
安装 CAP 文件。使用 scriptgen 工具将 CAP 文件转换为(安装) APDU 脚本文件。然后使用 apdutool 将脚本文件(安装 APDU 命令和 CAP 文件)发送到 Java Card 设备上的 C-JCRE 或 JCRE。JCRE 将 CAP 文件保存卡片内存中。
下列图总结了这些步骤。请注意,每个Java Card 供应商提供自己的工具,但是开发 Java Card applet 的步骤通常在开发工具箱之间是相同的:
图 2. Java Card 开发步骤
要了解关于如何使用 Sun 的 Java Card 开发工具箱方面的更多信息,请参见《 Java Card 开发工具箱用户指南 》,从中找到工具箱文档目录。另一个优秀的参考资料是文章“使用 Java Card 开发工具箱”。
编写卡片端 Java Card Applet
Sun 提供了两个模型用来编写 Java Card applet(javacard.framework.Applet):传统的 Java Card API 或者 Java Card 远程方法调用(JCRMI) API。我们可以使用其中任何一个模型来编写 Java Card applet。
使用 Java Card API
开发 Java Card applet 是一个两步完成的过程:
定义负责主机应用程序和 applet 之间接口的 APDU 命令和响应。
编写 Java Card applet 本身
首先,让我们看一下 Java Card applet 的结构。
Applet 结构
清单 1 说明了构造 Java Card applet 的方式:
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
...
}
清单 1. Java Card Applet 的结构
Java Card applet 通常定义它的 APDU 相关指令、它的构造函数和 Java Card applet 生命周期方法:install()、select()、deselect() 和 process()。最后,它还定义任意适当的专用方法。
定义 APDU 指令
不同的 Java Card 应用程序具有不同的接口(APDU)需求。信用卡 applet 可能支持验证 PIN 号的方式,完成信用和借记事务,并检查帐户的卡片余额。健康保险 applet 可能提供对健康保险信息、保险总额限制、医生、病人信息等内容的访问。需要根据应用程序的需求定义的准确的 APDU 。
举例来说,让我们完成经典的 Wallet 信用卡示例的一些部分。您能在 Sun Java Card 开发工具箱中的示例目录下发现本示例和其他示例的完整源代码。
首先,我们定义一个 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 命令会定义传入的数据。下面给出一个示例。让我们定义 Verify PIN APDU 命令,它验证从卡片读取器中传递来的 PIN 号。下一个表格定义了 Verify APDU:
表 2 — Verify APDU 命令
名称 CLA INS P1 P2 Lc Data Field Le (size of response)
验证 PIN 0x80 0x20 0 0 PIN Len PIN Value N/A
请注意,Le 字段、响应的大小是 N/A。这是由于不存在特定于应用程序的响应对 PIN 进行验证;成功或失败通过响应中 APDU 的状态字显示。
为了便于 APDU 处理,javacard.framework.ISO7816 接口定义了许多常量,我们可以用来从输入缓冲器中检索各个 APDU 字段,其中输入缓冲器通过 process() 方法被传递到 applet。
...
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 常量
现在,我们将定义类(CLA)和指令(INS),用于 Get Balance 和 Verify 命令,所获取卡片余额响应的大小,以及如果 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;
...
清单 3. Applet 的 APDU 定义
接下来,让我们定义一些 applet 构造函数和生命周期方法。
构造函数
定义一个初始化对象状态的专用构造函数。从 install() 方法中调用该构造函数;换句话说,构造函数在 applet 生命周期内只被调用一次。
/**
* 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. Applet 构造函数
在这个示例中,我们使用对象 javacard.framework.OwnerPIN表示个人识别号码;这个对象将存在于 Java Card Applet 的生命周期期间。回忆一下本系列文章的第 1 部分中的“管理内存和对象”部分,在 Java Card 环境中,数组和基本类型在对象声明中被声明,为了有利于对象的重用,应该最大限度减少对象实例化。在 applet 生命周期内只创建对象一次。轻松完成这一操作的一个简易的方法是在构造函数中创建对象,从 install() 方法(在 applet 生命周期内只创建一次)调用这个构造函数。为了利于重用,对象保留在原有存储范围内或在 applet 生命周期内被充分地引用,且这些成员变量的值在重用前应适当重置。由于垃圾收集器并不是总是可用的,应用程序可能从不回收分配给超出范围的对象的存储空间。
install() 方法
JCRE 在安装过程中调用 install()。您必须覆盖这个从 javacard.framework.Applet 类中继承的方法,且 install() 方法必须实例化 applet,如下所示:
/**
* 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() Applet 生命周期方法
install() 方法必须直接或间接调用 register() 方法来完成安装;如果这步失败将导致安装失败。在这个示例中,构造函数调用 register()。
select() 方法
JCRE 调用 select() 以便通知 applet 已经被选中用于 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;
}
清单 6. select() Applet 生命周期方法
deselect() 方法
JCRE 调用 deselect() 来通知 applet 已经被取消选中。除非想提供会话清除,否则不必实现该方法。通过 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();
}
清单 7. deselect() Applet 生命周期方法
在本示例中,我们将重置 PIN。
process() 方法 — 与 APDU 协同工作
一旦 applet 已经被选择,它就准备接收 APDU 命令,如 第 1 部分 中“Java Card Applet 的生命周期”一节中所述。
回想一下,APDU 命令从主机端(客户机端)应用程序中被发送到卡片上,如下所示:
图 3. APDU 命令、主机应用程序和 Java Card Applet 之间的响应流程
每当 JCRE 接收到 APDU 命令(从主机应用程序通过卡片读取器完成,或者如果使用 Sun Java Card 开发工具箱时通过 apdutool 完成)时,JCRE 调用 applet 的 process() 方法,将之作为一个参数传递给传入的命令(该参数位于 APDU 命令输入缓冲器中)。然后 process() 方法执行下列操作:
提取 APDU CLA 和 INS 字段
检索特定于应用程序的 P1、P2 和数据字段
处理 APDU 数据
生成并发送响应
适度返回或抛弃适当的 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;
}
}
清单 8. process() Applet 生命周期方法
process() 方法调用 getBalance() 和 verify() 方法。清单 9 显示了处理获取卡片余额 APDU 的方法 getBalance(),并将卡片余额返回并保存在卡片内。
/**
* 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);
}
清单 9. 处理 Get Balance APDU
getBalance() 方法通过调用 APDU.getBuffer() 方法获得对 APDU 缓冲器的引用。在返回响应(当前的卡片余额)前,applet 设置 JCRE 的模式以便通过调用方便返回预期响应大小的 APDU.setOutgoing() 方法发送。通过调用 APDU.setOutgoingLenth(),我们还必须设置在响应数据字段中字节的实际个数。在 APDU 缓冲器中的响应实际上通过调用 APDU.sendBytes() 发送。
Applet 并不直接返回代码(状态字);一旦 applet 调用 APDU.setOutgoing() 并提供所请求的信息,JCRE 便负责这项操作。状态字的值根据 process() 方法返回到 JCRE 的方式而发生变化。如果一切进展顺利,JCRE 将返回 9000,指示没有错误。Applet 可以通过抛出在 ISO7816 接口中所定义的一种异常或者应用程序制定的值来返回错误代码。在清单 9 中,如果预期响应不正确,方法 getBalance() 会抛出一个 ISO7816.SW_WRONG_LENGTH 代码。有效的状态代码值参考 ISO7816 接口的定义,或参考本文中第一部分中“响应 APDU”一节。
现在让我们查看一下清单 10 中的 verify() 方法。因为我们定义了 verify PIN APDU 命令要包含数据,因此 verify() 方法必须调用设置 JCRE 为接收模式的 APDU.setIncomingAndReceive() 方法,然后接收传入的数据。
/**
* 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);
}
清单 10. 处理 Verify APDU
这个方法通过调用 APDU.getBuffer() 获得对 APDU 缓冲器的引用,调用APDU.setIncomingAndReceive() 来接收命令数据,从传入的 APDU 缓冲器中获取 PIN 数据并验证 PIN。验证失败将引起状态代码 6900 被发送回主机应用程序。
有时所传入的数据会超过 APDU 缓冲器的数据容量,applet 必须以块的形式读取数据,直到不再有要读取的数据。在这种情况下,我们必须首先调用 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);
}
...
清单 11. 读取大量的传入数据
当每个块被读取时,applet 只得将其附加到另一个缓冲器上,否则只好处理它。
使用 Java Card RMI API
第二个可以进行 Java Card applet 编程的模型是基于 J2SE RMI 分布式对象模型的 Java Card RMI(JCRMI)。
该方法提供了以对象为中心的模型,其中 APDU 通信和处理在前面一节中被抽象化;而现在您需要处理这些对象。这一过程可以简化了基于 Java Card 技术的设备编程和集成。
在 RMI 模型中,服务器应用程序创建并生成可访问的远程对象,并且客户机应用程序获得对服务器的远程对象的远程引用,然后为这些对象调用它们的远程方法。在 JCRMI 中,Java Card applet 是服务器,主机应用程序是客户机。
Java Card RMI 简介
两个软件包提供了对 Java Card RMI 的支持:
java.rmi 定义了 Java 2 标准版 java.rmi 软件包的子集。它还定义了 Remote 接口和 RemoteException 类。除此之外,不包含其它传统的 java.rmi 类。
javacard.framework.service 定义了 Java Card applet 服务类,其中包括 RMI 服务类 CardRemoteObject 和 RMIService。
类 CardRemoteObject 定义了两种方法来启用和禁用对来自卡片外部的 对象 的远程访问。类 RMIService 处理 RMI 请求(将传入的 APDU 命令转换为远程方法调用)。
编写 JCRMI 应用程序类似于编写典型的基于 RMI 的应用程序:
将远程类的行为设为一个接口。
编写远程类的服务器实现和支持类。
编写一个使用远程服务的客户机程序和支持类。
请注意,正如您稍候将要看到的,JCRMI 不改变 applet 的基本结构和生命周期。
远程接口
创建远程服务的第一步是定义它的可见行为。远程接口定义 applet 提供的服务。正如标准 J2SE RMI 中规定的那样,所有 Java Card RMI 远程接口必须扩展 java.rmi.Remote 接口。要详细说明这一点,在此给出一个远程接口,公开获取卡片余额并将之保存在卡片里的方法。
import java.rmi.*;
import javacard.framework.*;
public interface MyRemoteInterface extends Remote {
...
public short getBalance() throws RemoteException;
...
// A complete credit card application would also define other
// methods such as credit() and debit() methods.
...
}
清单 12. 远程接口
MyRemoteInterface 定义了检索存储在智能卡中卡片余额的远程方法,在本示例中为 getBalance() 方法。请注意,除了特定于 Java Card 的导入之外,这个远程接口看起来完全象一个标准的 RMI 远程接口。
服务器实现
下一步是实现服务器的行为。服务器实现由 Java Card applet、已经定义的任意远程接口的实现和任意特定于应用程序的相关类组成。
Java Card Applet
Java Card applet 是一个 JCRMI 服务器,并且是可用于主机(客户机)应用程序的远程对象的所有者。在下面的图中解释了典型的 Java Card RMI applet 结构:
图 4. 典型的 Java Card RMI Applet 的结构
当与显式处理 APDU 消息的 applet 比较时,基于 JCRMI 的 applet 更像一个对象容器。正如您在图 4 中看到的,基于 JCRMI 的 applet 有一个或多个远程对象,一个 APDU Dispatcher 和一个接收 APDU 并将它们转换为远程方法调用的 RMIService。Java Card 远程类可以扩展 CardRemoteObject 类,以便自动导出对象,使得远程应用为可见状态。
JCRMI applet 必须扩展 javacard.framework.Applet,遵循标准的 applet 结构,并定义适当的生命周期方法。还必须安装和自我注册,并分发 APDU。下列代码片断说明一个基于 JCRMI 的 applet 的典型结构:
public class MyApplet extends javacard.framework.Applet {
private Dispatcher disp;
private RemoteService serv;
private Remote myRemoteInterface;
/**
* Construct the applet. Here instantiate the remote
* implementation(s), the APDU Dispatcher, and the
* RMIService. Before returning, register the applet.
*/
public MyApplet () {
// Create the implementation for my applet.
myRemoteInterface = new MyRemoteInterfaceImpl();
// Create a new Dispatcher that can hold a maximum of 1
// service, the RMIService.
disp = new Dispatcher((short)1);
// Create the RMIService
serv = new RMIService(myRemoteInterface);
disp.addService(serv, Dispatcher.PROCESS_COMMAND);
// Complete the registration process
register();
}
...
Applet 创建一个 Dispatcher 和 RMIService 来处理传入的 JCRMI APDU。
...
/**
* 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.
*/
public static void install(byte[] aid, short s, byte b) {
new MyApplet();
}
在 Java Card 环境下,JVM 的生命周期是物理卡片的生命周期,且不是所有的 Java Card 实现都提供垃圾收集器,因此有必要最大限度减少内存分配。对象在安装时创建,因此,只一次性向这些对象分配内存。
/**
* 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.
*
* This JCRMI version of the applet dispatches remote
* invocation APDUs by invoking the Dispatcher.
*
* 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 install method fails.
*/
public void process(APDU apdu) throws ISOException {
// Dispatch the incoming command APDU to the RMIService.
disp.process(apdu);
}
}
Applet 的 process() 接收了一个 APDU 命令并将该命令分发给 RMIService,RMIService 通过将该命令转换为 RMI 调用和后续响应而进行处理。
清单 13. Java Card RMI Applet
实现 Remote 对象
实现 JCRMI 远程对象类似于实现标准的 J2SE RMI 远程对象。二者的主要不同之处在于,在 JCRMI 中,远程对象有扩展 CardRemoteObject (除了实现远程接口之外)的选择。
CardRemoteObject 定义了两种方法,export() 和 unexport(),分别用于启用和禁用对来自卡片外部对象的访问。通过扩展 CardRemoteObject,可以自动导出远程对象的所有方法。如果决定不扩展 CardRemoteObject,您将负责通过调用 CardRemoteObject.export() 将它们导出。
import java.rmi.RemoteException;
import javacard.framework.service.CardRemoteObject;
import javacard.framework.Util;
import javacard.framework.UserException;
/**
* Provides the implementation for MyRemoteInterface.
*/
public class MyRemoteImpl extends CardRemoteObject implements MyRemoteInterface {
/** The balance. */
private short balance = 0;
/**
* The Constructor invokes the superclass constructor,
* which exports this remote implementation.
*/
public MyRemoteImpl() {
super(); // make this remote object visible
}
/**
* This method returns the balance.
* @return the stored balance.
* @throws RemoteException if a JCRMI exception is
* encountered
*/
public short getBalance() throws RemoteException {
return balance;
}
// Other methods
...
}
清单 14. 远程对象实现
完整的 Java Card RMI 应用程序流程
现在让我们总结一下 JCRMI 应用程序的流程图。客户机(主机)应用程序通过将 RMI APDU 传递给卡片内部的 JCRE,继而依次将这些 APDU 转发给适当的 JCRMI applet 来调用 RMI。这个 applet 将所接收到的 APDU 分发给 RMIService,继而依次处理 APDU 并将其转换为 RMI 调用。JCRMI applet 的典型流程如下所示:
图 5. 基于 Java Card RMI 的 Applet 的流程
简言之,熟练运用基于 APDU 的消息传递模型,JCRMI 提供了一个分发模型机制。JCRMI 消息被封装在被传递到 RMIService 的 APDU 消息内。RMIService负责解码 APDU 命令,将它们转换为方法调用和响应。这使得服务器往复地和客户机通信、传递方法信息、参数并返回值。
结束语
“Java Card 技术简介”的第 2 部分介绍了 Java Card applet 开发的方方面面:Java Card applet 的结构、Sun Java Card 开发工具箱、API 和可用于编写 applet 的编程模型:Java Card API 和 Java Card RMI API。
本系列文章的第 3 部分将介绍主机应用程序和可用于编写主机应用程序的 Java API:OpenCard Framework、Java Card RMI Client API 和用于 J2ME 的安全性和信任服务应用编程接口(SATSA)。
赞助商链接