(Auto)Unlock via VaultService (fixes #1044)

This commit is contained in:
Sebastian Stenzel
2020-02-13 23:50:26 +01:00
parent 57256d0733
commit f62faa72ce
8 changed files with 210 additions and 115 deletions

View File

@@ -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);

View File

@@ -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<KeychainAccess> keychain;
@Inject
public VaultService(ExecutorService executorService) {
public VaultService(ExecutorService executorService, Optional<KeychainAccess> 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<Vault> 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<Vault> createAutoUnlockTask(Vault vault, KeychainAccess keychainAccess) {
Task<Vault> 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<Vault> createUnlockTask(Vault vault, CharSequence passphrase) {
Task<Vault> 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<Vault> createLockTask(Vault vault, boolean forced) {
Task<Vault> task = new LockVaultTask(vault, forced);
@@ -145,6 +219,93 @@ public class VaultService {
}
}
private static class AutoUnlockVaultTask extends Task<Vault> {
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<Vault> {
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 {
}
}

View File

@@ -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<Vault> vaults;
private final Optional<KeychainAccess> keychain;
@Inject
AutoUnlocker(ObservableList<Vault> vaults, Optional<KeychainAccess> 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, ' ');
}
}
}
}

View File

@@ -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<Vault> vaults;
private final TrayMenuComponent.Builder trayComponent;
private final FxApplicationStarter fxApplicationStarter;
private final AppLaunchEventHandler launchEventHandler;
private final AutoUnlocker autoUnlocker;
private final Optional<MacFunctions> macFunctions;
@Inject
public UiLauncher(Settings settings, TrayMenuComponent.Builder trayComponent, FxApplicationStarter fxApplicationStarter, AppLaunchEventHandler launchEventHandler, AutoUnlocker autoUnlocker, Optional<MacFunctions> macFunctions) {
public UiLauncher(Settings settings, ObservableList<Vault> vaults, TrayMenuComponent.Builder trayComponent, FxApplicationStarter fxApplicationStarter, AppLaunchEventHandler launchEventHandler, Optional<MacFunctions> 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<Vault> vaultsWithAutoUnlockEnabled = vaults.filtered(v -> v.getVaultSettings().unlockAfterStartup().get());
if (!vaultsWithAutoUnlockEnabled.isEmpty()) {
fxApplicationStarter.get(hasTrayIcon).thenAccept(app -> app.getVaultService().attemptAutoUnlock(vaultsWithAutoUnlockEnabled));
}
launchEventHandler.startHandlingLaunchEvents(hasTrayIcon);
}

View File

@@ -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<ContentDisplay> unlockButtonState;
private final Optional<KeychainAccess> keychainAccess;
private final VaultService vaultService;
private final Lazy<Scene> successScene;
private final Lazy<Scene> invalidMountPointScene;
private final Lazy<Scene> genericErrorScene;
private final ObjectProperty<Exception> genericErrorCause;
private final ObjectProperty<Throwable> 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> keychainAccess, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, @FxmlScene(FxmlFile.UNLOCK_GENERIC_ERROR) Lazy<Scene> genericErrorScene, @Named("genericErrorCause") ObjectProperty<Exception> genericErrorCause, ForgetPasswordComponent.Builder forgetPassword) {
public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor, Optional<KeychainAccess> keychainAccess, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, @FxmlScene(FxmlFile.UNLOCK_GENERIC_ERROR) Lazy<Scene> genericErrorScene, @Named("genericErrorCause") ObjectProperty<Throwable> 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<Vault> 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 */

View File

@@ -50,7 +50,7 @@ abstract class UnlockModule {
@Provides
@Named("genericErrorCause")
@UnlockScoped
static ObjectProperty<Exception> provideGenericErrorCause() {
static ObjectProperty<Throwable> provideGenericErrorCause() {
return new SimpleObjectProperty<>();
}
@@ -109,7 +109,7 @@ abstract class UnlockModule {
@Provides
@IntoMap
@FxControllerKey(StackTraceController.class)
static FxController provideStackTraceController(@Named("genericErrorCause") ObjectProperty<Exception> errorCause) {
static FxController provideStackTraceController(@Named("genericErrorCause") ObjectProperty<Throwable> errorCause) {
return new StackTraceController(errorCause.get());
}

View File

@@ -15,6 +15,6 @@
<Button text="%vaultOptions.general.changePasswordBtn" onAction="#changePassword"/>
<Button text="%vaultOptions.general.showRecoveryKeyBtn" onAction="#showRecoveryKey"/>
<Button text="TODO recoverVault" onAction="#showRecoverVaultDialogue"/>
<CheckBox text="TODO unlock after startup" fx:id="unlockOnStartupCheckbox"/>
<CheckBox text="%vaultOptions.general.unlockAfterStartup" fx:id="unlockOnStartupCheckbox"/>
</children>
</VBox>

View File

@@ -208,3 +208,4 @@ passwordStrength.messageLabel.4=Very strong
# Quit
quit.prompt=Quit application? There are unlocked vaults.
quit.lockAndQuit=Lock and Quit
vaultOptions.general.unlockAfterStartup=Unlock vault when starting Cryptomator