- Fixed initial encryption of vaults, that already contain files

- Disabled some UI controls during background tasks
- Simplified background vs UI thread switches using https://github.com/totalvoidness/FXThreads
This commit is contained in:
Sebastian Stenzel
2014-12-24 14:10:30 +01:00
parent be5cf287c8
commit 3f32e4ee4b
7 changed files with 260 additions and 48 deletions

View File

@@ -1,6 +1,8 @@
package org.cryptomator.files;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -24,7 +26,7 @@ public class EncryptingFileVisitor extends SimpleFileVisitor<Path> implements Cr
this.cryptor = cryptor;
this.encryptionDecider = encryptionDecider;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
if (rootDir.equals(dir) || encryptionDecider.shouldEncrypt(dir)) {
@@ -36,12 +38,15 @@ public class EncryptingFileVisitor extends SimpleFileVisitor<Path> implements Cr
}
@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);
public FileVisitResult visitFile(Path plaintextFile, BasicFileAttributes attrs) throws IOException {
if (encryptionDecider.shouldEncrypt(plaintextFile)) {
final String plaintextName = plaintextFile.getFileName().toString();
final String encryptedName = cryptor.encryptPath(plaintextName, '/', '/', this);
final Path encryptedPath = plaintextFile.resolveSibling(encryptedName);
final InputStream plaintextIn = Files.newInputStream(plaintextFile, StandardOpenOption.READ);
final SeekableByteChannel ciphertextOut = Files.newByteChannel(encryptedPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
cryptor.encryptFile(plaintextIn, ciphertextOut);
Files.delete(plaintextFile);
}
return FileVisitResult.CONTINUE;
}
@@ -68,9 +73,9 @@ public class EncryptingFileVisitor extends SimpleFileVisitor<Path> implements Cr
final Path path = currentDir.resolve(metadataFile);
return Files.readAllBytes(path);
}
/* callback */
public interface EncryptionDecider {
boolean shouldEncrypt(Path path);
}

View File

@@ -20,6 +20,7 @@ import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.Future;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
@@ -30,6 +31,7 @@ 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.ProgressIndicator;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
@@ -41,6 +43,7 @@ import org.cryptomator.files.EncryptingFileVisitor;
import org.cryptomator.ui.controls.ClearOnDisableListener;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Directory;
import org.cryptomator.ui.util.FXThreads;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -65,6 +68,9 @@ public class InitializeController implements Initializable {
@FXML
private Button okButton;
@FXML
private ProgressIndicator progressIndicator;
@FXML
private Label messageLabel;
@@ -123,6 +129,7 @@ public class InitializeController implements Initializable {
@FXML
protected void initializeVault(ActionEvent event) {
setControlsDisabled(true);
if (!isDirectoryEmpty() && !shouldEncryptExistingFiles()) {
return;
}
@@ -131,18 +138,29 @@ public class InitializeController implements Initializable {
final CharSequence password = passwordField.getCharacters();
OutputStream masterKeyOutputStream = null;
try {
progressIndicator.setVisible(true);
masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
directory.getCryptor().encryptMasterKey(masterKeyOutputStream, password);
encryptExistingContents();
directory.getCryptor().swipeSensitiveData();
if (listener != null) {
listener.didInitialize(this);
}
final Future<?> futureDone = FXThreads.runOnBackgroundThread(this::encryptExistingContents);
FXThreads.runOnMainThreadWhenFinished(futureDone, (result) -> {
progressIndicator.setVisible(false);
progressIndicator.setVisible(false);
directory.getCryptor().swipeSensitiveData();
if (listener != null) {
listener.didInitialize(this);
}
});
} catch (FileAlreadyExistsException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
} catch (InvalidPathException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageLabel.setText(localization.getString("initialize.messageLabel.invalidPath"));
} catch (IOException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
LOG.error("I/O Exception", ex);
} finally {
usernameField.setText(null);
@@ -152,6 +170,13 @@ public class InitializeController implements Initializable {
}
}
private void setControlsDisabled(boolean disable) {
usernameField.setDisable(disable);
passwordField.setDisable(disable);
retypePasswordField.setDisable(disable);
okButton.setDisable(disable);
}
private boolean isDirectoryEmpty() {
try {
final DirectoryStream<Path> dirContents = Files.newDirectoryStream(directory.getPath());
@@ -172,9 +197,13 @@ public class InitializeController implements Initializable {
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 void encryptExistingContents() {
try {
final FileVisitor<Path> visitor = new EncryptingFileVisitor(directory.getPath(), directory.getCryptor(), this::shouldEncryptExistingFile);
Files.walkFileTree(directory.getPath(), visitor);
} catch (IOException ex) {
LOG.error("I/O Exception", ex);
}
}
private boolean shouldEncryptExistingFile(Path path) {

View File

@@ -16,12 +16,14 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ResourceBundle;
import java.util.concurrent.Future;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
@@ -34,6 +36,7 @@ import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Directory;
import org.cryptomator.ui.util.FXThreads;
import org.cryptomator.ui.util.MasterKeyFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -51,7 +54,10 @@ public class UnlockController implements Initializable {
@FXML
private SecPasswordField passwordField;
@FXML
private Button unlockButton;
@FXML
private ProgressIndicator progressIndicator;
@@ -82,6 +88,7 @@ public class UnlockController implements Initializable {
@FXML
private void didClickUnlockButton(ActionEvent event) {
setControlsDisabled(true);
final String masterKeyFileName = usernameBox.getValue() + Aes256Cryptor.MASTERKEY_FILE_EXT;
final Path masterKeyPath = directory.getPath().resolve(masterKeyFileName);
final CharSequence password = passwordField.getCharacters();
@@ -96,15 +103,23 @@ public class UnlockController implements Initializable {
return;
}
directory.setUnlocked(true);
directory.mountAsync(this::didUnlockAndMount);
final Future<Boolean> futureMount = FXThreads.runOnBackgroundThread(directory::mount);
FXThreads.runOnMainThreadWhenFinished(futureMount, this::didUnlockAndMount);
FXThreads.runOnMainThreadWhenFinished(futureMount, (result) -> {
setControlsDisabled(false);
});
} catch (DecryptFailedException | IOException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageLabel.setText(rb.getString("unlock.errorMessage.decryptionFailed"));
LOG.error("Decryption failed for technical reasons.", ex);
} catch (WrongPasswordException e) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageLabel.setText(rb.getString("unlock.errorMessage.wrongPassword"));
passwordField.requestFocus();
} catch (UnsupportedKeyLengthException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageLabel.setText(rb.getString("unlock.errorMessage.unsupportedKeyLengthInstallJCE"));
LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex);
@@ -114,6 +129,12 @@ public class UnlockController implements Initializable {
}
}
private void setControlsDisabled(boolean disable) {
usernameBox.setDisable(disable);
passwordField.setDisable(disable);
unlockButton.setDisable(disable);
}
private void findExistingUsernames() {
try {
DirectoryStream<Path> ds = MasterKeyFilter.filteredDirectory(directory.getPath());
@@ -132,15 +153,12 @@ public class UnlockController implements Initializable {
LOG.trace("Invalid path: " + directory.getPath(), e);
}
}
private Void didUnlockAndMount(boolean mountSuccess) {
Platform.runLater(() -> {
progressIndicator.setVisible(false);
if (listener != null) {
listener.didUnlock(this);
}
});
return null;
private void didUnlockAndMount(boolean mountSuccess) {
progressIndicator.setVisible(false);
if (listener != null) {
listener.didUnlock(this);
}
}
/* Getter/Setter */

View File

@@ -4,13 +4,9 @@ import java.io.IOException;
import java.io.Serializable;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.util.Callback;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.SamplingDecorator;
@@ -71,21 +67,7 @@ public class Directory implements Serializable {
}
}
public void mountAsync(Callback<Boolean, Void> callback) {
final FutureTask<Boolean> mountTask = new FutureTask<>(this::mount);
final Executor exec = Executors.newSingleThreadExecutor();
exec.execute(mountTask);
exec.execute(() -> {
try {
final Boolean result = mountTask.get();
callback.call(result);
} catch (Exception e) {
callback.call(false);
}
});
}
private boolean mount() {
public boolean mount() {
try {
webDavMount = WebDavMounter.mount(server.getPort());
return true;

View File

@@ -0,0 +1,175 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* https://github.com/totalvoidness/FXThreads
*
* Contributors:
* Sebastian Stenzel
******************************************************************************/
package org.cryptomator.ui.util;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javafx.application.Platform;
/**
* Use this utility class to spawn background tasks and wait for them to finish. <br/>
* <br/>
* <strong>Example use (ignoring exceptions):</strong>
*
* <pre>
* // get some string from a remote server:
* Future&lt;String&gt; futureBookName = runOnBackgroundThread(restResource::getBookName);
*
* // when done, update text label:
* runOnMainThreadWhenFinished(futureBookName, (bookName) -&gt; {
* myLabel.setText(bookName);
* });
* </pre>
*
* <strong>Example use (exception-aware):</strong>
*
* <pre>
* // get some string from a remote server:
* Future&lt;String&gt; futureBookName = runOnBackgroundThread(restResource::getBookName);
*
* // when done, update text label:
* runOnMainThreadWhenFinished(futureBookName, (bookName) -&gt; {
* myLabel.setText(bookName);
* }, (exception) -&gt; {
* myLabel.setText(&quot;An exception occured: &quot; + exception.getMessage());
* });
* </pre>
*/
public final class FXThreads {
private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool();
private static final CallbackWhenTaskFailed DUMMY_EXCEPTION_CALLBACK = (e) -> {
// ignore.
};
private FXThreads() {
throw new AssertionError("Not instantiable.");
}
/**
* Executes the given task on a background thread. If you want to react on the result on your JavaFX main thread, use
* {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
*
* <pre>
* // examples:
*
* Future&lt;String&gt; futureBookName1 = runOnBackgroundThread(restResource::getBookName);
*
* Future&lt;String&gt; futureBookName2 = runOnBackgroundThread(() -&gt; {
* return restResource.getBookName();
* });
* </pre>
*
* @param task The task to be executed on a background thread.
* @return A future result object, which you can use in {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
*/
public static <T> Future<T> runOnBackgroundThread(Callable<T> task) {
return EXECUTOR.submit(task);
}
/**
* Executes the given task on a background thread. If you want to react on the result on your JavaFX main thread, use
* {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
*
* <pre>
* // examples:
*
* Future&lt;?&gt; futureDone1 = runOnBackgroundThread(this::doSomeComplexCalculation);
*
* Future&lt;?&gt; futureDone2 = runOnBackgroundThread(() -&gt; {
* doSomeComplexCalculation();
* });
* </pre>
*
* @param task The task to be executed on a background thread.
* @return A future result object, which you can use in {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
*/
public static Future<?> runOnBackgroundThread(Runnable task) {
return EXECUTOR.submit(task);
}
/**
* Waits for the given task to complete and notifies the given successCallback. If an exception occurs, the callback will never be
* called. If you are interested in the exception, use
* {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
*
* <pre>
* // example:
*
* runOnMainThreadWhenFinished(futureBookName, (bookName) -&gt; {
* myLabel.setText(bookName);
* });
* </pre>
*
* @param task The task to wait for.
* @param successCallback The action to perform, when the task finished.
*/
public static <T> void runOnMainThreadWhenFinished(Future<T> task, CallbackWhenTaskFinished<T> successCallback) {
runOnBackgroundThread(() -> {
return "asd";
});
FXThreads.runOnMainThreadWhenFinished(task, successCallback, DUMMY_EXCEPTION_CALLBACK);
}
/**
* Waits for the given task to complete and notifies the given successCallback. If an exception occurs, the callback will never be
* called. If you are interested in the exception, use
* {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
*
* <pre>
* // example:
*
* runOnMainThreadWhenFinished(futureBookNamePossiblyFailing, (bookName) -&gt; {
* myLabel.setText(bookName);
* }, (exception) -&gt; {
* myLabel.setText(&quot;An exception occured: &quot; + exception.getMessage());
* });
* </pre>
*
* @param task The task to wait for.
* @param successCallback The action to perform, when the task finished.
*/
public static <T> void runOnMainThreadWhenFinished(Future<T> task, CallbackWhenTaskFinished<T> successCallback, CallbackWhenTaskFailed exceptionCallback) {
assertParamNotNull(task, "task must not be null.");
assertParamNotNull(successCallback, "successCallback must not be null.");
assertParamNotNull(exceptionCallback, "exceptionCallback must not be null.");
EXECUTOR.execute(() -> {
try {
final T result = task.get();
Platform.runLater(() -> {
successCallback.taskFinished(result);
});
} catch (Exception e) {
Platform.runLater(() -> {
exceptionCallback.taskFailed(e);
});
}
});
}
private static void assertParamNotNull(Object param, String msg) {
if (param == null) {
throw new IllegalArgumentException(msg);
}
}
public interface CallbackWhenTaskFinished<T> {
void taskFinished(T result);
}
public interface CallbackWhenTaskFailed {
void taskFailed(Throwable t);
}
}

View File

@@ -44,6 +44,9 @@
<!-- Row 3 -->
<Button fx:id="okButton" defaultButton="true" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" text="%initialize.button.ok" prefWidth="150.0" onAction="#initializeVault" focusTraversable="false" disable="true" />
<!-- Row 4 -->
<ProgressIndicator progress="-1" fx:id="progressIndicator" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="CENTER" visible="false"/>
<!-- Row 5 -->
<Label fx:id="messageLabel" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="2" />
</children>

View File

@@ -38,7 +38,7 @@
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
<!-- Row 2 -->
<Button text="%unlock.button.unlock" defaultButton="true" GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickUnlockButton" focusTraversable="false"/>
<Button fx:id="unlockButton" text="%unlock.button.unlock" defaultButton="true" GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickUnlockButton"/>
<!-- Row 3 -->
<ProgressIndicator progress="-1" fx:id="progressIndicator" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="CENTER" visible="false"/>