- 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)
This commit is contained in:
Sebastian Stenzel
2015-01-16 19:50:57 +01:00
parent 0b64c7ce25
commit d774546bf8
6 changed files with 78 additions and 38 deletions

View File

@@ -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>

View File

@@ -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);

View File

@@ -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

View File

@@ -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
*/

View File

@@ -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;
}

View File

@@ -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);