本文是对 Java 语言安全模块 JCE( Java Cryptography Extension,Java 密码学扩展)中 javax.crypto.Cipher
核心类的详细介绍及使用说明。
目录
-
Cipher
简介 - 初始化
transformation
- 算法(algorithm)
- 工作模式(mode)
- 填充模式(padding)
-
Cipher
属性和方法- 主要属性
- 主要方法
- 工作流程
- 附录
Cipher
简介
javax.crypto.Cipher
是 JCE ( Java Cryptography Extension,Java 密码学扩展)的核心,提供基于多种加解密算法的加解密功能。
初始化 transformation
初始化 transformation
(转换模式)由三部分组成,格式:算法/工作模式/填充模式(algorithm/mode/padding)
,如 DES/ECB/PKCS5Padding
。
算法(algorithm)
算法(algorithm),必填项,指具体加密算法的英文名称字符串,如 DES
、AES
、SHA-256
、RSA
等。
工作模式(mode)
工作模式(mode),非必填项,默认为 ECB,主要针对分组密码,分组密码是将明文消息编码后的数字序列(简称明文数字序列)划分成长度为 n
的组(可看成长度为 n
的矢量),每组分别在密钥控制下变换成等长的输出数字序列(简称密文数字序列)。
工作模式的出现主要基于以下原因:
- 当需要加密的明文长度十分大时(如大文件内容),由于硬件或性能原因需要分组加密。
- 多次使用相同的密钥对多个分组加密会引发安全问题。
本质上来说,工作模式是一项增强密码算法或使算法适应具体应用的技术,例如将分组密码应用于数据块组成的序列或数据流。目前已定义的工作模式有以下 5 种:
- ECB(Electronic Code Book,电子密码本),用相同的密钥分别对明文分组独立加密。
- CBC(Cipher Block Chaining,密码分组链接),加密算法的输入是上一个密文组和下一个明文组的异或。
- CFB(Cipher Feed Back,密文反馈),一次处理
s
位,上一个密文组作为加密算法的输入,产生的伪随机数输出与明文组异或后作为下一个密文组。 - OFB(Output Feed Back,输出反馈),与 CFB 类似,只是加密算法的输入是上一次加密的输出,并且使用整个分组。
- CTR(Counter,计数器),每个明文分组都与一个经过加密的计数器相异或,对每个后续分组计数器递增。
填充模式(padding)
填充模式(padding),非必填项,默认为 PKCS5Padding,块加密算法要求,原文数据长度为固定块大小的整数倍,如果原文数据长度大于固定块大小,则需要在固定块填充数据直至整个块的数据是完整的。例如如果约定块长度是 128,实际需要加密的原文长度是 129,则需要分为两个加密块,第二个加密块需要填充 127 位数据,填充模式决定了如果填充。
对原文进行填充主要基于以下原因:
- 安全性,由于对原始数据进行了填充,攻击者很难找到真正的原文位置。
- 由于块加密算法要求原文数据长度为固定块大小的整数倍,如果加密原文不满足这个条件,则需要在加密前填充原文数据至固定块大小的整数倍。
- 填充也为发送方与接收方提供了一种标准的形式以约束加密原文的大小。只有加解密双方知道填充方式,才可知道如何准确移去填充的数据并进行解密。
常见的填充方式有 5 种:
- 填充数据为填充字节序列的长度
填充字符串由一个字节序列 L 组成,每个字节填充该字节序列 L 的长度。
如固定块大小为8
,原文数据长度为9
,则填充字节序列长度为2 * 8 - 9 = 7
,即填充字节数为0x07
原文数据:FF FF FF FF FF FF FF FF FF
填充后数据:FF FF FF FF FF FF FF FF FF 07 07 07 07 07 07 07
- 填充数据为
0x80
后加0x00
填充字符串的第一个字节数是0x80
,后面的每个字节是0x00
。
原文数据:FF FF FF FF FF FF FF FF FF
填充后数据:FF FF FF FF FF FF FF FF FF 80 00 00 00 00 00 00
- 填充数据的最后一个字节为填充字节序列的长度
填充字符串的最后一个字节为该序列的长度,而前面的字节可以是0x00
,也可以是随机的字节序列。
原文数据:FF FF FF FF FF FF FF FF FF
填充后数据:FF FF FF FF FF FF FF FF FF 00 00 00 00 00 00 07
或FF FF FF FF FF FF FF FF FF 0A B0 0C 08 05 09 07
- 填充数据为空格
填充字符串的每个字节为空格对应的字节数0x20
。
原文数据:FF FF FF FF FF FF FF FF FF
填充后数据:FF FF FF FF FF FF FF FF FF 20 20 20 20 20 20 20
- 填充数据为
0x00
填充字符串的每个字节为0x00
。
原文数据:FF FF FF FF FF FF FF FF FF
填充后数据:FF FF FF FF FF FF FF FF FF 00 00 00 00 00 00 00
Java 原生支持的填充模式:
-
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的定义
其它填充模式需要依赖于第三方支持。
JCE 支持的 Cipher 配置:
算法 | 工作模式 | 填充模式 |
---|---|---|
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
|
Cipher
属性和方法
主要属性
属性名 | 数据类型 | 说明 |
---|---|---|
ENCRYPT_MODE |
整型 | 加密模式,用于初始化 |
DECRYPT_MODE |
整型 | 解密模式,用于初始化 |
WRAP_MODE |
整型 | 包装密钥模式,用于初始化 |
UNWRAP_MODE |
整型 | 解包装密钥模式,用于初始化 |
PUBLIC_KEY |
整型 | 解包装密钥模式下指定密钥类型为公钥 |
PRIVATE_KEY |
整型 | 解包装密钥模式下指定密钥类型为私钥 |
SECRET_KEY |
整型 | 解包装密钥模式下指定密钥类型为密钥,主要用于不是非对称加密的密钥(只有一个密钥,不包含私钥和公钥) |
主要方法
Cipher.getInstance
Cipher 提供了 3 个静态工厂方法用于生成其实例。这 3 个方法都有一个核心参数transformation
。其中 2 个方法需要java.security.Provider
全名或其实例对象,因为 Cipher 要从对应供应商中获取指定转换模式的实现,不带此参数的方法会从现成的所有java.security.Provider
中取出第一个满足transformation
的服务并从中实例化CipherSpi
(要理解Cipher
委托到内部持有的CipherSpi
实例完成具体的加解密功能)。实际上Cipher实例的初始化必须依赖于转换模式和提供商。-
init
此方法主要用于初始化 Cipher,共有 8 个重载方法:
init(int opmode, Certificate certificate)
init(int opmode, Certificate certificate, SecureRandom random)
init(int opmode, Key key)
init(int opmode, Key key, AlgorithmParameters params)
init(int opmode, Key key, AlgorithmParameterSpec params)
init(int opmode, Key key, AlgorithmParameterSpec params, SecureRandom random)
init(int opmode, Key key, AlgorithmParameters params, SecureRandom random)
init(int opmode, Key key, SecureRandom random)
参数说明:-
opmode
:操作模式,必填参数,可选值包括ENCRYPT_MODE
、DECRYPT_MODE
、WRAP_MODE
、UNWRAP_MODE
。 -
Key
:如果是对称加密则对应类型是SecretKey
,如果是非对称加密则对应类型是PublicKey
或PrivateKey
。 -
SecureRandom
:随机源,有些算法需要每次加密结果都不相同,这个时候需要依赖系统或者传入的随机源,一些要求每次加解密结果相同的算法如AES
不能使用此参数。 -
Certificate
:带有密钥的证书实现。
-
-
wrap
&unwrap
wrap
方法用于包装一个密钥,使用的时候需要注意 Cipher 的opmode
要初始化为WRAP_MODE
。
unwrap
方法用于解包装一个密钥,使用的时候需要注意 Ciphe r的opmode
要初始化为UNWRAP_MODE
,在调用unwrap
方法时候,需要指定之前包装密钥的算法和Key
的类型。
其实wrap
和unwrap
是一个互逆的操作:-
wrap
方法的作用是把原始的密钥通过某种加密算法包装为加密后的密钥,这样就可以避免在传递密钥的时候泄漏了密钥的明文。 -
unwrap
方法的作用是把加密后的密钥解包装为原始的密钥,得到密钥的明文。
-
update
update
方法主要用于部分加密或者部分解密,至于是加密还是解密取决于 Cipher 初始化时的opmode
参数。
原理:依赖于一个输入的缓冲区(带有需要被加密或者被解密的数据),返回值或者参数是一个输出的缓冲区,一些额外的参数可以通过偏移量和长度控制加密或者解密操作的数据段。部分加密或者解密操作完毕后,必须要调用Cipher.doFinal()
方法来结束加密或者解密操作。-
doFinal
doFinal
共有 7 个重载方法:-
doFinal()
- 结束多部分加密或者解密操作。
- 此方法需要在update调用链执行完毕之后调用,返回的结果是加密或者解密结果的一部分。
- 此方法正常调用结束之后Cipher会重置为初始化状态。
-
doFinal(byte[] input)
- 结束单部分加密或者解密操作。
- 此方法接收需要加密或者解密的完整报文,返回处理结果。
- 此方法正常调用结束之后Cipher会重置为初始化状态。
-
doFinal(byte[] output, int outputOffset)
- 结束多部分加密或者解密操作。
- 此方法需要在update调用链执行完毕之后调用,传入的output作为缓冲区接收加密或者解密结果的一部分。
- 此方法正常调用结束之后Cipher会重置为初始化状态。
-
doFinal(byte[] input, int inputOffset, int inputLen)
- 结束单部分或者多部分加密或者解密操作。
- 参数inputOffset为需要加解密的报文byte数组的起始位置,inputLen为需要加密或者解密的字节长度。
- 此方法正常调用结束之后Cipher会重置为初始化状态。
-
doFinal(byte[] input, int inputOffset, int inputLen, byte[] output)
- 结束单部分或者多部分加密或者解密操作。
- 参数inputOffset为需要加解密的报文byte数组的起始位置,inputLen为需要加密或者解密的字节长度,output用于接收加解密的结果。
- 此方法正常调用结束之后Cipher会重置为初始化状态。
-
doFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset)
- 结束单部分或者多部分加密或者解密操作。
- 参数inputOffset为需要加解密的报文byte数组的起始位置,inputLen为需要加密或者解密的字节长度,output用于接收加解密的结果,outputOffset用于设置output的起始位置
- 此方法正常调用结束之后Cipher会重置为初始化状态。
-
doFinal(ByteBuffer input, ByteBuffer output)
- 结束单部分或者多部分加密或者解密操作。
- 参数input为输入缓冲区,output为输出缓冲区。
- 此方法正常调用结束之后Cipher会重置为初始化状态。
-
doFinal()
主要功能是结束单部分或者多部分加密或者解密操作。
- 单部分加密或者解密适用于需要处理的报文长度较短无需分块的情况,这个时候直接使用byte[] doFinal(byte[] input)方法即可。
- 多部分加密或者解密适用于需要处理的报文长度长度较大,需要进行分块的情况,这个时候需要调用多次update方法变体进行部分块的加解密,最后调用doFinal方法变体进行部分加解密操作的结束。
举个例子,例如处理块的大小为 8
,实际需要加密的报文长度为 23
,那么需要分三块进行加密,前面 2
块长度为 8
的报文需要调用 update
进行部分加密,部分加密的结果可以从 update
的返回值获取到,最后长度为 7
(一般会填充到长度为块长度 8
)的报文则调用 doFinal
进行加密,结束整个部分加密的操作。另外,值得注意的是只要 Cipher 正常调用完任一个 doFinal
变体方法(过程中不抛出异常),那么 Cipher 会重置为初始化状态,可以继续使用,这个可复用的特性可以降低创建 Cipher 实例的性能损耗。
工作流程
Cipher 工作流程
-
Cipher.getInstance
创建一个 Cipher 实例:- 如果有
java.security.Provider
全名或其实例对象,则从对应供应商中获取指定转换模式的实现实例化CipherSpi
; - 如果没有此参数,则从现成的所有
java.security.Provider
中取出第一个满足transformation
的服务并从中实例化CipherSpi
。
在${JAVA_HONE}/jre/lib/security中的java.security文件中可以看到默认加载的提供商。如果需要添加额外或者自实现的Provider,可以通过java.security.Security的静态方法addProvider添加。
- 如果有
- 通过 Cipher 实例的
init
方法初始化,主要参数是opmode
和密钥。 - 根据初始化的方式和是否需要分组进行处理
- 需要分组处理:
update -> doFinal
- 不需要分组处理:执行
wrap -> unwrap -> doFinal
doFinal
方法返回最终结果。
- 需要分组处理: