【问题标题】:Can i avoid the cipher reinitialization per encrypt/decrypt call when using random salts per encryption?每次加密使用随机盐时,我可以避免每次加密/解密调用的密码重新初始化吗?
【发布时间】:2012-09-13 09:34:27
【问题描述】:

编辑

实际上重新初始化密码并没有那么慢。由于迭代次数,创建密钥本身很慢。

此外,迭代计数被忽略并且从未在加密本身中使用,仅在密钥生成时使用。根据所选择的算法,JCE api 会产生误导

原帖

由于 Java 中的密码学相当...密码学,我正在努力进行一些优化。在功能方面,这个类工作得很好,我希望它可以作为 AES 加密使用的一个例子

我在使用 BouncyCastle 的 AES 实现加密和解密数据时遇到性能问题(我没有比较,这是我测试的唯一一种实现)。实际上这个问题对于我决定使用的任何密码都是通用的。

主要问题是:我可以避免两个密码在每次加密/解密调用时重新初始化吗?太贵了

为了简单起见,请记住,以下代码已删除其异常处理,并进行了大量简化以保持对问题的关注。同步块是为了保证线程安全

顺便说一下,欢迎对代码的任何部分提供反馈

谢谢

import java.nio.charset.Charset;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Arrays;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;

import org.bouncycastle.jce.provider.BouncyCastleProvider;

public class AES {

    private static final int ITERATIONS = 120000;
    private static final int SALT_SIZE_IN_BYTES = 8;
    private static final String algorithm = "PBEWithSHA256And128BitAES-CBC-BC";
    private static final byte[] KEY_SALT = "a fixed key salt".getBytes(Charset.forName("UTF-8"));

    private Cipher encryptCipher;
    private Cipher decryptCipher;
    private SecretKey key;
    private RandomGenerator randomGenerator = new RandomGenerator();

    static {
        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null)
            Security.addProvider(new BouncyCastleProvider());
    }

    public AES(String passphrase) throws Exception {
        encryptCipher = Cipher.getInstance(algorithm);
        decryptCipher = Cipher.getInstance(algorithm);
        PBEKeySpec keySpec = new PBEKeySpec(passphrase.toCharArray(), KEY_SALT, ITERATIONS);
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(algorithm);
        key = keyFactory.generateSecret(keySpec);
    }

    public byte[] encrypt(byte[] data) throws Exception {
        byte[] salt = randomGenerator.generateRandom(SALT_SIZE_IN_BYTES);
        PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, ITERATIONS);
        data = DataUtil.append(data, salt);

        byte[] encrypted;
        synchronized (encryptCipher) {
            // as a security constrain, it is necessary to use different salts per encryption
            // core issue: want to avoid this reinitialization to change the salt that will be used. Its quite time consuming
            encryptCipher.init(javax.crypto.Cipher.ENCRYPT_MODE, key, parameterSpec);
            encrypted = encryptCipher.doFinal(data);
        }
        return DataUtil.append(encrypted, salt);
    }

    public byte[] decrypt(byte[] data) throws Exception {
        byte[] salt = extractSaltPart(data);
        data = extractDataPart(data);

        PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, ITERATIONS);

        byte[] decrypted;

        synchronized (decryptCipher) {
            // as a security constrain, it is necessary to use different salts per encryption
            // core issue: want to avoid this reinitialization to change the salt that will be used. Its quite time consuming
            decryptCipher.init(javax.crypto.Cipher.DECRYPT_MODE, key, parameterSpec); 
            decrypted = decryptCipher.doFinal(data);
        }

        byte[] decryptedSalt = extractSaltPart(decrypted);

        if (Arrays.equals(salt, decryptedSalt))
            return extractDataPart(decrypted);
        else
            throw new IllegalArgumentException("Encrypted data is corrupted: Bad Salt");
    }

    protected byte[] extractDataPart(byte[] bytes) {
        return DataUtil.extract(bytes, 0, bytes.length - SALT_SIZE_IN_BYTES);
    }

    protected byte[] extractSaltPart(byte[] bytes) {
        return DataUtil.extract(bytes, bytes.length - SALT_SIZE_IN_BYTES, SALT_SIZE_IN_BYTES);
    }

    // main method to basic check the code execution
    public static void main(String[] args) throws Exception {
        String plainText = "some plain text, have fun!";
        String passphrase = "this is a secret";

        byte[] data = plainText.getBytes(Charset.forName("UTF-8"));

        AES cipher = new AES(passphrase);
        byte[] encrypted = cipher.encrypt(data);
        byte[] decrypted = cipher.decrypt(encrypted);

        System.out.println("expected: true, actual: " + Arrays.equals(data, decrypted));
    }
}

// Utility class
class RandomGenerator {

    private SecureRandom random = new SecureRandom();

    public RandomGenerator() {
        random = new SecureRandom();
        random.nextBoolean();
    }

    public synchronized byte[] generateRandom(int length) {
        byte[] data = new byte[length];
        random.nextBytes(data);
        return data;
    }
}

// Utility class
class DataUtil {

    public static byte[] append(byte[] data, byte[] append) {
        byte[] merged = new byte[data.length + append.length];
        System.arraycopy(data, 0, merged, 0, data.length);
        System.arraycopy(append, 0, merged, data.length, append.length);
        return merged;
    }

    public static byte[] extract(byte[] data, int start, int length) {
        if (start + length > data.length)
            throw new IllegalArgumentException("Cannot extract " + length + " bytes starting from index " + start + " from data with length " + data.length);

        byte[] extracted = new byte[length];
        System.arraycopy(data, start, extracted, 0, length);
        return extracted;
    }

}

【问题讨论】:

  • 您的问题是什么?如需代码审查,请将您的问题发送至codereview.stackexchange.com
  • 我将标题编辑为更直接,并立即提出问题如果不够好我可以发送到代码审查

标签: java performance cryptography aes salt


【解决方案1】:

你运气不好。如果您每次都选择新的盐,这意味着您每次都使用新的密钥进行加密/解密,这意味着您每次都需要调用init

如果您想要更快的速度,只需添加您的信息:

byte[] salt = randomGenerator.generateRandom(SALT_SIZE_IN_BYTES);
encryptCipher.update(salt);
encrypted = encryptCipher.doFinal(data);

这样,您每次都使用相同的密钥,因此您无需重新初始化它。 (不要使用 PBE,只需使用 128 位 AES/CBC)。如果不知道您打算如何在现实世界中应用这种加密,就很难知道这是否足以满足您的需求。

附言迭代次数 == 120000?难怪这么慢。

【讨论】:

  • 正确 -- 您可以简单地为每条消息添加一个随机盐前缀。只要另一端知道忽略它,它就会执行您想要的操作,并且无需为每条消息重新初始化。 (而且,是的,120K 迭代有点多——4000 是在我拥有的一个应用程序中使用的。)
  • 我研究了更多密码学和分组密码,我注意到向分组密码添加盐(前置或附加)只会影响加盐块。不包含盐本身的其他块将始终以相同的方式加密。在普通消息中添加盐是完全没有必要的。我真正应该用来在每次加密中实现随机性的是使用随机初始化向量,它可以在加密后附加或附加到密文中,就像盐一样
  • 是的,使用随机 IV 等同于在消息开头使用一些随机数据。
【解决方案2】:
  1. 如果您只是传输数据并需要在飞行中对其进行加密,请使用 TLS/SSL。它的方式更快,不会中断。

  2. 确保您对密文使用了某种身份验证。在 GCM 或 CCM 模式下使用 MAC 或更好地使用 AES。否则你的加密是不安全的。

至于你的问题:是的,它是可以解决的。 只需生成一次密钥并重用它/ 。 AES 可以安全地使用相同的密钥发送多条消息。所以只需派生一次密钥/密码并继续使用它。

只需确保为每条消息使用新的 IV。

【讨论】:

  • 我不是用于数据传输,而是用于安全存储本身 感谢 MAC 上的提示,确实这种加密不是防篡改的。至于IV,我实际上为每条加密消息使用了一个新IV(随机,不可预测)
  • 是的,这是更好的方法。我在写这篇文章后不久就意识到了这一点。已更正
【解决方案3】:

实际上重新初始化密码并没有那么慢。由于迭代次数,创建密钥本身很慢。

此外,迭代计数被忽略并且从未在加密本身中使用,仅在密钥生成时使用。根据所选择的算法,JCE API 会产生误导

关于盐:在普通消息中添加盐是完全没有必要的。我真正应该用来在每次加密中实现随机性的是使用随机初始化向量,它可以像盐一样在加密后附加或附加到密文中。

【讨论】:

  • 来自 JavaDocs Cipher 类:“请注意,当初始化 Cipher 对象时,它会丢失所有先前获取的状态。换句话说,初始化 Cipher 等同于创建该 Cipher 的新实例并初始化它。” docs.oracle.com/javase/7/docs/api/javax/crypto/…
猜你喜欢
  • 1970-01-01
  • 2016-07-08
  • 2014-11-30
  • 2011-09-17
  • 2011-04-30
  • 1970-01-01
  • 2021-01-18
  • 2016-04-09
  • 1970-01-01
相关资源
最近更新 更多