我有一个辅助类 AES 加密/解密字符串,它主要是 Baeldung AES 加密示例
中提供的代码的克隆代码如下:
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Security;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public class CryptoHelper {
private static final String CIPHER_ALGORITM_NAME = "AES/CBC/PKCS5Padding";
private static final String HASHING_ALGO_NAME = "PBKDF2WithHmacSHA1";
private static final int KEY_TARGET_LENGTH = 256;
private static final int HASHING_ITERATIONS = 65536;
public static SecretKey getSecretKey(String string, byte[] salt)
throws NoSuchAlgorithmException, InvalidKeySpecException {
KeySpec spec = new PBEKeySpec(string.toCharArray(), salt, HASHING_ITERATIONS, KEY_TARGET_LENGTH);
try {
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(HASHING_ALGO_NAME);
SecretKey encryptedPassword = new SecretKeySpec(keyFactory.generateSecret(spec).getEncoded(), "AES");
return encryptedPassword;
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw e;
}
}
public static IvParameterSpec getInitializationVector() {
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
return new IvParameterSpec(iv);
}
public static String encrypt(String input, String password, byte[] salt)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException,
InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITM_NAME);
SecretKey secretKey = null;
try {
secretKey = getSecretKey(password, salt);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
// code
}
cipher.init(Cipher.ENCRYPT_MODE, secretKey, getInitializationVector());
byte[] cipherText = cipher.doFinal(input.getBytes());
return Base64.getEncoder().encodeToString(cipherText);
}
public static String decrypt(String encryptedText, String password, byte[] salt)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException,
InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITM_NAME);
SecretKey secretKey = null;
try {
secretKey = getSecretKey(password, salt);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
// code
}
cipher.init(Cipher.DECRYPT_MODE, secretKey, getInitializationVector());
byte[] plainText = cipher.doFinal(Base64.getDecoder().decode(encryptedText));
return new String(plainText);
}
}
现在我用单元测试来测试它,但是这个测试失败了
javax.crypto.BadPaddingException:给定的最终块不正确 衬垫。如果在解密过程中使用错误的密钥,可能会出现此类问题。
@Test
public void testDecrypt() {
String encryptedString = "";
String password = "password";
try {
encryptedString = CryptoHelper.encrypt("some string", password, password.getBytes());
} catch (InvalidKeyException | NoSuchPaddingException | NoSuchAlgorithmException
| InvalidAlgorithmParameterException | BadPaddingException | IllegalBlockSizeException e) {
e.printStackTrace();
assertNull(e);
}
try {
CryptoHelper.decrypt(encryptedString, password, password.getBytes());
} catch (InvalidKeyException | NoSuchPaddingException | NoSuchAlgorithmException
| InvalidAlgorithmParameterException | BadPaddingException | IllegalBlockSizeException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
这里出了什么问题?
这是我解决此问题的尝试,并消除了我在您的代码中注意到的其他安全问题。很明显,您只是复制了代码,而没有理解其背后的基本概念。
首先,您需要区分加密和散列。
在密码学领域,散列的用途之一是以非明文的形式存储用户密码。这是可能的,因为哈希函数是单向函数,这意味着您无法从哈希派生密码。通常在散列时,会使用密码的盐。盐应该是随机的数据位,对于您创建的每个哈希都是唯一的。此外,对于盐的大小也有安全指南。我认为根据 NIST,当前建议的盐长度应至少为 32 位。
hashing_function(input, salt) -> hashed_input
使用对称加密时,您需要选择算法、模式和填充模式以及唯一的 IV 值。在您的代码中,您选择了具有 CBC 模式和 PKCS5 填充的 AES 算法。如果您重复使用 IV,您就会面临比使用唯一 IV 时更容易执行的攻击。每次加密新序列时,都应该使用新的 IV 值。例如,在加密一个文件或消息时,您将使用一个唯一的 IV 值,但一旦您想要加密另一个文件或消息,您就应该使用新的 IV 值。散列和加密之间的主要区别在于,加密不是单向函数,这意味着使用正确的密钥和 iv,您可以解密加密的数据。
k - encryption key
iv - initialization vector
p - plaintext
c - ciphertext
E - encryption function
D - decryption function
=> E(p, k, iv) = c
=> D(c, k, iv) = p
您对密码进行哈希处理的原因是因为您想将文本密码转换为加密密钥。这称为键拉伸。您正在使用 PBKDF2 算法和 HMAC-SHA1,迭代次数为 65,536 次。我认为当前 NIST 建议使用 HMAC-SHA256 进行超过 70,000 次迭代。请记住,这是一个建议,算法、迭代和其他参数的组合取决于您所需的安全级别。
在此示例中,单元测试失败的原因是您使用不同的 IV 值进行加密和解密。一旦你解决了这个问题,你的测试就会通过。但是,在您的代码中,用于密码散列然后用作对称加密密钥的盐值不正确。盐应该是唯一的,并且在您截取的代码中,前提是您使用自己的密码作为盐来对密码进行哈希处理。本质上,你有这个:
hashing_function(input, input) -> hashed_input
,在这种情况下输入的是你的密码。
我会通过添加另一个名为
getSalt()
的方法来解决这个问题,并且我会通过向方法参数添加 IV 来更新加密和解密方法。
public static byte[] getSalt() {
byte[] salt = new byte[32];
new SecureRandom().nextBytes(salt);
return salt;
}
public static String encrypt(String input, String password, byte[] salt, IvParameterSpec iv)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException,
InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITM_NAME);
SecretKey secretKey = null;
try {
secretKey = getSecretKey(password, salt);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
// code
}
cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
byte[] cipherText = cipher.doFinal(input.getBytes());
return Base64.getEncoder().encodeToString(cipherText);
}
public static String decrypt(String encryptedText, String password, byte[] salt, IvParameterSpec iv)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException,
InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITM_NAME);
SecretKey secretKey = null;
try {
secretKey = getSecretKey(password, salt);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
// code
}
cipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
byte[] plainText = cipher.doFinal(Base64.getDecoder().decode(encryptedText));
return new String(plainText);
}
然后我会使用/测试这样的代码:
@Test
public void testDecrypt() {
String encryptedString;
String decryptedString;
String plaintext = "some string";
String password = "password";
byte[] salt = CryptoHelper.getSalt();
IvParameterSpec iv = CryptoHelper.getInitializationVector();
encryptedString = CryptoHelper.encrypt(plaintext, password, salt, iv);
decryptedString = CryptoHelper.decrypt(encryptedString, password, salt, iv);
assertEquals(plaintext,decryptedString)
}
最后,我会考虑从测试代码中删除 try-catch,因为它在当前形式下不提供任何值。如果正在测试的代码抛出异常,那么测试将失败,这通常是您希望发生的情况。一种例外是当您的测试想要引发异常时。
在Topaco的评论的帮助下解决了上述问题。而不是每次运行方法时选择一个新的随机数:
public static IvParameterSpec getInitializationVector() {
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
return new IvParameterSpec(iv);
}
我现在使用硬编码字节数组:
public static IvParameterSpec getInitializationVector() {
byte[] iv = {1, 2, 3, ... 16};
return new IvParameterSpec(iv);
}
请注意,这个解决方案并不完全安全,因为 IV 是硬编码的并且可以从源代码中提取。但是,我选择不将其传递给外部,因为我的应用程序不是很关键。我的一个正确的解决方案是,随机数需要与 IvParameterSpec 一起返回。