blob: 2de88e4d43f88a5382f6c628f27a2a4804c77138 [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 java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Random;
import org.apache.lucene.store.ByteBuffersDataInput;
import org.apache.lucene.store.ByteBuffersDataOutput;
import org.apache.lucene.store.ByteBuffersIndexInput;
import org.apache.lucene.store.ByteBuffersIndexOutput;
import org.apache.lucene.store.IndexInput;
import org.apache.lucene.store.OutputStreamIndexOutput;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.junit.Before;
import org.junit.Test;
import static com.carrotsearch.randomizedtesting.RandomizedTest.*;
import static org.apache.solr.encryption.crypto.AesCtrUtil.*;
/**
* Tests {@link EncryptingIndexOutput}.
*/
public class EncryptingIndexOutputTest extends BaseDataOutputTestCase<EncryptingIndexOutput> {
private byte[] key;
private boolean shouldSimulateWrongKey;
@Before
public void initializeEncryption() {
// AES key length can either 16, 24 or 32 bytes.
key = randomBytesOfLength(randomIntBetween(2, 4) * 8);
shouldSimulateWrongKey = false;
}
/**
* Verifies that the length of the encrypted output is the same as the original data.
* Verifies that all the file pointers in the encrypted output are the same as the original data pointers.
*/
@Test
public void testEncryptionLength() throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
OutputStreamIndexOutput delegateIndexOutput = new OutputStreamIndexOutput("test", "test", baos, 10);
byte[] key = new byte[32];
Arrays.fill(key, (byte) 1);
EncryptingIndexOutput indexOutput = new EncryptingIndexOutput(delegateIndexOutput, key, encrypterFactory()) {
@Override
protected int getBufferCapacity() {
// Reduce the buffer capacity to make sure we often write to the index output.
return AesCtrUtil.AES_BLOCK_SIZE;
}
};
indexOutput.writeByte((byte) 3);
// Check same file pointer.
assertEquals(1, indexOutput.getFilePointer());
byte[] bytes = "tomorrow morning".getBytes(StandardCharsets.UTF_16);
indexOutput.writeBytes(bytes, 0, bytes.length);
// Check same file pointer.
assertEquals(1 + bytes.length, indexOutput.getFilePointer());
indexOutput.close();
// Check the output size is equal to the original size + IV length.
assertEquals(1 + bytes.length + IV_LENGTH, baos.size());
}
/**
* Verify that with a wrong key we don't get the original data when decrypting.
*/
@Test
public void testWrongKey() {
shouldSimulateWrongKey = true;
// Run the testRandomizedWrites which encrypts data and then calls
// toBytes() which decrypts with a wrong key.
LuceneTestCase.expectThrows(AssertionError.class, this::testRandomizedWrites);
}
@Override
protected EncryptingIndexOutput newInstance() {
try {
return new TestBufferedEncryptingIndexOutput(new ByteBuffersDataOutput(), key, encrypterFactory());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
protected byte[] toBytes(EncryptingIndexOutput indexOutput) {
try {
indexOutput.close();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
ByteBuffersDataInput dataInput = ((TestBufferedEncryptingIndexOutput) indexOutput).dataOutput.toDataInput();
IndexInput indexInput = new ByteBuffersIndexInput(dataInput, "Test");
byte[] key = this.key.clone();
if (shouldSimulateWrongKey) {
key[0]++;
}
try (DecryptingIndexInput decryptingIndexInput = new DecryptingIndexInput(indexInput, key, encrypterFactory())) {
byte[] b = new byte[(int) decryptingIndexInput.length()];
decryptingIndexInput.readBytes(b, 0, b.length);
return b;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private AesCtrEncrypterFactory encrypterFactory() {
return randomBoolean() ? CipherAesCtrEncrypter.FACTORY : LightAesCtrEncrypter.FACTORY;
}
/**
* Replaces the {@link java.security.SecureRandom} by a repeatable {@link Random} for tests.
* This is used to generate a repeatable random IV.
*/
private static class TestBufferedEncryptingIndexOutput extends EncryptingIndexOutput {
private final ByteBuffersDataOutput dataOutput;
TestBufferedEncryptingIndexOutput(ByteBuffersDataOutput dataOutput, byte[] key, AesCtrEncrypterFactory encrypterFactory) throws IOException {
super(new ByteBuffersIndexOutput(dataOutput, "Test", "Test"), key, encrypterFactory);
this.dataOutput = dataOutput;
}
@Override
protected byte[] generateRandomIv() {
return randomBytesOfLength(IV_LENGTH);
}
}
}