2019年12月12日更新
与CBC等其他模式不同,GCM模式不需要IV是不可预测的。唯一的要求是,对于具有给定密钥的每次调用,IV必须是唯一的。如果针对给定的密钥重复一次,则可能会损害安全性。一种简单的实现方法是使用来自强伪随机数生成器的随机IV,如下所示。
也可以将序列或时间戳记用作IV,但听起来可能不那么琐碎。例如,如果系统未正确跟踪持久存储中已用作IV的序列,则调用可能会在系统重新引导后重复IV。同样,也没有完美的时钟。电脑时钟重新调整等。
此外,每2 ^ 32次调用后应旋转一次键。有关IV要求的更多详细信息,请参考此答案和NIST建议。
考虑到以下几点,这是我刚刚在Java 8中编写的加密和解密代码。希望有人会发现这个有用:
加密算法:具有256位密钥的分组密码AES被认为足够安全。要加密完整的消息,需要选择一种模式。建议使用经过身份验证的加密(同时提供机密性和完整性)。GCM,CCM和EAX是最常用的经过身份验证的加密模式。GCM通常是首选,并且在为GCM提供专用说明的英特尔体系结构中表现良好。所有这三种模式都是基于CTR(基于计数器)的模式,因此它们不需要填充。因此,它们不容易遭受与填充相关的攻击
GCM需要初始化向量(IV)。IV不是秘密。唯一的要求是它必须是随机的或不可预测的。在Java中,SecuredRandom
该类用于产生加密强度高的伪随机数。可以在该getInstance()
方法中指定伪随机数生成算法。但是,从Java 8开始,推荐的方法是使用getInstanceStrong()
将使用由Java 配置和提供的最强算法的方法。Provider
NIST建议GCM使用96位IV,以提高互操作性,效率和简化设计
为了确保额外的安全性,在下面的实现SecureRandom
中,每产生2 ^ 16个字节的伪随机字节生成后,重新播种
接收者需要知道IV才能解密密文。因此,IV需要与密文一起传输。一些实现将IV作为AD(关联数据)发送,这意味着将在密文和IV上计算身份验证标签。但是,这不是必需的。IV可以简单地在前面加上密文,因为如果IV在传输过程中由于故意的攻击或网络/文件系统错误而被更改,那么身份验证标签的验证将始终失败。
字符串不可用于保存明文消息或密钥,因为字符串是不可变的,因此我们无法在使用后清除它们。这些未清除的字符串随后会在内存中徘徊,并可能显示在堆转储中。出于同样的原因,调用这些加密或解密方法的客户端应在不再需要它们时清除包含消息或密钥的所有变量或数组。
遵循一般建议,没有提供者在代码中被硬编码
最后,为了通过网络或存储进行传输,应使用Base64编码对密钥或密文进行编码。Base64的详细信息可以在这里找到。应该遵循Java 8方法
字节数组可以使用以下方法清除:
Arrays.fill(clearTextMessageByteArray, Byte.MIN_VALUE);
但是,从Java 8开始,没有简单的方法可以清除,SecretKeyspec
并且SecretKey
由于这两个接口的实现似乎尚未实现destroy()
该接口的方法Destroyable
。在下面的代码中,编写了一个单独的方法来清除SecretKeySpec
andSecretKey
使用反射。
密钥应使用以下两种方法之一生成。
请注意,密钥是类似于密码的秘密,但是与供人使用的密码不同,密钥是供加密算法使用的,因此只能使用上述方式生成。
package com.sapbasu.javastudy;
import java.lang.reflect.Field;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class Crypto {
private static final int AUTH_TAG_SIZE = 128; // bits
// NIST recommendation: "For IVs, it is recommended that implementations
// restrict support to the length of 96 bits, to
// promote interoperability, efficiency, and simplicity of design."
private static final int IV_LEN = 12; // bytes
// number of random number bytes generated before re-seeding
private static final double PRNG_RESEED_INTERVAL = Math.pow(2, 16);
private static final String ENCRYPT_ALGO = "AES/GCM/NoPadding";
private static final List<Integer> ALLOWED_KEY_SIZES = Arrays
.asList(new Integer[] {128, 192, 256}); // bits
private static SecureRandom prng;
// Used to keep track of random number bytes generated by PRNG
// (for the purpose of re-seeding)
private static int bytesGenerated = 0;
public byte[] encrypt(byte[] input, SecretKeySpec key) throws Exception {
Objects.requireNonNull(input, "Input message cannot be null");
Objects.requireNonNull(key, "key cannot be null");
if (input.length == 0) {
throw new IllegalArgumentException("Length of message cannot be 0");
}
if (!ALLOWED_KEY_SIZES.contains(key.getEncoded().length * 8)) {
throw new IllegalArgumentException("Size of key must be 128, 192 or 256");
}
Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO);
byte[] iv = getIV(IV_LEN);
GCMParameterSpec gcmParamSpec = new GCMParameterSpec(AUTH_TAG_SIZE, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, gcmParamSpec);
byte[] messageCipher = cipher.doFinal(input);
// Prepend the IV with the message cipher
byte[] cipherText = new byte[messageCipher.length + IV_LEN];
System.arraycopy(iv, 0, cipherText, 0, IV_LEN);
System.arraycopy(messageCipher, 0, cipherText, IV_LEN,
messageCipher.length);
return cipherText;
}
public byte[] decrypt(byte[] input, SecretKeySpec key) throws Exception {
Objects.requireNonNull(input, "Input message cannot be null");
Objects.requireNonNull(key, "key cannot be null");
if (input.length == 0) {
throw new IllegalArgumentException("Input array cannot be empty");
}
byte[] iv = new byte[IV_LEN];
System.arraycopy(input, 0, iv, 0, IV_LEN);
byte[] messageCipher = new byte[input.length - IV_LEN];
System.arraycopy(input, IV_LEN, messageCipher, 0, input.length - IV_LEN);
GCMParameterSpec gcmParamSpec = new GCMParameterSpec(AUTH_TAG_SIZE, iv);
Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO);
cipher.init(Cipher.DECRYPT_MODE, key, gcmParamSpec);
return cipher.doFinal(messageCipher);
}
public byte[] getIV(int bytesNum) {
if (bytesNum < 1) throw new IllegalArgumentException(
"Number of bytes must be greater than 0");
byte[] iv = new byte[bytesNum];
prng = Optional.ofNullable(prng).orElseGet(() -> {
try {
prng = SecureRandom.getInstanceStrong();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Wrong algorithm name", e);
}
return prng;
});
if (bytesGenerated > PRNG_RESEED_INTERVAL || bytesGenerated == 0) {
prng.setSeed(prng.generateSeed(bytesNum));
bytesGenerated = 0;
}
prng.nextBytes(iv);
bytesGenerated = bytesGenerated + bytesNum;
return iv;
}
private static void clearSecret(Destroyable key)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, SecurityException {
Field keyField = key.getClass().getDeclaredField("key");
keyField.setAccessible(true);
byte[] encodedKey = (byte[]) keyField.get(key);
Arrays.fill(encodedKey, Byte.MIN_VALUE);
}
}
加密密钥主要可以通过两种方式生成:
没有任何密码
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(KEY_LEN, SecureRandom.getInstanceStrong());
SecretKey secretKey = keyGen.generateKey();
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(),
"AES");
Crypto.clearSecret(secretKey);
// After encryption or decryption with key
Crypto.clearSecret(secretKeySpec);
带密码
SecureRandom random = SecureRandom.getInstanceStrong();
byte[] salt = new byte[32];
random.nextBytes(salt);
PBEKeySpec keySpec = new PBEKeySpec(password, salt, iterations,
keyLength);
SecretKeyFactory keyFactory =
SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
SecretKey secretKey = keyFactory.generateSecret(keySpec);
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(),
"AES");
Crypto.clearSecret(secretKey);
// After encryption or decryption with key
Crypto.clearSecret(secretKeySpec);
根据评论更新
正如@MaartenBodewes指出的那样,我的回答没有String
按照问题的要求进行处理。因此,我会尽力弥补这一空白,以防万一有人偶然发现了这个答案而对处理感到疑惑String
。
如答案前面所述,在a中处理敏感信息String
通常不是一个好主意,因为它String
是不可变的,因此我们在使用后无法清除它。并且我们知道,即使当a String
没有强引用时,垃圾收集器也不会立即急于将其从堆中删除。就这样String
即使程序无法访问持续时间持续时间仍在内存中存在一个未知的时间窗口。问题在于,在该时间段内进行堆转储会泄露敏感信息。因此,始终最好处理字节数组或char数组中的所有敏感信息,然后在达到目的后将其填充为0。
但是,尽管有了这些知识,但是如果我们仍然遇到要加密的敏感信息位于的情况String
,我们首先需要将其转换为字节数组,然后调用上面介绍的encrypt
和decrypt
函数。(可以使用上面提供的代码段生成另一个输入键)。
String
可以通过以下方式将A 转换为字节:
byte[] inputBytes = inputString.getBytes(StandardCharsets.UTF_8);
从Java 8开始,String
内部通过UTF-16
编码存储在堆中。但是,我们UTF-8
这里使用的是因为它通常比占用更少的空间UTF-16
,尤其是对于ASCII字符。
同样,加密的字节数组也可以转换为字符串,如下所示:
String encryptedString = new String(encryptedBytes, StandardCharsets.UTF_8);