improved vault upgrading, preparation for migration to vault version 4

This commit is contained in:
Sebastian Stenzel
2016-06-30 22:09:45 +02:00
parent 07ba4eb537
commit 1468c6ec90
14 changed files with 277 additions and 89 deletions

View File

@@ -11,28 +11,28 @@ package org.cryptomator.crypto.engine;
public class UnsupportedVaultFormatException extends CryptoException {
private final Integer detectedVersion;
private final Integer supportedVersion;
private final Integer latestSupportedVersion;
public UnsupportedVaultFormatException(Integer detectedVersion, Integer supportedVersion) {
super("Tried to open vault of version " + detectedVersion + ", but can only handle version " + supportedVersion);
public UnsupportedVaultFormatException(Integer detectedVersion, Integer latestSupportedVersion) {
super("Tried to open vault of version " + detectedVersion + ", latest supported version is " + latestSupportedVersion);
this.detectedVersion = detectedVersion;
this.supportedVersion = supportedVersion;
this.latestSupportedVersion = latestSupportedVersion;
}
public Integer getDetectedVersion() {
return detectedVersion;
}
public Integer getSupportedVersion() {
return supportedVersion;
public Integer getLatestSupportedVersion() {
return latestSupportedVersion;
}
public boolean isVaultOlderThanSoftware() {
return detectedVersion == null || detectedVersion < supportedVersion;
return detectedVersion == null || detectedVersion < latestSupportedVersion;
}
public boolean isSoftwareOlderThanVault() {
return detectedVersion > supportedVersion;
return detectedVersion > latestSupportedVersion;
}
}

View File

@@ -1,10 +1,15 @@
package org.cryptomator.crypto.engine.impl;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
public final class Constants {
private Constants() {
}
static final Collection<Integer> SUPPORTED_VAULT_VERSIONS = Collections.unmodifiableCollection(Arrays.asList(3, 4));
static final Integer CURRENT_VAULT_VERSION = 4;
public static final int PAYLOAD_SIZE = 32 * 1024;

View File

@@ -9,6 +9,7 @@
package org.cryptomator.crypto.engine.impl;
import static org.cryptomator.crypto.engine.impl.Constants.CURRENT_VAULT_VERSION;
import static org.cryptomator.crypto.engine.impl.Constants.SUPPORTED_VAULT_VERSIONS;
import java.io.IOException;
import java.nio.ByteBuffer;
@@ -109,7 +110,7 @@ class CryptorImpl implements Cryptor {
assert keyFile != null;
// check version
if (!CURRENT_VAULT_VERSION.equals(keyFile.getVersion())) {
if (!SUPPORTED_VAULT_VERSIONS.contains(keyFile.getVersion())) {
throw new UnsupportedVaultFormatException(keyFile.getVersion(), CURRENT_VAULT_VERSION);
}

View File

@@ -8,8 +8,8 @@ public final class Constants {
static final String DATA_ROOT_DIR = "d";
static final String ROOT_DIRECOTRY_ID = "";
static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup";
public static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
public static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup";
static final String DIR_PREFIX = "0";

View File

@@ -26,6 +26,7 @@ import javax.inject.Singleton;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.controls.DirectoryListCell;
import org.cryptomator.ui.model.UpgradeStrategies;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.model.VaultFactory;
import org.cryptomator.ui.settings.Localization;
@@ -78,6 +79,7 @@ public class MainController extends LocalizedFXMLViewController {
private final Provider<UnlockedController> unlockedControllerProvider;
private final Lazy<ChangePasswordController> changePasswordController;
private final Lazy<SettingsController> settingsController;
private final Lazy<UpgradeStrategies> upgradeStrategies;
private final ObjectProperty<AbstractFXMLViewController> activeController = new SimpleObjectProperty<>();
private final ObservableList<Vault> vaults;
private final ObjectProperty<Vault> selectedVault = new SimpleObjectProperty<>();
@@ -90,7 +92,7 @@ public class MainController extends LocalizedFXMLViewController {
@Inject
public MainController(@Named("mainWindow") Stage mainWindow, Localization localization, Settings settings, VaultFactory vaultFactoy, Lazy<WelcomeController> welcomeController,
Lazy<InitializeController> initializeController, Lazy<NotFoundController> notFoundController, Lazy<UpgradeController> upgradeController, Lazy<UnlockController> unlockController,
Provider<UnlockedController> unlockedControllerProvider, Lazy<ChangePasswordController> changePasswordController, Lazy<SettingsController> settingsController) {
Provider<UnlockedController> unlockedControllerProvider, Lazy<ChangePasswordController> changePasswordController, Lazy<SettingsController> settingsController, Lazy<UpgradeStrategies> upgradeStrategies) {
super(localization);
this.mainWindow = mainWindow;
this.vaultFactoy = vaultFactoy;
@@ -102,6 +104,7 @@ public class MainController extends LocalizedFXMLViewController {
this.unlockedControllerProvider = unlockedControllerProvider;
this.changePasswordController = changePasswordController;
this.settingsController = settingsController;
this.upgradeStrategies = upgradeStrategies;
this.vaults = FXCollections.observableList(settings.getDirectories());
this.vaults.addListener((Change<? extends Vault> c) -> {
settings.save();
@@ -288,7 +291,7 @@ public class MainController extends LocalizedFXMLViewController {
this.showUnlockedView(newValue);
} else if (!newValue.doesVaultDirectoryExist()) {
this.showNotFoundView();
} else if (newValue.isValidVaultDirectory() && newValue.needsUpgrade()) {
} else if (newValue.isValidVaultDirectory() && upgradeStrategies.get().getUpgradeStrategy(newValue).isPresent()) {
this.showUpgradeView();
} else if (newValue.isValidVaultDirectory()) {
this.showUnlockView();

View File

@@ -7,8 +7,10 @@ import java.util.concurrent.ExecutorService;
import javax.inject.Inject;
import org.cryptomator.ui.model.UpgradeInstruction;
import org.cryptomator.ui.model.UpgradeInstruction.UpgradeFailedException;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.UpgradeStrategies;
import org.cryptomator.ui.model.UpgradeStrategy;
import org.cryptomator.ui.model.UpgradeStrategy.UpgradeFailedException;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.settings.Localization;
import org.fxmisc.easybind.EasyBind;
@@ -16,7 +18,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Platform;
import javafx.beans.binding.Binding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
@@ -30,19 +31,24 @@ public class UpgradeController extends LocalizedFXMLViewController {
private static final Logger LOG = LoggerFactory.getLogger(UpgradeController.class);
final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
final ObjectProperty<Optional<UpgradeStrategy>> strategy = new SimpleObjectProperty<>();
private final UpgradeStrategies strategies;
private final ExecutorService exec;
private final Binding<Optional<UpgradeInstruction>> upgradeInstruction = EasyBind.monadic(vault).map(Vault::availableUpgrade);
private Optional<UpgradeListener> listener = Optional.empty();
@Inject
public UpgradeController(Localization localization, ExecutorService exec) {
public UpgradeController(Localization localization, UpgradeStrategies strategies, ExecutorService exec) {
super(localization);
this.strategies = strategies;
this.exec = exec;
}
@FXML
private Label upgradeLabel;
@FXML
private SecPasswordField passwordField;
@FXML
private Button upgradeButton;
@@ -54,10 +60,12 @@ public class UpgradeController extends LocalizedFXMLViewController {
@Override
protected void initialize() {
upgradeLabel.textProperty().bind(EasyBind.monadic(upgradeInstruction).map(instruction -> {
upgradeLabel.textProperty().bind(EasyBind.monadic(strategy).map(instruction -> {
return instruction.map(this::upgradeNotification).orElse("");
}).orElse(""));
upgradeButton.disableProperty().bind(passwordField.textProperty().isEmpty().or(passwordField.disabledProperty()));
EasyBind.subscribe(vault, this::vaultDidChange);
}
@@ -68,14 +76,15 @@ public class UpgradeController extends LocalizedFXMLViewController {
private void vaultDidChange(Vault newVault) {
errorLabel.setText(null);
strategy.set(strategies.getUpgradeStrategy(newVault));
}
// ****************************************
// Upgrade label
// ****************************************
private String upgradeNotification(UpgradeInstruction instruction) {
return instruction.getNotification(vault.get(), localization);
private String upgradeNotification(UpgradeStrategy instruction) {
return instruction.getNotification(vault.get());
}
// ****************************************
@@ -84,36 +93,45 @@ public class UpgradeController extends LocalizedFXMLViewController {
@FXML
private void didClickUpgradeButton(ActionEvent event) {
upgradeInstruction.getValue().ifPresent(this::upgrade);
strategy.getValue().ifPresent(this::upgrade);
}
private void upgrade(UpgradeInstruction instruction) {
Vault v = vault.getValue();
Objects.requireNonNull(v);
private void upgrade(UpgradeStrategy instruction) {
Vault v = Objects.requireNonNull(vault.getValue());
passwordField.setDisable(true);
progressIndicator.setVisible(true);
upgradeButton.setDisable(true);
exec.submit(() -> {
if (!instruction.isApplicable(v)) {
LOG.error("No upgrade needed for " + v.path().getValue());
throw new IllegalStateException("No ugprade needed for " + v.path().getValue());
}
try {
instruction.upgrade(v, localization);
Platform.runLater(() -> {
progressIndicator.setVisible(false);
upgradeButton.setDisable(false);
listener.ifPresent(UpgradeListener::didUpgrade);
});
instruction.upgrade(v, passwordField.getCharacters());
Platform.runLater(this::showNextUpgrade);
} catch (UpgradeFailedException e) {
Platform.runLater(() -> {
errorLabel.setText(e.getLocalizedMessage());
});
} finally {
Platform.runLater(() -> {
progressIndicator.setVisible(false);
upgradeButton.setDisable(false);
passwordField.setDisable(false);
passwordField.swipe();
});
}
});
}
private void showNextUpgrade() {
errorLabel.setText(null);
Optional<UpgradeStrategy> nextStrategy = strategies.getUpgradeStrategy(vault.getValue());
if (nextStrategy.isPresent()) {
strategy.set(nextStrategy);
} else {
listener.ifPresent(UpgradeListener::didUpgrade);
}
}
/* callback */
public void setListener(UpgradeListener listener) {

View File

@@ -1,40 +0,0 @@
package org.cryptomator.ui.model;
import org.cryptomator.ui.settings.Localization;
public interface UpgradeInstruction {
static UpgradeInstruction[] AVAILABLE_INSTRUCTIONS = {new UpgradeVersion3DropBundleExtension()};
/**
* @return Localized string to display to the user when an upgrade is needed.
*/
String getNotification(Vault vault, Localization localization);
/**
* Upgrades a vault. Might take a moment, should be run in a background thread.
*/
void upgrade(Vault vault, Localization localization) throws UpgradeFailedException;
/**
* Determines in O(1), if an upgrade can be applied to a vault.
*
* @return <code>true</code> if and only if the vault can be migrated to a newer version without the risk of data losses.
*/
boolean isApplicable(Vault vault);
/**
* Thrown when data migration failed.
*/
public class UpgradeFailedException extends Exception {
UpgradeFailedException() {
}
UpgradeFailedException(String message) {
super(message);
}
}
}

View File

@@ -0,0 +1,27 @@
package org.cryptomator.ui.model;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class UpgradeStrategies {
private final Collection<UpgradeStrategy> strategies;
@Inject
public UpgradeStrategies(UpgradeVersion3DropBundleExtension upgrader1, UpgradeVersion3to4 upgrader2) {
strategies = Collections.unmodifiableList(Arrays.asList(upgrader1, upgrader2));
}
public Optional<UpgradeStrategy> getUpgradeStrategy(Vault vault) {
return strategies.stream().filter(strategy -> {
return strategy.isApplicable(vault);
}).findFirst();
}
}

View File

@@ -0,0 +1,87 @@
package org.cryptomator.ui.model;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import javax.inject.Provider;
import org.cryptomator.crypto.engine.Cryptor;
import org.cryptomator.crypto.engine.InvalidPassphraseException;
import org.cryptomator.crypto.engine.UnsupportedVaultFormatException;
import org.cryptomator.filesystem.crypto.Constants;
import org.cryptomator.ui.settings.Localization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class UpgradeStrategy {
private static final Logger LOG = LoggerFactory.getLogger(UpgradeStrategy.class);
protected final Provider<Cryptor> cryptorProvider;
protected final Localization localization;
UpgradeStrategy(Provider<Cryptor> cryptorProvider, Localization localization) {
this.cryptorProvider = cryptorProvider;
this.localization = localization;
}
/**
* @return Localized string to display to the user when an upgrade is needed.
*/
public abstract String getNotification(Vault vault);
/**
* Upgrades a vault. Might take a moment, should be run in a background thread.
*/
public void upgrade(Vault vault, CharSequence passphrase) throws UpgradeFailedException {
final Cryptor cryptor = cryptorProvider.get();
try {
final Path masterkeyFile = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME);
final byte[] masterkeyFileContents = Files.readAllBytes(masterkeyFile);
cryptor.readKeysFromMasterkeyFile(masterkeyFileContents, passphrase);
// create backup, as soon as we know the password was correct:
final Path masterkeyBackupFile = vault.path().getValue().resolve(Constants.MASTERKEY_BACKUP_FILENAME);
Files.copy(masterkeyFile, masterkeyBackupFile, StandardCopyOption.REPLACE_EXISTING);
// do stuff:
upgrade(vault, cryptor);
// write updated masterkey file:
final byte[] upgradedMasterkeyFileContents = cryptor.writeKeysToMasterkeyFile(passphrase);
final Path masterkeyFileAfterUpgrading = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME); // path may have changed
Files.write(masterkeyFileAfterUpgrading, upgradedMasterkeyFileContents, StandardOpenOption.TRUNCATE_EXISTING);
} catch (InvalidPassphraseException e) {
throw new UpgradeFailedException(localization.getString("unlock.errorMessage.wrongPassword"));
} catch (IOException | UnsupportedVaultFormatException e) {
LOG.warn("Upgrade failed.", e);
throw new UpgradeFailedException("Upgrade failed. Details in log message.");
} finally {
cryptor.destroy();
}
}
protected abstract void upgrade(Vault vault, Cryptor cryptor) throws UpgradeFailedException;
/**
* Determines in O(1), if an upgrade can be applied to a vault.
*
* @return <code>true</code> if and only if the vault can be migrated to a newer version without the risk of data losses.
*/
public abstract boolean isApplicable(Vault vault);
/**
* Thrown when data migration failed.
*/
public class UpgradeFailedException extends Exception {
UpgradeFailedException() {
}
UpgradeFailedException(String message) {
super(message);
}
}
}

View File

@@ -4,19 +4,36 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.engine.Cryptor;
import org.cryptomator.crypto.engine.InvalidPassphraseException;
import org.cryptomator.crypto.engine.UnsupportedVaultFormatException;
import org.cryptomator.filesystem.crypto.Constants;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.settings.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Platform;
class UpgradeVersion3DropBundleExtension implements UpgradeInstruction {
@Singleton
class UpgradeVersion3DropBundleExtension extends UpgradeStrategy {
private static final Logger LOG = LoggerFactory.getLogger(UpgradeVersion3DropBundleExtension.class);
private final Settings settings;
@Inject
public UpgradeVersion3DropBundleExtension(Provider<Cryptor> cryptorProvider, Localization localization, Settings settings) {
super(cryptorProvider, localization);
this.settings = settings;
}
@Override
public String getNotification(Vault vault, Localization localization) {
public String getNotification(Vault vault) {
String fmt = localization.getString("upgrade.version3dropBundleExtension.msg");
Path path = vault.path().getValue();
String oldVaultName = path.getFileName().toString();
@@ -25,7 +42,26 @@ class UpgradeVersion3DropBundleExtension implements UpgradeInstruction {
}
@Override
public void upgrade(Vault vault, Localization localization) throws UpgradeFailedException {
public void upgrade(Vault vault, CharSequence passphrase) throws UpgradeFailedException {
final Cryptor cryptor = cryptorProvider.get();
try {
final Path masterkeyFile = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME);
final byte[] masterkeyFileContents = Files.readAllBytes(masterkeyFile);
cryptor.readKeysFromMasterkeyFile(masterkeyFileContents, passphrase);
upgrade(vault, cryptor);
// don't write new masterkey. this is a special case, as we were stupid and didn't increase the vault version with this upgrade...
} catch (InvalidPassphraseException e) {
throw new UpgradeFailedException(localization.getString("unlock.errorMessage.wrongPassword"));
} catch (IOException | UnsupportedVaultFormatException e) {
LOG.warn("Upgrade failed.", e);
throw new UpgradeFailedException("Upgrade failed. Details in log message.");
} finally {
cryptor.destroy();
}
}
@Override
protected void upgrade(Vault vault, Cryptor cryptor) throws UpgradeFailedException {
Path path = vault.path().getValue();
String oldVaultName = path.getFileName().toString();
String newVaultName = StringUtils.removeEnd(oldVaultName, Vault.VAULT_FILE_EXTENSION);
@@ -39,6 +75,7 @@ class UpgradeVersion3DropBundleExtension implements UpgradeInstruction {
Files.move(path, path.resolveSibling(newVaultName));
Platform.runLater(() -> {
vault.setPath(newPath);
settings.save();
});
} catch (IOException e) {
LOG.error("Vault migration failed", e);

View File

@@ -0,0 +1,55 @@
package org.cryptomator.ui.model;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.cryptomator.crypto.engine.Cryptor;
import org.cryptomator.filesystem.crypto.Constants;
import org.cryptomator.ui.settings.Localization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
class UpgradeVersion3to4 extends UpgradeStrategy {
private static final Logger LOG = LoggerFactory.getLogger(UpgradeVersion3to4.class);
@Inject
public UpgradeVersion3to4(Provider<Cryptor> cryptorProvider, Localization localization) {
super(cryptorProvider, localization);
}
@Override
public String getNotification(Vault vault) {
return localization.getString("upgrade.version3to4.msg");
}
@Override
protected void upgrade(Vault vault, Cryptor cryptor) throws UpgradeFailedException {
throw new UpgradeFailedException("not yet implemented");
}
@Override
public boolean isApplicable(Vault vault) {
final Path masterkeyFile = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME);
try {
if (Files.isRegularFile(masterkeyFile)) {
final String keyContents = new String(Files.readAllBytes(masterkeyFile), StandardCharsets.UTF_8);
return keyContents.contains("\"version\":3") || keyContents.contains("\"version\": 3");
} else {
LOG.warn("Not a file: {}", masterkeyFile);
return false;
}
} catch (IOException e) {
LOG.warn("Could not determine, whether upgrade is applicable.", e);
return false;
}
}
}

View File

@@ -16,7 +16,6 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.Normalizer;
import java.text.Normalizer.Form;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
@@ -64,9 +63,9 @@ public class Vault implements CryptoFileSystemDelegate {
public static final String VAULT_FILE_EXTENSION = ".cryptomator";
private final ObjectProperty<Path> path;
private final DeferredCloser closer;
private final ShorteningFileSystemFactory shorteningFileSystemFactory;
private final CryptoFileSystemFactory cryptoFileSystemFactory;
private final DeferredCloser closer;
private final BooleanProperty unlocked = new SimpleBooleanProperty();
private final ObservableList<String> namesOfResourcesWithInvalidMac = FXThreads.observableListOnMainThread(FXCollections.observableArrayList());
private final Set<String> whitelistedResourcesWithInvalidMac = new HashSet<>();
@@ -82,9 +81,9 @@ public class Vault implements CryptoFileSystemDelegate {
*/
Vault(Path vaultDirectoryPath, ShorteningFileSystemFactory shorteningFileSystemFactory, CryptoFileSystemFactory cryptoFileSystemFactory, DeferredCloser closer) {
this.path = new SimpleObjectProperty<Path>(vaultDirectoryPath);
this.closer = closer;
this.shorteningFileSystemFactory = shorteningFileSystemFactory;
this.cryptoFileSystemFactory = cryptoFileSystemFactory;
this.closer = closer;
try {
setMountName(name().getValue());
@@ -167,16 +166,6 @@ public class Vault implements CryptoFileSystemDelegate {
Optionals.ifPresent(filesystemFrontend.get(), Frontend::unmount);
}
public boolean needsUpgrade() {
return availableUpgrade().isPresent();
}
public Optional<UpgradeInstruction> availableUpgrade() {
return Arrays.stream(UpgradeInstruction.AVAILABLE_INSTRUCTIONS).filter(instruction -> {
return instruction.isApplicable(this);
}).findAny();
}
// ******************************************************************************
// Delegate methods
// ********************************************************************************/

View File

@@ -12,10 +12,13 @@
<?import javafx.scene.control.ProgressIndicator?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.VBox?>
<?import org.cryptomator.ui.controls.SecPasswordField?>
<VBox prefWidth="400.0" prefHeight="400.0" spacing="24.0" alignment="CENTER" xmlns:fx="http://javafx.com/fxml" cacheShape="true" cache="true">
<Label fx:id="upgradeLabel" textAlignment="CENTER" wrapText="true"/>
<SecPasswordField fx:id="passwordField" prefWidth="300.0" cacheShape="true" cache="true" />
<Button fx:id="upgradeButton" text="%upgrade.button" prefWidth="150.0" onAction="#didClickUpgradeButton" cacheShape="true" cache="true" />

View File

@@ -43,6 +43,9 @@ upgrade.button=Upgrade vault
upgrade.version3dropBundleExtension.msg=This vault needs to be migrated to a newer format.\n"%1$s" will be renamed to "%2$s".\nPlease make sure synchronization has finished before proceeding.
upgrade.version3dropBundleExtension.err.alreadyExists=Automatic migration failed.\n"%s" already exists.
upgrade.version3to4.msg=This vault needs to be migrated to a newer format.\nEncrypted folder names will be updated.\nPlease make sure synchronization has finished before proceeding.
# unlock.fxml
unlock.label.password=Password
unlock.label.mountName=Drive name