Разделение симметричного шифрования AES

Сценарий 1 (рабочий базовый уровень):

Первый вариант использования прост и реализован/работает.

  1. В Java запишите поток на диск одним махом.
  2. Оберните выходной поток симметричным шифром, чтобы содержимое на диске было зашифровано.
  3. Позже прочитайте с диска. Оберните входной поток одним и тем же симметричным шифром одним махом, чтобы содержимое, извлеченное из входного потока, было открытым текстом и соответствовало оригиналу.

Сценарий 2 (подходящее решение не найдено):

  1. В Java запишите поток на диск.
  2. Разрешить добавление последующих байтов ("фрагментов") к файлу.
  3. Оберните выходной поток симметричным шифром, чтобы содержимое на диске было зашифровано.
  4. Используйте один и тот же шифр, чтобы все фрагменты были зашифрованы одинаково.
  5. Позже прочитайте с диска. Оберните входной поток одним и тем же симметричным шифром одним махом, чтобы содержимое, извлеченное из входного потока, было открытым текстом и соответствовало оригиналу.

Постановка проблемы:

Шифрование и дешифрование «abc» не дает того же результата, что и шифрование и дешифрование «a», «b» и «c» по отдельности, и поэтому «разбитый на части» файл, описанный в варианте использования 2, не будет успешно расшифрован.

// e.g.
decrypt(encrypt("abc")) != decrypt(encrypt("a") + encrypt("b") + encrypt("c"))

Актуальный вопрос:

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


person Robert Christian    schedule 30.01.2015    source источник
comment
Шутки в сторону ... учитывая вашу текущую постановку проблемы, нет решения, которое соответствовало бы вашим заявленным требованиям. Вы определенно ограничены в использовании алгоритма шифрования, где encrypt("abc") != encrypt("a") + encrypt("b") + encrypt("c")? Когда вы говорите (without requiring knowledge of indexes where file was appended), означает ли это, что вам запрещено использовать какие-либо средства записи или обнаружения специальной последовательности байтов для обозначения начала или длины фрагмента?   -  person gknicker    schedule 30.01.2015
comment
@gnicker, означает ли это, что вам запрещено использовать какие-либо средства записи или обнаружения специальной последовательности байтов для указания начала или длины фрагмента? - В идеале мне не нужно было бы сохранять метаданные о том, что мы добавили этот файл в байты 0, 1052, 10002331 и 232323231. Так что относитесь к каждому из этих разделов ([0,1052], [10002331] и т. д.) как к отдельно зашифрованному подразделы. Я бы предпочел построить входной поток для всего файла, т.е. [0 ,len(file)] и обернуть его одним потоком дешифрования, который не знает о точках останова.   -  person Robert Christian    schedule 31.01.2015
comment
Аналогичный вопрос - stackoverflow.com/ вопросы/10283637/   -  person Robert Christian    schedule 31.01.2015
comment
Это не может работать вообще для AES. Если вы можете ограничить свои точки останова кратным размеру блока, тогда это возможно.   -  person Artjom B.    schedule 31.01.2015
comment
@ArtjomB. - Альтернатива управлению точками останова, почему бы не использовать PKCS5Padding?   -  person Robert Christian    schedule 04.02.2015


Ответы (2)


К сожалению, в этом случае вы не можете получить свой торт и съесть его.

Вы должны либо

  1. записать несколько байтов длины в начале каждого фрагмента или
  2. использовать алгоритм шифрования, где decrypt(encrypt("abc")) == decrypt(encrypt("a") + encrypt("b") + encrypt("c")) (он же тривиальный и не рекомендуется)

Номер 1, безусловно, лучший выбор, и он проще, чем вы думаете. Подробности ниже.

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

Подробнее о номере 1

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

  1. Прочитайте первые четыре байта и преобразуйте их в целое число n.
  2. Прочитайте следующие n байта и расшифруйте.
  3. Прочитайте следующие четыре байта и преобразуйте их в целое число n.
  4. Прочитайте следующие n байт, расшифруйте и добавьте к первому расшифрованному фрагменту.
  5. Повторяйте шаги 3 и 4, пока не будет достигнут конец файла.

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

person gknicker    schedule 31.01.2015
comment
Виженер не сработает, потому что он основан на персонажах и очень слаб. - person Robert Christian; 04.02.2015
comment
Второй вариант нуждается в доработке. Я отредактировал исходный пост, чтобы читать расшифровать (зашифровать (abc)) == расшифровать (зашифровать (a) + зашифровать (b) + зашифровать (c)) вместо зашифровать (abc) == зашифровать (a) + зашифровать ( б) + зашифровать(в) - person Robert Christian; 04.02.2015
comment
Я вижу, что первый вариант работает, но для этого потребуется проприетарный поток расшифровки. - person Robert Christian; 04.02.2015

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

public static void appendAES(File file, byte[] data, byte[] key) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
    RandomAccessFile rfile = new RandomAccessFile(file,"rw");
    byte[] iv = new byte[16];
    byte[] lastBlock = null;
    if (rfile.length() % 16L != 0L) {
        throw new IllegalArgumentException("Invalid file length (not a multiple of block size)");
    } else if (rfile.length() == 16) {
        throw new IllegalArgumentException("Invalid file length (need 2 blocks for iv and data)");
    } else if (rfile.length() == 0L) { 
        // new file: start by appending an IV
        new SecureRandom().nextBytes(iv);
        rfile.write(iv);
        // we have our iv, and there's no prior data to reencrypt
    } else { 
        // file length is at least 2 blocks
        rfile.seek(rfile.length()-32); // second to last block
        rfile.read(iv); // get iv
        byte[] lastBlockEnc = new byte[16]; 
            // last block
            // it's padded, so we'll decrypt it and 
            // save it for the beginning of our data
        rfile.read(lastBlockEnc);
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key,"AES"), new IvParameterSpec(iv));
        lastBlock = cipher.doFinal(lastBlockEnc);
        rfile.seek(rfile.length()-16); 
            // position ourselves to overwrite the last block
    } 
    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key,"AES"), new IvParameterSpec(iv));
    byte[] out;
    if (lastBlock != null) { // lastBlock is null if we're starting a new file
        out = cipher.update(lastBlock);
        if (out != null) rfile.write(out);
    }
    out = cipher.doFinal(data);
    rfile.write(out);
    rfile.close();
}

public static void decryptAES(File file, OutputStream out, byte[] key) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
    // nothing special here, decrypt as usual
    FileInputStream fin = new FileInputStream(file);
    byte[] iv = new byte[16];
    if (fin.read(iv) < 16) {
        throw new IllegalArgumentException("Invalid file length (needs a full block for iv)");
    };
    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key,"AES"), new IvParameterSpec(iv));
    byte[] buff = new byte[1<<13]; //8kiB
    while (true) {
        int count = fin.read(buff);
        if (count == buff.length) {
            out.write(cipher.update(buff));
        } else {
            out.write(cipher.doFinal(buff,0,count));
            break;
        }
    }
    fin.close();
}

public static void main(String[] args) throws Exception {

    // prep the new encrypted output file reference
    File encryptedFileSpec = File.createTempFile("chunked_aes_encrypted.", ".test");

    // prep the new decrypted output file reference
    File decryptedFileSpec = File.createTempFile("chunked_aes_decrypted.", ".test");

    // generate a key spec 
    byte[] keySpec = new byte[]{0,12,2,8,4,5,6,7, 8, 9, 10, 11, 12, 13, 14, 15};

    // for debug/test purposes only, keep track of what's written 
    StringBuilder plainTextLog = new StringBuilder();

    // perform chunked output
    for (int i = 0; i<1000; i++) {

        // generate random text of variable length
        StringBuilder text = new StringBuilder();
        Random rand = new Random();
        int  n = rand.nextInt(5) + 1;
        for (int j = 0; j < n; j++) {
            text.append(UUID.randomUUID().toString()); // append random string
        }

        // record it for later comparison
        plainTextLog.append(text.toString());

        // write it out
        byte[] b = text.toString().getBytes("UTF-8");
        appendAES(encryptedFileSpec, b, keySpec);
    }

    System.out.println("Encrypted " + encryptedFileSpec.getAbsolutePath());

    // decrypt
    decryptAES(encryptedFileSpec, new FileOutputStream(decryptedFileSpec), keySpec);
    System.out.println("Decrypted " + decryptedFileSpec.getAbsolutePath());

    // compare expected output to actual
    MessageDigest md = MessageDigest.getInstance("MD5");
    byte[] expectedDigest = md.digest(plainTextLog.toString().getBytes("UTF-8"));

    byte[] expectedBytesEncoded = Base64.getEncoder().encode(expectedDigest);
    System.out.println("Expected decrypted content: " + new String(expectedBytesEncoded));

    byte[] actualBytes = Files.readAllBytes(Paths.get(decryptedFileSpec.toURI()));
    byte[] actualDigest = md.digest(actualBytes);
    byte[] actualBytesEncoded = Base64.getEncoder().encode(actualDigest);
    System.out.println("> Actual decrypted content: " + new String(actualBytesEncoded));


}
person Robert Christian    schedule 04.02.2015