This commit is contained in:
Sebastian Stenzel
2020-05-07 14:18:09 +02:00
parent 117fe78a4a
commit fecf9c0423
17 changed files with 375 additions and 320 deletions

View File

@@ -27,7 +27,7 @@ public class AddVaultSuccessController implements FxController {
@FXML
public void unlockAndClose() {
close();
fxApplication.showUnlockWindow(vault.get());
fxApplication.startUnlockWorkflow(vault.get());
}
@FXML

View File

@@ -0,0 +1,51 @@
package org.cryptomator.ui.common;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class UserInteractionLock<E extends Enum> {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private final BooleanProperty awaitingInteraction = new SimpleBooleanProperty();
private volatile E state;
public UserInteractionLock(E initialValue) {
state = initialValue;
}
public void interacted(E result) {
assert Platform.isFxApplicationThread();
lock.lock();
try {
state = result;
awaitingInteraction.set(false);
condition.signal();
} finally {
lock.unlock();
}
}
public E awaitInteraction() throws InterruptedException {
assert !Platform.isFxApplicationThread();
lock.lock();
try {
Platform.runLater(() -> awaitingInteraction.set(true));
condition.await();
return state;
} finally {
lock.unlock();
}
}
public ReadOnlyBooleanProperty awaitingInteraction() {
return awaitingInteraction;
}
}

View File

@@ -52,62 +52,6 @@ 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 {
List<Task<Vault>> unlockTasks = vaults.stream().map(v -> createAutoUnlockTask(v, keychain.get())).collect(Collectors.toList());
Task<Collection<Vault>> runSequentiallyTask = new RunSequentiallyTask(unlockTasks);
executorService.execute(runSequentiallyTask);
}
}
/**
* 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.
*
@@ -209,116 +153,6 @@ public class VaultService {
}
}
/**
* A task that runs a list of tasks in their given order
*/
private static class RunSequentiallyTask extends Task<Collection<Vault>> {
private final List<Task<Vault>> tasks;
public RunSequentiallyTask(List<Task<Vault>> tasks) {
this.tasks = List.copyOf(tasks);
}
@Override
protected List<Vault> call() throws ExecutionException, InterruptedException {
List<Vault> completed = new ArrayList<>();
for (Task<Vault> task : tasks) {
task.run();
Vault done = task.get();
completed.add(done);
}
return completed;
}
}
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
*/

View File

@@ -94,8 +94,8 @@ public class NiceSecurePasswordField extends StackPane {
passwordField.setPassword(password);
}
public void swipe() {
passwordField.swipe();
public void wipe() {
passwordField.wipe();
}
public void selectAll() {

View File

@@ -40,7 +40,7 @@ import java.util.Arrays;
*/
public class SecurePasswordField extends TextField {
private static final char SWIPE_CHAR = ' ';
private static final char WIPE_CHAR = ' ';
private static final int INITIAL_BUFFER_SIZE = 50;
private static final int GROW_BUFFER_SIZE = 50;
private static final String DEFAULT_PLACEHOLDER = "";
@@ -103,7 +103,7 @@ public class SecurePasswordField extends TextField {
if (e.getCode() == KeyCode.CAPS) {
updateCapsLocked();
} else if (SHORTCUT_BACKSPACE.match(e)) {
swipe();
wipe();
}
}
@@ -189,7 +189,7 @@ public class SecurePasswordField extends TextField {
if (length > content.length) {
char[] newContent = new char[length + GROW_BUFFER_SIZE];
System.arraycopy(content, 0, newContent, 0, content.length);
swipe(content);
wipe(content);
this.content = newContent;
}
}
@@ -201,7 +201,7 @@ public class SecurePasswordField extends TextField {
* @implNote The CharSequence will not copy the backing char[].
* Therefore any mutation to the SecurePasswordField's content will mutate or eventually swipe the returned CharSequence.
* @implSpec The CharSequence is usually in <a href="https://www.unicode.org/glossary/#normalization_form_c">NFC</a> representation (unless NFD-encoded char[] is set via {@link #setPassword(char[])}).
* @see #swipe()
* @see #wipe()
*/
@Override
public CharSequence getCharacters() {
@@ -220,7 +220,7 @@ public class SecurePasswordField extends TextField {
buf[i] = password.charAt(i);
}
setPassword(buf);
Arrays.fill(buf, SWIPE_CHAR);
Arrays.fill(buf, WIPE_CHAR);
}
/**
@@ -231,7 +231,7 @@ public class SecurePasswordField extends TextField {
* @param password
*/
public void setPassword(char[] password) {
swipe();
wipe();
content = Arrays.copyOf(password, password.length);
length = password.length;
@@ -242,14 +242,14 @@ public class SecurePasswordField extends TextField {
/**
* Destroys the stored password by overriding each character with a different character.
*/
public void swipe() {
swipe(content);
public void wipe() {
wipe(content);
length = 0;
setText(null);
}
private void swipe(char[] buffer) {
Arrays.fill(buffer, SWIPE_CHAR);
private void wipe(char[] buffer) {
Arrays.fill(buffer, WIPE_CHAR);
}
/* Observable Properties */

View File

@@ -27,6 +27,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Provider;
import java.awt.desktop.QuitResponse;
import java.util.Optional;
@@ -38,20 +39,20 @@ public class FxApplication extends Application {
private final Settings settings;
private final Lazy<MainWindowComponent> mainWindow;
private final Lazy<PreferencesComponent> preferencesWindow;
private final UnlockComponent.Builder unlockWindowBuilder;
private final QuitComponent.Builder quitWindowBuilder;
private final Provider<UnlockComponent.Builder> unlockWindowBuilderProvider;
private final Provider<QuitComponent.Builder> quitWindowBuilderProvider;
private final Optional<MacFunctions> macFunctions;
private final VaultService vaultService;
private final LicenseHolder licenseHolder;
private final BooleanBinding hasVisibleStages;
@Inject
FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, UnlockComponent.Builder unlockWindowBuilder, QuitComponent.Builder quitWindowBuilder, Optional<MacFunctions> macFunctions, VaultService vaultService, LicenseHolder licenseHolder, ObservableSet<Stage> visibleStages) {
FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Provider<UnlockComponent.Builder> unlockWindowBuilderProvider, Provider<QuitComponent.Builder> quitWindowBuilderProvider, Optional<MacFunctions> macFunctions, VaultService vaultService, LicenseHolder licenseHolder, ObservableSet<Stage> visibleStages) {
this.settings = settings;
this.mainWindow = mainWindow;
this.preferencesWindow = preferencesWindow;
this.unlockWindowBuilder = unlockWindowBuilder;
this.quitWindowBuilder = quitWindowBuilder;
this.unlockWindowBuilderProvider = unlockWindowBuilderProvider;
this.quitWindowBuilderProvider = quitWindowBuilderProvider;
this.macFunctions = macFunctions;
this.vaultService = vaultService;
this.licenseHolder = licenseHolder;
@@ -95,16 +96,16 @@ public class FxApplication extends Application {
});
}
public void showUnlockWindow(Vault vault) {
public void startUnlockWorkflow(Vault vault) {
Platform.runLater(() -> {
unlockWindowBuilder.vault(vault).build().showUnlockWindow();
unlockWindowBuilderProvider.get().vault(vault).build().startUnlockWorkflow();
LOG.debug("Showing UnlockWindow for {}", vault.getDisplayableName());
});
}
public void showQuitWindow(QuitResponse response) {
Platform.runLater(() -> {
quitWindowBuilder.quitResponse(response).build().showQuitWindow();
quitWindowBuilderProvider.get().quitResponse(response).build().showQuitWindow();
LOG.debug("Showing QuitWindow");
});
}

View File

@@ -64,7 +64,11 @@ public class UiLauncher {
// auto unlock
Collection<Vault> vaultsWithAutoUnlockEnabled = vaults.filtered(v -> v.getVaultSettings().unlockAfterStartup().get());
if (!vaultsWithAutoUnlockEnabled.isEmpty()) {
fxApplicationStarter.get(hasTrayIcon).thenAccept(app -> app.getVaultService().attemptAutoUnlock(vaultsWithAutoUnlockEnabled));
fxApplicationStarter.get(hasTrayIcon).thenAccept(app -> {
for (Vault vault : vaultsWithAutoUnlockEnabled){
app.startUnlockWorkflow(vault);
}
});
}
launchEventHandler.startHandlingLaunchEvents(hasTrayIcon);

View File

@@ -26,7 +26,7 @@ public class VaultDetailLockedController implements FxController {
@FXML
public void unlock() {
application.showUnlockWindow(vault.get());
application.startUnlockWorkflow(vault.get());
}
@FXML

View File

@@ -121,7 +121,7 @@ public class MigrationRunController implements FxController {
} else {
LOG.info("Migration of '{}' succeeded.", vault.getDisplayableName());
vault.setState(VaultState.LOCKED);
passwordField.swipe();
passwordField.wipe();
window.setScene(successScene.get());
}
}).onError(InvalidPassphraseException.class, e -> {

View File

@@ -1,8 +1,5 @@
package org.cryptomator.ui.migration;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
@@ -28,7 +25,7 @@ public class MigrationSuccessController implements FxController {
@FXML
public void unlockAndClose() {
close();
fxApplication.showUnlockWindow(vault);
fxApplication.startUnlockWorkflow(vault);
}
@FXML

View File

@@ -103,7 +103,7 @@ class TrayMenuController {
}
private void unlockVault(Vault vault) {
fxApplicationStarter.get(true).thenAccept(app -> app.showUnlockWindow(vault));
fxApplicationStarter.get(true).thenAccept(app -> app.startUnlockWorkflow(vault));
}
private void lockVault(Vault vault) {

View File

@@ -14,21 +14,23 @@ import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.common.vaults.Vault;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
@UnlockScoped
@Subcomponent(modules = {UnlockModule.class})
public interface UnlockComponent {
@UnlockWindow
Stage window();
ExecutorService defaultExecutorService();
@FxmlScene(FxmlFile.UNLOCK)
Lazy<Scene> scene();
default Stage showUnlockWindow() {
Stage stage = window();
stage.setScene(scene().get());
stage.show();
return stage;
UnlockWorkflow unlockWorkflow();
default Future<Boolean> startUnlockWorkflow() {
UnlockWorkflow workflow = unlockWorkflow();
defaultExecutorService().submit(workflow);
return workflow;
}
@Subcomponent.Builder

View File

@@ -1,39 +1,29 @@
package org.cryptomator.ui.unlock;
import dagger.Lazy;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContentDisplay;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.NotDirectoryException;
import java.util.Arrays;
import javax.inject.Named;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@UnlockScoped
public class UnlockController implements FxController {
@@ -42,124 +32,67 @@ public class UnlockController implements FxController {
private final Stage window;
private final Vault vault;
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 ErrorComponent.Builder errorComponent;
private final AtomicReference<char[]> password;
private final AtomicBoolean savePassword;
private final Optional<char[]> savedPassword;
private final UserInteractionLock<UnlockModule.PasswordEntry> passwordEntryLock;
private final ForgetPasswordComponent.Builder forgetPassword;
private final Optional<KeychainAccess> keychainAccess;
private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay;
private final BooleanBinding userInteractionDisabled;
private final BooleanProperty unlockButtonDisabled;
public NiceSecurePasswordField passwordField;
public CheckBox savePassword;
public CheckBox savePasswordCheckbox;
@Inject
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, ErrorComponent.Builder errorComponent, ForgetPasswordComponent.Builder forgetPassword) {
public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<UnlockModule.PasswordEntry> passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, Optional<KeychainAccess> keychainAccess) {
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.errorComponent = errorComponent;
this.password = password;
this.savePassword = savePassword;
this.savedPassword = savedPassword;
this.passwordEntryLock = passwordEntryLock;
this.forgetPassword = forgetPassword;
this.keychainAccess = keychainAccess;
this.unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, passwordEntryLock.awaitingInteraction());
this.userInteractionDisabled = passwordEntryLock.awaitingInteraction().not();
this.unlockButtonDisabled = new SimpleBooleanProperty();
}
public void initialize() {
if (keychainAccess.isPresent()) {
loadStoredPassword();
} else {
savePassword.setSelected(false);
savePasswordCheckbox.setSelected(savedPassword.isPresent());
if (password.get() != null) {
passwordField.setPassword(password.get());
}
unlockButtonDisabled.bind(vault.stateProperty().isNotEqualTo(VaultState.LOCKED).or(passwordField.textProperty().isEmpty()));
unlockButtonDisabled.bind(userInteractionDisabled.or(passwordField.textProperty().isEmpty()));
}
@FXML
public void cancel() {
LOG.debug("Unlock canceled by user.");
window.close();
passwordEntryLock.interacted(UnlockModule.PasswordEntry.CANCELED);
}
@FXML
public void unlock() {
LOG.trace("UnlockController.unlock()");
CharSequence password = passwordField.getCharacters();
Task<Vault> task = vaultService.createUnlockTask(vault, password);
passwordField.setDisable(true);
task.setOnSucceeded(event -> {
passwordField.setDisable(false);
if (keychainAccess.isPresent() && savePassword.isSelected()) {
try {
keychainAccess.get().storePassphrase(vault.getId(), password);
} catch (KeychainAccessException e) {
LOG.error("Failed to store passphrase in system keychain.", e);
}
}
passwordField.swipe();
LOG.info("Unlock of '{}' succeeded.", vault.getDisplayableName());
window.setScene(successScene.get());
});
task.setOnFailed(event -> {
passwordField.setDisable(false);
if (task.getException() instanceof InvalidPassphraseException) {
Animations.createShakeWindowAnimation(window).play();
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());
errorComponent.cause(task.getException()).window(window).returnToScene(window.getScene()).build().showErrorScene();
}
});
executor.execute(task);
CharSequence pwFieldContents = passwordField.getCharacters();
char[] pw = new char[pwFieldContents.length()];
for (int i = 0; i < pwFieldContents.length(); i++) {
pw[i] = pwFieldContents.charAt(i);
}
password.set(pw);
passwordEntryLock.interacted(UnlockModule.PasswordEntry.PASSWORD_ENTERED);
}
/* Save Password */
@FXML
private void didClickSavePasswordCheckbox() {
if (!savePassword.isSelected() && hasStoredPassword()) {
forgetPassword.vault(vault).owner(window).build().showForgetPassword().thenAccept(forgotten -> savePassword.setSelected(!forgotten));
}
}
private void loadStoredPassword() {
assert keychainAccess.isPresent();
char[] storedPw = null;
try {
storedPw = keychainAccess.get().loadPassphrase(vault.getId());
if (storedPw != null) {
savePassword.setSelected(true);
passwordField.setPassword(storedPw);
passwordField.selectRange(storedPw.length, storedPw.length);
}
} catch (KeychainAccessException e) {
LOG.error("Failed to load entry from system keychain.", e);
} finally {
if (storedPw != null) {
Arrays.fill(storedPw, ' ');
}
}
}
private boolean hasStoredPassword() {
char[] storedPw = null;
try {
storedPw = keychainAccess.get().loadPassphrase(vault.getId());
return storedPw != null;
} catch (KeychainAccessException e) {
return false;
} finally {
if (storedPw != null) {
Arrays.fill(storedPw, ' ');
}
savePassword.set(savePasswordCheckbox.isSelected());
if (!savePasswordCheckbox.isSelected() && savedPassword.isPresent()) {
forgetPassword.vault(vault).owner(window).build().showForgetPassword().thenAccept(forgotten -> savePasswordCheckbox.setSelected(!forgotten));
}
}
@@ -169,15 +102,20 @@ public class UnlockController implements FxController {
return vault;
}
public ObjectBinding<ContentDisplay> unlockButtonStateProperty() {
return unlockButtonState;
public ObjectBinding<ContentDisplay> unlockButtonContentDisplayProperty() {
return unlockButtonContentDisplay;
}
public ContentDisplay getUnlockButtonState() {
return switch (vault.getState()) {
case PROCESSING -> ContentDisplay.LEFT;
default -> ContentDisplay.TEXT_ONLY;
};
public ContentDisplay getUnlockButtonContentDisplay() {
return passwordEntryLock.awaitingInteraction().get() ? ContentDisplay.TEXT_ONLY : ContentDisplay.LEFT;
}
public BooleanBinding userInteractionDisabledProperty() {
return userInteractionDisabled;
}
public boolean isUserInteractionDisabled() {
return userInteractionDisabled.get();
}
public ReadOnlyBooleanProperty unlockButtonDisabledProperty() {

View File

@@ -9,6 +9,8 @@ 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.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController;
@@ -16,17 +18,64 @@ import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
import org.slf4j.Logger;
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 {
private static final Logger LOG = LoggerFactory.getLogger(UnlockModule.class);
public enum PasswordEntry {PASSWORD_ENTERED, CANCELED}
@Provides
@UnlockScoped
static UserInteractionLock<PasswordEntry> providePasswordEntryLock() {
return new UserInteractionLock<>(null);
}
@Provides
@Named("savedPassword")
@UnlockScoped
static Optional<char[]> provideStoredPassword(Optional<KeychainAccess> keychainAccess, @UnlockWindow Vault vault) {
return keychainAccess.map(k -> {
try {
return k.loadPassphrase(vault.getId());
} catch (KeychainAccessException e) {
LOG.error("Failed to load entry from system keychain.", e);
return null;
}
});
}
@Provides
@UnlockScoped
static AtomicReference<char[]> providePassword(@Named("savedPassword") Optional<char[]> storedPassword) {
return new AtomicReference(storedPassword.orElse(null));
}
@Provides
@Named("savePassword")
@UnlockScoped
static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional<char[]> storedPassword) {
return new AtomicBoolean(storedPassword.isPresent());
}
@Provides
@UnlockWindow
@UnlockScoped

View File

@@ -0,0 +1,179 @@
package org.cryptomator.ui.unlock;
import dagger.Lazy;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.stage.Stage;
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.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.unlock.UnlockModule.PasswordEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.IOException;
import java.nio.CharBuffer;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.FileSystemException;
import java.nio.file.NotDirectoryException;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/**
* A multi-step task that consists of background activities as well as user interaction.
* <p>
* This class runs the unlock process and controls when to display which UI.
*/
@UnlockScoped
public class UnlockWorkflow extends Task<Boolean> {
private static final Logger LOG = LoggerFactory.getLogger(UnlockWorkflow.class);
private final Stage window;
private final Vault vault;
private final AtomicReference<char[]> password;
private final AtomicBoolean savePassword;
private final Optional<char[]> savedPassword;
private final UserInteractionLock<PasswordEntry> passwordEntryLock;
private final Optional<KeychainAccess> keychain;
private final Lazy<Scene> unlockScene;
private final Lazy<Scene> successScene;
private final Lazy<Scene> invalidMountPointScene;
private final ErrorComponent.Builder errorComponent;
@Inject
UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<PasswordEntry> passwordEntryLock, Optional<KeychainAccess> keychain, @FxmlScene(FxmlFile.UNLOCK) Lazy<Scene> unlockScene, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, ErrorComponent.Builder errorComponent) {
this.window = window;
this.vault = vault;
this.password = password;
this.savePassword = savePassword;
this.savedPassword = savedPassword;
this.passwordEntryLock = passwordEntryLock;
this.keychain = keychain;
this.unlockScene = unlockScene;
this.successScene = successScene;
this.invalidMountPointScene = invalidMountPointScene;
this.errorComponent = errorComponent;
}
@Override
protected Boolean call() throws InterruptedException, IOException, Volume.VolumeException {
try {
if (attemptUnlock()) {
handleSuccess();
return true;
} else {
cancel(false); // set Tasks state to cancelled
return false;
}
} catch (NotDirectoryException | DirectoryNotEmptyException e) {
handleInvalidMountPoint(e);
throw e; // rethrow to trigger correct exception handling in Task
} catch (CryptoException | Volume.VolumeException | IOException e) {
handleGenericError(e);
throw e; // rethrow to trigger correct exception handling in Task
} finally {
wipePassword(password.get());
wipePassword(savedPassword.orElse(null));
}
}
private boolean attemptUnlock() throws InterruptedException, IOException, Volume.VolumeException {
boolean proceed = password.get() != null || askForPassword(false) == PasswordEntry.PASSWORD_ENTERED;
while (proceed) {
try {
vault.unlock(CharBuffer.wrap(password.get()));
return true;
} catch (InvalidPassphraseException e) {
proceed = askForPassword(true) == PasswordEntry.PASSWORD_ENTERED;
}
}
return false;
}
private PasswordEntry askForPassword(boolean animateShake) throws InterruptedException {
Platform.runLater(() -> {
window.setScene(unlockScene.get());
window.show();
if (animateShake) {
Animations.createShakeWindowAnimation(window).play();
}
});
return passwordEntryLock.awaitInteraction();
}
private void handleSuccess() {
LOG.info("Unlock of '{}' succeeded.", vault.getDisplayableName());
if (savePassword.get()) {
savePasswordToSystemkeychain();
}
Platform.runLater(() -> {
window.setScene(successScene.get()); // TODO only if enabled (see issue #1083)
});
}
private void savePasswordToSystemkeychain() {
if (keychain.isPresent()) {
try {
keychain.get().storePassphrase(vault.getId(), CharBuffer.wrap(password.get()));
} catch (KeychainAccessException e) {
LOG.error("Failed to store passphrase in system keychain.", e);
}
}
}
private void handleInvalidMountPoint(FileSystemException e) {
LOG.error("Unlock failed. Mount point not an empty directory: {}", e.getMessage());
Platform.runLater(() -> {
window.setScene(invalidMountPointScene.get());
});
}
private void handleGenericError(Exception e) {
LOG.error("Unlock failed for technical reasons.", e);
Platform.runLater(() -> {
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
});
}
private void wipePassword(char[] pw) {
if (pw != null) {
Arrays.fill(pw, ' ');
}
}
@Override
protected void scheduled() {
vault.setState(VaultState.PROCESSING);
}
@Override
protected void succeeded() {
vault.setState(VaultState.UNLOCKED);
}
@Override
protected void failed() {
vault.setState(VaultState.LOCKED);
}
@Override
protected void cancelled() {
vault.setState(VaultState.LOCKED);
}
}

View File

@@ -21,15 +21,15 @@
<children>
<VBox spacing="6">
<FormattedLabel format="%unlock.passwordPrompt" arg1="${controller.vault.displayableName}" wrapText="true"/>
<NiceSecurePasswordField fx:id="passwordField"/>
<CheckBox fx:id="savePassword" text="%unlock.savePassword" onAction="#didClickSavePasswordCheckbox" disable="${controller.vault.processing}" visible="${controller.keychainAccessAvailable}"/>
<NiceSecurePasswordField fx:id="passwordField" disable="${controller.userInteractionDisabled}"/>
<CheckBox fx:id="savePasswordCheckbox" text="%unlock.savePassword" onAction="#didClickSavePasswordCheckbox" disable="${controller.userInteractionDisabled}" visible="${controller.keychainAccessAvailable}"/>
</VBox>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<ButtonBar buttonMinWidth="120" buttonOrder="+CI">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel" disable="${controller.vault.processing}"/>
<Button text="%unlock.unlockBtn" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#unlock" contentDisplay="${controller.unlockButtonState}" disable="${controller.unlockButtonDisabled}">
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel" disable="${controller.userInteractionDisabled}"/>
<Button text="%unlock.unlockBtn" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#unlock" contentDisplay="${controller.unlockButtonContentDisplay}" disable="${controller.unlockButtonDisabled}">
<graphic>
<ProgressIndicator progress="-1" prefWidth="12" prefHeight="12"/>
</graphic>

View File

@@ -152,7 +152,7 @@ class SecurePasswordFieldTest {
CharSequence result1 = pwField.getCharacters();
Assertions.assertEquals("topSecret", result1.toString());
pwField.swipe();
pwField.wipe();
CharSequence result2 = pwField.getCharacters();
Assertions.assertEquals(" ", result1.toString());
Assertions.assertEquals("", result2.toString());