CtrCryptoInputStream.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.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.security.GeneralSecurityException;
import java.util.Properties;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import org.apache.commons.crypto.cipher.CryptoCipher;
import org.apache.commons.crypto.cipher.CryptoCipherFactory;
import org.apache.commons.crypto.stream.input.ChannelInput;
import org.apache.commons.crypto.stream.input.Input;
import org.apache.commons.crypto.stream.input.StreamInput;
import org.apache.commons.crypto.utils.AES;
import org.apache.commons.crypto.utils.Utils;
/**
* <p>
* CtrCryptoInputStream decrypts data. AES CTR mode is required in order to
* ensure that the plain text and cipher text have a 1:1 mapping. CTR crypto
* stream has stream characteristic which is useful for implement features like
* random seek. The decryption is buffer based. The key points of the decryption
* are (1) calculating the counter and (2) padding through stream position:
* </p>
* <p>
* counter = base + pos/(algorithm blocksize); padding = pos%(algorithm
* blocksize);
* </p>
* The underlying stream offset is maintained as state. It is not thread-safe.
*/
public class CtrCryptoInputStream extends CryptoInputStream {
/**
* <p>
* This method is only for Counter (CTR) mode. Generally the CryptoCipher
* calculates the IV and maintain encryption context internally.For example
* a Cipher will maintain its encryption context internally when we do
* encryption/decryption using the CryptoCipher#update interface.
* </p>
* <p>
* Encryption/Decryption is not always on the entire file. For example, in
* Hadoop, a node may only decrypt a portion of a file (i.e. a split). In
* these situations, the counter is derived from the file position.
* </p>
* The IV can be calculated by combining the initial IV and the counter with
* a lossless operation (concatenation, addition, or XOR).
*
* @see <a
* href="http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_.28CTR.29">
* http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_.28CTR.29</a>
*
* @param initIV initial IV
* @param counter counter for input stream position
* @param IV the IV for input stream position
*/
static void calculateIV(final byte[] initIV, long counter, final byte[] IV) {
int i = IV.length; // IV length
Utils.checkArgument(initIV.length == CryptoCipherFactory.AES_BLOCK_SIZE);
Utils.checkArgument(i == CryptoCipherFactory.AES_BLOCK_SIZE);
int j = 0; // counter bytes index
int sum = 0;
while (i-- > 0) {
// (sum >>> Byte.SIZE) is the carry for addition
sum = (initIV[i] & 0xff) + (sum >>> Byte.SIZE); // NOPMD
if (j++ < 8) { // Big-endian, and long is 8 bytes length
sum += (byte) counter & 0xff;
counter >>>= 8;
}
IV[i] = (byte) sum;
}
}
/**
* Underlying stream offset
*/
private long streamOffset;
/**
* The initial IV.
*/
private final byte[] initIV;
/**
* Initialization vector for the cipher.
*/
private final byte[] iv;
/**
* Padding = pos%(algorithm blocksize); Padding is put into
* {@link #inBuffer} before any other data goes in. The purpose of padding
* is to put the input data at proper position.
*/
private byte padding;
/**
* Flag to mark whether the cipher has been reset
*/
private boolean cipherReset;
/**
* Constructs a {@link CtrCryptoInputStream}.
*
* @param input the input data.
* @param cipher the CryptoCipher instance.
* @param bufferSize the bufferSize.
* @param key crypto key for the cipher.
* @param iv Initialization vector for the cipher.
* @throws IOException if an I/O error occurs.
*/
protected CtrCryptoInputStream(final Input input, final CryptoCipher cipher,
final int bufferSize, final byte[] key, final byte[] iv) throws IOException {
this(input, cipher, bufferSize, key, iv, 0);
}
/**
* Constructs a {@link CtrCryptoInputStream}.
*
* @param input the input data.
* @param cipher the CryptoCipher instance.
* @param bufferSize the bufferSize.
* @param key crypto key for the cipher.
* @param iv Initialization vector for the cipher.
* @param streamOffset the start offset in the stream.
* @throws IOException if an I/O error occurs.
*/
protected CtrCryptoInputStream(final Input input, final CryptoCipher cipher,
final int bufferSize, final byte[] key, final byte[] iv, final long streamOffset)
throws IOException {
super(input, cipher, bufferSize, AES.newSecretKeySpec(key),
new IvParameterSpec(iv));
this.initIV = iv.clone();
this.iv = iv.clone();
CryptoInputStream.checkStreamCipher(cipher);
resetStreamOffset(streamOffset);
}
/**
* Constructs a {@link CtrCryptoInputStream}.
*
* @param inputStream the input stream.
* @param cipher the CryptoCipher instance.
* @param bufferSize the bufferSize.
* @param key crypto key for the cipher.
* @param iv Initialization vector for the cipher.
* @throws IOException if an I/O error occurs.
*/
protected CtrCryptoInputStream(final InputStream inputStream, final CryptoCipher cipher,
final int bufferSize, final byte[] key, final byte[] iv) throws IOException {
this(inputStream, cipher, bufferSize, key, iv, 0);
}
/**
* Constructs a {@link CtrCryptoInputStream}.
*
* @param inputStream the InputStream instance.
* @param cipher the CryptoCipher instance.
* @param bufferSize the bufferSize.
* @param key crypto key for the cipher.
* @param iv Initialization vector for the cipher.
* @param streamOffset the start offset in the stream.
* @throws IOException if an I/O error occurs.
*/
@SuppressWarnings("resource") // Closing the instance closes the StreamInput
protected CtrCryptoInputStream(final InputStream inputStream, final CryptoCipher cipher,
final int bufferSize, final byte[] key, final byte[] iv, final long streamOffset)
throws IOException {
this(new StreamInput(inputStream, bufferSize), cipher, bufferSize, key, iv,
streamOffset);
}
/**
* Constructs a {@link CtrCryptoInputStream}.
*
* @param properties The {@code Properties} class represents a set of
* properties.
* @param inputStream the input stream.
* @param key crypto key for the cipher.
* @param iv Initialization vector for the cipher.
* @throws IOException if an I/O error occurs.
*/
public CtrCryptoInputStream(final Properties properties, final InputStream inputStream, final byte[] key,
final byte[] iv) throws IOException {
this(properties, inputStream, key, iv, 0);
}
/**
* Constructs a {@link CtrCryptoInputStream}.
*
* @param properties The {@code Properties} class represents a set of
* properties.
* @param inputStream the InputStream instance.
* @param key crypto key for the cipher.
* @param iv Initialization vector for the cipher.
* @param streamOffset the start offset in the stream.
* @throws IOException if an I/O error occurs.
*/
@SuppressWarnings("resource") // The CryptoCipher returned by getCipherInstance() is closed by CtrCryptoInputStream.
public CtrCryptoInputStream(final Properties properties, final InputStream inputStream, final byte[] key,
final byte[] iv, final long streamOffset) throws IOException {
this(inputStream, Utils.getCipherInstance(
AES.CTR_NO_PADDING, properties),
CryptoInputStream.getBufferSize(properties), key, iv, streamOffset);
}
/**
* Constructs a {@link CtrCryptoInputStream}.
*
* @param properties The {@code Properties} class represents a set of
* properties.
* @param channel the ReadableByteChannel instance.
* @param key crypto key for the cipher.
* @param iv Initialization vector for the cipher.
* @throws IOException if an I/O error occurs.
*/
public CtrCryptoInputStream(final Properties properties, final ReadableByteChannel channel,
final byte[] key, final byte[] iv) throws IOException {
this(properties, channel, key, iv, 0);
}
/**
* Constructs a {@link CtrCryptoInputStream}.
*
* @param properties The {@code Properties} class represents a set of
* properties.
* @param in the ReadableByteChannel instance.
* @param key crypto key for the cipher.
* @param iv Initialization vector for the cipher.
* @param streamOffset the start offset in the stream.
* @throws IOException if an I/O error occurs.
*/
@SuppressWarnings("resource") // The CryptoCipher returned by getCipherInstance() is closed by CtrCryptoInputStream.
public CtrCryptoInputStream(final Properties properties, final ReadableByteChannel in,
final byte[] key, final byte[] iv, final long streamOffset) throws IOException {
this(in, Utils.getCipherInstance(
AES.CTR_NO_PADDING, properties),
CryptoInputStream.getBufferSize(properties), key, iv, streamOffset);
}
/**
* Constructs a {@link CtrCryptoInputStream}.
*
* @param channel the ReadableByteChannel instance.
* @param cipher the cipher instance.
* @param bufferSize the bufferSize.
* @param key crypto key for the cipher.
* @param iv Initialization vector for the cipher.
* @throws IOException if an I/O error occurs.
*/
protected CtrCryptoInputStream(final ReadableByteChannel channel, final CryptoCipher cipher,
final int bufferSize, final byte[] key, final byte[] iv) throws IOException {
this(channel, cipher, bufferSize, key, iv, 0);
}
/**
* Constructs a {@link CtrCryptoInputStream}.
*
* @param channel the ReadableByteChannel instance.
* @param cipher the CryptoCipher instance.
* @param bufferSize the bufferSize.
* @param key crypto key for the cipher.
* @param iv Initialization vector for the cipher.
* @param streamOffset the start offset in the stream.
* @throws IOException if an I/O error occurs.
*/
@SuppressWarnings("resource") // Closing the instance closes the ChannelInput
protected CtrCryptoInputStream(final ReadableByteChannel channel, final CryptoCipher cipher,
final int bufferSize, final byte[] key, final byte[] iv, final long streamOffset)
throws IOException {
this(new ChannelInput(channel), cipher, bufferSize, key, iv, streamOffset);
}
/**
* Does 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().
*
* @throws IOException if an I/O error occurs.
*/
@Override
protected void decrypt() throws IOException {
Utils.checkState(inBuffer.position() >= padding);
if (inBuffer.position() == padding) {
// There is no real data in inBuffer.
return;
}
inBuffer.flip();
outBuffer.clear();
decryptBuffer(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);
}
}
/**
* Decrypts all data in buf: total n bytes from given start position. Output
* is also buf and same start position. buf.position() and buf.limit()
* should be unchanged after decryption.
*
* @param buf The buffer into which bytes are to be transferred.
* @param offset the start offset in the data.
* @param len the maximum number of decrypted data bytes to read.
* @throws IOException if an I/O error occurs.
*/
protected void decrypt(final ByteBuffer buf, final int offset, final int len)
throws IOException {
final int pos = buf.position();
final int limit = buf.limit();
int n = 0;
while (n < len) {
buf.position(offset + n);
buf.limit(offset + n + Math.min(len - n, inBuffer.remaining()));
inBuffer.put(buf);
// Do decryption
try {
decrypt();
buf.position(offset + n);
buf.limit(limit);
n += outBuffer.remaining();
buf.put(outBuffer);
} finally {
padding = postDecryption(streamOffset - (len - n));
}
}
buf.position(pos);
}
/**
* Does the decryption using out as output.
*
* @param out the output ByteBuffer.
* @throws IOException if an I/O error occurs.
*/
protected void decryptBuffer(final ByteBuffer out) throws IOException {
final int inputSize = inBuffer.remaining();
try {
final int n = cipher.update(inBuffer, out);
if (n < inputSize) {
/**
* Typically code will not get here. CryptoCipher#update will
* consume all input data and put result in outBuffer.
* CryptoCipher#doFinal will reset the cipher context.
*/
cipher.doFinal(inBuffer, out);
cipherReset = true;
}
} catch (final GeneralSecurityException e) {
throw new IOException(e);
}
}
/**
* Does the decryption using inBuffer as input and buf as output. Upon
* return, inBuffer is cleared; the buf's position will be equal to
* <i>p</i> {@code +} <i>n</i> where <i>p</i> is the position
* before decryption, <i>n</i> is the number of bytes decrypted. The buf's
* limit will not have changed.
*
* @param buf The buffer into which bytes are to be transferred.
* @throws IOException if an I/O error occurs.
*/
protected void decryptInPlace(final ByteBuffer buf) throws IOException {
Utils.checkState(inBuffer.position() >= padding);
Utils.checkState(buf.isDirect());
Utils.checkState(buf.remaining() >= inBuffer.position());
Utils.checkState(padding == 0);
if (inBuffer.position() == padding) {
// There is no real data in inBuffer.
return;
}
inBuffer.flip();
decryptBuffer(buf);
inBuffer.clear();
}
/**
* Decrypts more data by reading the under layer stream. The decrypted data
* will be put in the output buffer.
*
* @return The number of decrypted data. -1 if end of the decrypted stream.
* @throws IOException if an I/O error occurs.
*/
@Override
protected int decryptMore() throws IOException {
final int n = input.read(inBuffer);
if (n <= 0) {
return n;
}
streamOffset += n; // Read n bytes
decrypt();
padding = postDecryption(streamOffset);
return outBuffer.remaining();
}
/**
* Gets the counter for input stream position.
*
* @param position the given position in the data.
* @return the counter for input stream position.
*/
protected long getCounter(final long position) {
return position / cipher.getBlockSize();
}
/**
* Gets the initialization vector.
*
* @return the initIV.
*/
protected byte[] getInitIV() {
return initIV;
}
/**
* Gets the padding for input stream position.
*
* @param position the given position in the data.
* @return the padding for input stream position.
*/
protected byte getPadding(final long position) {
return (byte) (position % cipher.getBlockSize());
}
/**
* Gets the offset of the stream.
*
* @return the stream offset.
*/
protected long getStreamOffset() {
return streamOffset;
}
/**
* Gets the position of the stream.
*
* @return the position of the stream.
*/
protected long getStreamPosition() {
return streamOffset - outBuffer.remaining();
}
/**
* Overrides the {@link CtrCryptoInputStream#initCipher()}. Initializes the
* cipher.
*/
@Override
protected void initCipher() {
// Do nothing for initCipher
// Will reset the cipher when reset the stream offset
}
/**
* This method is executed immediately after decryption. Checks whether
* cipher should be updated and recalculate padding if needed.
*
* @param position the given position in the data..
* @return the byte.
* @throws IOException if an I/O error occurs.
*/
protected byte postDecryption(final long position) throws IOException {
byte padding = 0;
if (cipherReset) {
/*
* 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(position);
padding = getPadding(position);
inBuffer.position(padding);
}
return padding;
}
/**
* Overrides the {@link CtrCryptoInputStream#read(ByteBuffer)}. Reads a
* sequence of bytes from this channel into the given buffer.
*
* @param buf The buffer into which bytes are to be transferred.
* @return The number of bytes read, possibly zero, or {@code -1} if the
* channel has reached end-of-stream.
* @throws IOException if an I/O error occurs.
*/
@Override
public int read(final ByteBuffer buf) throws IOException {
checkStream();
int unread = outBuffer.remaining();
if (unread <= 0) { // Fill the unread decrypted data buffer firstly
final int n = input.read(inBuffer);
if (n <= 0) {
return n;
}
streamOffset += n; // Read n bytes
if (buf.isDirect() && buf.remaining() >= inBuffer.position()
&& padding == 0) {
// Use buf as the output buffer directly
decryptInPlace(buf);
padding = postDecryption(streamOffset);
return n;
}
// Use outBuffer as the output buffer
decrypt();
padding = postDecryption(streamOffset);
}
// Copy decrypted data from outBuffer to buf
unread = outBuffer.remaining();
final int toRead = buf.remaining();
if (toRead <= unread) {
final int limit = outBuffer.limit();
outBuffer.limit(outBuffer.position() + toRead);
buf.put(outBuffer);
outBuffer.limit(limit);
return toRead;
}
buf.put(outBuffer);
return unread;
}
/**
* Calculates the counter and iv, resets the cipher.
*
* @param position the given position in the data.
* @throws IOException if an I/O error occurs.
*/
protected void resetCipher(final long position) throws IOException {
final long counter = getCounter(position);
CtrCryptoInputStream.calculateIV(initIV, counter, iv);
try {
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
} catch (final GeneralSecurityException e) {
throw new IOException(e);
}
cipherReset = false;
}
/**
* Resets the underlying stream offset; clear {@link #inBuffer} and
* {@link #outBuffer}. This Typically happens during {@link #skip(long)}.
*
* @param offset the offset of the stream.
* @throws IOException if an I/O error occurs.
*/
protected void resetStreamOffset(final long offset) throws IOException {
streamOffset = offset;
inBuffer.clear();
outBuffer.clear();
outBuffer.limit(0);
resetCipher(offset);
padding = getPadding(offset);
inBuffer.position(padding); // Set proper position for input data.
}
/**
* Seeks the stream to a specific position relative to start of the under
* layer stream.
*
* @param position the given position in the data.
* @throws IOException if an I/O error occurs.
*/
public void seek(final long position) throws IOException {
Utils.checkArgument(position >= 0, "Cannot seek to negative offset.");
checkStream();
/*
* If data of target pos in the underlying stream has already been read
* and decrypted in outBuffer, we just need to re-position outBuffer.
*/
if (position >= getStreamPosition() && position <= getStreamOffset()) {
final int forward = (int) (position - getStreamPosition());
if (forward > 0) {
outBuffer.position(outBuffer.position() + forward);
}
} else {
input.seek(position);
resetStreamOffset(position);
}
}
/**
* Sets the offset of stream.
*
* @param streamOffset the stream offset.
*/
protected void setStreamOffset(final long streamOffset) {
this.streamOffset = streamOffset;
}
/**
* Overrides the {@link CryptoInputStream#skip(long)}. Skips over and
* discards {@code n} bytes of data from this input stream.
*
* @param n the number of bytes to be skipped.
* @return the actual number of bytes skipped.
* @throws IOException if an I/O error occurs.
*/
@Override
public long skip(long n) throws IOException {
Utils.checkArgument(n >= 0, "Negative skip length.");
checkStream();
if (n == 0) {
return 0;
}
if (n <= outBuffer.remaining()) {
final int pos = outBuffer.position() + (int) n;
outBuffer.position(pos);
return n;
}
/*
* Subtract outBuffer.remaining() to see how many bytes we need to
* skip in the underlying stream. Add outBuffer.remaining() to the
* actual number of skipped bytes in the underlying stream to get
* the number of skipped bytes from the user's point of view.
*/
n -= outBuffer.remaining();
long skipped = input.skip(n);
if (skipped < 0) {
skipped = 0;
}
final long pos = streamOffset + skipped;
skipped += outBuffer.remaining();
resetStreamOffset(pos);
return skipped;
}
}