【问题标题】:Alternative to using SecureRandom to generate AES key and IV替代使用 SecureRandom 生成 AES 密钥和 IV
【发布时间】:2015-01-07 02:21:06
【问题描述】:

我正在尝试使用 AES 加密算法加密文本,将此加密文本保存到文件中,然后稍后重新打开并解密。以下是我的加解密逻辑

    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(Cipher.ENCRYPT_MODE, key, iv);
    byte[] stringBytes = clear_text.getBytes();
    byte[] raw = cipher.doFinal(stringBytes);
    return Base64.encodeBase64String(raw);

这就是解密逻辑

    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(Cipher.DECRYPT_MODE, key, iv);
    byte[] raw = Base64.decodeBase64(encText);
    byte[] stringBytes = cipher.doFinal(raw);
    String clear_text = new String(stringBytes, "UTF8");
    return clear_text;

我得到一个 BadPaddingSize 异常。我的猜测是,通过使用 SecureRandom 类,这两种方法在加密或解密文本时都使用不同的密钥。有没有办法可以在两个例程中使用相同的键?

【问题讨论】:

  • 您不能使用不同的密钥(和 IV)加密然后解密。您必须将密钥(和 IV)存储在某处。这取决于你。由于我们对您的应用程序一无所知,因此您需要澄清并添加更多代码。
  • 是的,我的问题是如何为两者使用一个公用密钥。通过使用secureRandom,我每次尝试解密时都会更改密钥和IV。或者换句话说,如何保存 Key 类型的密钥并在需要时使用它?在不使用随机位的情况下生成密钥的另一种方法是什么 -

标签: java encryption aes symmetric-key


【解决方案1】:

是的,您可以使用相同的密钥;甚至需要使用相同的密钥。但是,您不应该使用相同的密钥/IV 组合,因为这不安全。所以通常 IV 会被添加到密文的前缀。

请注意,下面的实现向您展示了如何在没有SecureRandom 的情况下生成随机 IV,但这有点虚伪,因为 Cipher 类只会在内部使用默认值来创建 IV。对于 CBC,攻击者可能知道 IV,但攻击者应该无法将其与随机数据区分开来。

在这个例子中,关键数据简单地存储在一个“常量”中。将密钥存储在源代码中可能无法提供足够的安全性。相反,它通常使用公钥、密码、存储在 USB 密钥中、存储在智能卡或 HSM 等中进行加密。然而,密钥管理是一个广泛的主题,因此我不会为这个答案进一步讨论它。

但是,在 Java 中,您应该使用 SecretKey/SecretKeySpec 从已知数据创建密钥,并使用 IvParameterSpec 为已知 IV(或 Nonce)创建密钥。

import java.nio.charset.StandardCharsets;
import java.security.AlgorithmParameters;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidParameterSpecException;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class AESWithStaticKeyAndRandomIV {

    private static byte[] KEY = new byte[] { (byte) 0x14, (byte) 0x0b,
            (byte) 0x41, (byte) 0xb2, (byte) 0x2a, (byte) 0x29, (byte) 0xbe,
            (byte) 0xb4, (byte) 0x06, (byte) 0x1b, (byte) 0xda, (byte) 0x66,
            (byte) 0xb6, (byte) 0x74, (byte) 0x7e, (byte) 0x14 };

    public static byte[] encrypt(byte[] plaintext) {
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            SecretKey key = new SecretKeySpec(KEY, "AES");
            cipher.init(Cipher.ENCRYPT_MODE, key);

            AlgorithmParameters params = cipher.getParameters();
            byte[] iv = params.getParameterSpec(IvParameterSpec.class).getIV();

            byte[] ciphertext = new byte[iv.length
                    + cipher.getOutputSize(plaintext.length)];
            System.arraycopy(iv, 0, ciphertext, 0, iv.length);
            cipher.doFinal(plaintext, 0, plaintext.length, ciphertext,
                    iv.length);
            return ciphertext;
        } catch (InvalidKeyException | NoSuchAlgorithmException
                | NoSuchPaddingException | InvalidParameterSpecException
                | ShortBufferException | IllegalBlockSizeException
                | BadPaddingException e) {
            throw new IllegalStateException(
                    "CBC encryption with standard algorithm should never fail",
                    e);
        }
    }

    public static byte[] decrypt(byte[] ciphertext) throws IllegalBlockSizeException,
            BadPaddingException {
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            SecretKeySpec key = new SecretKeySpec(KEY, "AES");

            if (ciphertext.length < cipher.getBlockSize()) {
                throw new IllegalArgumentException(
                        "Ciphertext too small to contain IV");
            }

            IvParameterSpec ivSpec = new IvParameterSpec(ciphertext, 0,
                    cipher.getBlockSize());
            cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);

            byte[] plaintext = new byte[cipher.getOutputSize(ciphertext.length
                    - cipher.getBlockSize())];
            cipher.doFinal(ciphertext, cipher.getBlockSize(), ciphertext.length
                    - cipher.getBlockSize(), plaintext, 0);
            return plaintext;
        } catch (InvalidKeyException | NoSuchAlgorithmException
                | NoSuchPaddingException | ShortBufferException
                | InvalidAlgorithmParameterException e) {
            throw new IllegalStateException(
                    "CBC decryption with standard algorithm should be available",
                    e);
        }
    }

    public static void main(String[] args) throws Exception {
           byte[] plaintext = decrypt(encrypt("owlstead".getBytes(StandardCharsets.UTF_8)));
           System.out.println(new String(plaintext, StandardCharsets.UTF_8));
    }
}

使用密钥存储(您现在必须使用 JCEKS):

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.AlgorithmParameters;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStore.ProtectionParameter;
import java.security.KeyStore.SecretKeyEntry;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableEntryException;
import java.security.cert.CertificateException;
import java.security.spec.InvalidParameterSpecException;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class AESWithStaticKeyAndRandomIV {

    private static final String KEY_ALIAS = "secret";

    private static byte[] KEY = new byte[] { (byte) 0x14, (byte) 0x0b,
            (byte) 0x41, (byte) 0xb2, (byte) 0x2a, (byte) 0x29, (byte) 0xbe,
            (byte) 0xb4, (byte) 0x06, (byte) 0x1b, (byte) 0xda, (byte) 0x66,
            (byte) 0xb6, (byte) 0x74, (byte) 0x7e, (byte) 0x14 };

    private static ProtectionParameter PASSWORD = new KeyStore.PasswordProtection(
            new char[] {'p', 'a', 's', 's', 'w', 'o', 'r', 'd'});

    private final KeyStore store;

    private AESWithStaticKeyAndRandomIV(KeyStore store) {
        this.store = store;
    }

    public byte[] encrypt(byte[] plaintext) {
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            SecretKey key;
            try {
                key = ((SecretKeyEntry) store.getEntry(KEY_ALIAS, PASSWORD))
                        .getSecretKey();
            } catch (UnrecoverableEntryException | KeyStoreException e) {
                throw new IllegalStateException("What key?", e);
            }
            cipher.init(Cipher.ENCRYPT_MODE, key);

            AlgorithmParameters params = cipher.getParameters();
            byte[] iv = params.getParameterSpec(IvParameterSpec.class).getIV();

            byte[] ciphertext = new byte[iv.length
                    + cipher.getOutputSize(plaintext.length)];
            System.arraycopy(iv, 0, ciphertext, 0, iv.length);
            cipher.doFinal(plaintext, 0, plaintext.length, ciphertext,
                    iv.length);
            return ciphertext;
        } catch (InvalidKeyException | NoSuchAlgorithmException
                | NoSuchPaddingException | InvalidParameterSpecException
                | ShortBufferException | IllegalBlockSizeException
                | BadPaddingException e) {
            throw new IllegalStateException(
                    "CBC encryption with standard algorithm should never fail",
                    e);
        }
    }

    public byte[] decrypt(byte[] ciphertext) throws IllegalBlockSizeException,
            BadPaddingException {
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            SecretKey key;
            try {
                key = ((SecretKeyEntry) store.getEntry(KEY_ALIAS, PASSWORD))
                        .getSecretKey();
            } catch (UnrecoverableEntryException | KeyStoreException e) {
                throw new IllegalStateException("What key?", e);
            }

            if (ciphertext.length < cipher.getBlockSize()) {
                throw new IllegalArgumentException(
                        "Ciphertext too small to contain IV");
            }

            IvParameterSpec ivSpec = new IvParameterSpec(ciphertext, 0,
                    cipher.getBlockSize());
            cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);

            byte[] plaintext = new byte[cipher.getOutputSize(ciphertext.length
                    - cipher.getBlockSize())];
            cipher.doFinal(ciphertext, cipher.getBlockSize(), ciphertext.length
                    - cipher.getBlockSize(), plaintext, 0);
            return plaintext;
        } catch (InvalidKeyException | NoSuchAlgorithmException
                | NoSuchPaddingException | ShortBufferException
                | InvalidAlgorithmParameterException e) {
            throw new IllegalStateException(
                    "CBC decryption with standard algorithm should be available",
                    e);
        }
    }

    public static KeyStore createStoreWithSecretKey() {

        try {
            KeyStore keyStore = KeyStore.getInstance("JCEKS");
            keyStore.load(null);
            SecretKey key = new SecretKeySpec(KEY, "AES");
            keyStore.setEntry(KEY_ALIAS, new KeyStore.SecretKeyEntry(key), PASSWORD);
            return keyStore;
        } catch (KeyStoreException | NoSuchAlgorithmException
                | CertificateException | IOException e) {
            throw new IllegalStateException("Unable to create key store", e);
        }
    }

    public static void main(String[] args) throws Exception {
        AESWithStaticKeyAndRandomIV crypt = new AESWithStaticKeyAndRandomIV(
                createStoreWithSecretKey());

        byte[] plaintext = crypt.decrypt(crypt.encrypt("owlstead"
                .getBytes(StandardCharsets.UTF_8)));
        System.out.println(new String(plaintext, StandardCharsets.UTF_8));
    }
}

【讨论】:

  • 非常感谢,我可以使用使用 keytool 生成的密钥并将其保存在系统中的某个位置吗?如果是,IV 将如何工作?我也需要用其他语言解密这些明文,所以我需要一个公用密钥和 IV 才能使用
  • 添加了一些代码。 IV 应该始终是随机的,并且可以根据某个唯一值计算或与密文一起存储(如示例中所示)。它应该与密钥一起存储,因此不会在KeyStore 接口内。
  • 嗨 owlstead,感谢您添加代码,我尝试了您的代码,但仍然收到 Illegal blockSize 异常,您认为这是因为我将加密文本保存在文件中,然后从文件中读取并更改加密字符串的填充?
  • 是的,我认为这很可能。你试过readAllBytes吗?
  • 我对此解决方案的单元测试未能尝试比较 6 字节值的加密/解密结果,因为解密将返回意外的 16 字节。我将示例中的问题隔离为在解密中创建明文数组,该明文数组尝试使用 cipher.getOutputSize(..) 获取原始加密字节数组的大小未成功。它返回 6 个字节而不是 16 个(即块大小)。为了解决这个问题,我将所有非 IV 字节复制到他们自己的数组中,然后使用了普通的 doFinal(byte[]) 方法,该方法确实正确返回了 6 个字节。
猜你喜欢
  • 1970-01-01
  • 2013-11-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-02-21
  • 2011-12-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多