Compare commits

...

50 Commits
0.1.0 ... 0.3.0

Author SHA1 Message Date
Sebastian Stenzel
3f32e4ee4b - Fixed initial encryption of vaults, that already contain files
- Disabled some UI controls during background tasks
- Simplified background vs UI thread switches using https://github.com/totalvoidness/FXThreads
2014-12-24 14:10:30 +01:00
Sebastian Stenzel
be5cf287c8 - win7/8 theme 2014-12-23 22:28:51 +01:00
Sebastian Stenzel
71892108b3 - L&F improvements on OS X 2014-12-22 22:39:22 +01:00
Sebastian Stenzel
1770bab699 - updated metadata file names 2014-12-21 20:08:09 +01:00
Sebastian Stenzel
1d05e878ab - Support for HTTP Range header fields, thus vastly improved performance for video streaming
- Simplified cryptor implementation for partial decryption
2014-12-21 16:54:47 +01:00
Sebastian Stenzel
f76091ddc0 - Made unit tests I/O-independent 2014-12-20 16:46:50 +01:00
Sebastian Stenzel
6dff296872 - using java.util.Random in unit tests again, as performance doesn't change by using non-random PRNG - of course still using a cryptographically secure PRNG in production ;-) 2014-12-20 11:18:12 +01:00
Sebastian Stenzel
6d98442f7e - preparation for http range requests: cryptor supports partial decryption now 2014-12-20 10:47:26 +01:00
Sebastian Stenzel
3cdda99c67 - closing _all_ process streams
- allowing multiple accesses to stdout / stderr in O(1)
2014-12-16 20:46:48 +01:00
Sebastian Stenzel
6b45d62aa1 - reduced visibility 2014-12-16 17:18:20 +01:00
Sebastian Stenzel
b7f3f00ce2 - Further simplification by using Futures :) 2014-12-16 16:56:42 +01:00
Sebastian Stenzel
dbadf54893 - General Simplification
- Refactoring: Using Concurrency API now.
- TODO: Use Futures instead of blocking methods
2014-12-16 12:14:54 +01:00
Sebastian Stenzel
38a0cfb2eb - faster unit test using insecure PRNG - test only ;) 2014-12-16 12:13:01 +01:00
Sebastian Stenzel
7d6d061d95 - removed admin privileges in native installer 2014-12-16 01:44:57 +01:00
Sebastian Stenzel
c743fa8bdc - fixed clean unmounting
- fixed correct subprocess status codes (not using status code of parent shell)
2014-12-16 01:35:00 +01:00
Sebastian Stenzel
8c2fe14e41 - bugfix: slow webdav on windows (http://support.microsoft.com/kb/2445570)
- bugfix: windows mount on non-german installations
- bugfix: system-dependent implementation of mount commands now done in specific strategy. no linux-specific URI outside of mount package, thus working on OS X again and simplified windows code
- change: now using ipv6
2014-12-15 23:46:06 +01:00
Sebastian Stenzel
ac4f10ce93 Merge pull request #3 from markuskreusch/master
Refactoring of WebDav mounting
2014-12-15 23:05:52 +01:00
Markus Kreusch
4f15645bf9 Merge branch 'webdav-mounting' 2014-12-15 22:54:03 +01:00
Markus Kreusch
c1f4ab6ada Refactored script execution 2014-12-15 22:50:53 +01:00
Sebastian Stenzel
fd54393f36 Merge branch 'master' of https://github.com/totalvoidness/cryptomator 2014-12-15 09:39:11 +01:00
Markus Kreusch
a2c3b38a75 refactored WebDavMounter, now using strategy pattern 2014-12-14 21:54:10 +01:00
Sebastian Stenzel
2fb35c59d4 - remove vaults using context menu
- locked/unlocked indicator
2014-12-13 21:24:48 +01:00
Sebastian Stenzel
afc62656bf - learning mathematics 2014-12-13 15:08:26 +01:00
Sebastian Stenzel
9c8e4fbf3b Merge branch 'master' of https://github.com/totalvoidness/cryptomator 2014-12-11 20:07:56 +01:00
Sebastian Stenzel
470a609938 Makes test work on windows 2014-12-11 20:07:46 +01:00
Sebastian Stenzel
863b2ec423 - Added throuput statistics 2014-12-11 19:46:57 +01:00
Sebastian Stenzel
d0a420d6c0 - FileTimes used to create RFC 1123 strings are now interpreted as UTC dates 2014-12-11 17:11:29 +01:00
Sebastian Stenzel
51e2e94ca9 - All modules use Java 8 now
- Fixed incorrect "last modified" date
- Simpler warning dialog when using non-empty directory as new vault location
2014-12-11 17:03:19 +01:00
Sebastian Stenzel
d7efd7fc2f - Updated version number 2014-12-11 01:03:29 +01:00
Sebastian Stenzel
db36cfa22e Updated download Links 2014-12-11 01:01:22 +01:00
Sebastian Stenzel
cc15f2cdb4 ignoring test output 2014-12-11 00:35:15 +01:00
Sebastian Stenzel
b6546f24d5 - minimizes to tray when vaults are still unlocked (i.e. webdav shares still mounted) 2014-12-11 00:27:44 +01:00
Sebastian Stenzel
5fe54634a9 - cleanup
- fix: now showing correct view, when selecting an already mounted directory
2014-12-10 12:47:35 +01:00
Sebastian Stenzel
2fdf9be017 - Encrypt existing directory content on vault initialization 2014-12-09 18:25:59 +01:00
Sebastian Stenzel
1de2d9d2da linux mount with gvfs 2014-12-09 11:36:29 +01:00
Sebastian Stenzel
3a5917ef53 Updated Jetty 2014-12-09 10:57:26 +01:00
Sebastian Stenzel
d0f0c09585 - Improved shutdown hooks
- Redesigned UI, now a single-window application (todo: minimize to tray)
2014-12-09 10:50:09 +01:00
Sebastian Stenzel
884b894e04 bugfix: correct decryption of looooooong filenames (>255 chars) 2014-12-08 22:25:45 +01:00
Sebastian Stenzel
ebb3207854 fixed focus traversing ("tab order") of form fields 2014-12-06 16:31:24 +01:00
Sebastian Stenzel
8abd5ebc01 simplification 2014-12-06 16:07:19 +01:00
Sebastian Stenzel
ce197b3314 - support for long filenames
- increased thread count for webdav server
- fixed severe bug that allowed using non-random masterkeys (now throwing exceptions if this attempt is made)
2014-12-06 14:31:55 +01:00
Sebastian Stenzel
8ae7e95c41 added OS-dependant distribution package resources 2014-12-06 00:09:58 +01:00
Sebastian Stenzel
6830861346 webdav mounting on windows 2014-12-05 14:40:28 +01:00
Sebastian Stenzel
696b3412f2 Merge branch 'master' of https://github.com/totalvoidness/open-cloud-encryptor 2014-12-04 22:04:23 +01:00
Sebastian Stenzel
e7ba6f5c92 - Completely redesigned and much simpler user interface.
- Support for multiple simultaneous mounts
- Added shutdown hooks for secure unmounting
2014-12-04 22:04:04 +01:00
Sebastian Stenzel
8031b0c516 Merge pull request #2 from markuskreusch/switch-to-log4j-2
Switched to log4j 2
2014-11-30 22:52:41 +01:00
markus
047e1fe1d6 added log4j 2 configuration 2014-11-30 20:40:55 +01:00
markus
75f49b88d6 switched to log4j 2.1 2014-11-30 18:56:17 +01:00
markus
891e79cdae Moved logging and junit dependencies to parent pom 2014-11-30 18:50:28 +01:00
Sebastian Stenzel
b2f20f9a15 added download link 2014-11-30 00:24:16 +01:00
87 changed files with 8250 additions and 1201 deletions

1
.gitignore vendored
View File

@@ -10,3 +10,4 @@
.project
.classpath
target/
test-output/

View File

@@ -3,6 +3,8 @@ Cryptomator
Multiplatform transparent client-side encryption of your files in the cloud. You need Java 8 in order to run the application. Get the runtime environment here: http://www.oracle.com/technetwork/java/javase/downloads/index.html
If you want to take a look at the current beta version, go ahead and download [Cryptomator.dmg](https://github.com/totalvoidness/cryptomator/releases/download/v0.2.0/Cryptomator.dmg), [Cryptomator.exe](https://github.com/totalvoidness/cryptomator/releases/download/v0.2.0/Cryptomator.exe) or [Cryptomator.jar](https://github.com/totalvoidness/cryptomator/releases/download/v0.2.0/Cryptomator.jar).
## 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
@@ -17,7 +19,6 @@ Multiplatform transparent client-side encryption of your files in the cloud. You
## Security
- Default key length is 256 bit (falls back to 128 bit, if JCE isn't installed)
- PBKDF2 key generation
- 4096 bit internal masterkey
- Cryptographically secure random numbers for salts, IVs and the masterkey of course
- Sensitive data is swiped from the heap asap
- Lightweight: Complexity kills security
@@ -28,17 +29,13 @@ Multiplatform transparent client-side encryption of your files in the cloud. You
- *NEW:* No Metadata at all. Encrypted files can be decrypted even on completely shuffled file systems (if their contents are undamaged).
## Dependencies
- Java 8 (for UI only - runs headless on Java 7)
- Maven
- Awesome 3rd party open source libraries (Apache Commons, Apache Jackrabbit, Jetty, Jackson, ...)
- Java 8
- see pom.xml ;-)
## TODO
### Core
- Support for HTTP range requests
### UI
- Automount of WebDAV volumes for Win/Tux
- Native L&F
- Drive icons in WebDAV volumes
- Change password functionality
- Better explanations on UI

View File

@@ -12,13 +12,13 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.1.0</version>
<version>0.3.0-SNAPSHOT</version>
</parent>
<artifactId>core</artifactId>
<name>Cryptomator core I/O module</name>
<properties>
<jetty.version>9.1.0.v20131115</jetty.version>
<jetty.version>9.2.5.v20141112</jetty.version>
<jackrabbit.version>2.9.0</jackrabbit.version>
<commons.transaction.version>1.2</commons.transaction.version>
<jta.version>1.1</jta.version>
@@ -30,12 +30,6 @@
<artifactId>crypto-api</artifactId>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<!-- Jetty (Servlet Container) -->
<dependency>
<groupId>org.eclipse.jetty</groupId>
@@ -69,20 +63,4 @@
<artifactId>commons-collections4</artifactId>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

View File

@@ -0,0 +1,83 @@
package org.cryptomator.files;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.CryptorIOSupport;
public class EncryptingFileVisitor extends SimpleFileVisitor<Path> implements CryptorIOSupport {
private final Path rootDir;
private final Cryptor cryptor;
private final EncryptionDecider encryptionDecider;
private Path currentDir;
public EncryptingFileVisitor(Path rootDir, Cryptor cryptor, EncryptionDecider encryptionDecider) {
this.rootDir = rootDir;
this.cryptor = cryptor;
this.encryptionDecider = encryptionDecider;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
if (rootDir.equals(dir) || encryptionDecider.shouldEncrypt(dir)) {
this.currentDir = dir;
return FileVisitResult.CONTINUE;
} else {
return FileVisitResult.SKIP_SUBTREE;
}
}
@Override
public FileVisitResult visitFile(Path plaintextFile, BasicFileAttributes attrs) throws IOException {
if (encryptionDecider.shouldEncrypt(plaintextFile)) {
final String plaintextName = plaintextFile.getFileName().toString();
final String encryptedName = cryptor.encryptPath(plaintextName, '/', '/', this);
final Path encryptedPath = plaintextFile.resolveSibling(encryptedName);
final InputStream plaintextIn = Files.newInputStream(plaintextFile, StandardOpenOption.READ);
final SeekableByteChannel ciphertextOut = Files.newByteChannel(encryptedPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
cryptor.encryptFile(plaintextIn, ciphertextOut);
Files.delete(plaintextFile);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
if (encryptionDecider.shouldEncrypt(dir)) {
final String plaintext = dir.getFileName().toString();
final String encrypted = cryptor.encryptPath(plaintext, '/', '/', this);
final Path newPath = dir.resolveSibling(encrypted);
Files.move(dir, newPath, StandardCopyOption.ATOMIC_MOVE);
}
return FileVisitResult.CONTINUE;
}
@Override
public void writePathSpecificMetadata(String metadataFile, byte[] encryptedMetadata) throws IOException {
final Path path = currentDir.resolve(metadataFile);
Files.write(path, encryptedMetadata, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
}
@Override
public byte[] readPathSpecificMetadata(String metadataFile) throws IOException {
final Path path = currentDir.resolve(metadataFile);
return Files.readAllBytes(path);
}
/* callback */
public interface EncryptionDecider {
boolean shouldEncrypt(Path path);
}
}

View File

@@ -8,6 +8,9 @@
******************************************************************************/
package org.cryptomator.webdav;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.webdav.jackrabbit.WebDavServlet;
import org.eclipse.jetty.server.Connector;
@@ -15,68 +18,79 @@ import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class WebDAVServer {
public final class WebDavServer {
private static final Logger LOG = LoggerFactory.getLogger(WebDAVServer.class);
private static final WebDAVServer INSTANCE = new WebDAVServer();
private static final String LOCALHOST = "127.0.0.1";
private final Server server = new Server();
private static final Logger LOG = LoggerFactory.getLogger(WebDavServer.class);
private static final String LOCALHOST = "::1";
private static final int MAX_PENDING_REQUESTS = 200;
private static final int MAX_THREADS = 200;
private static final int MIN_THREADS = 4;
private static final int THREAD_IDLE_SECONDS = 20;
private final Server server;
private int port;
private WebDAVServer() {
// make constructor private
}
public static WebDAVServer getInstance() {
return INSTANCE;
public WebDavServer() {
final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(MAX_PENDING_REQUESTS);
final ThreadPool tp = new QueuedThreadPool(MAX_THREADS, MIN_THREADS, THREAD_IDLE_SECONDS, queue);
server = new Server(tp);
}
/**
* @param workDir Path of encrypted folder.
* @param cryptor A fully initialized cryptor instance ready to en- or decrypt streams.
* @return port, on which the server did start
* @return <code>true</code> upon success
*/
public int start(final String workDir, final Cryptor cryptor) {
public synchronized boolean start(final String workDir, final Cryptor cryptor) {
final ServerConnector connector = new ServerConnector(server);
connector.setHost(LOCALHOST);
final String contextPath = "/";
final String servletPathSpec = "/*";
final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.addServlet(getMiltonServletHolder(workDir, contextPath, cryptor), "/*");
context.addServlet(getWebDavServletHolder(workDir, contextPath, cryptor), servletPathSpec);
context.setContextPath(contextPath);
server.setHandler(context);
try {
server.setConnectors(new Connector[] {connector});
server.start();
port = connector.getLocalPort();
return true;
} catch (Exception ex) {
LOG.error("Server couldn't be started", ex);
return false;
}
return connector.getLocalPort();
}
public boolean isRunning() {
return server.isRunning();
}
public boolean stop() {
public synchronized boolean stop() {
try {
server.stop();
port = 0;
} catch (Exception ex) {
LOG.error("Server couldn't be stopped", ex);
}
return server.isStopped();
}
private ServletHolder getMiltonServletHolder(final String workDir, final String contextPath, final Cryptor cryptor) {
private ServletHolder getWebDavServletHolder(final String workDir, final String contextPath, final Cryptor cryptor) {
final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor));
result.setInitParameter(WebDavServlet.CFG_FS_ROOT, workDir);
result.setInitParameter(WebDavServlet.CFG_HTTP_ROOT, contextPath);
return result;
}
public int getPort() {
return port;
}
}

View File

@@ -0,0 +1,36 @@
package org.cryptomator.webdav.jackrabbit;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceFactory;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavServletRequest;
import org.apache.jackrabbit.webdav.DavServletResponse;
import org.apache.jackrabbit.webdav.DavSession;
abstract class AbstractSessionAwareWebDavResourceFactory implements DavResourceFactory {
@Override
public DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
final DavSession session = request.getDavSession();
if (session != null && session instanceof WebDavSession) {
return createDavResource(locator, (WebDavSession) session, request, response);
} else {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "Unsupported session type.");
}
}
protected abstract DavResource createDavResource(DavResourceLocator locator, WebDavSession session, DavServletRequest request, DavServletResponse response) throws DavException;
@Override
public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
if (session != null && session instanceof WebDavSession) {
return createDavResource(locator, (WebDavSession) session);
} else {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "Unsupported session type.");
}
}
protected abstract DavResource createDavResource(DavResourceLocator locator, WebDavSession session);
}

View File

@@ -8,7 +8,7 @@ import org.apache.commons.collections4.map.LRUMap;
final class BidiLRUMap<K, V> extends AbstractDualBidiMap<K, V> {
public BidiLRUMap(int maxSize) {
BidiLRUMap(int maxSize) {
super(new LRUMap<K, V>(maxSize), new LRUMap<V, K>(maxSize));
}

View File

@@ -8,26 +8,30 @@
******************************************************************************/
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import org.apache.commons.collections4.BidiMap;
import org.apache.jackrabbit.webdav.AbstractLocatorFactory;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.CryptorIOSupport;
import org.cryptomator.crypto.SensitiveDataSwipeListener;
public class WebDavLocatorFactory extends AbstractLocatorFactory implements SensitiveDataSwipeListener {
class WebDavLocatorFactory extends AbstractLocatorFactory implements SensitiveDataSwipeListener, CryptorIOSupport {
private static final int MAX_CACHED_PATHS = 10000;
private final Path fsRoot;
private final Cryptor cryptor;
private final BidiLRUMap<String, String> pathCache; // <decryptedPath, encryptedPath>
private final BidiMap<String, String> pathCache = new BidiLRUMap<>(MAX_CACHED_PATHS); // <decryptedPath, encryptedPath>
public WebDavLocatorFactory(String fsRoot, String httpRoot, Cryptor cryptor) {
WebDavLocatorFactory(String fsRoot, String httpRoot, Cryptor cryptor) {
super(httpRoot);
this.fsRoot = FileSystems.getDefault().getPath(fsRoot);
this.cryptor = cryptor;
this.pathCache = new BidiLRUMap<>(MAX_CACHED_PATHS);
cryptor.addSensitiveDataSwipeListener(this);
}
@@ -48,7 +52,7 @@ public class WebDavLocatorFactory extends AbstractLocatorFactory implements Sens
if (resourcePath == null) {
return fsRoot.toString();
}
final String encryptedRepoPath = cryptor.encryptPath(resourcePath, FileSystems.getDefault().getSeparator().charAt(0), '/');
final String encryptedRepoPath = cryptor.encryptPath(resourcePath, FileSystems.getDefault().getSeparator().charAt(0), '/', this);
return fsRoot.resolve(encryptedRepoPath).toString();
}
@@ -71,7 +75,7 @@ public class WebDavLocatorFactory extends AbstractLocatorFactory implements Sens
return null;
} else {
final Path relativeRepositoryPath = fsRoot.relativize(absRepoPath);
final String resourcePath = cryptor.decryptPath(relativeRepositoryPath.toString(), FileSystems.getDefault().getSeparator().charAt(0), '/');
final String resourcePath = cryptor.decryptPath(relativeRepositoryPath.toString(), FileSystems.getDefault().getSeparator().charAt(0), '/', this);
return resourcePath;
}
}
@@ -93,4 +97,22 @@ public class WebDavLocatorFactory extends AbstractLocatorFactory implements Sens
pathCache.clear();
}
/* Cryptor I/O Support */
@Override
public void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) throws IOException {
final Path metaDataFile = fsRoot.resolve(encryptedPath);
Files.write(metaDataFile, encryptedMetadata, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
}
@Override
public byte[] readPathSpecificMetadata(String encryptedPath) throws IOException {
final Path metaDataFile = fsRoot.resolve(encryptedPath);
if (!Files.isReadable(metaDataFile)) {
return null;
} else {
return Files.readAllBytes(metaDataFile);
}
}
}

View File

@@ -11,6 +11,7 @@ package org.cryptomator.webdav.jackrabbit;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavMethods;
import org.apache.jackrabbit.webdav.DavResource;
@@ -24,28 +25,32 @@ 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.PathUtils;
import org.cryptomator.webdav.jackrabbit.resources.ResourcePathUtils;
import org.eclipse.jetty.http.HttpHeader;
public class WebDavResourceFactory implements DavResourceFactory {
class WebDavResourceFactory implements DavResourceFactory {
private final LockManager lockManager = new SimpleLockManager();
private final Cryptor cryptor;
public WebDavResourceFactory(Cryptor cryptor) {
WebDavResourceFactory(Cryptor cryptor) {
this.cryptor = cryptor;
}
@Override
public DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
final Path path = PathUtils.getPhysicalPath(locator);
final Path path = ResourcePathUtils.getPhysicalPath(locator);
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
if (Files.exists(path)) {
return createResource(locator, request.getDavSession());
} else if (DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
return createDirectory(locator, request.getDavSession());
} else if (DavMethods.METHOD_PUT.equals(request.getMethod())) {
if (Files.isRegularFile(path) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) {
response.setStatus(HttpStatus.SC_PARTIAL_CONTENT);
return createFilePart(locator, request.getDavSession(), request);
} else if (Files.isRegularFile(path) || DavMethods.METHOD_PUT.equals(request.getMethod())) {
return createFile(locator, request.getDavSession());
} else if (Files.isDirectory(path) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
return createDirectory(locator, request.getDavSession());
} else {
return createNonExisting(locator, request.getDavSession());
}
@@ -53,17 +58,21 @@ public class WebDavResourceFactory implements DavResourceFactory {
@Override
public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
final Path path = PathUtils.getPhysicalPath(locator);
final Path path = ResourcePathUtils.getPhysicalPath(locator);
if (Files.isDirectory(path)) {
return createDirectory(locator, session);
} else if (Files.isRegularFile(path)) {
if (Files.isRegularFile(path)) {
return createFile(locator, session);
} else if (Files.isDirectory(path)) {
return createDirectory(locator, session);
} else {
return createNonExisting(locator, session);
}
}
private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, DavServletRequest request) {
return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor);
}
private EncryptedFile createFile(DavResourceLocator locator, DavSession session) {
return new EncryptedFile(this, locator, session, lockManager, cryptor);
}

View File

@@ -9,8 +9,15 @@
package org.cryptomator.webdav.jackrabbit;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.WebdavRequest;
public class WebDavSession implements DavSession {
class WebDavSession implements DavSession {
private final WebdavRequest request;
WebDavSession(WebdavRequest request) {
this.request = request;
}
@Override
public void addReference(Object reference) {
@@ -42,4 +49,8 @@ public class WebDavSession implements DavSession {
}
public WebdavRequest getRequest() {
return request;
}
}

View File

@@ -12,12 +12,12 @@ import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavSessionProvider;
import org.apache.jackrabbit.webdav.WebdavRequest;
public class WebDavSessionProvider implements DavSessionProvider {
class WebDavSessionProvider implements DavSessionProvider {
@Override
public boolean attachSession(WebdavRequest request) throws DavException {
// every user gets a session
request.setDavSession(new WebDavSession());
// every request gets a session
request.setDavSession(new WebDavSession(request));
return true;
}

View File

@@ -38,7 +38,7 @@ import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class AbstractEncryptedNode implements DavResource {
abstract class AbstractEncryptedNode implements DavResource {
private static final Logger LOG = LoggerFactory.getLogger(AbstractEncryptedNode.class);
private static final String DAV_COMPLIANCE_CLASSES = "1, 2";
@@ -72,7 +72,7 @@ public abstract class AbstractEncryptedNode implements DavResource {
@Override
public boolean exists() {
final Path path = PathUtils.getPhysicalPath(this);
final Path path = ResourcePathUtils.getPhysicalPath(this);
return Files.exists(path);
}
@@ -104,7 +104,7 @@ public abstract class AbstractEncryptedNode implements DavResource {
@Override
public long getModificationTime() {
final Path path = PathUtils.getPhysicalPath(this);
final Path path = ResourcePathUtils.getPhysicalPath(this);
try {
return Files.getLastModifiedTime(path).toMillis();
} catch (IOException e) {
@@ -173,8 +173,8 @@ public abstract class AbstractEncryptedNode implements DavResource {
@Override
public void move(DavResource dest) throws DavException {
final Path src = PathUtils.getPhysicalPath(this);
final Path dst = PathUtils.getPhysicalPath(dest);
final Path src = ResourcePathUtils.getPhysicalPath(this);
final Path dst = ResourcePathUtils.getPhysicalPath(dest);
try {
// check for conflicts:
if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) {
@@ -195,8 +195,8 @@ public abstract class AbstractEncryptedNode implements DavResource {
@Override
public void copy(DavResource dest, boolean shallow) throws DavException {
final Path src = PathUtils.getPhysicalPath(this);
final Path dst = PathUtils.getPhysicalPath(dest);
final Path src = ResourcePathUtils.getPhysicalPath(this);
final Path dst = ResourcePathUtils.getPhysicalPath(dest);
try {
// check for conflicts:
if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) {

View File

@@ -64,7 +64,7 @@ public class EncryptedDir extends AbstractEncryptedNode {
}
private void addMemberDir(DavResource resource, InputContext inputContext) throws DavException {
final Path childPath = PathUtils.getPhysicalPath(resource);
final Path childPath = ResourcePathUtils.getPhysicalPath(resource);
try {
Files.createDirectories(childPath);
} catch (SecurityException e) {
@@ -76,7 +76,7 @@ public class EncryptedDir extends AbstractEncryptedNode {
}
private void addMemberFile(DavResource resource, InputContext inputContext) throws DavException {
final Path childPath = PathUtils.getPhysicalPath(resource);
final Path childPath = ResourcePathUtils.getPhysicalPath(resource);
SeekableByteChannel channel = null;
try {
channel = Files.newByteChannel(childPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
@@ -94,7 +94,7 @@ public class EncryptedDir extends AbstractEncryptedNode {
@Override
public DavResourceIterator getMembers() {
final Path dir = PathUtils.getPhysicalPath(this);
final Path dir = ResourcePathUtils.getPhysicalPath(this);
try {
final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(dir, cryptor.getPayloadFilesFilter());
final List<DavResource> result = new ArrayList<>();
@@ -116,7 +116,7 @@ public class EncryptedDir extends AbstractEncryptedNode {
@Override
public void removeMember(DavResource member) throws DavException {
final Path memberPath = PathUtils.getPhysicalPath(member);
final Path memberPath = ResourcePathUtils.getPhysicalPath(member);
try {
Files.walkFileTree(memberPath, new DeletingFileVisitor());
} catch (SecurityException e) {
@@ -133,14 +133,14 @@ public class EncryptedDir extends AbstractEncryptedNode {
@Override
protected void determineProperties() {
final Path path = PathUtils.getPhysicalPath(this);
final Path path = ResourcePathUtils.getPhysicalPath(this);
properties.add(new ResourceType(ResourceType.COLLECTION));
properties.add(new DefaultDavProperty<Integer>(DavPropertyName.ISCOLLECTION, 1));
if (Files.exists(path)) {
try {
final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
properties.add(new DefaultDavProperty<Long>(DavPropertyName.CREATIONDATE, attrs.creationTime().toMillis()));
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETLASTMODIFIED, attrs.lastModifiedTime().toMillis()));
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
} catch (IOException e) {
LOG.error("Error determining metadata " + path.toString(), e);
// don't add any further properties

View File

@@ -30,6 +30,8 @@ import org.apache.jackrabbit.webdav.property.DavPropertyName;
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -63,9 +65,10 @@ public class EncryptedFile extends AbstractEncryptedNode {
@Override
public void spool(OutputContext outputContext) throws IOException {
final Path path = PathUtils.getPhysicalPath(this);
final Path path = ResourcePathUtils.getPhysicalPath(this);
if (Files.exists(path)) {
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString());
SeekableByteChannel channel = null;
try {
channel = Files.newByteChannel(path, StandardOpenOption.READ);
@@ -81,13 +84,12 @@ public class EncryptedFile extends AbstractEncryptedNode {
} finally {
IOUtils.closeQuietly(channel);
}
}
}
@Override
protected void determineProperties() {
final Path path = PathUtils.getPhysicalPath(this);
final Path path = ResourcePathUtils.getPhysicalPath(this);
if (Files.exists(path)) {
SeekableByteChannel channel = null;
try {
@@ -96,8 +98,9 @@ public class EncryptedFile extends AbstractEncryptedNode {
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, contentLength));
final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
properties.add(new DefaultDavProperty<Long>(DavPropertyName.CREATIONDATE, attrs.creationTime().toMillis()));
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETLASTMODIFIED, attrs.lastModifiedTime().toMillis()));
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
properties.add(new HttpHeaderProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString()));
} catch (IOException e) {
LOG.error("Error determining metadata " + path.toString(), e);
throw new IORuntimeException(e);

View File

@@ -0,0 +1,144 @@
package org.cryptomator.webdav.jackrabbit.resources;
import java.io.EOFException;
import java.io.IOException;
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 org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.jackrabbit.webdav.DavResourceFactory;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavServletRequest;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.io.OutputContext;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.eclipse.jetty.http.HttpHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 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 {
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 = '-';
/**
* e.g. range -500 (gets the last 500 bytes) -> (-1, 500)
*/
private static final Long SUFFIX_BYTE_RANGE_LOWER = -1L;
/**
* e.g. range 500- (gets all bytes from 500) -> (500, MAX_LONG)
*/
private static final Long SUFFIX_BYTE_RANGE_UPPER = Long.MAX_VALUE;
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);
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
if (rangeHeader == null) {
throw new IllegalArgumentException("HTTP request doesn't contain a range header");
}
determineByteRanges(rangeHeader);
}
private void determineByteRanges(String rangeHeader) {
final String byteRangeSet = StringUtils.removeStartIgnoreCase(rangeHeader, BYTE_UNIT_PREFIX);
final String[] byteRanges = StringUtils.split(byteRangeSet, RANGE_SET_SEP);
if (byteRanges.length == 0) {
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
}
for (final String byteRange : byteRanges) {
final String[] bytePos = StringUtils.splitPreserveAllTokens(byteRange, RANGE_SEP);
if (bytePos.length != 2 || bytePos[0].isEmpty() && bytePos[1].isEmpty()) {
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
}
final Long lower = bytePos[0].isEmpty() ? SUFFIX_BYTE_RANGE_LOWER : Long.valueOf(bytePos[0]);
final Long upper = bytePos[1].isEmpty() ? SUFFIX_BYTE_RANGE_UPPER : Long.valueOf(bytePos[1]);
if (lower > upper) {
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
}
requestedContentRanges.add(new ImmutablePair<Long, Long>(lower, upper));
}
}
/**
* @return One range, that spans all requested ranges.
*/
private Pair<Long, Long> getUnionRange(Long fileSize) {
final long lastByte = fileSize - 1;
final MutablePair<Long, Long> result = new MutablePair<Long, Long>();
for (Pair<Long, Long> range : requestedContentRanges) {
final long left;
final long right;
if (SUFFIX_BYTE_RANGE_LOWER.equals(range.getLeft())) {
left = lastByte - range.getRight();
right = lastByte;
} else if (SUFFIX_BYTE_RANGE_UPPER.equals(range.getRight())) {
left = range.getLeft();
right = lastByte;
} else {
left = range.getLeft();
right = range.getRight();
}
if (result.getLeft() == null || left < result.getLeft()) {
result.setLeft(left);
}
if (result.getRight() == null || right > result.getRight()) {
result.setRight(right);
}
}
return result;
}
@Override
public void spool(OutputContext outputContext) throws IOException {
final Path path = ResourcePathUtils.getPhysicalPath(this);
if (Files.exists(path)) {
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
SeekableByteChannel channel = null;
try {
channel = Files.newByteChannel(path, StandardOpenOption.READ);
final Long fileSize = cryptor.decryptedContentLength(channel);
final Pair<Long, Long> range = getUnionRange(fileSize);
final Long rangeLength = range.getRight() - range.getLeft() + 1;
outputContext.setContentLength(rangeLength);
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getLeft(), range.getRight(), fileSize));
if (outputContext.hasStream()) {
cryptor.decryptRange(channel, outputContext.getOutputStream(), range.getLeft(), rangeLength);
}
} catch (EOFException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Unexpected end of stream during delivery of partial content (client hung up).");
}
} catch (IOException e) {
LOG.error("Error reading file " + path.toString(), e);
throw new IORuntimeException(e);
} finally {
IOUtils.closeQuietly(channel);
}
}
}
private String getContentRangeHeader(long firstByte, long lastByte, long completeLength) {
return String.format("%d-%d/%d", firstByte, lastByte, completeLength);
}
}

View File

@@ -0,0 +1,28 @@
/*******************************************************************************
* 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.webdav.jackrabbit.resources;
import java.nio.file.attribute.FileTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.temporal.Temporal;
final class FileTimeUtils {
private FileTimeUtils() {
throw new IllegalStateException("not instantiable");
}
static String toRfc1123String(FileTime time) {
final Temporal date = OffsetDateTime.ofInstant(time.toInstant(), ZoneOffset.UTC);
return DateTimeFormatter.RFC_1123_DATE_TIME.format(date);
}
}

View File

@@ -0,0 +1,20 @@
package org.cryptomator.webdav.jackrabbit.resources;
import org.apache.jackrabbit.webdav.property.AbstractDavProperty;
import org.apache.jackrabbit.webdav.property.DavPropertyName;
class HttpHeaderProperty extends AbstractDavProperty<String> {
private final String value;
public HttpHeaderProperty(String key, String value) {
super(DavPropertyName.create(key), true);
this.value = value;
}
@Override
public String getValue() {
return value;
}
}

View File

@@ -14,9 +14,9 @@ import java.nio.file.Path;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceLocator;
public final class PathUtils {
public final class ResourcePathUtils {
private PathUtils() {
private ResourcePathUtils() {
throw new IllegalStateException("not instantiable");
}

View File

@@ -1,48 +0,0 @@
<?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
-->
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
<appender name="console" class="org.apache.log4j.ConsoleAppender">
<param name="Target" value="System.out"/>
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%16d %-5p [%c{1}:%L] %m%n" />
</layout>
<filter class="org.apache.log4j.varia.LevelRangeFilter">
<param name="LevelMin" value="debug" />
<param name="LevelMax" value="info" />
</filter>
</appender>
<appender name="stderr" class="org.apache.log4j.ConsoleAppender">
<param name="Target" value="System.err"/>
<param name="threshold" value="warn" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%16d %-5p [%c{1}:%L] %m%n" />
</layout>
</appender>
<appender name="fileAppender" class="org.apache.log4j.DailyRollingFileAppender">
<param name="File" value="/tmp/webdav.log" />
<param name="Append" value="true" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%16d %-5p [%c{1}:%L] %m%n" />
</layout>
</appender>
<root>
<priority value="DEBUG" />
<appender-ref ref="console" />
<appender-ref ref="stderr" />
</root>
</log4j:configuration>

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.1.0</version>
<version>0.3.0-SNAPSHOT</version>
</parent>
<artifactId>crypto-aes</artifactId>
<name>Cryptomator cryptographic module (AES)</name>
@@ -24,12 +24,6 @@
<artifactId>crypto-api</artifactId>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<!-- Commons -->
<dependency>
<groupId>commons-io</groupId>
@@ -48,33 +42,10 @@
<artifactId>commons-codec</artifactId>
</dependency>
<!-- JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- JUnit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

View File

@@ -26,9 +26,13 @@ import java.security.spec.KeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.zip.CRC32;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
@@ -42,12 +46,14 @@ import org.apache.commons.io.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.AbstractCryptor;
import org.cryptomator.crypto.CryptorIOSupport;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.cryptomator.crypto.io.SeekableByteChannelInputStream;
import org.cryptomator.crypto.io.SeekableByteChannelOutputStream;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicConfiguration, FileNamingConventions {
@@ -84,8 +90,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
*/
private final byte[] masterKey = new byte[MASTER_KEY_LENGTH];
private static final int SIZE_OF_LONG = Long.SIZE / Byte.SIZE;
private static final int SIZE_OF_INT = Integer.SIZE / Byte.SIZE;
private static final int SIZE_OF_LONG = Long.BYTES;
static {
try {
@@ -99,16 +104,27 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
}
/**
* Fills the masterkey with new random bytes.
* Creates a new Cryptor with a newly initialized PRNG.
*/
public void randomizeMasterKey() {
public Aes256Cryptor() {
SECURE_PRNG.setSeed(SECURE_PRNG.generateSeed(PRNG_SEED_LENGTH));
SECURE_PRNG.nextBytes(this.masterKey);
}
/**
* Creates a new Cryptor with the given PRNG.<br/>
* <strong>DO NOT USE IN PRODUCTION</strong>. This constructor must only be used in in unit tests. Do not change method visibility.
*
* @param prng Fast, possibly insecure PRNG.
*/
Aes256Cryptor(Random prng) {
prng.nextBytes(this.masterKey);
}
/**
* Encrypts the current masterKey with the given password and writes the result to the given output stream.
*/
@Override
public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException {
try {
// derive key:
@@ -144,6 +160,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
* @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In
* this case Java JCE needs to be installed.
*/
@Override
public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException {
byte[] decrypted = new byte[0];
try {
@@ -245,76 +262,112 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
}
}
private long crc32Sum(byte[] source) {
final CRC32 crc32 = new CRC32();
crc32.update(source);
return crc32.getValue();
}
@Override
public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep) {
public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
try {
final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
final String[] cleartextPathComps = StringUtils.split(cleartextPath, cleartextPathSep);
final List<String> encryptedPathComps = new ArrayList<>(cleartextPathComps.length);
for (final String cleartext : cleartextPathComps) {
final String encrypted = encryptPathComponent(cleartext, key);
final String encrypted = encryptPathComponent(cleartext, key, ioSupport);
encryptedPathComps.add(encrypted);
}
return StringUtils.join(encryptedPathComps, encryptedPathSep);
} catch (IllegalBlockSizeException | BadPaddingException e) {
} catch (IllegalBlockSizeException | BadPaddingException | IOException e) {
throw new IllegalStateException("Unable to encrypt path: " + cleartextPath, e);
}
}
private String encryptPathComponent(final String cleartext, final SecretKey key) throws IllegalBlockSizeException, BadPaddingException {
if (cleartext.length() > PLAINTEXT_FILENAME_LENGTH_LIMIT) {
return encryptLongPathComponent(cleartext, key);
/**
* Each path component, i.e. file or directory name separated by path separators, gets encrypted for its own.<br/>
* Encryption will blow up the filename length due to aes block sizes and base32 encoding. The result may be too long for some old file
* systems.<br/>
* This means that we need a workaround for filenames longer than the limit defined in
* {@link FileNamingConventions#ENCRYPTED_FILENAME_LENGTH_LIMIT}.<br/>
* <br/>
* In any case we will create the encrypted filename normally. For those, that are too long, we calculate a checksum. No
* cryptographically secure hash is needed here. We just want an uniform distribution for better load balancing. All encrypted filenames
* with the same checksum will then share a metadata file, in which a lookup map between encrypted filenames and short unique
* alternative names are stored.<br/>
* <br/>
* These alternative names consist of the checksum, a unique id and a special file extension defined in
* {@link FileNamingConventions#LONG_NAME_FILE_EXT}.
*/
private String encryptPathComponent(final String cleartext, final SecretKey key, CryptorIOSupport ioSupport) throws IllegalBlockSizeException, BadPaddingException, IOException {
final Cipher cipher = this.cipher(FILE_NAME_CIPHER, key, EMPTY_IV, Cipher.ENCRYPT_MODE);
final byte[] cleartextBytes = cleartext.getBytes(Charsets.UTF_8);
final byte[] encryptedBytes = cipher.doFinal(cleartextBytes);
final String encrypted = ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes) + BASIC_FILE_EXT;
if (encrypted.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) {
final String crc32 = Long.toHexString(crc32Sum(encrypted.getBytes()));
final String metadataFilename = crc32 + METADATA_FILE_EXT;
final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataFilename);
final String alternativeFileName = crc32 + LONG_NAME_PREFIX_SEPARATOR + metadata.getOrCreateUuidForEncryptedFilename(encrypted).toString() + LONG_NAME_FILE_EXT;
this.storeMetadata(ioSupport, metadataFilename, metadata);
return alternativeFileName;
} else {
return encryptShortPathComponent(cleartext, key);
return encrypted;
}
}
private String encryptShortPathComponent(final String cleartext, final SecretKey key) throws IllegalBlockSizeException, BadPaddingException {
final Cipher cipher = this.cipher(FILE_NAME_CIPHER, key, EMPTY_IV, Cipher.ENCRYPT_MODE);
final byte[] encryptedBytes = cipher.doFinal(cleartext.getBytes(Charsets.UTF_8));
return ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes) + BASIC_FILE_EXT;
}
private String encryptLongPathComponent(String cleartext, SecretKey key) {
throw new UnsupportedOperationException("not yet implemented");
}
@Override
public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep) {
public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
try {
final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
final String[] encryptedPathComps = StringUtils.split(encryptedPath, encryptedPathSep);
final List<String> cleartextPathComps = new ArrayList<>(encryptedPathComps.length);
for (final String encrypted : encryptedPathComps) {
final String cleartext = decryptPathComponent(encrypted, key);
final String cleartext = decryptPathComponent(encrypted, key, ioSupport);
cleartextPathComps.add(new String(cleartext));
}
return StringUtils.join(cleartextPathComps, cleartextPathSep);
} catch (IllegalBlockSizeException | BadPaddingException e) {
} catch (IllegalBlockSizeException | BadPaddingException | IOException e) {
throw new IllegalStateException("Unable to decrypt path: " + encryptedPath, e);
}
}
private String decryptPathComponent(final String encrypted, final SecretKey key) throws IllegalBlockSizeException, BadPaddingException {
/**
* @see #encryptPathComponent(String, SecretKey, CryptorIOSupport)
*/
private String decryptPathComponent(final String encrypted, final SecretKey key, CryptorIOSupport ioSupport) throws IllegalBlockSizeException, BadPaddingException, IOException {
final String ciphertext;
if (encrypted.endsWith(LONG_NAME_FILE_EXT)) {
return decryptLongPathComponent(encrypted, key);
final String basename = StringUtils.removeEnd(encrypted, LONG_NAME_FILE_EXT);
final String crc32 = StringUtils.substringBefore(basename, LONG_NAME_PREFIX_SEPARATOR);
final String uuid = StringUtils.substringAfter(basename, LONG_NAME_PREFIX_SEPARATOR);
final String metadataFilename = crc32 + METADATA_FILE_EXT;
final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataFilename);
ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
} else if (encrypted.endsWith(BASIC_FILE_EXT)) {
return decryptShortPathComponent(encrypted, key);
ciphertext = StringUtils.removeEndIgnoreCase(encrypted, BASIC_FILE_EXT);
} else {
throw new IllegalArgumentException("Unsupported path component: " + encrypted);
}
}
private String decryptShortPathComponent(final String encrypted, final SecretKey key) throws IllegalBlockSizeException, BadPaddingException {
final String basename = StringUtils.removeEndIgnoreCase(encrypted, BASIC_FILE_EXT);
final Cipher cipher = this.cipher(FILE_NAME_CIPHER, key, EMPTY_IV, Cipher.DECRYPT_MODE);
final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(basename);
final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(ciphertext);
final byte[] cleartextBytes = cipher.doFinal(encryptedBytes);
return new String(cleartextBytes, Charsets.UTF_8);
}
private String decryptLongPathComponent(final String encrypted, final SecretKey key) {
throw new UnsupportedOperationException("not yet implemented");
private LongFilenameMetadata getMetadata(CryptorIOSupport ioSupport, String metadataFile) throws IOException {
final byte[] fileContent = ioSupport.readPathSpecificMetadata(metadataFile);
if (fileContent == null) {
return new LongFilenameMetadata();
} else {
return objectMapper.readValue(fileContent, LongFilenameMetadata.class);
}
}
private void storeMetadata(CryptorIOSupport ioSupport, String metadataFile, LongFilenameMetadata metadata) throws JsonProcessingException, IOException {
ioSupport.writePathSpecificMetadata(metadataFile, objectMapper.writeValueAsBytes(metadata));
}
@Override
@@ -346,8 +399,39 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
// read content
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
final OutputStream cipheredOut = new CipherOutputStream(plaintextFile, cipher);
return IOUtils.copyLarge(in, cipheredOut);
final InputStream cipheredIn = new CipherInputStream(in, cipher);
return IOUtils.copyLarge(cipheredIn, plaintextFile);
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException {
// skip content size:
encryptedFile.position(SIZE_OF_LONG);
// read iv:
final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
final int read = encryptedFile.read(countingIv);
if (read != AES_BLOCK_LENGTH) {
throw new IOException("Failed to read encrypted file header.");
}
// seek relevant position and update iv:
long firstRelevantBlock = pos / AES_BLOCK_LENGTH; // cut of fraction!
long beginOfFirstRelevantBlock = firstRelevantBlock * AES_BLOCK_LENGTH;
long offsetInsideFirstRelevantBlock = pos - beginOfFirstRelevantBlock;
countingIv.putLong(AES_BLOCK_LENGTH - SIZE_OF_LONG, firstRelevantBlock);
// fast forward stream:
encryptedFile.position(SIZE_OF_LONG + AES_BLOCK_LENGTH + beginOfFirstRelevantBlock);
// derive secret key and generate cipher:
final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
final Cipher cipher = this.cipher(FILE_CONTENT_CIPHER, key, countingIv.array(), Cipher.DECRYPT_MODE);
// read content
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
final InputStream cipheredIn = new CipherInputStream(in, cipher);
return IOUtils.copyLarge(cipheredIn, plaintextFile, offsetInsideFirstRelevantBlock, length);
}
@Override
@@ -355,18 +439,22 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
// truncate file
encryptedFile.truncate(0);
// use an IV, whose last 4 bytes store an integer used in counter mode and write initial value to file.
// 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.putInt(AES_BLOCK_LENGTH - SIZE_OF_INT, 0);
countingIv.putLong(AES_BLOCK_LENGTH - SIZE_OF_LONG, 0l);
countingIv.position(0);
// derive secret key and generate cipher:
final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
final Cipher cipher = this.cipher(FILE_CONTENT_CIPHER, key, countingIv.array(), Cipher.ENCRYPT_MODE);
// skip 8 bytes (reserved for file size):
encryptedFile.position(SIZE_OF_LONG);
// 8 bytes (file size: temporarily -1):
final ByteBuffer fileSize = ByteBuffer.allocate(SIZE_OF_LONG);
fileSize.putLong(-1L);
fileSize.position(0);
encryptedFile.write(fileSize);
// write iv:
// 16 bytes (iv):
encryptedFile.write(countingIv);
// write content:
@@ -375,11 +463,11 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
final Long actualSize = IOUtils.copyLarge(plaintextFile, cipheredOut);
// write filesize
final ByteBuffer actualSizeBuffer = ByteBuffer.allocate(SIZE_OF_LONG);
actualSizeBuffer.putLong(actualSize);
actualSizeBuffer.position(0);
fileSize.position(0);
fileSize.putLong(actualSize);
fileSize.position(0);
encryptedFile.position(0);
encryptedFile.write(actualSizeBuffer);
encryptedFile.write(fileSize);
return actualSize;
}

View File

@@ -16,10 +16,9 @@ interface AesCryptographicConfiguration {
int PRNG_SEED_LENGTH = 16;
/**
* Number of bytes of the master key. Should be significantly higher than the {@link #AES_KEY_LENGTH}, as a corrupted masterkey can't be
* changed without decrypting and re-encrypting all files first.
* Number of bytes of the master key. Should be the maximum possible AES key length to provide best security.
*/
int MASTER_KEY_LENGTH = 512;
int MASTER_KEY_LENGTH = 256;
/**
* Number of bytes used as salt, where needed.

View File

@@ -28,23 +28,31 @@ interface FileNamingConventions {
/**
* Maximum length possible on file systems with a filename limit of 255 chars.<br/>
* 144 and 160 are multiples of 16 (128bit aes block size).<br/>
* 144 * 8/5 (base32) = 230,..<br/>
* 160 * 8/5 = 256<br/>
* Base 64 isn't supported on case-insensitive file systems.<br/>
* Also we would need a few chars for our file extension, so lets use {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT}.
*/
int PLAINTEXT_FILENAME_LENGTH_LIMIT = 144;
int ENCRYPTED_FILENAME_LENGTH_LIMIT = 250;
/**
* For plaintext file names <= {@value #PLAINTEXT_FILENAME_LENGTH_LIMIT} chars.
* For plaintext file names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
*/
String BASIC_FILE_EXT = ".aes";
/**
* For plaintext file names > {@value #PLAINTEXT_FILENAME_LENGTH_LIMIT} chars.
* For plaintext file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
*/
String LONG_NAME_FILE_EXT = ".lng.aes";
/**
* Prefix in file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars used to determine the corresponding metadata file.
*/
String LONG_NAME_PREFIX_SEPARATOR = "_";
/**
* For metadata files for a certain group of files. The cryptor may decide what files to assign to the same group; hopefully using some
* kind of uniform distribution for better load balancing.
*/
String METADATA_FILE_EXT = ".meta";
/**
* Matches both, {@value #BASIC_FILE_EXT} and {@value #LONG_NAME_FILE_EXT} files.
*/

View File

@@ -0,0 +1,49 @@
/*******************************************************************************
* 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.crypto.aes256;
import java.io.Serializable;
import java.util.UUID;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
class LongFilenameMetadata implements Serializable {
private static final long serialVersionUID = 6214509403824421320L;
@JsonDeserialize(as = DualHashBidiMap.class)
private BidiMap<UUID, String> encryptedFilenames = new DualHashBidiMap<>();
/* Getter/Setter */
public synchronized String getEncryptedFilenameForUUID(final UUID uuid) {
return encryptedFilenames.get(uuid);
}
public synchronized UUID getOrCreateUuidForEncryptedFilename(String encryptedFilename) {
UUID uuid = encryptedFilenames.getKey(encryptedFilename);
if (uuid == null) {
uuid = UUID.randomUUID();
encryptedFilenames.put(uuid, encryptedFilename);
}
return uuid;
}
public BidiMap<UUID, String> getEncryptedFilenames() {
return encryptedFilenames;
}
public void setEncryptedFilenames(BidiMap<UUID, String> encryptedFilenames) {
this.encryptedFilenames = encryptedFilenames;
}
}

View File

@@ -1,79 +0,0 @@
/*******************************************************************************
* 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.crypto.aes256;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
@JsonPropertyOrder(value = { "iv", "salt", "files" })
class Metadata implements Serializable {
private static final long serialVersionUID = 6214509403824421320L;
private byte[] iv;
private byte[] salt;
@JsonDeserialize(as = DualHashBidiMap.class)
private BidiMap<String, byte[]> filenames;
private Map<String, Long> filesizes;
Metadata() {
// used by jackson
}
Metadata(byte[] iv, byte[] salt) {
this.iv = iv;
this.salt = salt;
}
/* Getter/Setter */
public byte[] getIv() {
return iv;
}
public void setIv(byte[] iv) {
this.iv = iv;
}
public byte[] getSalt() {
return salt;
}
public void setSalt(byte[] salt) {
this.salt = salt;
}
public BidiMap<String, byte[]> getFilenames() {
if (filenames == null) {
filenames = new DualHashBidiMap<>();
}
return filenames;
}
public void setFilenames(BidiMap<String, byte[]> filesnames) {
this.filenames = filesnames;
}
public Map<String, Long> getFilesizes() {
if (filesizes == null) {
filesizes = new HashMap<>();
}
return filesizes;
}
public void setFilesizes(Map<String, Long> filesizes) {
this.filesizes = filesizes;
}
}

View File

@@ -8,97 +8,158 @@
******************************************************************************/
package org.cryptomator.crypto.aes256;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import org.apache.commons.io.FileUtils;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.apache.commons.io.IOUtils;
import org.cryptomator.crypto.CryptorIOSupport;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.junit.After;
import org.junit.Before;
import org.junit.Assert;
import org.junit.Test;
public class Aes256CryptorTest {
private Path tmpDir;
private Path masterKey;
@Before
public void prepareTmpDir() throws IOException {
final String tmpDirName = (String) System.getProperties().get("java.io.tmpdir");
final Path path = FileSystems.getDefault().getPath(tmpDirName);
tmpDir = Files.createTempDirectory(path, "oce-crypto-test");
masterKey = tmpDir.resolve("test" + Aes256Cryptor.MASTERKEY_FILE_EXT);
}
@After
public void dropTmpDir() throws IOException {
FileUtils.deleteDirectory(tmpDir.toFile());
}
/* ------------------------------------------------------------------------------- */
private static final Random TEST_PRNG = new Random();
@Test
public void testCorrectPassword() throws IOException, WrongPasswordException, DecryptFailedException, UnsupportedKeyLengthException {
final String pw = "asd";
final Aes256Cryptor cryptor = new Aes256Cryptor();
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
cryptor.encryptMasterKey(out, pw);
cryptor.swipeSensitiveData();
final Aes256Cryptor decryptor = new Aes256Cryptor();
final InputStream in = Files.newInputStream(masterKey, StandardOpenOption.READ);
final Aes256Cryptor decryptor = new Aes256Cryptor(TEST_PRNG);
final InputStream in = new ByteArrayInputStream(out.toByteArray());
decryptor.decryptMasterKey(in, pw);
IOUtils.closeQuietly(out);
IOUtils.closeQuietly(in);
}
@Test(expected = WrongPasswordException.class)
public void testWrongPassword() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
final String pw = "asd";
final Aes256Cryptor cryptor = new Aes256Cryptor();
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
cryptor.encryptMasterKey(out, pw);
cryptor.swipeSensitiveData();
final String wrongPw = "foo";
final Aes256Cryptor decryptor = new Aes256Cryptor();
final InputStream in = Files.newInputStream(masterKey, StandardOpenOption.READ);
final Aes256Cryptor decryptor = new Aes256Cryptor(TEST_PRNG);
final InputStream in = new ByteArrayInputStream(out.toByteArray());
decryptor.decryptMasterKey(in, wrongPw);
IOUtils.closeQuietly(out);
IOUtils.closeQuietly(in);
}
@Test(expected = NoSuchFileException.class)
public void testWrongLocation() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
final String pw = "asd";
final Aes256Cryptor cryptor = new Aes256Cryptor();
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
cryptor.encryptMasterKey(out, pw);
cryptor.swipeSensitiveData();
@Test
public void testEncryptionAndDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
// our test plaintext data:
final byte[] plaintextData = "Hello World".getBytes();
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
final Path wrongMasterKey = tmpDir.resolve("notExistingMasterKey.json");
final Aes256Cryptor decryptor = new Aes256Cryptor();
final InputStream in = Files.newInputStream(wrongMasterKey, StandardOpenOption.READ);
decryptor.decryptMasterKey(in, pw);
// init cryptor:
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(plaintextData.length + 200);
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
IOUtils.closeQuietly(encryptedOut);
// decrypt:
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
final Long numDecryptedBytes = cryptor.decryptedFile(encryptedIn, plaintextOut);
IOUtils.closeQuietly(encryptedIn);
IOUtils.closeQuietly(plaintextOut);
Assert.assertTrue(numDecryptedBytes > 0);
// check decrypted data:
final byte[] result = plaintextOut.toByteArray();
Assert.assertArrayEquals(plaintextData, result);
}
@Test(expected = FileAlreadyExistsException.class)
public void testReInitialization() throws IOException {
final String pw = "asd";
final Aes256Cryptor cryptor = new Aes256Cryptor();
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
cryptor.encryptMasterKey(out, pw);
cryptor.swipeSensitiveData();
@Test
public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
// our test plaintext data:
final byte[] plaintextData = new byte[65536 * Integer.BYTES];
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);
for (int i = 0; i < 65536; i++) {
bbIn.putInt(i);
}
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
// init cryptor:
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(plaintextData.length + 200);
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
IOUtils.closeQuietly(encryptedOut);
// decrypt:
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
final Long numDecryptedBytes = cryptor.decryptRange(encryptedIn, plaintextOut, 25000 * Integer.BYTES, 30000 * Integer.BYTES);
IOUtils.closeQuietly(encryptedIn);
IOUtils.closeQuietly(plaintextOut);
Assert.assertTrue(numDecryptedBytes > 0);
// check decrypted data:
final byte[] result = plaintextOut.toByteArray();
final byte[] expected = Arrays.copyOfRange(plaintextData, 25000 * Integer.BYTES, 55000 * Integer.BYTES);
Assert.assertArrayEquals(expected, result);
}
@Test
public void testEncryptionOfFilenames() throws IOException {
final CryptorIOSupport ioSupportMock = new CryptoIOSupportMock();
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
// short path components
final String originalPath1 = "foo/bar/baz";
final String encryptedPath1 = cryptor.encryptPath(originalPath1, '/', '/', ioSupportMock);
final String decryptedPath1 = cryptor.decryptPath(encryptedPath1, '/', '/', ioSupportMock);
Assert.assertEquals(originalPath1, decryptedPath1);
// long path components
final String str50chars = "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee";
final String originalPath2 = "foo/" + str50chars + str50chars + str50chars + str50chars + str50chars + "/baz";
final String encryptedPath2 = cryptor.encryptPath(originalPath2, '/', '/', ioSupportMock);
final String decryptedPath2 = cryptor.decryptPath(encryptedPath2, '/', '/', ioSupportMock);
Assert.assertEquals(originalPath2, decryptedPath2);
}
private static class CryptoIOSupportMock implements CryptorIOSupport {
private final Map<String, byte[]> map = new HashMap<>();
@Override
public void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) {
map.put(encryptedPath, encryptedMetadata);
}
@Override
public byte[] readPathSpecificMetadata(String encryptedPath) {
return map.get(encryptedPath);
}
final OutputStream outAgain = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
cryptor.encryptMasterKey(outAgain, pw);
cryptor.swipeSensitiveData();
}
}

View File

@@ -0,0 +1,79 @@
package org.cryptomator.crypto.aes256;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
class ByteBufferBackedSeekableChannel implements SeekableByteChannel {
private final ByteBuffer buffer;
private boolean open = true;
ByteBufferBackedSeekableChannel(ByteBuffer buffer) {
this.buffer = buffer;
}
@Override
public boolean isOpen() {
return open;
}
@Override
public void close() throws IOException {
open = false;
}
@Override
public int read(ByteBuffer dst) throws IOException {
if (buffer.remaining() == 0) {
return -1;
}
int num = Math.min(dst.remaining(), buffer.remaining());
byte[] bytes = new byte[num];
buffer.get(bytes);
dst.put(bytes);
return num;
}
@Override
public int write(ByteBuffer src) throws IOException {
int num = src.remaining();
if (buffer.remaining() < src.remaining()) {
buffer.limit(buffer.limit() + src.remaining());
}
buffer.put(src);
return num;
}
@Override
public long position() throws IOException {
return buffer.position();
}
@Override
public SeekableByteChannel position(long newPosition) throws IOException {
if (newPosition > Integer.MAX_VALUE) {
throw new UnsupportedOperationException();
}
if (newPosition > buffer.limit()) {
buffer.limit((int) newPosition);
}
buffer.position((int) newPosition);
return this;
}
@Override
public long size() throws IOException {
return buffer.limit();
}
@Override
public SeekableByteChannel truncate(long size) throws IOException {
if (size > Integer.MAX_VALUE) {
throw new UnsupportedOperationException();
}
buffer.limit((int) size);
return this;
}
}

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.1.0</version>
<version>0.3.0-SNAPSHOT</version>
</parent>
<artifactId>crypto-api</artifactId>
<name>Cryptomator cryptographic module API</name>
@@ -22,21 +22,9 @@
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

View File

@@ -1,3 +1,11 @@
/*******************************************************************************
* 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.crypto;
import java.util.HashSet;

View File

@@ -15,11 +15,31 @@ import java.nio.channels.SeekableByteChannel;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Path;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
/**
* Provides access to cryptographic functions. All methods are threadsafe.
*/
public interface Cryptor extends SensitiveDataSwipeListener {
/**
* Encrypts the current masterKey with the given password and writes the result to the given output stream.
*/
void encryptMasterKey(OutputStream out, CharSequence password) throws IOException;
/**
* Reads the encrypted masterkey from the given input stream and decrypts it with the given password.
*
* @throws DecryptFailedException If the decryption failed for various reasons (including wrong password).
* @throws WrongPasswordException If the provided password was wrong. Note: Sometimes the algorithm itself fails due to a wrong
* password. In this case a DecryptFailedException will be thrown.
* @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In
* this case Java JCE needs to be installed.
*/
void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException;
/**
* Encrypts each plaintext path component for its own.
*
@@ -32,7 +52,7 @@ public interface Cryptor extends SensitiveDataSwipeListener {
* @return Encrypted path components concatenated by the given encryptedPathSep. Must not start with encryptedPathSep, unless the
* encrypted path is explicitly absolute.
*/
String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep);
String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport);
/**
* Decrypts each encrypted path component for its own.
@@ -46,7 +66,7 @@ public interface Cryptor extends SensitiveDataSwipeListener {
* @return Decrypted path components concatenated by the given cleartextPathSep. Must not start with cleartextPathSep, unless the
* cleartext path is explicitly absolute.
*/
String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep);
String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport);
/**
* @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
@@ -59,6 +79,13 @@ public interface Cryptor extends SensitiveDataSwipeListener {
*/
Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException;
/**
* @param pos First byte (inclusive)
* @param length Number of requested bytes beginning at pos.
* @return Number of decrypted bytes. This might not be equal to the number of bytes requested due to potential overheads.
*/
Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException;
/**
* @return Number of encrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.
*/

View File

@@ -0,0 +1,26 @@
/*******************************************************************************
* 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.crypto;
/**
* Optional monitoring interface. If a cryptor implements this interface, it counts bytes de- and encrypted in a thread-safe manner.
*/
public interface CryptorIOSampling {
/**
* @return Number of encrypted bytes since the last reset.
*/
Long pollEncryptedBytes(boolean resetCounter);
/**
* @return Number of decrypted bytes since the last reset.
*/
Long pollDecryptedBytes(boolean resetCounter);
}

View File

@@ -0,0 +1,31 @@
/*******************************************************************************
* 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.crypto;
import java.io.IOException;
/**
* Methods that may be called by the Cryptor when accessing a path.
*/
public interface CryptorIOSupport {
/**
* Persists encryptedMetadata to the given encryptedPath.
*
* @param encryptedPath A relative path
* @throws IOException
*/
void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) throws IOException;
/**
* @return Previously written encryptedMetadata stored at the given encryptedPath or <code>null</code> if no such file exists.
*/
byte[] readPathSpecificMetadata(String encryptedPath) throws IOException;
}

View File

@@ -0,0 +1,167 @@
package org.cryptomator.crypto;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
public class SamplingDecorator implements Cryptor, CryptorIOSampling {
private final Cryptor cryptor;
private final AtomicLong encryptedBytes;
private final AtomicLong decryptedBytes;
private SamplingDecorator(Cryptor cryptor) {
this.cryptor = cryptor;
encryptedBytes = new AtomicLong();
decryptedBytes = new AtomicLong();
}
public static Cryptor decorate(Cryptor cryptor) {
return new SamplingDecorator(cryptor);
}
@Override
public void swipeSensitiveData() {
cryptor.swipeSensitiveData();
}
@Override
public Long pollEncryptedBytes(boolean resetCounter) {
if (resetCounter) {
return encryptedBytes.getAndSet(0);
} else {
return encryptedBytes.get();
}
}
@Override
public Long pollDecryptedBytes(boolean resetCounter) {
if (resetCounter) {
return decryptedBytes.getAndSet(0);
} else {
return decryptedBytes.get();
}
}
/* Cryptor */
@Override
public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException {
cryptor.encryptMasterKey(out, password);
}
@Override
public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException {
cryptor.decryptMasterKey(in, password);
}
@Override
public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
encryptedBytes.addAndGet(StringUtils.length(cleartextPath));
return cryptor.encryptPath(cleartextPath, encryptedPathSep, cleartextPathSep, ioSupport);
}
@Override
public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
decryptedBytes.addAndGet(StringUtils.length(encryptedPath));
return cryptor.decryptPath(encryptedPath, encryptedPathSep, cleartextPathSep, ioSupport);
}
@Override
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException {
return cryptor.decryptedContentLength(encryptedFile);
}
@Override
public Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException {
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
return cryptor.decryptedFile(encryptedFile, countingInputStream);
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException {
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
return cryptor.decryptRange(encryptedFile, countingInputStream, pos, length);
}
@Override
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException {
final InputStream countingInputStream = new CountingInputStream(encryptedBytes, plaintextFile);
return cryptor.encryptFile(countingInputStream, encryptedFile);
}
@Override
public Filter<Path> getPayloadFilesFilter() {
return cryptor.getPayloadFilesFilter();
}
@Override
public void addSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) {
cryptor.addSensitiveDataSwipeListener(listener);
}
@Override
public void removeSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) {
cryptor.removeSensitiveDataSwipeListener(listener);
}
private class CountingInputStream extends InputStream {
private final InputStream in;
private final AtomicLong counter;
private CountingInputStream(AtomicLong counter, InputStream in) {
this.in = in;
this.counter = counter;
}
@Override
public int read() throws IOException {
int count = in.read();
counter.addAndGet(count);
return count;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int count = in.read(b, off, len);
counter.addAndGet(count);
return count;
}
}
private class CountingOutputStream extends OutputStream {
private final OutputStream out;
private final AtomicLong counter;
private CountingOutputStream(AtomicLong counter, OutputStream out) {
this.out = out;
this.counter = counter;
}
@Override
public void write(int b) throws IOException {
counter.incrementAndGet();
out.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
counter.addAndGet(len);
out.write(b, off, len);
}
}
}

View File

@@ -1,3 +1,11 @@
/*******************************************************************************
* 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.crypto;
public interface SensitiveDataSwipeListener {

View File

@@ -1,25 +1,18 @@
<?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.1.0</version>
<version>0.3.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Cryptomator</name>
<organization>
<name>cryptomator.org</name>
<url>http://cryptomator.org</url>
</organization>
<developers>
<developer>
<name>Sebastian Stenzel</name>
@@ -32,8 +25,8 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- dependency versions -->
<log4j.version>1.2.16</log4j.version>
<slf4j.version>1.7.5</slf4j.version>
<log4j.version>2.1</log4j.version>
<slf4j.version>1.7.7</slf4j.version>
<junit.version>4.11</junit.version>
<commons-io.version>2.4</commons-io.version>
<commons-collections.version>4.0</commons-collections.version>
@@ -64,22 +57,27 @@
<artifactId>ui</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-jul</artifactId>
<version>${log4j.version}</version>
</dependency>
<!-- commons -->
@@ -121,6 +119,25 @@
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-jul</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
<modules>
<module>crypto-api</module>
<module>crypto-aes</module>
@@ -128,4 +145,18 @@
<module>ui</module>
</modules>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.1.0</version>
<version>0.3.0-SNAPSHOT</version>
</parent>
<artifactId>ui</artifactId>
<name>Cryptomator GUI</name>
@@ -21,6 +21,7 @@
<javafx.application.name>Cryptomator</javafx.application.name>
<exec.mainClass>org.cryptomator.ui.MainApplication</exec.mainClass>
<javafx.tools.ant.jar>${java.home}/../lib/ant-javafx.jar</javafx.tools.ant.jar>
<controlsfx.version>8.20.8</controlsfx.version>
</properties>
<dependencies>
@@ -48,84 +49,72 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- UI -->
<dependency>
<groupId>org.controlsfx</groupId>
<artifactId>controlsfx</artifactId>
<version>${controlsfx.version}</version>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>copy</id>
<phase>prepare-package</phase>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
<goal>single</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/libs</outputDirectory>
<includeScope>compile</includeScope>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<finalName>${javafx.application.name}</finalName>
<appendAssemblyId>false</appendAssemblyId>
<archive>
<manifestEntries>
<Main-Class>${exec.mainClass}</Main-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.7</version>
<executions>
<execution>
<id>native-launcher</id>
<phase>package</phase>
<id>create-deployment-bundle</id>
<phase>install</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="${javafx.tools.ant.jar}" />
<fx:application id="fxApp" version="${project.version}" name="${javafx.application.name}" mainClass="${exec.mainClass}" />
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="${project.basedir}:${javafx.tools.ant.jar}" />
<fx:jar destfile="${project.build.directory}/${project.build.finalName}">
<fx:application refid="fxApp" />
<fx:fileset dir="${project.build.directory}/classes" />
<fx:resources>
<fx:fileset dir="${project.build.directory}" includes="libs/*.jar" />
</fx:resources>
</fx:jar>
<fx:deploy outdir="${project.build.directory}/dist" outfile="${project.build.finalName}" nativeBundles="all">
<fx:info title="Cryptomator" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT">
<!-- todo provide .ico files for win -->
<fx:icon href="${project.build.outputDirectory}/logo.icns" width="512" height="512" />
</fx:info>
<fx:deploy nativeBundles="all" outdir="${project.build.directory}/dist" outfile="${project.build.finalName}" verbose="false">
<fx:application name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
<fx:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
<fx:platform basedir="" javafx="2.2+" j2se="8.0" />
<fx:application refid="fxApp" />
<fx:resources>
<!-- If you changed <fx:jar> above, don't forget to modify the line below -->
<fx:fileset dir="${project.build.directory}" includes="${project.build.finalName}.jar" />
<fx:fileset dir="${project.build.directory}" includes="libs/*.jar" />
<fx:fileset dir="${project.build.directory}" includes="${javafx.application.name}.jar" />
</fx:resources>
<fx:preferences install="false" />
<fx:permissions elevated="false" />
<fx:preferences install="true" />
</fx:deploy>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,228 +0,0 @@
/*******************************************************************************
* 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;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ResourceBundle;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.stage.DirectoryChooser;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.util.MasterKeyFilter;
import org.cryptomator.ui.util.WebDavMounter;
import org.cryptomator.ui.util.WebDavMounter.CommandFailedException;
import org.cryptomator.webdav.WebDAVServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AccessController implements Initializable {
private static final Logger LOG = LoggerFactory.getLogger(AccessController.class);
private final Aes256Cryptor cryptor = new Aes256Cryptor();
private ResourceBundle localization;
@FXML
private GridPane rootGridPane;
@FXML
private TextField workDirTextField;
@FXML
private ComboBox<String> usernameBox;
@FXML
private SecPasswordField passwordField;
@FXML
private Button startServerButton;
@FXML
private Label messageLabel;
@Override
public void initialize(URL url, ResourceBundle rb) {
this.localization = rb;
workDirTextField.textProperty().addListener(new WorkDirChangeListener());
usernameBox.valueProperty().addListener(new UsernameChangeListener());
workDirTextField.setText(Settings.load().getWebdavWorkDir());
usernameBox.setValue(Settings.load().getUsername());
}
/**
* Step 1: Choose encrypted storage:
*/
@FXML
protected void chooseWorkDir(ActionEvent event) {
messageLabel.setText(null);
final File currentFolder = new File(workDirTextField.getText());
final DirectoryChooser dirChooser = new DirectoryChooser();
if (currentFolder.exists()) {
dirChooser.setInitialDirectory(currentFolder);
}
final File file = dirChooser.showDialog(rootGridPane.getScene().getWindow());
if (file != null) {
workDirTextField.setText(file.toString());
}
}
private final class WorkDirChangeListener implements ChangeListener<String> {
@Override
public void changed(ObservableValue<? extends String> property, String oldValue, String newValue) {
if (StringUtils.isEmpty(newValue)) {
usernameBox.setDisable(true);
usernameBox.setValue(null);
return;
}
boolean storageLocationValid;
try {
final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText());
final DirectoryStream<Path> ds = MasterKeyFilter.filteredDirectory(storagePath);
final String masterKeyExt = Aes256Cryptor.MASTERKEY_FILE_EXT.toLowerCase();
usernameBox.getItems().clear();
for (final Path path : ds) {
final String fileName = path.getFileName().toString();
final int beginOfExt = fileName.toLowerCase().lastIndexOf(masterKeyExt);
final String baseName = fileName.substring(0, beginOfExt);
usernameBox.getItems().add(baseName);
}
storageLocationValid = !usernameBox.getItems().isEmpty();
} catch (InvalidPathException | IOException ex) {
LOG.trace("Invalid path: " + workDirTextField.getText(), ex);
storageLocationValid = false;
}
// valid encrypted folder?
if (storageLocationValid) {
Settings.load().setWebdavWorkDir(workDirTextField.getText());
Settings.save();
} else {
messageLabel.setText(localization.getString("access.messageLabel.invalidStorageLocation"));
}
// enable/disable next controls:
usernameBox.setDisable(!storageLocationValid);
if (usernameBox.getItems().size() == 1) {
usernameBox.setValue(usernameBox.getItems().get(0));
}
}
}
/**
* Step 2: Choose username
*/
private final class UsernameChangeListener implements ChangeListener<String> {
@Override
public void changed(ObservableValue<? extends String> property, String oldValue, String newValue) {
if (newValue != null) {
Settings.load().setUsername(newValue);
Settings.save();
}
passwordField.setDisable(StringUtils.isEmpty(newValue));
startServerButton.setDisable(StringUtils.isEmpty(newValue));
Platform.runLater(passwordField::requestFocus);
}
}
// step 3: Enter password
/**
* Step 4: Unlock storage
*/
@FXML
protected void startStopServer(ActionEvent event) {
messageLabel.setText(null);
if (WebDAVServer.getInstance().isRunning()) {
this.tryStop();
cryptor.swipeSensitiveData();
} else if (this.unlockStorage()) {
this.tryStart();
}
}
private boolean unlockStorage() {
final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText());
final String masterKeyFileName = usernameBox.getValue() + Aes256Cryptor.MASTERKEY_FILE_EXT;
final Path masterKeyPath = storagePath.resolve(masterKeyFileName);
final CharSequence password = passwordField.getCharacters();
InputStream masterKeyInputStream = null;
try {
masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ);
cryptor.decryptMasterKey(masterKeyInputStream, password);
return true;
} catch (NoSuchFileException e) {
messageLabel.setText(localization.getString("access.messageLabel.invalidStorageLocation"));
LOG.warn("Invalid path: " + storagePath.toString());
} catch (DecryptFailedException ex) {
messageLabel.setText(localization.getString("access.messageLabel.decryptionFailed"));
LOG.error("Decryption failed for technical reasons.", ex);
} catch (WrongPasswordException e) {
messageLabel.setText(localization.getString("access.messageLabel.wrongPassword"));
} catch (UnsupportedKeyLengthException ex) {
messageLabel.setText(localization.getString("access.messageLabel.unsupportedKeyLengthInstallJCE"));
LOG.error("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex);
} catch (IOException ex) {
LOG.error("I/O Exception", ex);
} finally {
passwordField.swipe();
IOUtils.closeQuietly(masterKeyInputStream);
}
return false;
}
private void tryStart() {
final Settings settings = Settings.load();
final int webdavPort = WebDAVServer.getInstance().start(settings.getWebdavWorkDir(), cryptor);
if (webdavPort > 0) {
startServerButton.setText(localization.getString("access.button.stopServer"));
passwordField.setDisable(true);
try {
WebDavMounter.mount(webdavPort);
} catch (CommandFailedException e) {
messageLabel.setText(String.format(localization.getString("access.messageLabel.mountFailed"), webdavPort));
LOG.error("Mounting WebDAV share failed.", e);
}
}
}
private void tryStop() {
try {
WebDavMounter.unmount(5);
if (WebDAVServer.getInstance().stop()) {
startServerButton.setText(localization.getString("access.button.startServer"));
passwordField.setDisable(false);
}
} catch (CommandFailedException e) {
LOG.warn("Unmounting WebDAV share failed.", e);
}
}
}

View File

@@ -8,193 +8,231 @@
******************************************************************************/
package org.cryptomator.ui;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.nio.file.DirectoryStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.Future;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.GridPane;
import javafx.stage.DirectoryChooser;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.files.EncryptingFileVisitor;
import org.cryptomator.ui.controls.ClearOnDisableListener;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.util.MasterKeyFilter;
import org.cryptomator.ui.model.Directory;
import org.cryptomator.ui.util.FXThreads;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class InitializeController implements Initializable {
private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class);
private static final int MAX_USERNAME_LENGTH = 200;
private static final int MAX_USERNAME_LENGTH = 250;
private ResourceBundle localization;
@FXML
private GridPane rootGridPane;
@FXML
private TextField workDirTextField;
private Directory directory;
private InitializationListener listener;
@FXML
private TextField usernameField;
@FXML
private SecPasswordField passwordField;
@FXML
private SecPasswordField retypePasswordField;
@FXML
private Button initWorkDirButton;
private Button okButton;
@FXML
private ProgressIndicator progressIndicator;
@FXML
private Label messageLabel;
@Override
public void initialize(URL url, ResourceBundle rb) {
this.localization = rb;
workDirTextField.textProperty().addListener(new WorkDirChangeListener());
usernameField.addEventFilter(KeyEvent.KEY_TYPED, new AlphaNumericKeyTypeEventFilter());
usernameField.textProperty().addListener(new UsernameChangeListener());
usernameField.disableProperty().addListener(new ClearOnDisableListener(usernameField));
passwordField.textProperty().addListener(new PasswordChangeListener());
passwordField.disableProperty().addListener(new ClearOnDisableListener(passwordField));
retypePasswordField.textProperty().addListener(new RetypePasswordChangeListener());
usernameField.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents);
usernameField.textProperty().addListener(this::usernameFieldDidChange);
passwordField.textProperty().addListener(this::passwordFieldDidChange);
retypePasswordField.textProperty().addListener(this::retypePasswordFieldDidChange);
retypePasswordField.disableProperty().addListener(new ClearOnDisableListener(retypePasswordField));
}
/**
* Step 1: Choose a directory, that shall be encrypted. On success, step 2 will be enabled.
*/
// ****************************************
// Username field
// ****************************************
public void filterAlphanumericKeyEvents(KeyEvent t) {
if (t.getCharacter() == null || t.getCharacter().length() == 0) {
return;
}
char c = t.getCharacter().charAt(0);
if (!CharUtils.isAsciiAlphanumeric(c)) {
t.consume();
}
}
public void usernameFieldDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
if (StringUtils.length(newValue) > MAX_USERNAME_LENGTH) {
usernameField.setText(newValue.substring(0, MAX_USERNAME_LENGTH));
}
passwordField.setDisable(StringUtils.isEmpty(newValue));
}
// ****************************************
// Password field
// ****************************************
private void passwordFieldDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
retypePasswordField.setDisable(StringUtils.isEmpty(newValue));
}
// ****************************************
// Retype password field
// ****************************************
private void retypePasswordFieldDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
boolean passwordsAreEqual = passwordField.getText().equals(retypePasswordField.getText());
okButton.setDisable(!passwordsAreEqual);
}
// ****************************************
// OK button
// ****************************************
@FXML
protected void chooseWorkDir(ActionEvent event) {
final File currentFolder = new File(workDirTextField.getText());
final DirectoryChooser dirChooser = new DirectoryChooser();
if (currentFolder.exists()) {
dirChooser.setInitialDirectory(currentFolder);
protected void initializeVault(ActionEvent event) {
setControlsDisabled(true);
if (!isDirectoryEmpty() && !shouldEncryptExistingFiles()) {
return;
}
final File file = dirChooser.showDialog(rootGridPane.getScene().getWindow());
if (file != null && file.canWrite()) {
workDirTextField.setText(file.toString());
}
}
private final class WorkDirChangeListener implements ChangeListener<String> {
@Override
public void changed(ObservableValue<? extends String> property, String oldValue, String newValue) {
if (StringUtils.isEmpty(newValue)) {
usernameField.setDisable(true);
return;
}
try {
final Path dir = FileSystems.getDefault().getPath(newValue);
final boolean containsMasterKeys = MasterKeyFilter.filteredDirectory(dir).iterator().hasNext();
if (containsMasterKeys) {
usernameField.setDisable(true);
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
} else {
usernameField.setDisable(false);
messageLabel.setText(null);
}
} catch (InvalidPathException | IOException e) {
usernameField.setDisable(true);
messageLabel.setText(localization.getString("initialize.messageLabel.invalidPath"));
}
}
}
/**
* Step 2: Choose a valid username
*/
private static final class AlphaNumericKeyTypeEventFilter implements EventHandler<KeyEvent> {
@Override
public void handle(KeyEvent t) {
if (t.getCharacter() == null || t.getCharacter().length() == 0) {
return;
}
char c = t.getCharacter().charAt(0);
if (!CharUtils.isAsciiAlphanumeric(c)) {
t.consume();
}
}
}
private final class UsernameChangeListener implements ChangeListener<String> {
@Override
public void changed(ObservableValue<? extends String> property, String oldValue, String newValue) {
if (StringUtils.length(newValue) > MAX_USERNAME_LENGTH) {
usernameField.setText(newValue.substring(0, MAX_USERNAME_LENGTH));
}
passwordField.setDisable(StringUtils.isEmpty(usernameField.getText()));
}
}
/**
* Step 3: Defina a password. On success, step 3 will be enabled.
*/
private final class PasswordChangeListener implements ChangeListener<String> {
@Override
public void changed(ObservableValue<? extends String> property, String oldValue, String newValue) {
retypePasswordField.setDisable(newValue.isEmpty());
}
}
/**
* Step 4: Retype the password. On success, step 4 will be enabled.
*/
private final class RetypePasswordChangeListener implements ChangeListener<String> {
@Override
public void changed(ObservableValue<? extends String> property, String oldValue, String newValue) {
boolean passwordsAreEqual = passwordField.getText().equals(retypePasswordField.getText());
initWorkDirButton.setDisable(!passwordsAreEqual);
}
}
/**
* Step 5: Generate master password file in working directory. On success, print success message.
*/
@FXML
protected void initWorkDir(ActionEvent event) {
final Aes256Cryptor cryptor = new Aes256Cryptor();
final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText());
final Path masterKeyPath = storagePath.resolve(usernameField.getText() + Aes256Cryptor.MASTERKEY_FILE_EXT);
final String masterKeyFileName = usernameField.getText() + Aes256Cryptor.MASTERKEY_FILE_EXT;
final Path masterKeyPath = directory.getPath().resolve(masterKeyFileName);
final CharSequence password = passwordField.getCharacters();
OutputStream masterKeyOutputStream = null;
try {
progressIndicator.setVisible(true);
masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
cryptor.encryptMasterKey(masterKeyOutputStream, password);
cryptor.swipeSensitiveData();
workDirTextField.clear();
directory.getCryptor().encryptMasterKey(masterKeyOutputStream, password);
final Future<?> futureDone = FXThreads.runOnBackgroundThread(this::encryptExistingContents);
FXThreads.runOnMainThreadWhenFinished(futureDone, (result) -> {
progressIndicator.setVisible(false);
progressIndicator.setVisible(false);
directory.getCryptor().swipeSensitiveData();
if (listener != null) {
listener.didInitialize(this);
}
});
} catch (FileAlreadyExistsException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
} catch (InvalidPathException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageLabel.setText(localization.getString("initialize.messageLabel.invalidPath"));
} catch (IOException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
LOG.error("I/O Exception", ex);
} finally {
swipePasswordFields();
usernameField.setText(null);
passwordField.swipe();
retypePasswordField.swipe();
IOUtils.closeQuietly(masterKeyOutputStream);
}
}
private void swipePasswordFields() {
passwordField.swipe();
retypePasswordField.swipe();
private void setControlsDisabled(boolean disable) {
usernameField.setDisable(disable);
passwordField.setDisable(disable);
retypePasswordField.setDisable(disable);
okButton.setDisable(disable);
}
private boolean isDirectoryEmpty() {
try {
final DirectoryStream<Path> dirContents = Files.newDirectoryStream(directory.getPath());
return !dirContents.iterator().hasNext();
} catch (IOException e) {
LOG.error("Failed to analyze directory.", e);
throw new IllegalStateException(e);
}
}
private boolean shouldEncryptExistingFiles() {
final Alert alert = new Alert(AlertType.CONFIRMATION);
alert.setTitle(localization.getString("initialize.alert.directoryIsNotEmpty.title"));
alert.setHeaderText(null);
alert.setContentText(localization.getString("initialize.alert.directoryIsNotEmpty.content"));
final Optional<ButtonType> result = alert.showAndWait();
return ButtonType.OK.equals(result.get());
}
private void encryptExistingContents() {
try {
final FileVisitor<Path> visitor = new EncryptingFileVisitor(directory.getPath(), directory.getCryptor(), this::shouldEncryptExistingFile);
Files.walkFileTree(directory.getPath(), visitor);
} catch (IOException ex) {
LOG.error("I/O Exception", ex);
}
}
private boolean shouldEncryptExistingFile(Path path) {
final String name = path.getFileName().toString();
return !directory.getPath().equals(path) && !name.endsWith(Aes256Cryptor.BASIC_FILE_EXT) && !name.endsWith(Aes256Cryptor.METADATA_FILE_EXT) && !name.endsWith(Aes256Cryptor.MASTERKEY_FILE_EXT);
}
/* Getter/Setter */
public Directory getDirectory() {
return directory;
}
public void setDirectory(Directory directory) {
this.directory = directory;
}
public InitializationListener getListener() {
return listener;
}
public void setListener(InitializationListener listener) {
this.listener = listener;
}
/* callback */
interface InitializationListener {
void didInitialize(InitializeController ctrl);
}
}

View File

@@ -10,50 +10,90 @@ package org.cryptomator.ui;
import java.io.IOException;
import java.util.ResourceBundle;
import java.util.Set;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.util.WebDavMounter;
import org.cryptomator.ui.util.WebDavMounter.CommandFailedException;
import org.cryptomator.webdav.WebDAVServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.cryptomator.ui.util.TrayIconUtil;
import org.eclipse.jetty.util.ConcurrentHashSet;
public class MainApplication extends Application {
private static final Logger LOG = LoggerFactory.getLogger(MainApplication.class);
private static final Set<Runnable> SHUTDOWN_TASKS = new ConcurrentHashSet<>();
private static final CleanShutdownPerformer CLEAN_SHUTDOWN_PERFORMER = new CleanShutdownPerformer();
public static void main(String[] args) {
launch(args);
Application.launch(args);
Runtime.getRuntime().addShutdownHook(CLEAN_SHUTDOWN_PERFORMER);
}
@Override
public void start(final Stage primaryStage) throws IOException {
final ResourceBundle localizations = ResourceBundle.getBundle("localization");
final Parent root = FXMLLoader.load(getClass().getResource("/main.fxml"), localizations);
chooseNativeStylesheet();
final ResourceBundle rb = ResourceBundle.getBundle("localization");
final FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/main.fxml"), rb);
final Parent root = loader.load();
final MainController ctrl = loader.getController();
ctrl.setStage(primaryStage);
final Scene scene = new Scene(root);
primaryStage.setTitle("Cryptomator");
primaryStage.setTitle(rb.getString("app.name"));
primaryStage.setScene(scene);
primaryStage.sizeToScene();
primaryStage.setResizable(false);
primaryStage.show();
TrayIconUtil.init(primaryStage, rb, () -> {
quit();
});
}
private void chooseNativeStylesheet() {
if (SystemUtils.IS_OS_MAC_OSX) {
setUserAgentStylesheet(getClass().getResource("/css/mac_theme.css").toString());
} else if (SystemUtils.IS_OS_LINUX) {
setUserAgentStylesheet(getClass().getResource("/css/linux_theme.css").toString());
} else if (SystemUtils.IS_OS_WINDOWS) {
setUserAgentStylesheet(getClass().getResource("/css/win_theme.css").toString());
}
}
private void quit() {
Platform.runLater(() -> {
CLEAN_SHUTDOWN_PERFORMER.run();
Settings.save();
Platform.exit();
System.exit(0);
});
}
@Override
public void stop() throws Exception {
try {
WebDavMounter.unmount(5);
} catch (CommandFailedException e) {
LOG.warn("Unmounting WebDAV share failed.", e);
}
WebDAVServer.getInstance().stop();
public void stop() {
CLEAN_SHUTDOWN_PERFORMER.run();
Settings.save();
super.stop();
}
public static void addShutdownTask(Runnable r) {
SHUTDOWN_TASKS.add(r);
}
public static void removeShutdownTask(Runnable r) {
SHUTDOWN_TASKS.remove(r);
}
private static class CleanShutdownPerformer extends Thread {
@Override
public void run() {
SHUTDOWN_TASKS.forEach(r -> {
r.run();
});
SHUTDOWN_TASKS.clear();
}
}
}

View File

@@ -8,48 +8,193 @@
******************************************************************************/
package org.cryptomator.ui;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Collection;
import java.util.ResourceBundle;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.ToggleGroup;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;
public class MainController {
import org.cryptomator.ui.InitializeController.InitializationListener;
import org.cryptomator.ui.UnlockController.UnlockListener;
import org.cryptomator.ui.UnlockedController.LockListener;
import org.cryptomator.ui.controls.DirectoryListCell;
import org.cryptomator.ui.model.Directory;
import org.cryptomator.ui.settings.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MainController implements Initializable, InitializationListener, UnlockListener, LockListener {
private static final Logger LOG = LoggerFactory.getLogger(MainController.class);
private Stage stage;
@FXML
private ToggleGroup toolbarButtonGroup;
private ContextMenu directoryContextMenu;
@FXML
private VBox rootVBox;
private HBox rootPane;
@FXML
private Pane initializePanel;
private ListView<Directory> directoryList;
@FXML
private Pane accessPanel;
private Pane contentPane;
@FXML
private Pane advancedPanel;
private ResourceBundle rb;
@FXML
protected void showInitializePane(ActionEvent event) {
showPanel(initializePanel);
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
final ObservableList<Directory> items = FXCollections.observableList(Settings.load().getDirectories());
directoryList.setItems(items);
directoryList.setCellFactory(this::createDirecoryListCell);
directoryList.getSelectionModel().getSelectedItems().addListener(this::selectedDirectoryDidChange);
}
@FXML
protected void showAccessPane(ActionEvent event) {
showPanel(accessPanel);
private void didClickAddDirectory(ActionEvent event) {
final DirectoryChooser dirChooser = new DirectoryChooser();
final File file = dirChooser.showDialog(stage);
if (file != null && file.canWrite()) {
final Directory dir = new Directory(file.toPath());
directoryList.getItems().add(dir);
directoryList.getSelectionModel().selectLast();
}
}
private ListCell<Directory> createDirecoryListCell(ListView<Directory> param) {
final DirectoryListCell cell = new DirectoryListCell();
cell.setContextMenu(directoryContextMenu);
return cell;
}
private void selectedDirectoryDidChange(ListChangeListener.Change<? extends Directory> change) {
final Directory selectedDir = directoryList.getSelectionModel().getSelectedItem();
if (selectedDir == null) {
stage.setTitle(rb.getString("app.name"));
showWelcomeView();
} else {
stage.setTitle(selectedDir.getName());
showDirectory(selectedDir);
}
}
@FXML
protected void showAdvancedPane(ActionEvent event) {
showPanel(advancedPanel);
private void didClickRemoveSelectedEntry(ActionEvent e) {
final Directory selectedDir = directoryList.getSelectionModel().getSelectedItem();
directoryList.getItems().remove(selectedDir);
directoryList.getSelectionModel().clearSelection();
}
private void showPanel(Pane panel) {
rootVBox.getChildren().remove(1);
rootVBox.getChildren().add(panel);
rootVBox.getScene().getWindow().sizeToScene();
// ****************************************
// Subcontroller for right panel
// ****************************************
private void showDirectory(Directory directory) {
try {
if (directory.isUnlocked()) {
this.showUnlockedView(directory);
} else if (directory.containsMasterKey()) {
this.showUnlockView(directory);
} else {
this.showInitializeView(directory);
}
} catch (IOException e) {
LOG.error("Failed to analyze directory.", e);
}
}
private <T> T showView(String fxml) {
try {
final FXMLLoader loader = new FXMLLoader(getClass().getResource(fxml), rb);
final Parent root = loader.load();
contentPane.getChildren().clear();
contentPane.getChildren().add(root);
return loader.getController();
} catch (IOException e) {
throw new IllegalStateException("Failed to load fxml file.", e);
}
}
private void showWelcomeView() {
this.showView("/fxml/welcome.fxml");
}
private void showInitializeView(Directory directory) {
final InitializeController ctrl = showView("/fxml/initialize.fxml");
ctrl.setDirectory(directory);
ctrl.setListener(this);
}
@Override
public void didInitialize(InitializeController ctrl) {
showUnlockView(ctrl.getDirectory());
}
private void showUnlockView(Directory directory) {
final UnlockController ctrl = showView("/fxml/unlock.fxml");
ctrl.setDirectory(directory);
ctrl.setListener(this);
}
@Override
public void didUnlock(UnlockController ctrl) {
showUnlockedView(ctrl.getDirectory());
Platform.setImplicitExit(false);
}
private void showUnlockedView(Directory directory) {
final UnlockedController ctrl = showView("/fxml/unlocked.fxml");
ctrl.setDirectory(directory);
ctrl.setListener(this);
}
@Override
public void didLock(UnlockedController ctrl) {
showUnlockView(ctrl.getDirectory());
if (getUnlockedDirectories().isEmpty()) {
Platform.setImplicitExit(true);
}
}
/* Convenience */
public Collection<Directory> getDirectories() {
return directoryList.getItems();
}
public Collection<Directory> getUnlockedDirectories() {
return getDirectories().stream().filter(d -> d.isUnlocked()).collect(Collectors.toSet());
}
/* public Getter/Setter */
public Stage getStage() {
return stage;
}
public void setStage(Stage stage) {
this.stage = stage;
}
}

View File

@@ -0,0 +1,189 @@
/*******************************************************************************
* 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;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ResourceBundle;
import java.util.concurrent.Future;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Directory;
import org.cryptomator.ui.util.FXThreads;
import org.cryptomator.ui.util.MasterKeyFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UnlockController implements Initializable {
private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class);
private ResourceBundle rb;
private UnlockListener listener;
private Directory directory;
@FXML
private ComboBox<String> usernameBox;
@FXML
private SecPasswordField passwordField;
@FXML
private Button unlockButton;
@FXML
private ProgressIndicator progressIndicator;
@FXML
private Label messageLabel;
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
usernameBox.valueProperty().addListener(this::didChooseUsername);
}
// ****************************************
// Username box
// ****************************************
public void didChooseUsername(ObservableValue<? extends String> property, String oldValue, String newValue) {
if (newValue != null) {
Platform.runLater(passwordField::requestFocus);
}
passwordField.setDisable(StringUtils.isEmpty(newValue));
}
// ****************************************
// Unlock button
// ****************************************
@FXML
private void didClickUnlockButton(ActionEvent event) {
setControlsDisabled(true);
final String masterKeyFileName = usernameBox.getValue() + Aes256Cryptor.MASTERKEY_FILE_EXT;
final Path masterKeyPath = directory.getPath().resolve(masterKeyFileName);
final CharSequence password = passwordField.getCharacters();
InputStream masterKeyInputStream = null;
try {
progressIndicator.setVisible(true);
masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ);
directory.getCryptor().decryptMasterKey(masterKeyInputStream, password);
if (!directory.startServer()) {
messageLabel.setText(rb.getString("unlock.messageLabel.startServerFailed"));
directory.getCryptor().swipeSensitiveData();
return;
}
directory.setUnlocked(true);
final Future<Boolean> futureMount = FXThreads.runOnBackgroundThread(directory::mount);
FXThreads.runOnMainThreadWhenFinished(futureMount, this::didUnlockAndMount);
FXThreads.runOnMainThreadWhenFinished(futureMount, (result) -> {
setControlsDisabled(false);
});
} catch (DecryptFailedException | IOException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageLabel.setText(rb.getString("unlock.errorMessage.decryptionFailed"));
LOG.error("Decryption failed for technical reasons.", ex);
} catch (WrongPasswordException e) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageLabel.setText(rb.getString("unlock.errorMessage.wrongPassword"));
passwordField.requestFocus();
} catch (UnsupportedKeyLengthException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageLabel.setText(rb.getString("unlock.errorMessage.unsupportedKeyLengthInstallJCE"));
LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex);
} finally {
passwordField.swipe();
IOUtils.closeQuietly(masterKeyInputStream);
}
}
private void setControlsDisabled(boolean disable) {
usernameBox.setDisable(disable);
passwordField.setDisable(disable);
unlockButton.setDisable(disable);
}
private void findExistingUsernames() {
try {
DirectoryStream<Path> ds = MasterKeyFilter.filteredDirectory(directory.getPath());
final String masterKeyExt = Aes256Cryptor.MASTERKEY_FILE_EXT.toLowerCase();
usernameBox.getItems().clear();
for (final Path path : ds) {
final String fileName = path.getFileName().toString();
final int beginOfExt = fileName.toLowerCase().lastIndexOf(masterKeyExt);
final String baseName = fileName.substring(0, beginOfExt);
usernameBox.getItems().add(baseName);
}
if (usernameBox.getItems().size() == 1) {
usernameBox.getSelectionModel().selectFirst();
}
} catch (IOException e) {
LOG.trace("Invalid path: " + directory.getPath(), e);
}
}
private void didUnlockAndMount(boolean mountSuccess) {
progressIndicator.setVisible(false);
if (listener != null) {
listener.didUnlock(this);
}
}
/* Getter/Setter */
public Directory getDirectory() {
return directory;
}
public void setDirectory(Directory directory) {
this.directory = directory;
this.findExistingUsernames();
}
public UnlockListener getListener() {
return listener;
}
public void setListener(UnlockListener listener) {
this.listener = listener;
}
/* callback */
interface UnlockListener {
void didUnlock(UnlockController ctrl);
}
}

View File

@@ -0,0 +1,150 @@
/*******************************************************************************
* 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;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Label;
import javafx.util.Duration;
import org.cryptomator.crypto.CryptorIOSampling;
import org.cryptomator.ui.model.Directory;
public class UnlockedController implements Initializable {
private static final int IO_SAMPLING_STEPS = 100;
private static final double IO_SAMPLING_INTERVAL = 0.25;
private ResourceBundle rb;
private LockListener listener;
private Directory directory;
private Timeline ioAnimation;
@FXML
private Label messageLabel;
@FXML
private LineChart<Number, Number> ioGraph;
@FXML
private NumberAxis xAxis;
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
}
@FXML
private void didClickCloseVault(ActionEvent event) {
directory.unmount();
directory.stopServer();
directory.setUnlocked(false);
if (listener != null) {
listener.didLock(this);
}
}
// ****************************************
// IO Graph
// ****************************************
private void startIoSampling(final CryptorIOSampling sampler) {
final Series<Number, Number> decryptedBytes = new Series<>();
decryptedBytes.setName("decrypted");
final Series<Number, Number> encryptedBytes = new Series<>();
encryptedBytes.setName("encrypted");
ioGraph.getData().add(decryptedBytes);
ioGraph.getData().add(encryptedBytes);
ioAnimation = new Timeline();
ioAnimation.getKeyFrames().add(new KeyFrame(Duration.seconds(IO_SAMPLING_INTERVAL), new IoSamplingAnimationHandler(sampler, decryptedBytes, encryptedBytes)));
ioAnimation.setCycleCount(Animation.INDEFINITE);
ioAnimation.play();
}
private class IoSamplingAnimationHandler implements EventHandler<ActionEvent> {
private static final double BYTES_TO_MEGABYTES_FACTOR = 1.0 / IO_SAMPLING_INTERVAL / 1024.0 / 1024.0;
private final CryptorIOSampling sampler;
private final Series<Number, Number> decryptedBytes;
private final Series<Number, Number> encryptedBytes;
private int step = 0;
public IoSamplingAnimationHandler(CryptorIOSampling sampler, Series<Number, Number> decryptedBytes, Series<Number, Number> encryptedBytes) {
this.sampler = sampler;
this.decryptedBytes = decryptedBytes;
this.encryptedBytes = encryptedBytes;
}
@Override
public void handle(ActionEvent event) {
step++;
final double decryptedMb = sampler.pollDecryptedBytes(true) * BYTES_TO_MEGABYTES_FACTOR;
decryptedBytes.getData().add(new Data<Number, Number>(step, decryptedMb));
if (decryptedBytes.getData().size() > IO_SAMPLING_STEPS) {
decryptedBytes.getData().remove(0);
}
final double encrypteddMb = sampler.pollEncryptedBytes(true) * BYTES_TO_MEGABYTES_FACTOR;
encryptedBytes.getData().add(new Data<Number, Number>(step, encrypteddMb));
if (encryptedBytes.getData().size() > IO_SAMPLING_STEPS) {
encryptedBytes.getData().remove(0);
}
xAxis.setLowerBound(step - IO_SAMPLING_STEPS);
xAxis.setUpperBound(step);
}
}
/* Getter/Setter */
public Directory getDirectory() {
return directory;
}
public void setDirectory(Directory directory) {
this.directory = directory;
final String msg = String.format(rb.getString("unlocked.messageLabel.runningOnPort"), directory.getServer().getPort());
messageLabel.setText(msg);
if (directory.getCryptor() instanceof CryptorIOSampling) {
startIoSampling((CryptorIOSampling) directory.getCryptor());
} else {
ioGraph.setVisible(false);
}
}
public LockListener getListener() {
return listener;
}
public void setListener(LockListener listener) {
this.listener = listener;
}
/* callback */
interface LockListener {
void didLock(UnlockedController ctrl);
}
}

View File

@@ -0,0 +1,64 @@
package org.cryptomator.ui.controls;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.ListCell;
import javafx.scene.control.Tooltip;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Circle;
import org.cryptomator.ui.model.Directory;
public class DirectoryListCell extends ListCell<Directory> implements ChangeListener<Boolean> {
// fill: #FD4943, stroke: #E1443F
private static final Color RED_FILL = Color.rgb(253, 73, 67);
private static final Color RED_STROKE = Color.rgb(225, 68, 63);
// fill: #28CA40, stroke: #30B740
private static final Color GREEN_FILL = Color.rgb(40, 202, 64);
private static final Color GREEN_STROKE = Color.rgb(48, 183, 64);
private final Circle statusIndicator = new Circle(4.5);
public DirectoryListCell() {
setGraphic(statusIndicator);
setGraphicTextGap(12.0);
setContentDisplay(ContentDisplay.LEFT);
}
@Override
protected void updateItem(Directory item, boolean empty) {
final Directory oldItem = super.getItem();
if (oldItem != null) {
oldItem.unlockedProperty().removeListener(this);
}
super.updateItem(item, empty);
if (item == null) {
setText(null);
setTooltip(null);
statusIndicator.setVisible(false);
} else {
setText(item.getName());
setTooltip(new Tooltip(item.getPath().toString()));
statusIndicator.setVisible(true);
item.unlockedProperty().addListener(this);
updateStatusIndicator();
}
}
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
updateStatusIndicator();
}
private void updateStatusIndicator() {
final Paint fillColor = getItem().isUnlocked() ? GREEN_FILL : RED_FILL;
final Paint strokeColor = getItem().isUnlocked() ? GREEN_STROKE : RED_STROKE;
statusIndicator.setFill(fillColor);
statusIndicator.setStroke(strokeColor);
}
}

View File

@@ -0,0 +1,154 @@
package org.cryptomator.ui.model;
import java.io.IOException;
import java.io.Serializable;
import java.nio.file.Files;
import java.nio.file.Path;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.SamplingDecorator;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.ui.MainApplication;
import org.cryptomator.ui.util.MasterKeyFilter;
import org.cryptomator.ui.util.mount.CommandFailedException;
import org.cryptomator.ui.util.mount.WebDavMount;
import org.cryptomator.ui.util.mount.WebDavMounter;
import org.cryptomator.webdav.WebDavServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
@JsonSerialize(using = DirectorySerializer.class)
@JsonDeserialize(using = DirectoryDeserializer.class)
public class Directory implements Serializable {
private static final long serialVersionUID = 3754487289683599469L;
private static final Logger LOG = LoggerFactory.getLogger(Directory.class);
private final WebDavServer server = new WebDavServer();
private final Cryptor cryptor = SamplingDecorator.decorate(new Aes256Cryptor());
private final ObjectProperty<Boolean> unlocked = new SimpleObjectProperty<Boolean>(this, "unlocked", Boolean.FALSE);
private final Path path;
// private boolean unlocked;
private WebDavMount webDavMount;
private final Runnable shutdownTask = new ShutdownTask();
public Directory(final Path path) {
if (!Files.isDirectory(path)) {
throw new IllegalArgumentException("Not a directory: " + path);
}
this.path = path;
}
public boolean containsMasterKey() throws IOException {
return MasterKeyFilter.filteredDirectory(path).iterator().hasNext();
}
public synchronized boolean startServer() {
if (server.start(path.toString(), cryptor)) {
MainApplication.addShutdownTask(shutdownTask);
return true;
} else {
return false;
}
}
public synchronized void stopServer() {
if (server.isRunning()) {
MainApplication.removeShutdownTask(shutdownTask);
this.unmount();
server.stop();
cryptor.swipeSensitiveData();
}
}
public boolean mount() {
try {
webDavMount = WebDavMounter.mount(server.getPort());
return true;
} catch (CommandFailedException e) {
LOG.warn("mount failed", e);
return false;
}
}
public boolean unmount() {
try {
if (webDavMount != null) {
webDavMount.unmount();
webDavMount = null;
}
return true;
} catch (CommandFailedException e) {
LOG.warn("unmount failed", e);
return false;
}
}
/* Getter/Setter */
public Path getPath() {
return path;
}
/**
* @return Directory name without preceeding path components
*/
public String getName() {
return path.getFileName().toString();
}
public Cryptor getCryptor() {
return cryptor;
}
public ObjectProperty<Boolean> unlockedProperty() {
return unlocked;
}
public boolean isUnlocked() {
return unlocked.get();
}
public void setUnlocked(boolean unlocked) {
this.unlocked.set(unlocked);
}
public WebDavServer getServer() {
return server;
}
/* hashcode/equals */
@Override
public int hashCode() {
return path.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Directory) {
final Directory other = (Directory) obj;
return this.path.equals(other.path);
} else {
return false;
}
}
/* graceful shutdown */
private class ShutdownTask implements Runnable {
@Override
public void run() {
stopServer();
}
}
}

View File

@@ -0,0 +1,23 @@
package org.cryptomator.ui.model;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
public class DirectoryDeserializer extends JsonDeserializer<Directory> {
@Override
public Directory deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
final JsonNode node = jp.readValueAsTree();
final String pathStr = node.get("path").asText();
final Path path = FileSystems.getDefault().getPath(pathStr);
return new Directory(path);
}
}

View File

@@ -0,0 +1,19 @@
package org.cryptomator.ui.model;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
public class DirectorySerializer extends JsonSerializer<Directory> {
@Override
public void serialize(Directory value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
jgen.writeStartObject();
jgen.writeStringField("path", value.getPath().toString());
jgen.writeEndObject();
}
}

View File

@@ -17,8 +17,11 @@ import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.model.Directory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -40,20 +43,19 @@ public class Settings implements Serializable {
final FileSystem fs = FileSystems.getDefault();
if (SystemUtils.IS_OS_WINDOWS && appdata != null) {
SETTINGS_DIR = fs.getPath(appdata, "opencloudencryptor");
SETTINGS_DIR = fs.getPath(appdata, "Cryptomator");
} else if (SystemUtils.IS_OS_WINDOWS && appdata == null) {
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".opencloudencryptor");
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".Cryptomator");
} else if (SystemUtils.IS_OS_MAC_OSX) {
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, "Library/Application Support/opencloudencryptor");
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, "Library/Application Support/Cryptomator");
} else {
// (os.contains("solaris") || os.contains("sunos") || os.contains("linux") || os.contains("unix"))
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".opencloudencryptor");
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".Cryptomator");
}
}
private String webdavWorkDir;
private List<Directory> directories;
private String username;
private int port;
private Settings() {
// private constructor
@@ -89,19 +91,20 @@ public class Settings implements Serializable {
}
private static Settings defaultSettings() {
final Settings result = new Settings();
result.setWebdavWorkDir(System.getProperty("user.home", "."));
return result;
return new Settings();
}
/* Getter/Setter */
public String getWebdavWorkDir() {
return webdavWorkDir;
public List<Directory> getDirectories() {
if (directories == null) {
directories = new ArrayList<>();
}
return directories;
}
public void setWebdavWorkDir(String webdavWorkDir) {
this.webdavWorkDir = webdavWorkDir;
public void setDirectories(List<Directory> directories) {
this.directories = directories;
}
public String getUsername() {
@@ -112,14 +115,4 @@ public class Settings implements Serializable {
this.username = username;
}
@Deprecated
public int getPort() {
return port;
}
@Deprecated
public void setPort(int port) {
this.port = port;
}
}

View File

@@ -0,0 +1,175 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* https://github.com/totalvoidness/FXThreads
*
* Contributors:
* Sebastian Stenzel
******************************************************************************/
package org.cryptomator.ui.util;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javafx.application.Platform;
/**
* Use this utility class to spawn background tasks and wait for them to finish. <br/>
* <br/>
* <strong>Example use (ignoring exceptions):</strong>
*
* <pre>
* // get some string from a remote server:
* Future&lt;String&gt; futureBookName = runOnBackgroundThread(restResource::getBookName);
*
* // when done, update text label:
* runOnMainThreadWhenFinished(futureBookName, (bookName) -&gt; {
* myLabel.setText(bookName);
* });
* </pre>
*
* <strong>Example use (exception-aware):</strong>
*
* <pre>
* // get some string from a remote server:
* Future&lt;String&gt; futureBookName = runOnBackgroundThread(restResource::getBookName);
*
* // when done, update text label:
* runOnMainThreadWhenFinished(futureBookName, (bookName) -&gt; {
* myLabel.setText(bookName);
* }, (exception) -&gt; {
* myLabel.setText(&quot;An exception occured: &quot; + exception.getMessage());
* });
* </pre>
*/
public final class FXThreads {
private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool();
private static final CallbackWhenTaskFailed DUMMY_EXCEPTION_CALLBACK = (e) -> {
// ignore.
};
private FXThreads() {
throw new AssertionError("Not instantiable.");
}
/**
* Executes the given task on a background thread. If you want to react on the result on your JavaFX main thread, use
* {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
*
* <pre>
* // examples:
*
* Future&lt;String&gt; futureBookName1 = runOnBackgroundThread(restResource::getBookName);
*
* Future&lt;String&gt; futureBookName2 = runOnBackgroundThread(() -&gt; {
* return restResource.getBookName();
* });
* </pre>
*
* @param task The task to be executed on a background thread.
* @return A future result object, which you can use in {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
*/
public static <T> Future<T> runOnBackgroundThread(Callable<T> task) {
return EXECUTOR.submit(task);
}
/**
* Executes the given task on a background thread. If you want to react on the result on your JavaFX main thread, use
* {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
*
* <pre>
* // examples:
*
* Future&lt;?&gt; futureDone1 = runOnBackgroundThread(this::doSomeComplexCalculation);
*
* Future&lt;?&gt; futureDone2 = runOnBackgroundThread(() -&gt; {
* doSomeComplexCalculation();
* });
* </pre>
*
* @param task The task to be executed on a background thread.
* @return A future result object, which you can use in {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
*/
public static Future<?> runOnBackgroundThread(Runnable task) {
return EXECUTOR.submit(task);
}
/**
* Waits for the given task to complete and notifies the given successCallback. If an exception occurs, the callback will never be
* called. If you are interested in the exception, use
* {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
*
* <pre>
* // example:
*
* runOnMainThreadWhenFinished(futureBookName, (bookName) -&gt; {
* myLabel.setText(bookName);
* });
* </pre>
*
* @param task The task to wait for.
* @param successCallback The action to perform, when the task finished.
*/
public static <T> void runOnMainThreadWhenFinished(Future<T> task, CallbackWhenTaskFinished<T> successCallback) {
runOnBackgroundThread(() -> {
return "asd";
});
FXThreads.runOnMainThreadWhenFinished(task, successCallback, DUMMY_EXCEPTION_CALLBACK);
}
/**
* Waits for the given task to complete and notifies the given successCallback. If an exception occurs, the callback will never be
* called. If you are interested in the exception, use
* {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
*
* <pre>
* // example:
*
* runOnMainThreadWhenFinished(futureBookNamePossiblyFailing, (bookName) -&gt; {
* myLabel.setText(bookName);
* }, (exception) -&gt; {
* myLabel.setText(&quot;An exception occured: &quot; + exception.getMessage());
* });
* </pre>
*
* @param task The task to wait for.
* @param successCallback The action to perform, when the task finished.
*/
public static <T> void runOnMainThreadWhenFinished(Future<T> task, CallbackWhenTaskFinished<T> successCallback, CallbackWhenTaskFailed exceptionCallback) {
assertParamNotNull(task, "task must not be null.");
assertParamNotNull(successCallback, "successCallback must not be null.");
assertParamNotNull(exceptionCallback, "exceptionCallback must not be null.");
EXECUTOR.execute(() -> {
try {
final T result = task.get();
Platform.runLater(() -> {
successCallback.taskFinished(result);
});
} catch (Exception e) {
Platform.runLater(() -> {
exceptionCallback.taskFailed(e);
});
}
});
}
private static void assertParamNotNull(Object param, String msg) {
if (param == null) {
throw new IllegalArgumentException(msg);
}
}
public interface CallbackWhenTaskFinished<T> {
void taskFinished(T result);
}
public interface CallbackWhenTaskFailed {
void taskFailed(Throwable t);
}
}

View File

@@ -0,0 +1,119 @@
package org.cryptomator.ui.util;
import java.awt.AWTException;
import java.awt.Image;
import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.SystemTray;
import java.awt.Toolkit;
import java.awt.TrayIcon;
import java.awt.TrayIcon.MessageType;
import java.awt.event.ActionEvent;
import java.io.IOException;
import java.util.ResourceBundle;
import javafx.application.Platform;
import javafx.stage.Stage;
import javax.swing.SwingUtilities;
import org.apache.commons.lang3.SystemUtils;
public final class TrayIconUtil {
private static TrayIconUtil INSTANCE;
private final Stage mainApplicationWindow;
private final ResourceBundle rb;
private final Runnable exitCommand;
/**
* This will add an icon to the system tray and modify the application shutdown procedure. Depending on
* {@link Platform#isImplicitExit()} the application may still be running, allowing shutdown using the tray menu.
*/
public synchronized static void init(Stage mainApplicationWindow, ResourceBundle rb, Runnable exitCommand) {
if (INSTANCE == null && SystemTray.isSupported()) {
INSTANCE = new TrayIconUtil(mainApplicationWindow, rb, exitCommand);
}
}
private TrayIconUtil(Stage mainApplicationWindow, ResourceBundle rb, Runnable exitCommand) {
this.mainApplicationWindow = mainApplicationWindow;
this.rb = rb;
this.exitCommand = exitCommand;
initTrayIcon();
}
private void initTrayIcon() {
final TrayIcon trayIcon = createTrayIcon();
try {
SystemTray.getSystemTray().add(trayIcon);
mainApplicationWindow.setOnCloseRequest((e) -> {
if (Platform.isImplicitExit()) {
exitCommand.run();
} else {
mainApplicationWindow.close();
this.showTrayNotification(trayIcon);
}
});
} catch (SecurityException | AWTException ex) {
// not working? then just go ahead and close the app
mainApplicationWindow.setOnCloseRequest((ev) -> {
exitCommand.run();
});
}
}
private TrayIcon createTrayIcon() {
final PopupMenu popup = new PopupMenu();
final MenuItem showItem = new MenuItem(rb.getString("tray.menu.open"));
showItem.addActionListener(this::restoreFromTray);
popup.add(showItem);
final MenuItem exitItem = new MenuItem(rb.getString("tray.menu.quit"));
exitItem.addActionListener(this::quitFromTray);
popup.add(exitItem);
final Image image = Toolkit.getDefaultToolkit().getImage(TrayIconUtil.class.getResource("/tray_icon.png"));
return new TrayIcon(image, rb.getString("app.name"), popup);
}
private void showTrayNotification(TrayIcon trayIcon) {
final Runnable notificationCmd;
if (SystemUtils.IS_OS_MAC_OSX) {
final String title = rb.getString("tray.infoMsg.title");
final String msg = rb.getString("tray.infoMsg.msg.osx");
final String notificationCenterAppleScript = String.format("display notification \"%s\" with title \"%s\"", msg, title);
notificationCmd = () -> {
try {
Runtime.getRuntime().exec(new String[] {"/usr/bin/osascript", "-e", notificationCenterAppleScript});
} catch (IOException e) {
// ignore, user will notice the tray icon anyway.
}
};
} else {
final String title = rb.getString("tray.infoMsg.title");
final String msg = rb.getString("tray.infoMsg.msg");
notificationCmd = () -> {
trayIcon.displayMessage(title, msg, MessageType.INFO);
};
}
SwingUtilities.invokeLater(() -> {
notificationCmd.run();
});
}
private void restoreFromTray(ActionEvent event) {
Platform.runLater(() -> {
mainApplicationWindow.show();
mainApplicationWindow.requestFocus();
});
}
private void quitFromTray(ActionEvent event) {
exitCommand.run();
}
}

View File

@@ -1,72 +0,0 @@
/*******************************************************************************
* 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.util;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.SystemUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class WebDavMounter {
private static final Logger LOG = LoggerFactory.getLogger(WebDavMounter.class);
private static final int CMD_DEFAULT_TIMEOUT = 1;
private WebDavMounter() {
throw new IllegalStateException("not instantiable.");
}
public static void mount(int localPort) throws CommandFailedException {
if (SystemUtils.IS_OS_MAC_OSX) {
exec("mkdir /Volumes/Cryptomator", CMD_DEFAULT_TIMEOUT);
exec("mount_webdav -S -v Cryptomator localhost:" + localPort + " /Volumes/Cryptomator", CMD_DEFAULT_TIMEOUT);
exec("open /Volumes/Cryptomator", CMD_DEFAULT_TIMEOUT);
}
}
public static void unmount(int timeout) throws CommandFailedException {
if (SystemUtils.IS_OS_MAC_OSX) {
exec("umount /Volumes/Cryptomator", timeout);
}
}
private static void exec(String cmd, int timoutSeconds) throws CommandFailedException {
try {
final Process proc = Runtime.getRuntime().exec(new String[] {"/bin/sh", "-c", cmd});
if (proc.waitFor(timoutSeconds, TimeUnit.SECONDS)) {
proc.destroy();
}
if (proc.exitValue() != 0) {
throw new CommandFailedException(IOUtils.toString(proc.getErrorStream()));
}
} catch (IOException | InterruptedException | IllegalThreadStateException e) {
LOG.error("Command execution failed.", e);
throw new CommandFailedException(e);
}
}
public static class CommandFailedException extends Exception {
private static final long serialVersionUID = 5784853630182321479L;
private CommandFailedException(String message) {
super(message);
}
private CommandFailedException(Throwable cause) {
super(cause);
}
}
}

View File

@@ -0,0 +1,98 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch
* Sebastian Stenzel - using Futures, lazy loading for out/err.
******************************************************************************/
package org.cryptomator.ui.util.command;
import static java.lang.String.format;
import java.io.IOException;
import org.apache.commons.io.IOUtils;
import org.cryptomator.ui.util.mount.CommandFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class CommandResult {
private static final Logger LOG = LoggerFactory.getLogger(CommandResult.class);
private final Process process;
private final String stdout;
private final String stderr;
private final CommandFailedException exception;
/**
* Constructs a CommandResult from a terminated process and closes all its streams.
* @param process An <strong>already finished</strong> process.
*/
CommandResult(Process process) {
String out = null;
String err = null;
CommandFailedException ex = null;
try {
out = IOUtils.toString(process.getInputStream());
err = IOUtils.toString(process.getErrorStream());
} catch (IOException e) {
ex = new CommandFailedException(e);
} finally {
this.process = process;
this.stdout = out;
this.stderr = err;
this.exception = ex;
IOUtils.closeQuietly(process.getInputStream());
IOUtils.closeQuietly(process.getOutputStream());
IOUtils.closeQuietly(process.getErrorStream());
logDebugInfo();
}
}
/**
* @return Data written to STDOUT
*/
public String getStdOut() throws CommandFailedException {
assertNoException();
return stdout;
}
/**
* @return Data written to STDERR
*/
public String getStdErr() throws CommandFailedException {
assertNoException();
return stderr;
}
/**
* @return Exit value of the process
*/
public int getExitValue() {
return process.exitValue();
}
private void logDebugInfo() {
if (LOG.isDebugEnabled()) {
LOG.debug("Command execution finished. Exit code: {}\n" + "Output:\n" + "{}\n" + "Error:\n" + "{}\n", process.exitValue(), stdout, stderr);
}
}
void assertOk() throws CommandFailedException {
assertNoException();
int exitValue = getExitValue();
if (exitValue != 0) {
throw new CommandFailedException(format("Command execution failed. Exit code: %d\n" + "# Output:\n" + "%s\n" + "# Error:\n" + "%s", exitValue, stdout, stderr));
}
}
private void assertNoException() throws CommandFailedException {
if (exception != null) {
throw exception;
}
}
}

View File

@@ -0,0 +1,100 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch
* Sebastian Stenzel - Refactoring
******************************************************************************/
package org.cryptomator.ui.util.command;
import static java.lang.String.format;
import static org.apache.commons.lang3.SystemUtils.IS_OS_UNIX;
import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.mount.CommandFailedException;
/**
* <p>
* Runs commands using a system compatible CLI.
* <p>
* To detect the system type {@link SystemUtils} is used. The following CLIs are used by default:
* <ul>
* <li><i>{@link #WINDOWS_DEFAULT_CLI}</i> if {@link SystemUtils#IS_OS_WINDOWS}
* <li><i>{@link #UNIX_DEFAULT_CLI}</i> if {@link SystemUtils#IS_OS_UNIX}
* </ul>
* <p>
* If the path to the executables differs from the default or the system can not be detected the Java system property
* {@value #CLI_EXECUTABLE_PROPERTY} can be set to define it.
* <p>
* If a CLI executable can not be determined using these methods operation of {@link CommandRunner} will fail with
* {@link IllegalStateException}s.
*
* @author Markus Kreusch
*/
final class CommandRunner {
public static final String CLI_EXECUTABLE_PROPERTY = "cryptomator.cli";
public static final String WINDOWS_DEFAULT_CLI[] = {"cmd", "/C"};
public static final String UNIX_DEFAULT_CLI[] = {"/bin/sh", "-c"};
private static final Executor CMD_EXECUTOR = Executors.newCachedThreadPool();
/**
* Executes all lines in the given script in the specified order. Stops as soon as the first command fails.
*
* @param script Script containing command lines and environment variables.
* @return Result of the last command, if it exited successfully.
* @throws CommandFailedException If one of the command lines in the given script fails.
*/
static CommandResult execute(Script script, long timeout, TimeUnit unit) throws CommandFailedException {
try {
final List<String> env = script.environment().entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.toList());
CommandResult result = null;
for (final String line : script.getLines()) {
final String[] cmds = ArrayUtils.add(determineCli(), line);
final Process proc = Runtime.getRuntime().exec(cmds, env.toArray(new String[0]));
result = run(proc, timeout, unit);
result.assertOk();
}
return result;
} catch (IOException e) {
throw new CommandFailedException(e);
}
}
private static CommandResult run(Process process, long timeout, TimeUnit unit) throws CommandFailedException {
try {
final FutureCommandResult futureCommandResult = new FutureCommandResult(process);
CMD_EXECUTOR.execute(futureCommandResult);
return futureCommandResult.get(timeout, unit);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new CommandFailedException("Waiting time elapsed before command execution finished");
}
}
private static String[] determineCli() {
final String cliFromProperty = System.getProperty(CLI_EXECUTABLE_PROPERTY);
if (cliFromProperty != null) {
return cliFromProperty.split("");
} else if (IS_OS_WINDOWS) {
return WINDOWS_DEFAULT_CLI;
} else if (IS_OS_UNIX) {
return UNIX_DEFAULT_CLI;
} else {
throw new IllegalStateException(format("Failed to determine cli to use. Set Java system property %s to the executable.", CLI_EXECUTABLE_PROPERTY));
}
}
}

View File

@@ -0,0 +1,111 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel
******************************************************************************/
package org.cryptomator.ui.util.command;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.cryptomator.ui.util.mount.CommandFailedException;
final class FutureCommandResult implements Future<CommandResult>, Runnable {
private final Process process;
private final AtomicBoolean canceled = new AtomicBoolean();
private final AtomicBoolean done = new AtomicBoolean();
private final Lock lock = new ReentrantLock();
private final Condition doneCondition = lock.newCondition();
private CommandFailedException exception;
FutureCommandResult(Process process) {
this.process = process;
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
if (done.get()) {
return false;
} else if (canceled.compareAndSet(false, true)) {
if (mayInterruptIfRunning) {
process.destroyForcibly();
}
}
return true;
}
@Override
public boolean isCancelled() {
return canceled.get();
}
private void setDone() {
lock.lock();
try {
done.set(true);
doneCondition.signalAll();
} finally {
lock.unlock();
}
}
@Override
public boolean isDone() {
return done.get();
}
@Override
public CommandResult get() throws InterruptedException, ExecutionException {
lock.lock();
try {
while(!done.get()) {
doneCondition.await();
}
} finally {
lock.unlock();
}
if (exception != null) {
throw new ExecutionException(exception);
}
return new CommandResult(process);
}
@Override
public CommandResult get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
lock.lock();
try {
while(!done.get()) {
doneCondition.await(timeout, unit);
}
} finally {
lock.unlock();
}
if (exception != null) {
throw new ExecutionException(exception);
}
return new CommandResult(process);
}
@Override
public void run() {
try {
process.waitFor();
} catch (InterruptedException e) {
exception = new CommandFailedException(e);
} finally {
setDone();
}
}
}

View File

@@ -0,0 +1,65 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch
******************************************************************************/
package org.cryptomator.ui.util.command;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.cryptomator.ui.util.mount.CommandFailedException;
public final class Script {
private static final int DEFAULT_TIMEOUT_MILLISECONDS = 3000;
public static Script fromLines(String... commands) {
return new Script(commands);
}
private final String[] lines;
private final Map<String, String> environment = new HashMap<>();
private Script(String[] lines) {
this.lines = lines;
setEnv(System.getenv());
}
public String[] getLines() {
return lines;
}
public CommandResult execute() throws CommandFailedException {
return CommandRunner.execute(this, DEFAULT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
}
public CommandResult execute(long timeout, TimeUnit unit) throws CommandFailedException {
return CommandRunner.execute(this, timeout, unit);
}
Map<String, String> environment() {
return environment;
}
public Script setEnv(Map<String, String> environment) {
this.environment.clear();
addEnv(environment);
return this;
}
public Script addEnv(Map<String, String> environment) {
this.environment.putAll(environment);
return this;
}
public Script addEnv(String name, String value) {
environment.put(name, value);
return this;
}
}

View File

@@ -0,0 +1,24 @@
/*******************************************************************************
* 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
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
******************************************************************************/
package org.cryptomator.ui.util.mount;
public class CommandFailedException extends Exception {
private static final long serialVersionUID = 5784853630182321479L;
public CommandFailedException(String message) {
super(message);
}
public CommandFailedException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,43 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
******************************************************************************/
package org.cryptomator.ui.util.mount;
/**
* A WebDavMounter acting as fallback if no other mounter works.
*
* @author Markus Kreusch
*/
final class FallbackWebDavMounter implements WebDavMounterStrategy {
@Override
public boolean shouldWork() {
return true;
}
@Override
public WebDavMount mount(int localPort) {
displayMountInstructions();
return new WebDavMount() {
@Override
public void unmount() {
displayUnmountInstructions();
}
};
}
private void displayMountInstructions() {
// TODO display message to user pointing to cryptomator.org/mounting#mount which describes what to do
// Machine-readable mount instructions: http://tools.ietf.org/html/rfc4709#page-5 :-)
}
private void displayUnmountInstructions() {
// TODO display message to user pointing to cryptomator.org/mounting#unmount which describes what to do
}
}

View File

@@ -0,0 +1,52 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel, Markus Kreusch
* 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
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
******************************************************************************/
package org.cryptomator.ui.util.mount;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.command.Script;
final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy {
@Override
public boolean shouldWork() {
if (SystemUtils.IS_OS_LINUX) {
final Script checkScripts = Script.fromLines("which gvfs-mount xdg-open");
try {
checkScripts.execute();
return true;
} catch (CommandFailedException e) {
return false;
}
} else {
return false;
}
}
@Override
public WebDavMount mount(int localPort) throws CommandFailedException {
final Script mountScript = Script.fromLines(
"set -x",
"gvfs-mount \"dav://[::1]:$PORT\"",
"xdg-open \"$URI\"")
.addEnv("PORT", String.valueOf(localPort));
final Script unmountScript = Script.fromLines(
"set -x",
"gvfs-mount -u \"dav://[::1]:$PORT\"")
.addEnv("URI", String.valueOf(localPort));
mountScript.execute();
return new WebDavMount() {
@Override
public void unmount() throws CommandFailedException {
unmountScript.execute();
}
};
}
}

View File

@@ -0,0 +1,43 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel, Markus Kreusch
* 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, strategy fine tuning
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
******************************************************************************/
package org.cryptomator.ui.util.mount;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.command.Script;
final class MacOsXWebDavMounter implements WebDavMounterStrategy {
@Override
public boolean shouldWork() {
return SystemUtils.IS_OS_MAC_OSX;
}
@Override
public WebDavMount mount(int localPort) throws CommandFailedException {
final String path = "/Volumes/Cryptomator" + localPort;
final Script mountScript = Script.fromLines(
"mkdir \"$MOUNT_PATH\"",
"mount_webdav -S -v Cryptomator \"[::1]:$PORT\" \"$MOUNT_PATH\"",
"open \"$MOUNT_PATH\"")
.addEnv("PORT", String.valueOf(localPort))
.addEnv("MOUNT_PATH", path);
final Script unmountScript = Script.fromLines(
"umount $MOUNT_PATH")
.addEnv("MOUNT_PATH", path);
mountScript.execute();
return new WebDavMount() {
@Override
public void unmount() throws CommandFailedException {
unmountScript.execute();
}
};
}
}

View File

@@ -0,0 +1,26 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
******************************************************************************/
package org.cryptomator.ui.util.mount;
/**
* A mounted webdav share.
*
* @author Markus Kreusch
*/
public interface WebDavMount {
/**
* Unmounts this {@code WebDavMount}.
*
* @throws CommandFailedException if the unmount operation fails
*/
void unmount() throws CommandFailedException;
}

View File

@@ -0,0 +1,55 @@
/*******************************************************************************
* 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
* Markus Kreusch - Refactored to use strategy pattern
******************************************************************************/
package org.cryptomator.ui.util.mount;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class WebDavMounter {
private static final Logger LOG = LoggerFactory.getLogger(WebDavMounter.class);
private static final WebDavMounterStrategy[] STRATEGIES = {new WindowsWebDavMounter(), new MacOsXWebDavMounter(), new LinuxGvfsWebDavMounter()};
private static volatile WebDavMounterStrategy choosenStrategy;
/**
* Tries to mount a given webdav share.
*
* @param localPort local TCP port of the webdav share
* @return a {@link WebDavMount} representing the mounted share
* @throws CommandFailedException if the mount operation fails
*/
public static WebDavMount mount(int localPort) throws CommandFailedException {
return chooseStrategy().mount(localPort);
}
private static WebDavMounterStrategy chooseStrategy() {
if (choosenStrategy == null) {
choosenStrategy = getStrategyWhichShouldWork();
}
return choosenStrategy;
}
private static WebDavMounterStrategy getStrategyWhichShouldWork() {
for (WebDavMounterStrategy strategy : STRATEGIES) {
if (strategy.shouldWork()) {
LOG.info("Using {}", strategy.getClass().getSimpleName());
return strategy;
}
}
return new FallbackWebDavMounter();
}
private WebDavMounter() {
throw new IllegalStateException("Class is not instantiable.");
}
}

View File

@@ -0,0 +1,34 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
* Sebastian Stenzel - minor strategy fine tuning
******************************************************************************/
package org.cryptomator.ui.util.mount;
/**
* A strategy able to mount a webdav share and display it to the user.
*
* @author Markus Kreusch
*/
interface WebDavMounterStrategy {
/**
* @return {@code false} if this {@code WebDavMounterStrategy} can not work on the local machine, {@code true} if it could work
*/
boolean shouldWork();
/**
* Tries to mount a given webdav share.
*
* @param localPort local TCP port of the webdav share
* @return a {@link WebDavMount} representing the mounted share
* @throws CommandFailedException if the mount operation fails
*/
WebDavMount mount(int localPort) throws CommandFailedException;
}

View File

@@ -0,0 +1,61 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel, Markus Kreusch
* 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, strategy fine tuning
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
******************************************************************************/
package org.cryptomator.ui.util.mount;
import static org.cryptomator.ui.util.command.Script.fromLines;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.command.CommandResult;
import org.cryptomator.ui.util.command.Script;
/**
* A {@link WebDavMounterStrategy} utilizing the "net use" command.
* <p>
* Tested on Windows 7 but should also work on Windows 8.
*
* @author Markus Kreusch
*/
final class WindowsWebDavMounter implements WebDavMounterStrategy {
private static final Pattern WIN_MOUNT_DRIVELETTER_PATTERN = Pattern.compile("\\s*([A-Z]:)\\s*");
@Override
public boolean shouldWork() {
return SystemUtils.IS_OS_WINDOWS;
}
@Override
public WebDavMount mount(int localPort) throws CommandFailedException {
final Script mountScript = fromLines("net use * http://0--1.ipv6-literal.net:%PORT% /persistent:no").addEnv("PORT", String.valueOf(localPort));
final CommandResult mountResult = mountScript.execute(30, TimeUnit.SECONDS);
final String driveLetter = getDriveLetter(mountResult.getStdOut());
final Script unmountScript = fromLines("net use " + driveLetter + " /delete").addEnv("DRIVE_LETTER", driveLetter);
return new WebDavMount() {
@Override
public void unmount() throws CommandFailedException {
unmountScript.execute();
}
};
}
private String getDriveLetter(String result) throws CommandFailedException {
final Matcher matcher = WIN_MOUNT_DRIVELETTER_PATTERN.matcher(result);
if (matcher.find()) {
return matcher.group(1);
} else {
throw new CommandFailedException("Failed to get a drive letter from net use output.");
}
}
}

View File

@@ -1,55 +0,0 @@
<?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.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import org.cryptomator.ui.controls.*?>
<GridPane fx:id="rootGridPane" fx:controller="org.cryptomator.ui.AccessController" xmlns:fx="http://javafx.com/fxml" styleClass="root" gridLinesVisible="false" vgap="5" hgap="5" prefWidth="480">
<stylesheets>
<URL value="@panels.css" />
</stylesheets>
<padding>
<Insets top="10" right="10" bottom="10" left="10" />
</padding>
<columnConstraints>
<ColumnConstraints minWidth="150" maxWidth="150" hgrow="NEVER" />
<ColumnConstraints minWidth="200" hgrow="ALWAYS" />
<ColumnConstraints minWidth="50" maxWidth="120" hgrow="NEVER" />
</columnConstraints>
<children>
<!-- Row 0 -->
<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%access.label.workDir" GridPane.halignment="RIGHT" />
<TextField fx:id="workDirTextField" GridPane.rowIndex="0" GridPane.columnIndex="1" />
<Button GridPane.rowIndex="0" GridPane.columnIndex="2" text="%access.button.chooseWorkDir" onAction="#chooseWorkDir" focusTraversable="false" />
<!-- Row 1 -->
<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%access.label.username" GridPane.halignment="RIGHT" />
<ComboBox fx:id="usernameBox" GridPane.rowIndex="1" GridPane.columnIndex="1" promptText="$access.label.username" disable="true" />
<!-- Row 2 -->
<Label GridPane.rowIndex="2" GridPane.columnIndex="0" text="%access.label.password" GridPane.halignment="RIGHT" />
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="2" GridPane.columnIndex="1" disable="true" />
<!-- Row 3 -->
<Button fx:id="startServerButton" text="%access.button.startServer" GridPane.rowIndex="3" GridPane.columnIndex="1" disable="true" defaultButton="true" onAction="#startStopServer" focusTraversable="false" />
<!-- Row 4 -->
<Label fx:id="messageLabel" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="3" textAlignment="CENTER" />
</children>
</GridPane>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,886 @@
/*
* 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
*
*/
.root {
-fx-font-family: 'lucida-grande';
-fx-font-smoothing-type: lcd;
-fx-font-size: 13.0;
/***************************************************************************
* *
* The main color palette from which the rest of the colors are derived. *
* *
**************************************************************************/
-fx-base: #FFFFFF;
-fx-background: #ECECEC;
/* Used for the inside of text boxes, password boxes, lists, trees, and
* tables. See also -fx-text-inner-color, which should be used as the
* -fx-text-fill value for text painted on top of backgrounds colored
* with -fx-control-inner-background.
*/
-fx-control-inner-background: #FFFFFF;
/* One of these colors will be chosen based upon a ladder calculation
* that uses the brightness of a background color. Instead of using these
* colors directly as -fx-text-fill values, the sections in this file should
* use a derived color to match the background in use. See also:
*
* -fx-text-base-color for text on top of -fx-base, -fx-color, and -fx-body-color
* -fx-text-background-color for text on top of -fx-background
* -fx-text-inner-color for text on top of -fx-control-inner-color
* -fx-selection-bar-text for text on top of -fx-selection-bar
*/
-fx-dark-text-color: black;
-fx-mid-text-color: #B5B5B5;
-fx-light-text-color: white;
/* A bright blue for highlighting/accenting objects. For example: selected
* text; selected items in menus, lists, trees, and tables; progress bars */
-fx-accent: #B2D7FF;
/* A bright blue for the focus indicator of objects. Typically used as the
* first color in -fx-background-color for the "focused" pseudo-class. Also
* typically used with insets of -1.4 to provide a glowing effect.
*/
-fx-focus-color: #78A6D7;
-fx-faint-focus-color: #8FBDEE;
/* The color that is used in styling controls. The default value is based
* on -fx-base, but is changed by pseudoclasses to change the base color.
* For example, the "hover" pseudoclass will typically set -fx-color to
* -fx-hover-base (see below) and the "armed" pseudoclass will typically
* set -fx-color to -fx-pressed-base.
*/
-fx-color: -fx-base;
/* The opacity level to use for the "disabled" pseudoclass.
*/
-fx-disabled-opacity: 0.6;
/* Chart Color Palette */
CHART_COLOR_1: #28CA40;
CHART_COLOR_2: #FD4943;
CHART_COLOR_3: #2283FB;
CHART_COLOR_4: #FAEA77;
CHART_COLOR_5: #FA9E78;
CHART_COLOR_6: #F47BF8;
CHART_COLOR_7: #c84164;
CHART_COLOR_8: #888888;
/***************************************************************************
* *
* Colors that are derived from the main color palette. *
* *
**************************************************************************/
/* The color to use for -fx-text-fill when text is to be painted on top of
* a background filled with the -fx-background color.
*/
-fx-text-background-color: -fx-dark-text-color;
/* A little darker than -fx-color and used to draw boxes around objects such
* as progress bars, scroll bars, scroll panes, trees, tables, and lists.
*/
-fx-box-border: #C8C8C8;
/* Darker than -fx-background and used to draw boxes around text boxes and
* password boxes.
*/
-fx-text-box-border: #B5B5B5;
/* A gradient that goes from a little darker than -fx-color on the top to
* even more darker than -fx-color on the bottom. Typically is the second
* color in the -fx-background-color list as the small thin border around
* a control. It is typically the same size as the control (i.e., insets
* are 0).
*/
-fx-outer-border: derive(-fx-color,-23%);
/* A gradient that goes from a bit lighter than -fx-color on the top to
* a little darker at the bottom. Typically is the third color in the
* -fx-background-color list as a thin highlight inside the outer border.
* Insets are typically 1.
*/
-fx-inner-border: linear-gradient(to bottom, derive(-fx-color,75%), derive(-fx-color,2%));
/* A gradient that goes from a little lighter than -fx-color at the top to
* a little darker than -fx-color at the bottom and is used to fill the
* body of many controls such as buttons. Typically is the fourth color
* in the -fx-background-color list and represents main body of the control.
* Insets are typically 2.
*/
-fx-body-color: linear-gradient(to bottom, derive(-fx-color,10%) ,derive(-fx-color,-6%));
/* The color to use as -fx-text-fill when painting text on top of
* backgrounds filled with -fx-base, -fx-color, and -fx-body-color.
*/
-fx-text-base-color: -fx-dark-text-color;
/* The color to use as -fx-text-fill when painting text on top of
* backgrounds filled with -fx-control-inner-background.
*/
-fx-text-inner-color: -fx-dark-text-color;
/* Background for items in list like things such as menus, lists, trees,
* and tables.
*
* TODO: it seems like this should be based upon -fx-accent and we should
* remove the setting -fx-background in all the sections that use
* -fx-selection-bar.
*/
-fx-selection-bar: #2283FB;
/* The color to use as -fx-text-fill when painting text on top of
* backgrounds filled with -fx-selection-bar.
*
* TODO: it seems like this should be derived from -fx-selection-bar.
*/
-fx-selection-bar-text: -fx-light-text-color;
/* These are needed for Popup */
-fx-background-color: inherit;
-fx-background-radius: inherit;
-fx-background-insets: inherit;
-fx-padding: inherit;
-fx-cell-focus-inner-border: -fx-selection-bar;
/***************************************************************************
* *
* Set the default background color for the scene *
* *
**************************************************************************/
-fx-background-color: -fx-background;
}
/*******************************************************************************
* *
* Common Styles *
* *
* These are styles that give a standard look to a whole range of controls *
* *
******************************************************************************/
/* ==== BUTTON LIKE THINGS ============================================== */
.button,
.toggle-button,
.radio-button > .radio,
.check-box > .box,
.menu-button,
.choice-box,
.color-picker.split-button > .color-picker-label,
.combo-box-base,
.combo-box-base > .arrow-button {
-fx-background-color: linear-gradient(to bottom, #C1C1C1 0%, #A6A6A6 100%), -fx-base;
-fx-background-insets: 0, 1;
-fx-background-radius: 4;
-fx-padding: 0.2em 0.8em 0.2em 0.8em;
-fx-text-fill: -fx-dark-text-color;
-fx-alignment: CENTER;
-fx-focus-traversable: false;
-fx-effect: dropshadow(one-pass-box, #DCDCDC, 0.0, 0.0, 0.0, 1.0);
}
.button:hover,
.toggle-button:hover,
.radio-button:hover > .radio,
.check-box:hover > .box,
.menu-button:hover,
.split-menu-button > .label:hover,
.split-menu-button > .arrow-button:hover,
.slider .thumb:hover,
.scroll-bar > .thumb:hover,
.scroll-bar > .increment-button:hover,
.scroll-bar > .decrement-button:hover,
.choice-box:hover,
.color-picker.split-button > .arrow-button:hover,
.color-picker.split-button > .color-picker-label:hover,
.combo-box-base:hover,
.tab-pane > .tab-header-area > .control-buttons-tab > .container > .tab-down-button:hover {
-fx-color: -fx-base;
}
.button:armed,
.button:default:armed,
.toggle-button:armed,
.radio-button:armed > .radio,
.check-box:armed .box,
.menu-button:armed,
.split-menu-button:armed > .label,
.split-menu-button > .arrow-button:pressed,
.split-menu-button:showing > .arrow-button,
.slider .thumb:pressed,
.scroll-bar > .thumb:pressed,
.scroll-bar > .increment-button:pressed,
.scroll-bar > .decrement-button:pressed,
.tab-pane > .tab-header-area > .control-buttons-tab > .container > .tab-down-button:pressed {
-fx-background-color: linear-gradient(to bottom, #237FFE 0%, #023FDD 100%), linear-gradient(to bottom, #4A97FD 0%, #0867E4 100%);
-fx-text-fill: -fx-light-text-color;
}
.button:focused,
.toggle-button:focused,
.radio-button:focused > .radio,
.check-box:focused > .box,
.menu-button:focused,
.choice-box:focused,
.color-picker.split-button:focused > .color-picker-label {
-fx-background-color: -fx-faint-focus-color, -fx-focus-color, -fx-inner-border, -fx-body-color;
-fx-background-insets: -2, -0.3, 1, 2;
-fx-background-radius: 7, 6, 4, 3;
}
/* ==== DISABLED THINGS ================================================= */
.button:disabled,
.toggle-button:disabled,
.radio-button:disabled,
.check-box:disabled,
.hyperlink:disabled,
.menu-button:disabled,
.split-menu-button:disabled,
.slider:disabled,
.scroll-pane:disabled,
.progress-bar:disabled,
.progress-indicator:disabled,
.text-input:disabled,
.choice-box:disabled,
.combo-box-base:disabled,
.list-view:disabled,
.tree-view:disabled,
.table-view:disabled,
.tree-table-view:disabled,
.tab-pane:disabled,
.tab-pane > .tab-header-area > .headers-region > .tab:disabled {
-fx-background-color: linear-gradient(to bottom, #D2D2D2 0%, #C4C4C4 100%), #F2F2F2;
-fx-background-insets: 0, 1;
-fx-text-fill: -fx-mid-text-color;
-fx-effect: dropshadow(one-pass-box, #E0E0E0, 0.0, 0.0, 0.0, 0.5);
}
/* ==== MNEMONIC THINGS ================================================= */
.button:show-mnemonics .mnemonic-underline,
.toggle-button:show-mnemonics .mnemonic-underline,
.radio-button:show-mnemonics .mnemonic-underline,
.check-box:show-mnemonics .mnemonic-underline,
.hyperlink:show-mnemonics > .mnemonic-underline,
.split-menu-button:show-mnemonics > .mnemonic-underline,
.menu-button:show-mnemonics > .mnemonic-underline {
-fx-stroke: -fx-text-base-color;
}
/* ==== ARROWS ========================================================== */
.combo-box-base > .arrow-button > .arrow {
-fx-background-color: -fx-light-text-color;
-fx-background-insets: 0 0 -1 0, 0;
-fx-padding: 9px 6px 0 0;
-fx-shape: "M 0 3 l 3 -3 l 3 3 m 0 3 l -3 3 l -3 -3";
}
/* ==== CHOICE BOX LIKE THINGS ========================================== */
.combo-box-base {
-fx-padding: 0;
}
/* ==== BOX LIKE THINGS ================================================= */
.scroll-pane,
.split-pane,
.list-view,
.tree-view,
.table-view,
.tree-table-view {
-fx-background-color: -fx-box-border, -fx-control-inner-background;
-fx-background-insets: 0, 1;
-fx-padding: 1;
}
/*******************************************************************************
* *
* Label *
* *
******************************************************************************/
.label {
-fx-text-fill: -fx-text-background-color;
}
/*******************************************************************************
* *
* Button & ToggleButton *
* *
******************************************************************************/
/* ==== DEFAULT ========================================================= */
.button:default {
-fx-background-color: linear-gradient(to bottom, #4AA0F9 0%, #045FFF 100%), linear-gradient(to bottom, #69B2FA 0%, #0D81FF 100%);
-fx-text-fill: -fx-light-text-color;
}
.button:default:disabled {
-fx-background-color: linear-gradient(to bottom, #D2D2D2 0%, #C4C4C4 100%), #F2F2F2;
-fx-background-insets: 0, 1;
-fx-text-fill: -fx-mid-text-color;
}
/*******************************************************************************
* *
* ToolBar *
* *
******************************************************************************/
.tool-bar:horizontal {
-fx-background-color: -fx-box-border, -fx-background;
-fx-background-insets: 0;
-fx-padding: 0.4em;
-fx-spacing: 0.2em;
-fx-alignment: CENTER_LEFT;
}
.tool-bar:horizontal > .container > .separator {
-fx-orientation: vertical;
}
/*******************************************************************************
* *
* ScrollBar *
* *
******************************************************************************/
.scroll-bar:horizontal,
.scroll-bar:vertical {
-fx-background-color: #E8E8E8, #FAFAFA;
}
.scroll-bar:disabled {
-fx-opacity: -fx-disabled-opacity;
}
.scroll-bar > .thumb {
-fx-background-color: #C1C1C1;
-fx-background-insets: 2px;
-fx-background-radius: 4px;
}
.scroll-bar > .thumb:hover {
-fx-background-color: #7D7D7D;
}
.scroll-bar > .increment-button,
.scroll-bar > .decrement-button {
-fx-background-color: transparent;
-fx-color: transparent;
}
.scroll-bar:horizontal > .increment-button,
.scroll-bar:horizontal > .decrement-button {
-fx-padding: 6px 0px;
}
.scroll-bar:vertical > .increment-button,
.scroll-bar:vertical > .decrement-button {
-fx-padding: 0px 6px;
}
/*******************************************************************************
* *
* Separator *
* *
******************************************************************************/
.separator:horizontal .line {
-fx-border-color: -fx-text-box-border transparent transparent transparent;
-fx-border-insets: 0, 1 0 0 0;
}
.separator:vertical .line {
-fx-border-color: transparent transparent transparent -fx-text-box-border;
-fx-border-width: 3, 1;
-fx-border-insets: 0, 0 0 0 1;
}
/*******************************************************************************
* *
* ProgressIndicator *
* *
******************************************************************************/
.progress-indicator {
-fx-indeterminate-segment-count: 12.0;
}
.progress-indicator > .determinate-indicator > .indicator {
-fx-background-color:
rgb(208.0, 208.0, 208.0),
linear-gradient(rgb(176.0, 176.0, 176.0), rgb(207.0, 207.0, 207.0)),
linear-gradient(rgb(190.0, 190.0, 190.0) 0.0%, rgb(213.0, 213.0, 213.0) 15.0%, rgb(230.0, 230.0, 230.0) 50.0%, rgb(235.0, 235.0, 235.0) 100.0%),
linear-gradient(to left, rgb(196.0, 196.0, 196.0, 0.5) 0.0%, rgb(220.0, 220.0, 220.0, 0.2) 2.0% , transparent);
-fx-background-insets: 0.5 0.0 -0.5 0.0, 0.0, 0.5, 1.0;
-fx-padding: 0.083333em;
}
.progress-indicator > .determinate-indicator > .progress {
-fx-background-color:
rgb(208.0, 208.0, 208.0),
radial-gradient(center 50.0% 50.0%, radius 50.0%, rgb(84.0, 170.0, 240.0), rgb(90.0, 192.0, 246.0));
-fx-background-insets: 0.0, 0.5;
-fx-padding: 0.166667em;
}
.progress-indicator > .determinate-indicator > .tick {
-fx-background-color: rgb(208.0, 208.0, 208.0), white;
-fx-background-insets: 1.0 0.0 -1.0 0.0, 0.0;
-fx-padding: 0.416667em;
-fx-shape: "m 18.174523,1027.137 c -0.18077,-0.4575 -0.184364,-0.8913 0.115901,-1.1721 0.300265,-0.2809 0.836622,-0.3601 1.288422,-0.041 0.4518,0.3191 2.020453,2.9316 2.020453,2.9316 l 5.41194,-8.0232 c -4e-6,0 0.516257,-0.6671 1.248682,-0.3099 0.648831,0.3165 0.559153,1.0373 0.559153,1.0373 0,0 -5.940433,9.3556 -6.15501,9.6287 -0.214577,0.273 -1.595078,0.2481 -1.817995,-0.027 -0.222917,-0.2751 -2.490777,-3.567 -2.671546,-4.0244 z";
-fx-scale-shape: false;
}
.progress-indicator > .percentage {
-fx-font-size: 0.916667em;
}
.progress-indicator:disabled {
-fx-opacity: -fx-disabled-opacity;
}
.progress-indicator:indeterminate > .spinner {
-fx-padding: 0.083333em;
}
.progress-indicator:indeterminate .segment {
-fx-background-color: rgb(95.0, 95.0, 98.0), rgb(122.0, 122.0, 125.0);
-fx-background-insets:0.0, 0.5;
}
.progress-indicator:indeterminate .segment0 {
-fx-shape:"m 14.321262,6.5816808 c -0.824944,0.3797564 -0.10368,1.8484772 0.718513,1.3544717 L 18.786514,5.9486042 C 19.644992,5.4932031 18.92648,4.1387308 18.068001,4.5941315 z";
}
.progress-indicator:indeterminate .segment1 {
-fx-shape:"m 15.372451,9.2445322 c -0.906719,-0.051108 -0.957826,1.5843588 0,1.5332498 l 4.241273,0 c 0.97179,0 0.97179,-1.5332498 0,-1.5332498 z";
}
.progress-indicator:indeterminate .segment2 {
-fx-shape:"m 14.423504,13.443113 c -0.824945,-0.379757 -0.10368,-1.848478 0.718512,-1.354472 l 3.746739,1.987548 c 0.858478,0.455401 0.139967,1.809873 -0.718512,1.354473 z";
}
.progress-indicator:indeterminate .segment3 {
-fx-shape:"m 12.10997,15.070611 c -0.49762,-0.759687 0.893182,-1.621681 1.327834,-0.766626 l 2.120636,3.673051 c 0.485895,0.841595 -0.841938,1.60822 -1.327833,0.766625 z";
}
.progress-indicator:indeterminate .segment4 {
-fx-shape:"m 9.2224559,19.539943 c -0.051108,0.906718 1.5843581,0.957826 1.5332501,0 l 0,-4.241273 c 0,-0.97179 -1.5332501,-0.97179 -1.5332501,0 z";
}
.progress-indicator:indeterminate .segment5 {
-fx-shape:"M 8.0465401,15.070611 C 8.5441601,14.310924 7.1533584,13.44893 6.7187068,14.303985 l -2.1206366,3.673051 c -0.485895,0.841595 0.8419383,1.60822 1.3278333,0.766625 z";
}
.progress-indicator:indeterminate .segment6 {
-fx-shape:"M 5.7330066,13.443113 C 6.5579512,13.063356 5.8366865,11.594635 5.0144939,12.088641 L 1.2677551,14.076189 C 0.409277,14.53159 1.1277888,15.886062 1.9862674,15.430662 z";
}
.progress-indicator:indeterminate .segment7 {
-fx-shape:"m 0.42171041,9.2083842 c -0.90671825,-0.051108 -0.95782608,1.5843588 0,1.5332498 l 4.24127319,0 c 0.9717899,0 0.9717899,-1.5332498 0,-1.5332498 z";
}
.progress-indicator:indeterminate .segment8 {
-fx-shape:"M 5.7330066,6.5305598 C 6.5579512,6.9103162 5.8366865,8.3790371 5.0144939,7.8850315 L 1.2677551,5.8974832 C 0.409277,5.4420823 1.1277888,4.0876101 1.9862674,4.5430105 z";
}
.progress-indicator:indeterminate .segment9 {
-fx-shape:"M 8.0465401,4.9030617 C 8.5441601,5.6627485 7.1533584,6.5247425 6.7187068,5.6696872 L 4.5980702,1.9966363 C 4.1121752,1.1550418 5.4400085,0.38841683 5.9259035,1.2300114 z";
}
.progress-indicator:indeterminate .segment10 {
-fx-shape:"m 9.2224559,4.62535 c -0.051108,0.9067177 1.5843581,0.957826 1.5332501,0 l 0,-4.24127319 c 0,-0.9717899 -1.5332501,-0.9717899 -1.5332501,0 z";
}
.progress-indicator:indeterminate .segment11 {
-fx-shape:"m 12.007729,4.9541827 c -0.49762,0.7596865 0.893181,1.6216808 1.327833,0.7666252 L 15.456199,2.0477574 C 15.942094,1.2061627 14.61426,0.43953765 14.128365,1.2811324 z";
}
/*******************************************************************************
* *
* Text COMMON *
* *
******************************************************************************/
.text-input {
-fx-text-fill: -fx-dark-text-color;
-fx-highlight-fill: derive(-fx-control-inner-background,-20%);
-fx-highlight-text-fill: -fx-text-inner-color;
-fx-prompt-text-fill: derive(-fx-control-inner-background,-30%);
-fx-border-color: -fx-text-box-border;
-fx-border-width: 1px;
-fx-background-color: #FFFFFF;
-fx-background-insets: 0;
-fx-background-radius: 0;
-fx-cursor: text;
-fx-padding: 2px;
}
.text-input:focused {
-fx-highlight-fill: -fx-accent;
-fx-border-color: -fx-focus-color;
-fx-border-width: 1px;
-fx-background-color: -fx-faint-focus-color, #FFFFFF;
-fx-background-insets: -3, 0;
-fx-background-radius: 3, 0;
-fx-prompt-text-fill: transparent;
}
/*******************************************************************************
* *
* PopupMenu *
* *
******************************************************************************/
.context-menu {
-fx-background-color: rgba(255.0, 255.0, 255.0, 0.9);
-fx-background-insets: 0;
-fx-background-radius: 4.0;
-fx-padding: 4px 0 4px 0;
-fx-effect: dropshadow(three-pass-box, rgba(0.0,0.0,0.0,0.6), 8.0, 0.0, 0.0, 0.0 );
}
.context-menu > .separator {
-fx-padding: 0.0em 0.333333em 0.0em 0.333333em; /* 0 4 0 4 */
}
/*******************************************************************************
* *
* MenuItem *
* *
******************************************************************************/
.menu-item {
-fx-background-color: transparent;
-fx-background-insets:0.0;
-fx-padding:0.2em 1em 0.2em 1em;
-fx-border-width: 0.0 0.0 0.0 0.0;
-fx-border-color:transparent;
}
.menu-item > .left-container {
-fx-padding: 0.458em 0.791em 0.458em 0.458em;
}
.menu-item > .graphic-container {
-fx-padding: 0em 0.333em 0em 0em;
}
.menu-item >.label {
-fx-padding: 0em 0.5em 0em 0em;
-fx-text-fill: -fx-text-base-color;
}
.menu-item:disabled > .label {
-fx-opacity: -fx-disabled-opacity;
}
.menu-item:focused {
-fx-background: -fx-accent;
-fx-background-color: -fx-selection-bar;
-fx-text-fill: -fx-selection-bar-text;
}
.menu-item:focused > .label {
-fx-text-fill: white;
}
.menu-item > .right-container {
-fx-padding: 0em 0em 0em 0.5em;
}
.menu-item:show-mnemonics > .mnemonic-underline {
-fx-stroke: -fx-text-fill;
}
.menu > .right-container > .arrow {
-fx-padding: 0.458em 0.167em 0.458em 0.167em; /* 4.5 2 4.5 2 */
-fx-background-color: -fx-color;
-fx-shape: "M0,-4L4,0L0,4Z";
-fx-scale-shape: false;
}
.menu:selected > .right-container > .arrow {
-fx-background-color: white;
}
.menu-item:disabled {
-fx-opacity: -fx-disabled-opacity;
}
/*******************************************************************************
* *
* ComboBox *
* *
******************************************************************************/
/* Customie the ListCell that appears in the ComboBox button itself */
.combo-box > .list-cell {
-fx-background: transparent;
-fx-background-color: transparent;
-fx-text-fill: -fx-text-base-color;
-fx-padding: 0.2em 0.5em 0.2em 0.5em;
}
.combo-box-base > .arrow-button {
-fx-background-color: linear-gradient(to bottom, #4AA0F9 0%, #045FFF 100%), linear-gradient(to bottom, #69B2FA 0%, #0D81FF 100%);
-fx-background-radius: 0 5 5 0, 0 4 4 0;
-fx-padding: 0.2em 0.5em 0.2em 0.5em;
-fx-background-insets: 0 0 0 1, 1;
}
.combo-box-popup > .list-view {
-fx-background-color: rgba(255.0, 255.0, 255.0, 0.9);
-fx-background-insets: 0;
-fx-background-radius: 4.0;
-fx-padding: 4px 0 4px 0;
-fx-effect: dropshadow(three-pass-box, rgba(0.0,0.0,0.0,0.6), 8.0, 0.0, 0.0, 0.0);
}
.combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell {
-fx-background-color: transparent;
-fx-padding:0.2em 1em 0.2em 1em;
-fx-border-color:transparent;
}
.combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected,
.combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected:hover {
-fx-background: -fx-accent;
-fx-background-color: -fx-selection-bar;
-fx-text-fill: -fx-selection-bar-text;
}
/*******************************************************************************
* *
* SplitPane *
* *
******************************************************************************/
.split-pane > .split-pane-divider {
-fx-padding: 0 0.25em 0 0.25em; /* 0 3 0 3 */
}
/*******************************************************************************
* *
* ListView and ListCell *
* *
******************************************************************************/
.list-view > .virtual-flow > .scroll-bar:vertical{
-fx-background-insets: 0, 0 0 0 1;
-fx-padding: -1 -1 -1 0;
}
.list-view > .virtual-flow > .scroll-bar:horizontal{
-fx-background-insets: 0, 1 0 0 0;
-fx-padding: 0 -1 -1 -1;
}
.list-view > .virtual-flow > .corner {
-fx-background-color: -fx-box-border, -fx-base;
-fx-background-insets: 0, 1 0 0 1;
}
.list-cell {
-fx-background-color: -fx-control-inner-background;
-fx-padding: 0.8em 0.5em 0.8em 0.5em;
-fx-text-fill: -fx-text-inner-color;
-fx-opacity: 1;
}
.list-view:focused > .virtual-flow > .clipped-container > .sheet > .list-cell:focused {
-fx-background-color: -fx-focus-color, -fx-cell-focus-inner-border, -fx-control-inner-background;
-fx-background-insets: 0, 1, 2;
}
.list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected {
-fx-background-color: -fx-focus-color, -fx-cell-focus-inner-border, -fx-selection-bar;
-fx-background-insets: 0, 1, 2;
-fx-background: -fx-accent;
-fx-text-fill: -fx-selection-bar-text;
}
.list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected,
.list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected:hover {
-fx-background: -fx-accent;
-fx-background-color: -fx-selection-bar;
-fx-text-fill: -fx-selection-bar-text;
}
.list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected:hover {
-fx-background: -fx-accent;
-fx-background-color: -fx-focus-color, -fx-cell-focus-inner-border, -fx-selection-bar;
-fx-background-insets: 0, 1, 2;
-fx-text-fill: -fx-selection-bar-text;
}
/*******************************************************************************
* *
* Tooltip *
* *
******************************************************************************/
.tooltip {
-fx-background-color: -fx-background;
-fx-padding: 0.2em 0.4em 0.2em 0.4em;
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.8), 2, 0, 0, 0);
-fx-font-size: 0.8em;
}
/*******************************************************************************
* *
* Charts *
* *
******************************************************************************/
.chart {
-fx-padding: 5px;
}
.chart-content {
-fx-padding: 10px;
}
.chart-title {
-fx-font-size: 1.4em;
}
.chart-legend {
-fx-background-color: linear-gradient(to bottom, derive(-fx-background, -10%), derive(-fx-background, -5%)),
linear-gradient(from 0px 0px to 0px 5px, derive(-fx-background, -5%), derive(-fx-background, 20%));
-fx-background-insets: 0,1;
-fx-background-radius: 6,5;
-fx-padding: 6px;
}
/*******************************************************************************
* *
* Axis *
* *
******************************************************************************/
.axis {
AXIS_COLOR: derive(-fx-background,-20%);
-fx-tick-label-font-size: 0.833333em; /* 10px */
-fx-tick-label-fill: derive(-fx-text-background-color, 30%);
}
.axis:top {
-fx-border-color: transparent transparent AXIS_COLOR transparent;
}
.axis:right {
-fx-border-color: transparent transparent transparent AXIS_COLOR;
}
.axis:bottom {
-fx-border-color: AXIS_COLOR transparent transparent transparent;
}
.axis:left {
-fx-border-color: transparent AXIS_COLOR transparent transparent;
}
.axis-tick-mark,
.axis-minor-tick-mark {
-fx-fill: null;
-fx-stroke: AXIS_COLOR;
}
/*******************************************************************************
* *
* ChartPlot *
* *
******************************************************************************/
.chart-vertical-grid-lines {
-fx-stroke: derive(-fx-background,-10%);
-fx-stroke-dash-array: 0.25em, 0.25em;
}
.chart-horizontal-grid-lines {
-fx-stroke: derive(-fx-background,-10%);
}
.chart-alternative-column-fill {
-fx-fill: null;
-fx-stroke: null;
}
.chart-alternative-row-fill {
-fx-fill: null;
-fx-stroke: null;
}
.chart-vertical-zero-line,
.chart-horizontal-zero-line {
-fx-stroke: derive(-fx-text-background-color, 40%);
}
/*******************************************************************************
* *
* LineChart *
* *
******************************************************************************/
.chart-line-symbol {
-fx-background-color: #f9d900, white;
-fx-background-insets: 0, 2;
-fx-background-radius: 5px;
-fx-padding: 5px;
}
.chart-series-line {
-fx-stroke: #f9d900;
-fx-stroke-width: 3px;
/*-fx-effect: dropshadow( two-pass-box , rgba(0,0,0,0.3) , 8, 0.0 , 0 , 3 );*/
}
.default-color0.chart-line-symbol { -fx-background-color: CHART_COLOR_1, white; }
.default-color1.chart-line-symbol { -fx-background-color: CHART_COLOR_2, white; }
.default-color2.chart-line-symbol { -fx-background-color: CHART_COLOR_3, white; }
.default-color3.chart-line-symbol { -fx-background-color: CHART_COLOR_4, white; }
.default-color4.chart-line-symbol { -fx-background-color: CHART_COLOR_5, white; }
.default-color5.chart-line-symbol { -fx-background-color: CHART_COLOR_6, white; }
.default-color6.chart-line-symbol { -fx-background-color: CHART_COLOR_7, white; }
.default-color7.chart-line-symbol { -fx-background-color: CHART_COLOR_8, white; }
.default-color0.chart-series-line { -fx-stroke: CHART_COLOR_1; }
.default-color1.chart-series-line { -fx-stroke: CHART_COLOR_2; }
.default-color2.chart-series-line { -fx-stroke: CHART_COLOR_3; }
.default-color3.chart-series-line { -fx-stroke: CHART_COLOR_4; }
.default-color4.chart-series-line { -fx-stroke: CHART_COLOR_5; }
.default-color5.chart-series-line { -fx-stroke: CHART_COLOR_6; }
.default-color6.chart-series-line { -fx-stroke: CHART_COLOR_7; }
.default-color7.chart-series-line { -fx-stroke: CHART_COLOR_8; }
/*******************************************************************************
* *
* Combinations *
* *
* This section is for special handling of when one control is nested inside *
* another control. There are many cases where we would end up with ugly *
* double borders that are fixed here. *
* *
******************************************************************************/
.split-pane > * > .table-view { -fx-padding: 0px; }
.split-pane > * > .list-view { -fx-padding: 0px; }
.split-pane > * > .tree-view { -fx-padding: 0px; }
.split-pane > * > .scroll-pane { -fx-padding: 0px; }
.split-pane > * > .split-pane {
-fx-background-insets: 0, 0;
-fx-padding: 0;
}
/* ############################################################################
# Workaround for RT-27627 #
############################################################################ */
.choice-box > .open-button > .arrow { doh: true; }
.split-menu-button:openvertically > .arrow-button > .arrow { doh: true; }
.tab-pane > .tab-header-area > .control-buttons-tab > .container > .tab-down-button > .arrow { doh: true; }
.tree-table-view { doh: true; }
.tree-table-view:focused { doh: true; }
.tree-table-view > .virtual-flow > .scroll-bar:vertical { doh: true; }
.tree-table-view > .virtual-flow > .scroll-bar:horizontal { doh: true; }
.tree-table-view > .virtual-flow > .corner { doh: true; }
.tree-table-row-cell:focused { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:focused:selected { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:selected > .tree-table-cell { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:selected { doh: true; }
.tree-table-view:row-selection:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:focused:selected:hover{ doh: true; }
.tree-table-row-cell:filled:selected:focused { doh: true; }
.tree-table-row-cell:filled:selected { doh: true; }
.tree-table-row-cell:selected:disabled { doh: true; }
.tree-table-view:row-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:hover { doh: true; }
.tree-table-view:row-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:focused:hover { doh: true; }
.tree-table-view:constrained-resize > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell > .tree-table-cell:last-visible { doh: true; }
.tree-table-view:constrained-resize > .column-header:last-visible { doh: true; }
.tree-table-view:constrained-resize .filler { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell > .tree-table-cell:focused { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled .tree-table-cell:focused:selected { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:selected { doh: true; }
.tree-table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:hover:selected { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:focused:selected:hover{ doh: true; }
.tree-table-row-cell:filled > .tree-table-cell:selected:focused { doh: true; }
.tree-table-row-cell:filled > .tree-table-cell:selected { doh: true; }
.tree-table-cell:selected:disabled { doh: true; }
.tree-table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:hover { doh: true; }
.tree-table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:focused:hover { doh: true; }
.tree-table-view .column-resize-line { doh: true; }
.tree-table-view > .column-header-background { doh: true; }
.tree-table-view .column-header { doh: true; }
.tree-table-view .filler { doh: true; }
.tree-table-view .column-header .sort-order{ doh: true; }
.tree-table-view > .column-header-background > .show-hide-columns-button{ doh: true; }
.tree-table-view .show-hide-column-image { doh: true; }
.tree-table-view .column-drag-header { doh: true; }
.tree-table-view .column-overlay { doh: true; }
.tree-table-view /*> column-header-background > nested-column-header >*/ .arrow { doh: true; }
.tree-table-view .empty-table { doh: true; }
.axis-minor-tick-mark { doh: true; }
.chart-horizontal-zero-line { doh: true; }
.stacked-bar-chart:horizontal .chart-bar { doh: true; }

View File

@@ -0,0 +1,874 @@
/*
* 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
*
*/
.root {
-fx-font-family: 'Segoe UI Semibold';
-fx-font-smoothing-type: lcd;
-fx-font-size: 12.0;
/***************************************************************************
* *
* The main color palette from which the rest of the colors are derived. *
* *
**************************************************************************/
-fx-base: #EAEAEA;
-fx-background: #F0F0F0;
/* Used for the inside of text boxes, password boxes, lists, trees, and
* tables. See also -fx-text-inner-color, which should be used as the
* -fx-text-fill value for text painted on top of backgrounds colored
* with -fx-control-inner-background.
*/
-fx-control-inner-background: #FFFFFF;
/* One of these colors will be chosen based upon a ladder calculation
* that uses the brightness of a background color. Instead of using these
* colors directly as -fx-text-fill values, the sections in this file should
* use a derived color to match the background in use. See also:
*
* -fx-text-base-color for text on top of -fx-base, -fx-color, and -fx-body-color
* -fx-text-background-color for text on top of -fx-background
* -fx-text-inner-color for text on top of -fx-control-inner-color
* -fx-selection-bar-text for text on top of -fx-selection-bar
*/
-fx-dark-text-color: black;
-fx-mid-text-color: #8B8B8B;
-fx-light-text-color: white;
/* A bright blue for highlighting/accenting objects. For example: selected
* text; selected items in menus, lists, trees, and tables; progress bars */
-fx-accent: #3399FF;
/* A bright blue for the focus indicator of objects. Typically used as the
* first color in -fx-background-color for the "focused" pseudo-class. Also
* typically used with insets of -1.4 to provide a glowing effect.
*/
-fx-focus-color: #3399FF;
/* The color that is used in styling controls. The default value is based
* on -fx-base, but is changed by pseudoclasses to change the base color.
* For example, the "hover" pseudoclass will typically set -fx-color to
* -fx-hover-base (see below) and the "armed" pseudoclass will typically
* set -fx-color to -fx-pressed-base.
*/
-fx-color: -fx-base;
/* The opacity level to use for the "disabled" pseudoclass.
*/
-fx-disabled-opacity: 0.6;
/* Chart Color Palette */
CHART_COLOR_1: #A1CD5f;
CHART_COLOR_2: #C75050;
CHART_COLOR_3: #3399FF;
CHART_COLOR_4: #FAEA77;
CHART_COLOR_5: #FA9E78;
CHART_COLOR_6: #F47BF8;
CHART_COLOR_7: #c84164;
CHART_COLOR_8: #888888;
/***************************************************************************
* *
* Colors that are derived from the main color palette. *
* *
**************************************************************************/
/* The color to use for -fx-text-fill when text is to be painted on top of
* a background filled with the -fx-background color.
*/
-fx-text-background-color: -fx-dark-text-color;
/* A little darker than -fx-color and used to draw boxes around objects such
* as progress bars, scroll bars, scroll panes, trees, tables, and lists.
*/
-fx-box-border: #ACACAC;
/* Darker than -fx-background and used to draw boxes around text boxes and
* password boxes.
*/
-fx-text-box-border: #ACACAC;
/* A gradient that goes from a little darker than -fx-color on the top to
* even more darker than -fx-color on the bottom. Typically is the second
* color in the -fx-background-color list as the small thin border around
* a control. It is typically the same size as the control (i.e., insets
* are 0).
*/
-fx-outer-border: derive(-fx-color,-23%);
/* A gradient that goes from a bit lighter than -fx-color on the top to
* a little darker at the bottom. Typically is the third color in the
* -fx-background-color list as a thin highlight inside the outer border.
* Insets are typically 1.
*/
-fx-inner-border: linear-gradient(to bottom, derive(-fx-color,75%), derive(-fx-color,2%));
/* A gradient that goes from a little lighter than -fx-color at the top to
* a little darker than -fx-color at the bottom and is used to fill the
* body of many controls such as buttons. Typically is the fourth color
* in the -fx-background-color list and represents main body of the control.
* Insets are typically 2.
*/
-fx-body-color: linear-gradient(to bottom, derive(-fx-color,10%) ,derive(-fx-color,-6%));
/* The color to use as -fx-text-fill when painting text on top of
* backgrounds filled with -fx-base, -fx-color, and -fx-body-color.
*/
-fx-text-base-color: -fx-dark-text-color;
/* The color to use as -fx-text-fill when painting text on top of
* backgrounds filled with -fx-control-inner-background.
*/
-fx-text-inner-color: -fx-dark-text-color;
/* Background for items in list like things such as menus, lists, trees,
* and tables.
*
* TODO: it seems like this should be based upon -fx-accent and we should
* remove the setting -fx-background in all the sections that use
* -fx-selection-bar.
*/
-fx-selection-bar: #3399FF;
/* The color to use as -fx-text-fill when painting text on top of
* backgrounds filled with -fx-selection-bar.
*
* TODO: it seems like this should be derived from -fx-selection-bar.
*/
-fx-selection-bar-text: -fx-light-text-color;
/* These are needed for Popup */
-fx-background-color: inherit;
-fx-background-radius: inherit;
-fx-background-insets: inherit;
-fx-padding: inherit;
-fx-cell-focus-inner-border: -fx-selection-bar;
/***************************************************************************
* *
* Set the default background color for the scene *
* *
**************************************************************************/
-fx-background-color: -fx-background;
}
/*******************************************************************************
* *
* Common Styles *
* *
* These are styles that give a standard look to a whole range of controls *
* *
******************************************************************************/
/* ==== BUTTON LIKE THINGS ============================================== */
.button,
.toggle-button,
.radio-button > .radio,
.check-box > .box,
.menu-button,
.choice-box,
.color-picker.split-button > .color-picker-label,
.combo-box-base,
.combo-box-base > .arrow-button {
-fx-background-color: -fx-box-border, linear-gradient(to bottom, #F0F0F0 0%, #E5E5E5 100%);
-fx-background-insets: 0, 1;
-fx-background-radius: 0, 0;
-fx-padding: 0.1em 0.6em 0.1em 0.6em;
-fx-text-fill: -fx-dark-text-color;
-fx-alignment: CENTER;
-fx-border-color: transparent;
-fx-border-insets: 2px;
}
.button:hover,
.toggle-button:hover,
.radio-button:hover > .radio,
.check-box:hover > .box,
.menu-button:hover,
.split-menu-button > .label:hover,
.split-menu-button > .arrow-button:hover,
.slider .thumb:hover,
.choice-box:hover,
.color-picker.split-button > .arrow-button:hover,
.color-picker.split-button > .color-picker-label:hover,
.combo-box-base:hover,
.tab-pane > .tab-header-area > .control-buttons-tab > .container > .tab-down-button:hover {
-fx-color: -fx-base;
}
.button:armed,
.button:default:armed,
.toggle-button:armed,
.radio-button:armed > .radio,
.check-box:armed .box,
.menu-button:armed,
.split-menu-button:armed > .label,
.split-menu-button > .arrow-button:pressed,
.split-menu-button:showing > .arrow-button,
.slider .thumb:pressed,
.tab-pane > .tab-header-area > .control-buttons-tab > .container > .tab-down-button:pressed {
-fx-background-color: -fx-focus-color, linear-gradient(to bottom, #DAECFC 0%, #C4E0FC 100%);
}
.button:focused,
.toggle-button:focused,
.radio-button:focused > .radio,
.check-box:focused > .box,
.menu-button:focused,
.choice-box:focused,
.color-picker.split-button:focused > .color-picker-label {
-fx-border-color: black;
-fx-border-insets: 2px;
-fx-border-style: dotted inside;
}
/* ==== DISABLED THINGS ================================================= */
.button:disabled,
.toggle-button:disabled,
.radio-button:disabled,
.check-box:disabled,
.hyperlink:disabled,
.menu-button:disabled,
.split-menu-button:disabled,
.slider:disabled,
.scroll-pane:disabled,
.progress-bar:disabled,
.progress-indicator:disabled,
.text-input:disabled,
.choice-box:disabled,
.combo-box-base:disabled,
.list-view:disabled,
.tree-view:disabled,
.table-view:disabled,
.tree-table-view:disabled,
.tab-pane:disabled,
.tab-pane > .tab-header-area > .headers-region > .tab:disabled {
-fx-background-color: linear-gradient(to bottom, #D2D2D2 0%, #C4C4C4 100%), #F2F2F2;
-fx-text-fill: -fx-mid-text-color;
}
/* ==== MNEMONIC THINGS ================================================= */
.button:show-mnemonics .mnemonic-underline,
.toggle-button:show-mnemonics .mnemonic-underline,
.radio-button:show-mnemonics .mnemonic-underline,
.check-box:show-mnemonics .mnemonic-underline,
.hyperlink:show-mnemonics > .mnemonic-underline,
.split-menu-button:show-mnemonics > .mnemonic-underline,
.menu-button:show-mnemonics > .mnemonic-underline {
-fx-stroke: -fx-text-base-color;
}
/* ==== ARROWS ========================================================== */
.combo-box-base > .arrow-button > .arrow {
-fx-background-color: #606060;
-fx-background-insets: 0;
-fx-padding: 2px 4px 2px 4px;
-fx-shape: "M 0 0 h 7 l -3.5 4 z";
}
/* ==== CHOICE BOX LIKE THINGS ========================================== */
.combo-box-base {
-fx-padding: 0;
}
/* ==== BOX LIKE THINGS ================================================= */
.scroll-pane,
.split-pane,
.list-view,
.tree-view,
.table-view,
.tree-table-view {
-fx-background-color: -fx-box-border, -fx-control-inner-background;
-fx-background-insets: 0, 1;
-fx-padding: 1;
}
/*******************************************************************************
* *
* Label *
* *
******************************************************************************/
.label {
-fx-text-fill: -fx-text-background-color;
}
/*******************************************************************************
* *
* Button & ToggleButton *
* *
******************************************************************************/
/* ==== DEFAULT ========================================================= */
.button:default {
-fx-background-color: -fx-focus-color, linear-gradient(to bottom, #F0F0F0 0%, #E5E5E5 100%);
}
.button:default:disabled {
-fx-background-color: linear-gradient(to bottom, #D2D2D2 0%, #C4C4C4 100%), #F2F2F2;
-fx-text-fill: -fx-mid-text-color;
}
/*******************************************************************************
* *
* ToolBar *
* *
******************************************************************************/
.tool-bar:horizontal {
-fx-background-color: -fx-box-border, -fx-background;
-fx-background-insets: 0;
-fx-padding: 0.4em;
-fx-spacing: 0.2em;
-fx-alignment: CENTER_LEFT;
}
.tool-bar:horizontal > .container > .separator {
-fx-orientation: vertical;
}
/*******************************************************************************
* *
* ScrollBar *
* *
******************************************************************************/
.scroll-bar:horizontal,
.scroll-bar:vertical {
-fx-background-color: -fx-base;
}
.scroll-bar:disabled {
-fx-opacity: -fx-disabled-opacity;
}
.scroll-bar > .thumb {
-fx-background-color: #CDCDCD;
}
.scroll-bar > .thumb:hover {
-fx-background-color: #A6A6A6;
}
.scroll-bar > .increment-button,
.scroll-bar > .decrement-button {
-fx-background-color: transparent;
-fx-color: transparent;
}
.scroll-bar:horizontal > .increment-button,
.scroll-bar:horizontal > .decrement-button {
-fx-padding: 5px 5px;
}
.scroll-bar:vertical > .increment-button,
.scroll-bar:vertical > .decrement-button {
-fx-padding: 5px 5px;
}
.scroll-bar > .increment-button,
.scroll-bar > .decrement-button {
-fx-background-color: -fx-outer-border, -fx-inner-border, -fx-body-color;
-fx-background-insets: 0, 1, 2;
-fx-color: transparent;
-fx-padding: 3px;
}
.scroll-bar > .increment-button > .increment-arrow,
.scroll-bar > .decrement-button > .decrement-arrow {
-fx-background-color: #606060;
}
.scroll-bar > .increment-button:hover > .increment-arrow,
.scroll-bar > .decrement-button:hover > .decrement-arrow {
-fx-background-color: #606060;
}
.scroll-bar > .increment-button:pressed > .increment-arrow,
.scroll-bar > .decrement-button:pressed > .decrement-arrow {
-fx-background-color: #606060;
}
.scroll-bar:horizontal > .increment-button > .increment-arrow {
-fx-padding: 9 7 0 0;
-fx-shape: "M0.315,1.457l1.414-1.414L5.686,4L1.729,7.957L0.315,6.543L2.857,4L0.315,1.457z";
}
.scroll-bar:vertical > .increment-button > .increment-arrow {
-fx-padding: 7 9 0 0 ;
-fx-shape: "M6.543,0.315l1.414,1.414L4,5.686L0.043,1.729l1.414-1.414L4,2.858L6.543,0.315z";
}
.scroll-bar:horizontal > .decrement-button > .decrement-arrow {
-fx-padding: 9 7 0 0;
-fx-shape: "M5.686,6.543L4.271,7.957L0.314,4l3.957-3.957l1.414,1.414L3.143,4L5.686,6.543z";
}
.scroll-bar:vertical > .decrement-button > .decrement-arrow {
-fx-padding: 7 9 0 0;
-fx-shape: "M1.457,5.563L0.042,4.149L4,0.193l3.957,3.957L6.543,5.563L4,3.021L1.457,5.563z";
}
/*******************************************************************************
* *
* Separator *
* *
******************************************************************************/
.separator:horizontal .line {
-fx-border-color: -fx-text-box-border transparent transparent transparent;
-fx-border-insets: 0, 1 0 0 0;
}
.separator:vertical .line {
-fx-border-color: transparent transparent transparent -fx-text-box-border;
-fx-border-width: 3, 1;
-fx-border-insets: 0, 0 0 0 1;
}
/*******************************************************************************
* *
* ProgressIndicator *
* *
******************************************************************************/
.progress-indicator {
-fx-indeterminate-segment-count: 12;
-fx-spin-enabled: true;
}
.progress-indicator > .determinate-indicator > .indicator {
-fx-background-color: -fx-box-border,
radial-gradient(center 50% 50%, radius 50%, -fx-control-inner-background 70%, derive(-fx-control-inner-background, -9%) 100%),
-fx-control-inner-background;
-fx-background-insets: 0, 1, 5 2 1 2;
-fx-padding: 1;
}
.progress-indicator > .determinate-indicator > .progress {
-fx-background-color: -fx-accent;
-fx-background-insets: 2;
-fx-padding: 1em; /* 9 */
}
/* TODO: scaling the shape seems to make it disappear */
.progress-indicator > .determinate-indicator > .tick {
-fx-background-color: white;
-fx-background-insets: 0;
-fx-padding: 0.416667em; /* 5 */
-fx-shape: "M-0.25,6.083c0.843-0.758,4.583,4.833,5.75,4.833S14.5-1.5,15.917-0.917c1.292,0.532-8.75,17.083-10.5,17.083C3,16.167-1.083,6.833-0.25,6.083z";
-fx-scale-shape: false;
}
.progress-indicator:indeterminate > .spinner {
-fx-padding: 0.833333em; /* 10 */
}
.progress-indicator > .percentage {
-fx-font-size: 0.916667em; /* 11pt - 1 less than the default font */
-fx-fill: -fx-text-background-color;
}
.progress-indicator:indeterminate .segment {
-fx-background-color: -fx-accent;
}
.progress-indicator:indeterminate .segment0 {
-fx-shape:"M10,0C9.998,0,9.995,0,9.992,0C9.991,0,9.991,0,9.99,0C9.988,0,9.986,0,9.985,0S9.982,0,9.981,0S9.979,0,9.978,0S9.975,0,9.974,0S9.972,0,9.971,0C9.969,0,9.968,0,9.966,0H9.965c-0.007,0-0.014,0-0.02,0C9.944,0,9.944,0,9.944,0C9.941,0,9.939,0,9.937,0c-0.77,0.007-1.389,0.634-1.384,1.404C8.557,2.176,9.185,2.8,9.956,2.8c0.001,0,0.003,0,0.004,0H10c0.773-0.002,1.4-0.63,1.4-1.404c0-0.77-0.622-1.393-1.392-1.396C10.006,0,10.003,0,10,0L10,0z";
}
.progress-indicator:indeterminate .segment1 {
-fx-shape:"M5.688,1.156c-0.236,0-0.476,0.06-0.696,0.187C4.98,1.349,4.969,1.356,4.958,1.363c0,0-0.001,0-0.001,0C4.955,1.364,4.954,1.365,4.952,1.366c-0.001,0-0.002,0.001-0.004,0.002c0,0,0,0-0.001,0C4.944,1.371,4.94,1.373,4.936,1.375c0,0,0,0-0.001,0C4.933,1.377,4.931,1.378,4.929,1.38C4.267,1.772,4.046,2.624,4.438,3.288c0.261,0.444,0.73,0.692,1.212,0.692c0.24,0,0.484-0.062,0.706-0.192l0.034-0.02C7.058,3.378,7.283,2.52,6.896,1.851C6.636,1.405,6.168,1.156,5.688,1.156L5.688,1.156z";
}
.progress-indicator:indeterminate .segment2 {
-fx-shape:"M2.534,4.326c-0.482,0-0.951,0.25-1.209,0.697C1.323,5.027,1.321,5.029,1.32,5.031l0,0C1.319,5.033,1.318,5.034,1.317,5.036S1.315,5.039,1.314,5.04c0,0.001,0,0.002-0.001,0.002C1.312,5.044,1.311,5.046,1.31,5.048c0,0,0,0,0,0.001C1.309,5.051,1.308,5.053,1.307,5.055C1.302,5.063,1.297,5.071,1.292,5.079l0,0C1.291,5.082,1.29,5.084,1.288,5.087c-0.376,0.67-0.141,1.519,0.529,1.898c0.218,0.123,0.456,0.182,0.69,0.182c0.488,0,0.963-0.255,1.222-0.708l0.02-0.035c0.383-0.671,0.149-1.527-0.521-1.912C3.008,4.386,2.769,4.326,2.534,4.326L2.534,4.326z";
}
.progress-indicator:indeterminate .segment3 {
-fx-shape:"M1.396,8.648c-0.002,0-0.005,0-0.008,0C0.619,8.652-0.001,9.278,0,10.047c0,0.002,0,0.006,0,0.008l0,0c0,0.019,0,0.037,0,0.056c0,0.001,0,0.002,0,0.003s0,0.003,0,0.004c0.01,0.765,0.632,1.378,1.396,1.378c0.005,0,0.01,0,0.015,0c0.773-0.009,1.395-0.642,1.389-1.415v-0.04C2.794,9.27,2.166,8.648,1.396,8.648L1.396,8.648z";
}
.progress-indicator:indeterminate .segment4 {
-fx-shape:"M2.579,12.955c-0.242,0-0.487,0.062-0.71,0.194c-0.664,0.391-0.885,1.242-0.499,1.906c0.013,0.021,0.025,0.043,0.038,0.063c0.262,0.436,0.724,0.678,1.197,0.678c0.243,0,0.49-0.063,0.714-0.197c0.665-0.396,0.883-1.257,0.489-1.922l-0.02-0.034C3.526,13.201,3.059,12.955,2.579,12.955L2.579,12.955z";
}
.progress-indicator:indeterminate .segment5 {
-fx-shape:"M5.772,16.09c-0.489,0-0.965,0.257-1.223,0.712c-0.38,0.671-0.146,1.52,0.523,1.901c0.002,0.001,0.004,0.002,0.007,0.004h0c0.004,0.002,0.008,0.004,0.012,0.007c0,0,0,0,0.001,0c0.001,0.001,0.002,0.002,0.004,0.002c0.001,0.001,0.003,0.002,0.004,0.003c0,0,0.001,0,0.001,0.001c0.012,0.006,0.023,0.013,0.035,0.019c0.214,0.119,0.446,0.176,0.675,0.176c0.489,0,0.963-0.258,1.22-0.716c0.377-0.675,0.137-1.529-0.537-1.908l-0.035-0.02C6.242,16.149,6.006,16.09,5.772,16.09L5.772,16.09z";
}
.progress-indicator:indeterminate .segment6 {
-fx-shape:"M10.14,17.198c-0.006,0-0.013,0-0.02,0h-0.039c-0.773,0.011-1.394,0.646-1.385,1.419c0.008,0.767,0.631,1.382,1.396,1.382c0.003,0,0.006,0,0.009-0.001c0.024,0,0.051,0,0.075-0.001c0.769-0.016,1.38-0.648,1.367-1.418C11.53,17.813,10.904,17.198,10.14,17.198L10.14,17.198z";
}
.progress-indicator:indeterminate .segment7 {
-fx-shape:"M14.433,15.97c-0.245,0-0.494,0.064-0.72,0.2l-0.034,0.021c-0.663,0.397-0.88,1.258-0.483,1.922c0.261,0.439,0.725,0.683,1.2,0.683c0.24,0,0.484-0.062,0.707-0.194c0.022-0.013,0.044-0.025,0.065-0.039c0.656-0.399,0.866-1.254,0.469-1.913C15.373,16.212,14.909,15.97,14.433,15.97L14.433,15.97z";
}
.progress-indicator:indeterminate .segment8 {
-fx-shape:"M17.539,12.748c-0.493,0-0.973,0.261-1.229,0.723l-0.02,0.034c-0.376,0.676-0.133,1.53,0.542,1.907c0.216,0.121,0.45,0.178,0.681,0.178c0.487,0,0.96-0.256,1.217-0.71c0.003-0.006,0.007-0.012,0.01-0.019c0.007-0.013,0.015-0.025,0.021-0.038l0,0c0.002-0.003,0.003-0.005,0.004-0.008c0.369-0.675,0.124-1.521-0.55-1.893C18.001,12.805,17.769,12.748,17.539,12.748L17.539,12.748z";
}
.progress-indicator:indeterminate .segment9 {
-fx-shape:"M18.603,8.408c-0.011,0-0.021,0-0.031,0c-0.773,0.018-1.388,0.657-1.373,1.431l0.001,0.04c0.015,0.765,0.641,1.377,1.403,1.377c0.008,0,0.016,0,0.023,0c0.77-0.013,1.383-0.646,1.373-1.414c0-0.003,0-0.006,0-0.009l0,0c-0.001-0.019-0.001-0.037-0.001-0.055c0-0.001,0-0.001-0.001-0.002c0-0.002,0-0.004,0-0.006C19.979,9.012,19.358,8.408,18.603,8.408L18.603,8.408z";
}
.progress-indicator:indeterminate .segment10 {
-fx-shape:"M17.345,4.121c-0.248,0-0.5,0.066-0.728,0.205c-0.659,0.403-0.869,1.266-0.468,1.927l0.021,0.034c0.265,0.435,0.728,0.675,1.202,0.675c0.247,0,0.497-0.065,0.724-0.202c0.659-0.397,0.871-1.252,0.477-1.912c-0.007-0.011-0.013-0.021-0.02-0.032c-0.001-0.002-0.002-0.003-0.002-0.004c-0.001,0-0.001-0.001-0.001-0.002c-0.004-0.005-0.008-0.011-0.011-0.017c0-0.001,0-0.001-0.001-0.001c-0.001-0.002-0.002-0.004-0.004-0.007C18.271,4.358,17.813,4.121,17.345,4.121L17.345,4.121z";
}
.progress-indicator:indeterminate .segment11 {
-fx-shape:"M14.104,1.039c-0.494,0-0.974,0.264-1.227,0.729c-0.37,0.679-0.12,1.531,0.559,1.903l0.034,0.019c0.214,0.117,0.445,0.173,0.673,0.173c0.495,0,0.976-0.262,1.231-0.726c0.371-0.674,0.129-1.519-0.542-1.894c-0.012-0.006-0.024-0.013-0.036-0.02c-0.007-0.004-0.014-0.008-0.021-0.012c-0.003-0.001-0.006-0.003-0.009-0.005C14.556,1.094,14.329,1.039,14.104,1.039L14.104,1.039z";
}
/*******************************************************************************
* *
* Text COMMON *
* *
******************************************************************************/
.text-input {
-fx-text-fill: -fx-dark-text-color;
-fx-highlight-fill: derive(-fx-control-inner-background,-20%);
-fx-highlight-text-fill: -fx-text-inner-color;
-fx-prompt-text-fill: -fx-control-inner-background;
-fx-border-color: -fx-text-box-border;
-fx-border-width: 1px;
-fx-background-color: #FFFFFF;
-fx-background-insets: 0;
-fx-background-radius: 0;
-fx-cursor: text;
-fx-padding: 2px;
}
.text-input:focused {
-fx-highlight-fill: -fx-accent;
-fx-border-color: -fx-focus-color;
-fx-border-width: 1px;
-fx-background-color: -fx-focus-color, #FFFFFF;
-fx-background-insets: 0, 1;
-fx-prompt-text-fill: -fx-control-inner-background;
}
/*******************************************************************************
* *
* PopupMenu *
* *
******************************************************************************/
.context-menu {
-fx-background-color: derive(-fx-background, -30%), -fx-background;
-fx-background-insets: 0, 1;
-fx-padding: 1px;
-fx-effect: dropshadow(three-pass-box, rgba(0.0,0.0,0.0,0.2), 2.0, 0.0, 3.0, 3.0);
}
.context-menu > .separator {
-fx-padding: 0.0em 0.333333em 0.0em 0.333333em; /* 0 4 0 4 */
}
/*******************************************************************************
* *
* MenuItem *
* *
******************************************************************************/
.menu-item {
-fx-background-color: transparent;
-fx-background-insets:0.0;
-fx-padding:0.2em 1em 0.2em 1em;
-fx-border-width: 0.0 0.0 0.0 0.0;
-fx-border-color:transparent;
}
.menu-item > .left-container {
-fx-padding: 0.458em 0.791em 0.458em 0.458em;
}
.menu-item > .graphic-container {
-fx-padding: 0em 0.333em 0em 0em;
}
.menu-item >.label {
-fx-padding: 0em 0.5em 0em 0em;
-fx-text-fill: -fx-text-base-color;
}
.menu-item:disabled > .label {
-fx-opacity: -fx-disabled-opacity;
}
.menu-item:focused {
-fx-background-color: -fx-focus-color, linear-gradient(to bottom, #DAECFC 0%, #C4E0FC 100%);
-fx-background-insets: 0, 1;
}
.menu-item > .right-container {
-fx-padding: 0em 0em 0em 0.5em;
}
.menu-item:show-mnemonics > .mnemonic-underline {
-fx-stroke: -fx-text-fill;
}
.menu > .right-container > .arrow {
-fx-padding: 0.458em 0.167em 0.458em 0.167em; /* 4.5 2 4.5 2 */
-fx-background-color: -fx-color;
-fx-shape: "M0,-4L4,0L0,4Z";
-fx-scale-shape: false;
}
.menu:selected > .right-container > .arrow {
-fx-background-color: white;
}
.menu-item:disabled {
-fx-opacity: -fx-disabled-opacity;
}
/*******************************************************************************
* *
* ComboBox *
* *
******************************************************************************/
/* Customie the ListCell that appears in the ComboBox button itself */
.combo-box > .list-cell {
-fx-background: transparent;
-fx-background-color: transparent;
-fx-text-fill: -fx-text-base-color;
-fx-padding: 0.1em;
}
.combo-box-base > .arrow-button {
-fx-background-color: transparent;
-fx-padding: 0 0.1em 0 0.1em;
}
.combo-box-popup > .list-view {
-fx-background-color: #606060, white;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow(three-pass-box, rgba(0.0,0.0,0.0,0.2), 2.0, 0.0, 3.0, 3.0);
}
.combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell {
-fx-background-color: transparent;
-fx-padding:0.1em;
-fx-border-color:transparent;
}
.combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected,
.combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected:hover {
-fx-background-color: -fx-focus-color, linear-gradient(to bottom, #DAECFC 0%, #C4E0FC 100%);
-fx-background-insets: 0, 1;
}
/*******************************************************************************
* *
* SplitPane *
* *
******************************************************************************/
.split-pane > .split-pane-divider {
-fx-padding: 0 0.25em 0 0.25em; /* 0 3 0 3 */
}
/*******************************************************************************
* *
* ListView and ListCell *
* *
******************************************************************************/
.list-view > .virtual-flow > .scroll-bar:vertical{
-fx-background-insets: 0, 0 0 0 1;
-fx-padding: -1 -1 -1 0;
}
.list-view > .virtual-flow > .scroll-bar:horizontal{
-fx-background-insets: 0, 1 0 0 0;
-fx-padding: 0 -1 -1 -1;
}
.list-view > .virtual-flow > .corner {
-fx-background-color: -fx-box-border, -fx-base;
-fx-background-insets: 0, 1 0 0 1;
}
.list-cell {
-fx-background-color: -fx-control-inner-background;
-fx-padding: 0.8em 0.5em 0.8em 0.5em;
-fx-text-fill: -fx-text-inner-color;
-fx-opacity: 1;
}
.list-view:focused > .virtual-flow > .clipped-container > .sheet > .list-cell:focused {
-fx-background-color: -fx-focus-color, -fx-cell-focus-inner-border, -fx-control-inner-background;
-fx-background-insets: 0, 1, 2;
}
.list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected {
-fx-background-color: #DEDEDE, #F7F7F7;
-fx-background-insets: 0, 1;
}
.list-cell:filled:hover {
-fx-background-color: #F7F7F7;
}
/*******************************************************************************
* *
* Tooltip *
* *
******************************************************************************/
.tooltip {
-fx-background-color: -fx-background;
-fx-padding: 0.2em 0.4em 0.2em 0.4em;
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.8), 2, 0, 0, 0);
-fx-font-size: 0.8em;
}
/*******************************************************************************
* *
* Charts *
* *
******************************************************************************/
.chart {
-fx-padding: 5px;
}
.chart-content {
-fx-padding: 10px;
}
.chart-title {
-fx-font-size: 1.4em;
}
.chart-legend {
-fx-background-color: linear-gradient(to bottom, derive(-fx-background, -10%), derive(-fx-background, -5%)),
linear-gradient(from 0px 0px to 0px 5px, derive(-fx-background, -5%), derive(-fx-background, 20%));
-fx-background-insets: 0,1;
-fx-background-radius: 6,5;
-fx-padding: 6px;
}
/*******************************************************************************
* *
* Axis *
* *
******************************************************************************/
.axis {
AXIS_COLOR: derive(-fx-background,-20%);
-fx-tick-label-font-size: 0.833333em; /* 10px */
-fx-tick-label-fill: derive(-fx-text-background-color, 30%);
}
.axis:top {
-fx-border-color: transparent transparent AXIS_COLOR transparent;
}
.axis:right {
-fx-border-color: transparent transparent transparent AXIS_COLOR;
}
.axis:bottom {
-fx-border-color: AXIS_COLOR transparent transparent transparent;
}
.axis:left {
-fx-border-color: transparent AXIS_COLOR transparent transparent;
}
.axis-tick-mark,
.axis-minor-tick-mark {
-fx-fill: null;
-fx-stroke: AXIS_COLOR;
}
/*******************************************************************************
* *
* ChartPlot *
* *
******************************************************************************/
.chart-vertical-grid-lines {
-fx-stroke: derive(-fx-background,-10%);
-fx-stroke-dash-array: 0.25em, 0.25em;
}
.chart-horizontal-grid-lines {
-fx-stroke: derive(-fx-background,-10%);
}
.chart-alternative-column-fill {
-fx-fill: null;
-fx-stroke: null;
}
.chart-alternative-row-fill {
-fx-fill: null;
-fx-stroke: null;
}
.chart-vertical-zero-line,
.chart-horizontal-zero-line {
-fx-stroke: derive(-fx-text-background-color, 40%);
}
/*******************************************************************************
* *
* LineChart *
* *
******************************************************************************/
.chart-line-symbol {
-fx-background-color: #f9d900, white;
-fx-background-insets: 0, 2;
-fx-background-radius: 5px;
-fx-padding: 5px;
}
.chart-series-line {
-fx-stroke: #f9d900;
-fx-stroke-width: 3px;
/*-fx-effect: dropshadow( two-pass-box , rgba(0,0,0,0.3) , 8, 0.0 , 0 , 3 );*/
}
.default-color0.chart-line-symbol { -fx-background-color: CHART_COLOR_1, white; }
.default-color1.chart-line-symbol { -fx-background-color: CHART_COLOR_2, white; }
.default-color2.chart-line-symbol { -fx-background-color: CHART_COLOR_3, white; }
.default-color3.chart-line-symbol { -fx-background-color: CHART_COLOR_4, white; }
.default-color4.chart-line-symbol { -fx-background-color: CHART_COLOR_5, white; }
.default-color5.chart-line-symbol { -fx-background-color: CHART_COLOR_6, white; }
.default-color6.chart-line-symbol { -fx-background-color: CHART_COLOR_7, white; }
.default-color7.chart-line-symbol { -fx-background-color: CHART_COLOR_8, white; }
.default-color0.chart-series-line { -fx-stroke: CHART_COLOR_1; }
.default-color1.chart-series-line { -fx-stroke: CHART_COLOR_2; }
.default-color2.chart-series-line { -fx-stroke: CHART_COLOR_3; }
.default-color3.chart-series-line { -fx-stroke: CHART_COLOR_4; }
.default-color4.chart-series-line { -fx-stroke: CHART_COLOR_5; }
.default-color5.chart-series-line { -fx-stroke: CHART_COLOR_6; }
.default-color6.chart-series-line { -fx-stroke: CHART_COLOR_7; }
.default-color7.chart-series-line { -fx-stroke: CHART_COLOR_8; }
/*******************************************************************************
* *
* Combinations *
* *
* This section is for special handling of when one control is nested inside *
* another control. There are many cases where we would end up with ugly *
* double borders that are fixed here. *
* *
******************************************************************************/
.split-pane > * > .table-view { -fx-padding: 0px; }
.split-pane > * > .list-view { -fx-padding: 0px; }
.split-pane > * > .tree-view { -fx-padding: 0px; }
.split-pane > * > .scroll-pane { -fx-padding: 0px; }
.split-pane > * > .split-pane {
-fx-background-insets: 0, 0;
-fx-padding: 0;
}
/* ############################################################################
# Workaround for RT-27627 #
############################################################################ */
.choice-box > .open-button > .arrow { doh: true; }
.split-menu-button:openvertically > .arrow-button > .arrow { doh: true; }
.tab-pane > .tab-header-area > .control-buttons-tab > .container > .tab-down-button > .arrow { doh: true; }
.tree-table-view { doh: true; }
.tree-table-view:focused { doh: true; }
.tree-table-view > .virtual-flow > .scroll-bar:vertical { doh: true; }
.tree-table-view > .virtual-flow > .scroll-bar:horizontal { doh: true; }
.tree-table-view > .virtual-flow > .corner { doh: true; }
.tree-table-row-cell:focused { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:focused:selected { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:selected > .tree-table-cell { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:selected { doh: true; }
.tree-table-view:row-selection:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:focused:selected:hover{ doh: true; }
.tree-table-row-cell:filled:selected:focused { doh: true; }
.tree-table-row-cell:filled:selected { doh: true; }
.tree-table-row-cell:selected:disabled { doh: true; }
.tree-table-view:row-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:hover { doh: true; }
.tree-table-view:row-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:focused:hover { doh: true; }
.tree-table-view:constrained-resize > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell > .tree-table-cell:last-visible { doh: true; }
.tree-table-view:constrained-resize > .column-header:last-visible { doh: true; }
.tree-table-view:constrained-resize .filler { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell > .tree-table-cell:focused { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled .tree-table-cell:focused:selected { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:selected { doh: true; }
.tree-table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:hover:selected { doh: true; }
.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:focused:selected:hover{ doh: true; }
.tree-table-row-cell:filled > .tree-table-cell:selected:focused { doh: true; }
.tree-table-row-cell:filled > .tree-table-cell:selected { doh: true; }
.tree-table-cell:selected:disabled { doh: true; }
.tree-table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:hover { doh: true; }
.tree-table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .tree-table-cell:focused:hover { doh: true; }
.tree-table-view .column-resize-line { doh: true; }
.tree-table-view > .column-header-background { doh: true; }
.tree-table-view .column-header { doh: true; }
.tree-table-view .filler { doh: true; }
.tree-table-view .column-header .sort-order{ doh: true; }
.tree-table-view > .column-header-background > .show-hide-columns-button{ doh: true; }
.tree-table-view .show-hide-column-image { doh: true; }
.tree-table-view .column-drag-header { doh: true; }
.tree-table-view .column-overlay { doh: true; }
.tree-table-view /*> column-header-background > nested-column-header >*/ .arrow { doh: true; }
.tree-table-view .empty-table { doh: true; }
.axis-minor-tick-mark { doh: true; }
.chart-horizontal-zero-line { doh: true; }
.stacked-bar-chart:horizontal .chart-bar { doh: true; }

View File

@@ -0,0 +1,55 @@
<?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.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import org.cryptomator.ui.controls.*?>
<?import javafx.scene.layout.HBox?>
<?import org.cryptomator.ui.controls.SecPasswordField?>
<?import javafx.scene.control.TextField?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.InitializeController" xmlns:fx="http://javafx.com/fxml">
<padding>
<Insets top="24.0" right="24.0" bottom="24.0" left="24.0" />
</padding>
<columnConstraints>
<ColumnConstraints percentWidth="38.2"/>
<ColumnConstraints percentWidth="61.8"/>
</columnConstraints>
<children>
<!-- Row 0 -->
<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%initialize.label.username" />
<TextField fx:id="usernameField" GridPane.rowIndex="0" GridPane.columnIndex="1" />
<!-- Row 1 -->
<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%initialize.label.password" />
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="1" GridPane.columnIndex="1" disable="true" />
<!-- Row 2 -->
<Label GridPane.rowIndex="2" GridPane.columnIndex="0" text="%initialize.label.retypePassword" />
<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="2" GridPane.columnIndex="1" disable="true" />
<!-- Row 3 -->
<Button fx:id="okButton" defaultButton="true" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" text="%initialize.button.ok" prefWidth="150.0" onAction="#initializeVault" focusTraversable="false" disable="true" />
<!-- Row 4 -->
<ProgressIndicator progress="-1" fx:id="progressIndicator" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="CENTER" visible="false"/>
<!-- Row 5 -->
<Label fx:id="messageLabel" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="2" />
</children>
</GridPane>

View File

@@ -0,0 +1,53 @@
<?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.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.control.ToolBar?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ContextMenu?>
<?import javafx.scene.control.MenuItem?>
<HBox fx:id="rootPane" prefHeight="400.0" prefWidth="600.0" fx:controller="org.cryptomator.ui.MainController" xmlns:fx="http://javafx.com/fxml">
<fx:define>
<fx:include fx:id="welcomeView" source="welcome.fxml" />
<ContextMenu fx:id="directoryContextMenu">
<items>
<MenuItem text="%main.directoryList.contextMenu.remove" onAction="#didClickRemoveSelectedEntry" />
<!-- TODO: -->
<MenuItem text="%main.directoryList.contextMenu.addUser" disable="true" />
<MenuItem text="%main.directoryList.contextMenu.changePassword" disable="true" />
</items>
</ContextMenu>
</fx:define>
<children>
<VBox prefWidth="200.0">
<children>
<ListView fx:id="directoryList" VBox.vgrow="ALWAYS" focusTraversable="false" />
<ToolBar VBox.vgrow="NEVER">
<items>
<Button text="+" onAction="#didClickAddDirectory" />
</items>
</ToolBar>
</children>
</VBox>
<Pane fx:id="contentPane">
<children>
<fx:reference source="welcomeView" />
</children>
</Pane>
</children>
</HBox>

View File

@@ -0,0 +1,51 @@
<?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.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import java.lang.String?>
<?import org.cryptomator.ui.controls.SecPasswordField?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ProgressIndicator?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.UnlockController" xmlns:fx="http://javafx.com/fxml">
<padding>
<Insets top="24.0" right="24.0" bottom="24.0" left="24.0" />
</padding>
<columnConstraints>
<ColumnConstraints percentWidth="38.2"/>
<ColumnConstraints percentWidth="61.8"/>
</columnConstraints>
<children>
<!-- Row 0 -->
<Label text="%unlock.label.username" GridPane.rowIndex="0" GridPane.columnIndex="0" />
<ComboBox fx:id="usernameBox" GridPane.rowIndex="0" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" promptText="$access.label.username" />
<!-- Row 1 -->
<Label text="%unlock.label.password" GridPane.rowIndex="1" GridPane.columnIndex="0" />
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
<!-- Row 2 -->
<Button fx:id="unlockButton" text="%unlock.button.unlock" defaultButton="true" GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickUnlockButton"/>
<!-- Row 3 -->
<ProgressIndicator progress="-1" fx:id="progressIndicator" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="CENTER" visible="false"/>
<!-- Row 5 -->
<Label fx:id="messageLabel" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="2" />
</children>
</GridPane>

View File

@@ -0,0 +1,46 @@
<?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.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import java.lang.String?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.chart.LineChart?>
<?import javafx.scene.chart.NumberAxis?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.UnlockedController" xmlns:fx="http://javafx.com/fxml">
<padding>
<Insets top="24.0" right="24.0" bottom="24.0" left="24.0" />
</padding>
<columnConstraints>
<ColumnConstraints percentWidth="38.2"/>
<ColumnConstraints percentWidth="61.8"/>
</columnConstraints>
<children>
<!-- Row 0 -->
<Label fx:id="messageLabel" GridPane.rowIndex="0" GridPane.columnIndex="0" GridPane.columnSpan="2" />
<!-- 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"/>
<!-- Row 2 -->
<LineChart fx:id="ioGraph" GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" animated="false" createSymbols="false" prefHeight="300.0" legendVisible="true" legendSide="BOTTOM" verticalZeroLineVisible="false" verticalGridLinesVisible="false" horizontalGridLinesVisible="true">
<xAxis><NumberAxis fx:id="xAxis" forceZeroInRange="false" tickMarkVisible="false" minorTickVisible="false" tickLabelsVisible="false" autoRanging="false"/></xAxis>
<yAxis><NumberAxis label="%unlocked.ioGraph.yAxis.label" autoRanging="true" forceZeroInRange="true" /></yAxis>
</LineChart>
</children>
</GridPane>

View File

@@ -0,0 +1,33 @@
<?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.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import java.lang.String?>
<?import javafx.scene.shape.Arc?>
<?import javafx.scene.shape.QuadCurve?>
<?import javafx.scene.shape.Path?>
<?import javafx.scene.shape.Line?>
<AnchorPane xmlns:fx="http://javafx.com/fxml">
<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"/>
<QuadCurve AnchorPane.leftAnchor="0.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="0.0" AnchorPane.topAnchor="370.0" startX="0.0" endX="10.0" startY="10.0" endY="0.0" strokeWidth="2.0"/>
<Line AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="380.0" startX="0.0" endX="10.0" startY="0.0" endY="10.0" strokeWidth="2.0"/>
</children>
</AnchorPane>

View File

@@ -1,59 +0,0 @@
<?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.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import org.cryptomator.ui.controls.*?>
<GridPane fx:id="rootGridPane" fx:controller="org.cryptomator.ui.InitializeController" xmlns:fx="http://javafx.com/fxml" styleClass="root" gridLinesVisible="false" vgap="5" hgap="5" prefWidth="480">
<stylesheets>
<URL value="@panels.css" />
</stylesheets>
<padding>
<Insets top="10" right="10" bottom="10" left="10" />
</padding>
<columnConstraints>
<ColumnConstraints minWidth="150" maxWidth="150" hgrow="NEVER" />
<ColumnConstraints minWidth="200" hgrow="ALWAYS" />
<ColumnConstraints minWidth="50" maxWidth="120" hgrow="NEVER" />
</columnConstraints>
<children>
<!-- Row 0 -->
<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%initialize.label.workDir" GridPane.halignment="RIGHT" />
<TextField fx:id="workDirTextField" GridPane.rowIndex="0" GridPane.columnIndex="1" editable="true" />
<Button fx:id="chooseWorkDirButton" GridPane.rowIndex="0" GridPane.columnIndex="2" text="%initialize.button.chooseWorkDir" onAction="#chooseWorkDir" focusTraversable="false" />
<!-- Row 1 -->
<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%initialize.label.username" GridPane.halignment="RIGHT" />
<TextField fx:id="usernameField" GridPane.rowIndex="1" GridPane.columnIndex="1" disable="true" />
<!-- Row 2 -->
<Label GridPane.rowIndex="2" GridPane.columnIndex="0" text="%initialize.label.password" GridPane.halignment="RIGHT" />
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="2" GridPane.columnIndex="1" disable="true" />
<!-- Row 3 -->
<Label GridPane.rowIndex="3" GridPane.columnIndex="0" text="%initialize.label.retypePassword" GridPane.halignment="RIGHT" />
<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="3" GridPane.columnIndex="1" disable="true" />
<!-- Row 4 -->
<Button fx:id="initWorkDirButton" text="%initialize.button.initWorkDir" GridPane.rowIndex="4" GridPane.columnIndex="1" defaultButton="true" onAction="#initWorkDir" disable="true" focusTraversable="false"/>
<!-- Row 5 -->
<Label fx:id="messageLabel" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="5" textOverrun="ELLIPSIS" />
</children>
</GridPane>

View File

@@ -6,29 +6,48 @@
# Contributors:
# Sebastian Stenzel - initial API and implementation
#-------------------------------------------------------------------------------
app.name=Cryptomator
# main.fxml
toolbarbutton.initialize=Initialize Vault
toolbarbutton.access=Access Vault
main.directoryList.contextMenu.remove=Remove from list
main.directoryList.contextMenu.addUser=Add user
main.directoryList.contextMenu.changePassword=Change password
# welcome.fxml
welcome.welcomeLabel=Welcome to Cryptomator
welcome.addButtonInstructionLabel=Start by adding a new vault :-)
# initialize.fxml
initialize.label.workDir=New vault location
initialize.button.chooseWorkDir=Choose...
initialize.label.username=Username
initialize.label.password=Password
initialize.label.retypePassword=Retype
initialize.button.initWorkDir=Initialize Vault
initialize.messageLabel.alreadyInitialized=Vault in this location already exists.
initialize.messageLabel.invalidPath=Invalid vault location.
initialize.label.retypePassword=Retype password
initialize.button.ok=Create vault
initialize.alert.directoryIsNotEmpty.title=The chosen directory is not empty
initialize.alert.directoryIsNotEmpty.content=All existing files inside this directory will get encrypted. Continue?
# access.fxml
access.label.workDir=Vault location
access.label.username=Username
access.label.password=Password
access.button.chooseWorkDir=Choose...
access.button.startServer=Start Server
access.button.stopServer=Stop Server
access.messageLabel.wrongPassword=Wrong password.
access.messageLabel.invalidStorageLocation=Vault directory invalid.
access.messageLabel.decryptionFailed=Decryption failed.
access.messageLabel.unsupportedKeyLengthInstallJCE=Decryption failed. Please install Oracle JCE.
access.messageLabel.mountFailed=Mounting WebDAV share (Port %d) failed.
# unlock.fxml
unlock.label.username=Username
unlock.label.password=Password
unlock.button.unlock=Unlock vault
unlock.errorMessage.wrongPassword=Wrong password.
unlock.errorMessage.decryptionFailed=Decryption failed.
unlock.errorMessage.unsupportedKeyLengthInstallJCE=Decryption failed. Please install Oracle JCE.
unlock.messageLabel.startServerFailed=Starting WebDAV server failed.
# unlocked.fxml
unlocked.messageLabel.runningOnPort=Vault is accessible via WebDAV on local port %d.
unlocked.button.lock=Lock vault
unlocked.ioGraph.yAxis.label=Throughput (MiB/s)
# tray icon
tray.menu.open=Open
tray.menu.quit=Quit
tray.infoMsg.title=Still running
tray.infoMsg.msg=Cryptomator is still alive. Quit it from the tray icon.
tray.infoMsg.msg.osx=Cryptomator is still alive. Quit it from the menu bar icon.

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright (c) 2014 Markus Kreusch
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Markus Kreusch - switched to log4j 2
-->
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
</Console>
<Console name="StdErr" target="SYSTEM_ERR">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
</Console>
</Appenders>
<Loggers>
<!-- show our own debug messages: -->
<Logger name="org.cryptomator" level="DEBUG"/>
<!-- mute dependencies: -->
<Root level="INFO">
<AppenderRef ref="Console" />
<AppenderRef ref="StdErr" />
</Root>
</Loggers>
</Configuration>

View File

@@ -1,32 +0,0 @@
@CHARSET "US-ASCII";
.text {
-fx-font-smoothing-type: lcd;
}
.tool-bar {
-fx-background-color: linear-gradient(to bottom, #888888, #222222);
-fx-padding: 5.0 10.0 5.0 10.0;
-fx-border-color: #888888;
-fx-border-width: 1.0 0.0 1.0 0.0;
-fx-border-insets: 0.0;
-fx-alignment: CENTER;
}
.tool-bar .toggle-button {
-fx-text-fill: #FFFFFF;
-fx-background-color: linear-gradient(to bottom, #888888, #222222);
-fx-border-color: #888888;
-fx-background-insets: 0.0, 1.0;
-fx-background-radius: 4.0, 4.0;
-fx-border-radius: 3.0;
-fx-border-width: 0.5;
-fx-font-family: "lucida-grande";
-fx-font-weight: bold;
}
.tool-bar .toggle-button:armed,
.tool-bar .toggle-button:selected {
-fx-background-color: linear-gradient(to bottom, #444444, #555555 30%, #000000);
-fx-border-color: #FFFFFF;
}

View File

@@ -1,40 +0,0 @@
<?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.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<VBox fx:id="rootVBox" fx:controller="org.cryptomator.ui.MainController" xmlns:fx="http://javafx.com/fxml">
<stylesheets>
<URL value="@main.css" />
</stylesheets>
<fx:define>
<fx:include fx:id="initializePanel" source="initialize.fxml" />
<fx:include fx:id="accessPanel" source="access.fxml" />
</fx:define>
<children>
<ToolBar>
<items>
<fx:define>
<ToggleGroup fx:id="toolbarButtonGroup" />
</fx:define>
<ToggleButton text="%toolbarbutton.initialize" toggleGroup="$toolbarButtonGroup" onAction="#showInitializePane" />
<ToggleButton text="%toolbarbutton.access" toggleGroup="$toolbarButtonGroup" onAction="#showAccessPane" selected="true" />
</items>
</ToolBar>
<fx:reference source="accessPanel"/>
</children>
</VBox>

View File

@@ -1,62 +0,0 @@
@CHARSET "US-ASCII";
.root {
-fx-background-color: linear-gradient(to bottom, #FFFFFF, #DDDDDD);
}
.text {
-fx-font-smoothing-type: lcd;
}
.label {
-fx-alignment: CENTER;
-fx-font-family: "lucida-grande";
}
.button,
.combo-box {
-fx-text-fill: #000000;
-fx-background-color: linear-gradient(to bottom, #FFFFFF, #DDDDDD);
-fx-border-color: #888888;
-fx-background-insets: 0.0, 1.0;
-fx-background-radius: 4.0, 4.0;
-fx-border-radius: 3.0;
-fx-border-width: 0.5;
-fx-font-family: "lucida-grande";
-fx-font-weight: normal;
}
.text-field {
-fx-border-radius: 3.0;
-fx-border-width: 0.5;
-fx-border-color: #888888;
-fx-focus-color: #FF0000;
-fx-background-color: transparent;
-fx-padding: 5 2 5 2;
}
.text-field:focused {
-fx-background-color: #DDDDDD;
}
.button:armed,
.button:selected,
.combo-box:armed,
.combo-box:selected {
-fx-background-color: linear-gradient(to bottom, #DDDDDD, #CCCCCC 30%, #EEEEEE);
}
.combo-box .list-cell {
-fx-background-color: transparent;
-fx-text-fill: -fx-text-base-color;
}
.combo-box .list-cell:hover {
-fx-background-color: #DDDDDD;
}
.combo-box-popup .list-view {
-fx-padding: 0 0 0 0;
-fx-background-insets: 0, 0;
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.6), 8, 0.0, 0, 0);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB