diff --git a/src/main/java/org/apache/commons/fileupload/disk/DiskFileItem.java b/src/main/java/org/apache/commons/fileupload/disk/DiskFileItem.java index 02fb010fa4..f97398569c 100644 --- a/src/main/java/org/apache/commons/fileupload/disk/DiskFileItem.java +++ b/src/main/java/org/apache/commons/fileupload/disk/DiskFileItem.java @@ -18,7 +18,6 @@ import java.io.ByteArrayInputStream; import java.io.File; -import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -35,7 +34,8 @@ import org.apache.commons.fileupload.util.Streams; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; -import org.apache.commons.io.output.DeferredFileOutputStream; +import org.apache.commons.io.output.CipheredDeferredFileOutputStream; + /** *
The default implementation of the
@@ -138,7 +138,7 @@ public class DiskFileItem
/**
* Output stream for this item.
*/
- private transient DeferredFileOutputStream dfos;
+ private transient CipheredDeferredFileOutputStream dfos;
/**
* The temporary file to use.
@@ -201,7 +201,7 @@ public DiskFileItem(final String fieldName,
public InputStream getInputStream()
throws IOException {
if (!isInMemory()) {
- return new FileInputStream(dfos.getFile());
+ return dfos.createCipheredInputStream();
}
if (cachedContent == null) {
@@ -284,7 +284,7 @@ public long getSize() {
if (dfos.isInMemory()) {
return dfos.getData().length;
}
- return dfos.getFile().length();
+ return dfos.getByteCount();
}
/**
@@ -308,7 +308,7 @@ public byte[] get() {
InputStream fis = null;
try {
- fis = new FileInputStream(dfos.getFile());
+ fis = dfos.createCipheredInputStream();
IOUtils.readFully(fis, fileData);
} catch (final IOException e) {
fileData = null;
@@ -500,7 +500,7 @@ public void setFormField(final boolean state) {
public OutputStream getOutputStream() throws IOException {
if (dfos == null) {
final File outputFile = getTempFile();
- dfos = new DeferredFileOutputStream(sizeThreshold, outputFile);
+ dfos = new CipheredDeferredFileOutputStream(sizeThreshold, outputFile);
}
return dfos;
}
diff --git a/src/main/java/org/apache/commons/fileupload/util/crypto/CipherServiceProvider.java b/src/main/java/org/apache/commons/fileupload/util/crypto/CipherServiceProvider.java
new file mode 100644
index 0000000000..7864e79cef
--- /dev/null
+++ b/src/main/java/org/apache/commons/fileupload/util/crypto/CipherServiceProvider.java
@@ -0,0 +1,40 @@
+/*
+ * 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.commons.fileupload.util.crypto;
+
+import javax.crypto.Cipher;
+
+/**
+ * Ciphers for temporary file encryption.
+ */
+public interface CipherServiceProvider {
+
+ CipherProvider getCipherProvider();
+
+ interface CipherProvider {
+
+ /**
+ * Cipher for encryption of temporary file.
+ */
+ Cipher getEncryptionCipher();
+
+ /**
+ * Cipher for decryption of temporary file.
+ */
+ Cipher getDecryptionCipher();
+ }
+}
diff --git a/src/main/java/org/apache/commons/fileupload/util/crypto/CipherSpiLoader.java b/src/main/java/org/apache/commons/fileupload/util/crypto/CipherSpiLoader.java
new file mode 100644
index 0000000000..4d9ee67933
--- /dev/null
+++ b/src/main/java/org/apache/commons/fileupload/util/crypto/CipherSpiLoader.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.commons.fileupload.util.crypto;
+
+import java.util.Iterator;
+import java.util.ServiceLoader;
+
+import org.apache.commons.fileupload.util.crypto.CipherServiceProvider.CipherProvider;
+
+public final class CipherSpiLoader {
+
+ /**
+ * This provider provide consequent enrcyption or decyption cipher. There is no effect, if it is null.
+ */
+ private static CipherServiceProvider cipherServiceProvider;
+
+ static {
+ Iterator
+ * This class originated in FileUpload processing. In this use case, you do not know in advance the size of the file
+ * being uploaded. If the file is small you want to store it in memory (for speed), but if the file is large you want to
+ * store it to file (to avoid memory issues).
+ *
+ * If the constructor specifying the file is used then it returns that same output file, even when threshold has not
+ * been reached.
+ *
+ * If constructor specifying a temporary file prefix/suffix is used then the temporary file created once the
+ * threshold is reached is returned If the threshold was not reached then {@code null} is returned.
+ *
+ * @return The file for this output stream, or {@code null} if no such file exists.
+ */
+ public File getFile() {
+ return outputFile;
+ }
+
+ /**
+ * Returns the current output stream. This may be memory based or disk based, depending on the current state with
+ * respect to the threshold.
+ *
+ * @return The underlying output stream.
+ *
+ * @throws IOException if an error occurs.
+ */
+ @Override
+ protected OutputStream getStream() throws IOException {
+ return currentOutputStream;
+ }
+
+ /**
+ * Determines whether or not the data for this output stream has been retained in memory.
+ *
+ * @return {@code true} if the data is available in memory; {@code false} otherwise.
+ */
+ public boolean isInMemory() {
+ return !isThresholdExceeded();
+ }
+
+ /**
+ * Switches the underlying output stream from a memory based stream to one that is backed by disk. This is the point
+ * at which we realize that too much data is being written to keep in memory, so we elect to switch to disk-based
+ * storage.
+ *
+ * @throws IOException if an error occurs.
+ */
+ @Override
+ protected void thresholdReached() throws IOException {
+ if (prefix != null) {
+ outputFile = File.createTempFile(prefix, suffix, directory);
+ }
+ FileUtils.forceMkdirParent(outputFile);
+ OutputStream fos = new FileOutputStream(outputFile);
+ if (getCipherProvider() != null) {
+ fos = new CipherOutputStream(fos, getCipherProvider().getEncryptionCipher());
+ }
+ try {
+ memoryOutputStream.writeTo(fos);
+ } catch (final IOException e) {
+ fos.close();
+ throw e;
+ }
+ currentOutputStream = fos;
+ memoryOutputStream = null;
+ }
+
+ /**
+ * Gets the current contents of this byte stream as an {@link InputStream}.
+ * If the data for this output stream has been retained in memory, the
+ * returned stream is backed by buffers of {@code this} stream,
+ * avoiding memory allocation and copy, thus saving space and time.
+ * Otherwise, the returned stream will be one that is created from the data
+ * that has been committed to disk.
+ *
+ * @return the current contents of this output stream.
+ * @throws IOException if this stream is not yet closed or an error occurs.
+ * @see org.apache.commons.io.output.ByteArrayOutputStream#toInputStream()
+ *
+ * @since 2.9.0
+ */
+ public InputStream toInputStream() throws IOException {
+ // we may only need to check if this is closed if we are working with a file
+ // but we should force the habit of closing whether we are working with
+ // a file or memory.
+ if (!closed) {
+ throw new IOException("Stream not closed");
+ }
+
+ if (isInMemory()) {
+ return memoryOutputStream.toInputStream();
+ }
+ return createCipheredInputStream();
+ }
+
+ public InputStream createCipheredInputStream() throws IOException {
+ InputStream is = new FileInputStream(outputFile);
+ if (getCipherProvider() != null) {
+ is = new CipherInputStream(is, getCipherProvider().getDecryptionCipher());
+ }
+ return is;
+ }
+
+ /**
+ * Writes the data from this output stream to the specified output stream, after
+ * it has been closed.
+ *
+ * @param outputStream output stream to write to.
+ * @throws NullPointerException if the OutputStream is {@code null}.
+ * @throws IOException if this stream is not yet closed or an error
+ * occurs.
+ */
+ public void writeTo(final OutputStream outputStream) throws IOException {
+ // we may only need to check if this is closed if we are working with a file
+ // but we should force the habit of closing whether we are working with
+ // a file or memory.
+ if (!closed) {
+ throw new IOException("Stream not closed");
+ }
+
+ if (isInMemory()) {
+ memoryOutputStream.writeTo(outputStream);
+ } else {
+ InputStream fis = createCipheredInputStream();
+ try {
+ IOUtils.copy(fis, outputStream);
+ } finally {
+ IOUtils.closeQuietly(fis);
+ }
+ }
+ }
+
+ private CipherProvider getCipherProvider() {
+ if (!cipherInitialized) {
+ cipherProvider = CipherSpiLoader.getCipherProvider();
+ cipherInitialized = true;
+ }
+ return cipherProvider;
+ }
+}
+
diff --git a/src/main/java/org/apache/commons/io/output/package-info.java b/src/main/java/org/apache/commons/io/output/package-info.java
new file mode 100644
index 0000000000..1a95f5c79e
--- /dev/null
+++ b/src/main/java/org/apache/commons/io/output/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * This package is for commons-io put-backed DefferedFileOutputStream.
+ */
+package org.apache.commons.io.output;
diff --git a/src/test/java/org/apache/commons/fileupload/DefaultFileItemTest.java b/src/test/java/org/apache/commons/fileupload/DefaultFileItemTest.java
index 890dbae9f1..aed9e11ac7 100644
--- a/src/test/java/org/apache/commons/fileupload/DefaultFileItemTest.java
+++ b/src/test/java/org/apache/commons/fileupload/DefaultFileItemTest.java
@@ -18,6 +18,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -27,7 +28,7 @@
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
-
+import org.apache.commons.fileupload.util.crypto.CipherSpiLoader;
import org.apache.commons.io.FileUtils;
import org.junit.Test;
@@ -133,7 +134,8 @@ public void testBelowThreshold() {
*/
@Test
public void testAboveThresholdDefaultRepository() {
- doTestAboveThreshold(null);
+ doTestAboveThreshold(null, false);
+ doTestAboveThreshold(null, true);
}
/**
@@ -146,7 +148,8 @@ public void testAboveThresholdSpecifiedRepository() throws IOException {
final String tempDirName = "testAboveThresholdSpecifiedRepository";
final File tempDir = new File(tempPath, tempDirName);
FileUtils.forceMkdir(tempDir);
- doTestAboveThreshold(tempDir);
+ doTestAboveThreshold(tempDir, false);
+ doTestAboveThreshold(tempDir, true);
assertTrue(tempDir.delete());
}
@@ -158,7 +161,11 @@ public void testAboveThresholdSpecifiedRepository() throws IOException {
* @param repository The directory within which temporary files will be
* created.
*/
- public void doTestAboveThreshold(final File repository) {
+ public void doTestAboveThreshold(final File repository, boolean withCipher) {
+
+ if (withCipher) {
+ CipherSpiLoader.changeCipherProvider(new TestCipherServiceProvider());
+ }
final FileItemFactory factory = createFactory(repository);
final String textFieldName = "textField";
final String textFieldValue = "01234567890123456789";
@@ -189,13 +196,23 @@ public void doTestAboveThreshold(final File repository) {
final File storeLocation = dfi.getStoreLocation();
assertNotNull(storeLocation);
assertTrue(storeLocation.exists());
- assertEquals(storeLocation.length(), testFieldValueBytes.length);
+ if (withCipher) {
+ assertNotEquals(storeLocation.length(), testFieldValueBytes.length);
+ assertEquals(item.getSize(), testFieldValueBytes.length);
+ }
+ else {
+ assertEquals(storeLocation.length(), testFieldValueBytes.length);
+ }
if (repository != null) {
assertEquals(storeLocation.getParentFile(), repository);
}
item.delete();
+
+ if (withCipher) {
+ CipherSpiLoader.changeCipherProvider(null);
+ }
}
diff --git a/src/test/java/org/apache/commons/fileupload/TestCipherProvider.java b/src/test/java/org/apache/commons/fileupload/TestCipherProvider.java
new file mode 100644
index 0000000000..0a5480e6e9
--- /dev/null
+++ b/src/test/java/org/apache/commons/fileupload/TestCipherProvider.java
@@ -0,0 +1,79 @@
+/*
+ * 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.commons.fileupload;
+
+import java.security.AlgorithmParameters;
+import java.security.GeneralSecurityException;
+import java.security.spec.KeySpec;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.commons.fileupload.util.crypto.CipherServiceProvider;
+
+public class TestCipherProvider implements CipherServiceProvider.CipherProvider {
+ private static final String CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding";
+
+ private KeySpec keySpec;
+ private SecretKey tmp;
+ private SecretKey secret;
+ private byte[] ivBytes;
+
+ public TestCipherProvider() {
+ super();
+ try {
+ keySpec = new PBEKeySpec("secret".toCharArray(), "salt".getBytes(), 1, 256);
+ tmp = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(keySpec);
+ secret = new SecretKeySpec(tmp.getEncoded(), "AES");
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException("TestCipherProvider doesn't initilized!", e);
+ }
+ }
+
+ @Override
+ public Cipher getEncryptionCipher() {
+
+ try {
+ Cipher ecipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
+ ecipher.init(Cipher.ENCRYPT_MODE, secret);
+ AlgorithmParameters params = ecipher.getParameters();
+ this.ivBytes = params.getParameterSpec(IvParameterSpec.class).getIV();
+
+ return ecipher;
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException("Encryption cipher doesn't initilized!", e);
+ }
+ }
+
+ @Override
+ public Cipher getDecryptionCipher() {
+
+ try {
+ Cipher dcipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
+ dcipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(ivBytes));
+
+ return dcipher;
+
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException("Decryption Cipher doesn't initilized!", e);
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/fileupload/TestCipherServiceProvider.java b/src/test/java/org/apache/commons/fileupload/TestCipherServiceProvider.java
new file mode 100644
index 0000000000..c975ed2f9e
--- /dev/null
+++ b/src/test/java/org/apache/commons/fileupload/TestCipherServiceProvider.java
@@ -0,0 +1,30 @@
+/*
+ * 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.commons.fileupload;
+
+import org.apache.commons.fileupload.util.crypto.CipherServiceProvider;
+
+public class TestCipherServiceProvider implements CipherServiceProvider {
+
+ private TestCipherProvider provider = new TestCipherProvider();
+
+ @Override
+ public CipherProvider getCipherProvider() {
+ return provider;
+ }
+
+}