Ver Fonte

- pad file contents to reach a multiple of 16 bytes (so AES/CTR always works on complete blocks) - references #24
- calculate MAC over complete ciphertext (including file length obfuscation trash data)

Sebastian Stenzel há 10 anos atrás
pai
commit
d774546bf8

+ 1 - 1
main/core/pom.xml

@@ -15,7 +15,7 @@
 		<version>0.5.0-SNAPSHOT</version>
 	</parent>
 	<artifactId>core</artifactId>
-	<name>Cryptomator core I/O module</name>
+	<name>Cryptomator WebDAV and I/O module</name>
 
 	<properties>
 		<jetty.version>9.2.5.v20141112</jetty.version>

+ 1 - 1
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);

+ 33 - 30
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:
-		macBuffer.position(0);
-		macBuffer.put(mac.doFinal());
+		// 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 fake content:
-		final int randomContentLength = (int) Math.ceil((Math.random() + 1.0) * actualSize / 20.0);
+		// 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 < randomContentLength; i += AES_BLOCK_LENGTH) {
-			cipheredOut.write(emptyBytes);
+		for (int i = 0; i < upToTenPercentFakeBlocks; i += AES_BLOCK_LENGTH) {
+			blockSizeBufferedOut.write(emptyBytes);
 		}
-		cipheredOut.flush();
+		blockSizeBufferedOut.flush();
 
-		// encrypt actualSize
+		// 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
+
+		// 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

+ 2 - 1
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.<br/>
+	 * <strong>Important</strong>: 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
 	 */

+ 3 - 1
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;
 	}
 

+ 37 - 3
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);