java - 具有基于密码的 SecretKeySpec 与 PBE 的 AES

标签 java security encryption cryptography

在阅读了几个关于实现 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 key )?
  • 如果对内容使用 salt,首选什么:一个静态值,或不同的值,但附加/前置到加密结果(就像对 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;
}
}

计划将生成盐安装时间,然后所有加密/解密操作都将保持不变。我假设这应该提供足够好的保护以防止彩虹表攻击。

更新 2:我不得不意识到我的 MAC 验证码并不是最理想的:MAC 已经经过 SHA-1 哈希处理,因此没有必要再创建一个 SHA1 摘要。我还调整了 MAC 验证,因此它不再使用 Arrays.equals,因为它容易受到定时攻击。

最佳答案

在获取用户输入的 key (例如键入的密码)时,您应该始终使用 key 扩展算法。按键拉伸(stretch)有一些好处。首先,它重新分配 key 的熵(SHA1 也这样做),使 key 看起来更随机(实际上并不是更随机,熵保持不变),其次,它使暴力破解 key 的计算量更大(增加显然有迭代)。使用随机盐还排除了使用预先计算的查找表。

您绝对应该为此使用标准算法,例如 PBKDF2 .在 Java 中,您可以通过 SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");

获取 key 工厂

如果您在不受您控制的环境中存储加密数据,您还应该在您的 IV+Ciphertext 上生成一个 MAC,并将其与您的密文一起存储。您可以像存储 IV 一样以明文形式添加它。在解密之前验证 MAC,您应该首先通过对 MAC 进行哈希处理来间接验证(此处使用简单的 SHA1),以免创建时序攻击 vector 。

HMACSHA1 等 MAC 算法需要类似于密码的 key 。您不应使用相同的 key 来加密和生成 MAC。您可以使用 key 拉伸(stretch)算法生成足够长的 key ,您可以将一部分用于密码,另一部分用于 MAC。

附录: 如果您使用的是 Java 7(或支持它的外部 JCA 提供程序),请使用 GCM 模式在 AES 密码中包含一个 MAC。 GCM 模式下的 AES 是一种经过身份验证的加密形式,可将完整性作为密码的一部分进行验证。实现 MAC 生成和验证有各种需要避免的陷阱(例如我提到的定时攻击或使用单独的 key )并且将其滚动到密码中是一件不容易搞砸的事情。

创建安全的加密系统不是一项简单的任务,有很多方法可以搞砸它并使整个过程不安全。与其通过将各种加密原语放在一起来创建您自己的加密系统,通常更好的做法是使用更高级别的库来处理 cookie 加密和数据存储或传输中数据的 SSL/TLS。

关于java - 具有基于密码的 SecretKeySpec 与 PBE 的 AES,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/19626080/

相关文章:

security - 如何搜索log4j漏洞?

node.js - 我可以从 jwt 键中删除除第一个和最后一个以外的所有换行符吗

c - C语言维吉尼亚密码的几个问题

java - Android 中使用 RSA 解密 AES key

java - Java中根据出现次数分割字符串

java - jackson : How to serialize into an unnamed array

c - 导致安全漏洞的缓冲区溢出示例

c# - 我需要开发敏感数据传输/存储/加密系统的建议

Java 关于 <E> in public static <E> void

java - Spring Boot - 不允许使用 POST 方法