Quellcode durchsuchen

- Encrypt existing directory content on vault initialization

Sebastian Stenzel vor 10 Jahren
Ursprung
Commit
2fdf9be017

+ 3 - 22
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);
 		}
 	}
 

+ 8 - 0
main/ui/pom.xml

@@ -21,6 +21,7 @@
 		<javafx.application.name>Cryptomator</javafx.application.name>
 		<exec.mainClass>org.cryptomator.ui.MainApplication</exec.mainClass>
 		<javafx.tools.ant.jar>${java.home}/../lib/ant-javafx.jar</javafx.tools.ant.jar>
+		<controlsfx.version>8.20.8</controlsfx.version>
 	</properties>
 
 	<dependencies>
@@ -48,6 +49,13 @@
 			<groupId>org.apache.commons</groupId>
 			<artifactId>commons-lang3</artifactId>
 		</dependency>
+		
+		<!-- UI -->
+		<dependency>
+		    <groupId>org.controlsfx</groupId>
+		    <artifactId>controlsfx</artifactId>
+		    <version>${controlsfx.version}</version>
+		</dependency>
 	</dependencies>
 
 	<build>

+ 41 - 0
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<Path> 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<ButtonType> result = alert.showAndWait();
+		return ButtonType.OK.equals(result.get());
+	}
+	
+	private void encryptExistingContents() throws IOException {
+		final FileVisitor<Path> 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 */
 

+ 78 - 0
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<Path> 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);
+	}
+
+}

+ 3 - 0
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