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

@@ -29,7 +29,6 @@ import org.apache.jackrabbit.webdav.lock.LockManager;
import org.apache.jackrabbit.webdav.property.DavPropertyName;
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.eclipse.jetty.http.HttpHeader;
@@ -61,12 +60,12 @@ class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
} catch (OverlappingFileLockException e) {
// file header currently locked, report -1 for unknown size.
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, -1l));
} catch (IOException e) {
LOG.error("Error reading filesize " + filePath.toString(), e);
throw new IORuntimeException(e);
} catch (MacAuthenticationFailedException e) {
LOG.warn("Content length couldn't be determined due to MAC authentication violation.");
// don't add content length DAV property
} catch (IOException e) {
LOG.error("Error reading filesize " + filePath.toString(), e);
throw new IORuntimeException(e);
}
}
this.contentLength = contentLength;
@@ -107,16 +106,11 @@ class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
outputContext.setContentLength(contentLength);
}
if (outputContext.hasStream()) {
cryptor.decryptFile(c, outputContext.getOutputStream());
final boolean authenticate = !cryptoWarningHandler.ignoreMac(getLocator().getResourcePath());
cryptor.decryptFile(c, outputContext.getOutputStream(), authenticate);
}
} catch (EOFException e) {
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);
} catch (DecryptFailedException e) {
throw new IOException("Error decrypting file " + filePath.toString(), e);
}
}
}

View File

@@ -14,8 +14,6 @@ import org.apache.jackrabbit.webdav.DavSession;
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;
@@ -62,18 +60,13 @@ class EncryptedFilePart extends EncryptedFile {
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ)) {
if (outputContext.hasStream()) {
cryptor.decryptRange(c, outputContext.getOutputStream(), range.getLeft(), rangeLength);
final boolean authenticate = !cryptoWarningHandler.ignoreMac(getLocator().getResourcePath());
cryptor.decryptRange(c, outputContext.getOutputStream(), range.getLeft(), rangeLength, authenticate);
}
} 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);
}
}

View File

@@ -8,21 +8,29 @@
******************************************************************************/
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.util.Collection;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavLocatorFactory;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceFactory;
import org.apache.jackrabbit.webdav.DavSessionProvider;
import org.apache.jackrabbit.webdav.WebdavRequest;
import org.apache.jackrabbit.webdav.WebdavResponse;
import org.apache.jackrabbit.webdav.server.AbstractWebdavServlet;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class WebDavServlet extends AbstractWebdavServlet {
private static final Logger LOG = LoggerFactory.getLogger(WebDavServlet.class);
private static final long serialVersionUID = 7965170007048673022L;
public static final String CFG_FS_ROOT = "cfg.fs.root";
private DavSessionProvider davSessionProvider;
@@ -81,4 +89,15 @@ public class WebDavServlet extends AbstractWebdavServlet {
this.davResourceFactory = resourceFactory;
}
@Override
protected void doGet(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException {
try {
super.doGet(request, response, resource);
} catch (MacAuthenticationFailedException e) {
LOG.warn("File integrity violation for " + resource.getLocator().getResourcePath());
cryptoWarningHandler.macAuthFailed(resource.getLocator().getResourcePath());
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
}
}
}

View File

@@ -358,7 +358,7 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
}
@Override
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException {
// read header:
encryptedFile.position(0l);
final ByteBuffer headerBuf = ByteBuffer.allocate(96);
@@ -397,14 +397,14 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
headerBuf.get(storedHeaderMac);
// calculate mac over first 64 bytes of header:
final Mac headerMac = this.hmacSha256(hMacMasterKey);
headerBuf.position(0);
headerBuf.limit(64);
headerMac.update(headerBuf);
// check header integrity:
if (!MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal())) {
throw new MacAuthenticationFailedException("Header MAC authentication failed.");
if (authenticate) {
final Mac headerMac = this.hmacSha256(hMacMasterKey);
headerBuf.position(0);
headerBuf.limit(64);
headerMac.update(headerBuf);
if (!MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal())) {
throw new MacAuthenticationFailedException("Header MAC authentication failed.");
}
}
// content decryption:
@@ -424,12 +424,14 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
}
// check MAC of current block:
contentMac.update(buffer, 0, n - 32);
final byte[] calculatedMac = contentMac.doFinal();
final byte[] storedMac = new byte[32];
System.arraycopy(buffer, n - 32, storedMac, 0, 32);
if (!MessageDigest.isEqual(calculatedMac, storedMac)) {
throw new MacAuthenticationFailedException("Content MAC authentication failed.");
if (authenticate) {
contentMac.update(buffer, 0, n - 32);
final byte[] calculatedMac = contentMac.doFinal();
final byte[] storedMac = new byte[32];
System.arraycopy(buffer, n - 32, storedMac, 0, 32);
if (!MessageDigest.isEqual(calculatedMac, storedMac)) {
throw new MacAuthenticationFailedException("Content MAC authentication failed.");
}
}
// decrypt block:
@@ -444,7 +446,7 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException {
// read header:
encryptedFile.position(0l);
final ByteBuffer headerBuf = ByteBuffer.allocate(96);
@@ -470,14 +472,14 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
headerBuf.get(storedHeaderMac);
// calculate mac over first 64 bytes of header:
final Mac headerMac = this.hmacSha256(hMacMasterKey);
headerBuf.position(0);
headerBuf.limit(64);
headerMac.update(headerBuf);
// check header integrity:
if (!MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal())) {
throw new MacAuthenticationFailedException("Header MAC authentication failed.");
if (authenticate) {
final Mac headerMac = this.hmacSha256(hMacMasterKey);
headerBuf.position(0);
headerBuf.limit(64);
headerMac.update(headerBuf);
if (!MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal())) {
throw new MacAuthenticationFailedException("Header MAC authentication failed.");
}
}
// find first relevant block:
@@ -509,12 +511,14 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
}
// check MAC of current block:
contentMac.update(buffer, 0, n - 32);
final byte[] calculatedMac = contentMac.doFinal();
final byte[] storedMac = new byte[32];
System.arraycopy(buffer, n - 32, storedMac, 0, 32);
if (!MessageDigest.isEqual(calculatedMac, storedMac)) {
throw new MacAuthenticationFailedException("Content MAC authentication failed.");
if (authenticate) {
contentMac.update(buffer, 0, n - 32);
final byte[] calculatedMac = contentMac.doFinal();
final byte[] storedMac = new byte[32];
System.arraycopy(buffer, n - 32, storedMac, 0, 32);
if (!MessageDigest.isEqual(calculatedMac, storedMac)) {
throw new MacAuthenticationFailedException("Content MAC authentication failed.");
}
}
// decrypt block:

View File

@@ -99,7 +99,7 @@ public class Aes256CryptorTest {
// decrypt modified content (should fail with DecryptFailedException):
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
cryptor.decryptFile(encryptedIn, plaintextOut);
cryptor.decryptFile(encryptedIn, plaintextOut, true);
}
@Test
@@ -127,7 +127,7 @@ public class Aes256CryptorTest {
// decrypt:
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
final Long numDecryptedBytes = cryptor.decryptFile(encryptedIn, plaintextOut);
final Long numDecryptedBytes = cryptor.decryptFile(encryptedIn, plaintextOut, true);
IOUtils.closeQuietly(encryptedIn);
IOUtils.closeQuietly(plaintextOut);
Assert.assertEquals(filesize.longValue(), numDecryptedBytes.longValue());
@@ -162,7 +162,7 @@ public class Aes256CryptorTest {
// decrypt:
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
final Long numDecryptedBytes = cryptor.decryptRange(encryptedIn, plaintextOut, 260000 * Integer.BYTES, 4000 * Integer.BYTES);
final Long numDecryptedBytes = cryptor.decryptRange(encryptedIn, plaintextOut, 260000 * Integer.BYTES, 4000 * Integer.BYTES, true);
IOUtils.closeQuietly(encryptedIn);
IOUtils.closeQuietly(plaintextOut);
Assert.assertTrue(numDecryptedBytes > 0);

View File

@@ -53,13 +53,13 @@ public class AbstractCryptorDecorator implements Cryptor {
}
@Override
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
return cryptor.decryptFile(encryptedFile, plaintextFile);
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException {
return cryptor.decryptFile(encryptedFile, plaintextFile, authenticate);
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
return cryptor.decryptRange(encryptedFile, plaintextFile, pos, length);
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException {
return cryptor.decryptRange(encryptedFile, plaintextFile, pos, length, authenticate);
}
@Override

View File

@@ -79,7 +79,7 @@ public interface Cryptor extends Destroyable {
* @return Number of decrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.
* @throws DecryptFailedException If decryption failed
*/
Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException;
Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException;
/**
* @param pos First byte (inclusive)
@@ -87,7 +87,7 @@ public interface Cryptor extends Destroyable {
* @return Number of decrypted bytes. This might not be equal to the number of bytes requested due to potential overheads.
* @throws DecryptFailedException If decryption failed
*/
Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException;
Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException;
/**
* @return Number of encrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.

View File

@@ -45,15 +45,15 @@ public class SamplingCryptorDecorator extends AbstractCryptorDecorator implement
/* Cryptor */
@Override
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException {
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
return cryptor.decryptFile(encryptedFile, countingInputStream);
return cryptor.decryptFile(encryptedFile, countingInputStream, authenticate);
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException {
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
return cryptor.decryptRange(encryptedFile, countingInputStream, pos, length);
return cryptor.decryptRange(encryptedFile, countingInputStream, pos, length, authenticate);
}
@Override

View File

@@ -0,0 +1,15 @@
package org.cryptomator.crypto.exceptions;
import java.io.IOException;
public class CryptingException extends IOException {
private static final long serialVersionUID = -6622699014483319376L;
public CryptingException(String string) {
super(string);
}
public CryptingException(String string, Throwable t) {
super(string, t);
}
}

View File

@@ -1,6 +1,6 @@
package org.cryptomator.crypto.exceptions;
public class DecryptFailedException extends StorageCryptingException {
public class DecryptFailedException extends CryptingException {
private static final long serialVersionUID = -3855673600374897828L;
public DecryptFailedException(Throwable t) {

View File

@@ -1,6 +1,6 @@
package org.cryptomator.crypto.exceptions;
public class EncryptFailedException extends StorageCryptingException {
public class EncryptFailedException extends CryptingException {
private static final long serialVersionUID = -3855673600374897828L;
public EncryptFailedException(String msg) {

View File

@@ -0,0 +1,11 @@
package org.cryptomator.crypto.exceptions;
public class MasterkeyDecryptionException extends Exception {
private static final long serialVersionUID = -6241452734672333206L;
public MasterkeyDecryptionException(String string) {
super(string);
}
}

View File

@@ -1,13 +0,0 @@
package org.cryptomator.crypto.exceptions;
public class StorageCryptingException extends Exception {
private static final long serialVersionUID = -6622699014483319376L;
public StorageCryptingException(String string) {
super(string);
}
public StorageCryptingException(String string, Throwable t) {
super(string, t);
}
}

View File

@@ -1,6 +1,6 @@
package org.cryptomator.crypto.exceptions;
public class UnsupportedKeyLengthException extends StorageCryptingException {
public class UnsupportedKeyLengthException extends MasterkeyDecryptionException {
private static final long serialVersionUID = 8114147446419390179L;
private final int requestedLength;

View File

@@ -1,6 +1,6 @@
package org.cryptomator.crypto.exceptions;
public class WrongPasswordException extends StorageCryptingException {
public class WrongPasswordException extends MasterkeyDecryptionException {
private static final long serialVersionUID = -602047799678568780L;
public WrongPasswordException() {

View File

@@ -32,6 +32,12 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<!-- apache commons -->
<dependency>

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