diff --git a/main/buildkit/pom.xml b/main/buildkit/pom.xml index d9061057a..66fb43c8a 100644 --- a/main/buildkit/pom.xml +++ b/main/buildkit/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.5.4 + 1.5.5 buildkit pom diff --git a/main/commons/pom.xml b/main/commons/pom.xml index f8fbd8640..d7ce3c1a2 100644 --- a/main/commons/pom.xml +++ b/main/commons/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.5.4 + 1.5.5 commons Cryptomator Commons diff --git a/main/keychain/pom.xml b/main/keychain/pom.xml index ed75445f4..591a17b26 100644 --- a/main/keychain/pom.xml +++ b/main/keychain/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.5.4 + 1.5.5 keychain System Keychain Access @@ -15,16 +15,27 @@ commons + + + org.openjfx + javafx-base + + + org.openjfx + javafx-graphics + + + org.apache.commons commons-lang3 + + com.google.code.gson gson - - com.google.guava guava diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccess.java deleted file mode 100644 index 563794dc0..000000000 --- a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccess.java +++ /dev/null @@ -1,38 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the accompanying LICENSE file. - *******************************************************************************/ -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) throws KeychainAccessException; - - /** - * @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) throws KeychainAccessException; - - /** - * 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) throws KeychainAccessException; - - /** - * Updates a passphrase with a given key. - * - * @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}. - * @param passphrase The secret to be updated in this keychain. - */ - void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException; -} diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java index a248d08f5..abd50287e 100644 --- a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java +++ b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java @@ -5,7 +5,36 @@ *******************************************************************************/ package org.cryptomator.keychain; -interface KeychainAccessStrategy extends KeychainAccess { +interface KeychainAccessStrategy { + + /** + * 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) throws KeychainAccessException; + + /** + * @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) throws KeychainAccessException; + + /** + * 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) throws KeychainAccessException; + + /** + * Updates a passphrase with a given key. Noop, if there is no item for the given key. + * + * @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}. + * @param passphrase The secret to be updated in this keychain. + */ + void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException; /** * @return true if this KeychainAccessStrategy works on the current machine. diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainManager.java b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainManager.java new file mode 100644 index 000000000..300b5b035 --- /dev/null +++ b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainManager.java @@ -0,0 +1,117 @@ +package org.cryptomator.keychain; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import javafx.application.Platform; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; + +public class KeychainManager implements KeychainAccessStrategy { + + private static final Logger LOG = LoggerFactory.getLogger(KeychainManager.class); + + private final KeychainAccessStrategy keychain; + private LoadingCache passphraseStoredProperties; + + KeychainManager(KeychainAccessStrategy keychain) { + assert keychain.isSupported(); + this.keychain = keychain; + this.passphraseStoredProperties = CacheBuilder.newBuilder() // + .weakValues() // + .build(CacheLoader.from(this::createStoredPassphraseProperty)); + } + + @Override + public void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException { + keychain.storePassphrase(key, passphrase); + setPassphraseStored(key, true); + } + + @Override + public char[] loadPassphrase(String key) throws KeychainAccessException { + char[] passphrase = keychain.loadPassphrase(key); + setPassphraseStored(key, passphrase != null); + return passphrase; + } + + @Override + public void deletePassphrase(String key) throws KeychainAccessException { + keychain.deletePassphrase(key); + setPassphraseStored(key, false); + } + + @Override + public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException { + keychain.changePassphrase(key, passphrase); + setPassphraseStored(key, true); + } + + @Override + public boolean isSupported() { + return true; + } + + /** + * Checks if the keychain knows a passphrase for the given key. + *

+ * Expensive operation. If possible, use {@link #getPassphraseStoredProperty(String)} instead. + * + * @param key The key to look up + * @return true if a password for key is stored. + * @throws KeychainAccessException + */ + public boolean isPassphraseStored(String key) throws KeychainAccessException { + char[] storedPw = null; + try { + storedPw = keychain.loadPassphrase(key); + return storedPw != null; + } finally { + if (storedPw != null) { + Arrays.fill(storedPw, ' '); + } + } + } + + private void setPassphraseStored(String key, boolean value) { + BooleanProperty property = passphraseStoredProperties.getIfPresent(key); + if (property != null) { + if (Platform.isFxApplicationThread()) { + property.set(value); + } else { + LOG.warn(""); + Platform.runLater(() -> property.set(value)); + } + } + } + + /** + * Returns an observable property for use in the UI that tells whether a passphrase is stored for the given key. + *

+ * Assuming that this process is the only process modifying Cryptomator-related items in the system keychain, this + * property stays in memory in an attempt to avoid unnecessary calls to the system keychain. Note that due to this + * fact the value stored in the returned property is not 100% reliable. Code defensively! + * + * @param key The key to look up + * @return An observable property which is true when it almost certain that a password for key is stored. + * @see #isPassphraseStored(String) + */ + public ReadOnlyBooleanProperty getPassphraseStoredProperty(String key) { + return passphraseStoredProperties.getUnchecked(key); + } + + private BooleanProperty createStoredPassphraseProperty(String key) { + try { + LOG.warn("LOAD"); // TODO remove + return new SimpleBooleanProperty(isPassphraseStored(key)); + } catch (KeychainAccessException e) { + return new SimpleBooleanProperty(false); + } + } + +} diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java index d36c8e4b5..1db94fec6 100644 --- a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java +++ b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java @@ -5,10 +5,10 @@ *******************************************************************************/ package org.cryptomator.keychain; -import com.google.common.collect.Sets; +import dagger.Binds; import dagger.Module; import dagger.Provides; -import dagger.multibindings.ElementsIntoSet; +import dagger.multibindings.IntoSet; import org.cryptomator.common.JniModule; import javax.inject.Singleton; @@ -16,18 +16,30 @@ import java.util.Optional; import java.util.Set; @Module(includes = {JniModule.class}) -public class KeychainModule { +public abstract class KeychainModule { - @Provides - @ElementsIntoSet - Set provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain, LinuxSecretServiceKeychainAccess linKeychain) { - return Sets.newHashSet(macKeychain, winKeychain, linKeychain); - } + @Binds + @IntoSet + abstract KeychainAccessStrategy bindMacSystemKeychainAccess(MacSystemKeychainAccess keychainAccessStrategy); + + @Binds + @IntoSet + abstract KeychainAccessStrategy bindWindowsProtectedKeychainAccess(WindowsProtectedKeychainAccess keychainAccessStrategy); + + @Binds + @IntoSet + abstract KeychainAccessStrategy bindLinuxSecretServiceKeychainAccess(LinuxSecretServiceKeychainAccess keychainAccessStrategy); @Provides @Singleton - public Optional provideSupportedKeychain(Set keychainAccessStrategies) { - return keychainAccessStrategies.stream().filter(KeychainAccessStrategy::isSupported).map(KeychainAccess.class::cast).findFirst(); + static Optional provideSupportedKeychain(Set keychainAccessStrategies) { + return keychainAccessStrategies.stream().filter(KeychainAccessStrategy::isSupported).findFirst(); + } + + @Provides + @Singleton + public static Optional provideKeychainManager(Optional keychainAccess) { + return keychainAccess.map(KeychainManager::new); } } diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/LinuxSecretServiceKeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/LinuxSecretServiceKeychainAccess.java index 7cf0b5e7b..f11bdbd45 100644 --- a/main/keychain/src/main/java/org/cryptomator/keychain/LinuxSecretServiceKeychainAccess.java +++ b/main/keychain/src/main/java/org/cryptomator/keychain/LinuxSecretServiceKeychainAccess.java @@ -3,11 +3,13 @@ package org.cryptomator.keychain; import org.apache.commons.lang3.SystemUtils; import javax.inject.Inject; +import javax.inject.Singleton; import java.util.Optional; /** * A facade to LinuxSecretServiceKeychainAccessImpl that doesn't depend on libraries that are unavailable on Mac and Windows. */ +@Singleton public class LinuxSecretServiceKeychainAccess implements KeychainAccessStrategy { // the actual implementation is hidden in this delegate object which is loaded via reflection, diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java index 382ec0aff..0021cc13d 100644 --- a/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java +++ b/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java @@ -8,11 +8,13 @@ package org.cryptomator.keychain; import java.util.Optional; import javax.inject.Inject; +import javax.inject.Singleton; import org.apache.commons.lang3.SystemUtils; import org.cryptomator.jni.MacFunctions; import org.cryptomator.jni.MacKeychainAccess; +@Singleton class MacSystemKeychainAccess implements KeychainAccessStrategy { private final Optional macFunctions; @@ -47,8 +49,10 @@ class MacSystemKeychainAccess implements KeychainAccessStrategy { } @Override - public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException { - storePassphrase(key, passphrase); + public void changePassphrase(String key, CharSequence passphrase) { + if (keychain().deletePassword(key)) { + keychain().storePassword(key, passphrase); + } } } diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java index 15525aca3..6668104a4 100644 --- a/main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java +++ b/main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java @@ -25,6 +25,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; +import javax.inject.Singleton; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -50,6 +51,7 @@ import java.util.stream.Collectors; import static java.nio.charset.StandardCharsets.UTF_8; +@Singleton class WindowsProtectedKeychainAccess implements KeychainAccessStrategy { private static final Logger LOG = LoggerFactory.getLogger(WindowsProtectedKeychainAccess.class); @@ -113,8 +115,11 @@ class WindowsProtectedKeychainAccess implements KeychainAccessStrategy { } @Override - public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException { - storePassphrase(key, passphrase); + public void changePassphrase(String key, CharSequence passphrase) { + loadKeychainEntriesIfNeeded(); + if (keychainEntries.remove(key) != null) { + storePassphrase(key, passphrase); + } } @Override diff --git a/main/keychain/src/test/java/org/cryptomator/keychain/KeychainManagerTest.java b/main/keychain/src/test/java/org/cryptomator/keychain/KeychainManagerTest.java new file mode 100644 index 000000000..67f10e035 --- /dev/null +++ b/main/keychain/src/test/java/org/cryptomator/keychain/KeychainManagerTest.java @@ -0,0 +1,55 @@ +package org.cryptomator.keychain; + + +import javafx.application.Platform; +import javafx.beans.property.ReadOnlyBooleanProperty; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + + +class KeychainManagerTest { + + @Test + public void testStoreAndLoad() throws KeychainAccessException { + KeychainManager keychainManager = new KeychainManager(new MapKeychainAccess()); + keychainManager.storePassphrase("test", "asd"); + Assertions.assertArrayEquals("asd".toCharArray(), keychainManager.loadPassphrase("test")); + } + + @Nested + public static class WhenObservingProperties { + + @BeforeAll + public static void startup() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + Platform.startup(latch::countDown); + latch.await(5, TimeUnit.SECONDS); + } + + @Test + public void testPropertyChangesWhenStoringPassword() throws KeychainAccessException, InterruptedException { + KeychainManager keychainManager = new KeychainManager(new MapKeychainAccess()); + ReadOnlyBooleanProperty property = keychainManager.getPassphraseStoredProperty("test"); + Assertions.assertEquals(false, property.get()); + + keychainManager.storePassphrase("test", "bar"); + + AtomicBoolean result = new AtomicBoolean(false); + CountDownLatch latch = new CountDownLatch(1); + Platform.runLater(() -> { + result.set(property.get()); + latch.countDown(); + }); + latch.await(1, TimeUnit.SECONDS); + Assertions.assertEquals(true, result.get()); + } + + } + +} \ No newline at end of file diff --git a/main/keychain/src/test/java/org/cryptomator/keychain/KeychainModuleTest.java b/main/keychain/src/test/java/org/cryptomator/keychain/KeychainModuleTest.java deleted file mode 100644 index 7b3c59899..000000000 --- a/main/keychain/src/test/java/org/cryptomator/keychain/KeychainModuleTest.java +++ /dev/null @@ -1,24 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the accompanying LICENSE file. - *******************************************************************************/ -package org.cryptomator.keychain; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.util.Optional; - -public class KeychainModuleTest { - - @Test - public void testGetKeychain() throws KeychainAccessException { - Optional keychainAccess = DaggerTestKeychainComponent.builder().keychainModule(new TestKeychainModule()).build().keychainAccess(); - Assertions.assertTrue(keychainAccess.isPresent()); - Assertions.assertTrue(keychainAccess.get() instanceof MapKeychainAccess); - keychainAccess.get().storePassphrase("test", "asd"); - Assertions.assertArrayEquals("asd".toCharArray(), keychainAccess.get().loadPassphrase("test")); - } - -} diff --git a/main/keychain/src/test/java/org/cryptomator/keychain/TestKeychainComponent.java b/main/keychain/src/test/java/org/cryptomator/keychain/TestKeychainComponent.java deleted file mode 100644 index 82c4b82ae..000000000 --- a/main/keychain/src/test/java/org/cryptomator/keychain/TestKeychainComponent.java +++ /dev/null @@ -1,19 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the accompanying LICENSE file. - *******************************************************************************/ -package org.cryptomator.keychain; - -import dagger.Component; - -import javax.inject.Singleton; -import java.util.Optional; - -@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 deleted file mode 100644 index d6e183a22..000000000 --- a/main/keychain/src/test/java/org/cryptomator/keychain/TestKeychainModule.java +++ /dev/null @@ -1,17 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the accompanying LICENSE file. - *******************************************************************************/ -package org.cryptomator.keychain; - -import java.util.Set; - -public class TestKeychainModule extends KeychainModule { - - @Override - Set provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain, LinuxSecretServiceKeychainAccess linKeychain) { - return Set.of(new MapKeychainAccess()); - } - -} diff --git a/main/launcher/pom.xml b/main/launcher/pom.xml index 2cfbdd700..3ebcd5481 100644 --- a/main/launcher/pom.xml +++ b/main/launcher/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.5.4 + 1.5.5 launcher Cryptomator Launcher diff --git a/main/pom.xml b/main/pom.xml index 44508275f..87cd12e56 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.cryptomator main - 1.5.4 + 1.5.5 pom Cryptomator @@ -24,7 +24,7 @@ UTF-8 - 1.9.10 + 1.9.11 2.2.2 1.2.3 1.1.15 diff --git a/main/ui/pom.xml b/main/ui/pom.xml index b06f238a6..ebf705eec 100644 --- a/main/ui/pom.xml +++ b/main/ui/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.5.4 + 1.5.5 ui Cryptomator GUI diff --git a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultSuccessController.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultSuccessController.java index 08ae84369..905efeb6d 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultSuccessController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultSuccessController.java @@ -9,6 +9,7 @@ import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.fxapp.FxApplication; import javax.inject.Inject; +import java.util.Optional; @AddVaultWizardScoped public class AddVaultSuccessController implements FxController { @@ -27,7 +28,7 @@ public class AddVaultSuccessController implements FxController { @FXML public void unlockAndClose() { close(); - fxApplication.startUnlockWorkflow(vault.get()); + fxApplication.startUnlockWorkflow(vault.get(), Optional.of(window)); } @FXML diff --git a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java index 3717a844f..7fc644f0d 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java @@ -18,7 +18,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; -import javax.inject.Named; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -57,7 +56,7 @@ public class ChooseExistingVaultController implements FxController { @FXML public void initialize() { - final String resource = SystemUtils.IS_OS_MAC ? "/select-masterkey-mac.png" : "/select-masterkey-win.png"; + final String resource = SystemUtils.IS_OS_MAC ? "/img/select-masterkey-mac.png" : "/img/select-masterkey-win.png"; try (InputStream in = getClass().getResourceAsStream(resource)) { this.screenshot = new Image(in); } catch (IOException e) { diff --git a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java index 51b70f470..5b4549db1 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java @@ -29,6 +29,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ResourceBundle; @@ -48,7 +49,6 @@ public class CreateNewVaultLocationController implements FxController { private final StringProperty vaultName; private final ResourceBundle resourceBundle; private final BooleanBinding validVaultPath; - private final BooleanBinding invalidVaultPath; private final BooleanProperty usePresetPath; private final StringProperty warningText; @@ -71,7 +71,6 @@ public class CreateNewVaultLocationController implements FxController { this.vaultName = vaultName; this.resourceBundle = resourceBundle; this.validVaultPath = Bindings.createBooleanBinding(this::isValidVaultPath, vaultPath); - this.invalidVaultPath = validVaultPath.not(); this.usePresetPath = new SimpleBooleanProperty(); this.warningText = new SimpleStringProperty(); } @@ -125,6 +124,9 @@ public class CreateNewVaultLocationController implements FxController { } catch (FileAlreadyExistsException e) { LOG.warn("Can not use already existing vault path {}", vaultPath.get()); warningText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists")); + } catch (NoSuchFileException e) { + LOG.warn("At least one path component does not exist of path {}", vaultPath.get()); + warningText.set(resourceBundle.getString("addvaultwizard.new.locationDoesNotExist")); } catch (IOException e) { LOG.error("Failed to create and delete directory at chosen vault path.", e); errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene(); @@ -135,7 +137,11 @@ public class CreateNewVaultLocationController implements FxController { public void chooseCustomVaultPath() { DirectoryChooser directoryChooser = new DirectoryChooser(); directoryChooser.setTitle(resourceBundle.getString("addvaultwizard.new.directoryPickerTitle")); - directoryChooser.setInitialDirectory(customVaultPath.toFile()); + if (Files.exists(customVaultPath)) { + directoryChooser.setInitialDirectory(customVaultPath.toFile()); + } else { + directoryChooser.setInitialDirectory(DEFAULT_CUSTOM_VAULT_PATH.toFile()); + } final File file = directoryChooser.showDialog(window); if (file != null) { customVaultPath = file.toPath().toAbsolutePath(); @@ -153,12 +159,12 @@ public class CreateNewVaultLocationController implements FxController { return vaultPath; } - public BooleanBinding invalidVaultPathProperty() { - return invalidVaultPath; + public BooleanBinding validVaultPathProperty() { + return validVaultPath; } - public Boolean getInvalidVaultPath() { - return invalidVaultPath.get(); + public Boolean getValidVaultPath() { + return validVaultPath.get(); } public LocationPresets getLocationPresets() { diff --git a/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java index acb2fd12d..7a641b1e7 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java @@ -10,8 +10,8 @@ import javafx.stage.Stage; import org.cryptomator.common.vaults.Vault; import org.cryptomator.cryptofs.CryptoFileSystemProvider; import org.cryptomator.cryptolib.api.InvalidPassphraseException; -import org.cryptomator.keychain.KeychainAccess; import org.cryptomator.keychain.KeychainAccessException; +import org.cryptomator.keychain.KeychainManager; import org.cryptomator.ui.common.Animations; import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.FxController; @@ -36,14 +36,14 @@ public class ChangePasswordController implements FxController { private final Vault vault; private final ObjectProperty newPassword; private final ErrorComponent.Builder errorComponent; - private final Optional keychain; + private final Optional keychain; public NiceSecurePasswordField oldPasswordField; public CheckBox finalConfirmationCheckbox; public Button finishButton; @Inject - public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty newPassword, ErrorComponent.Builder errorComponent, Optional keychain) { + public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty newPassword, ErrorComponent.Builder errorComponent, Optional keychain) { this.window = window; this.vault = vault; this.newPassword = newPassword; diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java b/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java index c96202dda..3e09e1fad 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java +++ b/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java @@ -4,16 +4,13 @@ import javafx.concurrent.Task; import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.VaultState; import org.cryptomator.common.vaults.Volume; -import org.cryptomator.cryptolib.api.InvalidPassphraseException; -import org.cryptomator.keychain.KeychainAccess; +import org.cryptomator.keychain.KeychainManager; import org.cryptomator.ui.fxapp.FxApplicationScoped; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; -import java.nio.CharBuffer; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; @@ -28,10 +25,10 @@ public class VaultService { private static final Logger LOG = LoggerFactory.getLogger(VaultService.class); private final ExecutorService executorService; - private final Optional keychain; + private final Optional keychain; @Inject - public VaultService(ExecutorService executorService, Optional keychain) { + public VaultService(ExecutorService executorService, Optional keychain) { this.executorService = executorService; this.keychain = keychain; } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java b/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java index 5ebe94068..7423fb899 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java @@ -26,6 +26,7 @@ public enum FontAwesome5Icon { INFO_CIRCLE("\uF05A"), // KEY("\uF084"), // LINK("\uF0C1"), // + UNLINK("\uf127"), LOCK("\uF023"), // LOCK_OPEN("\uF3C1"), // MAGIC("\uF0D0"), // diff --git a/main/ui/src/main/java/org/cryptomator/ui/forgetPassword/ForgetPasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/forgetPassword/ForgetPasswordController.java index e072840ba..ea1e0530e 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/forgetPassword/ForgetPasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/forgetPassword/ForgetPasswordController.java @@ -4,8 +4,8 @@ import javafx.beans.property.BooleanProperty; import javafx.fxml.FXML; import javafx.stage.Stage; import org.cryptomator.common.vaults.Vault; -import org.cryptomator.keychain.KeychainAccess; import org.cryptomator.keychain.KeychainAccessException; +import org.cryptomator.keychain.KeychainManager; import org.cryptomator.ui.common.FxController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -20,14 +20,14 @@ public class ForgetPasswordController implements FxController { private final Stage window; private final Vault vault; - private final Optional keychainAccess; + private final Optional keychain; private final BooleanProperty confirmedResult; @Inject - public ForgetPasswordController(@ForgetPasswordWindow Stage window, @ForgetPasswordWindow Vault vault, Optional keychainAccess, @ForgetPasswordWindow BooleanProperty confirmedResult) { + public ForgetPasswordController(@ForgetPasswordWindow Stage window, @ForgetPasswordWindow Vault vault, Optional keychain, @ForgetPasswordWindow BooleanProperty confirmedResult) { this.window = window; this.vault = vault; - this.keychainAccess = keychainAccess; + this.keychain = keychain; this.confirmedResult = confirmedResult; } @@ -38,9 +38,9 @@ public class ForgetPasswordController implements FxController { @FXML public void finish() { - if (keychainAccess.isPresent()) { + if (keychain.isPresent()) { try { - keychainAccess.get().deletePassphrase(vault.getId()); + keychain.get().deletePassphrase(vault.getId()); LOG.debug("Forgot password for vault {}.", vault.getDisplayableName()); confirmedResult.setValue(true); } catch (KeychainAccessException e) { diff --git a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java index c197ecf87..3172b6288 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java +++ b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java @@ -96,9 +96,9 @@ public class FxApplication extends Application { }); } - public void startUnlockWorkflow(Vault vault) { + public void startUnlockWorkflow(Vault vault, Optional owner) { Platform.runLater(() -> { - unlockWindowBuilderProvider.get().vault(vault).build().startUnlockWorkflow(); + unlockWindowBuilderProvider.get().vault(vault).owner(owner).build().startUnlockWorkflow(); LOG.debug("Showing UnlockWindow for {}", vault.getDisplayableName()); }); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java index 9b72548fd..b52d64ad3 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java @@ -9,14 +9,11 @@ import dagger.Binds; import dagger.Module; import dagger.Provides; import javafx.application.Application; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableSet; import javafx.scene.image.Image; import javafx.stage.Stage; import org.apache.commons.lang3.SystemUtils; -import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.StageFactory; import org.cryptomator.ui.mainwindow.MainWindowComponent; @@ -49,8 +46,8 @@ abstract class FxApplicationModule { } try { return List.of( // - createImageFromResource("/window_icon_32.png"), // - createImageFromResource("/window_icon_512.png") // + createImageFromResource("/img/window_icon_32.png"), // + createImageFromResource("/img/window_icon_512.png") // ); } catch (IOException e) { throw new UncheckedIOException("Failed to load embedded resource.", e); diff --git a/main/ui/src/main/java/org/cryptomator/ui/launcher/UiLauncher.java b/main/ui/src/main/java/org/cryptomator/ui/launcher/UiLauncher.java index 7024de477..d689b4ae2 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/launcher/UiLauncher.java +++ b/main/ui/src/main/java/org/cryptomator/ui/launcher/UiLauncher.java @@ -62,11 +62,11 @@ public class UiLauncher { Desktop.getDesktop().addAppEventListener((AppReopenedListener) e -> showMainWindowAsync(hasTrayIcon)); // auto unlock - Collection vaultsWithAutoUnlockEnabled = vaults.filtered(v -> v.getVaultSettings().unlockAfterStartup().get()); - if (!vaultsWithAutoUnlockEnabled.isEmpty()) { + Collection vaultsToAutoUnlock = vaults.filtered(this::shouldAttemptAutoUnlock); + if (!vaultsToAutoUnlock.isEmpty()) { fxApplicationStarter.get(hasTrayIcon).thenAccept(app -> { - for (Vault vault : vaultsWithAutoUnlockEnabled){ - app.startUnlockWorkflow(vault); + for (Vault vault : vaultsToAutoUnlock) { + app.startUnlockWorkflow(vault, Optional.empty()); } }); } @@ -74,6 +74,10 @@ public class UiLauncher { launchEventHandler.startHandlingLaunchEvents(hasTrayIcon); } + private boolean shouldAttemptAutoUnlock(Vault vault) { + return vault.isLocked() && vault.getVaultSettings().unlockAfterStartup().get(); + } + private void showMainWindowAsync(boolean hasTrayIcon) { fxApplicationStarter.get(hasTrayIcon).thenAccept(FxApplication::showMainWindow); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java index 434a5b7ef..55720521b 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java @@ -1,14 +1,20 @@ package org.cryptomator.ui.mainwindow; +import javafx.beans.binding.BooleanExpression; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.fxml.FXML; +import javafx.stage.Stage; import org.cryptomator.common.vaults.Vault; +import org.cryptomator.keychain.KeychainManager; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.fxapp.FxApplication; import org.cryptomator.ui.vaultoptions.VaultOptionsComponent; +import org.fxmisc.easybind.EasyBind; import javax.inject.Inject; +import java.util.Optional; @MainWindowScoped public class VaultDetailLockedController implements FxController { @@ -16,24 +22,34 @@ public class VaultDetailLockedController implements FxController { private final ReadOnlyObjectProperty vault; private final FxApplication application; private final VaultOptionsComponent.Builder vaultOptionsWindow; + private final Optional keychainManagerOptional; + private final Stage mainWindow; + private final BooleanExpression passwordSaved; @Inject - VaultDetailLockedController(ObjectProperty vault, FxApplication application, VaultOptionsComponent.Builder vaultOptionsWindow) { + VaultDetailLockedController(ObjectProperty vault, FxApplication application, VaultOptionsComponent.Builder vaultOptionsWindow, Optional keychainManagerOptional, @MainWindow Stage mainWindow) { this.vault = vault; this.application = application; this.vaultOptionsWindow = vaultOptionsWindow; + this.keychainManagerOptional = keychainManagerOptional; + this.mainWindow = mainWindow; + if (keychainManagerOptional.isPresent()) { + this.passwordSaved = BooleanExpression.booleanExpression(EasyBind.select(vault).selectObject(v -> keychainManagerOptional.get().getPassphraseStoredProperty(v.getId()))); + } else { + this.passwordSaved = new SimpleBooleanProperty(false); + } } @FXML public void unlock() { - application.startUnlockWorkflow(vault.get()); + application.startUnlockWorkflow(vault.get(), Optional.of(mainWindow)); } @FXML public void showVaultOptions() { vaultOptionsWindow.vault(vault.get()).build().showVaultOptionsWindow(); } - + /* Getter/Setter */ public ReadOnlyObjectProperty vaultProperty() { @@ -43,4 +59,14 @@ public class VaultDetailLockedController implements FxController { public Vault getVault() { return vault.get(); } + + public BooleanExpression passwordSavedProperty() { + return passwordSaved; + } + + public boolean isPasswordSaved() { + if (keychainManagerOptional.isPresent() && vault.get() != null) { + return keychainManagerOptional.get().getPassphraseStoredProperty(vault.get().getId()).get(); + } else return false; + } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/WelcomeController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/WelcomeController.java index 2646c1a36..95a65f6ac 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/WelcomeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/WelcomeController.java @@ -42,4 +42,5 @@ public class WelcomeController implements FxController { public boolean isNoVaultPresent() { return noVaultPresent.get(); } + } diff --git a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java index 05b460348..3123d4cb2 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java @@ -22,8 +22,8 @@ import org.cryptomator.cryptofs.migration.Migrators; import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener; import org.cryptomator.cryptofs.migration.api.MigrationProgressListener; import org.cryptomator.cryptolib.api.InvalidPassphraseException; -import org.cryptomator.keychain.KeychainAccess; import org.cryptomator.keychain.KeychainAccessException; +import org.cryptomator.keychain.KeychainManager; import org.cryptomator.ui.common.Animations; import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.FxController; @@ -55,7 +55,7 @@ public class MigrationRunController implements FxController { private final Vault vault; private final ExecutorService executor; private final ScheduledExecutorService scheduler; - private final Optional keychainAccess; + private final Optional keychain; private final ObjectProperty missingCapability; private final ErrorComponent.Builder errorComponent; private final Lazy startScene; @@ -69,13 +69,13 @@ public class MigrationRunController implements FxController { public NiceSecurePasswordField passwordField; @Inject - public MigrationRunController(@MigrationWindow Stage window, @MigrationWindow Vault vault, ExecutorService executor, ScheduledExecutorService scheduler, Optional keychainAccess, @Named("capabilityErrorCause") ObjectProperty missingCapability, @FxmlScene(FxmlFile.MIGRATION_START) Lazy startScene, @FxmlScene(FxmlFile.MIGRATION_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.MIGRATION_CAPABILITY_ERROR) Lazy capabilityErrorScene, @FxmlScene(FxmlFile.MIGRATION_IMPOSSIBLE) Lazy impossibleScene, ErrorComponent.Builder errorComponent) { + public MigrationRunController(@MigrationWindow Stage window, @MigrationWindow Vault vault, ExecutorService executor, ScheduledExecutorService scheduler, Optional keychain, @Named("capabilityErrorCause") ObjectProperty missingCapability, @FxmlScene(FxmlFile.MIGRATION_START) Lazy startScene, @FxmlScene(FxmlFile.MIGRATION_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.MIGRATION_CAPABILITY_ERROR) Lazy capabilityErrorScene, @FxmlScene(FxmlFile.MIGRATION_IMPOSSIBLE) Lazy impossibleScene, ErrorComponent.Builder errorComponent) { this.window = window; this.vault = vault; this.executor = executor; this.scheduler = scheduler; - this.keychainAccess = keychainAccess; + this.keychain = keychain; this.missingCapability = missingCapability; this.errorComponent = errorComponent; this.startScene = startScene; @@ -88,7 +88,7 @@ public class MigrationRunController implements FxController { } public void initialize() { - if (keychainAccess.isPresent()) { + if (keychain.isPresent()) { loadStoredPassword(); } migrationButtonDisabled.bind(vault.stateProperty().isNotEqualTo(VaultState.NEEDS_MIGRATION).or(passwordField.textProperty().isEmpty())); @@ -167,10 +167,10 @@ public class MigrationRunController implements FxController { } private void loadStoredPassword() { - assert keychainAccess.isPresent(); + assert keychain.isPresent(); char[] storedPw = null; try { - storedPw = keychainAccess.get().loadPassphrase(vault.getId()); + storedPw = keychain.get().loadPassphrase(vault.getId()); if (storedPw != null) { passwordField.setPassword(storedPw); passwordField.selectRange(storedPw.length, storedPw.length); diff --git a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationSuccessController.java b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationSuccessController.java index 6a64e5509..e0135f8b9 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationSuccessController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationSuccessController.java @@ -5,8 +5,10 @@ import javafx.stage.Stage; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.fxapp.FxApplication; +import org.cryptomator.ui.mainwindow.MainWindow; import javax.inject.Inject; +import java.util.Optional; @MigrationScoped public class MigrationSuccessController implements FxController { @@ -14,18 +16,20 @@ public class MigrationSuccessController implements FxController { private final FxApplication fxApplication; private final Stage window; private final Vault vault; + private final Stage mainWindow; @Inject - MigrationSuccessController(FxApplication fxApplication, @MigrationWindow Stage window, @MigrationWindow Vault vault) { + MigrationSuccessController(FxApplication fxApplication, @MigrationWindow Stage window, @MigrationWindow Vault vault, @MainWindow Stage mainWindow) { this.fxApplication = fxApplication; this.window = window; this.vault = vault; + this.mainWindow = mainWindow; } @FXML public void unlockAndClose() { close(); - fxApplication.startUnlockWorkflow(vault); + fxApplication.startUnlockWorkflow(vault, Optional.of(mainWindow)); } @FXML diff --git a/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayImageFactory.java b/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayImageFactory.java index a76c8b1e3..cbdf651b4 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayImageFactory.java +++ b/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayImageFactory.java @@ -30,13 +30,13 @@ class TrayImageFactory { .map(MacApplicationUiAppearance::getCurrentInterfaceStyle) // .orElse(MacApplicationUiInterfaceStyle.LIGHT); return switch (interfaceStyle) { - case DARK -> "/tray_icon_mac_white.png"; - case LIGHT -> "/tray_icon_mac_black.png"; + case DARK -> "/img/tray_icon_mac_white.png"; + case LIGHT -> "/img/tray_icon_mac_black.png"; }; } private String getWinOrLinuxResourceName() { - return "/tray_icon.png"; + return "/img/tray_icon.png"; } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java b/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java index 58c0f3bda..af12beebe 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java @@ -15,6 +15,7 @@ import java.awt.PopupMenu; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.EventObject; +import java.util.Optional; import java.util.ResourceBundle; import java.util.function.Consumer; @@ -103,7 +104,7 @@ class TrayMenuController { } private void unlockVault(Vault vault) { - fxApplicationStarter.get(true).thenAccept(app -> app.startUnlockWorkflow(vault)); + fxApplicationStarter.get(true).thenAccept(app -> app.startUnlockWorkflow(vault, Optional.empty())); } private void lockVault(Vault vault) { diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockComponent.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockComponent.java index f321f5890..18ae3b5f0 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockComponent.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockComponent.java @@ -6,18 +6,14 @@ package org.cryptomator.ui.unlock; import dagger.BindsInstance; -import dagger.Lazy; import dagger.Subcomponent; -import javafx.scene.Scene; import javafx.stage.Stage; -import org.cryptomator.ui.common.FxmlFile; -import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.common.vaults.Vault; -import java.util.concurrent.Executor; +import javax.inject.Named; +import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; @UnlockScoped @Subcomponent(modules = {UnlockModule.class}) @@ -26,7 +22,7 @@ public interface UnlockComponent { ExecutorService defaultExecutorService(); UnlockWorkflow unlockWorkflow(); - + default Future startUnlockWorkflow() { UnlockWorkflow workflow = unlockWorkflow(); defaultExecutorService().submit(workflow); @@ -35,10 +31,13 @@ public interface UnlockComponent { @Subcomponent.Builder interface Builder { - + @BindsInstance Builder vault(@UnlockWindow Vault vault); + @BindsInstance + Builder owner(@Named("unlockWindowOwner") Optional owner); + UnlockComponent build(); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java index 38e41323c..c59b2ef6d 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java @@ -1,5 +1,10 @@ package org.cryptomator.ui.unlock; +import javafx.animation.Animation; +import javafx.animation.Interpolator; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.beans.binding.ObjectBinding; @@ -9,9 +14,13 @@ import javafx.beans.property.SimpleBooleanProperty; import javafx.fxml.FXML; import javafx.scene.control.CheckBox; import javafx.scene.control.ContentDisplay; +import javafx.scene.image.ImageView; +import javafx.scene.transform.Rotate; +import javafx.scene.transform.Translate; import javafx.stage.Stage; +import javafx.util.Duration; import org.cryptomator.common.vaults.Vault; -import org.cryptomator.keychain.KeychainAccess; +import org.cryptomator.keychain.KeychainManager; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.UserInteractionLock; import org.cryptomator.ui.controls.NiceSecurePasswordField; @@ -38,15 +47,22 @@ public class UnlockController implements FxController { private final Optional savedPassword; private final UserInteractionLock passwordEntryLock; private final ForgetPasswordComponent.Builder forgetPassword; - private final Optional keychainAccess; + private final Optional keychain; private final ObjectBinding unlockButtonContentDisplay; private final BooleanBinding userInteractionDisabled; private final BooleanProperty unlockButtonDisabled; + public NiceSecurePasswordField passwordField; public CheckBox savePasswordCheckbox; + public ImageView face; + public ImageView leftArm; + public ImageView rightArm; + public ImageView legs; + public ImageView body; + public Animation unlockAnimation; @Inject - public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, AtomicReference password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional savedPassword, UserInteractionLock passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, Optional keychainAccess) { + public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, AtomicReference password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional savedPassword, UserInteractionLock passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, Optional keychain) { this.window = window; this.vault = vault; this.password = password; @@ -54,20 +70,61 @@ public class UnlockController implements FxController { this.savedPassword = savedPassword; this.passwordEntryLock = passwordEntryLock; this.forgetPassword = forgetPassword; - this.keychainAccess = keychainAccess; + this.keychain = keychain; this.unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, passwordEntryLock.awaitingInteraction()); this.userInteractionDisabled = passwordEntryLock.awaitingInteraction().not(); this.unlockButtonDisabled = new SimpleBooleanProperty(); + this.window.setOnCloseRequest(windowEvent -> cancel()); } + @FXML public void initialize() { savePasswordCheckbox.setSelected(savedPassword.isPresent()); if (password.get() != null) { passwordField.setPassword(password.get()); } unlockButtonDisabled.bind(userInteractionDisabled.or(passwordField.textProperty().isEmpty())); + + var leftArmTranslation = new Translate(24, 0); + var leftArmRotation = new Rotate(60, 16, 30, 0); + var leftArmRetracted = new KeyValue(leftArmTranslation.xProperty(), 24); + var leftArmExtended = new KeyValue(leftArmTranslation.xProperty(), 0.0); + var leftArmHorizontal = new KeyValue(leftArmRotation.angleProperty(), 60, Interpolator.EASE_OUT); + var leftArmHanging = new KeyValue(leftArmRotation.angleProperty(), 0); + leftArm.getTransforms().setAll(leftArmTranslation, leftArmRotation); + + var rightArmTranslation = new Translate(-24, 0); + var rightArmRotation = new Rotate(60, 48, 30, 0); + var rightArmRetracted = new KeyValue(rightArmTranslation.xProperty(), -24); + var rightArmExtended = new KeyValue(rightArmTranslation.xProperty(), 0.0); + var rightArmHorizontal = new KeyValue(rightArmRotation.angleProperty(), -60); + var rightArmHanging = new KeyValue(rightArmRotation.angleProperty(), 0, Interpolator.EASE_OUT); + rightArm.getTransforms().setAll(rightArmTranslation, rightArmRotation); + + var legsRetractedY = new KeyValue(legs.scaleYProperty(), 0); + var legsExtendedY = new KeyValue(legs.scaleYProperty(), 1, Interpolator.EASE_OUT); + var legsRetractedX = new KeyValue(legs.scaleXProperty(), 0); + var legsExtendedX = new KeyValue(legs.scaleXProperty(), 1, Interpolator.EASE_OUT); + legs.setScaleY(0); + legs.setScaleX(0); + + var faceHidden = new KeyValue(face.opacityProperty(), 0.0); + var faceVisible = new KeyValue(face.opacityProperty(), 1.0, Interpolator.LINEAR); + face.setOpacity(0); + + unlockAnimation = new Timeline( + new KeyFrame(Duration.ZERO, leftArmRetracted, leftArmHorizontal, rightArmRetracted, rightArmHorizontal, legsRetractedY, legsRetractedX, faceHidden), + new KeyFrame(Duration.millis(200), leftArmExtended, leftArmHorizontal, rightArmRetracted, rightArmHorizontal), + new KeyFrame(Duration.millis(400), leftArmExtended, leftArmHanging, rightArmExtended, rightArmHorizontal), + new KeyFrame(Duration.millis(600), leftArmExtended, leftArmHanging, rightArmExtended, rightArmHanging), + new KeyFrame(Duration.millis(800), legsExtendedY, legsExtendedX, faceHidden), + new KeyFrame(Duration.millis(1000), faceVisible) + ); + + passwordEntryLock.awaitingInteraction().addListener(observable -> stopUnlockAnimation()); } + @FXML public void cancel() { LOG.debug("Unlock canceled by user."); @@ -75,6 +132,7 @@ public class UnlockController implements FxController { passwordEntryLock.interacted(UnlockModule.PasswordEntry.CANCELED); } + @FXML public void unlock() { LOG.trace("UnlockController.unlock()"); @@ -88,6 +146,23 @@ public class UnlockController implements FxController { Arrays.fill(oldPw, ' '); } passwordEntryLock.interacted(UnlockModule.PasswordEntry.PASSWORD_ENTERED); + startUnlockAnimation(); + } + + private void startUnlockAnimation() { + leftArm.setVisible(true); + rightArm.setVisible(true); + legs.setVisible(true); + face.setVisible(true); + unlockAnimation.playFromStart(); + } + + private void stopUnlockAnimation() { + unlockAnimation.stop(); + leftArm.setVisible(false); + rightArm.setVisible(false); + legs.setVisible(false); + face.setVisible(false); } /* Save Password */ @@ -131,6 +206,6 @@ public class UnlockController implements FxController { } public boolean isKeychainAccessAvailable() { - return keychainAccess.isPresent(); + return keychain.isPresent(); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java index 4b1ba3cfe..e07583c84 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java @@ -5,12 +5,11 @@ import dagger.Module; import dagger.Provides; import dagger.multibindings.IntoMap; import javafx.scene.Scene; -import javafx.scene.image.Image; import javafx.stage.Modality; import javafx.stage.Stage; import org.cryptomator.common.vaults.Vault; -import org.cryptomator.keychain.KeychainAccess; import org.cryptomator.keychain.KeychainAccessException; +import org.cryptomator.keychain.KeychainManager; import org.cryptomator.ui.common.DefaultSceneFactory; import org.cryptomator.ui.common.FXMLLoaderFactory; import org.cryptomator.ui.common.FxController; @@ -25,16 +24,11 @@ import org.slf4j.LoggerFactory; import javax.inject.Named; import javax.inject.Provider; -import java.nio.CharBuffer; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.ResourceBundle; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; @Module(subcomponents = {ForgetPasswordComponent.class}) abstract class UnlockModule { @@ -52,8 +46,8 @@ abstract class UnlockModule { @Provides @Named("savedPassword") @UnlockScoped - static Optional provideStoredPassword(Optional keychainAccess, @UnlockWindow Vault vault) { - return keychainAccess.map(k -> { + static Optional provideStoredPassword(Optional keychain, @UnlockWindow Vault vault) { + return keychain.map(k -> { try { return k.loadPassphrase(vault.getId()); } catch (KeychainAccessException e) { @@ -62,7 +56,7 @@ abstract class UnlockModule { } }); } - + @Provides @UnlockScoped static AtomicReference providePassword(@Named("savedPassword") Optional storedPassword) { @@ -86,11 +80,16 @@ abstract class UnlockModule { @Provides @UnlockWindow @UnlockScoped - static Stage provideStage(StageFactory factory, @UnlockWindow Vault vault) { + static Stage provideStage(StageFactory factory, @UnlockWindow Vault vault, @Named("unlockWindowOwner") Optional owner) { Stage stage = factory.create(); stage.setTitle(vault.getDisplayableName()); stage.setResizable(false); - stage.initModality(Modality.APPLICATION_MODAL); + if (owner.isPresent()) { + stage.initOwner(owner.get()); + stage.initModality(Modality.WINDOW_MODAL); + } else { + stage.initModality(Modality.APPLICATION_MODAL); + } return stage; } diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockSuccessController.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockSuccessController.java index bf0d2e3fb..f7952ad9f 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockSuccessController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockSuccessController.java @@ -31,7 +31,7 @@ public class UnlockSuccessController implements FxController { private final VaultService vaultService; private final ObjectProperty revealButtonState; private final BooleanProperty revealButtonDisabled; - + public CheckBox rememberChoiceCheckbox; @Inject diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java index c2f6e47f4..08b8adf71 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java @@ -5,13 +5,14 @@ import javafx.application.Platform; import javafx.concurrent.Task; import javafx.scene.Scene; import javafx.stage.Stage; +import javafx.stage.Window; import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.VaultState; import org.cryptomator.common.vaults.Volume; import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.InvalidPassphraseException; -import org.cryptomator.keychain.KeychainAccess; import org.cryptomator.keychain.KeychainAccessException; +import org.cryptomator.keychain.KeychainManager; import org.cryptomator.ui.common.Animations; import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.FxmlFile; @@ -51,14 +52,14 @@ public class UnlockWorkflow extends Task { private final AtomicBoolean savePassword; private final Optional savedPassword; private final UserInteractionLock passwordEntryLock; - private final Optional keychain; + private final Optional keychain; private final Lazy unlockScene; private final Lazy successScene; private final Lazy invalidMountPointScene; private final ErrorComponent.Builder errorComponent; @Inject - UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, AtomicReference password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional savedPassword, UserInteractionLock passwordEntryLock, Optional keychain, @FxmlScene(FxmlFile.UNLOCK) Lazy unlockScene, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, ErrorComponent.Builder errorComponent) { + UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, AtomicReference password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional savedPassword, UserInteractionLock passwordEntryLock, Optional keychain, @FxmlScene(FxmlFile.UNLOCK) Lazy unlockScene, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, ErrorComponent.Builder errorComponent) { this.window = window; this.vault = vault; this.vaultService = vaultService; @@ -94,7 +95,7 @@ public class UnlockWorkflow extends Task { wipePassword(savedPassword.orElse(null)); } } - + private boolean attemptUnlock() throws InterruptedException, IOException, Volume.VolumeException { boolean proceed = password.get() != null || askForPassword(false) == PasswordEntry.PASSWORD_ENTERED; while (proceed) { @@ -112,6 +113,13 @@ public class UnlockWorkflow extends Task { Platform.runLater(() -> { window.setScene(unlockScene.get()); window.show(); + Window owner = window.getOwner(); + if (owner != null) { + window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2); + window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2); + } else { + window.centerOnScreen(); + } if (animateShake) { Animations.createShakeWindowAnimation(window).play(); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java index d03ac4e31..6ec7576dd 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java @@ -1,13 +1,19 @@ package org.cryptomator.ui.vaultoptions; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanExpression; +import javafx.beans.property.SimpleBooleanProperty; import javafx.fxml.FXML; import javafx.stage.Stage; import org.cryptomator.common.vaults.Vault; +import org.cryptomator.keychain.KeychainAccessException; +import org.cryptomator.keychain.KeychainManager; import org.cryptomator.ui.changepassword.ChangePasswordComponent; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.recoverykey.RecoveryKeyComponent; import javax.inject.Inject; +import java.util.Optional; @VaultOptionsScoped public class MasterkeyOptionsController implements FxController { @@ -16,13 +22,20 @@ public class MasterkeyOptionsController implements FxController { private final Stage window; private final ChangePasswordComponent.Builder changePasswordWindow; private final RecoveryKeyComponent.Builder recoveryKeyWindow; + private final Optional keychainManagerOptional; + private final BooleanExpression passwordSaved; + @Inject - MasterkeyOptionsController(@VaultOptionsWindow Vault vault, @VaultOptionsWindow Stage window, ChangePasswordComponent.Builder changePasswordWindow, RecoveryKeyComponent.Builder recoveryKeyWindow) { + MasterkeyOptionsController(@VaultOptionsWindow Vault vault, @VaultOptionsWindow Stage window, ChangePasswordComponent.Builder changePasswordWindow, RecoveryKeyComponent.Builder recoveryKeyWindow, Optional keychainManagerOptional) { this.vault = vault; this.window = window; this.changePasswordWindow = changePasswordWindow; this.recoveryKeyWindow = recoveryKeyWindow; + this.keychainManagerOptional = keychainManagerOptional; + if (keychainManagerOptional.isPresent()) { + this.passwordSaved = Bindings.createBooleanBinding(this::isPasswordSaved, keychainManagerOptional.get().getPassphraseStoredProperty(vault.getId())); + } else this.passwordSaved = new SimpleBooleanProperty(false); } @FXML @@ -39,4 +52,20 @@ public class MasterkeyOptionsController implements FxController { public void showRecoverVaultDialogue() { recoveryKeyWindow.vault(vault).owner(window).build().showRecoveryKeyRecoverWindow(); } + + @FXML + public void removePasswordFromKeychain() throws KeychainAccessException { + keychainManagerOptional.get().deletePassphrase(vault.getId()); + window.close(); + } + + public BooleanExpression passwordSavedProperty() { + return passwordSaved; + } + + public boolean isPasswordSaved() { + if (keychainManagerOptional.isPresent() && vault != null) { + return keychainManagerOptional.get().getPassphraseStoredProperty(vault.getId()).get(); + } else return false; + } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/wrongfilealert/WrongFileAlertController.java b/main/ui/src/main/java/org/cryptomator/ui/wrongfilealert/WrongFileAlertController.java index 6a11a9eba..292486a6a 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/wrongfilealert/WrongFileAlertController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/wrongfilealert/WrongFileAlertController.java @@ -30,7 +30,7 @@ public class WrongFileAlertController implements FxController { @FXML public void initialize() { - final String resource = SystemUtils.IS_OS_MAC ? "/vault-volume-mac.png" : "/vault-volume-win.png"; + final String resource = SystemUtils.IS_OS_MAC ? "/img/vault-volume-mac.png" : "/img/vault-volume-win.png"; try (InputStream in = getClass().getResourceAsStream(resource)) { this.screenshot = new Image(in); } catch (IOException e) { diff --git a/main/ui/src/main/resources/fxml/addvault_new_location.fxml b/main/ui/src/main/resources/fxml/addvault_new_location.fxml index 420a0f960..53336fb25 100644 --- a/main/ui/src/main/resources/fxml/addvault_new_location.fxml +++ b/main/ui/src/main/resources/fxml/addvault_new_location.fxml @@ -60,7 +60,7 @@ + - + + + + -