mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-22 12:41:27 +00:00
Choose key loading workflow depending on vaultconfig's key ID and allow KeyLoadingComponent to decide itself, what exceptions it can handle
This commit is contained in:
@@ -17,6 +17,8 @@ import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||
import org.cryptomator.cryptofs.VaultConfig;
|
||||
import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig;
|
||||
import org.cryptomator.cryptofs.common.Constants;
|
||||
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
@@ -66,13 +68,14 @@ public class Vault {
|
||||
private final BooleanBinding needsMigration;
|
||||
private final BooleanBinding unknownError;
|
||||
private final StringBinding accessPoint;
|
||||
private final Optional<UnverifiedVaultConfig> unverifiedVaultConfig;
|
||||
private final BooleanBinding accessPointPresent;
|
||||
private final BooleanProperty showingStats;
|
||||
|
||||
private volatile Volume volume;
|
||||
|
||||
@Inject
|
||||
Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, ObjectProperty<VaultState> state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, VaultStats stats) {
|
||||
Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, ObjectProperty<VaultState> state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, VaultStats stats, Optional<UnverifiedVaultConfig> unverifiedVaultConfig) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.volumeProvider = volumeProvider;
|
||||
this.defaultMountFlags = defaultMountFlags;
|
||||
@@ -80,6 +83,7 @@ public class Vault {
|
||||
this.state = state;
|
||||
this.lastKnownException = lastKnownException;
|
||||
this.stats = stats;
|
||||
this.unverifiedVaultConfig = unverifiedVaultConfig;
|
||||
this.displayName = Bindings.createStringBinding(this::getDisplayName, vaultSettings.displayName());
|
||||
this.displayablePath = Bindings.createStringBinding(this::getDisplayablePath, vaultSettings.path());
|
||||
this.locked = Bindings.createBooleanBinding(this::isLocked, state);
|
||||
@@ -299,6 +303,10 @@ public class Vault {
|
||||
return stats;
|
||||
}
|
||||
|
||||
public Optional<UnverifiedVaultConfig> getUnverifiedVaultConfig() {
|
||||
return unverifiedVaultConfig;
|
||||
}
|
||||
|
||||
public Observable[] observables() {
|
||||
return new Observable[]{state};
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@ package org.cryptomator.common.vaults;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Constants;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.cryptofs.VaultConfig;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -24,9 +26,11 @@ import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@Module
|
||||
@@ -53,6 +57,19 @@ public class VaultModule {
|
||||
return new SimpleObjectProperty<>(initialErrorCause);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@PerVault
|
||||
Optional<VaultConfig.UnverifiedVaultConfig> provideUnverifiedVaultConfig(VaultSettings settings) {
|
||||
Path vaultRoot = settings.path().get();
|
||||
Path configPath = vaultRoot.resolve(Constants.VAULTCONFIG_FILENAME);
|
||||
try {
|
||||
String token = Files.readString(configPath, StandardCharsets.US_ASCII);
|
||||
return Optional.of(VaultConfig.decode(token));
|
||||
} catch (IOException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Provides
|
||||
public Volume provideVolume(Settings settings, WebDavVolume webDavVolume, FuseVolume fuseVolume, DokanyVolume dokanyVolume) {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.cryptomator.ui.unlock;
|
||||
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface KeyLoadingComponent {
|
||||
|
||||
/**
|
||||
* @return A reusable masterkey loader, preconfigured with the vault of the current unlock process
|
||||
* @throws MasterkeyLoadingFailedException If unable to provide the masterkey loader
|
||||
*/
|
||||
MasterkeyLoader masterkeyLoader() throws MasterkeyLoadingFailedException;
|
||||
|
||||
/**
|
||||
* Allows the component to try and recover from an exception thrown while loading a masterkey.
|
||||
*
|
||||
* @param exception An exception thrown either by {@link #masterkeyLoader()} or by the returned {@link MasterkeyLoader}.
|
||||
* @return <code>true</code> if this component was able to handle the exception and another attempt should be made to load a masterkey
|
||||
*/
|
||||
default boolean recoverFromException(MasterkeyLoadingFailedException exception) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release any ressources or do follow-up tasks after loading a key.
|
||||
*
|
||||
* @param unlockedSuccessfully <code>true</code> if successfully unlocked a vault with the loaded key
|
||||
* @implNote This method might be invoked multiple times, depending on whether multiple attempts to load a key are started.
|
||||
*/
|
||||
default void cleanup(boolean unlockedSuccessfully) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
static KeyLoadingComponent exceptional(Exception exception) {
|
||||
return () -> {
|
||||
throw new MasterkeyLoadingFailedException("Can not load key", exception);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.cryptomator.ui.unlock;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntoMap;
|
||||
import dagger.multibindings.StringKey;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig;
|
||||
import org.cryptomator.ui.unlock.masterkeyfile.MasterkeyFileLoadingComponent;
|
||||
|
||||
import javafx.stage.Stage;
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Module(subcomponents = {MasterkeyFileLoadingComponent.class})
|
||||
abstract class KeyLoadingModule {
|
||||
|
||||
@Provides
|
||||
@UnlockScoped
|
||||
static Optional<URI> provideKeyId(@UnlockWindow Vault vault) {
|
||||
return vault.getUnverifiedVaultConfig().map(UnverifiedVaultConfig::getKeyId);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@UnlockScoped
|
||||
static KeyLoadingComponent provideKeyLoaderProvider(Optional<URI> keyId, Map<String, KeyLoadingComponent> keyLoaderProviders) {
|
||||
if (keyId.isEmpty()) {
|
||||
return KeyLoadingComponent.exceptional(new IllegalArgumentException("No key id provided"));
|
||||
} else {
|
||||
String scheme = keyId.get().getScheme();
|
||||
return keyLoaderProviders.getOrDefault(scheme, KeyLoadingComponent.exceptional(new IllegalArgumentException("Unsupported key id " + scheme)));
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@StringKey("masterkeyfile")
|
||||
static KeyLoadingComponent provideMasterkeyFileLoadingComponet(MasterkeyFileLoadingComponent.Builder compBuilder, @UnlockWindow Stage window, @UnlockWindow Vault vault) {
|
||||
return compBuilder.unlockWindow(window).vault(vault).build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
@UnlockScoped
|
||||
@Subcomponent(modules = {UnlockModule.class})
|
||||
@Subcomponent(modules = {UnlockModule.class, KeyLoadingModule.class})
|
||||
public interface UnlockComponent {
|
||||
|
||||
ExecutorService defaultExecutorService();
|
||||
|
||||
@@ -12,7 +12,6 @@ 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.unlock.masterkeyfile.MasterkeyFileLoadingComponent;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
@@ -23,7 +22,7 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
@Module(subcomponents = {MasterkeyFileLoadingComponent.class})
|
||||
@Module
|
||||
abstract class UnlockModule {
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -7,13 +7,11 @@ import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.common.vaults.Volume.VolumeException;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.VaultService;
|
||||
import org.cryptomator.ui.unlock.masterkeyfile.MasterkeyFileLoadingComponent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -43,17 +41,17 @@ public class UnlockWorkflow extends Task<Boolean> {
|
||||
private final Lazy<Scene> successScene;
|
||||
private final Lazy<Scene> invalidMountPointScene;
|
||||
private final ErrorComponent.Builder errorComponent;
|
||||
private final MasterkeyFileLoadingComponent.Builder masterkeyFileLoadingComponent;
|
||||
private final KeyLoadingComponent keyLoadingComp;
|
||||
|
||||
@Inject
|
||||
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, MasterkeyFileLoadingComponent.Builder masterkeyFileLoadingComponent) {
|
||||
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, KeyLoadingComponent keyLoadingComp) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.vaultService = vaultService;
|
||||
this.successScene = successScene;
|
||||
this.invalidMountPointScene = invalidMountPointScene;
|
||||
this.errorComponent = errorComponent;
|
||||
this.masterkeyFileLoadingComponent = masterkeyFileLoadingComponent;
|
||||
this.keyLoadingComp = keyLoadingComp;
|
||||
|
||||
setOnFailed(event -> {
|
||||
Throwable throwable = event.getSource().getException();
|
||||
@@ -68,8 +66,7 @@ public class UnlockWorkflow extends Task<Boolean> {
|
||||
@Override
|
||||
protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException, CryptoException {
|
||||
try {
|
||||
// TODO: allow unlock strategies other than MasterkeyFile-based eventually
|
||||
attemptUnlockUsingMasterkeyFile(0, null);
|
||||
attemptUnlock();
|
||||
handleSuccess();
|
||||
return true;
|
||||
} catch (UnlockCancelledException e) {
|
||||
@@ -78,17 +75,20 @@ public class UnlockWorkflow extends Task<Boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
private void attemptUnlockUsingMasterkeyFile(int attempt, Exception previousError) throws IOException, VolumeException, InvalidMountPointException, CryptoException {
|
||||
var fileLoadingComp = masterkeyFileLoadingComponent.unlockWindow(window).vault(vault).previousError(previousError).build();
|
||||
private void attemptUnlock() throws IOException, VolumeException, InvalidMountPointException, CryptoException {
|
||||
boolean success = false;
|
||||
try {
|
||||
vault.unlock(fileLoadingComp.masterkeyLoader());
|
||||
vault.unlock(keyLoadingComp.masterkeyLoader());
|
||||
success = true;
|
||||
} catch (InvalidPassphraseException e) {
|
||||
LOG.info("Unlock attempt #{} failed due to {}", attempt, e.getMessage());
|
||||
attemptUnlockUsingMasterkeyFile(attempt + 1, e);
|
||||
} catch (MasterkeyLoadingFailedException e) {
|
||||
if (keyLoadingComp.recoverFromException(e)) {
|
||||
LOG.info("Unlock attempt threw {}. Reattempting...", e.getClass().getSimpleName());
|
||||
attemptUnlock();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
fileLoadingComp.cleanup(success);
|
||||
keyLoadingComp.cleanup(success);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,19 +3,29 @@ package org.cryptomator.ui.unlock.masterkeyfile;
|
||||
import dagger.BindsInstance;
|
||||
import dagger.Subcomponent;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
import org.cryptomator.cryptolib.common.MasterkeyFileLoader;
|
||||
import org.cryptomator.ui.unlock.KeyLoadingComponent;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
@MasterkeyFileLoadingScoped
|
||||
@Subcomponent(modules = {MasterkeyFileLoadingModule.class})
|
||||
public interface MasterkeyFileLoadingComponent {
|
||||
public interface MasterkeyFileLoadingComponent extends KeyLoadingComponent {
|
||||
|
||||
MasterkeyFileLoadingFinisher finisher();
|
||||
|
||||
MasterkeyFileLoadingContext context();
|
||||
|
||||
@Override
|
||||
MasterkeyFileLoader masterkeyLoader();
|
||||
|
||||
@Override
|
||||
default boolean recoverFromException(MasterkeyLoadingFailedException exception) {
|
||||
return context().recoverFromException(exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void cleanup(boolean unlockedSuccessfully) {
|
||||
finisher().cleanup(unlockedSuccessfully);
|
||||
}
|
||||
@@ -23,9 +33,6 @@ public interface MasterkeyFileLoadingComponent {
|
||||
@Subcomponent.Builder
|
||||
interface Builder {
|
||||
|
||||
@BindsInstance
|
||||
Builder previousError(@Nullable Exception previousError);
|
||||
|
||||
@BindsInstance
|
||||
Builder vault(@MasterkeyFileLoading Vault vault);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.cryptomator.ui.unlock.masterkeyfile;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
import org.cryptomator.cryptolib.common.MasterkeyFileLoaderContext;
|
||||
import org.cryptomator.ui.common.Animations;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
@@ -11,7 +12,6 @@ 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 javafx.application.Platform;
|
||||
import javafx.scene.Scene;
|
||||
@@ -22,7 +22,7 @@ import java.nio.file.Path;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@MasterkeyFileLoadingScoped
|
||||
class MasterkeyFileLoadingContext implements MasterkeyFileLoaderContext {
|
||||
public class MasterkeyFileLoadingContext implements MasterkeyFileLoaderContext {
|
||||
|
||||
private final Stage window;
|
||||
private final Lazy<Scene> passphraseEntryScene;
|
||||
@@ -31,10 +31,11 @@ class MasterkeyFileLoadingContext implements MasterkeyFileLoaderContext {
|
||||
private final UserInteractionLock<MasterkeyFileProvision> masterkeyFileProvisionLock;
|
||||
private final AtomicReference<char[]> password;
|
||||
private final AtomicReference<Path> filePath;
|
||||
private final Exception previousError;
|
||||
|
||||
private boolean wrongPassword;
|
||||
|
||||
@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) {
|
||||
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) {
|
||||
this.window = window;
|
||||
this.passphraseEntryScene = passphraseEntryScene;
|
||||
this.selectMasterkeyFileScene = selectMasterkeyFileScene;
|
||||
@@ -42,11 +43,15 @@ class MasterkeyFileLoadingContext implements MasterkeyFileLoaderContext {
|
||||
this.masterkeyFileProvisionLock = masterkeyFileProvisionLock;
|
||||
this.password = password;
|
||||
this.filePath = filePath;
|
||||
this.previousError = previousError;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path getCorrectMasterkeyFilePath(String masterkeyFilePath) {
|
||||
if (filePath.get() != null) { // e.g. already chosen on previous attempt with wrong password
|
||||
return filePath.get();
|
||||
}
|
||||
|
||||
assert filePath.get() == null;
|
||||
try {
|
||||
if (askForCorrectMasterkeyFile() == MasterkeyFileProvision.MASTERKEYFILE_PROVIDED) {
|
||||
return filePath.get();
|
||||
@@ -105,11 +110,20 @@ class MasterkeyFileLoadingContext implements MasterkeyFileLoaderContext {
|
||||
} else {
|
||||
window.centerOnScreen();
|
||||
}
|
||||
if (previousError instanceof InvalidPassphraseException) {
|
||||
if (wrongPassword) {
|
||||
Animations.createShakeWindowAnimation(window).play();
|
||||
}
|
||||
});
|
||||
return passwordEntryLock.awaitInteraction();
|
||||
}
|
||||
|
||||
public boolean recoverFromException(MasterkeyLoadingFailedException exception) {
|
||||
if (exception instanceof InvalidPassphraseException) {
|
||||
this.wrongPassword = true;
|
||||
password.set(null);
|
||||
return true; // reattempting key load
|
||||
} else {
|
||||
return false; // nothing we can do
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user