该篇文章是对 Jakob Jenkov 个人博客 JCE 部分的翻译
Java Cryptography API 使你能够在 Java 中加密和解密数据,以及管理密钥、签名、验证消息、计算加密哈希等等。Cryptography 属于经常被简称为 crypto,所以你有时候可能会看到 Java crypto 而不是 Java Cryptography,这两个术语是同一个意思。
Java Cryptography API 由被称为 Java Cryptography Extension 提供,Java Cryptography Extension 经常被引用通过它的缩写 JCE。Java Cryptography Extension 长期以来一直是 Java 平台的一部分。JCE 最初与 Java 是分开的,因为美国对加密技术有一些出口限制。因此,最强的加密算法并未包含在标准 Java 平台中。如果你是美国境内的公司,你可以从 Java JCE 获得这些更强大的加密算法,但世界其他地区不得不使用较弱的算法。到 2017 年的时候,美国加密出口规则已经放宽了很多,因此,世界上大部分地区都可以通过 JCT 从国际加密标准中受益。
Java Cryptography Architecture(JCA)是 Java 加密 API 内部设计的名称。JCA 是围绕一些核心通用类和接口构建的。这些接口背后的真正功能是由 Providers 提供的。因此,你可以使用一个 Cipher 类来加密和解密某些数据,但具体的密码实现取决于所使用的具体 Provider。
Provider
java.security.Provider 类是 Java cryptography API 中的核心类。为了使用 Java crypto API,你需要一组 Provider。Java SDK 带有自己的 Provider。如果你没有显示设置 Provider,则会使用 Java SDK 默认的 Provider。但是此 Provider 可能不支持你要使用的加密算法。因此,你可能不得不设置自己的 Provider。
在 Java cryptography API 中最流行的 Provider 之一是 Bouncy Castle
,如下是一个设置 BouncyCastleProvider 的示例:
java
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.Security;
public class ProviderExample {
public static void main(String[] args) {
Security.addProvider(new BouncyCastleProvider());
}
}
Cipher
javax.crypto.Cipher 类表示一种加密算法,Cipher 是在密码学领域中一个加密算法的标准术语,这就是为什么 Java 类被叫做 Cipher 而不是 Encrypter/Decrypter 或者其他的原因。
你可以使用一个 Cipher 实例在 Java 中加密和解密数据。
创建 Cipher
在你使用 Cipher 之前需要先创建一个 Cipher 类的实例。你可以通过调用 Cipher 的 getInstance() 方法来创建一个 Cipher 实例,该方法需要传入一个用来表明你想要使用的加密算法的参数,实例如下:
ini
Cipher cipher = Cipher.getInstance("AES");
此示例创建了一个使用 AES 加密算法的 Cipher 实例。
Cipher 模式
一些加密算法可以在不同的模式下工作,加密模式指定有关算法应如何加密数据的详细信息,因此,加密模式影响了部分加密算法。
加密模式有时可以与多种不同的加密算法一起使用 ---- 就像附加到核心加密算法的一门技术。这就是为什么这些模式被认为与加密算法本身是分开的,而不是具体加密算法的 "附加组件"。以下是一些著名的 cipher 模式:
- ECB - Electronic Codebook
- CBC - Cipher Block Chaining
- CFB - Cipher Feedback
- OFB - Output Feedback
- CTR - Counter
初始化 Cipher 时,你可以将其模式附加到加密算法的名称后面。例如,要使用 Cipher Block Chaining(CBC)创建 AES Cipher 实例,你可以使用以下代码:
ini
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
由于密码块链接也需要 "填充方案",因此 "填充方案" 附加在加密算法名称字符串的末尾。
值得注意的是,默认的 Java SDK Provider 并不包含所有的加密算法和模式,你可能需要一个外部的 Provider 比如 Bouncy Castle 来使用你想要的模式和填充方案来实例化 Cipher。
初始化 Cipher
在使用 Cipher 实例之前,你必须对其进行初始化。初始化一个 Cipher 是指调用它的 init()
方法,init()
方法需要两个参数:
- 加密/解密密码操作模式
- 加密/解密密钥
下面是一个以加密方式初始化 Cipher 实例的例子:
vbnet
Key key = ... // get / create symmetric encryption key
cipher.init(Cipher.ENCRYPT_MODE, key);
下面是一个以解密方式初始化 Cipher 实例的例子:
vbnet
Key key = ... // get / create symmetric encryption key
cipher.init(Cipher.DECRYPT_MODE, key);
加密和解密数据
为了使用 Cipher 实例加密或解密数据,你可以调用以下两个方法中的一个:
- update()
- doFinal()
这两个方法都有多个接受不同参数的重载版本,我将在这里介绍最常用的版本。
如果你想要加密/解密单个数据块,只需调用 doFinal()
并传入需要加密/解密的数据。如下是一个加密示例:
ini
byte[] plainText = "abcdefghijklmnopqrstuvwxyz".getBytes("UTF-8");
byte[] cipherText = cipher.doFinal(plainText);
只需要在初始化 Cipher 的时候指定是加密模式还是解密模式,就可以使用同一个方法 doFinal 来加密/解密数据。
如果你想要加密/解密多个数据块,例如来自一个大文件的多个块,你可以为每个数据块调用一次 update()
,并在最后一个数据块调用 doFinal()
,下面是一个加密多个数据块的例子:
ini
byte[] data1 = "abcdefghijklmnopqrstuvwxyz".getBytes("UTF-8");
byte[] data2 = "zyxwvutsrqponmlkjihgfedcba".getBytes("UTF-8");
byte[] data3 = "01234567890123456789012345".getBytes("UTF-8");
byte[] cipherText1 = cipher.update(data1);
byte[] cipherText2 = cipher.update(data2);
byte[] cipherText3 = cipher.doFinal(data3);
最后一个数据块需要调用 doFinal()
的原因是,一些加密算法需要填充数据以适应确定的密码块大小(比如 8 byte 边界)。但是我们不想填充中间的数据块,因此中间数据块调用 update()
,最后一个数据块调用 doFinal()
。
当解密多个数据块时也是一样的,以下是使用 Cipher 实例解密多个数据块的示例:
ini
byte[] plainText1 = cipher.update(cipherText1);
byte[] plainText2 = cipher.update(cipherText2);
byte[] plainText3 = cipher.doFinal(cipherText3);
在这之前记得将 Cipher 实例初始化为解密模式。
加密/解密部分字节数组
Cipher 类的加解密方法能够加密/解密一个字节数组中的部分数据,你只需要简单地将 offset 和 length 传递给 update()
或者 doFinal()
方法即可,如下所示:
ini
int offset = 10;
int length = 24;
byte[] cipherText = cipher.doFinal(data, offset, length);
此示例加密从索引 8 开始的 24 个字节。
加密/解密到现有字节数组中
到目前为止,本教程中展示的所有加密和解密示例都是通过返回一个新的字节数组,然而,你也可以将加密/解密后的数据填充到现有的字节数组中,这对于减少创建的字节数组的数量很有用。
通过将目标字节数组作为参数传递给 update()
或 doFinal()
方法来可以实现这一点,示例如下:
ini
int offset = 10;
int length = 24;
byte[] dest = new byte[1024];
cipher.doFinal(data, offset, length, dest);
此示例加密源字节数组中从索引 10 开始的 24 个字节,并将加密后的数据从索引 0 开始填充到 dest 字节数组中,如果你想要为 dest 字节数组设置不同的起始索引,Cipher 类中有额外接收目标字节数组偏移量的版本的 update()
和 doFinal()
方法,下面示例中给 doFinal() 方法传递了目标字节数组的偏移量:
ini
int offset = 10;
int length = 24;
byte[] dest = new byte[1024];
int destOffset = 12
cipher.doFinal(data, offset, length, dest, destOffset);
重用 Cipher 实例
初始化 Cipher 实例是一项开销很大的操作。因此,重用 Cipher 实例是个好主意。幸运的是,Cipher 类在设计时考虑到了重用。
当你在 Cipher 实例上调用 doFinal()
方法时,Cipher 实例将返回到初始化后的状态。Cipher 实例能够用来加密解密数据多次。
下面是一个重用 Cipher 实例的例子:
ini
Cipher cipher = Cipher.getInstance("AES");
Key key = ... // get / create symmetric encryption key
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] data1 = "abcdefghijklmnopqrstuvwxyz".getBytes("UTF-8");
byte[] data2 = "zyxwvutsrqponmlkjihgfedcba".getBytes("UTF-8");
byte[] cipherText1 = cipher.update(data1);
byte[] cipherText2 = cipher.doFinal(data2);
byte[] data3 = "01234567890123456789012345".getBytes("UTF-8");
byte[] cipherText3 = cipher.doFinal(data3);
MessageDigest
Java MessageDigest 类表示一个加密散列算法,它被用来从二进制数据中计算消息摘要。当你收到一些加密数据时,你无法从数据本身看出它是否在传输过程中被修改,消息摘要可以帮助解决该问题。
为了能够检测加密数据是否在传输过程中被修改,发送方可以根据数据计算消息摘要并将其与数据一起发送。当你收到加密后的数据和消息摘要时,你可以根据数据重新计算消息摘要,并检查计算出的消息摘要是否与随数据接收的消息摘要相匹配。如果两个消息摘要匹配,则有可能加密数据在传输过程中未被修改。
创建 MessageDigest 实例
要创建 MessageDigest 实例,你可以调用 MessageDigest 类的静态方法 getInstance()。以下是创建 MessageDigest 实例的示例:
ini
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
传递给 getInstance()
方法的文本参数是要使用的具体消息摘要算法的名称。
消息摘要算法
Java Cryptography API 支持以下消息摘要算法:
- MD2
- MD5
- SHA-1
- SHA-256
- SHA-384
- SHA-512
并非所有的这些消息摘要算法都同样安全,建议使用 SHA-256 或更高版本以获得尽可能高的安全性。
计算 MessageDigest
一旦你创建了 MessageDigest 实例之后,你就可以使用它来计算数据的消息摘要了,如果你有单个数据块来计算消息摘要,请使用 digest()
方法,以下是从单个数据块计算消息摘要的方式:
ini
byte[] data1 = "0123456789".getBytes("UTF-8");
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
byte[] digest = messageDigest.digest(data1);
如果你有多个数据块要包含在同一个消息摘要中,请先调用 update()
方法并在最后调用 digest()
方法,以下是从多个数据块计算消息摘要的方式:
ini
byte[] data1 = "0123456789".getBytes("UTF-8");
byte[] data2 = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8");
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(data1);
messageDigest.update(data2);
byte[] digest = messageDigest.digest();
Mac
javax.crypto.Mac 类可以从二进制数据中创建 Message Authentication Code,一个 Mac 是一个使用安全密钥加密之后的消息摘要,只有当你有用密钥时,才能验证 MAC。
创建 Mac 实例
在你使用 Mac 类之前你必须先创建 Mac 实例,创建 Mac 实例可以使用 getInstance() 静态方法,示例如下:
ini
Mac mac = Mac.getInstance("HmacSHA256");
传递给 getInstance() 方法的字符串参数包含了要使用的 MAC 算法的名字,在这个例子中是 HmacSHA256。
初始化 Mac
在创建 Mac 实例之后,你需要初始化,初始化 Mac 实例是通过调用 init() 方法并传入安全密钥作为参数,如下所示:
ini
byte[] keyBytes = new byte[]{0,1,2,3,4,5,6,7,8 ,9,10,11,12,13,14,15};
String algorithm = "RawBytes";
SecretKeySpec key = new SecretKeySpec(keyBytes, algorithm);
mac.init(key);
init() 方法接受一个 Key 实例,在这个例子中是 SecreKeySpec,它是 Key 接口的实现类。
计算 Mac
初始化之后你就可以计算 Mac 的值了,计算 Mac 的值你需要调用 update() 或者 doFinal() 方法,如果你只有一个数据块需要计算,你可以直接使用 doFinal() 方法,如下所示:
ini
byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8");
byte[] macBytes = mac.doFinal(data);
如果你有多个数据块要计算,那么你必须给每个数据块调用 update() 方法,并在最后调用 doFinal() 方法,示例如下:
ini
byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8");
byte[] data2 = "0123456789".getBytes("UTF-8");
mac.update(data);
mac.update(data2);
byte[] macBytes = mac.doFinal();
Signature
java.security.Signature 类可以为二进制数据创建数字签名,数字签名是使用私钥/公钥对中的私钥加密后的消息摘要。任何拥有公钥的人都可以验证该数字签名。
创建 Signature 实例
在使用 Signature 类之前,你必须创建一个 Signature 实例,你可以通过调用静态方法 getInstance()
方法创建一个 Signature 实例,下面是一个创建 Signature 实例的示例:
ini
Signature signature = Signature.getInstance("SHA256WithDSA");
作为参数传递给 getInstance()
方法的字符串是要使用的数字签名算法的名称。
初始化 Signature 实例
创建 Signature 实例后,你需要先对其进行初始化,然后才能使用它。你通过调用 Signature 实例的 init()
方法来初始化实例,这是一个 Signature 初始化示例:
ini
SecureRandom secureRandom = new SecureRandom();
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
KeyPair keyPair = keyPairGenerator.generateKeyPair();
signature.initSign(keyPair.getPrivate(), secureRandom);
如你所见,Signature 实例是使用私钥/公钥对的私钥和一个 SecureRandom 实例初始化的。
创建数字签名
Signature 实例被初始化后,你就可以使用它来创建数字签名。你可以通过调用一次或多次 update()
方法来创建数字签名,并在最后调用 sign()
方法,以下是为二进制数据块创建数字签名的示例:
ini
byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8");
signature.update(data);
byte[] digitalSignature = signature.sign();
验证数字签名
如果要验证其他人创建的数字签名,则必须将 Signature 实例初始化为验证模式而不是签名模式。以下是将 Signature 实例初始化为验证模式的方式:
ini
Signature signature = Signature.getInstance("SHA256WithDSA");
signature.initVerify(keyPair.getPublic());
在上面的示例中,Signature 实例被初始化为验证模式,并将公钥/私钥对的公钥作为参数传递进去了。
一旦初始化为验证模式,你就可以使用 Signature 实例来验证一个数字签名了,如下示例展示了如何验证一个数字签名:
ini
byte[] data2 = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8");
signature2.update(data2);
boolean verified = signature2.verify(digitalSignature);
完整的签名和验证示例如下:
ini
SecureRandom secureRandom = new SecureRandom();
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
KeyPair keyPair = keyPairGenerator.generateKeyPair();
Signature signature = Signature.getInstance("SHA256WithDSA");
signature.initSign(keyPair.getPrivate(), secureRandom);
byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8");
signature.update(data);
byte[] digitalSignature = signature.sign();
Signature signature2 = Signature.getInstance("SHA256WithDSA");
signature2.initVerify(keyPair.getPublic());
signature2.update(data);
boolean verified = signature2.verify(digitalSignature);
System.out.println("verified = " + verified);
KeyGenerator
javax.crypto.KeyGenerator 被用于生成对称加密密钥,对称加密密钥是一种密钥,对称加密算法可以使用它来加密和解密数据。
创建 KeyGenerator 实例
在使用 KeyGenerator 类之前,你必须先创建一个 KeyGenerator 实例,你可以通过调用静态方法 getInstance()
并传递创建密钥的加密算法的名称来做到这一点,以下是创建 KeyGenerator 实例的示例:
ini
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
此示例创建一个 KeyGenerator 实例,该实例可以通过 AES 加密算法生成密钥。
初始化 KeyGenerator
创建 KeyGenerator 实例后,你必须对其进行初始化,初始化一个 KeyGenerator 实例是通过调用它的 init()
方法来完成的,这是初始化 KeyGenerator 实例的示例:
ini
SecureRandom secureRandom = new SecureRandom();
int keyBitSize = 256;
keyGenerator.init(keyBitSize, secureRandom);
KeyGenerator 的 init() 方法接收两个参数:要生成的密钥的 bit 位是多少,以及一个 SecureRandom 实例。
生成密钥
初始化 KeyGenerator 实例后,你可以使用它来生成密钥。生成密钥是通过调用 KeyGenerator.generateKey()
方法完成的。以下是生成对称密钥的示例:
ini
SecretKey secretKey = keyGenerator.generateKey();
KeyPair
java.security.KeyPair 代表一个非对称密钥对,换句话说,就是一个公钥私钥对,KeyPair 实例通常在执行非对称加密时使用,例如对数据进行加密或者签名的时候。
获取 KeyPair 实例
你通常会从 Java KeyStore 或 Java KeyPairGenerator 获取一个 KeyPair 实例。
访问 KeyPair 中的 Public Key
你可以通过调用 KeyPair.getPublic()
方法来访问一个 PulicKey,实例如下:
ini
PublicKey publicKey = keyPair.getPublic();
访问 KeyPair 中的 Private Key
你可以通过调用 KeyPair 的 getPrivate() 方法来访问 PrivateKey,示例如下:
ini
PrivateKey privateKey = keyPair.getPrivate();
KeyPairGenerator
java.security.KeyPairGenerator 类用于生成非对称加密/解密密钥对。非对称密钥对由两个密钥组成。第一个密钥通常用于加密数据,第二个密钥用于解密第一个密钥加密的数据。
最广为人知的非对称密钥对类型是公钥、私钥类型的密钥对。私钥用于加密数据,公钥用于解密数据。实际上,你也可以使用公钥加密数据并使用私钥解密。
私钥通常是保密的,公钥通常是公开的。
创建 KeyPairGenerator 实例
要使用 KeyPairGenerator,你必须首先创建一个 KeyPairGenerator 实例,创建 KeyPairGenerator 实例是通过调用 getInstance()
方法完成的,以下是创建 KeyPairGenerator 实例的示例:
ini
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
getInstance() 方法通过接收加密算法的名称作为参数来生成 KeyPairGenerator,在此例中,我们使用 RSA 加密算法。
初始化 KeyPairGenerator
根据生成密钥对的算法,你可能必须初始化 KeyPairGenerator 实例,初始化 KeyPairGenerator 是通过调用它的 initialize()
方法来完成的,下面是初始化一个 KeyPairGenerator 实例的例子:
ini
keyPairGenerator.initialize(2048);
此示例初始化 KeyPairGenerator 以生成大小为 2048 位的密钥。
生成 KeyPair
要使用一个 KeyPairGenerator 生成一个 KeyPair,你可以调用 generatorKeyPair()
方法,下面是一个生成 KeyPair 的示例:
ini
KeyPair keyPair = keyPairGenerator.generateKeyPair();
KeyStore
Java KeyStore 是一个可以存储密钥的数据库,一个 Java KeyStore 在 Java 中的抽象是 java.security.KeyStore 类。一个 KeyStore 可以被写入磁盘并再次读取,KeyStore 作为一个整体可以用密钥保护,并且 KeyStore 中的每个 key entry 可以有自己的密码来保护自己。这使得 KeyStore 成为安全处理加密密钥的有用机制。
一个 KeyStore 可以持有以下类型的密钥:
- Private keys
- Public keys + certificates
- Secret keys
私钥和公钥用于非对称加密,公钥可以有关联的证书,证书是一个用于验证持有公钥的个人、组织和设备身份的文档。证书通常由验证方进行数字签名作为证明。
安全密钥用于非对称加密,在大部分场景中,在一个安全连接被建立的时候会商定非对称密钥,因此你可能使用 KeyStore 存储公钥和私钥的时间存储安全密钥的时间多。
创建 KeyStore
你可以通过调用 KeyStore.getInstance()
方法来创建 KeyStore,以下是创建 KeyStore 的示例:
ini
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
此示例创建默认类型的 KeyStore 实例,也可以通过将不同的参数传递给 getInstance()
方法来创建其他类型的实例,例如,这是一个创建 PKCS12 类型的示例:
ini
KeyStore keyStore = KeyStore.getInstance("PKCS12");
加载 KeyStore
在 KeyStore 实例可以使用之前,必须先加载它。KeyStore 实例通常被写入磁盘或其他类型的存储以备后用。这就是为什么 KeyStore 假定你必须先读入它的数据才能使用它的原因。但是,可以初始化一个没有数据的空 KeyStore 实例。
从文件或其他存储中加载 KeyStore 数据是通过调用 KeyStore.load()
方法来完成的,该方法有两个参数:
- 加载数据的 InputStream
- 包含 KeyStore 密码的 char[]
这是加载 KeyStore 的示例:
ini
char[] keyStorePassword = "123abc".toCharArray();
try(InputStream keyStoreData = new FileInputStream("keystore.ks")){
keyStore.load(keyStoreData, keyStorePassword);
}
此示例加载位于 keystore.ks 文件中的 KeyStore。
如果你不想将任何数据加载到 KeyStore 中,只需给 InputStream 参数传递 null 即可,下例示例加载一个空的 KeyStore:
csharp
keyStore3.load(null, keyStorePassword);
你必须始终加载 KeyStore 实例,无论是有数据的 KeyStore 还是空的 KeyStore,否则 KeyStore 是未初始化的,所有对其方法的调用都将抛出异常。
获取 Keys
你可以通过调用 getEntry()
方法来获取 KeyStore 中的密钥,一个 KeyStore Entry 映射一个密钥的别名,每个密钥被自己的密钥保护。因此要访问密钥,你必须将密钥的别名和密钥的密码传递给 getEntry()
方法,下面是访问 KeyStore 实例中的 key entry 的示例:
ini
char[] keyPassword = "789xyz".toCharArray();
KeyStore.ProtectionParameter entryPassword =
new KeyStore.PasswordProtection(keyPassword);
KeyStore.Entry keyEntry = keyStore3.getEntry("keyAlias", entryPassword);
如果你知道要访问的密钥是一个私钥,则可以将 KeyStore.Entry 实例转换为 KeyStore.PrivateKeyEntry,示例如下:
ini
KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)
keyStore3.getEntry("keyAlias", entryPassword);
转换成 KeyStore.PrivateKeyEntry 之后,通过以下方法你能够访问私钥,证书和证书链。
- getPrivateKey()
- getCertificate()
- getCertificateChain()
设置 Keys
你也可以给 KeyStore 实例设置密钥,示例如下:
ini
SecretKey secretKey = getSecretKey();
KeyStore.SecretKeyEntry secretKeyEntry = new KeyStore.SecretKeyEntry(secretKey);
keyStore3.setEntry("keyAlias2", secretKeyEntry, entryPassword);
存储 KeyStore
有时,你可能想要存储一个 KeyStore 到存储器中(磁盘、数据库等),用于在某个时间再次加载它。你可以通过 store()
方法存储一个 KeyStore,示例如下:
ini
char[] keyStorePassword = "123abc".toCharArray();
try (FileOutputStream keyStoreOutputStream = new FileOutputStream("data/keystore.ks")) {
keyStore3.store(keyStoreOutputStream, keyStorePassword);
}