diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java index 9be4ddf39..dd52a84c2 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java @@ -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(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); } } } diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java index a506becd1..f8f7f9750 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java @@ -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); } } diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java index bebe801cc..8bb6686f3 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java @@ -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); + } + } + } diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java index d18b77c84..4acbbe446 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java @@ -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: diff --git a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java b/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java index fc87f5694..f31025d71 100644 --- a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java +++ b/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java @@ -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); diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptorDecorator.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptorDecorator.java index 81b024d0d..5767a717d 100644 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptorDecorator.java +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptorDecorator.java @@ -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 diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java index d6f637e8a..720ee011c 100644 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java @@ -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. diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingCryptorDecorator.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingCryptorDecorator.java index 8f056054b..704e12d1c 100644 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingCryptorDecorator.java +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingCryptorDecorator.java @@ -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 diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/CryptingException.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/CryptingException.java new file mode 100644 index 000000000..279c94028 --- /dev/null +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/CryptingException.java @@ -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); + } +} \ No newline at end of file diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/DecryptFailedException.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/DecryptFailedException.java index 5619463ee..d61cb9446 100644 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/DecryptFailedException.java +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/DecryptFailedException.java @@ -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) { diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/EncryptFailedException.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/EncryptFailedException.java index 62d981f92..6a535a90d 100644 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/EncryptFailedException.java +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/EncryptFailedException.java @@ -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) { diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/MasterkeyDecryptionException.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/MasterkeyDecryptionException.java new file mode 100644 index 000000000..277215e2e --- /dev/null +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/MasterkeyDecryptionException.java @@ -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); + } + +} diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/StorageCryptingException.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/StorageCryptingException.java deleted file mode 100644 index a618bbe6e..000000000 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/StorageCryptingException.java +++ /dev/null @@ -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); - } -} \ No newline at end of file diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/UnsupportedKeyLengthException.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/UnsupportedKeyLengthException.java index 0b9dfc499..548e05b6e 100644 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/UnsupportedKeyLengthException.java +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/UnsupportedKeyLengthException.java @@ -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; diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/WrongPasswordException.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/WrongPasswordException.java index 97fa04c67..d9e1c83b4 100644 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/WrongPasswordException.java +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/WrongPasswordException.java @@ -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() { diff --git a/main/ui/pom.xml b/main/ui/pom.xml index 7bcff00c0..fd55d66da 100644 --- a/main/ui/pom.xml +++ b/main/ui/pom.xml @@ -32,6 +32,12 @@ com.fasterxml.jackson.core jackson-databind + + + + com.google.guava + guava + diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java index d13ae94fa..aa603a71f 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java @@ -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(); diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/MacWarningsController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/MacWarningsController.java index d29b441bc..22fa8ffa5 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/MacWarningsController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/MacWarningsController.java @@ -1,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 warningsList; + private ListView warningsList; + + @FXML + private Button whitelistButton; private final Application application; - private final ListChangeListener macWarningsListener = this::warningsDidChange; - private final ListChangeListener weakMacWarningsListener = new WeakListChangeListener<>(macWarningsListener); + private final ObservableList warnings = FXCollections.observableArrayList(); + private final ListChangeListener unauthenticatedResourcesChangeListener = this::unauthenticatedResourcesDidChange; + private final ChangeListener 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() { + + @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 change) { - if (change.getList().isEmpty() && stage != null) { - change.getList().removeListener(weakMacWarningsListener); - stage.hide(); + private void unauthenticatedResourcesDidChange(Change 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 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(); + } + } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java index 50c0f5815..3c9c669b6 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java @@ -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 getDirectories() { + public Collection getVaults() { return vaultList.getItems(); } - public Collection getUnlockedDirectories() { - return getDirectories().stream().filter(d -> d.isUnlocked()).collect(Collectors.toSet()); + public Collection getUnlockedVaults() { + return getVaults().stream().filter(d -> d.isUnlocked()).collect(Collectors.toSet()); } /* public Getter/Setter */ diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java index 9155128c9..1a35982dd 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java @@ -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 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); diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java index 436023cab..f99933562 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java @@ -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 macWarningsListener = this::macWarningsDidChange; - private final ListChangeListener 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 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 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 { diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java index 78ec6df49..2e00b719b 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java @@ -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(); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/ActiveWindowStyleSupport.java b/main/ui/src/main/java/org/cryptomator/ui/util/ActiveWindowStyleSupport.java index 4455822a4..9eb6503df 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/ActiveWindowStyleSupport.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/ActiveWindowStyleSupport.java @@ -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 { @@ -18,9 +17,8 @@ public class ActiveWindowStyleSupport implements ChangeListener { } /** - * 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.
+ * 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.
*
* Example:
* @@ -32,7 +30,7 @@ public class ActiveWindowStyleSupport implements ChangeListener { * @return The observer */ public static ChangeListener startObservingFocus(final Window window) { - final ChangeListener observer = new WeakChangeListener(new ActiveWindowStyleSupport(window)); + final ChangeListener observer = new ActiveWindowStyleSupport(window); window.focusedProperty().addListener(observer); return observer; } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/ObservableListOnMainThread.java b/main/ui/src/main/java/org/cryptomator/ui/util/ObservableListOnMainThread.java index 379578b92..e9b60f8d2 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/ObservableListOnMainThread.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/ObservableListOnMainThread.java @@ -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 implements ObservableList { private final ObservableList list; @@ -173,8 +175,9 @@ class ObservableListOnMainThread implements ObservableList { } private void invalidated(Observable observable) { + final Collection listeners = ImmutableList.copyOf(invalidationListeners); Platform.runLater(() -> { - for (InvalidationListener listener : invalidationListeners) { + for (InvalidationListener listener : listeners) { listener.invalidated(this); } }); @@ -192,8 +195,9 @@ class ObservableListOnMainThread implements ObservableList { private void onChanged(Change change) { final Change c = new ListChange(change); + final Collection> listeners = ImmutableList.copyOf(listChangeListeners); Platform.runLater(() -> { - for (ListChangeListener listener : listChangeListeners) { + for (ListChangeListener listener : listeners) { listener.onChanged(c); } }); @@ -206,7 +210,7 @@ class ObservableListOnMainThread implements ObservableList { @Override public void removeListener(ListChangeListener listener) { - listChangeListeners.add(listener); + listChangeListeners.remove(listener); } private class ListChange extends ListChangeListener.Change { diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/ObservableSetOnMainThread.java b/main/ui/src/main/java/org/cryptomator/ui/util/ObservableSetOnMainThread.java index 64754042c..d49fb058b 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/ObservableSetOnMainThread.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/ObservableSetOnMainThread.java @@ -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 implements ObservableSet { private final ObservableSet set; @@ -91,8 +93,9 @@ class ObservableSetOnMainThread implements ObservableSet { } private void invalidated(Observable observable) { + final Collection listeners = ImmutableList.copyOf(invalidationListeners); Platform.runLater(() -> { - for (InvalidationListener listener : invalidationListeners) { + for (InvalidationListener listener : listeners) { listener.invalidated(this); } }); @@ -110,8 +113,9 @@ class ObservableSetOnMainThread implements ObservableSet { private void onChanged(Change change) { final Change c = new SetChange(this, change.getElementAdded(), change.getElementRemoved()); + final Collection> listeners = ImmutableList.copyOf(setChangeListeners); Platform.runLater(() -> { - for (SetChangeListener listener : setChangeListeners) { + for (SetChangeListener listener : listeners) { listener.onChanged(c); } }); diff --git a/main/ui/src/main/resources/fxml/mac_warnings.fxml b/main/ui/src/main/resources/fxml/mac_warnings.fxml index 0c53d4176..17322ff19 100644 --- a/main/ui/src/main/resources/fxml/mac_warnings.fxml +++ b/main/ui/src/main/resources/fxml/mac_warnings.fxml @@ -24,7 +24,7 @@ -