Compare commits

...

18 Commits
0.5.2 ... 0.6.0

Author SHA1 Message Date
Sebastian Stenzel
9024465d6c Beta 0.6.0 2015-03-14 22:09:25 +01:00
Sebastian Stenzel
f22142a876 Improved unmounting (failing, if encrypted drive is still busy) 2015-03-14 21:58:52 +01:00
Sebastian Stenzel
652c4cbafb Using 96 bit of random data and a 32 bit counter (as specified in https://tools.ietf.org/html/rfc3686#section-4). Thus maximum file size supported by Cryptomator is 64GiB, but decreasing risk of IV collisions to 1 : 2^48 2015-03-14 21:58:06 +01:00
Sebastian Stenzel
188a13b202 - better handling of MAC auth fails, providing link to help page
- use random data as file size obfuscation padding
- fixed osx unmount error
- new attempt to close #41
2015-03-14 19:11:24 +01:00
Sebastian Stenzel
75c21b4c9b fixes #37 2015-03-14 12:37:28 +01:00
Sebastian Stenzel
c7ecd612c9 added update notification 2015-03-14 12:34:11 +01:00
Sebastian Stenzel
3f8f0b1fa7 Update README.md 2015-03-13 13:24:35 +01:00
Sebastian Stenzel
2b4b359adb Merge branch '0.5.3'
Conflicts:
	main/core/pom.xml
	main/crypto-aes/pom.xml
	main/crypto-api/pom.xml
	main/pom.xml
	main/ui/pom.xml
2015-03-12 19:51:20 +01:00
Sebastian Stenzel
0562a909f9 fixes #46 2015-03-12 19:26:20 +01:00
Sebastian Stenzel
c10d80de18 fixes #35 2015-03-12 19:10:43 +01:00
Sebastian Stenzel
05abea0508 Updated welcome screen 2015-03-12 09:40:59 +01:00
Sebastian Stenzel
d19ffc327b improved windows WebDAV mounting 2015-03-11 21:18:53 +01:00
Sebastian Stenzel
a042c14fb9 changed version number 2015-03-11 19:38:11 +01:00
Sebastian Stenzel
a4be81267e preparation for some windows fixes, that need to be done during installation. This allows files of up to 4GiB 2015-03-11 19:36:20 +01:00
Sebastian Stenzel
c1dd902a10 Async MAC authentication for HTTP range requests. Fixes #38 2015-03-09 16:32:59 +01:00
Sebastian Stenzel
0994e7bb39 Show warning dialog, if MAC check failed. 2015-03-09 09:56:25 +01:00
Sebastian Stenzel
1f3b91f187 add license and gvfs dependencies to .deb package 2015-03-07 02:37:30 +01:00
Sebastian Stenzel
e883a04577 Merge remote-tracking branch 'origin/master' into 0.5.2
Conflicts:
	main/core/pom.xml
	main/crypto-aes/pom.xml
	main/crypto-api/pom.xml
	main/pom.xml
	main/ui/pom.xml
2015-03-06 15:06:31 +01:00
63 changed files with 1378 additions and 171 deletions

View File

@@ -1,6 +1,7 @@
Cryptomator
====================
[![Build Status](https://travis-ci.org/totalvoidness/cryptomator.svg?branch=master)](https://travis-ci.org/totalvoidness/cryptomator)
[![Join the chat at https://gitter.im/totalvoidness/cryptomator](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/totalvoidness/cryptomator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Multiplatform transparent client-side encryption of your files in the cloud.
@@ -8,8 +9,8 @@ Multiplatform transparent client-side encryption of your files in the cloud.
If you want to take a look at the current beta version, go ahead and get your copy of cryptomator on [Cryptomator.org](https://cryptomator.org) or clone and build Cryptomator using Maven (instructions below).
## Features
- Totally transparent: Just work on the encrypted volume, as if it was an USB drive
- Works with Dropbox, OneDrive (Skydrive), Google Drive and any other cloud storage, that syncs with a local directory
- Totally transparent: Just work on the encrypted volume, as if it was an USB flash drive
- Works with Dropbox, OneDrive (Skydrive), Google Drive and any other cloud storage, that syncs with a local directory.
- In fact it works with any directory. You can use it to encrypt as many folders as you like
- AES encryption with 256 bit key length
- Client-side. No accounts, no data shared with any online service
@@ -17,18 +18,19 @@ If you want to take a look at the current beta version, go ahead and get your co
- No need to provide credentials for any 3rd party service
- Open Source means: No backdoors. Control is better than trust
- Use as many encrypted folders in your dropbox as you want. Each having individual passwords
- No commerical interest, no government agency, no wasted taxpayers' money ;-)
### Privacy
- 256 bit keys (unlimited strength policy bundled with native binaries - 128 bit elsewhere)
- Scrypt key derivation
- Cryptographically secure random numbers for salts, IVs and the masterkey of course
- Sensitive data is swiped from the heap asap
- Lightweight: Complexity kills security
- Lightweight: [Complexity kills security](https://www.schneier.com/essays/archives/1999/11/a_plea_for_simplicit.html)
### Consistency
- HMAC over file contents to recognize changed ciphertext before decryption
- I/O operations are transactional and atomic, if the file systems supports it
- Each file contains all information needed for decryption (except for the key of course). No common metadata means no SPOF
- Each file contains all information needed for decryption (except for the key of course). No common metadata means no [SPOF](http://en.wikipedia.org/wiki/Single_point_of_failure)
## Building
@@ -36,14 +38,14 @@ If you want to take a look at the current beta version, go ahead and get your co
* Java 8
* Maven 3
* Optional: OS-dependent build tools for native packaging
* Optional: JCE unlimited strength policy (needed for 256 bit keys)
* Optional: JCE unlimited strength policy files (needed for 256 bit keys)
#### Building on Debian-based OS
```bash
apt-get install oracle-java8-installer oracle-java8-unlimited-jce-policy fakeroot maven git
git clone https://github.com/totalvoidness/cryptomator.git
cd cryptomator/main
git checkout v0.4.0
git checkout v0.5.1
mvn clean install
```
@@ -51,4 +53,3 @@ mvn clean install
Distributed under the MIT X Consortium license. See the LICENSE file for more info.
[![Build Status](https://travis-ci.org/totalvoidness/cryptomator.svg?branch=master)](https://travis-ci.org/totalvoidness/cryptomator)

View File

@@ -12,14 +12,14 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.5.2</version>
<version>0.6.0</version>
</parent>
<artifactId>core</artifactId>
<name>Cryptomator WebDAV and I/O module</name>
<properties>
<jetty.version>9.2.5.v20141112</jetty.version>
<jackrabbit.version>2.9.0</jackrabbit.version>
<jetty.version>9.2.10.v20150310</jetty.version>
<jackrabbit.version>2.9.1</jackrabbit.version>
<commons.transaction.version>1.2</commons.transaction.version>
<jta.version>1.1</jta.version>
</properties>
@@ -48,7 +48,13 @@
<artifactId>jackrabbit-webdav</artifactId>
<version>${jackrabbit.version}</version>
</dependency>
<!-- Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<!-- I/O -->
<dependency>
<groupId>commons-io</groupId>

View File

@@ -11,6 +11,7 @@ package org.cryptomator.webdav;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.Collection;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
@@ -83,11 +84,13 @@ public final class WebDavServer {
/**
* @param workDir Path of encrypted folder.
* @param cryptor A fully initialized cryptor instance ready to en- or decrypt streams.
* @param failingMacCollection A (observable, thread-safe) collection, to which the names of resources are written, whose MAC
* authentication fails.
* @param name The name of the folder. Must be non-empty and only contain any of
* _ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
* @return servlet
*/
public ServletLifeCycleAdapter createServlet(final Path workDir, final Cryptor cryptor, String name) {
public ServletLifeCycleAdapter createServlet(final Path workDir, final Cryptor cryptor, final Collection<String> failingMacCollection, final String name) {
try {
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("name empty");
@@ -98,7 +101,7 @@ public final class WebDavServer {
final URI uri = new URI(null, null, localConnector.getHost(), localConnector.getLocalPort(), "/" + UUID.randomUUID().toString() + "/" + name, null, null);
final ServletContextHandler servletContext = new ServletContextHandler(servletCollection, uri.getRawPath(), ServletContextHandler.SESSIONS);
final ServletHolder servlet = getWebDavServletHolder(workDir.toString(), cryptor);
final ServletHolder servlet = getWebDavServletHolder(workDir.toString(), cryptor, failingMacCollection);
servletContext.addServlet(servlet, "/*");
servletCollection.mapContexts();
@@ -110,8 +113,8 @@ public final class WebDavServer {
}
}
private ServletHolder getWebDavServletHolder(final String workDir, final Cryptor cryptor) {
final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor));
private ServletHolder getWebDavServletHolder(final String workDir, final Cryptor cryptor, final Collection<String> failingMacCollection) {
final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor, failingMacCollection));
result.setInitParameter(WebDavServlet.CFG_FS_ROOT, workDir);
return result;
}
@@ -123,7 +126,7 @@ public final class WebDavServer {
/**
* Exposes implementation-specific methods to other modules.
*/
public class ServletLifeCycleAdapter {
public class ServletLifeCycleAdapter implements AutoCloseable {
private final LifeCycle lifecycle;
private final URI servletUri;
@@ -161,6 +164,11 @@ public final class WebDavServer {
return servletUri;
}
@Override
public void close() throws Exception {
this.stop();
}
}
}

View File

@@ -6,7 +6,7 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.webdav.jackrabbit.resources;
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.nio.file.AtomicMoveNotSupportedException;

View File

@@ -0,0 +1,19 @@
package org.cryptomator.webdav.jackrabbit;
import java.util.Collection;
class CryptoWarningHandler {
private final Collection<String> resourcesWithInvalidMac;
public CryptoWarningHandler(Collection<String> resourcesWithInvalidMac) {
this.resourcesWithInvalidMac = resourcesWithInvalidMac;
}
public void macAuthFailed(String resourceName) {
if (!resourcesWithInvalidMac.contains(resourceName)) {
resourcesWithInvalidMac.add(resourceName);
}
}
}

View File

@@ -10,6 +10,7 @@ package org.cryptomator.webdav.jackrabbit;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.ExecutorService;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.jackrabbit.webdav.DavException;
@@ -23,20 +24,19 @@ import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.apache.jackrabbit.webdav.lock.SimpleLockManager;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.webdav.jackrabbit.resources.EncryptedDir;
import org.cryptomator.webdav.jackrabbit.resources.EncryptedFile;
import org.cryptomator.webdav.jackrabbit.resources.EncryptedFilePart;
import org.cryptomator.webdav.jackrabbit.resources.NonExistingNode;
import org.cryptomator.webdav.jackrabbit.resources.ResourcePathUtils;
import org.eclipse.jetty.http.HttpHeader;
class DavResourceFactoryImpl implements DavResourceFactory {
private final LockManager lockManager = new SimpleLockManager();
private final Cryptor cryptor;
private final CryptoWarningHandler cryptoWarningHandler;
private final ExecutorService backgroundTaskExecutor;
DavResourceFactoryImpl(Cryptor cryptor) {
DavResourceFactoryImpl(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, ExecutorService backgroundTaskExecutor) {
this.cryptor = cryptor;
this.cryptoWarningHandler = cryptoWarningHandler;
this.backgroundTaskExecutor = backgroundTaskExecutor;
}
@Override
@@ -70,11 +70,11 @@ class DavResourceFactoryImpl implements DavResourceFactory {
}
private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, DavServletRequest request) {
return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor);
return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, cryptoWarningHandler, backgroundTaskExecutor);
}
private EncryptedFile createFile(DavResourceLocator locator, DavSession session) {
return new EncryptedFile(this, locator, session, lockManager, cryptor);
return new EncryptedFile(this, locator, session, lockManager, cryptor, cryptoWarningHandler);
}
private EncryptedDir createDirectory(DavResourceLocator locator, DavSession session) {

View File

@@ -6,7 +6,7 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.webdav.jackrabbit.resources;
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.nio.channels.SeekableByteChannel;
@@ -36,13 +36,15 @@ import org.apache.jackrabbit.webdav.property.DavPropertyName;
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.apache.jackrabbit.webdav.property.ResourceType;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.exceptions.CounterOverflowException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
import org.cryptomator.webdav.exceptions.DavRuntimeException;
import org.cryptomator.webdav.exceptions.DecryptFailedRuntimeException;
import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EncryptedDir extends AbstractEncryptedNode {
class EncryptedDir extends AbstractEncryptedNode {
private static final Logger LOG = LoggerFactory.getLogger(EncryptedDir.class);
@@ -85,6 +87,12 @@ public class EncryptedDir extends AbstractEncryptedNode {
} catch (IOException e) {
LOG.error("Failed to create file.", e);
throw new IORuntimeException(e);
} catch (CounterOverflowException e) {
// lets indicate this to the client as a "file too big" error
throw new DavException(DavServletResponse.SC_INSUFFICIENT_SPACE_ON_RESOURCE, e);
} catch (EncryptFailedException e) {
LOG.error("Encryption failed for unknown reasons.", e);
throw new IllegalStateException("Encryption failed for unknown reasons.", e);
} finally {
IOUtils.closeQuietly(inputContext.getInputStream());
}

View File

@@ -6,7 +6,7 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.webdav.jackrabbit.resources;
package org.cryptomator.webdav.jackrabbit;
import java.io.EOFException;
import java.io.IOException;
@@ -36,12 +36,15 @@ import org.eclipse.jetty.http.HttpHeaderValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EncryptedFile extends AbstractEncryptedNode {
class EncryptedFile extends AbstractEncryptedNode {
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFile.class);
public EncryptedFile(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
protected final CryptoWarningHandler cryptoWarningHandler;
public EncryptedFile(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler) {
super(factory, locator, session, lockManager, cryptor);
this.cryptoWarningHandler = cryptoWarningHandler;
}
@Override
@@ -67,7 +70,7 @@ public class EncryptedFile extends AbstractEncryptedNode {
@Override
public void spool(OutputContext outputContext) throws IOException {
final Path path = ResourcePathUtils.getPhysicalPath(this);
if (Files.exists(path)) {
if (Files.isRegularFile(path)) {
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString());
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
@@ -81,7 +84,7 @@ public class EncryptedFile extends AbstractEncryptedNode {
} catch (EOFException e) {
LOG.warn("Unexpected end of stream (possibly client hung up).");
} catch (MacAuthenticationFailedException e) {
LOG.warn("MAC authentication failed, file content {} might be compromised.", getLocator().getResourcePath());
cryptoWarningHandler.macAuthFailed(getLocator().getResourcePath());
} catch (DecryptFailedException e) {
throw new IOException("Error decrypting file " + path.toString(), e);
}

View File

@@ -1,13 +1,16 @@
package org.cryptomator.webdav.jackrabbit.resources;
package org.cryptomator.webdav.jackrabbit;
import java.io.EOFException;
import java.io.IOException;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
@@ -25,17 +28,21 @@ import org.eclipse.jetty.http.HttpHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
/**
* Delivers only the requested range of bytes from a file.
*
* @see {@link https://tools.ietf.org/html/rfc7233#section-4}
*/
public class EncryptedFilePart extends EncryptedFile {
class EncryptedFilePart extends EncryptedFile {
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFilePart.class);
private static final String BYTE_UNIT_PREFIX = "bytes=";
private static final char RANGE_SET_SEP = ',';
private static final char RANGE_SEP = '-';
private static final Cache<DavResourceLocator, MacAuthenticationJob> cachedMacAuthenticationJobs = CacheBuilder.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build();
/**
* e.g. range -500 (gets the last 500 bytes) -> (-1, 500)
@@ -49,13 +56,23 @@ public class EncryptedFilePart extends EncryptedFile {
private final Set<Pair<Long, Long>> requestedContentRanges = new HashSet<Pair<Long, Long>>();
public EncryptedFilePart(DavResourceFactory factory, DavResourceLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor) {
super(factory, locator, session, lockManager, cryptor);
public EncryptedFilePart(DavResourceFactory factory, DavResourceLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler,
ExecutorService backgroundTaskExecutor) {
super(factory, locator, session, lockManager, cryptor, cryptoWarningHandler);
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
if (rangeHeader == null) {
throw new IllegalArgumentException("HTTP request doesn't contain a range header");
}
determineByteRanges(rangeHeader);
synchronized (cachedMacAuthenticationJobs) {
if (cachedMacAuthenticationJobs.getIfPresent(locator) == null) {
final MacAuthenticationJob macAuthJob = new MacAuthenticationJob(locator);
cachedMacAuthenticationJobs.put(locator, macAuthJob);
backgroundTaskExecutor.submit(macAuthJob);
}
}
}
private void determineByteRanges(String rangeHeader) {
@@ -110,7 +127,7 @@ public class EncryptedFilePart extends EncryptedFile {
@Override
public void spool(OutputContext outputContext) throws IOException {
final Path path = ResourcePathUtils.getPhysicalPath(this);
if (Files.exists(path)) {
if (Files.isRegularFile(path)) {
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
final Long fileSize = cryptor.decryptedContentLength(channel);
@@ -135,4 +152,48 @@ public class EncryptedFilePart extends EncryptedFile {
return String.format("%d-%d/%d", firstByte, lastByte, completeLength);
}
private class MacAuthenticationJob implements Runnable {
private final DavResourceLocator locator;
public MacAuthenticationJob(final DavResourceLocator locator) {
if (locator == null) {
throw new IllegalArgumentException("locator must not be null.");
}
this.locator = locator;
}
@Override
public void run() {
final Path path = ResourcePathUtils.getPhysicalPath(locator);
if (Files.isRegularFile(path) && Files.isReadable(path)) {
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
final boolean authentic = cryptor.isAuthentic(channel);
if (!authentic) {
cryptoWarningHandler.macAuthFailed(locator.getResourcePath());
}
} catch (ClosedByInterruptException ex) {
LOG.debug("Couldn't finish MAC verification due to interruption of worker thread.");
} catch (IOException e) {
LOG.error("IOException during MAC verification of " + path.toString(), e);
}
}
}
@Override
public int hashCode() {
return locator.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof MacAuthenticationJob) {
final MacAuthenticationJob other = (MacAuthenticationJob) obj;
return this.locator.equals(other.locator);
} else {
return false;
}
}
}
}

View File

@@ -6,7 +6,7 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.webdav.jackrabbit.resources;
package org.cryptomator.webdav.jackrabbit;
import java.nio.file.attribute.FileTime;
import java.time.Instant;

View File

@@ -1,4 +1,4 @@
package org.cryptomator.webdav.jackrabbit.resources;
package org.cryptomator.webdav.jackrabbit;
import org.apache.jackrabbit.webdav.property.AbstractDavProperty;
import org.apache.jackrabbit.webdav.property.DavPropertyName;

View File

@@ -6,7 +6,7 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.webdav.jackrabbit.resources;
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
@@ -21,7 +21,7 @@ import org.apache.jackrabbit.webdav.io.OutputContext;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.cryptomator.crypto.Cryptor;
public class NonExistingNode extends AbstractEncryptedNode {
class NonExistingNode extends AbstractEncryptedNode {
public NonExistingNode(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
super(factory, locator, session, lockManager, cryptor);

View File

@@ -6,7 +6,7 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.webdav.jackrabbit.resources;
package org.cryptomator.webdav.jackrabbit;
import java.nio.file.FileSystems;
import java.nio.file.Path;
@@ -14,7 +14,7 @@ import java.nio.file.Path;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceLocator;
public final class ResourcePathUtils {
final class ResourcePathUtils {
private ResourcePathUtils() {
throw new IllegalStateException("not instantiable");

View File

@@ -8,6 +8,11 @@
******************************************************************************/
package org.cryptomator.webdav.jackrabbit;
import java.util.Collection;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
@@ -27,22 +32,39 @@ public class WebDavServlet extends AbstractWebdavServlet {
private DavLocatorFactory davLocatorFactory;
private DavResourceFactory davResourceFactory;
private final Cryptor cryptor;
private final CryptoWarningHandler cryptoWarningHandler;
private ExecutorService backgroundTaskExecutor;
public WebDavServlet(final Cryptor cryptor) {
public WebDavServlet(final Cryptor cryptor, final Collection<String> failingMacCollection) {
super();
this.cryptor = cryptor;
this.cryptoWarningHandler = new CryptoWarningHandler(failingMacCollection);
}
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
davSessionProvider = new DavSessionProviderImpl();
final String fsRoot = config.getInitParameter(CFG_FS_ROOT);
this.davLocatorFactory = new DavLocatorFactoryImpl(fsRoot, cryptor);
backgroundTaskExecutor = Executors.newCachedThreadPool();
davSessionProvider = new DavSessionProviderImpl();
davLocatorFactory = new DavLocatorFactoryImpl(fsRoot, cryptor);
davResourceFactory = new DavResourceFactoryImpl(cryptor, cryptoWarningHandler, backgroundTaskExecutor);
}
this.davResourceFactory = new DavResourceFactoryImpl(cryptor);
@Override
public void destroy() {
backgroundTaskExecutor.shutdown();
try {
final boolean tasksFinished = backgroundTaskExecutor.awaitTermination(2, TimeUnit.SECONDS);
if (!tasksFinished) {
backgroundTaskExecutor.shutdownNow();
}
} catch (InterruptedException e) {
backgroundTaskExecutor.shutdownNow();
Thread.currentThread().interrupt();
} finally {
super.destroy();
}
}
@Override

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.5.2</version>
<version>0.6.0</version>
</parent>
<artifactId>crypto-aes</artifactId>
<name>Cryptomator cryptographic module (AES)</name>

View File

@@ -46,7 +46,10 @@ import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.crypto.generators.SCrypt;
import org.cryptomator.crypto.AbstractCryptor;
import org.cryptomator.crypto.CryptorIOSupport;
import org.cryptomator.crypto.aes256.CounterAwareInputStream.CounterAwareInputLimitReachedException;
import org.cryptomator.crypto.exceptions.CounterOverflowException;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
@@ -416,6 +419,33 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
encryptedFile.write(encryptedFileSizeBuffer);
}
@Override
public boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException {
// init mac:
final Mac calculatedMac = this.hmacSha256(hMacMasterKey);
// read stored mac:
encryptedFile.position(16);
final ByteBuffer storedMac = ByteBuffer.allocate(calculatedMac.getMacLength());
final int numMacBytesRead = encryptedFile.read(storedMac);
// check validity of header:
if (numMacBytesRead != calculatedMac.getMacLength()) {
throw new IOException("Failed to read file header.");
}
// go to begin of content:
encryptedFile.position(64);
// calculated MAC
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
final InputStream macIn = new MacInputStream(in, calculatedMac);
IOUtils.copyLarge(macIn, new NullOutputStream());
// compare (in constant time):
return MessageDigest.isEqual(storedMac.array(), calculatedMac.doFinal());
}
@Override
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
// read iv:
@@ -483,10 +513,10 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
long firstRelevantBlock = pos / AES_BLOCK_LENGTH; // cut of fraction!
long beginOfFirstRelevantBlock = firstRelevantBlock * AES_BLOCK_LENGTH;
long offsetInsideFirstRelevantBlock = pos - beginOfFirstRelevantBlock;
countingIv.putLong(AES_BLOCK_LENGTH - Long.BYTES, firstRelevantBlock);
countingIv.putInt(AES_BLOCK_LENGTH - Integer.BYTES, (int) firstRelevantBlock); // int-cast is possible, as max file size is 64GiB
// fast forward stream:
encryptedFile.position(64 + beginOfFirstRelevantBlock);
encryptedFile.position(64l + beginOfFirstRelevantBlock);
// generate cipher:
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.DECRYPT_MODE);
@@ -498,13 +528,13 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
}
@Override
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException {
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
// truncate file
encryptedFile.truncate(0);
// use an IV, whose last 8 bytes store a long used in counter mode and write initial value to file.
final ByteBuffer countingIv = ByteBuffer.wrap(randomData(AES_BLOCK_LENGTH));
countingIv.putLong(AES_BLOCK_LENGTH - Long.BYTES, 0l);
countingIv.putInt(AES_BLOCK_LENGTH - Integer.BYTES, 0);
encryptedFile.write(countingIv);
// init crypto stuff:
@@ -523,18 +553,29 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
final OutputStream macOut = new MacOutputStream(out, mac);
final OutputStream cipheredOut = new CipherOutputStream(macOut, cipher);
final OutputStream blockSizeBufferedOut = new BufferedOutputStream(cipheredOut, AES_BLOCK_LENGTH);
final Long plaintextSize = IOUtils.copyLarge(plaintextFile, blockSizeBufferedOut);
final InputStream lengthLimitingIn = new CounterAwareInputStream(plaintextFile);
final Long plaintextSize;
try {
plaintextSize = IOUtils.copyLarge(lengthLimitingIn, blockSizeBufferedOut);
} catch (CounterAwareInputLimitReachedException ex) {
encryptedFile.truncate(64l + CounterAwareInputStream.SIXTY_FOUR_GIGABYE);
encryptedContentLength(encryptedFile, CounterAwareInputStream.SIXTY_FOUR_GIGABYE);
// no additional padding needed here, as 64GiB is a multiple of 128bit
throw new CounterOverflowException("File size exceeds limit (64Gib). Aborting to prevent counter overflow.");
}
// ensure total byte count is a multiple of the block size, in CTR mode:
final int remainderToFillLastBlock = AES_BLOCK_LENGTH - (int) (plaintextSize % AES_BLOCK_LENGTH);
blockSizeBufferedOut.write(new byte[remainderToFillLastBlock]);
// append a few blocks of fake data:
final int numberOfPlaintextBlocks = (int) Math.ceil(plaintextSize / AES_BLOCK_LENGTH);
final int upToTenPercentFakeBlocks = (int) Math.ceil(Math.random() * 0.1 * numberOfPlaintextBlocks);
final byte[] emptyBytes = new byte[AES_BLOCK_LENGTH];
for (int i = 0; i < upToTenPercentFakeBlocks; i += AES_BLOCK_LENGTH) {
blockSizeBufferedOut.write(emptyBytes);
// for filesizes of up to 16GiB: append a few blocks of fake data:
if (plaintextSize < (long) (Integer.MAX_VALUE / 4) * AES_BLOCK_LENGTH) {
final int numberOfPlaintextBlocks = (int) Math.ceil(plaintextSize / AES_BLOCK_LENGTH);
final int upToTenPercentFakeBlocks = (int) Math.ceil(Math.random() * 0.1 * numberOfPlaintextBlocks);
final byte[] emptyBytes = this.randomData(AES_BLOCK_LENGTH);
for (int i = 0; i < upToTenPercentFakeBlocks; i += AES_BLOCK_LENGTH) {
blockSizeBufferedOut.write(emptyBytes);
}
}
blockSizeBufferedOut.flush();

View File

@@ -0,0 +1,59 @@
package org.cryptomator.crypto.aes256;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.atomic.AtomicLong;
import javax.crypto.Mac;
/**
* Updates a {@link Mac} with the bytes read from this stream.
*/
class CounterAwareInputStream extends FilterInputStream {
static final long SIXTY_FOUR_GIGABYE = 1024l * 1024l * 1024l * 64l;
private final AtomicLong counter;
/**
* @param in Stream from which to read contents, which will update the Mac.
* @param mac Mac to be updated during writes.
*/
public CounterAwareInputStream(InputStream in) {
super(in);
this.counter = new AtomicLong(0l);
}
@Override
public int read() throws IOException {
int b = in.read();
if (b != -1) {
final long currentValue = counter.incrementAndGet();
failWhen64GibReached(currentValue);
}
return b;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int read = in.read(b, off, len);
if (read > 0) {
final long currentValue = counter.addAndGet(read);
failWhen64GibReached(currentValue);
}
return read;
}
private void failWhen64GibReached(long currentValue) throws CounterAwareInputLimitReachedException {
if (currentValue > SIXTY_FOUR_GIGABYE) {
throw new CounterAwareInputLimitReachedException();
}
}
static class CounterAwareInputLimitReachedException extends IOException {
private static final long serialVersionUID = -1905012809288019359L;
}
}

View File

@@ -25,7 +25,9 @@ class MacInputStream extends FilterInputStream {
@Override
public int read() throws IOException {
int b = in.read();
mac.update((byte) b);
if (b != -1) {
mac.update((byte) b);
}
return b;
}

View File

@@ -21,6 +21,7 @@ import java.util.Map;
import org.apache.commons.io.IOUtils;
import org.cryptomator.crypto.CryptorIOSupport;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.junit.Assert;
@@ -69,8 +70,40 @@ public class Aes256CryptorTest {
}
}
@Test
public void testIntegrityAuthentication() throws IOException, DecryptFailedException, EncryptFailedException {
// our test plaintext data:
final byte[] plaintextData = "Hello World".getBytes();
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
// init cryptor:
final Aes256Cryptor cryptor = new Aes256Cryptor();
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(96);
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
IOUtils.closeQuietly(encryptedOut);
encryptedData.position(0);
// toggle one bit inf first content byte:
encryptedData.position(64);
final byte fifthByte = encryptedData.get();
encryptedData.position(64);
encryptedData.put((byte) (fifthByte ^ 0x01));
encryptedData.position(0);
// check mac (should return false)
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final boolean authentic = cryptor.isAuthentic(encryptedIn);
Assert.assertFalse(authentic);
}
@Test(expected = DecryptFailedException.class)
public void testIntegrityAuthentication() throws IOException, DecryptFailedException {
public void testIntegrityViolationDuringDecryption() throws IOException, DecryptFailedException, EncryptFailedException {
// our test plaintext data:
final byte[] plaintextData = "Hello World".getBytes();
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
@@ -102,7 +135,7 @@ public class Aes256CryptorTest {
}
@Test
public void testEncryptionAndDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
public void testEncryptionAndDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, EncryptFailedException {
// our test plaintext data:
final byte[] plaintextData = "Hello World".getBytes();
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
@@ -137,7 +170,7 @@ public class Aes256CryptorTest {
}
@Test
public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, EncryptFailedException {
// our test plaintext data:
final byte[] plaintextData = new byte[65536 * Integer.BYTES];
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.5.2</version>
<version>0.6.0</version>
</parent>
<artifactId>crypto-api</artifactId>
<name>Cryptomator cryptographic module API</name>

View File

@@ -16,6 +16,7 @@ import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Path;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
@@ -75,6 +76,11 @@ public interface Cryptor extends SensitiveDataSwipeListener {
*/
Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException;
/**
* @return true, if the stored MAC matches the calculated one.
*/
boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException;
/**
* @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
@@ -92,7 +98,7 @@ public interface Cryptor extends SensitiveDataSwipeListener {
/**
* @return Number of encrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.
*/
Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException;
Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException;
/**
* @return A filter, that returns <code>true</code> for encrypted files, i.e. if the file is an actual user payload and not a supporting

View File

@@ -10,6 +10,7 @@ import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
@@ -81,6 +82,11 @@ public class SamplingDecorator implements Cryptor, CryptorIOSampling {
return cryptor.decryptedContentLength(encryptedFile);
}
@Override
public boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException {
return cryptor.isAuthentic(encryptedFile);
}
@Override
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
@@ -94,7 +100,7 @@ public class SamplingDecorator implements Cryptor, CryptorIOSampling {
}
@Override
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException {
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
final InputStream countingInputStream = new CountingInputStream(encryptedBytes, plaintextFile);
return cryptor.encryptFile(countingInputStream, encryptedFile);
}

View File

@@ -0,0 +1,10 @@
package org.cryptomator.crypto.exceptions;
public class CounterOverflowException extends EncryptFailedException {
private static final long serialVersionUID = 380066751064534731L;
public CounterOverflowException(String msg) {
super(msg);
}
}

View File

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

View File

@@ -1,10 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright (c) 2014 Sebastian Stenzel This file is licensed under the terms of the MIT license. See the LICENSE.txt file for more info. Contributors: Sebastian Stenzel - initial API and implementation -->
<!--
Copyright (c) 2014 Sebastian Stenzel
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.5.2</version>
<version>0.6.0</version>
<packaging>pom</packaging>
<name>Cryptomator</name>
@@ -32,6 +39,7 @@
<commons-collections.version>4.0</commons-collections.version>
<commons-lang3.version>3.3.2</commons-lang3.version>
<commons-codec.version>1.10</commons-codec.version>
<commons-httpclient.version>3.1</commons-httpclient.version>
<jackson-databind.version>2.4.4</jackson-databind.version>
<mockito.version>1.10.19</mockito.version>
</properties>
@@ -103,7 +111,20 @@
<artifactId>commons-codec</artifactId>
<version>${commons-codec.version}</version>
</dependency>
<dependency>
<!-- org.apache.httpcomponents:httpclient is newer, but jackrabbit uses this version. We don't have a reason to upgrade -->
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>${commons-httpclient.version}</version>
</dependency>
<!-- Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
<!-- DI -->
<dependency>
<groupId>com.google.inject</groupId>

View File

@@ -0,0 +1,16 @@
Package: APPLICATION_PACKAGE
Version: APPLICATION_VERSION
Section: contrib/utils
Maintainer: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Homepage: https://cryptomator.org
Vcs-Git: https://github.com/totalvoidness/cryptomator.git
Vcs-Browser: https://github.com/totalvoidness/cryptomator
Priority: optional
Architecture: APPLICATION_ARCH
Provides: APPLICATION_PACKAGE
Installed-Size: APPLICATION_INSTALLED_SIZE
Depends: gvfs-bin, gvfs-backends, gvfs-fuse, xdg-utils
Description: Multi-platform client-side encryption of your cloud files.
Cryptomator provides free client-side AES encryption for your cloud files.
Create encrypted vaults, which get mounted as virtual volumes. Whatever
you save on one of these volumes will end up encrypted inside your vault.

View File

@@ -0,0 +1,23 @@
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: cryptomator
Source: <https://github.com/totalvoidness/cryptomator>
Copyright: 2015 Sebastian Stenzel <sebastian.stenzel@gmail.com> and contributors.
License: MIT
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
.
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@@ -0,0 +1,80 @@
;This file will be executed next to the application bundle image
;I.e. current directory will contain folder APPLICATION_NAME with application files
[Setup]
AppId={{PRODUCT_APP_IDENTIFIER}}
AppName=APPLICATION_NAME
AppVersion=APPLICATION_VERSION
AppVerName=APPLICATION_NAME APPLICATION_VERSION
AppPublisher=APPLICATION_VENDOR
AppComments=APPLICATION_COMMENTS
AppCopyright=APPLICATION_COPYRIGHT
AppPublisherURL=https://cryptomator.org/
;AppSupportURL=http://java.com/
;AppUpdatesURL=http://java.com/
DefaultDirName=APPLICATION_INSTALL_ROOT\APPLICATION_NAME
DisableStartupPrompt=Yes
DisableDirPage=No
DisableProgramGroupPage=Yes
DisableReadyPage=Yes
DisableFinishedPage=No
DisableWelcomePage=Yes
DefaultGroupName=APPLICATION_GROUP
;Optional License
LicenseFile=APPLICATION_LICENSE_FILE
;WinXP or above
MinVersion=0,5.1
OutputBaseFilename=INSTALLER_FILE_NAME
Compression=lzma
SolidCompression=yes
PrivilegesRequired=admin
SetupIconFile=APPLICATION_NAME\APPLICATION_NAME.ico
UninstallDisplayIcon={app}\APPLICATION_NAME.ico
UninstallDisplayName=APPLICATION_NAME
WizardImageStretch=No
WizardSmallImageFile=Cryptomator-setup-icon.bmp
WizardImageBackColor=$ffffff
ArchitecturesInstallIn64BitMode=ARCHITECTURE_BIT_MODE
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Registry]
;Root: HKCU; Subkey: "Software\Microsoft\Windows\CurrentVersion\Internet Settings"; ValueType: dword; ValueName: "AutoDetect"; ValueData: "0"
Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Services\WebClient\Parameters"; ValueType: dword; ValueName: "FileSizeLimitInBytes"; ValueData: "$ffffffff"
[Files]
Source: "APPLICATION_NAME\APPLICATION_NAME.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "APPLICATION_NAME\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{group}\APPLICATION_NAME"; Filename: "{app}\APPLICATION_NAME.exe"; IconFilename: "{app}\APPLICATION_NAME.ico"; Check: APPLICATION_MENU_SHORTCUT()
Name: "{commondesktop}\APPLICATION_NAME"; Filename: "{app}\APPLICATION_NAME.exe"; IconFilename: "{app}\APPLICATION_NAME.ico"; Check: APPLICATION_DESKTOP_SHORTCUT()
[Run]
Filename: "{app}\RUN_FILENAME.exe"; Description: "{cm:LaunchProgram,APPLICATION_NAME}"; Flags: nowait postinstall skipifsilent; Check: APPLICATION_NOT_SERVICE()
Filename: "{app}\RUN_FILENAME.exe"; Parameters: "-install -svcName ""APPLICATION_NAME"" -svcDesc ""APPLICATION_DESCRIPTION"" -mainExe ""APPLICATION_LAUNCHER_FILENAME"" START_ON_INSTALL RUN_AT_STARTUP"; Check: APPLICATION_SERVICE()
Filename: "net"; Parameters: "stop webclient"; Description: "Stopping WebClient..."; Flags: waituntilterminated runhidden
Filename: "net"; Parameters: "start webclient"; Description: "Restarting WebClient..."; Flags: waituntilterminated runhidden
[UninstallRun]
Filename: "{app}\RUN_FILENAME.exe "; Parameters: "-uninstall -svcName APPLICATION_NAME STOP_ON_UNINSTALL"; Check: APPLICATION_SERVICE()
[Code]
function returnTrue(): Boolean;
begin
Result := True;
end;
function returnFalse(): Boolean;
begin
Result := False;
end;
function InitializeSetup(): Boolean;
begin
// Possible future improvements:
// if version less or same => just launch app
// if upgrade => check if same app is running and wait for it to exit
// Add pack200/unpack200 support?
Result := True;
end;

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.5.2</version>
<version>0.6.0</version>
</parent>
<artifactId>ui</artifactId>
<name>Cryptomator GUI</name>
@@ -48,7 +48,11 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
</dependency>
<!-- DI -->
<dependency>
<groupId>com.google.inject</groupId>
@@ -78,6 +82,7 @@
<archive>
<manifestEntries>
<Main-Class>${exec.mainClass}</Main-Class>
<Implementation-Version>${project.version}</Implementation-Version>
</manifestEntries>
</archive>
</configuration>

View File

@@ -53,22 +53,19 @@ public class MainApplication extends Application {
}
private static Injector getInjector() {
try {
return Guice.createInjector(new MainModule());
} catch (Exception e) {
throw e;
}
return Guice.createInjector(new MainModule());
}
public MainApplication(Injector injector) {
this(injector.getInstance(ExecutorService.class), injector.getInstance(ControllerFactory.class), injector.getInstance(DeferredCloser.class));
this(injector.getInstance(ExecutorService.class), injector.getInstance(ControllerFactory.class), injector.getInstance(DeferredCloser.class), injector.getInstance(MainApplicationReference.class));
}
public MainApplication(ExecutorService executorService, ControllerFactory controllerFactory, DeferredCloser closer) {
public MainApplication(ExecutorService executorService, ControllerFactory controllerFactory, DeferredCloser closer, MainApplicationReference appRef) {
super();
this.executorService = executorService;
this.controllerFactory = controllerFactory;
this.closer = closer;
appRef.set(this);
}
@Override
@@ -179,4 +176,25 @@ public class MainApplication extends Application {
}
}
/**
* Needed to inject MainApplication. Problem: Application needs to be set asap after injector creation.
*/
static class MainApplicationReference {
private Application application;
private void set(Application application) {
this.application = application;
}
public Application get() {
if (application == null) {
throw new IllegalStateException("not yet ready.");
} else {
return application;
}
}
}
}

View File

@@ -8,22 +8,27 @@
******************************************************************************/
package org.cryptomator.ui;
import java.util.Comparator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javafx.application.Application;
import javafx.util.Callback;
import javax.inject.Named;
import javax.inject.Singleton;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.SamplingDecorator;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.ui.MainApplication.MainApplicationReference;
import org.cryptomator.ui.model.VaultFactory;
import org.cryptomator.ui.model.VaultObjectMapperProvider;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.settings.SettingsProvider;
import org.cryptomator.ui.util.DeferredCloser;
import org.cryptomator.ui.util.DeferredCloser.Closer;
import org.cryptomator.ui.util.SemVerComparator;
import org.cryptomator.ui.util.mount.WebDavMounter;
import org.cryptomator.ui.util.mount.WebDavMounterProvider;
import org.cryptomator.webdav.WebDavServer;
@@ -57,6 +62,24 @@ public class MainModule extends AbstractModule {
return cls -> injector.getInstance(cls);
}
@Provides
@Singleton
MainApplicationReference getApplicationBinding() {
return new MainApplicationReference();
}
@Provides
Application getApplication(MainApplicationReference ref) {
return ref.get();
}
@Provides
@Named("SemVer")
@Singleton
Comparator<String> getSemVerComparator() {
return new SemVerComparator();
}
@Provides
@Singleton
ExecutorService getExec() {

View File

@@ -0,0 +1,58 @@
package org.cryptomator.ui.controllers;
import javafx.application.Application;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.collections.WeakListChangeListener;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.ListView;
import javafx.stage.Stage;
import javax.inject.Inject;
public class MacWarningsController {
@FXML
private ListView<String> warningsList;
private Stage stage;
private final Application application;
@Inject
public MacWarningsController(Application application) {
this.application = application;
}
@FXML
private void didClickDismissButton(ActionEvent event) {
stage.hide();
}
@FXML
private void didClickMoreInformationButton(ActionEvent event) {
application.getHostServices().showDocument("https://cryptomator.org/help.html#macWarning");
}
public void setMacWarnings(ObservableList<String> macWarnings) {
this.warningsList.setItems(macWarnings);
this.warningsList.getItems().addListener(new WeakListChangeListener<String>(this::warningsDidChange));
}
// 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.hide();
}
}
public Stage getStage() {
return stage;
}
public void setStage(Stage stage) {
this.stage = stage;
}
}

View File

@@ -13,21 +13,25 @@ import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.SetChangeListener;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.geometry.Side;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
@@ -47,6 +51,8 @@ import org.cryptomator.ui.controls.DirectoryListCell;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.model.VaultFactory;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.util.ActiveWindowStyleSupport;
import org.cryptomator.ui.util.ObservableSetAggregator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -79,6 +85,9 @@ public class MainController implements Initializable, InitializationListener, Un
private final ControllerFactory controllerFactory;
private final Settings settings;
private final VaultFactory vaultFactoy;
private final ObservableList<String> aggregatedMacWarnings;
private final SetChangeListener<String> macWarningsAggregator;
private final AtomicBoolean macWarningsWindowVisible;
private ResourceBundle rb;
@@ -88,6 +97,9 @@ public class MainController implements Initializable, InitializationListener, Un
this.controllerFactory = controllerFactory;
this.settings = settings;
this.vaultFactoy = vaultFactoy;
this.aggregatedMacWarnings = FXCollections.observableList(new ArrayList<>());
this.macWarningsAggregator = new ObservableSetAggregator<>(this.aggregatedMacWarnings);
this.macWarningsWindowVisible = new AtomicBoolean();
}
@Override
@@ -98,6 +110,8 @@ public class MainController implements Initializable, InitializationListener, Un
vaultList.setItems(items);
vaultList.setCellFactory(this::createDirecoryListCell);
vaultList.getSelectionModel().getSelectedItems().addListener(this::selectedVaultDidChange);
aggregatedMacWarnings.addListener(this::macWarningsDidChange);
}
@FXML
@@ -124,18 +138,21 @@ public class MainController implements Initializable, InitializationListener, Un
final FileChooser fileChooser = new FileChooser();
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator vault", "*" + Vault.VAULT_FILE_EXTENSION));
final File file = fileChooser.showSaveDialog(stage);
if (file == null) {
return;
}
try {
if (file != null) {
final Path vaultDir;
// enforce .cryptomator file extension:
if (!file.getName().endsWith(Vault.VAULT_FILE_EXTENSION)) {
final Path correctedPath = file.toPath().resolveSibling(file.getName() + Vault.VAULT_FILE_EXTENSION);
vaultDir = Files.createDirectory(correctedPath);
} else {
vaultDir = Files.createDirectory(file.toPath());
}
addVault(vaultDir, true);
final Path vaultDir;
// enforce .cryptomator file extension:
if (!file.getName().endsWith(Vault.VAULT_FILE_EXTENSION)) {
vaultDir = file.toPath().resolveSibling(file.getName() + Vault.VAULT_FILE_EXTENSION);
} else {
vaultDir = file.toPath();
}
if (!Files.exists(vaultDir)) {
Files.createDirectory(vaultDir);
}
addVault(vaultDir, true);
} catch (IOException e) {
LOG.error("Unable to create vault", e);
}
@@ -181,7 +198,7 @@ public class MainController implements Initializable, InitializationListener, Un
private ListCell<Vault> createDirecoryListCell(ListView<Vault> param) {
final DirectoryListCell cell = new DirectoryListCell();
cell.setContextMenu(vaultListCellContextMenu);
cell.setVaultContextMenu(vaultListCellContextMenu);
return cell;
}
@@ -216,6 +233,12 @@ public class MainController implements Initializable, InitializationListener, Un
showChangePasswordView(selectedVault);
}
private void macWarningsDidChange(ListChangeListener.Change<? extends String> change) {
if (aggregatedMacWarnings.size() > 0) {
Platform.runLater(this::showMacWarningsWindow);
}
}
// ****************************************
// Subcontroller for right panel
// ****************************************
@@ -270,6 +293,7 @@ public class MainController implements Initializable, InitializationListener, Un
@Override
public void didUnlock(UnlockController ctrl) {
ctrl.getVault().getNamesOfResourcesWithInvalidMac().addListener(this.macWarningsAggregator);
showUnlockedView(ctrl.getVault());
Platform.setImplicitExit(false);
}
@@ -282,6 +306,7 @@ public class MainController implements Initializable, InitializationListener, Un
@Override
public void didLock(UnlockedController ctrl) {
ctrl.getVault().getNamesOfResourcesWithInvalidMac().removeListener(this.macWarningsAggregator);
showUnlockView(ctrl.getVault());
if (getUnlockedDirectories().isEmpty()) {
Platform.setImplicitExit(true);
@@ -299,6 +324,37 @@ public class MainController implements Initializable, InitializationListener, Un
showUnlockView(ctrl.getVault());
}
private void showMacWarningsWindow() {
if (macWarningsWindowVisible.getAndSet(true) == false) {
try {
final FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/mac_warnings.fxml"), rb);
loader.setControllerFactory(controllerFactory);
final Parent root = loader.load();
final Stage stage = new Stage();
stage.setTitle(rb.getString("macWarnings.windowTitle"));
stage.setScene(new Scene(root));
stage.sizeToScene();
stage.setResizable(false);
stage.setOnHidden(this::onHideMacWarningsWindow);
ActiveWindowStyleSupport.startObservingFocus(stage);
final MacWarningsController ctrl = loader.getController();
ctrl.setMacWarnings(this.aggregatedMacWarnings);
ctrl.setStage(stage);
stage.show();
} catch (IOException e) {
throw new IllegalStateException("Failed to load fxml file.", e);
}
}
}
private void onHideMacWarningsWindow(WindowEvent event) {
macWarningsWindowVisible.set(false);
aggregatedMacWarnings.clear();
}
/* Convenience */
public Collection<Vault> getDirectories() {

View File

@@ -113,10 +113,7 @@ public class UnlockController implements Initializable {
Files.copy(masterKeyPath, masterKeyBackupPath, StandardCopyOption.REPLACE_EXISTING);
vault.setUnlocked(true);
final Future<Boolean> futureMount = exec.submit(() -> vault.mount());
FXThreads.runOnMainThreadWhenFinished(exec, futureMount, this::didUnlockAndMount);
FXThreads.runOnMainThreadWhenFinished(exec, futureMount, (result) -> {
setControlsDisabled(false);
});
FXThreads.runOnMainThreadWhenFinished(exec, futureMount, this::unlockAndMountFinished);
} catch (DecryptFailedException | IOException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
@@ -143,9 +140,13 @@ public class UnlockController implements Initializable {
unlockButton.setDisable(disable);
}
private void didUnlockAndMount(boolean mountSuccess) {
private void unlockAndMountFinished(boolean mountSuccess) {
progressIndicator.setVisible(false);
if (listener != null) {
setControlsDisabled(false);
if (vault.isUnlocked() && !mountSuccess) {
vault.stopServer();
}
if (mountSuccess && listener != null) {
listener.didUnlock(this);
}
}
@@ -164,6 +165,8 @@ public class UnlockController implements Initializable {
// newValue is guaranteed to be a-z0-9, see #filterAlphanumericKeyEvents
if (newValue.isEmpty()) {
mountName.setText(vault.getMountName());
} else {
vault.setMountName(newValue);
}
}

View File

@@ -27,8 +27,7 @@ import javafx.util.Duration;
import org.cryptomator.crypto.CryptorIOSampling;
import org.cryptomator.ui.model.Vault;
import com.google.inject.Inject;
import org.cryptomator.ui.util.mount.CommandFailedException;
public class UnlockedController implements Initializable {
@@ -47,18 +46,21 @@ public class UnlockedController implements Initializable {
@FXML
private NumberAxis xAxis;
@Inject
public UnlockedController() {
super();
}
private ResourceBundle rb;
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
}
@FXML
private void didClickCloseVault(ActionEvent event) {
vault.unmount();
try {
vault.unmount();
} catch (CommandFailedException e) {
messageLabel.setText(rb.getString("unlocked.label.unmountFailed"));
return;
}
vault.stopServer();
vault.setUnlocked(false);
if (listener != null) {

View File

@@ -0,0 +1,114 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.ui.controllers;
import java.io.IOException;
import java.net.URL;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Hyperlink;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.lang3.SystemUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
public class WelcomeController implements Initializable {
@FXML
private ImageView botImageView;
@FXML
private Hyperlink updateLink;
private final Application app;
private final Comparator<String> semVerComparator;
private final ExecutorService executor;
private ResourceBundle rb;
@Inject
public WelcomeController(Application app, @Named("SemVer") Comparator<String> semVerComparator, ExecutorService executor) {
this.app = app;
this.semVerComparator = semVerComparator;
this.executor = executor;
}
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
this.botImageView.setImage(new Image(WelcomeController.class.getResource("/bot_welcome.png").toString()));
executor.execute(this::checkForUpdates);
}
private void checkForUpdates() {
final HttpClient client = new HttpClient();
final HttpMethod method = new GetMethod("https://cryptomator.org/downloads/latestVersion.json");
client.getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES);
client.getParams().setConnectionManagerTimeout(5000);
try {
client.executeMethod(method);
if (method.getStatusCode() == HttpStatus.SC_OK) {
final byte[] responseData = method.getResponseBody();
final ObjectMapper mapper = new ObjectMapper();
final Map<String, String> map = mapper.readValue(responseData, new TypeReference<HashMap<String, String>>() {
});
this.compareVersions(map);
}
} catch (IOException e) {
// no error handling required. Maybe next time the version check is successful.
}
}
private void compareVersions(final Map<String, String> latestVersions) {
final String latestVersion;
if (SystemUtils.IS_OS_MAC_OSX) {
latestVersion = latestVersions.get("mac");
} else if (SystemUtils.IS_OS_WINDOWS) {
latestVersion = latestVersions.get("win");
} else if (SystemUtils.IS_OS_LINUX) {
latestVersion = latestVersions.get("linux");
} else {
// no version check possible on unsupported OS
return;
}
final String currentVersion = WelcomeController.class.getPackage().getImplementationVersion();
if (currentVersion != null && semVerComparator.compare(currentVersion, latestVersion) < 0) {
final String msg = String.format(rb.getString("welcome.newVersionMessage"), latestVersion, currentVersion);
Platform.runLater(() -> {
this.updateLink.setText(msg);
this.updateLink.setVisible(true);
});
}
}
@FXML
public void didClickUpdateLink(ActionEvent event) {
app.getHostServices().showDocument("https://cryptomator.org/#download");
}
}

View File

@@ -3,6 +3,7 @@ package org.cryptomator.ui.controls;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Tooltip;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
@@ -21,6 +22,7 @@ public class DirectoryListCell extends DraggableListCell<Vault> implements Chang
private static final Color GREEN_STROKE = Color.rgb(48, 183, 64);
private final Circle statusIndicator = new Circle(4.5);
private ContextMenu vaultContextMenu;
public DirectoryListCell() {
setGraphic(statusIndicator);
@@ -38,6 +40,7 @@ public class DirectoryListCell extends DraggableListCell<Vault> implements Chang
if (item == null) {
setText(null);
setTooltip(null);
setContextMenu(null);
statusIndicator.setVisible(false);
} else {
setText(item.getName());
@@ -45,12 +48,14 @@ public class DirectoryListCell extends DraggableListCell<Vault> implements Chang
statusIndicator.setVisible(true);
item.unlockedProperty().addListener(this);
updateStatusIndicator();
updateContextMenu();
}
}
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
updateStatusIndicator();
updateContextMenu();
}
private void updateStatusIndicator() {
@@ -60,4 +65,16 @@ public class DirectoryListCell extends DraggableListCell<Vault> implements Chang
statusIndicator.setStroke(strokeColor);
}
private void updateContextMenu() {
if (getItem().isUnlocked()) {
this.setContextMenu(null);
} else {
this.setContextMenu(vaultContextMenu);
}
}
public void setVaultContextMenu(ContextMenu contextMenu) {
this.vaultContextMenu = contextMenu;
}
}

View File

@@ -10,11 +10,14 @@ import java.util.Optional;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.ui.util.DeferredClosable;
import org.cryptomator.ui.util.DeferredCloser;
import org.cryptomator.ui.util.FXThreads;
import org.cryptomator.ui.util.mount.CommandFailedException;
import org.cryptomator.ui.util.mount.WebDavMount;
import org.cryptomator.ui.util.mount.WebDavMounter;
@@ -38,6 +41,7 @@ public class Vault implements Serializable {
private final WebDavMounter mounter;
private final DeferredCloser closer;
private final ObjectProperty<Boolean> unlocked = new SimpleObjectProperty<Boolean>(this, "unlocked", Boolean.FALSE);
private final ObservableSet<String> namesOfResourcesWithInvalidMac = FXThreads.observableSetOnMainThread(FXCollections.observableSet());
private String mountName;
private DeferredClosable<ServletLifeCycleAdapter> webDavServlet = DeferredClosable.empty();
@@ -70,22 +74,29 @@ public class Vault implements Serializable {
}
public synchronized boolean startServer() {
namesOfResourcesWithInvalidMac.clear();
Optional<ServletLifeCycleAdapter> o = webDavServlet.get();
if (o.isPresent() && o.get().isRunning()) {
return false;
}
ServletLifeCycleAdapter servlet = server.createServlet(path, cryptor, getMountName());
ServletLifeCycleAdapter servlet = server.createServlet(path, cryptor, namesOfResourcesWithInvalidMac, mountName);
if (servlet.start()) {
webDavServlet = closer.closeLater(servlet, ServletLifeCycleAdapter::stop);
webDavServlet = closer.closeLater(servlet);
return true;
}
return false;
}
public void stopServer() {
unmount();
try {
unmount();
} catch (CommandFailedException e) {
LOG.warn("Unmounting failed. Locking anyway...", e);
}
webDavServlet.close();
cryptor.swipeSensitiveData();
setUnlocked(false);
namesOfResourcesWithInvalidMac.clear();
}
public boolean mount() {
@@ -94,7 +105,7 @@ public class Vault implements Serializable {
return false;
}
try {
webDavMount = closer.closeLater(mounter.mount(o.get().getServletUri(), getMountName()), WebDavMount::unmount);
webDavMount = closer.closeLater(mounter.mount(o.get().getServletUri(), mountName));
return true;
} catch (CommandFailedException e) {
LOG.warn("mount failed", e);
@@ -102,8 +113,12 @@ public class Vault implements Serializable {
}
}
public void unmount() {
webDavMount.close();
public void unmount() throws CommandFailedException {
final WebDavMount mnt = webDavMount.get().orElse(null);
if (mnt != null) {
mnt.unmount();
}
webDavMount = DeferredClosable.empty();
}
/* Getter/Setter */
@@ -139,6 +154,10 @@ public class Vault implements Serializable {
return mountName;
}
public ObservableSet<String> getNamesOfResourcesWithInvalidMac() {
return namesOfResourcesWithInvalidMac;
}
/**
* Tries to form a similar string using the regular latin alphabet.
*

View File

@@ -15,6 +15,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import javafx.application.Platform;
import javafx.collections.ObservableSet;
/**
* Use this utility class to spawn background tasks and wait for them to finish. <br/>
@@ -118,4 +119,8 @@ public final class FXThreads {
void taskFailed(Throwable t);
}
public static <E> ObservableSet<E> observableSetOnMainThread(ObservableSet<E> set) {
return new ObservableSetOnMainThread<E>(set);
}
}

View File

@@ -0,0 +1,44 @@
/*******************************************************************************
* Copyright (c) 2014 cryptomator.org
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial implementation
******************************************************************************/
package org.cryptomator.ui.util;
import java.util.Collection;
import javafx.collections.ObservableSet;
import javafx.collections.SetChangeListener;
/**
* From the moment on, this aggregator is added as an observer to one or many {@link ObservableSet}s, change-events will be passed through
* to the given aggregation.
*/
public class ObservableSetAggregator<E> implements SetChangeListener<E> {
private final Collection<E> aggregation;
/**
* @param aggregation Set to which elements from observed subsets shall be added.
*/
public ObservableSetAggregator(final Collection<E> aggregation) {
this.aggregation = aggregation;
}
@Override
public void onChanged(Change<? extends E> change) {
if (change.getSet() == aggregation) {
// break cycle if aggregator observes aggregation
return;
}
if (change.wasAdded()) {
aggregation.add(change.getElementAdded());
} else if (change.wasRemoved()) {
aggregation.remove(change.getElementRemoved());
}
}
}

View File

@@ -0,0 +1,163 @@
package org.cryptomator.ui.util;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.collections.ObservableSet;
import javafx.collections.SetChangeListener;
import javafx.collections.SetChangeListener.Change;
class ObservableSetOnMainThread<E> implements ObservableSet<E> {
private final ObservableSet<E> set;
private final Collection<InvalidationListener> invalidationListeners;
private final Collection<SetChangeListener<? super E>> setChangeListeners;
public ObservableSetOnMainThread(ObservableSet<E> set) {
this.set = set;
this.invalidationListeners = new HashSet<>();
this.setChangeListeners = new HashSet<>();
this.set.addListener(this::invalidated);
this.set.addListener(this::onChanged);
}
@Override
public int size() {
return set.size();
}
@Override
public boolean isEmpty() {
return set.isEmpty();
}
@Override
public boolean contains(Object o) {
return set.contains(o);
}
@Override
public Iterator<E> iterator() {
return set.iterator();
}
@Override
public Object[] toArray() {
return set.toArray();
}
@Override
public <T> T[] toArray(T[] a) {
return set.toArray(a);
}
@Override
public boolean add(E e) {
return set.add(e);
}
@Override
public boolean remove(Object o) {
return set.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return set.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends E> c) {
return set.addAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
return set.retainAll(c);
}
@Override
public boolean removeAll(Collection<?> c) {
return set.removeAll(c);
}
@Override
public void clear() {
set.clear();
}
private void invalidated(Observable observable) {
Platform.runLater(() -> {
for (InvalidationListener listener : invalidationListeners) {
listener.invalidated(this);
}
});
}
@Override
public void addListener(InvalidationListener listener) {
invalidationListeners.add(listener);
}
@Override
public void removeListener(InvalidationListener listener) {
invalidationListeners.remove(listener);
}
private void onChanged(Change<? extends E> change) {
final Change<? extends E> c = new SetChange(this, change.getElementAdded(), change.getElementRemoved());
Platform.runLater(() -> {
for (SetChangeListener<? super E> listener : setChangeListeners) {
listener.onChanged(c);
}
});
}
@Override
public void addListener(SetChangeListener<? super E> listener) {
setChangeListeners.add(listener);
}
@Override
public void removeListener(SetChangeListener<? super E> listener) {
setChangeListeners.add(listener);
}
private class SetChange extends SetChangeListener.Change<E> {
private final E added;
private final E removed;
public SetChange(ObservableSet<E> set, E added, E removed) {
super(set);
this.added = added;
this.removed = removed;
}
@Override
public boolean wasAdded() {
return added != null;
}
@Override
public boolean wasRemoved() {
return removed != null;
}
@Override
public E getElementAdded() {
return added;
}
@Override
public E getElementRemoved() {
return removed;
}
}
}

View File

@@ -0,0 +1,34 @@
package org.cryptomator.ui.util;
import java.util.Comparator;
import org.apache.commons.lang3.StringUtils;
public class SemVerComparator implements Comparator<String> {
@Override
public int compare(String version1, String version2) {
final String[] vComps1 = StringUtils.split(version1, '.');
final String[] vComps2 = StringUtils.split(version2, '.');
final int commonCompCount = Math.min(vComps1.length, vComps2.length);
for (int i = 0; i < commonCompCount; i++) {
int subversionComparisionResult = 0;
try {
final int v1 = Integer.parseInt(vComps1[i]);
final int v2 = Integer.parseInt(vComps2[i]);
subversionComparisionResult = v1 - v2;
} catch (NumberFormatException ex) {
// ok, lets compare this fragment lexicographically
subversionComparisionResult = vComps1[i].compareTo(vComps2[i]);
}
if (subversionComparisionResult != 0) {
return subversionComparisionResult;
}
}
// all in common so far? longest version string wins:
return vComps1.length - vComps2.length;
}
}

View File

@@ -0,0 +1,10 @@
package org.cryptomator.ui.util.mount;
abstract class AbstractWebDavMount implements WebDavMount {
@Override
public void close() throws Exception {
this.unmount();
}
}

View File

@@ -30,7 +30,7 @@ final class FallbackWebDavMounter implements WebDavMounterStrategy {
@Override
public WebDavMount mount(URI uri, String name) {
displayMountInstructions();
return new WebDavMount() {
return new AbstractWebDavMount() {
@Override
public void unmount() {
displayUnmountInstructions();

View File

@@ -48,7 +48,7 @@ final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy {
"gvfs-mount -u \"dav:$DAV_SSP\"")
.addEnv("DAV_SSP", uri.getRawSchemeSpecificPart());
mountScript.execute();
return new WebDavMount() {
return new AbstractWebDavMount() {
@Override
public void unmount() throws CommandFailedException {
unmountScript.execute();

View File

@@ -10,6 +10,7 @@
package org.cryptomator.ui.util.mount;
import java.net.URI;
import java.util.UUID;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.command.Script;
@@ -28,7 +29,8 @@ final class MacOsXWebDavMounter implements WebDavMounterStrategy {
@Override
public WebDavMount mount(URI uri, String name) throws CommandFailedException {
final String path = "/Volumes/Cryptomator" + uri.getRawPath().replace('/', '_');
// we don't use the uri to derive a path, as it *could* be longer than 255 chars.
final String path = "/Volumes/Cryptomator_" + UUID.randomUUID().toString();
final Script mountScript = Script.fromLines(
"mkdir \"$MOUNT_PATH\"",
"mount_webdav -S -v $MOUNT_NAME \"$DAV_AUTHORITY$DAV_PATH\" \"$MOUNT_PATH\"",
@@ -38,10 +40,10 @@ final class MacOsXWebDavMounter implements WebDavMounterStrategy {
.addEnv("MOUNT_PATH", path)
.addEnv("MOUNT_NAME", name);
final Script unmountScript = Script.fromLines(
"umount $MOUNT_PATH")
"diskutil umount $MOUNT_PATH")
.addEnv("MOUNT_PATH", path);
mountScript.execute();
return new WebDavMount() {
return new AbstractWebDavMount() {
@Override
public void unmount() throws CommandFailedException {
unmountScript.execute();

View File

@@ -8,13 +8,12 @@
******************************************************************************/
package org.cryptomator.ui.util.mount;
/**
* A mounted webdav share.
*
* @author Markus Kreusch
*/
public interface WebDavMount {
public interface WebDavMount extends AutoCloseable {
/**
* Unmounts this {@code WebDavMount}.
@@ -22,5 +21,5 @@ public interface WebDavMount {
* @throws CommandFailedException if the unmount operation fails
*/
void unmount() throws CommandFailedException;
}

View File

@@ -30,6 +30,7 @@ import org.cryptomator.ui.util.command.Script;
final class WindowsWebDavMounter implements WebDavMounterStrategy {
private static final Pattern WIN_MOUNT_DRIVELETTER_PATTERN = Pattern.compile("\\s*([A-Z]:)\\s*");
private static final int MAX_MOUNT_ATTEMPTS = 10;
@Override
public boolean shouldWork() {
@@ -38,9 +39,11 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
@Override
public void warmUp(int serverPort) {
final URI warmUpUri = URI.create("http://0--1.ipv6-literal.net:" + serverPort + "/bill-gates-mom-uses-goto");
try {
this.mount(warmUpUri, "WarmUpMount");
final Script proxyBypassCmd = fromLines("reg add \"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v \"ProxyOverride\" /d \"<local>;0--1.ipv6-literal.net\" /f");
proxyBypassCmd.execute();
final Script mountCmd = fromLines("net use * http://0--1.ipv6-literal.net:" + serverPort + "/bill-gates-mom-uses-goto /persistent:no");
mountCmd.execute();
} catch (CommandFailedException e) {
// will most certainly throw an exception, because this is a fake WebDav path. But now windows has some DNS things cached :)
}
@@ -48,13 +51,32 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
@Override
public WebDavMount mount(URI uri, String name) throws CommandFailedException {
final Script mountScript = fromLines("net use * http://0--1.ipv6-literal.net:%PORT%%DAV_PATH% /persistent:no")
.addEnv("PORT", String.valueOf(uri.getPort()))
.addEnv("DAV_PATH", uri.getRawPath());
final CommandResult mountResult = mountScript.execute(30, TimeUnit.SECONDS);
final String driveLetter = getDriveLetter(mountResult.getStdOut());
final Script mountScript = fromLines("net use * http://0--1.ipv6-literal.net:%PORT%%DAV_PATH% /persistent:no");
mountScript.addEnv("PORT", String.valueOf(uri.getPort())).addEnv("DAV_PATH", uri.getRawPath());
String driveLetter = null;
// The ugliness of the following 20 lines is solely windows' fault. Deal with it.
for (int i = 0; i < MAX_MOUNT_ATTEMPTS; i++) {
try {
final CommandResult mountResult = mountScript.execute(10, TimeUnit.SECONDS);
driveLetter = getDriveLetter(mountResult.getStdOut());
break;
} catch (CommandFailedException ex) {
if (i == MAX_MOUNT_ATTEMPTS - 1) {
throw ex;
} else {
try {
// retry after 500ms
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
final Script openExplorerScript = fromLines("start explorer.exe " + driveLetter);
openExplorerScript.execute();
final Script unmountScript = fromLines("net use " + driveLetter + " /delete").addEnv("DRIVE_LETTER", driveLetter);
return new WebDavMount() {
return new AbstractWebDavMount() {
@Override
public void unmount() throws CommandFailedException {
unmountScript.execute();

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -293,6 +293,20 @@
-fx-text-fill: -fx-text-background-color;
}
/*******************************************************************************
* *
* Hyperlink *
* *
******************************************************************************/
.hyperlink {
-fx-cursor: hand;
-fx-text-fill: #0069D9;
}
.hyperlink:hover {
-fx-underline: true;
}
/*******************************************************************************
* *
* Button & ToggleButton *

View File

@@ -295,6 +295,20 @@
-fx-text-fill: -fx-text-background-color;
}
/*******************************************************************************
* *
* Hyperlink *
* *
******************************************************************************/
.hyperlink {
-fx-cursor: hand;
-fx-text-fill: #3399FF;
}
.hyperlink:hover {
-fx-underline: true;
}
/*******************************************************************************
* *
* Button & ToggleButton *

View File

@@ -7,16 +7,16 @@
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<?import java.net.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import java.net.URL?>
<?import java.lang.String?>
<?import org.cryptomator.ui.controls.SecPasswordField?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ProgressIndicator?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.control.Button?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.controllers.ChangePasswordController" xmlns:fx="http://javafx.com/fxml">

View File

@@ -7,16 +7,14 @@
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<?import java.net.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import org.cryptomator.ui.controls.*?>
<?import java.net.URL?>
<?import javafx.scene.layout.HBox?>
<?import org.cryptomator.ui.controls.SecPasswordField?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.control.Label?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.controllers.InitializeController" xmlns:fx="http://javafx.com/fxml">
<padding>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2014 Sebastian Stenzel
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<?import java.net.URL?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.ListView?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.HBox?>
<VBox styleClass="root" alignment="CENTER" prefHeight="225.0" prefWidth="525.0" spacing="12.0" fx:controller="org.cryptomator.ui.controllers.MacWarningsController" xmlns:fx="http://javafx.com/fxml">
<padding><Insets top="12.0" right="12.0" bottom="12.0" left="12.0"/></padding>
<children>
<Label textAlignment="CENTER" text="%macWarnings.message"/>
<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 text="%macWarnings.moreInformationButton" defaultButton="true" prefWidth="200.0" onAction="#didClickMoreInformationButton" focusTraversable="false"/>
</children>
</HBox>
</children>
</VBox>

View File

@@ -16,7 +16,6 @@
<?import javafx.scene.control.ToggleButton?>
<?import javafx.scene.control.ContextMenu?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.Separator?>
<?import javafx.geometry.Insets?>
<HBox fx:id="rootPane" prefHeight="440.0" prefWidth="640.0" fx:controller="org.cryptomator.ui.controllers.MainController" xmlns:fx="http://javafx.com/fxml">

View File

@@ -7,17 +7,17 @@
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<?import java.net.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import java.net.URL?>
<?import java.lang.String?>
<?import org.cryptomator.ui.controls.SecPasswordField?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ProgressIndicator?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.control.TextField?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.controllers.UnlockController" xmlns:fx="http://javafx.com/fxml">
<padding>

View File

@@ -7,16 +7,15 @@
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<?import java.net.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import java.net.URL?>
<?import java.lang.String?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.chart.LineChart?>
<?import javafx.scene.chart.NumberAxis?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.layout.ColumnConstraints?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.controllers.UnlockedController" xmlns:fx="http://javafx.com/fxml">
<padding>
@@ -36,7 +35,8 @@
</LineChart>
<!-- Row 1 -->
<Button text="%unlocked.button.lock" defaultButton="true" GridPane.rowIndex="1" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickCloseVault" focusTraversable="false"/>
<Label fx:id="messageLabel" GridPane.rowIndex="1" GridPane.columnIndex="0" />
<Button text="%unlocked.button.lock" defaultButton="true" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickCloseVault" focusTraversable="false"/>
</children>
</GridPane>

View File

@@ -7,27 +7,30 @@
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<?import java.net.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import java.net.URL?>
<?import java.lang.String?>
<?import javafx.scene.shape.Arc?>
<?import javafx.scene.shape.QuadCurve?>
<?import javafx.scene.shape.Path?>
<?import javafx.scene.shape.Line?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.control.Hyperlink?>
<AnchorPane xmlns:fx="http://javafx.com/fxml">
<AnchorPane xmlns:fx="http://javafx.com/fxml" fx:controller="org.cryptomator.ui.controllers.WelcomeController">
<children>
<Label AnchorPane.leftAnchor="100.0" AnchorPane.topAnchor="50.0" style="-fx-font-size: 1.5em;" text="%welcome.welcomeLabel"/>
<Label AnchorPane.leftAnchor="120.0" AnchorPane.topAnchor="280.0" text="%welcome.addButtonInstructionLabel"/>
<Label AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="20.0" prefWidth="400.0" alignment="CENTER" style="-fx-font-size: 1.5em;" text="%welcome.welcomeLabel"/>
<Hyperlink AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="50.0" prefWidth="400.0" alignment="CENTER" fx:id="updateLink" onAction="#didClickUpdateLink" visible="false"/>
<QuadCurve AnchorPane.leftAnchor="4.0" AnchorPane.topAnchor="300.0" startX="200.0" startY="0.0" endX="0.0" endY="80.0" controlX="180.0" controlY="80.0" fill="TRANSPARENT" stroke="BLACK" strokeWidth="2.0"/>
<Line AnchorPane.leftAnchor="4.0" AnchorPane.topAnchor="370.0" startX="0.0" endX="10.0" startY="10.0" endY="0.0" strokeWidth="2.0"/>
<Line AnchorPane.leftAnchor="4.0" AnchorPane.topAnchor="380.0" startX="0.0" endX="10.0" startY="0.0" endY="10.0" strokeWidth="2.0"/>
<ImageView fx:id="botImageView" AnchorPane.leftAnchor="100.0" AnchorPane.topAnchor="200.0" fitHeight="200.0" preserveRatio="true" smooth="false"/>
<Line AnchorPane.leftAnchor="4.0" AnchorPane.topAnchor="380.0" startX="0.0" endX="6.0" startY="5.0" endY="0.0" strokeWidth="1.0"/>
<Line AnchorPane.leftAnchor="6.0" AnchorPane.topAnchor="385.0" startX="0.0" endX="15.0" startY="0.0" endY="0.0" strokeWidth="1.0"/>
<Line AnchorPane.leftAnchor="4.0" AnchorPane.topAnchor="385.0" startX="0.0" endX="6.0" startY="0.0" endY="5.0" strokeWidth="1.0"/>
<Label AnchorPane.leftAnchor="25.0" AnchorPane.topAnchor="377.0" text="%welcome.addButtonInstructionLabel"/>
</children>
</AnchorPane>

View File

@@ -15,18 +15,16 @@ main.directoryList.contextMenu.changePassword=Change password
main.addDirectory.contextMenu.new=Create new vault
main.addDirectory.contextMenu.open=Add existing vault
# welcome.fxml
welcome.welcomeLabel=Welcome to Cryptomator
welcome.addButtonInstructionLabel=Start by adding a new vault :-)
welcome.addButtonInstructionLabel=Start by adding a new vault
welcome.newVersionMessage=Version %s can be downloaded. This is %s.
# initialize.fxml
initialize.label.password=Password
initialize.label.retypePassword=Retype password
initialize.button.ok=Create vault
# unlock.fxml
unlock.label.password=Password
unlock.label.mountName=Drive name
@@ -48,8 +46,14 @@ changePassword.infoMessage.success=Password changed.
# unlocked.fxml
unlocked.button.lock=Lock vault
unlocked.label.unmountFailed=Ejecting drive failed.
unlocked.ioGraph.yAxis.label=Throughput (MiB/s)
# mac_warnings.fxml
macWarnings.windowTitle=Danger - MAC authentication failed
macWarnings.message=Cryptomator detected potentially malicious corruptions in the following files:
macWarnings.moreInformationButton=Learn more
macWarnings.dismissButton=I promise to be careful
# tray icon
tray.menu.open=Open

View File

@@ -1,12 +1,12 @@
package org.cryptomator.ui;
import static org.junit.Assert.*;
import org.junit.Test;
public class MainApplicationTest {
@Test
public void testInjection() throws Exception {
new MainApplication();
}
}

View File

@@ -0,0 +1,62 @@
package org.cryptomator.ui.test;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.InitializationError;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Module;
/**
* Taken from http://fabiostrozzi.eu/2011/03/27/junit-tests-easy-guice/
*/
public class GuiceJUnitRunner extends BlockJUnit4ClassRunner {
private final Injector injector;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface GuiceModules {
Class<?>[] value();
}
@Override
public Object createTest() throws Exception {
Object obj = super.createTest();
injector.injectMembers(obj);
return obj;
}
public GuiceJUnitRunner(Class<?> klass) throws InitializationError {
super(klass);
Class<?>[] classes = getModulesFor(klass);
injector = createInjectorFor(classes);
}
private Injector createInjectorFor(Class<?>[] classes) throws InitializationError {
Module[] modules = new Module[classes.length];
for (int i = 0; i < classes.length; i++) {
try {
modules[i] = (Module) (classes[i]).newInstance();
} catch (InstantiationException e) {
throw new InitializationError(e);
} catch (IllegalAccessException e) {
throw new InitializationError(e);
}
}
return Guice.createInjector(modules);
}
private Class<?>[] getModulesFor(Class<?> klass) throws InitializationError {
GuiceModules annotation = klass.getAnnotation(GuiceModules.class);
if (annotation == null)
throw new InitializationError("Missing @GuiceModules annotation for unit test '" + klass.getName() + "'");
return annotation.value();
}
}

View File

@@ -0,0 +1,51 @@
package org.cryptomator.ui.util;
import java.util.Comparator;
import javax.inject.Inject;
import javax.inject.Named;
import org.cryptomator.ui.MainModule;
import org.cryptomator.ui.test.GuiceJUnitRunner;
import org.cryptomator.ui.test.GuiceJUnitRunner.GuiceModules;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(GuiceJUnitRunner.class)
@GuiceModules(MainModule.class)
public class SemVerComparatorTest {
@Inject
@Named("SemVer")
private Comparator<String> semVerComparator;
// equal versions
@Test
public void compareEqualVersions() {
final int comparisonResult = semVerComparator.compare("1.23.4", "1.23.4");
Assert.assertEquals(0, Integer.signum(comparisonResult));
}
// newer versions in first argument
@Test
public void compareHigherToLowerVersions() {
Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.5", "1.23.4")));
Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.24.4", "1.23.4")));
Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23")));
Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4a", "1.23.4")));
}
// newer versions in second argument
@Test
public void compareLowerToHigherVersions() {
Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.5")));
Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4", "1.24.4")));
Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23", "1.23.4")));
Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4a")));
}
}