split package org.cryptoamtor.ui.unlock to allow for different kinds of unlock workflows

This commit is contained in:
Sebastian Stenzel
2021-03-01 15:58:00 +01:00
parent 7b08c5d287
commit 78e43d401d
16 changed files with 432 additions and 261 deletions
@@ -26,7 +26,7 @@ public enum FxmlFile {
RECOVERYKEY_RESET_PASSWORD("/fxml/recoverykey_reset_password.fxml"), //
RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), //
REMOVE_VAULT("/fxml/remove_vault.fxml"), //
UNLOCK("/fxml/unlock.fxml"),
UNLOCK_ENTER_PASSWORD("/fxml/unlock_enter_password.fxml"),
UNLOCK_INVALID_MOUNT_POINT("/fxml/unlock_invalid_mount_point.fxml"), //
UNLOCK_SELECT_MASTERKEYFILE("/fxml/unlock_select_masterkeyfile.fxml"), //
UNLOCK_SUCCESS("/fxml/unlock_success.fxml"), //
@@ -0,0 +1,14 @@
package org.cryptomator.ui.unlock;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
public class UnlockCancelledException extends MasterkeyLoadingFailedException {
public UnlockCancelledException(String message) {
super(message);
}
public UnlockCancelledException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -17,19 +17,17 @@ import javafx.stage.Stage;
public class UnlockInvalidMountPointController implements FxController {
private final Stage window;
private final Lazy<Scene> unlockScene;
private final Vault vault;
@Inject
UnlockInvalidMountPointController(@UnlockWindow Stage window, @FxmlScene(FxmlFile.UNLOCK) Lazy<Scene> unlockScene, @UnlockWindow Vault vault) {
UnlockInvalidMountPointController(@UnlockWindow Stage window, @UnlockWindow Vault vault) {
this.window = window;
this.unlockScene = unlockScene;
this.vault = vault;
}
@FXML
public void back() {
window.setScene(unlockScene.get());
public void close() {
window.close();
}
/* Getter/Setter */
@@ -4,96 +4,29 @@ import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
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 org.cryptomator.ui.unlock.masterkeyfile.MasterkeyFileLoadingComponent;
import javax.inject.Named;
import javax.inject.Provider;
import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@Module(subcomponents = {ForgetPasswordComponent.class})
@Module(subcomponents = {MasterkeyFileLoadingComponent.class})
abstract class UnlockModule {
private static final Logger LOG = LoggerFactory.getLogger(UnlockModule.class);
public enum PasswordEntry {
PASSWORD_ENTERED,
CANCELED
}
public enum MasterkeyFileProvision {
MASTERKEYFILE_PROVIDED,
CANCELED
}
@Provides
@UnlockScoped
static UserInteractionLock<PasswordEntry> providePasswordEntryLock() {
return new UserInteractionLock<>(null);
}
@Provides
@UnlockScoped
static UserInteractionLock<MasterkeyFileProvision> provideMasterkeyFileProvisionLock() {
return new UserInteractionLock<>(null);
}
@Provides
@Named("savedPassword")
@UnlockScoped
static Optional<char[]> provideStoredPassword(KeychainManager keychain, @UnlockWindow Vault vault) {
if (!keychain.isSupported()) {
return Optional.empty();
} else {
try {
return Optional.ofNullable(keychain.loadPassphrase(vault.getId()));
} catch (KeychainAccessException e) {
LOG.error("Failed to load entry from system keychain.", e);
return Optional.empty();
}
}
}
@Provides
@Named("userProvidedMasterkeyPath")
@UnlockScoped
static AtomicReference<Path> provideUserProvidedMasterkeyPath() {
return new AtomicReference();
}
@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
@@ -117,20 +50,6 @@ abstract class UnlockModule {
return stage;
}
@Provides
@FxmlScene(FxmlFile.UNLOCK)
@UnlockScoped
static Scene provideUnlockScene(@UnlockWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.UNLOCK);
}
@Provides
@FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE)
@UnlockScoped
static Scene provideUnlockSelectMasterkeyFileScene(@UnlockWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE);
}
@Provides
@FxmlScene(FxmlFile.UNLOCK_SUCCESS)
@UnlockScoped
@@ -147,16 +66,6 @@ abstract class UnlockModule {
// ------------------
@Binds
@IntoMap
@FxControllerKey(UnlockController.class)
abstract FxController bindUnlockController(UnlockController controller);
@Binds
@IntoMap
@FxControllerKey(UnlockSelectMasterkeyFileController.class)
abstract FxController bindUnlockSelectMasterkeyFileController(UnlockSelectMasterkeyFileController controller);
@Binds
@IntoMap
@FxControllerKey(UnlockSuccessController.class)
@@ -1,7 +1,6 @@
package org.cryptomator.ui.unlock;
import dagger.Lazy;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.vaults.MountPointRequirement;
import org.cryptomator.common.vaults.Vault;
@@ -12,36 +11,23 @@ import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.cryptolib.common.MasterkeyFileLoaderContext;
import org.cryptomator.integrations.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.common.VaultService;
import org.cryptomator.ui.unlock.UnlockModule.MasterkeyFileProvision;
import org.cryptomator.ui.unlock.UnlockModule.PasswordEntry;
import org.cryptomator.ui.unlock.masterkeyfile.MasterkeyFileLoadingComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.Window;
import java.io.IOException;
import java.nio.CharBuffer;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.NotDirectoryException;
import java.nio.file.Path;
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.
@@ -49,47 +35,29 @@ import java.util.concurrent.atomic.AtomicReference;
* This class runs the unlock process and controls when to display which UI.
*/
@UnlockScoped
public class UnlockWorkflow extends Task<Boolean> implements MasterkeyFileLoaderContext {
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 VaultService vaultService;
private final AtomicReference<char[]> password;
private final AtomicBoolean savePassword;
private final Optional<char[]> savedPassword;
private final AtomicReference<Path> correctMasterkeyPath;
private final UserInteractionLock<PasswordEntry> passwordEntryLock;
private final UserInteractionLock<MasterkeyFileProvision> masterkeyFileProvisionLock;
private final KeychainManager keychain;
private final Lazy<Scene> unlockScene;
private final Lazy<Scene> selectMasterkeyFileScene;
private final Lazy<Scene> successScene;
private final Lazy<Scene> invalidMountPointScene;
private final ErrorComponent.Builder errorComponent;
private final MasterkeyFileAccess masterkeyFileAccess;
private boolean didEnterWrongPassphrase = false;
private final MasterkeyFileLoadingComponent.Builder masterkeyFileLoadingComponent;
@Inject
UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, @Named("userProvidedMasterkeyPath") AtomicReference<Path> correctMasterkeyPath, UserInteractionLock<PasswordEntry> passwordEntryLock, UserInteractionLock<MasterkeyFileProvision> masterkeyFileProvisionLock, KeychainManager keychain, @FxmlScene(FxmlFile.UNLOCK) Lazy<Scene> unlockScene, @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) Lazy<Scene> selectMasterkeyFileScene, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, ErrorComponent.Builder errorComponent, MasterkeyFileAccess masterkeyFileAccess) {
UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, ErrorComponent.Builder errorComponent, MasterkeyFileAccess masterkeyFileAccess, MasterkeyFileLoadingComponent.Builder masterkeyFileLoadingComponent) {
this.window = window;
this.vault = vault;
this.vaultService = vaultService;
this.password = password;
this.savePassword = savePassword;
this.savedPassword = savedPassword;
this.correctMasterkeyPath = correctMasterkeyPath;
this.passwordEntryLock = passwordEntryLock;
this.masterkeyFileProvisionLock = masterkeyFileProvisionLock;
this.keychain = keychain;
this.unlockScene = unlockScene;
this.selectMasterkeyFileScene = selectMasterkeyFileScene;
this.successScene = successScene;
this.invalidMountPointScene = invalidMountPointScene;
this.errorComponent = errorComponent;
this.masterkeyFileAccess = masterkeyFileAccess;
this.masterkeyFileLoadingComponent = masterkeyFileLoadingComponent;
setOnFailed(event -> {
Throwable throwable = event.getSource().getException();
@@ -104,102 +72,32 @@ public class UnlockWorkflow extends Task<Boolean> implements MasterkeyFileLoader
@Override
protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException, CryptoException {
try {
MasterkeyLoader keyLoader = masterkeyFileAccess.keyLoader(vault.getPath(), this);
attemptUnlock(keyLoader, 0);
// TODO: allow unlock strategies other than MasterkeyFile-based eventually
attemptUnlockUsingMasterkeyFile(0, null);
handleSuccess();
return true;
} catch (UnlockCancelledException e) {
cancel(false); // set Tasks state to cancelled
return false;
} finally {
wipePassword(password.get());
wipePassword(savedPassword.orElse(null));
}
}
private void attemptUnlock(MasterkeyLoader keyLoader, int attempt) throws IOException, VolumeException, InvalidMountPointException, CryptoException {
private void attemptUnlockUsingMasterkeyFile(int attempt, Exception previousError) throws IOException, VolumeException, InvalidMountPointException, CryptoException {
var fileLoadingComp = masterkeyFileLoadingComponent.unlockWindow(window).vault(vault).previousError(previousError).build();
boolean success = false;
try {
vault.unlock(keyLoader);
vault.unlock(fileLoadingComp.masterkeyLoader());
success = true;
} catch (InvalidPassphraseException e) {
LOG.info("Unlock attempt #{} failed due to incorrect password", attempt);
wipePassword(password.getAndSet(null));
didEnterWrongPassphrase = true;
attemptUnlock(keyLoader, attempt + 1);
LOG.info("Unlock attempt #{} failed due to {}", attempt, e.getMessage());
attemptUnlockUsingMasterkeyFile(attempt + 1, e);
} finally {
fileLoadingComp.cleanup(success);
}
}
@Override
public Path getCorrectMasterkeyFilePath(String masterkeyFilePath) {
try {
if (askForCorrectMasterkeyFile() == MasterkeyFileProvision.MASTERKEYFILE_PROVIDED) {
return correctMasterkeyPath.get();
} else {
throw new UnlockCancelledException("Password entry cancelled.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new UnlockCancelledException("Password entry interrupted", e);
}
}
private MasterkeyFileProvision askForCorrectMasterkeyFile() throws InterruptedException {
Platform.runLater(() -> {
window.setScene(selectMasterkeyFileScene.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();
}
});
return masterkeyFileProvisionLock.awaitInteraction();
}
@Override
public CharSequence getPassphrase(Path path) throws UnlockCancelledException {
if (password.get() != null) { // e.g. pre-filled from keychain
return CharBuffer.wrap(password.get());
}
assert password.get() == null;
try {
if (askForPassphrase() == PasswordEntry.PASSWORD_ENTERED) {
assert password.get() != null;
return CharBuffer.wrap(password.get());
} else {
throw new UnlockCancelledException("Password entry cancelled.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new UnlockCancelledException("Password entry interrupted", e);
}
}
private PasswordEntry askForPassphrase() throws InterruptedException {
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 (didEnterWrongPassphrase) {
Animations.createShakeWindowAnimation(window).play();
}
});
return passwordEntryLock.awaitInteraction();
}
private void handleSuccess() {
LOG.info("Unlock of '{}' succeeded.", vault.getDisplayName());
if (savePassword.get()) {
savePasswordToSystemkeychain();
}
switch (vault.getVaultSettings().actionAfterUnlock().get()) {
case ASK -> Platform.runLater(() -> {
window.setScene(successScene.get());
@@ -213,16 +111,6 @@ public class UnlockWorkflow extends Task<Boolean> implements MasterkeyFileLoader
}
}
private void savePasswordToSystemkeychain() {
if (keychain.isSupported()) {
try {
keychain.storePassphrase(vault.getId(), CharBuffer.wrap(password.get()));
} catch (KeychainAccessException e) {
LOG.error("Failed to store passphrase in system keychain.", e);
}
}
}
private void handleInvalidMountPoint(InvalidMountPointException impExc) {
MountPointRequirement requirement = vault.getVolume().orElseThrow(() -> new IllegalStateException("Invalid Mountpoint without a Volume?!", impExc)).getMountPointRequirement();
assert requirement != MountPointRequirement.NONE; //An invalid MountPoint with no required MountPoint doesn't seem sensible
@@ -260,12 +148,6 @@ public class UnlockWorkflow extends Task<Boolean> implements MasterkeyFileLoader
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);
@@ -286,13 +168,4 @@ public class UnlockWorkflow extends Task<Boolean> implements MasterkeyFileLoader
vault.setState(VaultState.LOCKED);
}
private static class UnlockCancelledException extends MasterkeyLoadingFailedException {
public UnlockCancelledException(String message) {
super(message);
}
public UnlockCancelledException(String message, Throwable cause) {
super(message, cause);
}
}
}
@@ -0,0 +1,14 @@
package org.cryptomator.ui.unlock.masterkeyfile;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@interface MasterkeyFileLoading {
}
@@ -0,0 +1,38 @@
package org.cryptomator.ui.unlock.masterkeyfile;
import dagger.BindsInstance;
import dagger.Subcomponent;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptolib.common.MasterkeyFileLoader;
import javax.annotation.Nullable;
import javafx.stage.Stage;
@MasterkeyFileLoadingScoped
@Subcomponent(modules = {MasterkeyFileLoadingModule.class})
public interface MasterkeyFileLoadingComponent {
MasterkeyFileLoadingFinisher finisher();
MasterkeyFileLoader masterkeyLoader();
default void cleanup(boolean unlockedSuccessfully) {
finisher().cleanup(unlockedSuccessfully);
}
@Subcomponent.Builder
interface Builder {
@BindsInstance
Builder previousError(@Nullable Exception previousError);
@BindsInstance
Builder vault(@MasterkeyFileLoading Vault vault);
@BindsInstance
Builder unlockWindow(@MasterkeyFileLoading Stage unlockWindow);
MasterkeyFileLoadingComponent build();
}
}
@@ -0,0 +1,118 @@
package org.cryptomator.ui.unlock.masterkeyfile;
import dagger.Lazy;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.common.MasterkeyFileLoaderContext;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.unlock.UnlockCancelledException;
import org.cryptomator.ui.unlock.masterkeyfile.MasterkeyFileLoadingModule.MasterkeyFileProvision;
import org.cryptomator.ui.unlock.masterkeyfile.MasterkeyFileLoadingModule.PasswordEntry;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.security.auth.DestroyFailedException;
import javax.security.auth.Destroyable;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.Window;
import java.nio.CharBuffer;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicReference;
@MasterkeyFileLoadingScoped
class MasterkeyFileLoadingContext implements MasterkeyFileLoaderContext {
private final Stage window;
private final Lazy<Scene> passphraseEntryScene;
private final Lazy<Scene> selectMasterkeyFileScene;
private final UserInteractionLock<PasswordEntry> passwordEntryLock;
private final UserInteractionLock<MasterkeyFileProvision> masterkeyFileProvisionLock;
private final AtomicReference<char[]> password;
private final AtomicReference<Path> filePath;
private final Exception previousError;
@Inject
public MasterkeyFileLoadingContext(@MasterkeyFileLoading Stage window, @FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD) Lazy<Scene> passphraseEntryScene, @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) Lazy<Scene> selectMasterkeyFileScene, UserInteractionLock<PasswordEntry> passwordEntryLock, UserInteractionLock<MasterkeyFileProvision> masterkeyFileProvisionLock, AtomicReference<char[]> password, AtomicReference<Path> filePath, @Nullable Exception previousError) {
this.window = window;
this.passphraseEntryScene = passphraseEntryScene;
this.selectMasterkeyFileScene = selectMasterkeyFileScene;
this.passwordEntryLock = passwordEntryLock;
this.masterkeyFileProvisionLock = masterkeyFileProvisionLock;
this.password = password;
this.filePath = filePath;
this.previousError = previousError;
}
@Override
public Path getCorrectMasterkeyFilePath(String masterkeyFilePath) {
try {
if (askForCorrectMasterkeyFile() == MasterkeyFileProvision.MASTERKEYFILE_PROVIDED) {
return filePath.get();
} else {
throw new UnlockCancelledException("Choosing masterkey file cancelled.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new UnlockCancelledException("Choosing masterkey file interrupted", e);
}
}
private MasterkeyFileProvision askForCorrectMasterkeyFile() throws InterruptedException {
Platform.runLater(() -> {
window.setScene(selectMasterkeyFileScene.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();
}
});
return masterkeyFileProvisionLock.awaitInteraction();
}
@Override
public CharSequence getPassphrase(Path path) throws UnlockCancelledException {
if (password.get() != null) { // e.g. pre-filled from keychain
return CharBuffer.wrap(password.get());
}
assert password.get() == null;
try {
if (askForPassphrase() == PasswordEntry.PASSWORD_ENTERED) {
assert password.get() != null;
return CharBuffer.wrap(password.get());
} else {
throw new UnlockCancelledException("Password entry cancelled.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new UnlockCancelledException("Password entry interrupted", e);
}
}
private PasswordEntry askForPassphrase() throws InterruptedException {
Platform.runLater(() -> {
window.setScene(passphraseEntryScene.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 (previousError instanceof InvalidPassphraseException) {
Animations.createShakeWindowAnimation(window).play();
}
});
return passwordEntryLock.awaitInteraction();
}
}
@@ -0,0 +1,60 @@
package org.cryptomator.ui.unlock.masterkeyfile;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.nio.CharBuffer;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@MasterkeyFileLoadingScoped
public class MasterkeyFileLoadingFinisher {
private static final Logger LOG = LoggerFactory.getLogger(MasterkeyFileLoadingFinisher.class);
private final Vault vault;
private final Optional<char[]> storedPassword;
private final AtomicReference<char[]> enteredPassword;
private final boolean shouldSavePassword;
private final KeychainManager keychain;
@Inject
MasterkeyFileLoadingFinisher(@MasterkeyFileLoading Vault vault, @Named("savedPassword") Optional<char[]> storedPassword, AtomicReference<char[]> enteredPassword, @Named("savePassword")AtomicBoolean shouldSavePassword, KeychainManager keychain) {
this.vault = vault;
this.storedPassword = storedPassword;
this.enteredPassword = enteredPassword;
this.shouldSavePassword = shouldSavePassword.get();
this.keychain = keychain;
}
public void cleanup(boolean successfullyUnlocked) {
if (successfullyUnlocked && shouldSavePassword) {
savePasswordToSystemkeychain();
}
wipePassword(storedPassword.orElse(null));
wipePassword(enteredPassword.getAndSet(null));
}
private void savePasswordToSystemkeychain() {
if (keychain.isSupported()) {
try {
keychain.storePassphrase(vault.getId(), CharBuffer.wrap(enteredPassword.get()));
} catch (KeychainAccessException e) {
LOG.error("Failed to store passphrase in system keychain.", e);
}
}
}
private void wipePassword(char[] pw) {
if (pw != null) {
Arrays.fill(pw, ' ');
}
}
}
@@ -0,0 +1,133 @@
package org.cryptomator.ui.unlock.masterkeyfile;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.cryptolib.common.MasterkeyFileLoader;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene;
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 javafx.scene.Scene;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@Module(subcomponents = {ForgetPasswordComponent.class})
abstract class MasterkeyFileLoadingModule {
private static final Logger LOG = LoggerFactory.getLogger(MasterkeyFileLoadingModule.class);
public enum PasswordEntry {
PASSWORD_ENTERED,
CANCELED
}
public enum MasterkeyFileProvision {
MASTERKEYFILE_PROVIDED,
CANCELED
}
@Provides
@MasterkeyFileLoadingScoped
static MasterkeyFileLoader provideMasterkeyFileLoader(MasterkeyFileAccess masterkeyFileAccess, @MasterkeyFileLoading Vault vault, MasterkeyFileLoadingContext context) {
return masterkeyFileAccess.keyLoader(vault.getPath(), context);
}
@Provides
@MasterkeyFileLoadingScoped
static UserInteractionLock<PasswordEntry> providePasswordEntryLock() {
return new UserInteractionLock<>(null);
}
@Provides
@MasterkeyFileLoadingScoped
static UserInteractionLock<MasterkeyFileProvision> provideMasterkeyFileProvisionLock() {
return new UserInteractionLock<>(null);
}
@Provides
@Named("savedPassword")
@MasterkeyFileLoadingScoped
static Optional<char[]> provideStoredPassword(KeychainManager keychain, @MasterkeyFileLoading Vault vault) {
if (!keychain.isSupported()) {
return Optional.empty();
} else {
try {
return Optional.ofNullable(keychain.loadPassphrase(vault.getId()));
} catch (KeychainAccessException e) {
LOG.error("Failed to load entry from system keychain.", e);
return Optional.empty();
}
}
}
@Provides
@MasterkeyFileLoadingScoped
static AtomicReference<Path> provideUserProvidedMasterkeyPath() {
return new AtomicReference<>();
}
@Provides
@MasterkeyFileLoadingScoped
static AtomicReference<char[]> providePassword(@Named("savedPassword") Optional<char[]> storedPassword) {
return new AtomicReference<>(storedPassword.orElse(null));
}
@Provides
@Named("savePassword")
@MasterkeyFileLoadingScoped
static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional<char[]> storedPassword) {
return new AtomicBoolean(storedPassword.isPresent());
}
@Provides
@MasterkeyFileLoading
@MasterkeyFileLoadingScoped
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
}
@Provides
@FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD)
@MasterkeyFileLoadingScoped
static Scene provideUnlockScene(@MasterkeyFileLoading FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.UNLOCK_ENTER_PASSWORD);
}
@Provides
@FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE)
@MasterkeyFileLoadingScoped
static Scene provideUnlockSelectMasterkeyFileScene(@MasterkeyFileLoading FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE);
}
@Binds
@IntoMap
@FxControllerKey(PassphraseEntryController.class)
abstract FxController bindUnlockController(PassphraseEntryController controller);
@Binds
@IntoMap
@FxControllerKey(SelectMasterkeyFileController.class)
abstract FxController bindUnlockSelectMasterkeyFileController(SelectMasterkeyFileController controller);
}
@@ -0,0 +1,13 @@
package org.cryptomator.ui.unlock.masterkeyfile;
import javax.inject.Scope;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
@interface MasterkeyFileLoadingScoped {
}
@@ -1,4 +1,4 @@
package org.cryptomator.ui.unlock;
package org.cryptomator.ui.unlock.masterkeyfile;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
@@ -7,6 +7,7 @@ import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.common.WeakBindings;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
import org.cryptomator.ui.unlock.masterkeyfile.MasterkeyFileLoadingModule.PasswordEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -38,17 +39,17 @@ import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@UnlockScoped
public class UnlockController implements FxController {
@MasterkeyFileLoadingScoped
public class PassphraseEntryController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class);
private static final Logger LOG = LoggerFactory.getLogger(PassphraseEntryController.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<UnlockModule.PasswordEntry> passwordEntryLock;
private final UserInteractionLock<PasswordEntry> passwordEntryLock;
private final ForgetPasswordComponent.Builder forgetPassword;
private final KeychainManager keychain;
private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay;
@@ -66,7 +67,7 @@ public class UnlockController implements FxController {
public Animation unlockAnimation;
@Inject
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, KeychainManager keychain) {
public PassphraseEntryController(@MasterkeyFileLoading Stage window, @MasterkeyFileLoading Vault vault, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<PasswordEntry> passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) {
this.window = window;
this.vault = vault;
this.password = password;
@@ -138,7 +139,7 @@ public class UnlockController implements FxController {
// if not already interacted, mark this workflow as cancelled:
if (passwordEntryLock.awaitingInteraction().get()) {
LOG.debug("Unlock canceled by user.");
passwordEntryLock.interacted(UnlockModule.PasswordEntry.CANCELED);
passwordEntryLock.interacted(PasswordEntry.CANCELED);
}
}
@@ -154,7 +155,7 @@ public class UnlockController implements FxController {
if (oldPw != null) {
Arrays.fill(oldPw, ' ');
}
passwordEntryLock.interacted(UnlockModule.PasswordEntry.PASSWORD_ENTERED);
passwordEntryLock.interacted(PasswordEntry.PASSWORD_ENTERED);
startUnlockAnimation();
}
@@ -1,8 +1,8 @@
package org.cryptomator.ui.unlock;
package org.cryptomator.ui.unlock.masterkeyfile;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.unlock.UnlockModule.MasterkeyFileProvision;
import org.cryptomator.ui.unlock.masterkeyfile.MasterkeyFileLoadingModule.MasterkeyFileProvision;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -17,10 +17,10 @@ import java.nio.file.Path;
import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicReference;
@UnlockScoped
public class UnlockSelectMasterkeyFileController implements FxController {
@MasterkeyFileLoadingScoped
public class SelectMasterkeyFileController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(UnlockSelectMasterkeyFileController.class);
private static final Logger LOG = LoggerFactory.getLogger(SelectMasterkeyFileController.class);
private final Stage window;
private final AtomicReference<Path> masterkeyPath;
@@ -28,7 +28,7 @@ public class UnlockSelectMasterkeyFileController implements FxController {
private final ResourceBundle resourceBundle;
@Inject
public UnlockSelectMasterkeyFileController(@UnlockWindow Stage window, @Named("userProvidedMasterkeyPath") AtomicReference<Path> masterkeyPath, UserInteractionLock<MasterkeyFileProvision> masterkeyFileProvisionLock, ResourceBundle resourceBundle) {
public SelectMasterkeyFileController(@MasterkeyFileLoading Stage window, AtomicReference<Path> masterkeyPath, UserInteractionLock<MasterkeyFileProvision> masterkeyFileProvisionLock, ResourceBundle resourceBundle) {
this.window = window;
this.masterkeyPath = masterkeyPath;
this.masterkeyFileProvisionLock = masterkeyFileProvisionLock;
@@ -14,7 +14,7 @@
<?import javafx.scene.layout.VBox?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.unlock.UnlockController"
fx:controller="org.cryptomator.ui.unlock.masterkeyfile.PassphraseEntryController"
minWidth="400"
maxWidth="400"
minHeight="145"
@@ -37,10 +37,10 @@
<Region VBox.vgrow="ALWAYS"/>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<ButtonBar buttonMinWidth="120" buttonOrder="B+U">
<ButtonBar buttonMinWidth="120" buttonOrder="U+C">
<buttons>
<Button text="%generic.button.back" ButtonBar.buttonData="BACK_PREVIOUS" cancelButton="true" onAction="#back"/>
<Region ButtonBar.buttonData="OTHER"/>
<Button text="%generic.button.back" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
</buttons>
</ButtonBar>
</VBox>
@@ -11,7 +11,7 @@
<?import javafx.scene.shape.Circle?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.unlock.UnlockSelectMasterkeyFileController"
fx:controller="org.cryptomator.ui.unlock.masterkeyfile.SelectMasterkeyFileController"
minWidth="400"
maxWidth="400"
minHeight="145"