9

在阅读了几个关于实现 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,因为它容易受到定时攻击。

4

1 回答 1

5

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

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

如果您将加密数据存储在您无法控制的环境中,您还应该在您的 IV+Ciphertext 上生成一个 MAC,并将其与您的密文一起存储。您可以像存储 IV 一样以明文形式添加它。在解密之前验证 MAC,您应该首先通过散列 MAC 来间接验证(这里使用简单的 SHA1),以免创建定时攻击向量。

诸如 HMACSHA1 之类的 MAC 算法需要类似于密码的密钥。您不应使用相同的密钥来加密和生成 MAC。您可以使用密钥拉伸算法生成足够长的密钥,您可以将一部分用于您的密码,一部分用于您的 MAC。

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

创建安全的加密系统并非易事,有很多方法可以搞砸它并使整个过程不安全。与其通过将各种加密原语组合在一起来创建自己的加密系统,不如使用更高级别的库来处理诸如 cookie 加密和数据存储或传输中数据的 SSL/TLS 之类的事情。

于 2013-10-28T02:50:48.243 回答