Как эффективно прочитать фрагмент байтов заданного диапазона из большого зашифрованного файла в java?

У меня есть большой зашифрованный файл (10 ГБ+) на сервере. Мне нужно передать расшифрованный файл клиенту небольшими порциями. Когда клиент делает запрос на кусок байтов (скажем, от 18 до 45), я должен получить произвольный доступ к файлу, прочитать определенные байты, расшифровать его и передать клиенту с помощью ServletResponseStream.

Но поскольку файл зашифрован, мне нужно прочитать файл как блоки по 16 байт, чтобы правильно расшифровать.

Итак, если клиент запрашивает получить от 18 байта до 45, на сервере я должен прочитать файл в блоке, кратном 16 байтам. Поэтому мне нужно получить произвольный доступ к файлу с 16 по 48 байт. Затем расшифровать его. После расшифровки я должен пропустить 2 байта из первого и 3 байта из последнего, чтобы вернуть соответствующий фрагмент данных, запрошенный клиентом.

Вот что я пытаюсь сделать

Настройте начало и конец для зашифрованных файлов

long start = 15; // input from client
long end = 45; // input from client
long skipStart = 0; // need to skip for encrypted file
long skipEnd = 0;

// encrypted files, it must be access in blocks of 16 bytes
if(fileisEncrypted){
   skipStart = start % 16;  // skip 2 byte at start
   skipEnd = 16 - end % 16; // skip 3 byte at end
   start = start - skipStart; // start becomes 16
   end = end + skipEnd; // end becomes 48
}

Доступ к зашифрованным данным файла от начала до конца

try(final FileChannel channel = FileChannel.open(services.getPhysicalFile(datafile).toPath())){
    MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_ONLY, start, end-start);

    // *** No idea how to convert MappedByteBuffer into input stream ***
    // InputStream is = (How do I get inputstream for byte 16 to 48 here?)

    // the medhod I used earlier to decrypt the all file atonce, now somehow I need the inputstream of specific range
    is = new FileEncryptionUtil().getCipherInputStream(is,
                        EncodeUtil.decodeSeedValue(encryptionKeyRef), AESCipher.DECRYPT_MODE);

    // transfering decrypted input stream to servlet response
    OutputStream outputStream = response.getOutputStream();
    // *** now for chunk transfer, here I also need to 
    //     skip 2 bytes at the start and 3 bytes from the end. 
    //     How to do it? ***/
    org.apache.commons.io.IOUtils.copy(is, outputStream)
}

Мне не хватает нескольких шагов в приведенном выше коде. Я знаю, что могу попытаться прочитать байт за байтом и игнорировать 2 байта с первого и 3 байта с последнего. Но я не уверен, что это будет достаточно эффективно. Более того, клиент может запросить большой фрагмент, скажем, с 18-го по 2048-й байт, что потребует чтения и расшифровки почти двух гигабайт данных. Я боюсь, что создание большого массива байтов потребует слишком много памяти.

Как я могу сделать это эффективно, не слишком нагружая серверную обработку или память? Любые идеи?


person Burhan Uddin    schedule 12.02.2015    source источник


Ответы (2)


Поскольку вы не указали, какой режим шифрования вы используете, я предполагаю, что вы используете AES в режиме CTR, так как он предназначен для чтения случайных фрагментов больших файлов без необходимости их полной расшифровки.

С помощью AES-CTR вы можете передавать файл через код дешифрования и отправлять блоки обратно клиенту, как только они станут доступны. Таким образом, вам нужно всего несколько массивов размером с блок AES в памяти, все остальное читается с диска. Вам нужно будет добавить специальную логику, чтобы пропустить некоторые байты в первом и последнем блоке (но вам не нужно загружать все это в память).

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

person Augusto    schedule 15.02.2015
comment
Спасибо, я использовал настроение шифрования AES. Пожалуйста, проверьте обновленный вопрос, где единственное, что осталось выяснить, это обрезать последние байты от последнего, чтобы настроить последний блок. - person Burhan Uddin; 15.02.2015

После изучения в течение некоторого времени. Вот как я это решил. Сначала я создал класс ByteBufferInputStream. Читать с MappedByteBuffer

public class ByteBufferInputStream extends InputStream {
    private ByteBuffer byteBuffer;

    public ByteBufferInputStream () {
    }

    /** Creates a stream with a new non-direct buffer of the specified size. The position and limit of the buffer is zero. */
    public ByteBufferInputStream (int bufferSize) {
        this(ByteBuffer.allocate(bufferSize));
        byteBuffer.flip();
    }

    /** Creates an uninitialized stream that cannot be used until {@link #setByteBuffer(ByteBuffer)} is called. */
    public ByteBufferInputStream (ByteBuffer byteBuffer) {
        this.byteBuffer = byteBuffer;
    }

    public ByteBuffer getByteBuffer () {
        return byteBuffer;
    }

    public void setByteBuffer (ByteBuffer byteBuffer) {
        this.byteBuffer = byteBuffer;
    }

    public int read () throws IOException {
        if (!byteBuffer.hasRemaining()) return -1;
        return byteBuffer.get();
    }

    public int read (byte[] bytes, int offset, int length) throws IOException {
        int count = Math.min(byteBuffer.remaining(), length);
        if (count == 0) return -1;
        byteBuffer.get(bytes, offset, count);
        return count;
    }

    public int available () throws IOException {
        return byteBuffer.remaining();
    }
}

Затем создал класс BlockInputStream, расширив InputStream, что позволит пропускать лишние байты и читать внутренний входной поток блоками, кратными 16 байтам.

public class BlockInputStream extends InputStream {
    private final BufferedInputStream inputStream;
    private final long totalLength;
    private final long skip;
    private long read = 0;
    private byte[] buff = new byte[16];
    private ByteArrayInputStream blockInputStream;

    public BlockInputStream(InputStream inputStream, long skip, long length) throws IOException {
        this.inputStream = new BufferedInputStream(inputStream);
        this.skip = skip;
        this.totalLength = length + skip;
        if(skip > 0) {
            byte[] b = new byte[(int)skip];
            read(b);
            b = null;
        }
    }


    private int readBlock() throws IOException {
        int count = inputStream.read(buff);
        blockInputStream = new ByteArrayInputStream(buff);
        return count;
    }

    @Override
    public int read () throws IOException {
        byte[] b = new byte[1];
        read(b);
        return (int)b[1];
    }

    @Override
    public int read(byte[] b) throws IOException {
        return read(b, 0, b.length);
    }

    @Override
    public int read (byte[] bytes, int offset, int length) throws IOException {
        long remaining = totalLength - read;
        if(remaining < 1){
            return -1;
        }
        int bytesToRead = (int)Math.min(length, remaining);
        int n = 0;
        while(bytesToRead > 0){
            if(read % 16 == 0 && bytesToRead % 16 == 0){
                int count = inputStream.read(bytes, offset, bytesToRead);
                read += count;
                offset += count;
                bytesToRead -= count;
                n += count;
            } else {
                if(blockInputStream != null && blockInputStream.available() > 0) {
                    int len = Math.min(bytesToRead, blockInputStream.available());
                    int count = blockInputStream.read(bytes, offset, len);
                    read += count;
                    offset += count;
                    bytesToRead -= count;
                    n += count;
                } else {
                    readBlock();
                }
            }
        }
        return n;
    }

    @Override
    public int available () throws IOException {
        long remaining = totalLength - read;
        if(remaining < 1){
            return -1;
        }
        return inputStream.available();
    }

    @Override
    public long skip(long n) throws IOException {
        return inputStream.skip(n);
    }

    @Override
    public void close() throws IOException {
        inputStream.close();
    }

    @Override
    public synchronized void mark(int readlimit) {
        inputStream.mark(readlimit);
    }

    @Override
    public synchronized void reset() throws IOException {
        inputStream.reset();
    }

    @Override
    public boolean markSupported() {
        return inputStream.markSupported();
    }
}

Это моя последняя рабочая реализация с использованием этих двух классов.

private RangeData getRangeData(RangeInfo r) throws IOException, GeneralSecurityException, CryptoException {

    // used for encrypted files
    long blockStart = r.getStart();
    long blockEnd = r.getEnd();
    long blockLength = blockEnd - blockStart + 1;

    // encrypted files, it must be access in blocks of 16 bytes
    if(datafile.isEncrypted()){
        blockStart -= blockStart % 16;
        blockEnd = blockEnd | 15; // nearest multiple of 16 for length n = ((n−1)|15)+1
        blockLength = blockEnd - blockStart + 1;
    }

    try ( final FileChannel channel = FileChannel.open(services.getPhysicalFile(datafile).toPath()) )
    {
        MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_ONLY, blockStart, blockLength);
        InputStream inputStream = new ByteBufferInputStream(mappedByteBuffer);
        if(datafile.isEncrypted()) {
            String encryptionKeyRef = (String) settingsManager.getSetting(AppSetting.DEFAULT_ENCRYPTION_KEY);
            inputStream = new FileEncryptionUtil().getCipherInputStream(inputStream,
                    EncodeUtil.decodeSeedValue(encryptionKeyRef), AESCipher.DECRYPT_MODE);
            long skipStart = r.getStart() - blockStart;
            inputStream = new BlockInputStream(inputStream, skipStart, r.getLength()); // this will trim the data to n bytes at last
        }
        return new RangeData(r, inputStream);
    }
}
person Burhan Uddin    schedule 15.02.2015