Преглед на файлове

- increased vault version
- Showing "per vault" MAC authentication failure dialogs

Sebastian Stenzel преди 10 години
родител
ревизия
0d3a5b4e70

+ 6 - 8
main/core/src/main/java/org/cryptomator/webdav/WebDavServer.java

@@ -84,13 +84,11 @@ public final class WebDavServer {
 	/**
 	 * @param workDir Path of encrypted folder.
 	 * @param cryptor A fully initialized cryptor instance ready to en- or decrypt streams.
-	 * @param failingMacCollection A (observable, thread-safe) collection, to which the names of resources are written, whose MAC
-	 *            authentication fails.
-	 * @param name The name of the folder. Must be non-empty and only contain any of
-	 *            _ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
+	 * @param failingMacCollection A (observable, thread-safe) collection, to which the names of resources are written, whose MAC authentication fails.
+	 * @param name The name of the folder. Must be non-empty and only contain any of _ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
 	 * @return servlet
 	 */
-	public ServletLifeCycleAdapter createServlet(final Path workDir, final Cryptor cryptor, final Collection<String> failingMacCollection, final String name) {
+	public ServletLifeCycleAdapter createServlet(final Path workDir, final Cryptor cryptor, final Collection<String> failingMacCollection, final Collection<String> whitelistedResourceCollection, final String name) {
 		try {
 			if (StringUtils.isEmpty(name)) {
 				throw new IllegalArgumentException("name empty");
@@ -101,7 +99,7 @@ public final class WebDavServer {
 			final URI uri = new URI(null, null, localConnector.getHost(), localConnector.getLocalPort(), "/" + UUID.randomUUID().toString() + "/" + name, null, null);
 
 			final ServletContextHandler servletContext = new ServletContextHandler(servletCollection, uri.getRawPath(), ServletContextHandler.SESSIONS);
-			final ServletHolder servlet = getWebDavServletHolder(workDir.toString(), cryptor, failingMacCollection);
+			final ServletHolder servlet = getWebDavServletHolder(workDir.toString(), cryptor, failingMacCollection, whitelistedResourceCollection);
 			servletContext.addServlet(servlet, "/*");
 
 			servletCollection.mapContexts();
@@ -113,8 +111,8 @@ public final class WebDavServer {
 		}
 	}
 
-	private ServletHolder getWebDavServletHolder(final String workDir, final Cryptor cryptor, final Collection<String> failingMacCollection) {
-		final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor, failingMacCollection));
+	private ServletHolder getWebDavServletHolder(final String workDir, final Cryptor cryptor, final Collection<String> failingMacCollection, final Collection<String> whitelistedResourceCollection) {
+		final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor, failingMacCollection, whitelistedResourceCollection));
 		result.setInitParameter(WebDavServlet.CFG_FS_ROOT, workDir);
 		return result;
 	}

+ 11 - 4
main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoWarningHandler.java

@@ -5,15 +5,22 @@ import java.util.Collection;
 class CryptoWarningHandler {
 
 	private final Collection<String> resourcesWithInvalidMac;
+	private final Collection<String> whitelistedResources;
 
-	public CryptoWarningHandler(Collection<String> resourcesWithInvalidMac) {
+	public CryptoWarningHandler(Collection<String> resourcesWithInvalidMac, Collection<String> whitelistedResources) {
 		this.resourcesWithInvalidMac = resourcesWithInvalidMac;
+		this.whitelistedResources = whitelistedResources;
 	}
 
-	public void macAuthFailed(String resourceName) {
-		if (!resourcesWithInvalidMac.contains(resourceName)) {
-			resourcesWithInvalidMac.add(resourceName);
+	public void macAuthFailed(String resourcePath) {
+		// collection might be a list, but we don't want duplicates:
+		if (!resourcesWithInvalidMac.contains(resourcePath)) {
+			resourcesWithInvalidMac.add(resourcePath);
 		}
 	}
 
+	public boolean ignoreMac(String resourcePath) {
+		return whitelistedResources.contains(resourcePath);
+	}
+
 }

+ 1 - 1
main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java

@@ -107,8 +107,8 @@ class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
 				LOG.warn("Unexpected end of stream (possibly client hung up).");
 			} catch (MacAuthenticationFailedException e) {
 				LOG.warn("File integrity violation for " + getLocator().getResourcePath());
+				cryptoWarningHandler.macAuthFailed(getLocator().getResourcePath());
 				throw new IOException("Error decrypting file " + filePath.toString(), e);
-				// cryptoWarningHandler.macAuthFailed(getLocator().getResourcePath());
 			} catch (DecryptFailedException e) {
 				throw new IOException("Error decrypting file " + filePath.toString(), e);
 			}

+ 8 - 3
main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java

@@ -20,6 +20,7 @@ import org.apache.jackrabbit.webdav.io.OutputContext;
 import org.apache.jackrabbit.webdav.lock.LockManager;
 import org.cryptomator.crypto.Cryptor;
 import org.cryptomator.crypto.exceptions.DecryptFailedException;
+import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
 import org.eclipse.jetty.http.HttpHeader;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -111,19 +112,23 @@ class EncryptedFilePart extends EncryptedFile {
 	public void spool(OutputContext outputContext) throws IOException {
 		assert Files.isRegularFile(filePath);
 		outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis());
-		try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.READ)) {
-			final Long fileSize = cryptor.decryptedContentLength(channel);
+		try (final SeekableByteChannel c = Files.newByteChannel(filePath, StandardOpenOption.READ)) {
+			final Long fileSize = cryptor.decryptedContentLength(c);
 			final Pair<Long, Long> range = getUnionRange(fileSize);
 			final Long rangeLength = range.getRight() - range.getLeft() + 1;
 			outputContext.setContentLength(rangeLength);
 			outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getLeft(), range.getRight(), fileSize));
 			if (outputContext.hasStream()) {
-				cryptor.decryptRange(channel, outputContext.getOutputStream(), range.getLeft(), rangeLength);
+				cryptor.decryptRange(c, outputContext.getOutputStream(), range.getLeft(), rangeLength);
 			}
 		} catch (EOFException e) {
 			if (LOG.isDebugEnabled()) {
 				LOG.trace("Unexpected end of stream during delivery of partial content (client hung up).");
 			}
+		} catch (MacAuthenticationFailedException e) {
+			LOG.warn("File integrity violation for " + getLocator().getResourcePath());
+			cryptoWarningHandler.macAuthFailed(getLocator().getResourcePath());
+			throw new IOException("Error decrypting file " + filePath.toString(), e);
 		} catch (DecryptFailedException e) {
 			throw new IOException("Error decrypting file " + filePath.toString(), e);
 		}

+ 2 - 2
main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java

@@ -31,10 +31,10 @@ public class WebDavServlet extends AbstractWebdavServlet {
 	private final Cryptor cryptor;
 	private final CryptoWarningHandler cryptoWarningHandler;
 
-	public WebDavServlet(final Cryptor cryptor, final Collection<String> failingMacCollection) {
+	public WebDavServlet(final Cryptor cryptor, final Collection<String> failingMacCollection, final Collection<String> whitelistedResourceCollection) {
 		super();
 		this.cryptor = cryptor;
-		this.cryptoWarningHandler = new CryptoWarningHandler(failingMacCollection);
+		this.cryptoWarningHandler = new CryptoWarningHandler(failingMacCollection, whitelistedResourceCollection);
 	}
 
 	@Override

+ 1 - 1
main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/KeyFile.java

@@ -7,7 +7,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
 @JsonPropertyOrder(value = {"version", "scryptSalt", "scryptCostParam", "scryptBlockSize", "keyLength", "primaryMasterKey", "hMacMasterKey"})
 public class KeyFile implements Serializable {
 
-	static final Integer CURRENT_VERSION = 1;
+	static final Integer CURRENT_VERSION = 2;
 	private static final long serialVersionUID = 8578363158959619885L;
 
 	private Integer version;

+ 16 - 9
main/ui/src/main/java/org/cryptomator/ui/controllers/MacWarningsController.java

@@ -1,8 +1,8 @@
 package org.cryptomator.ui.controllers;
 
 import javafx.application.Application;
+import javafx.collections.ListChangeListener;
 import javafx.collections.ListChangeListener.Change;
-import javafx.collections.ObservableList;
 import javafx.collections.WeakListChangeListener;
 import javafx.event.ActionEvent;
 import javafx.fxml.FXML;
@@ -11,14 +11,18 @@ import javafx.stage.Stage;
 
 import javax.inject.Inject;
 
+import org.cryptomator.ui.model.Vault;
+
 public class MacWarningsController {
 
 	@FXML
 	private ListView<String> warningsList;
 
-	private Stage stage;
-
 	private final Application application;
+	private final ListChangeListener<? super String> macWarningsListener = this::warningsDidChange;
+	private final ListChangeListener<? super String> weakMacWarningsListener = new WeakListChangeListener<>(macWarningsListener);
+	private Stage stage;
+	private Vault vault;
 
 	@Inject
 	public MacWarningsController(Application application) {
@@ -27,6 +31,7 @@ public class MacWarningsController {
 
 	@FXML
 	private void didClickDismissButton(ActionEvent event) {
+		warningsList.getItems().removeListener(weakMacWarningsListener);
 		stage.hide();
 	}
 
@@ -35,14 +40,10 @@ public class MacWarningsController {
 		application.getHostServices().showDocument("https://cryptomator.org/help.html#macWarning");
 	}
 
-	public void setMacWarnings(ObservableList<String> macWarnings) {
-		this.warningsList.setItems(macWarnings);
-		this.warningsList.getItems().addListener(new WeakListChangeListener<String>(this::warningsDidChange));
-	}
-
 	// closes this window automatically, if all warnings disappeared (e.g. due to an unmount event)
 	private void warningsDidChange(Change<? extends String> change) {
-		if (change.getList().isEmpty()) {
+		if (change.getList().isEmpty() && stage != null) {
+			change.getList().removeListener(weakMacWarningsListener);
 			stage.hide();
 		}
 	}
@@ -55,4 +56,10 @@ public class MacWarningsController {
 		this.stage = stage;
 	}
 
+	public void setVault(Vault vault) {
+		this.vault = vault;
+		this.warningsList.setItems(vault.getNamesOfResourcesWithInvalidMac());
+		this.warningsList.getItems().addListener(weakMacWarningsListener);
+	}
+
 }

+ 0 - 53
main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java

@@ -13,25 +13,21 @@ import java.io.IOException;
 import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.ResourceBundle;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.stream.Collectors;
 
 import javafx.application.Platform;
 import javafx.collections.FXCollections;
 import javafx.collections.ListChangeListener;
 import javafx.collections.ObservableList;
-import javafx.collections.SetChangeListener;
 import javafx.event.ActionEvent;
 import javafx.fxml.FXML;
 import javafx.fxml.FXMLLoader;
 import javafx.fxml.Initializable;
 import javafx.geometry.Side;
 import javafx.scene.Parent;
-import javafx.scene.Scene;
 import javafx.scene.control.ContextMenu;
 import javafx.scene.control.ListCell;
 import javafx.scene.control.ListView;
@@ -51,8 +47,6 @@ import org.cryptomator.ui.controls.DirectoryListCell;
 import org.cryptomator.ui.model.Vault;
 import org.cryptomator.ui.model.VaultFactory;
 import org.cryptomator.ui.settings.Settings;
-import org.cryptomator.ui.util.ActiveWindowStyleSupport;
-import org.cryptomator.ui.util.ObservableSetAggregator;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -85,9 +79,6 @@ public class MainController implements Initializable, InitializationListener, Un
 	private final ControllerFactory controllerFactory;
 	private final Settings settings;
 	private final VaultFactory vaultFactoy;
-	private final ObservableList<String> aggregatedMacWarnings;
-	private final SetChangeListener<String> macWarningsAggregator;
-	private final AtomicBoolean macWarningsWindowVisible;
 
 	private ResourceBundle rb;
 
@@ -97,9 +88,6 @@ public class MainController implements Initializable, InitializationListener, Un
 		this.controllerFactory = controllerFactory;
 		this.settings = settings;
 		this.vaultFactoy = vaultFactoy;
-		this.aggregatedMacWarnings = FXCollections.observableList(new ArrayList<>());
-		this.macWarningsAggregator = new ObservableSetAggregator<>(this.aggregatedMacWarnings);
-		this.macWarningsWindowVisible = new AtomicBoolean();
 	}
 
 	@Override
@@ -110,8 +98,6 @@ public class MainController implements Initializable, InitializationListener, Un
 		vaultList.setItems(items);
 		vaultList.setCellFactory(this::createDirecoryListCell);
 		vaultList.getSelectionModel().getSelectedItems().addListener(this::selectedVaultDidChange);
-
-		aggregatedMacWarnings.addListener(this::macWarningsDidChange);
 	}
 
 	@FXML
@@ -233,12 +219,6 @@ public class MainController implements Initializable, InitializationListener, Un
 		showChangePasswordView(selectedVault);
 	}
 
-	private void macWarningsDidChange(ListChangeListener.Change<? extends String> change) {
-		if (aggregatedMacWarnings.size() > 0) {
-			Platform.runLater(this::showMacWarningsWindow);
-		}
-	}
-
 	// ****************************************
 	// Subcontroller for right panel
 	// ****************************************
@@ -293,7 +273,6 @@ public class MainController implements Initializable, InitializationListener, Un
 
 	@Override
 	public void didUnlock(UnlockController ctrl) {
-		ctrl.getVault().getNamesOfResourcesWithInvalidMac().addListener(this.macWarningsAggregator);
 		showUnlockedView(ctrl.getVault());
 		Platform.setImplicitExit(false);
 	}
@@ -306,7 +285,6 @@ public class MainController implements Initializable, InitializationListener, Un
 
 	@Override
 	public void didLock(UnlockedController ctrl) {
-		ctrl.getVault().getNamesOfResourcesWithInvalidMac().removeListener(this.macWarningsAggregator);
 		showUnlockView(ctrl.getVault());
 		if (getUnlockedDirectories().isEmpty()) {
 			Platform.setImplicitExit(true);
@@ -324,37 +302,6 @@ public class MainController implements Initializable, InitializationListener, Un
 		showUnlockView(ctrl.getVault());
 	}
 
-	private void showMacWarningsWindow() {
-		if (macWarningsWindowVisible.getAndSet(true) == false) {
-			try {
-				final FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/mac_warnings.fxml"), rb);
-				loader.setControllerFactory(controllerFactory);
-
-				final Parent root = loader.load();
-				final Stage stage = new Stage();
-				stage.setTitle(rb.getString("macWarnings.windowTitle"));
-				stage.setScene(new Scene(root));
-				stage.sizeToScene();
-				stage.setResizable(false);
-				stage.setOnHidden(this::onHideMacWarningsWindow);
-				ActiveWindowStyleSupport.startObservingFocus(stage);
-
-				final MacWarningsController ctrl = loader.getController();
-				ctrl.setMacWarnings(this.aggregatedMacWarnings);
-				ctrl.setStage(stage);
-
-				stage.show();
-			} catch (IOException e) {
-				throw new IllegalStateException("Failed to load fxml file.", e);
-			}
-		}
-	}
-
-	private void onHideMacWarningsWindow(WindowEvent event) {
-		macWarningsWindowVisible.set(false);
-		aggregatedMacWarnings.clear();
-	}
-
 	/* Convenience */
 
 	public Collection<Vault> getDirectories() {

+ 69 - 4
main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java

@@ -8,31 +8,49 @@
  ******************************************************************************/
 package org.cryptomator.ui.controllers;
 
+import java.io.IOException;
 import java.net.URL;
 import java.util.ResourceBundle;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import javafx.animation.Animation;
 import javafx.animation.KeyFrame;
 import javafx.animation.Timeline;
+import javafx.application.Platform;
+import javafx.collections.ListChangeListener;
+import javafx.collections.WeakListChangeListener;
 import javafx.event.ActionEvent;
 import javafx.event.EventHandler;
 import javafx.fxml.FXML;
+import javafx.fxml.FXMLLoader;
 import javafx.fxml.Initializable;
+import javafx.scene.Parent;
+import javafx.scene.Scene;
 import javafx.scene.chart.LineChart;
 import javafx.scene.chart.NumberAxis;
 import javafx.scene.chart.XYChart.Data;
 import javafx.scene.chart.XYChart.Series;
 import javafx.scene.control.Label;
+import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
 import javafx.util.Duration;
 
 import org.cryptomator.crypto.CryptorIOSampling;
+import org.cryptomator.ui.MainModule.ControllerFactory;
 import org.cryptomator.ui.model.Vault;
+import org.cryptomator.ui.util.ActiveWindowStyleSupport;
 import org.cryptomator.ui.util.mount.CommandFailedException;
 
+import com.google.inject.Inject;
+
 public class UnlockedController implements Initializable {
 
 	private static final int IO_SAMPLING_STEPS = 100;
 	private static final double IO_SAMPLING_INTERVAL = 0.25;
+	private final ControllerFactory controllerFactory;
+	private final ListChangeListener<String> macWarningsListener = this::macWarningsDidChange;
+	private final ListChangeListener<String> weakMacWarningsListener = new WeakListChangeListener<>(macWarningsListener);
+	private final AtomicBoolean macWarningsWindowVisible = new AtomicBoolean();
 	private LockListener listener;
 	private Vault vault;
 	private Timeline ioAnimation;
@@ -48,6 +66,11 @@ public class UnlockedController implements Initializable {
 
 	private ResourceBundle rb;
 
+	@Inject
+	public UnlockedController(ControllerFactory controllerFactory) {
+		this.controllerFactory = controllerFactory;
+	}
+
 	@Override
 	public void initialize(URL url, ResourceBundle rb) {
 		this.rb = rb;
@@ -61,6 +84,7 @@ public class UnlockedController implements Initializable {
 			messageLabel.setText(rb.getString("unlocked.label.unmountFailed"));
 			return;
 		}
+		vault.getNamesOfResourcesWithInvalidMac().removeListener(weakMacWarningsListener);
 		vault.stopServer();
 		vault.setUnlocked(false);
 		if (listener != null) {
@@ -68,6 +92,46 @@ public class UnlockedController implements Initializable {
 		}
 	}
 
+	// ****************************************
+	// MAC Auth Warnings
+	// ****************************************
+
+	private void macWarningsDidChange(ListChangeListener.Change<? extends String> change) {
+		if (change.getList().size() > 0) {
+			Platform.runLater(this::showMacWarningsWindow);
+		}
+	}
+
+	private void showMacWarningsWindow() {
+		if (macWarningsWindowVisible.getAndSet(true) == false) {
+			try {
+				final FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/mac_warnings.fxml"), rb);
+				loader.setControllerFactory(controllerFactory);
+
+				final Parent root = loader.load();
+				final Stage stage = new Stage();
+				stage.setTitle(String.format(rb.getString("macWarnings.windowTitle"), vault.getName()));
+				stage.setScene(new Scene(root));
+				stage.sizeToScene();
+				stage.setResizable(false);
+				stage.setOnHidden(this::onHideMacWarningsWindow);
+				ActiveWindowStyleSupport.startObservingFocus(stage);
+
+				final MacWarningsController ctrl = loader.getController();
+				ctrl.setVault(vault);
+				ctrl.setStage(stage);
+
+				stage.show();
+			} catch (IOException e) {
+				throw new IllegalStateException("Failed to load fxml file.", e);
+			}
+		}
+	}
+
+	private void onHideMacWarningsWindow(WindowEvent event) {
+		macWarningsWindowVisible.set(false);
+	}
+
 	// ****************************************
 	// IO Graph
 	// ****************************************
@@ -128,11 +192,12 @@ public class UnlockedController implements Initializable {
 		return vault;
 	}
 
-	public void setVault(Vault directory) {
-		this.vault = directory;
+	public void setVault(Vault vault) {
+		this.vault = vault;
+		vault.getNamesOfResourcesWithInvalidMac().addListener(weakMacWarningsListener);
 
-		if (directory.getCryptor() instanceof CryptorIOSampling) {
-			startIoSampling((CryptorIOSampling) directory.getCryptor());
+		if (vault.getCryptor() instanceof CryptorIOSampling) {
+			startIoSampling((CryptorIOSampling) vault.getCryptor());
 		} else {
 			ioGraph.setVisible(false);
 		}

+ 13 - 4
main/ui/src/main/java/org/cryptomator/ui/model/Vault.java

@@ -6,12 +6,14 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.text.Normalizer;
 import java.text.Normalizer.Form;
+import java.util.HashSet;
 import java.util.Optional;
+import java.util.Set;
 
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
 import javafx.collections.FXCollections;
-import javafx.collections.ObservableSet;
+import javafx.collections.ObservableList;
 
 import javax.security.auth.DestroyFailedException;
 
@@ -43,7 +45,8 @@ public class Vault implements Serializable {
 	private final WebDavMounter mounter;
 	private final DeferredCloser closer;
 	private final ObjectProperty<Boolean> unlocked = new SimpleObjectProperty<Boolean>(this, "unlocked", Boolean.FALSE);
-	private final ObservableSet<String> namesOfResourcesWithInvalidMac = FXThreads.observableSetOnMainThread(FXCollections.observableSet());
+	private final ObservableList<String> namesOfResourcesWithInvalidMac = FXThreads.observableListOnMainThread(FXCollections.observableArrayList());
+	private final Set<String> whitelistedResourcesWithInvalidMac = new HashSet<>();
 
 	private String mountName;
 	private DeferredClosable<ServletLifeCycleAdapter> webDavServlet = DeferredClosable.empty();
@@ -77,11 +80,12 @@ public class Vault implements Serializable {
 
 	public synchronized boolean startServer() {
 		namesOfResourcesWithInvalidMac.clear();
+		whitelistedResourcesWithInvalidMac.clear();
 		Optional<ServletLifeCycleAdapter> o = webDavServlet.get();
 		if (o.isPresent() && o.get().isRunning()) {
 			return false;
 		}
-		ServletLifeCycleAdapter servlet = server.createServlet(path, cryptor, namesOfResourcesWithInvalidMac, mountName);
+		ServletLifeCycleAdapter servlet = server.createServlet(path, cryptor, namesOfResourcesWithInvalidMac, whitelistedResourcesWithInvalidMac, mountName);
 		if (servlet.start()) {
 			webDavServlet = closer.closeLater(servlet);
 			return true;
@@ -102,6 +106,7 @@ public class Vault implements Serializable {
 			LOG.error("Destruction of cryptor throw an exception.", e);
 		}
 		setUnlocked(false);
+		whitelistedResourcesWithInvalidMac.clear();
 		namesOfResourcesWithInvalidMac.clear();
 	}
 
@@ -160,10 +165,14 @@ public class Vault implements Serializable {
 		return mountName;
 	}
 
-	public ObservableSet<String> getNamesOfResourcesWithInvalidMac() {
+	public ObservableList<String> getNamesOfResourcesWithInvalidMac() {
 		return namesOfResourcesWithInvalidMac;
 	}
 
+	public Set<String> getWhitelistedResourcesWithInvalidMac() {
+		return whitelistedResourcesWithInvalidMac;
+	}
+
 	/**
 	 * Tries to form a similar string using the regular latin alphabet.
 	 * 

+ 7 - 4
main/ui/src/main/java/org/cryptomator/ui/util/FXThreads.java

@@ -15,6 +15,7 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
 
 import javafx.application.Platform;
+import javafx.collections.ObservableList;
 import javafx.collections.ObservableSet;
 
 /**
@@ -53,8 +54,7 @@ public final class FXThreads {
 	};
 
 	/**
-	 * 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
+	 * 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(ExecutorService, Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
 	 * 
 	 * <pre>
@@ -74,8 +74,7 @@ public final class FXThreads {
 	}
 
 	/**
-	 * 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
+	 * 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(ExecutorService, Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
 	 * 
 	 * <pre>
@@ -123,4 +122,8 @@ public final class FXThreads {
 		return new ObservableSetOnMainThread<E>(set);
 	}
 
+	public static <E> ObservableList<E> observableListOnMainThread(ObservableList<E> list) {
+		return new ObservableListOnMainThread<E>(list);
+	}
+
 }

+ 272 - 0
main/ui/src/main/java/org/cryptomator/ui/util/ObservableListOnMainThread.java

@@ -0,0 +1,272 @@
+package org.cryptomator.ui.util;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+
+import javafx.application.Platform;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ListChangeListener.Change;
+import javafx.collections.ObservableList;
+
+class ObservableListOnMainThread<E> implements ObservableList<E> {
+
+	private final ObservableList<E> list;
+	private final Collection<InvalidationListener> invalidationListeners;
+	private final Collection<ListChangeListener<? super E>> listChangeListeners;
+
+	public ObservableListOnMainThread(ObservableList<E> list) {
+		this.list = list;
+		this.invalidationListeners = new HashSet<>();
+		this.listChangeListeners = new HashSet<>();
+		this.list.addListener(this::invalidated);
+		this.list.addListener(this::onChanged);
+	}
+
+	@Override
+	public int size() {
+		return list.size();
+	}
+
+	@Override
+	public boolean isEmpty() {
+		return list.isEmpty();
+	}
+
+	@Override
+	public boolean contains(Object o) {
+		return list.contains(o);
+	}
+
+	@Override
+	public Iterator<E> iterator() {
+		return list.iterator();
+	}
+
+	@Override
+	public Object[] toArray() {
+		return list.toArray();
+	}
+
+	@Override
+	public <T> T[] toArray(T[] a) {
+		return list.toArray(a);
+	}
+
+	@Override
+	public boolean add(E e) {
+		return list.add(e);
+	}
+
+	@Override
+	public boolean remove(Object o) {
+		return list.remove(o);
+	}
+
+	@Override
+	public boolean containsAll(Collection<?> c) {
+		return list.containsAll(c);
+	}
+
+	@Override
+	public boolean addAll(Collection<? extends E> c) {
+		return list.addAll(c);
+	}
+
+	@Override
+	public boolean addAll(int index, Collection<? extends E> c) {
+		return list.addAll(index, c);
+	}
+
+	@Override
+	public boolean removeAll(Collection<?> c) {
+		return list.removeAll(c);
+	}
+
+	@Override
+	public boolean retainAll(Collection<?> c) {
+		return list.retainAll(c);
+	}
+
+	@Override
+	public void clear() {
+		list.clear();
+	}
+
+	@Override
+	public E get(int index) {
+		return list.get(index);
+	}
+
+	@Override
+	public E set(int index, E element) {
+		return list.set(index, element);
+	}
+
+	@Override
+	public void add(int index, E element) {
+		list.add(index, element);
+	}
+
+	@Override
+	public E remove(int index) {
+		return list.remove(index);
+	}
+
+	@Override
+	public int indexOf(Object o) {
+		return list.indexOf(o);
+	}
+
+	@Override
+	public int lastIndexOf(Object o) {
+		return list.lastIndexOf(o);
+	}
+
+	@Override
+	public ListIterator<E> listIterator() {
+		return list.listIterator();
+	}
+
+	@Override
+	public ListIterator<E> listIterator(int index) {
+		return list.listIterator(index);
+	}
+
+	@Override
+	public List<E> subList(int fromIndex, int toIndex) {
+		return list.subList(fromIndex, toIndex);
+	}
+
+	@Override
+	public boolean addAll(@SuppressWarnings("unchecked") E... elements) {
+		return list.addAll(elements);
+	}
+
+	@Override
+	public boolean setAll(@SuppressWarnings("unchecked") E... elements) {
+		return list.addAll(elements);
+	}
+
+	@Override
+	public boolean setAll(Collection<? extends E> col) {
+		return list.setAll(col);
+	}
+
+	@Override
+	public boolean removeAll(@SuppressWarnings("unchecked") E... elements) {
+		return list.removeAll(elements);
+	}
+
+	@Override
+	public boolean retainAll(@SuppressWarnings("unchecked") E... elements) {
+		return list.retainAll(elements);
+	}
+
+	@Override
+	public void remove(int from, int to) {
+		list.remove(from, to);
+	}
+
+	private void invalidated(Observable observable) {
+		Platform.runLater(() -> {
+			for (InvalidationListener listener : invalidationListeners) {
+				listener.invalidated(this);
+			}
+		});
+	}
+
+	@Override
+	public void addListener(InvalidationListener listener) {
+		invalidationListeners.add(listener);
+	}
+
+	@Override
+	public void removeListener(InvalidationListener listener) {
+		invalidationListeners.remove(listener);
+	}
+
+	private void onChanged(Change<? extends E> change) {
+		final Change<? extends E> c = new ListChange(change);
+		Platform.runLater(() -> {
+			for (ListChangeListener<? super E> listener : listChangeListeners) {
+				listener.onChanged(c);
+			}
+		});
+	}
+
+	@Override
+	public void addListener(ListChangeListener<? super E> listener) {
+		listChangeListeners.add(listener);
+	}
+
+	@Override
+	public void removeListener(ListChangeListener<? super E> listener) {
+		listChangeListeners.add(listener);
+	}
+
+	private class ListChange extends ListChangeListener.Change<E> {
+
+		private final Change<? extends E> originalChange;
+
+		public ListChange(Change<? extends E> change) {
+			super(ObservableListOnMainThread.this);
+			this.originalChange = change;
+		}
+
+		@Override
+		public boolean wasAdded() {
+			return originalChange.wasAdded();
+		}
+
+		@Override
+		public boolean wasRemoved() {
+			return originalChange.wasRemoved();
+		}
+
+		@Override
+		public boolean next() {
+			return originalChange.next();
+		}
+
+		@Override
+		public void reset() {
+			originalChange.reset();
+		}
+
+		@Override
+		public int getFrom() {
+			return originalChange.getFrom();
+		}
+
+		@Override
+		public int getTo() {
+			return originalChange.getTo();
+		}
+
+		@Override
+		@SuppressWarnings("unchecked")
+		public List<E> getRemoved() {
+			return (List<E>) originalChange.getRemoved();
+		}
+
+		@Override
+		protected int[] getPermutation() {
+			if (originalChange.wasPermutated()) {
+				int[] permutations = new int[originalChange.getTo() - originalChange.getFrom()];
+				for (int i = 0; i < permutations.length; i++) {
+					permutations[i] = originalChange.getPermutation(i);
+				}
+				return permutations;
+			} else {
+				return new int[0];
+			}
+		}
+
+	}
+
+}

+ 0 - 44
main/ui/src/main/java/org/cryptomator/ui/util/ObservableSetAggregator.java

@@ -1,44 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2014 cryptomator.org
- * This file is licensed under the terms of the MIT license.
- * See the LICENSE.txt file for more info.
- * 
- * Contributors:
- *    Sebastian Stenzel - initial implementation
- ******************************************************************************/
-package org.cryptomator.ui.util;
-
-import java.util.Collection;
-
-import javafx.collections.ObservableSet;
-import javafx.collections.SetChangeListener;
-
-/**
- * From the moment on, this aggregator is added as an observer to one or many {@link ObservableSet}s, change-events will be passed through
- * to the given aggregation.
- */
-public class ObservableSetAggregator<E> implements SetChangeListener<E> {
-
-	private final Collection<E> aggregation;
-
-	/**
-	 * @param aggregation Set to which elements from observed subsets shall be added.
-	 */
-	public ObservableSetAggregator(final Collection<E> aggregation) {
-		this.aggregation = aggregation;
-	}
-
-	@Override
-	public void onChanged(Change<? extends E> change) {
-		if (change.getSet() == aggregation) {
-			// break cycle if aggregator observes aggregation
-			return;
-		}
-		if (change.wasAdded()) {
-			aggregation.add(change.getElementAdded());
-		} else if (change.wasRemoved()) {
-			aggregation.remove(change.getElementRemoved());
-		}
-	}
-
-}

+ 1 - 1
main/ui/src/main/resources/localization.properties

@@ -56,7 +56,7 @@ unlocked.label.unmountFailed=Ejecting drive failed.
 unlocked.ioGraph.yAxis.label=Throughput (MiB/s)
 
 # mac_warnings.fxml
-macWarnings.windowTitle=Danger - MAC authentication failed
+macWarnings.windowTitle=Danger - Corrupted file in %s
 macWarnings.message=Cryptomator detected potentially malicious corruptions in the following files:
 macWarnings.moreInformationButton=Learn more
 macWarnings.dismissButton=I promise to be careful