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

# Conflicts:
#	pom.xml
This commit is contained in:
Armin Schrenk
2025-03-18 11:21:54 +01:00
7 changed files with 193 additions and 65 deletions

View File

@@ -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;
}

View File

@@ -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));
EasyBind.includeWhen(dropZone.getStyleClass(), ACTIVE_CLASS, draggingOver);
decryptNameDropZone.setOnDragOver(e -> handleDragOver(e, draggingOverDecryptName));
decryptNameDropZone.setOnDragDropped(e -> handleDragDropped(e, this::getCleartextName, this::copyDecryptedNamesToClipboard));
decryptNameDropZone.setOnDragExited(_ -> draggingOverDecryptName.setValue(false));
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()) {
wrongFileAlert.build().showWrongFileAlertWindow();
} else {
revealOrCopyPaths(ciphertextPaths);
}
event.setDropCompleted(!ciphertextPaths.isEmpty());
event.consume();
} else if (DragEvent.DRAG_EXITED.equals(event.getEventType())) {
draggingOver.set(false);
prop.set(true);
}
}
private VaultStatisticsComponent buildVaultStats(Vault vault) {
return vaultStatsBuilder.vault(vault).build();
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 {
positiveAction.accept(objects);
}
event.setDropCompleted(!objects.isEmpty());
event.consume();
}
}
@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 chooseEncryptedFileAndCopyNames() {
var fileChooser = new FileChooser();
fileChooser.setTitle(resourceBundle.getString("main.vaultDetail.decryptName.filePickerTitle"));
fileChooser.setInitialDirectory(vault.getValue().getPath().toFile());
var ciphertextNode = fileChooser.showOpenDialog(mainWindow);
if (ciphertextNode != null) {
var nodeName = getCleartextName(ciphertextNode.toPath());
copyDecryptedNamesToClipboard(List.of(nodeName));
}
}
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);
});
}
@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();
}
}

View File

@@ -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 *

Binary file not shown.

View File

@@ -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 *

View File

@@ -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>
</HBox>
</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>
</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">

View File

@@ -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