Как правильно зашифровать и расшифровать файл с помощью секретного ключа, полученного из пароля

Я пытаюсь разработать правильный процесс шифрования и дешифрования файла с использованием стандарта PBEWithHmacSHA256AndAES_256.

Насколько я понимаю, глядя на этот пример кода от Oracle.

Я понял, что нужна соль, а также количество итераций и стандарт хеширования.

Итак, у меня есть основной метод, переходящий в метод шифрования:

  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");
}

Затем у меня также есть мой основной метод, переходящий в метод дешифрования:

  1. определяемый пользователем пароль String inputKEY в виде строки (также используется этот метод для других запусков дешифрования)

  2. Строка для inputIV была передана как null, поскольку не используется для PBE.

  3. файл зашифрованного текста inputFile в виде строки

  4. имя создаваемого файла открытого текста outputFile в виде строки

    частный статический void дешифровать (String inputKEY, String inputIV, String inputFile, String outputFile) {Cipher cipher = 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, и поэтому способ, которым я это реализовал, вероятно, неправильный. Может ли кто-нибудь указать на то, что кажется неправильным?


person CHOCO BLOCK    schedule 03.09.2020    source источник
comment
Вы получаете сообщения об ошибках или расшифрованный файл не соответствует исходному файлу? Кстати: сегодня значение итерации должно быть ›10.000.   -  person Michael Fehr    schedule 03.09.2020
comment
Он перехватывает метод try-catch в нижней части метода дешифрования, но все равно завершается. И да, расшифрованный файл не равен исходному файлу. Я думаю, что также может быть вероятность того, что расшифрованный файл будет иметь меньший размер, чем простой текст.   -  person CHOCO BLOCK    schedule 03.09.2020
comment
Было бы неплохо добавить к вашему вопросу полную трассировку стека ошибок.   -  person Michael Fehr    schedule 03.09.2020


Ответы (1)


Основные проблемы:

  • IV и Salt нельзя записывать внутри цикла for.
  • IV хранится в encrypt не в кодировке Base64, а в кодировке Base64 в decrypt.
  • 16-байтовая соль хранится в encrypt (необязательно) в кодировке Base64, то есть сохраняется 24 байта. Однако в decrypt загружается только 16 байт.

Также:

  • При кодировании / декодировании иногда кодировка не указывается, поэтому используется кодировка по умолчанию.
  • encrypt и decrypt используют разные типы параметров для ключа и IV.
  • В коде много ошибок копирования / вставки.

Примечание. В отличие от вашего кода, связанный код определяет помимо ключа также 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);
            }
        } 
    }
}

РЕДАКТИРОВАТЬ - Относительно чтения соли и IV в decrypt:
Как указал GPI в своем комментарии, _ 13_ обычно читает b.length байт , но это не гарантируется. Более надежным является определение длины считываемых данных и вызов метода в цикле до тех пор, пока данные не будут завершены. Другой альтернативой является использование _ 15_, который гарантированно прочитает len байт (если не обнаружен конец потока или не возникнет исключение), как Zabuzard предложенный. В коде теперь используется последний, т.е. read был заменен на readNBytes​.

person user 9014097    schedule 03.09.2020
comment
+1. Я не специалист по криптографии, поэтому не сужу о его безопасности, но на уровне Java мне это нравится. Незначительный захват, я знаю, что FileInputStream всегда (если файл достаточно длинный) будет читать buffer.length байта при вызове read(buffer) в любой реализации JVM, которую я использовал. Но так не должно быть должно быть. Итак, в вашем методе дешифрования, читая IV и соль, я обязательно напишу шаблон, чтобы убедиться, что фактическое количество байтов было прочитано. Было бы здорово также с точки зрения безопасности, потому что, если ожидаемое количество байтов не достигается, происходит что-то подозрительное. - person GPI; 03.09.2020
comment
Или используйте readNBytes, тогда вы также получите обратно N байта. - person Zabuzard; 03.09.2020
comment
Еще раз ура! В итоге я использовал read (), но, безусловно, полезно знать и о readNBytes. Я думаю, мне нужно провести дополнительное исследование, чтобы точно знать, как работают потоки ввода и вывода ... - person CHOCO BLOCK; 04.09.2020