Cipher的分类
Java中的Cipher类提供了对数据加密解密的功能。根据加密算法的不同,大致可以分为三类:
- 对称加密(Symmetric):加密和解密使用相同的密钥(SecretKey)。参与加密解密的双方,需要预先商定好密钥。如果保证了密钥的安全,理论上则可以保证加密数据的安全。
- 非对称加密(Asymmetric):加密和解密使用不同的密钥。比如,加密的时候使用公钥(Public Key),解密的时候需要使用对应的私钥(Private Key),反之亦然。一般将公钥公开,私钥自己保留。比如,B需要给A发送一段加密信息。首先B会向A请求一个A的公钥。B利用请求到的公钥对消息进行加密,并且发送给A。即使消息在传输过程中被截获,因为没有A的私钥,所以也无法解密数据,得到的无非是一些没有意义的字节而已。因此,保证私钥的安全,理论上就可以保证数据的安全。
-
混合加密(Hybrid):对称加密和非对称加密各有优点:
- 对称加密的速度比较快,但是,在分发密钥的时候存在安全隐患(比如密钥在传输过中被人截获);
- 非对称加密不需要事先商定密钥,只需要把公钥公开,因为只有持有私钥的人才能够解密使用公钥加密的数据,而私钥一般是不需要公开的,但是,缺点是非对称加密的效率较对称加密慢了许多。
根据一次加密操作的数据量不同,也可以把加密分成块加密(Block Cipher)和流加密(Stream Cipher)。所谓Block Cipher,就是把数据被拆分成一些固定大小的数据块(比如64 Bits),一次加密一个数据块。与之相对的是流式加密(Stream Cipher),也就是说每次加密的数据长度更小,比如一个Bit或者一个Byte。目前比较流行的是Block Cipher,Java中的Cipher类就是针对于Block Cipher实现的。而且,通过给Block Cipher指定不同的Mode,也可以达到模拟Stream Cipher的作用。
Cipher类使用方法
Cipher是一个Engine类,想要获得它的实例,需要从JCA中请求——通过调用Cipher类的静态方法getInstace来完成请求。在请求Engine实例的时候,Cipher和其它的Engine类是不一样的:请求其它的Engine类时,一般是传入一个算法名称,进而得到一个实现了这种算法的Engine类的实例。但是,在请求Cipher实例的时候,需要传入的是一个“转换模式”——Transformation。一个Transformation包含3个信息:
- Algorithm:表示Cipher使用的算法,如:DES、AES等。
- Mode:用于在加密之前或者之后对Block做一些处理,提高安全性。如:ECB、CBC、CFB等。
- Padding:对于Block Cipher而言,每种算法对于Block Size是有要求的。比如,DES算法的Block Size是8 Bytes;而AES算法的Block Size是16 Bytes。然而,如果需要加密的数据的长度不是Block Size的整数倍,那么,在加密之前先要进行Padding,将数据补全到Block Size的整数倍,以便于接下来进行分块加密。这个属性就使用于指定Padding的规则。
Transformation包含的三个信息需要使用“/”进行分隔。比如我想要使用AES算法,ECB Mode以及PKCS5Padding,那么,我们需要传入的Transformation为:AES/ECB/PKCS5Padding。
Cipher加密步骤
- 调用Cipher.getInstance通过传入Transformation向JCA请求对应的Cipher实例。
- 通过调用init方法,设置Cipher的状态和Key,对Cipher进行初始化。Cipher本身是有状态的,一个Cipher要么用于加密,要么用于解密。在调用init方法的时候,需要通过传入Cipher的状态来指定当前的Cipher将用于加密还是解密。Cipher的状态在Cipher类中有静态常量定义(ENCRYPT_MODE, DECRYPT_MODE)。另外,每种加密算法都有自己的Key,此处需要用加密算法对应的Key来初始化Cipher。
- 调用update方法,将待加密的数据传入Cipher中(可选)。
- 调用doFinal方法获得最终的加密结果。
注意:在调用update方法的时候,其返回值是传入的数据的加密结果。如果累计传入到update方法的数据量不足一个Block,那么update方法不会返回任何结果(一个空的byte[])。一旦累计的数据量超过了1倍或者1倍以上的Block Size,那么,这个方法将返回之前所有可以构成Block的数据的加密结果,并且清空已经返回了的Block。之后无论是调用doFinal还是update方法,都不会再返回这些已经返回过的加密结果了。所以说,如果使用update方式逐次的更新数据,那么需要将每次调用update方法的返回值保存起来,最终才能拼接成一个完整的加密结果。
无论目前累计传入Cipher的数据是否可以构成一个Block,如果调用了doFinal方法,那么cipher会将传入的数据进行padding,使之构成Block Size的整数倍,然后返回加密结果。
下面是一个AES加密的例子
public class AESDemo { public static byte[] encrypt(byte[] data, Key key) { try { Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); //此处可以省略Mode和Padding,Java的默认实现将会使用默认的Mode和Padding(ECB和PKCS5Padding),也就是说"AES"将是一样的效果 cipher.init(Cipher.ENCRYPT_MODE, key); byte[] cipherData = cipher.doFinal(data); return cipherData; } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException ex) { throw new RuntimeException(ex); } } public static String encrypt(String text, Key key) { try { byte[] data = text.getBytes("UTF-8"); byte[] cipherData = encrypt(data, key); String cipherText = DatatypeConverter.printHexBinary(cipherData); //这里使用Base64更好一些,为了之后分析方便,我在这里使用了Hex return cipherText; } catch (UnsupportedEncodingException ex) { throw new RuntimeException(ex); } } public static void main(String[] args) { byte[] encodedKey = { 9, 93, -54, 55, -33, -43, 42, 102, -89, 116, 125, -19, 120, 59, 92, -110 }; SecretKey key = new SecretKeySpec(encodedKey, "AES"); String text = "Hello Cipher"; String cipherText = encrypt(text, key); System.out.println(cipherText); } }
运行这个Demo程序,将得到如下输出:
B7324301EDA3890DFA73042934352254
下面来分析以下这个输出结果——长度为32的16进制串,也就是说加密的结果是16 Bytes。
需要加密的消息是"Hello Cipher"——长度为12的字符串。因为都是ASCII字符,所以每个字符占用一个字节,共计12 Bytes。
之所以加密前的数据长度和加密后的数据长度不一样,是因为AES的Block Size是16 Bytes。因为"Hello Cipher"只有12 Bytes,在进行加密的时候会把它Padding到16 Bytes,因此最终的加密结果就是16 Bytes。
Cipher解密步骤
解密的步骤和加密非常相似,唯一的区别是在init Cipher的时候需要将Cipher的状态设置为解密-DECRYPT_MODE
- 调用Cipher.getInstance通过Transformation请求对应的Cipher实例。
- 调用init方法,传入状态参数DECRYPT_MODE,以及解密时需要用到的Key。
- 调用update方法,将待解密的数据传入Cipher中(可选)。
- 调用doFinal方法获得最终解密的结果。
下面是AES解密的例子:
public class AESDemo { public static byte[] decrypt(byte[] cipherData, Key key) { try { Cipher cipher = Cipher.getInstance("AES"); cipher.init(Cipher.DECRYPT_MODE, key); byte[] data = cipher.doFinal(cipherData); return data; } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException ex) { throw new RuntimeException(ex); } } public static String decrypt(String cipherText, Key key) { try { byte[] cipherData = DatatypeConverter.parseHexBinary(cipherText); byte[] data = decrypt(cipherData, key); String text = new String(data, "UTF-8"); return text; } catch (UnsupportedEncodingException ex) { throw new RuntimeException(ex); } } public static void main(String[] args) { byte[] encodedKey = { 9, 93, -54, 55, -33, -43, 42, 102, -89, 116, 125, -19, 120, 59, 92, -110 }; SecretKey key = new SecretKeySpec(encodedKey, "AES"); String cipherText = "B7324301EDA3890DFA73042934352254"; String text = decrypt(cipherText, key); System.out.println(text); } }
运行这个程序,得到如下输出:
Hello Cipher
可以看到,已经从密文中准确的解密出了明文。
关于Mode
对于Block Cipher支持很多种Mode,每种Mode都可以解决一些安全性问题,当然每种Mode都有各自的优点和缺点。这是一个比较庞大的话题,此处暂时不进行讨论。此处仅仅讲解以下最简单的Mode——ECB(Electronic Code Book)
与其说ECB是简单的Mode,不如说ECB根本没有什么功能。它对每个Block不进行任何处理,Block被加密成密文后,直接进行输出,Block和Block之间没有任何关联。对于ECB Mode而言,最大的优点就是block之间没有关联,比如在传输或者存储的过程中,某个字节损坏了(无法进行解密),那么影响的仅仅是其中的一个block,对于其它的block依然可以进行解密,因为block之间没有关联。同样ECB的优点也是它的缺点,导致了一些安全问题,使得采用ECB Mode进行加密的密文容易被攻击(篡改,猜测,解密等...)。
运行下面一段代码:
public class ECBDemo { public static void main(String[] args) { try { Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); System.out.println("Block Size: " + cipher.getBlockSize()); SecretKey key = new SecretKeySpec(new byte[]{-56, -29, 33, 117, -17, 123, 82, 108, -71, 82, 102, -63, -13, 29, 105, 26, -90, -115, 70, -125, -69, -126, -43, -86, 49, -108, 109, 64, 112, 2, -122, 79}, "AES"); cipher.init(Cipher.ENCRYPT_MODE, key); byte[] data1 = "How are you doing. I have a question...".getBytes("UTF-8"); byte[] data2 = "How are you doing. I was thinking...".getBytes("UTF-8"); byte[] cipherData1 = cipher.doFinal(data1); byte[] cipherData2 = cipher.doFinal(data2); System.out.println(DatatypeConverter.printHexBinary(cipherData1)); System.out.println(DatatypeConverter.printHexBinary(cipherData2)); } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | UnsupportedEncodingException | IllegalBlockSizeException | BadPaddingException ex) { throw new RuntimeException(ex); } } }
将会得到如下输出:
B237616BC0754F0EC16460F9A857D3D67EFB95DA272E49063324C858B2818B189053685C7A7ED49529D8CBCF1FD5552A B237616BC0754F0EC16460F9A857D3D651FB47DC9EF1BEB43E26917883A5F8514383EF98D738EA87D34EA807F4D2958B
我们可以看到,因为两个字符串都是以"How are you doing. "开头,因此导致了第一个Block的加密结果相同。这样就产生了安全隐患,我们可以对这些相同的部分进行猜测,或者篡改等。
如何解决ECB存在的问题呢?需要用到其它的Mode,比如常用的CBC, PCBC, CFB等等。
关于Padding
目前Padding的操作是在Cipher内部帮助我们完成的,让我们来看看Padding的数据是什么样的。
如何来一探究竟呢?上面我们使用AES/ECB/PKCS5Padding对"Hello Cipher"进行了加密,加密结果是B7324301EDA3890DFA73042934352254。因为这个是加密的结果,而且,数据是先Padding然后再加密,所以从这个加密过的结果上是没有办法知道Padding是什么样子的。如果直接用上面的代码解密,Cipher将自动处理掉Padding的内容,导致我们依然没有办法看到Padding是什么样的。
那么想要获取Padding的数据,简单的思路是这样,在解密的时候使用这个Transformation - "AES/ECB/NoPadding"。什么意思呢?这里我们对Cipher撒了个谎“我的加密数据使用AES/ECB进行加密的,其中没有使用Padding(其实是有的,我们心里清楚),所以你不需要帮我把Padding的内容处理掉,直接返回给我解密后的结果即可”。代码如下:
public class PKCS5PaddingInspector { public static void main(String[] args) throws Exception { byte[] encodedKey = { 9, 93, -54, 55, -33, -43, 42, 102, -89, 116, 125, -19, 120, 59, 92, -110 }; SecretKey key = new SecretKeySpec(encodedKey, "AES"); String cipherText = "B7324301EDA3890DFA73042934352254"; byte[] cipherData = DatatypeConverter.parseHexBinary(cipherText); Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); cipher.init(Cipher.DECRYPT_MODE, key); byte[] data = cipher.doFinal(cipherData); System.out.println(Arrays.toString(data)); } }
运行上面的代码,得到如下结果:
[72, 101, 108, 108, 111, 32, 67, 105, 112, 104, 101, 114, 4, 4, 4, 4]
分析:
因为,"Hello Cipher"只有12 Bytes,而且,AES的Block Size是16 Bytes,因此需要Padding 4 Bytes。看到上面结果里最后四个字节了么?那就是Padding的数据。下面我们给出PKCS5Padding的规范:
- 如果需要在末尾添加n个字节,那么添加的n个字节的值都是n
- 如果不需要进行padding(正好是Block Size的倍数),那么在消息末尾添加值为Block Size的Block Size个字节。比如Block Size是16 Bytes,那么就需要添加16个值为16的Byte。
因此只要是使用了PKCS#5进行padding的消息,都会被添加上额外的字节。那么在解密之后,可以通过判断末尾字节的值,来确定末尾有多少个字节是Padding的,从而进行剔除。
总结
这里只讲解了Cipher的一些基础用法,对于更加深入的用法以后会慢慢说明,也可以去查找其它资料。
虽然这里仅仅以AES算法作为了例子,但是对于其它算法也是相同的操作流程,无非是使用的Transformation不一样罢了。甚至非对称加密算法也是一样的,只需要记住,加密用公钥,解密就需要用私钥,反之亦然,即可。
本文没有详细的讲解Cipher Mode,其实这一部分的内容还是比较重要的,想要真正的把Cipher应用在实际的系统中仅仅依靠ECB是不行的,因为ECB的安全级别实在是太低了,相对来说CBC比较好些,理解简单,而且也可以解决ECB中存在的安全隐患。
除了PKCS5Padding,Java的默认实现中还提供了一些其它的Padding方法:方法列表请参见 这里
Thanks for reading.
参考资料
- 《Java Crypotography》—— Jonathan B. Knudsen
- 《Java Security 2nd Edition》
- 《Java 加密与解密的艺术》—— 梁栋
No comments :
Post a Comment