diff --git a/main/core/pom.xml b/main/core/pom.xml
index a517083ab..1686286bb 100644
--- a/main/core/pom.xml
+++ b/main/core/pom.xml
@@ -15,7 +15,7 @@
0.5.0-SNAPSHOT
core
- Cryptomator core I/O module
+ Cryptomator WebDAV and I/O module
9.2.5.v20141112
diff --git a/main/core/src/main/java/org/cryptomator/webdav/WebDavServer.java b/main/core/src/main/java/org/cryptomator/webdav/WebDavServer.java
index a02ac2bd4..3757359d2 100644
--- a/main/core/src/main/java/org/cryptomator/webdav/WebDavServer.java
+++ b/main/core/src/main/java/org/cryptomator/webdav/WebDavServer.java
@@ -92,7 +92,7 @@ public final class WebDavServer {
final ServletHolder servlet = getWebDavServletHolder(workDir.toString(), pathPrefix, checkFileIntegrity, cryptor);
servletContext.addServlet(servlet, pathSpec);
- LOG.info("{} available on http://{}", workDir, uri.getRawSchemeSpecificPart());
+ LOG.info("{} available on http:{}", workDir, uri.getRawSchemeSpecificPart());
return new ServletLifeCycleAdapter(servlet, uri);
} catch (URISyntaxException e) {
throw new IllegalStateException("Invalid hard-coded URI components.", e);
diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java
index ee44bd1af..d5af20da1 100644
--- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java
+++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java
@@ -8,6 +8,7 @@
******************************************************************************/
package org.cryptomator.crypto.aes256;
+import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -405,30 +406,27 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
@Override
public boolean authenticateContent(SeekableByteChannel encryptedFile) throws IOException {
- // read file size:
- final Long fileSize = decryptedContentLength(encryptedFile);
-
// init mac:
- final Mac mac = this.hmacSha256(hMacMasterKey);
+ final Mac calculatedMac = this.hmacSha256(hMacMasterKey);
// read stored mac:
encryptedFile.position(16);
- final ByteBuffer macBuffer = ByteBuffer.allocate(mac.getMacLength());
- final int numMacBytesRead = encryptedFile.read(macBuffer);
+ final ByteBuffer storedMac = ByteBuffer.allocate(calculatedMac.getMacLength());
+ final int numMacBytesRead = encryptedFile.read(storedMac);
// check validity of header:
- if (numMacBytesRead != mac.getMacLength() || fileSize == null) {
+ if (numMacBytesRead != calculatedMac.getMacLength()) {
throw new IOException("Failed to read file header.");
}
// read all encrypted data and calculate mac:
encryptedFile.position(64);
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
- final InputStream macIn = new MacInputStream(in, mac);
- IOUtils.copyLarge(macIn, new NullOutputStream(), 0, fileSize);
+ final InputStream macIn = new MacInputStream(in, calculatedMac);
+ IOUtils.copyLarge(macIn, new NullOutputStream());
// compare (in constant time):
- return MessageDigest.isEqual(macBuffer.array(), mac.doFinal());
+ return MessageDigest.isEqual(storedMac.array(), calculatedMac.doFinal());
}
@Override
@@ -540,40 +538,45 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
final OutputStream out = new SeekableByteChannelOutputStream(encryptedFile);
final OutputStream macOut = new MacOutputStream(out, mac);
final OutputStream cipheredOut = new CipherOutputStream(macOut, cipher);
- final Long actualSize = IOUtils.copyLarge(plaintextFile, cipheredOut);
+ final OutputStream blockSizeBufferedOut = new BufferedOutputStream(cipheredOut, AES_BLOCK_LENGTH);
+ final Long plaintextSize = IOUtils.copyLarge(plaintextFile, blockSizeBufferedOut);
- // copy MAC:
+ // ensure total byte count is a multiple of the block size, in CTR mode:
+ final int remainderToFillLastBlock = AES_BLOCK_LENGTH - (int) (plaintextSize % AES_BLOCK_LENGTH);
+ blockSizeBufferedOut.write(new byte[remainderToFillLastBlock]);
+
+ // append a few blocks of fake data:
+ final int numberOfPlaintextBlocks = (int) Math.ceil(plaintextSize / AES_BLOCK_LENGTH);
+ final int upToTenPercentFakeBlocks = (int) Math.ceil(Math.random() * 0.1 * numberOfPlaintextBlocks);
+ final byte[] emptyBytes = new byte[AES_BLOCK_LENGTH];
+ for (int i = 0; i < upToTenPercentFakeBlocks; i += AES_BLOCK_LENGTH) {
+ blockSizeBufferedOut.write(emptyBytes);
+ }
+ blockSizeBufferedOut.flush();
+
+ // write MAC of total ciphertext:
macBuffer.position(0);
macBuffer.put(mac.doFinal());
+ macBuffer.position(0);
+ encryptedFile.position(16); // right behind the IV
+ encryptedFile.write(macBuffer); // 256 bit MAC
- // append fake content:
- final int randomContentLength = (int) Math.ceil((Math.random() + 1.0) * actualSize / 20.0);
- final byte[] emptyBytes = new byte[AES_BLOCK_LENGTH];
- for (int i = 0; i < randomContentLength; i += AES_BLOCK_LENGTH) {
- cipheredOut.write(emptyBytes);
- }
- cipheredOut.flush();
-
- // encrypt actualSize
+ // encrypt and write plaintextSize
try {
final ByteBuffer fileSizeBuffer = ByteBuffer.allocate(Long.BYTES);
- fileSizeBuffer.putLong(actualSize);
+ fileSizeBuffer.putLong(plaintextSize);
final Cipher sizeCipher = aesEcbCipher(primaryMasterKey, Cipher.ENCRYPT_MODE);
final byte[] encryptedFileSize = sizeCipher.doFinal(fileSizeBuffer.array());
encryptedFileSizeBuffer.position(0);
encryptedFileSizeBuffer.put(encryptedFileSize);
+ encryptedFileSizeBuffer.position(0);
+ encryptedFile.position(48); // right behind the IV and MAC
+ encryptedFile.write(encryptedFileSizeBuffer);
} catch (IllegalBlockSizeException | BadPaddingException e) {
- throw new IllegalStateException(e);
+ throw new IllegalStateException("Block size must be valid, as padding is requested. BadPaddingException not possible in encrypt mode.", e);
}
- // write file header
- encryptedFile.position(16); // skip already written 128 bit IV
- macBuffer.position(0);
- encryptedFile.write(macBuffer); // 256 bit MAC
- encryptedFileSizeBuffer.position(0);
- encryptedFile.write(encryptedFileSizeBuffer); // 128 bit encrypted file size
-
- return actualSize;
+ return plaintextSize;
}
@Override
diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesCryptographicConfiguration.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesCryptographicConfiguration.java
index e2c7ee3ac..7cdf319ec 100644
--- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesCryptographicConfiguration.java
+++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesCryptographicConfiguration.java
@@ -60,7 +60,8 @@ interface AesCryptographicConfiguration {
String AES_KEYWRAP_CIPHER = "AESWrap";
/**
- * Cipher specs for file name and file content encryption. Using CTR-mode for random access.
+ * Cipher specs for file name and file content encryption. Using CTR-mode for random access.
+ * Important: As JCE doesn't support a padding, input must be a multiple of the block size.
*
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher
*/
diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/MacInputStream.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/MacInputStream.java
index 464fc4eac..0bec34ce1 100644
--- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/MacInputStream.java
+++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/MacInputStream.java
@@ -32,7 +32,9 @@ class MacInputStream extends FilterInputStream {
@Override
public int read(byte[] b, int off, int len) throws IOException {
int read = in.read(b, off, len);
- mac.update(b, off, len);
+ if (read > 0) {
+ mac.update(b, off, read);
+ }
return read;
}
diff --git a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java b/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java
index b0e2ed354..59c66e1cc 100644
--- a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java
+++ b/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java
@@ -14,12 +14,26 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.Security;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
import org.apache.commons.io.IOUtils;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.cryptomator.crypto.CryptorIOSupport;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
@@ -82,7 +96,7 @@ public class Aes256CryptorTest {
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
// encrypt:
- final ByteBuffer encryptedData = ByteBuffer.allocate(64 + plaintextData.length * 4);
+ final ByteBuffer encryptedData = ByteBuffer.allocate(96);
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
@@ -109,6 +123,26 @@ public class Aes256CryptorTest {
Assert.assertFalse(isContentUnmodified2);
}
+ @Test
+ public void foo() throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, NoSuchProviderException {
+ Security.addProvider(new BouncyCastleProvider());
+
+ final byte[] iv = new byte[16];
+ final byte[] keyBytes = new byte[16];
+ final SecretKey key = new SecretKeySpec(keyBytes, "AES");
+ final Cipher pkcs5PaddedCipher = Cipher.getInstance("AES/CTR/PKCS5Padding", BouncyCastleProvider.PROVIDER_NAME);
+ pkcs5PaddedCipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
+ final Cipher unpaddedCipher = Cipher.getInstance("AES/CTR/NoPadding");
+ unpaddedCipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
+
+ // test data:
+ final byte[] plaintextData = "Hello World".getBytes();
+ final byte[] pkcs5PaddedCiphertext = pkcs5PaddedCipher.doFinal(plaintextData);
+ final byte[] unpaddedCiphertext = unpaddedCipher.doFinal(plaintextData);
+
+ Assert.assertFalse(Arrays.equals(pkcs5PaddedCiphertext, unpaddedCiphertext));
+ }
+
@Test
public void testEncryptionAndDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
// our test plaintext data:
@@ -119,7 +153,7 @@ public class Aes256CryptorTest {
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
// encrypt:
- final ByteBuffer encryptedData = ByteBuffer.allocate(64 + plaintextData.length * 4);
+ final ByteBuffer encryptedData = ByteBuffer.allocate(96);
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
@@ -158,7 +192,7 @@ public class Aes256CryptorTest {
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
// encrypt:
- final ByteBuffer encryptedData = ByteBuffer.allocate(64 + plaintextData.length * 4);
+ final ByteBuffer encryptedData = ByteBuffer.allocate((int) (64 + plaintextData.length * 1.2));
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);