【问题标题】:How to correctly encrypt and decrypt a file using a secret key derived from a password如何使用从密码派生的密钥正确加密和解​​密文件
【发布时间】:2020-12-22 07:11:53
【问题描述】:

我正在尝试制定使用“PBEWithHmacSHA256AndAES_256”标准加密和解密文件的正确流程。

根据我从 Oracle 看到的 example code 的理解。

我已经收集到需要盐,以及迭代计数和哈希标准。

所以我有我的主要方法,传入加密方法:

  1. 用户定义的密码new String(key).toCharArray()作为字节数组(使用此方法进行其他加密运行)
  2. 作为字节数组的安全随机 IV initVector
  3. 纯文本文件inputFile 作为字符串
  4. 要创建的密文文件outputFile的名称作为字符串

我已经按照代码示例编写了我认为对加密方法正确的代码。我通过将它们都附加到密文来存储用于解密的盐和 IV。

private static void encrypt(byte[] key, byte[] initVector, String inputFile, String outputFile) //exceptions for throws... {
    //Initalisation for encryption
    Cipher cipher;

    byte[] salt = new byte[16];

        SecureRandom rand = new SecureRandom();

        // Salt randomly generated with base64
        rand.nextBytes(salt);
        System.out.println("my salt should be" + Base64.getEncoder().encodeToString(salt));
        salt = Base64.getEncoder().encode(salt);

        // Iteration count
        int count = 1000;

        IvParameterSpec iv = new IvParameterSpec(initVector);
        
        // Create PBE parameter set
        PBEParameterSpec pbeParamSpec = new PBEParameterSpec(Base64.getDecoder().decode(salt), count, iv);                
        // Convert pass into SecretKey object
        PBEKeySpec pbeKeySpec = new PBEKeySpec(new String(key).toCharArray());
        SecretKeyFactory keyFac = SecretKeyFactory.getInstance("PBEWithHmacSHA256AndAES_256");
        SecretKey pbeKey;
        try {
            pbeKey = keyFac.generateSecret(pbeKeySpec);
        } catch (InvalidKeySpecException e) {
            System.out.println("Sorry, the password specified cannot be used as a secret key");
            System.out.println("Please check that your password uses valid characters");
            return;
        }

        // Create PBE Cipher
        cipher = Cipher.getInstance("PBEWithHmacSHA256AndAES_256");

        // Initialize PBE Cipher with key and parameters
        cipher.init(Cipher.ENCRYPT_MODE, pbeKey, pbeParamSpec);
    }

    //File error checking and file handling (i.e. generating file paths)...

    System.out.println("Secret key is " + Base64.getEncoder().encodeToString(key));
    System.out.println("IV is " + Base64.getEncoder().encodeToString(initVector));

    //Special file reading and writing with 'Cipher Stream'
    try (InputStream fin = FileEncryptor.class.getResourceAsStream(loadFile.getName());
            OutputStream fout = Files.newOutputStream(saveFile);

            CipherOutputStream cipherOut = new CipherOutputStream(fout, cipher) {
            }) {
        final byte[] bytes = new byte[1024];
        for(int length=fin.read(bytes); length!=-1; length = fin.read(bytes)){

                fout.write(initVector);
                fout.write(salt);

            cipherOut.write(bytes, 0, length);

        }
    } catch (IOException e) {
        System.out.println("Something went wrong with reading and writing these files!");
        System.out.println("Please check you have the latest version of this program");
        System.out.println("Contact your IT admin to make sure you have sufficient privileges");
    }
    System.out.println("SUCCESS! Encryption finished, saved at specified location");
}

那我也有我的main方法,传入解密方法:

  1. 一个用户定义的密码String inputKEY作为一个字符串(也使用此方法进行其他解密运行)

  2. inputIV 的字符串已作为 null 传入,因为不用于 PBE。

  3. 密文文件inputFile作为字符串

  4. 要创建的以字符串形式显示纯文本文件 outputFile 的名称

    private static void decrypt(String inputKEY, String inputIV, String inputFile, String outputFile) { 密码密码 = null;

     //File error checking and file handling (i.e. generating file paths)...
    
     InputStream encryptedData = Files.newInputStream(loadFilePath);
    
    
         PBEKeySpec pbeKeySpec = new PBEKeySpec(inputKEY.toCharArray());
         SecretKeyFactory keyFac = SecretKeyFactory.getInstance("PBEWithHmacSHA256AndAES_256");
         SecretKey pbeKey = null;
         try {
             pbeKey = keyFac.generateSecret(pbeKeySpec);
         } catch (InvalidKeySpecException e) {
             // TODO Auto-generated catch block
             e.printStackTrace();
         }
         byte[] initVect = new byte[16];
         encryptedData.read(initVect);
    
         IvParameterSpec iv = new IvParameterSpec(Base64.getDecoder().decode(initVect);
    
         byte[] salt = new byte[16];
         encryptedData.read(salt);
    
         PBEParameterSpec pbeParamSpec = new PBEParameterSpec(Base64.getDecoder().decode(salt), 1000, iv);  
         cipher = Cipher.getInstance("PBEWithHmacSHA256AndAES_256");
    
         System.out.println("my salt should be" + Base64.getEncoder().encodeToString(Base64.getDecoder().decode(salt)));
    
         cipher.init(Cipher.DECRYPT_MODE, pbeKey, pbeParamSpec); 
    
     try (CipherInputStream decryptStream = new CipherInputStream(encryptedData, cipher);    
             OutputStream decryptedOut = Files.newOutputStream(saveFile)){
         final byte[] bytes = new byte[1024];
         for(int length=decryptStream.read(bytes); length!=-1; length = decryptStream.read(bytes)){
             decryptedOut.write(bytes, 0, length);
         }
     } catch (IOException e) { //This is caught when decryption is run
         System.out.println("Something went wrong with reading and writing these files!");
         System.out.println("Please check you have the latest version of this program");
         System.out.println("Contact your IT admin to make sure you have sufficient privileges");
     }
    
     System.out.println("SUCESS! Decryption finished, saved at specified location");
    

    }

我认为我对 PBE 的理解有问题,因此我实施它的方式可能是错误的。谁能指出似乎有什么问题?

【问题讨论】:

  • 您是否收到错误或解密文件与原始文件不相等?顺便说一句:今天的迭代值应该 > 10.000。
  • 它捕获了解密方法底部的try-catch方法,但仍然完成。是的,解密文件不等于原始文件。我认为解密文件的文件大小也可能比纯文本小。
  • 将完整的错误堆栈跟踪添加到您的问题中可能是个好主意。

标签: java encryption salt jce


【解决方案1】:

主要问题是:

  • IV 和 Salt 不得写入 for 循环内。
  • IV 存储在 encrypt 中,不是 Base64 编码的,而是在 decrypt 中进行 Base64 解码的。
  • 16 字节的 salt 存储在 encrypt(不必要地)Base64 编码中,即存储 24 字节。但是在 decrypt 中只加载了 16 个字节。

还有:

  • 编码/解码时,有时没有指定编码,所以使用默认编码。
  • encrypt 和 decrypt 对 key 和 IV 使用不同的参数类型。
  • 代码中有很多复制/粘贴错误。

注意:与您的代码相比,linked 代码除了密钥之外还确定密码和盐的 IV。 在您的代码中,IV 被传递。因此,您必须确保密钥/IV 对只能使用一次。通常每次加密都会生成一个随机IV。

在以下代码中(基于您的代码,但为了简单起见,没有异常处理)这些问题已得到修复/优化。此外,代码应用 FileInputStream 和 FileOutputStream 而不是你的类(但这不是必需的):

private static void encrypt(String key, byte[] initVector, String inputFile, String outputFile) throws Exception {

    // Key
    PBEKeySpec pbeKeySpec = new PBEKeySpec(key.toCharArray());
    SecretKeyFactory keyFac = SecretKeyFactory.getInstance("PBEWithHmacSHA256AndAES_256");
    SecretKey pbeKey = keyFac.generateSecret(pbeKeySpec);

    // IV
    IvParameterSpec iv = new IvParameterSpec(initVector);

    // Salt
    SecureRandom rand = new SecureRandom();
    byte[] salt = new byte[16];
    rand.nextBytes(salt);

    // ParameterSpec
    int count = 1000; // should be larger, see Michael Fehr's comment
    PBEParameterSpec pbeParamSpec = new PBEParameterSpec(salt, count, iv);

    // Cipher
    Cipher cipher = Cipher.getInstance("PBEWithHmacSHA256AndAES_256");
    cipher.init(Cipher.ENCRYPT_MODE, pbeKey, pbeParamSpec);

    try (FileInputStream fin = new FileInputStream(inputFile);
         FileOutputStream fout = new FileOutputStream(outputFile);
         CipherOutputStream cipherOut = new CipherOutputStream(fout, cipher)) {
    
        // Write IV, Salt
        fout.write(initVector);
        fout.write(salt);
    
        // Encrypt
        final byte[] bytes = new byte[1024];
        for (int length = fin.read(bytes); length != -1; length = fin.read(bytes)) {
            cipherOut.write(bytes, 0, length);
        }
    } 
}
private static void decrypt(String key, byte[] initVect, String inputFile, String outputFile) throws Exception {

    try (FileInputStream encryptedData = new FileInputStream(inputFile);
         FileOutputStream decryptedOut = new FileOutputStream(outputFile)) {

        // Key
        PBEKeySpec pbeKeySpec = new PBEKeySpec(key.toCharArray());
        SecretKeyFactory keyFac = SecretKeyFactory.getInstance("PBEWithHmacSHA256AndAES_256");
        SecretKey pbeKey = keyFac.generateSecret(pbeKeySpec);

        // Read IV
        if (initVect == null) {
            initVect = encryptedData.readNBytes(16);
        }
        IvParameterSpec iv = new IvParameterSpec(initVect);

        // Read salt
        byte[] salt = encryptedData.readNBytes(16);

        // ParameterSpec
        int count = 1000;
        PBEParameterSpec pbeParamSpec = new PBEParameterSpec(salt, count, iv);

        // Cipher
        Cipher cipher = Cipher.getInstance("PBEWithHmacSHA256AndAES_256");
        cipher.init(Cipher.DECRYPT_MODE, pbeKey, pbeParamSpec);

        try (CipherInputStream decryptStream = new CipherInputStream(encryptedData, cipher)) {
        
            // Decrypt
            final byte[] bytes = new byte[1024];
            for (int length = decryptStream.read(bytes); length != -1; length = decryptStream.read(bytes)) {
                decryptedOut.write(bytes, 0, length);
            }
        } 
    }
}

编辑 - 关于 decrypt 中盐和 IV 的阅读:
正如 GPI 在他们的评论中指出的那样,FileInputStream.read(byte[] b) 通常读作 b.length字节,但这是不保证。更鲁棒的是判断读取数据的长度,循环调用方法,直到数据完成。另一种选择是使用InputStream.readNBytes​(int len),这是保证读取len字节(除非遇到流结束或抛出异常),因为Zabuzard有建议。在代码中,现在使用的是后者,即read被readNBytes​替换。

【讨论】:

  • +1。我不是加密专家,所以我不会评判它的安全性,但在 Java 级别,这对我来说看起来不错。我知道FileInputStream 在我使用的任何JVM 实现中调用read(buffer) 时将“总是”(如果文件足够长)读取buffer.length 字节。但它并非必须如此。因此,在您的解密方法中,在读取 IV 和 salt 时,我肯定会编写样板文件以确保已读取实际字节数。这在安全方面也很棒,因为如果没有达到预期的字节数,就会发生一些可疑的事情。
  • 或者使用readNBytes,那么你也会得到N字节。
  • 再次欢呼!我最终使用了 read(),但了解 readNBytes 肯定也很有用。我想我需要自己做更多的研究来确切地了解输入和输出流是如何工作的......
猜你喜欢
  • 1970-01-01
  • 2019-08-13
  • 2011-05-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-02
  • 2022-04-07
  • 1970-01-01
相关资源
最近更新 更多