Java卡:编程准则和最佳实践

本文档旨在为Java Card开发人员提供经过验证的技术指南,重点放在applet的设计和优化上。我们希望这些指南将有助于面对Java Card开发的特殊性,即:
•卡资源极为有限。 EEPROM大小通常为32-64Kb,用于保存卡片发行后小应用程序(Applet)和应用程序数据。 RAM大小通常约为4KB,并且必须容纳应用程序栈,瞬态数据和各种运行时缓冲区。你负担不起任何浪费
•CPU功率非常有限。另外,Java Card applet比本机代码慢得多,因此您不想执行不必要的操作
•即使您的小程序在平台上运行良好,您仍然希望节省尽可能多的资源,以便它也可以在更多受限的平台上运行(较少的堆栈,较小的事务缓冲区等)。还记得吗?编写一次,随处运行;-)

Applet设计

Java Card不是Java。花式的面向对象设计会导致小程序变胖和变慢,这是因为:
•CAP文件中的类/接口开销
•运行时的虚拟方法开销

下面介绍的设计准则是基于我们自己的经验。和往常一样,当然还有改进的余地,但是遵循它们肯定可以防止大量常见(以及一些不是那么常见)的错误。

高级设计

通常在设计时会犯下悲剧性的错误。如果发生这种情况,那简直是只有完整的重新设计才会拯救你……为避免这种情况,请非常仔细地阅读本节并确保您遵循这些准则。从合理的面向对象设计开始。这将帮助您了解applet的工作原理,尤其是当applet具有复杂功能(PKCS,EMV等)时。将问题分解为较小的对象仍然是找出问题的最佳方法。不过,您必须避免以下陷阱:

•类扩散:一切都不是对象。因此请控制 类/抽象类/接口 的数量
•实用工具 类/接口:将这些方法移至实际实例化的类,而不是定义仅包含静态方法的类。对于仅包含常量的接口,也是如此:将常量移至实际类
•较深的类层次结构:类树越深,继承方法的调用就越昂贵。继承还意味着您将嵌套地调用构造函数,这使栈(stack)处于危险之中。简而言之,继承的必要性必须明确:如果没有必要性,就不要继承。
•设计模式:模式在规范中看起来很棒,但是其实现意味着很多抽象类,接口和深层树
•系统化的get/set方法:同样,这在UML图表中看起来不错,但是对于Java Card应用程序来说,它的负担却很大。增加成员可见性将使您可以删除它们,从而减小代码大小并提高执行速度
•程序包扩散:实际上,除非一个程序包将由另一个applet使用,否则实际上无需将应用程序分解为程序包。看起来更好,但是跨包方法调用有一定的成本:如果不是必要,则避免使用它
•C样式错误处理:请勿在您的方法中使用C样式错误处理,即检查返回代码以查看是否发生了错误。这会产生效率低下的代码(更不用说难看的代码了),因为即使没有发生错误,您也会继续运行与错误相关的代码。您的设计必须使用Java异常:错误仅在发生后才需要处理!

设计完成后,您的应用程序应包括一个或两个软件包,一到十个类和不超过三个层级(level )的类。任何较大的问题都需要进行彻底调查和论证。一旦实现并测试了applet,您就可以确信已正确理解并实现了功能要求。在这个阶段,您可能会开始打破面向对象(OO)设计并进行优化以减小代码大小和/或提高性能。

数据存储

您的设计必须考虑以下约束:

•快速写入RAM
•写入EEPROM的速度非常慢
•分配对象甚至更糟
•垃圾收集不是Java Card 2.1.1的一部分

内存分配

关键的规则是, 在安装时创建所有实例数据,即在javacard.framework.Applet.install()调用的applet构造函数中创建:使用新的临时数组分配,所有显式初始化的对象构造。这样,在applet的使用期间就不会面临内存不足的风险:在安装时所有内容都已保留。此外,内存分配不会降低applet命令的速度。

内存写入

对象存储在EEPROM中。这意味着每次执行
myObject.myByte =(byte)5
时,您都在写入EEPROM。必须尽可能避免这种情况。显然,您需要存储持久性数据,但是在许多情况下,您只需要存储临时数据(中间结果,发送回终端的数据等)。这是APDU缓冲区派上用场的地方!毕竟,它是一个“普通”字节数组,因此请尽可能多地使用它。

安全

如果将小程序提交给安全评估(通用标准等),则可以采取一些特定步骤:
•将所有安全功能(密钥/ PIN处理等)隔离在单独的类中。这样,只需要记录和评估这些类
•尽可能使用Java Card API(PIN,密钥等)。如果平台提供了您需要的安全服务,则applet实施这些服务是无用且不安全的

固定大小的PIN

使用固定大小的PIN,即,将PIN长度存储在PIN本身中,并填充其余字节。这有助于防止定时攻击,还简化了PIN处理。

反DFA RSA签名

在将结果发送到终端之前,请验证所有RSA签名。这将减慢签名操作的速度,但可以防止DFA攻击。

其他需要注意的事项

注意平台错误:它们可能会对您的设计产生影响。
如果小程序必须可互操作,请不要使用任何专有的API。


小程序优化

本节列出了许多实际的(即经过测试和验证的)优化。在应用它们之前,请记住优化的黄金法则:
•专注于最常运行的代码:在这里您将有所作为
•您通常必须在大小和速度之间进行选择
•每次优化通过后运行基准测试
•每次优化通过后运行验证测试

减少代码大小

避免复杂的面向对象设计

如果您阅读了上一节,这应该很明显。简而言之:
•使类/接口的数量最少。组合“相似”类并在实例化时使用构造函数参数对其进行区分
•尽量减少方法数量。尤其要摆脱所有get/set方法:提高数据成员的可见性(例如,从私有到可见的包),以减少get/set方法的数量。由于您保存了方法调用,因此这也将提高性能

权衡:封装被削弱。如果您控制程序包中的所有类,那么这不成问题。

删除无效代码

查找未使用的变量和代码。听起来很明显,但您会感到惊讶…
另外,请尝试使初始化/个性化设置尽可能短。它只需要运行一次,因此非常重。也许个性化系统可以执行更复杂的操作?

分解重复代码

查找所有冗余代码并将其分解。如果这意味着分解类并将其替换为私有/静态方法,请执行此操作。

权衡:如果经常调用该代码,则可能会减慢速度。

限制参数的个数

对于虚拟方法,尝试使用不超过3个参数,对于静态方法,尝试使用不超过4个参数。这样,编译器将使用数字或字节码快捷方式,这有助于减小代码大小:aload_x(1字节)而不是aload x(2字节),依此类推。
权衡:如果这意味着从对象实例保存/读取数据,则速度会明显下降。

减少EEPROM消耗

回收所有对象。

Java Card标准不需要垃圾收集。因此,除非您坚持要浪费内存,否则必须跟踪旧对象并重用它们。切记:Java Card虚拟机“永远”运行,因此,如果某个对象变得不可访问,则其内存将”永远”消失(或者至少在删除小程序之前)。即使您的平台提供了专有的垃圾回收,您也最好重用对象。分配新对象很慢,垃圾回收也很慢。你明白了…

仔细分配数组

操作系统以32字节的块(称为clusters)分配内存。一个cluster无法在两个对象之间共享,因此任何对象都将至少吞噬一个群集,无论它多么小。此外,虚拟机将标头附加到所有对象:
•6个字节用于“普通”对象
•8个字节用于基本类型数组
•对象数组12个字节

这意味着您不必创建多个相同类型的小数组,而应将它们组合成一个数组,并使用固定偏移量访问后者。

折衷方案:代码复杂度略有增加。


减少内存消耗

重用局部变量

与其在需要时分配新的局部变量,不如尝试重用先前声明的局部变量。
折衷:滥用此技术会产生无法读取的代码。

仔细分配临时数组 (重复了,上面提到过)

与其创建多个相同类型的小型临时数组,不如将它们组合成一个数组并使用固定偏移量对其进行访问。
折衷方案:代码复杂度略有增加。

限制参数的个数 ( 重复了)

对于虚拟方法,尝试使用不超过3个参数,对于静态方法,尝试使用不超过4个参数。这样,编译器将使用短字节码指令,这有助于减小代码大小:aload_x(1字节)而不是aload x(2字节),依此类推。
权衡:如果这意味着从对象实例保存/读取数据,则速度会明显下降。

避免深层嵌套的方法调用

危险的嵌套调用通常发生在深层类树(基类方法调用)和递归上。后者产生了花哨且紧凑的代码,但它也往往会很快粉碎stack……三遍检查最终条件,并确保测试实际上触发了最坏的情况。
权衡:可能增加代码大小。
当心开关/案例结构中的局部变量

不必在每种情况下分配新的局部变量,而仅在切换之前声明一个。


提高执行速度

开启编译器优化

如果您的编译器提供了优化标志,请使用它们(对于Javac,为-O)。
权衡:可能的代码大小增加。

避免复杂的面向对象设计

有关详细信息,请参见Applet设计和Applet优化。
权衡:封装被削弱。如果您控制程序包中的所有类,那么这不成问题。

支持静态,私有和最终方法

所有公共/受保护的Java方法都是隐式虚拟方法(与C++不同,在C++中,必须将它们声明为虚拟)。这意味着始终进行动态绑定,即,虚拟机在调用公共/受保护的方法之前必须始终确定对象的实际类型。这种查找成本很高,尤其是如果要调用的方法是从基类继承的。这是不欢迎使用深层树的主要原因。

要解决此问题:
1.支持静态方法,因为它们不受动态绑定的约束,这使得它们调用起来更快
2.如果方法不是静态的,请尝试将其设为私有。私有方法不能在派生类中被覆盖,因此虚拟机更容易找到它们
3.如果某个方法不能私有,请尽快将其定型。这将有助于虚拟机

使用本机API

只要平台提供本机代码来执行操作,就使用它!本机方法比您能想到的任何聪明的Java代码要快得多。
尤其是:
•使用
Util.arrayFillNonAtomic()
Util.arrayCopy()
Util.arrayCopyNonAtomic()
初始化或修改数组

•Util.arrayFillNonAtomic()对于在加密操作期间填充缓冲区特别有用
•使用Util.getShort(),Util.setShort()处理byte [] / short转换


避免不必要的初始化

Java Card虚拟机保证将新分配的数据成员和局部变量设置为0 / false / null。

不要将异常用于流程控制

使用异常进行流控制从来都不是一个好主意,Java Card也不例外。异常处理非常慢,除异常处理外,不应将其用于其他任何用途。换句话说,如果您正在考虑使用throw模拟goto,只需加倍努力,寻找更好的解决方案。

使用RAM缓冲区

写入EEPROM的速度大约比写入RAM的速度慢1,000倍,因此,执行的次数越少越好。您可以使用APDU缓冲区或瞬态数组来存储会话数据和临时结果。这对于中间解密解密操作特别有效,并且也更安全。

清理循环

访问实例成员比访问局部变量要昂贵。如果必须执行重复访问,则在第一次访问时将实例成员存储在本地变量中,然后仅使用该本地变量。数组访问(array [i],array.length)和方法调用也是如此。这将大大提高性能。

使用逻辑表达式的求值顺序

逻辑表达式(测试等)从左到右评估。如果条件失败,则不会评估其余测试。如果其中一种情况几乎总是错误,请首先进行检查:您将避免执行不必要的代码。
尽可能经常使用返回值

许多API的返回值都是有意义的:使用它可以节省不必要的操作。

谨慎使用事务(transactions)

由于许多本机API已经进行事务写操作,因此请确保必须在applet中明确使用事务。特别是,如果您不需要交易,请不要使用Util.arrayCopy(),请改用Util.arrayCopyNonAtomic()。

同时检查APDU的类(CLA)和指令(INS)

当使用OP Secure Messaging时,这特别有效,因为所有安全命令将使用两个不同的类(CLA)值。

同时检查P1和P2

尽可能同时检查P1和P2。您的代码将更快,更紧凑。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注