Bläddra i källkod

Merge branch 'feature/decrypt-name' into release/1.16.0

# Conflicts:
#	pom.xml
Armin Schrenk 1 månad sedan
förälder
incheckning
30862146b0

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

@@ -412,6 +412,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;
 	}

+ 121 - 51
src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java

@@ -6,6 +6,7 @@ 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;
@@ -39,10 +40,14 @@ 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;
+import java.util.stream.Collectors;
 
 @MainWindowScoped
 public class VaultDetailUnlockedController implements FxController {
@@ -62,11 +67,15 @@ public class VaultDetailUnlockedController implements FxController {
 	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();
+	private final BooleanProperty cleartextNamesCopied = 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) {
@@ -92,72 +101,90 @@ 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 -> handleDragDropped(e, this::getCleartextName, this::copyDecryptedNamesToClipboard));
+		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());
+	public void chooseDecryptedFileAndReveal() {
+		Preconditions.checkState(accessibleViaPath.getValue());
+		var fileChooser = new FileChooser();
+		fileChooser.setTitle(resourceBundle.getString("main.vaultDetail.locateEncrypted.filePickerTitle"));
+		fileChooser.setInitialDirectory(Path.of(mountPoint.getValue()).toFile());
+		var cleartextFile = fileChooser.showOpenDialog(mainWindow);
+		if (cleartextFile != null) {
+			var ciphertextPath = getCiphertextPath(cleartextFile.toPath());
+			if (ciphertextPath != null) {
+				revealOrCopyPaths(List.of(ciphertextPath));
+			}
+		}
 	}
 
 	@FXML
-	public void copyMountUri() {
-		ClipboardContent clipboardContent = new ClipboardContent();
-		clipboardContent.putString(mountPoint.getValue());
-		Clipboard.getSystemClipboard().setContent(clipboardContent);
-	}
+	public void chooseEncryptedFileAndCopyNames() {
+		var fileChooser = new FileChooser();
+		fileChooser.setTitle(resourceBundle.getString("main.vaultDetail.decryptName.filePickerTitle"));
 
-	@FXML
-	public void lock() {
-		appWindows.startLockWorkflow(vault.get(), mainWindow);
+		fileChooser.setInitialDirectory(vault.getValue().getPath().toFile());
+		var ciphertextNode = fileChooser.showOpenDialog(mainWindow);
+		if (ciphertextNode != null) {
+			var nodeName = getCleartextName(ciphertextNode.toPath());
+			copyDecryptedNamesToClipboard(List.of(nodeName));
+		}
 	}
 
-	@FXML
-	public void showVaultStatistics() {
-		vaultStats.getUnchecked(vault.get()).showVaultStatisticsWindow();
+	private void copyDecryptedNamesToClipboard(List<CipherToCleartext> mapping) {
+		if (mapping.size() == 1) {
+			Clipboard.getSystemClipboard().setContent(Map.of(DataFormat.PLAIN_TEXT, mapping.getFirst().cleartext));
+		} else {
+			var content = mapping.stream().map(CipherToCleartext::toString).collect(Collectors.joining("\n"));
+			Clipboard.getSystemClipboard().setContent(Map.of(DataFormat.PLAIN_TEXT, content));
+		}
+		cleartextNamesCopied.setValue(true);
+		CompletableFuture.delayedExecutor(2, TimeUnit.SECONDS, Platform::runLater).execute(() -> {
+			cleartextNamesCopied.set(false);
+		});
 	}
 
-	@FXML
-	public void chooseFileAndReveal() {
-		Preconditions.checkState(accessibleViaPath.getValue());
-		var fileChooser = new FileChooser();
-		fileChooser.setTitle(resourceBundle.getString("main.vaultDetail.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);
+	@Nullable
+	private CipherToCleartext getCleartextName(Path ciphertextNode) {
+		try {
+			return new CipherToCleartext(ciphertextNode.getFileName().toString(), vault.get().getCleartextName(ciphertextNode));
+		} catch (IOException e) {
+			LOG.warn("Failed to decrypt filename for {}", ciphertextNode, e);
+			return null;
 		}
 	}
 
@@ -165,16 +192,17 @@ public class VaultDetailUnlockedController implements FxController {
 		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 +234,40 @@ 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();
+	}
+
+	record CipherToCleartext(String ciphertext, String cleartext) {
+
+		@Override
+		public String toString() {
+			return ciphertext + " > " + cleartext;
+		}
+	}
+
 	/* Getter/Setter */
 
 	public ReadOnlyObjectProperty<Vault> vaultProperty() {
@@ -247,4 +309,12 @@ public class VaultDetailUnlockedController implements FxController {
 	public boolean isCiphertextPathsCopied() {
 		return ciphertextPathsCopied.get();
 	}
+
+	public BooleanProperty cleartextNamesCopiedProperty() {
+		return cleartextNamesCopied;
+	}
+
+	public boolean isCleartextNamesCopied() {
+		return cleartextNamesCopied.get();
+	}
 }

+ 11 - 0
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,13 @@
 	-fx-fill: TEXT_FILL;
 }
 
+.cryptic-text {
+	-fx-background-color: MAIN_BG;
+	-fx-text-fill: TEXT_FILL;
+	-fx-font-family: 'Fira Code';
+	-fx-font-size: 1.1em;
+}
+
 /*******************************************************************************
  *                                                                             *
  * Glyph Icons                                                                 *

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


+ 11 - 0
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                                                       *
@@ -124,6 +128,13 @@
 	-fx-fill: TEXT_FILL;
 }
 
+.cryptic-text {
+	-fx-background-color: MAIN_BG;
+	-fx-text-fill: TEXT_FILL;
+	-fx-font-family: 'Fira Code';
+	-fx-font-size: 1.1em;
+}
+
 /*******************************************************************************
  *                                                                             *
  * Glyph Icons                                                                 *

+ 29 - 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,47 @@
 	<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}">
+			<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>
+		</StackPane>
+		<!-- decrypt file name -->
+		<StackPane>
+			<padding>
+				<Insets topRightBottomLeft="0"/>
+			</padding>
+			<Button fx:id="decryptNameDropZone" styleClass="drag-n-drop" text="%main.vaultDetail.decryptName.buttonLabel" minWidth="120" maxWidth="180" prefHeight="72" wrapText="true" textAlignment="CENTER" onAction="#chooseEncryptedFileAndCopyNames" contentDisplay="TOP" visible="${!controller.cleartextNamesCopied}" managed="${!controller.cleartextNamesCopied}">
+				<graphic>
+					<Text styleClass="cryptic-text" text="101010 → abc"/>
+				</graphic>
+				<tooltip>
+					<Tooltip text="%main.vaultDetail.decryptName.tooltip"/>
+				</tooltip>
+			</Button>
+			<Button styleClass="drag-n-drop" text="%main.vaultDetail.decryptName.copied" minWidth="120" maxWidth="180" prefHeight="72" wrapText="true" textAlignment="CENTER" onAction="#chooseEncryptedFileAndCopyNames" contentDisplay="TOP" visible="${controller.cleartextNamesCopied}" managed="${controller.cleartextNamesCopied}">
 				<graphic>
 					<FontAwesome5IconView glyph="CHECK" glyphSize="15"/>
 				</graphic>
 			</Button>
-		</HBox>
+		</StackPane>
 
 		<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">

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

@@ -423,7 +423,11 @@ 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.filePickerTitle=Select encrypted file
+main.vaultDetail.decryptName.tooltip=Choose an encrypted vault file to decrypt its name
+main.vaultDetail.decryptName.copied=Names copied to clipboard!
 ### Missing
 main.vaultDetail.missing.info=Cryptomator could not find a vault at this path.
 main.vaultDetail.missing.recheck=Recheck