diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java index d63167c3b..32dc94f14 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -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"), // diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockCancelledException.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockCancelledException.java new file mode 100644 index 000000000..c795eb5ea --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockCancelledException.java @@ -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); + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java index 1850953c9..2e0f339f5 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java @@ -17,19 +17,17 @@ import javafx.stage.Stage; public class UnlockInvalidMountPointController implements FxController { private final Stage window; - private final Lazy unlockScene; private final Vault vault; @Inject - UnlockInvalidMountPointController(@UnlockWindow Stage window, @FxmlScene(FxmlFile.UNLOCK) Lazy 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 */ 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 37940e3a2..87a9f147c 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 @@ -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 providePasswordEntryLock() { - return new UserInteractionLock<>(null); - } - - @Provides - @UnlockScoped - static UserInteractionLock provideMasterkeyFileProvisionLock() { - return new UserInteractionLock<>(null); - } - - @Provides - @Named("savedPassword") - @UnlockScoped - static Optional 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 provideUserProvidedMasterkeyPath() { - return new AtomicReference(); - } - - @Provides - @UnlockScoped - static AtomicReference providePassword(@Named("savedPassword") Optional storedPassword) { - return new AtomicReference(storedPassword.orElse(null)); - } - - @Provides - @Named("savePassword") - @UnlockScoped - static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional 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) diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java index 926658f06..2a6837a9c 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java @@ -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 implements MasterkeyFileLoaderContext { +public class UnlockWorkflow extends Task { private static final Logger LOG = LoggerFactory.getLogger(UnlockWorkflow.class); private final Stage window; private final Vault vault; private final VaultService vaultService; - private final AtomicReference password; - private final AtomicBoolean savePassword; - private final Optional savedPassword; - private final AtomicReference correctMasterkeyPath; - private final UserInteractionLock passwordEntryLock; - private final UserInteractionLock masterkeyFileProvisionLock; - private final KeychainManager keychain; - private final Lazy unlockScene; - private final Lazy selectMasterkeyFileScene; private final Lazy successScene; private final Lazy 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 password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional savedPassword, @Named("userProvidedMasterkeyPath") AtomicReference correctMasterkeyPath, UserInteractionLock passwordEntryLock, UserInteractionLock masterkeyFileProvisionLock, KeychainManager keychain, @FxmlScene(FxmlFile.UNLOCK) Lazy unlockScene, @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) Lazy selectMasterkeyFileScene, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, ErrorComponent.Builder errorComponent, MasterkeyFileAccess masterkeyFileAccess) { + UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy 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 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 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 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 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); - } - } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/MasterkeyFileLoading.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/MasterkeyFileLoading.java new file mode 100644 index 000000000..2d7a8a921 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/MasterkeyFileLoading.java @@ -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 { + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/MasterkeyFileLoadingComponent.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/MasterkeyFileLoadingComponent.java new file mode 100644 index 000000000..268e0237b --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/MasterkeyFileLoadingComponent.java @@ -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(); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/MasterkeyFileLoadingContext.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/MasterkeyFileLoadingContext.java new file mode 100644 index 000000000..7225fd2e1 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/MasterkeyFileLoadingContext.java @@ -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 passphraseEntryScene; + private final Lazy selectMasterkeyFileScene; + private final UserInteractionLock passwordEntryLock; + private final UserInteractionLock masterkeyFileProvisionLock; + private final AtomicReference password; + private final AtomicReference filePath; + private final Exception previousError; + + @Inject + public MasterkeyFileLoadingContext(@MasterkeyFileLoading Stage window, @FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD) Lazy passphraseEntryScene, @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) Lazy selectMasterkeyFileScene, UserInteractionLock passwordEntryLock, UserInteractionLock masterkeyFileProvisionLock, AtomicReference password, AtomicReference 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(); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/MasterkeyFileLoadingFinisher.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/MasterkeyFileLoadingFinisher.java new file mode 100644 index 000000000..8a16d3a12 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/MasterkeyFileLoadingFinisher.java @@ -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 storedPassword; + private final AtomicReference enteredPassword; + private final boolean shouldSavePassword; + private final KeychainManager keychain; + + @Inject + MasterkeyFileLoadingFinisher(@MasterkeyFileLoading Vault vault, @Named("savedPassword") Optional storedPassword, AtomicReference 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, ' '); + } + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/MasterkeyFileLoadingModule.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/MasterkeyFileLoadingModule.java new file mode 100644 index 000000000..9c3530357 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/MasterkeyFileLoadingModule.java @@ -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 providePasswordEntryLock() { + return new UserInteractionLock<>(null); + } + + @Provides + @MasterkeyFileLoadingScoped + static UserInteractionLock provideMasterkeyFileProvisionLock() { + return new UserInteractionLock<>(null); + } + + @Provides + @Named("savedPassword") + @MasterkeyFileLoadingScoped + static Optional 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 provideUserProvidedMasterkeyPath() { + return new AtomicReference<>(); + } + + @Provides + @MasterkeyFileLoadingScoped + static AtomicReference providePassword(@Named("savedPassword") Optional storedPassword) { + return new AtomicReference<>(storedPassword.orElse(null)); + } + + @Provides + @Named("savePassword") + @MasterkeyFileLoadingScoped + static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional storedPassword) { + return new AtomicBoolean(storedPassword.isPresent()); + } + + @Provides + @MasterkeyFileLoading + @MasterkeyFileLoadingScoped + static FxmlLoaderFactory provideFxmlLoaderFactory(Map, Provider> 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); + + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/MasterkeyFileLoadingScoped.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/MasterkeyFileLoadingScoped.java new file mode 100644 index 000000000..1eeac1e84 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/MasterkeyFileLoadingScoped.java @@ -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 { + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/PassphraseEntryController.java similarity index 89% rename from main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java rename to main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/PassphraseEntryController.java index 674b16142..914a2a91b 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/PassphraseEntryController.java @@ -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 password; private final AtomicBoolean savePassword; private final Optional savedPassword; - private final UserInteractionLock passwordEntryLock; + private final UserInteractionLock passwordEntryLock; private final ForgetPasswordComponent.Builder forgetPassword; private final KeychainManager keychain; private final ObjectBinding unlockButtonContentDisplay; @@ -66,7 +67,7 @@ public class UnlockController implements FxController { public Animation unlockAnimation; @Inject - public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, AtomicReference password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional savedPassword, UserInteractionLock passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) { + public PassphraseEntryController(@MasterkeyFileLoading Stage window, @MasterkeyFileLoading Vault vault, AtomicReference password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional savedPassword, UserInteractionLock 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(); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockSelectMasterkeyFileController.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/SelectMasterkeyFileController.java similarity index 77% rename from main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockSelectMasterkeyFileController.java rename to main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/SelectMasterkeyFileController.java index 68c63067c..f255d83b6 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockSelectMasterkeyFileController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/masterkeyfile/SelectMasterkeyFileController.java @@ -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 masterkeyPath; @@ -28,7 +28,7 @@ public class UnlockSelectMasterkeyFileController implements FxController { private final ResourceBundle resourceBundle; @Inject - public UnlockSelectMasterkeyFileController(@UnlockWindow Stage window, @Named("userProvidedMasterkeyPath") AtomicReference masterkeyPath, UserInteractionLock masterkeyFileProvisionLock, ResourceBundle resourceBundle) { + public SelectMasterkeyFileController(@MasterkeyFileLoading Stage window, AtomicReference masterkeyPath, UserInteractionLock masterkeyFileProvisionLock, ResourceBundle resourceBundle) { this.window = window; this.masterkeyPath = masterkeyPath; this.masterkeyFileProvisionLock = masterkeyFileProvisionLock; diff --git a/main/ui/src/main/resources/fxml/unlock.fxml b/main/ui/src/main/resources/fxml/unlock_enter_password.fxml similarity index 97% rename from main/ui/src/main/resources/fxml/unlock.fxml rename to main/ui/src/main/resources/fxml/unlock_enter_password.fxml index 54adc5279..ff9cc675d 100644 --- a/main/ui/src/main/resources/fxml/unlock.fxml +++ b/main/ui/src/main/resources/fxml/unlock_enter_password.fxml @@ -14,7 +14,7 @@ - + -