mirror of
https://github.com/cryptomator/cryptomator.git
synced 2024-11-23 03:59:51 +00:00
Using 96 bit of random data and a 32 bit counter (as specified in https://tools.ietf.org/html/rfc3686#section-4). Thus maximum file size supported by Cryptomator is 64GiB, but decreasing risk of IV collisions to 1 : 2^48
This commit is contained in:
parent
188a13b202
commit
652c4cbafb
@ -36,6 +36,8 @@ import org.apache.jackrabbit.webdav.property.DavPropertyName;
|
||||
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
|
||||
import org.apache.jackrabbit.webdav.property.ResourceType;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.exceptions.CounterOverflowException;
|
||||
import org.cryptomator.crypto.exceptions.EncryptFailedException;
|
||||
import org.cryptomator.webdav.exceptions.DavRuntimeException;
|
||||
import org.cryptomator.webdav.exceptions.DecryptFailedRuntimeException;
|
||||
import org.cryptomator.webdav.exceptions.IORuntimeException;
|
||||
@ -85,6 +87,12 @@ class EncryptedDir extends AbstractEncryptedNode {
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to create file.", e);
|
||||
throw new IORuntimeException(e);
|
||||
} catch (CounterOverflowException e) {
|
||||
// lets indicate this to the client as a "file too big" error
|
||||
throw new DavException(DavServletResponse.SC_INSUFFICIENT_SPACE_ON_RESOURCE, e);
|
||||
} catch (EncryptFailedException e) {
|
||||
LOG.error("Encryption failed for unknown reasons.", e);
|
||||
throw new IllegalStateException("Encryption failed for unknown reasons.", e);
|
||||
} finally {
|
||||
IOUtils.closeQuietly(inputContext.getInputStream());
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ 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;
|
||||
@ -171,6 +172,8 @@ class EncryptedFilePart extends EncryptedFile {
|
||||
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 " + path.toString(), e);
|
||||
}
|
||||
|
@ -46,7 +46,10 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.bouncycastle.crypto.generators.SCrypt;
|
||||
import org.cryptomator.crypto.AbstractCryptor;
|
||||
import org.cryptomator.crypto.CryptorIOSupport;
|
||||
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;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
@ -510,10 +513,10 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
long firstRelevantBlock = pos / AES_BLOCK_LENGTH; // cut of fraction!
|
||||
long beginOfFirstRelevantBlock = firstRelevantBlock * AES_BLOCK_LENGTH;
|
||||
long offsetInsideFirstRelevantBlock = pos - beginOfFirstRelevantBlock;
|
||||
countingIv.putLong(AES_BLOCK_LENGTH - Long.BYTES, firstRelevantBlock);
|
||||
countingIv.putInt(AES_BLOCK_LENGTH - Integer.BYTES, (int) firstRelevantBlock); // int-cast is possible, as max file size is 64GiB
|
||||
|
||||
// fast forward stream:
|
||||
encryptedFile.position(64 + beginOfFirstRelevantBlock);
|
||||
encryptedFile.position(64l + beginOfFirstRelevantBlock);
|
||||
|
||||
// generate cipher:
|
||||
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.DECRYPT_MODE);
|
||||
@ -525,13 +528,13 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException {
|
||||
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
|
||||
// truncate file
|
||||
encryptedFile.truncate(0);
|
||||
|
||||
// use an IV, whose last 8 bytes store a long used in counter mode and write initial value to file.
|
||||
final ByteBuffer countingIv = ByteBuffer.wrap(randomData(AES_BLOCK_LENGTH));
|
||||
countingIv.putLong(AES_BLOCK_LENGTH - Long.BYTES, 0l);
|
||||
countingIv.putInt(AES_BLOCK_LENGTH - Integer.BYTES, 0);
|
||||
encryptedFile.write(countingIv);
|
||||
|
||||
// init crypto stuff:
|
||||
@ -550,18 +553,29 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
final OutputStream macOut = new MacOutputStream(out, mac);
|
||||
final OutputStream cipheredOut = new CipherOutputStream(macOut, cipher);
|
||||
final OutputStream blockSizeBufferedOut = new BufferedOutputStream(cipheredOut, AES_BLOCK_LENGTH);
|
||||
final Long plaintextSize = IOUtils.copyLarge(plaintextFile, blockSizeBufferedOut);
|
||||
final InputStream lengthLimitingIn = new CounterAwareInputStream(plaintextFile);
|
||||
final Long plaintextSize;
|
||||
try {
|
||||
plaintextSize = IOUtils.copyLarge(lengthLimitingIn, blockSizeBufferedOut);
|
||||
} catch (CounterAwareInputLimitReachedException ex) {
|
||||
encryptedFile.truncate(64l + CounterAwareInputStream.SIXTY_FOUR_GIGABYE);
|
||||
encryptedContentLength(encryptedFile, CounterAwareInputStream.SIXTY_FOUR_GIGABYE);
|
||||
// no additional padding needed here, as 64GiB is a multiple of 128bit
|
||||
throw new CounterOverflowException("File size exceeds limit (64Gib). Aborting to prevent counter overflow.");
|
||||
}
|
||||
|
||||
// ensure total byte count is a multiple of the block size, in CTR mode:
|
||||
final int remainderToFillLastBlock = AES_BLOCK_LENGTH - (int) (plaintextSize % AES_BLOCK_LENGTH);
|
||||
blockSizeBufferedOut.write(new byte[remainderToFillLastBlock]);
|
||||
|
||||
// append a few blocks of fake data:
|
||||
final int numberOfPlaintextBlocks = (int) Math.ceil(plaintextSize / AES_BLOCK_LENGTH);
|
||||
final int upToTenPercentFakeBlocks = (int) Math.ceil(Math.random() * 0.1 * numberOfPlaintextBlocks);
|
||||
final byte[] emptyBytes = this.randomData(AES_BLOCK_LENGTH);
|
||||
for (int i = 0; i < upToTenPercentFakeBlocks; i += AES_BLOCK_LENGTH) {
|
||||
blockSizeBufferedOut.write(emptyBytes);
|
||||
// for filesizes of up to 16GiB: append a few blocks of fake data:
|
||||
if (plaintextSize < (long) (Integer.MAX_VALUE / 4) * AES_BLOCK_LENGTH) {
|
||||
final int numberOfPlaintextBlocks = (int) Math.ceil(plaintextSize / AES_BLOCK_LENGTH);
|
||||
final int upToTenPercentFakeBlocks = (int) Math.ceil(Math.random() * 0.1 * numberOfPlaintextBlocks);
|
||||
final byte[] emptyBytes = this.randomData(AES_BLOCK_LENGTH);
|
||||
for (int i = 0; i < upToTenPercentFakeBlocks; i += AES_BLOCK_LENGTH) {
|
||||
blockSizeBufferedOut.write(emptyBytes);
|
||||
}
|
||||
}
|
||||
blockSizeBufferedOut.flush();
|
||||
|
||||
|
@ -0,0 +1,59 @@
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
|
||||
/**
|
||||
* Updates a {@link Mac} with the bytes read from this stream.
|
||||
*/
|
||||
class CounterAwareInputStream extends FilterInputStream {
|
||||
|
||||
static final long SIXTY_FOUR_GIGABYE = 1024l * 1024l * 1024l * 64l;
|
||||
|
||||
private final AtomicLong counter;
|
||||
|
||||
/**
|
||||
* @param in Stream from which to read contents, which will update the Mac.
|
||||
* @param mac Mac to be updated during writes.
|
||||
*/
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -25,7 +25,9 @@ class MacInputStream extends FilterInputStream {
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
int b = in.read();
|
||||
mac.update((byte) b);
|
||||
if (b != -1) {
|
||||
mac.update((byte) b);
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@ import java.util.Map;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.cryptomator.crypto.CryptorIOSupport;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.EncryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
import org.junit.Assert;
|
||||
@ -70,7 +71,7 @@ public class Aes256CryptorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIntegrityAuthentication() throws IOException, DecryptFailedException {
|
||||
public void testIntegrityAuthentication() throws IOException, DecryptFailedException, EncryptFailedException {
|
||||
// our test plaintext data:
|
||||
final byte[] plaintextData = "Hello World".getBytes();
|
||||
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
|
||||
@ -102,7 +103,7 @@ public class Aes256CryptorTest {
|
||||
}
|
||||
|
||||
@Test(expected = DecryptFailedException.class)
|
||||
public void testIntegrityViolationDuringDecryption() throws IOException, DecryptFailedException {
|
||||
public void testIntegrityViolationDuringDecryption() throws IOException, DecryptFailedException, EncryptFailedException {
|
||||
// our test plaintext data:
|
||||
final byte[] plaintextData = "Hello World".getBytes();
|
||||
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
|
||||
@ -134,7 +135,7 @@ public class Aes256CryptorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncryptionAndDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
|
||||
public void testEncryptionAndDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, EncryptFailedException {
|
||||
// our test plaintext data:
|
||||
final byte[] plaintextData = "Hello World".getBytes();
|
||||
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
|
||||
@ -169,7 +170,7 @@ public class Aes256CryptorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
|
||||
public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, EncryptFailedException {
|
||||
// our test plaintext data:
|
||||
final byte[] plaintextData = new byte[65536 * Integer.BYTES];
|
||||
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);
|
||||
|
@ -16,6 +16,7 @@ import java.nio.file.DirectoryStream.Filter;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.EncryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
|
||||
@ -97,7 +98,7 @@ public interface Cryptor extends SensitiveDataSwipeListener {
|
||||
/**
|
||||
* @return Number of encrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.
|
||||
*/
|
||||
Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException;
|
||||
Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException;
|
||||
|
||||
/**
|
||||
* @return A filter, that returns <code>true</code> for encrypted files, i.e. if the file is an actual user payload and not a supporting
|
||||
|
@ -10,6 +10,7 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.EncryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
|
||||
@ -99,7 +100,7 @@ public class SamplingDecorator implements Cryptor, CryptorIOSampling {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException {
|
||||
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
|
||||
final InputStream countingInputStream = new CountingInputStream(encryptedBytes, plaintextFile);
|
||||
return cryptor.encryptFile(countingInputStream, encryptedFile);
|
||||
}
|
||||
|
@ -0,0 +1,10 @@
|
||||
package org.cryptomator.crypto.exceptions;
|
||||
|
||||
public class CounterOverflowException extends EncryptFailedException {
|
||||
private static final long serialVersionUID = 380066751064534731L;
|
||||
|
||||
public CounterOverflowException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package org.cryptomator.crypto.exceptions;
|
||||
|
||||
public class EncryptFailedException extends StorageCryptingException {
|
||||
private static final long serialVersionUID = -3855673600374897828L;
|
||||
|
||||
public EncryptFailedException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user