Merge pull request #3289 from cryptomator/feature/share-vault

Feature: Introduce 'Share Vault' Functionality
This commit is contained in:
mindmonk
2024-01-26 16:08:06 +01:00
committed by GitHub
19 changed files with 380 additions and 5 deletions

View File

@@ -45,6 +45,7 @@ public enum FxmlFile {
RECOVERYKEY_RESET_PASSWORD_SUCCESS("/fxml/recoverykey_reset_password_success.fxml"), //
RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), //
REMOVE_VAULT("/fxml/remove_vault.fxml"), //
SHARE_VAULT("/fxml/share_vault.fxml"), //
UPDATE_REMINDER("/fxml/update_reminder.fxml"), //
UNLOCK_ENTER_PASSWORD("/fxml/unlock_enter_password.fxml"),
UNLOCK_REQUIRES_RESTART("/fxml/unlock_requires_restart.fxml"), //

View File

@@ -47,6 +47,7 @@ public enum FontAwesome5Icon {
QUESTION_CIRCLE("\uf059"), //
REDO("\uF01E"), //
SEARCH("\uF002"), //
SHARE("\uF064"), //
SPINNER("\uF110"), //
STETHOSCOPE("\uF0f1"), //
SYNC("\uF021"), //

View File

@@ -13,6 +13,7 @@ import org.cryptomator.ui.lock.LockComponent;
import org.cryptomator.ui.mainwindow.MainWindowComponent;
import org.cryptomator.ui.preferences.PreferencesComponent;
import org.cryptomator.ui.quit.QuitComponent;
import org.cryptomator.ui.sharevault.ShareVaultComponent;
import org.cryptomator.ui.traymenu.TrayMenuComponent;
import org.cryptomator.ui.unlock.UnlockComponent;
import org.cryptomator.ui.updatereminder.UpdateReminderComponent;
@@ -22,7 +23,17 @@ import javafx.scene.image.Image;
import java.io.IOException;
import java.io.InputStream;
@Module(includes = {UpdateCheckerModule.class}, subcomponents = {TrayMenuComponent.class, MainWindowComponent.class, PreferencesComponent.class, VaultOptionsComponent.class, UnlockComponent.class, LockComponent.class, QuitComponent.class, ErrorComponent.class, HealthCheckComponent.class, UpdateReminderComponent.class})
@Module(includes = {UpdateCheckerModule.class}, subcomponents = {TrayMenuComponent.class, //
MainWindowComponent.class, //
PreferencesComponent.class, //
VaultOptionsComponent.class, //
UnlockComponent.class, //
LockComponent.class, //
QuitComponent.class, //
ErrorComponent.class, //
HealthCheckComponent.class, //
UpdateReminderComponent.class, //
ShareVaultComponent.class})
abstract class FxApplicationModule {
private static Image createImageFromResource(String resourceName) throws IOException {

View File

@@ -11,6 +11,7 @@ import org.cryptomator.ui.mainwindow.MainWindowComponent;
import org.cryptomator.ui.preferences.PreferencesComponent;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import org.cryptomator.ui.quit.QuitComponent;
import org.cryptomator.ui.sharevault.ShareVaultComponent;
import org.cryptomator.ui.unlock.UnlockComponent;
import org.cryptomator.ui.unlock.UnlockWorkflow;
import org.cryptomator.ui.updatereminder.UpdateReminderComponent;
@@ -51,6 +52,7 @@ public class FxApplicationWindows {
private final ErrorComponent.Factory errorWindowFactory;
private final ExecutorService executor;
private final VaultOptionsComponent.Factory vaultOptionsWindow;
private final ShareVaultComponent.Factory shareVaultWindow;
private final FilteredList<Window> visibleWindows;
@Inject
@@ -64,6 +66,7 @@ public class FxApplicationWindows {
LockComponent.Factory lockWorkflowFactory, //
ErrorComponent.Factory errorWindowFactory, //
VaultOptionsComponent.Factory vaultOptionsWindow, //
ShareVaultComponent.Factory shareVaultWindow, //
ExecutorService executor) {
this.primaryStage = primaryStage;
this.trayIntegration = trayIntegration;
@@ -76,6 +79,7 @@ public class FxApplicationWindows {
this.errorWindowFactory = errorWindowFactory;
this.executor = executor;
this.vaultOptionsWindow = vaultOptionsWindow;
this.shareVaultWindow = shareVaultWindow;
this.visibleWindows = Window.getWindows().filtered(Window::isShowing);
}
@@ -122,6 +126,10 @@ public class FxApplicationWindows {
return CompletableFuture.supplyAsync(() -> preferencesWindow.get().showPreferencesWindow(selectedTab), Platform::runLater).whenComplete(this::reportErrors);
}
public void showShareVaultWindow(Vault vault) {
CompletableFuture.runAsync(() -> shareVaultWindow.create(vault).showShareVaultWindow(), Platform::runLater);
}
public CompletionStage<Stage> showVaultOptionsWindow(Vault vault, SelectedVaultOptionsTab tab) {
return showMainWindow().thenApplyAsync((window) -> vaultOptionsWindow.create(vault).showVaultOptionsWindow(tab), Platform::runLater).whenComplete(this::reportErrors);
}

View File

@@ -44,6 +44,11 @@ public class VaultDetailLockedController implements FxController {
appWindows.startUnlockWorkflow(vault.get(), mainWindow);
}
@FXML
public void share() {
appWindows.showShareVaultWindow(vault.get());
}
@FXML
public void showVaultOptions() {
vaultOptionsWindow.create(vault.get()).showVaultOptionsWindow(SelectedVaultOptionsTab.ANY);

View File

@@ -0,0 +1,34 @@
package org.cryptomator.ui.sharevault;
import dagger.BindsInstance;
import dagger.Lazy;
import dagger.Subcomponent;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javafx.scene.Scene;
import javafx.stage.Stage;
@ShareVaultScoped
@Subcomponent(modules = {ShareVaultModule.class})
public interface ShareVaultComponent {
@ShareVaultWindow
Stage window();
@FxmlScene(FxmlFile.SHARE_VAULT)
Lazy<Scene> scene();
default void showShareVaultWindow(){
Stage stage = window();
stage.setScene(scene().get());
stage.show();
}
@Subcomponent.Factory
interface Factory {
ShareVaultComponent create(@BindsInstance @ShareVaultWindow Vault vault);
}
}

View File

@@ -0,0 +1,76 @@
package org.cryptomator.ui.sharevault;
import dagger.Lazy;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.keyloading.hub.HubKeyLoadingStrategy;
import javax.inject.Inject;
import javafx.application.Application;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URISyntaxException;
@ShareVaultScoped
public class ShareVaultController implements FxController {
private static final String SCHEME_PREFIX = "hub+";
private static final String VISIT_HUB_URL = "https://cryptomator.org/hub/";
private static final String BEST_PRACTICES_URL = "https://docs.cryptomator.org/en/latest/security/best-practices/#sharing-of-vaults";
private final Stage window;
private final Lazy<Application> application;
private final Vault vault;
private final Boolean hubVault;
@Inject
ShareVaultController(@ShareVaultWindow Stage window, //
Lazy<Application> application, //
@ShareVaultWindow Vault vault) {
this.window = window;
this.application = application;
this.vault = vault;
var vaultScheme = vault.getVaultConfigCache().getUnchecked().getKeyId().getScheme();
this.hubVault = (vaultScheme.equals(HubKeyLoadingStrategy.SCHEME_HUB_HTTP) || vaultScheme.equals(HubKeyLoadingStrategy.SCHEME_HUB_HTTPS));
}
@FXML
public void close() {
window.close();
}
@FXML
public void visitHub() {
application.get().getHostServices().showDocument(VISIT_HUB_URL);
}
@FXML
public void openHub() {
application.get().getHostServices().showDocument(getHubUri(vault).toString());
}
@FXML
public void visitBestPractices() {
application.get().getHostServices().showDocument(BEST_PRACTICES_URL);
}
private static URI getHubUri(Vault vault) {
try {
var keyID = new URI(vault.getVaultConfigCache().get().getKeyId().toString());
assert keyID.getScheme().startsWith(SCHEME_PREFIX);
return new URI(keyID.getScheme().substring(SCHEME_PREFIX.length()) + "://" + keyID.getHost() + "/app/vaults");
} catch (IOException e) {
throw new UncheckedIOException(e);
} catch (URISyntaxException e) {
throw new IllegalStateException("URI constructed from params known to be valid", e);
}
}
public boolean isHubVault() {
return hubVault;
}
}

View File

@@ -0,0 +1,54 @@
package org.cryptomator.ui.sharevault;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import javax.inject.Provider;
import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.util.Map;
import java.util.ResourceBundle;
@Module
abstract class ShareVaultModule {
@Provides
@ShareVaultWindow
@ShareVaultScoped
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
}
@Provides
@ShareVaultWindow
@ShareVaultScoped
static Stage provideStage(StageFactory factory, ResourceBundle resourceBundle) {
Stage stage = factory.create();
stage.setResizable(false);
stage.initModality(Modality.APPLICATION_MODAL);
stage.setTitle(resourceBundle.getString("shareVault.title"));
return stage;
}
@Provides
@FxmlScene(FxmlFile.SHARE_VAULT)
@ShareVaultScoped
static Scene provideShareVaultScene(@ShareVaultWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.SHARE_VAULT);
}
@Binds
@IntoMap
@FxControllerKey(ShareVaultController.class)
abstract FxController bindShareVaultController(ShareVaultController controller);
}

View File

@@ -0,0 +1,13 @@
package org.cryptomator.ui.sharevault;
import javax.inject.Scope;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
@interface ShareVaultScoped {
}

View File

@@ -0,0 +1,14 @@
package org.cryptomator.ui.sharevault;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@interface ShareVaultWindow {
}

View File

@@ -936,3 +936,16 @@
-fx-padding: 0.5px;
-fx-background-color: CONTROL_BORDER_NORMAL;
}
/*******************************************************************************
* *
* Ad box *
* *
******************************************************************************/
.ad-box {
-fx-padding: 12px;
-fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL;
-fx-background-insets: 0, 1px;
-fx-background-radius: 4px;
}

View File

@@ -935,3 +935,16 @@
-fx-padding: 0.5px;
-fx-background-color: CONTROL_BORDER_NORMAL;
}
/*******************************************************************************
* *
* Ad box *
* *
******************************************************************************/
.ad-box {
-fx-padding: 12px;
-fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL;
-fx-background-insets: 0, 1px;
-fx-background-radius: 4px;
}

View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.shape.Circle?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.control.Hyperlink?>
<?import javafx.scene.control.Tooltip?>
<?import javafx.scene.Group?>
<HBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.sharevault.ShareVaultController"
prefWidth="540"
spacing="12">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<Group>
<StackPane>
<padding>
<Insets topRightBottomLeft="6"/>
</padding>
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="INFO" glyphSize="24"/>
</StackPane>
</Group>
<VBox>
<VBox HBox.hgrow="ALWAYS" visible="${controller.hubVault}" managed="${controller.hubVault}">
<Label text="%shareVault.hub.message" styleClass="label-large" wrapText="true">
<padding>
<Insets bottom="6" top="6"/>
</padding>
</Label>
<Label text="%shareVault.hub.description" wrapText="true"/>
<VBox>
<padding>
<Insets left="6"/>
</padding>
<Label text="%shareVault.hub.instruction.1" wrapText="true"/>
<Label text="%shareVault.hub.instruction.2" wrapText="true"/>
</VBox>
</VBox>
<VBox HBox.hgrow="ALWAYS" visible="${!controller.hubVault}" managed="${!controller.hubVault}">
<Label text="%shareVault.message" styleClass="label-large" wrapText="true">
<padding>
<Insets bottom="6" top="6"/>
</padding>
</Label>
<Label text="%shareVault.description" wrapText="true"/>
<VBox>
<padding>
<Insets left="6"/>
</padding>
<Label text="%shareVault.instruction.1" wrapText="true"/>
<Label text="%shareVault.instruction.2" wrapText="true"/>
</VBox>
<Region minHeight="6"/>
<HBox spacing="6">
<Hyperlink contentDisplay="GRAPHIC_ONLY" onAction="#visitBestPractices">
<graphic>
<FontAwesome5IconView glyph="QUESTION_CIRCLE" styleClass="glyph-icon-muted"/>
</graphic>
<tooltip>
<Tooltip text="%shareVault.docsTooltip" showDelay="100ms"/>
</tooltip>
</Hyperlink>
<Label text="%shareVault.remarkBestPractices" wrapText="true"/>
</HBox>
<Region minHeight="12"/>
<HBox alignment="CENTER_LEFT" spacing="6" styleClass="ad-box">
<VBox spacing="6" alignment="CENTER_LEFT">
<ImageView HBox.hgrow="ALWAYS" fitWidth="180" preserveRatio="true" cache="true">
<Image url="@../img/hub_logo.png"/>
</ImageView>
<Label text="%shareVault.hubAd.description" style="-fx-font-weight: bold;" wrapText="true"/>
<VBox spacing="6" alignment="CENTER_LEFT">
<padding>
<Insets left="6"/>
</padding>
<Label text="%shareVault.hubAd.keyManagement" wrapText="true"/>
<Label text="%shareVault.hubAd.authentication" wrapText="true"/>
<Label text="%shareVault.hubAd.encryption" wrapText="true"/>
</VBox>
</VBox>
<Region HBox.hgrow="ALWAYS"/>
<ImageView HBox.hgrow="ALWAYS" fitWidth="180" preserveRatio="true" cache="true">
<Image url="@../img/group-magic.png"/>
</ImageView>
</HBox>
</VBox>
<Region VBox.vgrow="ALWAYS" minHeight="18"/>
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
<buttons>
<Button text="%generic.button.close" ButtonBar.buttonData="CANCEL_CLOSE" onAction="#close"/>
<Button text="%shareVault.hub.openHub" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#openHub" visible="${controller.hubVault}" managed="${controller.hubVault}"/>
<Button text="%shareVault.visitHub" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#visitHub" visible="${!controller.hubVault}" managed="${!controller.hubVault}"/>
</buttons>
</ButtonBar>
</VBox>
</HBox>

View File

@@ -24,14 +24,17 @@
<FontAwesome5IconView glyph="KEY" glyphSize="15"/>
</graphic>
</Button>
<Hyperlink text="%main.vaultDetail.passwordSavedInKeychain" visible="${controller.passwordSaved}" onAction="#showKeyVaultOptions">
<Hyperlink text="%main.vaultDetail.passwordSavedInKeychain" visible="${controller.passwordSaved}" managed="${controller.passwordSaved}" onAction="#showKeyVaultOptions">
<graphic>
<FontAwesome5IconView glyph="LOCK"/>
</graphic>
</Hyperlink>
<Button text="%main.vaultDetail.share" minWidth="120" onAction="#share">
<graphic>
<FontAwesome5IconView glyph="SHARE" glyphSize="15"/>
</graphic>
</Button>
<Region VBox.vgrow="ALWAYS"/>
<HBox alignment="BOTTOM_RIGHT">
<Button text="%main.vaultDetail.optionsBtn" minWidth="120" onAction="#showVaultOptions">
<graphic>

View File

@@ -393,6 +393,7 @@ main.vaultDetail.unlockBtn=Unlock…
main.vaultDetail.unlockNowBtn=Unlock Now
main.vaultDetail.optionsBtn=Vault Options
main.vaultDetail.passwordSavedInKeychain=Password saved
main.vaultDetail.share=Share…
### Unlocked
main.vaultDetail.unlockedStatus=UNLOCKED
main.vaultDetail.accessLocation=Your vault's contents are accessible here:
@@ -530,4 +531,24 @@ updateReminder.message=Check for Updates?
updateReminder.description=Stay updated with new features, bug fixes, and security improvements. We recommend to automatically check for updates.
updateReminder.notNow=Not Now
updateReminder.yesOnce=Yes, Once
updateReminder.yesAutomatically=Yes, Automatically
updateReminder.yesAutomatically=Yes, Automatically
# Share Vault
shareVault.title=Share Vault
shareVault.message=Would you like to share your vault with others?
shareVault.description=Always be careful when sharing your vault with other people. In short, follow these steps:
shareVault.instruction.1=1. Share access of the encrypted vault folder via cloud storage.
shareVault.instruction.2=2. Share the vault password in a secure way.
shareVault.remarkBestPractices=For more information, check out the best practices suggestions in our docs.
shareVault.docsTooltip=Open the documentation to learn more about sharing of vaults.
shareVault.hubAd.description=The secure way to work in teams
shareVault.hubAd.keyManagement=• Zero-knowledge key management
shareVault.hubAd.authentication=• Strong authentication
shareVault.hubAd.encryption=• End-to-end encryption
shareVault.visitHub=Visit Cryptomator Hub
shareVault.hub.message=How to share a Hub vault
shareVault.hub.description=In order to share the vault content with another team member, you have to perform two steps:
shareVault.hub.instruction.1=1. Share access of the encrypted vault folder via cloud storage.
shareVault.hub.instruction.2=2. Grant access to team member in Cryptomator Hub.
shareVault.hub.openHub=Open Cryptomator Hub

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB