Cipher类指南

2023/07/02

1. 概述

简而言之,加密是对消息进行编码的过程,只有授权用户才能理解或访问它。

该消息称为明文,使用加密算法(cipher)进行加密,生成的密文只能由授权用户通过解密读取。

在本文中,我们将详细介绍核心Cipher类,它在Java中提供密码加密和解密功能

2. Cipher类

Java Cryptography Extension(JCE)是Java Cryptography Architecture(JCA)的一部分,它为应用程序提供用于数据加密和解密以及私有数据散列的加密算法。

位于javax.crypto包中的Cipher类构成了JCE框架的核心,提供加密和解密的功能。

2.1 Cipher实例化

要实例化Cipher对象,我们调用静态getInstance方法,传递所请求转换的名称。可选地,可以指定提供者的名称。

让我们编写一个示例类来说明Cipher的实例化:

public class Encryptor {

    public byte[] encryptMessage(byte[] message, byte[] keyBytes) throws InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException {
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        // ...
    }
}

转换AES/ECB/PKCS5Padding告诉getInstance方法将Cipher对象实例化为具有ECB操作模式和PKCS5填充方案AES密码。

我们还可以通过在转换中仅指定算法来实例化Cipher对象:

Cipher cipher = Cipher.getInstance("AES");

在这种情况下,Java将为模式和填充方案使用特定于提供者的默认值。

请注意,如果转换为null、空或格式无效,或者提供者不支持它,则getInstance将抛出NoSuchAlgorithmException。

如果转换包含不受支持的填充方案,它将抛出NoSuchPaddingException。

2.2 线程安全

Cipher类是有状态的,没有任何形式的内部同步。事实上,像init()update()这样的方法会改变特定Cipher实例的内部状态。

因此,Cipher类不是线程安全的。所以我们应该为每个加密/解密需要创建一个Cipher实例。

2.3 Key

Key接口表示加密操作的密钥,密钥是不透明的容器,其中包含编码密钥、密钥的编码格式及其加密算法。

密钥通常是通过密钥生成器、证书或使用密钥工厂密钥规范获得的。

让我们从提供的密钥字节创建一个对称Key:

SecretKey secretKey = new SecretKeySpec(keyBytes, "AES");

2.4 Cipher初始化

我们调用init()方法使用密钥证书和指示密码操作模式的opmode来初始化Cipher对象

或者,我们可以传入一个随机源。默认情况下,使用最高优先级已安装提供程序的SecureRandom实现。否则,它将使用系统提供的源。

我们可以选择性地指定一组特定于算法的参数。例如,我们可以传递一个IvParameterSpec来指定一个初始化向量

以下是可用的密码操作模式:

  • ENCRYPT_MODE:将cipher对象初始化为加密模式
  • DECRYPT_MODE:将cipher对象初始化为解密模式
  • WRAP_MODE:将cipher对象初始化为密钥包装模式
  • UNWRAP_MODE:将cipher对象初始化为密钥解包模式

让我们初始化Cipher对象:

Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKey secretKey = new SecretKeySpec(keyBytes, "AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
// ...

现在,如果提供的密钥不适合初始化Cipher,例如当密钥长度/编码无效时,init方法将抛出InvalidKeyException。

当Cipher需要某些无法从密钥确定的算法参数时,或者如果密钥的密钥大小超过最大允许密钥大小(由配置的JCE权限策略文件确定),也会抛出此错误。

让我们看一个使用Certificate的例子:

public byte[] encryptMessage(byte[] message, Certificate certificate) throws InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException {
    Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
    cipher.init(Cipher.ENCRYPT_MODE, certificate);
    // ...
}

Cipher对象通过调用getPublicKey方法从证书中获取用于数据加密的公钥。

2.5 加密和解密

初始化Cipher对象后,我们调用doFinal()方法执行加密或解密操作。此方法返回包含加密或解密消息的字节数组。

doFinal()方法还将Cipher对象重置为之前通过调用init()方法初始化时所处的状态,使Cipher对象可用于加密或解密其他消息。

让我们在encryptMessage方法中调用doFinal:

public byte[] encryptMessage(byte[] message, byte[] keyBytes) throws InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException, 
    BadPaddingException, IllegalBlockSizeException {
 
    Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    SecretKey secretKey = new SecretKeySpec(keyBytes, "AES");
    cipher.init(Cipher.ENCRYPT_MODE, secretKey);
    return cipher.doFinal(message);
}

要执行解密操作,我们将opmode更改为DECRYPT_MODE:

public byte[] decryptMessage(byte[] encryptedMessage, byte[] keyBytes) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, 
    BadPaddingException, IllegalBlockSizeException {
 
    Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    SecretKey secretKey = new SecretKeySpec(keyBytes, "AES");
    cipher.init(Cipher.DECRYPT_MODE, secretKey);
    return cipher.doFinal(encryptedMessage);
}

2.6 提供者

JCE旨在使用基于提供者的架构允许将合格的加密库(例如BouncyCastle)作为安全提供程序插入,并允许无缝添加新算法

现在让我们将BouncyCastle添加为安全提供程序,我们可以静态或动态地添加安全提供程序

要静态添加BouncyCastle,我们修改位于<JAVA_HOME>/jre/lib/security文件夹中的java.security文件

在列表的末尾添加行:

...
security.provider.4=com.sun.net.ssl.internal.ssl.Provider
security.provider.5=com.sun.crypto.provider.SunJCE
security.provider.6=sun.security.jgss.SunProvider
security.provider.7=org.bouncycastle.jce.provider.BouncyCastleProvider

添加提供程序属性时,属性键的格式为security.provider.N,其中数字N比列表中的最后一个多1。

我们还可以动态添加BouncyCastle安全提供程序,而无需修改安全文件:

Security.addProvider(new BouncyCastleProvider());

现在,我们可以在Cipher初始化期间指定提供者:

Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding", "BC");

BC指定BouncyCastle作为提供者,我们可以通过Security.getProviders()方法获取已注册提供程序的列表。

3. 测试加解密

让我们编写一个示例测试来说明消息加密和解密。

在这个测试中,我们使用带有128位密钥的AES加密算法并断言解密结果等于原始消息文本:

@Test
public void whenIsEncryptedAndDecrypted_thenDecryptedEqualsOriginal() throws Exception {
    String encryptionKeyString =  "thisisa128bitkey";
    String originalMessage = "This is a secret message";
    byte[] encryptionKeyBytes = encryptionKeyString.getBytes();

    Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    SecretKey secretKey = new SecretKeySpec(encryptionKeyBytes, "AES");
    cipher.init(Cipher.ENCRYPT_MODE, secretKey);

    byte[] encryptedMessageBytes = cipher.doFinal(message.getBytes());

    cipher.init(Cipher.DECRYPT_MODE, secretKey);

    byte[] decryptedMessageBytes = cipher.doFinal(encryptedMessageBytes);
    assertThat(originalMessage).isEqualTo(new String(decryptedMessageBytes));
}

4. 总结

在本文中,我们讨论了Cipher类并提供了使用示例。有关Cipher类和JCE框架的更多详细信息,请参阅类文档Java Cryptography Architecture(JCA)参考指南

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章