diff --git a/.travis.yml b/.travis.yml index b2845a704..83a72d52d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,24 +1,30 @@ -sudo: required - -dist: trusty - language: java - +sudo: required +dist: trusty jdk: - oraclejdk8 - +cache: + directories: + - $HOME/.m2 env: global: - - secure: "Lgj042RD0X3rB8VZVZLWP1GetLhjd3PqI5JbJMlzgHJpDI6RkFIBLN9SWAGmkLPCehIp2zA5tu9+UVy0NNMxm9xz6SyjMCaxS28/fnYEXaNmwwDSF6O6gLUbdxyzoYIFPYOPmFxpzhebqnNIsxaM29oZpgRgUGqosCczQxiB+Ng=" #coveralls - secure: "IfYURwZaDWuBDvyn47n0k1Zod/IQw1FF+CS5nnV08Q+NfC3vGGJMwV8m59XnbfwnWGxwvCaAbk4qP6s6+ijgZNKkvgfFMo3rfTok5zt43bIqgaFOANYV+OC/1c59gYD6ZUxhW5iNgMgU3qdsRtJuwSmfkVv/jKyLGfAbS4kN8BA=" #coverity - secure: "lV9OwUbHMrMpLUH1CY+Z4puLDdFXytudyPlG1eGRsesdpuG6KM3uQVz6uAtf6lrU8DRbMM/T7ML+PmvQ4UoPPYLdLxESLLBat2qUPOIVBOhTSlCc7I0DmGy04CSvkeMy8dPaQC0ukgNiR7zwoNzfcpGRN/U9S8tziDruuHoZSrg=" #bintray - -before_install: "curl -L --cookie 'oraclelicense=accept-securebackup-cookie;' http://download.oracle.com/otn-pub/java/jce/8/jce_policy-8.zip -o /tmp/policy.zip && sudo unzip -j -o /tmp/policy.zip *.jar -d `jdk_switcher home oraclejdk8`/jre/lib/security && rm /tmp/policy.zip" - -script: mvn -fmain/pom.xml clean test - -after_success: mvn -fmain/pom.xml -Ptest-coverage clean test jacoco:report-aggregate coveralls:report - +addons: + coverity_scan: + project: + name: "cryptomator/cryptomator" + notification_email: sebastian.stenzel@cryptomator.org + build_command: "mvn -fmain/pom.xml clean test -DskipTests" + branch_pattern: release.* +install: +# "clean" needed until https://bugs.openjdk.java.net/browse/JDK-8067747 is resolved. +- mvn -fmain/pom.xml clean package -DskipTests dependency:go-offline -Ptest-coverage +- mvn -fmain/pom.xml clean package -DskipTests dependency:go-offline -Prelease +script: +- mvn --update-snapshots -fmain/pom.xml -Ptest-coverage clean test jacoco:report-aggregate +after_success: +- "bash <(curl -s https://codecov.io/bash)" notifications: webhooks: urls: @@ -31,17 +37,8 @@ notifications: secure: "lngJ/HEAFBbD5AdiO9avMqptKpZHdmEwOzS9FabZjkdFh7yAYueTk5RniPUvShjsKtThYm7cJ8AtDMDwc07NvPrzbMBRtUJGwuDT+7c7YFALGFJ1NYi+emkC9x1oafvmPgEYSE+tMKzNcwrHi3ytGgKdIotsKwaF35QNXYA9aMs=" on_success: change on_failure: always - -before_deploy: mvn -fmain/pom.xml -Prelease clean package -DskipTests - -addons: - coverity_scan: - project: - name: "cryptomator/cryptomator" - notification_email: sebastian.stenzel@cryptomator.org - build_command: "mvn -fmain/pom.xml clean test -DskipTests" - branch_pattern: release.* - +before_deploy: +- mvn -fmain/pom.xml -Prelease clean package -DskipTests deploy: - provider: releases prerelease: false diff --git a/README.md b/README.md index 1b79ec1bf..b587298b5 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Download native binaries of Cryptomator on [cryptomator.org](https://cryptomator ## Features -- Works with Dropbox, Google Drive, OneDrive, and any other cloud storage service that synchronizes with a local directory +- Works with Dropbox, Google Drive, OneDrive, Nextcloud and any other cloud storage service which synchronizes with a local directory - Open Source means: No backdoors, control is better than trust - Client-side: No accounts, no data shared with any online service - Totally transparent: Just work on the virtual drive as if it were a USB flash drive diff --git a/main/ant-kit/pom.xml b/main/ant-kit/pom.xml index 9745ea3df..601346301 100644 --- a/main/ant-kit/pom.xml +++ b/main/ant-kit/pom.xml @@ -8,7 +8,7 @@ org.cryptomator main - 1.1.4 + 1.2.0 ant-kit pom diff --git a/main/ant-kit/src/main/resources/build.xml b/main/ant-kit/src/main/resources/build.xml index 06dcfc257..89dd5c4bb 100644 --- a/main/ant-kit/src/main/resources/build.xml +++ b/main/ant-kit/src/main/resources/build.xml @@ -21,21 +21,6 @@ - - - - - - - - - - - - - - - @@ -45,7 +30,9 @@ - + + + @@ -66,7 +53,9 @@ - + + + diff --git a/main/commons-test/pom.xml b/main/commons-test/pom.xml index 4cfb26495..21158bbaa 100644 --- a/main/commons-test/pom.xml +++ b/main/commons-test/pom.xml @@ -10,7 +10,7 @@ org.cryptomator main - 1.1.4 + 1.2.0 commons-test Cryptomator common test dependencies diff --git a/main/commons/pom.xml b/main/commons/pom.xml index e0d30d868..152fa7313 100644 --- a/main/commons/pom.xml +++ b/main/commons/pom.xml @@ -10,7 +10,7 @@ org.cryptomator main - 1.1.4 + 1.2.0 commons Cryptomator common diff --git a/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java b/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java index eb7a9400f..1316c50d1 100644 --- a/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java +++ b/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java @@ -17,16 +17,17 @@ public final class LazyInitializer { * @return The initialized value */ public static T initializeLazily(AtomicReference reference, Supplier factory) { - final T existingInstance = reference.get(); - if (existingInstance != null) { - return existingInstance; + final T existing = reference.get(); + if (existing != null) { + return existing; } else { - final T newInstance = factory.get(); - if (reference.compareAndSet(null, newInstance)) { - return newInstance; - } else { - return reference.get(); - } + return reference.updateAndGet(currentValue -> { + if (currentValue == null) { + return factory.get(); + } else { + return currentValue; + } + }); } } diff --git a/main/commons/src/test/java/org/cryptomator/common/WeakValuedCacheTest.java b/main/commons/src/test/java/org/cryptomator/common/WeakValuedCacheTest.java index 58830356c..d9d1b009d 100644 --- a/main/commons/src/test/java/org/cryptomator/common/WeakValuedCacheTest.java +++ b/main/commons/src/test/java/org/cryptomator/common/WeakValuedCacheTest.java @@ -9,8 +9,8 @@ import static org.mockito.Mockito.when; import java.util.function.Function; -import org.cryptomator.common.WeakValuedCache; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; @@ -83,6 +83,7 @@ public class WeakValuedCacheTest { assertThat(result, is(sameInstance(theValue))); } + @Ignore @Test public void testCacheDoesNotPreventGarbageCollectionOfValues() { when(loader.apply(A_KEY)).thenAnswer(this::createValueUsingMoreThanHalfTheJvmMemory); diff --git a/main/filesystem-api/pom.xml b/main/filesystem-api/pom.xml index 1de71aa2b..3f7342a3b 100644 --- a/main/filesystem-api/pom.xml +++ b/main/filesystem-api/pom.xml @@ -9,7 +9,7 @@ org.cryptomator main - 1.1.4 + 1.2.0 filesystem-api Cryptomator filesystem: API 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-charsets/pom.xml b/main/filesystem-charsets/pom.xml index 6775acc44..e8aa48799 100644 --- a/main/filesystem-charsets/pom.xml +++ b/main/filesystem-charsets/pom.xml @@ -12,7 +12,7 @@ org.cryptomator main - 1.1.4 + 1.2.0 filesystem-charsets Cryptomator filesystem: Charset compatibility layer diff --git a/main/filesystem-crypto-integration-tests/pom.xml b/main/filesystem-crypto-integration-tests/pom.xml index fec263561..af67a1480 100644 --- a/main/filesystem-crypto-integration-tests/pom.xml +++ b/main/filesystem-crypto-integration-tests/pom.xml @@ -12,7 +12,7 @@ org.cryptomator main - 1.1.4 + 1.2.0 filesystem-crypto-integration-tests Cryptomator filesystem: Encryption layer tests 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/pom.xml b/main/filesystem-crypto/pom.xml index cff7e46ca..517566bbe 100644 --- a/main/filesystem-crypto/pom.xml +++ b/main/filesystem-crypto/pom.xml @@ -12,14 +12,14 @@ org.cryptomator main - 1.1.4 + 1.2.0 filesystem-crypto Cryptomator filesystem: Encryption layer 1.51 - 1.0.7 + 1.0.8 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..ef88c4a8d 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.equals(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..fdc12bb2a 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; @@ -56,11 +54,11 @@ class FileContentDecryptorImpl implements FileContentDecryptor { this.header = FileHeader.decrypt(headerKey, hmacSha256, header); this.authenticate = authenticate; this.chunkNumber = firstCiphertextByte / CHUNK_SIZE; // floor() by int-truncation - } - - @Override - public long contentLength() { - return header.getPayload().getFilesize(); + + // vault version 5 and onwards should have filesize: -1 + if (this.header.getPayload().getFilesize() != -1l) { + throw new UncheckedIOException(new IOException("Attempted to decrypt file with invalid header (probably from previous vault version)")); + } } @Override @@ -105,15 +103,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/crypto/engine/impl/FilenameCryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java index 75408f609..e19cb0e25 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java @@ -15,6 +15,7 @@ import java.security.NoSuchAlgorithmException; import java.util.regex.Pattern; import javax.crypto.AEADBadTagException; +import javax.crypto.IllegalBlockSizeException; import javax.crypto.SecretKey; import org.apache.commons.codec.binary.Base32; @@ -70,8 +71,8 @@ class FilenameCryptorImpl implements FilenameCryptor { try { final byte[] cleartextBytes = AES_SIV.get().decrypt(encryptionKey, macKey, encryptedBytes, associatedData); return new String(cleartextBytes, UTF_8); - } catch (AEADBadTagException e) { - throw new AuthenticationFailedException("Authentication failed.", e); + } catch (AEADBadTagException | IllegalBlockSizeException e) { + throw new AuthenticationFailedException("Invalid ciphertext.", e); } } 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/main/java/org/cryptomator/filesystem/crypto/Masterkeys.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/Masterkeys.java index 9e8578dc2..98b488836 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/Masterkeys.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/Masterkeys.java @@ -103,6 +103,7 @@ class Masterkeys { private static void writeMasterKey(File file, Cryptor cryptor, CharSequence passphrase) throws UncheckedIOException { try (WritableFile writable = file.openWritable()) { + writable.truncate(); final byte[] fileContents = cryptor.writeKeysToMasterkeyFile(passphrase); writable.write(ByteBuffer.wrap(fileContents)); } 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/FileContentDecryptorImplTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImplTest.java index 173d13348..631811da7 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImplTest.java +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImplTest.java @@ -45,7 +45,7 @@ public class FileContentDecryptorImplTest { final byte[] keyBytes = new byte[32]; final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES"); final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256"); - final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA=="); + final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg=="); final byte[] content = Base64.decode("AAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3Og="); try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) { @@ -68,7 +68,7 @@ public class FileContentDecryptorImplTest { final byte[] keyBytes = new byte[32]; final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES"); final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256"); - final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJa=="); + final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJG=="); try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) { @@ -80,7 +80,7 @@ public class FileContentDecryptorImplTest { final byte[] keyBytes = new byte[32]; final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES"); final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256"); - final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA=="); + final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg=="); final byte[] content = Base64.decode("aAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3Og="); try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) { @@ -101,7 +101,7 @@ public class FileContentDecryptorImplTest { final byte[] keyBytes = new byte[32]; final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES"); final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256"); - final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA=="); + final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg=="); final byte[] content = Base64.decode("AAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3OG="); try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, false)) { @@ -124,7 +124,7 @@ public class FileContentDecryptorImplTest { final byte[] keyBytes = new byte[32]; final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES"); final SecretKey macKey = new SecretKeySpec(keyBytes, "AES"); - final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA=="); + final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg=="); try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) { decryptor.cancelWithException(new IOException("can not do")); 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-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileHeaderTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileHeaderTest.java index 391fa88b4..bbe61a522 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileHeaderTest.java +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileHeaderTest.java @@ -53,13 +53,26 @@ public class FileHeaderTest { @Test public void testDecryption() { + final byte[] keyBytes = new byte[32]; + final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES"); + final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256"); + final ByteBuffer headerBuf = ByteBuffer.wrap(Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==")); + final FileHeader header = FileHeader.decrypt(headerKey, new ThreadLocalMac(macKey, "HmacSHA256"), headerBuf); + + Assert.assertEquals(-1l, header.getPayload().getFilesize()); + Assert.assertArrayEquals(new byte[16], header.getIv()); + Assert.assertArrayEquals(new byte[32], header.getPayload().getContentKey().getEncoded()); + } + + @Test + public void testDecryptionOfOldHeader() { final byte[] keyBytes = new byte[32]; final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES"); final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256"); final ByteBuffer headerBuf = ByteBuffer.wrap(Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==")); final FileHeader header = FileHeader.decrypt(headerKey, new ThreadLocalMac(macKey, "HmacSHA256"), headerBuf); - Assert.assertEquals(42, header.getPayload().getFilesize()); + Assert.assertEquals(42l, header.getPayload().getFilesize()); Assert.assertArrayEquals(new byte[16], header.getIv()); Assert.assertArrayEquals(new byte[32], header.getPayload().getContentKey().getEncoded()); } diff --git a/main/filesystem-inmemory/pom.xml b/main/filesystem-inmemory/pom.xml index f015a33d1..d40ac9f35 100644 --- a/main/filesystem-inmemory/pom.xml +++ b/main/filesystem-inmemory/pom.xml @@ -12,7 +12,7 @@ org.cryptomator main - 1.1.4 + 1.2.0 filesystem-inmemory Cryptomator filesystem: In-memory mock 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-invariants-tests/pom.xml b/main/filesystem-invariants-tests/pom.xml index 7fa900479..9ba7f6433 100644 --- a/main/filesystem-invariants-tests/pom.xml +++ b/main/filesystem-invariants-tests/pom.xml @@ -9,7 +9,7 @@ org.cryptomator main - 1.1.4 + 1.2.0 filesystem-invariants-tests Cryptomator filesystem: Invariants tests diff --git a/main/filesystem-nameshortening/pom.xml b/main/filesystem-nameshortening/pom.xml index e81a2b4b7..362163018 100644 --- a/main/filesystem-nameshortening/pom.xml +++ b/main/filesystem-nameshortening/pom.xml @@ -12,7 +12,7 @@ org.cryptomator main - 1.1.4 + 1.2.0 filesystem-nameshortening Cryptomator filesystem: Name shortening layer diff --git a/main/filesystem-nio/pom.xml b/main/filesystem-nio/pom.xml index ff97f717a..7df577334 100644 --- a/main/filesystem-nio/pom.xml +++ b/main/filesystem-nio/pom.xml @@ -7,7 +7,7 @@ org.cryptomator main - 1.1.4 + 1.2.0 filesystem-nio Cryptomator filesystem: NIO-based physical layer 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/filesystem-stats/pom.xml b/main/filesystem-stats/pom.xml index db78fccd0..3a7937188 100644 --- a/main/filesystem-stats/pom.xml +++ b/main/filesystem-stats/pom.xml @@ -12,7 +12,7 @@ org.cryptomator main - 1.1.4 + 1.2.0 filesystem-stats Cryptomator filesystem: Throughput statistics diff --git a/main/frontend-api/pom.xml b/main/frontend-api/pom.xml index 7eedb470e..f06a270db 100644 --- a/main/frontend-api/pom.xml +++ b/main/frontend-api/pom.xml @@ -12,7 +12,7 @@ org.cryptomator main - 1.1.4 + 1.2.0 frontend-api Cryptomator frontend: API diff --git a/main/frontend-webdav/pom.xml b/main/frontend-webdav/pom.xml index 77f9d8efa..083961c2a 100644 --- a/main/frontend-webdav/pom.xml +++ b/main/frontend-webdav/pom.xml @@ -12,7 +12,7 @@ org.cryptomator main - 1.1.4 + 1.2.0 frontend-webdav Cryptomator frontend: WebDAV frontend 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..0c40d7e0f 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 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); } 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/jacoco-report/pom.xml b/main/jacoco-report/pom.xml index cd76ed0c3..3131db4ea 100644 --- a/main/jacoco-report/pom.xml +++ b/main/jacoco-report/pom.xml @@ -5,7 +5,7 @@ org.cryptomator main - 1.1.4 + 1.2.0 jacoco-report Cryptomator Code Coverage Report diff --git a/main/keychain/pom.xml b/main/keychain/pom.xml new file mode 100644 index 000000000..2d497f4d7 --- /dev/null +++ b/main/keychain/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + org.cryptomator + main + 1.2.0 + + keychain + System Keychain Access + + + + org.apache.commons + commons-lang3 + + + com.google.code.gson + gson + 2.7 + + + commons-codec + commons-codec + + + org.bouncycastle + bcprov-jdk15on + 1.54 + + + org.cryptomator + jni + + + + + com.google.dagger + dagger + + + com.google.dagger + dagger-compiler + provided + + + + + org.cryptomator + commons-test + + + \ No newline at end of file diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccess.java new file mode 100644 index 000000000..abd7a3ee8 --- /dev/null +++ b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccess.java @@ -0,0 +1,26 @@ +package org.cryptomator.keychain; + +public interface KeychainAccess { + + /** + * Associates a passphrase with a given key. + * + * @param key Key used to retrieve the passphrase via {@link #loadPassphrase(String)}. + * @param passphrase The secret to store in this keychain. + */ + void storePassphrase(String key, CharSequence passphrase); + + /** + * @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}. + * @return The stored passphrase for the given key or null if no value for the given key could be found. + */ + char[] loadPassphrase(String key); + + /** + * Deletes a passphrase with a given key. + * + * @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}. + */ + void deletePassphrase(String key); + +} diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java new file mode 100644 index 000000000..b304d6edf --- /dev/null +++ b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java @@ -0,0 +1,10 @@ +package org.cryptomator.keychain; + +interface KeychainAccessStrategy extends KeychainAccess { + + /** + * @return true if this KeychainAccessStrategy works on the current machine. + */ + boolean isSupported(); + +} diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java new file mode 100644 index 000000000..3f2cf6abc --- /dev/null +++ b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java @@ -0,0 +1,28 @@ +package org.cryptomator.keychain; + +import java.util.Optional; +import java.util.Set; + +import org.cryptomator.jni.JniModule; + +import com.google.common.collect.Sets; + +import dagger.Module; +import dagger.Provides; +import dagger.multibindings.ElementsIntoSet; + +@Module(includes = {JniModule.class}) +public class KeychainModule { + + @Provides + @ElementsIntoSet + Set provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain) { + return Sets.newHashSet(macKeychain, winKeychain); + } + + @Provides + public Optional provideSupportedKeychain(Set keychainAccessStrategies) { + return keychainAccessStrategies.stream().filter(KeychainAccessStrategy::isSupported).map(KeychainAccess.class::cast).findFirst(); + } + +} diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java new file mode 100644 index 000000000..5e8b27b6c --- /dev/null +++ b/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java @@ -0,0 +1,44 @@ +package org.cryptomator.keychain; + +import java.util.Optional; + +import javax.inject.Inject; + +import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.jni.MacFunctions; +import org.cryptomator.jni.MacKeychainAccess; + +class MacSystemKeychainAccess implements KeychainAccessStrategy { + + private final MacKeychainAccess keychain; + + @Inject + public MacSystemKeychainAccess(Optional macFunctions) { + if (macFunctions.isPresent()) { + this.keychain = macFunctions.get().keychainAccess(); + } else { + this.keychain = null; + } + } + + @Override + public void storePassphrase(String key, CharSequence passphrase) { + keychain.storePassword(key, passphrase); + } + + @Override + public char[] loadPassphrase(String key) { + return keychain.loadPassword(key); + } + + @Override + public boolean isSupported() { + return SystemUtils.IS_OS_MAC_OSX && keychain != null; + } + + @Override + public void deletePassphrase(String key) { + keychain.deletePassword(key); + } + +} diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java new file mode 100644 index 000000000..a0ced12eb --- /dev/null +++ b/main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java @@ -0,0 +1,191 @@ +package org.cryptomator.keychain; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.lang.reflect.Type; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import javax.inject.Inject; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.jni.WinDataProtection; +import org.cryptomator.jni.WinFunctions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; + +class WindowsProtectedKeychainAccess implements KeychainAccessStrategy { + + private static final Logger LOG = LoggerFactory.getLogger(WindowsProtectedKeychainAccess.class); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting() // + .registerTypeHierarchyAdapter(byte[].class, new ByteArrayJsonAdapter()) // + .disableHtmlEscaping().create(); + + private final WinDataProtection dataProtection; + private final Path keychainPath; + private Map keychainEntries; + + @Inject + public WindowsProtectedKeychainAccess(Optional winFunctions) { + if (winFunctions.isPresent()) { + this.dataProtection = winFunctions.get().dataProtection(); + } else { + this.dataProtection = null; + } + String keychainPathProperty = System.getProperty("cryptomator.keychainPath"); + if (dataProtection != null && keychainPathProperty == null) { + LOG.warn("Windows DataProtection module loaded, but no cryptomator.keychainPath property found."); + } + if (keychainPathProperty != null) { + if (keychainPathProperty.startsWith("~/")) { + keychainPathProperty = SystemUtils.USER_HOME + keychainPathProperty.substring(1); + } + this.keychainPath = FileSystems.getDefault().getPath(keychainPathProperty); + } else { + this.keychainPath = null; + } + } + + @Override + public void storePassphrase(String key, CharSequence passphrase) { + loadKeychainEntriesIfNeeded(); + ByteBuffer buf = UTF_8.encode(CharBuffer.wrap(passphrase)); + byte[] cleartext = new byte[buf.remaining()]; + buf.get(cleartext); + KeychainEntry entry = new KeychainEntry(); + entry.salt = generateSalt(); + entry.ciphertext = dataProtection.protect(cleartext, entry.salt); + Arrays.fill(buf.array(), (byte) 0x00); + Arrays.fill(cleartext, (byte) 0x00); + keychainEntries.put(key, entry); + saveKeychainEntries(); + } + + @Override + public char[] loadPassphrase(String key) { + loadKeychainEntriesIfNeeded(); + KeychainEntry entry = keychainEntries.get(key); + if (entry == null) { + return null; + } + byte[] cleartext = dataProtection.unprotect(entry.ciphertext, entry.salt); + if (cleartext == null) { + return null; + } + CharBuffer buf = UTF_8.decode(ByteBuffer.wrap(cleartext)); + char[] passphrase = new char[buf.remaining()]; + buf.get(passphrase); + Arrays.fill(cleartext, (byte) 0x00); + Arrays.fill(buf.array(), (char) 0x00); + return passphrase; + } + + @Override + public void deletePassphrase(String key) { + loadKeychainEntriesIfNeeded(); + keychainEntries.remove(key); + saveKeychainEntries(); + } + + @Override + public boolean isSupported() { + return SystemUtils.IS_OS_WINDOWS && dataProtection != null && keychainPath != null; + } + + private byte[] generateSalt() { + byte[] result = new byte[2 * Long.BYTES]; + UUID uuid = UUID.randomUUID(); + ByteBuffer buf = ByteBuffer.wrap(result); + buf.putLong(uuid.getMostSignificantBits()); + buf.putLong(uuid.getLeastSignificantBits()); + return result; + } + + private void loadKeychainEntriesIfNeeded() { + if (keychainEntries == null) { + loadKeychainEntries(); + } + assert keychainEntries != null; + } + + private void loadKeychainEntries() { + Type type = new TypeToken>() { + }.getType(); + try (InputStream in = Files.newInputStream(keychainPath, StandardOpenOption.READ); // + Reader reader = new InputStreamReader(in, UTF_8)) { + keychainEntries = GSON.fromJson(reader, type); + } catch (JsonParseException | NoSuchFileException e) { + LOG.info("Creating new keychain at path {}", keychainPath); + } catch (IOException e) { + throw new UncheckedIOException("Could not read keychain from path " + keychainPath, e); + } + if (keychainEntries == null) { + keychainEntries = new HashMap<>(); + } + } + + private void saveKeychainEntries() { + try (OutputStream out = Files.newOutputStream(keychainPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); // + Writer writer = new OutputStreamWriter(out, UTF_8)) { + GSON.toJson(keychainEntries, writer); + } catch (IOException e) { + throw new UncheckedIOException("Could not read keychain from path " + keychainPath, e); + } + } + + private static class KeychainEntry { + @SerializedName("ciphertext") + byte[] ciphertext; + @SerializedName("salt") + byte[] salt; + } + + private static class ByteArrayJsonAdapter implements JsonSerializer, JsonDeserializer { + + private static final Base64 BASE64 = new Base64(); + + @Override + public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return BASE64.decode(json.getAsString().getBytes(StandardCharsets.UTF_8)); + } + + @Override + public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(new String(BASE64.encode(src), StandardCharsets.UTF_8)); + } + + } + +} diff --git a/main/keychain/src/test/java/org/cryptomator/keychain/KeychainModuleTest.java b/main/keychain/src/test/java/org/cryptomator/keychain/KeychainModuleTest.java new file mode 100644 index 000000000..2d8fe4e9e --- /dev/null +++ b/main/keychain/src/test/java/org/cryptomator/keychain/KeychainModuleTest.java @@ -0,0 +1,17 @@ +package org.cryptomator.keychain; + +import java.util.Optional; + +import org.junit.Assert; +import org.junit.Test; + +public class KeychainModuleTest { + + @Test + public void testGetKeychain() { + Optional keychainAccess = DaggerTestKeychainComponent.builder().jniModule(new TestJniModule()).keychainModule(new TestKeychainModule()).build().keychainAccess(); + Assert.assertTrue(keychainAccess.isPresent()); + Assert.assertTrue(keychainAccess.get() instanceof MapKeychainAccess); + } + +} diff --git a/main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java b/main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java new file mode 100644 index 000000000..8b3bb3e07 --- /dev/null +++ b/main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java @@ -0,0 +1,34 @@ +package org.cryptomator.keychain; + +import java.util.HashMap; +import java.util.Map; + +class MapKeychainAccess implements KeychainAccessStrategy { + + private final Map map = new HashMap<>(); + + @Override + public void storePassphrase(String key, CharSequence passphrase) { + char[] pw = new char[passphrase.length()]; + for (int i = 0; i < passphrase.length(); i++) { + pw[i] = passphrase.charAt(i); + } + map.put(key, pw); + } + + @Override + public char[] loadPassphrase(String key) { + return map.get(key); + } + + @Override + public void deletePassphrase(String key) { + map.remove(key); + } + + @Override + public boolean isSupported() { + return true; + } + +} diff --git a/main/keychain/src/test/java/org/cryptomator/keychain/TestJniModule.java b/main/keychain/src/test/java/org/cryptomator/keychain/TestJniModule.java new file mode 100644 index 000000000..79d770b57 --- /dev/null +++ b/main/keychain/src/test/java/org/cryptomator/keychain/TestJniModule.java @@ -0,0 +1,23 @@ +package org.cryptomator.keychain; + +import java.util.Optional; + +import org.cryptomator.jni.JniModule; +import org.cryptomator.jni.MacFunctions; +import org.cryptomator.jni.WinFunctions; + +import dagger.Lazy; + +public class TestJniModule extends JniModule { + + @Override + public Optional winFunctions(Lazy winFunction) { + return Optional.empty(); + } + + @Override + public Optional macFunctions(Lazy winFunction) { + return Optional.empty(); + } + +} diff --git a/main/keychain/src/test/java/org/cryptomator/keychain/TestKeychainComponent.java b/main/keychain/src/test/java/org/cryptomator/keychain/TestKeychainComponent.java new file mode 100644 index 000000000..30e42ee52 --- /dev/null +++ b/main/keychain/src/test/java/org/cryptomator/keychain/TestKeychainComponent.java @@ -0,0 +1,15 @@ +package org.cryptomator.keychain; + +import java.util.Optional; + +import javax.inject.Singleton; + +import dagger.Component; + +@Singleton +@Component(modules = KeychainModule.class) +interface TestKeychainComponent { + + Optional keychainAccess(); + +} diff --git a/main/keychain/src/test/java/org/cryptomator/keychain/TestKeychainModule.java b/main/keychain/src/test/java/org/cryptomator/keychain/TestKeychainModule.java new file mode 100644 index 000000000..4bb403906 --- /dev/null +++ b/main/keychain/src/test/java/org/cryptomator/keychain/TestKeychainModule.java @@ -0,0 +1,14 @@ +package org.cryptomator.keychain; + +import java.util.Set; + +import com.google.common.collect.Sets; + +public class TestKeychainModule extends KeychainModule { + + @Override + Set provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain) { + return Sets.newHashSet(new MapKeychainAccess()); + } + +} diff --git a/main/keychain/src/test/java/org/cryptomator/keychain/WindowsProtectedKeychainAccessTest.java b/main/keychain/src/test/java/org/cryptomator/keychain/WindowsProtectedKeychainAccessTest.java new file mode 100644 index 000000000..9b3fd94e8 --- /dev/null +++ b/main/keychain/src/test/java/org/cryptomator/keychain/WindowsProtectedKeychainAccessTest.java @@ -0,0 +1,60 @@ +package org.cryptomator.keychain; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; + +import org.cryptomator.jni.WinDataProtection; +import org.cryptomator.jni.WinFunctions; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; + +public class WindowsProtectedKeychainAccessTest { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private Path tmpFile; + private WindowsProtectedKeychainAccess keychain; + + @Before + public void setup() throws IOException, ReflectiveOperationException { + tmpFile = Files.createTempFile("unit-tests", ".tmp"); + System.setProperty("cryptomator.keychainPath", tmpFile.toAbsolutePath().normalize().toString()); + WinFunctions winFunctions = Mockito.mock(WinFunctions.class); + WinDataProtection winDataProtection = Mockito.mock(WinDataProtection.class); + Mockito.when(winFunctions.dataProtection()).thenReturn(winDataProtection); + Answer answerReturningFirstArg = invocation -> invocation.getArgumentAt(0, byte[].class).clone(); + Mockito.when(winDataProtection.protect(Mockito.any(), Mockito.any())).thenAnswer(answerReturningFirstArg); + Mockito.when(winDataProtection.unprotect(Mockito.any(), Mockito.any())).thenAnswer(answerReturningFirstArg); + keychain = new WindowsProtectedKeychainAccess(Optional.of(winFunctions)); + } + + @After + public void teardown() throws IOException { + Files.deleteIfExists(tmpFile); + } + + @Test + public void testStoreAndLoad() { + String storedPw1 = "topSecret"; + String storedPw2 = "bottomSecret"; + keychain.storePassphrase("myPassword", storedPw1); + keychain.storePassphrase("myOtherPassword", storedPw2); + String loadedPw1 = new String(keychain.loadPassphrase("myPassword")); + String loadedPw2 = new String(keychain.loadPassphrase("myOtherPassword")); + Assert.assertEquals(storedPw1, loadedPw1); + Assert.assertEquals(storedPw2, loadedPw2); + keychain.deletePassphrase("myPassword"); + Assert.assertNull(keychain.loadPassphrase("myPassword")); + Assert.assertNull(keychain.loadPassphrase("nonExistingPassword")); + } + +} diff --git a/main/keychain/src/test/resources/log4j2.xml b/main/keychain/src/test/resources/log4j2.xml new file mode 100644 index 000000000..39c2f8545 --- /dev/null +++ b/main/keychain/src/test/resources/log4j2.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/main/pom.xml b/main/pom.xml index 718936741..313f85004 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -1,13 +1,12 @@ - - + 4.0.0 org.cryptomator main - 1.1.4 + 1.2.0 pom Cryptomator @@ -28,6 +27,8 @@ UTF-8 + 1.0.2 + 1.0.0 2.1 1.7.7 4.12 @@ -40,9 +41,22 @@ 3.1 2.4.4 1.10.19 - 2.4 + 2.6.1 + + + ossrh-snapshots + https://oss.sonatype.org/content/repositories/snapshots + + false + + + true + + + + @@ -57,7 +71,6 @@ ${project.version} test - org.cryptomator filesystem-api @@ -106,7 +119,6 @@ filesystem-stats ${project.version} - org.cryptomator frontend-api @@ -117,12 +129,28 @@ frontend-webdav ${project.version} - + + org.cryptomator + keychain + ${project.version} + org.cryptomator ui ${project.version} + + + + org.cryptomator + cryptolib + ${cryptomator.cryptolib.version} + + + org.cryptomator + jni + ${cryptomator.jni.version} + @@ -168,19 +196,18 @@ ${commons-codec.version} - + commons-httpclient commons-httpclient ${commons-httpclient.version} - + org.fxmisc.easybind easybind 1.0.3 - + @@ -267,14 +294,15 @@ filesystem-inmemory filesystem-nio filesystem-nameshortening + filesystem-charsets filesystem-crypto filesystem-crypto-integration-tests filesystem-stats filesystem-invariants-tests frontend-api frontend-webdav + keychain ui - filesystem-charsets @@ -343,17 +371,6 @@ 1.8 - - org.eluder.coveralls - coveralls-maven-plugin - 4.0.0 - - - jacoco-report/target/site/jacoco-aggregate/jacoco.xml - - ${env.COVERALLS_REPO_TOKEN} - - diff --git a/main/uber-jar/pom.xml b/main/uber-jar/pom.xml index 43c6bb55a..dadceab2a 100644 --- a/main/uber-jar/pom.xml +++ b/main/uber-jar/pom.xml @@ -12,7 +12,7 @@ org.cryptomator main - 1.1.4 + 1.2.0 uber-jar pom diff --git a/main/ui/pom.xml b/main/ui/pom.xml index c1f1f07f8..82b2df321 100644 --- a/main/ui/pom.xml +++ b/main/ui/pom.xml @@ -12,7 +12,7 @@ org.cryptomator main - 1.1.4 + 1.2.0 ui Cryptomator GUI @@ -54,6 +54,20 @@ org.cryptomator frontend-webdav + + org.cryptomator + jni + + + org.cryptomator + keychain + + + + + org.cryptomator + cryptolib + diff --git a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java index 095caeb89..e82e006e9 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java +++ b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java @@ -8,10 +8,12 @@ *******************************************************************************/ package org.cryptomator.ui; +import java.util.Optional; import java.util.concurrent.ExecutorService; import javax.inject.Singleton; +import org.cryptomator.jni.MacFunctions; import org.cryptomator.ui.controllers.MainController; import org.cryptomator.ui.settings.Localization; import org.cryptomator.ui.util.AsyncTaskService; @@ -34,4 +36,7 @@ interface CryptomatorComponent { Localization localization(); ExitUtil exitUtil(); -} \ No newline at end of file + + Optional nativeMacFunctions(); + +} 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 b62f631c4..17fcb7ef6 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.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -16,10 +18,16 @@ import javax.inject.Singleton; import org.cryptomator.common.CommonsModule; import org.cryptomator.crypto.engine.impl.CryptoEngineModule; +import org.cryptomator.cryptolib.CryptoLibModule; 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.keychain.KeychainModule; +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; @@ -31,9 +39,10 @@ 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}) +@Module(includes = {CryptoEngineModule.class, CommonsModule.class, WebDavModule.class, KeychainModule.class, JniModule.class, CryptoLibModule.class}) class CryptomatorModule { private static final Logger LOG = LoggerFactory.getLogger(CryptomatorModule.class); @@ -93,10 +102,17 @@ 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); } + 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/ExitUtil.java b/main/ui/src/main/java/org/cryptomator/ui/ExitUtil.java index ff0cbe0dd..7352d149c 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/ExitUtil.java +++ b/main/ui/src/main/java/org/cryptomator/ui/ExitUtil.java @@ -21,6 +21,7 @@ import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.IOException; +import java.util.Optional; import java.util.concurrent.TimeUnit; import javax.inject.Inject; @@ -32,6 +33,9 @@ import javax.script.ScriptException; import javax.swing.SwingUtilities; import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.jni.JniException; +import org.cryptomator.jni.MacApplicationUiState; +import org.cryptomator.jni.MacFunctions; import org.cryptomator.ui.settings.Localization; import org.cryptomator.ui.settings.Settings; import org.slf4j.Logger; @@ -48,12 +52,14 @@ class ExitUtil { private final Stage mainWindow; private final Localization localization; private final Settings settings; + private final Optional macFunctions; @Inject - public ExitUtil(@Named("mainWindow") Stage mainWindow, Localization localization, Settings settings) { + public ExitUtil(@Named("mainWindow") Stage mainWindow, Localization localization, Settings settings, Optional macFunctions) { this.mainWindow = mainWindow; this.localization = localization; this.settings = settings; + this.macFunctions = macFunctions; } public void initExitHandler(Runnable exitCommand) { @@ -88,6 +94,7 @@ class ExitUtil { if (Platform.isImplicitExit()) { exitCommand.run(); } else { + macFunctions.map(MacFunctions::uiState).ifPresent(JniException.ignore(MacApplicationUiState::transformToAgentApplication)); mainWindow.close(); this.showTrayNotification(trayIcon); } @@ -189,6 +196,7 @@ class ExitUtil { private void restoreFromTray(ActionEvent event) { Platform.runLater(() -> { + macFunctions.map(MacFunctions::uiState).ifPresent(JniException.ignore(MacApplicationUiState::transformToForegroundApplication)); mainWindow.show(); mainWindow.requestFocus(); }); diff --git a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java index d28721ffa..11628a65c 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java +++ b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java @@ -12,9 +12,12 @@ import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.util.concurrent.ExecutionException; import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.cryptolib.common.SecureRandomModule; import org.cryptomator.ui.controllers.MainController; import org.cryptomator.ui.util.ActiveWindowStyleSupport; import org.cryptomator.ui.util.DeferredCloser; @@ -40,7 +43,15 @@ public class MainApplication extends Application { @Override public void start(Stage primaryStage) throws IOException { LOG.info("JavaFX application started"); - final CryptomatorComponent comp = DaggerCryptomatorComponent.builder().cryptomatorModule(new CryptomatorModule(this, primaryStage)).build(); + final CryptomatorComponent comp; + try { + comp = DaggerCryptomatorComponent.builder() // + .cryptomatorModule(new CryptomatorModule(this, primaryStage)) // + .secureRandomModule(new SecureRandomModule(SecureRandom.getInstanceStrong())) // + .build(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Every implementation of the Java platform is required to support at least one strong SecureRandom implementation.", e); + } final MainController mainCtrl = comp.mainController(); closer = comp.deferredCloser(); diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java index b1a76529b..b254931fb 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java @@ -12,6 +12,7 @@ package org.cryptomator.ui.controllers; import java.io.IOException; import java.io.UncheckedIOException; import java.net.URL; +import java.util.Objects; import java.util.Optional; import javax.inject.Inject; @@ -31,9 +32,7 @@ import javafx.application.Application; import javafx.application.Platform; import javafx.beans.binding.BooleanBinding; import javafx.beans.property.IntegerProperty; -import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleIntegerProperty; -import javafx.beans.property.SimpleObjectProperty; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Button; @@ -49,9 +48,9 @@ public class ChangePasswordController extends LocalizedFXMLViewController { private final Application app; private final PasswordStrengthUtil strengthRater; - final ObjectProperty vault = new SimpleObjectProperty<>(); - private Optional listener = Optional.empty(); private final IntegerProperty passwordStrength = new SimpleIntegerProperty(); // 0-4 + private Optional listener = Optional.empty(); + private Vault vault; @Inject public ChangePasswordController(Application app, PasswordStrengthUtil strengthRater, Localization localization) { @@ -101,7 +100,6 @@ public class ChangePasswordController extends LocalizedFXMLViewController { BooleanBinding oldPasswordIsEmpty = oldPasswordField.textProperty().isEmpty(); BooleanBinding newPasswordIsEmpty = newPasswordField.textProperty().isEmpty(); BooleanBinding passwordsDiffer = newPasswordField.textProperty().isNotEqualTo(retypePasswordField.textProperty()); - EasyBind.subscribe(vault, this::vaultDidChange); changePasswordButton.disableProperty().bind(oldPasswordIsEmpty.or(newPasswordIsEmpty.or(passwordsDiffer))); passwordStrength.bind(EasyBind.map(newPasswordField.textProperty(), strengthRater::computeRate)); @@ -118,13 +116,15 @@ public class ChangePasswordController extends LocalizedFXMLViewController { return getClass().getResource("/fxml/change_password.fxml"); } - private void vaultDidChange(Vault newVault) { - oldPasswordField.clear(); - newPasswordField.clear(); - retypePasswordField.clear(); + void setVault(Vault vault) { + this.vault = Objects.requireNonNull(vault); + oldPasswordField.swipe(); + newPasswordField.swipe(); + retypePasswordField.swipe(); // trigger "default" change to refresh key bindings: changePasswordButton.setDefaultButton(false); changePasswordButton.setDefaultButton(true); + messageText.setText(null); } // **************************************** @@ -144,8 +144,8 @@ public class ChangePasswordController extends LocalizedFXMLViewController { private void didClickChangePasswordButton(ActionEvent event) { downloadsPageLink.setVisible(false); try { - vault.get().changePassphrase(oldPasswordField.getCharacters(), newPasswordField.getCharacters()); - messageText.setText(localization.getString("changePassword.infoMessage.success")); + vault.changePassphrase(oldPasswordField.getCharacters(), newPasswordField.getCharacters()); + messageText.setText(null); listener.ifPresent(this::invokeListenerLater); } catch (InvalidPassphraseException e) { messageText.setText(localization.getString("changePassword.errorMessage.wrongPassword")); diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java index 2989e9eaf..d45cce37a 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java @@ -10,9 +10,10 @@ package org.cryptomator.ui.controllers; import java.io.IOException; -import java.io.UncheckedIOException; import java.net.URL; +import java.nio.file.DirectoryNotEmptyException; import java.nio.file.FileAlreadyExistsException; +import java.util.Objects; import java.util.Optional; import javax.inject.Inject; @@ -29,9 +30,7 @@ import org.slf4j.LoggerFactory; import javafx.application.Platform; import javafx.beans.binding.BooleanBinding; import javafx.beans.property.IntegerProperty; -import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleIntegerProperty; -import javafx.beans.property.SimpleObjectProperty; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Button; @@ -44,9 +43,9 @@ public class InitializeController extends LocalizedFXMLViewController { private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class); private final PasswordStrengthUtil strengthRater; - final ObjectProperty vault = new SimpleObjectProperty<>(); - private Optional listener = Optional.empty(); private final IntegerProperty passwordStrength = new SimpleIntegerProperty(); // 0-4 + private Optional listener = Optional.empty(); + private Vault vault; @Inject public InitializeController(Localization localization, PasswordStrengthUtil strengthRater) { @@ -88,7 +87,6 @@ public class InitializeController extends LocalizedFXMLViewController { public void initialize() { BooleanBinding passwordIsEmpty = passwordField.textProperty().isEmpty(); BooleanBinding passwordsDiffer = passwordField.textProperty().isNotEqualTo(retypePasswordField.textProperty()); - EasyBind.subscribe(vault, this::vaultDidChange); okButton.disableProperty().bind(passwordIsEmpty.or(passwordsDiffer)); passwordStrength.bind(EasyBind.map(passwordField.textProperty(), strengthRater::computeRate)); @@ -105,9 +103,10 @@ public class InitializeController extends LocalizedFXMLViewController { return getClass().getResource("/fxml/initialize.fxml"); } - private void vaultDidChange(Vault newVault) { - passwordField.clear(); - retypePasswordField.clear(); + void setVault(Vault vault) { + this.vault = Objects.requireNonNull(vault); + passwordField.swipe(); + retypePasswordField.swipe(); // trigger "default" change to refresh key bindings: okButton.setDefaultButton(false); okButton.setDefaultButton(true); @@ -121,11 +120,13 @@ public class InitializeController extends LocalizedFXMLViewController { protected void initializeVault(ActionEvent event) { final CharSequence passphrase = passwordField.getCharacters(); try { - vault.get().create(passphrase); + vault.create(passphrase); listener.ifPresent(this::invokeListenerLater); } catch (FileAlreadyExistsException ex) { messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized")); - } catch (UncheckedIOException | IOException ex) { + } catch (DirectoryNotEmptyException ex) { + messageLabel.setText(localization.getString("initialize.messageLabel.notEmpty")); + } catch (IOException ex) { LOG.error("I/O Exception", ex); messageLabel.setText(localization.getString("initialize.messageLabel.initializationFailed")); } finally { 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..9c9c194ba 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()); @@ -319,7 +315,8 @@ public class MainController extends LocalizedFXMLViewController { private void showInitializeView() { final InitializeController ctrl = initializeController.get(); - ctrl.vault.bind(selectedVault); + ctrl.loadFxml(); + ctrl.setVault(selectedVault.get()); ctrl.setListener(this::didInitialize); activeController.set(ctrl); } @@ -330,7 +327,8 @@ public class MainController extends LocalizedFXMLViewController { private void showUpgradeView() { final UpgradeController ctrl = upgradeController.get(); - ctrl.vault.bind(selectedVault); + ctrl.loadFxml(); + ctrl.setVault(selectedVault.get()); ctrl.setListener(this::didUpgrade); activeController.set(ctrl); } @@ -341,7 +339,8 @@ public class MainController extends LocalizedFXMLViewController { private void showUnlockView() { final UnlockController ctrl = unlockController.get(); - ctrl.vault.bind(selectedVault); + ctrl.loadFxml(); + ctrl.setVault(selectedVault.get()); ctrl.setListener(this::didUnlock); activeController.set(ctrl); } @@ -357,6 +356,7 @@ public class MainController extends LocalizedFXMLViewController { final UnlockedController ctrl = unlockedVaults.computeIfAbsent(vault, k -> { return unlockedControllerProvider.get(); }); + ctrl.loadFxml(); ctrl.setVault(vault); ctrl.setListener(this::didLock); activeController.set(ctrl); @@ -372,7 +372,8 @@ public class MainController extends LocalizedFXMLViewController { private void showChangePasswordView() { final ChangePasswordController ctrl = changePasswordController.get(); - ctrl.vault.bind(selectedVault); + ctrl.loadFxml(); + ctrl.setVault(selectedVault.get()); ctrl.setListener(this::didChangePassword); activeController.set(ctrl); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java index d17175dc7..f6b19cb8a 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java @@ -9,7 +9,9 @@ package org.cryptomator.ui.controllers; import java.net.URL; +import java.util.Arrays; import java.util.Comparator; +import java.util.Objects; import java.util.Optional; import javax.inject.Inject; @@ -22,25 +24,24 @@ import org.cryptomator.frontend.CommandFailedException; import org.cryptomator.frontend.FrontendCreationFailedException; import org.cryptomator.frontend.FrontendFactory; import org.cryptomator.frontend.webdav.mount.WindowsDriveLetters; +import org.cryptomator.keychain.KeychainAccess; import org.cryptomator.ui.controls.SecPasswordField; import org.cryptomator.ui.model.Vault; import org.cryptomator.ui.settings.Localization; import org.cryptomator.ui.settings.Settings; import org.cryptomator.ui.util.AsyncTaskService; -import org.fxmisc.easybind.EasyBind; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import dagger.Lazy; import javafx.application.Application; import javafx.application.Platform; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; import javafx.scene.control.ChoiceBox; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; @@ -61,22 +62,34 @@ public class UnlockController extends LocalizedFXMLViewController { private final Settings settings; private final WindowsDriveLetters driveLetters; private final ChangeListener driveLetterChangeListener = this::winDriveLetterDidChange; - final ObjectProperty vault = new SimpleObjectProperty<>(); + private final Optional keychainAccess; + private Vault vault; private Optional listener = Optional.empty(); @Inject - public UnlockController(Application app, Localization localization, AsyncTaskService asyncTaskService, Lazy frontendFactory, Settings settings, WindowsDriveLetters driveLetters) { + public UnlockController(Application app, Localization localization, AsyncTaskService asyncTaskService, Lazy frontendFactory, Settings settings, WindowsDriveLetters driveLetters, + Optional keychainAccess) { super(localization); this.app = app; this.asyncTaskService = asyncTaskService; this.frontendFactory = frontendFactory; this.settings = settings; this.driveLetters = driveLetters; + this.keychainAccess = keychainAccess; } @FXML private SecPasswordField passwordField; + @FXML + private Button advancedOptionsButton; + + @FXML + private Button unlockButton; + + @FXML + private CheckBox savePassword; + @FXML private TextField mountName; @@ -86,12 +99,6 @@ public class UnlockController extends LocalizedFXMLViewController { @FXML private ChoiceBox winDriveLetter; - @FXML - private Button advancedOptionsButton; - - @FXML - private Button unlockButton; - @FXML private ProgressIndicator progressIndicator; @@ -107,8 +114,10 @@ public class UnlockController extends LocalizedFXMLViewController { @Override public void initialize() { advancedOptions.managedProperty().bind(advancedOptions.visibleProperty()); + unlockButton.disableProperty().bind(passwordField.textProperty().isEmpty()); mountName.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents); mountName.textProperty().addListener(this::mountNameDidChange); + savePassword.setDisable(!keychainAccess.isPresent()); if (SystemUtils.IS_OS_WINDOWS) { winDriveLetter.setConverter(new WinDriveLetterLabelConverter()); } else { @@ -117,9 +126,6 @@ public class UnlockController extends LocalizedFXMLViewController { winDriveLetter.setVisible(false); winDriveLetter.setManaged(false); } - unlockButton.disableProperty().bind(passwordField.textProperty().isEmpty()); - - EasyBind.subscribe(vault, this::vaultDidChange); } @Override @@ -127,11 +133,15 @@ public class UnlockController extends LocalizedFXMLViewController { return getClass().getResource("/fxml/unlock.fxml"); } - private void vaultDidChange(Vault newVault) { - if (newVault == null) { + void setVault(Vault vault) { + // trigger "default" change to refresh key bindings: + unlockButton.setDefaultButton(false); + unlockButton.setDefaultButton(true); + if (vault.equals(this.vault)) { return; } - passwordField.clear(); + this.vault = Objects.requireNonNull(vault); + passwordField.swipe(); advancedOptions.setVisible(false); advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.show")); progressIndicator.setVisible(false); @@ -145,13 +155,21 @@ public class UnlockController extends LocalizedFXMLViewController { } downloadsPageLink.setVisible(false); messageText.setText(null); - mountName.setText(newVault.getMountName()); + mountName.setText(vault.getMountName()); if (SystemUtils.IS_OS_WINDOWS) { chooseSelectedDriveLetter(); } - // trigger "default" change to refresh key bindings: - unlockButton.setDefaultButton(false); - unlockButton.setDefaultButton(true); + savePassword.setSelected(false); + // auto-fill pw from keychain: + if (keychainAccess.isPresent()) { + char[] storedPw = keychainAccess.get().loadPassphrase(vault.getId()); + if (storedPw != null) { + savePassword.setSelected(true); + passwordField.setText(new String(storedPw)); + passwordField.selectRange(storedPw.length, storedPw.length); + Arrays.fill(storedPw, ' '); + } + } } // **************************************** @@ -160,7 +178,7 @@ public class UnlockController extends LocalizedFXMLViewController { @FXML public void didClickDownloadsLink(ActionEvent event) { - app.getHostServices().showDocument("https://cryptomator.org/downloads/"); + app.getHostServices().showDocument("https://cryptomator.org/downloads/#allVersions"); } // **************************************** @@ -188,14 +206,11 @@ public class UnlockController extends LocalizedFXMLViewController { } private void mountNameDidChange(ObservableValue property, String oldValue, String newValue) { - if (vault.get() == null) { - return; - } // newValue is guaranteed to be a-z0-9_, see #filterAlphanumericKeyEvents if (newValue.isEmpty()) { - mountName.setText(vault.get().getMountName()); + mountName.setText(vault.getMountName()); } else { - vault.get().setMountName(newValue); + vault.setMountName(newValue); } } @@ -242,20 +257,17 @@ public class UnlockController extends LocalizedFXMLViewController { } private void winDriveLetterDidChange(ObservableValue property, Character oldValue, Character newValue) { - if (vault.get() == null) { - return; - } - vault.get().setWinDriveLetter(newValue); + vault.setWinDriveLetter(newValue); settings.save(); } private void chooseSelectedDriveLetter() { assert SystemUtils.IS_OS_WINDOWS; // if the vault prefers a drive letter, that is currently occupied, this is our last chance to reset this: - if (driveLetters.getOccupiedDriveLetters().contains(vault.get().getWinDriveLetter())) { - vault.get().setWinDriveLetter(null); + if (driveLetters.getOccupiedDriveLetters().contains(vault.getWinDriveLetter())) { + vault.setWinDriveLetter(null); } - final Character letter = vault.get().getWinDriveLetter(); + final Character letter = vault.getWinDriveLetter(); if (letter == null) { // first option is known to be 'auto-assign' due to #WinDriveLetterComparator. this.winDriveLetter.getSelectionModel().selectFirst(); @@ -275,10 +287,10 @@ public class UnlockController extends LocalizedFXMLViewController { progressIndicator.setVisible(true); downloadsPageLink.setVisible(false); CharSequence password = passwordField.getCharacters(); - asyncTaskService.asyncTaskOf(() -> this.unlock(vault.get(), password)).run(); + asyncTaskService.asyncTaskOf(() -> this.unlock(password)).run(); } - private void unlock(Vault vault, CharSequence password) { + private void unlock(CharSequence password) { try { vault.activateFrontend(frontendFactory.get(), settings, password); vault.reveal(); @@ -286,18 +298,28 @@ public class UnlockController extends LocalizedFXMLViewController { messageText.setText(null); listener.ifPresent(lstnr -> lstnr.didUnlock(vault)); }); + if (keychainAccess.isPresent() && savePassword.isSelected()) { + keychainAccess.get().storePassphrase(vault.getId(), password); + } else { + passwordField.swipe(); + } } catch (InvalidPassphraseException e) { Platform.runLater(() -> { messageText.setText(localization.getString("unlock.errorMessage.wrongPassword")); + passwordField.selectAll(); passwordField.requestFocus(); }); } catch (UnsupportedVaultFormatException e) { Platform.runLater(() -> { - downloadsPageLink.setVisible(true); if (e.isVaultOlderThanSoftware()) { + // whitespace after localized text used as separator between text and hyperlink messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " "); + downloadsPageLink.setVisible(true); } else if (e.isSoftwareOlderThanVault()) { messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " "); + downloadsPageLink.setVisible(true); + } else if (e.getDetectedVersion() == Integer.MAX_VALUE) { + messageText.setText(localization.getString("unlock.errorMessage.unauthenticVersionMac")); } }); } catch (FrontendCreationFailedException | CommandFailedException e) { @@ -307,7 +329,6 @@ public class UnlockController extends LocalizedFXMLViewController { }); } finally { Platform.runLater(() -> { - passwordField.swipe(); mountName.setDisable(false); advancedOptionsButton.setDisable(false); progressIndicator.setVisible(false); diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java index 729f3fdf9..8e6a4e97f 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java @@ -15,21 +15,23 @@ import org.cryptomator.ui.settings.Localization; import org.cryptomator.ui.util.AsyncTaskService; import org.fxmisc.easybind.EasyBind; +import javafx.beans.binding.BooleanExpression; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; import javafx.scene.control.Label; import javafx.scene.control.ProgressIndicator; public class UpgradeController extends LocalizedFXMLViewController { - final ObjectProperty vault = new SimpleObjectProperty<>(); - final ObjectProperty> strategy = new SimpleObjectProperty<>(); + private final ObjectProperty> strategy = new SimpleObjectProperty<>(); private final UpgradeStrategies strategies; private final AsyncTaskService asyncTaskService; private Optional listener = Optional.empty(); + private Vault vault; @Inject public UpgradeController(Localization localization, UpgradeStrategies strategies, AsyncTaskService asyncTaskService) { @@ -44,6 +46,9 @@ public class UpgradeController extends LocalizedFXMLViewController { @FXML private SecPasswordField passwordField; + @FXML + private CheckBox confirmationCheckbox; + @FXML private Button upgradeButton; @@ -59,9 +64,9 @@ public class UpgradeController extends LocalizedFXMLViewController { return instruction.map(this::upgradeNotification).orElse(""); }).orElse("")); - upgradeButton.disableProperty().bind(passwordField.textProperty().isEmpty().or(passwordField.disabledProperty())); - - EasyBind.subscribe(vault, this::vaultDidChange); + BooleanExpression passwordProvided = passwordField.textProperty().isNotEmpty().and(passwordField.disabledProperty().not()); + BooleanExpression syncFinished = confirmationCheckbox.selectedProperty(); + upgradeButton.disableProperty().bind(passwordProvided.not().or(syncFinished.not())); } @Override @@ -69,9 +74,10 @@ public class UpgradeController extends LocalizedFXMLViewController { return getClass().getResource("/fxml/upgrade.fxml"); } - private void vaultDidChange(Vault newVault) { + void setVault(Vault vault) { + this.vault = Objects.requireNonNull(vault); errorLabel.setText(null); - strategy.set(strategies.getUpgradeStrategy(newVault)); + strategy.set(strategies.getUpgradeStrategy(vault)); // trigger "default" change to refresh key bindings: upgradeButton.setDefaultButton(false); upgradeButton.setDefaultButton(true); @@ -82,7 +88,7 @@ public class UpgradeController extends LocalizedFXMLViewController { // **************************************** private String upgradeNotification(UpgradeStrategy instruction) { - return instruction.getNotification(vault.get()); + return instruction.getNotification(vault); } // **************************************** @@ -95,15 +101,14 @@ public class UpgradeController extends LocalizedFXMLViewController { } private void upgrade(UpgradeStrategy instruction) { - Vault v = Objects.requireNonNull(vault.getValue()); passwordField.setDisable(true); progressIndicator.setVisible(true); asyncTaskService // .asyncTaskOf(() -> { - if (!instruction.isApplicable(v)) { - throw new IllegalStateException("No ugprade needed for " + v.path().getValue()); + if (!instruction.isApplicable(vault)) { + throw new IllegalStateException("No ugprade needed for " + vault.path().getValue()); } - instruction.upgrade(v, passwordField.getCharacters()); + instruction.upgrade(vault, passwordField.getCharacters()); }) // .onSuccess(this::showNextUpgrade) // .onError(UpgradeFailedException.class, e -> { @@ -118,7 +123,7 @@ public class UpgradeController extends LocalizedFXMLViewController { private void showNextUpgrade() { errorLabel.setText(null); - Optional nextStrategy = strategies.getUpgradeStrategy(vault.getValue()); + Optional nextStrategy = strategies.getUpgradeStrategy(vault); if (nextStrategy.isPresent()) { strategy.set(nextStrategy); } else { diff --git a/main/ui/src/main/java/org/cryptomator/ui/logging/ConfigurableFileAppender.java b/main/ui/src/main/java/org/cryptomator/ui/logging/ConfigurableFileAppender.java index 7354fb544..9074f9a87 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/logging/ConfigurableFileAppender.java +++ b/main/ui/src/main/java/org/cryptomator/ui/logging/ConfigurableFileAppender.java @@ -19,6 +19,7 @@ import java.util.regex.Pattern; import org.apache.commons.lang3.SystemUtils; import org.apache.logging.log4j.core.Filter; import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.appender.AbstractAppender; import org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender; import org.apache.logging.log4j.core.appender.FileManager; import org.apache.logging.log4j.core.config.plugins.Plugin; @@ -26,10 +27,11 @@ import org.apache.logging.log4j.core.config.plugins.PluginAttribute; import org.apache.logging.log4j.core.config.plugins.PluginElement; import org.apache.logging.log4j.core.config.plugins.PluginFactory; import org.apache.logging.log4j.core.layout.PatternLayout; +import org.apache.logging.log4j.core.util.Booleans; import org.apache.logging.log4j.util.Strings; /** - * A preconfigured FileAppender only relying on a configurable system property, e.g. -DlogPath=/var/log/cryptomator.log.
+ * A preconfigured FileAppender only relying on a configurable system property, e.g. -Dcryptomator.logPath=/var/log/cryptomator.log.
* Other than the normal {@link org.apache.logging.log4j.core.appender.FileAppender} paths can be resolved relative to the users home directory. */ @Plugin(name = "ConfigurableFile", category = "Core", elementType = "appender", printObject = true) @@ -37,7 +39,6 @@ public class ConfigurableFileAppender extends AbstractOutputStreamAppender layout, Filter filter, FileManager manager) { @@ -46,9 +47,8 @@ public class ConfigurableFileAppender extends AbstractOutputStreamAppender layout) { - if (name == null) { LOGGER.error("No name provided for HomeDirectoryAwareFileAppender"); return null; @@ -59,41 +59,16 @@ public class ConfigurableFileAppender extends AbstractOutputStreamAppender 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 4f67ca3f2..8f80f693b 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,37 @@ 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) { + } catch (UnsupportedVaultFormatException e) { + if (e.getDetectedVersion() == Integer.MAX_VALUE) { + LOG.warn("Version MAC authentication error in vault {}", vault.path().get()); + throw new UpgradeFailedException(localization.getString("unlock.errorMessage.unauthenticVersionMac")); + } else { + LOG.warn("Upgrade failed.", e); + throw new UpgradeFailedException("Upgrade failed. Details in log message."); + } + } catch (IOException 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 +82,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..873c69716 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,17 @@ 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 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.api.CryptoLibVersion; +import org.cryptomator.cryptolib.api.CryptoLibVersion.Version; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.CryptorProvider; import org.cryptomator.ui.settings.Localization; import org.cryptomator.ui.settings.Settings; import org.slf4j.Logger; @@ -28,8 +26,8 @@ class UpgradeVersion3DropBundleExtension extends UpgradeStrategy { private final Settings settings; @Inject - public UpgradeVersion3DropBundleExtension(Provider cryptorProvider, Localization localization, Settings settings) { - super(cryptorProvider, localization); + public UpgradeVersion3DropBundleExtension(@CryptoLibVersion(Version.ONE) CryptorProvider version1CryptorProvider, Localization localization, Settings settings) { + super(version1CryptorProvider, localization, 3, 3); this.settings = settings; } @@ -42,25 +40,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 +52,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 +69,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..49b5c13a3 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,20 @@ 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.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.api.CryptoLibVersion; +import org.cryptomator.cryptolib.api.CryptoLibVersion.Version; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.CryptorProvider; +import org.cryptomator.cryptolib.common.MessageDigestSupplier; import org.cryptomator.ui.settings.Localization; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,17 +41,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(@CryptoLibVersion(Version.ONE) CryptorProvider version1CryptorProvider, Localization localization) { + super(version1CryptorProvider, localization, 3, 4); } @Override @@ -144,21 +140,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..487f98b85 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion4to5.java @@ -0,0 +1,130 @@ +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.util.regex.Pattern; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.cryptomator.cryptolib.Cryptors; +import org.cryptomator.cryptolib.api.CryptoLibVersion; +import org.cryptomator.cryptolib.api.CryptoLibVersion.Version; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.CryptorProvider; +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(@CryptoLibVersion(Version.ONE) CryptorProvider version1CryptorProvider, Localization localization) { + super(version1CryptorProvider, 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/Vault.java b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java index 07c83530d..086fffa90 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java @@ -12,6 +12,7 @@ import static org.apache.commons.lang3.StringUtils.stripStart; import java.io.IOException; import java.io.UncheckedIOException; +import java.nio.file.DirectoryNotEmptyException; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; @@ -30,6 +31,7 @@ import org.apache.commons.lang3.SystemUtils; import org.cryptomator.common.LazyInitializer; import org.cryptomator.common.Optionals; import org.cryptomator.crypto.engine.InvalidPassphraseException; +import org.cryptomator.filesystem.File; import org.cryptomator.filesystem.FileSystem; import org.cryptomator.filesystem.charsets.NormalizedNameFileSystem; import org.cryptomator.filesystem.crypto.CryptoFileSystemDelegate; @@ -109,8 +111,10 @@ public class Vault implements CryptoFileSystemDelegate { public void create(CharSequence passphrase) throws IOException { try { FileSystem fs = getNioFileSystem(); - if (fs.children().count() > 0) { - throw new FileAlreadyExistsException(null, null, "Vault location not empty."); + if (fs.files().map(File::name).filter(s -> s.endsWith(VAULT_FILE_EXTENSION)).count() > 0) { + throw new FileAlreadyExistsException("masterkey.cryptomator", null, "Vault location not empty."); + } else if (fs.folders().count() > 0) { + throw new DirectoryNotEmptyException(fs.toString()); } cryptoFileSystemFactory.initializeNew(fs, passphrase); } catch (UncheckedIOException e) { @@ -164,7 +168,7 @@ public class Vault implements CryptoFileSystemDelegate { ); } - public void reveal() throws CommandFailedException { + public synchronized void reveal() throws CommandFailedException { Optionals.ifPresent(filesystemFrontend.get(), Frontend::reveal); } @@ -186,7 +190,7 @@ public class Vault implements CryptoFileSystemDelegate { // Getter/Setter // *******************************************************************************/ - public String getWebDavUrl() { + public synchronized String getWebDavUrl() { return filesystemFrontend.get().map(Frontend::getWebDavUrl).orElseThrow(IllegalStateException::new); } 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(); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/settings/Localization.java b/main/ui/src/main/java/org/cryptomator/ui/settings/Localization.java index 1c2fbf8d5..c13000625 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/settings/Localization.java +++ b/main/ui/src/main/java/org/cryptomator/ui/settings/Localization.java @@ -39,7 +39,7 @@ public class Localization extends ResourceBundle { Objects.requireNonNull(in); Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8); this.fallback = new PropertyResourceBundle(reader); - LOG.info("Loaded localization from {}", LOCALIZATION_FILE); + LOG.info("Loaded localization from bundle:{}", LOCALIZATION_FILE); } catch (IOException e) { throw new UncheckedIOException(e); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/settings/SettingsProvider.java b/main/ui/src/main/java/org/cryptomator/ui/settings/SettingsProvider.java index 417b6c369..5617815d3 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/settings/SettingsProvider.java +++ b/main/ui/src/main/java/org/cryptomator/ui/settings/SettingsProvider.java @@ -17,6 +17,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -28,6 +29,7 @@ import javax.inject.Named; import javax.inject.Provider; import javax.inject.Singleton; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,23 +40,17 @@ import com.fasterxml.jackson.databind.ObjectMapper; public class SettingsProvider implements Provider { private static final Logger LOG = LoggerFactory.getLogger(SettingsProvider.class); - private static final Path SETTINGS_DIR; - private static final String SETTINGS_FILE = "settings.json"; + private static final Path DEFAULT_SETTINGS_PATH; private static final long SAVE_DELAY_MS = 1000; static { - final String appdata = System.getenv("APPDATA"); final FileSystem fs = FileSystems.getDefault(); - - if (SystemUtils.IS_OS_WINDOWS && appdata != null) { - SETTINGS_DIR = fs.getPath(appdata, "Cryptomator"); - } else if (SystemUtils.IS_OS_WINDOWS && appdata == null) { - SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".Cryptomator"); + if (SystemUtils.IS_OS_WINDOWS) { + DEFAULT_SETTINGS_PATH = fs.getPath(SystemUtils.USER_HOME, "AppData/Roaming/Cryptomator/settings.json"); } else if (SystemUtils.IS_OS_MAC_OSX) { - SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, "Library/Application Support/Cryptomator"); + DEFAULT_SETTINGS_PATH = fs.getPath(SystemUtils.USER_HOME, "Library/Application Support/Cryptomator/settings.json"); } else { - // (os.contains("solaris") || os.contains("sunos") || os.contains("linux") || os.contains("unix")) - SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".Cryptomator"); + DEFAULT_SETTINGS_PATH = fs.getPath(SystemUtils.USER_HOME, ".Cryptomator/settings.json"); } } @@ -68,11 +64,15 @@ public class SettingsProvider implements Provider { } private Path getSettingsPath() throws IOException { - String settingsPathProperty = System.getProperty("cryptomator.settingsPath"); - if (settingsPathProperty == null) { - return SETTINGS_DIR.resolve(SETTINGS_FILE); + final String settingsPathProperty = System.getProperty("cryptomator.settingsPath"); + return Optional.ofNullable(settingsPathProperty).filter(StringUtils::isNotBlank).map(this::replaceHomeDir).map(FileSystems.getDefault()::getPath).orElse(DEFAULT_SETTINGS_PATH); + } + + private String replaceHomeDir(String path) { + if (path.startsWith("~/")) { + return SystemUtils.USER_HOME + path.substring(1); } else { - return FileSystems.getDefault().getPath(settingsPathProperty); + return path; } } @@ -96,7 +96,7 @@ public class SettingsProvider implements Provider { } ScheduledFuture saveCmd = saveScheduler.schedule(() -> { this.save(settings); - } , SAVE_DELAY_MS, TimeUnit.MILLISECONDS); + }, SAVE_DELAY_MS, TimeUnit.MILLISECONDS); ScheduledFuture previousSaveCmd = scheduledSaveCmd.getAndSet(saveCmd); if (previousSaveCmd != null) { previousSaveCmd.cancel(false); diff --git a/main/ui/src/main/resources/css/linux_theme.css b/main/ui/src/main/resources/css/linux_theme.css index 38f453feb..f3b2f3576 100644 --- a/main/ui/src/main/resources/css/linux_theme.css +++ b/main/ui/src/main/resources/css/linux_theme.css @@ -305,17 +305,17 @@ ****************************************************************************/ .check-box { - -fx-label-padding: 0 0 0 3; + -fx-label-padding: 0 0 0 6px; -fx-text-fill: COLOR_TEXT; } .check-box > .box { - -fx-padding: 3; + -fx-padding: 3px; -fx-background-color: COLOR_BORDER_DARK, #FFF; -fx-background-insets: 0, 1; } .check-box > .box > .mark { -fx-background-color: transparent; - -fx-padding: 4; + -fx-padding: 4px; -fx-shape: "M-1,4, L-1,5.5 L3.5,8.5 L9,0 L9,-1 L7,-1 L3,6 L1,4 Z"; } .check-box:selected > .box { diff --git a/main/ui/src/main/resources/css/mac_theme.css b/main/ui/src/main/resources/css/mac_theme.css index bb5fa2053..ffd4b637e 100644 --- a/main/ui/src/main/resources/css/mac_theme.css +++ b/main/ui/src/main/resources/css/mac_theme.css @@ -386,7 +386,7 @@ ******************************************************************************/ .check-box { - -fx-label-padding: 0 0 0 3px; + -fx-label-padding: 0 0 0 6px; -fx-text-fill: COLOR_TEXT; } .check-box > .box { diff --git a/main/ui/src/main/resources/css/win_theme.css b/main/ui/src/main/resources/css/win_theme.css index 72fa8cb53..0c0c29c84 100644 --- a/main/ui/src/main/resources/css/win_theme.css +++ b/main/ui/src/main/resources/css/win_theme.css @@ -354,7 +354,7 @@ ******************************************************************************/ .check-box { - -fx-label-padding: 0 0 0 3px; + -fx-label-padding: 0 0 0 6px; -fx-text-fill: COLOR_TEXT; } .check-box > .box { diff --git a/main/ui/src/main/resources/fxml/unlock.fxml b/main/ui/src/main/resources/fxml/unlock.fxml index 851d33fc9..0a8ea2fb8 100644 --- a/main/ui/src/main/resources/fxml/unlock.fxml +++ b/main/ui/src/main/resources/fxml/unlock.fxml @@ -68,12 +68,16 @@ -