PositionedCipherInputStream.java
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.crypto.stream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.util.Properties;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.ShortBufferException;
import org.apache.commons.crypto.cipher.Cipher;
import org.apache.commons.crypto.cipher.CipherFactory;
import org.apache.commons.crypto.stream.input.Input;
import org.apache.commons.crypto.utils.IOUtils;
import org.apache.commons.crypto.utils.Utils;
import static org.apache.commons.crypto.cipher.CipherTransformation.AES_CTR_NOPADDING;
/**
* PositionedCipherInputStream provides the capability to decrypt the stream starting
* at random position as well as provides the foundation for positioned read for
* decrypting. This needs a stream cipher mode such as AES CTR mode.
*/
public class PositionedCipherInputStream extends CTRCipherInputStream {
/**
* DirectBuffer pool
*/
private final Queue<ByteBuffer> bufferPool = new
ConcurrentLinkedQueue<ByteBuffer>();
/**
* Cipher pool
*/
private final Queue<CipherState> cipherPool = new
ConcurrentLinkedQueue<CipherState>();
public PositionedCipherInputStream(Properties props, Input in,
byte[] key, byte[] iv, long streamOffset) throws IOException {
this(in, Utils.getCipherInstance(AES_CTR_NOPADDING, props),
Utils.getBufferSize(props), key, iv, streamOffset);
}
public PositionedCipherInputStream(
Input input,
Cipher cipher,
int bufferSize,
byte[] key,
byte[] iv,
long streamOffset) throws IOException {
super(input, cipher, bufferSize, key, iv, streamOffset);
}
/**
* Read upto the specified number of bytes from a given position
* within a stream and return the number of bytes read. This does not
* change the current offset of the stream, and is thread-safe.
*/
public int read(long position, byte[] buffer, int offset, int length)
throws IOException {
checkStream();
final int n = input.read(position, buffer, offset, length);
if (n > 0) {
// This operation does not change the current offset of the file
decrypt(position, buffer, offset, n);
}
return n;
}
/**
* Read the specified number of bytes from a given position within a stream.
* This does not change the current offset of the stream and is thread-safe.
*/
public void readFully(long position, byte[] buffer, int offset, int length)
throws IOException {
checkStream();
IOUtils.readFully(input, position, buffer, offset, length);
if (length > 0) {
// This operation does not change the current offset of the file
decrypt(position, buffer, offset, length);
}
}
public void readFully(long position, byte[] buffer) throws IOException {
readFully(position, buffer, 0, buffer.length);
}
/**
* Decrypt length bytes in buffer starting at offset. Output is also put
* into buffer starting at offset. It is thread-safe.
*/
protected void decrypt(long position, byte[] buffer, int offset, int length)
throws IOException {
ByteBuffer inBuffer = getBuffer();
ByteBuffer outBuffer = getBuffer();
CipherState state = null;
try {
state = getCipherState();
byte[] iv = getInitIV().clone();
resetCipher(state, position, iv);
byte padding = getPadding(position);
inBuffer.position(padding); // Set proper position for input data.
int n = 0;
while (n < length) {
int toDecrypt = Math.min(length - n, inBuffer.remaining());
inBuffer.put(buffer, offset + n, toDecrypt);
// Do decryption
decrypt(state, inBuffer, outBuffer, padding);
outBuffer.get(buffer, offset + n, toDecrypt);
n += toDecrypt;
padding = postDecryption(state, inBuffer, position + n, iv);
}
} finally {
returnBuffer(inBuffer);
returnBuffer(outBuffer);
returnCipherState(state);
}
}
/**
* Do the decryption using inBuffer as input and outBuffer as output.
* Upon return, inBuffer is cleared; the decrypted data starts at
* outBuffer.position() and ends at outBuffer.limit();
*/
private void decrypt(CipherState state, ByteBuffer inBuffer,
ByteBuffer outBuffer, byte padding) throws IOException {
Utils.checkState(inBuffer.position() >= padding);
if(inBuffer.position() == padding) {
// There is no real data in inBuffer.
return;
}
inBuffer.flip();
outBuffer.clear();
decryptBuffer(state, inBuffer, outBuffer);
inBuffer.clear();
outBuffer.flip();
if (padding > 0) {
/*
* The plain text and cipher text have a 1:1 mapping, they start at the
* same position.
*/
outBuffer.position(padding);
}
}
private void decryptBuffer(CipherState state, ByteBuffer inBuffer, ByteBuffer outBuffer)
throws IOException {
int inputSize = inBuffer.remaining();
try {
int n = state.getCipher().update(inBuffer, outBuffer);
if (n < inputSize) {
/**
* Typically code will not get here. Cipher#update will consume all
* input data and put result in outBuffer.
* Cipher#doFinal will reset the cipher context.
*/
state.getCipher().doFinal(inBuffer, outBuffer);
state.reset(true);
}
} catch (ShortBufferException e) {
throw new IOException(e);
} catch (IllegalBlockSizeException e) {
throw new IOException(e);
} catch (BadPaddingException e) {
throw new IOException(e);
}
}
/**
* This method is executed immediately after decryption. Check whether
* cipher should be updated and recalculate padding if needed.
*/
private byte postDecryption(CipherState state, ByteBuffer inBuffer,
long position, byte[] iv) throws IOException {
byte padding = 0;
if (state.isReset()) {
/*
* This code is generally not executed since the cipher usually
* maintains cipher context (e.g. the counter) internally. However,
* some implementations can't maintain context so a re-init is necessary
* after each decryption call.
*/
resetCipher(state, position, iv);
padding = getPadding(position);
inBuffer.position(padding);
}
return padding;
}
/** Calculate the counter and iv, reset the cipher. */
private void resetCipher(CipherState state, long position, byte[] iv)
throws IOException {
final long counter = getCounter(position);
Utils.calculateIV(getInitIV(), counter, iv);
try {
state.getCipher().init(Cipher.DECRYPT_MODE, getKey(), iv);
} catch (InvalidKeyException e) {
throw new IOException(e);
} catch (InvalidAlgorithmParameterException e) {
throw new IOException(e);
}
state.reset(false);
}
/** Get Cipher from pool */
private CipherState getCipherState() throws IOException {
CipherState state = cipherPool.poll();
if (state == null) {
Cipher cipher;
try {
cipher = CipherFactory.getInstance(getCipher().getTransformation(),
getCipher().getProperties());
} catch (GeneralSecurityException e) {
throw new IOException(e);
}
state = new CipherState(cipher);
}
return state;
}
/** Return Cipher to pool */
private void returnCipherState(CipherState state) {
if (state != null) {
cipherPool.add(state);
}
}
/** Get direct buffer from pool */
private ByteBuffer getBuffer() {
ByteBuffer buffer = bufferPool.poll();
if (buffer == null) {
buffer = ByteBuffer.allocateDirect(getBufferSize());
}
return buffer;
}
/** Return direct buffer to pool */
private void returnBuffer(ByteBuffer buf) {
if (buf != null) {
buf.clear();
bufferPool.add(buf);
}
}
@Override
public void close() throws IOException {
if (!isOpen()) {
return;
}
cleanBufferPool();
super.close();
}
/** Clean direct buffer pool */
private void cleanBufferPool() {
ByteBuffer buf;
while ((buf = bufferPool.poll()) != null) {
Utils.freeDirectBuffer(buf);
}
}
private class CipherState {
private Cipher cipher;
private boolean reset;
public CipherState(Cipher cipher) {
this.cipher = cipher;
this.reset = false;
}
public Cipher getCipher() {
return cipher;
}
public boolean isReset() {
return reset;
}
public void reset(boolean reset) {
this.reset = reset;
}
}
}