diff --git a/.travis.yml b/.travis.yml index 04803a603..6744d39ba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,9 @@ sudo: required dist: trusty jdk: - oraclejdk8 +cache: + directories: + - $HOME/.m2 env: global: - secure: "IfYURwZaDWuBDvyn47n0k1Zod/IQw1FF+CS5nnV08Q+NfC3vGGJMwV8m59XnbfwnWGxwvCaAbk4qP6s6+ijgZNKkvgfFMo3rfTok5zt43bIqgaFOANYV+OC/1c59gYD6ZUxhW5iNgMgU3qdsRtJuwSmfkVv/jKyLGfAbS4kN8BA=" #coverity diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/File.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/File.java index ddcefd11a..e973c7255 100644 --- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/File.java +++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/File.java @@ -17,6 +17,13 @@ public interface File extends Node, Comparable { static final int EOF = -1; + /** + * @return The current size of the file. This value is a snapshot and might have been changed by concurrent modifications. + * @throws UncheckedIOException + * if an {@link IOException} occurs + */ + long size() throws UncheckedIOException; + /** *

* Opens this file for reading. @@ -39,7 +46,6 @@ public interface File extends Node, Comparable { * if an {@link IOException} occurs while opening the file, the * file does not exist or is a directory */ - ReadableFile openReadable() throws UncheckedIOException; /** diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/ReadableFile.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/ReadableFile.java index 398d87e72..300ad32ec 100644 --- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/ReadableFile.java +++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/ReadableFile.java @@ -30,13 +30,6 @@ public interface ReadableFile extends ReadableByteChannel { @Override int read(ByteBuffer target) throws UncheckedIOException; - /** - * @return The current size of the file. This value is a snapshot and might have been changed by concurrent modifications. - * @throws UncheckedIOException - * if an {@link IOException} occurs - */ - long size() throws UncheckedIOException; - /** *

* Fast-forwards or rewinds the file to the specified position. diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingFile.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingFile.java index 49bc9eb6d..af119c90c 100644 --- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingFile.java +++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingFile.java @@ -15,7 +15,7 @@ import org.cryptomator.filesystem.File; import org.cryptomator.filesystem.ReadableFile; import org.cryptomator.filesystem.WritableFile; -public abstract class DelegatingFile> extends DelegatingNodeimplements File { +public abstract class DelegatingFile> extends DelegatingNode implements File { private final D parent; @@ -29,6 +29,11 @@ public abstract class DelegatingFile> extends D return Optional.of(parent); } + @Override + public long size() throws UncheckedIOException { + return delegate.size(); + } + @Override public ReadableFile openReadable() throws UncheckedIOException { return delegate.openReadable(); diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingReadableFile.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingReadableFile.java index 435601782..d9a022d06 100644 --- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingReadableFile.java +++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingReadableFile.java @@ -31,11 +31,6 @@ public class DelegatingReadableFile implements ReadableFile { return delegate.read(target); } - @Override - public long size() throws UncheckedIOException { - return delegate.size(); - } - @Override public void position(long position) throws UncheckedIOException { delegate.position(position); diff --git a/main/filesystem-api/src/test/java/org/cryptomator/filesystem/delegating/DelegatingFileTest.java b/main/filesystem-api/src/test/java/org/cryptomator/filesystem/delegating/DelegatingFileTest.java index 2e20b1de4..36365d3e2 100644 --- a/main/filesystem-api/src/test/java/org/cryptomator/filesystem/delegating/DelegatingFileTest.java +++ b/main/filesystem-api/src/test/java/org/cryptomator/filesystem/delegating/DelegatingFileTest.java @@ -30,6 +30,16 @@ public class DelegatingFileTest { Assert.assertEquals(mockFile.name(), delegatingFile.name()); } + @Test + public void testSize() { + File mockFile = Mockito.mock(File.class); + DelegatingFile delegatingFile = new TestDelegatingFile(null, mockFile); + + Mockito.when(mockFile.size()).thenReturn(42l); + Assert.assertEquals(42l, delegatingFile.size()); + Mockito.verify(mockFile).size(); + } + @Test public void testParent() { Folder mockFolder = Mockito.mock(Folder.class); diff --git a/main/filesystem-api/src/test/java/org/cryptomator/filesystem/delegating/DelegatingReadableFileTest.java b/main/filesystem-api/src/test/java/org/cryptomator/filesystem/delegating/DelegatingReadableFileTest.java index 0777bd5da..cd3fb106a 100644 --- a/main/filesystem-api/src/test/java/org/cryptomator/filesystem/delegating/DelegatingReadableFileTest.java +++ b/main/filesystem-api/src/test/java/org/cryptomator/filesystem/delegating/DelegatingReadableFileTest.java @@ -42,17 +42,6 @@ public class DelegatingReadableFileTest { Mockito.verify(mockReadableFile).read(buf); } - @Test - public void testSize() { - ReadableFile mockReadableFile = Mockito.mock(ReadableFile.class); - @SuppressWarnings("resource") - DelegatingReadableFile delegatingReadableFile = new DelegatingReadableFile(mockReadableFile); - - Mockito.when(mockReadableFile.size()).thenReturn(42l); - Assert.assertEquals(42l, delegatingReadableFile.size()); - Mockito.verify(mockReadableFile).size(); - } - @Test public void testPosition() { ReadableFile mockReadableFile = Mockito.mock(ReadableFile.class); diff --git a/main/filesystem-crypto-integration-tests/src/test/java/org/cryptomator/filesystem/crypto/CryptoFileSystemIntegrationTest.java b/main/filesystem-crypto-integration-tests/src/test/java/org/cryptomator/filesystem/crypto/CryptoFileSystemIntegrationTest.java index de1f2a6d1..9d8b26869 100644 --- a/main/filesystem-crypto-integration-tests/src/test/java/org/cryptomator/filesystem/crypto/CryptoFileSystemIntegrationTest.java +++ b/main/filesystem-crypto-integration-tests/src/test/java/org/cryptomator/filesystem/crypto/CryptoFileSystemIntegrationTest.java @@ -130,7 +130,7 @@ public class CryptoFileSystemIntegrationTest { // toggle last bit try (WritableFile writable = physicalFile.openWritable(); ReadableFile readable = physicalFile.openReadable()) { - ByteBuffer buf = ByteBuffer.allocate((int) readable.size()); + ByteBuffer buf = ByteBuffer.allocate((int) physicalFile.size()); readable.read(buf); buf.array()[buf.limit() - 1] ^= 0x01; buf.flip(); diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentDecryptor.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentDecryptor.java index 016e9871e..be7c468a9 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentDecryptor.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentDecryptor.java @@ -19,11 +19,6 @@ import javax.security.auth.Destroyable; */ public interface FileContentDecryptor extends Destroyable, Closeable { - /** - * @return Number of bytes of the decrypted file. - */ - long contentLength(); - /** * Appends further ciphertext to this decryptor. This method might block until space becomes available. If so, it is interruptable. * diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/Constants.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/Constants.java index efbe84bfc..218f4c3aa 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/Constants.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/Constants.java @@ -1,16 +1,11 @@ package org.cryptomator.crypto.engine.impl; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; - public final class Constants { private Constants() { } - static final Collection SUPPORTED_VAULT_VERSIONS = Collections.unmodifiableCollection(Arrays.asList(3, 4)); - static final Integer CURRENT_VAULT_VERSION = 4; + static final Integer CURRENT_VAULT_VERSION = 5; public static final int PAYLOAD_SIZE = 32 * 1024; public static final int NONCE_SIZE = 16; diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java index 836e6c134..166a34b12 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java @@ -9,7 +9,6 @@ package org.cryptomator.crypto.engine.impl; import static org.cryptomator.crypto.engine.impl.Constants.CURRENT_VAULT_VERSION; -import static org.cryptomator.crypto.engine.impl.Constants.SUPPORTED_VAULT_VERSIONS; import java.io.IOException; import java.nio.ByteBuffer; @@ -110,7 +109,7 @@ class CryptorImpl implements Cryptor { assert keyFile != null; // check version - if (!SUPPORTED_VAULT_VERSIONS.contains(keyFile.getVersion())) { + if (CURRENT_VAULT_VERSION != keyFile.getVersion()) { throw new UnsupportedVaultFormatException(keyFile.getVersion(), CURRENT_VAULT_VERSION); } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java index e619cddc5..0088f6a51 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java @@ -22,7 +22,6 @@ import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.atomic.LongAdder; import java.util.function.Supplier; import javax.crypto.Cipher; @@ -47,7 +46,6 @@ class FileContentDecryptorImpl implements FileContentDecryptor { private final Supplier hmacSha256; private final FileHeader header; private final boolean authenticate; - private final LongAdder cleartextBytesDecrypted = new LongAdder(); private ByteBuffer ciphertextBuffer = ByteBuffer.allocate(CHUNK_SIZE); private long chunkNumber = 0; @@ -58,11 +56,6 @@ class FileContentDecryptorImpl implements FileContentDecryptor { this.chunkNumber = firstCiphertextByte / CHUNK_SIZE; // floor() by int-truncation } - @Override - public long contentLength() { - return header.getPayload().getFilesize(); - } - @Override public void append(ByteBuffer ciphertext) throws InterruptedException { if (ciphertext == FileContentCryptor.EOF) { @@ -105,15 +98,7 @@ class FileContentDecryptorImpl implements FileContentDecryptor { @Override public ByteBuffer cleartext() throws InterruptedException { try { - final ByteBuffer cleartext = dataProcessor.processedData(); - long bytesUntilLogicalEof = contentLength() - cleartextBytesDecrypted.sum(); - if (bytesUntilLogicalEof <= 0) { - return FileContentCryptor.EOF; - } else if (bytesUntilLogicalEof < cleartext.remaining()) { - cleartext.limit((int) bytesUntilLogicalEof); - } - cleartextBytesDecrypted.add(cleartext.remaining()); - return cleartext; + return dataProcessor.processedData(); } catch (ExecutionException e) { if (e.getCause() instanceof AuthenticationFailedException) { throw new AuthenticationFailedException(e); diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImpl.java index 20b2eeea7..71c0b121b 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImpl.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImpl.java @@ -36,8 +36,6 @@ import org.cryptomator.io.ByteBuffers; class FileContentEncryptorImpl implements FileContentEncryptor { private static final String HMAC_SHA256 = "HmacSHA256"; - private static final int PADDING_LOWER_BOUND = 4 * 1024; // 4k - private static final int PADDING_UPPER_BOUND = 16 * 1024 * 1024; // 16M private static final int NUM_THREADS = Runtime.getRuntime().availableProcessors(); private static final int READ_AHEAD = 2; private static final ExecutorService SHARED_DECRYPTION_EXECUTOR = Executors.newFixedThreadPool(NUM_THREADS); @@ -63,7 +61,7 @@ class FileContentEncryptorImpl implements FileContentEncryptor { @Override public ByteBuffer getHeader() { - header.getPayload().setFilesize(cleartextBytesScheduledForEncryption.sum()); + header.getPayload().setFilesize(-1l); return header.toByteBuffer(headerKey, hmacSha256); } @@ -76,7 +74,6 @@ class FileContentEncryptorImpl implements FileContentEncryptor { public void append(ByteBuffer cleartext) throws InterruptedException { cleartextBytesScheduledForEncryption.add(cleartext.remaining()); if (cleartext == FileContentCryptor.EOF) { - appendSizeObfuscationPadding(cleartextBytesScheduledForEncryption.sum()); submitCleartextBuffer(); submitEof(); } else { @@ -84,19 +81,6 @@ class FileContentEncryptorImpl implements FileContentEncryptor { } } - private void appendSizeObfuscationPadding(long actualSize) throws InterruptedException { - final int maxPaddingLength = (int) Math.min(Math.max(actualSize / 10, PADDING_LOWER_BOUND), PADDING_UPPER_BOUND); // preferably 10%, but at least lower bound and no more than upper bound - final int randomPaddingLength = randomSource.nextInt(maxPaddingLength); - final ByteBuffer buf = ByteBuffer.allocate(PAYLOAD_SIZE); - int remainingPadding = randomPaddingLength; - while (remainingPadding > 0) { - int bytesInCurrentIteration = Math.min(remainingPadding, PAYLOAD_SIZE); - buf.clear().limit(bytesInCurrentIteration); - appendAllAndSubmitIfFull(buf); - remainingPadding -= bytesInCurrentIteration; - } - } - private void appendAllAndSubmitIfFull(ByteBuffer cleartext) throws InterruptedException { while (cleartext.hasRemaining()) { ByteBuffers.copy(cleartext, cleartextBuffer); diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/BlockAlignedReadableFile.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/BlockAlignedReadableFile.java index 34e37e925..4ae1c0a10 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/BlockAlignedReadableFile.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/BlockAlignedReadableFile.java @@ -100,11 +100,6 @@ class BlockAlignedReadableFile implements ReadableFile { return delegate.isOpen(); } - @Override - public long size() throws UncheckedIOException { - return delegate.size(); - } - @Override public void close() throws UncheckedIOException { delegate.close(); diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/ConflictResolver.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/ConflictResolver.java index 138ec226f..a15c0a6fe 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/ConflictResolver.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/ConflictResolver.java @@ -79,10 +79,10 @@ final class ConflictResolver { } private boolean isSameFileBasedOnSample(File file1, File file2, int sampleSize) { - try (ReadableFile r1 = file1.openReadable(); ReadableFile r2 = file2.openReadable()) { - if (r1.size() != r2.size()) { - return false; - } else { + if (file1.size() != file2.size()) { + return false; + } else { + try (ReadableFile r1 = file1.openReadable(); ReadableFile r2 = file2.openReadable()) { ByteBuffer beginOfFile1 = ByteBuffer.allocate(sampleSize); ByteBuffer beginOfFile2 = ByteBuffer.allocate(sampleSize); int bytesRead1 = r1.read(beginOfFile1); diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFile.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFile.java index ff0df04e7..9d2afa238 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFile.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFile.java @@ -8,6 +8,9 @@ *******************************************************************************/ package org.cryptomator.filesystem.crypto; +import static org.cryptomator.crypto.engine.impl.Constants.CHUNK_SIZE; +import static org.cryptomator.crypto.engine.impl.Constants.PAYLOAD_SIZE; + import java.io.UncheckedIOException; import java.nio.file.FileAlreadyExistsException; import java.util.Optional; @@ -28,6 +31,25 @@ class CryptoFile extends CryptoNode implements File { return parent().get().encryptChildName(name()); } + @Override + public long size() throws UncheckedIOException { + if (!physicalFile().isPresent()) { + return -1l; + } else { + File file = physicalFile().get(); + long ciphertextSize = file.size() - cryptor.getFileContentCryptor().getHeaderSize(); + long overheadPerChunk = CHUNK_SIZE - PAYLOAD_SIZE; + long numFullChunks = ciphertextSize / CHUNK_SIZE; // floor by int-truncation + long additionalCiphertextBytes = ciphertextSize % CHUNK_SIZE; + if (additionalCiphertextBytes > 0 && additionalCiphertextBytes <= overheadPerChunk) { + throw new IllegalArgumentException("Method not defined for input value " + ciphertextSize); + } + long additionalCleartextBytes = (additionalCiphertextBytes == 0) ? 0 : additionalCiphertextBytes - overheadPerChunk; + assert additionalCleartextBytes >= 0; + return PAYLOAD_SIZE * numFullChunks + additionalCleartextBytes; + } + } + @Override public ReadableFile openReadable() { boolean authenticate = !fileSystem().delegate().shouldSkipAuthentication(toString()); diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoReadableFile.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoReadableFile.java index c915750f0..9792643f2 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoReadableFile.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoReadableFile.java @@ -70,12 +70,6 @@ class CryptoReadableFile implements ReadableFile { } } - @Override - public long size() throws UncheckedIOException { - assert decryptor != null : "decryptor is always being set during position(long)"; - return decryptor.contentLength(); - } - @Override public void position(long position) throws UncheckedIOException { if (readAheadTask != null) { diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/NoFileContentCryptor.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/NoFileContentCryptor.java index d7a061583..5a342d6e4 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/NoFileContentCryptor.java +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/NoFileContentCryptor.java @@ -44,16 +44,9 @@ class NoFileContentCryptor implements FileContentCryptor { private class Decryptor implements FileContentDecryptor { private final BlockingQueue> cleartextQueue = new LinkedBlockingQueue<>(); - private final long contentLength; private Decryptor(ByteBuffer header) { assert header.remaining() == Long.BYTES; - this.contentLength = header.getLong(); - } - - @Override - public long contentLength() { - return contentLength; } @Override diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/CryptorImplTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/CryptorImplTest.java index 8bd890a76..cb30b9bd5 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/CryptorImplTest.java +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/CryptorImplTest.java @@ -21,7 +21,7 @@ public class CryptorImplTest { @Test public void testMasterkeyDecryptionWithCorrectPassphrase() throws IOException { - final String testMasterKey = "{\"version\":4,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // + final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}"; @@ -31,7 +31,7 @@ public class CryptorImplTest { @Test(expected = InvalidPassphraseException.class) public void testMasterkeyDecryptionWithWrongPassphrase() throws IOException { - final String testMasterKey = "{\"version\":4,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // + final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}"; @@ -52,7 +52,7 @@ public class CryptorImplTest { @Ignore @Test(expected = UnsupportedVaultFormatException.class) public void testMasterkeyDecryptionWithMissingVersionMac() throws IOException { - final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // + final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"}"; final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl(); @@ -62,7 +62,7 @@ public class CryptorImplTest { @Ignore @Test(expected = UnsupportedVaultFormatException.class) public void testMasterkeyDecryptionWithWrongVersionMac() throws IOException { - final String testMasterKey = "{\"version\":4,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // + final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + "\"versionMac\":\"z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfoK=\"}"; @@ -72,14 +72,13 @@ public class CryptorImplTest { @Test public void testMasterkeyEncryption() throws IOException { - final String expectedMasterKey = "{\"version\":4,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," // + final String expectedMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," // + "\"primaryMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," // + "\"hmacMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," // - + "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}"; + + "\"versionMac\":\"yuwoRE9GSdgQ2b//qRpTCj3W0qsVLxYVa7/KB3PkfA4=\"}"; final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl(); cryptor.randomizeMasterkey(); final byte[] masterkeyFile = cryptor.writeKeysToMasterkeyFile("asd"); - System.out.println(new String(masterkeyFile)); Assert.assertArrayEquals(expectedMasterKey.getBytes(), masterkeyFile); } diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImplTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImplTest.java index 71ed1b825..6fa92c74b 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImplTest.java +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImplTest.java @@ -43,20 +43,6 @@ public class FileContentCryptorImplTest { }; - private static final SecureRandom RANDOM_MOCK_2 = new SecureRandom() { - - @Override - public int nextInt(int bound) { - return 500; - } - - @Override - public void nextBytes(byte[] bytes) { - Arrays.fill(bytes, (byte) 0x00); - } - - }; - @Test(expected = IllegalArgumentException.class) public void testShortHeaderInDecryptor() throws InterruptedException { final byte[] keyBytes = new byte[32]; @@ -137,45 +123,6 @@ public class FileContentCryptorImplTest { Assert.assertArrayEquals("cleartext message".getBytes(), result); } - @Test - public void testEncryptionAndDecryptionWithSizeObfuscationPadding() throws InterruptedException { - final byte[] keyBytes = new byte[32]; - final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES"); - final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256"); - FileContentCryptor cryptor = new FileContentCryptorImpl(encryptionKey, macKey, RANDOM_MOCK_2); - - ByteBuffer header = ByteBuffer.allocate(cryptor.getHeaderSize()); - ByteBuffer ciphertext = ByteBuffer.allocate(16 + 11 + 500 + 32 + 1); // 16 bytes iv + 11 bytes ciphertext + 500 bytes padding + 32 bytes mac + 1. - try (FileContentEncryptor encryptor = cryptor.createFileContentEncryptor(Optional.empty(), 0)) { - encryptor.append(ByteBuffer.wrap("hello world".getBytes())); - encryptor.append(FileContentCryptor.EOF); - ByteBuffer buf; - while ((buf = encryptor.ciphertext()) != FileContentCryptor.EOF) { - ByteBuffers.copy(buf, ciphertext); - } - ByteBuffers.copy(encryptor.getHeader(), header); - } - header.flip(); - ciphertext.flip(); - - Assert.assertEquals(16 + 11 + 500 + 32, ciphertext.remaining()); - - ByteBuffer plaintext = ByteBuffer.allocate(12); // 11 bytes plaintext + 1 - try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header, 0, true)) { - decryptor.append(ciphertext); - decryptor.append(FileContentCryptor.EOF); - ByteBuffer buf; - while ((buf = decryptor.cleartext()) != FileContentCryptor.EOF) { - ByteBuffers.copy(buf, plaintext); - } - } - plaintext.flip(); - - byte[] result = new byte[plaintext.remaining()]; - plaintext.get(result); - Assert.assertArrayEquals("hello world".getBytes(), result); - } - @Test(timeout = 20000) // assuming a minimum speed of 10mb/s during encryption and decryption 20s should be enough public void testEncryptionAndDecryptionSpeed() throws InterruptedException, IOException { final byte[] keyBytes = new byte[32]; diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImplTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImplTest.java index b99964a77..7c36c28e4 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImplTest.java +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImplTest.java @@ -35,20 +35,6 @@ public class FileContentEncryptorImplTest { }; - private static final SecureRandom RANDOM_MOCK_2 = new SecureRandom() { - - @Override - public int nextInt(int bound) { - return 42; - } - - @Override - public void nextBytes(byte[] bytes) { - Arrays.fill(bytes, (byte) 0x00); - } - - }; - @Test public void testEncryption() throws InterruptedException { final byte[] keyBytes = new byte[32]; @@ -95,24 +81,4 @@ public class FileContentEncryptorImplTest { } } - @Test - public void testSizeObfuscation() throws InterruptedException { - final byte[] keyBytes = new byte[32]; - final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES"); - final SecretKey macKey = new SecretKeySpec(keyBytes, "AES"); - - try (FileContentEncryptor encryptor = new FileContentEncryptorImpl(headerKey, macKey, RANDOM_MOCK_2, 0)) { - encryptor.append(FileContentCryptor.EOF); - - ByteBuffer result = ByteBuffer.allocate(91); // 16 bytes iv + 42 bytes size obfuscation + 32 bytes mac + 1 - ByteBuffer buf; - while ((buf = encryptor.ciphertext()) != FileContentCryptor.EOF) { - ByteBuffers.copy(buf, result); - } - result.flip(); - - Assert.assertEquals(90, result.remaining()); - } - } - } diff --git a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java index 568f431d1..781b079b1 100644 --- a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java +++ b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java @@ -43,6 +43,11 @@ class InMemoryFile extends InMemoryNode implements File { return buf; } + @Override + public long size() throws UncheckedIOException { + return content.get().limit(); + } + @Override public void moveTo(File destination) throws UncheckedIOException { if (destination instanceof InMemoryFile) { @@ -103,7 +108,7 @@ class InMemoryFile extends InMemoryNode implements File { throw new UncheckedIOException(new FileAlreadyExistsException(k)); } else { if (v == null) { - assert!content.get().hasRemaining(); + assert !content.get().hasRemaining(); this.creationTime = Instant.now(); } this.lastModified = Instant.now(); @@ -120,7 +125,7 @@ class InMemoryFile extends InMemoryNode implements File { // returning null removes the entry. return null; }); - assert!this.exists(); + assert !this.exists(); } @Override diff --git a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryReadableFile.java b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryReadableFile.java index 90eab31f3..8d2212596 100644 --- a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryReadableFile.java +++ b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryReadableFile.java @@ -51,11 +51,6 @@ class InMemoryReadableFile implements ReadableFile { } } - @Override - public long size() throws UncheckedIOException { - return contentGetter.get().limit(); - } - @Override public void position(long position) throws UncheckedIOException { assert position < Integer.MAX_VALUE : "Can not use that big in-memory files."; diff --git a/main/filesystem-inmemory/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileSystemTest.java b/main/filesystem-inmemory/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileSystemTest.java index 7b913b0ba..d262e2513 100644 --- a/main/filesystem-inmemory/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileSystemTest.java +++ b/main/filesystem-inmemory/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileSystemTest.java @@ -104,9 +104,7 @@ public class InMemoryFileSystemTest { Assert.assertTrue(fooFile.exists()); // check if size = 11 bytes - try (ReadableFile readable = fooFile.openReadable()) { - Assert.assertEquals(11, readable.size()); - } + Assert.assertEquals(11, fooFile.size()); // copy foo to bar File barFile = fs.file("bar.txt"); diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/DefaultNioAccess.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/DefaultNioAccess.java index 2196b7a3e..5def2b008 100644 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/DefaultNioAccess.java +++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/DefaultNioAccess.java @@ -17,6 +17,11 @@ import java.util.stream.Stream; class DefaultNioAccess implements NioAccess { + @Override + public long size(Path path) throws IOException { + return Files.size(path); + } + @Override public AsynchronousFileChannel open(Path path, OpenOption... options) throws IOException { return AsynchronousFileChannel.open(path, options); @@ -59,7 +64,8 @@ class DefaultNioAccess implements NioAccess { } catch (AccessDeniedException e) { // workaround for https://github.com/cryptomator/cryptomator/issues/317 try { - if (path.toFile().delete()) return; + if (path.toFile().delete()) + return; } catch (UnsupportedOperationException e2) { // ignore } diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioAccess.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioAccess.java index cb75026f4..f19c46a18 100644 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioAccess.java +++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioAccess.java @@ -16,6 +16,8 @@ interface NioAccess { public static final Holder DEFAULT = new Holder<>(new DefaultNioAccess()); + long size(Path path) throws IOException; + AsynchronousFileChannel open(Path path, OpenOption... options) throws IOException; boolean isRegularFile(Path path, LinkOption... options); diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFile.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFile.java index e1afc2718..a82e0d289 100644 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFile.java +++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFile.java @@ -27,6 +27,15 @@ class NioFile extends NioNode implements File { sharedChannel = instanceFactory.sharedFileChannel(path, nioAccess); } + @Override + public long size() throws UncheckedIOException { + try { + return nioAccess.size(path); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + @Override public ReadableFile openReadable() throws UncheckedIOException { if (lock.getWriteHoldCount() > 0) { diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/ReadableNioFile.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/ReadableNioFile.java index ef6621eb1..b7bff19e9 100644 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/ReadableNioFile.java +++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/ReadableNioFile.java @@ -41,11 +41,6 @@ class ReadableNioFile implements ReadableFile { return open; } - @Override - public long size() throws UncheckedIOException { - return channel.size(); - } - @Override public void position(long position) throws UncheckedIOException { assertOpen(); diff --git a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileTest.java b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileTest.java index ec4269fc9..05dbe75e7 100644 --- a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileTest.java +++ b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileTest.java @@ -16,6 +16,7 @@ import static org.mockito.Mockito.when; import java.io.IOException; import java.io.UncheckedIOException; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.attribute.FileTime; import java.time.Instant; @@ -85,6 +86,27 @@ public class NioFileTest { } + public class Size { + + @Test + public void testSizeReturnsSizeOfRegularFile() throws IOException { + when(nioAccess.size(path)).thenReturn(42l); + + assertThat(inTest.size(), is(42l)); + } + + @Test + public void testSizeThrowsExceptionIfRegularFileThrowsException() throws IOException { + Throwable t = new NoSuchFileException("foo"); + when(nioAccess.size(path)).thenThrow(t); + + thrown.expect(UncheckedIOException.class); + thrown.expectCause(org.hamcrest.Matchers.sameInstance(t)); + inTest.size(); + } + + } + public class Open { @Test diff --git a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/ReadableNioFileTest.java b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/ReadableNioFileTest.java index 4ff589f43..f398aa2c5 100644 --- a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/ReadableNioFileTest.java +++ b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/ReadableNioFileTest.java @@ -83,16 +83,6 @@ public class ReadableNioFileTest { inTest.position(-1); } - @Test - public void testSizeReturnsSizeOfChannel() { - long expectedSize = 85472; - when(channel.size()).thenReturn(expectedSize); - - long actualSize = inTest.size(); - - assertThat(actualSize, is(expectedSize)); - } - @Test public void testReadDelegatesToChannelReadFullyWithZeroPositionIfNotSet() { ByteBuffer buffer = mock(ByteBuffer.class); diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/Tarpit.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/Tarpit.java index 011adddc6..14c528fef 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/Tarpit.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/Tarpit.java @@ -7,8 +7,10 @@ package org.cryptomator.frontend.webdav; import static java.lang.Math.max; import static java.lang.System.currentTimeMillis; +import static java.util.Collections.synchronizedSet; import java.io.Serializable; +import java.util.Collection; import java.util.HashSet; import java.util.Optional; import java.util.Set; @@ -27,18 +29,15 @@ class Tarpit implements Serializable { private static final Logger LOG = LoggerFactory.getLogger(Tarpit.class); private static final long DELAY_MS = 10000; - private final Set validFrontendIds = new HashSet<>(); + private final Set validFrontendIds = synchronizedSet(new HashSet<>()); @Inject - public Tarpit() { - } + public Tarpit() {} - public void register(FrontendId frontendId) { - validFrontendIds.add(frontendId); - } - - public void unregister(FrontendId frontendId) { - validFrontendIds.remove(frontendId); + public void setValidFrontendIds(Collection validFrontendIds) { + this.validFrontendIds.retainAll(validFrontendIds); + this.validFrontendIds.addAll(validFrontendIds); + validFrontendIds.forEach(System.out::println); } public void handle(HttpServletRequest req) { diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavFrontend.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavFrontend.java index f80e5543b..0bfd9515d 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavFrontend.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavFrontend.java @@ -24,15 +24,13 @@ class WebDavFrontend implements Frontend { private final WebDavMounterProvider webdavMounterProvider; private final ServletContextHandler handler; private final URI uri; - private final Runnable afterClose; private WebDavMount mount; - public WebDavFrontend(WebDavMounterProvider webdavMounterProvider, ServletContextHandler handler, URI uri, Runnable afterUnmount) throws FrontendCreationFailedException { + public WebDavFrontend(WebDavMounterProvider webdavMounterProvider, ServletContextHandler handler, URI uri) throws FrontendCreationFailedException { this.webdavMounterProvider = webdavMounterProvider; this.handler = handler; this.uri = uri; - this.afterClose = afterUnmount; try { handler.start(); } catch (Exception e) { @@ -42,12 +40,8 @@ class WebDavFrontend implements Frontend { @Override public void close() throws Exception { - try { - unmount(); - handler.stop(); - } finally { - afterClose.run(); - } + unmount(); + handler.stop(); } @Override diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServer.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServer.java index 0f18bbab5..cf40f1ce1 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServer.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServer.java @@ -12,6 +12,7 @@ import static java.lang.String.format; import java.net.URI; import java.net.URISyntaxException; +import java.util.Collection; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @@ -117,9 +118,12 @@ public class WebDavServer implements FrontendFactory { throw new IllegalStateException(e); } final ServletContextHandler handler = addServlet(root, uri); - tarpit.register(id); LOG.info("Servlet available under " + uri); - return new WebDavFrontend(webdavMounterProvider, handler, uri, () -> tarpit.unregister(id)); + return new WebDavFrontend(webdavMounterProvider, handler, uri); + } + + public void setValidFrontendIds(Collection validFrontendIds) { + tarpit.setValidFrontendIds(validFrontendIds); } } diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/DavFile.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/DavFile.java index bdde344b6..9c5d0ee8f 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/DavFile.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/DavFile.java @@ -32,14 +32,11 @@ import org.cryptomator.filesystem.File; import org.cryptomator.filesystem.Folder; import org.cryptomator.filesystem.ReadableFile; import org.cryptomator.filesystem.jackrabbit.FileLocator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.google.common.io.ByteStreams; class DavFile extends DavNode { - private static final Logger LOG = LoggerFactory.getLogger(DavFile.class); protected static final String CONTENT_TYPE_VALUE = "application/octet-stream"; protected static final String CONTENT_DISPOSITION_HEADER = "Content-Disposition"; protected static final String CONTENT_DISPOSITION_VALUE = "attachment"; @@ -64,8 +61,8 @@ class DavFile extends DavNode { outputContext.setContentType(CONTENT_TYPE_VALUE); outputContext.setProperty(CONTENT_DISPOSITION_HEADER, CONTENT_DISPOSITION_VALUE); outputContext.setProperty(X_CONTENT_TYPE_OPTIONS_HEADER, X_CONTENT_TYPE_OPTIONS_VALUE); + outputContext.setContentLength(node.size()); try (ReadableFile src = node.openReadable(); WritableByteChannel dst = Channels.newChannel(outputContext.getOutputStream())) { - outputContext.setContentLength(src.size()); ByteStreams.copy(src, dst); } } @@ -157,12 +154,7 @@ class DavFile extends DavNode { private Optional> sizeProperty() { if (node.exists()) { - try (ReadableFile src = node.openReadable()) { - return Optional.of(new DefaultDavProperty(DavPropertyName.GETCONTENTLENGTH, src.size())); - } catch (RuntimeException e) { - LOG.warn("Could not determine file size of " + getResourcePath(), e); - return Optional.empty(); - } + return Optional.of(new DefaultDavProperty(DavPropertyName.GETCONTENTLENGTH, node.size())); } else { return Optional.empty(); } diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/DavFileWithRange.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/DavFileWithRange.java index 96a0bb547..8151b96c3 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/DavFileWithRange.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/DavFileWithRange.java @@ -47,8 +47,8 @@ class DavFileWithRange extends DavFile { if (!outputContext.hasStream()) { return; } + final long contentLength = node.size(); try (ReadableFile src = node.openReadable(); OutputStream out = outputContext.getOutputStream()) { - final long contentLength = src.size(); final Pair range = getEffectiveRange(contentLength); if (range.getLeft() < 0 || range.getLeft() > range.getRight() || range.getRight() > contentLength) { outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), "bytes */" + contentLength); diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/DavFileWithUnsatisfiableRange.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/DavFileWithUnsatisfiableRange.java index b612d1aac..1b820f90e 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/DavFileWithUnsatisfiableRange.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/jackrabbitservlet/DavFileWithUnsatisfiableRange.java @@ -39,10 +39,10 @@ class DavFileWithUnsatisfiableRange extends DavFile { if (!outputContext.hasStream()) { return; } + final long contentLength = node.size(); + outputContext.setContentLength(contentLength); + outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), "bytes */" + contentLength); try (ReadableFile src = node.openReadable(); OutputStream out = outputContext.getOutputStream()) { - final long contentLength = src.size(); - outputContext.setContentLength(contentLength); - outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), "bytes */" + contentLength); ByteStreams.copy(src, Channels.newChannel(out)); } } diff --git a/main/pom.xml b/main/pom.xml index e4ddb32c9..f208d53c3 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -27,6 +27,7 @@ UTF-8 + 1.0.0-SNAPSHOT 1.0.0-SNAPSHOT 2.1 1.7.7 @@ -124,7 +125,12 @@ ${project.version} - + + + org.cryptomator + cryptolib + ${cryptomator.cryptolib.version} + org.cryptomator jni diff --git a/main/ui/pom.xml b/main/ui/pom.xml index 885a875bc..bf6ab3fec 100644 --- a/main/ui/pom.xml +++ b/main/ui/pom.xml @@ -59,6 +59,12 @@ jni + + + org.cryptomator + cryptolib + + org.fxmisc.easybind diff --git a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java index f4cc072ae..473e213d0 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java @@ -8,6 +8,8 @@ *******************************************************************************/ package org.cryptomator.ui; +import static java.util.stream.Collectors.toList; + import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -18,11 +20,14 @@ import javax.inject.Singleton; import org.cryptomator.common.CommonsModule; import org.cryptomator.crypto.engine.impl.CryptoEngineModule; import org.cryptomator.frontend.FrontendFactory; +import org.cryptomator.frontend.FrontendId; import org.cryptomator.frontend.webdav.WebDavModule; import org.cryptomator.frontend.webdav.WebDavServer; import org.cryptomator.jni.JniModule; import org.cryptomator.jni.MacFunctions; +import org.cryptomator.ui.model.Vault; import org.cryptomator.ui.model.VaultObjectMapperProvider; +import org.cryptomator.ui.model.Vaults; import org.cryptomator.ui.settings.Settings; import org.cryptomator.ui.settings.SettingsProvider; import org.cryptomator.ui.util.DeferredCloser; @@ -34,6 +39,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import dagger.Module; import dagger.Provides; import javafx.application.Application; +import javafx.beans.Observable; import javafx.stage.Stage; @Module(includes = {CryptoEngineModule.class, CommonsModule.class, WebDavModule.class}) @@ -96,7 +102,9 @@ class CryptomatorModule { @Provides @Singleton - FrontendFactory provideFrontendFactory(DeferredCloser closer, WebDavServer webDavServer, Settings settings) { + FrontendFactory provideFrontendFactory(DeferredCloser closer, WebDavServer webDavServer, Vaults vaults, Settings settings) { + vaults.addListener((Observable o) -> setValidFrontendIds(webDavServer, vaults)); + setValidFrontendIds(webDavServer, vaults); webDavServer.setPort(settings.getPort()); webDavServer.start(); return closer.closeLater(webDavServer, WebDavServer::stop).get().orElseThrow(IllegalStateException::new); @@ -108,4 +116,9 @@ class CryptomatorModule { return JniModule.macFunctions(); } + private void setValidFrontendIds(WebDavServer webDavServer, Vaults vaults) { + webDavServer.setValidFrontendIds(vaults.stream() // + .map(Vault::getId).map(FrontendId::from).collect(toList())); + } + } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java index c9dee3248..478c6be6b 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java @@ -29,6 +29,7 @@ import org.cryptomator.ui.controls.DirectoryListCell; import org.cryptomator.ui.model.UpgradeStrategies; import org.cryptomator.ui.model.Vault; import org.cryptomator.ui.model.VaultFactory; +import org.cryptomator.ui.model.Vaults; import org.cryptomator.ui.settings.Localization; import org.cryptomator.ui.settings.Settings; import org.cryptomator.ui.util.DialogBuilderUtil; @@ -44,9 +45,6 @@ import javafx.beans.binding.BooleanBinding; import javafx.beans.binding.BooleanExpression; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; -import javafx.collections.FXCollections; -import javafx.collections.ListChangeListener.Change; -import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.geometry.Side; @@ -81,7 +79,7 @@ public class MainController extends LocalizedFXMLViewController { private final Lazy settingsController; private final Lazy upgradeStrategies; private final ObjectProperty activeController = new SimpleObjectProperty<>(); - private final ObservableList vaults; + private final Vaults vaults; private final ObjectProperty selectedVault = new SimpleObjectProperty<>(); private final BooleanExpression isSelectedVaultUnlocked = BooleanBinding.booleanExpression(EasyBind.select(selectedVault).selectObject(Vault::unlockedProperty).orElse(false)); private final BooleanExpression isSelectedVaultValid = BooleanBinding.booleanExpression(EasyBind.monadic(selectedVault).map(Vault::isValidVaultDirectory).orElse(false)); @@ -92,7 +90,8 @@ public class MainController extends LocalizedFXMLViewController { @Inject public MainController(@Named("mainWindow") Stage mainWindow, Localization localization, Settings settings, VaultFactory vaultFactoy, Lazy welcomeController, Lazy initializeController, Lazy notFoundController, Lazy upgradeController, Lazy unlockController, - Provider unlockedControllerProvider, Lazy changePasswordController, Lazy settingsController, Lazy upgradeStrategies) { + Provider unlockedControllerProvider, Lazy changePasswordController, Lazy settingsController, Lazy upgradeStrategies, + Vaults vaults) { super(localization); this.mainWindow = mainWindow; this.vaultFactoy = vaultFactoy; @@ -105,10 +104,7 @@ public class MainController extends LocalizedFXMLViewController { this.changePasswordController = changePasswordController; this.settingsController = settingsController; this.upgradeStrategies = upgradeStrategies; - this.vaults = FXCollections.observableList(settings.getDirectories()); - this.vaults.addListener((Change c) -> { - settings.save(); - }); + this.vaults = vaults; // derived bindings: this.isShowingSettings = activeController.isEqualTo(settingsController.get()); diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategies.java b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategies.java index ba4407c39..d29b45ff2 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategies.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategies.java @@ -14,8 +14,8 @@ public class UpgradeStrategies { private final Collection strategies; @Inject - public UpgradeStrategies(UpgradeVersion3DropBundleExtension upgrader1, UpgradeVersion3to4 upgrader2) { - strategies = Collections.unmodifiableList(Arrays.asList(upgrader1, upgrader2)); + public UpgradeStrategies(UpgradeVersion3DropBundleExtension upgrader1, UpgradeVersion3to4 upgrader2, UpgradeVersion4to5 upgrader3) { + strategies = Collections.unmodifiableList(Arrays.asList(upgrader1, upgrader2, upgrader3)); } public Optional getUpgradeStrategy(Vault vault) { diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategy.java b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategy.java index 6f7f147ce..9d77b93d4 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategy.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategy.java @@ -6,11 +6,11 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; -import javax.inject.Provider; - -import org.cryptomator.crypto.engine.Cryptor; -import org.cryptomator.crypto.engine.InvalidPassphraseException; -import org.cryptomator.crypto.engine.UnsupportedVaultFormatException; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.CryptorProvider; +import org.cryptomator.cryptolib.api.InvalidPassphraseException; +import org.cryptomator.cryptolib.api.KeyFile; +import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; import org.cryptomator.filesystem.crypto.Constants; import org.cryptomator.ui.settings.Localization; import org.slf4j.Logger; @@ -20,12 +20,16 @@ public abstract class UpgradeStrategy { private static final Logger LOG = LoggerFactory.getLogger(UpgradeStrategy.class); - protected final Provider cryptorProvider; + protected final CryptorProvider cryptorProvider; protected final Localization localization; + protected final int vaultVersionBeforeUpgrade; + protected final int vaultVersionAfterUpgrade; - UpgradeStrategy(Provider cryptorProvider, Localization localization) { + UpgradeStrategy(CryptorProvider cryptorProvider, Localization localization, int vaultVersionBeforeUpgrade, int vaultVersionAfterUpgrade) { this.cryptorProvider = cryptorProvider; this.localization = localization; + this.vaultVersionBeforeUpgrade = vaultVersionBeforeUpgrade; + this.vaultVersionAfterUpgrade = vaultVersionAfterUpgrade; } /** @@ -37,27 +41,29 @@ public abstract class UpgradeStrategy { * Upgrades a vault. Might take a moment, should be run in a background thread. */ public void upgrade(Vault vault, CharSequence passphrase) throws UpgradeFailedException { - final Cryptor cryptor = cryptorProvider.get(); + Cryptor cryptor = null; try { final Path masterkeyFile = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME); final byte[] masterkeyFileContents = Files.readAllBytes(masterkeyFile); - cryptor.readKeysFromMasterkeyFile(masterkeyFileContents, passphrase); + cryptor = cryptorProvider.createFromKeyFile(KeyFile.parse(masterkeyFileContents), passphrase, vaultVersionBeforeUpgrade); // create backup, as soon as we know the password was correct: final Path masterkeyBackupFile = vault.path().getValue().resolve(Constants.MASTERKEY_BACKUP_FILENAME); Files.copy(masterkeyFile, masterkeyBackupFile, StandardCopyOption.REPLACE_EXISTING); // do stuff: upgrade(vault, cryptor); // write updated masterkey file: - final byte[] upgradedMasterkeyFileContents = cryptor.writeKeysToMasterkeyFile(passphrase); - final Path masterkeyFileAfterUpgrading = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME); // path may have changed - Files.write(masterkeyFileAfterUpgrading, upgradedMasterkeyFileContents, StandardOpenOption.TRUNCATE_EXISTING); + final byte[] upgradedMasterkeyFileContents = cryptor.writeKeysToMasterkeyFile(passphrase, vaultVersionAfterUpgrade).serialize(); + final Path masterkeyFileAfterUpgrade = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME); // path may have changed + Files.write(masterkeyFileAfterUpgrade, upgradedMasterkeyFileContents, StandardOpenOption.TRUNCATE_EXISTING); } catch (InvalidPassphraseException e) { throw new UpgradeFailedException(localization.getString("unlock.errorMessage.wrongPassword")); } catch (IOException | UnsupportedVaultFormatException e) { LOG.warn("Upgrade failed.", e); throw new UpgradeFailedException("Upgrade failed. Details in log message."); } finally { - cryptor.destroy(); + if (cryptor != null) { + cryptor.destroy(); + } } } @@ -68,7 +74,21 @@ public abstract class UpgradeStrategy { * * @return true if and only if the vault can be migrated to a newer version without the risk of data losses. */ - public abstract boolean isApplicable(Vault vault); + public boolean isApplicable(Vault vault) { + final Path masterkeyFile = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME); + try { + if (Files.isRegularFile(masterkeyFile)) { + byte[] masterkeyFileContents = Files.readAllBytes(masterkeyFile); + return KeyFile.parse(masterkeyFileContents).getVersion() == vaultVersionBeforeUpgrade; + } else { + LOG.warn("Not a file: {}", masterkeyFile); + return false; + } + } catch (IOException e) { + LOG.warn("Could not determine, whether upgrade is applicable.", e); + return false; + } + } /** * Thrown when data migration failed. diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3DropBundleExtension.java b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3DropBundleExtension.java index 04fa63f30..a0462cd65 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3DropBundleExtension.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3DropBundleExtension.java @@ -1,19 +1,16 @@ package org.cryptomator.ui.model; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.security.SecureRandom; import javax.inject.Inject; -import javax.inject.Provider; import javax.inject.Singleton; import org.apache.commons.lang3.StringUtils; -import org.cryptomator.crypto.engine.Cryptor; -import org.cryptomator.crypto.engine.InvalidPassphraseException; -import org.cryptomator.crypto.engine.UnsupportedVaultFormatException; -import org.cryptomator.filesystem.crypto.Constants; +import org.cryptomator.cryptolib.Cryptors; +import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.ui.settings.Localization; import org.cryptomator.ui.settings.Settings; import org.slf4j.Logger; @@ -28,8 +25,8 @@ class UpgradeVersion3DropBundleExtension extends UpgradeStrategy { private final Settings settings; @Inject - public UpgradeVersion3DropBundleExtension(Provider cryptorProvider, Localization localization, Settings settings) { - super(cryptorProvider, localization); + public UpgradeVersion3DropBundleExtension(SecureRandom secureRandom, Localization localization, Settings settings) { + super(Cryptors.version1(secureRandom), localization, 3, 3); this.settings = settings; } @@ -42,25 +39,6 @@ class UpgradeVersion3DropBundleExtension extends UpgradeStrategy { return String.format(fmt, oldVaultName, newVaultName); } - @Override - public void upgrade(Vault vault, CharSequence passphrase) throws UpgradeFailedException { - final Cryptor cryptor = cryptorProvider.get(); - try { - final Path masterkeyFile = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME); - final byte[] masterkeyFileContents = Files.readAllBytes(masterkeyFile); - cryptor.readKeysFromMasterkeyFile(masterkeyFileContents, passphrase); - upgrade(vault, cryptor); - // don't write new masterkey. this is a special case, as we were stupid and didn't increase the vault version with this upgrade... - } catch (InvalidPassphraseException e) { - throw new UpgradeFailedException(localization.getString("unlock.errorMessage.wrongPassword")); - } catch (IOException | UnsupportedVaultFormatException e) { - LOG.warn("Upgrade failed.", e); - throw new UpgradeFailedException("Upgrade failed. Details in log message."); - } finally { - cryptor.destroy(); - } - } - @Override protected void upgrade(Vault vault, Cryptor cryptor) throws UpgradeFailedException { Path path = vault.path().getValue(); @@ -73,6 +51,7 @@ class UpgradeVersion3DropBundleExtension extends UpgradeStrategy { throw new UpgradeFailedException(msg); } else { try { + LOG.info("Renaming {} to {}", path, newPath.getFileName()); Files.move(path, path.resolveSibling(newVaultName)); Platform.runLater(() -> { vault.setPath(newPath); @@ -89,19 +68,7 @@ class UpgradeVersion3DropBundleExtension extends UpgradeStrategy { public boolean isApplicable(Vault vault) { Path vaultPath = vault.path().getValue(); if (vaultPath.toString().endsWith(Vault.VAULT_FILE_EXTENSION)) { - final Path masterkeyFile = vaultPath.resolve(Constants.MASTERKEY_FILENAME); - try { - if (Files.isRegularFile(masterkeyFile)) { - final String keyContents = new String(Files.readAllBytes(masterkeyFile), StandardCharsets.UTF_8); - return keyContents.contains("\"version\":3") || keyContents.contains("\"version\": 3"); - } else { - LOG.warn("Not a file: {}", masterkeyFile); - return false; - } - } catch (IOException e) { - LOG.warn("Could not determine, whether upgrade is applicable.", e); - return false; - } + return super.isApplicable(vault); } else { return false; } diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3to4.java b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3to4.java index 51a75b263..17bbcaac1 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3to4.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3to4.java @@ -9,19 +9,19 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.inject.Inject; -import javax.inject.Provider; import javax.inject.Singleton; import org.apache.commons.codec.binary.Base32; import org.apache.commons.codec.binary.BaseNCodec; import org.apache.commons.lang3.StringUtils; -import org.cryptomator.crypto.engine.Cryptor; -import org.cryptomator.filesystem.crypto.Constants; +import org.cryptomator.cryptolib.Cryptors; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.common.MessageDigestSupplier; import org.cryptomator.ui.settings.Localization; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,17 +40,12 @@ class UpgradeVersion3to4 extends UpgradeStrategy { private static final String OLD_FOLDER_SUFFIX = "_"; private static final String NEW_FOLDER_PREFIX = "0"; - private final MessageDigest sha1; + private final MessageDigest sha1 = MessageDigestSupplier.SHA1.get(); private final BaseNCodec base32 = new Base32(); @Inject - public UpgradeVersion3to4(Provider cryptorProvider, Localization localization) { - super(cryptorProvider, localization); - try { - sha1 = MessageDigest.getInstance("SHA-1"); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError("SHA-1 exists in every JVM"); - } + public UpgradeVersion3to4(SecureRandom secureRandom, Localization localization) { + super(Cryptors.version1(secureRandom), localization, 3, 4); } @Override @@ -144,21 +139,4 @@ class UpgradeVersion3to4 extends UpgradeStrategy { } } - @Override - public boolean isApplicable(Vault vault) { - final Path masterkeyFile = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME); - try { - if (Files.isRegularFile(masterkeyFile)) { - final String keyContents = new String(Files.readAllBytes(masterkeyFile), UTF_8); - return keyContents.contains("\"version\":3") || keyContents.contains("\"version\": 3"); - } else { - LOG.warn("Not a file: {}", masterkeyFile); - return false; - } - } catch (IOException e) { - LOG.warn("Could not determine, whether upgrade is applicable.", e); - return false; - } - } - } diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion4to5.java b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion4to5.java new file mode 100644 index 000000000..0d8389250 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion4to5.java @@ -0,0 +1,128 @@ +package org.cryptomator.ui.model; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.security.SecureRandom; +import java.util.regex.Pattern; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.cryptomator.cryptolib.Cryptors; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.FileHeader; +import org.cryptomator.ui.settings.Localization; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Contains the collective knowledge of all creatures who were alive during the development of vault format 3. + * This class uses no external classes from the crypto or shortening layer by purpose, so we don't need legacy code inside these. + */ +@Singleton +class UpgradeVersion4to5 extends UpgradeStrategy { + + private static final Logger LOG = LoggerFactory.getLogger(UpgradeVersion4to5.class); + private static final Pattern BASE32_PATTERN = Pattern.compile("^([A-Z2-7]{8})*[A-Z2-7=]{8}"); + + @Inject + public UpgradeVersion4to5(SecureRandom secureRandom, Localization localization) { + super(Cryptors.version1(secureRandom), localization, 4, 5); + } + + @Override + public String getNotification(Vault vault) { + return localization.getString("upgrade.version3to4.msg"); + } + + @Override + protected void upgrade(Vault vault, Cryptor cryptor) throws UpgradeFailedException { + Path dataDir = vault.path().get().resolve("d"); + if (!Files.isDirectory(dataDir)) { + return; // empty vault. no migration needed. + } + try { + Files.walkFileTree(dataDir, new SimpleFileVisitor() { + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (BASE32_PATTERN.matcher(file.getFileName().toString()).find() && attrs.size() > cryptor.fileHeaderCryptor().headerSize()) { + migrate(file, attrs, cryptor); + } + return FileVisitResult.CONTINUE; + } + + }); + } catch (IOException e) { + LOG.error("Migration failed.", e); + throw new UpgradeFailedException(localization.getString("upgrade.version3to4.err.io")); + } + LOG.info("Migration finished."); + } + + private void migrate(Path file, BasicFileAttributes attrs, Cryptor cryptor) throws IOException { + try (FileChannel ch = FileChannel.open(file, StandardOpenOption.READ, StandardOpenOption.WRITE)) { + // read header: + ByteBuffer headerBuf = ByteBuffer.allocate(cryptor.fileHeaderCryptor().headerSize()); + ch.read(headerBuf); + headerBuf.flip(); + FileHeader header = cryptor.fileHeaderCryptor().decryptHeader(headerBuf); + long cleartextSize = header.getFilesize(); + if (cleartextSize < 0) { + LOG.info("Skipping already migrated file {}.", file); + return; + } else if (cleartextSize > attrs.size()) { + LOG.warn("Skipping file {} with invalid file size {}/{}", file, cleartextSize, attrs.size()); + return; + } + int headerSize = cryptor.fileHeaderCryptor().headerSize(); + int ciphertextChunkSize = cryptor.fileContentCryptor().ciphertextChunkSize(); + int cleartextChunkSize = cryptor.fileContentCryptor().cleartextChunkSize(); + long newCiphertextSize = Cryptors.ciphertextSize(cleartextSize, cryptor); + long newEOF = headerSize + newCiphertextSize; + long newFullChunks = newCiphertextSize / ciphertextChunkSize; // int-truncation + long newAdditionalCiphertextBytes = newCiphertextSize % ciphertextChunkSize; + if (newAdditionalCiphertextBytes == 0) { + // (new) last block is already correct. just truncate: + LOG.info("Migrating {} of cleartext size {}: Truncating to new ciphertext size: {}", file, cleartextSize, newEOF); + ch.truncate(newEOF); + } else { + // last block may contain padding and needs to be re-encrypted: + long lastChunkIdx = newFullChunks; + LOG.info("Migrating {} of cleartext size {}: Re-encrypting chunk {}. New ciphertext size: {}", file, cleartextSize, lastChunkIdx, newEOF); + long beginOfLastChunk = headerSize + lastChunkIdx * ciphertextChunkSize; + assert beginOfLastChunk < newEOF; + int lastCleartextChunkLength = (int) (cleartextSize % cleartextChunkSize); + assert lastCleartextChunkLength < cleartextChunkSize; + assert lastCleartextChunkLength > 0; + ch.position(beginOfLastChunk); + ByteBuffer lastCiphertextChunk = ByteBuffer.allocate(ciphertextChunkSize); + int read = ch.read(lastCiphertextChunk); + if (read != -1) { + lastCiphertextChunk.flip(); + ByteBuffer lastCleartextChunk = cryptor.fileContentCryptor().decryptChunk(lastCiphertextChunk, lastChunkIdx, header, true); + lastCleartextChunk.position(0).limit(lastCleartextChunkLength); + assert lastCleartextChunk.remaining() == lastCleartextChunkLength; + ByteBuffer newLastChunkCiphertext = cryptor.fileContentCryptor().encryptChunk(lastCleartextChunk, lastChunkIdx, header); + ch.truncate(beginOfLastChunk); + ch.write(newLastChunkCiphertext); + } else { + LOG.error("Reached EOF at position {}/{}", beginOfLastChunk, newEOF); + return; // must exit method before changing header! + } + } + header.setFilesize(-1l); + ByteBuffer newHeaderBuf = cryptor.fileHeaderCryptor().encryptHeader(header); + ch.position(0); + ch.write(newHeaderBuf); + } + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Vaults.java b/main/ui/src/main/java/org/cryptomator/ui/model/Vaults.java new file mode 100644 index 000000000..9ac94d66e --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Vaults.java @@ -0,0 +1,177 @@ +package org.cryptomator.ui.model; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.cryptomator.ui.settings.Settings; + +import javafx.beans.InvalidationListener; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ListChangeListener.Change; +import javafx.collections.ObservableList; + +@Singleton +public class Vaults implements ObservableList { + + private final ObservableList delegate; + + @Inject + public Vaults(Settings settings) { + this.delegate = FXCollections.observableList(settings.getDirectories()); + addListener((Change change) -> settings.save()); + } + + public void addListener(ListChangeListener listener) { + delegate.addListener(listener); + } + + public void removeListener(ListChangeListener listener) { + delegate.removeListener(listener); + } + + public void addListener(InvalidationListener listener) { + delegate.addListener(listener); + } + + public boolean addAll(Vault... elements) { + return delegate.addAll(elements); + } + + public boolean setAll(Vault... elements) { + return delegate.setAll(elements); + } + + public boolean setAll(Collection col) { + return delegate.setAll(col); + } + + public boolean removeAll(Vault... elements) { + return delegate.removeAll(elements); + } + + public void removeListener(InvalidationListener listener) { + delegate.removeListener(listener); + } + + public boolean retainAll(Vault... elements) { + return delegate.retainAll(elements); + } + + public void remove(int from, int to) { + delegate.remove(from, to); + } + + public int size() { + return delegate.size(); + } + + public boolean isEmpty() { + return delegate.isEmpty(); + } + + public boolean contains(Object o) { + return delegate.contains(o); + } + + public Iterator iterator() { + return delegate.iterator(); + } + + public Object[] toArray() { + return delegate.toArray(); + } + + public T[] toArray(T[] a) { + return delegate.toArray(a); + } + + public boolean add(Vault e) { + return delegate.add(e); + } + + public boolean remove(Object o) { + return delegate.remove(o); + } + + public boolean containsAll(Collection c) { + return delegate.containsAll(c); + } + + public boolean addAll(Collection c) { + return delegate.addAll(c); + } + + public boolean addAll(int index, Collection c) { + return delegate.addAll(index, c); + } + + public boolean removeAll(Collection c) { + return delegate.removeAll(c); + } + + public boolean retainAll(Collection c) { + return delegate.retainAll(c); + } + + public void clear() { + delegate.clear(); + } + + public Vault get(int index) { + return delegate.get(index); + } + + public Vault set(int index, Vault element) { + return delegate.set(index, element); + } + + public void add(int index, Vault element) { + delegate.add(index, element); + } + + public Vault remove(int index) { + return delegate.remove(index); + } + + public int indexOf(Object o) { + return delegate.indexOf(o); + } + + public int lastIndexOf(Object o) { + return delegate.lastIndexOf(o); + } + + public ListIterator listIterator() { + return delegate.listIterator(); + } + + public ListIterator listIterator(int index) { + return delegate.listIterator(index); + } + + public List subList(int fromIndex, int toIndex) { + return delegate.subList(fromIndex, toIndex); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || getClass() != obj.getClass()) return false; + return internalEquals((Vaults)obj); + } + + private boolean internalEquals(Vaults other) { + return delegate.equals(other.delegate); + } + + public int hashCode() { + return delegate.hashCode(); + } + +}