diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoResourceFactory.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoResourceFactory.java index b8037dde8..64fcf009f 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoResourceFactory.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoResourceFactory.java @@ -5,7 +5,6 @@ import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; -import java.util.concurrent.ExecutorService; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.io.FilenameUtils; @@ -28,15 +27,13 @@ public class CryptoResourceFactory implements DavResourceFactory, FileConstants private final LockManager lockManager = new SimpleLockManager(); private final Cryptor cryptor; private final CryptoWarningHandler cryptoWarningHandler; - private final ExecutorService backgroundTaskExecutor; private final Path dataRoot; private final FilenameTranslator filenameTranslator; - CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, ExecutorService backgroundTaskExecutor, String vaultRoot) { + CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, String vaultRoot) { Path vaultRootPath = FileSystems.getDefault().getPath(vaultRoot); this.cryptor = cryptor; this.cryptoWarningHandler = cryptoWarningHandler; - this.backgroundTaskExecutor = backgroundTaskExecutor; this.dataRoot = vaultRootPath.resolve("d"); this.filenameTranslator = new FilenameTranslator(cryptor, vaultRootPath); } @@ -151,7 +148,7 @@ public class CryptoResourceFactory implements DavResourceFactory, FileConstants } private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, DavServletRequest request, Path filePath) { - return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, cryptoWarningHandler, backgroundTaskExecutor, filePath); + return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, cryptoWarningHandler, filePath); } private EncryptedFile createFile(DavResourceLocator locator, DavSession session, Path filePath) { diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java index 7ebdcece6..21630a677 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java @@ -107,7 +107,9 @@ class EncryptedFile extends AbstractEncryptedNode implements FileConstants { } catch (EOFException e) { LOG.warn("Unexpected end of stream (possibly client hung up)."); } catch (MacAuthenticationFailedException e) { - cryptoWarningHandler.macAuthFailed(getLocator().getResourcePath()); + LOG.warn("File integrity violation for " + getLocator().getResourcePath()); + throw new IOException("Error decrypting file " + filePath.toString(), e); + // cryptoWarningHandler.macAuthFailed(getLocator().getResourcePath()); } catch (DecryptFailedException e) { throw new IOException("Error decrypting file " + filePath.toString(), e); } diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java index 2f997d0cd..ed949d112 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java @@ -2,15 +2,12 @@ package org.cryptomator.webdav.jackrabbit; import java.io.EOFException; import java.io.IOException; -import java.nio.channels.ClosedByInterruptException; import java.nio.channels.SeekableByteChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.HashSet; import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.TimeUnit; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; @@ -27,9 +24,6 @@ import org.eclipse.jetty.http.HttpHeader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; - /** * Delivers only the requested range of bytes from a file. * @@ -41,7 +35,6 @@ class EncryptedFilePart extends EncryptedFile { private static final String BYTE_UNIT_PREFIX = "bytes="; private static final char RANGE_SET_SEP = ','; private static final char RANGE_SEP = '-'; - private static final Cache cachedMacAuthenticationJobs = CacheBuilder.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build(); /** * e.g. range -500 (gets the last 500 bytes) -> (-1, 500) @@ -56,22 +49,13 @@ class EncryptedFilePart extends EncryptedFile { private final Set> requestedContentRanges = new HashSet>(); public EncryptedFilePart(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, - ExecutorService backgroundTaskExecutor, Path filePath) { + Path filePath) { super(factory, locator, session, lockManager, cryptor, cryptoWarningHandler, filePath); final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString()); if (rangeHeader == null) { throw new IllegalArgumentException("HTTP request doesn't contain a range header"); } determineByteRanges(rangeHeader); - - synchronized (cachedMacAuthenticationJobs) { - if (cachedMacAuthenticationJobs.getIfPresent(locator) == null) { - final MacAuthenticationJob macAuthJob = new MacAuthenticationJob(locator); - cachedMacAuthenticationJobs.put(locator, macAuthJob); - backgroundTaskExecutor.submit(macAuthJob); - } - } - } private void determineByteRanges(String rangeHeader) { @@ -149,46 +133,4 @@ class EncryptedFilePart extends EncryptedFile { return String.format("%d-%d/%d", firstByte, lastByte, completeLength); } - private class MacAuthenticationJob implements Runnable { - - private final DavResourceLocator locator; - - public MacAuthenticationJob(final DavResourceLocator locator) { - if (locator == null) { - throw new IllegalArgumentException("locator must not be null."); - } - this.locator = locator; - } - - @Override - public void run() { - assert Files.isRegularFile(filePath); - try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.READ)) { - final boolean authentic = cryptor.isAuthentic(channel); - if (!authentic) { - cryptoWarningHandler.macAuthFailed(locator.getResourcePath()); - } - } catch (ClosedByInterruptException ex) { - LOG.debug("Couldn't finish MAC verification due to interruption of worker thread."); - } catch (IOException e) { - LOG.error("IOException during MAC verification of " + filePath.toString(), e); - } - } - - @Override - public int hashCode() { - return locator.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof MacAuthenticationJob) { - final MacAuthenticationJob other = (MacAuthenticationJob) obj; - return this.locator.equals(other.locator); - } else { - return false; - } - } - } - } diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java index 0f956f134..ca2b8990e 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java @@ -9,9 +9,6 @@ package org.cryptomator.webdav.jackrabbit; import java.util.Collection; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; import javax.servlet.ServletConfig; import javax.servlet.ServletException; @@ -33,7 +30,6 @@ public class WebDavServlet extends AbstractWebdavServlet { private DavResourceFactory davResourceFactory; private final Cryptor cryptor; private final CryptoWarningHandler cryptoWarningHandler; - private ExecutorService backgroundTaskExecutor; public WebDavServlet(final Cryptor cryptor, final Collection failingMacCollection) { super(); @@ -45,26 +41,9 @@ public class WebDavServlet extends AbstractWebdavServlet { public void init(ServletConfig config) throws ServletException { super.init(config); final String fsRoot = config.getInitParameter(CFG_FS_ROOT); - backgroundTaskExecutor = Executors.newCachedThreadPool(); davSessionProvider = new DavSessionProviderImpl(); davLocatorFactory = new CleartextLocatorFactory(config.getServletContext().getContextPath()); - davResourceFactory = new CryptoResourceFactory(cryptor, cryptoWarningHandler, backgroundTaskExecutor, fsRoot); - } - - @Override - public void destroy() { - backgroundTaskExecutor.shutdown(); - try { - final boolean tasksFinished = backgroundTaskExecutor.awaitTermination(2, TimeUnit.SECONDS); - if (!tasksFinished) { - backgroundTaskExecutor.shutdownNow(); - } - } catch (InterruptedException e) { - backgroundTaskExecutor.shutdownNow(); - Thread.currentThread().interrupt(); - } finally { - super.destroy(); - } + davResourceFactory = new CryptoResourceFactory(cryptor, cryptoWarningHandler, fsRoot); } @Override 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 c6cb9ff00..d231c511e 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,7 +8,6 @@ ******************************************************************************/ package org.cryptomator.crypto.aes256; -import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -36,11 +35,8 @@ import javax.security.auth.DestroyFailedException; import javax.security.auth.Destroyable; import org.apache.commons.io.IOUtils; -import org.apache.commons.io.output.NullOutputStream; import org.bouncycastle.crypto.generators.SCrypt; import org.cryptomator.crypto.Cryptor; -import org.cryptomator.crypto.aes256.CounterAwareInputStream.CounterAwareInputLimitReachedException; -import org.cryptomator.crypto.exceptions.CounterOverflowException; import org.cryptomator.crypto.exceptions.DecryptFailedException; import org.cryptomator.crypto.exceptions.EncryptFailedException; import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException; @@ -211,7 +207,7 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration { } catch (InvalidKeyException ex) { throw new IllegalArgumentException("Invalid key.", ex); } catch (NoSuchAlgorithmException | NoSuchPaddingException ex) { - throw new IllegalStateException("Algorithm/Padding should exist and accept GCM specs.", ex); + throw new IllegalStateException("Algorithm/Padding should exist.", ex); } } @@ -308,7 +304,7 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration { public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException { // read header: encryptedFile.position(0); - final ByteBuffer headerBuf = ByteBuffer.allocate(64); + final ByteBuffer headerBuf = ByteBuffer.allocate(96); final int headerBytesRead = encryptedFile.read(headerBuf); if (headerBytesRead != headerBuf.capacity()) { return null; @@ -326,13 +322,13 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration { // read stored header mac: final byte[] storedHeaderMac = new byte[32]; - headerBuf.position(32); + headerBuf.position(64); headerBuf.get(storedHeaderMac); - // calculate mac over first 32 bytes of header: + // calculate mac over first 64 bytes of header: final Mac headerMac = this.hmacSha256(hMacMasterKey); headerBuf.rewind(); - headerBuf.limit(32); + headerBuf.limit(64); headerMac.update(headerBuf); final boolean macMatches = MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal()); @@ -340,70 +336,29 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration { throw new MacAuthenticationFailedException("MAC authentication failed."); } - return decryptContentLength(encryptedContentLengthBytes, iv); + final byte[] decryptedContentLengthBytes = decryptHeaderData(encryptedContentLengthBytes, iv); + final ByteBuffer fileSizeBuffer = ByteBuffer.wrap(decryptedContentLengthBytes); + return fileSizeBuffer.getLong(); } - private long decryptContentLength(byte[] encryptedContentLengthBytes, byte[] iv) { + private byte[] decryptHeaderData(byte[] ciphertextBytes, byte[] iv) { try { final Cipher sizeCipher = aesCbcCipher(primaryMasterKey, iv, Cipher.DECRYPT_MODE); - final byte[] decryptedFileSize = sizeCipher.doFinal(encryptedContentLengthBytes); - final ByteBuffer fileSizeBuffer = ByteBuffer.wrap(decryptedFileSize); - return fileSizeBuffer.getLong(); + return sizeCipher.doFinal(ciphertextBytes); } catch (IllegalBlockSizeException | BadPaddingException e) { throw new IllegalStateException(e); } } - private byte[] encryptContentLength(long contentLength, byte[] iv) { + private byte[] encryptHeaderData(byte[] plaintextBytes, byte[] iv) { try { - final ByteBuffer fileSizeBuffer = ByteBuffer.allocate(Long.BYTES); - fileSizeBuffer.putLong(contentLength); final Cipher sizeCipher = aesCbcCipher(primaryMasterKey, iv, Cipher.ENCRYPT_MODE); - return sizeCipher.doFinal(fileSizeBuffer.array()); + return sizeCipher.doFinal(plaintextBytes); } catch (IllegalBlockSizeException | BadPaddingException e) { throw new IllegalStateException("Block size must be valid, as padding is requested. BadPaddingException not possible in encrypt mode.", e); } } - @Override - public boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException { - // read header: - encryptedFile.position(0l); - final ByteBuffer headerBuf = ByteBuffer.allocate(96); - final int headerBytesRead = encryptedFile.read(headerBuf); - if (headerBytesRead != headerBuf.capacity()) { - throw new IOException("Failed to read file header."); - } - - // read header mac: - final byte[] storedHeaderMac = new byte[32]; - headerBuf.position(32); - headerBuf.get(storedHeaderMac); - - // read content mac: - final byte[] storedContentMac = new byte[32]; - headerBuf.position(64); - headerBuf.get(storedContentMac); - - // calculate mac over first 32 bytes of header: - final Mac headerMac = this.hmacSha256(hMacMasterKey); - headerBuf.position(0); - headerBuf.limit(32); - headerMac.update(headerBuf); - - // calculate mac over content: - encryptedFile.position(96l); - final Mac contentMac = this.hmacSha256(hMacMasterKey); - final InputStream in = new SeekableByteChannelInputStream(encryptedFile); - final InputStream macIn = new MacInputStream(in, contentMac); - IOUtils.copyLarge(macIn, new NullOutputStream()); - - // compare (in constant time): - final boolean headerMacMatches = MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal()); - final boolean contentMacMatches = MessageDigest.isEqual(storedContentMac, contentMac.doFinal()); - return headerMacMatches && contentMacMatches; - } - @Override public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException { // read header: @@ -419,45 +374,74 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration { headerBuf.position(0); headerBuf.get(iv); + // derive nonce used in counter mode from IV by setting last 64bit to 0: + final ByteBuffer nonceBuf = ByteBuffer.wrap(iv.clone()); + nonceBuf.putLong(AES_BLOCK_LENGTH - Long.BYTES, 0); + final byte[] nonce = nonceBuf.array(); + // read content length: final byte[] encryptedContentLengthBytes = new byte[AES_BLOCK_LENGTH]; headerBuf.position(16); headerBuf.get(encryptedContentLengthBytes); - final Long fileSize = decryptContentLength(encryptedContentLengthBytes, iv); + final byte[] decryptedContentLengthBytes = decryptHeaderData(encryptedContentLengthBytes, iv); + final ByteBuffer fileSizeBuffer = ByteBuffer.wrap(decryptedContentLengthBytes); + final Long fileSize = fileSizeBuffer.getLong(); + + // read content key: + final byte[] encryptedContentKeyBytes = new byte[32]; + headerBuf.position(32); + headerBuf.get(encryptedContentKeyBytes); + final byte[] contentKeyBytes = decryptHeaderData(encryptedContentKeyBytes, iv); // read header mac: - final byte[] headerMac = new byte[32]; - headerBuf.position(32); - headerBuf.get(headerMac); - - // read content mac: - final byte[] contentMac = new byte[32]; + final byte[] storedHeaderMac = new byte[32]; headerBuf.position(64); - headerBuf.get(contentMac); + headerBuf.get(storedHeaderMac); - // decrypt content - encryptedFile.position(96l); - final Mac calculatedContentMac = this.hmacSha256(hMacMasterKey); - final Cipher cipher = this.aesCtrCipher(primaryMasterKey, iv, Cipher.DECRYPT_MODE); - final InputStream in = new SeekableByteChannelInputStream(encryptedFile); - final InputStream macIn = new MacInputStream(in, calculatedContentMac); - final InputStream cipheredIn = new CipherInputStream(macIn, cipher); - final long bytesDecrypted = IOUtils.copyLarge(cipheredIn, plaintextFile, 0, fileSize); + // calculate mac over first 64 bytes of header: + final Mac headerMac = this.hmacSha256(hMacMasterKey); + headerBuf.position(0); + headerBuf.limit(64); + headerMac.update(headerBuf); - // drain remaining bytes to /dev/null to complete MAC calculation: - IOUtils.copyLarge(macIn, new NullOutputStream()); - - // compare (in constant time): - final boolean macMatches = MessageDigest.isEqual(contentMac, calculatedContentMac.doFinal()); - if (!macMatches) { - // This exception will be thrown AFTER we sent the decrypted content to the user. - // This has two advantages: - // - we don't need to read files twice - // - we can still restore files suffering from non-malicious bit rotting - // Anyway me MUST make sure to warn the user. This will be done by the UI when catching this exception. - throw new MacAuthenticationFailedException("MAC authentication failed."); + // check header integrity: + if (!MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal())) { + throw new MacAuthenticationFailedException("Header MAC authentication failed."); } + // content decryption: + encryptedFile.position(96l); + final SecretKey contentKey = new SecretKeySpec(contentKeyBytes, AES_KEY_ALGORITHM); + final Cipher cipher = this.aesCtrCipher(contentKey, nonce, Cipher.DECRYPT_MODE); + final Mac contentMac = this.hmacSha256(hMacMasterKey); + + // reading ciphered input and MACs interleaved: + long bytesDecrypted = 0; + final InputStream in = new SeekableByteChannelInputStream(encryptedFile); + byte[] buffer = new byte[1024 * 1024 + 32]; + int n = 0; + while ((n = IOUtils.read(in, buffer)) > 0) { + if (n < 32) { + throw new DecryptFailedException("Invalid file content, missing MAC."); + } + + // check MAC of current block: + contentMac.update(buffer, 0, n - 32); + final byte[] calculatedMac = contentMac.doFinal(); + final byte[] storedMac = new byte[32]; + System.arraycopy(buffer, n - 32, storedMac, 0, 32); + if (!MessageDigest.isEqual(calculatedMac, storedMac)) { + throw new MacAuthenticationFailedException("Content MAC authentication failed."); + } + + // decrypt block: + final byte[] plaintext = cipher.update(buffer, 0, n - 32); + final int plaintextLengthWithoutPadding = (int) Math.min(plaintext.length, fileSize - bytesDecrypted); // plaintext.length is known to be a 32 bit int + plaintextFile.write(plaintext, 0, plaintextLengthWithoutPadding); + bytesDecrypted += plaintextLengthWithoutPadding; + } + destroyQuietly(contentKey); + return bytesDecrypted; } @@ -492,61 +476,65 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration { } /** - * header = {16 byte iv, 16 byte filesize, 32 byte headerMac, 32 byte contentMac} + * header = {16 byte iv, 16 byte filesize, 32 byte contentKey, 32 byte headerMac} */ @Override public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException { // truncate file encryptedFile.truncate(0l); - // use an IV, whose last 8 bytes store a long used in counter mode and write initial value to file. - final ByteBuffer ivBuf = ByteBuffer.wrap(randomData(AES_BLOCK_LENGTH)); - ivBuf.putInt(AES_BLOCK_LENGTH - Integer.BYTES, 0); - final byte[] iv = ivBuf.array(); + // choose a random IV: + final byte[] iv = randomData(AES_BLOCK_LENGTH); - // 96 byte header buffer (16 IV, 16 size, 32 headerMac, 32 contentMac), filled after writing the content + // derive nonce used in counter mode from IV by setting last 64bit to 0: + final ByteBuffer nonceBuf = ByteBuffer.wrap(iv.clone()); + nonceBuf.putLong(AES_BLOCK_LENGTH - Long.BYTES, 0); + final byte[] nonce = nonceBuf.array(); + + // choose a random content key: + final byte[] contentKeyBytes = randomData(32); + + // 96 byte header buffer (16 IV, 16 size, 32 content key, 32 headerMac), filled after writing the content final ByteBuffer headerBuf = ByteBuffer.allocate(96); headerBuf.limit(96); encryptedFile.write(headerBuf); + // add random length padding to obfuscate file length: + final byte[] randomPadding = this.randomData(AES_BLOCK_LENGTH); + final LengthObfuscationInputStream in = new LengthObfuscationInputStream(plaintextFile, randomPadding); + // content encryption: - final Cipher cipher = this.aesCtrCipher(primaryMasterKey, iv, Cipher.ENCRYPT_MODE); + final SecretKey contentKey = new SecretKeySpec(contentKeyBytes, AES_KEY_ALGORITHM); + final Cipher cipher = this.aesCtrCipher(contentKey, nonce, Cipher.ENCRYPT_MODE); final Mac contentMac = this.hmacSha256(hMacMasterKey); final OutputStream out = new SeekableByteChannelOutputStream(encryptedFile); final OutputStream macOut = new MacOutputStream(out, contentMac); + @SuppressWarnings("resource") final OutputStream cipheredOut = new CipherOutputStream(macOut, cipher); - final OutputStream blockSizeBufferedOut = new BufferedOutputStream(cipheredOut, AES_BLOCK_LENGTH); - final InputStream lengthLimitingIn = new CounterAwareInputStream(plaintextFile); - final Long plaintextSize; - try { - plaintextSize = IOUtils.copyLarge(lengthLimitingIn, blockSizeBufferedOut); - } catch (CounterAwareInputLimitReachedException ex) { - encryptedFile.truncate(0l); - throw new CounterOverflowException("File size exceeds limit (64Gib). Aborting to prevent counter overflow."); - } - // add random length padding to obfuscate file length: - final long numberOfPlaintextBlocks = (int) Math.ceil(plaintextSize / AES_BLOCK_LENGTH); - final long minAdditionalBlocks = 4; - final long maxAdditionalBlocks = Math.min(numberOfPlaintextBlocks >> 3, 1024 * 1024); // 12,5% of original blocks, but not more than 1M blocks (16MiBs) - final long availableBlocks = (1l << 32) - numberOfPlaintextBlocks; // before reaching limit of 2^32 blocks - final long additionalBlocks = (long) Math.min(Math.random() * Math.max(minAdditionalBlocks, maxAdditionalBlocks), availableBlocks); - final byte[] randomPadding = this.randomData(AES_BLOCK_LENGTH); - for (int i = 0; i < additionalBlocks; i += AES_BLOCK_LENGTH) { - blockSizeBufferedOut.write(randomPadding); + // writing ciphered output and MACs interleaved: + byte[] buffer = new byte[1024 * 1024]; + int n = 0; + while ((n = IOUtils.read(in, buffer)) > 0) { + cipheredOut.write(buffer, 0, n); + final byte[] mac = contentMac.doFinal(); + out.write(mac); } - blockSizeBufferedOut.flush(); + destroyQuietly(contentKey); // create and write header: + final long plaintextSize = in.getRealInputLength(); + final ByteBuffer fileSizeBuffer = ByteBuffer.allocate(AES_BLOCK_LENGTH); + fileSizeBuffer.putLong(plaintextSize); headerBuf.clear(); headerBuf.put(iv); - headerBuf.put(encryptContentLength(plaintextSize, iv)); + headerBuf.put(encryptHeaderData(fileSizeBuffer.array(), iv)); + headerBuf.put(encryptHeaderData(contentKeyBytes, iv)); headerBuf.flip(); final Mac headerMac = this.hmacSha256(hMacMasterKey); headerMac.update(headerBuf); headerBuf.limit(96); headerBuf.put(headerMac.doFinal()); - headerBuf.put(contentMac.doFinal()); headerBuf.flip(); encryptedFile.position(0); encryptedFile.write(headerBuf); 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 b31dcfe99..09a3e2f0a 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 @@ -74,7 +74,7 @@ interface AesCryptographicConfiguration { * * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#impl */ - String AES_CBC_CIPHER = "AES/CBC/PKCS5Padding"; + String AES_CBC_CIPHER = "AES/CBC/NoPadding"; /** * AES block size is 128 bit or 16 bytes. diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/CounterAwareInputStream.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/CounterAwareInputStream.java deleted file mode 100644 index a4cea2bf8..000000000 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/CounterAwareInputStream.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.cryptomator.crypto.aes256; - -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.concurrent.atomic.AtomicLong; - -/** - * Throws an exception, if more than (2^32)-1 16 byte blocks will be encrypted (would result in an counter overflow).
- * From https://tools.ietf.org/html/rfc3686: Using the encryption process described in section 2.1, this construction permits each packet to consist of up to: (2^32)-1 blocks - */ -class CounterAwareInputStream extends FilterInputStream { - - static final long SIXTY_FOUR_GIGABYE = ((1l << 32) - 1) * 16; - - private final AtomicLong counter; - - /** - * @param in Stream from which to read contents, which will update the Mac. - */ - public CounterAwareInputStream(InputStream in) { - super(in); - this.counter = new AtomicLong(0l); - } - - @Override - public int read() throws IOException { - int b = in.read(); - if (b != -1) { - final long currentValue = counter.incrementAndGet(); - failWhen64GibReached(currentValue); - } - return b; - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - int read = in.read(b, off, len); - if (read > 0) { - final long currentValue = counter.addAndGet(read); - failWhen64GibReached(currentValue); - } - return read; - } - - private void failWhen64GibReached(long currentValue) throws CounterAwareInputLimitReachedException { - if (currentValue > SIXTY_FOUR_GIGABYE) { - throw new CounterAwareInputLimitReachedException(); - } - } - - static class CounterAwareInputLimitReachedException extends IOException { - private static final long serialVersionUID = -1905012809288019359L; - - } - -} diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/LengthObfuscationInputStream.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/LengthObfuscationInputStream.java new file mode 100644 index 000000000..0b1defce2 --- /dev/null +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/LengthObfuscationInputStream.java @@ -0,0 +1,128 @@ +package org.cryptomator.crypto.aes256; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Not thread-safe! + */ +public class LengthObfuscationInputStream extends FilterInputStream { + + private final byte[] padding; + private int paddingLength = -1; + private long inputBytesRead = 0; + private int paddingBytesRead = 0; + + LengthObfuscationInputStream(InputStream in, byte[] padding) { + super(in); + this.padding = padding; + } + + long getRealInputLength() { + return inputBytesRead; + } + + private void choosePaddingLengthOnce() { + if (paddingLength == -1) { + long upperBound = Math.min(inputBytesRead / 10, 16 * 1024 * 1024); // 10% of original bytes, but not more than 16MiBs + paddingLength = (int) (Math.random() * upperBound); + } + } + + @Override + public int read() throws IOException { + final int b = in.read(); + if (b != -1) { + // stream available: + inputBytesRead++; + return b; + } else { + choosePaddingLengthOnce(); + return readFromPadding(); + } + } + + private int readFromPadding() { + if (paddingLength == -1) { + throw new IllegalStateException("No padding length chosen yet."); + } + + if (paddingBytesRead < paddingLength) { + // padding available: + return padding[paddingBytesRead++ % padding.length]; + } else { + // end of stream AND padding + return -1; + } + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + final int n = in.read(b, 0, len); + final int bytesRead = Math.max(0, n); // EOF -> 0 + inputBytesRead += bytesRead; + + if (bytesRead == len) { + return bytesRead; + } else if (bytesRead < len) { + choosePaddingLengthOnce(); + final int additionalBytesNeeded = len - bytesRead; + final int m = readFromPadding(b, bytesRead, additionalBytesNeeded); + final int additionalBytesRead = Math.max(0, m); // EOF -> 0 + return (n == -1 && m == -1) ? -1 : bytesRead + additionalBytesRead; + } else { + // bytesRead > len: + throw new IllegalStateException("read more bytes than requested."); + } + } + + private int readFromPadding(byte[] b, int off, int len) { + if (paddingLength == -1) { + throw new IllegalStateException("No padding length chosen yet."); + } + + final int remainingPadding = paddingLength - paddingBytesRead; + if (remainingPadding > len) { + // padding available: + for (int i = 0; i < len; i++) { + b[off + i] = padding[paddingBytesRead + i % padding.length]; + } + paddingBytesRead += len; + return len; + } else if (remainingPadding > 0) { + // partly available: + for (int i = 0; i < remainingPadding; i++) { + b[off + i] = padding[paddingBytesRead + i % padding.length]; + } + paddingBytesRead += remainingPadding; + return remainingPadding; + } else { + // end of stream AND padding + return -1; + } + } + + @Override + public long skip(long n) throws IOException { + throw new IOException("Skip not supported"); + } + + @Override + public int available() throws IOException { + final int inputAvailable = in.available(); + if (inputAvailable > 0) { + return inputAvailable; + } else { + // remaining padding + choosePaddingLengthOnce(); + return paddingLength - paddingBytesRead; + } + } + + @Override + public boolean markSupported() { + return false; + } + +} 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 0abbe5b32..483214be5 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 @@ -9,6 +9,7 @@ import javax.crypto.Mac; /** * Updates a {@link Mac} with the bytes read from this stream. */ +@Deprecated class MacInputStream extends FilterInputStream { private final Mac mac; 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 ae55c916d..d4338d9f6 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 @@ -70,38 +70,6 @@ public class Aes256CryptorTest { } } - @Test - public void testIntegrityAuthentication() throws IOException, DecryptFailedException, EncryptFailedException { - // our test plaintext data: - final byte[] plaintextData = "Hello World".getBytes(); - final InputStream plaintextIn = new ByteArrayInputStream(plaintextData); - - // init cryptor: - final Aes256Cryptor cryptor = new Aes256Cryptor(); - - // encrypt: - final ByteBuffer encryptedData = ByteBuffer.allocate(256); - final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData); - cryptor.encryptFile(plaintextIn, encryptedOut); - IOUtils.closeQuietly(plaintextIn); - IOUtils.closeQuietly(encryptedOut); - - encryptedData.position(0); - - // toggle one bit inf first content byte: - encryptedData.position(64); - final byte fifthByte = encryptedData.get(); - encryptedData.position(64); - encryptedData.put((byte) (fifthByte ^ 0x01)); - - encryptedData.position(0); - - // check mac (should return false) - final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData); - final boolean authentic = cryptor.isAuthentic(encryptedIn); - Assert.assertFalse(authentic); - } - @Test(expected = DecryptFailedException.class) public void testIntegrityViolationDuringDecryption() throws IOException, DecryptFailedException, EncryptFailedException { // our test plaintext data: diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptorDecorator.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptorDecorator.java index 71d10ed10..81b024d0d 100644 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptorDecorator.java +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptorDecorator.java @@ -52,11 +52,6 @@ public class AbstractCryptorDecorator implements Cryptor { return cryptor.decryptedContentLength(encryptedFile); } - @Override - public boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException { - return cryptor.isAuthentic(encryptedFile); - } - @Override public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException { return cryptor.decryptFile(encryptedFile, plaintextFile); diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java index 2474ee907..d6f637e8a 100644 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java @@ -75,11 +75,6 @@ public interface Cryptor extends Destroyable { */ Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException; - /** - * @return true, if the stored MAC matches the calculated one. - */ - boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException; - /** * @return Number of decrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it. * @throws DecryptFailedException If decryption failed