aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAndreas Beeker <kiwiwings@apache.org>2020-04-28 23:08:05 +0000
committerAndreas Beeker <kiwiwings@apache.org>2020-04-28 23:08:05 +0000
commit8b4f463ed3d11034888c672228e5057db802a92d (patch)
tree723277d09a44869d3befba9061ae5df87268ca8a /src
parent23acaff78d1e2a08a879a1422e4ed18ff6290e12 (diff)
downloadpoi-8b4f463ed3d11034888c672228e5057db802a92d.tar.gz
poi-8b4f463ed3d11034888c672228e5057db802a92d.zip
#64387 - Big POIFS stream result in OOM
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1877144 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to 'src')
-rw-r--r--src/java/org/apache/poi/poifs/filesystem/BlockStore.java35
-rw-r--r--src/java/org/apache/poi/poifs/filesystem/POIFSFileSystem.java14
-rw-r--r--src/java/org/apache/poi/poifs/filesystem/POIFSMiniStore.java15
-rw-r--r--src/java/org/apache/poi/poifs/filesystem/POIFSStream.java9
-rw-r--r--src/java/org/apache/poi/poifs/nio/FileBackedDataSource.java293
-rw-r--r--src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptor.java159
6 files changed, 323 insertions, 202 deletions
diff --git a/src/java/org/apache/poi/poifs/filesystem/BlockStore.java b/src/java/org/apache/poi/poifs/filesystem/BlockStore.java
index a56d111f3f..bdd016f860 100644
--- a/src/java/org/apache/poi/poifs/filesystem/BlockStore.java
+++ b/src/java/org/apache/poi/poifs/filesystem/BlockStore.java
@@ -1,4 +1,3 @@
-
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
@@ -32,53 +31,59 @@ public abstract class BlockStore {
* Returns the size of the blocks managed through the block store.
*/
protected abstract int getBlockStoreBlockSize();
-
+
/**
* Load the block at the given offset.
*/
protected abstract ByteBuffer getBlockAt(final int offset) throws IOException;
-
+
/**
* Extends the file if required to hold blocks up to
- * the specified offset, and return the block from there.
+ * the specified offset, and return the block from there.
*/
protected abstract ByteBuffer createBlockIfNeeded(final int offset) throws IOException;
-
+
+ /**
+ * Releases a mmap-ed buffer, which you are sure won't be used again
+ * @param buffer the buffer
+ */
+ protected abstract void releaseBuffer(ByteBuffer buffer);
+
/**
* Returns the BATBlock that handles the specified offset,
* and the relative index within it
*/
protected abstract BATBlockAndIndex getBATBlockAndIndex(final int offset);
-
+
/**
* Works out what block follows the specified one.
*/
protected abstract int getNextBlock(final int offset);
-
+
/**
* Changes the record of what block follows the specified one.
*/
protected abstract void setNextBlock(final int offset, final int nextBlock);
-
+
/**
* Finds a free block, and returns its offset.
* This method will extend the file/stream if needed, and if doing
* so, allocate new FAT blocks to address the extra space.
*/
protected abstract int getFreeBlock() throws IOException;
-
+
/**
- * Creates a Detector for loops in the chain
+ * Creates a Detector for loops in the chain
*/
protected abstract ChainLoopDetector getChainLoopDetector() throws IOException;
-
+
/**
* Used to detect if a chain has a loop in it, so
* we can bail out with an error rather than
- * spinning away for ever...
+ * spinning away for ever...
*/
protected class ChainLoopDetector {
- private boolean[] used_blocks;
+ private final boolean[] used_blocks;
protected ChainLoopDetector(long rawSize) {
int blkSize = getBlockStoreBlockSize();
int numBlocks = (int)(rawSize / blkSize);
@@ -94,11 +99,11 @@ public abstract class BlockStore {
// blocks we've allocated for them, so are safe
return;
}
-
+
// Claiming an existing block, ensure there's no loop
if(used_blocks[offset]) {
throw new IllegalStateException(
- "Potential loop detected - Block " + offset +
+ "Potential loop detected - Block " + offset +
" was already claimed but was just requested again"
);
}
diff --git a/src/java/org/apache/poi/poifs/filesystem/POIFSFileSystem.java b/src/java/org/apache/poi/poifs/filesystem/POIFSFileSystem.java
index 23d9b3e2b0..22577980a3 100644
--- a/src/java/org/apache/poi/poifs/filesystem/POIFSFileSystem.java
+++ b/src/java/org/apache/poi/poifs/filesystem/POIFSFileSystem.java
@@ -83,8 +83,8 @@ public class POIFSFileSystem extends BlockStore
private POIFSMiniStore _mini_store;
private PropertyTable _property_table;
- private List<BATBlock> _xbat_blocks;
- private List<BATBlock> _bat_blocks;
+ private final List<BATBlock> _xbat_blocks;
+ private final List<BATBlock> _bat_blocks;
private HeaderBlock _header;
private DirectoryNode _root;
@@ -113,7 +113,7 @@ public class POIFSFileSystem extends BlockStore
protected void createNewDataSource() {
// Data needs to initially hold just the header block,
// a single bat block, and an empty properties section
- long blockSize = ArithmeticUtils.mulAndCheck((long)bigBlockSize.getBigBlockSize(), (long)3);
+ long blockSize = ArithmeticUtils.mulAndCheck(bigBlockSize.getBigBlockSize(), 3L);
_data = new ByteArrayBackedDataSource(IOUtils.safelyAllocate(blockSize, MAX_RECORD_LENGTH));
}
@@ -409,7 +409,7 @@ public class POIFSFileSystem extends BlockStore
// Ensure there's a spot in the file for it
ByteBuffer buffer = ByteBuffer.allocate(bigBlockSize.getBigBlockSize());
// Header isn't in BATs
- long writeTo = ArithmeticUtils.mulAndCheck((1 + (long)offset), (long)bigBlockSize.getBigBlockSize());
+ long writeTo = ArithmeticUtils.mulAndCheck(1L + offset, bigBlockSize.getBigBlockSize());
_data.write(buffer, writeTo);
// All done
return newBAT;
@@ -937,6 +937,12 @@ public class POIFSFileSystem extends BlockStore
return _header;
}
+ @Override
+ protected void releaseBuffer(ByteBuffer buffer) {
+ if (_data instanceof FileBackedDataSource) {
+ ((FileBackedDataSource)_data).releaseBuffer(buffer);
+ }
+ }
private static void sanityCheckBlockCount(int block_count) throws IOException {
if (block_count <= 0) {
diff --git a/src/java/org/apache/poi/poifs/filesystem/POIFSMiniStore.java b/src/java/org/apache/poi/poifs/filesystem/POIFSMiniStore.java
index 5bd83cbdf5..069ebc0292 100644
--- a/src/java/org/apache/poi/poifs/filesystem/POIFSMiniStore.java
+++ b/src/java/org/apache/poi/poifs/filesystem/POIFSMiniStore.java
@@ -37,11 +37,11 @@ import org.apache.poi.poifs.storage.HeaderBlock;
*/
public class POIFSMiniStore extends BlockStore
{
- private POIFSFileSystem _filesystem;
+ private final POIFSFileSystem _filesystem;
private POIFSStream _mini_stream;
- private List<BATBlock> _sbat_blocks;
- private HeaderBlock _header;
- private RootProperty _root;
+ private final List<BATBlock> _sbat_blocks;
+ private final HeaderBlock _header;
+ private final RootProperty _root;
POIFSMiniStore(POIFSFileSystem filesystem, RootProperty root,
List<BATBlock> sbats, HeaderBlock header)
@@ -93,7 +93,7 @@ public class POIFSMiniStore extends BlockStore
if (! firstInStore) {
try {
return getBlockAt(offset);
- } catch(NoSuchElementException e) {}
+ } catch(NoSuchElementException ignored) {}
}
// Need to extend the stream
@@ -259,4 +259,9 @@ public class POIFSMiniStore extends BlockStore
// RootProperty.setSize does the sbat -> bytes conversion for us
_filesystem._get_property_table().getRoot().setSize(blocksUsed);
}
+
+ @Override
+ protected void releaseBuffer(ByteBuffer buffer) {
+ _filesystem.releaseBuffer(buffer);
+ }
}
diff --git a/src/java/org/apache/poi/poifs/filesystem/POIFSStream.java b/src/java/org/apache/poi/poifs/filesystem/POIFSStream.java
index 87bd12092d..356b78c9f7 100644
--- a/src/java/org/apache/poi/poifs/filesystem/POIFSStream.java
+++ b/src/java/org/apache/poi/poifs/filesystem/POIFSStream.java
@@ -47,7 +47,7 @@ import org.apache.poi.poifs.storage.HeaderBlock;
public class POIFSStream implements Iterable<ByteBuffer>
{
- private BlockStore blockStore;
+ private final BlockStore blockStore;
private int startBlock;
private OutputStream outStream;
@@ -140,8 +140,8 @@ public class POIFSStream implements Iterable<ByteBuffer>
/**
* Class that handles a streaming read of one stream
*/
- protected class StreamBlockByteBufferIterator implements Iterator<ByteBuffer> {
- private ChainLoopDetector loopDetector;
+ private class StreamBlockByteBufferIterator implements Iterator<ByteBuffer> {
+ private final ChainLoopDetector loopDetector;
private int nextBlock;
StreamBlockByteBufferIterator(int firstBlock) {
@@ -221,6 +221,9 @@ public class POIFSStream implements Iterable<ByteBuffer>
nextBlock = blockStore.getNextBlock(thisBlock);
}
+ if (buffer != null) {
+ blockStore.releaseBuffer(buffer);
+ }
buffer = blockStore.createBlockIfNeeded(thisBlock);
// Update pointers
diff --git a/src/java/org/apache/poi/poifs/nio/FileBackedDataSource.java b/src/java/org/apache/poi/poifs/nio/FileBackedDataSource.java
index ee08de13d7..5b77db4d92 100644
--- a/src/java/org/apache/poi/poifs/nio/FileBackedDataSource.java
+++ b/src/java/org/apache/poi/poifs/nio/FileBackedDataSource.java
@@ -17,10 +17,6 @@
package org.apache.poi.poifs.nio;
-import org.apache.poi.util.IOUtils;
-import org.apache.poi.util.POILogFactory;
-import org.apache.poi.util.POILogger;
-
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -30,151 +26,160 @@ import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.IdentityHashMap;
+
+import org.apache.poi.util.IOUtils;
+import org.apache.poi.util.POILogFactory;
+import org.apache.poi.util.POILogger;
/**
* A POIFS {@link DataSource} backed by a File
*/
public class FileBackedDataSource extends DataSource {
- private final static POILogger logger = POILogFactory.getLogger( FileBackedDataSource.class );
-
- private FileChannel channel;
- private boolean writable;
- // remember file base, which needs to be closed too
- private RandomAccessFile srcFile;
-
- // Buffers which map to a file-portion are not closed automatically when the Channel is closed
- // therefore we need to keep the list of mapped buffers and do some ugly reflection to try to
- // clean the buffer during close().
- // See https://bz.apache.org/bugzilla/show_bug.cgi?id=58480,
- // http://stackoverflow.com/questions/3602783/file-access-synchronized-on-java-object and
- // http://bugs.java.com/view_bug.do?bug_id=4724038 for related discussions
- private List<ByteBuffer> buffersToClean = new ArrayList<>();
-
- public FileBackedDataSource(File file) throws FileNotFoundException {
- this(newSrcFile(file, "r"), true);
- }
-
- public FileBackedDataSource(File file, boolean readOnly) throws FileNotFoundException {
- this(newSrcFile(file, readOnly ? "r" : "rw"), readOnly);
- }
-
- public FileBackedDataSource(RandomAccessFile srcFile, boolean readOnly) {
- this(srcFile.getChannel(), readOnly);
- this.srcFile = srcFile;
- }
-
- public FileBackedDataSource(FileChannel channel, boolean readOnly) {
- this.channel = channel;
- this.writable = !readOnly;
- }
-
- public boolean isWriteable() {
- return this.writable;
- }
-
- public FileChannel getChannel() {
- return this.channel;
- }
-
- @Override
- public ByteBuffer read(int length, long position) throws IOException {
- if(position >= size()) {
- throw new IndexOutOfBoundsException("Position " + position + " past the end of the file");
- }
-
- // TODO Could we do the read-only case with MapMode.PRIVATE instead?
- // See https://docs.oracle.com/javase/7/docs/api/java/nio/channels/FileChannel.MapMode.html#PRIVATE
- // Or should we have 3 modes instead of the current boolean -
- // read-write, read-only, read-to-write-elsewhere?
-
- // Do we read or map (for read/write)?
- ByteBuffer dst;
- if (writable) {
- dst = channel.map(FileChannel.MapMode.READ_WRITE, position, length);
-
- // remember this buffer for cleanup
- buffersToClean.add(dst);
- } else {
- // allocate the buffer on the heap if we cannot map the data in directly
- channel.position(position);
- dst = ByteBuffer.allocate(length);
-
- // Read the contents and check that we could read some data
- int worked = IOUtils.readFully(channel, dst);
- if(worked == -1) {
- throw new IndexOutOfBoundsException("Position " + position + " past the end of the file");
- }
- }
-
- // make it ready for reading
- dst.position(0);
-
- // All done
- return dst;
- }
-
- @Override
- public void write(ByteBuffer src, long position) throws IOException {
- channel.write(src, position);
- }
-
- @Override
- public void copyTo(OutputStream stream) throws IOException {
- // Wrap the OutputSteam as a channel
- try (WritableByteChannel out = Channels.newChannel(stream)) {
- // Now do the transfer
- channel.transferTo(0, channel.size(), out);
- }
- }
-
- @Override
- public long size() throws IOException {
- return channel.size();
- }
-
- @Override
- public void close() throws IOException {
- // also ensure that all buffers are unmapped so we do not keep files locked on Windows
- // We consider it a bug if a Buffer is still in use now!
- for(ByteBuffer buffer : buffersToClean) {
- unmap(buffer);
- }
- buffersToClean.clear();
-
- if (srcFile != null) {
- // see http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4796385
- srcFile.close();
- } else {
- channel.close();
- }
- }
-
- private static RandomAccessFile newSrcFile(File file, String mode) throws FileNotFoundException {
- if(!file.exists()) {
- throw new FileNotFoundException(file.toString());
+ private final static POILogger logger = POILogFactory.getLogger(FileBackedDataSource.class);
+
+ private final FileChannel channel;
+ private final boolean writable;
+ // remember file base, which needs to be closed too
+ private RandomAccessFile srcFile;
+
+ // Buffers which map to a file-portion are not closed automatically when the Channel is closed
+ // therefore we need to keep the list of mapped buffers and do some ugly reflection to try to
+ // clean the buffer during close().
+ // See https://bz.apache.org/bugzilla/show_bug.cgi?id=58480,
+ // http://stackoverflow.com/questions/3602783/file-access-synchronized-on-java-object and
+ // http://bugs.java.com/view_bug.do?bug_id=4724038 for related discussions
+ // https://stackoverflow.com/questions/36077641/java-when-does-direct-buffer-released
+ private final IdentityHashMap<ByteBuffer,ByteBuffer> buffersToClean = new IdentityHashMap<>();
+
+ public FileBackedDataSource(File file) throws FileNotFoundException {
+ this(newSrcFile(file, "r"), true);
+ }
+
+ public FileBackedDataSource(File file, boolean readOnly) throws FileNotFoundException {
+ this(newSrcFile(file, readOnly ? "r" : "rw"), readOnly);
+ }
+
+ public FileBackedDataSource(RandomAccessFile srcFile, boolean readOnly) {
+ this(srcFile.getChannel(), readOnly);
+ this.srcFile = srcFile;
+ }
+
+ public FileBackedDataSource(FileChannel channel, boolean readOnly) {
+ this.channel = channel;
+ this.writable = !readOnly;
+ }
+
+ public boolean isWriteable() {
+ return this.writable;
+ }
+
+ public FileChannel getChannel() {
+ return this.channel;
+ }
+
+ @Override
+ public ByteBuffer read(int length, long position) throws IOException {
+ if (position >= size()) {
+ throw new IndexOutOfBoundsException("Position " + position + " past the end of the file");
+ }
+
+ // TODO Could we do the read-only case with MapMode.PRIVATE instead?
+ // See https://docs.oracle.com/javase/7/docs/api/java/nio/channels/FileChannel.MapMode.html#PRIVATE
+ // Or should we have 3 modes instead of the current boolean -
+ // read-write, read-only, read-to-write-elsewhere?
+
+ // Do we read or map (for read/write)?
+ ByteBuffer dst;
+ if (writable) {
+ dst = channel.map(FileChannel.MapMode.READ_WRITE, position, length);
+
+ // remember this buffer for cleanup
+ buffersToClean.put(dst,dst);
+ } else {
+ // allocate the buffer on the heap if we cannot map the data in directly
+ channel.position(position);
+ dst = ByteBuffer.allocate(length);
+
+ // Read the contents and check that we could read some data
+ int worked = IOUtils.readFully(channel, dst);
+ if (worked == -1) {
+ throw new IndexOutOfBoundsException("Position " + position + " past the end of the file");
+ }
+ }
+
+ // make it ready for reading
+ dst.position(0);
+
+ // All done
+ return dst;
+ }
+
+ @Override
+ public void write(ByteBuffer src, long position) throws IOException {
+ channel.write(src, position);
+ }
+
+ @Override
+ public void copyTo(OutputStream stream) throws IOException {
+ // Wrap the OutputSteam as a channel
+ try (WritableByteChannel out = Channels.newChannel(stream)) {
+ // Now do the transfer
+ channel.transferTo(0, channel.size(), out);
+ }
+ }
+
+ @Override
+ public long size() throws IOException {
+ return channel.size();
+ }
+
+ public void releaseBuffer(ByteBuffer buffer) {
+ ByteBuffer previous = buffersToClean.remove(buffer);
+ if (previous != null) {
+ unmap(previous);
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ // also ensure that all buffers are unmapped so we do not keep files locked on Windows
+ // We consider it a bug if a Buffer is still in use now!
+ buffersToClean.forEach((k,v) -> unmap(v));
+ buffersToClean.clear();
+
+ if (srcFile != null) {
+ // see http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4796385
+ srcFile.close();
+ } else {
+ channel.close();
+ }
+ }
+
+ private static RandomAccessFile newSrcFile(File file, String mode) throws FileNotFoundException {
+ if (!file.exists()) {
+ throw new FileNotFoundException(file.toString());
}
return new RandomAccessFile(file, mode);
- }
-
- // need to use reflection to avoid depending on the sun.nio internal API
- // unfortunately this might break silently with newer/other Java implementations,
- // but we at least have unit-tests which will indicate this when run on Windows
- private static void unmap(final ByteBuffer buffer) {
- // not necessary for HeapByteBuffer, avoid lots of log-output on this class
- if(buffer.getClass().getName().endsWith("HeapByteBuffer")) {
- return;
- }
-
- if (CleanerUtil.UNMAP_SUPPORTED) {
- try {
- CleanerUtil.getCleaner().freeBuffer(buffer);
- } catch (IOException e) {
- logger.log(POILogger.WARN, "Failed to unmap the buffer", e);
- }
- } else {
- logger.log(POILogger.DEBUG, CleanerUtil.UNMAP_NOT_SUPPORTED_REASON);
- }
- }
+ }
+
+ // need to use reflection to avoid depending on the sun.nio internal API
+ // unfortunately this might break silently with newer/other Java implementations,
+ // but we at least have unit-tests which will indicate this when run on Windows
+ private static void unmap(final ByteBuffer buffer) {
+ // not necessary for HeapByteBuffer, avoid lots of log-output on this class
+ if (buffer.getClass().getName().endsWith("HeapByteBuffer")) {
+ return;
+ }
+
+ if (CleanerUtil.UNMAP_SUPPORTED) {
+ try {
+ CleanerUtil.getCleaner().freeBuffer(buffer);
+ } catch (IOException e) {
+ logger.log(POILogger.WARN, "Failed to unmap the buffer", e);
+ }
+ } else {
+ logger.log(POILogger.DEBUG, CleanerUtil.UNMAP_NOT_SUPPORTED_REASON);
+ }
+ }
}
diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptor.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptor.java
index d7d83ae95a..55314125f0 100644
--- a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptor.java
+++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptor.java
@@ -16,6 +16,13 @@
==================================================================== */
package org.apache.poi.poifs.crypt;
+import static org.apache.poi.poifs.crypt.CryptoFunctions.getMessageDigest;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -23,7 +30,11 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.security.DigestInputStream;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
import java.util.Iterator;
+import java.util.Random;
import javax.crypto.Cipher;
@@ -33,8 +44,14 @@ import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.poifs.crypt.agile.AgileDecryptor;
import org.apache.poi.poifs.crypt.agile.AgileEncryptionHeader;
import org.apache.poi.poifs.crypt.agile.AgileEncryptionVerifier;
-import org.apache.poi.poifs.filesystem.*;
+import org.apache.poi.poifs.filesystem.DirectoryNode;
+import org.apache.poi.poifs.filesystem.DocumentEntry;
+import org.apache.poi.poifs.filesystem.DocumentNode;
+import org.apache.poi.poifs.filesystem.Entry;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+import org.apache.poi.poifs.filesystem.TempFilePOIFSFileSystem;
import org.apache.poi.util.IOUtils;
+import org.apache.poi.util.NullOutputStream;
import org.apache.poi.util.TempFile;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
@@ -42,13 +59,11 @@ import org.junit.Assume;
import org.junit.Ignore;
import org.junit.Test;
-import static org.junit.Assert.*;
-
public class TestEncryptor {
@Test
public void binaryRC4Encryption() throws Exception {
// please contribute a real sample file, which is binary rc4 encrypted
- // ... at least the output can be opened in Excel Viewer
+ // ... at least the output can be opened in Excel Viewer
String password = "pass";
final byte[] payloadExpected;
@@ -80,7 +95,7 @@ public class TestEncryptor {
payloadActual = IOUtils.toByteArray(is);
}
}
-
+
assertArrayEquals(payloadExpected, payloadActual);
}
@@ -167,7 +182,7 @@ public class TestEncryptor {
// the hmacs of the file always differ, as we use PKCS5-padding to pad the bytes
// whereas office just uses random bytes
// byte integrityHash[] = d.getIntegrityHmacValue();
-
+
final EncryptionInfo infoActual = new EncryptionInfo(
EncryptionMode.agile
, infoExpected.getVerifier().getCipherAlgorithm()
@@ -211,7 +226,7 @@ public class TestEncryptor {
encPackActual = IOUtils.toByteArray(is, entry.getSize()-16);
}
}
-
+
AgileEncryptionHeader aehExpected = (AgileEncryptionHeader)infoExpected.getHeader();
AgileEncryptionHeader aehActual = (AgileEncryptionHeader)infoActual.getHeader();
assertArrayEquals(aehExpected.getEncryptedHmacKey(), aehActual.getEncryptedHmacKey());
@@ -219,7 +234,7 @@ public class TestEncryptor {
assertArrayEquals(payloadExpected, payloadActual);
assertArrayEquals(encPackExpected, encPackActual);
}
-
+
@Test
public void standardEncryption() throws Exception {
File file = POIDataSamples.getDocumentInstance().getFile("bug53475-password-is-solrcell.docx");
@@ -247,8 +262,8 @@ public class TestEncryptor {
final byte[] verifierExpected = d.getVerifier();
final byte[] keySpec = d.getSecretKey().getEncoded();
final byte[] keySalt = infoExpected.getHeader().getKeySalt();
-
-
+
+
final EncryptionInfo infoActual = new EncryptionInfo(
EncryptionMode.standard
, infoExpected.getVerifier().getCipherAlgorithm()
@@ -257,7 +272,7 @@ public class TestEncryptor {
, infoExpected.getHeader().getBlockSize()
, infoExpected.getVerifier().getChainingMode()
);
-
+
final Encryptor e = Encryptor.getInstance(infoActual);
e.confirmPassword(pass, keySpec, keySalt, verifierExpected, verifierSaltExpected, null);
@@ -304,10 +319,10 @@ public class TestEncryptor {
assertArrayEquals(payloadExpected, payloadActual);
}
-
+
/**
* Ensure we can encrypt a package that is missing the Core
- * Properties, eg one from dodgy versions of Jasper Reports
+ * Properties, eg one from dodgy versions of Jasper Reports
* See https://github.com/nestoru/xlsxenc/ and
* http://stackoverflow.com/questions/28593223
*/
@@ -341,7 +356,7 @@ public class TestEncryptor {
encBytes = baos.toByteArray();
}
}
-
+
try (POIFSFileSystem inpFS = new POIFSFileSystem(new ByteArrayInputStream(encBytes))) {
// Check we can decrypt it
@@ -359,7 +374,7 @@ public class TestEncryptor {
}
}
}
-
+
@Test
@Ignore
public void inPlaceRewrite() throws Exception {
@@ -369,7 +384,7 @@ public class TestEncryptor {
InputStream fis = POIDataSamples.getPOIFSInstance().openResourceAsStream("protected_agile.docx")) {
IOUtils.copy(fis, fos);
}
-
+
try (POIFSFileSystem fs = new POIFSFileSystem(f, false)) {
// decrypt the protected file - in this case it was encrypted with the default password
@@ -396,26 +411,26 @@ public class TestEncryptor {
}
}
}
-
-
+
+
private void listEntry(DocumentNode de, String ext, String path) throws IOException {
path += "\\" + de.getName().replaceAll("[\\p{Cntrl}]", "_");
System.out.println(ext+": "+path+" ("+de.getSize()+" bytes)");
-
+
String name = de.getName().replaceAll("[\\p{Cntrl}]", "_");
-
+
InputStream is = ((DirectoryNode)de.getParent()).createDocumentInputStream(de);
FileOutputStream fos = new FileOutputStream("solr."+name+"."+ext);
IOUtils.copy(is, fos);
fos.close();
is.close();
}
-
+
@SuppressWarnings("unused")
private void listDir(DirectoryNode dn, String ext, String path) throws IOException {
path += "\\" + dn.getName().replace('\u0006', '_');
System.out.println(ext+": "+path+" ("+dn.getStorageClsid()+")");
-
+
Iterator<Entry> iter = dn.getEntries();
while (iter.hasNext()) {
Entry ent = iter.next();
@@ -447,21 +462,21 @@ public class TestEncryptor {
// @@ -208,6 +208,13 @@
// protected int invokeCipher(int posInChunk, boolean doFinal) throws GeneralSecurityException {
// byte plain[] = (_plainByteFlags.isEmpty()) ? null : _chunk.clone();
- //
+ //
// + if (posInChunk < 4096) {
// + _cipher.update(_chunk, 0, posInChunk, _chunk);
// + byte bla[] = { (byte)0x7A,(byte)0x0F,(byte)0x27,(byte)0xF0,(byte)0x17,(byte)0x6E,(byte)0x77,(byte)0x05,(byte)0xB9,(byte)0xDA,(byte)0x49,(byte)0xF9,(byte)0xD7,(byte)0x8E,(byte)0x03,(byte)0x1D };
// + System.arraycopy(bla, 0, _chunk, posInChunk-2, bla.length);
// + return posInChunk-2+bla.length;
// + }
- // +
+ // +
// int ciLen = (doFinal)
// ? _cipher.doFinal(_chunk, 0, posInChunk, _chunk)
// : _cipher.update(_chunk, 0, posInChunk, _chunk);
//
// --- src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileDecryptor.java (revision 1766745)
// +++ src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileDecryptor.java (working copy)
- //
+ //
// @@ -300,7 +297,7 @@
// protected static Cipher initCipherForBlock(Cipher existing, int block, boolean lastChunk, EncryptionInfo encryptionInfo, SecretKey skey, int encryptionMode)
// throws GeneralSecurityException {
@@ -495,12 +510,12 @@ public class TestEncryptor {
aehHeader.setCipherAlgorithm(CipherAlgorithm.aes128);
aehHeader.setHashAlgorithm(HashAlgorithm.sha1);
AgileEncryptionVerifier aehVerifier = (AgileEncryptionVerifier)eiNew.getVerifier();
-
+
// this cast might look strange - if the setters would be public, it will become obsolete
// see http://stackoverflow.com/questions/5637650/overriding-protected-methods-in-java
((EncryptionVerifier)aehVerifier).setCipherAlgorithm(CipherAlgorithm.aes256);
aehVerifier.setHashAlgorithm(HashAlgorithm.sha512);
-
+
Encryptor enc = eiNew.getEncryptor();
enc.confirmPassword("Test001!!",
infoOrig.getDecryptor().getSecretKey().getEncoded(),
@@ -529,10 +544,10 @@ public class TestEncryptor {
}
assertArrayEquals(epOrigBytes, epNewBytes);
-
+
Decryptor decReload = infoReload.getDecryptor();
assertTrue(decReload.verifyPassword("Test001!!"));
-
+
AgileEncryptionHeader aehOrig = (AgileEncryptionHeader)infoOrig.getHeader();
AgileEncryptionHeader aehReload = (AgileEncryptionHeader)infoReload.getHeader();
assertEquals(aehOrig.getBlockSize(), aehReload.getBlockSize());
@@ -547,7 +562,7 @@ public class TestEncryptor {
assertEquals(aehOrig.getHashAlgorithm(), aehReload.getHashAlgorithm());
assertArrayEquals(aehOrig.getKeySalt(), aehReload.getKeySalt());
assertEquals(aehOrig.getKeySize(), aehReload.getKeySize());
-
+
AgileEncryptionVerifier aevOrig = (AgileEncryptionVerifier)infoOrig.getVerifier();
AgileEncryptionVerifier aevReload = (AgileEncryptionVerifier)infoReload.getVerifier();
assertEquals(aevOrig.getBlockSize(), aevReload.getBlockSize());
@@ -563,11 +578,93 @@ public class TestEncryptor {
AgileDecryptor adOrig = (AgileDecryptor)infoOrig.getDecryptor();
AgileDecryptor adReload = (AgileDecryptor)infoReload.getDecryptor();
-
+
assertArrayEquals(adOrig.getIntegrityHmacKey(), adReload.getIntegrityHmacKey());
// doesn't work without mocking ... see above
// assertArrayEquals(adOrig.getIntegrityHmacValue(), adReload.getIntegrityHmacValue());
assertArrayEquals(adOrig.getSecretKey().getEncoded(), adReload.getSecretKey().getEncoded());
assertArrayEquals(adOrig.getVerifier(), adReload.getVerifier());
}
+
+ @Test
+ public void smallFile() throws IOException, GeneralSecurityException {
+ // see https://stackoverflow.com/questions/61463301
+ final int tinyFileSize = 80_000_000;
+ final String pass = "s3cr3t";
+
+ File tmpFile = TempFile.createTempFile("tiny", ".bin");
+
+ // create/populate empty file
+ try (POIFSFileSystem poifs = new POIFSFileSystem();
+ FileOutputStream fos = new FileOutputStream(tmpFile)) {
+ poifs.writeFilesystem(fos);
+ }
+
+ EncryptionInfo info1 = new EncryptionInfo(EncryptionMode.agile);
+ Encryptor enc = info1.getEncryptor();
+ enc.confirmPassword(pass);
+
+ final MessageDigest md = getMessageDigest(HashAlgorithm.sha256);
+
+ // reopen as mmap-ed file
+ try (POIFSFileSystem poifs = new POIFSFileSystem(tmpFile, false)) {
+ try (OutputStream os = enc.getDataStream(poifs);
+ RandomStream rs = new RandomStream(md)) {
+ IOUtils.copy(rs, os, tinyFileSize);
+ }
+ poifs.writeFilesystem();
+ }
+
+ final byte[] digest1 = md.digest();
+ md.reset();
+
+ // reopen and check the digest
+ try (POIFSFileSystem poifs = new POIFSFileSystem(tmpFile)) {
+ EncryptionInfo info2 = new EncryptionInfo(poifs);
+ Decryptor dec = info2.getDecryptor();
+ boolean passOk = dec.verifyPassword(pass);
+ assertTrue(passOk);
+
+ try (InputStream is = dec.getDataStream(poifs);
+ DigestInputStream dis = new DigestInputStream(is, md);
+ NullOutputStream nos = new NullOutputStream()) {
+ IOUtils.copy(dis, nos);
+ }
+ }
+
+ final byte[] digest2 = md.digest();
+ assertArrayEquals(digest1, digest2);
+
+ boolean isDeleted = tmpFile.delete();
+ assertTrue(isDeleted);
+ }
+
+ private static final class RandomStream extends InputStream {
+ private final Random rand = new Random();
+ private final byte[] buf = new byte[1024];
+ private final MessageDigest md;
+
+ private RandomStream(MessageDigest md) {
+ this.md = md;
+ }
+
+ @Override
+ public int read() {
+ int ret = rand.nextInt(256);
+ md.update((byte)ret);
+ return ret;
+ }
+
+ @Override
+ public int read(byte[] b, final int off, int len) {
+ for (int start = off; start-off < len; start += buf.length) {
+ rand.nextBytes(buf);
+ int copyLen = Math.min(buf.length, len-(start-off));
+ System.arraycopy(buf, 0, b, start, copyLen);
+ md.update(buf, 0, copyLen);
+ }
+ return len;
+ }
+ }
+
}