mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-20 19:51:27 +00:00
- 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:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user