bug 60153: encrypt SXSSF temporary files; patch from PJ Fanning

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1763943 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Javen O'Neal 2016-10-09 04:43:14 +00:00
parent dd1ed5b840
commit 3e4da0cd75
6 changed files with 343 additions and 132 deletions

View File

@ -24,8 +24,7 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.io.OutputStream;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
@ -56,22 +55,14 @@ public class GZIPSheetDataWriter extends SheetDataWriter {
return TempFile.createTempFile("poi-sxssf-sheet-xml", ".gz");
}
/**
* @return a wrapped instance of GZIPOutputStream
*/
@Override
public Writer createWriter(File fd)throws IOException {
return new OutputStreamWriter(new GZIPOutputStream(new FileOutputStream(fd)), "UTF-8");
protected InputStream decorateInputStream(FileInputStream fis) throws IOException {
return new GZIPInputStream(fis);
}
/**
* @return a GZIPInputStream stream to read the compressed temp file
*/
@Override
public InputStream getWorksheetXMLInputStream() throws IOException {
File fd = getTempFile();
return new GZIPInputStream(new FileInputStream(fd));
protected OutputStream decorateOutputStream(FileOutputStream fos) throws IOException {
return new GZIPOutputStream(fos);
}
}

View 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
{

View File

@ -25,6 +25,7 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Iterator;
@ -69,7 +70,7 @@ public class SheetDataWriter {
_out = createWriter(_fd);
}
public SheetDataWriter(SharedStringsTable sharedStringsTable) throws IOException{
public SheetDataWriter(SharedStringsTable sharedStringsTable) throws IOException {
this();
this._sharedStringSource = sharedStringsTable;
}
@ -90,8 +91,23 @@ public class SheetDataWriter {
*
* @param fd the file to write to
*/
public Writer createWriter(File fd)throws IOException {
return new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fd), "UTF-8"));
public Writer createWriter(File fd) throws IOException {
final OutputStream decorated = decorateOutputStream(new FileOutputStream(fd));
return new BufferedWriter(new OutputStreamWriter(decorated, "UTF-8"));
}
/**
* Override this to translate (such as encrypt or compress) the file output stream
* as it is being written to disk.
* The default behavior is to to pass the stream through unmodified.
*
* @param fos the stream to decorate
* @return a decorated stream
* @throws IOException
* @see #decorateInputStream(FileInputStream)
*/
protected OutputStream decorateOutputStream(FileOutputStream fos) throws IOException {
return fos;
}
/**
@ -112,7 +128,21 @@ public class SheetDataWriter {
*/
public InputStream getWorksheetXMLInputStream() throws IOException {
File fd = getTempFile();
return new FileInputStream(fd);
return decorateInputStream(new FileInputStream(fd));
}
/**
* Override this to translate (such as decrypt or expand) the file input stream
* as it is being read from disk.
* The default behavior is to to pass the stream through unmodified.
*
* @param fis the stream to decorate
* @return a decorated stream
* @throws IOException
* @see #decorateOutputStream(FileOutputStream)
*/
protected InputStream decorateInputStream(FileInputStream fis) throws IOException {
return fis;
}
public int getNumberOfFlushedRows() {

View File

@ -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);
}
}

View 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;
}
}
}

View File

@ -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);
}
}
}