Merge pull request #3821 from cryptomator/feature/restore-vaultconfig

Feature: Vault Recovery Enhancements
This commit is contained in:
Armin Schrenk
2025-10-16 16:36:58 +02:00
committed by GitHub
48 changed files with 1736 additions and 256 deletions

View File

@@ -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<Path> 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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<CryptorProvider.Scheme> detect(Masterkey masterkey, Path vaultPath) {
try (Stream<Path> paths = Files.walk(vaultPath.resolve(DATA_DIR_NAME))) {
Optional<Path> 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<CryptorProvider.Scheme> 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();
}
}

View File

@@ -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
}

View File

@@ -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;
}
}

View File

@@ -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<MountService> 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;
}
}

View File

@@ -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> 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;
}

View File

@@ -20,7 +20,7 @@ public class VaultConfigCache {
private final VaultSettings settings;
private final AtomicReference<VaultConfig.UnverifiedVaultConfig> config;
VaultConfigCache(VaultSettings settings) {
public VaultConfigCache(VaultSettings settings) {
this.settings = settings;
this.config = new AtomicReference<>(null);
}

View File

@@ -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;
};
}

View File

@@ -25,6 +25,16 @@ public class VaultState extends ObservableValueBase<VaultState.Value> 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
*/

View File

@@ -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"), //

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -28,7 +28,7 @@ import static org.cryptomator.common.vaults.VaultState.Value.*;
@FxApplicationScoped
public class FxApplicationTerminator {
private static final Set<VaultState.Value> STATES_ALLOWING_TERMINATION = EnumSet.of(LOCKED, NEEDS_MIGRATION, MISSING, ERROR);
private static final Set<VaultState.Value> STATES_ALLOWING_TERMINATION = EnumSet.of(LOCKED, NEEDS_MIGRATION, MISSING, ERROR, VAULT_CONFIG_MISSING, ALL_MISSING);
private static final Set<VaultState.Value> STATES_PREVENT_TERMINATION = EnumSet.of(PROCESSING);
private static final Logger LOG = LoggerFactory.getLogger(FxApplicationTerminator.class);

View File

@@ -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<Path> 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<Path> result, ResourceBundle resourceBundle) {
public ChooseMasterkeyFileController(@KeyLoading Stage window, //
@KeyLoading Vault vault, //
CompletableFuture<Path> 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();
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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<Vault> 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<Vault> 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

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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<Double> cellSize;
private final Dialogs dialogs;
private final VaultComponent.Factory vaultComponentFactory;
private final RecoveryKeyComponent.Factory recoveryKeyWindow;
private final List<MountService> mountServices;
public ListView<Vault> 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<MountService> 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<Path> 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<Path> selectedDirectory = chooseValidVaultDirectory();
if (selectedDirectory.isEmpty()) {
return;
}
Path path = selectedDirectory.get();
Optional<Vault> 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();
}
}

View File

@@ -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<Scene> recoverScene();
@FxmlScene(FxmlFile.RECOVERYKEY_ONBOARDING)
Lazy<Scene> 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<RecoveryActionType> recoverType);
}
}

View File

@@ -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<Scene> successScene;
private final Lazy<Scene> 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<RecoveryActionType> 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<Scene> 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<Scene> successScene, //
@FxmlScene(FxmlFile.RECOVERYKEY_EXPERT_SETTINGS) Lazy<Scene> recoverykeyExpertSettingsScene, //
@RecoveryKeyWindow Vault vault, //
RecoveryKeyFactory recoveryKeyFactory, //
MasterkeyFileAccess masterkeyFileAccess, //
ExecutorService executor, //
@RecoveryKeyWindow StringProperty recoveryKey, //
@Named("shorteningThreshold") IntegerProperty shorteningThreshold, //
@Named("recoverType") ObjectProperty<RecoveryActionType> 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();

View File

@@ -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> application;
private final Vault vault;
private final ObjectProperty<RecoveryActionType> recoverType;
private final IntegerProperty shorteningThreshold;
private final Lazy<Scene> resetPasswordScene;
private final Lazy<Scene> createScene;
private final Lazy<Scene> onBoardingScene;
private final Lazy<Scene> recoverScene;
private final BooleanBinding validShorteningThreshold;
@FXML
public CheckBox expertSettingsCheckBox;
@FXML
public NumericTextField shorteningThresholdTextField;
@Inject
public RecoveryKeyExpertSettingsController(@RecoveryKeyWindow Stage window, //
Lazy<Application> application, //
@RecoveryKeyWindow Vault vault, //
@Named("recoverType") ObjectProperty<RecoveryActionType> recoverType, //
@Named("shorteningThreshold") IntegerProperty shorteningThreshold, //
@FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) Lazy<Scene> resetPasswordScene, //
@FxmlScene(FxmlFile.RECOVERYKEY_CREATE) Lazy<Scene> createScene, //
@FxmlScene(FxmlFile.RECOVERYKEY_ONBOARDING) Lazy<Scene> onBoardingScene, //
@FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy<Scene> 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());
}
}
}

View File

@@ -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<CryptorProvider.Scheme> 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<RecoveryActionType> recoverType, @Named("cipherCombo") ObjectProperty<CryptorProvider.Scheme> cipherCombo, @Nullable MasterkeyFileAccess masterkeyFileAccess) {
return new RecoveryKeyValidateController(vault, vaultConfig, recoveryKey, recoveryKeyFactory, masterkeyFileAccess, recoverType, cipherCombo);
}
@Provides

View File

@@ -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<Scene> recoverykeyRecoverScene;
private final Lazy<Scene> recoverykeyExpertSettingsScene;
private final ObjectProperty<RecoveryActionType> 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<Scene> recoverykeyRecoverScene, //
@FxmlScene(FxmlFile.RECOVERYKEY_EXPERT_SETTINGS) Lazy<Scene> recoverykeyExpertSettingsScene, //
@Named("recoverType") ObjectProperty<RecoveryActionType> 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();
}
}

View File

@@ -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<Scene> resetPasswordScene;
private final Vault vault;
private final Lazy<Scene> nextScene;
private final Lazy<Scene> onBoardingScene;
private final ResourceBundle resourceBundle;
public ObjectProperty<RecoveryActionType> 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<Scene> resetPasswordScene, ResourceBundle resourceBundle) {
public RecoveryKeyRecoverController(@RecoveryKeyWindow Stage window, //
@RecoveryKeyWindow Vault vault, //
ResourceBundle resourceBundle, //
@FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) Lazy<Scene> resetPasswordScene, //
@FxmlScene(FxmlFile.RECOVERYKEY_EXPERT_SETTINGS) Lazy<Scene> expertSettingsScene, //
@FxmlScene(FxmlFile.RECOVERYKEY_ONBOARDING) Lazy<Scene> onBoardingScene, //
@Named("recoverType") ObjectProperty<RecoveryActionType> 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 */

View File

@@ -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<Scene> recoverResetPasswordSuccessScene;
private final Lazy<Scene> recoverExpertSettingsScene;
private final Lazy<Scene> recoverykeyRecoverScene;
private final FxApplicationWindows appWindows;
private final MasterkeyFileAccess masterkeyFileAccess;
private final VaultListManager vaultListManager;
private final IntegerProperty shorteningThreshold;
private final ObjectProperty<RecoveryActionType> recoverType;
private final ObjectProperty<CryptorProvider.Scheme> 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<Scene> recoverResetPasswordSuccessScene, FxApplicationWindows appWindows) {
public RecoveryKeyResetPasswordController(@RecoveryKeyWindow Stage window, //
@RecoveryKeyWindow Vault vault, //
RecoveryKeyFactory recoveryKeyFactory, //
ExecutorService executor, //
@RecoveryKeyWindow StringProperty recoveryKey, //
@FxmlScene(FxmlFile.RECOVERYKEY_EXPERT_SETTINGS) Lazy<Scene> recoverExpertSettingsScene, //
@FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy<Scene> recoverykeyRecoverScene, //
FxApplicationWindows appWindows, //
MasterkeyFileAccess masterkeyFileAccess, //
VaultListManager vaultListManager, //
@Named("shorteningThreshold") IntegerProperty shorteningThreshold, //
@Named("recoverType") ObjectProperty<RecoveryActionType> recoverType, //
@Named("cipherCombo") ObjectProperty<CryptorProvider.Scheme> 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<Void> 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<Void> {
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 */

View File

@@ -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();
}
}

View File

@@ -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<Boolean> recoveryKeyInvalid;
private final RecoveryKeyFactory recoveryKeyFactory;
private final ObjectProperty<RecoveryKeyState> recoveryKeyState;
private final ObjectProperty<CryptorProvider.Scheme> cipherCombo;
private final AutoCompleter autoCompleter;
private final ObjectProperty<RecoveryActionType> 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<RecoveryActionType> recoverType, //
@Named("cipherCombo") ObjectProperty<CryptorProvider.Scheme> 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);
}
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -6,7 +6,12 @@
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.VBox?>
<VBox xmlns:fx="http://javafx.com/fxml"
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.scene.shape.Circle?>
<?import javafx.scene.Group?>
<HBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.convertvault.HubToPasswordConvertController"
minWidth="400"
@@ -17,7 +22,16 @@
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<Group>
<StackPane>
<padding>
<Insets topRightBottomLeft="6"/>
</padding>
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="KEY" glyphSize="24"/>
</StackPane>
</Group>
<VBox spacing="12" HBox.hgrow="ALWAYS" alignment="TOP_CENTER">
<fx:include fx:id="newPassword" source="new_password.fxml"/>
<Region VBox.vgrow="ALWAYS"/>
@@ -26,7 +40,7 @@
<ButtonBar buttonMinWidth="120" buttonOrder="+CI">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
<Button fx:id="convertBtn" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#convert"> <!-- for button logic, see controller -->
<Button fx:id="convertBtn" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#convert">
<graphic>
<FontAwesome5Spinner glyphSize="12"/>
</graphic>
@@ -34,5 +48,5 @@
</buttons>
</ButtonBar>
</VBox>
</children>
</VBox>
</VBox>
</HBox>

View File

@@ -5,7 +5,10 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Button?>
<VBox xmlns:fx="http://javafx.com/fxml"
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.scene.shape.Circle?>
<?import javafx.scene.Group?>
<HBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.convertvault.HubToPasswordStartController"
minWidth="400"
@@ -16,18 +19,26 @@
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<Group>
<StackPane>
<padding>
<Insets topRightBottomLeft="6"/>
</padding>
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="SYNC" glyphSize="24"/>
</StackPane>
</Group>
<VBox spacing="12" HBox.hgrow="ALWAYS" alignment="TOP_CENTER">
<fx:include fx:id="recoveryKeyValidate" source="recoverykey_validate.fxml"/>
<Region VBox.vgrow="ALWAYS"/>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#next" disable="${!controller.validateController.recoveryKeyCorrect}"/>
</buttons>
</ButtonBar>
</VBox>
</children>
</VBox>
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#next" disable="${!controller.validateController.recoveryKeyCorrect}"/>
</buttons>
</ButtonBar>
</VBox>
</HBox>

View File

@@ -40,7 +40,7 @@
<Insets bottom="6" top="6"/>
</padding>
</Label>
<FormattedLabel format="%recoveryKey.create.description" arg1="${controller.vault.displayName}" wrapText="true">
<FormattedLabel fx:id="descriptionLabel" format="%recoveryKey.create.description" arg1="${controller.vault.displayName}" wrapText="true">
<padding>
<Insets bottom="6"/>
</padding>
@@ -49,8 +49,8 @@
<Region VBox.vgrow="ALWAYS" minHeight="18"/>
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#createRecoveryKey" disable="${passwordField.text.empty}"/>
<Button fx:id="cancelButton" text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
<Button fx:id="nextButton" text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#createRecoveryKey" disable="${passwordField.text.empty}"/>
</buttons>
</ButtonBar>
</VBox>

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Hyperlink?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Tooltip?>
<?import javafx.scene.Group?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import org.cryptomator.ui.controls.NumericTextField?>
<HBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.recoverykey.RecoveryKeyExpertSettingsController"
minWidth="400"
maxWidth="400"
minHeight="145"
spacing="12"
alignment="TOP_CENTER">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<Group>
<StackPane>
<padding>
<Insets topRightBottomLeft="6"/>
</padding>
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="QUESTION" glyphSize="24"/>
</StackPane>
</Group>
<VBox HBox.hgrow="ALWAYS">
<Label styleClass="label-large" text="Expert Settings" wrapText="true">
<padding>
<Insets bottom="6" top="6"/>
</padding>
</Label>
<CheckBox fx:id="expertSettingsCheckBox" text="%addvaultwizard.new.expertSettings.enableExpertSettingsCheckbox" onAction="#toggleUseExpertSettings"/>
<VBox spacing="6" visible="${expertSettingsCheckBox.selected}">
<HBox spacing="2" HBox.hgrow="NEVER">
<Label text="%addvaultwizard.new.expertSettings.shorteningThreshold.title"/>
<Region prefWidth="2"/>
<Hyperlink contentDisplay="GRAPHIC_ONLY" onAction="#openDocs">
<graphic>
<FontAwesome5IconView glyph="QUESTION_CIRCLE" styleClass="glyph-icon-muted"/>
</graphic>
<tooltip>
<Tooltip text="%addvaultwizard.new.expertSettings.shorteningThreshold.tooltip" showDelay="10ms"/>
</tooltip>
</Hyperlink>
</HBox>
<Label text="%recover.expertSettings.shorteningThreshold.title" wrapText="true"/>
<NumericTextField fx:id="shorteningThresholdTextField"/>
<HBox alignment="TOP_RIGHT">
<Region minWidth="4" prefWidth="4" HBox.hgrow="NEVER"/>
<StackPane>
<Label styleClass="label-muted" text="%addvaultwizard.new.expertSettings.shorteningThreshold.invalid" textAlignment="RIGHT" alignment="CENTER_RIGHT" visible="${!controller.validShorteningThreshold}" managed="${!controller.validShorteningThreshold}" graphicTextGap="6">
<graphic>
<FontAwesome5IconView styleClass="glyph-icon-red" glyph="TIMES"/>
</graphic>
</Label>
<Label styleClass="label-muted" text="%addvaultwizard.new.expertSettings.shorteningThreshold.valid" textAlignment="RIGHT" alignment="CENTER_RIGHT" visible="${controller.validShorteningThreshold}" managed="${controller.validShorteningThreshold}" graphicTextGap="6">
<graphic>
<FontAwesome5IconView styleClass="glyph-icon-primary" glyph="CHECK"/>
</graphic>
</Label>
</StackPane>
</HBox>
</VBox>
<Region VBox.vgrow="ALWAYS" minHeight="18"/>
<ButtonBar buttonMinWidth="120" buttonOrder="+C">
<buttons>
<Button text="%generic.button.back" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#back"/>
<Button text="%generic.button.next" ButtonBar.buttonData="CANCEL_CLOSE" defaultButton="true" onAction="#next"/>
</buttons>
</ButtonBar>
</VBox>
</HBox>

View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.RadioButton?>
<?import javafx.scene.control.ToggleGroup?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.scene.Group?>
<HBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:id="hBox"
fx:controller="org.cryptomator.ui.recoverykey.RecoveryKeyOnboardingController"
prefWidth="480"
minHeight="242"
spacing="12"
VBox.vgrow="ALWAYS">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<Group>
<StackPane>
<padding>
<Insets topRightBottomLeft="6"/>
</padding>
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="SYNC" glyphSize="24"/>
</StackPane>
</Group>
<VBox HBox.hgrow="ALWAYS" spacing="12">
<Label fx:id="titleLabel" text="%recover.recoverVaultConfig.title" styleClass="label-extra-large"/>
<VBox spacing="6">
<VBox fx:id="chooseMethodeBox" spacing="6">
<Label text="%recover.onBoarding.chooseMethod"/>
<RadioButton fx:id="recoveryKeyRadio" text="%recover.onBoarding.useRecoveryKey" selected="true">
<toggleGroup>
<ToggleGroup fx:id="methodToggleGroup"/>
</toggleGroup>
</RadioButton>
<RadioButton fx:id="passwordRadio" text="%recover.onBoarding.usePassword"/>
</VBox>
<Label fx:id="messageLabel" wrapText="true"/>
<Label fx:id="pleaseConfirm" wrapText="true"/>
<GridPane alignment="CENTER_LEFT">
<padding>
<Insets left="6"/>
</padding>
<columnConstraints>
<ColumnConstraints minWidth="20" halignment="LEFT"/>
<ColumnConstraints />
</columnConstraints>
<rowConstraints>
<RowConstraints valignment="TOP"/>
<RowConstraints valignment="TOP"/>
<RowConstraints valignment="TOP"/>
</rowConstraints>
<Label text="1." GridPane.rowIndex="0" GridPane.columnIndex="0"/>
<Label text="%recover.onBoarding.intro.ensure" wrapText="true" GridPane.rowIndex="0" GridPane.columnIndex="1"/>
<Label text="2." GridPane.rowIndex="1" GridPane.columnIndex="0"/>
<Label fx:id="secondTextDesc" text="%recover.onBoarding.intro.recoveryKey" wrapText="true" GridPane.rowIndex="1" GridPane.columnIndex="1"/>
</GridPane>
<CheckBox fx:id="affirmationBox" text="%recover.onBoarding.affirmation"/>
</VBox>
<Region VBox.vgrow="ALWAYS"/>
<ButtonBar buttonMinWidth="120" buttonOrder="+CX" VBox.vgrow="NEVER">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
<Button fx:id="nextButton" text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD"
disable="${!affirmationBox.selected}" defaultButton="true" onAction="#next"/>
</buttons>
</ButtonBar>
</VBox>
</HBox>

View File

@@ -3,32 +3,46 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<VBox xmlns:fx="http://javafx.com/fxml"
<?import javafx.scene.shape.Circle?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.scene.Group?>
<HBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.recoverykey.RecoveryKeyRecoverController"
minWidth="400"
maxWidth="400"
minHeight="145"
spacing="12"
alignment="TOP_CENTER">
spacing="12">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<Group>
<StackPane>
<padding>
<Insets topRightBottomLeft="6"/>
</padding>
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="SYNC" glyphSize="24"/>
</StackPane>
</Group>
<fx:include fx:id="recoveryKeyValidate" source="recoverykey_validate.fxml"/>
<VBox spacing="12" HBox.hgrow="ALWAYS" alignment="TOP_CENTER">
<fx:include fx:id="recoveryKeyValidate" source="recoverykey_validate.fxml"/>
<Region VBox.vgrow="ALWAYS"/>
<Region VBox.vgrow="ALWAYS"/>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
<ButtonBar buttonMinWidth="120" buttonOrder="+BX">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
<Button fx:id="cancelButton" ButtonBar.buttonData="BACK_PREVIOUS" cancelButton="true" onAction="#closeOrReturn"/>
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#recover" disable="${!controller.validateController.recoveryKeyCorrect}"/>
</buttons>
</ButtonBar>
</VBox>
</children>
</VBox>
</HBox>

View File

@@ -5,7 +5,12 @@
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.VBox?>
<VBox xmlns:fx="http://javafx.com/fxml"
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.scene.shape.Circle?>
<?import javafx.scene.Group?>
<HBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.recoverykey.RecoveryKeyResetPasswordController"
minWidth="400"
@@ -17,17 +22,28 @@
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<fx:include fx:id="newPassword" source="new_password.fxml"/>
<Group>
<StackPane>
<padding>
<Insets topRightBottomLeft="6"/>
</padding>
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="KEY" glyphSize="24"/>
</StackPane>
</Group>
<VBox spacing="12" HBox.hgrow="ALWAYS" alignment="TOP_CENTER">
<fx:include fx:id="newPassword" source="new_password.fxml"/>
<Region VBox.vgrow="ALWAYS"/>
<Region VBox.vgrow="ALWAYS"/>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<ButtonBar buttonMinWidth="120" buttonOrder="+CI">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
<Button text="%recoveryKey.recover.resetBtn" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#resetPassword" disable="${!controller.passwordSufficientAndMatching}"/>
</buttons>
</ButtonBar>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<ButtonBar buttonMinWidth="120" buttonOrder="+CI">
<buttons>
<Button text="%generic.button.back" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
<Button fx:id="nextButton" ButtonBar.buttonData="FINISH" defaultButton="true" disable="${!controller.passwordSufficientAndMatching}" onAction="#next"/>
</buttons>
</ButtonBar>
</VBox>
</VBox>
</children>
</VBox>
</HBox>

View File

@@ -1,53 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<?import javafx.scene.Group?>
<?import javafx.scene.layout.Region?>
<HBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.recoverykey.RecoveryKeyResetPasswordSuccessController"
minWidth="400"
maxWidth="400"
minHeight="145"
spacing="12"
alignment="TOP_LEFT">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<Group>
<StackPane>
<padding>
<Insets topRightBottomLeft="6"/>
</padding>
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="CHECK" glyphSize="24"/>
</StackPane>
</Group>
<VBox HBox.hgrow="ALWAYS">
<Label styleClass="label-large" text="%recoveryKey.recover.resetSuccess.message" wrapText="true" textAlignment="LEFT">
<padding>
<Insets bottom="6" top="6"/>
</padding>
</Label>
<Label text="%recoveryKey.recover.resetSuccess.description" wrapText="true" textAlignment="LEFT"/>
<Region VBox.vgrow="ALWAYS" minHeight="18"/>
<ButtonBar buttonMinWidth="120" buttonOrder="+C">
<buttons>
<Button text="%generic.button.close" ButtonBar.buttonData="CANCEL_CLOSE" defaultButton="true" cancelButton="true" onAction="#close"/>
</buttons>
</ButtonBar>
</VBox>
</children>
</HBox>

View File

@@ -2,48 +2,40 @@
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import org.cryptomator.ui.controls.FormattedLabel?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.recoverykey.RecoveryKeyValidateController"
minWidth="400"
maxWidth="400"
minHeight="145"
spacing="12"
alignment="TOP_CENTER">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
spacing="12">
<children>
<FormattedLabel format="%recoveryKey.recover.prompt" arg1="${controller.vault.displayName}" wrapText="true"/>
<TextArea wrapText="true" prefRowCount="4" fx:id="textarea" textFormatter="${controller.recoveryKeyTextFormatter}" onKeyPressed="#onKeyPressed"/>
<StackPane>
<Label text="Just some Filler" visible="false" graphicTextGap="6">
<VBox>
<Label text="Just some Filler" visible="false" managed="${textarea.text.empty}" graphicTextGap="6">
<graphic>
<FontAwesome5IconView glyph="ANCHOR"/>
</graphic>
</Label>
<Label text="%recoveryKey.recover.correctKey" graphicTextGap="6" contentDisplay="LEFT" visible="${(!textarea.text.empty) &amp;&amp; controller.recoveryKeyCorrect}">
<Label text="%recoveryKey.recover.correctKey" graphicTextGap="6" visible="${(!textarea.text.empty) &amp;&amp; controller.recoveryKeyCorrect}" managed="${(!textarea.text.empty) &amp;&amp; controller.recoveryKeyCorrect}">
<graphic>
<FontAwesome5IconView glyph="CHECK"/>
</graphic>
</Label>
<Label text="%recoveryKey.recover.wrongKey" graphicTextGap="6" contentDisplay="LEFT" visible="${(!textarea.text.empty) &amp;&amp; controller.recoveryKeyWrong}">
<Label text="%recoveryKey.recover.wrongKey" graphicTextGap="6" visible="${(!textarea.text.empty) &amp;&amp; controller.recoveryKeyWrong}" managed="${(!textarea.text.empty) &amp;&amp; controller.recoveryKeyWrong}">
<graphic>
<FontAwesome5IconView glyph="TIMES" styleClass="glyph-icon-red"/>
</graphic>
</Label>
<Label text="%recoveryKey.recover.invalidKey" graphicTextGap="6" contentDisplay="LEFT" visible="${(!textarea.text.empty) &amp;&amp; controller.recoveryKeyInvalid}">
<Label text="%recoveryKey.recover.invalidKey" graphicTextGap="6" visible="${(!textarea.text.empty) &amp;&amp; controller.recoveryKeyInvalid}" managed="${(!textarea.text.empty) &amp;&amp; controller.recoveryKeyInvalid}">
<graphic>
<FontAwesome5IconView glyph="TIMES" styleClass="glyph-icon-red"/>
</graphic>
</Label>
</StackPane>
</VBox>
</children>
</VBox>

View File

@@ -12,6 +12,7 @@
<?import javafx.scene.Group?>
<?import javafx.scene.layout.Region?>
<?import org.cryptomator.ui.controls.FormattedLabel?>
<?import javafx.scene.control.CheckBox?>
<HBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.keyloading.masterkeyfile.ChooseMasterkeyFileController"
@@ -40,12 +41,13 @@
</padding>
</Label>
<FormattedLabel format="%unlock.chooseMasterkey.description" arg1="${controller.displayName}" wrapText="true"/>
<CheckBox fx:id="restoreInsteadCheckBox" text="%unlock.chooseMasterkey.restoreInstead" wrapText="true"/>
<Region VBox.vgrow="ALWAYS" minHeight="18"/>
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel"/>
<Button text="%generic.button.choose" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#proceed"/>
<Button fx:id="forwardButton" text="%generic.button.choose" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#proceed"/>
</buttons>
</ButtonBar>
</VBox>

View File

@@ -53,5 +53,6 @@
<fx:include VBox.vgrow="ALWAYS" source="vault_detail_missing.fxml" visible="${controller.vault.missing}" managed="${controller.vault.missing}"/>
<fx:include VBox.vgrow="ALWAYS" source="vault_detail_needsmigration.fxml" visible="${controller.vault.needsMigration}" managed="${controller.vault.needsMigration}"/>
<fx:include VBox.vgrow="ALWAYS" source="vault_detail_unknownerror.fxml" visible="${controller.vault.unknownError}" managed="${controller.vault.unknownError}"/>
<fx:include VBox.vgrow="ALWAYS" source="vault_detail_missing_vault_config.fxml" visible="${controller.vault.missingVaultConfig}" managed="${controller.vault.missingVaultConfig}"/>
</children>
</VBox>

View File

@@ -0,0 +1,42 @@
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.mainwindow.VaultDetailMissingVaultController"
alignment="TOP_CENTER"
spacing="9">
<VBox spacing="9" alignment="CENTER">
<StackPane>
<Circle styleClass="glyph-icon-primary" radius="48"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="FILE" glyphSize="48"/>
<FontAwesome5IconView styleClass="glyph-icon-primary" glyph="SEARCH" glyphSize="24">
<StackPane.margin>
<Insets top="12"/>
</StackPane.margin>
</FontAwesome5IconView>
</StackPane>
<Label text="%main.vaultDetail.missingVaultConfig.info" wrapText="true"/>
</VBox>
<VBox spacing="6" alignment="CENTER">
<Button text="%main.vaultDetail.missing.recheck" minWidth="120" onAction="#recheck">
<graphic>
<FontAwesome5IconView glyph="REDO"/>
</graphic>
</Button>
<Button text="%main.vaultDetail.missingVaultConfig.restore" minWidth="120" onAction="#restoreVaultConfig">
<graphic>
<FontAwesome5IconView glyph="MAGIC"/>
</graphic>
</Button>
<Button text="%main.vaultDetail.missing.remove" minWidth="120" onAction="#didClickRemoveVault">
<graphic>
<FontAwesome5IconView glyph="TRASH"/>
</graphic>
</Button>
</VBox>
</VBox>

View File

@@ -13,8 +13,6 @@
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Arc?>
<?import javafx.scene.shape.Circle?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.shape.Rectangle?>
<?import javafx.scene.layout.AnchorPane?>
<StackPane xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
@@ -80,6 +78,11 @@
<FontAwesome5IconView glyph="FOLDER_OPEN" textAlignment="CENTER" wrappingWidth="14"/>
</graphic>
</MenuItem>
<MenuItem styleClass="dropdown-button-context-menu-item" text="%main.vaultlist.addVaultBtn.menuItemRecover" onAction="#didClickRecoverExistingVault">
<graphic>
<FontAwesome5IconView glyph="SYNC" textAlignment="CENTER" wrappingWidth="14"/>
</graphic>
</MenuItem>
</items>
</ContextMenu>
</fx:define>

View File

@@ -97,6 +97,7 @@ addvault.new.readme.accessLocation.4=Feel free to remove this file.
## Existing
addvaultwizard.existing.title=Add Existing Vault
addvaultwizard.existing.instruction=Choose the "vault.cryptomator" file of your existing vault. If only a file named "masterkey.cryptomator" exists, select that instead.
addvaultwizard.existing.restore=Restore…
addvaultwizard.existing.chooseBtn=Choose…
addvaultwizard.existing.filePickerTitle=Select Vault File
addvaultwizard.existing.filePickerMimeDesc=Cryptomator Vault
@@ -128,6 +129,7 @@ unlock.unlockBtn=Unlock
## Select
unlock.chooseMasterkey.message=Masterkey file not found
unlock.chooseMasterkey.description=Cryptomator could not find the masterkey file for vault "%s". Please choose the key file manually.
unlock.chooseMasterkey.restoreInstead=Restore the masterkey file instead
unlock.chooseMasterkey.filePickerTitle=Select Masterkey File
unlock.chooseMasterkey.filePickerMimeDesc=Cryptomator Masterkey
## Success
@@ -395,8 +397,9 @@ main.vaultlist.contextMenu.unlockNow=Unlock Now
main.vaultlist.contextMenu.vaultoptions=Show Vault Options
main.vaultlist.contextMenu.reveal=Reveal Drive
main.vaultlist.contextMenu.share=Share…
main.vaultlist.addVaultBtn.menuItemNew=Create New Vault...
main.vaultlist.addVaultBtn.menuItemExisting=Open Existing Vault...
main.vaultlist.addVaultBtn.menuItemNew=Create New Vault
main.vaultlist.addVaultBtn.menuItemExisting=Open Existing Vault
main.vaultlist.addVaultBtn.menuItemRecover=Recover Existing Vault…
main.vaultlist.showEventsButton.tooltip=Open event view
##Notificaition
main.notification.updateAvailable=Update is available.
@@ -434,6 +437,9 @@ main.vaultDetail.missing.info=Cryptomator could not find a vault at this path.
main.vaultDetail.missing.recheck=Recheck
main.vaultDetail.missing.remove=Remove from Vault List…
main.vaultDetail.missing.changeLocation=Change Vault Location…
### Missing Vault Config
main.vaultDetail.missingVaultConfig.info=Vault config is missing.
main.vaultDetail.missingVaultConfig.restore=Restore vault config
### Needs Migration
main.vaultDetail.migrateButton=Upgrade Vault
main.vaultDetail.migratePrompt=Your vault needs to be upgraded to a new format, before you can access it
@@ -498,6 +504,7 @@ vaultOptions.hub.convertBtn=Convert to Password-Based Vault
recoveryKey.display.title=Show Recovery Key
recoveryKey.create.message=Password required
recoveryKey.create.description=Enter the password for "%s" to show its recovery key.
recoveryKey.recover.description=Enter the password for "%s" to recover the vault config.
recoveryKey.display.description=The following recovery key can be used to restore access to "%s":
recoveryKey.display.StorageHints=Keep it somewhere very secure, e.g.:\n • Store it using a password manager\n • Save it on a USB flash drive\n • Print it on paper
## Reset Password
@@ -510,9 +517,59 @@ recoveryKey.recover.invalidKey=This recovery key is not valid
recoveryKey.printout.heading=Cryptomator Recovery Key\n"%s"\n
### Reset Password
recoveryKey.recover.resetBtn=Reset
recoveryKey.recover.recoverBtn=Recover
### Recovery Key Password Reset Success
recoveryKey.recover.resetSuccess.message=Password reset successful
recoveryKey.recover.resetSuccess.description=You can unlock your vault with the new password.
### Recovery Key Vault Config Reset Success
recoveryKey.recover.resetVaultConfigSuccess.message=Vault config reset successful
recoveryKey.recover.resetMasterkeyFileSuccess.message=Masterkey file reset successful
recoveryKey.recover.resetMasterkeyFileSuccess.description=You can unlock your vault with your password now.
# Recover Vault Config File and/or Masterkey
##Add Existing Vault without recovery - Dialog
recover.existing.title=Vault Added
recover.existing.message=The vault was added successfully
recover.existing.description=Your vault "%s" has been added to the vault list. No recovery process was necessary.
##Vault Already Exists - Dialog
recover.alreadyExists.title=Vault Already Exists
recover.alreadyExists.message=This vault has already been added
recover.alreadyExists.description=Your vault "%s" is already present in your vault list and was therefore not added again.
##Invalid Selection - Dialog
recover.invalidSelection.title=Invalid Selection
recover.invalidSelection.message=Your selection is not a vault
recover.invalidSelection.description=The selected folder must be a valid Cryptomator vault.
## Contact Hub Vault Owner - Dialog
contactHubVaultOwner.title=Hub Vault
contactHubVaultOwner.message=This vault was created with Cryptomator Hub
contactHubVaultOwner.description=Please reach out to the vault owner to restore the missing file. They can download the vault template from Cryptomator Hub.
##Dialog Title
recover.recoverVaultConfig.title=Recover Vault Config
recover.recoverMasterkey.title=Recover Masterkey
## OnBoarding
recover.onBoarding.chooseMethod=Choose recovery method:
recover.onBoarding.useRecoveryKey=Use Recovery Key
recover.onBoarding.usePassword=Use Password
recover.onBoarding.intro=Make sure to check the following:
recover.onBoarding.pleaseConfirm=Before proceeding, please confirm that:
recover.onBoarding.otherwisePleaseConfirm=Otherwise, please confirm that:
recover.onBoarding.allMissing.intro=If this vault is managed by Cryptomator Hub, the vault owner must restore it for you.
recover.onBoarding.intro.ensure=All files are fully synced.
recover.onBoarding.affirmation=I have read and understood these requirements
###Vault Config Missing
recover.onBoarding.intro.recoveryKey=You have the recovery key and know if expert settings were used.
recover.onBoarding.intro.password=You have the vault password and know if expert settings were used.
###Masterkey Missing
recover.onBoarding.intro.masterkey.recoveryKey=You have the vault recovery key.
## Expert Settings
recover.expertSettings.shorteningThreshold.title=This value must match the one used before recovery to ensure compatibility with previously encrypted data.
# Convert Vault
convertVault.title=Convert Vault