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?
The way encryption works in AES CTR mode is that we generate some random bits with the encryption key provided and the IV. With these random bits we then XOR them with our string. This creates a randomized text.
Out of 128-bit, 192-bit, and 256-bit AES encryption, which progressively use more rounds of encryption for improved security, 128-bit AES encryption is technically the least secure.
AES-CTR mode does not require plaintext padding. However, ESP does require padding to 32-bit word-align the authentication data.
CTR (short for counter) is a popular AES block cipher mode in which every step can be done in parallel. CTR is similar to OFB as it also involves XOR-ing a sequence of pad vectors with the plaintext and ciphertext blocks.
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 :)).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With