javax.crypto.Cipher,翻译为密码,其实叫作密码器更加合适。Cipher是JCA(Java Cryptographic Extension,Java加密扩展)的核心,提供基于多种加解密算法的加解密功能。在不了解Cipher以前,咱们在完成一些须要加解密的模块的时候老是须要处处拷贝代码,甚至有些错误的用法也被无数次拷贝,踩坑以后又要拷贝补坑的代码。为何不尝试理解Cipher而后合理地使用呢?java
转换模式transformation通常由三个部分组成,格式是:算法/工做模式/填充模式(algorithm/mode/padding)。例如:DES/CBC/PKCS5Padding。算法
算法就是指具体加解密算法的名称英文字符串,例如"SHA-256"、"RSA"等,这里不对具体算法的实现原理作具体展开。apache
工做模式其实主要是针对分组密码。分组密码是将明文消息编码表示后的数字(简称明文数字)序列,划分红长度为n的组(可当作长度为n的矢量),每组分别在密钥的控制下变换成等长的输出数字(简称密文数字)序列。工做模式的出现主要基于下面缘由:编程
从本质上讲,工做模式是一项加强密码算法或者使算法适应具体应用的技术,例如将分组密码应用于数据块组成的序列或者数据流。目前主要包括下面五种由NIST定义的工做模式:数组
模式 | 名称 | 描述 | 典型应用 |
---|---|---|---|
电子密码本(ECB) | Electronic CodeBook | 用相同的密钥分别对明文分组独立加密 | 单个数据的安全传输(例如一个加密密钥) |
密码分组连接(CBC) | Cipher Block Chaining | 加密算法的输入是上一个密文组合下一个明文组的异或 | 面向分组的通用传输或者认证 |
密文反馈(CFB) | Cipher FeedBack | 一次处理s位,上一块密文做为加密算法的输入,产生的伪随机数输出与明文异或做为下一单元的密文 | 面向分组的通用传输或者认证 |
输出反馈(OFB) | Output FeedBack | 与CFB相似,只是加密算法的输入是上一次加密的输出,而且使用整个分组 | 噪声信道上的数据流的传输(如卫星通讯) |
计数器(CTR) | Counter | 每一个明文分组都与一个通过加密的计数器相异或。对每一个后续分组计数器递增 | 面向分组的通用传输或者用于高速需求 |
上面五种工做模式能够用于3DES和AES在内的任何分组密码,至于选择哪种工做模式须要结合实际状况分析。安全
Padding指的是:块加密算法要求原文数据长度为固定块大小的整数倍,若是原文数据长度大于固定块大小,则须要在固定块填充数据直到整个块的数据是完整的。例如咱们约定块的长度为128,可是须要加密的原文长度为129,那么须要分红两个加密块,第二个加密块须要填充127长度的数据,填充模式决定怎么填充数据。网络
对数据在加密时进行填充、解密时去除填充则是通讯双方须要重点考虑的因素。对原文进行填充,主要基于如下缘由:app
经常使用的填充方式至少有5种,不一样编程语言实现加解密时用到的填充多数来自于这些方式或它们的变种方式。如下五种填充模式摘抄自参考资料的论文:dom
1.填充数据为填充字节序列的长度编程语言
这种填充方式中,填充字符串由一个字节序列组成,每一个字节填充该字节序列的长度。假定块长度为8,原文数据长度为9,则填充字节数 等于0x07;若是明文数据长度为8的整数倍,则填充字节数为0x08。填充字符串以下:
2.填充数据为0x80后加0x00
这种填充方式中,填充字符串的第一个字节数是0x80,后面的每一个字节是0x00。假定块长度为8,原文数据长度为9或者为8的整数倍,则 填充字符串以下:
3.填充数据的最后一个字节为填充字节序列的长度
这种填充方式中,填充字符串的最后一个字节为该序列的长度,而前面的字节能够是0x00,也能够是随机的字节序列。假定块长度为8,原文数据长度为9或者为8的整数倍,则填充字符串以下:
4.填充数据为空格
这种填充方式中,填充字符串的每一个字节为空格对应的字节数0x20。假定块长度为8,原文数据长度为9或者为8的整数倍,则填充字符串以下:
5.填充数据为0x00
这种填充方式中,填充字符串的每一个字节为0x00。假定块长度为8,原文数据长度为9或者8的整数倍,则填充字符串以下:
SunJCE Provider支持的Cipher的部分详细信息以下:
algorithm(算法) | mode(工做模式) | padding(填充模式) |
---|---|---|
AES | EBC、CBC、PCBC、CTR、CTS、CFB、CFB8-CFB128等 | NoPadding、ISO10126Padding、PKCS5Padding |
AESWrap | EBC | NoPadding |
ARCFOUR | EBC | NoPadding |
Blowfish、DES、DESede、RC2 | EBC、CBC、PCBC、CTR、CTS、CFB、CFB8-CFB128等 | NoPadding、ISO10126Padding、PKCS5Padding |
DESedeWrap | CBC | NoPadding |
PBEWithMD5AndDES、PBEWithMD5AndTripleDES、PBEWithSHA1AndDESede、PBEWithSHA1AndRC2_40 | CBC | PKCS5Padding |
RSA | ECB、NONE | NoPadding、PKCS1Padding等 |
Java原生支持的Padding(Cipher)汇总以下:
填充模式 | 描述 |
---|---|
NoPadding | 不采用填充模式 |
ISO10126Padding | XML加密语法和处理文档中有详细描述 |
OAEPPadding, OAEPWith<digest>And<mgf>Padding | PKCS1中定义的最优非对称加密填充方案,digest表明消息摘要类型,mgf表明掩码生成函数,例如:OAEPWithMD5AndMGF1Padding或者OAEPWithSHA-512AndMGF1Padding |
PKCS1Padding | PKCS1,RSA算法使用 |
PKCS5Padding | PKCS5,RSA算法使用 |
SSL3Padding | 见SSL Protocol Version 3.0的定义 |
其余Padding须要第三方Provider提供。
Cipher提供三个静态工厂方法getInstance用于构建其实例,三个方法以下:
public static final Cipher getInstance(String transformation) throws NoSuchAlgorithmException, NoSuchPaddingException public static final Cipher getInstance(String transformation, String provider) throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException public static final Cipher getInstance(String transformation, Provider provider) throws NoSuchAlgorithmException, NoSuchPaddingException
其中transformation,这里称为转换(模式),是核心参数,见前面一个小节的解析。另外,有两个工厂方法要求必须传入java.security.Provider的全类名或者实例,由于Cipher要从对应的提供商中获取指定转换模式的实现,第一个工厂方法只有单参数transformation,它会从现成全部的java.security.Provider中匹配取出第一个知足transformation的服务,从中实例化CipherSpi(要理解Cipher委托到内部持有的CipherSpi实例完成具体的加解密功能)。实际上Cipher实例的初始化必须依赖于转换模式和提供商。
init方法一共有八个变体方法,此方法主要用于初始化Cipher。
//额外参数是Key(密钥) public final void init(int opmode, Key key) throws InvalidKeyException //额外参数是Key(密钥)和SecureRandom(随机源) public final void init(int opmode, Key key, SecureRandom random) throws InvalidKeyException //额外参数是Key(密钥)和AlgorithmParameterSpec(算法参数透明定义) public final void init(int opmode, Key key, AlgorithmParameterSpec params) throws InvalidKeyException, InvalidAlgorithmParameterException //额外参数是Key(密钥)、AlgorithmParameterSpec(算法参数透明定义)和SecureRandom(随机源) public final void init(int opmode, Key key, AlgorithmParameterSpec params, SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException //额外参数是Key(密钥)、AlgorithmParameters(算法参数) public final void init(int opmode, Key key, AlgorithmParameters params) throws InvalidKeyException, InvalidAlgorithmParameterException //额外参数是Key(密钥)、AlgorithmParameters(算法参数)、SecureRandom(随机源) public final void init(int opmode, Key key, AlgorithmParameters params, SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException //额外参数是Certificate(证书) public final void init(int opmode, Certificate certificate) throws InvalidKeyException //额外参数是Certificate(证书)、SecureRandom(随机源) public final void init(int opmode, Certificate certificate, SecureRandom random) throws InvalidKeyException
opmode(操做模式)是必须参数,可选值是ENCRYPT_MODE、DECRYPT_MODE、WRAP_MODE和UNWRAP_MODE。Key类型参数若是不是非对称加密,对应的类型是SecretKey,若是是非对称加密,能够是PublicKey或者PrivateKey。SecureRandom是随机源,由于有些算法须要每次加密结果都不相同,这个时候须要依赖系统或者传入的随机源,一些要求每次加解密结果相同的算法如AES不能使用此参数(或者必须指定固定的随机源种子)。Certificate是带有密钥的证书实现。算法参数主要包括IV(initialization vector,初始化向量)等等。
wrap方法用于包装一个密钥。
public final byte[] wrap(Key key) throws IllegalBlockSizeException, InvalidKeyException
wrap方法使用的时候须要注意Cipher的opmode要初始化为WRAP_MODE。
unwrap方法用于解包装一个密钥。
public final Key unwrap(byte[] wrappedKey, String wrappedKeyAlgorithm, int wrappedKeyType) throws InvalidKeyException, NoSuchAlgorithmException
unwrap方法使用的时候须要注意Cipher的opmode要初始化为UNWRAP_MODE,在调用unwrap方法时候,须要指定以前包装密钥的算法和Key的类型。
其实wrap和unwrap是一个互逆的操做:
public enum EncryptUtils { /** * 单例 */ SINGLETON; private static final String SECRECT = "passwrod"; public String wrap(String keyString) throws Exception { KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); //初始化密钥生成器,指定密钥长度为128,指定随机源的种子为指定的密钥(这里是"passward") keyGenerator.init(128, new SecureRandom(SECRECT.getBytes())); SecretKey secretKey = keyGenerator.generateKey(); SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), "AES"); Cipher cipher = Cipher.getInstance("AES"); cipher.init(Cipher.WRAP_MODE, secretKeySpec); SecretKeySpec key = new SecretKeySpec(keyString.getBytes(), "AES"); byte[] bytes = cipher.wrap(key); return Hex.encodeHexString(bytes); } public String unwrap(String keyString) throws Exception { byte[] rawKey = Hex.decodeHex(keyString); KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); //初始化密钥生成器,指定密钥长度为128,指定随机源的种子为指定的密钥(这里是"passward") keyGenerator.init(128, new SecureRandom(SECRECT.getBytes())); SecretKey secretKey = keyGenerator.generateKey(); SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), "AES"); Cipher cipher = Cipher.getInstance("AES"); cipher.init(Cipher.UNWRAP_MODE, secretKeySpec); SecretKey key = (SecretKey) cipher.unwrap(rawKey, "AES", Cipher.SECRET_KEY); return new String(key.getEncoded()); } public static void main(String[] args) throws Exception { String wrapKey = EncryptUtils.SINGLETON.wrap("doge"); System.out.println(wrapKey); System.out.println(EncryptUtils.SINGLETON.unwrap(wrapKey)); } }
上面的例子是经过AES对密钥进行包装和解包装,调用main方法,输出:
77050742188d4b97a1d401db902b864d doge
update方法有多个变体,其实意义相差无几:
public final byte[] update(byte[] input) public final byte[] update(byte[] input, int inputOffset, int inputLen) public final int update(byte[] input, int inputOffset, int inputLen, byte[] output) throws ShortBufferException public final int update(ByteBuffer input, ByteBuffer output) throws ShortBufferException
update方法主要用于部分加密或者部分解密,至于加密或是解密取决于Cipher初始化时候的opmode。即便它有多个变体,可是套路是同样的:依赖于一个输入的缓冲区(带有须要被加密或者被解密的数据)、返回值或者参数是一个输出的缓冲区,一些额外的参数能够经过偏移量和长度控制加密或者解密操做的数据段。部分加密或者解密操做完毕后,必需要调用Cipher#doFinal()
方法来结束加密或者解密操做。
doFinal方法也存在多个变体:
/** * 结束多部分加密或者解密操做。 * 此方法须要在update调用链执行完毕以后调用,返回的结果是加密或者解密结果的一部分。 * 此方法正常调用结束以后Cipher会重置为初始化状态。 */ public final byte[] doFinal() throws IllegalBlockSizeException, BadPaddingException /** * 结束多部分加密或者解密操做。 * 此方法须要在update调用链执行完毕以后调用,传入的output做为缓冲区接收加密或者解密结果的一部分。 * 此方法正常调用结束以后Cipher会重置为初始化状态。 */ public final int doFinal(byte[] output, int outputOffset) throws IllegalBlockSizeException, ShortBufferException, BadPaddingException /** * 结束单部分加密或者解密操做。 * 此方法接收须要加密或者解密的完整报文,返回处理结果 * 此方法正常调用结束以后Cipher会重置为初始化状态。 */ public final byte[] doFinal(byte[] input) throws IllegalBlockSizeException, BadPaddingException /** * 结束单部分或者多部分加密或者解密操做。 * 参数inputOffset为须要加解密的报文byte数组的起始位置,inputLen为须要加密或者解密的字节长度 * 此方法正常调用结束以后Cipher会重置为初始化状态。 */ public final byte[] doFinal(byte[] input, int inputOffset, int inputLen) throws IllegalBlockSizeException, BadPaddingException /** * 结束单部分或者多部分加密或者解密操做。 * 参数inputOffset为须要加解密的报文byte数组的起始位置,inputLen为须要加密或者解密的字节长度,output用于接收加解密的结果 * 此方法正常调用结束以后Cipher会重置为初始化状态。 */ public final int doFinal(byte[] input, int inputOffset, int inputLen, byte[] output) throws ShortBufferException, IllegalBlockSizeException, BadPaddingException /** * 结束单部分或者多部分加密或者解密操做。 * 参数inputOffset为须要加解密的报文byte数组的起始位置,inputLen为须要加密或者解密的字节长度, * output用于接收加解密的结果,outputOffset用于设置output的起始位置 * 此方法正常调用结束以后Cipher会重置为初始化状态。 */ public final int doFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) throws ShortBufferException, IllegalBlockSizeException, BadPaddingException /** * 结束单部分或者多部分加密或者解密操做。 * 参数input为输入缓冲区,output为输出缓冲区 * 此方法正常调用结束以后Cipher会重置为初始化状态。 */ public final int doFinal(ByteBuffer input, ByteBuffer output) throws ShortBufferException, IllegalBlockSizeException, BadPaddingException
doFinal主要功能是结束单部分或者多部分加密或者解密操做。单部分加密或者解密适用于须要处理的报文长度较短无需分块的状况,这个时候直接使用byte[] doFinal(byte[] input)
方法便可。多部分加密或者解密适用于须要处理的报文长度长度较大,须要进行分块的状况,这个时候须要调用屡次update
方法变体进行部分块的加解密,最后调用doFinal
方法变体进行部分加解密操做的结束。举个例子,例如处理块的大小为8,实际须要加密的报文长度为23,那么须要分三块进行加密,前面2块长度为8的报文须要调用update进行部分加密,部分加密的结果能够从update的返回值获取到,最后的7长度(其实通常会填充到长度为块长度8)的报文则调用doFinal进行加密,结束整个部分加密的操做。另外,值得注意的是只要Cipher正常调用完任一个doFinal
变体方法(过程当中不抛出异常),那么Cipher会重置为初始化状态,能够继续使用,这个可复用的特性能够下降建立Cipher实例的性能损耗。
首先ADD的意思是Additional Authentication Data(额外的身份认证数据)。updateADD也有三个方法变体:
public final void updateAAD(byte[] src) public final void updateAAD(byte[] src, int offset, int len) public final void updateAAD(ByteBuffer src)
它的方法变体都只依赖一个输入缓冲区,带有额外的身份认证数据,通常使用在GCM
或者CCM
加解密算法中。若是使用此方法,它的调用必须在Cipher的update
和doFinal
变体方法以前调用,其实理解起来也很简单,身份验证必须在实际的加解密操做以前进行。目前,updateADD
的资料比较少,笔者在生产环境找那个也还没有实践过,因此不作展开分析。
其余方法主要是Getter方法,用于获取Cipher的相关信息。
下面画一个图来详细分析一下Cipher的工做流程:
固然上图只分析了Cipher的使用过程,其实还有一个重要的步骤就是密钥的处理,可是密钥的处理和具体的算法使用是相关的,因此图中没有体现。再放一张官方描述Cipher加载的流程:
主要过程包括:
一、建立Cipher实例,这个时候会从平台中全部的提供商(Provider)中根据transformation匹配第一个可使用的CipherSpi实例,"算法/工做模式/填充模式"必须彻底匹配才能选中。
在${JAVA_HONE}/jre/lib/security中的java.security文件中能够看到默认加载的提供商。若是须要添加额外或者自实现的Provider,能够经过java.security.Security的静态方法addProvider添加。
三、根据初始化的方式和是否须要分组处理,选择合适的方法进行调用。
为了方便Cipher的使用,最好先引入apache-codec
依赖,这样能简化Hex、Base64等操做。
<dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.11</version> </dependency>
大多数状况下,加密后的byte数组的中元素取值不在Unicode码点的范围内,表面上看到的就是乱码,实际上它们是有意义的,所以须要考虑把这种byte数组转换为非乱码的字符串以便传输,常见的方式有Hex(二进制转换为十六进制)、Base64等等。下面举例中没有针对异常类型进行处理统一外抛,切勿模仿,还有,全部的字符串转化为字节数组都没有指定字符编码,所以只能使用非中文的明文进行处理。
加密模式下,Cipher只能用于加密,主要由init方法中的opmode决定。举个例子:
public String encryptByAes(String content, String password) throws Exception { //这里指定了算法为AES_128,工做模式为EBC,填充模式为NoPadding Cipher cipher = Cipher.getInstance("AES_128/ECB/NoPadding"); KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); //由于AES要求密钥的长度为128,咱们须要固定的密码,所以随机源的种子须要设置为咱们的密码数组 keyGenerator.init(128, new SecureRandom(password.getBytes())); SecretKey secretKey = keyGenerator.generateKey(); SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), "AES"); //基于加密模式和密钥初始化Cipher cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec); //单部分加密结束,重置Cipher byte[] bytes = cipher.doFinal(content.getBytes()); //加密后的密文由二进制序列转化为十六进制序列,依赖apache-codec包 return Hex.encodeHexString(bytes); }
其实整个过程Cipher的使用都很简单,比较复杂的反而是密钥生成的过程。上面的例子须要注意,由于使用了填充模式为NoPadding,输入的须要加密的报文长度必须是16(128bit)的倍数。
解密模式的使用大体和加密模式是相同的,把处理过程逆转过来就行:
public String decryptByAes(String content, String password) throws Exception { //这里要把十六进制的序列转化回二进制的序列,依赖apache-codec包 byte[] bytes = Hex.decodeHex(content); //这里指定了算法为AES_128,工做模式为EBC,填充模式为NoPadding Cipher cipher = Cipher.getInstance("AES_128/ECB/NoPadding"); KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); //由于AES要求密钥的长度为128,咱们须要固定的密码,所以随机源的种子须要设置为咱们的密码数组 keyGenerator.init(128, new SecureRandom(password.getBytes())); SecretKey secretKey = keyGenerator.generateKey(); SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), "AES"); //基于解密模式和密钥初始化Cipher cipher.init(Cipher.DECRYPT_MODE, secretKeySpec); //单部分加密结束,重置Cipher byte[] result = cipher.doFinal(bytes); return new String(result); }
上面的例子须要注意,由于使用了填充模式为NoPadding,输入的须要加密的报文长度必须是16(128bit)的倍数。
密钥的包装和解包装模式是一对互逆的操做,主要做用是经过算法对密钥进行加解密,从而提升密钥泄漏的难度。
public enum EncryptUtils { /** * 单例 */ SINGLETON; private static final String SECRECT = "passwrod"; public String wrap(String keyString) throws Exception { KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); //初始化密钥生成器,指定密钥长度为128,指定随机源的种子为指定的密钥(这里是"passward") keyGenerator.init(128, new SecureRandom(SECRECT.getBytes())); SecretKey secretKey = keyGenerator.generateKey(); SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), "AES"); Cipher cipher = Cipher.getInstance("AES"); cipher.init(Cipher.WRAP_MODE, secretKeySpec); SecretKeySpec key = new SecretKeySpec(keyString.getBytes(), "AES"); byte[] bytes = cipher.wrap(key); return Hex.encodeHexString(bytes); } public String unwrap(String keyString) throws Exception { byte[] rawKey = Hex.decodeHex(keyString); KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); //初始化密钥生成器,指定密钥长度为128,指定随机源的种子为指定的密钥(这里是"passward") keyGenerator.init(128, new SecureRandom(SECRECT.getBytes())); SecretKey secretKey = keyGenerator.generateKey(); SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), "AES"); Cipher cipher = Cipher.getInstance("AES"); cipher.init(Cipher.UNWRAP_MODE, secretKeySpec); SecretKey key = (SecretKey) cipher.unwrap(rawKey, "AES", Cipher.SECRET_KEY); return new String(key.getEncoded()); } public static void main(String[] args) throws Exception { String wrapKey = EncryptUtils.SINGLETON.wrap("doge"); System.out.println(wrapKey); System.out.println(EncryptUtils.SINGLETON.unwrap(wrapKey)); } }
当一个须要加密的报文十分长的时候,咱们能够考虑把报文切割成多个小段,而后针对每一个小段进行加密,这就是分组加密。分组解密的过程类同,能够看做是分组加密的逆向过程。下面仍是用AES算法为例举个例子:
import org.apache.commons.codec.binary.Hex; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.security.SecureRandom; /** * @author throwable * @version v1.0 * @description * @since 2018/8/15 1:06 */ public enum Part { /** * SINGLETON */ SINGLETON; private static final String PASSWORD = "throwable"; private Cipher createCipher() throws Exception { return Cipher.getInstance("AES"); } public String encrypt(String content) throws Exception { Cipher cipher = createCipher(); KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); //由于AES要求密钥的长度为128,咱们须要固定的密码,所以随机源的种子须要设置为咱们的密码数组 keyGenerator.init(128, new SecureRandom(PASSWORD.getBytes())); SecretKey secretKey = keyGenerator.generateKey(); SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), "AES"); //基于加密模式和密钥初始化Cipher cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec); byte[] raw = content.getBytes(); StringBuilder builder = new StringBuilder(); //[0,9] byte[] first = cipher.update(raw, 0, 10); builder.append(Hex.encodeHexString(first)); //[10,19] byte[] second = cipher.update(raw, 10, 10); builder.append(Hex.encodeHexString(second)); //[20,25] byte[] third = cipher.update(raw, 20, 6); builder.append(Hex.encodeHexString(third)); //多部分加密结束,获得最后一段加密的结果,重置Cipher byte[] bytes = cipher.doFinal(); String last = Hex.encodeHexString(bytes); builder.append(last); return builder.toString(); } public String decrypt(String content) throws Exception { byte[] raw = Hex.decodeHex(content); Cipher cipher = createCipher(); KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); //由于AES要求密钥的长度为128,咱们须要固定的密码,所以随机源的种子须要设置为咱们的密码数组 keyGenerator.init(128, new SecureRandom(PASSWORD.getBytes())); SecretKey secretKey = keyGenerator.generateKey(); SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), "AES"); //基于解密模式和密钥初始化Cipher cipher.init(Cipher.DECRYPT_MODE, secretKeySpec); StringBuilder builder = new StringBuilder(); //[0,14] byte[] first = cipher.update(raw, 0, 15); builder.append(new String(first)); //[15,29] byte[] second = cipher.update(raw, 15, 15); builder.append(new String(second)); //[30,31] byte[] third = cipher.update(raw, 30, 2); builder.append(new String(third)); //多部分解密结束,获得最后一段解密的结果,重置Cipher byte[] bytes = cipher.doFinal(); builder.append(new String(bytes)); return builder.toString(); } public static void main(String[] args) throws Exception{ String raw = "abcdefghijklmnopqrstyuwxyz"; String e = Part.SINGLETON.encrypt(raw); System.out.println(e); System.out.println(Part.SINGLETON.decrypt(e)); } }
上面的分段下标已经在注释中给出,分段的规则由实际状况考虑,通常AES加解密报文不大的时候能够直接单部分加解密便可,这里仅仅是为了作展现。
咱们能够直接查看当前的使用的JDK中Cipher的全部提供商和支持的加解密服务,简单写个main函数就行:
import java.security.Provider; import java.security.Security; import java.util.Set; public class Main { public static void main(String[] args) throws Exception { Provider[] providers = Security.getProviders(); if (null != providers) { for (Provider provider : providers) { Set<Provider.Service> services = provider.getServices(); for (Provider.Service service : services) { if ("Cipher".equals(service.getType())) { System.out.println(String.format("provider:%s,type:%s,algorithm:%s", service.getProvider(), service.getType(), service.getAlgorithm())); } } } } } }
笔者使用的JDK是JDK8的最后一个更新的版本8u181(1.8.0_181),运行main函数输出以下:
provider:SunJCE version 1.8,type:Cipher,algorithm:RSA provider:SunJCE version 1.8,type:Cipher,algorithm:DES provider:SunJCE version 1.8,type:Cipher,algorithm:DESede provider:SunJCE version 1.8,type:Cipher,algorithm:DESedeWrap provider:SunJCE version 1.8,type:Cipher,algorithm:PBEWithMD5AndDES provider:SunJCE version 1.8,type:Cipher,algorithm:PBEWithMD5AndTripleDES provider:SunJCE version 1.8,type:Cipher,algorithm:PBEWithSHA1AndDESede provider:SunJCE version 1.8,type:Cipher,algorithm:PBEWithSHA1AndRC2_40 provider:SunJCE version 1.8,type:Cipher,algorithm:PBEWithSHA1AndRC2_128 .....输出内容太多忽略剩余部分
由于Java原生支持的transformation是有限的,有些时候咱们须要使用一些算法其余工做模式或者填充模式原生没法支持,这个时候咱们须要引入第三方的Provider甚至本身实现Provider。常见的第三方Provider是bouncycastle(BC),目前BC的最新依赖为:
<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.60</version> </dependency>
举个例子,Java原生是不支持AESWRAP算法的,所以能够引入BC的依赖,再使用转换模式AESWRAP。
import org.apache.commons.codec.binary.Hex; import org.bouncycastle.jce.provider.BouncyCastleProvider; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.security.MessageDigest; import java.security.SecureRandom; import java.security.Security; public enum EncryptUtils { /** * SINGLETON */ SINGLETON; private static final String SECRET = "throwable"; private static final String CHARSET = "UTF-8"; //装载BC提供商 static { Security.addProvider(new BouncyCastleProvider()); } private Cipher createAesCipher() throws Exception { return Cipher.getInstance("AESWRAP"); } public String encryptByAes(String raw) throws Exception { Cipher aesCipher = createAesCipher(); KeyGenerator keyGenerator = KeyGenerator.getInstance("AESWRAP"); keyGenerator.init(128, new SecureRandom(SECRET.getBytes(CHARSET))); SecretKey secretKey = keyGenerator.generateKey(); SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), "AESWRAP"); aesCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec); byte[] bytes = aesCipher.doFinal(raw.getBytes(CHARSET)); return Hex.encodeHexString(bytes); } public String decryptByAes(String raw) throws Exception { byte[] bytes = Hex.decodeHex(raw); Cipher aesCipher = createAesCipher(); KeyGenerator keyGenerator = KeyGenerator.getInstance("AESWRAP"); keyGenerator.init(128, new SecureRandom(SECRET.getBytes(CHARSET))); SecretKey secretKey = keyGenerator.generateKey(); SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), "AESWRAP"); aesCipher.init(Cipher.DECRYPT_MODE, secretKeySpec); return new String(aesCipher.doFinal(bytes), CHARSET); } public static void main(String[] args) throws Exception { String raw = "throwable-a-doge"; String en = EncryptUtils.SINGLETON.encryptByAes(raw); System.out.println(en); String de = EncryptUtils.SINGLETON.decryptByAes(en); System.out.println(de); } }
上面的例子须要注意,由于使用了AESWRAP算法,输入的须要加密的报文长度必须是8的倍数。
熟练掌握Cipher的用法、转换模式transformation的一些知识以后,影响咱们编写加解密模块代码的主要因素就是加解密算法的原理或者使用,这些须要咱们去学习专门的加解密算法相关的知识。另外,有些时候咱们发现不一样平台或者不一样语言使用同一个加密算法不能相互解密加密,其实缘由很简单,绝大部分缘由是工做模式选取或者填充模式选取的不一样致使的,排除掉这两点,剩下的可能性就是算法的实现不相同,依据这三点因素(或者说就是transformation这惟一的因素)去判断和寻找解决方案便可。关于加解密算法原理、工做模式等相关知识能够参考下面的资料。
参考资料:
另外,一些特殊的方法例如Ciper#updateADD
暂时没遇到使用场景,这里就不写没实践过的Demo。下一篇文章将会介绍一些主流的加解密算法的基本原理和经过Cipher对这些算法进行加解密应用。
(本文完 c-7-d)