如何从字符串正确地重新创建 SecretKey

How to properly recreate SecretKey from string

我正在尝试制作一个加密解密应用程序。我有两个 classes - 一个具有生成密钥、加密和解密的功能,第二个用于 JavaFX GUI。在 GUI class 我有 4 个文本区域:第一个用于编写要加密的文本,第二个用于加密文本,第三个用于密钥 (String encodedKey = Base64.getEncoder().encodeToString(klucz.getEncoded());),第四个用于解密文本。

问题是,我无法解密文本。我正在尝试像这样重新创建 SecretKey:

String encodedKey = textAreaKey.getText();                
byte[] decodedKey = Base64.getDecoder().decode(encodedKey);
SecretKey klucz = new SecretKeySpec(decodedKey, "DESede");

当我加密密钥时如下所示:com.sun.crypto.provider.DESedeKey@4f964d80 当我尝试重新创建它时:javax.crypto.spec.SecretKeySpec@4f964d80 我得到 javax.crypto.IllegalBlockSizeException: Input length must be multiple of 8 when decrypting with padded cipher

这是我的第一个 class:

public class Encryption {

    public static SecretKey generateKey() throws NoSuchAlgorithmException {
        Security.addProvider(new com.sun.crypto.provider.SunJCE());
        KeyGenerator keygen = KeyGenerator.getInstance("DESede");
        keygen.init(168);
        SecretKey klucz = keygen.generateKey();

        return klucz;
    }

    static byte[] encrypt(byte[] plainTextByte, SecretKey klucz)
        throws Exception {
        Cipher cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, klucz);
        byte[] encryptedBytes = cipher.doFinal(plainTextByte);
        return encryptedBytes;
    }

    static byte[] decrypt(byte[] encryptedBytes, SecretKey klucz)
        throws Exception {
        Cipher cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, klucz);
        byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
        return decryptedBytes;
    }
}

编辑

    btnEncrypt.setOnAction((ActionEvent event) -> {
        try {
            String plainText = textAreaToEncrypt.getText();
            SecretKey klucz = Encryption.generateKey();                
            byte[] plainTextByte = plainText.getBytes();                
            byte[] encryptedBytes = Encryption.encrypt(plainTextByte, klucz);                
            String encryptedText = Base64.getEncoder().encodeToString(encryptedBytes);
            textAreaEncryptedText.setText(encryptedText);                
            byte[] byteKey = klucz.getEncoded();
            String stringKey = Base64.getEncoder().encodeToString(byteKey);
            textAreaKey.setTextstringKey
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    });

    btnDecrypt.setOnAction((ActionEvent event) -> {
        try {
            String stringKey = textAreaKey.getText();                
            byte[] decodedKey = Base64.getDecoder().decode(encodedKey);
            SecretKey klucz2 = new SecretKeySpec(decodedKey, "DESede");                
            String encryptedText = textAreaEncryptedText.getText();
            byte[] encryptedBytes = Base64.getDecoder().decode(encryptedText.getBytes());                
            byte[] decryptedBytes = Encryption.decrypt(encryptedBytes, klucz2;
            String decryptedText = Base64.getEncoder().encodeToString(decryptedBytes);

            textAreaDecryptedText.setText(decryptedText);

        } catch (Exception ex) {
            ex.printStackTrace();
        }
    });

你的问题之一在这里:

String encryptedText = new String(encryptedBytes, "UTF8");

通常,密文中的许多字节序列不是有效的 UTF-8 编码字符。当您尝试创建 String 时,此格式错误的序列将被替换为 "replacement character",然后来自密文的信息将无法挽回地丢失。当您将 String 转换回字节并尝试对其进行解密时,损坏的密文会引发错误。

如果您需要将密文表示为字符串,请使用 base-64 编码,就像您对密钥所做的那样。

另一个主要问题是您没有指定完整的转换。您应该明确指定密码的 "mode" 和 "padding",例如 "DESede/ECB/PKCS5Padding"。

正确的模式将取决于您的任务。 ECB 通常不安全,但更安全的模式会增加一些复杂性,这可能超出您的任务范围。研究您的说明并在必要时与您的老师明确要求。

主要有两个问题:

  1. 您不应该使用用户输入的密码作为密钥(它们之间是有区别的)。密钥必须具有特定大小,具体取决于密码(3des 为 16 或 24 字节)

  2. Direct 3DES (DESede) 是一次加密 8 个字节的分组密码。要加密多个块,有一些方法定义了如何正确地做到这一点。它是调用 Block cipher mode.

为了正确加密,您还需要注意一些事情

从密码创建密钥

假设您要使用 DESede (3des)。 密钥必须具有固定大小 - 16 或 24 字节。要从密码正确生成密钥,您应该使用 PBKDF。有些人对"must use"很敏感,但是忽略这一步确实会损害主要使用用户输入密码的加密安全性。

对于 3DES,您可以使用:

        int keySize = 16*8;
        int iterations = 800000;
        char[] password = "password".toCharArray();
        SecureRandom random = new SecureRandom();
        byte[] salt = random.generateSeed(8);

        SecretKeyFactory secKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512");
        KeySpec spec = new PBEKeySpec(password, salt, iterations, keySize);
        SecretKey pbeSecretKey = secKeyFactory.generateSecret(spec);
        SecretKey desSecret = new SecretKeySpec(pbeSecretKey.getEncoded(), "DESede");

        // iv needs to have block size
        // we will use the salt for simplification
        IvParameterSpec ivParam = new IvParameterSpec(salt);

        Cipher cipher = Cipher.getInstance("DESEde/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE,  desSecret, ivParam);

        System.out.println("salt: "+Base64.getEncoder().encodeToString(salt));
        System.out.println(cipher.getIV().length+" iv: "+Base64.getEncoder().encodeToString(cipher.getIV()));
        byte[] ciphertext = cipher.doFinal("plaintext input".getBytes());
        System.out.println("encrypted: "+Base64.getEncoder().encodeToString(ciphertext));

如果您可以确保您的密码具有良好的熵(足够长且足够随机),您可以使用简单的散列

        MessageDigest dgst = MessageDigest.getInstance("sha-1");
        byte[] hash = dgst.digest("some long, complex and random password".getBytes());
        byte[] keyBytes = new byte[keySize/8];
        System.arraycopy(hash, 0, keyBytes, 0, keySize/8);
        SecretKey desSecret = new SecretKeySpec(keyBytes, "DESede");

salt 用于随机化输出,应该使用。

加密的输出应该是 salt | cipthertext | tag(不一定按此顺序,但您需要所有这些才能正确加密)。

要解密输出,您需要将输出拆分为盐、密文和标签。

我在 Whosebug 的示例中经常看到零向量(静态 salt 或 iv),但在许多情况下,它可能会导致破译的密码泄露密钥或明文。

区块链模式需要初始化向量 iv(加密比单个块更长的输入),我们可以使用盐也来自钥匙 当具有相同的大小时(在我们的例子中为 8 个字节)。对于真正安全的解决方案,密码盐应该更长。

该标签为认证标签,确保密文未被任何人篡改。您可以使用明文或密文的 HMAC。重要的是,您应该为 HMAC 使用与加密不同的密钥。但是 - 我相信在你的情况下,即使没有 hmac 标签,你的家庭作业也可以