restore config by recoverykey

auto restore when bkup exists
added new VaultStates
This commit is contained in:
Jan-Peter Klein
2024-12-11 16:30:56 +01:00
parent 5114e2ad22
commit ba3667ab51
13 changed files with 277 additions and 18 deletions

View File

@@ -70,6 +70,8 @@ public class Vault {
private final BooleanBinding missing;
private final BooleanBinding needsMigration;
private final BooleanBinding unknownError;
private final BooleanBinding missingMasterkey;
private final BooleanBinding missingVaultConfig;
private final ObjectBinding<Mountpoint> mountPoint;
private final Mounter mounter;
private final Settings settings;
@@ -96,6 +98,8 @@ 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.missingMasterkey = Bindings.createBooleanBinding(this::isMissingMasterkey, 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);
@@ -315,6 +319,22 @@ public class Vault {
return state.get() == VaultState.Value.ERROR;
}
public BooleanBinding missingMasterkeyProperty() {
return missingMasterkey;
}
public boolean isMissingMasterkey() {
return state.get() == VaultState.Value.MASTERKEY_MISSING;
}
public BooleanBinding missingVaultConfigProperty() {
return missingVaultConfig;
}
public boolean isMissingVaultConfig() {
return state.get() == VaultState.Value.VAULT_CONFIG_MISSING;
}
public ReadOnlyStringProperty displayNameProperty() {
return vaultSettings.displayName;
}

View File

@@ -25,15 +25,20 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.stream.Stream;
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.MASTERKEY_MISSING;
import static org.cryptomator.common.vaults.VaultState.Value.MISSING;
import static org.cryptomator.common.vaults.VaultState.Value.VAULT_CONFIG_MISSING;
@Singleton
public class VaultListManager {
@@ -129,7 +134,7 @@ public class VaultListManager {
VaultState state = vault.stateProperty();
VaultState.Value previousState = state.getValue();
return switch (previousState) {
case LOCKED, NEEDS_MIGRATION, MISSING -> {
case LOCKED, NEEDS_MIGRATION, MISSING, VAULT_CONFIG_MISSING, MASTERKEY_MISSING -> {
try {
var determinedState = determineVaultState(vault.getPath());
if (determinedState == LOCKED) {
@@ -149,7 +154,56 @@ public class VaultListManager {
}
private static VaultState.Value determineVaultState(Path pathToVault) throws IOException {
if (!Files.exists(pathToVault)) {
Path pathToVaultConfig = Path.of(pathToVault.toString(),"vault.cryptomator");
Path pathToMasterkey = Path.of(pathToVault.toString(),"masterkey.cryptomator");
if (!Files.exists(pathToVaultConfig)) {
try (Stream<Path> files = Files.list(pathToVaultConfig.getParent())) {
Path backupFile = files.filter(file -> {
String fileName = file.getFileName().toString();
return fileName.startsWith("vault.cryptomator") && fileName.endsWith(".bkup");
}).findFirst().orElse(null);
if (backupFile != null) {
try {
Files.copy(backupFile, pathToVaultConfig, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
LOG.error("error",e);
return VAULT_CONFIG_MISSING;
}
} else {
return VAULT_CONFIG_MISSING;
}
} catch (IOException e) {
LOG.error("error",e);
return VAULT_CONFIG_MISSING;
}
}
else if (!Files.exists(pathToMasterkey)) {
//return VaultState.Value.MASTERKEY_MISSING;
try (Stream<Path> files = Files.list(pathToMasterkey.getParent())) {
Path backupFile = files.filter(file -> {
String fileName = file.getFileName().toString();
return fileName.startsWith("masterkey.cryptomator") && fileName.endsWith(".bkup");
}).findFirst().orElse(null);
if (backupFile != null) {
try {
Files.copy(backupFile, pathToMasterkey, StandardCopyOption.REPLACE_EXISTING);
return MASTERKEY_MISSING;
} catch (IOException e) {
LOG.error("error",e);
return MASTERKEY_MISSING;
}
} else {
return MASTERKEY_MISSING;
}
} catch (IOException e) {
LOG.error("error",e);
return MASTERKEY_MISSING;
}
}
else if (!Files.exists(pathToVault)) {
return VaultState.Value.MISSING;
}
return switch (CryptoFileSystemProvider.checkDirStructureForVault(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME)) {

View File

@@ -25,6 +25,10 @@ public class VaultState extends ObservableValueBase<VaultState.Value> implements
*/
MISSING,
VAULT_CONFIG_MISSING,
MASTERKEY_MISSING,
/**
* Vault requires migration to a newer vault format
*/

View File

@@ -16,6 +16,7 @@ import org.cryptomator.ui.common.StageInitializer;
import org.cryptomator.ui.error.ErrorComponent;
import org.cryptomator.ui.fxapp.PrimaryStage;
import org.cryptomator.ui.migration.MigrationComponent;
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
import org.cryptomator.ui.removevault.RemoveVaultComponent;
import org.cryptomator.ui.stats.VaultStatisticsComponent;
import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent;
@@ -30,7 +31,7 @@ import javafx.stage.Stage;
import java.util.Map;
import java.util.ResourceBundle;
@Module(subcomponents = {AddVaultWizardComponent.class, MigrationComponent.class, RemoveVaultComponent.class, VaultStatisticsComponent.class, WrongFileAlertComponent.class, ErrorComponent.class})
@Module(subcomponents = {AddVaultWizardComponent.class, MigrationComponent.class, RemoveVaultComponent.class, VaultStatisticsComponent.class, WrongFileAlertComponent.class, ErrorComponent.class, RecoveryKeyComponent.class})
abstract class MainWindowModule {
@Provides

View File

@@ -53,6 +53,8 @@ public class VaultDetailController implements FxController {
case PROCESSING -> FontAwesome5Icon.SPINNER;
case UNLOCKED -> FontAwesome5Icon.LOCK_OPEN;
case NEEDS_MIGRATION, MISSING, ERROR -> FontAwesome5Icon.EXCLAMATION_TRIANGLE;
case VAULT_CONFIG_MISSING -> FontAwesome5Icon.COGS;
case MASTERKEY_MISSING -> FontAwesome5Icon.KEY;
};
} else {
return FontAwesome5Icon.EXCLAMATION_TRIANGLE;

View File

@@ -3,6 +3,7 @@ package org.cryptomator.ui.mainwindow;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
import org.cryptomator.ui.removevault.RemoveVaultComponent;
import javax.inject.Inject;
@@ -22,14 +23,17 @@ public class VaultDetailMissingVaultController implements FxController {
private final RemoveVaultComponent.Builder removeVault;
private final ResourceBundle resourceBundle;
private final Stage window;
private final RecoveryKeyComponent.Factory recoveryKeyWindow;
@Inject
public VaultDetailMissingVaultController(ObjectProperty<Vault> vault, RemoveVaultComponent.Builder removeVault, ResourceBundle resourceBundle, @MainWindow Stage window) {
public VaultDetailMissingVaultController(ObjectProperty<Vault> vault, RemoveVaultComponent.Builder removeVault, ResourceBundle resourceBundle, @MainWindow Stage window, RecoveryKeyComponent.Factory recoveryKeyWindow) {
this.vault = vault;
this.removeVault = removeVault;
this.resourceBundle = resourceBundle;
this.window = window;
this.recoveryKeyWindow = recoveryKeyWindow;
}
@FXML
@@ -42,6 +46,15 @@ public class VaultDetailMissingVaultController implements FxController {
removeVault.vault(vault.get()).build().showRemoveVault();
}
@FXML
void restoreVaultConfig(){
recoveryKeyWindow.create(vault.get(), window).showRecoveryKeyRecoverWindow("Recover VaultConfig");
}
@FXML
void restoreMasterkey(){
recoveryKeyWindow.create(vault.get(), window).showRecoveryKeyRecoverWindow("Recover Masterkey");
}
@FXML
void changeLocation() {
// copied from ChooseExistingVaultController class

View File

@@ -47,6 +47,8 @@ public class VaultListCellController implements FxController {
case PROCESSING -> FontAwesome5Icon.SPINNER;
case UNLOCKED -> FontAwesome5Icon.LOCK_OPEN;
case NEEDS_MIGRATION, MISSING, ERROR -> FontAwesome5Icon.EXCLAMATION_TRIANGLE;
case VAULT_CONFIG_MISSING -> FontAwesome5Icon.COGS;
case MASTERKEY_MISSING -> FontAwesome5Icon.KEY;
};
} else {
return FontAwesome5Icon.EXCLAMATION_TRIANGLE;

View File

@@ -38,6 +38,13 @@ public interface RecoveryKeyComponent {
stage.show();
}
default void showRecoveryKeyRecoverWindow(String title) {
Stage stage = window();
stage.setScene(recoverScene().get());
stage.setTitle(title);
stage.sizeToScene();
stage.show();
}
@Subcomponent.Factory
interface Factory {

View File

@@ -2,6 +2,13 @@ package org.cryptomator.ui.recoverykey;
import dagger.Lazy;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
@@ -18,8 +25,17 @@ import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
import java.nio.file.CopyOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Comparator;
import java.util.concurrent.ExecutorService;
import static org.cryptomator.common.Constants.DEFAULT_KEY_ID;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
@RecoveryKeyScoped
public class RecoveryKeyResetPasswordController implements FxController {
@@ -32,11 +48,12 @@ public class RecoveryKeyResetPasswordController implements FxController {
private final StringProperty recoveryKey;
private final Lazy<Scene> recoverResetPasswordSuccessScene;
private final FxApplicationWindows appWindows;
private final MasterkeyFileAccess masterkeyFileAccess;
public NewPasswordController newPasswordController;
@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_RESET_PASSWORD_SUCCESS) Lazy<Scene> recoverResetPasswordSuccessScene, FxApplicationWindows appWindows, MasterkeyFileAccess masterkeyFileAccess) {
this.window = window;
this.vault = vault;
this.recoveryKeyFactory = recoveryKeyFactory;
@@ -44,6 +61,7 @@ public class RecoveryKeyResetPasswordController implements FxController {
this.recoveryKey = recoveryKey;
this.recoverResetPasswordSuccessScene = recoverResetPasswordSuccessScene;
this.appWindows = appWindows;
this.masterkeyFileAccess = masterkeyFileAccess;
}
@FXML
@@ -53,19 +71,60 @@ public class RecoveryKeyResetPasswordController implements FxController {
@FXML
public void resetPassword() {
Task<Void> task = new ResetPasswordTask();
task.setOnScheduled(event -> {
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.setOnFailed(event -> {
LOG.error("Resetting password failed.", task.getException());
appWindows.showErrorWindow(task.getException(), window, null);
});
executor.submit(task);
if(vault.isMissingVaultConfig()){
Path vaultPath = vault.getPath();
Path recoveryPath = vaultPath.resolve("r");
try {
Files.createDirectory(recoveryPath);
recoveryKeyFactory.newMasterkeyFileWithPassphrase(recoveryPath, recoveryKey.get(), newPasswordController.passwordField.getCharacters());
} catch (IOException e) {
LOG.error("Creating directory or recovering masterkey failed", e);
}
Path masterkeyFilePath = recoveryPath.resolve(MASTERKEY_FILENAME);
try (Masterkey masterkey = masterkeyFileAccess.load(masterkeyFilePath, newPasswordController.passwordField.getCharacters())) {
try {
MasterkeyLoader loader = ignored -> masterkey.copy();
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
.withCipherCombo(CryptorProvider.Scheme.SIV_CTRMAC) //
.withKeyLoader(loader) //
.withShorteningThreshold(220) //
.build();
CryptoFileSystemProvider.initialize(recoveryPath, fsProps, DEFAULT_KEY_ID);
} catch (CryptoException | IOException e) {
LOG.error("Recovering vault failed", e);
}
Files.move(masterkeyFilePath, vaultPath.resolve(MASTERKEY_FILENAME), StandardCopyOption.REPLACE_EXISTING);
Files.move(recoveryPath.resolve(VAULTCONFIG_FILENAME), vaultPath.resolve(VAULTCONFIG_FILENAME));
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);
}
});
}
window.setScene(recoverResetPasswordSuccessScene.get());
} catch (IOException e) {
LOG.error("Moving recovered files failed", e);
}
}
else {
Task<Void> task = new ResetPasswordTask();
task.setOnScheduled(event -> {
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.setOnFailed(event -> {
LOG.error("Resetting password failed.", task.getException());
appWindows.showErrorWindow(task.getException(), window, null);
});
executor.submit(task);
}
}
private class ResetPasswordTask extends Task<Void> {

View File

@@ -53,5 +53,7 @@
<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_masterkey.fxml" visible="${controller.vault.missingMasterkey}" managed="${controller.vault.missingMasterkey}"/>
<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,44 @@
<?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">
<children>
<VBox spacing="9" alignment="CENTER">
<StackPane alignment="CENTER">
<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.missingMasterkey.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.missingMasterkey.restore" minWidth="120" onAction="#restoreMasterkey">
<graphic>
<FontAwesome5IconView glyph="MAGIC"/>
</graphic>
</Button>
<Button text="%main.vaultDetail.missing.remove" minWidth="120" onAction="#didClickRemoveVault">
<graphic>
<FontAwesome5IconView glyph="TRASH"/>
</graphic>
</Button>
</VBox>
</children>
</VBox>

View File

@@ -0,0 +1,44 @@
<?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">
<children>
<VBox spacing="9" alignment="CENTER">
<StackPane alignment="CENTER">
<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>
</children>
</VBox>

View File

@@ -429,6 +429,13 @@ 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=VaultConfig is missing.
main.vaultDetail.missingVaultConfig.restore=Restore VaultConfig
### Missing Masterkey
main.vaultDetail.missingMasterkey.info=Masterkey is missing.
main.vaultDetail.missingMasterkey.restore=Restore Masterkey
### 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