前面一篇DES算法API使用示例代码写得比较渣,特别是在部门里的老前辈帮我看了下代码风格之后深感如此。
本篇介绍本人写的一个国际算法(区别于国密算法SM2/SM3这些)API调用的示例applet:
话不多说,直接先上代码,后面再补充解释下,代码上也有我附带的较为详细的注释。
(1)Des API调用文件-Des.java:
package helloWorld;
import javacard.framework.JCSystem;
import javacard.security.DESKey;
import javacard.security.Key;
import javacard.security.KeyBuilder;
import javacardx.crypto.Cipher;
public class Des
{
private Cipher DESEngine;
private Key myKey;
private byte[] temp;
private RandGenerator rand;
public Des()
{
//必须先初始化(获得实例instance才能init否则报错)
DESEngine = Cipher.getInstance(Cipher.ALG_DES_CBC_ISO9797_M1, false);
//buildKey创建的是未初始化的key,密钥值需用setKey函数手动赋值
myKey = KeyBuilder.buildKey(KeyBuilder.TYPE_DES, KeyBuilder.LENGTH_DES, false);
//设置暂存变量temp
temp = JCSystem.makeTransientByteArray((short)100, JCSystem.CLEAR_ON_DESELECT);
//给rand对象也分配空间,不然无法执行RandGenerator类的代码!!
rand = new RandGenerator();
//****** 1 *******首先自动生成个密钥
//产生64bit随机数
temp = rand.GenrateSecureRand((short)100);
//Util.arrayFillNonAtomic(temp1, (short)16, (short)48, (byte)0x11);
//设置密钥--拿随机数当密钥.
//注意!DES密钥必须要是8字节的倍数长度,如果不是,下面这个setKey函数会截取64bits长度,剩余的抛弃掉!所以上面下来的100bits随机数只会取64bits
((DESKey)myKey).setKey(temp, (short)0);
}
public void init(boolean isEncryption)
{
//short b = myKey.getSize(); //可用debug查看该变量值
if(isEncryption)
//****** 2 *******初始化加密密钥和加密模式
DESEngine.init(myKey, Cipher.MODE_ENCRYPT);
else
DESEngine.init(myKey, Cipher.MODE_DECRYPT);
}
public void GetResult(byte[] inBuf, short inOffset, short inLength, byte[] outBuf, short outOffset)
{
//****** 3 *******传入密文/明文进行加密并得到明文/密文
//特别注意DES加密结果是8的倍数,所以outBuf开辟的空间至少要为8字节.并且DES解密只能处理8的倍数次方的密文输入.否则6F00
DESEngine.doFinal(inBuf, inOffset, inLength, outBuf, outOffset);
}
}
/* DES注意事项:
*
* 注意一:DES密钥必须要是8字节的倍数长度,如果不是,setKey函数会截取64bits长度,剩余的抛弃掉!所以上面下来的100bits随机数只会取64bits
*
* 注意二:DES加密结果是8的倍数,所以outBuf开辟的空间至少要为8字节
*
* 注意三:DES解密只能处理8的倍数次方的密文输入.否则又6F00.明文传入长度随意(>=0),函数也自动会有padding
* */
(2)Rsa.java:
package helloWorld;
import javacardx.crypto.Cipher;
import javacard.framework.ISO7816;
import javacard.framework.ISOException;
import javacard.security.KeyBuilder;
import javacard.security.KeyPair;
public class Rsa
{
private Cipher RSAEngine;
//非对称加密算法需要用密钥对的形式存储密钥(公钥私钥):囊括PublicKey和PrivateKey对象
//而不能用publicKey、privateKey分开来存[是因为公钥和私钥的生成的相关而不是独立的?]
private KeyPair keypair;
public Rsa()
{
//new一个密钥对对象
//第二个参数决定了密钥长度的同时,决定了生成密文的长度(因为密文长度=密钥长度[模数])为512比特,也就是64字节,转成十六进制表示为40
keypair = new KeyPair(KeyPair.ALG_RSA_CRT, KeyBuilder.LENGTH_RSA_512);//只支持这个构造函数,用KeyPair(PublicKey,PrivateKey)构造会异常
//调用函数自动生成随机的密钥(包括公钥和私钥)
keypair.genKeyPair();
try
{
//用Cipher.ALG_RSA_ISO14888会出错--6F00,ISO7816--接触式,14...--非接(触式)
RSAEngine = Cipher.getInstance(Cipher.ALG_RSA_PKCS1, false);
}
catch(Exception e)
{
ISOException.throwIt(ISO7816.SW_COMMAND_NOT_ALLOWED);
}
}
public void init(boolean isEncryption)
{
if(isEncryption)
RSAEngine.init(keypair.getPrivate(), Cipher.MODE_ENCRYPT);
else
RSAEngine.init(keypair.getPublic(), Cipher.MODE_DECRYPT);
}
public void GetResult(byte[] inBuf, short inOffset, short inLength, byte[] outBuf, short outOffset)
{
//****** 3 *******传入密文进行加密并得到密文
RSAEngine.doFinal(inBuf, inOffset, inLength, outBuf, outOffset);
}
}
/* RSA注意事项:
*
*
* 注意一:
* 为何不是所有传入的密文都能解密(6F00)?
* 并且只有用本次的密钥产生过的密文格式传入去解密才能no error
* 这是因为如果你拿什么密钥都能解密别人的密文,那就违背了密码算法的本意了呀!!!
*
*
* 注意二:
* RSA最终生成的密钥长度>=64字节且为64字节的倍数,若不足,则genKeyPair函数会自动补全到位
*
* 注意三:RSA算法生成的密文的长度 = 密钥的长度,所以这里注意给dofinal函数传入的输出缓冲区的大小不能太小
*
* 注意四:RSA明文传入加密,长度可随意(>=0),函数会自动padding。
* 但是传入解密的密文必须是 >= 密钥长度,且为密钥长度的倍数,最后解密出来的明文长度也是等于密钥长度
*
* */
(3)Aes.java:
/**
* AES
*/
package helloWorld;
import javacard.security.Key;
import javacard.security.KeyBuilder;
import javacard.security.AESKey;
import javacardx.crypto.Cipher;
import javacard.framework.JCSystem;
/**
* @author lv.lang
*
*/
public class Aes {
private Key myKey;
private Cipher AESEngine;
private byte[] temp;
private RandGenerator rand;
/**
*
*/
public Aes() {
rand = new RandGenerator();
temp = JCSystem.makeTransientByteArray((short)128, JCSystem.CLEAR_ON_DESELECT);
temp = rand.GenrateSecureRand((short)128);
//这里的算法参数只支持NOPAD,其他如ALG_AES_CBC_PKCS5都会报6A80,为什么?
AESEngine = Cipher.getInstance(Cipher.ALG_AES_BLOCK_128_ECB_NOPAD, true);
myKey = KeyBuilder.buildKey(KeyBuilder.TYPE_AES, KeyBuilder.LENGTH_AES_128, false);
((AESKey)myKey).setKey(temp, (short)0);
}
public void init(boolean isEncryption)
{
//short b = myKey.getSize(); //可用debug查看该变量值
if(isEncryption)
AESEngine.init(myKey, Cipher.MODE_ENCRYPT);
else
AESEngine.init(myKey, Cipher.MODE_DECRYPT);
}
public void GetResult(byte[] inBuf, short inOffset, short inLength, byte[] outBuf, short outOffset)
{
AESEngine.doFinal(inBuf, inOffset, inLength, outBuf, outOffset);
}
}
(4)Hash.java:
/**
* MessageDigest
*/
package helloWorld;
/**
* @author lv.lang
*
*/
import javacard.security.MessageDigest;
public class Hash {
private MessageDigest HashEngine;
public Hash() {
HashEngine = MessageDigest.getInstance(MessageDigest.ALG_SHA, false);
//HashEngine.getInitializedMessageDigestInstance(MessageDigest.ALG_SHA, false).
}
public void GetResult(byte[] inBuf, short inOffset, short inLength, byte[] outBuf, short outOffset)
{
//sha-1产生的摘要结果固定为160bit[20bytes]
//其他sha对应的摘要字节数可查看MessageDigest.ALG_弹出的注释窗口
HashEngine.doFinal(inBuf, inOffset, inLength, outBuf, outOffset);
}
}
/*
* HASH如sha、md5,只是将原文产生一段信息摘要,仅用来验证(只能自验?只能自验有啥用处)消息的完整性也就是检测原文是否被篡改。
* 也就是说hash的作用是保证任意一段原文对应唯一的hash值。
* 并且sha/md5这些都是带密钥的哈希,也就是说不同密钥下同个原文产生的hash又不同!
* 但是明显攻击者我自己用随便一段消息生成hash值,发出去看到的也是"读的通"的消息且hash正确。
* 所以做数字签名在hash基础上,还需要验证身份!那就是在hash值之后加上非对称密钥加解密!
*
*
*/
(5)RandGenerator.java:
package helloWorld;
import javacard.framework.JCSystem;
import javacard.security.RandomData;
public class RandGenerator
{
private byte[] temp; //随机数的值
private RandomData random;
private byte size; //随机数长度
//构造函数
public RandGenerator()
{
size = (byte)4;
temp = JCSystem.makeTransientByteArray((short)4, JCSystem.CLEAR_ON_DESELECT);
//类当中有getInstance的都要先调用这个函数获取对象实例才能使用其他方法,不然6F00
random = RandomData.getInstance(RandomData.ALG_SECURE_RANDOM);
}
//产生length长度的随机数并返回
public final byte[] GenrateSecureRand(short length)
{
temp = new byte[length];
//生成4bit的随机数
random.generateData(temp, (short)0, (short)length);
return temp;
}
//返回随机数长度
public final byte GetRandSize()
{
return size;
}
}
(6)调用签名API文件的Sign.java:
/**
* Signarure
*
*/
package helloWorld;
/**
* @author lv.lang
*
*/
import javacard.framework.JCSystem;
import javacard.security.Key;
import javacard.security.KeyBuilder;
import javacard.security.Signature;
import javacard.security.HMACKey;
public class Sign {
private Signature signEngine;
private Key myKey;
private RandGenerator rand;
private byte[]temp;
public Sign() {
signEngine = Signature.getInstance(Signature.ALG_HMAC_SHA1, false);
myKey = KeyBuilder.buildKey(KeyBuilder.TYPE_HMAC, KeyBuilder.LENGTH_HMAC_SHA_1_BLOCK_64, false);
temp = JCSystem.makeTransientByteArray((short)64, JCSystem.CLEAR_ON_DESELECT);
rand = new RandGenerator();
temp = rand.GenrateSecureRand((short)100);
((HMACKey)myKey).setKey(temp, (short)0, (short)64);
}
public void init(boolean isSign)
{
if(isSign)
signEngine.init(myKey, Signature.MODE_SIGN);
else
signEngine.init(myKey, Signature.MODE_VERIFY);
}
public short sign(byte[] inBuff, short inOffset, short inLength, byte[] sigBuff, short sigOffset)
{
return signEngine.sign(inBuff, inOffset, inLength, sigBuff, sigOffset);
}
public boolean verify(byte[] inBuff, short inOffset, short inLength, byte[] sigBuff, short sigOffset, short sigLength)
{
return signEngine.verify(inBuff, inOffset, inLength, sigBuff, sigOffset, sigLength);
}
}
(7)主文件Hello.java:
package helloWorld;
//import Hello;
import javacard.framework.APDU;
import javacard.framework.Applet;
import javacard.framework.ISO7816;
import javacard.framework.ISOException;
import javacard.framework.Util;
public class Hello extends Applet {
//下面这些都是未分配空间的实例化!需要后面自己使用new关键字或者用getInstance函数分配空间!
private Des des;
private Aes aes;
private Rsa rsa;
private Sign mySign;
private Hash hmac;
byte[] result; //存储加密或解密后的结果
public Hello(){
//所有new的都应该尽量在构造时完成,否则每次发送一句APDU命令它都会new一遍空间出来,导致空间浪费
rsa = new Rsa();
aes = new Aes();
mySign = new Sign();
des = new Des();
hmac = new Hash();
result = new byte[64];//此处开辟空间的大小(S)按:DES的空间S >= 明文/密文长度, RSA的S >= 密钥字节数(512比特长度的密钥为64字节)
}
public static void install(byte[] bArray, short bOffset, byte bLength) {
// GP-compliant JavaCard applet registration
new Hello().register(bArray, (short) (bOffset + 1), bArray[bOffset]);
}
public void process(APDU apdu) {
// Good practice: Return 9000 on SELECT
if (selectingApplet()) {
return;
}
//将缓冲区与数组buf建立映射绑定
byte[] buf = apdu.getBuffer();
short lc = apdu.setIncomingAndReceive();//读取data并返回data长度lc
byte ins = buf[ISO7816.OFFSET_INS];
byte p1 = buf[ISO7816.OFFSET_P1]; //p1用于判断是加密还是解密
short signLen = (short)0;
switch (ins) {
case (byte) 0x00: //INS == 0x00 表明要用DES加密
if(p1 == (byte)0x00)
des.init(true); //p1 == 00 表示加密,否则表示解密
else
des.init(false);
//****** 3 *******传入明文/密文进行DES加密并得到密文/明文
des.GetResult(buf, (short)ISO7816.OFFSET_CDATA, lc, result, (short)0);
Util.arrayCopyNonAtomic(result, (short)0, buf, (short)ISO7816.OFFSET_CDATA, lc);
apdu.setOutgoingAndSend((short)5, lc);
break; //一定要有break否则会继续进入switch循环
case (byte) 0x01: //INS == 0x01 表示要用RSA算法
if(p1 == (byte)0x00)
rsa.init(true);
else
rsa.init(false);
rsa.GetResult(buf, (short)ISO7816.OFFSET_CDATA, lc, result, (short)0);
Util.arrayCopyNonAtomic(result, (short)0, buf, (short)ISO7816.OFFSET_CDATA, (short)64);
apdu.setOutgoingAndSend((short)5, (short)64);
break;
case (byte)0x02: //Hash-SHA
hmac.GetResult(buf, (short)ISO7816.OFFSET_CDATA, lc, result, (short)0);
Util.arrayCopyNonAtomic(result, (short)0, buf, (short)ISO7816.OFFSET_CDATA, (short)64);
apdu.setOutgoingAndSend((short)ISO7816.OFFSET_CDATA, (short)64);
break;
case (byte) 0x03: //AES
if(p1 == (byte)0x00)
aes.init(true); //p1 == 00 表示加密,否则表示解密
else
aes.init(false);
//****** 3 *******传入明文/密文进行DES加密并得到密文/明文
aes.GetResult(buf, (short)ISO7816.OFFSET_CDATA, lc, result, (short)0);
Util.arrayCopyNonAtomic(result, (short)0, buf, (short)ISO7816.OFFSET_CDATA, lc);
apdu.setOutgoingAndSend((short)5, lc);
break;
case (byte)0x04: //signature
if(p1 == (byte)0x00) //p1 == 00 表示做签
{
mySign.init(true);
signLen = mySign.sign(buf, (short)ISO7816.OFFSET_CDATA, lc, result, (short)0);
}
else //表示验签
{
mySign.init(false);
mySign.verify(buf, (short)ISO7816.OFFSET_CDATA, lc, result, (short)0, signLen);
}
default:
// good practice: If you don't know the INStruction, say so:
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}
}
}
/* 主文件注意事项:
*
* 注意一:所有new的都应该尽量在构造时完成,否则每次发送一句APDU命令它都会new一遍空间出来,导致空间浪费
*
* 注意二:加密/解密得到的result数组开辟空间的大小(S)按:
* DES的空间S >= 明文/密文长度, RSA的S >= 密钥字节数(512比特长度的密钥为64字节)
*
* 注意三:switch-case的每个case之后一定要有break否则会继续进入switch循环
*
*
* */
1、DES和AES均作为对称密钥算法,使用大同小异,只需要修改下小范围的参数,流程都是密钥生成->设置密钥->初始化引擎->传入数据进行加解密获得结果,这里我都是用Javacard随机数生成的API来产生一个随机数的字节数组来充当密钥。
2、RSA作为非对称密钥算法,与前面对称密钥算法流程差不多,只是这里因为它有公钥和私钥,需要用一个KeyPair对象来存储密钥(包含公钥和私钥进去),这里我用KeyPair类API提供的一个自动生成密钥对的函数来生成密钥,也可以自己对公钥和私钥一个个来手动初始化。
3、然后就是消息摘要MessageDigest类APi的使用,这个类是提供来做hash的,也就是对消息产生摘要,注意这里只是生成消息摘要,并不能作为数字签名,因为它只进行了消息篡改与否的验证,并未达到“发送方身份验证/不可否认性”的功能。
4、随机数接口使用,上一篇有提到,这个也没啥好说的。
5、Signature数字签名API,这个略麻烦,流程是:生成密钥->设置密钥->初始化引擎->产生签名或验签,在使用JCOP测试的时候发现buildKey时好些算法模式参数都不被支持(install时返回6A80),对于签名API的使用示例自己还在调试中,所以后面也没测试结果截图放出来。后面有机会再补上签名API的详细介绍。
使用JCOP Shell工具进行测试:
DES测试:
(首先传入8个字节的数据进行DES加密,然后拿加密后的密文再充当数据传进去做DES解密)
需要特别注意的是DES密钥长度规范、DES加密时开辟的outBuffer的空间(8)、以及传入的密文长度,在Des.java代码文件中都有详细提到。
AES加解密:
(同样是先加密,然后再拿生成的密文扔进去解密,这里我还弄了个“输入密文长度不合规范导致返回6F00异常”的示例)
同样AES需要特别注意outBuffer开辟空间的大小,以及传入密文的长度规范,这里AES我用的是16字节的密钥,并且算法模式是No Padding的(因为Padding的那几种模式在我的JCOP工具中在初始化阶段就报这个算法模式参数的错误!),所以传入密文至少要是16个字节才能解密。哦,这里我走得匆忙忘了测试看AES密文传入是否非得是16字节倍数长度了(估计是的)。
3、MessageDigest(Hash)的测试:
(注意sha-1哈希算法生成的摘要长度固定为20个字节,即便你输入的原文长度很短,它也会给你做Padding)
4、RSA算法测试:
(后面两个[INS == 01]的才是RSA的测试样例,前面两个是DES的。先进行RSA的加密,将密文[64字节])
RSA传入的明文长度也和对称密钥的一样,随意。但是传入密文时,长度有严格要求,看上面代码文件我写的注释。
最后再次强调一次,对于java这种面向对象编程的语言,因为操作往往都是针对对象的,而对象操作就有一个很重要的使命:空间的管理。所以也像前面博客强调的,必须要给对象分配空间程序才能去执行对象里面的代码!同时由于Javacard的特性,卡内存储空间很小,所以new出来的对象尽量重复使用,并且为了避免每次process applet的时候都new一遍对象,应在install或者构造函数时就完成分配空间/实例化对象这些工作,这样才能避免每次发送一次apdu命令都产生一次对象,除非代码判断下对象是否为null,若不为空则先=null,把前面的对象给毙掉再new。这个是前辈们给我看了代码风格后的教训。
/********************* 分隔线 ************************************/
2016-7-27日找“师傅”帮我看了下工程代码风格,发现自己写的代码还有很多问题。
(1)首先,对于INS值判断的case里面的执行过程,应该封装成一个函数,如下面的代码。
(2)其次,很多中间变量可以省掉,比如p1,p2就不必重新再定义一遍了,直接用buf[ISO7816....],因为Javacard应用开发必须注意资源的节省!
(3)然后,Des.java这些辅助类在实际应用开发中没必要这样写,而是应该直接在主applet文件里面直接建立对象和相关函数,避免文件的链接的同时减小了代码占用的空间,要知道卡片里面的空间是非常宝贵的,所以才需要对java文件进行压缩再压缩得到cap包,最后把压缩到精致的字节码文件烧到卡片上去执行。
(4)然后顺带解决了几个关键问题,首先,之前发现使用MessageDigest产生的摘要每次都是随机变化的不符合实际需求,后面师傅帮我测试了下,才发现时因为自己测试时输入的消息太短了!只要输入的消息足够长,那么产生的摘要就是固定的,所以才能进行验证。因为太短的话会对消息进行padding再做摘要,而这个padding的内容应该是随机变化的,然后padding的内容和原消息交叉混合下,就导致前后摘要看起来毫无关联地随机性变化。
(5)还有个问题就是AES或者Sign里面选用的算法模式,发现很多算法模式在使用JCOP工具调试时,在install过程中报6A80(错误的参数)错,师傅说估计是因为JCOP使用的算法库不支持该算法模式,即便工程没报错(工程使用的API jar包里有该模式选项)。
(6)然后还有个比较大的问题是,自己还需要去做算法模式的遍历!而不是仅仅测试通过一两种模式就狂欢了。比如sha,就得遍历sha1,sha256……所有模式。写成一个遍历了所有算法组合的applet工程,下次有实际任务说要去测试卡片里面所有的算法库,才能快速用到这个先前开发好的applet去测试,不然每次都要写一个applet对于这样一个很基础简单的小测试来说多花时间。
所以针对上述问题,下面的代码做了些改善。后面需要自己继续完善的地方还有:中间变量的继续压缩,多个文件整合到同个文件,以及算法所有组合的遍历……
最后顺带记录下今天问到关于一个问题的解答:
在javacard中,new出来的对象时存放在EEPROM空间中永久保存的,那么如果重复使用呢?每次跑程序的时候不是会重新new或者需要新创建的句柄去和旧的对象空间关联起来么?
回答:其实后面的问题并不是自己想象中的那样,要清楚在applet的生命周期中,install仅仅只执行了一遍!也就是说,在安装applet环节中new出来的对象被永久使用了,后面不会再执行到这部分的代码去new对象了,因为后面就相当于一个无限循环(从process函数里面开始执行代码),install那些代码只在最开始时执行了一遍。所以并不存在说new了多个对象的问题。
2016-7-28代码改善版本:
(1)Sign.java
/**
* Signarure
*
*/
package helloWorld;
/**
* @author lv.lang
*
*/
import javacard.security.Signature;
public class Sign {
private Signature signEngine;
//private Key myKey;
//private RandGenerator rand;
//private byte[]temp;
private Rsa rsa;
public Sign() {
rsa = new Rsa();
//算法模式参数选用不适会导致6A80,为啥? //ALG_RSA_MD5_PKCS1模式决定了最后生成的签名长度=RSA密钥长度(如64字节)
signEngine = Signature.getInstance(Signature.ALG_RSA_MD5_PKCS1, false);
//myKey = KeyBuilder.buildKey(KeyBuilder.TYPE_HMAC, KeyBuilder.LENGTH_HMAC_SHA_1_BLOCK_64, false);
//temp = JCSystem.makeTransientByteArray((short)64, JCSystem.CLEAR_ON_DESELECT);
//rand = new RandGenerator();
//temp = rand.GenrateSecureRand((short)100);
//((HMACKey)myKey).setKey(temp, (short)0, (short)64);
}
public void init(boolean isSign)
{
if(isSign)
signEngine.init(rsa.GetPrivateKey(), Signature.MODE_SIGN);
//signEngine.init(myKey, Signature.MODE_SIGN);
else
signEngine.init(rsa.GetPublicKey(), Signature.MODE_VERIFY);
//signEngine.init(myKey, Signature.MODE_VERIFY);
}
//做签
public short sign(byte[] inBuff, short inOffset, short inLength, byte[] sigBuff, short sigOffset)
{
return signEngine.sign(inBuff, inOffset, inLength, sigBuff, sigOffset);
}
//验签
public boolean verify(byte[] inBuff, short inOffset, short inLength, byte[] sigBuff, short sigOffset, short sigLength)
{
return signEngine.verify(inBuff, inOffset, inLength, sigBuff, sigOffset, sigLength);
}
}
(2)RandGenerator.java
package helloWorld;
import javacard.framework.JCSystem;
import javacard.security.RandomData;
public class RandGenerator
{
private byte[] temp; //随机数的值
private RandomData random;
private byte size; //随机数长度
//构造函数
public RandGenerator()
{
size = (byte)4;
temp = JCSystem.makeTransientByteArray((short)4, JCSystem.CLEAR_ON_DESELECT);
//类当中有getInstance的都要先调用这个函数获取对象实例才能使用其他方法,不然6F00
random = RandomData.getInstance(RandomData.ALG_SECURE_RANDOM);
}
//产生length长度的随机数并返回
public final byte[] GenrateSecureRand(short length)
{
temp = new byte[length];
//生成4bit的随机数
random.generateData(temp, (short)0, (short)length);
return temp;
}
//返回随机数长度
public final byte GetRandSize()
{
return size;
}
}
(3)Hash.java
/**
* MessageDigest
*/
package helloWorld;
/**
* @author lv.lang
*
*/
import javacard.security.MessageDigest;
public class Hash {
private MessageDigest HashEngine;
public Hash() {
HashEngine = MessageDigest.getInstance(MessageDigest.ALG_SHA, false);
//HashEngine.getInitializedMessageDigestInstance(MessageDigest.ALG_SHA, false).
}
public void GetResult(byte[] inBuf, short inOffset, short inLength, byte[] outBuf, short outOffset)
{
//sha-1产生的摘要结果固定为160bit[20bytes]
//其他sha对应的摘要字节数可查看MessageDigest.ALG_弹出的注释窗口
HashEngine.doFinal(inBuf, inOffset, inLength, outBuf, outOffset);
}
}
/*
* 1.sha-1产生的摘要结果固定为160bit[20bytes],并且即便你输入的原文过短,它也会帮你做padding
*
* 2.因为padding的数是随机的,所以如果输入过短,会导致自动padding,然后经过摘要算法的混啊混啊,最后得到的摘要是随机变化的!
* 所以要保证输入的消息足够长,才能得到固定的摘要。而且实际应用中,传入的一般都是文件级别的数据,并不会有这么短的数据!
*/
(4)Rsa.java
package helloWorld;
import javacardx.crypto.Cipher;
import javacard.framework.ISO7816;
import javacard.framework.ISOException;
import javacard.security.KeyBuilder;
import javacard.security.KeyPair;
import javacard.security.Key;
public class Rsa
{
private Cipher RSAEngine;
//非对称加密算法需要用密钥对的形式存储密钥(公钥私钥):囊括PublicKey和PrivateKey对象
//而不能用publicKey、privateKey分开来存[是因为公钥和私钥的生成的相关而不是独立的?]
private KeyPair keypair;
public Rsa()
{
//new一个密钥对对象
//第二个参数决定了密钥长度的同时,决定了生成密文的长度(因为密文长度=密钥长度[模数])为512比特,也就是64字节,转成十六进制表示为40
keypair = new KeyPair(KeyPair.ALG_RSA_CRT, KeyBuilder.LENGTH_RSA_512);//只支持这个构造函数,用KeyPair(PublicKey,PrivateKey)构造会异常
//调用函数自动生成随机的密钥(包括公钥和私钥)
keypair.genKeyPair();
try
{
//用Cipher.ALG_RSA_ISO14888会出错--6F00,ISO7816--接触式,14...--非接(触式)
RSAEngine = Cipher.getInstance(Cipher.ALG_RSA_PKCS1, false);
}
catch(Exception e)
{
ISOException.throwIt(ISO7816.SW_COMMAND_NOT_ALLOWED);
}
}
public void init(boolean isEncryption)
{
if(isEncryption)
RSAEngine.init(keypair.getPrivate(), Cipher.MODE_ENCRYPT);
else
RSAEngine.init(keypair.getPublic(), Cipher.MODE_DECRYPT);
}
public void GetResult(byte[] inBuf, short inOffset, short inLength, byte[] outBuf, short outOffset)
{
//****** 3 *******传入密文进行加密并得到密文
RSAEngine.doFinal(inBuf, inOffset, inLength, outBuf, outOffset);
}
public Key GetPrivateKey()
{
return keypair.getPrivate();
}
public Key GetPublicKey()
{
return keypair.getPublic();
}
}
/* RSA注意事项:
*
*
* 注意一:
* 为何不是所有传入的密文都能解密(6F00)?
* 并且只有用本次的密钥产生过的密文格式传入去解密才能no error
* 这是因为如果你拿什么密钥都能解密别人的密文,那就违背了密码算法的本意了呀!!!
*
*
* 注意二:
* RSA最终生成的密钥长度>=64bits且为64bits的倍数,若不足,则genKeyPair函数会自动补全到位
*
* 注意三:RSA算法生成的密文的长度 = 密钥的长度,所以这里注意给dofinal函数传入的输出缓冲区的大小不能太小
*
* 注意四:RSA明文传入加密,长度可随意(>=0),函数会自动padding。
* 但是传入解密的密文必须是 >= 密钥长度(如64字节),且为密钥长度的倍数,最后解密出来的明文长度也是等于密钥长度
*
* */
(5)Des.java
package helloWorld;
import javacard.framework.JCSystem;
import javacard.security.DESKey;
import javacard.security.Key;
import javacard.security.KeyBuilder;
import javacardx.crypto.Cipher;
public class Des
{
private Cipher DESEngine;
private Key myKey;
private byte[] temp;
private RandGenerator rand;
public Des()
{
//必须先初始化(获得实例instance才能init否则报错)
DESEngine = Cipher.getInstance(Cipher.ALG_DES_CBC_ISO9797_M1, false);
//buildKey创建的是未初始化的key,密钥值需用setKey函数手动赋值
myKey = KeyBuilder.buildKey(KeyBuilder.TYPE_DES, KeyBuilder.LENGTH_DES, false);
//设置暂存变量temp
temp = JCSystem.makeTransientByteArray((short)100, JCSystem.CLEAR_ON_DESELECT);
//给rand对象也分配空间,不然无法执行RandGenerator类的代码!!
rand = new RandGenerator();
//****** 1 *******首先自动生成个密钥
//产生64bit随机数
temp = rand.GenrateSecureRand((short)100);
//Util.arrayFillNonAtomic(temp1, (short)16, (short)48, (byte)0x11);
//设置密钥--拿随机数当密钥.
//注意!DES密钥必须要是8字节的倍数长度,如果不是,下面这个setKey函数会截取64bits长度,剩余的抛弃掉!所以上面下来的100bits随机数只会取64bits
((DESKey)myKey).setKey(temp, (short)0);
}
public void init(boolean isEncryption)
{
//short b = myKey.getSize(); //可用debug查看该变量值
if(isEncryption)
//****** 2 *******初始化加密密钥和加密模式
DESEngine.init(myKey, Cipher.MODE_ENCRYPT);
else
DESEngine.init(myKey, Cipher.MODE_DECRYPT);
}
public void GetResult(byte[] inBuf, short inOffset, short inLength, byte[] outBuf, short outOffset)
{
//****** 3 *******传入密文/明文进行加密并得到明文/密文
//特别注意DES加密结果是8的倍数,所以outBuf开辟的空间至少要为8字节.并且DES解密只能处理8的倍数次方的密文输入.否则6F00
DESEngine.doFinal(inBuf, inOffset, inLength, outBuf, outOffset);
}
}
/* DES注意事项:
*
* 注意一:DES密钥必须要是8字节的倍数长度,如果不是,setKey函数会截取64bits长度,剩余的抛弃掉!所以上面下来的100bits随机数只会取64bits
*
* 注意二:DES加密结果是8的倍数,所以outBuf开辟的空间至少要为8字节
*
* 注意三:注意算法(DES/AES/RSA等都是)并不会对密文进行padding,所以密文传入长度需要>=密钥长度(如8字节)且为密钥长度的倍数,否则均会报6F00.
* 明文传入长度随意(>=0),函数也自动会有padding
* */
(6)Aes.java
/**
* AES
*/
package helloWorld;
import javacard.security.Key;
import javacard.security.KeyBuilder;
import javacard.security.AESKey;
import javacardx.crypto.Cipher;
import javacard.framework.JCSystem;
/**
* @author lv.lang
*
*/
public class Aes {
private Key myKey;
private Cipher AESEngine;
private byte[] temp;
private RandGenerator rand;
/**
*
*/
public Aes() {
rand = new RandGenerator();
temp = JCSystem.makeTransientByteArray((short)128, JCSystem.CLEAR_ON_DESELECT);
temp = rand.GenrateSecureRand((short)128);
//这里的算法参数只支持NOPAD,其他如ALG_AES_CBC_PKCS5都会报6A80,估计是因为JCOP工具不支持
AESEngine = Cipher.getInstance(Cipher.ALG_AES_BLOCK_128_ECB_NOPAD, true);
myKey = KeyBuilder.buildKey(KeyBuilder.TYPE_AES, KeyBuilder.LENGTH_AES_128, false);
((AESKey)myKey).setKey(temp, (short)0);
}
public void init(boolean isEncryption)
{
/**
* 代码改进三:Cipher.MODE_ENCRYPT这类参数的确定用p2确定,为了省空间和效率牺牲代码可观性
*/
//short b = myKey.getSize(); //可用debug查看该变量值
if(isEncryption)
AESEngine.init(myKey, Cipher.MODE_ENCRYPT);
else
AESEngine.init(myKey, Cipher.MODE_DECRYPT);
}
public void GetResult(byte[] inBuf, short inOffset, short inLength, byte[] outBuf, short outOffset)
{
AESEngine.doFinal(inBuf, inOffset, inLength, outBuf, outOffset);
}
}
/*注意事项:
* 1.如果getInstance中的算法模式选用了NOPAD的话,明文传入也必须达到>=密钥长度(如16字节)且为密钥长度的倍数
*
* 2.密文传入当然也和DES一样,密文不会给你padding的,所以传入长度需要>=密钥长度(如16字节)且为密钥长度的倍数
*
* 3.为啥getInstance中的算法模式很多选项选用了都会报0A80呢?不支持这种模式的算法吗?
*
*/
(7)Hello.java
package helloWorld;
//import Hello;
import javacard.framework.APDU;
import javacard.framework.Applet;
import javacard.framework.ISO7816;
import javacard.framework.ISOException;
import javacard.framework.Util;
public class Hello extends Applet {
//下面这些都是未分配空间的实例化!需要后面自己使用new关键字或者用getInstance函数分配空间!
private Des des;
private Aes aes;
private Rsa rsa;
private Sign mySign;
private Hash hmac;
byte[] result; //存储加密或解密后的结果
public Hello(){
//所有new的都应该尽量在构造时完成,否则每次发送一句APDU命令它都会new一遍空间出来,导致空间浪费
rsa = new Rsa();
aes = new Aes();
mySign = new Sign();
des = new Des();
hmac = new Hash();
result = new byte[64];//此处开辟空间的大小(S)按:DES的空间S >= 明文/密文长度, RSA的S >= 密钥字节数(512比特长度的密钥为64字节)
}
/**
* 代码风格改进一:
* 把case里面对每种INS的处理封装成函数放构造函数和install及process中间
* */
private void handleDES(APDU apdu) throws ISOException
{
byte[] buf = apdu.getBuffer();
apdu.setIncomingAndReceive();//读取data,必不可少
/**
* 代码风格改进二:直接通过buf传递p1,p2,lc等这些参数,从而减少中间变量的使用
* */
if(buf[ISO7816.OFFSET_P1] == (byte)0x00)
des.init(true); //p1 == 00 表示加密,否则表示解密
else
des.init(false);
//****** 3 *******传入明文/密文进行DES加密并得到密文/明文
des.GetResult(buf, (short)ISO7816.OFFSET_CDATA, (short)buf[ISO7816.OFFSET_LC], result, (short)0);
Util.arrayCopyNonAtomic(result, (short)0, buf, (short)ISO7816.OFFSET_CDATA, (short)buf[ISO7816.OFFSET_LC]);
apdu.setOutgoingAndSend((short)5, (short)buf[ISO7816.OFFSET_LC]);
}
private void handleRSA(APDU apdu) throws ISOException
{
byte[] buf = apdu.getBuffer();
apdu.setIncomingAndReceive();//读取data
if(buf[ISO7816.OFFSET_P1] == (byte)0x00)
rsa.init(true);
else
rsa.init(false);
rsa.GetResult(buf, (short)ISO7816.OFFSET_CDATA, (short)buf[ISO7816.OFFSET_LC], result, (short)0);
Util.arrayCopyNonAtomic(result, (short)0, buf, (short)ISO7816.OFFSET_CDATA, (short)64);
apdu.setOutgoingAndSend((short)5, (short)64);
}
private void handleSHA(APDU apdu) throws ISOException
{
byte[] buf = apdu.getBuffer();
apdu.setIncomingAndReceive();//读取data
hmac.GetResult(buf, (short)ISO7816.OFFSET_CDATA, (short)buf[ISO7816.OFFSET_LC], result, (short)0);
Util.arrayCopyNonAtomic(result, (short)0, buf, (short)ISO7816.OFFSET_CDATA, (short)64);
apdu.setOutgoingAndSend((short)ISO7816.OFFSET_CDATA, (short)64);
}
private void handleAES(APDU apdu) throws ISOException
{
byte[] buf = apdu.getBuffer();
apdu.setIncomingAndReceive();//读取data
if(buf[ISO7816.OFFSET_P1] == (byte)0x00)
aes.init(true); //p1 == 00 表示加密,否则表示解密
else
aes.init(false);
//****** 3 *******传入明文/密文进行DES加密并得到密文/明文
aes.GetResult(buf, (short)ISO7816.OFFSET_CDATA, (short)buf[ISO7816.OFFSET_LC], result, (short)0);
Util.arrayCopyNonAtomic(result, (short)0, buf, (short)ISO7816.OFFSET_CDATA, (short)buf[ISO7816.OFFSET_LC]);
apdu.setOutgoingAndSend((short)5, (short)buf[ISO7816.OFFSET_LC]);
}
private void handleSIGN(APDU apdu) throws ISOException
{
byte[] buf = apdu.getBuffer();
apdu.setIncomingAndReceive();//读取data
short signLen = (short)0;
if(buf[ISO7816.OFFSET_P1] == (byte)0x00) //p1 == 00 表示做签
{
mySign.init(true);
signLen = mySign.sign(buf, (short)ISO7816.OFFSET_CDATA, (short)buf[ISO7816.OFFSET_LC], result, (short)0);
Util.arrayCopyNonAtomic(result, (short)0, buf, (short)ISO7816.OFFSET_CDATA, signLen);
apdu.setOutgoingAndSend((short)ISO7816.OFFSET_CDATA, signLen);
}
else //表示验签
{
mySign.init(false);
//需要apdu同时输入Message以及签名,所以用p2参数来切分开二者
//short temp = (short)(lc - p2); //仅用作调试时观察值的变化
boolean verifyIsPass = mySign.verify(buf, (short)ISO7816.OFFSET_CDATA, (short)buf[ISO7816.OFFSET_P2], buf, (short)(ISO7816.OFFSET_CDATA+buf[ISO7816.OFFSET_P2]), (short)(buf[ISO7816.OFFSET_LC] - buf[ISO7816.OFFSET_P2]));
if(verifyIsPass)
buf[ISO7816.OFFSET_CDATA] = (byte)0x00;
else
buf[ISO7816.OFFSET_CDATA] = (byte)0x01;
apdu.setOutgoingAndSend((short)ISO7816.OFFSET_CDATA, (short)1);
}
}
public static void install(byte[] bArray, short bOffset, byte bLength) {
// GP-compliant JavaCard applet registration
new Hello().register(bArray, (short) (bOffset + 1), bArray[bOffset]);
}
public void process(APDU apdu) {
// Good practice: Return 9000 on SELECT
if (selectingApplet()) {
return;
}
/*
//将缓冲区与数组buf建立映射绑定
byte[] buf = apdu.getBuffer();
short lc = apdu.setIncomingAndReceive();//读取data并返回data长度lc
byte ins = buf[ISO7816.OFFSET_INS];
byte p1 = buf[ISO7816.OFFSET_P1]; //p1用于判断是加密还是解密
short signLen = (short)0;
byte p2 = buf[ISO7816.OFFSET_P2];*/
byte[] buf = apdu.getBuffer();
switch (buf[ISO7816.OFFSET_INS]) {
case (byte) 0x00: //INS == 0x00 表明要用DES加密
handleDES(apdu);
break; //一定要有break否则会继续进入switch循环
case (byte) 0x01: //INS == 0x01 表示要用RSA算法
handleRSA(apdu);
break;
case (byte)0x02: //Hash-SHA
handleSHA(apdu);
break;
case (byte) 0x03: //AES
handleAES(apdu);
break;
case (byte)0x04: //signature
handleSIGN(apdu);
break;
default:
// good practice: If you don't know the INStruction, say so:
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}
}
}
/**主文件注意事项:
*
* 注意一:所有new的都应该尽量在构造时完成,否则每次发送一句APDU命令它都会new一遍空间出来,导致空间浪费
*
* 注意二:加密/解密得到的result数组开辟空间的大小(S)按:
* DES的空间S >= 明文/密文长度, RSA的S >= 密钥字节数(512比特长度的密钥为64字节)
*
* 注意三:switch-case的每个case之后一定要有break否则会继续进入switch循环
*
* 注意三:验签时因为需要同时传入原始消息以及签名,所以一段apdu命令需要包含了“消息+签名”
* 所以注意lc的值等于lenOf(Message)+lenOf(sign)。
* 同时,为了切分开这两种数据,我额外用到了p2参数。
* 还有要注意的是p2和lc的值传进去的都是十六进制表达,Applet在收到apdu之后会把它们自动转成十进制表示(debug即可观察到),
* 所以p2表达的是Message字节长度的十六进制表达,lc表达的是data部分的字节长度的十六进制表达,并不需要画蛇添足去转换成十进制再send
*
* 代码风格改进三:实际项目开发时需要将Des.java等这些辅助文件全部集成到主Applet文件中
*
* */
************* 分隔线 *****************
隔了几天发现自己的代码有一个比较严重的错误,就是对于short定义的长度问题,例如在建立暂存数组或者复制数组的函数里面:
都有一个short length的参数,这里的short length自己一度以为表示有多少个bit,然后需要再根据bits计算bytes,但是这两天才发现它表示的是有length个字节!而不是length个bit。例如JCSystem.makeTransientBooleanArray(64,...)这样表示的是建立一个64字节的字节数组,而不是64bits的数组!上面的代码我没直接修改这个错误, 让它成为前车之鉴吧,后面会继续发布更新版的博文和代码。
******* 2016-9-7更新 *********
最新版的代码做了些小优化,已放到我的github上,地址: