Переглянути джерело

implement core functionality

Armin Schrenk 2 роки тому
батько
коміт
219ee0da9a

+ 97 - 1
src/main/java/org/cryptomator/ui/convertvault/HubToLocalConvertController.java

@@ -1,22 +1,66 @@
 package org.cryptomator.ui.convertvault;
 
+import com.google.common.base.Preconditions;
+import org.cryptomator.common.Passphrase;
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.cryptofs.CryptoFileSystemProperties;
+import org.cryptomator.cryptofs.CryptoFileSystemProvider;
+import org.cryptomator.cryptofs.common.BackupHelper;
+import org.cryptomator.cryptolib.api.CryptoException;
+import org.cryptomator.cryptolib.api.MasterkeyLoader;
+import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
+import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
 import org.cryptomator.ui.changepassword.NewPasswordController;
 import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.recoverykey.RecoveryKeyFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
+import javafx.application.Platform;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.StringProperty;
 import javafx.fxml.FXML;
 import javafx.stage.Stage;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutorService;
+
+import static org.cryptomator.common.Constants.DEFAULT_KEY_ID;
+import static org.cryptomator.common.Constants.MASTERKEY_BACKUP_SUFFIX;
+import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
+import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
 
 public class HubToLocalConvertController implements FxController {
 
+	private static final Logger LOG = LoggerFactory.getLogger(HubToLocalConvertController.class);
+
 	private final Stage window;
+	private final Vault vault;
+	private final StringProperty recoveryKey;
+	private final RecoveryKeyFactory recoveryKeyFactory;
+	private final MasterkeyFileAccess masterkeyFileAccess;
+	private final ExecutorService backgroundExecutorService;
+	private final BooleanProperty isConverting;
 
 	@FXML
 	NewPasswordController newPasswordController;
 
 	@Inject
-	public HubToLocalConvertController(@ConvertVaultWindow Stage window) {
+	public HubToLocalConvertController(@ConvertVaultWindow Stage window, @ConvertVaultWindow Vault vault, @ConvertVaultWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory, MasterkeyFileAccess masterkeyFileAccess, ExecutorService backgroundExecutorService) {
 		this.window = window;
+		this.vault = vault;
+		this.recoveryKey = recoveryKey;
+		this.recoveryKeyFactory = recoveryKeyFactory;
+		this.masterkeyFileAccess = masterkeyFileAccess;
+		this.backgroundExecutorService = backgroundExecutorService;
+		this.isConverting = new SimpleBooleanProperty(false);
 	}
 
 	@FXML
@@ -30,12 +74,64 @@ public class HubToLocalConvertController implements FxController {
 
 	@FXML
 	public void convert() {
+		Preconditions.checkState(newPasswordController.isGoodPassword());
+		LOG.info("Converting hub vault {} to local", vault.getPath());
+		CompletableFuture.runAsync(() -> isConverting.setValue(true), Platform::runLater) //
+				.thenRunAsync(this::convertInternal, backgroundExecutorService) //TODO: which executor is used?
+				.whenCompleteAsync((result, exception) -> {
+					isConverting.setValue(false);
+					if (exception == null) { //TODO: check, how the exceptions are wrapped
+						LOG.info("Conversion of vault {} succeeded.", vault.getPath());
+					} else {
+						LOG.error("Conversion of vault {} failed.", vault.getPath(), exception);
+					}
+				}, Platform::runLater); //
 		//window.setScene(resetPasswordScene.get());
 	}
 
+	//visible for testing
+	void convertInternal() throws CompletionException, IllegalArgumentException {
+		var passphrase = newPasswordController.getNewPassword();
+		try {
+			recoveryKeyFactory.newMasterkeyFileWithPassphrase(vault.getPath(), recoveryKey.get(), passphrase);
+			LOG.debug("Successfully created masterkey file for vault {}", vault.getPath());
+			backupHubConfig(vault.getPath().resolve(VAULTCONFIG_FILENAME));
+			replaceWithLocalConfig(passphrase);
+		} catch (MasterkeyLoadingFailedException e) {
+			throw new CompletionException(new IOException("Vault conversion failed", e));
+		} catch (IOException e) {
+			throw new CompletionException("Vault conversion failed", e);
+		} finally {
+			passphrase.destroy();
+		}
+	}
+
+	//visible for testing
+	void backupHubConfig(Path hubConfigPath) throws IOException {
+		byte[] hubConfigBytes = Files.readAllBytes(hubConfigPath);
+		Path backupPath = vault.getPath().resolve(VAULTCONFIG_FILENAME + BackupHelper.generateFileIdSuffix(hubConfigBytes) + MASTERKEY_BACKUP_SUFFIX);
+		Files.move(hubConfigPath, backupPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); //TODO: should this be an atomic move?
+		LOG.debug("Successfully created vault config backup {} for vault {}", backupPath.getFileName(), vault.getPath());
+	}
+
+	//visible for testing
+	void replaceWithLocalConfig(Passphrase passphrase) throws IOException, MasterkeyLoadingFailedException {
+		var unverifiedVaultConfig = vault.getVaultConfigCache().get();
+		try (var masterkey = masterkeyFileAccess.load(vault.getPath().resolve(MASTERKEY_FILENAME), passphrase)) {
+			var vaultConfig = unverifiedVaultConfig.verify(masterkey.getEncoded(), unverifiedVaultConfig.allegedVaultVersion());
+			MasterkeyLoader loader = ignored -> masterkey.copy();
+			CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
+					.withCipherCombo(vaultConfig.getCipherCombo()) //
+					.withKeyLoader(loader) //
+					.build();
+			CryptoFileSystemProvider.initialize(vault.getPath(), fsProps, DEFAULT_KEY_ID);
+		}
+	}
+
 	/* Getter/Setter */
 
 	public NewPasswordController getNewPasswordController() {
 		return newPasswordController;
 	}
+
 }

+ 140 - 0
src/test/java/org/cryptomator/ui/convertvault/HubToLocalConvertControllerTest.java

@@ -0,0 +1,140 @@
+package org.cryptomator.ui.convertvault;
+
+import org.cryptomator.common.Constants;
+import org.cryptomator.common.Passphrase;
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
+import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
+import org.cryptomator.ui.changepassword.NewPasswordController;
+import org.cryptomator.ui.recoverykey.RecoveryKeyFactory;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.Mockito;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.StringProperty;
+import javafx.stage.Stage;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.Optional;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutorService;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.times;
+
+public class HubToLocalConvertControllerTest {
+
+	Stage window;
+	Vault vault;
+	StringProperty recoveryKey;
+	RecoveryKeyFactory recoveryKeyFactory;
+	MasterkeyFileAccess masterkeyFileAccess;
+	ExecutorService backgroundExecutorService;
+	BooleanProperty isConverting;
+	NewPasswordController newPasswordController;
+
+	HubToLocalConvertController inTest;
+
+	@BeforeEach
+	public void beforeEach() {
+		window = Mockito.mock(Stage.class);
+		vault = Mockito.mock(Vault.class);
+		recoveryKey = Mockito.mock(StringProperty.class);
+		recoveryKeyFactory = Mockito.mock(RecoveryKeyFactory.class);
+		masterkeyFileAccess = Mockito.mock(MasterkeyFileAccess.class);
+		backgroundExecutorService = Mockito.mock(ExecutorService.class);
+		isConverting = Mockito.mock(BooleanProperty.class);
+		newPasswordController = Mockito.mock(NewPasswordController.class);
+		inTest = new HubToLocalConvertController(window, vault, recoveryKey, recoveryKeyFactory, masterkeyFileAccess, backgroundExecutorService);
+		inTest.newPasswordController = newPasswordController;
+	}
+
+	@Test
+	public void testBackupHubConfig(@TempDir Path tmpDir) throws IOException {
+		Path configPath = tmpDir.resolve(Constants.VAULTCONFIG_FILENAME);
+		Files.writeString(configPath, "hello config", StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+
+		Mockito.when(vault.getPath()).thenReturn(tmpDir);
+
+		inTest.backupHubConfig(configPath);
+		Optional<Path> result = Files.list(tmpDir).filter(p -> {
+			var fileName = p.getFileName().toString();
+			return fileName.startsWith(Constants.VAULTCONFIG_FILENAME) && fileName.endsWith(Constants.MASTERKEY_BACKUP_SUFFIX);
+		}).findAny();
+
+		Assertions.assertTrue(Files.notExists(configPath));
+		Assertions.assertTrue(result.isPresent());
+		Assertions.assertEquals("hello config", Files.readString(result.get(), StandardCharsets.UTF_8));
+	}
+
+	@Nested
+	class ConvertInternalTests {
+
+		Passphrase passphrase = Mockito.mock(Passphrase.class);
+		Path vaultPath = Mockito.mock(Path.class, "/vault/path");
+		Path configPath = Mockito.mock(Path.class, "/vault/path/config");
+		String actualRecoveryKey = "recoveryKey";
+		HubToLocalConvertController inSpy;
+
+		@BeforeEach
+		public void beforeEach() throws IOException {
+			inSpy = Mockito.spy(inTest);
+			Mockito.when(newPasswordController.getNewPassword()).thenReturn(passphrase);
+			Mockito.when(recoveryKey.get()).thenReturn(actualRecoveryKey);
+			Mockito.when(vault.getPath()).thenReturn(vaultPath);
+			Mockito.when(vaultPath.resolve(anyString())).thenReturn(configPath);
+			Mockito.doNothing().when(recoveryKeyFactory).newMasterkeyFileWithPassphrase(any(), anyString(), any());
+			Mockito.doNothing().when(inSpy).backupHubConfig(any());
+			Mockito.doNothing().when(inSpy).replaceWithLocalConfig(any());
+			Mockito.doNothing().when(passphrase).destroy();
+		}
+
+
+		@Test
+		public void testConvertInternal() throws IOException {
+			inSpy.convertInternal();
+
+			Mockito.verify(recoveryKeyFactory, times(1)).newMasterkeyFileWithPassphrase(vaultPath, actualRecoveryKey, passphrase);
+			Mockito.verify(inSpy, times(1)).backupHubConfig(configPath);
+			Mockito.verify(inSpy, times(1)).replaceWithLocalConfig(passphrase);
+			Mockito.verify(passphrase, times(1)).destroy();
+		}
+
+		@Test
+		public void testConvertInternalWrapsCryptoException() throws IOException {
+			Mockito.doThrow(new MasterkeyLoadingFailedException("yadda")).when(inSpy).replaceWithLocalConfig(any());
+
+			Assertions.assertThrows(CompletionException.class, inSpy::convertInternal);
+
+			Mockito.verify(passphrase, times(1)).destroy();
+		}
+
+		@Test
+		public void testConvertInternalWrapsIOException() throws IOException {
+			Mockito.doThrow(new IOException("yudu")).when(inSpy).backupHubConfig(any());
+
+			Assertions.assertThrows(CompletionException.class, inSpy::convertInternal);
+
+			Mockito.verify(passphrase, times(1)).destroy();
+		}
+
+		@Test
+		public void testConvertInternalNotWrapsIAE() throws IOException {
+			Mockito.doThrow(new IllegalArgumentException("yudu")).when(recoveryKeyFactory).newMasterkeyFileWithPassphrase(any(),anyString(),any());
+
+			Assertions.assertThrows(IllegalArgumentException.class, inSpy::convertInternal);
+
+			Mockito.verify(passphrase, times(1)).destroy();
+		}
+	}
+
+
+}