]> source.dussan.org Git - poi.git/commitdiff
bug 60153: encrypt SXSSF temporary files; patch from PJ Fanning
authorJaven O'Neal <onealj@apache.org>
Sun, 9 Oct 2016 04:43:14 +0000 (04:43 +0000)
committerJaven O'Neal <onealj@apache.org>
Sun, 9 Oct 2016 04:43:14 +0000 (04:43 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1763943 13f79535-47bb-0310-9956-ffa450edef68

src/ooxml/java/org/apache/poi/xssf/streaming/GZIPSheetDataWriter.java
src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFWorkbook.java
src/ooxml/java/org/apache/poi/xssf/streaming/SheetDataWriter.java
src/ooxml/testcases/org/apache/poi/poifs/crypt/AesZipFileZipEntrySource.java [new file with mode: 0644]
src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSecureTempZip.java
src/ooxml/testcases/org/apache/poi/xssf/streaming/TestSXSSFWorkbookWithCustomZipEntrySource.java [new file with mode: 0644]

index d194f4fb164ca36e4bd2e843efcebf3f150b9b6d..45ce381531c02cffc56e20421c37ea29584d7afa 100644 (file)
@@ -24,8 +24,7 @@ import java.io.FileInputStream;
 import java.io.FileOutputStream;\r
 import java.io.IOException;\r
 import java.io.InputStream;\r
-import java.io.OutputStreamWriter;\r
-import java.io.Writer;\r
+import java.io.OutputStream;\r
 import java.util.zip.GZIPInputStream;\r
 import java.util.zip.GZIPOutputStream;\r
 \r
@@ -56,22 +55,14 @@ public class GZIPSheetDataWriter extends SheetDataWriter {
         return TempFile.createTempFile("poi-sxssf-sheet-xml", ".gz");\r
     }\r
 \r
-    /**\r
-     * @return a wrapped instance of GZIPOutputStream\r
-     */\r
     @Override\r
-       public Writer createWriter(File fd)throws IOException {\r
-        return new OutputStreamWriter(new GZIPOutputStream(new FileOutputStream(fd)), "UTF-8");\r
+    protected InputStream decorateInputStream(FileInputStream fis) throws IOException {\r
+        return new GZIPInputStream(fis);\r
     }\r
 \r
-\r
-    /**\r
-     * @return a GZIPInputStream stream to read the compressed temp file\r
-     */\r
     @Override\r
-       public InputStream getWorksheetXMLInputStream() throws IOException {\r
-        File fd = getTempFile();\r
-        return new GZIPInputStream(new FileInputStream(fd));\r
+    protected OutputStream decorateOutputStream(FileOutputStream fos) throws IOException {\r
+        return new GZIPOutputStream(fos);\r
     }\r
 \r
 }\r
index 6b9ce374fbb00a71652207df1c5b4e6084403430..18a77bf569e835a4205f5a0554cdd53b15d6e9d4 100644 (file)
@@ -35,6 +35,8 @@ import java.util.zip.ZipFile;
 import java.util.zip.ZipOutputStream;
 
 import org.apache.poi.openxml4j.opc.OPCPackage;
+import org.apache.poi.openxml4j.util.ZipEntrySource;
+import org.apache.poi.openxml4j.util.ZipFileZipEntrySource;
 import org.apache.poi.ss.SpreadsheetVersion;
 import org.apache.poi.ss.formula.udf.UDFFinder;
 import org.apache.poi.ss.usermodel.CellStyle;
@@ -46,6 +48,7 @@ import org.apache.poi.ss.usermodel.PictureData;
 import org.apache.poi.ss.usermodel.Row.MissingCellPolicy;
 import org.apache.poi.ss.usermodel.Sheet;
 import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.util.Internal;
 import org.apache.poi.util.NotImplemented;
 import org.apache.poi.util.POILogFactory;
 import org.apache.poi.util.POILogger;
@@ -287,6 +290,14 @@ public class SXSSFWorkbook implements Workbook {
         _randomAccessWindowSize = rowAccessWindowSize;
     }
 
+    /**
+     * Get whether temp files should be compressed.
+     *
+     * @return whether to compress temp files
+     */
+    public boolean isCompressTempFiles() {
+        return _compressTmpFiles;
+    }
     /**
      * Set whether temp files should be compressed.
      * <p>
@@ -300,11 +311,16 @@ public class SXSSFWorkbook implements Workbook {
      * </p>
      * @param compress whether to compress temp files
      */
-    public void setCompressTempFiles(boolean compress){
+    public void setCompressTempFiles(boolean compress) {
         _compressTmpFiles = compress;
     }
+    
+    @Internal
+    protected SharedStringsTable getSharedStringSource() {
+        return _sharedStringSource;
+    }
 
-    SheetDataWriter createSheetDataWriter() throws IOException {
+    protected SheetDataWriter createSheetDataWriter() throws IOException {
         if(_compressTmpFiles) {
             return new GZIPSheetDataWriter(_sharedStringSource);
         }
@@ -353,21 +369,19 @@ public class SXSSFWorkbook implements Workbook {
         return null;
     }
 
-    private void injectData(File zipfile, OutputStream out) throws IOException 
+    protected void injectData(ZipEntrySource zipEntrySource, OutputStream out) throws IOException 
     {
-       // don't use ZipHelper.openZipFile here - see #59743
-        ZipFile zip = new ZipFile(zipfile);
         try
         {
             ZipOutputStream zos = new ZipOutputStream(out);
             try
             {
-                Enumeration<? extends ZipEntry> en = zip.entries();
+                Enumeration<? extends ZipEntry> en = zipEntrySource.getEntries();
                 while (en.hasMoreElements()) 
                 {
                     ZipEntry ze = en.nextElement();
                     zos.putNextEntry(new ZipEntry(ze.getName()));
-                    InputStream is = zip.getInputStream(ze);
+                    InputStream is = zipEntrySource.getInputStream(ze);
                     XSSFSheet xSheet=getSheetFromZipEntryName(ze.getName());
                     if(xSheet!=null)
                     {
@@ -396,7 +410,7 @@ public class SXSSFWorkbook implements Workbook {
         }
         finally
         {
-            zip.close();
+            zipEntrySource.close();
         }
     }
     private static void copyStream(InputStream in, OutputStream out) throws IOException {
@@ -945,7 +959,8 @@ public class SXSSFWorkbook implements Workbook {
             }
 
             //Substitute the template entries with the generated sheet data files
-            injectData(tmplFile, stream);
+            final ZipEntrySource source = new ZipFileZipEntrySource(new ZipFile(tmplFile));
+            injectData(source, stream);
         }
         finally
         {
index 4bd0c6bad96929537a48b3cebd2e0bdd2ddf3da6..e9b0b8017e80a23f97d018770dd6aae3d52bf74d 100644 (file)
@@ -25,6 +25,7 @@ import java.io.FileInputStream;
 import java.io.FileOutputStream;\r
 import java.io.IOException;\r
 import java.io.InputStream;\r
+import java.io.OutputStream;\r
 import java.io.OutputStreamWriter;\r
 import java.io.Writer;\r
 import java.util.Iterator;\r
@@ -69,7 +70,7 @@ public class SheetDataWriter {
         _out = createWriter(_fd);\r
     }\r
 \r
-    public SheetDataWriter(SharedStringsTable sharedStringsTable) throws IOException{\r
+    public SheetDataWriter(SharedStringsTable sharedStringsTable) throws IOException {\r
         this();\r
         this._sharedStringSource = sharedStringsTable;\r
     }\r
@@ -90,8 +91,23 @@ public class SheetDataWriter {
      * \r
      * @param  fd the file to write to\r
      */\r
-    public Writer createWriter(File fd)throws IOException {\r
-        return new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fd), "UTF-8"));\r
+    public Writer createWriter(File fd) throws IOException {\r
+        final OutputStream decorated = decorateOutputStream(new FileOutputStream(fd));\r
+        return new BufferedWriter(new OutputStreamWriter(decorated, "UTF-8"));\r
+    }\r
+    \r
+    /**\r
+     * Override this to translate (such as encrypt or compress) the file output stream\r
+     * as it is being written to disk.\r
+     * The default behavior is to to pass the stream through unmodified.\r
+     *\r
+     * @param fos  the stream to decorate\r
+     * @return a decorated stream\r
+     * @throws IOException\r
+     * @see #decorateInputStream(FileInputStream)\r
+     */\r
+    protected OutputStream decorateOutputStream(FileOutputStream fos) throws IOException {\r
+        return fos;\r
     }\r
 \r
     /**\r
@@ -112,7 +128,21 @@ public class SheetDataWriter {
      */\r
     public InputStream getWorksheetXMLInputStream() throws IOException {\r
         File fd = getTempFile();\r
-        return new FileInputStream(fd);\r
+        return decorateInputStream(new FileInputStream(fd));\r
+    }\r
+    \r
+    /**\r
+     * Override this to translate (such as decrypt or expand) the file input stream\r
+     * as it is being read from disk.\r
+     * The default behavior is to to pass the stream through unmodified.\r
+     *\r
+     * @param fis  the stream to decorate\r
+     * @return a decorated stream\r
+     * @throws IOException\r
+     * @see #decorateOutputStream(FileOutputStream)\r
+     */\r
+    protected InputStream decorateInputStream(FileInputStream fis) throws IOException {\r
+        return fis;\r
     }\r
 \r
     public int getNumberOfFlushedRows() {\r
diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/AesZipFileZipEntrySource.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/AesZipFileZipEntrySource.java
new file mode 100644 (file)
index 0000000..03cee82
--- /dev/null
@@ -0,0 +1,135 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+
+package org.apache.poi.poifs.crypt;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.util.Enumeration;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.poi.openxml4j.util.ZipEntrySource;
+import org.apache.poi.util.IOUtils;
+import org.apache.poi.util.TempFile;
+
+public class AesZipFileZipEntrySource implements ZipEntrySource {
+    final File tmpFile;
+    final ZipFile zipFile;
+    final Cipher ci;
+    boolean closed;
+
+    public AesZipFileZipEntrySource(File tmpFile, Cipher ci) throws IOException {
+        this.tmpFile = tmpFile;
+        this.zipFile = new ZipFile(tmpFile);
+        this.ci = ci;
+        this.closed = false;
+    }
+
+    /**
+     * Note: the file sizes are rounded up to the next cipher block size,
+     * so don't rely on file sizes of these custom encrypted zip file entries!
+     */
+    public Enumeration<? extends ZipEntry> getEntries() {
+        return zipFile.entries();
+    }
+
+    @SuppressWarnings("resource")
+    public InputStream getInputStream(ZipEntry entry) throws IOException {
+        InputStream is = zipFile.getInputStream(entry);
+        return new CipherInputStream(is, ci);
+    }
+
+    @Override
+    public void close() throws IOException {
+        zipFile.close();
+        tmpFile.delete();
+        closed = true;
+    }
+    
+    @Override
+    public boolean isClosed() {
+        return closed;
+    }
+    
+    public static ZipEntrySource createZipEntrySource(InputStream is) throws IOException, GeneralSecurityException {
+        // generate session key
+        SecureRandom sr = new SecureRandom();
+        byte[] ivBytes = new byte[16], keyBytes = new byte[16];
+        sr.nextBytes(ivBytes);
+        sr.nextBytes(keyBytes);
+        final File tmpFile = TempFile.createTempFile("protectedXlsx", ".zip");
+        copyToFile(is, tmpFile, CipherAlgorithm.aes128, keyBytes, ivBytes);
+        IOUtils.closeQuietly(is);
+        return fileToSource(tmpFile, CipherAlgorithm.aes128, keyBytes, ivBytes);
+    }
+    
+    private static void copyToFile(InputStream is, File tmpFile, CipherAlgorithm cipherAlgorithm, byte keyBytes[], byte ivBytes[]) throws IOException, GeneralSecurityException {
+        SecretKeySpec skeySpec = new SecretKeySpec(keyBytes, cipherAlgorithm.jceId);
+        Cipher ciEnc = CryptoFunctions.getCipher(skeySpec, cipherAlgorithm, ChainingMode.cbc, ivBytes, Cipher.ENCRYPT_MODE, "PKCS5Padding");
+
+        ZipInputStream zis = new ZipInputStream(is);
+        FileOutputStream fos = new FileOutputStream(tmpFile);
+        ZipOutputStream zos = new ZipOutputStream(fos);
+
+        ZipEntry ze;
+        while ((ze = zis.getNextEntry()) != null) {
+            // the cipher output stream pads the data, therefore we can't reuse the ZipEntry with set sizes
+            // as those will be validated upon close()
+            ZipEntry zeNew = new ZipEntry(ze.getName());
+            zeNew.setComment(ze.getComment());
+            zeNew.setExtra(ze.getExtra());
+            zeNew.setTime(ze.getTime());
+            // zeNew.setMethod(ze.getMethod());
+            zos.putNextEntry(zeNew);
+            FilterOutputStream fos2 = new FilterOutputStream(zos){
+                // don't close underlying ZipOutputStream
+                public void close() {}
+            };
+            CipherOutputStream cos = new CipherOutputStream(fos2, ciEnc);
+            IOUtils.copy(zis, cos);
+            cos.close();
+            fos2.close();
+            zos.closeEntry();
+            zis.closeEntry();
+        }
+        zos.close();
+        fos.close();
+        zis.close();
+    }
+    
+    private static ZipEntrySource fileToSource(File tmpFile, CipherAlgorithm cipherAlgorithm, byte keyBytes[], byte ivBytes[]) throws ZipException, IOException {
+        SecretKeySpec skeySpec = new SecretKeySpec(keyBytes, cipherAlgorithm.jceId);
+        Cipher ciDec = CryptoFunctions.getCipher(skeySpec, cipherAlgorithm, ChainingMode.cbc, ivBytes, Cipher.DECRYPT_MODE, "PKCS5Padding");
+        return new AesZipFileZipEntrySource(tmpFile, ciDec);
+    }
+
+}
+
index 868a382279e3be6c554ff75180b86a03b5c80814..77ae0033ab8496324684c47dad05240ee7bcc78d 100644 (file)
@@ -22,30 +22,14 @@ import static org.junit.Assert.assertTrue;
 
 import java.io.File;
 import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.FilterOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.security.GeneralSecurityException;
-import java.security.SecureRandom;
-import java.util.Enumeration;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipException;
-import java.util.zip.ZipFile;
-import java.util.zip.ZipInputStream;
-import java.util.zip.ZipOutputStream;
-
-import javax.crypto.Cipher;
-import javax.crypto.CipherInputStream;
-import javax.crypto.CipherOutputStream;
-import javax.crypto.spec.SecretKeySpec;
 
 import org.apache.poi.openxml4j.exceptions.OpenXML4JException;
 import org.apache.poi.openxml4j.opc.OPCPackage;
 import org.apache.poi.openxml4j.util.ZipEntrySource;
 import org.apache.poi.poifs.filesystem.POIFSFileSystem;
-import org.apache.poi.util.IOUtils;
-import org.apache.poi.util.TempFile;
 import org.apache.poi.xssf.XSSFTestDataSamples;
 import org.apache.poi.xssf.extractor.XSSFEventBasedExcelExtractor;
 import org.apache.poi.xssf.usermodel.XSSFWorkbook;
@@ -59,7 +43,6 @@ public class TestSecureTempZip {
      */
     @Test
     public void protectedTempZip() throws IOException, GeneralSecurityException, XmlException, OpenXML4JException {
-        final File tmpFile = TempFile.createTempFile("protectedXlsx", ".zip");
         File tikaProt = XSSFTestDataSamples.getSampleFile("protected_passtika.xlsx");
         FileInputStream fis = new FileInputStream(tikaProt);
         POIFSFileSystem poifs = new POIFSFileSystem(fis);
@@ -68,19 +51,11 @@ public class TestSecureTempZip {
         boolean passOk = dec.verifyPassword("tika");
         assertTrue(passOk);
 
-        // generate session key
-        SecureRandom sr = new SecureRandom();
-        byte[] ivBytes = new byte[16], keyBytes = new byte[16];
-        sr.nextBytes(ivBytes);
-        sr.nextBytes(keyBytes);
-        
         // extract encrypted ooxml file and write to custom encrypted zip file 
         InputStream is = dec.getDataStream(poifs);
-        copyToFile(is, tmpFile, CipherAlgorithm.aes128, keyBytes, ivBytes);
-        is.close();
         
         // provide ZipEntrySource to poi which decrypts on the fly
-        ZipEntrySource source = fileToSource(tmpFile, CipherAlgorithm.aes128, keyBytes, ivBytes);
+        ZipEntrySource source = AesZipFileZipEntrySource.createZipEntrySource(is);
 
         // test the source
         OPCPackage opc = OPCPackage.open(source);
@@ -102,84 +77,5 @@ public class TestSecureTempZip {
         source.close();
         poifs.close();
         fis.close();
-        tmpFile.delete();
-    }
-    
-    private void copyToFile(InputStream is, File tmpFile, CipherAlgorithm cipherAlgorithm, byte keyBytes[], byte ivBytes[]) throws IOException, GeneralSecurityException {
-        SecretKeySpec skeySpec = new SecretKeySpec(keyBytes, cipherAlgorithm.jceId);
-        Cipher ciEnc = CryptoFunctions.getCipher(skeySpec, cipherAlgorithm, ChainingMode.cbc, ivBytes, Cipher.ENCRYPT_MODE, "PKCS5Padding");
-
-        ZipInputStream zis = new ZipInputStream(is);
-        FileOutputStream fos = new FileOutputStream(tmpFile);
-        ZipOutputStream zos = new ZipOutputStream(fos);
-
-        ZipEntry ze;
-        while ((ze = zis.getNextEntry()) != null) {
-            // the cipher output stream pads the data, therefore we can't reuse the ZipEntry with set sizes
-            // as those will be validated upon close()
-            ZipEntry zeNew = new ZipEntry(ze.getName());
-            zeNew.setComment(ze.getComment());
-            zeNew.setExtra(ze.getExtra());
-            zeNew.setTime(ze.getTime());
-            // zeNew.setMethod(ze.getMethod());
-            zos.putNextEntry(zeNew);
-            FilterOutputStream fos2 = new FilterOutputStream(zos){
-                // don't close underlying ZipOutputStream
-                public void close() {}
-            };
-            CipherOutputStream cos = new CipherOutputStream(fos2, ciEnc);
-            IOUtils.copy(zis, cos);
-            cos.close();
-            fos2.close();
-            zos.closeEntry();
-            zis.closeEntry();
-        }
-        zos.close();
-        fos.close();
-        zis.close();
-    }
-    
-    private ZipEntrySource fileToSource(File tmpFile, CipherAlgorithm cipherAlgorithm, byte keyBytes[], byte ivBytes[]) throws ZipException, IOException {
-        SecretKeySpec skeySpec = new SecretKeySpec(keyBytes, cipherAlgorithm.jceId);
-        Cipher ciDec = CryptoFunctions.getCipher(skeySpec, cipherAlgorithm, ChainingMode.cbc, ivBytes, Cipher.DECRYPT_MODE, "PKCS5Padding");
-        ZipFile zf = new ZipFile(tmpFile);
-        return new AesZipFileZipEntrySource(zf, ciDec);
-    }
-
-    static class AesZipFileZipEntrySource implements ZipEntrySource {
-        final ZipFile zipFile;
-        final Cipher ci;
-        boolean closed;
-
-        AesZipFileZipEntrySource(ZipFile zipFile, Cipher ci) {
-            this.zipFile = zipFile;
-            this.ci = ci;
-            this.closed = false;
-        }
-
-        /**
-         * Note: the file sizes are rounded up to the next cipher block size,
-         * so don't rely on file sizes of these custom encrypted zip file entries!
-         */
-        public Enumeration<? extends ZipEntry> getEntries() {
-            return zipFile.entries();
-        }
-
-        @SuppressWarnings("resource")
-        public InputStream getInputStream(ZipEntry entry) throws IOException {
-            InputStream is = zipFile.getInputStream(entry);
-            return new CipherInputStream(is, ci);
-        }
-
-        @Override
-        public void close() throws IOException {
-            zipFile.close();
-            closed = true;
-        }
-        
-        @Override
-        public boolean isClosed() {
-            return closed;
-        }
     }
 }
diff --git a/src/ooxml/testcases/org/apache/poi/xssf/streaming/TestSXSSFWorkbookWithCustomZipEntrySource.java b/src/ooxml/testcases/org/apache/poi/xssf/streaming/TestSXSSFWorkbookWithCustomZipEntrySource.java
new file mode 100644 (file)
index 0000000..8e1bd76
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+ *  ====================================================================
+ *    Licensed to the Apache Software Foundation (ASF) under one or more
+ *    contributor license agreements.  See the NOTICE file distributed with
+ *    this work for additional information regarding copyright ownership.
+ *    The ASF licenses this file to You under the Apache License, Version 2.0
+ *    (the "License"); you may not use this file except in compliance with
+ *    the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ * ====================================================================
+ */
+
+package org.apache.poi.xssf.streaming;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.poi.openxml4j.util.ZipEntrySource;
+import org.apache.poi.poifs.crypt.AesZipFileZipEntrySource;
+import org.apache.poi.poifs.crypt.ChainingMode;
+import org.apache.poi.poifs.crypt.CipherAlgorithm;
+import org.apache.poi.poifs.crypt.CryptoFunctions;
+import org.apache.poi.util.POILogFactory;
+import org.apache.poi.util.POILogger;
+import org.apache.poi.xssf.usermodel.XSSFCell;
+import org.apache.poi.xssf.usermodel.XSSFRow;
+import org.apache.poi.xssf.usermodel.XSSFSheet;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.junit.Test;
+
+/**
+ * This class tests that an SXSSFWorkbook can be written and read where all temporary disk I/O
+ * is encrypted, but the final saved workbook is not encrypted
+ */
+public final class TestSXSSFWorkbookWithCustomZipEntrySource {
+
+    @Test
+    public void customZipEntrySource() throws IOException, GeneralSecurityException {
+        final String sheetName = "TestSheet1";
+        final String cellValue = "customZipEntrySource";
+        SXSSFWorkbookWithCustomZipEntrySource workbook = new SXSSFWorkbookWithCustomZipEntrySource();
+        SXSSFSheet sheet1 = workbook.createSheet(sheetName);
+        SXSSFRow row1 = sheet1.createRow(1);
+        SXSSFCell cell1 = row1.createCell(1);
+        cell1.setCellValue(cellValue);
+        ByteArrayOutputStream os = new ByteArrayOutputStream(8192);
+        workbook.write(os);
+        workbook.close();
+        workbook.dispose();
+        XSSFWorkbook xwb = new XSSFWorkbook(new ByteArrayInputStream(os.toByteArray()));
+        XSSFSheet xs1 = xwb.getSheetAt(0);
+        assertEquals(sheetName, xs1.getSheetName());
+        XSSFRow xr1 = xs1.getRow(1);
+        XSSFCell xc1 = xr1.getCell(1);
+        assertEquals(cellValue, xc1.getStringCellValue());
+        xwb.close();
+    }
+    
+    static class SXSSFWorkbookWithCustomZipEntrySource extends SXSSFWorkbook {
+
+        private static final POILogger logger = POILogFactory.getLogger(SXSSFWorkbookWithCustomZipEntrySource.class);
+
+        @Override
+        public void write(OutputStream stream) throws IOException {
+            flushSheets();
+            ByteArrayOutputStream os = new ByteArrayOutputStream();
+            getXSSFWorkbook().write(os);
+            ZipEntrySource source = null;
+            try {
+                // provide ZipEntrySource to poi which decrypts on the fly
+                source = AesZipFileZipEntrySource.createZipEntrySource(new ByteArrayInputStream(os.toByteArray()));
+                injectData(source, stream);
+            } catch (GeneralSecurityException e) {
+                throw new IOException(e);
+            } finally {
+                source.close();
+            }
+        }
+
+        @Override
+        protected SheetDataWriter createSheetDataWriter() throws IOException {
+            //log values to ensure these values are accessible to subclasses
+            logger.log(POILogger.INFO, "isCompressTempFiles: " + isCompressTempFiles());
+            logger.log(POILogger.INFO, "SharedStringSource: " + getSharedStringSource());
+            return new SheetDataWriterWithDecorator();
+        }
+    }
+    
+    static class SheetDataWriterWithDecorator extends SheetDataWriter {
+        final static CipherAlgorithm cipherAlgorithm = CipherAlgorithm.aes128;
+        SecretKeySpec skeySpec;
+        byte[] ivBytes;
+
+        public SheetDataWriterWithDecorator() throws IOException {
+            super();
+        }
+
+        void init() {
+            if(skeySpec == null) {
+                SecureRandom sr = new SecureRandom();
+                ivBytes = new byte[16];
+                byte[] keyBytes = new byte[16];
+                sr.nextBytes(ivBytes);
+                sr.nextBytes(keyBytes);
+                skeySpec = new SecretKeySpec(keyBytes, cipherAlgorithm.jceId);
+            }
+        }
+        
+        @Override
+        protected OutputStream decorateOutputStream(FileOutputStream fos) {
+            init();
+            Cipher ciEnc = CryptoFunctions.getCipher(skeySpec, cipherAlgorithm, ChainingMode.cbc, ivBytes, Cipher.ENCRYPT_MODE, "PKCS5Padding");
+            return new CipherOutputStream(fos, ciEnc);
+        }
+
+        @Override
+        protected InputStream decorateInputStream(FileInputStream fis) {
+            Cipher ciDec = CryptoFunctions.getCipher(skeySpec, cipherAlgorithm, ChainingMode.cbc, ivBytes, Cipher.DECRYPT_MODE, "PKCS5Padding");
+            return new CipherInputStream(fis, ciDec);
+        }
+        
+    }
+}