diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java index 582bf27dd..c3b53f7e1 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java @@ -10,6 +10,8 @@ import org.cryptomator.ui.dialogs.SimpleDialog; import org.cryptomator.ui.error.ErrorComponent; import org.cryptomator.ui.eventview.EventViewComponent; import org.cryptomator.ui.importtemplate.ImportTemplateComponent; +import org.cryptomator.ui.importtemplate.MalformedTemplateException; +import org.cryptomator.ui.importtemplate.VaultTemplate; import org.cryptomator.ui.lock.LockComponent; import org.cryptomator.ui.mainwindow.MainWindowComponent; import org.cryptomator.ui.notification.NotificationComponent; @@ -38,6 +40,7 @@ import java.awt.desktop.AppReopenedListener; import java.awt.desktop.QuitResponse; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutorService; import java.util.function.Supplier; @@ -148,12 +151,51 @@ public class FxApplicationWindows { CompletableFuture.runAsync(() -> shareVaultWindow.create(vault).showShareVaultWindow(), Platform::runLater); } + /** + * Shows the import dialog for the given vault template. + *
+ * The archive is unpacked and validated up front on a background thread, so a template that could never be imported
+ * is rejected before the user is asked to choose a storage location - in which case the returned stage is the main
+ * window, having shown an error dialog. On success the unpacked template is handed to the import window, which owns
+ * it from then on and discards it when it closes.
+ */
public CompletionStage
+ * Callers distinguish the two to decide what to show the user: a malformed template is a dead end, whereas a
+ * destination problem is recoverable by picking a different location.
+ */
+public class MalformedTemplateException extends IOException {
+
+ private static final long serialVersionUID = 1L;
+
+ public MalformedTemplateException(String message) {
+ super(message);
+ }
+
+ public MalformedTemplateException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/org/cryptomator/ui/importtemplate/VaultTemplate.java b/src/main/java/org/cryptomator/ui/importtemplate/VaultTemplate.java
new file mode 100644
index 000000000..a0600671a
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/importtemplate/VaultTemplate.java
@@ -0,0 +1,150 @@
+package org.cryptomator.ui.importtemplate;
+
+import org.cryptomator.common.Constants;
+import org.cryptomator.cryptofs.VaultConfig;
+import org.cryptomator.cryptofs.VaultConfigLoadException;
+import org.cryptomator.ui.keyloading.hub.HubConfig;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.VisibleForTesting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+
+/**
+ * A vault template that has been unpacked to a temporary directory and found sound.
+ *
+ * Holding an instance means the template can be imported: it was a readable ZIP within the entry and size
+ * limits, held exactly one decodable {@value Constants#VAULTCONFIG_FILENAME}, and no entry escaped the extraction
+ * directory. Unpacking therefore happens once, when the deeplink arrives, so a template that could never be imported is
+ * rejected before the user is asked to choose a storage location - and {@link #moveTo(Path)} is left with nothing to
+ * validate but the destination.
+ *
+ * The instance owns a temporary directory and must be {@link #close() closed}, whether or not the
+ * import completes.
+ *
+ * {@link #hubUrl()} is read from the unverified vault config: its signature is keyed on the masterkey, which is
+ * only obtained by completing the Hub flow. It is shown so the user can recognize an unexpected Hub; the authoritative
+ * trust decision remains with {@code CheckHostTrustController} at unlock time.
+ */
+public final class VaultTemplate implements AutoCloseable {
+
+ private static final Logger LOG = LoggerFactory.getLogger(VaultTemplate.class);
+
+ private final Path tempDir;
+ private final Path vaultRoot;
+ private final @Nullable String hubUrl;
+
+ private VaultTemplate(Path tempDir, Path vaultRoot, @Nullable String hubUrl) {
+ this.tempDir = tempDir;
+ this.vaultRoot = vaultRoot;
+ this.hubUrl = hubUrl;
+ }
+
+ /**
+ * Unpacks and validates the given archive on {@code executor}, off the FX thread.
+ *
+ * Failures arrive as a {@link CompletionException} wrapping the cause, so callers should unwrap before deciding
+ * what to show: a {@link MalformedTemplateException} means the template itself is a dead end, any other
+ * {@link IOException} means unpacking failed for an environmental reason.
+ *
+ * @param archive the ZIP archive bytes
+ * @param executor the executor to unpack on
+ * @return the unpacked template, which the caller must close
+ * @see #extract(byte[])
+ */
+ public static CompletionStage
+ * This touches the file system and should not run on the FX thread.
+ *
+ * @param archive the ZIP archive bytes
+ * @return the unpacked template, which the caller must close
+ * @throws MalformedTemplateException if the archive is unusable
+ * @throws IOException if unpacking fails
+ */
+ public static VaultTemplate extract(byte[] archive) throws IOException {
+ return extract(archive, null);
+ }
+
+ @VisibleForTesting
+ static VaultTemplate extract(byte[] archive, @Nullable Path tempParent) throws IOException {
+ Path tempDir = tempParent == null //
+ ? Files.createTempDirectory("vault-template-") //
+ : Files.createTempDirectory(tempParent, "vault-template-");
+ try {
+ var vaultRoot = VaultTemplateExtractor.extractTo(archive, tempDir);
+ return new VaultTemplate(tempDir, vaultRoot, readHubUrl(vaultRoot));
+ } catch (IOException | RuntimeException e) {
+ VaultTemplateExtractor.deleteQuietly(tempDir);
+ throw e;
+ }
+ }
+
+ private static @Nullable String readHubUrl(Path vaultRoot) throws IOException {
+ var token = Files.readString(vaultRoot.resolve(Constants.VAULTCONFIG_FILENAME), StandardCharsets.US_ASCII).trim();
+ HubConfig hubConfig;
+ try {
+ hubConfig = VaultConfig.decode(token).getHeader("hub", HubConfig.class);
+ } catch (VaultConfigLoadException e) {
+ throw new MalformedTemplateException("Template does not contain a decodable vault config.", e);
+ }
+ if (hubConfig == null) {
+ return null; // not a hub vault - allowed, the user just cannot be shown a Hub
+ }
+ try {
+ return hubConfig.getApiBaseUrl().toString();
+ } catch (RuntimeException e) {
+ // hub header present but unusable (e.g. neither apiBaseUrl nor devicesResourceUrl set)
+ LOG.warn("Vault template declares an unusable hub config.", e);
+ return null;
+ }
+ }
+
+ /**
+ * The Hub this template's vault belongs to, or {@code null} if its config declares none.
+ *
+ * Unverified - see the class documentation.
+ */
+ public @Nullable String hubUrl() {
+ return hubUrl;
+ }
+
+ /**
+ * Moves the unpacked vault to its final location.
+ *
+ * @param destination the target vault directory, which must not yet exist
+ * @throws java.nio.file.FileAlreadyExistsException if {@code destination} already exists
+ * @throws IOException if the vault cannot be moved
+ */
+ public void moveTo(Path destination) throws IOException {
+ VaultTemplateExtractor.moveToDestination(vaultRoot, destination);
+ }
+
+ /**
+ * Deletes the temporary directory. Safe to call after a successful {@link #moveTo(Path)}, which only moves the
+ * vault directory out of it.
+ */
+ @Override
+ public void close() {
+ VaultTemplateExtractor.deleteQuietly(tempDir);
+ }
+}
diff --git a/src/main/java/org/cryptomator/ui/importtemplate/VaultTemplateExtractor.java b/src/main/java/org/cryptomator/ui/importtemplate/VaultTemplateExtractor.java
index a7561de2d..2a5637398 100644
--- a/src/main/java/org/cryptomator/ui/importtemplate/VaultTemplateExtractor.java
+++ b/src/main/java/org/cryptomator/ui/importtemplate/VaultTemplateExtractor.java
@@ -20,13 +20,15 @@ import java.util.stream.Stream;
import java.util.zip.ZipException;
/**
- * Unpacks a vault template (a ZIP archive holding a ready-made vault) and moves the contained vault to a destination.
+ * Low-level mechanics of unpacking a vault template (a ZIP archive holding a ready-made vault).
*
- * The archive is expanded into a temporary directory using the Java ZIP {@link FileSystem}, and the vault directory
- * (identified by containing a {@value Constants#VAULTCONFIG_FILENAME} file) is then moved to the destination in a single
- * step, so the vault only ever appears complete at its final location.
+ * The archive is expanded into a directory using the Java ZIP {@link FileSystem}, and the vault directory is located by
+ * finding the {@value Constants#VAULTCONFIG_FILENAME} it contains. Moving that directory to its final location is a
+ * separate step, so the vault only ever appears complete at its destination.
+ *
+ * Lifecycle of the temporary directory is owned by {@link VaultTemplate}, not by this class.
*/
-public final class VaultTemplateExtractor {
+final class VaultTemplateExtractor {
private static final Logger LOG = LoggerFactory.getLogger(VaultTemplateExtractor.class);
// On Windows, URI template (base64url encoded) is is given on command line argument. Windows API restrict the length
@@ -39,34 +41,41 @@ public final class VaultTemplateExtractor {
}
/**
- * Unpacks the given template and moves the contained vault to {@code destination}.
+ * Unpacks the given template into {@code targetDir} and locates the vault it contains.
*
- * @param template the ZIP archive bytes
- * @param destination the target vault directory, which must not yet exist
- * @return {@code destination}
- * @throws MalformedTemplateException if the template is not a valid zip or does not contain a vault
- * @throws IOException if the destination already exists, or the
- * files cannot be written or moved
+ * @param template the ZIP archive bytes
+ * @param targetDir an existing, empty directory to unpack into
+ * @return the unpacked vault directory below {@code targetDir}
+ * @throws MalformedTemplateException if the template is not a readable ZIP, exceeds the entry or size limits, or
+ * does not contain exactly one {@value Constants#VAULTCONFIG_FILENAME}
+ * @throws IOException if the files cannot be written
*/
- public static Path extractAndMove(byte[] template, Path destination) throws IOException {
+ static Path extractTo(byte[] template, Path targetDir) throws IOException {
+ Path tmpZip = Files.createTempFile("vault-template-", ".zip");
+ try {
+ Files.write(tmpZip, template);
+ return unzip(tmpZip, targetDir);
+ } finally {
+ deleteQuietly(tmpZip);
+ }
+ }
+
+ /**
+ * Moves an unpacked vault to its final location, creating missing parent directories.
+ *
+ * @param vaultRoot the unpacked vault directory
+ * @param destination the target vault directory, which must not yet exist
+ * @throws FileAlreadyExistsException if {@code destination} already exists
+ */
+ static void moveToDestination(Path vaultRoot, Path destination) throws IOException {
if (Files.exists(destination)) {
throw new FileAlreadyExistsException(destination.toString());
}
- Path tmpZip = Files.createTempFile("vault-template-", ".zip");
- Path tmpDir = Files.createTempDirectory("vault-template-");
- try {
- Files.write(tmpZip, template);
- Path vaultRoot = unzip(tmpZip, tmpDir);
- Path parent = destination.getParent();
- if (parent != null) {
- Files.createDirectories(parent);
- }
- move(vaultRoot, destination);
- return destination;
- } finally {
- deleteQuietly(tmpDir);
- deleteQuietly(tmpZip);
+ Path parent = destination.getParent();
+ if (parent != null) {
+ Files.createDirectories(parent);
}
+ move(vaultRoot, destination);
}
private static Path unzip(Path zipFile, Path targetDir) throws IOException {
@@ -183,7 +192,7 @@ public final class VaultTemplateExtractor {
});
}
- private static void deleteQuietly(Path path) {
+ static void deleteQuietly(Path path) {
try {
deleteRecursively(path);
} catch (IOException e) {
@@ -203,16 +212,4 @@ public final class VaultTemplateExtractor {
}
}
- public static class MalformedTemplateException extends IOException {
- private static final long serialVersionUID = 1L;
-
- public MalformedTemplateException(String message) {
- super(message);
- }
-
- public MalformedTemplateException(String message, Throwable cause) {
- super(message, cause);
- }
- }
-
}
diff --git a/src/main/resources/fxml/import_template_location.fxml b/src/main/resources/fxml/import_template_location.fxml
index 03915875a..a580350d9 100644
--- a/src/main/resources/fxml/import_template_location.fxml
+++ b/src/main/resources/fxml/import_template_location.fxml
@@ -30,6 +30,11 @@