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/11/13 13:41:03 UTC

(solr-sandbox) branch main updated: Encryption classes use arrays instead of ByteBuffers. (#86)

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 9baea9c  Encryption classes use arrays instead of ByteBuffers. (#86)
9baea9c is described below

commit 9baea9c15caabe8d1dc4fe128f7b9494ff03a29d
Author: Bruno Roustant <33...@users.noreply.github.com>
AuthorDate: Mon Nov 13 14:40:58 2023 +0100

    Encryption classes use arrays instead of ByteBuffers. (#86)
---
 .../solr/encryption/crypto/AesCtrEncrypter.java    |  25 +++--
 .../apache/solr/encryption/crypto/AesCtrUtil.java  |  59 ++++++-----
 .../encryption/crypto/CipherAesCtrEncrypter.java   |  22 ++---
 .../crypto/DecryptingChannelInputStream.java       |  72 +++++++++-----
 .../encryption/crypto/DecryptingIndexInput.java    | 109 +++++++++++----------
 .../encryption/crypto/DecryptingInputStream.java   |  75 ++++++++------
 .../encryption/crypto/EncryptingIndexOutput.java   |  68 +++++++------
 .../encryption/crypto/EncryptingOutputStream.java  |  60 ++++++++----
 .../encryption/crypto/LightAesCtrEncrypter.java    |  18 +---
 .../encryption/crypto/AesCtrEncrypterTest.java     |  65 ++++++------
 .../crypto/DecryptingIndexInputTest.java           |  58 +++++------
 .../crypto/EncryptingIndexOutputTest.java          |   9 +-
 12 files changed, 341 insertions(+), 299 deletions(-)

diff --git a/encryption/src/main/java/org/apache/solr/encryption/crypto/AesCtrEncrypter.java b/encryption/src/main/java/org/apache/solr/encryption/crypto/AesCtrEncrypter.java
index 16fc33e..fd9ba1c 100644
--- a/encryption/src/main/java/org/apache/solr/encryption/crypto/AesCtrEncrypter.java
+++ b/encryption/src/main/java/org/apache/solr/encryption/crypto/AesCtrEncrypter.java
@@ -16,43 +16,42 @@
  */
 package org.apache.solr.encryption.crypto;
 
-import java.nio.ByteBuffer;
-
 /**
  * Stateful Encrypter specialized for AES algorithm in CTR (counter) mode with no padding.
  * <p>In CTR mode, encryption and decryption are actually the same operation, so this API does not require specifying
  * whether it is used to encrypt or decrypt.
  * <p>An {@link AesCtrEncrypter} must be first {@link #init(long) initialized} before it can be used to
- * {@link #process(ByteBuffer, ByteBuffer) encrypt/decrypt}.
+ * {@link #process encrypt/decrypt}.
  * <p>Not thread safe.
  */
 public interface AesCtrEncrypter extends Cloneable {
 
   /**
-   * Initializes this encrypter at the provided CTR block counter (counter of blocks of size {@link AesCtrUtil#AES_BLOCK_SIZE}).
+   * Initializes this encrypter at the provided CTR block counter (counter of blocks of size
+   * {@link AesCtrUtil#AES_BLOCK_SIZE}).
    * <p>For example, the data byte at index i is inside the block at counter = i / {@link AesCtrUtil#AES_BLOCK_SIZE}.
    * CTR mode computes an IV for this block based on the initial IV (at counter 0) and the provided counter. This allows
    * efficient random access to encrypted data. Only the target block needs to be decrypted.
-   * <p>This method must be called first. Then the next call to {@link #process(ByteBuffer, ByteBuffer)} will start at the
-   * beginning of the block: the first byte of data at input buffer {@link ByteBuffer#position()} must be the first byte
-   * of the block.
+   * <p>This method must be called first. Then the next call to {@link #process} will start at the beginning of the
+   * block: the first byte of input data must be the first byte of the block.
    */
   void init(long counter);
 
   /**
    * Encrypts/decrypts the provided input buffer data and stores the encrypted/decrypted data in an output buffer.
    * In CTR mode, encryption and decryption are actually the same operation.
-   * <p>Both buffers must be backed by array ({@link ByteBuffer#hasArray()} returns true), and must not share the same
-   * array.
    * <p>Do not call this method when this {@link AesCtrEncrypter} is not {@link #init(long) initialized}.
    * <p>This method takes care of incrementing the CTR counter while encrypting/decrypting the data. It can be called
    * repeatedly without calling {@link #init(long)} again. {@link #init(long)} is called only to jump to a given block.
    *
-   * @param inBuffer  Input data from {@link ByteBuffer#position()} (inclusive) to {@link ByteBuffer#limit()} (exclusive).
-   * @param outBuffer Output data to be stored at {@link ByteBuffer#position()}. outBuffer {@link ByteBuffer#remaining()}
-   *                  must be greater than or equal to inBuffer {@link ByteBuffer#remaining()}.
+   * @param inBuffer  Input data buffer.
+   * @param inOffset  Offset (inclusive) of the first byte of input data.
+   * @param length    Number of input bytes to process.
+   * @param outBuffer Output data buffer where to put the processed bytes. Its capacity must be greater than or equal
+   *                  {@code length}.
+   * @param outOffset Output offset (inclusive) where to put the first output byte.
    */
-  void process(ByteBuffer inBuffer, ByteBuffer outBuffer);
+  void process(byte[] inBuffer, int inOffset, int length, byte[] outBuffer, int outOffset);
 
   /**
    * Clones this {@link AesCtrEncrypter} for efficiency as it clones the internal encryption key and IV.
diff --git a/encryption/src/main/java/org/apache/solr/encryption/crypto/AesCtrUtil.java b/encryption/src/main/java/org/apache/solr/encryption/crypto/AesCtrUtil.java
index 7546a9c..63c397a 100644
--- a/encryption/src/main/java/org/apache/solr/encryption/crypto/AesCtrUtil.java
+++ b/encryption/src/main/java/org/apache/solr/encryption/crypto/AesCtrUtil.java
@@ -32,41 +32,47 @@ public class AesCtrUtil {
    * AES/CTR IV length. It is equal to {@link #AES_BLOCK_SIZE}. It is defined separately mainly for code clarity.
    */
   public static final int IV_LENGTH = AES_BLOCK_SIZE;
+  /** CTR counter length. 5 bytes means we can encrypt files up to 2^(5 x 8) x 16 B = 17.5 TB */
+  private static final int COUNTER_LENGTH = 5;
+  private static final long COUNTER_MAX_VALUE = (1L << (COUNTER_LENGTH * Byte.SIZE)) - 1;
 
   /**
    * Checks a key for AES. Its length must be either 16, 24 or 32 bytes.
+   * @return true
+   * @throws IllegalArgumentException If the key length is invalid.
    */
-  public static void checkAesKey(byte[] key) {
+  public static boolean checkAesKey(byte[] key) {
     if (key.length != 16 && key.length != 24 && key.length != 32) {
       // AES requires either 128, 192 or 256 bits keys.
       throw new IllegalArgumentException("Invalid AES key length; it must be either 128, 192 or 256 bits long");
     }
+    return true;
   }
 
   /**
-   * Checks the CTR counter. It must be positive or null.
+   * Checks the CTR counter. It must be greater than or equal to 0, and less than or equal to
+   * {@link #COUNTER_MAX_VALUE}.
+   * @return true
+   * @throws IllegalArgumentException If the counter is invalid.
    */
-  public static void checkCtrCounter(long counter) {
-    if (counter < 0) {
-      throw new IllegalArgumentException("Illegal counter=" + counter);
+  public static boolean checkCtrCounter(long counter) {
+    if (counter < 0 || counter > COUNTER_MAX_VALUE) {
+      throw new IllegalArgumentException("Invalid counter=" + counter);
     }
+    return true;
   }
 
   /**
    * Generates a random IV for AES/CTR of length {@link #IV_LENGTH}.
    */
   public static byte[] generateRandomAesCtrIv(SecureRandom secureRandom) {
-    // IV length must be the AES block size.
-    // IV must be random for the CTR mode. It starts with counter 0, so it's simply IV.
+    // The IV length must be the AES block size.
+    // For the CTR mode, the IV is composed of a random NONCE (first bytes) and a counter (last bytes).
+    // com.sun.crypto.provider.CounterMode.increment() increments the counter starting from the last byte.
+    byte[] nonce = new byte[IV_LENGTH - COUNTER_LENGTH];
+    secureRandom.nextBytes(nonce);
     byte[] iv = new byte[IV_LENGTH];
-    do {
-      secureRandom.nextBytes(iv);
-      // Ensure that we have enough bits left to allow the 8 bytes counter to add with the carry.
-      // The high-order byte is at index 0.
-      // We check that there is at least one unset bit in the 3 highest bytes. It guarantees
-      // that we can add with the carry at least 5 bytes of the counter, which means we handle
-      // files of at least 2^(5*8) * 2 * 16 B = 35,000 GB.
-    } while (iv[0] == -1 && iv[1] == -1 && iv[2] == -1);
+    System.arraycopy(nonce, 0, iv, 0, nonce.length);
     return iv;
   }
 
@@ -74,20 +80,13 @@ public class AesCtrUtil {
    * Builds an AES/CTR IV based on the provided counter and an initial IV.
    * The built IV is the same as with {@code com.sun.crypto.provider.CounterMode.increment()}.
    */
-  public static void buildAesCtrIv(byte[] initialIv, long counter, byte[] iv) {
-    assert initialIv.length == IV_LENGTH && iv.length == IV_LENGTH;
-    int ivIndex = iv.length;
-    int counterIndex = 0;
-    int sum = 0;
-    while (ivIndex-- > 0) {
-      // (sum >>> Byte.SIZE) is the carry for counter addition.
-      sum = (initialIv[ivIndex] & 0xff) + (sum >>> Byte.SIZE);
-      // Add long counter.
-      if (counterIndex++ < 8) {
-        sum += (byte) counter & 0xff;
-        counter >>>= 8;
-      }
-      iv[ivIndex] = (byte) sum;
-    }
+  public static void buildAesCtrIv(byte[] iv, long counter) {
+    assert iv.length == IV_LENGTH;
+    assert checkCtrCounter(counter);
+    iv[iv.length - 1] = (byte) counter;
+    iv[iv.length - 2] = (byte) (counter >>= 8);
+    iv[iv.length - 3] = (byte) (counter >>= 8);
+    iv[iv.length - 4] = (byte) (counter >>= 8);
+    iv[iv.length - 5] = (byte) (counter >> 8);
   }
 }
\ No newline at end of file
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 b11a294..2244da5 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
@@ -21,7 +21,6 @@ import javax.crypto.NoSuchPaddingException;
 import javax.crypto.ShortBufferException;
 import javax.crypto.spec.IvParameterSpec;
 import javax.crypto.spec.SecretKeySpec;
-import java.nio.ByteBuffer;
 import java.security.InvalidAlgorithmParameterException;
 import java.security.InvalidKeyException;
 import java.security.Key;
@@ -33,8 +32,8 @@ import static org.apache.solr.encryption.crypto.AesCtrUtil.*;
  * {@link AesCtrEncrypter} backed by a {@link javax.crypto.Cipher} with the "AES/CTR/NoPadding" transformation.
  * <p>This encrypter loads the internal {@link javax.crypto.CipherSpi} implementation from the classpath, see
  * {@link Cipher#getInstance(String)}. It is heavy to create, to initialize and to clone, but the
- * {@link #process(ByteBuffer, ByteBuffer) encryption} is extremely fast thanks to {@code HotSpotIntrinsicCandidate}
- * annotation in com.sun.crypto.provider.CounterMode.
+ * {@link #process encryption} is fast thanks to the {@code HotSpotIntrinsicCandidate} annotation in
+ * {@code com.sun.crypto.provider.CounterMode}.
  */
 public class CipherAesCtrEncrypter implements AesCtrEncrypter {
 
@@ -49,7 +48,6 @@ public class CipherAesCtrEncrypter implements AesCtrEncrypter {
   private byte[] iv;
   private ReusableIvParameterSpec ivParameterSpec;
   private Cipher cipher;
-  private long counter;
 
   /**
    * @param key The encryption key. It is cloned internally, its content is not modified, and no reference to it is kept.
@@ -68,11 +66,7 @@ public class CipherAesCtrEncrypter implements AesCtrEncrypter {
 
   @Override
   public void init(long counter) {
-    checkCtrCounter(counter);
-    if (counter != this.counter) {
-      this.counter = counter;
-      buildAesCtrIv(initialIv, counter, iv);
-    }
+    buildAesCtrIv(iv, counter);
     try {
       cipher.init(Cipher.ENCRYPT_MODE, key, ivParameterSpec, SecureRandomProvider.get());
     } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
@@ -81,13 +75,10 @@ public class CipherAesCtrEncrypter implements AesCtrEncrypter {
   }
 
   @Override
-  public void process(ByteBuffer inBuffer, ByteBuffer outBuffer) {
+  public void process(byte[] inBuffer, int inOffset, int length, byte[] outBuffer, int outOffset) {
     try {
-      int inputSize = inBuffer.remaining();
-      int numEncryptedBytes = cipher.update(inBuffer, outBuffer);
-      if (numEncryptedBytes < inputSize) {
-        throw new UnsupportedOperationException(Cipher.class.getSimpleName() + " implementation does not maintain an encryption context; this is not supported");
-      }
+      int numEncryptedBytes = cipher.update(inBuffer, inOffset, length, outBuffer, outOffset);
+      assert numEncryptedBytes == length : Cipher.class.getSimpleName() + " implementation does not maintain an encryption context; this is not supported";
     } catch (ShortBufferException e) {
       throw new RuntimeException(e);
     }
@@ -105,7 +96,6 @@ public class CipherAesCtrEncrypter implements AesCtrEncrypter {
     clone.iv = initialIv.clone();
     clone.ivParameterSpec = new ReusableIvParameterSpec(clone.iv);
     clone.cipher = createAesCtrCipher();
-    clone.counter = 0;
     return clone;
   }
 
diff --git a/encryption/src/main/java/org/apache/solr/encryption/crypto/DecryptingChannelInputStream.java b/encryption/src/main/java/org/apache/solr/encryption/crypto/DecryptingChannelInputStream.java
index 496c08c..81631f2 100644
--- a/encryption/src/main/java/org/apache/solr/encryption/crypto/DecryptingChannelInputStream.java
+++ b/encryption/src/main/java/org/apache/solr/encryption/crypto/DecryptingChannelInputStream.java
@@ -44,7 +44,9 @@ public class DecryptingChannelInputStream extends TransactionLog.ChannelFastInpu
   private final byte[] iv;
   private final AesCtrEncrypter encrypter;
   private final ByteBuffer inBuffer;
-  private final ByteBuffer outBuffer;
+  private final byte[] outBuffer;
+  private int outPos;
+  private int outSize;
   private int padding;
   private long filePointer;
 
@@ -66,6 +68,29 @@ public class DecryptingChannelInputStream extends TransactionLog.ChannelFastInpu
                                       byte[] key,
                                       AesCtrEncrypterFactory factory)
     throws IOException {
+    this(channel, offset, position, key, factory, BUFFER_CAPACITY);
+  }
+
+  /**
+   * @param channel  The delegate {@link FileChannel} to read and decrypt data from.
+   * @param offset   Base offset in the {@link FileChannel}. The IV at the beginning of
+   *                 the file starts at this offset. {@link #seek(long)} positions are
+   *                 relative to this base offset + {@link AesCtrUtil#IV_LENGTH}.
+   * @param position Initial read position, relative to the base offset. Setting a positive
+   *                 position is equivalent to setting a zero position and then calling
+   *                 {@link #setPosition(long)}.
+   * @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}.
+   * @param bufferCapacity The encryption buffer capacity. It must be a multiple of {@link AesCtrUtil#AES_BLOCK_SIZE}.
+   */
+  public DecryptingChannelInputStream(FileChannel channel,
+                                      long offset,
+                                      long position,
+                                      byte[] key,
+                                      AesCtrEncrypterFactory factory,
+                                      int bufferCapacity)
+    throws IOException {
     super(channel, position);
     assert offset >= 0;
     assert position >= 0;
@@ -73,11 +98,9 @@ public class DecryptingChannelInputStream extends TransactionLog.ChannelFastInpu
     iv = new byte[IV_LENGTH];
     channel.read(ByteBuffer.wrap(iv, 0, iv.length), offset);
     encrypter = factory.create(key, iv);
-    inBuffer = ByteBuffer.allocate(BUFFER_CAPACITY);
-    outBuffer = ByteBuffer.allocate(BUFFER_CAPACITY + AES_BLOCK_SIZE);
-    outBuffer.limit(0);
-    assert inBuffer.hasArray() && outBuffer.hasArray();
-    assert inBuffer.arrayOffset() == 0;
+    assert bufferCapacity % AES_BLOCK_SIZE == 0;
+    inBuffer = ByteBuffer.allocate(bufferCapacity);
+    outBuffer = new byte[bufferCapacity + AES_BLOCK_SIZE];
     setPosition(position);
   }
 
@@ -95,20 +118,20 @@ public class DecryptingChannelInputStream extends TransactionLog.ChannelFastInpu
     int numDecrypted = 0;
     while (length > 0) {
       // Transfer decrypted bytes from outBuffer.
-      int outRemaining = outBuffer.remaining();
+      int outRemaining = outSize - outPos;
       if (outRemaining > 0) {
         if (length <= outRemaining) {
-          outBuffer.get(target, offset, length);
+          System.arraycopy(outBuffer, outPos, target, offset, length);
+          outPos += length;
           numDecrypted += length;
           return numDecrypted;
         }
-        outBuffer.get(target, offset, outRemaining);
+        System.arraycopy(outBuffer, outPos, target, offset, outRemaining);
+        outPos += outRemaining;
         numDecrypted += outRemaining;
-        assert outBuffer.remaining() == 0;
         offset += outRemaining;
         length -= outRemaining;
       }
-
       if (!readToFillBuffer(length)) {
         return numDecrypted == 0 ? -1 : numDecrypted;
       }
@@ -138,16 +161,13 @@ public class DecryptingChannelInputStream extends TransactionLog.ChannelFastInpu
   }
 
   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;
-    }
+    int inPos = inBuffer.position();
+    assert inPos > padding : "inPos=" + inPos + " padding=" + padding;
+    encrypter.process(inBuffer.array(), 0, inPos, outBuffer, 0);
+    outSize = inPos;
+    inBuffer.position(0);
+    outPos = padding;
+    padding = 0;
   }
 
   @Override
@@ -158,11 +178,11 @@ public class DecryptingChannelInputStream extends TransactionLog.ChannelFastInpu
       pos = (int) (position - getBufferPos());
     } else {
       long channelPosition = filePointer - offset - IV_LENGTH;
-      long currentPosition = channelPosition - outBuffer.remaining();
+      long currentPosition = channelPosition - (outSize - outPos);
       if (position >= currentPosition && position <= channelPosition) {
         // The target position is within the buffered output. Just move the output buffer position.
-        outBuffer.position(outBuffer.position() + (int) (position - currentPosition));
-        assert position == channelPosition - outBuffer.remaining();
+        outPos += (int) (position - currentPosition);
+        assert position == channelPosition - (outSize - outPos);
       } else {
         setPosition(position);
       }
@@ -173,9 +193,7 @@ public class DecryptingChannelInputStream extends TransactionLog.ChannelFastInpu
   }
 
   private void setPosition(long position) {
-    inBuffer.clear();
-    outBuffer.clear();
-    outBuffer.limit(0);
+    outPos = outSize = 0;
     // Compute the counter by ignoring the IV and the channel offset, if any.
     long counter = position / AES_BLOCK_SIZE;
     encrypter.init(counter);
diff --git a/encryption/src/main/java/org/apache/solr/encryption/crypto/DecryptingIndexInput.java b/encryption/src/main/java/org/apache/solr/encryption/crypto/DecryptingIndexInput.java
index 072ca25..a1a7fe8 100644
--- a/encryption/src/main/java/org/apache/solr/encryption/crypto/DecryptingIndexInput.java
+++ b/encryption/src/main/java/org/apache/solr/encryption/crypto/DecryptingIndexInput.java
@@ -20,7 +20,6 @@ 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.*;
 
@@ -40,7 +39,7 @@ 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.
    */
-  static final int BUFFER_CAPACITY = 6 * AES_BLOCK_SIZE; // 96 B
+  public static final int BUFFER_CAPACITY = 6 * AES_BLOCK_SIZE; // 96 B
 
   static final long AES_BLOCK_SIZE_MOD_MASK = AES_BLOCK_SIZE - 1;
 
@@ -51,10 +50,12 @@ public class DecryptingIndexInput extends IndexInput {
   private final long sliceEnd;
   private IndexInput indexInput;
   private AesCtrEncrypter encrypter;
-  private ByteBuffer inBuffer;
-  private ByteBuffer outBuffer;
-  private byte[] inArray;
+  private byte[] inBuffer;
+  private byte[] outBuffer;
   private byte[] oneByteBuf;
+  private int inPos;
+  private int outPos;
+  private int outSize;
   private int padding;
   private boolean closed;
 
@@ -67,13 +68,30 @@ public class DecryptingIndexInput extends IndexInput {
    * @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(indexInput, key, factory, BUFFER_CAPACITY);
+  }
+
+  /**
+   * @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.
+   * @param bufferCapacity The encryption buffer capacity. It must be a multiple of {@link AesCtrUtil#AES_BLOCK_SIZE}.
+   */
+  public DecryptingIndexInput(IndexInput indexInput,
+                              byte[] key,
+                              AesCtrEncrypterFactory factory,
+                              int bufferCapacity) 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));
+         indexInput.getFilePointer() + IV_LENGTH,
+         indexInput.getFilePointer() + IV_LENGTH,
+         indexInput.length() - indexInput.getFilePointer() - IV_LENGTH,
+         false,
+         indexInput,
+         createEncrypter(indexInput, key, factory),
+         bufferCapacity);
   }
 
   private DecryptingIndexInput(String resourceDescription,
@@ -82,7 +100,8 @@ public class DecryptingIndexInput extends IndexInput {
                                long sliceLength,
                                boolean isClone,
                                IndexInput indexInput,
-                               AesCtrEncrypter encrypter) {
+                               AesCtrEncrypter encrypter,
+                               int bufferCapacity) {
     super(resourceDescription);
     assert delegateOffset >= 0 && sliceOffset >= 0 && sliceLength >= 0;
     this.delegateOffset = delegateOffset;
@@ -92,12 +111,9 @@ public class DecryptingIndexInput extends IndexInput {
     this.indexInput = indexInput;
     this.encrypter = encrypter;
     encrypter.init(0);
-    inBuffer = ByteBuffer.allocate(BUFFER_CAPACITY);
-    outBuffer = ByteBuffer.allocate(BUFFER_CAPACITY + AES_BLOCK_SIZE);
-    outBuffer.limit(0);
-    assert inBuffer.hasArray() && outBuffer.hasArray();
-    assert inBuffer.arrayOffset() == 0;
-    inArray = inBuffer.array();
+    assert bufferCapacity % AES_BLOCK_SIZE == 0;
+    inBuffer = new byte[bufferCapacity];
+    outBuffer = new byte[bufferCapacity + AES_BLOCK_SIZE];
     oneByteBuf = new byte[1];
   }
 
@@ -132,7 +148,7 @@ public class DecryptingIndexInput extends IndexInput {
    * Gets the current internal position in the delegate {@link IndexInput}. It includes IV length.
    */
   private long getPosition() {
-    return indexInput.getFilePointer() - outBuffer.remaining();
+    return indexInput.getFilePointer() - (outSize - outPos);
   }
 
   @Override
@@ -145,11 +161,11 @@ public class DecryptingIndexInput extends IndexInput {
     }
     long targetPosition = position + sliceOffset;
     long delegatePosition = indexInput.getFilePointer();
-    long currentPosition = delegatePosition - outBuffer.remaining();
+    long currentPosition = delegatePosition - (outSize - outPos);
     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();
+      outPos += (int) (targetPosition - currentPosition);
+      assert targetPosition == delegatePosition - (outSize - outPos);
     } else {
       indexInput.seek(targetPosition);
       setPosition(targetPosition);
@@ -157,15 +173,13 @@ public class DecryptingIndexInput extends IndexInput {
   }
 
   private void setPosition(long position) {
-    inBuffer.clear();
-    outBuffer.clear();
-    outBuffer.limit(0);
+    outPos = outSize = 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);
+    inPos = padding;
   }
 
   /**
@@ -185,8 +199,9 @@ public class DecryptingIndexInput extends IndexInput {
       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());
+    DecryptingIndexInput slice = new DecryptingIndexInput(
+      getFullSliceDescription(sliceDescription), delegateOffset, sliceOffset + offset, length, true,
+      indexInput.clone(), encrypter.clone(), inBuffer.length);
     slice.seek(0);
     return slice;
   }
@@ -209,18 +224,18 @@ public class DecryptingIndexInput extends IndexInput {
     }
     while (length > 0) {
       // Transfer decrypted bytes from outBuffer.
-      int outRemaining = outBuffer.remaining();
+      int outRemaining = outSize - outPos;
       if (outRemaining > 0) {
         if (length <= outRemaining) {
-          outBuffer.get(b, offset, length);
+          System.arraycopy(outBuffer, outPos, b, offset, length);
+          outPos += length;
           return;
         }
-        outBuffer.get(b, offset, outRemaining);
-        assert outBuffer.remaining() == 0;
+        System.arraycopy(outBuffer, outPos, b, offset, outRemaining);
+        outPos += outRemaining;
         offset += outRemaining;
         length -= outRemaining;
       }
-
       readToFillBuffer(length);
       decryptBuffer();
     }
@@ -228,26 +243,21 @@ public class DecryptingIndexInput extends IndexInput {
 
   private void readToFillBuffer(int length) throws IOException {
     assert length > 0;
-    int inRemaining = inBuffer.remaining();
+    int inRemaining = inBuffer.length - inPos;
     if (inRemaining > 0) {
-      int position = inBuffer.position();
       int numBytesToRead = Math.min(inRemaining, length);
-      indexInput.readBytes(inArray, position, numBytesToRead);
-      inBuffer.position(position + numBytesToRead);
+      indexInput.readBytes(inBuffer, inPos, numBytesToRead);
+      inPos += 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;
-    }
+    assert inPos > padding : "inPos=" + inPos + " padding=" + padding;
+    encrypter.process(inBuffer, 0, inPos, outBuffer, 0);
+    outSize = inPos;
+    inPos = 0;
+    outPos = padding;
+    padding = 0;
   }
 
   @Override
@@ -257,9 +267,8 @@ public class DecryptingIndexInput extends IndexInput {
     clone.indexInput = indexInput.clone();
     assert clone.indexInput.getFilePointer() == indexInput.getFilePointer();
     clone.encrypter = encrypter.clone();
-    clone.inBuffer = ByteBuffer.allocate(BUFFER_CAPACITY);
-    clone.outBuffer = ByteBuffer.allocate(BUFFER_CAPACITY + AES_BLOCK_SIZE);
-    clone.inArray = clone.inBuffer.array();
+    clone.inBuffer = new byte[inBuffer.length];
+    clone.outBuffer = new byte[outBuffer.length];
     clone.oneByteBuf = new byte[1];
     // The clone must be initialized.
     clone.setPosition(getPosition());
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
index b94982f..9c52e7c 100644
--- a/encryption/src/main/java/org/apache/solr/encryption/crypto/DecryptingInputStream.java
+++ b/encryption/src/main/java/org/apache/solr/encryption/crypto/DecryptingInputStream.java
@@ -18,7 +18,6 @@ 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;
@@ -40,10 +39,12 @@ public class DecryptingInputStream extends InputStream {
   private final InputStream inputStream;
   private final byte[] iv;
   private final AesCtrEncrypter encrypter;
-  private final ByteBuffer inBuffer;
-  private final ByteBuffer outBuffer;
-  private final byte[] inArray;
+  private final byte[] inBuffer;
+  private final byte[] outBuffer;
   private final byte[] oneByteBuf;
+  private int inPos;
+  private int outPos;
+  private int outSize;
   private int padding;
   private boolean closed;
 
@@ -75,16 +76,35 @@ public class DecryptingInputStream extends InputStream {
                                byte[] key,
                                AesCtrEncrypterFactory factory)
     throws IOException {
+    this(inputStream, position, iv, key, factory, BUFFER_CAPACITY);
+  }
+
+  /**
+   * @param inputStream The delegate {@link InputStream} to read and decrypt data from.
+   * @param position    The position in the input stream. A non-zero position means the
+   *                    input skips the beginning of the file, in this case the iv should
+   *                    be provided and not null.
+   * @param iv          The IV to use (not read) if the position is greater than zero;
+   *                    or null to read it at the beginning of the input.
+   * @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}.
+   * @param bufferCapacity The encryption buffer capacity. It must be a multiple of {@link AesCtrUtil#AES_BLOCK_SIZE}.
+   */
+  public DecryptingInputStream(InputStream inputStream,
+                               long position,
+                               byte[] iv,
+                               byte[] key,
+                               AesCtrEncrypterFactory factory,
+                               int bufferCapacity)
+    throws IOException {
     if (position < 0) {
       throw new IllegalArgumentException("Invalid position " + position);
     }
     this.inputStream = inputStream;
-    inBuffer = ByteBuffer.allocate(BUFFER_CAPACITY);
-    outBuffer = ByteBuffer.allocate(BUFFER_CAPACITY + AES_BLOCK_SIZE);
-    outBuffer.limit(0);
-    assert inBuffer.hasArray() && outBuffer.hasArray();
-    assert inBuffer.arrayOffset() == 0;
-    inArray = inBuffer.array();
+    assert bufferCapacity % AES_BLOCK_SIZE == 0;
+    inBuffer = new byte[bufferCapacity];
+    outBuffer = new byte[bufferCapacity + AES_BLOCK_SIZE];
     oneByteBuf = new byte[1];
     long counter;
     if (position == 0) {
@@ -100,7 +120,7 @@ public class DecryptingInputStream extends InputStream {
     } else {
       counter = position / AES_BLOCK_SIZE;
       padding = (int) (position & AES_BLOCK_SIZE_MOD_MASK);
-      inBuffer.position(padding);
+      inPos = padding;
     }
     this.iv = iv;
     encrypter = factory.create(key, iv);
@@ -138,20 +158,20 @@ public class DecryptingInputStream extends InputStream {
     int numDecrypted = 0;
     while (length > 0) {
       // Transfer decrypted bytes from outBuffer.
-      int outRemaining = outBuffer.remaining();
+      int outRemaining = outSize - outPos;
       if (outRemaining > 0) {
         if (length <= outRemaining) {
-          outBuffer.get(b, offset, length);
+          System.arraycopy(outBuffer, outPos, b, offset, length);
+          outPos += length;
           numDecrypted += length;
           return numDecrypted;
         }
-        outBuffer.get(b, offset, outRemaining);
+        System.arraycopy(outBuffer, outPos, b, offset, outRemaining);
+        outPos += outRemaining;
         numDecrypted += outRemaining;
-        assert outBuffer.remaining() == 0;
         offset += outRemaining;
         length -= outRemaining;
       }
-
       if (!readToFillBuffer(length)) {
         return numDecrypted == 0 ? -1 : numDecrypted;
       }
@@ -162,29 +182,24 @@ public class DecryptingInputStream extends InputStream {
 
   private boolean readToFillBuffer(int length) throws IOException {
     assert length > 0;
-    int inRemaining = inBuffer.remaining();
+    int inRemaining = inBuffer.length - inPos;
     if (inRemaining > 0) {
-      int position = inBuffer.position();
       int numBytesToRead = Math.min(inRemaining, length);
-      int n = inputStream.read(inArray, position, numBytesToRead);
+      int n = inputStream.read(inBuffer, inPos, numBytesToRead);
       if (n == -1) {
         return false;
       }
-      inBuffer.position(position + n);
+      inPos += n;
     }
     return true;
   }
 
   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;
-    }
+    assert inPos > padding : "inPos=" + inPos + " padding=" + padding;
+    encrypter.process(inBuffer, 0, inPos, outBuffer, 0);
+    outSize = inPos;
+    inPos = 0;
+    outPos = padding;
+    padding = 0;
   }
 }
\ No newline at end of file
diff --git a/encryption/src/main/java/org/apache/solr/encryption/crypto/EncryptingIndexOutput.java b/encryption/src/main/java/org/apache/solr/encryption/crypto/EncryptingIndexOutput.java
index 7c3ccc2..7e18770 100644
--- a/encryption/src/main/java/org/apache/solr/encryption/crypto/EncryptingIndexOutput.java
+++ b/encryption/src/main/java/org/apache/solr/encryption/crypto/EncryptingIndexOutput.java
@@ -20,7 +20,6 @@ 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;
 
@@ -42,15 +41,15 @@ public class EncryptingIndexOutput extends IndexOutput {
   /**
    * Must be a multiple of {@link AesCtrUtil#AES_BLOCK_SIZE}.
    */
-  static final int BUFFER_CAPACITY = 64 * AES_BLOCK_SIZE; // 1024
+  public 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[] inBuffer;
+  private final byte[] outBuffer;
   private final byte[] oneByteBuf;
   private final Checksum clearChecksum;
+  private int inSize;
   private long filePointer;
   private boolean closed;
 
@@ -62,7 +61,26 @@ public class EncryptingIndexOutput extends IndexOutput {
    *                    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 {
+  public EncryptingIndexOutput(IndexOutput indexOutput, byte[] key, AesCtrEncrypterFactory factory)
+    throws IOException {
+    this(indexOutput, key, factory, BUFFER_CAPACITY);
+  }
+
+  /**
+   * @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.
+   * @param bufferCapacity The encryption buffer capacity. It must be a multiple of {@link AesCtrUtil#AES_BLOCK_SIZE}.
+   */
+  public EncryptingIndexOutput(IndexOutput indexOutput,
+                               byte[] key,
+                               AesCtrEncrypterFactory factory,
+                               int bufferCapacity)
+    throws IOException {
     super("Encrypting " + indexOutput.toString(), indexOutput.getName());
     this.indexOutput = indexOutput;
 
@@ -72,14 +90,10 @@ public class EncryptingIndexOutput extends IndexOutput {
     // 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();
+    assert bufferCapacity % AES_BLOCK_SIZE == 0;
+    inBuffer = new byte[bufferCapacity];
+    outBuffer = new byte[bufferCapacity];
     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());
   }
@@ -91,19 +105,12 @@ public class EncryptingIndexOutput extends IndexOutput {
     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) {
+        if (inSize != 0) {
           encryptBufferAndWrite();
         }
       } finally {
@@ -139,12 +146,14 @@ public class EncryptingIndexOutput extends IndexOutput {
     clearChecksum.update(b, offset, length);
     filePointer += length;
     while (length > 0) {
-      int remaining = inBuffer.remaining();
+      int remaining = inBuffer.length - inSize;
       if (length < remaining) {
-        inBuffer.put(b, offset, length);
+        System.arraycopy(b, offset, inBuffer, inSize, length);
+        inSize += length;
         break;
       } else {
-        inBuffer.put(b, offset, remaining);
+        System.arraycopy(b, offset, inBuffer, inSize, remaining);
+        inSize += remaining;
         offset += remaining;
         length -= remaining;
         encryptBufferAndWrite();
@@ -153,12 +162,9 @@ public class EncryptingIndexOutput extends IndexOutput {
   }
 
   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());
+    assert inSize > 0;
+    encrypter.process(inBuffer, 0, inSize, outBuffer, 0);
+    indexOutput.writeBytes(outBuffer, 0, inSize);
+    inSize = 0;
   }
 }
\ 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
index b57086b..d8554ed 100644
--- a/encryption/src/main/java/org/apache/solr/encryption/crypto/EncryptingOutputStream.java
+++ b/encryption/src/main/java/org/apache/solr/encryption/crypto/EncryptingOutputStream.java
@@ -18,7 +18,6 @@ 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;
@@ -40,10 +39,10 @@ public class EncryptingOutputStream extends OutputStream {
   private final OutputStream outputStream;
   private final byte[] iv;
   private final AesCtrEncrypter encrypter;
-  private final ByteBuffer inBuffer;
-  private final ByteBuffer outBuffer;
-  private final byte[] outArray;
+  private final byte[] inBuffer;
+  private final byte[] outBuffer;
   private final byte[] oneByteBuf;
+  private int inSize;
   private int padding;
   private boolean closed;
 
@@ -76,15 +75,35 @@ public class EncryptingOutputStream extends OutputStream {
                                 byte[] key,
                                 AesCtrEncrypterFactory factory)
     throws IOException {
+    this(outputStream, position, iv, key, factory, BUFFER_CAPACITY);
+  }
+
+  /**
+   * @param outputStream The delegate {@link OutputStream} to write encrypted data to.
+   * @param position     The position in the output stream. A non-zero position means the
+   *                     output is reopened to append more data, in this case the iv should
+   *                     be provided and not null.
+   * @param iv           The IV to use (not written) if the position is greater than zero;
+   *                     or null to generate a random one and write it at the beginning of
+   *                     the output.
+   * @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,
+                                long position,
+                                byte[] iv,
+                                byte[] key,
+                                AesCtrEncrypterFactory factory,
+                                int bufferCapacity)
+    throws IOException {
     if (position < 0) {
       throw new IllegalArgumentException("Invalid position " + position);
     }
     this.outputStream = outputStream;
-    inBuffer = ByteBuffer.allocate(BUFFER_CAPACITY);
-    outBuffer = ByteBuffer.allocate(BUFFER_CAPACITY + AES_BLOCK_SIZE);
-    assert inBuffer.hasArray() && outBuffer.hasArray();
-    assert outBuffer.arrayOffset() == 0;
-    outArray = outBuffer.array();
+    assert bufferCapacity % AES_BLOCK_SIZE == 0;
+    inBuffer = new byte[bufferCapacity];
+    outBuffer = new byte[bufferCapacity + AES_BLOCK_SIZE];
     oneByteBuf = new byte[1];
     long counter;
     if (position == 0) {
@@ -97,7 +116,7 @@ public class EncryptingOutputStream extends OutputStream {
     } else {
       counter = position / AES_BLOCK_SIZE;
       padding = (int) (position & (AES_BLOCK_SIZE - 1));
-      inBuffer.position(padding);
+      inSize = padding;
     }
     this.iv = iv;
     encrypter = factory.create(key, iv);
@@ -120,7 +139,7 @@ public class EncryptingOutputStream extends OutputStream {
 
   @Override
   public void flush() throws IOException {
-    if (inBuffer.position() > padding) {
+    if (inSize > padding) {
       encryptBufferAndWrite();
     }
   }
@@ -151,12 +170,14 @@ public class EncryptingOutputStream extends OutputStream {
                                            + ", arrayLength=" + b.length + ")");
     }
     while (length > 0) {
-      int remaining = inBuffer.remaining();
+      int remaining = inBuffer.length - inSize;
       if (length < remaining) {
-        inBuffer.put(b, offset, length);
+        System.arraycopy(b, offset, inBuffer, inSize, length);
+        inSize += length;
         break;
       } else {
-        inBuffer.put(b, offset, remaining);
+        System.arraycopy(b, offset, inBuffer, inSize, remaining);
+        inSize += remaining;
         offset += remaining;
         length -= remaining;
         encryptBufferAndWrite();
@@ -165,13 +186,10 @@ public class EncryptingOutputStream extends OutputStream {
   }
 
   private void encryptBufferAndWrite() throws IOException {
-    assert inBuffer.position() > padding : "position=" + inBuffer.position() + ", padding=" + padding;
-    inBuffer.flip();
-    outBuffer.clear();
-    encrypter.process(inBuffer, outBuffer);
-    inBuffer.clear();
-    outBuffer.flip();
-    outputStream.write(outArray, padding, outBuffer.limit() - padding);
+    assert inSize > padding : "inSize=" + inSize + " padding=" + padding;
+    encrypter.process(inBuffer, 0, inSize, outBuffer, 0);
+    outputStream.write(outBuffer, padding, inSize - padding);
+    inSize = 0;
     padding = 0;
   }
 }
\ No newline at end of file
diff --git a/encryption/src/main/java/org/apache/solr/encryption/crypto/LightAesCtrEncrypter.java b/encryption/src/main/java/org/apache/solr/encryption/crypto/LightAesCtrEncrypter.java
index 76b4c15..4aae7ad 100644
--- a/encryption/src/main/java/org/apache/solr/encryption/crypto/LightAesCtrEncrypter.java
+++ b/encryption/src/main/java/org/apache/solr/encryption/crypto/LightAesCtrEncrypter.java
@@ -19,7 +19,6 @@ package org.apache.solr.encryption.crypto;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
-import java.nio.ByteBuffer;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
 
@@ -100,8 +99,7 @@ public class LightAesCtrEncrypter implements AesCtrEncrypter {
 
   @Override
   public void init(long counter) {
-    checkCtrCounter(counter);
-    buildAesCtrIv(initialIv, counter, iv);
+    buildAesCtrIv(iv, counter);
     try {
       COUNTER_MODE_IV_FIELD.set(counterMode, iv);
       COUNTER_MODE_RESET_METHOD.invoke(counterMode);
@@ -111,21 +109,13 @@ public class LightAesCtrEncrypter implements AesCtrEncrypter {
   }
 
   @Override
-  public void process(ByteBuffer inBuffer, ByteBuffer outBuffer) {
-    assert inBuffer.array() != outBuffer.array() : "Input and output buffers must not be backed by the same array";
-    int length = inBuffer.remaining();
-    if (length > outBuffer.remaining()) {
-      throw new IllegalArgumentException("Output buffer does not have enough remaining space (needs " + length + " B)");
-    }
-    int outPos = outBuffer.position();
+  public void process(byte[] inBuffer, int inOffset, int length, byte[] outBuffer, int outOffset) {
+    assert outOffset + length <= outBuffer.length;
     try {
-      COUNTER_MODE_CRYPT_METHOD.invoke(counterMode, inBuffer.array(), inBuffer.arrayOffset() + inBuffer.position(),
-        length, outBuffer.array(), outBuffer.arrayOffset() + outPos);
+      COUNTER_MODE_CRYPT_METHOD.invoke(counterMode, inBuffer, inOffset, length, outBuffer, outOffset);
     } catch (Exception e) {
       throw new RuntimeException(e);
     }
-    inBuffer.position(inBuffer.limit());
-    outBuffer.position(outPos + length);
   }
 
   @Override
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 ef7807d..c8f2809 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
@@ -16,13 +16,11 @@
  */
 package org.apache.solr.encryption.crypto;
 
-import java.nio.ByteBuffer;
-
 import com.carrotsearch.randomizedtesting.RandomizedTest;
 import org.junit.Test;
 
-import static junit.framework.TestCase.assertEquals;
 import static org.apache.solr.encryption.crypto.AesCtrUtil.*;
+import static org.junit.Assert.assertArrayEquals;
 
 /**
  * Tests {@link AesCtrEncrypter} implementations.
@@ -35,21 +33,25 @@ public class AesCtrEncrypterTest extends RandomizedTest {
    */
   @Test
   public void testEncryptionDecryption() {
-    for (int i = 0; i < 100; i++) {
-      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);
+    for (int i = 0; i < 3000; i++) {
+      try {
+        byte[] clearData = generateRandomBytes(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);
 
-      ByteBuffer encryptedDataLight = crypt(clearData, encrypter1);
-      ByteBuffer encryptedDataCipher = crypt(clearData, encrypter2);
-      assertEquals(encryptedDataCipher, encryptedDataLight);
+        byte[] encryptedDataLight = crypt(clearData, encrypter1);
+        byte[] encryptedDataCipher = crypt(clearData, encrypter2);
+        assertArrayEquals(encryptedDataCipher, encryptedDataLight);
 
-      ByteBuffer decryptedData = crypt(encryptedDataLight, encrypter1);
-      assertEquals(clearData, decryptedData);
-      decryptedData = crypt(encryptedDataLight, encrypter2);
-      assertEquals(clearData, decryptedData);
+        byte[] decryptedData = crypt(encryptedDataLight, encrypter1);
+        assertArrayEquals(clearData, decryptedData);
+        decryptedData = crypt(encryptedDataLight, encrypter2);
+        assertArrayEquals(clearData, decryptedData);
+      } catch (RuntimeException e) {
+        throw new RuntimeException("Exception at i=" + i, e);
+      }
     }
   }
 
@@ -60,29 +62,28 @@ public class AesCtrEncrypterTest extends RandomizedTest {
     return CipherAesCtrEncrypter.FACTORY;
   }
 
-  private static ByteBuffer generateRandomData(int numBytes) {
-    ByteBuffer buffer = ByteBuffer.allocate(numBytes);
+  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++) {
-      buffer.put((byte) randomInt());
+      b[i] = (byte) randomInt();
     }
-    buffer.position(0);
-    return buffer;
+    return b;
   }
 
-  private ByteBuffer crypt(ByteBuffer inputBuffer, AesCtrEncrypter encrypter) {
+  private byte[] crypt(byte[] 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(randomIntBetween(0, 50) + 1, inputBuffer.remaining());
-      ByteBuffer inputSlice = inputBuffer.slice();
-      inputSlice.limit(inputSlice.position() + length);
-      encrypter.process(inputSlice, outputBuffer);
-      inputBuffer.position(inputBuffer.position() + length);
+    byte[] outputBuffer = new byte[inputBuffer.length];
+    int inIndex = 0;
+    int outIndex = 0;
+    while (inIndex < inputBuffer.length) {
+      int length = Math.min(randomIntBetween(0, 50) + 1, inputBuffer.length - inIndex);
+      encrypter.process(inputBuffer, inIndex, length, outputBuffer, outIndex);
+      inIndex += length;
+      outIndex += length;
     }
-    inputBuffer.position(inputInitialPosition);
-    outputBuffer.position(0);
     return outputBuffer;
   }
 
diff --git a/encryption/src/test/java/org/apache/solr/encryption/crypto/DecryptingIndexInputTest.java b/encryption/src/test/java/org/apache/solr/encryption/crypto/DecryptingIndexInputTest.java
index 393f8c0..7caf30b 100644
--- a/encryption/src/test/java/org/apache/solr/encryption/crypto/DecryptingIndexInputTest.java
+++ b/encryption/src/test/java/org/apache/solr/encryption/crypto/DecryptingIndexInputTest.java
@@ -122,16 +122,17 @@ public class DecryptingIndexInputTest extends RandomizedTest {
       long outputLength = indexOutput.getFilePointer();
       indexOutput.close();
 
-      IndexInput indexInput = createDecryptingIndexInput(dataOutput, offset).slice("Test", prefix.length, outputLength - prefix.length - suffix.length);
+      try (IndexInput indexInput = createDecryptingIndexInput(dataOutput, offset)) {
+        IndexInput sliceInput = indexInput.slice("Test", prefix.length, outputLength - prefix.length - suffix.length);
 
-      assertEquals(0, indexInput.getFilePointer());
-      assertEquals(outputLength - prefix.length - suffix.length, indexInput.length());
-      for (IOConsumer<DataInput> c : reply) {
-        c.accept(indexInput);
-      }
+        assertEquals(0, sliceInput.getFilePointer());
+        assertEquals(outputLength - prefix.length - suffix.length, sliceInput.length());
+        for (IOConsumer<DataInput> c : reply) {
+          c.accept(sliceInput);
+        }
 
-      LuceneTestCase.expectThrows(EOFException.class, indexInput::readByte);
-      indexInput.close();
+        LuceneTestCase.expectThrows(EOFException.class, sliceInput::readByte);
+      }
     }
   }
 
@@ -173,31 +174,32 @@ public class DecryptingIndexInputTest extends RandomizedTest {
       long outputLength = indexOutput.getFilePointer();
       indexOutput.close();
 
-      IndexInput indexInput = createDecryptingIndexInput(dataOutput, offset).slice("Test", prefix.length, outputLength - prefix.length);
+      try (IndexInput indexInput = createDecryptingIndexInput(dataOutput, offset)) {
+        IndexInput sliceInput = indexInput.slice("Test", prefix.length, outputLength - prefix.length);
 
-      indexInput.seek(0);
-      for (IOConsumer<DataInput> c : reply) {
-        c.accept(indexInput);
-      }
+        sliceInput.seek(0);
+        for (IOConsumer<DataInput> c : reply) {
+          c.accept(sliceInput);
+        }
 
-      indexInput.seek(0);
-      for (IOConsumer<DataInput> c : reply) {
-        c.accept(indexInput);
-      }
+        sliceInput.seek(0);
+        for (IOConsumer<DataInput> c : reply) {
+          c.accept(sliceInput);
+        }
 
-      byte[] clearData = clearDataOutput.toArrayCopy();
-      clearData = ArrayUtil.copyOfSubArray(clearData, prefix.length, clearData.length);
+        byte[] clearData = clearDataOutput.toArrayCopy();
+        clearData = ArrayUtil.copyOfSubArray(clearData, prefix.length, clearData.length);
 
-      for (int i = 0; i < 1000; i++) {
-        int offs = randomIntBetween(0, clearData.length - 1);
-        indexInput.seek(offs);
-        assertEquals(offs, indexInput.getFilePointer());
-        assertEquals("reps=" + reps + " i=" + i + ", offs=" + offs, clearData[offs], indexInput.readByte());
+        for (int i = 0; i < 1000; i++) {
+          int offs = randomIntBetween(0, clearData.length - 1);
+          sliceInput.seek(offs);
+          assertEquals(offs, sliceInput.getFilePointer());
+          assertEquals("reps=" + reps + " i=" + i + ", offs=" + offs, clearData[offs], sliceInput.readByte());
+        }
+        sliceInput.seek(sliceInput.length());
+        assertEquals(sliceInput.length(), sliceInput.getFilePointer());
+        LuceneTestCase.expectThrows(EOFException.class, sliceInput::readByte);
       }
-      indexInput.seek(indexInput.length());
-      assertEquals(indexInput.length(), indexInput.getFilePointer());
-      LuceneTestCase.expectThrows(EOFException.class, indexInput::readByte);
-      indexInput.close();
     }
   }
 
diff --git a/encryption/src/test/java/org/apache/solr/encryption/crypto/EncryptingIndexOutputTest.java b/encryption/src/test/java/org/apache/solr/encryption/crypto/EncryptingIndexOutputTest.java
index 2de88e4..edb6849 100644
--- a/encryption/src/test/java/org/apache/solr/encryption/crypto/EncryptingIndexOutputTest.java
+++ b/encryption/src/test/java/org/apache/solr/encryption/crypto/EncryptingIndexOutputTest.java
@@ -61,13 +61,8 @@ public class EncryptingIndexOutputTest extends BaseDataOutputTestCase<Encrypting
     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;
-      }
-    };
+    // Reduce the buffer capacity to make sure we often write to the index output.
+    EncryptingIndexOutput indexOutput = new EncryptingIndexOutput(delegateIndexOutput, key, encrypterFactory(), AesCtrUtil.AES_BLOCK_SIZE);
     indexOutput.writeByte((byte) 3);
     // Check same file pointer.
     assertEquals(1, indexOutput.getFilePointer());