diff --git a/src/main/java/org/cryptomator/common/recovery/BackupRestorer.java b/src/main/java/org/cryptomator/common/recovery/BackupRestorer.java new file mode 100644 index 000000000..e5afc5112 --- /dev/null +++ b/src/main/java/org/cryptomator/common/recovery/BackupRestorer.java @@ -0,0 +1,53 @@ +package org.cryptomator.common.recovery; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileTime; +import java.util.stream.Stream; + +import static org.cryptomator.common.Constants.MASTERKEY_BACKUP_SUFFIX; + +public final class BackupRestorer { + + private static final Logger LOG = LoggerFactory.getLogger(BackupRestorer.class); + + private BackupRestorer() {} + + public static void restoreIfBackupPresent(Path vaultPath, String filePrefix) { + Path targetFile = vaultPath.resolve(filePrefix); + + try (Stream files = Files.list(vaultPath)) { + files.filter(file -> isFileMatchingPattern(file.getFileName().toString(), filePrefix)) + .max((f1, f2) -> { + try { + FileTime time1 = Files.getLastModifiedTime(f1); + FileTime time2 = Files.getLastModifiedTime(f2); + return time1.compareTo(time2); + } catch (IOException e) { + return 0; + } + }) + .ifPresent(backupFile -> copyBackupFile(backupFile, targetFile)); + } catch (IOException e) { + LOG.info("Unable to restore backup files in '{}'", vaultPath, e); + } + } + + private static boolean isFileMatchingPattern(String fileName, String filePrefix) { + return fileName.startsWith(filePrefix) && fileName.endsWith(MASTERKEY_BACKUP_SUFFIX); + } + + private static void copyBackupFile(Path backupFile, Path configPath) { + try { + Files.copy(backupFile, configPath, StandardCopyOption.REPLACE_EXISTING); + LOG.debug("Backup restored - file: '{}' path: '{}'", backupFile, configPath); + } catch (IOException e) { + LOG.warn("Unable to copy backup file from '{}' to '{}'", backupFile, configPath, e); + } + } +} diff --git a/src/main/java/org/cryptomator/common/recovery/CryptoFsInitializer.java b/src/main/java/org/cryptomator/common/recovery/CryptoFsInitializer.java new file mode 100644 index 000000000..359405d15 --- /dev/null +++ b/src/main/java/org/cryptomator/common/recovery/CryptoFsInitializer.java @@ -0,0 +1,33 @@ +package org.cryptomator.common.recovery; + +import java.io.IOException; +import java.nio.file.Path; + +import org.cryptomator.cryptofs.CryptoFileSystemProperties; +import org.cryptomator.cryptofs.CryptoFileSystemProvider; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.CryptorProvider; +import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.MasterkeyLoader; + +import static org.cryptomator.common.Constants.DEFAULT_KEY_ID; + +public final class CryptoFsInitializer { + + private CryptoFsInitializer() {} + + public static void init(Path recoveryPath, + Masterkey masterkey, + int shorteningThreshold, + CryptorProvider.Scheme scheme) throws IOException, CryptoException { + + MasterkeyLoader loader = ignored -> masterkey.copy(); + CryptoFileSystemProperties fsProps = CryptoFileSystemProperties // + .cryptoFileSystemProperties() // + .withCipherCombo(scheme) // + .withKeyLoader(loader) // + .withShorteningThreshold(shorteningThreshold) // + .build(); + CryptoFileSystemProvider.initialize(recoveryPath, fsProps, DEFAULT_KEY_ID); + } +} diff --git a/src/main/java/org/cryptomator/common/recovery/MasterkeyService.java b/src/main/java/org/cryptomator/common/recovery/MasterkeyService.java new file mode 100644 index 000000000..7d487ec54 --- /dev/null +++ b/src/main/java/org/cryptomator/common/recovery/MasterkeyService.java @@ -0,0 +1,101 @@ +package org.cryptomator.common.recovery; + +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.CryptorProvider; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; +import org.cryptomator.ui.recoverykey.RecoveryKeyFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; + +import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; +import static org.cryptomator.cryptofs.common.Constants.DATA_DIR_NAME; + +public final class MasterkeyService { + + private static final Logger LOG = LoggerFactory.getLogger(MasterkeyService.class); + + private MasterkeyService() {} + + public static void recoverFromRecoveryKey(String recoveryKey, RecoveryKeyFactory recoveryKeyFactory, Path recoveryPath, CharSequence newPassword) throws IOException { + recoveryKeyFactory.newMasterkeyFileWithPassphrase(recoveryPath, recoveryKey, newPassword); + } + + public static Masterkey load(MasterkeyFileAccess masterkeyFileAccess, Path masterkeyFilePath, CharSequence password) throws IOException { + return masterkeyFileAccess.load(masterkeyFilePath, password); + } + + public static CryptorProvider.Scheme validateRecoveryKeyAndDetectCombo(RecoveryKeyFactory recoveryKeyFactory, // + Vault vault, String recoveryKey, // + MasterkeyFileAccess masterkeyFileAccess) throws IOException, CryptoException, NoSuchElementException { + String tmpPass = UUID.randomUUID().toString(); + try (RecoveryDirectory recoveryDirectory = RecoveryDirectory.create(vault.getPath())) { + Path tempRecoveryPath = recoveryDirectory.getRecoveryPath(); + recoverFromRecoveryKey(recoveryKey, recoveryKeyFactory, tempRecoveryPath, tmpPass); + Path masterkeyFilePath = tempRecoveryPath.resolve(MASTERKEY_FILENAME); + + try (Masterkey mk = load(masterkeyFileAccess, masterkeyFilePath, tmpPass)) { + return detect(mk, vault.getPath()).orElseThrow(); + } + } + } + + public static Optional detect(Masterkey masterkey, Path vaultPath) { + try (Stream paths = Files.walk(vaultPath.resolve(DATA_DIR_NAME))) { + Optional c9rFile = paths // + .filter(p -> p.toString().endsWith(".c9r")) // + .filter(p -> !p.endsWith("dir.c9r")) // + .findFirst(); + if (c9rFile.isEmpty()) { + LOG.info("Unable to detect Crypto scheme: No *.c9r file found in {}", vaultPath); + return Optional.empty(); + } + return determineScheme(c9rFile.get(), masterkey); + } catch (IOException e) { + LOG.info("Unable to detect Crypto scheme: Failed to inspect vault", e); + return Optional.empty(); + } + } + + private static Optional determineScheme(Path c9rFile, Masterkey masterkey) { + return Arrays.stream(CryptorProvider.Scheme.values()).filter(scheme -> { + try (Cryptor cryptor = CryptorProvider.forScheme(scheme).provide(masterkey.copy(), SecureRandom.getInstanceStrong())) { + int headerSize = cryptor.fileHeaderCryptor().headerSize(); + + ByteBuffer headerBuf = ByteBuffer.allocate(headerSize); + + try (FileChannel channel = FileChannel.open(c9rFile, StandardOpenOption.READ)) { + channel.read(headerBuf, 0); + } + + headerBuf.flip(); + cryptor.fileHeaderCryptor().decryptHeader(headerBuf.duplicate()); + LOG.debug("Detected Crypto scheme: {}", scheme); + return true; + } catch (IllegalArgumentException | CryptoException e) { + LOG.debug("Could not decrypt with scheme: {}", scheme); + return false; + } catch (IOException | NoSuchAlgorithmException e) { + LOG.warn("Unable to detect Crypto scheme: Failed to decrypt .c9r file", e); + return false; + } + }).findFirst(); + } + +} diff --git a/src/main/java/org/cryptomator/common/recovery/RecoveryActionType.java b/src/main/java/org/cryptomator/common/recovery/RecoveryActionType.java new file mode 100644 index 000000000..a05073dad --- /dev/null +++ b/src/main/java/org/cryptomator/common/recovery/RecoveryActionType.java @@ -0,0 +1,10 @@ +package org.cryptomator.common.recovery; + +public enum RecoveryActionType { + RESTORE_ALL, + RESTORE_MASTERKEY, + RESTORE_VAULT_CONFIG, + RESET_PASSWORD, + SHOW_KEY, + CONVERT_VAULT +} diff --git a/src/main/java/org/cryptomator/common/recovery/RecoveryDirectory.java b/src/main/java/org/cryptomator/common/recovery/RecoveryDirectory.java new file mode 100644 index 000000000..be2af245b --- /dev/null +++ b/src/main/java/org/cryptomator/common/recovery/RecoveryDirectory.java @@ -0,0 +1,56 @@ +package org.cryptomator.common.recovery; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Comparator; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class RecoveryDirectory implements AutoCloseable { + + private static final Logger LOG = LoggerFactory.getLogger(RecoveryDirectory.class); + + private final Path recoveryPath; + private final Path vaultPath; + + private RecoveryDirectory(Path vaultPath, Path recoveryPath) { + this.vaultPath = vaultPath; + this.recoveryPath = recoveryPath; + } + + public static RecoveryDirectory create(Path vaultPath) throws IOException { + Path tempDir = Files.createTempDirectory("cryptomator"); + return new RecoveryDirectory(vaultPath, tempDir); + } + + public void moveRecoveredFile(String file) throws IOException { + Files.move(recoveryPath.resolve(file), vaultPath.resolve(file), StandardCopyOption.REPLACE_EXISTING); + } + + private void deleteRecoveryDirectory() { + try (var paths = Files.walk(recoveryPath)) { + paths.sorted(Comparator.reverseOrder()).forEach(p -> { + try { + Files.delete(p); + } catch (IOException e) { + LOG.info("Unable to delete {}. Please delete it manually.", p); + } + }); + } catch (IOException e) { + LOG.error("Failed to clean up recovery directory", e); + } + } + + @Override + public void close() { + deleteRecoveryDirectory(); + } + + public Path getRecoveryPath() { + return recoveryPath; + } + +} diff --git a/src/main/java/org/cryptomator/common/recovery/VaultPreparator.java b/src/main/java/org/cryptomator/common/recovery/VaultPreparator.java new file mode 100644 index 000000000..d99ba50a0 --- /dev/null +++ b/src/main/java/org/cryptomator/common/recovery/VaultPreparator.java @@ -0,0 +1,54 @@ +package org.cryptomator.common.recovery; + +import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.common.settings.VaultSettings; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.common.vaults.VaultComponent; +import org.cryptomator.common.vaults.VaultConfigCache; +import org.cryptomator.common.vaults.VaultListManager; +import org.cryptomator.integrations.mount.MountService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.ResourceBundle; + +import static org.cryptomator.common.vaults.VaultState.Value.LOCKED; + +public final class VaultPreparator { + + private static final Logger LOG = LoggerFactory.getLogger(VaultPreparator.class); + + private VaultPreparator() {} + + public static Vault prepareVault(Path selectedDirectory, // + VaultComponent.Factory vaultComponentFactory, // + List mountServices, // + ResourceBundle resourceBundle) { + VaultSettings vaultSettings = VaultSettings.withRandomId(); + vaultSettings.path.set(selectedDirectory); + if (selectedDirectory.getFileName() != null) { + vaultSettings.displayName.set(selectedDirectory.getFileName().toString()); + } else { + vaultSettings.displayName.set(resourceBundle.getString("defaults.vault.vaultName")); + } + + var wrapper = new VaultConfigCache(vaultSettings); + Vault vault = vaultComponentFactory.create(vaultSettings, wrapper, LOCKED, null).vault(); + try { + VaultListManager.determineVaultState(vault.getPath()); + } catch (IOException e) { + LOG.warn("Failed to determine vault state for {}", vaultSettings.path.get(), e); + } + + //due to https://github.com/cryptomator/cryptomator/issues/2880#issuecomment-1680313498 + var nameOfWinfspLocalMounter = "org.cryptomator.frontend.fuse.mount.WinFspMountProvider"; + if (SystemUtils.IS_OS_WINDOWS && vaultSettings.path.get().toString().contains("Dropbox") && mountServices.stream().anyMatch(s -> s.getClass().getName().equals(nameOfWinfspLocalMounter))) { + vaultSettings.mountService.setValue(nameOfWinfspLocalMounter); + } + + return vault; + } +} diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java index 2e1ae4bba..2fa613e3e 100644 --- a/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/src/main/java/org/cryptomator/common/vaults/Vault.java @@ -23,7 +23,6 @@ import org.cryptomator.cryptofs.event.FilesystemEvent; import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.MasterkeyLoader; import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; -import org.cryptomator.event.VaultEvent; import org.cryptomator.integrations.mount.MountFailedException; import org.cryptomator.integrations.mount.Mountpoint; import org.cryptomator.integrations.mount.UnmountFailedException; @@ -35,7 +34,6 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Named; -import javafx.application.Platform; import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; @@ -75,6 +73,7 @@ public class Vault { private final BooleanBinding missing; private final BooleanBinding needsMigration; private final BooleanBinding unknownError; + private final BooleanBinding missingVaultConfig; private final ObjectBinding mountPoint; private final Mounter mounter; private final Settings settings; @@ -103,6 +102,7 @@ public class Vault { this.processing = Bindings.createBooleanBinding(this::isProcessing, state); this.unlocked = Bindings.createBooleanBinding(this::isUnlocked, state); this.missing = Bindings.createBooleanBinding(this::isMissing, state); + this.missingVaultConfig = Bindings.createBooleanBinding(this::isMissingVaultConfig, state); this.needsMigration = Bindings.createBooleanBinding(this::isNeedsMigration, state); this.unknownError = Bindings.createBooleanBinding(this::isUnknownError, state); this.mountPoint = Bindings.createObjectBinding(this::getMountPoint, state); @@ -336,6 +336,14 @@ public class Vault { return state.get() == VaultState.Value.ERROR; } + public BooleanBinding missingVaultConfigProperty() { + return missingVaultConfig; + } + + public boolean isMissingVaultConfig() { + return state.get() == VaultState.Value.VAULT_CONFIG_MISSING || state.get() == VaultState.Value.ALL_MISSING; + } + public ReadOnlyStringProperty displayNameProperty() { return vaultSettings.displayName; } diff --git a/src/main/java/org/cryptomator/common/vaults/VaultConfigCache.java b/src/main/java/org/cryptomator/common/vaults/VaultConfigCache.java index b879b1f81..4a95fe50b 100644 --- a/src/main/java/org/cryptomator/common/vaults/VaultConfigCache.java +++ b/src/main/java/org/cryptomator/common/vaults/VaultConfigCache.java @@ -20,7 +20,7 @@ public class VaultConfigCache { private final VaultSettings settings; private final AtomicReference config; - VaultConfigCache(VaultSettings settings) { + public VaultConfigCache(VaultSettings settings) { this.settings = settings; this.config = new AtomicReference<>(null); } diff --git a/src/main/java/org/cryptomator/common/vaults/VaultListManager.java b/src/main/java/org/cryptomator/common/vaults/VaultListManager.java index ce1b2433c..e73075d0d 100644 --- a/src/main/java/org/cryptomator/common/vaults/VaultListManager.java +++ b/src/main/java/org/cryptomator/common/vaults/VaultListManager.java @@ -9,6 +9,7 @@ package org.cryptomator.common.vaults; import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.common.recovery.BackupRestorer; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.cryptofs.CryptoFileSystemProvider; @@ -34,9 +35,7 @@ import java.util.ResourceBundle; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME; -import static org.cryptomator.common.vaults.VaultState.Value.ERROR; -import static org.cryptomator.common.vaults.VaultState.Value.LOCKED; -import static org.cryptomator.common.vaults.VaultState.Value.NEEDS_MIGRATION; +import static org.cryptomator.common.vaults.VaultState.Value.*; @Singleton public class VaultListManager { @@ -67,6 +66,12 @@ public class VaultListManager { autoLocker.init(); } + public boolean isAlreadyAdded(Path vaultPath) { + assert vaultPath.isAbsolute(); + assert vaultPath.normalize().equals(vaultPath); + return vaultList.stream().anyMatch(v -> vaultPath.equals(v.getPath())); + } + public Vault add(Path pathToVault) throws IOException { Path normalizedPathToVault = pathToVault.normalize().toAbsolutePath(); if (CryptoFileSystemProvider.checkDirStructureForVault(normalizedPathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) == DirStructure.UNRELATED) { @@ -114,59 +119,122 @@ public class VaultListManager { .findAny(); } + public void addVault(Vault vault) { + Path path = vault.getPath().normalize().toAbsolutePath(); + if (!isAlreadyAdded(path)) { + vaultList.add(vault); + } + } + private Vault create(VaultSettings vaultSettings) { var wrapper = new VaultConfigCache(vaultSettings); try { var vaultState = determineVaultState(vaultSettings.path.get()); - if (vaultState == LOCKED) { //for legacy reasons: pre v8 vault do not have a config, but they are in the NEEDS_MIGRATION state - wrapper.reloadConfig(); - if (Objects.isNull(vaultSettings.lastKnownKeyLoader.get())) { - var keyIdScheme = wrapper.get().getKeyId().getScheme(); - vaultSettings.lastKnownKeyLoader.set(keyIdScheme); - } - } else if (vaultState == NEEDS_MIGRATION) { - vaultSettings.lastKnownKeyLoader.set(MasterkeyFileLoadingStrategy.SCHEME); - } + initializeLastKnownKeyLoaderIfPossible(vaultSettings, vaultState, wrapper); + return vaultComponentFactory.create(vaultSettings, wrapper, vaultState, null).vault(); } catch (IOException e) { - LOG.warn("Failed to determine vault state for " + vaultSettings.path.get(), e); + LOG.warn("Failed to determine vault state for {}", vaultSettings.path.get(), e); return vaultComponentFactory.create(vaultSettings, wrapper, ERROR, e).vault(); } } + private void initializeLastKnownKeyLoaderIfPossible(VaultSettings vaultSettings, VaultState.Value vaultState, VaultConfigCache wrapper) throws IOException { + if (vaultSettings.lastKnownKeyLoader.get() != null) { + return; + } + + switch (vaultState) { + case LOCKED -> { + wrapper.reloadConfig(); + vaultSettings.lastKnownKeyLoader.set(wrapper.get().getKeyId().getScheme()); + } + case NEEDS_MIGRATION -> { + //for legacy reasons: pre v8 vault do not have a config, but they are in the NEEDS_MIGRATION state + vaultSettings.lastKnownKeyLoader.set(MasterkeyFileLoadingStrategy.SCHEME); + } + case VAULT_CONFIG_MISSING -> { + //Nothing to do here, since there is no config to read + } + case MISSING, ALL_MISSING, ERROR, PROCESSING -> { + // no config available or not safe to load + } + default -> { + if (Files.exists(vaultSettings.path.get().resolve(VAULTCONFIG_FILENAME))) { + try { + wrapper.reloadConfig(); + vaultSettings.lastKnownKeyLoader.set(wrapper.get().getKeyId().getScheme()); + } catch (IOException e) { + LOG.debug("Unable to load config for {}", vaultSettings.path.get(), e); + } + } + } + } + } + public static VaultState.Value redetermineVaultState(Vault vault) { VaultState state = vault.stateProperty(); - VaultState.Value previousState = state.getValue(); - return switch (previousState) { - case LOCKED, NEEDS_MIGRATION, MISSING -> { - try { - var determinedState = determineVaultState(vault.getPath()); - if (determinedState == LOCKED) { - vault.getVaultConfigCache().reloadConfig(); - } - state.set(determinedState); - yield determinedState; - } catch (IOException e) { - LOG.warn("Failed to determine vault state for " + vault.getPath(), e); - state.set(ERROR); - vault.setLastKnownException(e); - yield ERROR; - } + VaultState.Value previous = state.getValue(); + + if (previous.equals(UNLOCKED) || previous.equals(PROCESSING)) { + return previous; + } + + try { + VaultState.Value determined = determineVaultState(vault.getPath()); + + if (determined == LOCKED) { + vault.getVaultConfigCache().reloadConfig(); } - case ERROR, UNLOCKED, PROCESSING -> previousState; - }; + + state.set(determined); + return determined; + } catch (IOException e) { + LOG.warn("Failed to (re)determine vault state for {}", vault.getPath(), e); + vault.setLastKnownException(e); + state.set(ERROR); + return ERROR; + } } - private static VaultState.Value determineVaultState(Path pathToVault) throws IOException { + public static VaultState.Value determineVaultState(Path pathToVault) throws IOException { if (!Files.exists(pathToVault)) { - return VaultState.Value.MISSING; + return MISSING; } + + VaultState.Value structureResult = checkDirStructure(pathToVault); + + if (structureResult == LOCKED || structureResult == NEEDS_MIGRATION) { + return structureResult; + } + + Path pathToVaultConfig = pathToVault.resolve(VAULTCONFIG_FILENAME); + Path pathToMasterkey = pathToVault.resolve(MASTERKEY_FILENAME); + + if (!Files.exists(pathToVaultConfig)) { + BackupRestorer.restoreIfBackupPresent(pathToVault, VAULTCONFIG_FILENAME); + } + if (!Files.exists(pathToMasterkey)) { + BackupRestorer.restoreIfBackupPresent(pathToVault, MASTERKEY_FILENAME); + } + + boolean hasConfig = Files.exists(pathToVaultConfig); + + if (!hasConfig && !Files.exists(pathToMasterkey)) { + return ALL_MISSING; + } + if (!hasConfig) { + return VAULT_CONFIG_MISSING; + } + + return checkDirStructure(pathToVault); + } + + private static VaultState.Value checkDirStructure(Path pathToVault) throws IOException { return switch (CryptoFileSystemProvider.checkDirStructureForVault(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME)) { - case VAULT -> VaultState.Value.LOCKED; - case UNRELATED -> VaultState.Value.MISSING; - case MAYBE_LEGACY -> Migrators.get().needsMigration(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) ? // - VaultState.Value.NEEDS_MIGRATION // - : VaultState.Value.MISSING; + case VAULT -> LOCKED; + case UNRELATED -> MISSING; + case MAYBE_LEGACY -> Migrators.get().needsMigration(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) ? NEEDS_MIGRATION : MISSING; }; } diff --git a/src/main/java/org/cryptomator/common/vaults/VaultState.java b/src/main/java/org/cryptomator/common/vaults/VaultState.java index ff09c8b82..f8b9b412a 100644 --- a/src/main/java/org/cryptomator/common/vaults/VaultState.java +++ b/src/main/java/org/cryptomator/common/vaults/VaultState.java @@ -25,6 +25,16 @@ public class VaultState extends ObservableValueBase implements */ MISSING, + /** + * No vault config found at the provided path + */ + VAULT_CONFIG_MISSING, + + /** + * No vault config and masterkey found at the provided path + */ + ALL_MISSING, + /** * Vault requires migration to a newer vault format */ diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java index ce8c65a37..68607808d 100644 --- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -42,9 +42,10 @@ public enum FxmlFile { QUIT("/fxml/quit.fxml"), // QUIT_FORCED("/fxml/quit_forced.fxml"), // RECOVERYKEY_CREATE("/fxml/recoverykey_create.fxml"), // + RECOVERYKEY_EXPERT_SETTINGS("/fxml/recoverykey_expert_settings.fxml"), // + RECOVERYKEY_ONBOARDING("/fxml/recoverykey_onboarding.fxml"), // RECOVERYKEY_RECOVER("/fxml/recoverykey_recover.fxml"), // RECOVERYKEY_RESET_PASSWORD("/fxml/recoverykey_reset_password.fxml"), // - RECOVERYKEY_RESET_PASSWORD_SUCCESS("/fxml/recoverykey_reset_password_success.fxml"), // RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), // SHARE_VAULT("/fxml/share_vault.fxml"), // SIMPLE_DIALOG("/fxml/simple_dialog.fxml"), // diff --git a/src/main/java/org/cryptomator/ui/convertvault/ConvertVaultModule.java b/src/main/java/org/cryptomator/ui/convertvault/ConvertVaultModule.java index f70242d2b..d010ede43 100644 --- a/src/main/java/org/cryptomator/ui/convertvault/ConvertVaultModule.java +++ b/src/main/java/org/cryptomator/ui/convertvault/ConvertVaultModule.java @@ -4,8 +4,10 @@ import dagger.Binds; import dagger.Module; import dagger.Provides; import dagger.multibindings.IntoMap; +import org.cryptomator.common.recovery.RecoveryActionType; import org.cryptomator.common.vaults.Vault; import org.cryptomator.cryptofs.VaultConfig; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; import org.cryptomator.ui.changepassword.NewPasswordController; import org.cryptomator.ui.changepassword.PasswordStrengthUtil; import org.cryptomator.ui.common.DefaultSceneFactory; @@ -20,6 +22,7 @@ import org.cryptomator.ui.recoverykey.RecoveryKeyValidateController; import javax.inject.Named; import javax.inject.Provider; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.scene.Scene; @@ -119,8 +122,8 @@ abstract class ConvertVaultModule { @Provides @IntoMap @FxControllerKey(RecoveryKeyValidateController.class) - static FxController bindRecoveryKeyValidateController(@ConvertVaultWindow Vault vault, @ConvertVaultWindow VaultConfig.UnverifiedVaultConfig vaultConfig, @ConvertVaultWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory) { - return new RecoveryKeyValidateController(vault, vaultConfig, recoveryKey, recoveryKeyFactory); + static FxController provideRecoveryKeyValidateController(@ConvertVaultWindow Vault vault, @ConvertVaultWindow VaultConfig.UnverifiedVaultConfig vaultConfig, @ConvertVaultWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory, MasterkeyFileAccess masterkeyFileAccess) { + return new RecoveryKeyValidateController(vault, vaultConfig, recoveryKey, recoveryKeyFactory, masterkeyFileAccess, new SimpleObjectProperty<>(RecoveryActionType.CONVERT_VAULT), new SimpleObjectProperty<>(null)); } } diff --git a/src/main/java/org/cryptomator/ui/dialogs/Dialogs.java b/src/main/java/org/cryptomator/ui/dialogs/Dialogs.java index 837bea012..6f3a86026 100644 --- a/src/main/java/org/cryptomator/ui/dialogs/Dialogs.java +++ b/src/main/java/org/cryptomator/ui/dialogs/Dialogs.java @@ -20,6 +20,8 @@ public class Dialogs { private final ResourceBundle resourceBundle; private final StageFactory stageFactory; + private static final String BUTTON_KEY_CLOSE = "generic.button.close"; + @Inject public Dialogs(ResourceBundle resourceBundle, StageFactory stageFactory) { this.resourceBundle = resourceBundle; @@ -47,6 +49,43 @@ public class Dialogs { }); } + public SimpleDialog.Builder prepareContactHubVaultOwner(Stage window) { + return createDialogBuilder().setOwner(window) // + .setTitleKey("contactHubVaultOwner.title") // + .setMessageKey("contactHubVaultOwner.message") // + .setDescriptionKey("contactHubVaultOwner.description") // + .setIcon(FontAwesome5Icon.EXCLAMATION)// + .setOkButtonKey(BUTTON_KEY_CLOSE); + } + + public SimpleDialog.Builder prepareRecoveryVaultAdded(Stage window, String displayName) { + return createDialogBuilder().setOwner(window) // + .setTitleKey("recover.existing.title") // + .setMessageKey("recover.existing.message") // + .setDescriptionKey("recover.existing.description", displayName) // + .setIcon(FontAwesome5Icon.CHECK)// + .setOkButtonKey(BUTTON_KEY_CLOSE); + } + public SimpleDialog.Builder prepareRecoveryVaultAlreadyExists(Stage window, String displayName) { + return createDialogBuilder().setOwner(window) // + .setTitleKey("recover.alreadyExists.title") // + .setMessageKey("recover.alreadyExists.message") // + .setDescriptionKey("recover.alreadyExists.description", displayName) // + .setIcon(FontAwesome5Icon.EXCLAMATION)// + .setOkButtonKey(BUTTON_KEY_CLOSE); + } + + public SimpleDialog.Builder prepareRecoverPasswordSuccess(Stage window) { + return createDialogBuilder() + .setOwner(window) // + .setTitleKey("recoveryKey.recover.title") // + .setMessageKey("recoveryKey.recover.resetSuccess.message") // + .setDescriptionKey("recoveryKey.recover.resetSuccess.description") // + .setIcon(FontAwesome5Icon.CHECK) + .setOkAction(Stage::close) + .setOkButtonKey(BUTTON_KEY_CLOSE); + } + public SimpleDialog.Builder prepareRemoveCertDialog(Stage window, Settings settings) { return createDialogBuilder() // .setOwner(window) // @@ -69,7 +108,7 @@ public class Dialogs { .setMessageKey("dokanySupportEnd.message") // .setDescriptionKey("dokanySupportEnd.description") // .setIcon(FontAwesome5Icon.EXCLAMATION) // - .setOkButtonKey("generic.button.close") // + .setOkButtonKey(BUTTON_KEY_CLOSE) // .setCancelButtonKey("dokanySupportEnd.preferencesBtn") // .setOkAction(Stage::close) // .setCancelAction(cancelAction); @@ -83,8 +122,20 @@ public class Dialogs { .setDescriptionKey("retryIfReadonly.description") // .setIcon(FontAwesome5Icon.EXCLAMATION) // .setOkButtonKey("retryIfReadonly.retry") // - .setCancelButtonKey("generic.button.close") // + .setCancelButtonKey(BUTTON_KEY_CLOSE) // .setOkAction(okAction) // .setCancelAction(Stage::close); } + + public SimpleDialog.Builder prepareNoDDirectorySelectedDialog(Stage window) { + return createDialogBuilder() // + .setOwner(window) // + .setTitleKey("recover.invalidSelection.title") // + .setMessageKey("recover.invalidSelection.message") // + .setDescriptionKey("recover.invalidSelection.description") // + .setIcon(FontAwesome5Icon.EXCLAMATION) // + .setOkButtonKey("generic.button.change") // + .setOkAction(Stage::close); + } + } diff --git a/src/main/java/org/cryptomator/ui/dialogs/SimpleDialog.java b/src/main/java/org/cryptomator/ui/dialogs/SimpleDialog.java index 08f77849e..5efe4abfe 100644 --- a/src/main/java/org/cryptomator/ui/dialogs/SimpleDialog.java +++ b/src/main/java/org/cryptomator/ui/dialogs/SimpleDialog.java @@ -30,7 +30,7 @@ public class SimpleDialog { FxmlLoaderFactory loaderFactory = FxmlLoaderFactory.forController( // new SimpleDialogController(resolveText(builder.messageKey, null), // - resolveText(builder.descriptionKey, null), // + resolveText(builder.descriptionKey, builder.descriptionArgs), // builder.icon, // resolveText(builder.okButtonKey, null), // builder.cancelButtonKey != null ? resolveText(builder.cancelButtonKey, null) : null, // @@ -66,6 +66,7 @@ public class SimpleDialog { private String[] titleArgs; private String messageKey; private String descriptionKey; + private String[] descriptionArgs; private String okButtonKey; private String cancelButtonKey; private FontAwesome5Icon icon; @@ -93,8 +94,9 @@ public class SimpleDialog { return this; } - public Builder setDescriptionKey(String descriptionKey) { + public Builder setDescriptionKey(String descriptionKey, String... args) { this.descriptionKey = descriptionKey; + this.descriptionArgs = args; return this; } diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java index 8eb221883..74abac546 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java @@ -15,15 +15,13 @@ import org.cryptomator.ui.lock.LockComponent; import org.cryptomator.ui.mainwindow.MainWindowComponent; import org.cryptomator.ui.preferences.PreferencesComponent; import org.cryptomator.ui.quit.QuitComponent; +import org.cryptomator.ui.recoverykey.RecoveryKeyComponent; import org.cryptomator.ui.sharevault.ShareVaultComponent; import org.cryptomator.ui.traymenu.TrayMenuComponent; import org.cryptomator.ui.unlock.UnlockComponent; import org.cryptomator.ui.updatereminder.UpdateReminderComponent; import org.cryptomator.ui.vaultoptions.VaultOptionsComponent; -import javax.inject.Named; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; import javafx.scene.image.Image; import java.io.IOException; import java.io.InputStream; @@ -40,7 +38,8 @@ import java.io.InputStream; HealthCheckComponent.class, // UpdateReminderComponent.class, // ShareVaultComponent.class, // - EventViewComponent.class}) + EventViewComponent.class, // + RecoveryKeyComponent.class}) abstract class FxApplicationModule { private static Image createImageFromResource(String resourceName) throws IOException { diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationTerminator.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationTerminator.java index 89258dc6c..91ff918e3 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationTerminator.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationTerminator.java @@ -28,7 +28,7 @@ import static org.cryptomator.common.vaults.VaultState.Value.*; @FxApplicationScoped public class FxApplicationTerminator { - private static final Set STATES_ALLOWING_TERMINATION = EnumSet.of(LOCKED, NEEDS_MIGRATION, MISSING, ERROR); + private static final Set STATES_ALLOWING_TERMINATION = EnumSet.of(LOCKED, NEEDS_MIGRATION, MISSING, ERROR, VAULT_CONFIG_MISSING, ALL_MISSING); private static final Set STATES_PREVENT_TERMINATION = EnumSet.of(PROCESSING); private static final Logger LOG = LoggerFactory.getLogger(FxApplicationTerminator.class); diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileController.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileController.java index 9b2231921..4e1c663e9 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileController.java @@ -1,14 +1,18 @@ package org.cryptomator.ui.keyloading.masterkeyfile; +import org.cryptomator.common.recovery.RecoveryActionType; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.keyloading.KeyLoading; +import org.cryptomator.ui.recoverykey.RecoveryKeyComponent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; -import javafx.beans.binding.StringBinding; +import javafx.beans.property.SimpleObjectProperty; import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; import javafx.stage.FileChooser; import javafx.stage.Stage; import javafx.stage.WindowEvent; @@ -27,17 +31,41 @@ public class ChooseMasterkeyFileController implements FxController { private final Stage window; private final Vault vault; private final CompletableFuture result; + private final RecoveryKeyComponent.Factory recoveryKeyWindow; private final ResourceBundle resourceBundle; + @FXML + private CheckBox restoreInsteadCheckBox; + @FXML + private Button forwardButton; + @Inject - public ChooseMasterkeyFileController(@KeyLoading Stage window, @KeyLoading Vault vault, CompletableFuture result, ResourceBundle resourceBundle) { + public ChooseMasterkeyFileController(@KeyLoading Stage window, // + @KeyLoading Vault vault, // + CompletableFuture result, // + RecoveryKeyComponent.Factory recoveryKeyWindow, // + ResourceBundle resourceBundle) { this.window = window; this.vault = vault; this.result = result; + this.recoveryKeyWindow = recoveryKeyWindow; this.resourceBundle = resourceBundle; this.window.setOnHiding(this::windowClosed); } + @FXML + private void initialize() { + restoreInsteadCheckBox.selectedProperty().addListener((_, _, newVal) -> { + if (newVal) { + forwardButton.setText(resourceBundle.getString("addvaultwizard.existing.restore")); + forwardButton.setOnAction(_ -> restoreMasterkey()); + } else { + forwardButton.setText(resourceBundle.getString("generic.button.choose")); + forwardButton.setOnAction(_ -> proceed()); + } + }); + } + @FXML public void cancel() { window.close(); @@ -47,6 +75,13 @@ public class ChooseMasterkeyFileController implements FxController { result.cancel(true); } + @FXML + void restoreMasterkey() { + Stage ownerStage = (Stage) window.getOwner(); + window.close(); + recoveryKeyWindow.create(vault, ownerStage, new SimpleObjectProperty<>(RecoveryActionType.RESTORE_MASTERKEY)).showOnboardingDialogWindow(); + } + @FXML public void proceed() { LOG.trace("proceed()"); @@ -62,7 +97,7 @@ public class ChooseMasterkeyFileController implements FxController { //--- Setter & Getter --- - public String getDisplayName(){ + public String getDisplayName() { return vault.getDisplayName(); } diff --git a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java index fa1b441d9..a186fbe71 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java @@ -18,6 +18,7 @@ import org.cryptomator.ui.error.ErrorComponent; import org.cryptomator.ui.fxapp.FxApplicationTerminator; import org.cryptomator.ui.fxapp.PrimaryStage; import org.cryptomator.ui.migration.MigrationComponent; +import org.cryptomator.ui.recoverykey.RecoveryKeyComponent; import org.cryptomator.ui.stats.VaultStatisticsComponent; import org.cryptomator.ui.traymenu.TrayMenuComponent; import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent; @@ -32,7 +33,7 @@ import javafx.stage.Stage; import java.util.Map; import java.util.ResourceBundle; -@Module(subcomponents = {AddVaultWizardComponent.class, MigrationComponent.class, VaultStatisticsComponent.class, WrongFileAlertComponent.class, ErrorComponent.class}) +@Module(subcomponents = {AddVaultWizardComponent.class, MigrationComponent.class, VaultStatisticsComponent.class, WrongFileAlertComponent.class, ErrorComponent.class, RecoveryKeyComponent.class}) abstract class MainWindowModule { @Provides diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailController.java index 7e309fdaf..be4f7f78c 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailController.java @@ -52,7 +52,7 @@ public class VaultDetailController implements FxController { case LOCKED -> FontAwesome5Icon.LOCK; case PROCESSING -> FontAwesome5Icon.SPINNER; case UNLOCKED -> FontAwesome5Icon.LOCK_OPEN; - case NEEDS_MIGRATION, MISSING, ERROR -> FontAwesome5Icon.EXCLAMATION_TRIANGLE; + case NEEDS_MIGRATION, MISSING, VAULT_CONFIG_MISSING, ALL_MISSING, ERROR -> FontAwesome5Icon.EXCLAMATION_TRIANGLE; }; } else { return FontAwesome5Icon.EXCLAMATION_TRIANGLE; diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailMissingVaultController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailMissingVaultController.java index 6f57a0d17..85e71937b 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailMissingVaultController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailMissingVaultController.java @@ -1,20 +1,26 @@ package org.cryptomator.ui.mainwindow; +import org.cryptomator.common.recovery.RecoveryActionType; import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.dialogs.Dialogs; +import org.cryptomator.ui.keyloading.KeyLoadingStrategy; +import org.cryptomator.ui.recoverykey.RecoveryKeyComponent; import javax.inject.Inject; import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.stage.FileChooser; import javafx.stage.Stage; import java.io.File; +import java.nio.file.Files; import java.util.ResourceBundle; import static org.cryptomator.common.Constants.CRYPTOMATOR_FILENAME_GLOB; +import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; @MainWindowScoped public class VaultDetailMissingVaultController implements FxController { @@ -23,6 +29,7 @@ public class VaultDetailMissingVaultController implements FxController { private final ObservableList vaults; private final ResourceBundle resourceBundle; private final Stage window; + private final RecoveryKeyComponent.Factory recoveryKeyWindow; private final Dialogs dialogs; @Inject @@ -30,11 +37,13 @@ public class VaultDetailMissingVaultController implements FxController { ObservableList vaults, // ResourceBundle resourceBundle, // @MainWindow Stage window, // - Dialogs dialogs) { + Dialogs dialogs, // + RecoveryKeyComponent.Factory recoveryKeyWindow) { this.vault = vault; this.vaults = vaults; this.resourceBundle = resourceBundle; this.window = window; + this.recoveryKeyWindow = recoveryKeyWindow; this.dialogs = dialogs; } @@ -48,6 +57,19 @@ public class VaultDetailMissingVaultController implements FxController { dialogs.prepareRemoveVaultDialog(window, vault.get(), vaults).build().showAndWait(); } + @FXML + void restoreVaultConfig() { + if(KeyLoadingStrategy.isHubVault(vault.get().getVaultSettings().lastKnownKeyLoader.get())){ + dialogs.prepareContactHubVaultOwner(window).build().showAndWait(); + } + else if(Files.exists(vault.get().getPath().resolve(MASTERKEY_FILENAME))){ + recoveryKeyWindow.create(vault.get(), window, new SimpleObjectProperty<>(RecoveryActionType.RESTORE_VAULT_CONFIG)).showOnboardingDialogWindow(); + } + else { + recoveryKeyWindow.create(vault.get(), window, new SimpleObjectProperty<>(RecoveryActionType.RESTORE_ALL)).showOnboardingDialogWindow(); + } + } + @FXML void changeLocation() { // copied from ChooseExistingVaultController class diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellController.java index 75ce21dfe..9324c8c7b 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellController.java @@ -55,7 +55,7 @@ public class VaultListCellController implements FxController { case LOCKED -> FontAwesome5Icon.LOCK; case PROCESSING -> FontAwesome5Icon.SPINNER; case UNLOCKED -> FontAwesome5Icon.LOCK_OPEN; - case NEEDS_MIGRATION, MISSING, ERROR -> FontAwesome5Icon.EXCLAMATION_TRIANGLE; + case NEEDS_MIGRATION, MISSING, VAULT_CONFIG_MISSING, ALL_MISSING, ERROR -> FontAwesome5Icon.EXCLAMATION_TRIANGLE; }; } else { return FontAwesome5Icon.EXCLAMATION_TRIANGLE; diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultListContextMenuController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultListContextMenuController.java index 5e1fb8c35..db667f111 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultListContextMenuController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultListContextMenuController.java @@ -20,11 +20,13 @@ import javafx.stage.Stage; import java.util.EnumSet; import java.util.Objects; +import static org.cryptomator.common.vaults.VaultState.Value.ALL_MISSING; import static org.cryptomator.common.vaults.VaultState.Value.ERROR; import static org.cryptomator.common.vaults.VaultState.Value.LOCKED; import static org.cryptomator.common.vaults.VaultState.Value.MISSING; import static org.cryptomator.common.vaults.VaultState.Value.NEEDS_MIGRATION; import static org.cryptomator.common.vaults.VaultState.Value.UNLOCKED; +import static org.cryptomator.common.vaults.VaultState.Value.VAULT_CONFIG_MISSING; @MainWindowScoped public class VaultListContextMenuController implements FxController { @@ -63,7 +65,7 @@ public class VaultListContextMenuController implements FxController { this.selectedVaultState = selectedVault.flatMap(Vault::stateProperty).orElse(null); this.selectedVaultPassphraseStored = selectedVault.map(this::isPasswordStored).orElse(false); - this.selectedVaultRemovable = selectedVaultState.map(EnumSet.of(LOCKED, MISSING, ERROR, NEEDS_MIGRATION)::contains).orElse(false); + this.selectedVaultRemovable = selectedVaultState.map(EnumSet.of(LOCKED, MISSING, ERROR, NEEDS_MIGRATION, ALL_MISSING, VAULT_CONFIG_MISSING)::contains).orElse(false); this.selectedVaultUnlockable = selectedVaultState.map(LOCKED::equals).orElse(false); this.selectedVaultLockable = selectedVaultState.map(UNLOCKED::equals).orElse(false); } diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java index a457ade3f..f25528498 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java @@ -1,11 +1,16 @@ package org.cryptomator.ui.mainwindow; import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.common.recovery.RecoveryActionType; +import org.cryptomator.common.recovery.VaultPreparator; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.vaults.Vault; +import org.cryptomator.common.vaults.VaultComponent; import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.cryptofs.CryptoFileSystemProvider; import org.cryptomator.cryptofs.DirStructure; +import org.cryptomator.cryptofs.common.Constants; +import org.cryptomator.integrations.mount.MountService; import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.VaultService; @@ -13,6 +18,7 @@ import org.cryptomator.ui.dialogs.Dialogs; import org.cryptomator.ui.fxapp.FxFSEventList; import org.cryptomator.ui.fxapp.FxApplicationWindows; import org.cryptomator.ui.preferences.SelectedPreferencesTab; +import org.cryptomator.ui.recoverykey.RecoveryKeyComponent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,6 +28,7 @@ import javafx.beans.binding.BooleanBinding; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; @@ -37,11 +44,14 @@ import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; import javafx.scene.input.TransferMode; import javafx.scene.layout.StackPane; +import javafx.stage.DirectoryChooser; import javafx.stage.Stage; import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.EnumSet; +import java.util.List; import java.util.Optional; import java.util.ResourceBundle; import java.util.Set; @@ -50,10 +60,12 @@ import java.util.stream.Collectors; import static org.cryptomator.common.Constants.CRYPTOMATOR_FILENAME_EXT; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME; +import static org.cryptomator.common.vaults.VaultState.Value.ALL_MISSING; import static org.cryptomator.common.vaults.VaultState.Value.ERROR; import static org.cryptomator.common.vaults.VaultState.Value.LOCKED; import static org.cryptomator.common.vaults.VaultState.Value.MISSING; import static org.cryptomator.common.vaults.VaultState.Value.NEEDS_MIGRATION; +import static org.cryptomator.common.vaults.VaultState.Value.VAULT_CONFIG_MISSING; @MainWindowScoped public class VaultListController implements FxController { @@ -75,6 +87,10 @@ public class VaultListController implements FxController { private final ObservableValue cellSize; private final Dialogs dialogs; + private final VaultComponent.Factory vaultComponentFactory; + private final RecoveryKeyComponent.Factory recoveryKeyWindow; + private final List mountServices; + public ListView vaultList; public StackPane root; @FXML @@ -94,6 +110,9 @@ public class VaultListController implements FxController { FxApplicationWindows appWindows, // Settings settings, // Dialogs dialogs, // + RecoveryKeyComponent.Factory recoveryKeyWindow, // + VaultComponent.Factory vaultComponentFactory, // + List mountServices, // FxFSEventList fxFSEventList) { this.mainWindow = mainWindow; this.vaults = vaults; @@ -105,6 +124,9 @@ public class VaultListController implements FxController { this.resourceBundle = resourceBundle; this.appWindows = appWindows; this.dialogs = dialogs; + this.recoveryKeyWindow = recoveryKeyWindow; + this.vaultComponentFactory = vaultComponentFactory; + this.mountServices = mountServices; this.emptyVaultList = Bindings.isEmpty(vaults); this.unreadEvents = fxFSEventList.unreadEventsProperty(); @@ -204,6 +226,26 @@ public class VaultListController implements FxController { VaultListManager.redetermineVaultState(newValue); } + private Optional chooseValidVaultDirectory() { + DirectoryChooser directoryChooser = new DirectoryChooser(); + File selectedDirectory; + + do { + selectedDirectory = directoryChooser.showDialog(mainWindow); + if (selectedDirectory == null) { + return Optional.empty(); + } + + Path selectedPath = selectedDirectory.toPath(); + if (!Files.isDirectory(selectedPath.resolve(Constants.DATA_DIR_NAME))) { + dialogs.prepareNoDDirectorySelectedDialog(mainWindow).build().showAndWait(); + selectedDirectory = null; + } + } while (selectedDirectory == null); + + return Optional.of(selectedDirectory.toPath()); + } + @FXML public void didClickAddNewVault() { addVaultWizard.build().showAddNewVaultWizard(resourceBundle); @@ -214,9 +256,40 @@ public class VaultListController implements FxController { addVaultWizard.build().showAddExistingVaultWizard(resourceBundle); } + @FXML + public void didClickRecoverExistingVault() { + Optional selectedDirectory = chooseValidVaultDirectory(); + if (selectedDirectory.isEmpty()) { + return; + } + + Path path = selectedDirectory.get(); + Optional matchingVaultListEntry = vaultListManager.get(path); + if (matchingVaultListEntry.isPresent()) { + dialogs.prepareRecoveryVaultAlreadyExists(mainWindow, matchingVaultListEntry.get().getDisplayName()) // + .setOkAction(Stage::close) // + .build().showAndWait(); + return; + } + + Vault preparedVault = VaultPreparator.prepareVault(path, vaultComponentFactory, mountServices, resourceBundle); + VaultListManager.redetermineVaultState(preparedVault); + + switch (preparedVault.getState()) { + case VAULT_CONFIG_MISSING -> recoveryKeyWindow.create(preparedVault, mainWindow, new SimpleObjectProperty<>(RecoveryActionType.RESTORE_VAULT_CONFIG)).showOnboardingDialogWindow(); + case ALL_MISSING -> recoveryKeyWindow.create(preparedVault, mainWindow, new SimpleObjectProperty<>(RecoveryActionType.RESTORE_ALL)).showOnboardingDialogWindow(); + case LOCKED, NEEDS_MIGRATION -> { + vaultListManager.addVault(preparedVault); + dialogs.prepareRecoveryVaultAdded(mainWindow, preparedVault.getDisplayName()).setOkAction(Stage::close).build().showAndWait(); + } + default -> LOG.warn("Unhandled vault state during recovery: {}", preparedVault.getState()); + } + + } + private void pressedShortcutToRemoveVault() { final var vault = selectedVault.get(); - if (vault != null && EnumSet.of(LOCKED, MISSING, ERROR, NEEDS_MIGRATION).contains(vault.getState())) { + if (vault != null && EnumSet.of(LOCKED, MISSING, ERROR, NEEDS_MIGRATION, ALL_MISSING, VAULT_CONFIG_MISSING).contains(vault.getState())) { dialogs.prepareRemoveVaultDialog(mainWindow, vault, vaults).build().showAndWait(); } } diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyComponent.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyComponent.java index 3986fa01d..6bfe36a4f 100644 --- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyComponent.java +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyComponent.java @@ -3,11 +3,13 @@ package org.cryptomator.ui.recoverykey; import dagger.BindsInstance; import dagger.Lazy; import dagger.Subcomponent; +import org.cryptomator.common.recovery.RecoveryActionType; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; import javax.inject.Named; +import javafx.beans.property.ObjectProperty; import javafx.scene.Scene; import javafx.stage.Stage; @@ -24,6 +26,9 @@ public interface RecoveryKeyComponent { @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy recoverScene(); + @FxmlScene(FxmlFile.RECOVERYKEY_ONBOARDING) + Lazy recoverOnboardingScene(); + default void showRecoveryKeyCreationWindow() { Stage stage = window(); stage.setScene(creationScene().get()); @@ -38,11 +43,19 @@ public interface RecoveryKeyComponent { stage.show(); } + default void showOnboardingDialogWindow() { + Stage stage = window(); + stage.setScene(recoverOnboardingScene().get()); + stage.sizeToScene(); + stage.show(); + } @Subcomponent.Factory interface Factory { - RecoveryKeyComponent create(@BindsInstance @RecoveryKeyWindow Vault vault, @BindsInstance @Named("keyRecoveryOwner") Stage owner); + RecoveryKeyComponent create(@BindsInstance @RecoveryKeyWindow Vault vault, // + @BindsInstance @Named("keyRecoveryOwner") Stage owner, // + @BindsInstance @Named("recoverType") ObjectProperty recoverType); } } diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java index 77f191015..456de11f4 100644 --- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java @@ -1,28 +1,45 @@ package org.cryptomator.ui.recoverykey; import dagger.Lazy; +import org.cryptomator.common.recovery.CryptoFsInitializer; +import org.cryptomator.common.recovery.MasterkeyService; +import org.cryptomator.common.recovery.RecoveryActionType; +import org.cryptomator.common.recovery.RecoveryDirectory; import org.cryptomator.common.vaults.Vault; +import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.InvalidPassphraseException; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; import org.cryptomator.ui.common.Animations; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; +import org.cryptomator.ui.controls.FormattedLabel; import org.cryptomator.ui.controls.NiceSecurePasswordField; +import org.cryptomator.ui.dialogs.Dialogs; import org.cryptomator.ui.fxapp.FxApplicationWindows; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; +import javax.inject.Named; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; import javafx.beans.property.StringProperty; import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.scene.Scene; +import javafx.scene.control.Button; import javafx.stage.Stage; import java.io.IOException; +import java.nio.file.Path; import java.util.ResourceBundle; import java.util.concurrent.ExecutorService; +import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; +import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME; + @RecoveryKeyScoped public class RecoveryKeyCreationController implements FxController { @@ -30,23 +47,71 @@ public class RecoveryKeyCreationController implements FxController { private final Stage window; private final Lazy successScene; + private final Lazy recoverykeyExpertSettingsScene; + private final MasterkeyFileAccess masterkeyFileAccess; private final Vault vault; private final ExecutorService executor; private final RecoveryKeyFactory recoveryKeyFactory; private final StringProperty recoveryKeyProperty; private final FxApplicationWindows appWindows; public NiceSecurePasswordField passwordField; + private final IntegerProperty shorteningThreshold; + private final ObjectProperty recoverType; + private final ResourceBundle resourceBundle; + public FormattedLabel descriptionLabel; + public Button cancelButton; + public Button nextButton; + private final VaultListManager vaultListManager; + private final Dialogs dialogs; @Inject - public RecoveryKeyCreationController(@RecoveryKeyWindow Stage window, @FxmlScene(FxmlFile.RECOVERYKEY_SUCCESS) Lazy successScene, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, FxApplicationWindows appWindows, ResourceBundle resourceBundle) { + public RecoveryKeyCreationController(FxApplicationWindows appWindows, // + @RecoveryKeyWindow Stage window, // + @FxmlScene(FxmlFile.RECOVERYKEY_SUCCESS) Lazy successScene, // + @FxmlScene(FxmlFile.RECOVERYKEY_EXPERT_SETTINGS) Lazy recoverykeyExpertSettingsScene, // + @RecoveryKeyWindow Vault vault, // + RecoveryKeyFactory recoveryKeyFactory, // + MasterkeyFileAccess masterkeyFileAccess, // + ExecutorService executor, // + @RecoveryKeyWindow StringProperty recoveryKey, // + @Named("shorteningThreshold") IntegerProperty shorteningThreshold, // + @Named("recoverType") ObjectProperty recoverType, // + VaultListManager vaultListManager, // + ResourceBundle resourceBundle, // + Dialogs dialogs) { this.window = window; - window.setTitle(resourceBundle.getString("recoveryKey.display.title")); this.successScene = successScene; + this.recoverykeyExpertSettingsScene = recoverykeyExpertSettingsScene; this.vault = vault; this.executor = executor; this.recoveryKeyFactory = recoveryKeyFactory; this.recoveryKeyProperty = recoveryKey; this.appWindows = appWindows; + this.recoverType = recoverType; + this.resourceBundle = resourceBundle; + this.masterkeyFileAccess = masterkeyFileAccess; + this.shorteningThreshold = shorteningThreshold; + this.vaultListManager = vaultListManager; + this.dialogs = dialogs; + } + + @FXML + public void initialize() { + if (recoverType.get() == RecoveryActionType.SHOW_KEY) { + window.setTitle(resourceBundle.getString("recoveryKey.display.title")); + } else if (recoverType.get() == RecoveryActionType.RESTORE_VAULT_CONFIG) { + window.setTitle(resourceBundle.getString("recover.recoverVaultConfig.title")); + descriptionLabel.formatProperty().set(resourceBundle.getString("recoveryKey.recover.description")); + cancelButton.setOnAction((_) -> back()); + cancelButton.setText(resourceBundle.getString("generic.button.back")); + nextButton.setOnAction((_) -> restoreWithPassword()); + } + } + + @FXML + public void back() { + window.setScene(recoverykeyExpertSettingsScene.get()); + window.centerOnScreen(); } @FXML @@ -71,6 +136,42 @@ public class RecoveryKeyCreationController implements FxController { executor.submit(task); } + @FXML + public void restoreWithPassword() { + + try (RecoveryDirectory recoveryDirectory = RecoveryDirectory.create(vault.getPath())) { + Path recoveryPath = recoveryDirectory.getRecoveryPath(); + + Path masterkeyFilePath = vault.getPath().resolve(MASTERKEY_FILENAME); + + try (Masterkey masterkey = MasterkeyService.load(masterkeyFileAccess, masterkeyFilePath, passwordField.getCharacters())) { + var combo = MasterkeyService.detect(masterkey, vault.getPath()) + .orElseThrow(() -> new IllegalStateException("Could not detect combo for vault path: " + vault.getPath())); + + CryptoFsInitializer.init(recoveryPath, masterkey, shorteningThreshold.get(), combo); + } + + recoveryDirectory.moveRecoveredFile(VAULTCONFIG_FILENAME); + + if (!vaultListManager.isAlreadyAdded(vault.getPath())) { + vaultListManager.add(vault.getPath()); + } + window.close(); + dialogs.prepareRecoverPasswordSuccess((Stage)window.getOwner()) // + .setTitleKey("recover.recoverVaultConfig.title") // + .setMessageKey("recoveryKey.recover.resetVaultConfigSuccess.message") // + .setDescriptionKey("recoveryKey.recover.resetMasterkeyFileSuccess.description") + .build().showAndWait(); + + } catch (InvalidPassphraseException e) { + LOG.info("Password invalid", e); + Animations.createShakeWindowAnimation(window).play(); + } catch (IOException | CryptoException | IllegalStateException e) { + LOG.error("Recovery process failed", e); + appWindows.showErrorWindow(e, window, null); + } + } + @FXML public void close() { window.close(); diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyExpertSettingsController.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyExpertSettingsController.java new file mode 100644 index 000000000..5e72b8969 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyExpertSettingsController.java @@ -0,0 +1,123 @@ +package org.cryptomator.ui.recoverykey; + +import javax.inject.Inject; +import javax.inject.Named; +import javafx.application.Application; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; +import javafx.fxml.FXML; +import javafx.scene.Scene; +import javafx.scene.control.CheckBox; +import javafx.stage.Stage; + +import dagger.Lazy; +import org.cryptomator.common.recovery.RecoveryActionType; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.common.vaults.VaultState; +import org.cryptomator.ui.addvaultwizard.CreateNewVaultExpertSettingsController; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; +import org.cryptomator.ui.controls.NumericTextField; + +@RecoveryKeyScoped +public class RecoveryKeyExpertSettingsController implements FxController { + + public static final int MAX_SHORTENING_THRESHOLD = 220; + public static final int MIN_SHORTENING_THRESHOLD = 36; + private static final String DOCS_NAME_SHORTENING_URL = "https://docs.cryptomator.org/security/vault/#name-shortening"; + + private final Stage window; + private final Lazy application; + private final Vault vault; + private final ObjectProperty recoverType; + private final IntegerProperty shorteningThreshold; + private final Lazy resetPasswordScene; + private final Lazy createScene; + private final Lazy onBoardingScene; + private final Lazy recoverScene; + private final BooleanBinding validShorteningThreshold; + + @FXML + public CheckBox expertSettingsCheckBox; + @FXML + public NumericTextField shorteningThresholdTextField; + + @Inject + public RecoveryKeyExpertSettingsController(@RecoveryKeyWindow Stage window, // + Lazy application, // + @RecoveryKeyWindow Vault vault, // + @Named("recoverType") ObjectProperty recoverType, // + @Named("shorteningThreshold") IntegerProperty shorteningThreshold, // + @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) Lazy resetPasswordScene, // + @FxmlScene(FxmlFile.RECOVERYKEY_CREATE) Lazy createScene, // + @FxmlScene(FxmlFile.RECOVERYKEY_ONBOARDING) Lazy onBoardingScene, // + @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy recoverScene) { + this.window = window; + this.application = application; + this.vault = vault; + this.recoverType = recoverType; + this.shorteningThreshold = shorteningThreshold; + this.resetPasswordScene = resetPasswordScene; + this.createScene = createScene; + this.onBoardingScene = onBoardingScene; + this.recoverScene = recoverScene; + this.validShorteningThreshold = Bindings.createBooleanBinding(this::isValidShorteningThreshold, shorteningThreshold); + } + + @FXML + public void initialize() { + shorteningThresholdTextField.setPromptText(MIN_SHORTENING_THRESHOLD + "-" + MAX_SHORTENING_THRESHOLD); + shorteningThresholdTextField.setText(Integer.toString(MAX_SHORTENING_THRESHOLD)); + shorteningThresholdTextField.textProperty().addListener((_, _, newValue) -> { + try { + int intValue = Integer.parseInt(newValue); + shorteningThreshold.set(intValue); + } catch (NumberFormatException e) { + shorteningThreshold.set(0); //the value is set to 0 to ensure that an invalid value assignment is detected during a NumberFormatException + } + }); + } + + @FXML + public void toggleUseExpertSettings() { + if (!expertSettingsCheckBox.isSelected()) { + shorteningThresholdTextField.setText(Integer.toString(CreateNewVaultExpertSettingsController.MAX_SHORTENING_THRESHOLD)); + } + } + + public void openDocs() { + application.get().getHostServices().showDocument(DOCS_NAME_SHORTENING_URL); + } + + public BooleanBinding validShorteningThresholdProperty() { + return validShorteningThreshold; + } + + public boolean isValidShorteningThreshold() { + var value = shorteningThreshold.get(); + return value >= MIN_SHORTENING_THRESHOLD && value <= MAX_SHORTENING_THRESHOLD; + } + + @FXML + public void back() { + if (recoverType.get() == RecoveryActionType.RESTORE_ALL && vault.getState() == VaultState.Value.VAULT_CONFIG_MISSING) { + window.setScene(recoverScene.get()); + } else if (recoverType.get() == RecoveryActionType.RESTORE_ALL && vault.getState() == VaultState.Value.ALL_MISSING) { + window.setScene(recoverScene.get()); + } else if (recoverType.get() == RecoveryActionType.RESTORE_VAULT_CONFIG) { + window.setScene(onBoardingScene.get()); + } + } + + @FXML + public void next() { + if (recoverType.get() == RecoveryActionType.RESTORE_VAULT_CONFIG) { + window.setScene(createScene.get()); + } else { + window.setScene(resetPasswordScene.get()); + } + } +} diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java index 06095eebc..809d16b61 100644 --- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java @@ -5,8 +5,12 @@ import dagger.Module; import dagger.Provides; import dagger.multibindings.IntoMap; import org.cryptomator.common.Nullable; +import org.cryptomator.common.recovery.RecoveryActionType; import org.cryptomator.common.vaults.Vault; import org.cryptomator.cryptofs.VaultConfig; +import org.cryptomator.cryptolib.api.CryptorProvider; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; +import org.cryptomator.ui.addvaultwizard.CreateNewVaultExpertSettingsController; import org.cryptomator.ui.common.DefaultSceneFactory; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxControllerKey; @@ -19,6 +23,10 @@ import org.cryptomator.ui.common.StageFactory; import javax.inject.Named; import javax.inject.Provider; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.scene.Scene; @@ -99,12 +107,18 @@ abstract class RecoveryKeyModule { } @Provides - @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD_SUCCESS) + @FxmlScene(FxmlFile.RECOVERYKEY_ONBOARDING) @RecoveryKeyScoped - static Scene provideRecoveryKeyResetPasswordSuccessScene(@RecoveryKeyWindow FxmlLoaderFactory fxmlLoaders) { - return fxmlLoaders.createScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD_SUCCESS); + static Scene provideRecoveryKeyOnboardingScene(@RecoveryKeyWindow FxmlLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene(FxmlFile.RECOVERYKEY_ONBOARDING); } + @Provides + @FxmlScene(FxmlFile.RECOVERYKEY_EXPERT_SETTINGS) + @RecoveryKeyScoped + static Scene provideRecoveryKeyExpertSettingsScene(@RecoveryKeyWindow FxmlLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene(FxmlFile.RECOVERYKEY_EXPERT_SETTINGS); + } // ------------------ @@ -120,6 +134,25 @@ abstract class RecoveryKeyModule { return new RecoveryKeyDisplayController(window, vault.getDisplayName(), recoveryKey.get(), localization); } + @Provides + @Named("shorteningThreshold") + @RecoveryKeyScoped + static IntegerProperty provideShorteningThreshold() { + return new SimpleIntegerProperty(CreateNewVaultExpertSettingsController.MAX_SHORTENING_THRESHOLD); + } + + @Provides + @Named("cipherCombo") + @RecoveryKeyScoped + static ObjectProperty provideCipherCombo() { + return new SimpleObjectProperty<>(); + } + + @Binds + @IntoMap + @FxControllerKey(RecoveryKeyExpertSettingsController.class) + abstract FxController provideRecoveryKeyExpertSettingsController(RecoveryKeyExpertSettingsController controller); + @Binds @IntoMap @FxControllerKey(RecoveryKeyRecoverController.class) @@ -137,14 +170,14 @@ abstract class RecoveryKeyModule { @Binds @IntoMap - @FxControllerKey(RecoveryKeyResetPasswordSuccessController.class) - abstract FxController bindRecoveryKeyResetPasswordSuccessController(RecoveryKeyResetPasswordSuccessController controller); + @FxControllerKey(RecoveryKeyOnboardingController.class) + abstract FxController bindRecoveryKeyOnboardingController(RecoveryKeyOnboardingController controller); @Provides @IntoMap @FxControllerKey(RecoveryKeyValidateController.class) - static FxController bindRecoveryKeyValidateController(@RecoveryKeyWindow Vault vault, @RecoveryKeyWindow @Nullable VaultConfig.UnverifiedVaultConfig vaultConfig, @RecoveryKeyWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory) { - return new RecoveryKeyValidateController(vault, vaultConfig, recoveryKey, recoveryKeyFactory); + static FxController bindRecoveryKeyValidateController(@RecoveryKeyWindow Vault vault, @RecoveryKeyWindow @Nullable VaultConfig.UnverifiedVaultConfig vaultConfig, @RecoveryKeyWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory, @Named("recoverType") ObjectProperty recoverType, @Named("cipherCombo") ObjectProperty cipherCombo, @Nullable MasterkeyFileAccess masterkeyFileAccess) { + return new RecoveryKeyValidateController(vault, vaultConfig, recoveryKey, recoveryKeyFactory, masterkeyFileAccess, recoverType, cipherCombo); } @Provides diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyOnboardingController.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyOnboardingController.java new file mode 100644 index 000000000..dd15413d8 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyOnboardingController.java @@ -0,0 +1,176 @@ +package org.cryptomator.ui.recoverykey; + +import dagger.Lazy; +import org.cryptomator.common.recovery.RecoveryActionType; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.common.vaults.VaultState; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; + +import javax.inject.Inject; +import javax.inject.Named; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.ObjectProperty; +import javafx.fxml.FXML; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.Toggle; +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; +import java.util.ResourceBundle; + +import static org.cryptomator.common.recovery.RecoveryActionType.RESTORE_ALL; +import static org.cryptomator.common.recovery.RecoveryActionType.RESTORE_VAULT_CONFIG; + +@RecoveryKeyScoped +public class RecoveryKeyOnboardingController implements FxController { + + private final Stage window; + private final Vault vault; + private final Lazy recoverykeyRecoverScene; + private final Lazy recoverykeyExpertSettingsScene; + private final ObjectProperty recoverType; + private final ResourceBundle resourceBundle; + + public Label titleLabel; + public Label messageLabel; + public Label pleaseConfirm; + public Label secondTextDesc; + + @FXML + private CheckBox affirmationBox; + @FXML + private RadioButton recoveryKeyRadio; + @FXML + private RadioButton passwordRadio; + @FXML + private Button nextButton; + @FXML + private VBox chooseMethodeBox; + @FXML + private ToggleGroup methodToggleGroup = new ToggleGroup(); + @FXML + private HBox hBox; + + @Inject + public RecoveryKeyOnboardingController(@RecoveryKeyWindow Stage window, // + @RecoveryKeyWindow Vault vault, // + @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy recoverykeyRecoverScene, // + @FxmlScene(FxmlFile.RECOVERYKEY_EXPERT_SETTINGS) Lazy recoverykeyExpertSettingsScene, // + @Named("recoverType") ObjectProperty recoverType, // + ResourceBundle resourceBundle) { + this.window = window; + this.vault = vault; + this.recoverykeyRecoverScene = recoverykeyRecoverScene; + this.recoverykeyExpertSettingsScene = recoverykeyExpertSettingsScene; + this.recoverType = recoverType; + this.resourceBundle = resourceBundle; + } + + @FXML + public void initialize() { + recoveryKeyRadio.setToggleGroup(methodToggleGroup); + passwordRadio.setToggleGroup(methodToggleGroup); + + BooleanBinding showMethodSelection = Bindings.createBooleanBinding( + () -> recoverType.get() == RecoveryActionType.RESTORE_VAULT_CONFIG, recoverType); + chooseMethodeBox.visibleProperty().bind(showMethodSelection); + chooseMethodeBox.managedProperty().bind(showMethodSelection); + + nextButton.disableProperty().bind( + affirmationBox.selectedProperty().not() + .or(methodToggleGroup.selectedToggleProperty().isNull().and(showMethodSelection)) + ); + + switch (recoverType.get()) { + case RESTORE_MASTERKEY -> { + window.setTitle(resourceBundle.getString("recover.recoverMasterkey.title")); + messageLabel.setVisible(false); + messageLabel.setManaged(false); + pleaseConfirm.setText(resourceBundle.getString("recover.onBoarding.pleaseConfirm")); + } + case RESTORE_ALL -> { + window.setTitle(resourceBundle.getString("recover.recoverVaultConfig.title")); + messageLabel.setVisible(true); + messageLabel.setManaged(true); + pleaseConfirm.setText(resourceBundle.getString("recover.onBoarding.otherwisePleaseConfirm")); + } + case RESTORE_VAULT_CONFIG -> { + window.setTitle(resourceBundle.getString("recover.recoverVaultConfig.title")); + messageLabel.setVisible(false); + messageLabel.setManaged(false); + pleaseConfirm.setText(resourceBundle.getString("recover.onBoarding.pleaseConfirm")); + } + default -> window.setTitle(""); + } + + if (vault.getState() == VaultState.Value.ALL_MISSING) { + messageLabel.setText(resourceBundle.getString("recover.onBoarding.allMissing.intro")); + } else { + messageLabel.setText(resourceBundle.getString("recover.onBoarding.intro")); + } + + titleLabel.textProperty().bind(Bindings.createStringBinding(() -> + recoverType.get() == RecoveryActionType.RESTORE_MASTERKEY + ? resourceBundle.getString("recover.recoverMasterkey.title") + : resourceBundle.getString("recover.recoverVaultConfig.title"), recoverType)); + + BooleanBinding isRestoreMasterkey = Bindings.createBooleanBinding( + () -> recoverType.get() == RecoveryActionType.RESTORE_MASTERKEY, recoverType); + hBox.minHeightProperty().bind(Bindings.when(isRestoreMasterkey).then(206.0).otherwise(Region.USE_COMPUTED_SIZE)); + + secondTextDesc.textProperty().bind(Bindings.createStringBinding(() -> { + RecoveryActionType type = recoverType.get(); + Toggle sel = methodToggleGroup.getSelectedToggle(); + return switch (type) { + case RESTORE_VAULT_CONFIG -> resourceBundle.getString(sel == passwordRadio + ? "recover.onBoarding.intro.password" + : "recover.onBoarding.intro.recoveryKey"); + case RESTORE_MASTERKEY -> resourceBundle.getString("recover.onBoarding.intro.masterkey.recoveryKey"); + case RESTORE_ALL -> resourceBundle.getString("recover.onBoarding.intro.recoveryKey"); + default -> ""; + }; + }, recoverType, methodToggleGroup.selectedToggleProperty())); + + showMethodSelection.addListener((_, _, nowShown) -> { + if (nowShown && methodToggleGroup.getSelectedToggle() == null) { + methodToggleGroup.selectToggle(recoveryKeyRadio); + } + }); + } + + @FXML + public void close() { + window.close(); + } + + @FXML + public void next() { + switch (recoverType.get()) { + case RESTORE_VAULT_CONFIG, RESTORE_ALL -> { + Object selectedToggle = methodToggleGroup.getSelectedToggle(); + if (selectedToggle == recoveryKeyRadio) { + recoverType.set(RESTORE_ALL); + window.setScene(recoverykeyRecoverScene.get()); + } else if (selectedToggle == passwordRadio) { + recoverType.set(RESTORE_VAULT_CONFIG); + window.setScene(recoverykeyExpertSettingsScene.get()); + } else { + window.setScene(recoverykeyRecoverScene.get()); + } + } + case RESTORE_MASTERKEY -> window.setScene(recoverykeyRecoverScene.get()); + default -> window.setScene(recoverykeyRecoverScene.get()); // Fallback + } + window.centerOnScreen(); + } + +} diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java index 944c52043..8eb505d47 100644 --- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java @@ -1,54 +1,105 @@ package org.cryptomator.ui.recoverykey; import dagger.Lazy; -import org.cryptomator.common.Nullable; +import org.cryptomator.common.recovery.RecoveryActionType; import org.cryptomator.common.vaults.Vault; -import org.cryptomator.cryptofs.VaultConfig; +import org.cryptomator.common.vaults.VaultState; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import javax.inject.Inject; -import javafx.beans.Observable; -import javafx.beans.property.StringProperty; -import javafx.beans.value.ObservableValue; +import javax.inject.Named; +import javafx.beans.property.ObjectProperty; import javafx.fxml.FXML; import javafx.scene.Scene; +import javafx.scene.control.Button; import javafx.stage.Stage; import java.util.ResourceBundle; @RecoveryKeyScoped public class RecoveryKeyRecoverController implements FxController { - private static final Logger LOG = LoggerFactory.getLogger(RecoveryKeyCreationController.class); - private final Stage window; - private final Lazy resetPasswordScene; + private final Vault vault; + private final Lazy nextScene; + private final Lazy onBoardingScene; + private final ResourceBundle resourceBundle; + public ObjectProperty recoverType; + + @FXML + private Button cancelButton; @FXML RecoveryKeyValidateController recoveryKeyValidateController; @Inject - public RecoveryKeyRecoverController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, @RecoveryKeyWindow StringProperty recoveryKey, @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) Lazy resetPasswordScene, ResourceBundle resourceBundle) { + public RecoveryKeyRecoverController(@RecoveryKeyWindow Stage window, // + @RecoveryKeyWindow Vault vault, // + ResourceBundle resourceBundle, // + @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) Lazy resetPasswordScene, // + @FxmlScene(FxmlFile.RECOVERYKEY_EXPERT_SETTINGS) Lazy expertSettingsScene, // + @FxmlScene(FxmlFile.RECOVERYKEY_ONBOARDING) Lazy onBoardingScene, // + @Named("recoverType") ObjectProperty recoverType) { this.window = window; - window.setTitle(resourceBundle.getString("recoveryKey.recover.title")); - this.resetPasswordScene = resetPasswordScene; + this.vault = vault; + this.resourceBundle = resourceBundle; + this.onBoardingScene = onBoardingScene; + this.recoverType = recoverType; + this.nextScene = switch (recoverType.get()) { + case RESTORE_ALL, RESTORE_VAULT_CONFIG -> { + window.setTitle(resourceBundle.getString("recover.recoverVaultConfig.title")); + yield expertSettingsScene; + } + case RESTORE_MASTERKEY -> { + window.setTitle(resourceBundle.getString("recover.recoverMasterkey.title")); + yield resetPasswordScene; + } + case RESET_PASSWORD -> { + window.setTitle(resourceBundle.getString("recoveryKey.recover.title")); + yield resetPasswordScene; + } + case SHOW_KEY -> { + window.setTitle(resourceBundle.getString("recoveryKey.display.title")); + yield resetPasswordScene; + } + default -> throw new IllegalArgumentException("Unexpected recovery action type: " + recoverType.get()); + }; } @FXML public void initialize() { + if (recoverType.get() == RecoveryActionType.RESET_PASSWORD) { + cancelButton.setText(resourceBundle.getString("generic.button.cancel")); + } else { + cancelButton.setText(resourceBundle.getString("generic.button.back")); + } } @FXML - public void close() { - window.close(); + public void closeOrReturn() { + switch (recoverType.get()) { + case RESET_PASSWORD -> window.close(); + case RESTORE_MASTERKEY -> { + window.setScene(onBoardingScene.get()); + window.centerOnScreen(); + } + default -> { + if(vault.getState().equals(VaultState.Value.ALL_MISSING)){ + recoverType.set(RecoveryActionType.RESTORE_ALL); + } + else { + recoverType.set(RecoveryActionType.RESTORE_VAULT_CONFIG); + } + window.setScene(onBoardingScene.get()); + window.centerOnScreen(); + } + } } @FXML public void recover() { - window.setScene(resetPasswordScene.get()); + window.setScene(nextScene.get()); } /* Getter/Setter */ diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java index 18a952ea5..0c06ba9b2 100644 --- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java @@ -1,25 +1,44 @@ package org.cryptomator.ui.recoverykey; import dagger.Lazy; +import org.cryptomator.common.recovery.CryptoFsInitializer; +import org.cryptomator.common.recovery.MasterkeyService; +import org.cryptomator.common.recovery.RecoveryActionType; +import org.cryptomator.common.recovery.RecoveryDirectory; import org.cryptomator.common.vaults.Vault; +import org.cryptomator.common.vaults.VaultListManager; +import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.CryptorProvider; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; +import org.cryptomator.ui.changepassword.NewPasswordController; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; -import org.cryptomator.ui.changepassword.NewPasswordController; +import org.cryptomator.ui.dialogs.Dialogs; import org.cryptomator.ui.fxapp.FxApplicationWindows; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; +import javax.inject.Named; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.StringProperty; import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.scene.Scene; +import javafx.scene.control.Button; import javafx.stage.Stage; import java.io.IOException; +import java.nio.file.Path; +import java.util.ResourceBundle; import java.util.concurrent.ExecutorService; +import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; +import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME; + @RecoveryKeyScoped public class RecoveryKeyResetPasswordController implements FxController { @@ -30,48 +49,140 @@ public class RecoveryKeyResetPasswordController implements FxController { private final RecoveryKeyFactory recoveryKeyFactory; private final ExecutorService executor; private final StringProperty recoveryKey; - private final Lazy recoverResetPasswordSuccessScene; + private final Lazy recoverExpertSettingsScene; + private final Lazy recoverykeyRecoverScene; private final FxApplicationWindows appWindows; + private final MasterkeyFileAccess masterkeyFileAccess; + private final VaultListManager vaultListManager; + private final IntegerProperty shorteningThreshold; + private final ObjectProperty recoverType; + private final ObjectProperty cipherCombo; + private final ResourceBundle resourceBundle; + private final Dialogs dialogs; public NewPasswordController newPasswordController; + public Button nextButton; @Inject - public RecoveryKeyResetPasswordController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD_SUCCESS) Lazy recoverResetPasswordSuccessScene, FxApplicationWindows appWindows) { + public RecoveryKeyResetPasswordController(@RecoveryKeyWindow Stage window, // + @RecoveryKeyWindow Vault vault, // + RecoveryKeyFactory recoveryKeyFactory, // + ExecutorService executor, // + @RecoveryKeyWindow StringProperty recoveryKey, // + @FxmlScene(FxmlFile.RECOVERYKEY_EXPERT_SETTINGS) Lazy recoverExpertSettingsScene, // + @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy recoverykeyRecoverScene, // + FxApplicationWindows appWindows, // + MasterkeyFileAccess masterkeyFileAccess, // + VaultListManager vaultListManager, // + @Named("shorteningThreshold") IntegerProperty shorteningThreshold, // + @Named("recoverType") ObjectProperty recoverType, // + @Named("cipherCombo") ObjectProperty cipherCombo, // + ResourceBundle resourceBundle, // + Dialogs dialogs) { this.window = window; this.vault = vault; this.recoveryKeyFactory = recoveryKeyFactory; this.executor = executor; this.recoveryKey = recoveryKey; - this.recoverResetPasswordSuccessScene = recoverResetPasswordSuccessScene; + this.recoverExpertSettingsScene = recoverExpertSettingsScene; + this.recoverykeyRecoverScene = recoverykeyRecoverScene; this.appWindows = appWindows; + this.masterkeyFileAccess = masterkeyFileAccess; + this.vaultListManager = vaultListManager; + this.shorteningThreshold = shorteningThreshold; + this.cipherCombo = cipherCombo; + this.recoverType = recoverType; + this.resourceBundle = resourceBundle; + this.dialogs = dialogs; + } + + @FXML + public void initialize() { + switch (recoverType.get()) { + case RESTORE_MASTERKEY, RESTORE_ALL -> nextButton.setText(resourceBundle.getString("recoveryKey.recover.recoverBtn")); + case RESET_PASSWORD -> nextButton.setText(resourceBundle.getString("recoveryKey.recover.resetBtn")); + default -> nextButton.setText(resourceBundle.getString("recoveryKey.recover.recoverBtn")); // Fallback + } } @FXML public void close() { - window.close(); + switch (recoverType.get()) { + case RESTORE_ALL -> window.setScene(recoverExpertSettingsScene.get()); + case RESTORE_MASTERKEY, RESET_PASSWORD -> window.setScene(recoverykeyRecoverScene.get()); + default -> window.close(); + } + } + + @FXML + public void next() { + switch (recoverType.get()) { + case RESTORE_ALL -> restorePassword(); + case RESTORE_MASTERKEY, RESET_PASSWORD -> resetPassword(); + default -> resetPassword(); // Fallback + } + } + + @FXML + public void restorePassword() { + try (RecoveryDirectory recoveryDirectory = RecoveryDirectory.create(vault.getPath())) { + Path recoveryPath = recoveryDirectory.getRecoveryPath(); + MasterkeyService.recoverFromRecoveryKey(recoveryKey.get(), recoveryKeyFactory, recoveryPath, newPasswordController.passwordField.getCharacters()); + + try (Masterkey masterkey = MasterkeyService.load(masterkeyFileAccess, recoveryPath.resolve(MASTERKEY_FILENAME), newPasswordController.passwordField.getCharacters())) { + CryptoFsInitializer.init(recoveryPath, masterkey, shorteningThreshold.get(), cipherCombo.get()); + } + + recoveryDirectory.moveRecoveredFile(MASTERKEY_FILENAME); + recoveryDirectory.moveRecoveredFile(VAULTCONFIG_FILENAME); + + if (!vaultListManager.isAlreadyAdded(vault.getPath())) { + vaultListManager.add(vault.getPath()); + } + window.close(); + dialogs.prepareRecoverPasswordSuccess((Stage)window.getOwner()) // + .setTitleKey("recover.recoverVaultConfig.title") // + .setMessageKey("recoveryKey.recover.resetVaultConfigSuccess.message") // + .build().showAndWait(); + + } catch (IOException | CryptoException e) { + LOG.error("Recovery process failed", e); + appWindows.showErrorWindow(e, window, null); + } } @FXML public void resetPassword() { Task task = new ResetPasswordTask(); - task.setOnScheduled(event -> { + + task.setOnScheduled(_ -> { LOG.debug("Using recovery key to reset password for {}.", vault.getDisplayablePath()); }); - task.setOnSucceeded(event -> { - LOG.info("Used recovery key to reset password for {}.", vault.getDisplayablePath()); - window.setScene(recoverResetPasswordSuccessScene.get()); + + task.setOnSucceeded(_ -> { + LOG.debug("Used recovery key to reset password for {}.", vault.getDisplayablePath()); + window.close(); + switch (recoverType.get()){ + case RESET_PASSWORD -> dialogs.prepareRecoverPasswordSuccess((Stage)window.getOwner()).build().showAndWait(); + case RESTORE_MASTERKEY -> dialogs.prepareRecoverPasswordSuccess((Stage)window.getOwner()).setTitleKey("recover.recoverMasterkey.title").setMessageKey("recoveryKey.recover.resetMasterkeyFileSuccess.message").build().showAndWait(); + default -> dialogs.prepareRecoverPasswordSuccess(window).build().showAndWait(); // Fallback + } }); - task.setOnFailed(event -> { + + task.setOnFailed(_ -> { LOG.error("Resetting password failed.", task.getException()); appWindows.showErrorWindow(task.getException(), window, null); }); + executor.submit(task); } private class ResetPasswordTask extends Task { - private ResetPasswordTask() { - setOnFailed(event -> LOG.error("Failed to reset password", getException())); + private static final Logger LOG = LoggerFactory.getLogger(ResetPasswordTask.class); + + public ResetPasswordTask() { + setOnFailed(_ -> LOG.error("Failed to reset password", getException())); } @Override @@ -79,7 +190,6 @@ public class RecoveryKeyResetPasswordController implements FxController { recoveryKeyFactory.newMasterkeyFileWithPassphrase(vault.getPath(), recoveryKey.get(), newPasswordController.passwordField.getCharacters()); return null; } - } /* Getter/Setter */ diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordSuccessController.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordSuccessController.java deleted file mode 100644 index b8b106d8b..000000000 --- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordSuccessController.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.cryptomator.ui.recoverykey; - -import org.cryptomator.ui.common.FxController; - -import javax.inject.Inject; -import javafx.fxml.FXML; -import javafx.stage.Stage; - -@RecoveryKeyScoped -public class RecoveryKeyResetPasswordSuccessController implements FxController { - - private final Stage window; - - @Inject - public RecoveryKeyResetPasswordSuccessController(@RecoveryKeyWindow Stage window) { - this.window = window; - } - - @FXML - public void close() { - window.close(); - } - -} diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyValidateController.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyValidateController.java index 4a8224ffe..35f4c15ed 100644 --- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyValidateController.java +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyValidateController.java @@ -1,18 +1,23 @@ package org.cryptomator.ui.recoverykey; - import com.google.common.base.CharMatcher; import com.google.common.base.Strings; import org.cryptomator.common.Nullable; import org.cryptomator.common.ObservableUtil; +import org.cryptomator.common.recovery.MasterkeyService; +import org.cryptomator.common.recovery.RecoveryActionType; import org.cryptomator.common.vaults.Vault; import org.cryptomator.cryptofs.VaultConfig; import org.cryptomator.cryptofs.VaultConfigLoadException; import org.cryptomator.cryptofs.VaultKeyInvalidException; +import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.CryptorProvider; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; import org.cryptomator.ui.common.FxController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.inject.Named; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.StringProperty; @@ -22,10 +27,12 @@ import javafx.scene.control.TextArea; import javafx.scene.control.TextFormatter; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; +import java.io.IOException; +import java.util.NoSuchElementException; public class RecoveryKeyValidateController implements FxController { - private static final Logger LOG = LoggerFactory.getLogger(RecoveryKeyCreationController.class); + private static final Logger LOG = LoggerFactory.getLogger(RecoveryKeyValidateController.class); private static final CharMatcher ALLOWED_CHARS = CharMatcher.inRange('a', 'z').or(CharMatcher.is(' ')); private final Vault vault; @@ -36,13 +43,23 @@ public class RecoveryKeyValidateController implements FxController { private final ObservableValue recoveryKeyInvalid; private final RecoveryKeyFactory recoveryKeyFactory; private final ObjectProperty recoveryKeyState; + private final ObjectProperty cipherCombo; private final AutoCompleter autoCompleter; + private final ObjectProperty recoverType; + private final MasterkeyFileAccess masterkeyFileAccess; private volatile boolean isWrongKey; public TextArea textarea; - public RecoveryKeyValidateController(Vault vault, @Nullable VaultConfig.UnverifiedVaultConfig vaultConfig, StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory) { + public RecoveryKeyValidateController(Vault vault, // + @Nullable VaultConfig.UnverifiedVaultConfig vaultConfig, // + StringProperty recoveryKey, // + RecoveryKeyFactory recoveryKeyFactory, // + MasterkeyFileAccess masterkeyFileAccess, // + @Named("recoverType") ObjectProperty recoverType, // + @Named("cipherCombo") ObjectProperty cipherCombo + ) { this.vault = vault; this.unverifiedVaultConfig = vaultConfig; this.recoveryKey = recoveryKey; @@ -52,6 +69,9 @@ public class RecoveryKeyValidateController implements FxController { this.recoveryKeyCorrect = ObservableUtil.mapWithDefault(recoveryKeyState, RecoveryKeyState.CORRECT::equals, false); this.recoveryKeyWrong = ObservableUtil.mapWithDefault(recoveryKeyState, RecoveryKeyState.WRONG::equals, false); this.recoveryKeyInvalid = ObservableUtil.mapWithDefault(recoveryKeyState, RecoveryKeyState.INVALID::equals, false); + this.recoverType = recoverType; + this.cipherCombo = cipherCombo; + this.masterkeyFileAccess = masterkeyFileAccess; } @FXML @@ -117,14 +137,37 @@ public class RecoveryKeyValidateController implements FxController { } private void validateRecoveryKey() { - isWrongKey = false; - var valid = recoveryKeyFactory.validateRecoveryKey(recoveryKey.get(), unverifiedVaultConfig != null ? this::checkKeyAgainstVaultConfig : null); - if (valid) { - recoveryKeyState.set(RecoveryKeyState.CORRECT); - } else if (isWrongKey) { //set via side effect in checkKeyAgainstVaultConfig() - recoveryKeyState.set(RecoveryKeyState.WRONG); - } else { - recoveryKeyState.set(RecoveryKeyState.INVALID); + switch (recoverType.get()) { + case RESTORE_ALL, RESTORE_VAULT_CONFIG -> { + try { + var scheme = MasterkeyService.validateRecoveryKeyAndDetectCombo(recoveryKeyFactory, vault, recoveryKey.get(), masterkeyFileAccess); + cipherCombo.set(scheme); + recoveryKeyState.set(RecoveryKeyState.CORRECT); + } catch (CryptoException e) { + LOG.info("Recovery key is valid but crypto scheme couldn't be determined", e); + recoveryKeyState.set(RecoveryKeyState.WRONG); + } catch (IllegalArgumentException e) { + LOG.info("Recovery key is syntactically invalid", e); + recoveryKeyState.set(RecoveryKeyState.INVALID); + } catch (IOException e) { + LOG.warn("IO error while validating recovery key", e); + recoveryKeyState.set(RecoveryKeyState.INVALID); + } catch (NoSuchElementException e) { + LOG.warn("Could not determine scheme from masterkey during recovery key validation, because no valid *.c9r file is present in vault", e); + recoveryKeyState.set(RecoveryKeyState.INVALID); + } + } + case RESTORE_MASTERKEY, RESET_PASSWORD, SHOW_KEY, CONVERT_VAULT -> { + isWrongKey = false; + boolean valid = recoveryKeyFactory.validateRecoveryKey(recoveryKey.get(), unverifiedVaultConfig != null ? this::checkKeyAgainstVaultConfig : null); + if (valid) { + recoveryKeyState.set(RecoveryKeyState.CORRECT); + } else if (isWrongKey) { //set via side effect in checkKeyAgainstVaultConfig() + recoveryKeyState.set(RecoveryKeyState.WRONG); + } else { + recoveryKeyState.set(RecoveryKeyState.INVALID); + } + } } } diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java b/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java index 95f13d383..1c8d758fc 100644 --- a/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java +++ b/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java @@ -15,6 +15,7 @@ import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.StageFactory; import org.cryptomator.ui.keyloading.KeyLoadingComponent; import org.cryptomator.ui.keyloading.KeyLoadingStrategy; +import org.cryptomator.ui.recoverykey.RecoveryKeyComponent; import org.jetbrains.annotations.Nullable; import javax.inject.Named; @@ -27,7 +28,7 @@ import javafx.stage.Stage; import java.util.Map; import java.util.ResourceBundle; -@Module(subcomponents = {KeyLoadingComponent.class}) +@Module(subcomponents = {KeyLoadingComponent.class, RecoveryKeyComponent.class}) abstract class UnlockModule { @Provides diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java index dd003d93d..67ae2f42d 100644 --- a/src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java +++ b/src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java @@ -1,16 +1,16 @@ package org.cryptomator.ui.vaultoptions; import org.cryptomator.common.keychain.KeychainManager; +import org.cryptomator.common.recovery.RecoveryActionType; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.changepassword.ChangePasswordComponent; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.forgetpassword.ForgetPasswordComponent; import org.cryptomator.ui.recoverykey.RecoveryKeyComponent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import javax.inject.Inject; import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; import javafx.stage.Stage; @@ -18,8 +18,6 @@ import javafx.stage.Stage; @VaultOptionsScoped public class MasterkeyOptionsController implements FxController { - private static final Logger LOG = LoggerFactory.getLogger(MasterkeyOptionsController.class); - private final Vault vault; private final Stage window; private final ChangePasswordComponent.Builder changePasswordWindow; @@ -51,12 +49,12 @@ public class MasterkeyOptionsController implements FxController { @FXML public void showRecoveryKey() { - recoveryKeyWindow.create(vault, window).showRecoveryKeyCreationWindow(); + recoveryKeyWindow.create(vault, window, new SimpleObjectProperty<>(RecoveryActionType.SHOW_KEY)).showRecoveryKeyCreationWindow(); } @FXML public void showRecoverVaultDialog() { - recoveryKeyWindow.create(vault, window).showRecoveryKeyRecoverWindow(); + recoveryKeyWindow.create(vault, window, new SimpleObjectProperty<>(RecoveryActionType.RESET_PASSWORD)).showRecoveryKeyRecoverWindow(); } @FXML diff --git a/src/main/resources/fxml/convertvault_hubtopassword_convert.fxml b/src/main/resources/fxml/convertvault_hubtopassword_convert.fxml index 7ea190fd4..7797b41b2 100644 --- a/src/main/resources/fxml/convertvault_hubtopassword_convert.fxml +++ b/src/main/resources/fxml/convertvault_hubtopassword_convert.fxml @@ -6,7 +6,12 @@ - + + + + + - + + + + + + + + + + @@ -26,7 +40,7 @@