mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-18 02:31:27 +00:00
implement core functionality
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user