From d774546bf8440a336091768876b3066d89e954f4 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 16 Jan 2015 19:50:57 +0100 Subject: [PATCH] - 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) --- main/core/pom.xml | 2 +- .../org/cryptomator/webdav/WebDavServer.java | 2 +- .../crypto/aes256/Aes256Cryptor.java | 65 ++++++++++--------- .../aes256/AesCryptographicConfiguration.java | 3 +- .../crypto/aes256/MacInputStream.java | 4 +- .../crypto/aes256/Aes256CryptorTest.java | 40 +++++++++++- 6 files changed, 78 insertions(+), 38 deletions(-) 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);