diff --git a/README.md b/README.md
index 3d4f13d13..d463fd5e1 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@ Cryptomator
Multiplatform transparent client-side encryption of your files in the cloud.
-If you want to take a look at the current beta version, go ahead and get your copy of cryptomator on [Cryptomator.org](http://cryptomator.org) or clone and build Cryptomator using Maven (instructions below).
+If you want to take a look at the current beta version, go ahead and get your copy of cryptomator on [Cryptomator.org](https://cryptomator.org) or clone and build Cryptomator using Maven (instructions below).
## Features
- Totally transparent: Just work on the encrypted volume, as if it was an USB drive
diff --git a/main/core/pom.xml b/main/core/pom.xml
index aed6fbb88..80c8af541 100644
--- a/main/core/pom.xml
+++ b/main/core/pom.xml
@@ -12,7 +12,7 @@
org.cryptomator
main
- 0.5.2
+ 0.6.0-SNAPSHOT
core
Cryptomator WebDAV and I/O module
diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedDir.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedDir.java
index 8c95b62b3..afcc3f578 100644
--- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedDir.java
+++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedDir.java
@@ -78,9 +78,7 @@ public class EncryptedDir extends AbstractEncryptedNode {
private void addMemberFile(DavResource resource, InputContext inputContext) throws DavException {
final Path childPath = ResourcePathUtils.getPhysicalPath(resource);
- SeekableByteChannel channel = null;
- try {
- channel = Files.newByteChannel(childPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
+ try (final SeekableByteChannel channel = Files.newByteChannel(childPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
cryptor.encryptFile(inputContext.getInputStream(), channel);
} catch (SecurityException e) {
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
@@ -88,7 +86,6 @@ public class EncryptedDir extends AbstractEncryptedNode {
LOG.error("Failed to create file.", e);
throw new IORuntimeException(e);
} finally {
- IOUtils.closeQuietly(channel);
IOUtils.closeQuietly(inputContext.getInputStream());
}
}
@@ -124,7 +121,9 @@ public class EncryptedDir extends AbstractEncryptedNode {
public void removeMember(DavResource member) throws DavException {
final Path memberPath = ResourcePathUtils.getPhysicalPath(member);
try {
- Files.walkFileTree(memberPath, new DeletingFileVisitor());
+ if (Files.exists(memberPath)) {
+ Files.walkFileTree(memberPath, new DeletingFileVisitor());
+ }
} catch (SecurityException e) {
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
} catch (IOException e) {
diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedFile.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedFile.java
index 83d017a8f..2cd0369fe 100644
--- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedFile.java
+++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedFile.java
@@ -16,7 +16,6 @@ import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
-import org.apache.commons.io.IOUtils;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceFactory;
@@ -30,6 +29,7 @@ import org.apache.jackrabbit.webdav.property.DavPropertyName;
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
+import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
@@ -70,19 +70,20 @@ public class EncryptedFile extends AbstractEncryptedNode {
if (Files.exists(path)) {
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString());
- SeekableByteChannel channel = null;
- try {
- channel = Files.newByteChannel(path, StandardOpenOption.READ);
- outputContext.setContentLength(cryptor.decryptedContentLength(channel));
+ try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
+ final Long contentLength = cryptor.decryptedContentLength(channel);
+ if (contentLength != null) {
+ outputContext.setContentLength(contentLength);
+ }
if (outputContext.hasStream()) {
- cryptor.decryptedFile(channel, outputContext.getOutputStream());
+ cryptor.decryptFile(channel, outputContext.getOutputStream());
}
} catch (EOFException e) {
LOG.warn("Unexpected end of stream (possibly client hung up).");
+ } catch (MacAuthenticationFailedException e) {
+ LOG.warn("MAC authentication failed, file content {} might be compromised.", getLocator().getResourcePath());
} catch (DecryptFailedException e) {
throw new IOException("Error decrypting file " + path.toString(), e);
- } finally {
- IOUtils.closeQuietly(channel);
}
}
}
@@ -91,12 +92,15 @@ public class EncryptedFile extends AbstractEncryptedNode {
protected void determineProperties() {
final Path path = ResourcePathUtils.getPhysicalPath(this);
if (Files.exists(path)) {
- SeekableByteChannel channel = null;
- try {
- channel = Files.newByteChannel(path, StandardOpenOption.READ);
+ try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
final Long contentLength = cryptor.decryptedContentLength(channel);
properties.add(new DefaultDavProperty(DavPropertyName.GETCONTENTLENGTH, contentLength));
+ } catch (IOException e) {
+ LOG.error("Error reading filesize " + path.toString(), e);
+ throw new IORuntimeException(e);
+ }
+ try {
final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
properties.add(new DefaultDavProperty(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
properties.add(new DefaultDavProperty(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
@@ -104,8 +108,6 @@ public class EncryptedFile extends AbstractEncryptedNode {
} catch (IOException e) {
LOG.error("Error determining metadata " + path.toString(), e);
throw new IORuntimeException(e);
- } finally {
- IOUtils.closeQuietly(channel);
}
}
}
diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedFilePart.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedFilePart.java
index c257b39fb..b3c27fb18 100644
--- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedFilePart.java
+++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedFilePart.java
@@ -9,7 +9,6 @@ import java.nio.file.StandardOpenOption;
import java.util.HashSet;
import java.util.Set;
-import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.MutablePair;
@@ -113,9 +112,7 @@ public class EncryptedFilePart extends EncryptedFile {
final Path path = ResourcePathUtils.getPhysicalPath(this);
if (Files.exists(path)) {
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
- SeekableByteChannel channel = null;
- try {
- channel = Files.newByteChannel(path, StandardOpenOption.READ);
+ try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
final Long fileSize = cryptor.decryptedContentLength(channel);
final Pair range = getUnionRange(fileSize);
final Long rangeLength = range.getRight() - range.getLeft() + 1;
@@ -130,8 +127,6 @@ public class EncryptedFilePart extends EncryptedFile {
}
} catch (DecryptFailedException e) {
throw new IOException("Error decrypting file " + path.toString(), e);
- } finally {
- IOUtils.closeQuietly(channel);
}
}
}
diff --git a/main/crypto-aes/pom.xml b/main/crypto-aes/pom.xml
index 15362ea40..85101bfda 100644
--- a/main/crypto-aes/pom.xml
+++ b/main/crypto-aes/pom.xml
@@ -12,7 +12,7 @@
org.cryptomator
main
- 0.5.2
+ 0.6.0-SNAPSHOT
crypto-aes
Cryptomator cryptographic module (AES)
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 0f2b3ee44..a4ed5b505 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
@@ -47,6 +47,7 @@ import org.bouncycastle.crypto.generators.SCrypt;
import org.cryptomator.crypto.AbstractCryptor;
import org.cryptomator.crypto.CryptorIOSupport;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
+import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.cryptomator.crypto.io.SeekableByteChannelInputStream;
@@ -369,34 +370,6 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
ioSupport.writePathSpecificMetadata(metadataFile, objectMapper.writeValueAsBytes(metadata));
}
- private void authenticateContent(SeekableByteChannel encryptedFile) throws IOException, DecryptFailedException {
- // init mac:
- final Mac calculatedMac = this.hmacSha256(hMacMasterKey);
-
- // read stored mac:
- encryptedFile.position(16);
- final ByteBuffer storedMac = ByteBuffer.allocate(calculatedMac.getMacLength());
- final int numMacBytesRead = encryptedFile.read(storedMac);
-
- // check validity of header:
- if (numMacBytesRead != calculatedMac.getMacLength()) {
- throw new IOException("Failed to read file header.");
- }
-
- // read all encrypted data and calculate mac:
- encryptedFile.position(64);
- final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
- final InputStream macIn = new MacInputStream(in, calculatedMac);
- IOUtils.copyLarge(macIn, new NullOutputStream());
-
- // compare (in constant time):
- boolean macMatches = MessageDigest.isEqual(storedMac.array(), calculatedMac.doFinal());
-
- if (!macMatches) {
- throw new DecryptFailedException("MAC authentication failed.");
- }
- }
-
@Override
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException {
// skip 128bit IV + 256 bit MAC:
@@ -422,24 +395,49 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
}
}
+ private void encryptedContentLength(SeekableByteChannel encryptedFile, Long contentLength) throws IOException {
+ final ByteBuffer encryptedFileSizeBuffer;
+
+ // encrypt content length in ECB mode (content length is less than one block):
+ try {
+ final ByteBuffer fileSizeBuffer = ByteBuffer.allocate(Long.BYTES);
+ fileSizeBuffer.putLong(contentLength);
+ final Cipher sizeCipher = aesEcbCipher(primaryMasterKey, Cipher.ENCRYPT_MODE);
+ final byte[] encryptedFileSize = sizeCipher.doFinal(fileSizeBuffer.array());
+ encryptedFileSizeBuffer = ByteBuffer.wrap(encryptedFileSize);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ throw new IllegalStateException("Block size must be valid, as padding is requested. BadPaddingException not possible in encrypt mode.", e);
+ }
+
+ // skip 128bit IV + 256 bit MAC:
+ encryptedFile.position(48);
+
+ // write result:
+ encryptedFile.write(encryptedFileSizeBuffer);
+ }
+
@Override
- public Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
+ public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
// read iv:
encryptedFile.position(0);
final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
final int numIvBytesRead = encryptedFile.read(countingIv);
+ // init mac:
+ final Mac calculatedMac = this.hmacSha256(hMacMasterKey);
+
+ // read stored mac:
+ final ByteBuffer storedMac = ByteBuffer.allocate(calculatedMac.getMacLength());
+ final int numMacBytesRead = encryptedFile.read(storedMac);
+
// read file size:
final Long fileSize = decryptedContentLength(encryptedFile);
// check validity of header:
- if (numIvBytesRead != AES_BLOCK_LENGTH || fileSize == null) {
+ if (numIvBytesRead != AES_BLOCK_LENGTH || numMacBytesRead != calculatedMac.getMacLength() || fileSize == null) {
throw new IOException("Failed to read file header.");
}
- // check MAC:
- this.authenticateContent(encryptedFile);
-
// go to begin of content:
encryptedFile.position(64);
@@ -448,8 +446,25 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
// read content
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
- final InputStream cipheredIn = new CipherInputStream(in, cipher);
- return IOUtils.copyLarge(cipheredIn, plaintextFile, 0, fileSize);
+ final InputStream macIn = new MacInputStream(in, calculatedMac);
+ final InputStream cipheredIn = new CipherInputStream(macIn, cipher);
+ final long bytesDecrypted = IOUtils.copyLarge(cipheredIn, plaintextFile, 0, fileSize);
+
+ // drain remaining bytes to /dev/null to complete MAC calculation:
+ IOUtils.copyLarge(macIn, new NullOutputStream());
+
+ // compare (in constant time):
+ final boolean macMatches = MessageDigest.isEqual(storedMac.array(), calculatedMac.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.");
+ }
+
+ return bytesDecrypted;
}
@Override
@@ -464,9 +479,6 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
throw new IOException("Failed to read file header.");
}
- // check MAC:
- this.authenticateContent(encryptedFile);
-
// seek relevant position and update iv:
long firstRelevantBlock = pos / AES_BLOCK_LENGTH; // cut of fraction!
long beginOfFirstRelevantBlock = firstRelevantBlock * AES_BLOCK_LENGTH;
@@ -493,7 +505,6 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
// 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.position(0);
encryptedFile.write(countingIv);
// init crypto stuff:
@@ -504,9 +515,8 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
final ByteBuffer macBuffer = ByteBuffer.allocate(mac.getMacLength());
encryptedFile.write(macBuffer);
- // init filesize buffer and skip 16 bytes
- final ByteBuffer encryptedFileSizeBuffer = ByteBuffer.allocate(AES_BLOCK_LENGTH);
- encryptedFile.write(encryptedFileSizeBuffer);
+ // encrypt and write "zero length" as a placeholder, which will be read by concurrent requests, as long as encryption didn't finish:
+ encryptedContentLength(encryptedFile, 0l);
// write content:
final OutputStream out = new SeekableByteChannelOutputStream(encryptedFile);
@@ -529,26 +539,14 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
blockSizeBufferedOut.flush();
// write MAC of total ciphertext:
- macBuffer.position(0);
+ macBuffer.clear();
macBuffer.put(mac.doFinal());
- macBuffer.position(0);
+ macBuffer.flip();
encryptedFile.position(16); // right behind the IV
encryptedFile.write(macBuffer); // 256 bit MAC
- // encrypt and write plaintextSize
- try {
- final ByteBuffer fileSizeBuffer = ByteBuffer.allocate(Long.BYTES);
- fileSizeBuffer.putLong(plaintextSize);
- final Cipher sizeCipher = aesEcbCipher(primaryMasterKey, Cipher.ENCRYPT_MODE);
- final byte[] encryptedFileSize = sizeCipher.doFinal(fileSizeBuffer.array());
- encryptedFileSizeBuffer.position(0);
- encryptedFileSizeBuffer.put(encryptedFileSize);
- encryptedFileSizeBuffer.position(0);
- encryptedFile.position(48); // right behind the IV and MAC
- encryptedFile.write(encryptedFileSizeBuffer);
- } catch (IllegalBlockSizeException | BadPaddingException e) {
- throw new IllegalStateException("Block size must be valid, as padding is requested. BadPaddingException not possible in encrypt mode.", e);
- }
+ // encrypt and write plaintextSize:
+ encryptedContentLength(encryptedFile, plaintextSize);
return plaintextSize;
}
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 7d486c8c8..4b29d9aed 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
@@ -98,7 +98,7 @@ public class Aes256CryptorTest {
// decrypt modified content (should fail with DecryptFailedException):
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
- cryptor.decryptedFile(encryptedIn, plaintextOut);
+ cryptor.decryptFile(encryptedIn, plaintextOut);
}
@Test
@@ -126,7 +126,7 @@ public class Aes256CryptorTest {
// decrypt:
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
- final Long numDecryptedBytes = cryptor.decryptedFile(encryptedIn, plaintextOut);
+ final Long numDecryptedBytes = cryptor.decryptFile(encryptedIn, plaintextOut);
IOUtils.closeQuietly(encryptedIn);
IOUtils.closeQuietly(plaintextOut);
Assert.assertEquals(filesize.longValue(), numDecryptedBytes.longValue());
diff --git a/main/crypto-api/pom.xml b/main/crypto-api/pom.xml
index 1dba0a2f8..ffb9cbec2 100644
--- a/main/crypto-api/pom.xml
+++ b/main/crypto-api/pom.xml
@@ -12,7 +12,7 @@
org.cryptomator
main
- 0.5.2
+ 0.6.0-SNAPSHOT
crypto-api
Cryptomator cryptographic module API
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 2183c8048..b9e05f358 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
@@ -79,7 +79,7 @@ public interface Cryptor extends SensitiveDataSwipeListener {
* @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
*/
- Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException;
+ Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException;
/**
* @param pos First byte (inclusive)
diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingDecorator.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingDecorator.java
index e160838ab..7c3e9245f 100644
--- a/main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingDecorator.java
+++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingDecorator.java
@@ -82,9 +82,9 @@ public class SamplingDecorator implements Cryptor, CryptorIOSampling {
}
@Override
- public Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
+ public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
- return cryptor.decryptedFile(encryptedFile, countingInputStream);
+ return cryptor.decryptFile(encryptedFile, countingInputStream);
}
@Override
diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/MacAuthenticationFailedException.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/MacAuthenticationFailedException.java
new file mode 100644
index 000000000..7535c35fe
--- /dev/null
+++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/MacAuthenticationFailedException.java
@@ -0,0 +1,11 @@
+package org.cryptomator.crypto.exceptions;
+
+public class MacAuthenticationFailedException extends DecryptFailedException {
+
+ private static final long serialVersionUID = -5577052361643658772L;
+
+ public MacAuthenticationFailedException(String msg) {
+ super(msg);
+ }
+
+}
diff --git a/main/pom.xml b/main/pom.xml
index 12cf9e969..6342f352b 100644
--- a/main/pom.xml
+++ b/main/pom.xml
@@ -4,7 +4,7 @@
4.0.0
org.cryptomator
main
- 0.5.2
+ 0.6.0-SNAPSHOT
pom
Cryptomator
diff --git a/main/ui/pom.xml b/main/ui/pom.xml
index 0810a24f5..c70c4875d 100644
--- a/main/ui/pom.xml
+++ b/main/ui/pom.xml
@@ -12,7 +12,7 @@
org.cryptomator
main
- 0.5.2
+ 0.6.0-SNAPSHOT
ui
Cryptomator GUI