New MAC authentication warning, preventing CCAs, but allowing to force-decrypt unauthentic files.

This commit is contained in:
Sebastian Stenzel
2015-07-09 17:16:43 +02:00
parent 9d2d847727
commit 685e347524
27 changed files with 276 additions and 157 deletions

View File

@@ -20,7 +20,6 @@ import javafx.scene.control.Button;
import javafx.scene.control.Hyperlink;
import javafx.scene.text.Text;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
@@ -109,7 +108,7 @@ public class ChangePasswordController implements Initializable {
try (final InputStream masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ)) {
vault.getCryptor().decryptMasterKey(masterKeyInputStream, oldPassword);
Files.copy(masterKeyPath, masterKeyBackupPath, StandardCopyOption.REPLACE_EXISTING);
} catch (DecryptFailedException | IOException ex) {
} catch (IOException ex) {
messageText.setText(rb.getString("changePassword.errorMessage.decryptionFailed"));
LOG.error("Decryption failed for technical reasons.", ex);
newPasswordField.swipe();

View File

@@ -1,38 +1,83 @@
package org.cryptomator.ui.controllers;
import java.net.URL;
import java.util.ResourceBundle;
import java.util.stream.Collectors;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.WeakListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.control.cell.CheckBoxListCell;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import javax.inject.Inject;
import org.cryptomator.ui.model.Vault;
public class MacWarningsController {
public class MacWarningsController implements Initializable {
@FXML
private ListView<String> warningsList;
private ListView<Warning> warningsList;
@FXML
private Button whitelistButton;
private final Application application;
private final ListChangeListener<? super String> macWarningsListener = this::warningsDidChange;
private final ListChangeListener<? super String> weakMacWarningsListener = new WeakListChangeListener<>(macWarningsListener);
private final ObservableList<Warning> warnings = FXCollections.observableArrayList();
private final ListChangeListener<String> unauthenticatedResourcesChangeListener = this::unauthenticatedResourcesDidChange;
private final ChangeListener<Boolean> stageVisibilityChangeListener = this::windowVisibilityDidChange;
private Stage stage;
private Vault vault;
private ResourceBundle rb;
@Inject
public MacWarningsController(Application application) {
this.application = application;
}
@Override
public void initialize(URL location, ResourceBundle rb) {
this.rb = rb;
warnings.addListener(this::warningsDidInvalidate);
warningsList.setItems(warnings);
warningsList.setCellFactory(CheckBoxListCell.forListView(Warning::selectedProperty, new StringConverter<Warning>() {
@Override
public String toString(Warning object) {
return object.getName();
}
@Override
public Warning fromString(String string) {
return null;
}
}));
}
@FXML
private void didClickDismissButton(ActionEvent event) {
warningsList.getItems().removeListener(weakMacWarningsListener);
stage.hide();
private void didClickWhitelistButton(ActionEvent event) {
warnings.filtered(w -> w.isSelected()).stream().forEach(w -> {
final String resourceToBeWhitelisted = w.getName();
vault.getWhitelistedResourcesWithInvalidMac().add(resourceToBeWhitelisted);
vault.getNamesOfResourcesWithInvalidMac().remove(resourceToBeWhitelisted);
});
warnings.removeIf(w -> w.isSelected());
}
@FXML
@@ -40,26 +85,70 @@ public class MacWarningsController {
application.getHostServices().showDocument("https://cryptomator.org/help.html#macWarning");
}
// 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() && stage != null) {
change.getList().removeListener(weakMacWarningsListener);
stage.hide();
private void unauthenticatedResourcesDidChange(Change<? extends String> change) {
while (change.next()) {
if (change.wasAdded()) {
warnings.addAll(change.getAddedSubList().stream().map(Warning::new).collect(Collectors.toList()));
} else if (change.wasRemoved()) {
change.getRemoved().forEach(str -> {
warnings.removeIf(w -> str.equals(w.name.get()));
});
}
}
}
public Stage getStage() {
return stage;
private void warningsDidInvalidate(Observable observable) {
disableWhitelistButtonIfNothingSelected();
}
private void windowVisibilityDidChange(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
if (Boolean.TRUE.equals(newValue)) {
stage.setTitle(String.format(rb.getString("macWarnings.windowTitle"), vault.getName()));
warnings.addAll(vault.getNamesOfResourcesWithInvalidMac().stream().map(Warning::new).collect(Collectors.toList()));
vault.getNamesOfResourcesWithInvalidMac().addListener(this.unauthenticatedResourcesChangeListener);
} else {
vault.getNamesOfResourcesWithInvalidMac().clear();
vault.getNamesOfResourcesWithInvalidMac().removeListener(this.unauthenticatedResourcesChangeListener);
}
}
private void disableWhitelistButtonIfNothingSelected() {
whitelistButton.setDisable(warnings.filtered(w -> w.isSelected()).isEmpty());
}
public void setStage(Stage stage) {
this.stage = stage;
stage.showingProperty().addListener(new WeakChangeListener<>(stageVisibilityChangeListener));
}
public void setVault(Vault vault) {
this.vault = vault;
this.warningsList.setItems(vault.getNamesOfResourcesWithInvalidMac());
this.warningsList.getItems().addListener(weakMacWarningsListener);
}
private class Warning {
private final ReadOnlyStringWrapper name = new ReadOnlyStringWrapper();
private final BooleanProperty selected = new SimpleBooleanProperty(false);
public Warning(String name) {
this.name.set(name);
this.selectedProperty().addListener(change -> {
disableWhitelistButtonIfNothingSelected();
});
}
public String getName() {
return name.get();
}
public BooleanProperty selectedProperty() {
return selected;
}
public boolean isSelected() {
return selected.get();
}
}
}

View File

@@ -286,7 +286,7 @@ public class MainController implements Initializable, InitializationListener, Un
@Override
public void didLock(UnlockedController ctrl) {
showUnlockView(ctrl.getVault());
if (getUnlockedDirectories().isEmpty()) {
if (getUnlockedVaults().isEmpty()) {
Platform.setImplicitExit(true);
}
}
@@ -304,12 +304,12 @@ public class MainController implements Initializable, InitializationListener, Un
/* Convenience */
public Collection<Vault> getDirectories() {
public Collection<Vault> getVaults() {
return vaultList.getItems();
}
public Collection<Vault> getUnlockedDirectories() {
return getDirectories().stream().filter(d -> d.isUnlocked()).collect(Collectors.toSet());
public Collection<Vault> getUnlockedVaults() {
return getVaults().stream().filter(d -> d.isUnlocked()).collect(Collectors.toSet());
}
/* public Getter/Setter */

View File

@@ -35,7 +35,6 @@ import javafx.scene.text.Text;
import javax.security.auth.DestroyFailedException;
import org.apache.commons.lang3.CharUtils;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
@@ -134,7 +133,7 @@ public class UnlockController implements Initializable {
vault.setUnlocked(true);
final Future<Boolean> futureMount = exec.submit(() -> vault.mount());
FXThreads.runOnMainThreadWhenFinished(exec, futureMount, this::unlockAndMountFinished);
} catch (DecryptFailedException | IOException ex) {
} catch (IOException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageText.setText(rb.getString("unlock.errorMessage.decryptionFailed"));
@@ -178,6 +177,7 @@ public class UnlockController implements Initializable {
setControlsDisabled(false);
if (vault.isUnlocked() && !mountSuccess) {
vault.stopServer();
vault.setUnlocked(false);
}
if (mountSuccess && listener != null) {
listener.didUnlock(this);

View File

@@ -11,14 +11,12 @@ 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;
@@ -32,7 +30,6 @@ 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;
@@ -48,9 +45,8 @@ 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 final Stage macWarningWindow = new Stage();
private MacWarningsController macWarningCtrl;
private LockListener listener;
private Vault vault;
private Timeline ioAnimation;
@@ -74,6 +70,22 @@ public class UnlockedController implements Initializable {
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
try {
final FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/mac_warnings.fxml"), rb);
loader.setControllerFactory(controllerFactory);
final Parent root = loader.load();
macWarningWindow.setScene(new Scene(root));
macWarningWindow.sizeToScene();
macWarningWindow.setResizable(false);
ActiveWindowStyleSupport.startObservingFocus(macWarningWindow);
macWarningCtrl = loader.getController();
macWarningCtrl.setStage(macWarningWindow);
} catch (IOException e) {
throw new IllegalStateException("Failed to load fxml file.", e);
}
}
@FXML
@@ -84,7 +96,6 @@ 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) {
@@ -98,40 +109,16 @@ public class UnlockedController implements Initializable {
private void macWarningsDidChange(ListChangeListener.Change<? extends String> change) {
if (change.getList().size() > 0) {
Platform.runLater(this::showMacWarningsWindow);
Platform.runLater(() -> {
macWarningWindow.show();
});
} else {
Platform.runLater(() -> {
macWarningWindow.hide();
});
}
}
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
// ****************************************
@@ -194,8 +181,18 @@ public class UnlockedController implements Initializable {
public void setVault(Vault vault) {
this.vault = vault;
vault.getNamesOfResourcesWithInvalidMac().addListener(weakMacWarningsListener);
macWarningCtrl.setVault(vault);
// listen to MAC warnings as long as this vault is unlocked:
final ListChangeListener<String> macWarningsListener = this::macWarningsDidChange;
vault.getNamesOfResourcesWithInvalidMac().addListener(macWarningsListener);
vault.unlockedProperty().addListener((observable, oldValue, newValue) -> {
if (Boolean.FALSE.equals(newValue)) {
vault.getNamesOfResourcesWithInvalidMac().removeListener(macWarningsListener);
}
});
// sample crypto-throughput:
if (vault.getCryptor() instanceof CryptorIOSampling) {
startIoSampling((CryptorIOSampling) vault.getCryptor());
} else {

View File

@@ -105,7 +105,6 @@ public class Vault implements Serializable {
} catch (DestroyFailedException e) {
LOG.error("Destruction of cryptor throw an exception.", e);
}
setUnlocked(false);
whitelistedResourcesWithInvalidMac.clear();
namesOfResourcesWithInvalidMac.clear();
}

View File

@@ -2,7 +2,6 @@ package org.cryptomator.ui.util;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.stage.Window;
public class ActiveWindowStyleSupport implements ChangeListener<Boolean> {
@@ -18,9 +17,8 @@ public class ActiveWindowStyleSupport implements ChangeListener<Boolean> {
}
/**
* Creates and registers a listener on the given window, that will add the class {@value #ACTIVE_WINDOW_STYLE_CLASS} to the scenes root
* element, if the window is active. Otherwise {@value #INACTIVE_WINDOW_STYLE_CLASS} will be added. Allows CSS rules to be defined
* depending on the window's focus.<br/>
* Creates and registers a listener on the given window, that will add the class {@value #ACTIVE_WINDOW_STYLE_CLASS} to the scenes root element, if the window is active. Otherwise
* {@value #INACTIVE_WINDOW_STYLE_CLASS} will be added. Allows CSS rules to be defined depending on the window's focus.<br/>
* <br/>
* Example:<br/>
* <code>
@@ -32,7 +30,7 @@ public class ActiveWindowStyleSupport implements ChangeListener<Boolean> {
* @return The observer
*/
public static ChangeListener<Boolean> startObservingFocus(final Window window) {
final ChangeListener<Boolean> observer = new WeakChangeListener<Boolean>(new ActiveWindowStyleSupport(window));
final ChangeListener<Boolean> observer = new ActiveWindowStyleSupport(window);
window.focusedProperty().addListener(observer);
return observer;
}

View File

@@ -13,6 +13,8 @@ import javafx.collections.ListChangeListener;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import com.google.common.collect.ImmutableList;
class ObservableListOnMainThread<E> implements ObservableList<E> {
private final ObservableList<E> list;
@@ -173,8 +175,9 @@ class ObservableListOnMainThread<E> implements ObservableList<E> {
}
private void invalidated(Observable observable) {
final Collection<InvalidationListener> listeners = ImmutableList.copyOf(invalidationListeners);
Platform.runLater(() -> {
for (InvalidationListener listener : invalidationListeners) {
for (InvalidationListener listener : listeners) {
listener.invalidated(this);
}
});
@@ -192,8 +195,9 @@ class ObservableListOnMainThread<E> implements ObservableList<E> {
private void onChanged(Change<? extends E> change) {
final Change<? extends E> c = new ListChange(change);
final Collection<ListChangeListener<? super E>> listeners = ImmutableList.copyOf(listChangeListeners);
Platform.runLater(() -> {
for (ListChangeListener<? super E> listener : listChangeListeners) {
for (ListChangeListener<? super E> listener : listeners) {
listener.onChanged(c);
}
});
@@ -206,7 +210,7 @@ class ObservableListOnMainThread<E> implements ObservableList<E> {
@Override
public void removeListener(ListChangeListener<? super E> listener) {
listChangeListeners.add(listener);
listChangeListeners.remove(listener);
}
private class ListChange extends ListChangeListener.Change<E> {

View File

@@ -11,6 +11,8 @@ import javafx.collections.ObservableSet;
import javafx.collections.SetChangeListener;
import javafx.collections.SetChangeListener.Change;
import com.google.common.collect.ImmutableList;
class ObservableSetOnMainThread<E> implements ObservableSet<E> {
private final ObservableSet<E> set;
@@ -91,8 +93,9 @@ class ObservableSetOnMainThread<E> implements ObservableSet<E> {
}
private void invalidated(Observable observable) {
final Collection<InvalidationListener> listeners = ImmutableList.copyOf(invalidationListeners);
Platform.runLater(() -> {
for (InvalidationListener listener : invalidationListeners) {
for (InvalidationListener listener : listeners) {
listener.invalidated(this);
}
});
@@ -110,8 +113,9 @@ class ObservableSetOnMainThread<E> implements ObservableSet<E> {
private void onChanged(Change<? extends E> change) {
final Change<? extends E> c = new SetChange(this, change.getElementAdded(), change.getElementRemoved());
final Collection<SetChangeListener<? super E>> listeners = ImmutableList.copyOf(setChangeListeners);
Platform.runLater(() -> {
for (SetChangeListener<? super E> listener : setChangeListeners) {
for (SetChangeListener<? super E> listener : listeners) {
listener.onChanged(c);
}
});

View File

@@ -24,7 +24,7 @@
<ListView fx:id="warningsList" VBox.vgrow="ALWAYS" focusTraversable="false" />
<HBox alignment="CENTER_RIGHT" spacing="12.0">
<children>
<Button text="%macWarnings.dismissButton" prefWidth="200.0" onAction="#didClickDismissButton" focusTraversable="false"/>
<Button fx:id="whitelistButton" text="%macWarnings.whitelistButton" prefWidth="200.0" onAction="#didClickWhitelistButton" focusTraversable="false"/>
<Button text="%macWarnings.moreInformationButton" defaultButton="true" prefWidth="200.0" onAction="#didClickMoreInformationButton" focusTraversable="false"/>
</children>
</HBox>

View File

@@ -59,7 +59,7 @@ unlocked.ioGraph.yAxis.label=Throughput (MiB/s)
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
macWarnings.whitelistButton=Decrypt selected anyway
# tray icon
tray.menu.open=Open