zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    BZOJ 2212/BZOJ 3702
    BZOJ 4761 Cow Navigation
    BZOJ 3209 花神的数论题
    BZOJ 4760 Hoof, Paper, Scissors
    BZOJ 3620 似乎在梦中见过的样子
    BZOJ 3940 Censoring
    BZOJ 3942 Censoring
    BZOJ 3571 画框
    BZOJ 1937 最小生成树
    BZOJ 1058 报表统计
  • 原文地址:https://www.cnblogs.com/lvlang/p/10586431.html
Copyright © 2011-2022 走看看