]> source.dussan.org Git - poi.git/commitdiff
#59841 - OOXML: enable custom zip streams via OPCPackage.open(ZipEntrySource)
authorAndreas Beeker <kiwiwings@apache.org>
Sun, 17 Jul 2016 00:14:06 +0000 (00:14 +0000)
committerAndreas Beeker <kiwiwings@apache.org>
Sun, 17 Jul 2016 00:14:06 +0000 (00:14 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1753003 13f79535-47bb-0310-9956-ffa450edef68

src/ooxml/java/org/apache/poi/openxml4j/opc/OPCPackage.java
src/ooxml/java/org/apache/poi/xssf/eventusermodel/ReadOnlySharedStringsTable.java
src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSecureTempZip.java [new file with mode: 0644]

index c70e496593bb9fc756a7e87edf0a8ab1d415a552..a438c061ff8b987f43698e74f53c79fe6a5b05ee 100644 (file)
@@ -53,6 +53,7 @@ import org.apache.poi.openxml4j.opc.internal.marshallers.ZipPackagePropertiesMar
 import org.apache.poi.openxml4j.opc.internal.unmarshallers.PackagePropertiesUnmarshaller;
 import org.apache.poi.openxml4j.opc.internal.unmarshallers.UnmarshallContext;
 import org.apache.poi.openxml4j.util.Nullable;
+import org.apache.poi.openxml4j.util.ZipEntrySource;
 import org.apache.poi.util.NotImplemented;
 import org.apache.poi.util.POILogFactory;
 import org.apache.poi.util.POILogger;
@@ -200,6 +201,42 @@ public abstract class OPCPackage implements RelationshipSource, Closeable {
       return open(file, defaultPackageAccess);
    }
 
+   /**
+    * Open an user provided {@link ZipEntrySource} with read-only permission.
+    * This method can be used to stream data into POI.
+    * Opposed to other open variants, the data is read as-is, e.g. there aren't
+    * any zip-bomb protection put in place.
+    *
+    * @param zipEntry the custom source
+    * @return A Package object
+    * @throws InvalidFormatException if a parsing error occur.
+    */
+   public static OPCPackage open(ZipEntrySource zipEntry)
+   throws InvalidFormatException {
+       OPCPackage pack = new ZipPackage(zipEntry, PackageAccess.READ);
+       try {
+           if (pack.partList == null) {
+               pack.getParts();
+           }
+           // pack.originalPackagePath = file.getAbsolutePath();
+           return pack;
+       } catch (InvalidFormatException e) {
+           try {
+               pack.close();
+           } catch (IOException e1) {
+               throw new IllegalStateException(e);
+           }
+           throw e;
+       } catch (RuntimeException e) {
+           try {
+               pack.close();
+           } catch (IOException e1) {
+               throw new IllegalStateException(e);
+           }
+           throw e;
+       }
+   }
+   
        /**
         * Open a package.
         *
index b03c505a64455ba322100d74bdc3cf0b57e923d2..f2554e6b4f2a387b1d93ff5be122010f92a92b91 100644 (file)
@@ -20,6 +20,7 @@ import static org.apache.poi.xssf.usermodel.XSSFRelation.NS_SPREADSHEETML;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.PushbackInputStream;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -140,8 +141,12 @@ public class ReadOnlySharedStringsTable extends DefaultHandler {
      * @throws ParserConfigurationException
      */
     public void readFrom(InputStream is) throws IOException, SAXException {
-        if (is.available() > 0) {
-            InputSource sheetSource = new InputSource(is);
+        // test if the file is empty, otherwise parse it
+        PushbackInputStream pis = new PushbackInputStream(is, 1);
+        int emptyTest = pis.read();
+        if (emptyTest > -1) {
+            pis.unread(emptyTest);
+            InputSource sheetSource = new InputSource(pis);
             try {
                 XMLReader sheetParser = SAXHelper.newXMLReader();
                 sheetParser.setContentHandler(this);
diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSecureTempZip.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSecureTempZip.java
new file mode 100644 (file)
index 0000000..9993d93
--- /dev/null
@@ -0,0 +1,175 @@
+/* ====================================================================
+   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 static org.junit.Assert.assertEquals;
+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.xssf.XSSFTestDataSamples;
+import org.apache.poi.xssf.extractor.XSSFEventBasedExcelExtractor;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.apache.xmlbeans.XmlException;
+import org.junit.Test;
+
+public class TestSecureTempZip {
+    /**
+     * Test case for #59841 - this is an example on how to use encrypted temp files,
+     * which are streamed into POI opposed to having everything in memory
+     */
+    @Test
+    public void protectedTempZip() throws IOException, GeneralSecurityException, XmlException, OpenXML4JException {
+        final File tmpFile = new File("build/tmp", "protectedXlsx.zip");
+        File tikaProt = XSSFTestDataSamples.getSampleFile("protected_passtika.xlsx");
+        FileInputStream fis = new FileInputStream(tikaProt);
+        POIFSFileSystem poifs = new POIFSFileSystem(fis);
+        EncryptionInfo ei = new EncryptionInfo(poifs);
+        Decryptor dec = ei.getDecryptor();
+        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);
+
+        // test the source
+        OPCPackage opc = OPCPackage.open(source);
+        String expected = "This is an Encrypted Excel spreadsheet.";
+        
+        XSSFEventBasedExcelExtractor extractor = new XSSFEventBasedExcelExtractor(opc);
+        extractor.setIncludeSheetNames(false);
+        String txt = extractor.getText();
+        assertEquals(expected, txt.trim());
+        
+        XSSFWorkbook wb = new XSSFWorkbook(opc);
+        txt = wb.getSheetAt(0).getRow(0).getCell(0).getStringCellValue();
+        assertEquals(expected, txt);
+
+        extractor.close();
+        
+        wb.close();
+        opc.close();
+        source.close();
+        poifs.close();
+        fis.close();
+    }
+    
+    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;
+
+        AesZipFileZipEntrySource(ZipFile zipFile, Cipher ci) {
+            this.zipFile = zipFile;
+            this.ci = ci;
+        }
+
+        /**
+         * 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();
+        }
+    }
+}