You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by br...@apache.org on 2023/09/18 08:03:53 UTC
[solr-sandbox] branch main updated: Add CharStreamEncrypter. (#71)
This is an automated email from the ASF dual-hosted git repository.
broustant pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr-sandbox.git
The following commit(s) were added to refs/heads/main by this push:
new fc865c1 Add CharStreamEncrypter. (#71)
fc865c1 is described below
commit fc865c133f7553171a67b7369907cf1519ddd787
Author: Bruno Roustant <33...@users.noreply.github.com>
AuthorDate: Mon Sep 18 10:03:48 2023 +0200
Add CharStreamEncrypter. (#71)
---
encryption/build.gradle | 5 +
.../encryption/crypto/CharStreamEncrypter.java | 188 +++++++++++++++++++++
.../encryption/crypto/CipherAesCtrEncrypter.java | 1 +
.../encryption/crypto/DecryptingInputStream.java | 162 ++++++++++++++++++
.../encryption/crypto/EncryptingOutputStream.java | 138 +++++++++++++++
.../encryption/crypto/AesCtrEncrypterTest.java | 27 +--
.../encryption/crypto/CharStreamEncrypterTest.java | 64 +++++++
7 files changed, 567 insertions(+), 18 deletions(-)
diff --git a/encryption/build.gradle b/encryption/build.gradle
index 1e5ccdb..294c2cd 100644
--- a/encryption/build.gradle
+++ b/encryption/build.gradle
@@ -37,6 +37,11 @@ dependencies {
implementation 'org.apache.lucene:lucene-core:9.7.0'
implementation 'com.google.code.findbugs:jsr305:3.0.2'
+ // commons-io and commons-codec are only required by the tool class
+ // CharStreamEncrypter, which is not used for the index encryption.
+ implementation 'commons-io:commons-io:2.11.0'
+ implementation 'commons-codec:commons-codec:1.16.0'
+
testImplementation 'org.apache.solr:solr-test-framework:9.3.0'
testImplementation 'org.apache.lucene:lucene-test-framework:9.7.0'
}
diff --git a/encryption/src/main/java/org/apache/solr/encryption/crypto/CharStreamEncrypter.java b/encryption/src/main/java/org/apache/solr/encryption/crypto/CharStreamEncrypter.java
new file mode 100644
index 0000000..d370a1e
--- /dev/null
+++ b/encryption/src/main/java/org/apache/solr/encryption/crypto/CharStreamEncrypter.java
@@ -0,0 +1,188 @@
+/*
+ * 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.commons.codec.binary.Base64InputStream;
+import org.apache.commons.codec.binary.Base64OutputStream;
+import org.apache.commons.io.input.ReaderInputStream;
+import org.apache.commons.io.output.AppendableWriter;
+import org.apache.commons.io.output.WriterOutputStream;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.Writer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Encrypts a character stream to a base 64 encoding compatible with JSON.
+ * <p>
+ * The whole encryption and base 64 encoding process is streamed, with no large
+ * buffers allocated. The encryption transformation is AES/CTR/NoPadding.
+ * A secure random IV is generated for each encryption and appended as the first
+ * appended chars.
+ */
+public class CharStreamEncrypter {
+
+ private static final int BUFFER_MIN_SIZE = 128;
+ private static final int BUFFER_MAX_SIZE = 8192;
+
+ private final AesCtrEncrypterFactory factory;
+
+ public CharStreamEncrypter(AesCtrEncrypterFactory factory) {
+ this.factory = factory;
+ }
+
+ /**
+ * Encrypts an input string to base 64 characters compatible with JSON.
+ *
+ * @param key AES key, can either 16, 24 or 32 bytes.
+ * @param output where to append the encrypted base 64 chars.
+ * @throws IOException propagates any exception thrown when appending to the output.
+ */
+ public void encrypt(String input, byte[] key, Appendable output)
+ throws IOException {
+ encrypt(new StringReader(input), input.length(), key, output);
+ }
+
+ /**
+ * Encrypts a char reader stream to base 64 characters compatible with JSON.
+ *
+ * @param inputSizeHint optional hint for the input size; or -1 if unknown.
+ * @param key AES key, can either 16, 24 or 32 bytes.
+ * @param output where to append the encrypted base 64 chars.
+ * @throws IOException propagates any exception thrown when appending to the output.
+ */
+ public void encrypt(Reader inputReader, int inputSizeHint, byte[] key, Appendable output)
+ throws IOException {
+ // Don't use jdk Base64.getEncoder().wrap() because it's buggy.
+ int bufferSize = getBufferSize(inputSizeHint);
+ try (OutputStreamWriter encryptedOutputWriter =
+ new OutputStreamWriter(
+ new EncryptingOutputStream(
+ new Base64OutputStream(
+ new LightWriterOutputStream(
+ toWriter(output),
+ StandardCharsets.ISO_8859_1,
+ bufferSize
+ )
+ ),
+ key,
+ factory
+ ),
+ StandardCharsets.UTF_8
+ )
+ ) {
+ transfer(inputReader,
+ encryptedOutputWriter,
+ bufferSize
+ );
+ }
+ }
+
+ /**
+ * Decrypts an input string previously encrypted with {@link #encrypt}.
+ *
+ * @param key AES key, can either 16, 24 or 32 bytes.
+ * @param output where to append the decrypted chars.
+ * @throws IOException propagates any exception thrown when appending to the output.
+ */
+ public void decrypt(String input, byte[] key, Appendable output)
+ throws IOException {
+ decrypt(new StringReader(input), input.length(), key, output);
+ }
+
+ /**
+ * Decrypts a char reader stream previously encrypted with {@link #encrypt}.
+ *
+ * @param inputSizeHint optional hint for the input size; or -1 if unknown.
+ * @param key AES key, can either 16, 24 or 32 bytes.
+ * @param output where to append the decrypted chars.
+ * @throws IOException propagates any exception thrown when appending to the output.
+ */
+ public void decrypt(Reader inputReader, int inputSizeHint, byte[] key, Appendable output)
+ throws IOException {
+ // Don't use jdk Base64.getDecoder().wrap() because it's buggy.
+ int bufferSize = getBufferSize(inputSizeHint);
+ try (InputStreamReader decryptedInputReader =
+ new InputStreamReader(
+ new DecryptingInputStream(
+ new Base64InputStream(
+ new ReaderInputStream(
+ inputReader,
+ StandardCharsets.ISO_8859_1,
+ bufferSize
+ )
+ ),
+ key,
+ factory
+ ),
+ StandardCharsets.UTF_8
+ )
+ ) {
+ transfer(decryptedInputReader,
+ toWriter(output),
+ bufferSize
+ );
+ }
+ }
+
+ private static int getBufferSize(int inputSizeHint) {
+ return inputSizeHint < 0 ? BUFFER_MAX_SIZE
+ : Math.min(Math.max(inputSizeHint / 16, BUFFER_MIN_SIZE), BUFFER_MAX_SIZE);
+ }
+
+ private static Writer toWriter(Appendable appendable) {
+ return appendable instanceof Writer ? (Writer) appendable : new AppendableWriter<>(appendable);
+ }
+
+ /**
+ * Similar to {@link Reader#transferTo(Writer)} with a provided buffer size.
+ */
+ private static void transfer(Reader input, Writer output, int bufferSize)
+ throws IOException {
+ char[] buffer = new char[bufferSize];
+ int nRead;
+ while ((nRead = input.read(buffer, 0, bufferSize)) >= 0) {
+ output.write(buffer, 0, nRead);
+ }
+ }
+
+ /**
+ * Same as {@link WriterOutputStream} without creating a buffer for each
+ * call to {@link #write(int)}.
+ */
+ private static class LightWriterOutputStream extends WriterOutputStream {
+
+ private final byte[] oneByteBuf = new byte[1];
+
+ public LightWriterOutputStream(Writer writer,
+ Charset charset,
+ int bufferSize) {
+ super(writer, charset, bufferSize, false);
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ oneByteBuf[0] = (byte) b;
+ write(oneByteBuf, 0, 1);
+ }
+ }
+}
diff --git a/encryption/src/main/java/org/apache/solr/encryption/crypto/CipherAesCtrEncrypter.java b/encryption/src/main/java/org/apache/solr/encryption/crypto/CipherAesCtrEncrypter.java
index d1bfa90..b11a294 100644
--- a/encryption/src/main/java/org/apache/solr/encryption/crypto/CipherAesCtrEncrypter.java
+++ b/encryption/src/main/java/org/apache/solr/encryption/crypto/CipherAesCtrEncrypter.java
@@ -133,6 +133,7 @@ public class CipherAesCtrEncrypter implements AesCtrEncrypter {
this.iv = iv;
}
+ @Override
public byte[] getIV() {
return iv.clone();
}
diff --git a/encryption/src/main/java/org/apache/solr/encryption/crypto/DecryptingInputStream.java b/encryption/src/main/java/org/apache/solr/encryption/crypto/DecryptingInputStream.java
new file mode 100644
index 0000000..909466f
--- /dev/null
+++ b/encryption/src/main/java/org/apache/solr/encryption/crypto/DecryptingInputStream.java
@@ -0,0 +1,162 @@
+/*
+ * 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.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+import static org.apache.solr.encryption.crypto.AesCtrUtil.AES_BLOCK_SIZE;
+import static org.apache.solr.encryption.crypto.AesCtrUtil.IV_LENGTH;
+
+/**
+ * {@link InputStream} that reads from a delegate {@link InputStream} and decrypts data on the fly.
+ * <p>The encryption transformation is AES/CTR/NoPadding. It decrypts the data previously encrypted
+ * with an {@link EncryptingOutputStream}.
+ * <p>It first reads the CTR Initialization Vector (IV). This random IV is not encrypted. Then it
+ * can decrypt the rest of the file.
+ *
+ * @see EncryptingOutputStream
+ * @see AesCtrEncrypter
+ */
+public class DecryptingInputStream extends InputStream {
+
+ /**
+ * 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 final InputStream inputStream;
+ private final AesCtrEncrypter encrypter;
+ private final ByteBuffer inBuffer;
+ private final ByteBuffer outBuffer;
+ private final byte[] inArray;
+ private final byte[] oneByteBuf;
+ private boolean closed;
+
+ /**
+ * @param inputStream The delegate {@link InputStream} to read and decrypt data from.
+ * @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}.
+ */
+ public DecryptingInputStream(InputStream inputStream, byte[] key, AesCtrEncrypterFactory factory) throws IOException {
+ this.inputStream = inputStream;
+ this.encrypter = createEncrypter(inputStream, key, factory);
+ 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 input stream.
+ */
+ private static AesCtrEncrypter createEncrypter(InputStream inputStream,
+ byte[] key,
+ AesCtrEncrypterFactory factory)
+ throws IOException {
+ byte[] iv = new byte[IV_LENGTH];
+ int n = inputStream.read(iv, 0, iv.length);
+ if (n != iv.length) {
+ throw new IOException("Missing IV");
+ }
+ 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;
+ inputStream.close();
+ }
+ }
+
+ @Override
+ public int read() throws IOException {
+ int n = read(oneByteBuf, 0, 1);
+ return n == -1 ? -1 : oneByteBuf[0] & 0xFF;
+ }
+
+ @Override
+ public int read(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 + ")");
+ }
+ int numRead = 0;
+ while (length > 0) {
+ // Transfer decrypted bytes from outBuffer.
+ int outRemaining = outBuffer.remaining();
+ if (outRemaining > 0) {
+ if (length <= outRemaining) {
+ outBuffer.get(b, offset, length);
+ numRead += length;
+ return numRead;
+ }
+ outBuffer.get(b, offset, outRemaining);
+ numRead += outRemaining;
+ assert outBuffer.remaining() == 0;
+ offset += outRemaining;
+ length -= outRemaining;
+ }
+
+ if (!readToFillBuffer(length)) {
+ return numRead == 0 ? -1 : numRead;
+ }
+ decryptBuffer();
+ }
+ return numRead;
+ }
+
+ private boolean 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);
+ int n = inputStream.read(inArray, position, numBytesToRead);
+ if (n == -1) {
+ return false;
+ }
+ inBuffer.position(position + n);
+ }
+ return true;
+ }
+
+ private void decryptBuffer() {
+ inBuffer.flip();
+ outBuffer.clear();
+ encrypter.process(inBuffer, outBuffer);
+ inBuffer.clear();
+ outBuffer.flip();
+ }
+}
\ No newline at end of file
diff --git a/encryption/src/main/java/org/apache/solr/encryption/crypto/EncryptingOutputStream.java b/encryption/src/main/java/org/apache/solr/encryption/crypto/EncryptingOutputStream.java
new file mode 100644
index 0000000..c5f6da1
--- /dev/null
+++ b/encryption/src/main/java/org/apache/solr/encryption/crypto/EncryptingOutputStream.java
@@ -0,0 +1,138 @@
+/*
+ * 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.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+
+import static org.apache.solr.encryption.crypto.AesCtrUtil.AES_BLOCK_SIZE;
+import static org.apache.solr.encryption.crypto.AesCtrUtil.generateRandomAesCtrIv;
+
+/**
+ * {@link OutputStream} that encrypts data and writes to a delegate {@link OutputStream} on the fly.
+ * <p>The encryption transformation is AES/CTR/NoPadding. Use a {@link DecryptingInputStream} to
+ * decrypt the encrypted data.
+ * <p>It generates a cryptographically strong random CTR Initialization Vector (IV). This random IV
+ * is not encrypted and is skipped by any {@link DecryptingInputStream} reading the written data.
+ * Then it can encrypt the rest of the file.
+ *
+ * @see DecryptingInputStream
+ * @see AesCtrEncrypter
+ */
+public class EncryptingOutputStream extends OutputStream {
+
+ /**
+ * Must be a multiple of {@link AesCtrUtil#AES_BLOCK_SIZE}.
+ */
+ private static final int BUFFER_CAPACITY = 64 * AES_BLOCK_SIZE; // 1024
+
+ private final OutputStream outputStream;
+ private final AesCtrEncrypter encrypter;
+ private final ByteBuffer inBuffer;
+ private final ByteBuffer outBuffer;
+ private final byte[] outArray;
+ private final byte[] oneByteBuf;
+ private boolean closed;
+
+ /**
+ * @param outputStream The delegate {@link OutputStream} to write encrypted data to.
+ * @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}.
+ */
+ public EncryptingOutputStream(OutputStream outputStream, byte[] key, AesCtrEncrypterFactory factory)
+ throws IOException {
+ this.outputStream = outputStream;
+
+ byte[] iv = generateRandomIv();
+ encrypter = factory.create(key, iv);
+ encrypter.init(0);
+ // IV is written at the beginning of the output stream. It's public.
+ outputStream.write(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];
+ }
+
+ /**
+ * 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 {
+ outputStream.close();
+ }
+ }
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ oneByteBuf[0] = (byte) b;
+ write(oneByteBuf, 0, oneByteBuf.length);
+ }
+
+ @Override
+ public void write(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 + ")");
+ }
+ 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();
+ outputStream.write(outArray, 0, outBuffer.limit());
+ }
+}
\ No newline at end of file
diff --git a/encryption/src/test/java/org/apache/solr/encryption/crypto/AesCtrEncrypterTest.java b/encryption/src/test/java/org/apache/solr/encryption/crypto/AesCtrEncrypterTest.java
index 7852424..ef7807d 100644
--- a/encryption/src/test/java/org/apache/solr/encryption/crypto/AesCtrEncrypterTest.java
+++ b/encryption/src/test/java/org/apache/solr/encryption/crypto/AesCtrEncrypterTest.java
@@ -18,15 +18,16 @@ package org.apache.solr.encryption.crypto;
import java.nio.ByteBuffer;
-import org.apache.lucene.tests.util.LuceneTestCase;
+import com.carrotsearch.randomizedtesting.RandomizedTest;
import org.junit.Test;
+import static junit.framework.TestCase.assertEquals;
import static org.apache.solr.encryption.crypto.AesCtrUtil.*;
/**
* Tests {@link AesCtrEncrypter} implementations.
*/
-public class AesCtrEncrypterTest extends LuceneTestCase {
+public class AesCtrEncrypterTest extends RandomizedTest {
/**
* Verifies that {@link AesCtrEncrypter} implementations encrypt and decrypt data exactly the
@@ -35,8 +36,8 @@ public class AesCtrEncrypterTest extends LuceneTestCase {
@Test
public void testEncryptionDecryption() {
for (int i = 0; i < 100; i++) {
- ByteBuffer clearData = generateRandomData(10000);
- byte[] key = generateRandomBytes(AES_BLOCK_SIZE);
+ ByteBuffer clearData = generateRandomData(randomIntBetween(5000, 10000));
+ byte[] key = randomBytesOfLength(randomIntBetween(2, 4) * 8);
byte[] iv = generateRandomAesCtrIv(SecureRandomProvider.get());
AesCtrEncrypter encrypter1 = encrypterFactory().create(key, iv);
AesCtrEncrypter encrypter2 = encrypterFactory().create(key, iv);
@@ -54,7 +55,7 @@ public class AesCtrEncrypterTest extends LuceneTestCase {
private AesCtrEncrypterFactory encrypterFactory() {
if (LightAesCtrEncrypter.isSupported()) {
- return random().nextBoolean() ? CipherAesCtrEncrypter.FACTORY : LightAesCtrEncrypter.FACTORY;
+ return randomBoolean() ? CipherAesCtrEncrypter.FACTORY : LightAesCtrEncrypter.FACTORY;
}
return CipherAesCtrEncrypter.FACTORY;
}
@@ -62,29 +63,19 @@ public class AesCtrEncrypterTest extends LuceneTestCase {
private static ByteBuffer generateRandomData(int numBytes) {
ByteBuffer buffer = ByteBuffer.allocate(numBytes);
for (int i = 0; i < numBytes; i++) {
- buffer.put((byte) random().nextInt());
+ buffer.put((byte) randomInt());
}
buffer.position(0);
return buffer;
}
- private static byte[] generateRandomBytes(int numBytes) {
- byte[] b = new byte[numBytes];
- // Random.nextBytes(byte[]) does not produce good enough randomness here,
- // it has a bias to produce 0 and -1 bytes.
- for (int i = 0; i < numBytes; i++) {
- b[i] = (byte) random().nextInt();
- }
- return b;
- }
-
private ByteBuffer crypt(ByteBuffer inputBuffer, AesCtrEncrypter encrypter) {
encrypter = randomClone(encrypter);
encrypter.init(0);
int inputInitialPosition = inputBuffer.position();
ByteBuffer outputBuffer = ByteBuffer.allocate(inputBuffer.capacity());
while (inputBuffer.remaining() > 0) {
- int length = Math.min(random().nextInt(51) + 1, inputBuffer.remaining());
+ int length = Math.min(randomIntBetween(0, 50) + 1, inputBuffer.remaining());
ByteBuffer inputSlice = inputBuffer.slice();
inputSlice.limit(inputSlice.position() + length);
encrypter.process(inputSlice, outputBuffer);
@@ -96,6 +87,6 @@ public class AesCtrEncrypterTest extends LuceneTestCase {
}
private static AesCtrEncrypter randomClone(AesCtrEncrypter encrypter) {
- return random().nextBoolean() ? encrypter.clone() : encrypter;
+ return randomBoolean() ? encrypter.clone() : encrypter;
}
}
\ No newline at end of file
diff --git a/encryption/src/test/java/org/apache/solr/encryption/crypto/CharStreamEncrypterTest.java b/encryption/src/test/java/org/apache/solr/encryption/crypto/CharStreamEncrypterTest.java
new file mode 100644
index 0000000..d6aa764
--- /dev/null
+++ b/encryption/src/test/java/org/apache/solr/encryption/crypto/CharStreamEncrypterTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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 com.carrotsearch.randomizedtesting.RandomizedTest;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+
+/** Tests {@link CharStreamEncrypter}. */
+public class CharStreamEncrypterTest extends RandomizedTest {
+
+ @Test
+ public void testEmptyString() throws Exception {
+ checkEncryptionDecryption("", new CharStreamEncrypter(encrypterFactory()));
+ }
+
+ @Test
+ public void testRandomString() throws Exception {
+ CharStreamEncrypter encrypter = new CharStreamEncrypter(encrypterFactory());
+ for (int i = 0; i < 100; i++) {
+ checkEncryptionDecryption(randomUnicodeOfCodepointLengthBetween(1, 10000), encrypter);
+ }
+ }
+
+ private void checkEncryptionDecryption(String inputString, CharStreamEncrypter encrypter)
+ throws IOException {
+
+ // AES key length can either 16, 24 or 32 bytes.
+ byte[] key = randomBytesOfLength(randomIntBetween(2, 4) * 8);
+
+ // Encrypt the input string.
+ StringBuilder encryptedBuilder = new StringBuilder();
+ encrypter.encrypt(inputString, key, encryptedBuilder);
+
+ // Decrypt the encrypted string.
+ StringBuilder decryptedBuilder = new StringBuilder();
+ encrypter.decrypt(encryptedBuilder.toString(), key, decryptedBuilder);
+ assertEquals(inputString, decryptedBuilder.toString());
+ }
+
+ private AesCtrEncrypterFactory encrypterFactory() {
+ if (LightAesCtrEncrypter.isSupported()) {
+ return randomBoolean() ? CipherAesCtrEncrypter.FACTORY : LightAesCtrEncrypter.FACTORY;
+ }
+ return CipherAesCtrEncrypter.FACTORY;
+ }
+}