Java 加密/解密简介
介绍密码学
密码学是一种使用代码和数字密钥保护数据和通信的方法,以确保信息以未篡改的方式传递给预期发送者以供进一步处理。
了解密码学的基本概念,例如加密和解密,对于开发人员至关重要,因为您可能会发现自己正在处理与以下内容相关的功能
- 数字签名:数字签名是一种密码学方法,通过它可以验证文档的来源、发送者的身份、文档签署或发送的时间和日期等。数字签名充当信息上的加密身份验证戳。
- 电子交易:在电子货币系统中使用加密可以保护传统的交易数据,例如帐户详细信息和交易金额。数字签名可以替代手写签名或信用卡授权,而公钥加密可以提供机密性。
- 电子邮件系统中的加密/解密。
- 时间戳以证明特定电子文档在特定时间存在或已交付。处理包含高度敏感信息的电子合同或档案是有效的现实世界示例。
由于密码学使用数据,因此数据可以是明文(纯文本)或密文(密码)。明文数据意味着消息以自然格式,攻击者可以阅读。密文数据意味着消息以攻击者无法阅读但预期接收者可以阅读的格式。
您可以使用加密过程将消息从明文转换为密文。类似地,您可以通过使用用于创建原始消息的加密算法和密钥来通过解密将密文转换为明文。通常,加密或解密过程基于公开可用的算法,但对数据的控制是通过安全的密钥获得的。
您可以使用哈希函数将任意大小的字节集映射到有限大小的相对唯一的字节集。精心设计的密码哈希函数应使用盐,即与密钥或密码连接的随机(或伪随机)位字符串。您可以通过使用初始化向量 (IV) 对明文块序列进行加密来引入额外的密码学差异,从而提高安全性。
注意:本文中提供的代码片段旨在说明 Java API 在高级别的工作方式。为了清晰起见,它们有时会被简化。安全性可能是一个复杂的话题,并且始终针对您的特定需求,因此您应始终咨询您的安全专家以了解您的特定要求。
JDK 中可用的密码学标准
Java 密码学基于定义明确的国际标准,这些标准允许各种平台运行。这些标准包括
- TLS (传输层安全) v1.2、v1.3 – RFC 5246、RFC 8446
- RSA 密码学规范 PKCS #1 – RFC 8017
- 密码令牌接口标准 (PKCS#11)
- 如ANSI X9.62 等中定义的 ECDSA 签名算法。
安全环境不断发展,例如,引入了更强大的算法,而旧的算法被认为安全性较低。Oracle JDK 定期更新以应对这些变化并保持 Java 平台的安全性。 Oracle JDK 密码学路线图 反映了 Oracle 在 Oracle JDK 中提供的安全提供程序中应用的最新和即将发生的更改。
Java 密码学体系结构 (JCA) 是使用 Java 编程语言进行密码学操作的框架,是 Java 安全 API 的一部分。其目标是提供密码学算法独立性和可扩展性、互操作性以及与安全提供程序无关的实现。
JCA 包含引擎类,这些类通过以下方式与特定类型的密码学服务交互
- 加密、数字签名、消息摘要等密码学操作
- 密钥和算法参数
- 密钥库或证书,它们封装了密码学数据,可以在更高抽象层使用。
JDK 包含一系列提供程序的实际密码学实现,例如 Sun
、SunRSASign
、SunJCE
等。要使用 JCA,应用程序会请求特定类型的对象(例如 MessageDigest)和特定算法或服务(例如 SHA-256
算法),并从已安装的提供程序之一获取实现。或者,您可以从特定提供程序请求对象(例如,来自下图的 ProviderC
)。
图 1:ProviderC 的请求对象 provider.MessageDigest.getInstance("SHA-256", "ProviderC")
来源:Java 安全概述
如果您想获取已安装提供程序的列表,只需调用 java.security.Security.getProviders()
。您可以在 Jshell 中复制以下代码片段以打印 JDK 中找到的每个提供程序的可用密码学算法列表
jshell>
...> import java.security.Security;
...> import java.util.Set;
...> import java.util.TreeSet;
...>
...> Set<String> algos = new TreeSet<>();
...> for (Provider provider : Security.getProviders()){
...> Set <Provider.Service> service = provider.getServices();
...> service.stream().map(Provider.Service::getAlgorithm).forEach(algos::add);
...> }
...> algos.forEach(System.out::println);
...>
algos ==> []
1.2.840.113554.1.2.2
1.3.6.1.5.5.2
AES
AES/GCM/NoPadding
AES/KW/NoPadding
AES/KW/PKCS5Padding
AES/KWP/NoPadding
AES_128/OFB/NoPadding
AES_192/CBC/NoPadding
// list was truncated for display purposes
一些流行的提供程序示例包括:SunPKCS11
、SunMSCAPI (Windows)
、BouncyCastle
、RSA JSAFE
、SafeNet
。如果您要使用的提供程序不在打印的列表中,您也可以按照以下步骤注册它
- 将提供程序类放在
CLASSPATH
上。 - 注册提供程序,方法是
- 静态地修改 conf/security/java.security 配置文件,例如
security.provider.5=SunJCEII
。请注意,在 JDK 8 中,java.security
文件位于 java.home/lib/security/java.security 中。 - 动态地调用
Security.addProvider(java.security.Provider)
和Security.insertProviderAt(java.security.Provider,int)
。
- 静态地修改 conf/security/java.security 配置文件,例如
- 提供程序的优先级顺序通过简单的数字排序声明。
Java 中的基本加密/解密
在使用数据加密时,您可以使用此安全控制机制来保护三种类型的数据状态
- 静止数据是指不在设备或网络之间主动移动的信息,存储在数据库中或保存在磁盘上。
- 动态数据是指从一个网络点到另一个网络点传输的信息。
- 使用中的数据是指加载到内存中并由用户主动访问和处理的信息。
加密对于所有三种数据状态都很重要,可以提供额外的保护层以防止攻击。加密有两种方法:对称加密和非对称加密。
实现基本对称加密/解密
对称或共享密钥加密是一种方法,其中双方共享一个密钥,该密钥由双方保密。例如,发送者 A
可以使用共享密钥加密消息,然后接收者 B
只能使用该密钥解密加密的消息。
要使用 Java 实现对称加密,您首先需要生成一个共享密钥。您可以使用以下代码片段来完成此操作
public static SecretKey generateKey() throws NoSuchAlgorithmException {
KeyGenerator keygenerator = KeyGenerator.getInstance("AES");
keygenerator.init(128);
return keygenerator.generateKey();
}
在前面的示例中,您首先实例化一个使用 AES
算法的密钥生成器。接下来,您初始化密钥生成器以使用 128 位密钥大小并需要随机字节。从 JDK 19 开始,AES
算法的默认大小已从 128 位增加到 256 位(如果密码策略允许),否则默认值将回退到 128 位。最后,生成一个密钥。
为了增强加密/解密机制,您可以使用任意值初始化一个向量 (IV)
public static IvParameterSpec generateIv() {
byte[] initializationVector = new byte[16];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(initializationVector);
return new IvParameterSpec(initializationVector);
}
由于对称加密将固定长度的明文数据块转换为密文块,因此它可以在块密码中使用多种模式
-
ECB(电子密码本模式)
-
CBC(密码块链接模式)
-
CCM(计数器/CBC 模式)
-
CFB(密码反馈模式)
-
OFB/OFBx(输出反馈)
-
CTR(计数器模式)
-
GCM(伽罗瓦/计数器模式)
-
KW(密钥包装模式)
-
KWP(密钥包装填充模式)
-
PCBC(传播密码块链接)
您可以在 Java 安全标准算法名称规范
的密码部分中检查所有模式和支持的转换。接下来,您需要在加密方法中指定块密码,在获取 Cipher
类的实例时
public static byte[] encrypt(String input, SecretKey key, IvParameterSpec iv)
throws Exception {
Cipher cipher = Cipher.getInstance("AES/CFB8/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key, iv);
return cipher.doFinal(input.getBytes(StandardCharsets.UTF_8));
}
要将密文转换回原始明文,您应该使用相同的块密码、密钥和 IV
public static String decrypt(byte[] cipherText, SecretKey key, IvParameterSpec iv) throws Exception {
Cipher cipher = Cipher.getInstance("AES/CFB8/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, key, iv);
byte[] plainText = cipher.doFinal(cipherText);
return new String(plainText);
}
在密码上调用的 doFinal()
方法在单部分操作中加密或解密数据,或完成多部分操作并返回一个字节数组。因此,让我们将这些方法放在一起以加密和解密消息
public static void main(String[] args) throws Exception {
SecretKey symmetricKey = generateKey();
IvParameterSpec iv = generateIv();
// Takes input from the keyboard
Scanner message = new Scanner(System.in);
String plainText = message.nextLine();
message.close();
// Encrypt the message using the symmetric key
byte[] cipherText = encrypt(plainText, symmetricKey, iv);
System.out.println("The encrypted message is: " + cipherText);
// Decrypt the encrypted message
String decryptedText = decrypt(cipherText, symmetricKey, iv);
System.out.println( "Your original message is: " + decryptedText);
}
如果您需要一种廉价的计算加密方法,对称加密是一个有效的选择,因为它需要创建发送方和接收方都可以使用的短单个密钥(40-512 位)。如果您正在寻找使用不同的、更长的密钥进行加密和解密的选项,请继续阅读有关非对称加密和解密的信息。
实现基本非对称加密/解密
非对称加密使用一对数学相关的密钥,一个用于加密,另一个用于解密。在下面的示例中,Key1
用于加密,Key2
用于解密。
在这样的系统中,A
可以使用接收者 B
的公钥加密消息,但只有 B
拥有的私钥才能解密消息。在一对密钥中,公钥对所有人可见。私钥是秘密密钥,主要用于解密或使用数字签名进行加密。
要在 Java 中实现非对称加密,您首先需要通过获取 KeyPairGenerator
的实例来生成密钥对(公钥、私钥)(在本例中为 RSA 算法)。根据所选算法,KeyPairGenerator
对象使用 3072 位密钥大小和通过 SecureRandom
类初始化的随机数
public static KeyPair generateRSAKKeyPair() throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(3072);
return keyPairGenerator.generateKeyPair();
}
如果您使用的是 JDK 19 或更高版本,您应该注意 RSA
、RSASSA-PSS
和 DH
算法的默认密钥大小已从 2048 位增加到 3072 位。接下来,让我们实现使用公钥将明文转换为密文的加密方法
public static byte[] encrypt(String plainText, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
}
要将密文转换回原始明文,您可以使用私钥
public static String decrypt(byte[] cipherText, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] result = cipher.doFinal(cipherText);
return new String(result);
}
使用之前的方法,您可以编写一个小型程序来模拟非对称加密和解密的工作方式
public static void main(String[] args) throws Exception {
KeyPair keypair = generateRSAKKeyPair();
// takes input from the keyboard
Scanner message = new Scanner(System.in);
System.out.print("Enter the message you want to encrypt using RSA: ");
String plainText = message.nextLine();
message.close();
byte[] cipherText = encrypt(plainText, keypair.getPublic());
System.out.print("The encrypted text is: ");
System.out.println(HexFormat.of().formatHex(cipherText));
String decryptedText = decrypt(cipherText, keypair.getPrivate());
System.out.println("The decrypted text is: " + decryptedText);
}
您可以通过使用 MessageDigest
对消息进行哈希来确保通过不安全通道传输的消息的发送者和完整性。要实现这一点,您应该创建消息的摘要并使用私钥对其进行加密
public static byte[] generateDigitalSignature(byte[] plainText, PrivateKey privateKey) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] messageHash = md.digest(plainText);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
return cipher.doFinal(messageHash);
}
此摘要称为数字签名,只有拥有发送者公钥的接收者才能解密。要验证消息和发送者的真实性,您应该使用公钥
public static boolean verify(byte[] plainText, byte[] digitalSignature, PublicKey publicKey) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hashedMessage = md.digest(plainText);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, publicKey);
byte[] decryptedMessageHash = cipher.doFinal(digitalSignature);
return Arrays.equals(decryptedMessageHash, hashedMessage);
}
您可以在下面找到一个示例调用,它将使用上述方法
public static void main(String[] args) throws Exception{
byte[] digitalSignature = generateDigitalSignature(plainText.getBytes(), keypair.getPrivate());
System.out.println("Signature Value: " + HexFormat.of().formatHex(digitalSignature));
System.out.println("Verification: " + verify(plainText.getBytes(), digitalSignature, keypair.getPublic()));
}
恭喜,您已经了解了 JCA 如何支持在 Java 中使用密码学以及如何使用 Java 安全 API 实现基本的加密和解密机制。
有用链接
更多学习
上次更新: 2023 年 2 月 10 日