new, more secure encryption scheme

- fixed flaw reported by Stan Drapkin (SecurityDriven.NET) reducing effective key size to 96 bit
- multiple file content MACs for 1MB blocks, preventing chosen ciphertext attacks, as authentication now happens before decryption
- allowing files bigger than 64GiB
This commit is contained in:
Sebastian Stenzel
2015-06-21 18:51:39 +02:00
parent d7186bb2dd
commit 45cf87d089
12 changed files with 238 additions and 300 deletions

View File

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

View File

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

View File

@@ -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<DavResourceLocator, MacAuthenticationJob> 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<Pair<Long, Long>> requestedContentRanges = new HashSet<Pair<Long, Long>>();
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;
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -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).<br/>
* From https://tools.ietf.org/html/rfc3686: <cite> Using the encryption process described in section 2.1, this construction permits each packet to consist of up to: (2^32)-1 blocks</cite>
*/
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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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