mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-04-21 10:06:55 -04:00
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:
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user