if linked hub vault is alread present unlock/reveal

Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
This commit is contained in:
Armin Schrenk
2026-08-31 18:59:34 +02:00
parent d2b1f86d85
commit 4b6afb5fd6
4 changed files with 181 additions and 7 deletions
@@ -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<Vault> vaults;
private final VaultService vaultService;
private final Stage primaryStage;
private final Dialogs dialogs;
@Inject
public AppLaunchEventHandler(@Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue, ExecutorService executorService, FxApplicationWindows appWindows, VaultListManager vaultListManager, VaultService vaultService, @PrimaryStage Stage primaryStage, Dialogs dialogs) {
public AppLaunchEventHandler(@Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue, ExecutorService executorService, FxApplicationWindows appWindows, VaultListManager vaultListManager, ObservableList<Vault> 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();
}
}
@@ -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.
* <p>
* 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.
* <p>
* 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<Vault> findByVaultId(Collection<Vault> 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());
}
}
@@ -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
@@ -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;
}
}