Sfoglia il codice sorgente

Feature: Decrypt encrypted file name (#3788)

Closes #2713
Armin Schrenk 2 mesi fa
parent
commit
1d8466a9e3

+ 11 - 0
src/main/java/org/cryptomator/common/vaults/Vault.java

@@ -423,6 +423,17 @@ public class Vault {
 		}
 	}
 
+	/**
+	 * Gets the cleartext name from a given path to an encrypted vault file
+	 */
+	public String getCleartextName(Path ciphertextPath) throws IOException {
+		if (!state.getValue().equals(VaultState.Value.UNLOCKED)) {
+			throw new IllegalStateException("Vault is not unlocked");
+		}
+		var fs = cryptoFileSystem.get();
+		return fs.getCleartextName(ciphertextPath);
+	}
+
 	public VaultConfigCache getVaultConfigCache() {
 		return configCache;
 	}

+ 1 - 0
src/main/java/org/cryptomator/ui/common/FxmlFile.java

@@ -12,6 +12,7 @@ public enum FxmlFile {
 	CONVERTVAULT_HUBTOPASSWORD_START("/fxml/convertvault_hubtopassword_start.fxml"), //
 	CONVERTVAULT_HUBTOPASSWORD_CONVERT("/fxml/convertvault_hubtopassword_convert.fxml"), //
 	CONVERTVAULT_HUBTOPASSWORD_SUCCESS("/fxml/convertvault_hubtopassword_success.fxml"), //
+	DECRYPTNAMES("/fxml/decryptnames.fxml"), //
 	ERROR("/fxml/error.fxml"), //
 	EVENT_VIEW("/fxml/eventview.fxml"), //
 	FORGET_PASSWORD("/fxml/forget_password.fxml"), //

+ 25 - 0
src/main/java/org/cryptomator/ui/decryptname/CipherAndCleartext.java

@@ -0,0 +1,25 @@
+package org.cryptomator.ui.decryptname;
+
+import javafx.beans.property.ReadOnlyStringWrapper;
+import javafx.beans.value.ObservableValue;
+import java.nio.file.Path;
+
+public record CipherAndCleartext(Path ciphertext, String cleartextName) {
+
+	public String getCiphertextFilename() {
+		return ciphertext.getFileName().toString();
+	}
+
+	public ObservableValue<String> ciphertextFilenameProperty() {
+		return new ReadOnlyStringWrapper(getCiphertextFilename());
+	}
+
+	public String getCleartextName() {
+		return cleartextName;
+	}
+
+	public ObservableValue<String> cleartextNameProperty() {
+		return new ReadOnlyStringWrapper(getCleartextName());
+	}
+
+}

+ 233 - 0
src/main/java/org/cryptomator/ui/decryptname/DecryptFileNamesViewController.java

@@ -0,0 +1,233 @@
+package org.cryptomator.ui.decryptname;
+
+import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.cryptofs.common.Constants;
+import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.controls.FontAwesome5Icon;
+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.ListProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleListProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.fxml.FXML;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+import javafx.scene.control.cell.PropertyValueFactory;
+import javafx.scene.input.Clipboard;
+import javafx.scene.input.DataFormat;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyCodeCombination;
+import javafx.scene.input.TransferMode;
+import javafx.stage.FileChooser;
+import javafx.stage.Stage;
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.ResourceBundle;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+@DecryptNameScoped
+public class DecryptFileNamesViewController implements FxController {
+
+	private static final Logger LOG = LoggerFactory.getLogger(DecryptFileNamesViewController.class);
+	private static final KeyCodeCombination COPY_TO_CLIPBOARD_SHORTCUT = new KeyCodeCombination(KeyCode.C, KeyCodeCombination.SHORTCUT_DOWN);
+	private static final String COPY_TO_CLIPBOARD_SHORTCUT_STRING_WIN = "CTRL+C";
+	private static final String COPY_TO_CLIPBOARD_SHORTCUT_STRING_MAC = "⌘C";
+	private static final String COPY_TO_CLIPBOARD_SHORTCUT_STRING_LINUX = "CTRL+C";
+
+	private final ListProperty<CipherAndCleartext> mapping;
+	private final StringProperty dropZoneText = new SimpleStringProperty();
+	private final ObjectProperty<FontAwesome5Icon> dropZoneIcon = new SimpleObjectProperty<>();
+	private final BooleanProperty wrongFilesSelected = new SimpleBooleanProperty(false);
+	private final Stage window;
+	private final Vault vault;
+	private final ResourceBundle resourceBundle;
+	private final List<Path> initialList;
+
+	@FXML
+	public TableColumn<CipherAndCleartext, String> ciphertextColumn;
+	@FXML
+	public TableColumn<CipherAndCleartext, String> cleartextColumn;
+	@FXML
+	public TableView<CipherAndCleartext> cipherToCleartextTable;
+
+	@Inject
+	public DecryptFileNamesViewController(@DecryptNameWindow Stage window, @DecryptNameWindow Vault vault, @DecryptNameWindow List<Path> pathsToDecrypt, ResourceBundle resourceBundle) {
+		this.window = window;
+		this.vault = vault;
+		this.resourceBundle = resourceBundle;
+		this.mapping = new SimpleListProperty<>(FXCollections.observableArrayList());
+		this.initialList = pathsToDecrypt;
+	}
+
+	@FXML
+	public void initialize() {
+		cipherToCleartextTable.setItems(mapping);
+		cipherToCleartextTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS);
+		//DragNDrop
+		cipherToCleartextTable.setOnDragEntered(event -> {
+			if (event.getGestureSource() == null && event.getDragboard().hasFiles()) {
+				cipherToCleartextTable.setItems(FXCollections.emptyObservableList());
+			}
+		});
+		cipherToCleartextTable.setOnDragOver(event -> {
+			if (event.getGestureSource() == null && event.getDragboard().hasFiles()) {
+				if (SystemUtils.IS_OS_WINDOWS || SystemUtils.IS_OS_MAC) {
+					event.acceptTransferModes(TransferMode.LINK);
+				} else {
+					event.acceptTransferModes(TransferMode.ANY);
+				}
+			}
+		});
+		cipherToCleartextTable.setOnDragDropped(event -> {
+			if (event.getGestureSource() == null && event.getDragboard().hasFiles()) {
+				checkAndDecrypt(event.getDragboard().getFiles().stream().map(File::toPath).toList());
+				cipherToCleartextTable.setItems(mapping);
+			}
+		});
+		cipherToCleartextTable.setOnDragExited(_ -> cipherToCleartextTable.setItems(mapping));
+		//selectionModel and copy-to-clipboard action
+		cipherToCleartextTable.getSelectionModel().setCellSelectionEnabled(true);
+		cipherToCleartextTable.setOnKeyPressed(keyEvent -> {
+			if (COPY_TO_CLIPBOARD_SHORTCUT.match(keyEvent)) {
+				copySingleCelltoClipboard();
+			}
+		});
+		ciphertextColumn.setCellValueFactory(new PropertyValueFactory<>("ciphertextFilename"));
+		cleartextColumn.setCellValueFactory(new PropertyValueFactory<>("cleartextName"));
+
+		dropZoneText.setValue(resourceBundle.getString("decryptNames.dropZone.message"));
+		dropZoneIcon.setValue(FontAwesome5Icon.FILE_IMPORT);
+
+		wrongFilesSelected.addListener((_, _, areWrongFiles) -> {
+			if (areWrongFiles) {
+				CompletableFuture.delayedExecutor(5, TimeUnit.SECONDS, Platform::runLater).execute(() -> {
+					dropZoneText.setValue(resourceBundle.getString("decryptNames.dropZone.message"));
+					dropZoneIcon.setValue(FontAwesome5Icon.FILE_IMPORT);
+					wrongFilesSelected.setValue(false);
+				});
+			}
+		});
+		if (!initialList.isEmpty()) {
+			checkAndDecrypt(initialList);
+		}
+	}
+
+	private void copySingleCelltoClipboard() {
+		cipherToCleartextTable.getSelectionModel().getSelectedCells().stream().findFirst().ifPresent(tablePosition -> {
+			var selectedItem = cipherToCleartextTable.getSelectionModel().getSelectedItem();
+			//TODO: give user feedback, if content is copied -> must be done via a custom cell factory to access the actual table cell!
+			if (tablePosition.getTableColumn().equals(ciphertextColumn)) {
+				Clipboard.getSystemClipboard().setContent(Map.of(DataFormat.PLAIN_TEXT, selectedItem.ciphertext().toString()));
+			} else {
+				Clipboard.getSystemClipboard().setContent(Map.of(DataFormat.PLAIN_TEXT, selectedItem.cleartextName()));
+			}
+		});
+	}
+
+	@FXML
+	public void selectFiles() {
+		var fileChooser = new FileChooser();
+		fileChooser.setTitle(resourceBundle.getString("decryptNames.filePicker.title"));
+		fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter(resourceBundle.getString("decryptNames.filePicker.extensionDescription"), List.of("*.c9r")));
+		fileChooser.setInitialDirectory(vault.getPath().toFile());
+		var ciphertextNodes = fileChooser.showOpenMultipleDialog(window);
+		if (ciphertextNodes != null) {
+			checkAndDecrypt(ciphertextNodes.stream().map(File::toPath).toList());
+		}
+	}
+
+	private void checkAndDecrypt(List<Path> pathsToDecrypt) {
+		mapping.clear();
+		//Assumption: All files are in the same directory
+		var testPath = pathsToDecrypt.getFirst();
+		if (!testPath.startsWith(vault.getPath())) {
+			setDropZoneError(resourceBundle.getString("decryptNames.dropZone.error.foreignFiles").formatted(vault.getDisplayName()));
+			return;
+		}
+		if (pathsToDecrypt.size() == 1 && testPath.endsWith(Constants.DIR_ID_BACKUP_FILE_NAME)) {
+			setDropZoneError(resourceBundle.getString("decryptNames.dropZone.error.vaultInternalFiles"));
+			return;
+		}
+
+		try {
+			var newMapping = pathsToDecrypt.stream().filter(p -> !p.endsWith(Constants.DIR_ID_BACKUP_FILE_NAME)).map(this::getCleartextName).toList();
+			mapping.addAll(newMapping);
+		} catch (UncheckedIOException e) {
+			setDropZoneError(resourceBundle.getString("decryptNames.dropZone.error.generic"));
+			LOG.info("Failed to decrypt filenames for directory {}", testPath.getParent(), e);
+		} catch (IllegalArgumentException e) {
+			setDropZoneError(resourceBundle.getString("decryptNames.dropZone.error.vaultInternalFiles"));
+		} catch (UnsupportedOperationException e) {
+			setDropZoneError(resourceBundle.getString("decryptNames.dropZone.error.noDirIdBackup"));
+		}
+	}
+
+	private void setDropZoneError(String text) {
+		dropZoneIcon.setValue(FontAwesome5Icon.TIMES);
+		dropZoneText.setValue(text);
+		wrongFilesSelected.setValue(true);
+	}
+
+	private CipherAndCleartext getCleartextName(Path ciphertextNode) {
+		try {
+			var cleartextName = vault.getCleartextName(ciphertextNode);
+			return new CipherAndCleartext(ciphertextNode, cleartextName);
+		} catch (IOException e) {
+			throw new UncheckedIOException(e);
+		}
+	}
+
+	//obvservable getter
+
+	public ObservableValue<String> dropZoneTextProperty() {
+		return dropZoneText;
+	}
+
+	public String getDropZoneText() {
+		return dropZoneText.get();
+	}
+
+	public ObservableValue<FontAwesome5Icon> dropZoneIconProperty() {
+		return dropZoneIcon;
+	}
+
+	public FontAwesome5Icon getDropZoneIcon() {
+		return dropZoneIcon.get();
+	}
+
+	public void clearTable() {
+		mapping.clear();
+	}
+
+	public void copyTableToClipboard() {
+		var csv = mapping.stream().map(cipherAndClear -> "\"" + cipherAndClear.ciphertext() + "\", \"" + cipherAndClear.cleartextName() + "\"").collect(Collectors.joining("\n"));
+		Clipboard.getSystemClipboard().setContent(Map.of(DataFormat.PLAIN_TEXT, csv));
+	}
+
+	public String getCopyToClipboardShortcutString() {
+		if (SystemUtils.IS_OS_WINDOWS) {
+			return COPY_TO_CLIPBOARD_SHORTCUT_STRING_WIN;
+		} else if (SystemUtils.IS_OS_MAC) {
+			return COPY_TO_CLIPBOARD_SHORTCUT_STRING_MAC;
+		} else {
+			return COPY_TO_CLIPBOARD_SHORTCUT_STRING_LINUX;
+		}
+	}
+}

+ 50 - 0
src/main/java/org/cryptomator/ui/decryptname/DecryptNameComponent.java

@@ -0,0 +1,50 @@
+package org.cryptomator.ui.decryptname;
+
+import dagger.BindsInstance;
+import dagger.Lazy;
+import dagger.Subcomponent;
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.common.vaults.VaultState;
+import org.cryptomator.ui.common.FxmlFile;
+import org.cryptomator.ui.common.FxmlScene;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Named;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+import java.nio.file.Path;
+import java.util.List;
+
+@DecryptNameScoped
+@Subcomponent(modules = DecryptNameModule.class)
+public interface DecryptNameComponent {
+
+	Logger LOG = LoggerFactory.getLogger(DecryptNameComponent.class);
+
+	@DecryptNameWindow
+	Stage window();
+
+	@FxmlScene(FxmlFile.DECRYPTNAMES)
+	Lazy<Scene> decryptNamesView();
+
+	@DecryptNameWindow
+	Vault vault();
+
+	default void showDecryptFileNameWindow() {
+		Stage s = window();
+		s.setScene(decryptNamesView().get());
+		s.sizeToScene();
+		if (vault().isUnlocked()) {
+			s.show();
+		} else {
+			LOG.error("Aborted showing DecryptFileName window: vault state is not {}, but {}.", VaultState.Value.UNLOCKED, vault().getState());
+		}
+	}
+
+	@Subcomponent.Factory
+	interface Factory {
+
+		DecryptNameComponent create(@BindsInstance @DecryptNameWindow Vault vault, @BindsInstance @Named("windowOwner") Stage owner, @BindsInstance @DecryptNameWindow List<Path> pathsToDecrypt);
+	}
+}

+ 59 - 0
src/main/java/org/cryptomator/ui/decryptname/DecryptNameModule.java

@@ -0,0 +1,59 @@
+package org.cryptomator.ui.decryptname;
+
+import dagger.Binds;
+import dagger.Module;
+import dagger.Provides;
+import dagger.multibindings.IntoMap;
+import org.cryptomator.common.vaults.Vault;
+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.Named;
+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
+public abstract class DecryptNameModule {
+
+	@Provides
+	@DecryptNameScoped
+	@DecryptNameWindow
+	static Stage provideStage(StageFactory factory, @Named("windowOwner") Stage owner, @DecryptNameWindow Vault vault, ResourceBundle resourceBundle) {
+		Stage stage = factory.create();
+		stage.setResizable(true);
+		stage.initModality(Modality.WINDOW_MODAL);
+		stage.initOwner(owner);
+		stage.setTitle(resourceBundle.getString("decryptNames.title"));
+		vault.stateProperty().addListener(((_, _, _) -> stage.close())); //as soon as the state changes from unlocked, close the window
+		return stage;
+	}
+
+	@Provides
+	@DecryptNameScoped
+	@DecryptNameWindow
+	static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
+		return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
+	}
+
+	@Provides
+	@FxmlScene(FxmlFile.DECRYPTNAMES)
+	@DecryptNameScoped
+	static Scene provideDecryptNamesViewScene(@DecryptNameWindow FxmlLoaderFactory fxmlLoaders) {
+		return fxmlLoaders.createScene(FxmlFile.DECRYPTNAMES);
+	}
+
+	@Binds
+	@IntoMap
+	@FxControllerKey(DecryptFileNamesViewController.class)
+	abstract FxController bindDecryptNamesViewController(DecryptFileNamesViewController controller);
+
+}

+ 11 - 0
src/main/java/org/cryptomator/ui/decryptname/DecryptNameScoped.java

@@ -0,0 +1,11 @@
+package org.cryptomator.ui.decryptname;
+
+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 DecryptNameScoped {}

+ 12 - 0
src/main/java/org/cryptomator/ui/decryptname/DecryptNameWindow.java

@@ -0,0 +1,12 @@
+package org.cryptomator.ui.decryptname;
+
+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 DecryptNameWindow {}

+ 2 - 0
src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java

@@ -7,6 +7,7 @@ package org.cryptomator.ui.fxapp;
 
 import dagger.Module;
 import dagger.Provides;
+import org.cryptomator.ui.decryptname.DecryptNameComponent;
 import org.cryptomator.ui.error.ErrorComponent;
 import org.cryptomator.ui.eventview.EventViewComponent;
 import org.cryptomator.ui.health.HealthCheckComponent;
@@ -28,6 +29,7 @@ import java.io.IOException;
 import java.io.InputStream;
 
 @Module(includes = {UpdateCheckerModule.class}, subcomponents = {TrayMenuComponent.class, //
+		DecryptNameComponent.class, //
 		MainWindowComponent.class, //
 		PreferencesComponent.class, //
 		VaultOptionsComponent.class, //

+ 93 - 55
src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java

@@ -6,12 +6,14 @@ import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.tobiasdiez.easybind.EasyBind;
 import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.common.Nullable;
 import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.integrations.mount.Mountpoint;
 import org.cryptomator.integrations.revealpath.RevealFailedException;
 import org.cryptomator.integrations.revealpath.RevealPathService;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.common.VaultService;
+import org.cryptomator.ui.decryptname.DecryptNameComponent;
 import org.cryptomator.ui.fxapp.FxApplicationWindows;
 import org.cryptomator.ui.stats.VaultStatisticsComponent;
 import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent;
@@ -39,10 +41,13 @@ import java.io.IOException;
 import java.nio.file.Path;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.ResourceBundle;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Function;
 
 @MainWindowScoped
 public class VaultDetailUnlockedController implements FxController {
@@ -56,26 +61,39 @@ public class VaultDetailUnlockedController implements FxController {
 	private final WrongFileAlertComponent.Builder wrongFileAlert;
 	private final Stage mainWindow;
 	private final Optional<RevealPathService> revealPathService;
+	private final DecryptNameComponent.Factory decryptNameWindowFactory;
 	private final ResourceBundle resourceBundle;
 	private final LoadingCache<Vault, VaultStatisticsComponent> vaultStats;
 	private final VaultStatisticsComponent.Builder vaultStatsBuilder;
 	private final ObservableValue<Boolean> accessibleViaPath;
 	private final ObservableValue<Boolean> accessibleViaUri;
 	private final ObservableValue<String> mountPoint;
-	private final BooleanProperty draggingOver = new SimpleBooleanProperty();
+	private final BooleanProperty draggingOverLocateEncrypted = new SimpleBooleanProperty();
+	private final BooleanProperty draggingOverDecryptName = new SimpleBooleanProperty();
 	private final BooleanProperty ciphertextPathsCopied = new SimpleBooleanProperty();
 
-	//FXML
-	public Button dropZone;
+	@FXML
+	public Button revealEncryptedDropZone;
+	@FXML
+	public Button decryptNameDropZone;
 
 	@Inject
-	public VaultDetailUnlockedController(ObjectProperty<Vault> vault, FxApplicationWindows appWindows, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder, WrongFileAlertComponent.Builder wrongFileAlert, @MainWindow Stage mainWindow, Optional<RevealPathService> revealPathService, ResourceBundle resourceBundle) {
+	public VaultDetailUnlockedController(ObjectProperty<Vault> vault, //
+										 FxApplicationWindows appWindows, //
+										 VaultService vaultService, //
+										 VaultStatisticsComponent.Builder vaultStatsBuilder, //
+										 WrongFileAlertComponent.Builder wrongFileAlert, //
+										 @MainWindow Stage mainWindow, //
+										 Optional<RevealPathService> revealPathService, //
+										 DecryptNameComponent.Factory decryptNameWindowFactory, //
+										 ResourceBundle resourceBundle) {
 		this.vault = vault;
 		this.appWindows = appWindows;
 		this.vaultService = vaultService;
 		this.wrongFileAlert = wrongFileAlert;
 		this.mainWindow = mainWindow;
 		this.revealPathService = revealPathService;
+		this.decryptNameWindowFactory = decryptNameWindowFactory;
 		this.resourceBundle = resourceBundle;
 		this.vaultStats = CacheBuilder.newBuilder().weakValues().build(CacheLoader.from(this::buildVaultStats));
 		this.vaultStatsBuilder = vaultStatsBuilder;
@@ -92,89 +110,81 @@ public class VaultDetailUnlockedController implements FxController {
 	}
 
 	public void initialize() {
-		dropZone.setOnDragEntered(this::handleDragEvent);
-		dropZone.setOnDragOver(this::handleDragEvent);
-		dropZone.setOnDragDropped(this::handleDragEvent);
-		dropZone.setOnDragExited(this::handleDragEvent);
+		revealEncryptedDropZone.setOnDragOver(e -> handleDragOver(e, draggingOverLocateEncrypted));
+		revealEncryptedDropZone.setOnDragDropped(e -> handleDragDropped(e, this::getCiphertextPath, this::revealOrCopyPaths));
+		revealEncryptedDropZone.setOnDragExited(_ -> draggingOverLocateEncrypted.setValue(false));
+
+		decryptNameDropZone.setOnDragOver(e -> handleDragOver(e, draggingOverDecryptName));
+		decryptNameDropZone.setOnDragDropped(e -> showDecryptNameWindow(e.getDragboard().getFiles().stream().map(File::toPath).toList()));
+		decryptNameDropZone.setOnDragExited(_ -> draggingOverDecryptName.setValue(false));
 
-		EasyBind.includeWhen(dropZone.getStyleClass(), ACTIVE_CLASS, draggingOver);
+		EasyBind.includeWhen(revealEncryptedDropZone.getStyleClass(), ACTIVE_CLASS, draggingOverLocateEncrypted);
+		EasyBind.includeWhen(decryptNameDropZone.getStyleClass(), ACTIVE_CLASS, draggingOverDecryptName);
 	}
 
-	private void handleDragEvent(DragEvent event) {
-		if (DragEvent.DRAG_OVER.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) {
-			if(SystemUtils.IS_OS_WINDOWS || SystemUtils.IS_OS_MAC) {
+	private void handleDragOver(DragEvent event, BooleanProperty prop) {
+		if (event.getGestureSource() == null && event.getDragboard().hasFiles()) {
+			if (SystemUtils.IS_OS_WINDOWS || SystemUtils.IS_OS_MAC) {
 				event.acceptTransferModes(TransferMode.LINK);
 			} else {
 				event.acceptTransferModes(TransferMode.ANY);
 			}
-			draggingOver.set(true);
-		} else if (DragEvent.DRAG_DROPPED.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) {
-			List<Path> ciphertextPaths = event.getDragboard().getFiles().stream().map(File::toPath).map(this::getCiphertextPath).flatMap(Optional::stream).toList();
-			if (ciphertextPaths.isEmpty()) {
+			prop.set(true);
+		}
+	}
+
+	private <T> void handleDragDropped(DragEvent event, Function<Path, T> computation, Consumer<List<T>> positiveAction) {
+		if (event.getGestureSource() == null && event.getDragboard().hasFiles()) {
+			List<T> objects = event.getDragboard().getFiles().stream().map(File::toPath).map(computation).filter(Objects::nonNull).toList();
+			if (objects.isEmpty()) {
 				wrongFileAlert.build().showWrongFileAlertWindow();
 			} else {
-				revealOrCopyPaths(ciphertextPaths);
+				positiveAction.accept(objects);
 			}
-			event.setDropCompleted(!ciphertextPaths.isEmpty());
+			event.setDropCompleted(!objects.isEmpty());
 			event.consume();
-		} else if (DragEvent.DRAG_EXITED.equals(event.getEventType())) {
-			draggingOver.set(false);
 		}
 	}
 
-	private VaultStatisticsComponent buildVaultStats(Vault vault) {
-		return vaultStatsBuilder.vault(vault).build();
-	}
-
-	@FXML
-	public void revealAccessLocation() {
-		vaultService.reveal(vault.get());
-	}
-
 	@FXML
-	public void copyMountUri() {
-		ClipboardContent clipboardContent = new ClipboardContent();
-		clipboardContent.putString(mountPoint.getValue());
-		Clipboard.getSystemClipboard().setContent(clipboardContent);
-	}
-
-	@FXML
-	public void lock() {
-		appWindows.startLockWorkflow(vault.get(), mainWindow);
-	}
-
-	@FXML
-	public void showVaultStatistics() {
-		vaultStats.getUnchecked(vault.get()).showVaultStatisticsWindow();
-	}
-
-	@FXML
-	public void chooseFileAndReveal() {
+	public void chooseDecryptedFileAndReveal() {
 		Preconditions.checkState(accessibleViaPath.getValue());
 		var fileChooser = new FileChooser();
-		fileChooser.setTitle(resourceBundle.getString("main.vaultDetail.filePickerTitle"));
+		fileChooser.setTitle(resourceBundle.getString("main.vaultDetail.locateEncrypted.filePickerTitle"));
 		fileChooser.setInitialDirectory(Path.of(mountPoint.getValue()).toFile());
 		var cleartextFile = fileChooser.showOpenDialog(mainWindow);
 		if (cleartextFile != null) {
-			var ciphertextPaths = getCiphertextPath(cleartextFile.toPath()).stream().toList();
-			revealOrCopyPaths(ciphertextPaths);
+			var ciphertextPath = getCiphertextPath(cleartextFile.toPath());
+			if (ciphertextPath != null) {
+				revealOrCopyPaths(List.of(ciphertextPath));
+			}
 		}
 	}
 
+	@FXML
+	public void showDecryptNameWindow() {
+		showDecryptNameWindow(List.of());
+	}
+
+	private void showDecryptNameWindow(List<Path> pathsToDecrypt) {
+		decryptNameWindowFactory.create(vault.get(), mainWindow, pathsToDecrypt).showDecryptFileNameWindow();
+	}
+
 	private boolean startsWithVaultAccessPoint(Path path) {
 		return path.startsWith(Path.of(mountPoint.getValue()));
 	}
 
-	private Optional<Path> getCiphertextPath(Path path) {
+	@Nullable
+	private Path getCiphertextPath(Path path) {
 		if (!startsWithVaultAccessPoint(path)) {
-			LOG.debug("Path does not start with access point of selected vault: {}", path);
-			return Optional.empty();
+			LOG.debug("Path does not start with mount point of selected vault: {}", path);
+			return null;
 		}
 		try {
-			return Optional.of(vault.get().getCiphertextPath(path));
+			return vault.get().getCiphertextPath(path);
 		} catch (IOException e) {
 			LOG.warn("Unable to get ciphertext path from path: {}", path, e);
-			return Optional.empty();
+			return null;
 		}
 	}
 
@@ -206,6 +216,32 @@ public class VaultDetailUnlockedController implements FxController {
 		});
 	}
 
+	private VaultStatisticsComponent buildVaultStats(Vault vault) {
+		return vaultStatsBuilder.vault(vault).build();
+	}
+
+	@FXML
+	public void revealAccessLocation() {
+		vaultService.reveal(vault.get());
+	}
+
+	@FXML
+	public void copyMountUri() {
+		ClipboardContent clipboardContent = new ClipboardContent();
+		clipboardContent.putString(mountPoint.getValue());
+		Clipboard.getSystemClipboard().setContent(clipboardContent);
+	}
+
+	@FXML
+	public void lock() {
+		appWindows.startLockWorkflow(vault.get(), mainWindow);
+	}
+
+	@FXML
+	public void showVaultStatistics() {
+		vaultStats.getUnchecked(vault.get()).showVaultStatisticsWindow();
+	}
+
 	/* Getter/Setter */
 
 	public ReadOnlyObjectProperty<Vault> vaultProperty() {
@@ -247,4 +283,6 @@ public class VaultDetailUnlockedController implements FxController {
 	public boolean isCiphertextPathsCopied() {
 		return ciphertextPathsCopied.get();
 	}
+
 }
+

+ 174 - 1
src/main/resources/css/dark_theme.css

@@ -16,6 +16,10 @@
 	src: url('opensans_bold.ttf');
 }
 
+@font-face {
+	src: url('firacode_regular.ttf');
+}
+
 /*******************************************************************************
  *                                                                             *
  * Root Styling & Colors                                                       *
@@ -125,6 +129,12 @@
 	-fx-fill: TEXT_FILL;
 }
 
+.cryptic-text {
+	-fx-fill: TEXT_FILL;
+	-fx-font-family: 'Fira Code';
+	-fx-font-size: 1.1em;
+}
+
 /*******************************************************************************
  *                                                                             *
  * Glyph Icons                                                                 *
@@ -1012,4 +1022,167 @@
 	-fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL;
 	-fx-background-insets: 0, 1px;
 	-fx-background-radius: 4px;
-}
+}
+
+/*******************************************************************************
+ *                                                                             *
+ * Decrypt Name Window
+ *                                                                             *
+ ******************************************************************************/
+
+.decrypt-name-window .button-bar {
+	-fx-min-height:42px;
+	-fx-max-height:42px;
+	-fx-background-color: MAIN_BG;
+	-fx-border-color: transparent transparent CONTROL_BORDER_NORMAL transparent;
+	-fx-border-width: 0 0 1px 0;
+}
+
+.decrypt-name-window .button-bar .button-right {
+	-fx-border-color: transparent transparent transparent CONTROL_BORDER_NORMAL;
+	-fx-border-width: 0 0 0 1px;
+	-fx-background-color: MAIN_BG;
+	-fx-background-radius: 0px;
+	-fx-min-height: 42px;
+	-fx-max-height: 42px;
+}
+
+.decrypt-name-window .button-bar .button-right:armed {
+	-fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_ARMED;
+}
+
+.decrypt-name-window .table-view {
+    -fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL;
+    -fx-background-insets: 0,1;
+    /* There is some oddness if padding is in em values rather than pixels,
+       in particular, the left border of the control doesn't show. */
+    -fx-padding: 1; /* 0.083333em; */
+}
+
+.table-view > .placeholder {
+	-fx-background-color: transparent;
+	-fx-background-radius: 0px;
+}
+
+.table-view > .placeholder > .button {
+	-fx-border-width: 0;
+    -fx-border-color: transparent;
+	-fx-background-radius: 0px;
+}
+
+.table-view:focused {
+    -fx-background-color: CONTROL_BORDER_FOCUSED, CONTROL_BG_NORMAL;
+    -fx-background-insets: 0, 1;
+    -fx-background-radius: 0, 0;
+    /* There is some oddness if padding is in em values rather than pixels,
+      in particular, the left border of the control doesn't show. */
+    -fx-padding: 1; /* 0.083333em; */
+}
+
+.table-view > .virtual-flow > .scroll-bar:vertical {
+    -fx-background-insets: 0, 0 0 0 1;
+    -fx-padding: -1 -1 -1 0;
+}
+
+.table-view > .virtual-flow > .corner {
+    -fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL ;
+    -fx-background-insets: 0, 1 0 0 1;
+}
+
+/* Each row in the table is a table-row-cell. Inside a table-row-cell is any
+   number of table-cell. */
+.table-row-cell {
+    -fx-background-color: GRAY_3, CONTROL_BG_NORMAL;
+    -fx-background-insets: 0, 0 0 1 0;
+    -fx-padding: 0.0em; /* 0 */
+    -fx-text-fill: TEXT_FILL;
+}
+
+.table-row-cell:odd {
+    -fx-background-color: GRAY_3, GRAY_1;
+    -fx-background-insets: 0, 0 0 1 0;
+}
+
+.table-cell {
+    -fx-padding: 3px 6px 3px 6px;
+    -fx-background-color: transparent;
+    -fx-border-color: transparent CONTROL_BORDER_NORMAL transparent transparent;
+    -fx-border-width: 1px;
+    -fx-cell-size: 30px;
+    -fx-text-fill: TEXT_FILL;
+    -fx-text-overrun: center-ellipsis;
+}
+
+.table-view:focused > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled:selected > .table-cell {
+    -fx-text-fill: TEXT_FILL;
+}
+
+/* selected, hover - not specified */
+
+/* selected, focused, hover */
+/* selected, focused */
+/* selected */
+.table-view:focused:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:selected,
+.table-view:focused:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:focused:selected,
+.table-view:focused:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:focused:selected:hover {
+    -fx-background-color: CONTROL_PRIMARY_BG_NORMAL, PRIMARY_D1;
+    -fx-background-insets: 0 0 0 0, 1 1 1 3;
+   	-fx-text-fill: TEXT_FILL;
+}
+/* focused */
+.table-view:focused:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:focused {
+    -fx-background-color: CONTROL_PRIMARY_BORDER_FOCUSED, CONTROL_PRIMARY_BG_NORMAL , CONTROL_BG_NORMAL;
+    -fx-background-insets: 0 1 0 0, 1 2 1 1, 2 3 2 2;
+    -fx-text-fill: TEXT_FILL;
+}
+/* focused, hover */
+.table-view:focused:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:focused:hover {
+    -fx-background-color: CONTROL_PRIMARY_BORDER_FOCUSED, CONTROL_PRIMARY_BG_NORMAL , PRIMARY_D2;
+    -fx-background-insets: 0 1 0 0, 1 2 1 1, 2 3 2 2;
+    -fx-text-fill: TEXT_FILL;
+}
+/* hover */
+.table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:hover {
+	-fx-background-color: PRIMARY_D2;
+	-fx-text-fill: TEXT_FILL;
+	-fx-background-insets: 0 0 1 0;
+}
+
+/* The column-resize-line is shown when the user is attempting to resize a column. */
+.table-view .column-resize-line {
+    -fx-background-color: CONTROL_BG_ARMED;
+    -fx-padding: 0.0em 0.0416667em 0.0em 0.0416667em; /* 0 0.571429 0 0.571429 */
+}
+
+/* This is the area behind the column headers. An ideal place to specify background
+   and border colors for the whole area (not individual column-header's). */
+.table-view .column-header-background {
+    -fx-background-color: GRAY_2;
+    -fx-padding: 0;
+}
+
+/* The column header row is made up of a number of column-header, one for each
+   TableColumn, and a 'filler' area that extends from the right-most column
+   to the edge of the tableview, or up to the 'column control' button. */
+.table-view .column-header {
+    -fx-text-fill: TEXT_FILL;
+    -fx-font-size: 1.083333em; /* 13pt ;  1 more than the default font */
+    -fx-size: 24;
+    -fx-border-style: solid;
+    -fx-border-color:
+    	transparent
+        GRAY_3
+        GRAY_3
+        transparent;
+    -fx-border-insets: 0 0 0 0;
+    -fx-border-width: 0.083333em;
+}
+
+.table-view .column-header .label {
+    -fx-alignment: center;
+}
+
+.table-view .empty-table {
+    -fx-background-color: MAIN_BG;
+    -fx-font-size: 1.166667em; /* 14pt - 2 more than the default font */
+}

BIN
src/main/resources/css/firacode_regular.ttf


+ 175 - 1
src/main/resources/css/light_theme.css

@@ -16,6 +16,10 @@
 	src: url('opensans_bold.ttf');
 }
 
+@font-face {
+	src: url('firacode_regular.ttf');
+}
+
 /*******************************************************************************
  *                                                                             *
  * Root Styling & Colors                                                       *
@@ -79,6 +83,7 @@
 	PROGRESS_INDICATOR_END: GRAY_4;
 	PROGRESS_BAR_BG: GRAY_8;
 
+
 	-fx-background-color: MAIN_BG;
 	-fx-text-fill: TEXT_FILL;
 	-fx-font-family: 'Open Sans';
@@ -124,6 +129,12 @@
 	-fx-fill: TEXT_FILL;
 }
 
+.cryptic-text {
+	-fx-fill: TEXT_FILL;
+	-fx-font-family: 'Fira Code';
+	-fx-font-size: 1.1em;
+}
+
 /*******************************************************************************
  *                                                                             *
  * Glyph Icons                                                                 *
@@ -1011,4 +1022,167 @@
 	-fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL;
 	-fx-background-insets: 0, 1px;
 	-fx-background-radius: 4px;
-}
+}
+
+/*******************************************************************************
+ *                                                                             *
+ * Decrypt Name Window
+ *                                                                             *
+ ******************************************************************************/
+
+.decrypt-name-window .button-bar {
+	-fx-min-height:42px;
+	-fx-max-height:42px;
+	-fx-background-color: MAIN_BG;
+	-fx-border-color: transparent transparent CONTROL_BORDER_NORMAL transparent;
+	-fx-border-width: 0 0 1px 0;
+}
+
+.decrypt-name-window .button-bar .button-right {
+	-fx-border-color: transparent transparent transparent CONTROL_BORDER_NORMAL;
+	-fx-border-width: 0 0 0 1px;
+	-fx-background-color: MAIN_BG;
+	-fx-background-radius: 0px;
+	-fx-min-height: 42px;
+	-fx-max-height: 42px;
+}
+
+.decrypt-name-window .button-bar .button-right:armed {
+	-fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_ARMED;
+}
+
+.decrypt-name-window .table-view {
+    -fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL;
+    -fx-background-insets: 0,1;
+    /* There is some oddness if padding is in em values rather than pixels,
+       in particular, the left border of the control doesn't show. */
+    -fx-padding: 1; /* 0.083333em; */
+}
+
+.table-view > .placeholder {
+	-fx-background-color: transparent;
+	-fx-background-radius: 0px;
+}
+
+.table-view > .placeholder > .button {
+	-fx-border-width: 0;
+    -fx-border-color: transparent;
+	-fx-background-radius: 0px;
+}
+
+.table-view:focused {
+    -fx-background-color: CONTROL_BORDER_FOCUSED, CONTROL_BG_NORMAL;
+    -fx-background-insets: 0, 1;
+    -fx-background-radius: 0, 0;
+    /* There is some oddness if padding is in em values rather than pixels,
+      in particular, the left border of the control doesn't show. */
+    -fx-padding: 1; /* 0.083333em; */
+}
+
+.table-view > .virtual-flow > .scroll-bar:vertical {
+    -fx-background-insets: 0, 0 0 0 1;
+    -fx-padding: -1 -1 -1 0;
+}
+
+.table-view > .virtual-flow > .corner {
+    -fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL ;
+    -fx-background-insets: 0, 1 0 0 1;
+}
+
+/* Each row in the table is a table-row-cell. Inside a table-row-cell is any
+   number of table-cell. */
+.table-row-cell {
+    -fx-background-color: GRAY_6, CONTROL_BG_NORMAL;
+    -fx-background-insets: 0, 0 0 1 0;
+    -fx-padding: 0.0em; /* 0 */
+    -fx-text-fill: TEXT_FILL;
+}
+
+.table-row-cell:odd {
+    -fx-background-color: GRAY_6, GRAY_9;
+    -fx-background-insets: 0, 0 0 1 0;
+}
+
+.table-cell {
+    -fx-padding: 3px 6px 3px 6px;
+    -fx-background-color: transparent;
+    -fx-border-color: transparent CONTROL_BORDER_NORMAL transparent transparent;
+    -fx-border-width: 1px;
+    -fx-cell-size: 30px;
+    -fx-text-fill: TEXT_FILL;
+    -fx-text-overrun: center-ellipsis;
+}
+
+.table-view:focused > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled:selected > .table-cell {
+    -fx-text-fill: TEXT_FILL;
+}
+
+/* selected, hover - not specified */
+
+/* selected, focused, hover */
+/* selected, focused */
+/* selected */
+.table-view:focused:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:selected,
+.table-view:focused:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:focused:selected,
+.table-view:focused:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:focused:selected:hover {
+    -fx-background-color: CONTROL_PRIMARY_BG_NORMAL, CONTROL_BG_SELECTED;
+    -fx-background-insets: 0 0 0 0, 1 1 1 3;
+   	-fx-text-fill: TEXT_FILL;
+}
+/* focused */
+.table-view:focused:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:focused {
+    -fx-background-color: CONTROL_PRIMARY_BORDER_FOCUSED, CONTROL_PRIMARY_BG_NORMAL , CONTROL_BG_NORMAL;
+    -fx-background-insets: 0 1 0 0, 1 2 1 1, 2 3 2 2;
+    -fx-text-fill: TEXT_FILL;
+}
+/* focused, hover */
+.table-view:focused:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:focused:hover {
+    -fx-background-color: CONTROL_PRIMARY_BORDER_FOCUSED, CONTROL_PRIMARY_BG_NORMAL , PRIMARY_L2;
+    -fx-background-insets: 0 1 0 0, 1 2 1 1, 2 3 2 2;
+    -fx-text-fill: TEXT_FILL;
+}
+/* hover */
+.table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled > .table-cell:hover {
+	-fx-background-color: PRIMARY_L2;
+	-fx-text-fill: TEXT_FILL;
+	-fx-background-insets: 0 0 1 0;
+}
+
+/* The column-resize-line is shown when the user is attempting to resize a column. */
+.table-view .column-resize-line {
+    -fx-background-color: CONTROL_BG_ARMED;
+    -fx-padding: 0.0em 0.0416667em 0.0em 0.0416667em; /* 0 0.571429 0 0.571429 */
+}
+
+/* This is the area behind the column headers. An ideal place to specify background
+   and border colors for the whole area (not individual column-header's). */
+.table-view .column-header-background {
+    -fx-background-color: GRAY_7;
+    -fx-padding: 0;
+}
+
+/* The column header row is made up of a number of column-header, one for each
+   TableColumn, and a 'filler' area that extends from the right-most column
+   to the edge of the tableview, or up to the 'column control' button. */
+.table-view .column-header {
+    -fx-text-fill: TEXT_FILL;
+    -fx-font-size: 1.083333em; /* 13pt ;  1 more than the default font */
+    -fx-size: 24;
+    -fx-border-style: solid;
+    -fx-border-color:
+        CONTROL_BORDER_NORMAL
+        GRAY_5
+        GRAY_5
+        transparent;
+    -fx-border-insets: 0 0 0 0;
+    -fx-border-width: 0.083333em;
+}
+
+.table-view .column-header .label {
+    -fx-alignment: center;
+}
+
+.table-view .empty-table {
+    -fx-background-color: MAIN_BG;
+    -fx-font-size: 1.166667em; /* 14pt - 2 more than the default font */
+}

+ 70 - 0
src/main/resources/fxml/decryptnames.fxml

@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
+<?import org.cryptomator.ui.controls.FormattedLabel?>
+<?import javafx.geometry.Insets?>
+<?import javafx.scene.control.Button?>
+<?import javafx.scene.control.TableColumn?>
+<?import javafx.scene.control.TableView?>
+<?import javafx.scene.control.Tooltip?>
+<?import javafx.scene.layout.HBox?>
+<?import javafx.scene.layout.Region?>
+<?import javafx.scene.layout.VBox?>
+<VBox xmlns="http://javafx.com/javafx"
+	  xmlns:fx="http://javafx.com/fxml"
+	  fx:controller="org.cryptomator.ui.decryptname.DecryptFileNamesViewController"
+	  styleClass="decrypt-name-window"
+	  minWidth="400"
+	  maxWidth="400"
+	  minHeight="145">
+	<HBox styleClass="button-bar" alignment="CENTER">
+		<padding>
+			<Insets left="6"/>
+		</padding>
+		<Region HBox.hgrow="ALWAYS"/>
+		<Button styleClass="button-right" contentDisplay="GRAPHIC_ONLY" onAction="#copyTableToClipboard">
+			<graphic>
+				<FontAwesome5IconView glyph="CLIPBOARD" glyphSize="16"/>
+			</graphic>
+			<tooltip>
+				<Tooltip text="%decryptNames.copyTable.tooltip"/>
+			</tooltip>
+		</Button>
+		<Button styleClass="button-right" contentDisplay="GRAPHIC_ONLY" onAction="#clearTable">
+			<graphic>
+				<FontAwesome5IconView glyph="TRASH" glyphSize="16"/>
+			</graphic>
+			<tooltip>
+				<Tooltip text="%decryptNames.clearTable.tooltip"/>
+			</tooltip>
+		</Button>
+	</HBox>
+	<TableView fx:id="cipherToCleartextTable" VBox.vgrow="ALWAYS">
+		<placeholder>
+			<Button alignment="CENTER" onAction="#selectFiles" text="${controller.dropZoneText}" contentDisplay="TOP" maxWidth="Infinity" maxHeight="Infinity">
+				<graphic>
+					<FontAwesome5IconView glyph="${controller.dropZoneIcon}" glyphSize="16"/>
+				</graphic>
+			</Button>
+		</placeholder>
+		<columns>
+			<TableColumn fx:id="ciphertextColumn" prefWidth="${cipherToCleartextTable.width * 0.5}">
+				<graphic>
+					<FontAwesome5IconView glyph="LOCK"/>
+				</graphic>
+			</TableColumn>
+			<TableColumn fx:id="cleartextColumn" prefWidth="${cipherToCleartextTable.width * 0.5}">
+				<graphic>
+					<FontAwesome5IconView glyph="LOCK_OPEN"/>
+				</graphic>
+			</TableColumn>
+		</columns>
+	</TableView>
+	<HBox>
+		<padding>
+			<Insets topRightBottomLeft="6"/>
+		</padding>
+		<Region HBox.hgrow="ALWAYS"/>
+		<FormattedLabel styleClass="label-small" format="%decryptNames.copyHint" arg1="${controller.copyToClipboardShortcutString}"/>
+	</HBox>
+</VBox>

+ 20 - 8
src/main/resources/fxml/vault_detail_unlocked.fxml

@@ -1,12 +1,14 @@
 <?import org.cryptomator.ui.controls.FontAwesome5IconView?>
 <?import org.cryptomator.ui.controls.ThroughputLabel?>
+<?import javafx.geometry.Insets?>
 <?import javafx.scene.control.Button?>
 <?import javafx.scene.control.Label?>
+<?import javafx.scene.control.Tooltip?>
 <?import javafx.scene.layout.HBox?>
 <?import javafx.scene.layout.Region?>
+<?import javafx.scene.layout.StackPane?>
 <?import javafx.scene.layout.VBox?>
-<?import javafx.scene.control.Tooltip?>
-<?import javafx.geometry.Insets?>
+<?import javafx.scene.text.Text?>
 <VBox xmlns:fx="http://javafx.com/fxml"
 	  xmlns="http://javafx.com/javafx"
 	  fx:controller="org.cryptomator.ui.mainwindow.VaultDetailUnlockedController"
@@ -44,28 +46,38 @@
 	<Region VBox.vgrow="ALWAYS"/>
 
 	<HBox alignment="BOTTOM_CENTER">
-		<HBox visible="${controller.accessibleViaPath}" managed="${controller.accessibleViaPath}">
+		<StackPane visible="${controller.accessibleViaPath}" managed="${controller.accessibleViaPath}">
 			<padding>
 				<Insets topRightBottomLeft="0"/>
 			</padding>
-			<Button fx:id="dropZone" styleClass="drag-n-drop" text="%main.vaultDetail.locateEncryptedFileBtn" minWidth="120" maxWidth="180" wrapText="true" textAlignment="CENTER" onAction="#chooseFileAndReveal" contentDisplay="TOP" visible="${!controller.ciphertextPathsCopied}" managed="${!controller.ciphertextPathsCopied}">
+			<Button fx:id="revealEncryptedDropZone" styleClass="drag-n-drop" text="%main.vaultDetail.locateEncryptedFileBtn" minWidth="120" maxWidth="180" prefHeight="72" wrapText="true" textAlignment="CENTER" onAction="#chooseDecryptedFileAndReveal" contentDisplay="TOP" visible="${!controller.ciphertextPathsCopied}" managed="${!controller.ciphertextPathsCopied}">
 				<graphic>
-					<FontAwesome5IconView glyph="FILE_DOWNLOAD" glyphSize="15"/>
+					<Text styleClass="cryptic-text" text="abc → 101010"/>
 				</graphic>
 				<tooltip>
 					<Tooltip text="%main.vaultDetail.locateEncryptedFileBtn.tooltip"/>
 				</tooltip>
 			</Button>
-			<Button styleClass="drag-n-drop" text="%main.vaultDetail.encryptedPathsCopied" minWidth="120" maxWidth="180" wrapText="true" textAlignment="CENTER" onAction="#chooseFileAndReveal" contentDisplay="TOP" visible="${controller.ciphertextPathsCopied}" managed="${controller.ciphertextPathsCopied}">
+			<!-- TODO: instead of showing a button, show on error a small dialog and if copied, show a tooltip -->
+			<Button styleClass="drag-n-drop" text="%main.vaultDetail.encryptedPathsCopied" minWidth="120" maxWidth="180" prefHeight="72" wrapText="true" textAlignment="CENTER" onAction="#chooseDecryptedFileAndReveal" contentDisplay="TOP" visible="${controller.ciphertextPathsCopied}" managed="${controller.ciphertextPathsCopied}">
 				<graphic>
 					<FontAwesome5IconView glyph="CHECK" glyphSize="15"/>
 				</graphic>
 			</Button>
-		</HBox>
+		</StackPane>
+		<!-- decrypt file name -->
+		<Button fx:id="decryptNameDropZone" styleClass="drag-n-drop" text="%main.vaultDetail.decryptName.buttonLabel" minWidth="120" maxWidth="180" prefHeight="72" wrapText="true" textAlignment="CENTER" onAction="#showDecryptNameWindow" contentDisplay="TOP">
+			<graphic>
+				<Text styleClass="cryptic-text" text="101010 → abc"/>
+			</graphic>
+			<tooltip>
+				<Tooltip text="%main.vaultDetail.decryptName.tooltip"/>
+			</tooltip>
+		</Button>
 
 		<Region HBox.hgrow="ALWAYS"/>
 
-		<Button text="%main.vaultDetail.stats" minWidth="120" onAction="#showVaultStatistics" contentDisplay="BOTTOM">
+		<Button text="%main.vaultDetail.stats" minWidth="120" onAction="#showVaultStatistics" contentDisplay="BOTTOM" prefHeight="72">
 			<graphic>
 				<VBox spacing="6">
 					<HBox alignment="CENTER_RIGHT" spacing="6">

+ 17 - 1
src/main/resources/i18n/strings.properties

@@ -425,7 +425,9 @@ main.vaultDetail.stats=Vault Statistics
 main.vaultDetail.locateEncryptedFileBtn=Locate Encrypted File
 main.vaultDetail.locateEncryptedFileBtn.tooltip=Choose a file from your vault to locate its encrypted counterpart
 main.vaultDetail.encryptedPathsCopied=Paths Copied to Clipboard!
-main.vaultDetail.filePickerTitle=Select File Inside Vault
+main.vaultDetail.locateEncrypted.filePickerTitle=Select File Inside Vault
+main.vaultDetail.decryptName.buttonLabel=Decrypt File Name
+main.vaultDetail.decryptName.tooltip=Choose an encrypted vault file to decrypt its name
 ### Missing
 main.vaultDetail.missing.info=Cryptomator could not find a vault at this path.
 main.vaultDetail.missing.recheck=Recheck
@@ -581,6 +583,20 @@ shareVault.hub.instruction.1=1. Share access of the encrypted vault folder via c
 shareVault.hub.instruction.2=2. Grant access to team member in Cryptomator Hub.
 shareVault.hub.openHub=Open Cryptomator Hub
 
+# Decrypt File Names
+decryptNames.title=Decrypt File Names
+decryptNames.filePicker.title=Select encrypted file
+decryptNames.filePicker.extensionDescription=Cryptomator encrypted file
+decryptNames.copyTable.tooltip=Copy table
+decryptNames.clearTable.tooltip=Clear table
+decryptNames.copyHint=Copy cell content with %s
+decryptNames.dropZone.message=Drop files or click to select
+decryptNames.dropZone.error.vaultInternalFiles=Vault internal files with no decrypt-able name selected
+decryptNames.dropZone.error.foreignFiles=Files do not belong to vault "%s"
+decryptNames.dropZone.error.noDirIdBackup=Directory of selected files does not contain dirId.c9r file
+decryptNames.dropZone.error.generic=Failed to decrypt file names
+
+
 # Event View
 eventView.title=Events
 eventView.filter.allVaults=All