• 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

  • 相关阅读:
    Linux(一)简介与安装
    BBS项目(四)
    BBS项目(三)
    BBS项目(二)
    BBS项目(一)
    会话控制
    SQL表连接查询
    [转]使用GROUP BY WITH ROLLUP改善统计性能
    MySQL中的set和enum
    PHP操作MySQL
  • 原文地址:https://www.cnblogs.com/lvlang/p/10586431.html
Copyright © 2020-2023  润新知