在阅读了几个关于实现 AES 的 stackoverflow 问题后,我想我开始了解基础知识了:
- 每次我都应该生成一个新的 IV
- 使用 PBE 时,迭代次数应在 1000-4000+ 左右
- 由于我无法预测要加密的数据量,我不应该使用 ECB 密码模式
我的环境很简单:
- 密码应该是安全的,它是一个安全随机生成的随机 32 个字符(即不是由用户设置的)。
- 生成的加密内容最终可能会被存储为 cookie,因此它们在某种程度上是公开的
基于这些,我想出了以下Java代码:
public class SecureEncryption {
private static final String CONTENT = "thisneedstobestoredverysecurely";
private static final String PASSPHRASE = "mysuperstrongpassword";
private static final int IV_LENGTH = 16;
public static void main(String[] args) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
byte[] passphrase = digest.digest(PASSPHRASE.getBytes("UTF-8"));
Cipher instance = Cipher.getInstance("AES/CFB/NoPadding");
passphrase = Arrays.copyOf(passphrase, 16);
SecretKeySpec secretKey = new SecretKeySpec(passphrase, "AES");
byte[] iv = new byte[16];
SecureRandom sr = new SecureRandom();
sr.nextBytes(iv);
instance.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
byte[] encrypted = instance.doFinal(CONTENT.getBytes("UTF-8"));
byte[] result = addIVtoEncrypted(iv, encrypted);
System.arraycopy(result, 0, iv, 0, IV_LENGTH);
System.arraycopy(result, IV_LENGTH, encrypted, 0, result.length - IV_LENGTH);
instance.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
byte[] decrypted = instance.doFinal(encrypted);
System.out.println(new String(decrypted, "UTF-8"));
}
private static byte[] addIVtoEncrypted(byte[] iv, byte[] encrypted) {
byte[] ret = new byte[IV_LENGTH + encrypted.length];
System.arraycopy(iv, 0, ret, 0, IV_LENGTH);
System.arraycopy(encrypted, 0, ret, IV_LENGTH, encrypted.length);
return ret;
}
}
虽然这很好用,但我不确定它是否尽可能安全。我现在对以下事情有点迷茫:
- 使用 AES+SHA 的 PBE 是否更安全?盐+迭代计数是否大大增加了安全性?如果是这样,我应该使用哪种 PBE 的确切组合?
- 我是否应该考虑将盐用于可加密内容(而不是用于 PBE 密钥)?
- 如果对内容使用盐,首选什么:一个静态值,或不同的值,但附加/附加到加密结果(就像使用 IV 一样)?
更新:根据这里收到的建议,我重写了我的实现:
public class SecureEncryption {
private static final String CONTENT = "thisneedstobestoredverysecurely";
private static final String PASSPHRASE = "mysuperstrongpassword";
private static final int IV_LENGTH = 16;
private static final int AES_KEY_LENGTH = 16;
private static final int MAC_KEY_LENGTH = 16;
private static final int MAC_LENGTH = 20;
private static final int ITERATION_COUNT = 4096;
private static final String AES = "AES";
private static final String CIPHER_ALGORITHM = "AES/CFB/NoPadding";
private static final String SECRET_KEY_ALGORITHM = "PBKDF2WithHmacSHA1";
private static final String MAC_ALGORITHM = "HmacSHA1";
public static void main(String[] args) throws Exception {
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
SecureRandom sr = new SecureRandom();
byte[] salt = new byte[16];
sr.nextBytes(salt);
SecretKeyFactory factory = SecretKeyFactory.getInstance(SECRET_KEY_ALGORITHM);
SecretKey secretKey = factory.generateSecret(new PBEKeySpec(PASSPHRASE.toCharArray(), salt, ITERATION_COUNT, 256));
byte[] secretBytes = secretKey.getEncoded();
byte[] iv = new byte[16];
sr.nextBytes(iv);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secretBytes, 0, AES_KEY_LENGTH, AES), new IvParameterSpec(iv));
byte[] encrypted = cipher.doFinal(CONTENT.getBytes("UTF-8"));
byte[] result = concatArrays(iv, encrypted);
byte[] macResult = getMAC(secretBytes, result);
result = concatArrays(macResult, result);
System.arraycopy(result, 0, macResult, 0, MAC_LENGTH);
System.arraycopy(result, MAC_LENGTH, iv, 0, IV_LENGTH);
System.arraycopy(result, MAC_LENGTH + IV_LENGTH, encrypted, 0, result.length - IV_LENGTH - MAC_LENGTH);
if (!Arrays.equals(getDigest(getMAC(secretBytes, concatArrays(iv, encrypted))), getDigest(macResult))) {
System.out.println("Invalid MAC");
}
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secretBytes, 0, AES_KEY_LENGTH, AES), new IvParameterSpec(iv));
byte[] decrypted = cipher.doFinal(encrypted);
System.out.println(new String(decrypted, "UTF-8"));
}
private static byte[] getDigest(byte[] mac) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA1");
return digest.digest(mac);
}
private static byte[] getMAC(byte[] secretBytes, byte[] data) throws Exception {
Mac mac = Mac.getInstance(MAC_ALGORITHM);
mac.init(new SecretKeySpec(secretBytes, AES_KEY_LENGTH, MAC_KEY_LENGTH, MAC_ALGORITHM));
return mac.doFinal(data);
}
private static byte[] concatArrays(byte[] first, byte[] second) {
byte[] ret = new byte[first.length + second.length];
System.arraycopy(first, 0, ret, 0, first.length);
System.arraycopy(second, 0, ret, first.length, second.length);
return ret;
}
}
计划将生成 salt 安装时间,然后对于所有加密/解密操作将保持不变。我假设这应该为彩虹表攻击提供足够好的保护。
更新 2:我必须意识到我的 MAC 验证码不是很理想:MAC 已经经过 SHA-1 哈希处理,因此创建另一个 SHA1 摘要毫无意义。我还调整了 MAC 验证,使其不再使用 Arrays.equals,因为它容易受到定时攻击。