blob: 2fb1749d4bfc5cbdfe8d28fd947a8e9caece56d1 [file] [log] [blame]
/*
* ====================================================================
* 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.temp;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.SecureRandom;
import java.util.Enumeration;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.poi.openxml4j.util.ZipEntrySource;
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.Beta;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.TempFile;
/**
* An example <code>ZipEntrySource</code> that has encrypted temp files to ensure that
* sensitive data is not stored in raw format on disk.
*/
@Beta
public final class AesZipFileZipEntrySource implements ZipEntrySource {
private static final Logger LOG = LogManager.getLogger(AesZipFileZipEntrySource.class);
private static final String PADDING = "PKCS5Padding";
private final File tmpFile;
private final ZipFile zipFile;
private final Cipher ci;
private boolean closed;
private 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!
*/
@Override
public Enumeration<? extends ZipArchiveEntry> getEntries() {
return zipFile.getEntries();
}
@Override
public ZipArchiveEntry getEntry(String path) {
return zipFile.getEntry(path);
}
@Override
public InputStream getInputStream(ZipArchiveEntry entry) throws IOException {
InputStream is = zipFile.getInputStream(entry);
return new CipherInputStream(is, ci);
}
@Override
public void close() throws IOException {
if(!closed) {
zipFile.close();
if (!tmpFile.delete()) {
LOG.atWarn().log("{} can't be removed (or was already removed.", tmpFile.getAbsolutePath());
}
}
closed = true;
}
@Override
public boolean isClosed() {
return closed;
}
public static AesZipFileZipEntrySource createZipEntrySource(InputStream is) throws IOException {
// 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, keyBytes, ivBytes);
IOUtils.closeQuietly(is);
return fileToSource(tmpFile, keyBytes, ivBytes);
}
private static void copyToFile(InputStream is, File tmpFile, byte[] keyBytes, byte[] ivBytes) throws IOException {
SecretKeySpec skeySpec = new SecretKeySpec(keyBytes, CipherAlgorithm.aes128.jceId);
Cipher ciEnc = CryptoFunctions.getCipher(skeySpec, CipherAlgorithm.aes128, ChainingMode.cbc, ivBytes, Cipher.ENCRYPT_MODE, PADDING);
try (ZipArchiveInputStream zis = new ZipArchiveInputStream(is);
FileOutputStream fos = new FileOutputStream(tmpFile);
ZipArchiveOutputStream zos = new ZipArchiveOutputStream(fos)) {
ZipArchiveEntry ze;
while ((ze = zis.getNextZipEntry()) != 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()
ZipArchiveEntry zeNew = new ZipArchiveEntry(ze.getName());
zeNew.setComment(ze.getComment());
zeNew.setExtra(ze.getExtra());
zeNew.setTime(ze.getTime());
// zeNew.setMethod(ze.getMethod());
zos.putArchiveEntry(zeNew);
FilterOutputStream fos2 = new FilterOutputStream(zos) {
// don't close underlying ZipOutputStream
@Override
public void close() {
}
};
CipherOutputStream cos = new CipherOutputStream(fos2, ciEnc);
IOUtils.copy(zis, cos);
cos.close();
fos2.close();
zos.closeArchiveEntry();
}
}
}
private static AesZipFileZipEntrySource fileToSource(File tmpFile, byte[] keyBytes, byte[] ivBytes) throws IOException {
SecretKeySpec skeySpec = new SecretKeySpec(keyBytes, CipherAlgorithm.aes128.jceId);
Cipher ciDec = CryptoFunctions.getCipher(skeySpec, CipherAlgorithm.aes128, ChainingMode.cbc, ivBytes, Cipher.DECRYPT_MODE, PADDING);
return new AesZipFileZipEntrySource(tmpFile, ciDec);
}
}