JCA - Cipher类
Cipher类
Cipher类提供用于加密和解密的加密密码的功能。加密是处理数据(称为明文)和密钥的过程,并且产生对不知道密钥的第三方毫无意义的数据(密文)。解密是相反的过程:取密文和密钥并产生明文。
对称加密 VS. 非对称加密
有两种主要加密类型:对称(也称为密钥)和非对称(或公钥密码)。在对称密码学中,同一个密钥既能加密也能解密数据。保持密钥私密性对保持数据保密至关重要。另一方面,非对称加密使用公钥/私钥对来加密数据。数据通过一个密钥加密,通过另一个密钥解密。用户首先生成公钥/私钥对,然后将公钥发布在任何人都可以访问的可信数据库中。希望与该用户安全通信的用户使用检索到的公钥来加密数据。只有私钥的持有者才能解密。保密私钥对此方案至关重要。
非对称算法(如RSA)通常比对称算法慢得多。 这些算法不能有效地保护大量的数据。实际上,非对称算法被用来保护较小的用来初始化对称算法的对称密钥。
块加密 VS. 流加密
加密有两种主要类型:块(分组)和流。 块加密一次处理整个块,通常是多个字节的长度。如果没有足够的数据来创建完整的输入块,则必须填充数据:也就是说,在加密之前,必须添加虚拟字节以使数据是密码块大小的倍数。这些字节在解密阶段被剥离。填充既可以由应用程序完成,也可以通过初始化密码来使用填充类型,例如“PKCS5PADDING”。
相比之下,流密码一次只能处理一个小单元(通常是一个字节,甚至一点点)的传入数据。 这允许密码处理任意数量的数据而不需要填充
。
操作模式
当使用简单的块加密进行加密时,两个相同的明文块将总是产生相同的密文块。如果注意到重复文本块,那么密码分析者将会更容易破解密文。为了增加文本的复杂性,反馈模式
使用前面的输出块在应用加密算法之前改变输入块。第一个块需要一个初始值,这个值被称为初始化向量(IV)。由于IV在加密之前只是简单地改变数据,所以IV应该是随机的,但不一定需要保密。有多种模式,例如CBC(Cipher Block Chaining,密码块链接),CFB(Cipher Feedback Mode,密码反馈模式)和OFB(Output Feedback Mode,输出反馈模式)
。ECB(Electronic Codebook Mode,电子码本模式)
是一种不受块位置或其他密文块影响的模式。因为如果ECB密文使用相同的明文/密钥,ECB密文是相同的,这种模式通常不适合加密应用,不应该使用。
一些算法如AES和RSA允许不同长度的密钥,但其他算法则是固定的,如3DES。使用更长的密钥进行加密通常意味着对消息恢复的更强的抵抗力。通常,安全和时间之间有一个折衷,所以选择适当的密钥长度。
大多数算法使用二进制密钥。大多数人类也无法记忆长序列的二进制数字,即使以十六进制表示。而字符密码更容易记忆。由于字符密码通常是从少量字符中选择的(例如[a-zA-Z0-9]),因此定义了诸如基于密码的加密(Password-Based Encryption, PBE)
等协议,这些协议使用字符密码并生成强二进制密钥。为了使攻击者从口令到密钥的破解非常耗时(通过所谓的“字典式攻击”,其中常用字典字-值映射是预先计算的),大多数PBE实现将以随机数混合,被称为盐,以增加密钥的随机性。
更新的密码模式,例如带有关联数据的认证加密(Authenticated Encryption with Associated Data,AEAD)
(例如,伽罗瓦/计数器模式( Galois/Counter Mode, GCM)
),可以对数据进行加密并同时验证结果信息。在计算生成的AEAD标记(Mac)期间可以使用附加关联数据(Additional Associated Data, AAD)
,但是这个AAD数据不会以密文的形式输出。(例如,某些数据可能不需要保密,但应该计算标签计算以检测修改。)Cipher.updateAAD()方法可用于在标签计算中包含AAD。
使用GCM模式的AES加密
使用GCM的AES加密是一种AEAD加密,与非AEAD密码具有不同的使用模式。除了常规数据外,还需要AAD,对于加密/解密这是可选的,但AAD必须在数据加密/解密之前提供。另外,为了安全地使用GCM,调用者不应该重复使用密钥和IV组合来进行加密。这意味着每次加密操作时,密码对象应该用不同的一组参数显式地重新初始化。
SecretKey myKey = ...
byte[] myAAD = ...
byte[] plainText = ...
int myTLen = ...
byte[] myIv = ...
GCMParameterSpec myParams = new GCMParameterSpec(myTLen, myIv);
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
c.init(Cipher.ENCRYPT_MODE, myKey, myParams);
//ADD是可选的,这里,ADD必须在update/doFinal调用之前提供
c.updateAAD(myAAD); // if AAD is non-null
byte[] cipherText = new byte[c.getOutputSize(plainText.length)];
c.doFinal(plainText, 0, plainText.length, cipherText); // conclusion of encryption operation
// To decrypt, same AAD and GCM parameters must be supplied
c.init(Cipher.DECRYPT_MODE, myKey, myParams);
c.updateAAD(myAAD);
byte[] recoveredText = c.doFinal(cipherText);
// MUST CHANGE IV VALUE if the same key were to be used again for encryption
byte[] newIv = ...;
myParams = new GCMParameterSpec(myTLen, newIv);
创建Cipher 对象
Cipher对象是通过使用一个Cipher getInstance()静态工厂方法获得的。在这里,算法名称与其他引擎类稍有不同,因为它不仅指定算法名称,而且指定“转换”。 转换是一个字符串,它描述了在给定输入上执行的操作(或操作集)以产生一些输出。 转换总是包括密码算法的名称(例如,AES),并且可以跟随模式和填充方案。
转换的格式如下:
- “algorithm/mode/padding” or
- “algorithm”
例如,以下是有效的转换:
- “AES/CBC/PKCS5Padding”
- “AES”
如果只指定了一个转换名称,系统将确定在环境中是否有所需转换的实现,如果有多个转换名称,则返回一个首选项。
如果同时指定了转换名称和Provider包,系统将确定所请求的包中是否存在所请求转换的实现,如果没有,则抛出异常。
建议使用指定算法、模式和填充的转换。如果不这样做,Provider将使用默认值。例如,SunJCE和SunPKCS11提供程序将ECB用作默认模式,将PKCS5Padding用作许多对称密码的默认填充。
这意味着在使用SunJCE Provider时,下面2段代码时等价的:
Cipher c1 = Cipher.getInstance("AES/ECB/PKCS5Padding");
Cipher c1 = Cipher.getInstance("AES");
注意:ECB模式是最简单的块密码模式,并且是JDK/JRE中的默认模式。ECB适用于单个数据块,但绝对不应该用于多个数据块。
使用CFB(Cipher Feedback Mode,密码反馈模式)
和OFB(Output Feedback Mode,输出反馈模式)
等模式,块加密可以以小于密码实际块大小的单位加密数据。在请求这种模式时,可以通过在“AES/CFB8/NoPadding”和“AES/OFB32/PKCS5Padding”转换中将处理的位数附加到模式名称进行一次性设置
。如果没有指定这样的数字,则使用提供者特定的默认值。(例如,SunJCE提供程序使用默认值为128位的AES)。因此,可以使用8位模式(如CFB8或OFB8)将块密码转换为面向字节的流密码。
本文档的附录A包含一个标准名称列表,可用于指定转换的算法名称、模式和填充部分。
工厂方法返回的对象是未初始化的,必须在对象使用之前进行初始化。
初始化Cipher对象
通过getInstance()获得的Cipher对象必须初始化为四种模式之一,模式在Cipher类中定义为final整数常量。这些模式可以通过它们的符号名称来引用,这些符号名称将在下面显示,同时还会描述每种模式的用途:
ENCRYPT_MODE
加密数据
DECRYPT_MODE
解密数据
WRAP_MODE
将java.security.Key包装为字节,以便可以安全地传输密钥.
UNWRAP_MODE
将之前包装的密钥解包到java.security.Key对象中.
Cipher的每个初始化方法都采用操作模式参数(opmode),并初始化为该模式的Cipher对象。其他参数包括包含密钥(key)或包含密钥的证书(certificate),算法参数(params)以及随机源(random)。
调用以下init方法之一,初始化Cipher对象:
public void init(int opmode, Key key);
public void init(int opmode, Certificate certificate);
public void init(int opmode, Key key, SecureRandom random);
public void init(int opmode, Certificate certificate,SecureRandom random);
public void init(int opmode, Key key, AlgorithmParameterSpec params);
public void init(int opmode, Key key, AlgorithmParameterSpec params, SecureRandom random);
public void init(int opmode, Key key, AlgorithmParameters params);
public void init(int opmode, Key key, AlgorithmParameters params, SecureRandom random);
如果用于加密的Cipher对象初始化需要参数(例如,初始化向量),但是没有给init方法提供参数,则底层密码实现本身应该提供所需的参数,或者通过生成随机参数或者通过使用 默认的,特定于provider的参数集合。
但是,用于解密的Cipher对象初始化需要参数,但是没有参数提供给init方法,则将引发InvalidKeyException或InvalidAlgorithmParameterException异常,具体取决于所使用的init方法。
有关更多详细信息,请参阅关于算法参数管理一节.
加密和解密必须使用相同的参数。
请注意,当一个Cipher对象被初始化后,它将失去所有先前获得的状态。换句话说,初始化Cipher就相当于创建一个新的Cipher实例,并初始化它。例如,如果Cipher首先被初始化为用给定密钥进行解密,然后初始化用于加密,则在解密模式下将失去获得的所有状态。
数据加密和解密
数据可以在一个步骤(单部分操作)或多个步骤(多部分操作)中加密或解密。如果事先不知道数据将要运行多长时间,或者数据太长而无法一次存储在内存中,则多步骤操作时非常有用的。
要在一个步骤中加密或解密数据,请调用其中一个doFinal方法:
public byte[] doFinal(byte[] input);
public byte[] doFinal(byte[] input, int inputOffset, int inputLen);
public int doFinal(byte[] input, int inputOffset, int inputLen, byte[] output);
public int doFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset)
要以多个步骤加密或解密数据,请调用其中一种update方法:
public byte[] update(byte[] input);
public byte[] update(byte[] input, int inputOffset, int inputLen);
public int update(byte[] input, int inputOffset, int inputLen, byte[] output);
public int update(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset)
多部分操作必须由上述doFinal方法之一终止(如果最后一步仍有一些输入数据),或者通过以下doFinal方法之一(如果没有输入数据留给最后一步):
public byte[] doFinal();
public int doFinal(byte[] output, int outputOffset);
如果指定的转换中将填充(或非填充)作为请求的一部分,则所有doFinal方法都将处理任何必要的填充(或非填充)
调用doFinal会将Cipher对象重置为通过调用init初始化时的状态。也就是说,Cipher对象被重置并且可用于加密或解密(取决于在对init的调用中指定的操作模式)更多的数据。
封装或解封密钥
封装密钥可以将密钥从一个地方安全地转移到另一个地方。
wrap/unwrap API使得编写代码更方便,因为它直接处理密钥对象。这些方法还可以安全地传输基于硬件的密钥。
要包装一个Key,首先要将Cipher对象为初始化WRAP_MODE模式,然后调用以下内容:
public final byte[] wrap(Key key);
如果您将打包的密钥字节(调用wrap的结果)提供给解包它们的其他人,请务必发送收件人需要的附加信息,以便进行解包:
- 密钥算法名称, 和
- 封包密钥的类型 (Cipher.SECRET_KEY, Cipher.PRIVATE_KEY, or Cipher.PUBLIC_KEY之一).
密钥算法名称可以通过从Key接口调用getAlgorithm方法来确定:
public String getAlgorithm();
要解包之前wrap返回的字节,请首先初始化UNWRAP_MODE的Cipher对象,然后调用以下内容:
public final Key unwrap(byte[] wrappedKey, String wrappedKeyAlgorithm, int wrappedKeyType);
这里,wrappedKey是之前调用wrap返回的字节,wrappedKeyAlgorithm是包装的密钥的算法,wrappedKeyType是包装密钥的类型,类型必须是Cipher.SECRET_KEY,Cipher.PRIVATE_KEY或Cipher.PUBLIC_KEY之一。
算法参数管理
底层Cipher实现使用的参数(通过应用程序显式传递给init方法或由底层实现本身生成)可以通过调用其getParameters方法从Cipher对象中获取,该方法将参数作为java.security.AlgorithmParameters对象返回(如果没有使用参数,则返回null)。如果参数是初始化向量(IV),也可以通过调用getIV方法来获取。
在以下示例中,实现基于密码加密(PBE)的Cipher对象仅使用一个密钥而没有参数进行初始化。但是,所选择的基于密码加密算法需要两个参数:一个salt和一个迭代计数。这些将由底层算法实现本身生成。 应用程序可以从Cipher对象中获取生成的参数,如下所示:
import javax.crypto.*;
import java.security.AlgorithmParameters;
//获取PBE Cipher对象
Cipher c = Cipher.getInstance("PBEWithHmacSHA256AndAES_256");
//不提供任何参数初始化用于加密的Cipher。这里,“myKey”假设指向已经产生的密钥
c.init(Cipher.ENCRYPT_MODE, myKey);
//加密一些数据并且存储之后用于解密的密文
byte[] cipherText = c.doFinal("This is just an example".getBytes());
//获取底层cipher实现产生的参数
AlgorithmParameters algParams = c.getParameters();
//获取参数编码并存储
byte[] encodedAlgParams = algParams.getEncoded();
必须使用与加密相同的参数进行解密。可以从它们的编码实例化,并用于初始化进行解密的相应Cipher对象,如下所示:
import javax.crypto.*;
import java.security.AlgorithmParameters;
//获取PBE的参数对象
AlgorithmParameters algParams;
algParams = AlgorithmParameters.getInstance("PBEWithHmacSHA256AndAES_256");
//使用上面的参数编码初始化
algParams.init(encodedAlgParams);
//获取PBE的cipher对象
Cipher c = Cipher.getInstance("PBEWithHmacSHA256AndAES_256");
//使用传入AlgorithmParameter对象的init()初始化解密用的cipher,传入上面的algParams对象
c.init(Cipher.DECRYPT_MODE, myKey, algParams);
如果在初始化Cipher对象时没有指定任何参数,并且您不确定底层实现是否使用任何参数,则可以通过简单地调用Cipher对象的getParameters方法并检查返回的值来获取。返回值为null表示没有使用参数。
SunJCE Provider实现的密码算法使用以下参数:
- AES,DES-EDE和Blowfish用于反馈模式(即CBC,CFB,OFB或PCBC)时,使用初始化向量(IV)。javax.crypto.spec.IvParameterSpec类可用于使用给定的IV初始化Cipher对象。
- PBE加密算法使用一组参数,包括盐和迭代计数。javax.crypto.spec.PBEParameterSpec类可用于使用给定的salt和迭代次数初始化实现PBE算法的Cipher对象(例如:PBEWithHmacSHA256AndAES_256)。
注意,如果使用SealedObject类,则不必担心存储或传输解密操作使用的任何算法参数。 该类将用于密封(加密)的参数附加到加密的对象内容,并使用相同的参数来解密(解密)。
Cipher输出注意事项
Cipher的update和doFinal方法允许调用者指定要将数据加密或解密的输出缓冲区。在这些情况下,传递足够大的缓冲区来保存加密或解密操作的结果是很重要的。
Cipher中的以下方法可用于确定输出缓冲区的大小:
public int getOutputSize(int inputLen)
参考
Java ™ Cryptography Architecture (JCA) Reference Guide
Java Cryptography Architecture (JCA) Reference Guide
Java加密体系结构(JCA)参考指南
[译]JCA参考指南(一):介绍
[译]JCA参考指南(二):核心类和接口
JCA-Java加密框架