blob: eff5fe413a3080eb067d27dedb8fb5e3a1760e17 [file] [log] [blame]
/*
* 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.solr.encryption.crypto;
import org.apache.lucene.store.IndexInput;
import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
import static org.apache.solr.encryption.crypto.AesCtrUtil.*;
/**
* {@link IndexInput} that reads from a delegate {@link IndexInput} and decrypts data on the fly.
* <p>It decrypts with the AES algorithm in CTR (counter) mode with no padding. It is appropriate for random access of
* the read-only index files. It can decrypt data previously encrypted with an {@link EncryptingIndexOutput}.
* <p>It first reads the CTR Initialization Vector (IV). This random IV is not encrypted. Then it can decrypt the rest
* of the file, which probably contains a header and footer, with random access.
*
* @see EncryptingIndexOutput
* @see AesCtrEncrypter
*/
public class DecryptingIndexInput extends IndexInput {
/**
* Must be a multiple of {@link AesCtrUtil#AES_BLOCK_SIZE}.
* Benchmarks showed that 6 x {@link AesCtrUtil#AES_BLOCK_SIZE} is a good buffer size.
*/
private static final int BUFFER_CAPACITY = 6 * AES_BLOCK_SIZE; // 96 B
private static final long AES_BLOCK_SIZE_MOD_MASK = AES_BLOCK_SIZE - 1;
// Most fields are not final for the clone() method.
private boolean isClone;
private final long delegateOffset;
private final long sliceOffset;
private final long sliceEnd;
private IndexInput indexInput;
private AesCtrEncrypter encrypter;
private ByteBuffer inBuffer;
private ByteBuffer outBuffer;
private byte[] inArray;
private byte[] oneByteBuf;
private int padding;
private boolean closed;
/**
* @param indexInput The delegate {@link IndexInput} to read and decrypt data from. Its current file pointer may be
* greater than or equal to zero, this allows for example the caller to first read some special
* encryption header followed by a key id to retrieve the key secret.
* @param key The encryption key secret. It is cloned internally, its content is not modified, and no
* reference to it is kept.
* @param factory The factory to use to create one instance of {@link AesCtrEncrypter}. This instance may be cloned.
*/
public DecryptingIndexInput(IndexInput indexInput, byte[] key, AesCtrEncrypterFactory factory) throws IOException {
this("Decrypting " + indexInput.toString(),
indexInput.getFilePointer() + IV_LENGTH,
indexInput.getFilePointer() + IV_LENGTH,
indexInput.length() - indexInput.getFilePointer() - IV_LENGTH,
false,
indexInput,
createEncrypter(indexInput, key, factory));
}
private DecryptingIndexInput(String resourceDescription,
long delegateOffset,
long sliceOffset,
long sliceLength,
boolean isClone,
IndexInput indexInput,
AesCtrEncrypter encrypter) {
super(resourceDescription);
assert delegateOffset >= 0 && sliceOffset >= 0 && sliceLength >= 0;
this.delegateOffset = delegateOffset;
this.sliceOffset = sliceOffset;
this.sliceEnd = sliceOffset + sliceLength;
this.isClone = isClone;
this.indexInput = indexInput;
this.encrypter = encrypter;
encrypter.init(0);
inBuffer = ByteBuffer.allocate(getBufferCapacity());
outBuffer = ByteBuffer.allocate(getBufferCapacity() + AES_BLOCK_SIZE);
outBuffer.limit(0);
assert inBuffer.hasArray() && outBuffer.hasArray();
assert inBuffer.arrayOffset() == 0;
inArray = inBuffer.array();
oneByteBuf = new byte[1];
}
/**
* Creates the {@link AesCtrEncrypter} based on the secret key and the IV at the beginning of the index input.
*/
private static AesCtrEncrypter createEncrypter(IndexInput indexInput,
byte[] key,
AesCtrEncrypterFactory factory)
throws IOException {
byte[] iv = new byte[IV_LENGTH];
indexInput.readBytes(iv, 0, iv.length, false);
return factory.create(key, iv);
}
/**
* Gets the buffer capacity. It must be a multiple of {@link AesCtrUtil#AES_BLOCK_SIZE}.
*/
protected int getBufferCapacity() {
return BUFFER_CAPACITY;
}
@Override
public void close() throws IOException {
if (!closed) {
closed = true;
if (!isClone) {
indexInput.close();
}
}
}
@Override
public long getFilePointer() {
return getPosition() - sliceOffset;
}
/**
* Gets the current internal position in the delegate {@link IndexInput}. It includes IV length.
*/
private long getPosition() {
return indexInput.getFilePointer() - outBuffer.remaining();
}
@Override
public void seek(long position) throws IOException {
if (position < 0) {
throw new IllegalArgumentException("Invalid position=" + position);
}
if (position > length()) {
throw new EOFException("Seek beyond EOF (position=" + position + ", length=" + length() + ") in " + this);
}
long targetPosition = position + sliceOffset;
long delegatePosition = indexInput.getFilePointer();
long currentPosition = delegatePosition - outBuffer.remaining();
if (targetPosition >= currentPosition && targetPosition <= delegatePosition) {
// The target position is within the buffered output. Just move the output buffer position.
outBuffer.position(outBuffer.position() + (int) (targetPosition - currentPosition));
assert targetPosition == delegatePosition - outBuffer.remaining();
} else {
indexInput.seek(targetPosition);
setPosition(targetPosition);
}
}
private void setPosition(long position) {
inBuffer.clear();
outBuffer.clear();
outBuffer.limit(0);
// Compute the counter by ignoring the IV and the delegate offset, if any.
long delegatePosition = position - delegateOffset;
long counter = delegatePosition / AES_BLOCK_SIZE;
encrypter.init(counter);
padding = (int) (delegatePosition & AES_BLOCK_SIZE_MOD_MASK);
inBuffer.position(padding);
}
/**
* Returns the number of encrypted/decrypted bytes in the file.
* <p>It is the logical length of the file, not the physical length. It excludes the IV added artificially to manage
* the encryption. It includes only and all the encrypted bytes (probably a header, content, and a footer).
* <p>With AES/CTR/NoPadding encryption, the length of the encrypted data is identical to the length of the decrypted data.
*/
@Override
public long length() {
return sliceEnd - sliceOffset;
}
@Override
public IndexInput slice(String sliceDescription, long offset, long length) throws IOException {
if (offset < 0 || length < 0 || offset + length > length()) {
throw new IllegalArgumentException("Slice \"" + sliceDescription + "\" out of bounds (offset=" + offset
+ ", sliceLength=" + length + ", fileLength=" + length() + ") of " + this);
}
DecryptingIndexInput slice = new DecryptingIndexInput(getFullSliceDescription(sliceDescription),
delegateOffset, sliceOffset + offset, length, true, indexInput.clone(), encrypter.clone());
slice.seek(0);
return slice;
}
@Override
public byte readByte() throws IOException {
readBytes(oneByteBuf, 0, 1);
return oneByteBuf[0];
}
@Override
public void readBytes(byte[] b, int offset, int length) throws IOException {
if (offset < 0 || length < 0 || offset + length > b.length) {
throw new IllegalArgumentException("Invalid read buffer parameters (offset=" + offset + ", length=" + length
+ ", arrayLength=" + b.length + ")");
}
if (getPosition() + length > sliceEnd) {
throw new EOFException("Read beyond EOF (position=" + (getPosition() - sliceOffset) + ", arrayLength=" + length
+ ", fileLength=" + length() + ") in " + this);
}
while (length > 0) {
// Transfer decrypted bytes from outBuffer.
int outRemaining = outBuffer.remaining();
if (outRemaining > 0) {
if (length <= outRemaining) {
outBuffer.get(b, offset, length);
return;
}
outBuffer.get(b, offset, outRemaining);
assert outBuffer.remaining() == 0;
offset += outRemaining;
length -= outRemaining;
}
readToFillBuffer(length);
decryptBuffer();
}
}
private void readToFillBuffer(int length) throws IOException {
assert length > 0;
int inRemaining = inBuffer.remaining();
if (inRemaining > 0) {
int position = inBuffer.position();
int numBytesToRead = Math.min(inRemaining, length);
indexInput.readBytes(inArray, position, numBytesToRead);
inBuffer.position(position + numBytesToRead);
}
}
private void decryptBuffer() {
assert inBuffer.position() > padding : "position=" + inBuffer.position() + ", padding=" + padding;
inBuffer.flip();
outBuffer.clear();
encrypter.process(inBuffer, outBuffer);
inBuffer.clear();
outBuffer.flip();
if (padding > 0) {
outBuffer.position(padding);
padding = 0;
}
}
@Override
public DecryptingIndexInput clone() {
DecryptingIndexInput clone = (DecryptingIndexInput) super.clone();
clone.isClone = true;
clone.indexInput = indexInput.clone();
assert clone.indexInput.getFilePointer() == indexInput.getFilePointer();
clone.encrypter = encrypter.clone();
clone.inBuffer = ByteBuffer.allocate(getBufferCapacity());
clone.outBuffer = ByteBuffer.allocate(getBufferCapacity() + AES_BLOCK_SIZE);
clone.inArray = clone.inBuffer.array();
clone.oneByteBuf = new byte[1];
// The clone must be initialized.
clone.setPosition(getPosition());
return clone;
}
}