new recovery package in common

This commit is contained in:
Jan-Peter Klein
2025-05-19 14:27:05 +02:00
parent e1cc61037e
commit 90cbb0c5f6
18 changed files with 456 additions and 94 deletions

View File

@@ -0,0 +1,52 @@
package org.cryptomator.common.recovery;
import static org.cryptomator.common.vaults.VaultState.Value.*;
import java.io.IOException;
import java.nio.file.*;
import java.util.stream.Stream;
import org.cryptomator.common.vaults.VaultState.Value;
public final class BackupRestorer {
private BackupRestorer() {}
public static boolean restoreIfPresent(Path vaultPath, Value vaultState) {
Path targetFile;
switch (vaultState) {
case VAULT_CONFIG_MISSING -> targetFile = vaultPath.resolve("vault.cryptomator");
case MASTERKEY_MISSING -> targetFile = vaultPath.resolve("masterkey.cryptomator");
default -> {
return false;
}
}
try (Stream<Path> files = Files.list(vaultPath)) {
return files
.filter(file -> isValidBackupFileForState(file.getFileName().toString(), vaultState))
.findFirst()
.map(backupFile -> copyBackupFile(backupFile, targetFile))
.orElse(false);
} catch (IOException e) {
return false;
}
}
private static boolean isValidBackupFileForState(String fileName, Value vaultState) {
return switch (vaultState) {
case VAULT_CONFIG_MISSING -> fileName.startsWith("vault.cryptomator") && fileName.endsWith(".bkup");
case MASTERKEY_MISSING -> fileName.startsWith("masterkey.cryptomator") && fileName.endsWith(".bkup");
default -> false;
};
}
private static boolean copyBackupFile(Path backupFile, Path configPath) {
try {
Files.copy(backupFile, configPath, StandardCopyOption.REPLACE_EXISTING);
return true;
} catch (IOException e) {
return false;
}
}
}

View File

@@ -0,0 +1,33 @@
package org.cryptomator.common.recovery;
import java.io.IOException;
import java.nio.file.Path;
import javafx.beans.property.IntegerProperty;
import org.cryptomator.common.Constants;
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptolib.api.*;
import static org.cryptomator.common.Constants.DEFAULT_KEY_ID;
public final class CryptoFsInitializer {
private CryptoFsInitializer() {}
public static void init(Path recoveryPath,
Masterkey masterkey,
IntegerProperty shorteningThreshold,
CryptorProvider.Scheme scheme) throws IOException, CryptoException {
MasterkeyLoader loader = ignored -> masterkey.copy();
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties //
.cryptoFileSystemProperties() //
.withCipherCombo(scheme) //
.withKeyLoader(loader) //
.withShorteningThreshold(shorteningThreshold.get()) //
.build();
CryptoFileSystemProvider.initialize(recoveryPath, fsProps, DEFAULT_KEY_ID);
}
}

View File

@@ -0,0 +1,102 @@
package org.cryptomator.common.recovery;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptolib.api.*;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.ui.recoverykey.RecoveryKeyFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.beans.property.StringProperty;
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 Optional<CryptorProvider.Scheme> validateRecoveryKeyAndDetectCombo(RecoveryKeyFactory recoveryKeyFactory, Vault vault, StringProperty recoveryKey, MasterkeyFileAccess masterkeyFileAccess, AtomicBoolean illegalArgumentExceptionOccurred) {
var tmpPass = UUID.randomUUID().toString();
try(RecoveryDirectory recoveryDirectory = RecoveryDirectory.create(vault.getPath())) {
var tempRecoveryPath = recoveryDirectory.getRecoveryPath();
recoverFromRecoveryKey(recoveryKey.get(), recoveryKeyFactory, tempRecoveryPath, tmpPass);
var masterkeyFilePath = tempRecoveryPath.resolve(MASTERKEY_FILENAME);
try (Masterkey mk = load(masterkeyFileAccess, masterkeyFilePath, tmpPass)) {
return detect(mk.getEncoded(), vault.getPath());
} catch (IOException | CryptoException e) {
LOG.info("Recovery key validation failed", e);
return Optional.empty();
} catch (IllegalArgumentException e) {
illegalArgumentExceptionOccurred.set(true);
return Optional.empty();
}
} catch (IOException | CryptoException e) {
LOG.info("Recovery key validation failed");
} catch (IllegalArgumentException e) {
LOG.info("Recovery key has an illegal argument");
illegalArgumentExceptionOccurred.set(true);
}
return Optional.empty();
}
public static Optional<CryptorProvider.Scheme> detect(byte[] masterkey, Path vaultPath) {
try (Stream<Path> paths = Files.walk(vaultPath.resolve(DATA_DIR_NAME))) {
Path c9rFile = paths.filter(p -> p.toString().endsWith(".c9r"))
.findFirst().orElse(null);
if (c9rFile == null) {
LOG.info("No *.c9r file found in {}", vaultPath);
return Optional.empty();
}
return determineScheme(c9rFile, masterkey); // jetzt auch ein Optional
} catch (IOException e) {
LOG.debug("Failed to inspect vault", e);
return Optional.empty();
}
}
private static Optional<CryptorProvider.Scheme> determineScheme(Path c9rFile, byte[] masterkey) {
try {
ByteBuffer header = ByteBuffer.wrap(Files.readAllBytes(c9rFile));
return Arrays.stream(CryptorProvider.Scheme.values())
.filter(s -> tryDecrypt(header, new Masterkey(masterkey), s))
.findFirst();
} catch (IOException e) {
LOG.info("Failed to decrypt .c9r file", e);
return Optional.empty();
}
}
private static boolean tryDecrypt(ByteBuffer header, Masterkey masterkey, CryptorProvider.Scheme scheme) {
try (Cryptor cryptor = CryptorProvider.forScheme(scheme).provide(masterkey, SecureRandom.getInstanceStrong())) {
cryptor.fileHeaderCryptor().decryptHeader(header.duplicate());
return true;
} catch (Exception e) {
return false;
}
}
}

View File

@@ -0,0 +1,9 @@
package org.cryptomator.common.recovery;
public enum RecoveryActionType {
RESTORE_VAULT_CONFIG,
RESTORE_MASTERKEY,
RESET_PASSWORD,
SHOW_KEY,
CONVERT_VAULT
}

View File

@@ -0,0 +1,65 @@
package org.cryptomator.common.recovery;
import java.io.IOException;
import java.nio.file.*;
import java.util.Comparator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
public final class RecoveryDirectory implements AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(RecoveryDirectory.class);
private final Path recoveryPath;
private final Path vaultPath;
private static Path addR(Path p){
return p.resolve("r");
}
private RecoveryDirectory(Path vaultPath) {
this.vaultPath = vaultPath;
this.recoveryPath = addR(vaultPath);
}
public static RecoveryDirectory create(Path vaultPath) throws IOException {
//TODO: Files.createTmpDirectory Doku lesen und ggf nutzen
Path recovery = addR(vaultPath);
Files.createDirectory(recovery);
return new RecoveryDirectory(vaultPath);
}
public void moveRecoveredFiles() throws IOException {
Files.move(recoveryPath.resolve(MASTERKEY_FILENAME), vaultPath.resolve(MASTERKEY_FILENAME), StandardCopyOption.REPLACE_EXISTING);
Files.move(recoveryPath.resolve(VAULTCONFIG_FILENAME), vaultPath.resolve(VAULTCONFIG_FILENAME)); //TODO: ? StandardCopyOption.REPLACE_EXISTING
}
private void deleteRecoveryDirectory() {
try (var paths = Files.walk(recoveryPath)) {
paths.sorted(Comparator.reverseOrder()).forEach(p -> { //TODO: wieso reverseOrder
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

@@ -26,9 +26,11 @@ 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.MASTERKEY_MISSING;
import static org.cryptomator.common.vaults.VaultState.Value.PROCESSING;
import static org.cryptomator.common.vaults.VaultState.Value.UNLOCKED;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.RecoverUtil;
import org.cryptomator.common.recovery.BackupRestorer;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
@@ -146,45 +148,35 @@ public class VaultListManager {
return vaultComponentFactory.create(vaultSettings, wrapper, ERROR, e).vault();
}
}
public static VaultState.Value redetermineVaultState(Vault vault) {
VaultState state = vault.stateProperty();
VaultState.Value previousState = state.getValue();
return switch (previousState) {
case LOCKED, NEEDS_MIGRATION, MISSING, VAULT_CONFIG_MISSING, MASTERKEY_MISSING -> {
try {
var determinedState = determineVaultState(vault.getPath(), vault.getVaultSettings());
if (determinedState == MASTERKEY_MISSING) {
var vaultScheme = vault.getVaultConfigCache().getUnchecked().getKeyId().getScheme();
if (KeyLoadingStrategy.isHubVault(vaultScheme)) {
determinedState = LOCKED;
}
}
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 state = vault.stateProperty();
VaultState.Value previous = state.getValue();
if (previous.equals(UNLOCKED)||previous.equals(PROCESSING)) {
return previous;
}
try {
VaultState.Value determined = determineVaultState(vault.getPath(), vault.getVaultSettings());
if (determined == MASTERKEY_MISSING) {
if (KeyLoadingStrategy.isHubVault(vault.getVaultConfigCache().getUnchecked().getKeyId().getScheme())) {
determined = LOCKED;
}
}
case ERROR -> {
try {
var determinedState = determineVaultState(vault.getPath(), vault.getVaultSettings());
state.set(determinedState);
yield determinedState;
} catch (IOException e) {
LOG.warn("Failed to redetermine vault state for {}", vault.getPath(), e);
vault.setLastKnownException(e);
yield ERROR;
}
if (determined == LOCKED) {
vault.getVaultConfigCache().reloadConfig();
}
case 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, VaultSettings vaultSettings) throws IOException {
@@ -195,9 +187,12 @@ public class VaultListManager {
return VaultState.Value.MISSING;
}
boolean vaultConfigRestored = Files.notExists(pathToVaultConfig) && RecoverUtil.restoreBackupIfAvailable(pathToVaultConfig, VaultState.Value.VAULT_CONFIG_MISSING);
boolean vaultConfigRestored = Files.notExists(pathToVaultConfig)
&& BackupRestorer.restoreIfPresent(pathToVaultConfig.getParent(), VaultState.Value.VAULT_CONFIG_MISSING);
boolean masterkeyRestored = Files.notExists(pathToMasterkey) && KeyLoadingStrategy.isMasterkeyFileVault(vaultSettings.lastKnownKeyLoader.get()) && RecoverUtil.restoreBackupIfAvailable(pathToMasterkey, VaultState.Value.MASTERKEY_MISSING);
boolean masterkeyRestored = Files.notExists(pathToMasterkey)
&& KeyLoadingStrategy.isMasterkeyFileVault(vaultSettings.lastKnownKeyLoader.get())
&& BackupRestorer.restoreIfPresent(pathToMasterkey.getParent(), VaultState.Value.MASTERKEY_MISSING);
if (vaultConfigRestored || masterkeyRestored) {
return LOCKED;

View File

@@ -22,12 +22,15 @@ import java.util.Optional;
import java.util.ResourceBundle;
import static org.cryptomator.common.Constants.CRYPTOMATOR_FILENAME_GLOB;
import static org.cryptomator.common.vaults.VaultState.Value.VAULT_CONFIG_MISSING;
import dagger.Lazy;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.RecoverUtil;
import org.cryptomator.common.recovery.RecoveryActionType;
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.cryptomator.integrations.uiappearance.Theme;
@@ -131,14 +134,50 @@ public class ChooseExistingVaultController implements FxController {
@FXML
public void restoreVaultConfigWithRecoveryKey() {
DirectoryChooser directoryChooser = new DirectoryChooser();
Optional<Vault> optionalVault = RecoverUtil.checkAndPrepareVaultFromDirectory(directoryChooser, window, dialogs, vaultComponentFactory, mountServices);
File selectedDirectory;
do {
selectedDirectory = directoryChooser.showDialog(window);
boolean hasSubfolderD = new File(selectedDirectory, "d").isDirectory();
if (!hasSubfolderD) {
dialogs.prepareNoDDirectorySelectedDialog(window).build().showAndWait();
selectedDirectory = null;
}
} while (selectedDirectory == null);
Optional<Vault> optionalVault = prepareVault(selectedDirectory,vaultComponentFactory,
mountServices);
//TODO: optional raus, und mit error dialog arbeiten (UI kram in UI package!) hier nur fehler werfen
optionalVault.ifPresent(vault -> {
ObjectProperty<RecoverUtil.Type> recoverTypeProperty = new SimpleObjectProperty<>(RecoverUtil.Type.RESTORE_VAULT_CONFIG);
ObjectProperty<RecoveryActionType> recoverTypeProperty = new SimpleObjectProperty<>(RecoveryActionType.RESTORE_VAULT_CONFIG);
recoveryKeyWindow.create(vault, window, recoverTypeProperty).showIsHubVaultDialogWindow();
});
}
public static Optional<Vault> prepareVault(File selectedDirectory, VaultComponent.Factory vaultComponentFactory, List<MountService> mountServices) {
Path selectedPath = selectedDirectory.toPath();
VaultSettings vaultSettings = VaultSettings.withRandomId();
vaultSettings.path.set(selectedPath);
if (selectedPath.getFileName() != null) {
vaultSettings.displayName.set(selectedPath.getFileName().toString());
} else {
vaultSettings.displayName.set("defaultVaultName");
}
var wrapper = new VaultConfigCache(vaultSettings);
Vault vault = vaultComponentFactory.create(vaultSettings, wrapper, VAULT_CONFIG_MISSING, null).vault(); //TODO: VAULT_CONFIG_MISSING nicht sicher, stand nochmal überprüfen
//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 Optional.of(vault);
}
/* Getter */
public ObservableValue<Image> screenshotProperty() {

View File

@@ -4,7 +4,7 @@ import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import org.cryptomator.common.RecoverUtil;
import org.cryptomator.common.recovery.RecoveryActionType;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.ui.changepassword.NewPasswordController;
@@ -122,7 +122,7 @@ abstract class ConvertVaultModule {
@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, new SimpleObjectProperty<>(RecoverUtil.Type.CONVERT_VAULT), null, null);
return new RecoveryKeyValidateController(vault, vaultConfig, recoveryKey, recoveryKeyFactory, new SimpleObjectProperty<>(RecoveryActionType.CONVERT_VAULT), null, null);
}
}

View File

@@ -1,6 +1,6 @@
package org.cryptomator.ui.mainwindow;
import org.cryptomator.common.RecoverUtil;
import org.cryptomator.common.recovery.RecoveryActionType;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.ui.common.FxController;
@@ -61,14 +61,14 @@ public class VaultDetailMissingVaultController implements FxController {
dialogs.prepareContactHubAdmin(window).build().showAndWait();
}
else {
ObjectProperty<RecoverUtil.Type> recoverTypeProperty = new SimpleObjectProperty<>(RecoverUtil.Type.RESTORE_VAULT_CONFIG);
ObjectProperty<RecoveryActionType> recoverTypeProperty = new SimpleObjectProperty<>(RecoveryActionType.RESTORE_VAULT_CONFIG);
recoveryKeyWindow.create(vault.get(), window, recoverTypeProperty).showIsHubVaultDialogWindow();
}
}
@FXML
void restoreMasterkey() {
ObjectProperty<RecoverUtil.Type> recoverTypeProperty = new SimpleObjectProperty<>(RecoverUtil.Type.RESTORE_MASTERKEY);
ObjectProperty<RecoveryActionType> recoverTypeProperty = new SimpleObjectProperty<>(RecoveryActionType.RESTORE_MASTERKEY);
recoveryKeyWindow.create(vault.get(), window, recoverTypeProperty).showRecoveryKeyRecoverWindow();
}

View File

@@ -3,7 +3,7 @@ package org.cryptomator.ui.recoverykey;
import dagger.BindsInstance;
import dagger.Lazy;
import dagger.Subcomponent;
import org.cryptomator.common.RecoverUtil;
import org.cryptomator.common.recovery.RecoveryActionType;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
@@ -55,7 +55,7 @@ public interface RecoveryKeyComponent {
RecoveryKeyComponent create(@BindsInstance @RecoveryKeyWindow Vault vault, //
@BindsInstance @Named("keyRecoveryOwner") Stage owner, //
@BindsInstance @Named("recoverType") ObjectProperty<RecoverUtil.Type> recoverType);
@BindsInstance @Named("recoverType") ObjectProperty<RecoveryActionType> recoverType);
}
}

View File

@@ -9,7 +9,7 @@ import javafx.stage.Stage;
import java.util.ResourceBundle;
import dagger.Lazy;
import org.cryptomator.common.RecoverUtil;
import org.cryptomator.common.recovery.RecoveryActionType;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
@@ -19,12 +19,12 @@ public class RecoveryKeyIsHubVaultController implements FxController {
private final Stage window;
private final Lazy<Scene> recoverykeyRecoverScene;
private final ObjectProperty<RecoverUtil.Type> recoverType;
private final ObjectProperty<RecoveryActionType> recoverType;
@Inject
public RecoveryKeyIsHubVaultController(@RecoveryKeyWindow Stage window, //
@FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy<Scene> recoverykeyRecoverScene, //
@Named("recoverType") ObjectProperty<RecoverUtil.Type> recoverType, //
@Named("recoverType") ObjectProperty<RecoveryActionType> recoverType, //
ResourceBundle resourceBundle) {
this.window = window;
window.setTitle(resourceBundle.getString("recoveryKey.recoverVaultConfig.title"));
@@ -40,7 +40,7 @@ public class RecoveryKeyIsHubVaultController implements FxController {
@FXML
public void recover() {
recoverType.set(RecoverUtil.Type.RESTORE_VAULT_CONFIG);
recoverType.set(RecoveryActionType.RESTORE_VAULT_CONFIG);
window.setScene(recoverykeyRecoverScene.get());
}
}

View File

@@ -5,7 +5,7 @@ import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import org.cryptomator.common.Nullable;
import org.cryptomator.common.RecoverUtil;
import org.cryptomator.common.recovery.RecoveryActionType;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptolib.api.CryptorProvider;
@@ -177,7 +177,7 @@ abstract class RecoveryKeyModule {
@Provides
@IntoMap
@FxControllerKey(RecoveryKeyValidateController.class)
static FxController bindRecoveryKeyValidateController(@RecoveryKeyWindow Vault vault, @RecoveryKeyWindow @Nullable VaultConfig.UnverifiedVaultConfig vaultConfig, @RecoveryKeyWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory, @Named("recoverType") ObjectProperty<RecoverUtil.Type> recoverType, @Named("cipherCombo") ObjectProperty<CryptorProvider.Scheme> cipherCombo, @Nullable MasterkeyFileAccess masterkeyFileAccess) {
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, recoverType, cipherCombo, masterkeyFileAccess);
}

View File

@@ -1,7 +1,7 @@
package org.cryptomator.ui.recoverykey;
import dagger.Lazy;
import org.cryptomator.common.RecoverUtil;
import org.cryptomator.common.recovery.RecoveryActionType;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
@@ -27,7 +27,7 @@ public class RecoveryKeyRecoverController implements FxController {
public RecoveryKeyRecoverController(@RecoveryKeyWindow Stage window, //
@FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) Lazy<Scene> resetPasswordScene, //
@FxmlScene(FxmlFile.RECOVERYKEY_EXPERT_SETTINGS) Lazy<Scene> expertSettingsScene, //
ResourceBundle resourceBundle, @Named("recoverType") ObjectProperty<RecoverUtil.Type> recoverType) {
ResourceBundle resourceBundle, @Named("recoverType") ObjectProperty<RecoveryActionType> recoverType) {
this.window = window;
this.nextScene = switch (recoverType.get()) {

View File

@@ -5,6 +5,7 @@ import javax.inject.Named;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.concurrent.Task;
@@ -19,7 +20,10 @@ import java.util.concurrent.ExecutorService;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
import dagger.Lazy;
import org.cryptomator.common.RecoverUtil;
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;
@@ -50,7 +54,7 @@ public class RecoveryKeyResetPasswordController implements FxController {
private final MasterkeyFileAccess masterkeyFileAccess;
private final VaultListManager vaultListManager;
private final IntegerProperty shorteningThreshold;
private final ObjectProperty<RecoverUtil.Type> recoverType;
private final ObjectProperty<RecoveryActionType> recoverType;
private final ObjectProperty<CryptorProvider.Scheme> cipherCombo;
private final ResourceBundle resourceBundle;
private final StringProperty buttonText = new SimpleStringProperty();
@@ -70,7 +74,8 @@ public class RecoveryKeyResetPasswordController implements FxController {
MasterkeyFileAccess masterkeyFileAccess, //
VaultListManager vaultListManager, //
@Named("shorteningThreshold") IntegerProperty shorteningThreshold, //
@Named("recoverType") ObjectProperty<RecoverUtil.Type> recoverType, @Named("cipherCombo") ObjectProperty<CryptorProvider.Scheme> cipherCombo,//
@Named("recoverType") ObjectProperty<RecoveryActionType> recoverType, //
@Named("cipherCombo") ObjectProperty<CryptorProvider.Scheme> cipherCombo,//
ResourceBundle resourceBundle, Dialogs dialogs) {
this.window = window;
this.vault = vault;
@@ -90,8 +95,8 @@ public class RecoveryKeyResetPasswordController implements FxController {
initButtonText(recoverType.get());
}
private void initButtonText(RecoverUtil.Type type) {
if (type == RecoverUtil.Type.RESTORE_MASTERKEY) {
private void initButtonText(RecoveryActionType type) {
if (type == RecoveryActionType.RESTORE_MASTERKEY) {
buttonText.set(resourceBundle.getString("generic.button.close"));
} else {
buttonText.set(resourceBundle.getString("generic.button.back"));
@@ -100,42 +105,68 @@ public class RecoveryKeyResetPasswordController implements FxController {
@FXML
public void close() {
if (recoverType.getValue().equals(RecoverUtil.Type.RESTORE_MASTERKEY)) {
if (recoverType.getValue().equals(RecoveryActionType.RESTORE_MASTERKEY)) {
window.close();
} else {
window.setScene(recoverExpertSettingsScene.get());
}
}
@FXML
public void restorePassword() {
try (RecoveryDirectory recoveryDirectory = RecoveryDirectory.create(vault.getPath())) {
Path recoveryPath = recoveryDirectory.getRecoveryPath();
MasterkeyService.recoverFromRecoveryKey(recoveryKey.get(), recoveryKeyFactory, recoveryPath, newPasswordController.passwordField.getCharacters());
Path masterkeyFilePath = recoveryPath.resolve(MASTERKEY_FILENAME);
try (Masterkey masterkey = MasterkeyService.load(masterkeyFileAccess, masterkeyFilePath, newPasswordController.passwordField.getCharacters())) {
CryptoFsInitializer.init(recoveryPath, masterkey, shorteningThreshold, cipherCombo.get());
}
recoveryDirectory.moveRecoveredFiles();
if (!vaultListManager.containsVault(vault.getPath())) {
vaultListManager.add(vault.getPath());
}
dialogs.prepareRecoverPasswordSuccess(window, owner, resourceBundle)
.setTitleKey("recoveryKey.recoverVaultConfig.title")
.setMessageKey("recoveryKey.recover.resetVaultConfigSuccess.message")
.build().showAndWait();
window.close();
} catch (IOException | CryptoException e) {
LOG.error("Recovery process failed", e);
appWindows.showErrorWindow(e, window, null);
}
}
@FXML
public void resetPassword() {
if (vault.isMissingVaultConfig()) {
try {
Path recoveryPath = RecoverUtil.createRecoveryDirectory(vault.getPath());
RecoverUtil.createNewMasterkeyFile(recoveryKeyFactory, recoveryPath, recoveryKey.get(), newPasswordController.passwordField.getCharacters());
Path masterkeyFilePath = recoveryPath.resolve(MASTERKEY_FILENAME);
Task<Void> task = new ResetPasswordTask(recoveryKeyFactory, vault, recoveryKey, newPasswordController);
try (Masterkey masterkey = RecoverUtil.loadMasterkey(masterkeyFileAccess, masterkeyFilePath, newPasswordController.passwordField.getCharacters())) {
RecoverUtil.initializeCryptoFileSystem(recoveryPath, masterkey, shorteningThreshold, cipherCombo.get());
}
task.setOnScheduled(_ -> LOG.debug("Using recovery key to reset password for {}.", vault.getDisplayablePath()));
RecoverUtil.moveRecoveredFiles(recoveryPath, vault.getPath());
RecoverUtil.deleteRecoveryDirectory(recoveryPath);
RecoverUtil.addVaultToList(vaultListManager, vault.getPath());
dialogs.prepareRecoverPasswordSuccess(window, owner, resourceBundle).setTitleKey("recoveryKey.recoverVaultConfig.title").setMessageKey("recoveryKey.recover.resetVaultConfigSuccess.message").build().showAndWait();
window.close();
} catch (IOException | CryptoException e) {
LOG.error("Recovery process failed", e);
task.setOnSucceeded(_ -> {
LOG.info("Used recovery key to reset password for {}.", vault.getDisplayablePath());
if (vault.getState().equals(org.cryptomator.common.vaults.VaultState.Value.MASTERKEY_MISSING)) {
dialogs.prepareRecoverPasswordSuccess(window, owner, resourceBundle)
.setTitleKey("recoveryKey.recoverMasterkey.title")
.setMessageKey("recoveryKey.recover.resetMasterkeyFileSuccess.message")
.build().showAndWait();
} else {
dialogs.prepareRecoverPasswordSuccess(window, owner, resourceBundle)
.build().showAndWait();
}
} else {
Task<Void> task = RecoverUtil.createResetPasswordTask( //
resourceBundle, owner, recoveryKeyFactory, //
vault, recoveryKey, newPasswordController, //
window, appWindows, dialogs);
executor.submit(task);
}
window.close();
});
task.setOnFailed(_ -> {
LOG.error("Resetting password failed.", task.getException());
appWindows.showErrorWindow(task.getException(), window, null);
});
executor.submit(task);
}
/* Getter/Setter */
@@ -155,5 +186,37 @@ public class RecoveryKeyResetPasswordController implements FxController {
public boolean isPasswordSufficientAndMatching() {
return newPasswordController.isGoodPassword();
}
private final ReadOnlyBooleanWrapper vaultConfigMissing = new ReadOnlyBooleanWrapper();
public ReadOnlyBooleanProperty vaultConfigMissingProperty() {
return vaultConfigMissing.getReadOnlyProperty();
}
public boolean isVaultConfigMissing() {
return vault.isMissingVaultConfig();
}
private static class ResetPasswordTask extends Task<Void> {
private static final Logger LOG = LoggerFactory.getLogger(ResetPasswordTask.class);
private final RecoveryKeyFactory recoveryKeyFactory;
private final Vault vault;
private final StringProperty recoveryKey;
private final NewPasswordController newPasswordController;
public ResetPasswordTask(RecoveryKeyFactory recoveryKeyFactory, Vault vault, StringProperty recoveryKey, NewPasswordController newPasswordController) {
this.recoveryKeyFactory = recoveryKeyFactory;
this.vault = vault;
this.recoveryKey = recoveryKey;
this.newPasswordController = newPasswordController;
setOnFailed(_ -> LOG.error("Failed to reset password", getException()));
}
@Override
protected Void call() throws IOException, IllegalArgumentException {
recoveryKeyFactory.newMasterkeyFileWithPassphrase(vault.getPath(), recoveryKey.get(), newPasswordController.passwordField.getCharacters());
return null;
}
}
}

View File

@@ -5,7 +5,6 @@ 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.RecoverUtil;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptofs.VaultConfigLoadException;
@@ -28,6 +27,9 @@ import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import java.util.concurrent.atomic.AtomicBoolean;
import org.cryptomator.common.recovery.MasterkeyService;
import org.cryptomator.common.recovery.RecoveryActionType;
public class RecoveryKeyValidateController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(RecoveryKeyCreationController.class);
@@ -43,7 +45,7 @@ public class RecoveryKeyValidateController implements FxController {
private final ObjectProperty<RecoveryKeyState> recoveryKeyState;
private final ObjectProperty<CryptorProvider.Scheme> cipherCombo;
private final AutoCompleter autoCompleter;
private final ObjectProperty<RecoverUtil.Type> recoverType;
private final ObjectProperty<RecoveryActionType> recoverType;
private final MasterkeyFileAccess masterkeyFileAccess;
private volatile boolean isWrongKey;
@@ -54,7 +56,7 @@ public class RecoveryKeyValidateController implements FxController {
@Nullable VaultConfig.UnverifiedVaultConfig vaultConfig, //
StringProperty recoveryKey, //
RecoveryKeyFactory recoveryKeyFactory, //
@Named("recoverType") ObjectProperty<RecoverUtil.Type> recoverType, //
@Named("recoverType") ObjectProperty<RecoveryActionType> recoverType, //
@Named("cipherCombo") ObjectProperty<CryptorProvider.Scheme> cipherCombo,//
MasterkeyFileAccess masterkeyFileAccess) {
this.vault = vault;
@@ -137,7 +139,7 @@ public class RecoveryKeyValidateController implements FxController {
switch (recoverType.get()) {
case RESTORE_VAULT_CONFIG -> {
AtomicBoolean illegalArgumentExceptionOccurred = new AtomicBoolean(false);
var combo = RecoverUtil.validateRecoveryKeyAndGetCombo(
var combo = MasterkeyService.validateRecoveryKeyAndDetectCombo(
recoveryKeyFactory, vault, recoveryKey, masterkeyFileAccess, illegalArgumentExceptionOccurred);
combo.ifPresent(cipherCombo::set);
if (illegalArgumentExceptionOccurred.get()) {

View File

@@ -1,7 +1,7 @@
package org.cryptomator.ui.vaultoptions;
import org.cryptomator.common.RecoverUtil;
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;
@@ -50,13 +50,13 @@ public class MasterkeyOptionsController implements FxController {
@FXML
public void showRecoveryKey() {
ObjectProperty<RecoverUtil.Type> recoverTypeProperty = new SimpleObjectProperty<>(RecoverUtil.Type.SHOW_KEY);
ObjectProperty<RecoveryActionType> recoverTypeProperty = new SimpleObjectProperty<>(RecoveryActionType.SHOW_KEY);
recoveryKeyWindow.create(vault, window, recoverTypeProperty).showRecoveryKeyCreationWindow();
}
@FXML
public void showRecoverVaultDialog() {
ObjectProperty<RecoverUtil.Type> recoverTypeProperty = new SimpleObjectProperty<>(RecoverUtil.Type.RESET_PASSWORD);
ObjectProperty<RecoveryActionType> recoverTypeProperty = new SimpleObjectProperty<>(RecoveryActionType.RESET_PASSWORD);
recoveryKeyWindow.create(vault, window, recoverTypeProperty).showRecoveryKeyRecoverWindow();
}

View File

@@ -25,7 +25,8 @@
<ButtonBar buttonMinWidth="120" buttonOrder="+CI">
<buttons>
<Button text="${controller.buttonText}" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
<Button text="%recoveryKey.recover.resetBtn" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#resetPassword" disable="${!controller.passwordSufficientAndMatching}"/>
<Button text="%recoveryKey.recover.resetBtn" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#resetPassword" managed="${!controller.vaultConfigMissing}" disable="${!controller.passwordSufficientAndMatching}"/>
<Button text="%recoveryKey.recover.recoverBtn" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#restorePassword" managed="${controller.vaultConfigMissing}" disable="${!controller.passwordSufficientAndMatching}"/>
</buttons>
</ButtonBar>
</VBox>

View File

@@ -519,6 +519,7 @@ 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.