Seeking in AES-CTR-encrypted input

2019-03-19 03:52发布

As AES in CTR mode is great for random access, lets say I have a data source created with a CipherOutputStream in AES-CTR mode. The library underneath—which is not mine—uses a RandomAccessFile that allows to seek to a specific byte offset in the file.

My initial thought would be to use a CipherInputStream with a Cipher initialized with the right parameters, but the API for that doesn't do seeking and states to not support mark and reset.

Is there a part of the API that I've missed that can do this for me, should I look into the configuration of CTR's IV/block counter and recreate that with a custom input stream (which sounds like shotgun aimed at self to me) or take some other approach I've missed?

1条回答
等我变得足够好
2楼-- · 2019-03-19 04:36

Right, noticed I just got a Tumbleweed badge for this, 'yay'...

I ended up looking up exactly how the IV is updated in CTR mode. This turns out to do a simple +1 for each AES block it processes. I implemented reading along the following lines.

Given a class that implements a read-like method that would read the next byte in a byte sequence that is encrypted and needs to support seeking in that sequence and the following variables:

  • BLOCK_SIZE: fixed at 16 (128 bits, AES block size);
  • cipher: an instance of javax.crypto.Cipher, initialized to deal with AES;
  • delegate: a java.io.InputStream that wraps an encrypted resource that allows random access;
  • input: a javax.crypto.CipherInputStream we'll be serving reads from (the stream will take care of the decryption).

The seek method is implemented as such:

void seek(long pos) {
    // calculate the block number that contains the byte we need to seek to
    long block = pos / BLOCK_SIZE;
    // allocate a 16-byte buffer
    ByteBuffer buffer = ByteBuffer.allocate(BLOCK_SIZE);
    // fill the first 12 bytes with the original IV (the iv minus the actual counter value)
    buffer.put(cipher.getIV(), 0, BLOCK_SIZE - 4);
    // set the counter of the IV to the calculated block index + 1 (counter starts at 1)
    buffer.putInt(block + 1);
    IvParameterSpec iv = new IvParameterSpec(buffer.array());
    // re-init the Cipher instance with the new IV
    cipher.init(Cipher.ENCRYPT_MODE, key, iv);
    // seek the delegate wrapper (like seek() in a RandomAccessFile and 
    // recreate the delegate stream to read from the new location)
    // recreate the input stream we're serving reads from
    input = new CipherInputStream(delegate, cipher);
    // next read will be at the block boundary, need to skip some bytes to arrive at pos
    int toSkip = (int) (pos % BLOCK_SIZE);
    byte[] garbage = new byte[toSkip];
    // read bytes into a garbage array for as long as we need (should be max BLOCK_SIZE
    // bytes
    int skipped = input.read(garbage, 0, toSkip);
    while (skipped < toSkip) {
        skipped += input.read(garbage, 0, toSkip - skipped);
    }

    // at this point, the CipherStream is positioned at pos, next read will serve the 
    // plain byte at pos
}

Note that seeking the delegate resource is omitted here, as this depends on what is underneath the delegate InputStream. Also note that the initial IV is required to be started at counter 1 (the last 4 bytes).

Unittests show that this approach works (performance benchmarks will be done at some point in the future :)).

查看更多
登录 后发表回答