From 02b3019f6cd999bcea8e9a7cb47e87de95711b02 Mon Sep 17 00:00:00 2001 From: Javen O'Neal Date: Sun, 9 Oct 2016 12:58:46 +0000 Subject: [PATCH] bug 60153: patch from PJ Fanning to demonstrate that SXSSFWorkbook SheetDataWriter can write encrypted temporary files to disk and the workbook can be AES encrypted when written to disk for a fully secure disk environment git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1763969 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/openxml4j/util/ZipEntrySource.java | 4 +- .../poifs/crypt/AesZipFileZipEntrySource.java | 6 +- ...SXSSFWorkbookWithCustomZipEntrySource.java | 49 ++++++++ ...SXSSFWorkbookWithCustomZipEntrySource.java | 110 ++++++++++++++++-- 4 files changed, 159 insertions(+), 10 deletions(-) create mode 100644 src/ooxml/testcases/org/apache/poi/xssf/streaming/TempFileRecordingSXSSFWorkbookWithCustomZipEntrySource.java diff --git a/src/ooxml/java/org/apache/poi/openxml4j/util/ZipEntrySource.java b/src/ooxml/java/org/apache/poi/openxml4j/util/ZipEntrySource.java index 51ad32ce6..8ce82325c 100644 --- a/src/ooxml/java/org/apache/poi/openxml4j/util/ZipEntrySource.java +++ b/src/ooxml/java/org/apache/poi/openxml4j/util/ZipEntrySource.java @@ -16,6 +16,7 @@ ==================================================================== */ package org.apache.poi.openxml4j.util; +import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.util.Enumeration; @@ -28,7 +29,7 @@ import java.util.zip.ZipEntry; * needing to worry about ZipFile vs ZipInputStream * being annoyingly very different. */ -public interface ZipEntrySource { +public interface ZipEntrySource extends Closeable { /** * Returns an Enumeration of all the Entries */ @@ -44,6 +45,7 @@ public interface ZipEntrySource { * Indicates we are done with reading, and * resources may be freed */ + @Override public void close() throws IOException; /** diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/AesZipFileZipEntrySource.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/AesZipFileZipEntrySource.java index 03cee8263..96a46c45e 100644 --- a/src/ooxml/testcases/org/apache/poi/poifs/crypt/AesZipFileZipEntrySource.java +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/AesZipFileZipEntrySource.java @@ -69,8 +69,10 @@ public class AesZipFileZipEntrySource implements ZipEntrySource { @Override public void close() throws IOException { - zipFile.close(); - tmpFile.delete(); + if(!closed) { + zipFile.close(); + tmpFile.delete(); + } closed = true; } diff --git a/src/ooxml/testcases/org/apache/poi/xssf/streaming/TempFileRecordingSXSSFWorkbookWithCustomZipEntrySource.java b/src/ooxml/testcases/org/apache/poi/xssf/streaming/TempFileRecordingSXSSFWorkbookWithCustomZipEntrySource.java new file mode 100644 index 000000000..6c5a6c87e --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/xssf/streaming/TempFileRecordingSXSSFWorkbookWithCustomZipEntrySource.java @@ -0,0 +1,49 @@ +/* ==================================================================== + 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 java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.poi.xssf.streaming.TestSXSSFWorkbookWithCustomZipEntrySource.SXSSFWorkbookWithCustomZipEntrySource; +import org.apache.poi.xssf.streaming.TestSXSSFWorkbookWithCustomZipEntrySource.SheetDataWriterWithDecorator; + +// a class to record a list of temporary files that are written to disk +// afterwards, a test function can check whether these files were encrypted or not +public class TempFileRecordingSXSSFWorkbookWithCustomZipEntrySource extends SXSSFWorkbookWithCustomZipEntrySource { + + private final List tempFiles = new ArrayList(); + + List getTempFiles() { + return new ArrayList(tempFiles); + } + + @Override + protected SheetDataWriter createSheetDataWriter() throws IOException { + return new TempFileRecordingSheetDataWriterWithDecorator(); + } + + class TempFileRecordingSheetDataWriterWithDecorator extends SheetDataWriterWithDecorator { + + TempFileRecordingSheetDataWriterWithDecorator() throws IOException { + super(); + tempFiles.add(getTempFile()); + } + } +} \ No newline at end of file diff --git a/src/ooxml/testcases/org/apache/poi/xssf/streaming/TestSXSSFWorkbookWithCustomZipEntrySource.java b/src/ooxml/testcases/org/apache/poi/xssf/streaming/TestSXSSFWorkbookWithCustomZipEntrySource.java index 8e1bd76ca..751e5f9a3 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/streaming/TestSXSSFWorkbookWithCustomZipEntrySource.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/streaming/TestSXSSFWorkbookWithCustomZipEntrySource.java @@ -19,10 +19,14 @@ package org.apache.poi.xssf.streaming; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -30,19 +34,25 @@ import java.io.InputStream; import java.io.OutputStream; import java.security.GeneralSecurityException; import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.CipherOutputStream; import javax.crypto.spec.SecretKeySpec; +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.openxml4j.opc.OPCPackage; 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.IOUtils; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; +import org.apache.poi.util.TempFile; import org.apache.poi.xssf.usermodel.XSSFCell; import org.apache.poi.xssf.usermodel.XSSFRow; import org.apache.poi.xssf.usermodel.XSSFSheet; @@ -54,11 +64,13 @@ import org.junit.Test; * is encrypted, but the final saved workbook is not encrypted */ public final class TestSXSSFWorkbookWithCustomZipEntrySource { - + + final String sheetName = "TestSheet1"; + final String cellValue = "customZipEntrySource"; + + // write an unencrypted workbook to disk, but any temporary files are encrypted @Test - public void customZipEntrySource() throws IOException, GeneralSecurityException { - final String sheetName = "TestSheet1"; - final String cellValue = "customZipEntrySource"; + public void customZipEntrySource() throws IOException { SXSSFWorkbookWithCustomZipEntrySource workbook = new SXSSFWorkbookWithCustomZipEntrySource(); SXSSFSheet sheet1 = workbook.createSheet(sheetName); SXSSFRow row1 = sheet1.createRow(1); @@ -77,6 +89,54 @@ public final class TestSXSSFWorkbookWithCustomZipEntrySource { xwb.close(); } + // write an encrypted workbook to disk, and encrypt any temporary files as well + @Test + public void customZipEntrySourceForWriteAndRead() throws IOException, GeneralSecurityException, InvalidFormatException { + SXSSFWorkbookWithCustomZipEntrySource workbook = new SXSSFWorkbookWithCustomZipEntrySource(); + SXSSFSheet sheet1 = workbook.createSheet(sheetName); + SXSSFRow row1 = sheet1.createRow(1); + SXSSFCell cell1 = row1.createCell(1); + cell1.setCellValue(cellValue); + EncryptedTempData tempData = new EncryptedTempData(); + workbook.write(tempData.getOutputStream()); + workbook.close(); + workbook.dispose(); + ZipEntrySource zipEntrySource = AesZipFileZipEntrySource.createZipEntrySource(tempData.getInputStream()); + tempData.dispose(); + OPCPackage opc = OPCPackage.open(zipEntrySource); + XSSFWorkbook xwb = new XSSFWorkbook(opc); + zipEntrySource.close(); + 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(); + opc.close(); + } + + @Test + public void validateTempFilesAreEncrypted() throws IOException { + TempFileRecordingSXSSFWorkbookWithCustomZipEntrySource workbook = new TempFileRecordingSXSSFWorkbookWithCustomZipEntrySource(); + 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(); + List tempFiles = workbook.getTempFiles(); + assertEquals(1, tempFiles.size()); + File tempFile = tempFiles.get(0); + assertTrue("tempFile exists?", tempFile.exists()); + byte[] data = IOUtils.toByteArray(new FileInputStream(tempFile)); + String text = new String(data, UTF_8); + assertFalse(text.contains(sheetName)); + assertFalse(text.contains(cellValue)); + workbook.dispose(); + assertFalse("tempFile deleted after dispose?", tempFile.exists()); + } + static class SXSSFWorkbookWithCustomZipEntrySource extends SXSSFWorkbook { private static final POILogger logger = POILogFactory.getLogger(SXSSFWorkbookWithCustomZipEntrySource.class); @@ -84,17 +144,19 @@ public final class TestSXSSFWorkbookWithCustomZipEntrySource { @Override public void write(OutputStream stream) throws IOException { flushSheets(); - ByteArrayOutputStream os = new ByteArrayOutputStream(); + EncryptedTempData tempData = new EncryptedTempData(); + OutputStream os = tempData.getOutputStream(); getXSSFWorkbook().write(os); + os.close(); ZipEntrySource source = null; try { // provide ZipEntrySource to poi which decrypts on the fly - source = AesZipFileZipEntrySource.createZipEntrySource(new ByteArrayInputStream(os.toByteArray())); + source = AesZipFileZipEntrySource.createZipEntrySource(tempData.getInputStream()); injectData(source, stream); } catch (GeneralSecurityException e) { throw new IOException(e); } finally { - source.close(); + IOUtils.closeQuietly(source); } } @@ -107,6 +169,7 @@ public final class TestSXSSFWorkbookWithCustomZipEntrySource { } } + static class SheetDataWriterWithDecorator extends SheetDataWriter { final static CipherAlgorithm cipherAlgorithm = CipherAlgorithm.aes128; SecretKeySpec skeySpec; @@ -141,4 +204,37 @@ public final class TestSXSSFWorkbookWithCustomZipEntrySource { } } + + // a class to save and read an AES-encrypted workbook + static class EncryptedTempData { + final static CipherAlgorithm cipherAlgorithm = CipherAlgorithm.aes128; + final SecretKeySpec skeySpec; + final byte[] ivBytes; + final File tempFile; + + EncryptedTempData() throws IOException { + 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); + tempFile = TempFile.createTempFile("poi-temp-data", ".tmp"); + } + + OutputStream getOutputStream() throws IOException { + Cipher ciEnc = CryptoFunctions.getCipher(skeySpec, cipherAlgorithm, ChainingMode.cbc, ivBytes, Cipher.ENCRYPT_MODE, null); + return new CipherOutputStream(new FileOutputStream(tempFile), ciEnc); + } + + InputStream getInputStream() throws IOException { + Cipher ciDec = CryptoFunctions.getCipher(skeySpec, cipherAlgorithm, ChainingMode.cbc, ivBytes, Cipher.DECRYPT_MODE, null); + return new CipherInputStream(new FileInputStream(tempFile), ciDec); + } + + void dispose() { + tempFile.delete(); + } + } + }