This is an automated email from the ASF dual-hosted git repository.
bruno-roustant 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 a72cc01 Fix invisible flaw in DecryptingIndexInput.seek() and extract
DecryptionBuffer (#131)
a72cc01 is described below
commit a72cc014bf14cc965515cdbd08dee183a39bb297
Author: Antoine Gruzelle <[email protected]>
AuthorDate: Fri May 22 15:21:27 2026 +0200
Fix invisible flaw in DecryptingIndexInput.seek() and extract
DecryptionBuffer (#131)
---
.../encryption/crypto/DecryptingIndexInput.java | 206 ++++++++++++++-------
.../crypto/DecryptingIndexInputTest.java | 37 ++++
2 files changed, 173 insertions(+), 70 deletions(-)
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 a459330..da1f3fa 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
@@ -51,14 +51,8 @@ public class DecryptingIndexInput extends FilterIndexInput {
private final long sliceOffset;
private final long sliceEnd;
private IndexInput indexInput;
- private AesCtrEncrypter encrypter;
- private byte[] inBuffer;
- private byte[] outBuffer;
+ private DecryptionBuffer buffer;
private byte[] oneByteBuf;
- private int inPos;
- private int outPos;
- private int outSize;
- private int padding;
private boolean closed;
/**
@@ -111,11 +105,7 @@ public class DecryptingIndexInput extends FilterIndexInput
{
this.sliceEnd = sliceOffset + sliceLength;
this.isClone = isClone;
this.indexInput = indexInput;
- this.encrypter = encrypter;
- encrypter.init(0);
- assert bufferCapacity % AES_BLOCK_SIZE == 0;
- inBuffer = new byte[bufferCapacity];
- outBuffer = new byte[bufferCapacity + AES_BLOCK_SIZE];
+ this.buffer = new DecryptionBuffer(encrypter, bufferCapacity);
oneByteBuf = new byte[1];
}
@@ -150,7 +140,7 @@ public class DecryptingIndexInput extends FilterIndexInput {
* Gets the current internal position in the delegate {@link IndexInput}. It
includes IV length.
*/
private long getPosition() {
- return indexInput.getFilePointer() - (outSize - outPos);
+ return indexInput.getFilePointer() - buffer.bufferedAhead();
}
@Override
@@ -163,27 +153,17 @@ public class DecryptingIndexInput extends
FilterIndexInput {
}
long targetPosition = position + sliceOffset;
long delegatePosition = indexInput.getFilePointer();
- long currentPosition = delegatePosition - (outSize - outPos);
- if (targetPosition >= currentPosition && targetPosition <=
delegatePosition) {
+ long currentPosition = delegatePosition - buffer.bufferedAhead();
+ if (buffer.hasData() && targetPosition >= currentPosition &&
targetPosition <= delegatePosition) {
// The target position is within the buffered output. Just move the
output buffer position.
- outPos += (int) (targetPosition - currentPosition);
- assert targetPosition == delegatePosition - (outSize - outPos);
+ buffer.skipBytes((int) (targetPosition - currentPosition));
+ assert targetPosition == delegatePosition - buffer.bufferedAhead();
} else {
indexInput.seek(targetPosition);
- setPosition(targetPosition);
+ buffer.seek(targetPosition, delegateOffset);
}
}
- private void setPosition(long position) {
- 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);
- inPos = padding;
- }
-
/**
* Returns the number of encrypted/decrypted bytes in the file.
* <p>It is the logical length of the file, not the physical length. It
excludes the IV added artificially to manage
@@ -203,7 +183,7 @@ public class DecryptingIndexInput extends FilterIndexInput {
}
DecryptingIndexInput slice = new DecryptingIndexInput(
getFullSliceDescription(sliceDescription), delegateOffset, sliceOffset +
offset, length, true,
- indexInput.clone(), encrypter.clone(), inBuffer.length);
+ indexInput.clone(), buffer.cloneEncrypter(), buffer.capacity());
slice.seek(0);
return slice;
}
@@ -224,42 +204,7 @@ public class DecryptingIndexInput extends FilterIndexInput
{
throw new EOFException("Read beyond EOF (position=" + (getPosition() -
sliceOffset) + ", arrayLength=" + length
+ ", fileLength=" + length() + ") in " + this);
}
- while (length > 0) {
- // Transfer decrypted bytes from outBuffer.
- int outRemaining = outSize - outPos;
- if (outRemaining > 0) {
- if (length <= outRemaining) {
- System.arraycopy(outBuffer, outPos, b, offset, length);
- outPos += length;
- return;
- }
- System.arraycopy(outBuffer, outPos, b, offset, outRemaining);
- outPos += outRemaining;
- offset += outRemaining;
- length -= outRemaining;
- }
- readToFillBuffer(length);
- decryptBuffer();
- }
- }
-
- private void readToFillBuffer(int length) throws IOException {
- assert length > 0;
- int inRemaining = inBuffer.length - inPos;
- if (inRemaining > 0) {
- int numBytesToRead = Math.min(inRemaining, length);
- indexInput.readBytes(inBuffer, inPos, numBytesToRead);
- inPos += numBytesToRead;
- }
- }
-
- private void decryptBuffer() {
- assert inPos > padding : "inPos=" + inPos + " padding=" + padding;
- encrypter.process(inBuffer, 0, inPos, outBuffer, 0);
- outSize = inPos;
- inPos = 0;
- outPos = padding;
- padding = 0;
+ buffer.readDecrypted(indexInput, b, offset, length);
}
@Override
@@ -268,12 +213,133 @@ public class DecryptingIndexInput extends
FilterIndexInput {
clone.isClone = true;
clone.indexInput = indexInput.clone();
assert clone.indexInput.getFilePointer() == indexInput.getFilePointer();
- clone.encrypter = encrypter.clone();
- clone.inBuffer = new byte[inBuffer.length];
- clone.outBuffer = new byte[outBuffer.length];
+ clone.buffer = buffer.clone();
clone.oneByteBuf = new byte[1];
// The clone must be initialized.
- clone.setPosition(getPosition());
+ clone.buffer.seek(getPosition(), delegateOffset);
return clone;
}
-}
\ No newline at end of file
+
+ /**
+ * Manages the AES-CTR decryption buffers and encrypter state.
+ * <p>Reads encrypted bytes from a delegate {@link IndexInput} into an input
buffer,
+ * decrypts them into an output buffer, and serves decrypted bytes to the
caller.
+ */
+ private static class DecryptionBuffer implements Cloneable {
+
+ AesCtrEncrypter encrypter;
+ byte[] inBuffer;
+ byte[] outBuffer;
+ int inPos;
+ int outPos;
+ int outSize;
+ int padding;
+
+ DecryptionBuffer(AesCtrEncrypter encrypter, int bufferCapacity) {
+ assert bufferCapacity % AES_BLOCK_SIZE == 0;
+ this.encrypter = encrypter;
+ encrypter.init(0);
+ inBuffer = new byte[bufferCapacity];
+ outBuffer = new byte[bufferCapacity + AES_BLOCK_SIZE];
+ }
+
+ /** Whether there are decrypted bytes available in the output buffer. */
+ boolean hasData() {
+ return outSize > 0;
+ }
+
+ /** Number of decrypted bytes in the output buffer not yet consumed. */
+ int bufferedAhead() {
+ return outSize - outPos;
+ }
+
+ /** The input buffer capacity (used when creating slices with the same
buffer size). */
+ int capacity() {
+ return inBuffer.length;
+ }
+
+ /**
+ * Advances the read position within the already-decrypted output buffer.
+ * The caller must ensure the target falls within the buffered range.
+ */
+ void skipBytes(int bytesCount) {
+ outPos += bytesCount;
+ assert outPos <= outSize;
+ }
+
+ /**
+ * Resets the buffer state and initializes the AES-CTR counter and padding
for the given
+ * absolute position in the delegate file.
+ *
+ * @param absolutePosition position in the delegate {@link IndexInput}
(includes IV offset)
+ * @param delegateOffset the position right after the IV — the "zero" of
the AES-CTR stream
+ */
+ void seek(long absolutePosition, long delegateOffset) {
+ outPos = outSize = 0;
+ long relativePosition = absolutePosition - delegateOffset;
+ long counter = relativePosition / AES_BLOCK_SIZE;
+ encrypter.init(counter);
+ padding = (int) (relativePosition & AES_BLOCK_SIZE_MOD_MASK);
+ inPos = padding;
+ }
+
+ /**
+ * Reads and decrypts bytes from the delegate into the target array.
+ * Serves from the output buffer first, then reads and decrypts more as
needed.
+ */
+ void readDecrypted(IndexInput delegate, byte[] target, int offset, int
length) throws IOException {
+ while (length > 0) {
+ int outRemaining = outSize - outPos;
+ if (outRemaining > 0) {
+ if (length <= outRemaining) {
+ System.arraycopy(outBuffer, outPos, target, offset, length);
+ outPos += length;
+ return;
+ }
+ System.arraycopy(outBuffer, outPos, target, offset, outRemaining);
+ outPos += outRemaining;
+ offset += outRemaining;
+ length -= outRemaining;
+ }
+ readFromDelegate(delegate, length);
+ decrypt();
+ }
+ }
+
+ private void readFromDelegate(IndexInput delegate, int length) throws
IOException {
+ assert length > 0;
+ int inRemaining = inBuffer.length - inPos;
+ if (inRemaining > 0) {
+ int numBytesToRead = Math.min(inRemaining, length);
+ delegate.readBytes(inBuffer, inPos, numBytesToRead);
+ inPos += numBytesToRead;
+ }
+ }
+
+ private void decrypt() {
+ assert inPos > padding : "inPos=" + inPos + " padding=" + padding;
+ encrypter.process(inBuffer, 0, inPos, outBuffer, 0);
+ outSize = inPos;
+ inPos = 0;
+ outPos = padding;
+ padding = 0;
+ }
+
+ AesCtrEncrypter cloneEncrypter() {
+ return encrypter.clone();
+ }
+
+ @Override
+ public DecryptionBuffer clone() {
+ try {
+ DecryptionBuffer clone = (DecryptionBuffer) super.clone();
+ clone.encrypter = encrypter.clone();
+ clone.inBuffer = new byte[inBuffer.length];
+ clone.outBuffer = new byte[outBuffer.length];
+ return clone;
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError(e);
+ }
+ }
+ }
+}
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 0e6337d..e820cf3 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
@@ -151,6 +151,43 @@ public class DecryptingIndexInputTest extends
RandomizedTest {
indexInput.close();
}
+ /**
+ * Reproduces a bug where creating a sub-slice at offset 0 from a slice at a
non-zero offset
+ * would produce corrupted decrypted data. This is the pattern used by
+ * {@link IndexInput#randomAccessSlice(long, long)}, which internally calls
+ * slice("randomaccess", 0, length).
+ *
+ * The root cause: when slice() creates a new DecryptingIndexInput, the
+ * constructor sets encrypter.init(0) (AES counter = 0) and the subsequent
+ * seek(0) is supposed to call setPosition() to initialize the correct
+ * AES counter and padding. But when the cloned delegate's file pointer
already equals the
+ * target position (which happens with offset=0), seek() took a buffer
shortcut
+ * and skipped setPosition(), leaving the counter and padding at wrong
values.
+ *
+ * With prefixSize=17: counter should be 17/16=1 (not 0) and padding should
be 17%16=1 (not 0).
+ * Both are wrong without the fix, triggering both dimensions of the bug.
+ */
+ @Test
+ public void testRandomAccessSlice() throws Exception {
+ ByteBuffersDataOutput dataOutput = new ByteBuffersDataOutput();
+ EncryptingIndexOutput indexOutput =
createEncryptingIndexOutput(dataOutput);
+ int bytesBeforeFirstSlice = 17;
+ byte[] prefix = new byte[bytesBeforeFirstSlice];
+ indexOutput.writeBytes(prefix, 0, prefix.length);
+ String data = "Hello, world!";
+ indexOutput.writeBytes(data.getBytes(), 0, data.length());
+ indexOutput.close();
+
+ DecryptingIndexInput root = createDecryptingIndexInput(dataOutput, 0);
+ // Simulates reading a sub-file (.tip for example) at a non-zero offset
inside a compound file (.cfs)
+ IndexInput firstSlice = root.slice(".tip", bytesBeforeFirstSlice,
data.length());
+ // Simulates randomAccessSlice(0, length) - creates a sub-slice at offset 0
+ IndexInput subSlice = firstSlice.slice("randomaccess", 0, data.length());
+ byte[] result = new byte[data.length()];
+ subSlice.readBytes(result, 0, data.length());
+ assertEquals(data, new String(result));
+ }
+
@Test
public void testSeek() throws Exception {
for (int reps = randomIntBetween(1, 200); --reps > 0; ) {