diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavLocatorFactory.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavLocatorFactory.java
index 576ce16f4..7cd819f90 100644
--- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavLocatorFactory.java
+++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavLocatorFactory.java
@@ -9,8 +9,6 @@
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.channels.SeekableByteChannel;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -104,15 +102,7 @@ public class WebDavLocatorFactory extends AbstractLocatorFactory implements Sens
@Override
public void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) throws IOException {
final Path metaDataFile = fsRoot.resolve(encryptedPath);
- final SeekableByteChannel channel = Files.newByteChannel(metaDataFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
- try {
- final ByteBuffer buffer = ByteBuffer.wrap(encryptedMetadata);
- while (channel.write(buffer) > 0) {
- // continue writing.
- }
- } finally {
- channel.close();
- }
+ Files.write(metaDataFile, encryptedMetadata, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
}
@Override
@@ -120,17 +110,8 @@ public class WebDavLocatorFactory extends AbstractLocatorFactory implements Sens
final Path metaDataFile = fsRoot.resolve(encryptedPath);
if (!Files.isReadable(metaDataFile)) {
return null;
- }
- final long metaDataFileSize = Files.size(metaDataFile);
- final SeekableByteChannel channel = Files.newByteChannel(metaDataFile, StandardOpenOption.READ);
- try {
- final ByteBuffer buffer = ByteBuffer.allocate((int) metaDataFileSize);
- while (channel.read(buffer) > 0) {
- // continue reading.
- }
- return buffer.array();
- } finally {
- channel.close();
+ } else {
+ return Files.readAllBytes(metaDataFile);
}
}
diff --git a/main/ui/pom.xml b/main/ui/pom.xml
index 6c1491b99..f1b6a4fa8 100644
--- a/main/ui/pom.xml
+++ b/main/ui/pom.xml
@@ -21,6 +21,7 @@
Cryptomator
org.cryptomator.ui.MainApplication
${java.home}/../lib/ant-javafx.jar
+ 8.20.8
@@ -48,6 +49,13 @@
org.apache.commons
commons-lang3
+
+
+
+ org.controlsfx
+ controlsfx
+ ${controlsfx.version}
+
diff --git a/main/ui/src/main/java/org/cryptomator/ui/InitializeController.java b/main/ui/src/main/java/org/cryptomator/ui/InitializeController.java
index 5ff50dad8..368910805 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/InitializeController.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/InitializeController.java
@@ -11,18 +11,24 @@ package org.cryptomator.ui;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
+import java.nio.file.DirectoryStream;
import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
+import java.util.Optional;
import java.util.ResourceBundle;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
+import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
@@ -34,6 +40,7 @@ import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.ui.controls.ClearOnDisableListener;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Directory;
+import org.cryptomator.ui.util.EncryptingFileVisitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -116,6 +123,9 @@ public class InitializeController implements Initializable {
@FXML
protected void initializeVault(ActionEvent event) {
+ if (!isDirectoryEmpty() && !shouldEncryptExistingFiles()) {
+ return;
+ }
final String masterKeyFileName = usernameField.getText() + Aes256Cryptor.MASTERKEY_FILE_EXT;
final Path masterKeyPath = directory.getPath().resolve(masterKeyFileName);
final CharSequence password = passwordField.getCharacters();
@@ -124,6 +134,7 @@ public class InitializeController implements Initializable {
masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
directory.getCryptor().randomizeMasterKey();
directory.getCryptor().encryptMasterKey(masterKeyOutputStream, password);
+ encryptExistingContents();
directory.getCryptor().swipeSensitiveData();
if (listener != null) {
listener.didInitialize(this);
@@ -141,6 +152,36 @@ public class InitializeController implements Initializable {
IOUtils.closeQuietly(masterKeyOutputStream);
}
}
+
+ private boolean isDirectoryEmpty() {
+ try {
+ final DirectoryStream dirContents = Files.newDirectoryStream(directory.getPath());
+ return !dirContents.iterator().hasNext();
+ } catch (IOException e) {
+ LOG.error("Failed to analyze directory.", e);
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private boolean shouldEncryptExistingFiles() {
+ final Alert alert = new Alert(AlertType.CONFIRMATION);
+ alert.setTitle(localization.getString("initialize.alert.directoryIsNotEmpty.title"));
+ alert.setHeaderText(localization.getString("initialize.alert.directoryIsNotEmpty.header"));
+ alert.setContentText(localization.getString("initialize.alert.directoryIsNotEmpty.content"));
+
+ final Optional result = alert.showAndWait();
+ return ButtonType.OK.equals(result.get());
+ }
+
+ private void encryptExistingContents() throws IOException {
+ final FileVisitor visitor = new EncryptingFileVisitor(directory.getPath(), directory.getCryptor(), this::shouldEncryptExistingFile);
+ Files.walkFileTree(directory.getPath(), visitor);
+ }
+
+ private boolean shouldEncryptExistingFile(Path path) {
+ final String name = path.getFileName().toString();
+ return !directory.getPath().equals(path) && !name.endsWith(Aes256Cryptor.BASIC_FILE_EXT) && !name.endsWith(Aes256Cryptor.METADATA_FILE_EXT) && !name.endsWith(Aes256Cryptor.MASTERKEY_FILE_EXT);
+ }
/* Getter/Setter */
diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/EncryptingFileVisitor.java b/main/ui/src/main/java/org/cryptomator/ui/util/EncryptingFileVisitor.java
new file mode 100644
index 000000000..dcfbaec5c
--- /dev/null
+++ b/main/ui/src/main/java/org/cryptomator/ui/util/EncryptingFileVisitor.java
@@ -0,0 +1,78 @@
+package org.cryptomator.ui.util;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
+import java.nio.file.attribute.BasicFileAttributes;
+
+import org.cryptomator.crypto.Cryptor;
+import org.cryptomator.crypto.CryptorIOSupport;
+
+public class EncryptingFileVisitor extends SimpleFileVisitor implements CryptorIOSupport {
+
+ private final Path rootDir;
+ private final Cryptor cryptor;
+ private final EncryptionDecider encryptionDecider;
+ private Path currentDir;
+
+ public EncryptingFileVisitor(Path rootDir, Cryptor cryptor, EncryptionDecider encryptionDecider) {
+ this.rootDir = rootDir;
+ this.cryptor = cryptor;
+ this.encryptionDecider = encryptionDecider;
+ }
+
+ @Override
+ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
+ if (rootDir.equals(dir) || encryptionDecider.shouldEncrypt(dir)) {
+ this.currentDir = dir;
+ return FileVisitResult.CONTINUE;
+ } else {
+ return FileVisitResult.SKIP_SUBTREE;
+ }
+ }
+
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ if (encryptionDecider.shouldEncrypt(file)) {
+ final String plaintext = file.getFileName().toString();
+ final String encrypted = cryptor.encryptPath(plaintext, '/', '/', this);
+ final Path newPath = file.resolveSibling(encrypted);
+ Files.move(file, newPath, StandardCopyOption.ATOMIC_MOVE);
+ }
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+ if (encryptionDecider.shouldEncrypt(dir)) {
+ final String plaintext = dir.getFileName().toString();
+ final String encrypted = cryptor.encryptPath(plaintext, '/', '/', this);
+ final Path newPath = dir.resolveSibling(encrypted);
+ Files.move(dir, newPath, StandardCopyOption.ATOMIC_MOVE);
+ }
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public void writePathSpecificMetadata(String metadataFile, byte[] encryptedMetadata) throws IOException {
+ final Path path = currentDir.resolve(metadataFile);
+ Files.write(path, encryptedMetadata, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
+ }
+
+ @Override
+ public byte[] readPathSpecificMetadata(String metadataFile) throws IOException {
+ final Path path = currentDir.resolve(metadataFile);
+ return Files.readAllBytes(path);
+ }
+
+ /* callback */
+
+ public interface EncryptionDecider {
+ boolean shouldEncrypt(Path path);
+ }
+
+}
diff --git a/main/ui/src/main/resources/localization.properties b/main/ui/src/main/resources/localization.properties
index 8a8e2c5a5..165304d4f 100644
--- a/main/ui/src/main/resources/localization.properties
+++ b/main/ui/src/main/resources/localization.properties
@@ -17,6 +17,9 @@ initialize.label.username=Username
initialize.label.password=Password
initialize.label.retypePassword=Retype password
initialize.button.ok=Create vault
+initialize.alert.directoryIsNotEmpty.title=Confirm
+initialize.alert.directoryIsNotEmpty.header=The chosen directory is not empty.
+initialize.alert.directoryIsNotEmpty.content=All existing files inside this directory will get encrypted. Continue?
# unlock.fxml