diff options
author | Andreas Beeker <kiwiwings@apache.org> | 2020-04-28 23:08:05 +0000 |
---|---|---|
committer | Andreas Beeker <kiwiwings@apache.org> | 2020-04-28 23:08:05 +0000 |
commit | 8b4f463ed3d11034888c672228e5057db802a92d (patch) | |
tree | 723277d09a44869d3befba9061ae5df87268ca8a /src | |
parent | 23acaff78d1e2a08a879a1422e4ed18ff6290e12 (diff) | |
download | poi-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')
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; + } + } + } |