From 0d3a5b4e70cb3fd5f8d131f134a097d15a6e389f Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 26 Jun 2015 23:35:24 +0200 Subject: [PATCH] - increased vault version - Showing "per vault" MAC authentication failure dialogs --- .../org/cryptomator/webdav/WebDavServer.java | 14 +- .../jackrabbit/CryptoWarningHandler.java | 15 +- .../webdav/jackrabbit/EncryptedFile.java | 2 +- .../webdav/jackrabbit/EncryptedFilePart.java | 11 +- .../webdav/jackrabbit/WebDavServlet.java | 4 +- .../cryptomator/crypto/aes256/KeyFile.java | 2 +- .../ui/controllers/MacWarningsController.java | 25 +- .../ui/controllers/MainController.java | 53 ---- .../ui/controllers/UnlockedController.java | 73 ++++- .../java/org/cryptomator/ui/model/Vault.java | 17 +- .../org/cryptomator/ui/util/FXThreads.java | 11 +- .../ui/util/ObservableListOnMainThread.java | 272 ++++++++++++++++++ .../ui/util/ObservableSetAggregator.java | 44 --- .../main/resources/localization.properties | 2 +- 14 files changed, 407 insertions(+), 138 deletions(-) create mode 100644 main/ui/src/main/java/org/cryptomator/ui/util/ObservableListOnMainThread.java delete mode 100644 main/ui/src/main/java/org/cryptomator/ui/util/ObservableSetAggregator.java diff --git a/main/core/src/main/java/org/cryptomator/webdav/WebDavServer.java b/main/core/src/main/java/org/cryptomator/webdav/WebDavServer.java index a0ff29ba3..64870545f 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/WebDavServer.java +++ b/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 failingMacCollection, final String name) { + public ServletLifeCycleAdapter createServlet(final Path workDir, final Cryptor cryptor, final Collection failingMacCollection, final Collection 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 failingMacCollection) { - final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor, failingMacCollection)); + private ServletHolder getWebDavServletHolder(final String workDir, final Cryptor cryptor, final Collection failingMacCollection, final Collection whitelistedResourceCollection) { + final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor, failingMacCollection, whitelistedResourceCollection)); result.setInitParameter(WebDavServlet.CFG_FS_ROOT, workDir); return result; } diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoWarningHandler.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoWarningHandler.java index 841c9565d..104dd65b4 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoWarningHandler.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoWarningHandler.java @@ -5,15 +5,22 @@ import java.util.Collection; class CryptoWarningHandler { private final Collection resourcesWithInvalidMac; + private final Collection whitelistedResources; - public CryptoWarningHandler(Collection resourcesWithInvalidMac) { + public CryptoWarningHandler(Collection resourcesWithInvalidMac, Collection 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); + } + } diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java index fab70b343..daf8b73ab 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java +++ b/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); } diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java index ed949d112..fc0490bd3 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java +++ b/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 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); } diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java index ca2b8990e..bebe801cc 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java +++ b/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 failingMacCollection) { + public WebDavServlet(final Cryptor cryptor, final Collection failingMacCollection, final Collection whitelistedResourceCollection) { super(); this.cryptor = cryptor; - this.cryptoWarningHandler = new CryptoWarningHandler(failingMacCollection); + this.cryptoWarningHandler = new CryptoWarningHandler(failingMacCollection, whitelistedResourceCollection); } @Override diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/KeyFile.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/KeyFile.java index dec5d477a..3bfd40582 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/KeyFile.java +++ b/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; diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/MacWarningsController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/MacWarningsController.java index ed15698ea..d29b441bc 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/MacWarningsController.java +++ b/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 warningsList; - private Stage stage; - private final Application application; + private final ListChangeListener macWarningsListener = this::warningsDidChange; + private final ListChangeListener 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 macWarnings) { - this.warningsList.setItems(macWarnings); - this.warningsList.getItems().addListener(new WeakListChangeListener(this::warningsDidChange)); - } - // closes this window automatically, if all warnings disappeared (e.g. due to an unmount event) private void warningsDidChange(Change 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); + } + } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java index 803aeb795..50c0f5815 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java +++ b/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 aggregatedMacWarnings; - private final SetChangeListener 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 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 getDirectories() { diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java index 38a12aa0b..436023cab 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java +++ b/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 macWarningsListener = this::macWarningsDidChange; + private final ListChangeListener 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 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); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java index 2a65ad2aa..78ec6df49 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java +++ b/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 unlocked = new SimpleObjectProperty(this, "unlocked", Boolean.FALSE); - private final ObservableSet namesOfResourcesWithInvalidMac = FXThreads.observableSetOnMainThread(FXCollections.observableSet()); + private final ObservableList namesOfResourcesWithInvalidMac = FXThreads.observableListOnMainThread(FXCollections.observableArrayList()); + private final Set whitelistedResourcesWithInvalidMac = new HashSet<>(); private String mountName; private DeferredClosable webDavServlet = DeferredClosable.empty(); @@ -77,11 +80,12 @@ public class Vault implements Serializable { public synchronized boolean startServer() { namesOfResourcesWithInvalidMac.clear(); + whitelistedResourcesWithInvalidMac.clear(); Optional 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 getNamesOfResourcesWithInvalidMac() { + public ObservableList getNamesOfResourcesWithInvalidMac() { return namesOfResourcesWithInvalidMac; } + public Set getWhitelistedResourcesWithInvalidMac() { + return whitelistedResourcesWithInvalidMac; + } + /** * Tries to form a similar string using the regular latin alphabet. * diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/FXThreads.java b/main/ui/src/main/java/org/cryptomator/ui/util/FXThreads.java index a1d58635c..312e07ad4 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/FXThreads.java +++ b/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. * *
@@ -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.
 	 * 
 	 * 
@@ -123,4 +122,8 @@ public final class FXThreads {
 		return new ObservableSetOnMainThread(set);
 	}
 
+	public static  ObservableList observableListOnMainThread(ObservableList list) {
+		return new ObservableListOnMainThread(list);
+	}
+
 }
diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/ObservableListOnMainThread.java b/main/ui/src/main/java/org/cryptomator/ui/util/ObservableListOnMainThread.java
new file mode 100644
index 000000000..379578b92
--- /dev/null
+++ b/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 implements ObservableList {
+
+	private final ObservableList list;
+	private final Collection invalidationListeners;
+	private final Collection> listChangeListeners;
+
+	public ObservableListOnMainThread(ObservableList 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 iterator() {
+		return list.iterator();
+	}
+
+	@Override
+	public Object[] toArray() {
+		return list.toArray();
+	}
+
+	@Override
+	public  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 c) {
+		return list.addAll(c);
+	}
+
+	@Override
+	public boolean addAll(int index, Collection 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 listIterator() {
+		return list.listIterator();
+	}
+
+	@Override
+	public ListIterator listIterator(int index) {
+		return list.listIterator(index);
+	}
+
+	@Override
+	public List 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 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 change) {
+		final Change c = new ListChange(change);
+		Platform.runLater(() -> {
+			for (ListChangeListener listener : listChangeListeners) {
+				listener.onChanged(c);
+			}
+		});
+	}
+
+	@Override
+	public void addListener(ListChangeListener listener) {
+		listChangeListeners.add(listener);
+	}
+
+	@Override
+	public void removeListener(ListChangeListener listener) {
+		listChangeListeners.add(listener);
+	}
+
+	private class ListChange extends ListChangeListener.Change {
+
+		private final Change originalChange;
+
+		public ListChange(Change 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 getRemoved() {
+			return (List) 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];
+			}
+		}
+
+	}
+
+}
\ No newline at end of file
diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/ObservableSetAggregator.java b/main/ui/src/main/java/org/cryptomator/ui/util/ObservableSetAggregator.java
deleted file mode 100644
index 68fff9e79..000000000
--- a/main/ui/src/main/java/org/cryptomator/ui/util/ObservableSetAggregator.java
+++ /dev/null
@@ -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 implements SetChangeListener {
-
-	private final Collection aggregation;
-
-	/**
-	 * @param aggregation Set to which elements from observed subsets shall be added.
-	 */
-	public ObservableSetAggregator(final Collection aggregation) {
-		this.aggregation = aggregation;
-	}
-
-	@Override
-	public void onChanged(Change 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());
-		}
-	}
-
-}
diff --git a/main/ui/src/main/resources/localization.properties b/main/ui/src/main/resources/localization.properties
index 204857086..6d42847c7 100644
--- a/main/ui/src/main/resources/localization.properties
+++ b/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