Cipher类
Cipher类提供用于加密和解密的加密密码的功能。 加密是取数据(称为明文)和密钥的过程,并且产生对不知道密钥的第三方毫无意义的数据(密文)。 解密是相反的过程:取密文和密钥并产生明文。
对称与非对称密码学
有两种主要类型的加密:对称(也称为密钥)和非对称(或公钥密码)。 在对称密码学中,同一个密钥既能加密也能解密数据。 保持密钥私密对保持数据保密至关重要。 另一方面,非对称加密使用公钥/私钥对来加密数据。 用一个密钥加密的数据与另一个密钥解密。 用户首先生成公钥/私钥对,然后将公钥发布在任何人都可以访问的可信数据库中。 希望与该用户安全通信的用户使用检索到的公钥来加密数据。 只有私钥的持有者才能解密。 保密私钥对此方案至关重要。
不对称算法(如RSA)通常比对称算法慢得多。 这些算法不能有效地保护大量的数据。 实际上,不对称算法被用来交换较小的秘密密钥,用来初始化对称算法。
流加密与分组加密
有两种主要类型:块加密和流加密。 块加密(分组加密)一次处理整个块,通常是多个字节的长度。 如果没有足够的数据来创建完整的输入块,则必须填充数据:也就是说,在加密之前,必须添加虚拟字节以使密码块大小成倍数。 这些字节在解密阶段被剥离。 填充既可以由应用程序完成,也可以通过初始化密码来使用填充类型,例如“PKCS5PADDING”。 相比之下,流加密一次只能处理一个小单元(通常是一个字节,甚至一点点)的传入数据。 这允许密码处理任意数量的数据而不用填充。
操作模式
当使用简单的分组密码进行加密时,两个相同的明文块将总是产生相同的密文块。如果他们注意到重复文本块,那么试图破解密文的密码分析者将会有更容易的工作。为了增加文本的复杂性,反馈模式使用前面的输出块在应用加密算法之前改变输入块。第一个块需要一个初始值,这个值被称为初始化向量(IV)。由于IV在加密之前只是简单地改变数据,所以IV应该是随机的,但不一定需要保密。有多种模式,例如CBC(密码块链接),CFB(密码反馈模式)和OFB(输出反馈模式)。 ECB(电子码本模式)是一种不受块位置或其他密文块影响的模式。因为如果ECB密文使用相同的明文/密钥,ECB密文是相同的,这种模式通常不适合加密应用,不应该使用。
更新的密码模式,例如带有关联数据的认证加密(AEAD)(例如,伽罗瓦/计数器模式(GCM)),可以对数据进行加密并同时验证结果信息。 在计算生成的AEAD标记(Mac)期间可以使用附加关联数据(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);
// AAD is optional, if present, it must be supplied before any update/doFinal calls. 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);
秘钥:
一些算法如AES和RSA允许不同长度的密钥,但其他算法则是固定的,如3DES。 使用更长的密钥进行加密通常意味着对消息恢复的更强的抵抗力。 像往常一样,安全和时间之间有一个折衷,所以选择适当的密钥长度。
大多数算法使用二进制密钥。 即以十六进制表示,大多数人类也无法记忆长序列的二进制数字。 字符密码更容易记忆。 由于字符密码通常是从少量字符中选择的(例如[a-zA-Z0-9]),因此已经定义了诸如“基于密码的加密”(PBE)等协议,这些协议使用字符密码并生成强二进制密钥。 为了使攻击者从口令到密钥的任务非常耗时(通过所谓的“字典式攻击”,其中常用字典字 - 值映射是预先计算的),大多数PBE实现将以随机数混合, 被称为盐,以增加关键的随机性。
密码对象是通过使用密码getInstance()静态工厂方法之一获得的。 在这里,算法名称与其他引擎类稍有不同,因为它不仅指定算法名称,而且指定“变换”。 转换是一个字符串,它描述了在给定输入上执行的操作(或操作集)以产生一些输出。 变换总是包括密码算法的名称(例如,AES),并且可以跟随模式和填充方案。
转型的形式是:
- “algorithm/mode/padding” or
- “algorithm”
例如,以下是有效的转换:
"AES/CBC/PKCS5Padding"
"AES"
如果只指定了一个转换名称,系统将确定在环境中是否有所需的转换实现,如果有多个转换名称,则返回一个首选项。
如果同时指定了转换名称和包提供者,系统将确定所请求的包中是否存在所请求转换的实现,如果没有,则抛出异常。
建议使用完全指定算法,模式和填充的转换。 如果不这样做,提供者将使用默认值。 例如,SunJCE和SunPKCS11提供程序将ECB用作默认模式,将PKCS5Padding用作许多对称密码的默认填充。
这意味着在SunJCE提供商的情况下:
Cipher c1 = Cipher.getInstance("AES/ECB/PKCS5Padding");
和
Cipher c1 = Cipher.getInstance("AES");
是等同的语句。
**注意:**ECB模式是最简单的块密码模式,并且是JDK / JRE中的默认模式。 ECB适用于单个数据块,但绝对不应该用于多个数据块。
使用CFB和OFB等模式,分组密码可以以小于密码实际块大小的单位加密数据。 在请求这种模式时,可以通过在“AES / CFB8 / NoPadding”和“AES / OFB32 / PKCS5Padding”转换中将模式名称附加到模式名称来一次指定要处理的位数。 如果没有指定这样的号码,则使用提供者特定的默认值。 (例如,SunJCE提供程序使用默认值为128位的AES)。因此,可以使用8位模式(如CFB8或OFB8)将块密码转换为面向字节的流密码。
初始化Cipher对象
通过getInstance获得的Cipher对象必须初始化为四种模式之一,在Cipher类中定义为最终整数常量。 这些模式可以通过它们的符号名称来引用,这些符号名称将在下面显示,同时还会描述每种模式的用途:
ENCRYPT_MODE
数据加密.
DECRYPT_MODE
数据解密.
WRAP_MODE
将java.security.Key包装为字节,以便可以安全地传输密钥.
UNWRAP_MODE
将之前包装的密钥解包到java.security.Key对象中.
每个密码初始化方法都采用操作模式参数(opmode),并初始化该模式的密码对象。 其他参数包括包含密钥(certificate)的密钥(key)或证书,算法参数(params)以及随机源(random)。
要初始化Cipher对象,请调用以下init方法之一:
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方法,则将引发InvalidKeyException或InvalidAlgorithmParameterException异常,具体取决于所使用的init方法。
必须使用与加密相同的参数进行解密。
请注意,当一个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)
要以多个步骤加密或解密数据,请调用其中一种更新方法:
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使得编写代码更方便,因为它直接处理关键对象。 这些方法还可以安全地传输基于硬件的密