diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/StackTraceController.java b/main/ui/src/main/java/org/cryptomator/ui/common/StackTraceController.java index accab1b89..4dd6d91ba 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/common/StackTraceController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/common/StackTraceController.java @@ -8,11 +8,11 @@ public class StackTraceController implements FxController { private final String stackTrace; - public StackTraceController(Exception cause) { + public StackTraceController(Throwable cause) { this.stackTrace = provideStackTrace(cause); } - static String provideStackTrace(Exception cause) { + static String provideStackTrace(Throwable cause) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); cause.printStackTrace(new PrintStream(baos)); return baos.toString(StandardCharsets.UTF_8); 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 ec2030fd2..68e1fecb0 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,15 +4,20 @@ 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.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; +import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; @@ -23,10 +28,12 @@ public class VaultService { private static final Logger LOG = LoggerFactory.getLogger(VaultService.class); private final ExecutorService executorService; + private final Optional keychain; @Inject - public VaultService(ExecutorService executorService) { + public VaultService(ExecutorService executorService, Optional keychain) { this.executorService = executorService; + this.keychain = keychain; } public void reveal(Vault vault) { @@ -45,6 +52,72 @@ public class VaultService { return task; } + /** + * Attempts to unlock all given vaults in a background thread using passwords stored in the system keychain. + * + * @param vaults The vaults to unlock + * @implNote No-op if no system keychain is present + */ + public void attemptAutoUnlock(Collection vaults) { + if (!keychain.isPresent()) { + LOG.debug("No system keychain found. Unable to auto unlock without saved passwords."); + } else { + for (Vault vault : vaults) { + attemptAutoUnlock(vault, keychain.get()); + } + } + } + + /** + * Unlocks a vault in a background thread using a stored passphrase + * + * @param vault The vault to unlock + * @param keychainAccess The system keychain holding the passphrase for the vault + */ + public void attemptAutoUnlock(Vault vault, KeychainAccess keychainAccess) { + executorService.execute(createAutoUnlockTask(vault, keychainAccess)); + } + + /** + * Creates but doesn't start an auto-unlock task. + * + * @param vault The vault to unlock + * @param keychainAccess The system keychain holding the passphrase for the vault + * @return The task + */ + public Task createAutoUnlockTask(Vault vault, KeychainAccess keychainAccess) { + Task task = new AutoUnlockVaultTask(vault, keychainAccess); + task.setOnSucceeded(evt -> LOG.info("Auto-unlocked {}", vault.getDisplayableName())); + task.setOnFailed(evt -> LOG.error("Failed to auto-unlock " + vault.getDisplayableName(), evt.getSource().getException())); + return task; + } + + /** + * Unlocks a vault in a background thread + * + * @param vault The vault to unlock + * @param passphrase The password to use - wipe this param asap + * @implNote A copy of the passphrase will be made, which is wiped as soon as the task ran. + */ + public void unlock(Vault vault, CharSequence passphrase) { + executorService.execute(createUnlockTask(vault, passphrase)); + } + + /** + * Creates but doesn't start an unlock task. + * + * @param vault The vault to unlock + * @param passphrase The password to use - wipe this param asap + * @return The task + * @implNote A copy of the passphrase will be made, which is wiped as soon as the task ran. + */ + public Task createUnlockTask(Vault vault, CharSequence passphrase) { + Task task = new UnlockVaultTask(vault, passphrase); + task.setOnSucceeded(evt -> LOG.info("Unlocked {}", vault.getDisplayableName())); + task.setOnFailed(evt -> LOG.error("Failed to unlock " + vault.getDisplayableName(), evt.getSource().getException())); + return task; + } + /** * Locks a vault in a background thread. * @@ -60,6 +133,7 @@ public class VaultService { * * @param vault The vault to lock * @param forced Whether to attempt a forced lock + * @return The task */ public Task createLockTask(Vault vault, boolean forced) { Task task = new LockVaultTask(vault, forced); @@ -145,6 +219,93 @@ public class VaultService { } } + private static class AutoUnlockVaultTask extends Task { + + private final Vault vault; + private final KeychainAccess keychain; + + public AutoUnlockVaultTask(Vault vault, KeychainAccess keychain) { + this.vault = vault; + this.keychain = keychain; + } + + @Override + protected Vault call() throws Exception { + char[] storedPw = null; + try { + storedPw = keychain.loadPassphrase(vault.getId()); + if (storedPw == null) { + throw new InvalidPassphraseException(); + } + vault.unlock(CharBuffer.wrap(storedPw)); + } finally { + if (storedPw != null) { + Arrays.fill(storedPw, ' '); + } + } + return vault; + } + + @Override + protected void scheduled() { + vault.setState(VaultState.PROCESSING); + } + + @Override + protected void succeeded() { + vault.setState(VaultState.UNLOCKED); + } + + @Override + protected void failed() { + vault.setState(VaultState.LOCKED); + } + } + + private static class UnlockVaultTask extends Task { + + private final Vault vault; + private final CharBuffer passphrase; + + /** + * @param vault The vault to unlock + * @param passphrase The password to use - wipe this param asap + * @implNote A copy of the passphrase will be made, which is wiped as soon as the task ran. + */ + public UnlockVaultTask(Vault vault, CharSequence passphrase) { + this.vault = vault; + this.passphrase = CharBuffer.allocate(passphrase.length()); + for (int i = 0; i < passphrase.length(); i++) { + this.passphrase.put(i, passphrase.charAt(i)); + } + } + + @Override + protected Vault call() throws Exception { + try { + vault.unlock(passphrase); + } finally { + Arrays.fill(passphrase.array(), ' '); + } + return vault; + } + + @Override + protected void scheduled() { + vault.setState(VaultState.PROCESSING); + } + + @Override + protected void succeeded() { + vault.setState(VaultState.UNLOCKED); + } + + @Override + protected void failed() { + vault.setState(VaultState.LOCKED); + } + } + /** * A task that locks a vault */ @@ -186,5 +347,4 @@ public class VaultService { } - } diff --git a/main/ui/src/main/java/org/cryptomator/ui/launcher/AutoUnlocker.java b/main/ui/src/main/java/org/cryptomator/ui/launcher/AutoUnlocker.java deleted file mode 100644 index 21b3d5a6c..000000000 --- a/main/ui/src/main/java/org/cryptomator/ui/launcher/AutoUnlocker.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.cryptomator.ui.launcher; - -import javafx.application.Platform; -import javafx.collections.ObservableList; -import org.cryptomator.common.vaults.Vault; -import org.cryptomator.common.vaults.VaultState; -import org.cryptomator.common.vaults.Volume; -import org.cryptomator.keychain.KeychainAccess; -import org.cryptomator.keychain.KeychainAccessException; -import org.cryptomator.keychain.KeychainModule; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.inject.Inject; -import javax.inject.Singleton; -import java.io.IOException; -import java.nio.CharBuffer; -import java.util.Arrays; -import java.util.Optional; - -@Singleton -class AutoUnlocker { - - private static final Logger LOG = LoggerFactory.getLogger(AutoUnlocker.class); - - private final ObservableList vaults; - private final Optional keychain; - - @Inject - AutoUnlocker(ObservableList vaults, Optional keychain) { - this.vaults = vaults; - this.keychain = keychain; - } - - /** - * Attempts to unlock all vaults that have been configured for auto unlock. - * If an attempt fails (i.e. because the stored password is wrong) it will be silently skipped. - */ - public void autoUnlock() { - if (!keychain.isPresent()) { - LOG.info("No system keychain found. Skipping auto unlock."); - return; - } - // TODO: do async - vaults.filtered(v -> v.getVaultSettings().unlockAfterStartup().get()).forEach(this::autoUnlock); - } - - private void autoUnlock(Vault vault) { - if (vault.getState() != VaultState.LOCKED) { - LOG.warn("Can't unlock vault {} due to its state {}", vault.getDisplayablePath(), vault.getState()); - return; - } - assert keychain.isPresent(); - char[] storedPw = null; - try { - storedPw = keychain.get().loadPassphrase(vault.getId()); - if (storedPw == null) { - LOG.warn("No passphrase stored in keychain for vault registered for auto unlocking: {}", vault.getPath()); - } else { - vault.unlock(CharBuffer.wrap(storedPw)); - // TODO - // Platform.runLater(() -> vault.setState(VaultState.UNLOCKED)); - LOG.info("Unlocked vault {}", vault.getDisplayablePath()); - } - } catch (IOException | Volume.VolumeException | KeychainAccessException e) { - LOG.error("Auto unlock failed.", e); - } finally { - if (storedPw != null) { - Arrays.fill(storedPw, ' '); - } - } - } - -} 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 0fd9425d0..f44071d86 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 @@ -1,6 +1,8 @@ package org.cryptomator.ui.launcher; +import javafx.collections.ObservableList; import org.cryptomator.common.settings.Settings; +import org.cryptomator.common.vaults.Vault; import org.cryptomator.jni.JniException; import org.cryptomator.jni.MacApplicationUiState; import org.cryptomator.jni.MacFunctions; @@ -13,9 +15,8 @@ import javax.inject.Inject; import javax.inject.Singleton; import java.awt.Desktop; import java.awt.SystemTray; -import java.awt.desktop.AppReopenedEvent; import java.awt.desktop.AppReopenedListener; -import java.awt.desktop.SystemEventListener; +import java.util.Collection; import java.util.Optional; @Singleton @@ -24,19 +25,19 @@ public class UiLauncher { private static final Logger LOG = LoggerFactory.getLogger(UiLauncher.class); private final Settings settings; + private final ObservableList vaults; private final TrayMenuComponent.Builder trayComponent; private final FxApplicationStarter fxApplicationStarter; private final AppLaunchEventHandler launchEventHandler; - private final AutoUnlocker autoUnlocker; private final Optional macFunctions; @Inject - public UiLauncher(Settings settings, TrayMenuComponent.Builder trayComponent, FxApplicationStarter fxApplicationStarter, AppLaunchEventHandler launchEventHandler, AutoUnlocker autoUnlocker, Optional macFunctions) { + public UiLauncher(Settings settings, ObservableList vaults, TrayMenuComponent.Builder trayComponent, FxApplicationStarter fxApplicationStarter, AppLaunchEventHandler launchEventHandler, Optional macFunctions) { this.settings = settings; + this.vaults = vaults; this.trayComponent = trayComponent; this.fxApplicationStarter = fxApplicationStarter; this.launchEventHandler = launchEventHandler; - this.autoUnlocker = autoUnlocker; this.macFunctions = macFunctions; } @@ -59,9 +60,12 @@ public class UiLauncher { // register app reopen listener Desktop.getDesktop().addAppEventListener((AppReopenedListener) e -> showMainWindowAsync(hasTrayIcon)); - - // auto unlock - no shit! - autoUnlocker.autoUnlock(); + + // auto unlock + Collection vaultsWithAutoUnlockEnabled = vaults.filtered(v -> v.getVaultSettings().unlockAfterStartup().get()); + if (!vaultsWithAutoUnlockEnabled.isEmpty()) { + fxApplicationStarter.get(hasTrayIcon).thenAccept(app -> app.getVaultService().attemptAutoUnlock(vaultsWithAutoUnlockEnabled)); + } launchEventHandler.startHandlingLaunchEvents(hasTrayIcon); } 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 837dc827f..ec9013d51 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 @@ -11,6 +11,7 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.WritableValue; +import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.scene.Scene; import javafx.scene.control.CheckBox; @@ -27,6 +28,7 @@ import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.Tasks; +import org.cryptomator.ui.common.VaultService; import org.cryptomator.ui.controls.NiceSecurePasswordField; import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent; import org.slf4j.Logger; @@ -50,22 +52,24 @@ public class UnlockController implements FxController { private final ExecutorService executor; private final ObjectBinding unlockButtonState; private final Optional keychainAccess; + private final VaultService vaultService; private final Lazy successScene; private final Lazy invalidMountPointScene; private final Lazy genericErrorScene; - private final ObjectProperty genericErrorCause; + private final ObjectProperty genericErrorCause; private final ForgetPasswordComponent.Builder forgetPassword; private final BooleanProperty unlockButtonDisabled; public NiceSecurePasswordField passwordField; public CheckBox savePassword; @Inject - public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor, Optional keychainAccess, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, @FxmlScene(FxmlFile.UNLOCK_GENERIC_ERROR) Lazy genericErrorScene, @Named("genericErrorCause") ObjectProperty genericErrorCause, ForgetPasswordComponent.Builder forgetPassword) { + public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor, Optional keychainAccess, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, @FxmlScene(FxmlFile.UNLOCK_GENERIC_ERROR) Lazy genericErrorScene, @Named("genericErrorCause") ObjectProperty genericErrorCause, ForgetPasswordComponent.Builder forgetPassword) { this.window = window; this.vault = vault; this.executor = executor; this.unlockButtonState = Bindings.createObjectBinding(this::getUnlockButtonState, vault.stateProperty()); this.keychainAccess = keychainAccess; + this.vaultService = vaultService; this.successScene = successScene; this.invalidMountPointScene = invalidMountPointScene; this.genericErrorScene = genericErrorScene; @@ -93,36 +97,36 @@ public class UnlockController implements FxController { public void unlock() { LOG.trace("UnlockController.unlock()"); CharSequence password = passwordField.getCharacters(); - vault.setState(VaultState.PROCESSING); - Tasks.create(() -> { - vault.unlock(password); + + Task task = vaultService.createUnlockTask(vault, password); + task.setOnSucceeded(event -> { if (keychainAccess.isPresent() && savePassword.isSelected()) { - keychainAccess.get().storePassphrase(vault.getId(), password); + try { + keychainAccess.get().storePassphrase(vault.getId(), password); + } catch (KeychainAccessException e) { + LOG.error("Failed to store passphrase in system keychain.", e); + } } - }).onSuccess(() -> { - vault.setState(VaultState.UNLOCKED); passwordField.swipe(); LOG.info("Unlock of '{}' succeeded.", vault.getDisplayableName()); window.setScene(successScene.get()); - }).onError(InvalidPassphraseException.class, e -> { - shakeWindow(); - passwordField.selectAll(); - passwordField.requestFocus(); - }).onError(NotDirectoryException.class, e -> { - LOG.error("Unlock failed. Mount point not a directory: {}", e.getMessage()); - window.setScene(invalidMountPointScene.get()); - }).onError(DirectoryNotEmptyException.class, e -> { - LOG.error("Unlock failed. Mount point not empty: {}", e.getMessage()); - window.setScene(invalidMountPointScene.get()); - }).onError(Exception.class, e -> { // including RuntimeExceptions - LOG.error("Unlock failed for technical reasons.", e); - genericErrorCause.set(e); - window.setScene(genericErrorScene.get()); - }).andFinally(() -> { - if (!vault.isUnlocked()) { - vault.setState(VaultState.LOCKED); + }); + task.setOnFailed(event -> { + if (task.getException() instanceof InvalidPassphraseException) { + shakeWindow(); + passwordField.selectAll(); + passwordField.requestFocus(); + } else if (task.getException() instanceof NotDirectoryException + || task.getException() instanceof DirectoryNotEmptyException) { + LOG.error("Unlock failed. Mount point not an empty directory: {}", task.getException().getMessage()); + window.setScene(invalidMountPointScene.get()); + } else { + LOG.error("Unlock failed for technical reasons.", task.getException()); + genericErrorCause.set(task.getException()); + window.setScene(genericErrorScene.get()); } - }).runOnce(executor); + }); + executor.execute(task); } /* Save Password */ 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 0ab1f0c62..d06c9daed 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 @@ -50,7 +50,7 @@ abstract class UnlockModule { @Provides @Named("genericErrorCause") @UnlockScoped - static ObjectProperty provideGenericErrorCause() { + static ObjectProperty provideGenericErrorCause() { return new SimpleObjectProperty<>(); } @@ -109,7 +109,7 @@ abstract class UnlockModule { @Provides @IntoMap @FxControllerKey(StackTraceController.class) - static FxController provideStackTraceController(@Named("genericErrorCause") ObjectProperty errorCause) { + static FxController provideStackTraceController(@Named("genericErrorCause") ObjectProperty errorCause) { return new StackTraceController(errorCause.get()); } diff --git a/main/ui/src/main/resources/fxml/vault_options_general.fxml b/main/ui/src/main/resources/fxml/vault_options_general.fxml index dc35c6e2a..856284284 100644 --- a/main/ui/src/main/resources/fxml/vault_options_general.fxml +++ b/main/ui/src/main/resources/fxml/vault_options_general.fxml @@ -15,6 +15,6 @@