Javacard DES/AES/RSA/Hash/Sinature算法API使用示例

前面一篇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上,地址:

https://github.com/Victor-Lv/Algorithm

posted @ 2016-07-26 23:02  Victor_Lv  阅读(585)  评论(1编辑  收藏  举报