Шифрование и дешифрование между Java и Javascript не будут работать

ИЗМЕНИТЬ 1

В методе decryptFile часть расшифровки ничего не выводит..

let decrypted = CryptoJS.AES.decrypt(e.target.result, CryptoJS.enc.Utf8.parse(key), {
    iv: CryptoJS.enc.Utf8.parse(iv),
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
});


EDIT 2 Ссылка, приведенная в разделе комментариев, частично решает проблему. Он шифрует и расшифровывает кросс-платформу, но он довольно медленный из-за PBKDF2 с хешированием SHA256. Я не могу найти способ использовать только часть AES, а не часть PKBDF2.


ПЕРВОНАЧАЛЬНЫЙ ТЕКСТ

Я использую тот же ключ и IV для версии Java и Javascript. Я не могу расшифровать файл в Javascript, который был зашифрован на Java, и я не могу расшифровать файл на Java, который был зашифрован в Javascript. Мне нужно, чтобы эти два были совместимы с eachother, но я не могу понять, как я пытаюсь расшифровать файл в Javascript, который ранее был зашифрован на Java. Я успешно выполнил расшифровку и шифрование текста между ними, но когда я хотел бы, например, расшифровать файл, зашифрованный на Java, он просто не сработает.

Шифрование/дешифрование файла в Java:

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class Test {
    private SecretKey secretKey;
    private IvParameterSpec ivParameterSpec;

    private String key = "ThisIsMyGreatKey";
    private byte[] ivKey = "ABCDEFGHabcdefgh".getBytes();

    public static void main(String[] args) {
        try {
            new Test().run();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void run() {
        ivParameterSpec = new IvParameterSpec(ivKey);
        secretKey = new SecretKeySpec(key.getBytes(), "AES");
        encryptOrDecryptFile(Cipher.ENCRYPT_MODE,
            new File("src/cactus.jpg"), new File("src/cactus-encrypted.jpg"));
    }

    private void encryptOrDecryptFile(int mode, File inputFile, File outputFile) {
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(mode, secretKey, ivParameterSpec);

            // Read input
            byte[] input = new byte[(int) inputFile.length()];
            FileInputStream inputStream = new FileInputStream(inputFile);
            inputStream.read(input);

            // Encrypt and write to output
            byte[] output = cipher.doFinal(input);
            FileOutputStream outputStream = new FileOutputStream(outputFile);
            outputStream.write(output);

            inputStream.close();
            outputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Шифрование/расшифровка в Javascript

<input type="file" id="file-input" onchange="handleFile(this)">
<button onclick="useEncryptionForFile()" id="encrypt-file">Encrypt File</button>
<button onclick="useDecryptionForFile()" id="decrypt-file">Decrypt File</button>
<textarea id="output"></textarea>
<img id="example">

<script>
    let key = "ThisIsMyGreatKey";
    let iv = "ABCDEFGHabcdefgh";
    let useEncryption, useDecryption;

    let input = document.getElementById("file-input");
    let output = document.getElementById("output");
    let example = document.getElementById("example");

    function handleFile(element) {
        if (element.files && element.files[0]) {
            let file = element.files[0];
            if (useDecryption) {
                decryptFile(file);
            } else {
                encryptFile(file);
            }
        }
    }

    function encryptFile(file) {
        let reader = new FileReader();

        reader.onload = function (e) {
            let encrypted = CryptoJS.AES.encrypt(e.target.result, CryptoJS.enc.Utf8.parse(key), {
                iv: CryptoJS.enc.Utf8.parse(iv),
                mode: CryptoJS.mode.CBC,
                padding: CryptoJS.pad.Pkcs7
            });

            output.textContent = encrypted;

            let a = document.createElement("a");
            a.setAttribute('href', 'data:application/octet-stream,' + encrypted);
            a.setAttribute('download', file.name + '.encrypted');
            a.click();
        };

        reader.readAsDataURL(file);
    }

    function decryptFile(file) {
        let reader = new FileReader();
        reader.onload = function (e) {
            let decrypted = CryptoJS.AES.decrypt(e.target.result, CryptoJS.enc.Utf8.parse(key), {
                iv: CryptoJS.enc.Utf8.parse(iv),
                mode: CryptoJS.mode.CBC,
                padding: CryptoJS.pad.Pkcs7
            });

             // Decrypted is emtpy    
            output.textContent = decrypted;

            // Desperate try to get something working
            example.src = "data:image/png;base64," + btoa(decrypted);

            let a = document.createElement("a");
            a.setAttribute('href', decrypted);
            a.setAttribute('download', file.name.replace('encrypted', 'decrypted'));
            a.click();
        };

        reader.readAsText(file);
    }

    function useEncryptionForFile() {
        document.getElementById("encrypt-file").style.backgroundColor = "#757575";
        document.getElementById("decrypt-file").style.backgroundColor = "#FFFFFF";
        useEncryption = true;
      useDecryption = false;
    }

    function useDecryptionForFile() {
        document.getElementById("encrypt-file").style.backgroundColor = "#FFFFFF";
        document.getElementById("decrypt-file").style.backgroundColor = "#757575";
        useDecryption = true;
      useEncryption = false;
    }
</script>    

Я также сделал Fiddle на случай, если вам понравится больше :), и источник Java можно скачать здесь.

В источнике Java я использовал файл cactus.jpg в качестве файла, но можно использовать любой файл :). Кактус можно найти здесь.

Как я могу расшифровать файл, который был зашифрован в Java? Я попытался преобразовать содержимое blob в String, извлекая данные как ArrayBuffer и преобразовывая его в String, получая его как текст и передавая его методу дешифрования, но ничего не работает.

Библиотека, которую я использую в Javascript, - это CryptoJS, а в Java - стандартные библиотеки Crypto.

Я нашел другие похожие (1, 2) вопросы. Однако, я думаю, они слишком сильно отличаются друг от друга, так как ответ на эти вопросы не касается этой проблемы, а скорее небольшая ошибка.

Если я забуду какие-либо данные, скажите, пожалуйста.

Ответ 1

Проблема заключается в том, что вы интерпретируете результат дешифрования как строку UTF8. Это не так, как это работает. Файлы - это просто произвольные байты, они не обязательно составляют строку UTF8. Результат вашего дешифрования правильный, если вы просто не пытаетесь интерпретировать его как UTF8.

Ответ 2

Во-первых, попробуйте отправить простой зашифрованный текст из java в javascript или наоборот и проверить, работает ли код.

Если код работает для простого текста, т.е. Вы можете отправить зашифрованную строку из Java и успешно расшифровать ее на JavaScript или наоборот, то что вы можете сделать, это Base64 закодировать зашифрованные байты/файл, а затем передать текст и затем декодировать и расшифровывать его на другом конце.

Если код не работает для простого текста, вы можете попробовать зашифровать простой текст в javascript и java самостоятельно и проверить, являются ли результаты одинаковыми. В противном случае существует некоторая несоответствие в логике шифрования/дешифрования между java и javascript.

РЕДАКТИРОВАТЬ:

Поскольку вы упомянули, что код работает для String, у меня есть пример ниже для преобразования файла в Base64 String с использованием общей библиотеки кодеков apache в java.

private static String encodeFileToBase64Binary(String fileName) throws IOException {
    File file = new File(fileName);
    byte[] encoded = Base64.encodeBase64(FileUtils.readFileToByteArray(file));
    return new String(encoded, StandardCharsets.US_ASCII);
}

Теперь вы шифруете эту строку и отправляете ее в javascript. В javascript сначала дешифровать строку, а затем преобразовать ее в файл-объект.

Например.

 function base64toFile(encodedstring,filename,mimeType){
   return new File([encodedstring.arrayBuffer()],filename, {type:mimeType});

}   

//Usage example:
base64toFile('aGVsbG8gd29ybGQ=', 'hello.txt', 'text/plain');

Ответ 3

Ссылка, которая была дана в разделе комментариев, частично решает проблему. Он шифрует и расшифровывает кросс-платформу, но он довольно медленный из-за PBKDF2 с хешированием SHA256. Я не могу найти способ использовать только часть AES, а не часть PKBDF2.

Цель PBKDF2 состоит в том, чтобы превратить пользовательский пароль (который обычно является текстовой строкой переменной длины и редко имеет более нескольких десятков бит эффективной энтропии *) в ключ AES (который должен быть двоичной строкой точно 128, 192 или 256 бит, в зависимости от используемого варианта AES, и должны иметь полную энтропию **, если вы хотите, чтобы шифр был таким же сильным, как и предполагалось).

Чтобы сделать это, он должен быть медленным; единственный способ угадать пароль с, скажем, 30 битами энтропии так же сложно, как угадать ключ AES с 128 бит энтропии, было бы заставить процесс преобразования пароля в ключ взять столько времени, сколько 2 128 - 30= 2 98 AES-шифрования. Конечно, это действительно невозможно, поэтому на практике люди склонны применять всего несколько тысяч (≈ 2 10) до нескольких миллиардов (≈ 2 30) итераций PBKDF2 и надеются, что этого достаточно. Что это может быть, если ваш счетчик итераций находится ближе к верхней границе диапазона, если пользователь достаточно умен и достаточно мотивирован, чтобы выбрать достаточно хороший пароль (скажем, случайная Diceware ключевой фразу вместо abc123 или pa$$w0rd) для начала, и если ваш противник не является NSA.

Во всяком случае, суть всего, что, если вы хотите использовать шифрование на основе паролей, вам в значительной степени придется использовать PBKDF2 (или что-то подобное). Однако это не обязательно означает, что вы должны запускать медленную функцию деривации ключей каждый раз, когда вы шифруете или дешифруете что-то. На самом деле, если вы знаете, что вам нужно будет зашифровать или дешифровать несколько файлов с помощью одного и того же пароля, гораздо эффективнее извлечь ключ AES из пароля один раз, а затем сохранить ключ AES в памяти столько, сколько вам нужно Это. (Делать это безопасно тоже не тривиально, но многие крипто-библиотеки будут обрабатывать его, по крайней мере, достаточно хорошо, если вы будете использовать свои встроенные ключевые объекты для хранения ключей, и в любом случае это вряд ли будет самой слабой ссылкой в вашей безопасности приложения.)

Другим вариантом, конечно же, является не использование паролей вообще, а просто создание (псевдо) случайного ключа AES (с использованием криптографически безопасного генератора случайных битовых строк) и сохранение на всех устройствах, которым необходим доступ к нему. Опять же, конечно, сложная часть здесь безопасно хранит ключ.

В частности, если вы выполняете криптографию на стороне клиента с помощью JavaScript в браузере, то просто встраивание ключа в код JS (или где-либо еще на странице) выведет его всем, у кого есть доступ к консоли браузера dev (и также может привести к тому, что ключ остается слева, например, в кеше браузера). И, конечно же, вы должны использовать HTTPS, так как в противном случае кто-либо, например, подслушивает общедоступное Wi-Fi-соединение клиента, также может захватить копию ключа.


Ps. Возможно, вы заметили, что на самом деле я не включил какой-либо код, показывающий, как сделать обычное шифрование AES с помощью PBKDF2 выше. Я не сделал этого по двум причинам:

  1. Если вы не понимаете крипто-примитивов, с которыми вы работаете достаточно хорошо, чтобы, например, отделить вывод ключа от шифрования, тогда вам действительно не следует писать криптокод (как и все, кроме игрушечного упражнения).

    Это может показаться суровым, но это реальность - в отличие от многих других подполей программирования, где вы можете просто взять фрагмент кода, который вы не полностью понимаете, и настраивать его до тех пор, пока он не работает, с криптографическим кодом (и с кодом безопасности в вообще), вам нужно знать, что вы делаете, и делать это в первый раз.

    С другими типами кода, что-то, что, кажется, работает большую часть времени, часто достаточно хорошо, и вы можете исправить любые оставшиеся ошибки по мере их появления. С крипто, глючный код может быть легко полностью небезопасно, глядя, как она прекрасно работает, и вы не узнаете, что она сломана, пока кто-нибудь не ломает его и крадет все данные, которые вы работали так трудно держать в секрете. Или, возможно, только после того, как это уже произошло.

  2. В любом случае коллекция образцов кода, с которыми вы связаны (<disclaimer> и которые я еще не рассмотрел подробно </disclaimer>), уже включает в себя набор методов, которые принимают двоичные ключи AES и не используют PBKDF2. В частности, это методы encryptString() encrypt() и decrypt() (в отличие от encryptString() и decryptString(), которые используют PBKDF2).

    Обратите внимание, что при вызове encrypt() и/или decrypt() вам нужно будет предоставить ключ AES в формате, ожидаемом при реализации. В общем, это может зависеть от базовой крипто-библиотеки, используемой кодом. Например, реализация Java, по-видимому, ожидает массив размером 128/8 = 16 элементов byte[]. (Было бы неплохо, если бы он также мог напрямую использовать объект SecretKeySpec, но это не так.) Вместо реализации JS WebCrypto требуется 16-байтовый Uint8Array. Это, по- видимому, отлично подходит для реализации node.js, хотя он также может принимать Buffer.


*) То есть, приличная программа для взлома паролей, основанная на глубоких знаниях общих методов и привычек для человеческого пароля, редко будет принимать более нескольких миллиардов (≈ 2 30) или триллионов (≈ 2 40) попыток угадать большинство человеко-выбранных паролей.Фактически, в зависимости от того, насколько ленивы или неопытные ваши пользователи, даже просто тестирование нескольких тысяч наиболее распространенных паролей может быть весьма эффективным.

**) То есть, угадывание ключа не должно быть проще, чем угадать совершенно случайно выбранную битовую строку той же длины.