mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-04-19 00:56:52 -04:00
New MAC authentication warning, preventing CCAs, but allowing to force-decrypt unauthentic files.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user