diff --git a/src/main/java/org/cryptomator/ui/fxapp/AppLaunchEventHandler.java b/src/main/java/org/cryptomator/ui/fxapp/AppLaunchEventHandler.java index 546107b7a..8ace286c1 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/AppLaunchEventHandler.java +++ b/src/main/java/org/cryptomator/ui/fxapp/AppLaunchEventHandler.java @@ -9,12 +9,14 @@ import org.cryptomator.launcher.OpenHubVaultEvent; import org.cryptomator.launcher.RevealRunningEvent; import org.cryptomator.ui.common.VaultService; import org.cryptomator.ui.dialogs.Dialogs; +import org.cryptomator.ui.keyloading.hub.HubVaults; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Named; import javafx.application.Platform; +import javafx.collections.ObservableList; import javafx.stage.Stage; import java.io.IOException; import java.nio.file.Path; @@ -34,16 +36,18 @@ class AppLaunchEventHandler { private final ExecutorService executorService; private final FxApplicationWindows appWindows; private final VaultListManager vaultListManager; + private final ObservableList vaults; private final VaultService vaultService; private final Stage primaryStage; private final Dialogs dialogs; @Inject - public AppLaunchEventHandler(@Named("launchEventQueue") BlockingQueue launchEventQueue, ExecutorService executorService, FxApplicationWindows appWindows, VaultListManager vaultListManager, VaultService vaultService, @PrimaryStage Stage primaryStage, Dialogs dialogs) { + public AppLaunchEventHandler(@Named("launchEventQueue") BlockingQueue launchEventQueue, ExecutorService executorService, FxApplicationWindows appWindows, VaultListManager vaultListManager, ObservableList vaults, VaultService vaultService, @PrimaryStage Stage primaryStage, Dialogs dialogs) { this.launchEventQueue = launchEventQueue; this.executorService = executorService; this.appWindows = appWindows; this.vaultListManager = vaultListManager; + this.vaults = vaults; this.vaultService = vaultService; this.primaryStage = primaryStage; this.dialogs = dialogs; @@ -69,11 +73,29 @@ class AppLaunchEventHandler { switch (event) { case RevealRunningEvent _ -> appWindows.showMainWindow(); case OpenFileEvent openFileEvent -> openFileEvent.pathsToOpen().forEach(this::openPotentialVault); - // TODO: show the hub vault open flow, see docs/hub-vault-open-deeplink-plan.md - case OpenHubVaultEvent openHubVaultEvent -> { - LOG.info("Received request to open hub vault {}.", openHubVaultEvent.vaultId()); - appWindows.showMainWindow(); - } + case OpenHubVaultEvent openHubVaultEvent -> openHubVault(openHubVaultEvent); + } + } + + /** + * Whether a hub vault is set up on this machine is a purely local question - hub manages the vault's key, not where + * it lives. Only if it is not set up here do we need to ask hub about it. + */ + private void openHubVault(OpenHubVaultEvent event) { + var existing = HubVaults.findByVaultId(vaults, event.vaultId()); + if (existing.isPresent()) { + var vault = existing.get(); + Platform.runLater(() -> { + if (vault.isUnlocked()) { + vaultService.reveal(vault); + } else if (vault.isLocked()) { + appWindows.startUnlockWorkflow(vault, null); + } + }); + } else { + //TODO: authenticate, ask hub for the vault's details and offer to add it, see docs/hub-vault-open-deeplink-plan.md + LOG.info("Hub vault {} is not set up on this machine.", event.vaultId()); + appWindows.showMainWindow(); } } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubVaults.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubVaults.java new file mode 100644 index 000000000..c1ee5186b --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubVaults.java @@ -0,0 +1,59 @@ +package org.cryptomator.ui.keyloading.hub; + +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.cryptofs.VaultConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Collection; +import java.util.Optional; +import java.util.UUID; + +/** + * Locates Hub vaults among the vaults set up on this machine. + *

+ * Hub manages a vault's key, not where it lives or how it is laid out, so the local vault list is the only place that + * can answer whether a given Hub vault is already set up here. + */ +public final class HubVaults { + + private static final Logger LOG = LoggerFactory.getLogger(HubVaults.class); + + private HubVaults() { + } + + /** + * Finds the Hub vault with the given id. + *

+ * A vault whose config cannot be read - e.g. because it sits on storage that is currently unavailable - is skipped + * rather than failing the lookup: one unreachable vault must not prevent finding a different one. + * + * @param vaults the vaults set up on this machine + * @param hubVaultId the vault's id within its Hub instance + * @return the local vault, or empty if none of them is that Hub vault + */ + public static Optional findByVaultId(Collection vaults, UUID hubVaultId) { + return vaults.stream() // + .filter(vault -> hasVaultId(vault, hubVaultId)) // + .findAny(); + } + + private static boolean hasVaultId(Vault vault, UUID hubVaultId) { + try { + return hasVaultId(vault.getVaultConfigCache().get(), hubVaultId); + } catch (IOException e) { + LOG.debug("Skipping vault {} while looking for hub vault {}, its config is not readable.", vault.getPath(), hubVaultId); + return false; + } + } + + private static boolean hasVaultId(VaultConfig.UnverifiedVaultConfig config, UUID hubVaultId) { + var keyIdScheme = config.getKeyId().getScheme(); + if (keyIdScheme == null || !keyIdScheme.startsWith(HubKeyLoadingStrategy.SCHEME_PREFIX)) { + return false; //not a hub vault, so it cannot be the one we are looking for + } + return hubVaultId.toString().equalsIgnoreCase(config.allegedVaultId()); + } + +} diff --git a/src/test/java/org/cryptomator/ui/fxapp/AppLaunchEventHandlerTest.java b/src/test/java/org/cryptomator/ui/fxapp/AppLaunchEventHandlerTest.java index f5994c105..6693ee3c6 100644 --- a/src/test/java/org/cryptomator/ui/fxapp/AppLaunchEventHandlerTest.java +++ b/src/test/java/org/cryptomator/ui/fxapp/AppLaunchEventHandlerTest.java @@ -10,6 +10,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import javafx.collections.FXCollections; import javafx.stage.Stage; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; @@ -32,7 +33,7 @@ public class AppLaunchEventHandlerTest { queue = new LinkedBlockingQueue<>(); executor = Executors.newSingleThreadExecutor(); appWindows = mock(FxApplicationWindows.class); - handler = new AppLaunchEventHandler(queue, executor, appWindows, mock(VaultListManager.class), mock(VaultService.class), mock(Stage.class), mock(Dialogs.class)); + handler = new AppLaunchEventHandler(queue, executor, appWindows, mock(VaultListManager.class), FXCollections.observableArrayList(), mock(VaultService.class), mock(Stage.class), mock(Dialogs.class)); } @AfterEach diff --git a/src/test/java/org/cryptomator/ui/keyloading/hub/HubVaultsTest.java b/src/test/java/org/cryptomator/ui/keyloading/hub/HubVaultsTest.java new file mode 100644 index 000000000..9555046bf --- /dev/null +++ b/src/test/java/org/cryptomator/ui/keyloading/hub/HubVaultsTest.java @@ -0,0 +1,92 @@ +package org.cryptomator.ui.keyloading.hub; + +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.common.vaults.VaultConfigCache; +import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URI; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class HubVaultsTest { + + private static final UUID VAULT_ID = UUID.fromString("d3a1f0b2-7c4e-4a1d-9f3b-2e5c6a7b8c9d"); + private static final UUID OTHER_VAULT_ID = UUID.fromString("11111111-2222-3333-4444-555555555555"); + + @Test + @DisplayName("the hub vault with the requested id is found") + public void testFindsMatchingVault() throws IOException { + var wanted = hubVault(VAULT_ID); + var vaults = List.of(hubVault(OTHER_VAULT_ID), wanted); + + Assertions.assertEquals(Optional.of(wanted), HubVaults.findByVaultId(vaults, VAULT_ID)); + } + + @Test + @DisplayName("no vault with the requested id yields empty") + public void testNoMatch() throws IOException { + var vaults = List.of(hubVault(OTHER_VAULT_ID)); + + Assertions.assertEquals(Optional.empty(), HubVaults.findByVaultId(vaults, VAULT_ID)); + } + + @Test + @DisplayName("a password vault never matches, even carrying the same id") + public void testIgnoresPasswordVault() throws IOException { + var vaults = List.of(vault("masterkeyfile:masterkey.cryptomator", VAULT_ID)); + + Assertions.assertEquals(Optional.empty(), HubVaults.findByVaultId(vaults, VAULT_ID)); + } + + @Test + @DisplayName("a vault whose config cannot be read is skipped, not fatal") + public void testSkipsUnreadableVault() throws IOException { + // e.g. a vault on a network drive that is currently offline - it must not hide a vault further down the list + var wanted = hubVault(VAULT_ID); + var vaults = List.of(unreadableVault(), wanted); + + Assertions.assertEquals(Optional.of(wanted), HubVaults.findByVaultId(vaults, VAULT_ID)); + } + + + // setup/mock provider + + private static Vault hubVault(UUID vaultId) throws IOException { + return vault("hub+https://hub.example.com/api/vaults/" + vaultId, vaultId); + } + + private static Vault vault(String keyId, UUID vaultId) throws IOException { + var configCache = mock(VaultConfigCache.class); + var config = mockConfig(keyId, vaultId); + when(configCache.get()).thenReturn(config); + return vaultWith(configCache); + } + + private static Vault unreadableVault() throws IOException { + var configCache = mock(VaultConfigCache.class); + when(configCache.get()).thenThrow(new IOException("vault directory unavailable")); + return vaultWith(configCache); + } + + private static Vault vaultWith(VaultConfigCache configCache) { + var vault = mock(Vault.class); + when(vault.getVaultConfigCache()).thenReturn(configCache); + return vault; + } + + private static UnverifiedVaultConfig mockConfig(String keyId, UUID vaultId) { + var mock = mock(UnverifiedVaultConfig.class); + when(mock.getKeyId()).thenReturn(URI.create(keyId)); + when(mock.allegedVaultId()).thenReturn(vaultId.toString()); + return mock; + } + +}