blob: 4a14e0116b1617465edc60992e63ca45cec3e0e5 [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.BufferedChecksum;
import org.apache.lucene.store.IndexOutput;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.zip.CRC32;
import java.util.zip.Checksum;
import static org.apache.solr.encryption.crypto.AesCtrUtil.*;
/**
* {@link IndexOutput} that encrypts data and writes to a delegate {@link IndexOutput} on the fly.
* <p>It encrypts with the AES algorithm in CTR (counter) mode with no padding. It is appropriate for random access of
* the read-only index files. Use a {@link DecryptingIndexInput} to decrypt this file.
* <p>It generates a cryptographically strong random CTR Initialization Vector (IV). This random IV is not encrypted and
* is skipped by any {@link DecryptingIndexInput} reading the written data. Then it can encrypt the rest of the file
* which probably contains a header and footer.
*
* @see DecryptingIndexInput
* @see AesCtrEncrypter
*/
public class EncryptingIndexOutput extends IndexOutput {
/**
* Must be a multiple of {@link AesCtrUtil#AES_BLOCK_SIZE}.
*/
private static final int BUFFER_CAPACITY = 64 * AES_BLOCK_SIZE; // 1024
private final IndexOutput indexOutput;
private final AesCtrEncrypter encrypter;
private final ByteBuffer inBuffer;
private final ByteBuffer outBuffer;
private final byte[] outArray;
private final byte[] oneByteBuf;
private final Checksum clearChecksum;
private long filePointer;
private boolean closed;
/**
* @param indexOutput The delegate {@link IndexOutput} to write encrypted data to. Its current file pointer may be
* greater than or equal to zero, this allows for example the caller to first write some special
* encryption header followed by a key id, to identify the key secret used to encrypt.
* @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 EncryptingIndexOutput(IndexOutput indexOutput, byte[] key, AesCtrEncrypterFactory factory) throws IOException {
super("Encrypting " + indexOutput.toString(), indexOutput.getName());
this.indexOutput = indexOutput;
byte[] iv = generateRandomIv();
encrypter = factory.create(key, iv);
encrypter.init(0);
// IV is written at the beginning of the index output. It's public.
// Even if the delegate indexOutput is positioned after the initial IV, this index output file pointer is 0 initially.
indexOutput.writeBytes(iv, 0, iv.length);
inBuffer = ByteBuffer.allocate(getBufferCapacity());
outBuffer = ByteBuffer.allocate(getBufferCapacity() + AES_BLOCK_SIZE);
assert inBuffer.hasArray() && outBuffer.hasArray();
assert outBuffer.arrayOffset() == 0;
outArray = outBuffer.array();
oneByteBuf = new byte[1];
// Compute the checksum to skip the initial IV, because an external checksum checker will not see it.
clearChecksum = new BufferedChecksum(new CRC32());
}
/**
* Generates a cryptographically strong CTR random IV of length {@link AesCtrUtil#IV_LENGTH}.
*/
protected byte[] generateRandomIv() {
return generateRandomAesCtrIv(SecureRandomProvider.get());
}
/**
* 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;
try {
if (inBuffer.position() != 0) {
encryptBufferAndWrite();
}
} finally {
indexOutput.close();
}
}
}
@Override
public long getFilePointer() {
// With AES/CTR/NoPadding, the encrypted and decrypted data have the same length.
// We return here the file pointer excluding the initial IV length at the beginning of the file.
return filePointer;
}
@Override
public long getChecksum() {
// The checksum is computed on the clear data, excluding the initial IV.
return clearChecksum.getValue();
}
@Override
public void writeByte(byte b) throws IOException {
oneByteBuf[0] = b;
writeBytes(oneByteBuf, 0, oneByteBuf.length);
}
@Override
public void writeBytes(byte[] b, int offset, int length) throws IOException {
if (offset < 0 || length < 0 || offset + length > b.length) {
throw new IllegalArgumentException("Invalid write buffer parameters (offset=" + offset + ", length=" + length + ", arrayLength=" + b.length + ")");
}
clearChecksum.update(b, offset, length);
filePointer += length;
while (length > 0) {
int remaining = inBuffer.remaining();
if (length < remaining) {
inBuffer.put(b, offset, length);
break;
} else {
inBuffer.put(b, offset, remaining);
offset += remaining;
length -= remaining;
encryptBufferAndWrite();
}
}
}
private void encryptBufferAndWrite() throws IOException {
assert inBuffer.position() != 0;
inBuffer.flip();
outBuffer.clear();
encrypter.process(inBuffer, outBuffer);
inBuffer.clear();
outBuffer.flip();
indexOutput.writeBytes(outArray, 0, outBuffer.limit());
}
}