Compare commits

..

20 Commits
0.1.0 ... 0.2.0

Author SHA1 Message Date
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
48 changed files with 1732 additions and 950 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 run OS X and want to take a look at the current alpha version, go ahead and [download Cryptomator.dmg](https://github.com/totalvoidness/cryptomator/releases/download/v0.1.0/Cryptomator.dmg).
## 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

View File

@@ -12,13 +12,13 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.1.0</version>
<version>0.2.0</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>
@@ -71,18 +65,16 @@
</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>
<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>
</build>
</project>

View File

@@ -0,0 +1,78 @@
package org.cryptomator.files;
import java.io.IOException;
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 file, BasicFileAttributes attrs) throws IOException {
if (encryptionDecider.shouldEncrypt(file)) {
final String plaintext = file.getFileName().toString();
final String encrypted = cryptor.encryptPath(plaintext, '/', '/', this);
final Path newPath = file.resolveSibling(encrypted);
Files.move(file, newPath, StandardCopyOption.ATOMIC_MOVE);
}
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,30 +18,34 @@ 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 {
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 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);
@@ -52,20 +59,22 @@ public final class WebDAVServer {
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);
}
@@ -79,4 +88,8 @@ public final class WebDAVServer {
return result;
}
public int getPort() {
return port;
}
}

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 {
public 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) {
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

@@ -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.2.0</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,24 @@
<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>
<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>
</build>
</project>

View File

@@ -26,6 +26,8 @@ import java.security.spec.KeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.zip.CRC32;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
@@ -40,14 +42,17 @@ import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.io.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
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 {
@@ -73,6 +78,11 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
*/
private static final int AES_KEY_LENGTH;
/**
*
*/
private static final byte[] EMPTY_MASTER_KEY = new byte[MASTER_KEY_LENGTH];
/**
* Jackson JSON-Mapper.
*/
@@ -82,7 +92,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
* The decrypted master key. Its lifecycle starts with {@link #randomData(int)} or {@link #encryptMasterKey(Path, CharSequence)}. Its
* lifecycle ends with {@link #swipeSensitiveData()}.
*/
private final byte[] masterKey = new byte[MASTER_KEY_LENGTH];
private final byte[] masterKey = Arrays.copyOf(EMPTY_MASTER_KEY, 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;
@@ -110,6 +120,9 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
* Encrypts the current masterKey with the given password and writes the result to the given output stream.
*/
public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException {
if (ArrayUtils.isEquals(this.masterKey, EMPTY_MASTER_KEY)) {
throw new IllegalStateException("Masterkey not yet initialized.");
}
try {
// derive key:
final byte[] userSalt = randomData(SALT_LENGTH);
@@ -245,76 +258,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 = String.valueOf(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

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

@@ -17,13 +17,16 @@ import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.io.FileUtils;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
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.Assert;
import org.junit.Before;
import org.junit.Test;
@@ -47,11 +50,20 @@ public class Aes256CryptorTest {
/* ------------------------------------------------------------------------------- */
@Test(expected = IllegalStateException.class)
public void testUninitializedMasterKey() 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);
}
@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);
cryptor.randomizeMasterKey();
cryptor.encryptMasterKey(out, pw);
cryptor.swipeSensitiveData();
@@ -65,6 +77,7 @@ public class Aes256CryptorTest {
final String pw = "asd";
final Aes256Cryptor cryptor = new Aes256Cryptor();
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
cryptor.randomizeMasterKey();
cryptor.encryptMasterKey(out, pw);
cryptor.swipeSensitiveData();
@@ -79,6 +92,7 @@ public class Aes256CryptorTest {
final String pw = "asd";
final Aes256Cryptor cryptor = new Aes256Cryptor();
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
cryptor.randomizeMasterKey();
cryptor.encryptMasterKey(out, pw);
cryptor.swipeSensitiveData();
@@ -93,6 +107,7 @@ public class Aes256CryptorTest {
final String pw = "asd";
final Aes256Cryptor cryptor = new Aes256Cryptor();
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
cryptor.randomizeMasterKey();
cryptor.encryptMasterKey(out, pw);
cryptor.swipeSensitiveData();
@@ -101,4 +116,40 @@ public class Aes256CryptorTest {
cryptor.swipeSensitiveData();
}
@Test
public void testEncryptionOfFilenames() throws IOException {
final CryptorIOSupport ioSupportMock = new CryptoIOSupportMock();
final Aes256Cryptor cryptor = new Aes256Cryptor();
cryptor.randomizeMasterKey();
// 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);
}
}
}

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.1.0</version>
<version>0.2.0</version>
</parent>
<artifactId>crypto-api</artifactId>
<name>Cryptomator cryptographic module API</name>
@@ -25,18 +25,16 @@
</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>
<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>
</build>
</project>

View File

@@ -32,7 +32,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 +46,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.

View File

@@ -0,0 +1,23 @@
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

@@ -1,25 +1,21 @@
<?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
-->
<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">
<!-- 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.2.0</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 +28,7 @@
<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>
<junit.version>4.11</junit.version>
<commons-io.version>2.4</commons-io.version>
<commons-collections.version>4.0</commons-collections.version>
@@ -64,22 +59,22 @@
<artifactId>ui</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-jul</artifactId>
<version>${log4j.version}</version>
</dependency>
<!-- commons -->
@@ -121,6 +116,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>

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.2.0</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,82 @@
<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-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<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="true" />
<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,178 +8,137 @@
******************************************************************************/
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 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.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.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 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) {
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 {
masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
cryptor.encryptMasterKey(masterKeyOutputStream, password);
cryptor.swipeSensitiveData();
workDirTextField.clear();
directory.getCryptor().randomizeMasterKey();
directory.getCryptor().encryptMasterKey(masterKeyOutputStream, password);
encryptExistingContents();
directory.getCryptor().swipeSensitiveData();
if (listener != null) {
listener.didInitialize(this);
}
} catch (FileAlreadyExistsException ex) {
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
} catch (InvalidPathException ex) {
@@ -187,14 +146,65 @@ public class InitializeController implements Initializable {
} catch (IOException ex) {
LOG.error("I/O Exception", ex);
} finally {
swipePasswordFields();
usernameField.setText(null);
passwordField.swipe();
retypePasswordField.swipe();
IOUtils.closeQuietly(masterKeyOutputStream);
}
}
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(localization.getString("initialize.alert.directoryIsNotEmpty.header"));
alert.setContentText(localization.getString("initialize.alert.directoryIsNotEmpty.content"));
private void swipePasswordFields() {
passwordField.swipe();
retypePasswordField.swipe();
final Optional<ButtonType> result = alert.showAndWait();
return ButtonType.OK.equals(result.get());
}
private void encryptExistingContents() throws IOException {
final FileVisitor<Path> visitor = new EncryptingFileVisitor(directory.getPath(), directory.getCryptor(), this::shouldEncryptExistingFile);
Files.walkFileTree(directory.getPath(), visitor);
}
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,78 @@ 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.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);
final ResourceBundle rb = ResourceBundle.getBundle("localization");
final FXMLLoader loader = new FXMLLoader(getClass().getResource("/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 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,169 @@
******************************************************************************/
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.ListChangeListener;
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.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 HBox rootPane;
@FXML
private VBox rootVBox;
private ListView<Directory> directoryList;
@FXML
private Pane initializePanel;
private Pane contentPane;
@FXML
private Pane accessPanel;
private ResourceBundle rb;
@FXML
private Pane advancedPanel;
@FXML
protected void showInitializePane(ActionEvent event) {
showPanel(initializePanel);
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
directoryList.setCellFactory(this::createDirecoryListCell);
directoryList.getSelectionModel().getSelectedItems().addListener(this::selectedDirectoryDidChange);
directoryList.getItems().addAll(Settings.load().getDirectories());
}
@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);
Settings.load().getDirectories().clear();
Settings.load().getDirectories().addAll(directoryList.getItems());
directoryList.getSelectionModel().selectLast();
}
}
@FXML
protected void showAdvancedPane(ActionEvent event) {
showPanel(advancedPanel);
private ListCell<Directory> createDirecoryListCell(ListView<Directory> param) {
return new DirectoryListCell();
}
private void showPanel(Pane panel) {
rootVBox.getChildren().remove(1);
rootVBox.getChildren().add(panel);
rootVBox.getScene().getWindow().sizeToScene();
private void selectedDirectoryDidChange(ListChangeListener.Change<? extends Directory> change) {
final Directory selectedDir = directoryList.getSelectionModel().getSelectedItem();
stage.setTitle(selectedDir.getName());
showDirectory(selectedDir);
}
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);
}
}
// ****************************************
// Subcontroller for right panel
// ****************************************
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 showInitializeView(Directory directory) {
final InitializeController ctrl = showView("/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("/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("/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,156 @@
/*******************************************************************************
* 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 javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
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.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 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
protected void didClickUnlockButton(ActionEvent event) {
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 {
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);
directory.mount();
if (listener != null) {
listener.didUnlock(this);
}
} catch (DecryptFailedException | IOException ex) {
messageLabel.setText(rb.getString("unlock.errorMessage.decryptionFailed"));
LOG.error("Decryption failed for technical reasons.", ex);
} catch (WrongPasswordException e) {
messageLabel.setText(rb.getString("unlock.errorMessage.wrongPassword"));
} catch (UnsupportedKeyLengthException ex) {
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 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);
}
}
/* 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,72 @@
/*******************************************************************************
* 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.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import org.cryptomator.ui.model.Directory;
public class UnlockedController implements Initializable {
private ResourceBundle rb;
private LockListener listener;
private Directory directory;
@FXML
private Label messageLabel;
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
}
@FXML
protected void closeVault(ActionEvent event) {
directory.unmount();
directory.stopServer();
directory.setUnlocked(false);
if (listener != null) {
listener.didLock(this);
}
}
/* 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);
}
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,19 @@
package org.cryptomator.ui.controls;
import javafx.scene.control.ListCell;
import org.cryptomator.ui.model.Directory;
public class DirectoryListCell extends ListCell<Directory> {
@Override
protected void updateItem(Directory item, boolean empty) {
super.updateItem(item, empty);
if (item == null) {
setText(null);
} else {
setText(item.getName());
}
}
}

View File

@@ -0,0 +1,144 @@
package org.cryptomator.ui.model;
import java.io.IOException;
import java.io.Serializable;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.ui.MainApplication;
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;
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 Aes256Cryptor cryptor = new Aes256Cryptor();
private final Path path;
private boolean unlocked;
private String unmountCommand;
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 {
unmountCommand = WebDavMounter.mount(server.getPort());
return true;
} catch (CommandFailedException e) {
LOG.warn("mount failed", e);
return false;
}
}
public boolean unmount() {
try {
if (StringUtils.isNotEmpty(unmountCommand)) {
WebDavMounter.unmount(unmountCommand);
unmountCommand = 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 Aes256Cryptor getCryptor() {
return cryptor;
}
public boolean isUnlocked() {
return unlocked;
}
public void setUnlocked(boolean unlocked) {
this.unlocked = 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.Collection;
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 Collection<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 Collection<Directory> getDirectories() {
if (directories == null) {
directories = new ArrayList<>();
}
return directories;
}
public void setWebdavWorkDir(String webdavWorkDir) {
this.webdavWorkDir = webdavWorkDir;
public void setDirectories(Collection<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,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

@@ -10,6 +10,8 @@ package org.cryptomator.ui.util;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.SystemUtils;
@@ -19,35 +21,60 @@ 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 static final int CMD_DEFAULT_TIMEOUT = 3;
private static final Pattern WIN_MOUNT_DRIVELETTER_PATTERN = Pattern.compile("\\s*[A-Z]:\\s*");
private WebDavMounter() {
throw new IllegalStateException("not instantiable.");
}
public static void mount(int localPort) throws CommandFailedException {
/**
* @return Unmount Command
*/
public static synchronized String 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);
exec("mkdir /Volumes/Cryptomator" + localPort, CMD_DEFAULT_TIMEOUT);
exec("mount_webdav -S -v Cryptomator localhost:" + localPort + " /Volumes/Cryptomator" + localPort, CMD_DEFAULT_TIMEOUT);
exec("open /Volumes/Cryptomator" + localPort, CMD_DEFAULT_TIMEOUT);
return "umount /Volumes/Cryptomator" + localPort;
} else if (SystemUtils.IS_OS_WINDOWS) {
final String result = exec("net use * http://127.0.0.1:" + localPort + " /persistent:no", CMD_DEFAULT_TIMEOUT);
final Matcher matcher = WIN_MOUNT_DRIVELETTER_PATTERN.matcher(result);
if (matcher.find()) {
final String driveLetter = matcher.group();
return "net use " + driveLetter + " /delete";
}
} else if (SystemUtils.IS_OS_LINUX) {
// TODO check result of "which gvfs-mount" first and choose a good strategy. also refactor this class ;-)
exec("gvfs-mount dav://localhost:" + localPort, CMD_DEFAULT_TIMEOUT);
exec("xdg-open dav://localhost:" + localPort, CMD_DEFAULT_TIMEOUT);
return "gvfs-mount -u dav://localhost:" + localPort;
}
return null;
}
public static void unmount(String command) throws CommandFailedException {
if (command != null) {
exec(command, 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 {
private static String 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)) {
final Process proc;
if (SystemUtils.IS_OS_WINDOWS) {
proc = Runtime.getRuntime().exec(new String[] {"cmd", "/C", cmd});
} else {
proc = Runtime.getRuntime().exec(new String[] {"/bin/sh", "-c", cmd});
}
if (!proc.waitFor(timoutSeconds, TimeUnit.SECONDS)) {
proc.destroy();
throw new CommandFailedException("Timeout executing command " + cmd);
}
if (proc.exitValue() != 0) {
throw new CommandFailedException(IOUtils.toString(proc.getErrorStream()));
}
return IOUtils.toString(proc.getInputStream());
} catch (IOException | InterruptedException | IllegalThreadStateException e) {
LOG.error("Command execution failed.", e);
throw new CommandFailedException(e);

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>

View File

@@ -13,46 +13,39 @@
<?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 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>
<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="10" right="10" bottom="10" left="10" />
<Insets top="24.0" right="24.0" bottom="24.0" left="24.0" />
</padding>
<columnConstraints>
<ColumnConstraints minWidth="150" maxWidth="150" hgrow="NEVER" />
<ColumnConstraints minWidth="200" hgrow="ALWAYS" />
<ColumnConstraints minWidth="50" maxWidth="120" hgrow="NEVER" />
<ColumnConstraints percentWidth="38.2"/>
<ColumnConstraints percentWidth="61.8"/>
</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" />
<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.username" GridPane.halignment="RIGHT" />
<TextField fx:id="usernameField" GridPane.rowIndex="1" GridPane.columnIndex="1" disable="true" />
<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.password" GridPane.halignment="RIGHT" />
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="2" GridPane.columnIndex="1" disable="true" />
<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 -->
<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"/>
<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 5 -->
<Label fx:id="messageLabel" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="5" textOverrun="ELLIPSIS" />
<Label fx:id="messageLabel" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="2" />
</children>
</GridPane>

View File

@@ -6,29 +6,42 @@
# Contributors:
# Sebastian Stenzel - initial API and implementation
#-------------------------------------------------------------------------------
# main.fxml
toolbarbutton.initialize=Initialize Vault
toolbarbutton.access=Access Vault
app.name=Cryptomator
# welcome.fxml
welcome.welcomeLabel=Welcome to Cryptomator
# 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=Confirm
initialize.alert.directoryIsNotEmpty.header=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
# 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 +1,119 @@
@CHARSET "US-ASCII";
.root {
-fx-background-color: linear-gradient(to bottom, #FFFFFF, #DDDDDD);
}
.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);
.button,
.combo-box {
-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;
.text-field {
-fx-border-radius: 3.0;
-fx-border-width: 0.5;
}
.button.green,
.button.red,
.split-menu-button.green,
.split-menu-button.red {
-fx-background-radius: 3.0;
-fx-background-color: #FFFFFF;
-fx-background-insets: 1px 1px 1px 1px;
}
.button.green,
.button.red,
.split-menu-button.green > .label,
.split-menu-button.red > .label {
-fx-text-fill: #FFF;
-fx-alignment: CENTER;
-fx-font-weight: bold;
-fx-font-family: "lucida-grande";
}
.split-menu-button.green > .arrow-button > .arrow,
.split-menu-button.red > .arrow-button > .arrow {
-fx-background-color: #FFF;
}
.button.green,
.split-menu-button.green > .label,
.split-menu-button.green > .arrow-button {
-fx-background-color: linear-gradient(to bottom, #33EE55, #22AA33);
}
.button.green:hover,
.split-menu-button.green > .label:hover,
.split-menu-button.green > .arrow-button:hover {
-fx-background-color: linear-gradient(to bottom, #33EE55, #118822);
}
.button.green:armed,
.split-menu-button.green:armed > .label,
.split-menu-button.green > .arrow-button:pressed,
.split-menu-button.green:showing > .arrow-button {
-fx-background-color: linear-gradient(to bottom, #118822, #22AA33 20%, #33EE55);
}
.button.green:disabled,
.split-menu-button.green:disabled,
.split-menu-button.green:disabled > .label,
.split-menu-button.green:disabled > .arrow-button {
-fx-background-color: #22AA33;
}
.button.red,
.split-menu-button.red > .label,
.split-menu-button.red > .arrow-button {
-fx-background-color: linear-gradient(to bottom, #EE5533, #AA3322);
}
.button.red:hover,
.split-menu-button.red > .label:hover,
.split-menu-button.red > .arrow-button:hover {
-fx-background-color: linear-gradient(to bottom, #EE5533, #882211);
}
.button.red:armed,
.split-menu-button.red:armed > .label,
.split-menu-button.red > .arrow-button:pressed,
.split-menu-button.red:showing > .arrow-button {
-fx-background-color: linear-gradient(to bottom, #882211, #AA3322 20%, #EE5533);
}
.button.red:disabled,
.split-menu-button.red:disabled,
.split-menu-button.red:disabled > .label,
.split-menu-button.red:disabled > .arrow-button {
-fx-background-color: #AA3322;
}
.split-menu-button .menu-item:focused {
-fx-background-color: #CCC;
}
.split-menu-button .menu-item .label {
-fx-text-fill: #000000;
}
.text-field {
-fx-border-radius: 3.0;
-fx-border-width: 0.5;
-fx-border-color: #888888;
-fx-background-color: #FFFFFF;
-fx-padding: 4 2 4 2;
}
.text-field:focused {
-fx-background-color: #FFFFFF;
}

View File

@@ -7,34 +7,37 @@
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>
<?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?>
<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="initializePanel" source="initialize.fxml" />
<fx:include fx:id="accessPanel" source="access.fxml" />
<fx:include fx:id="welcomeView" source="welcome.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"/>
<VBox prefWidth="200.0">
<children>
<ListView fx:id="directoryList" VBox.vgrow="ALWAYS" />
<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>
</VBox>
</HBox>

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

View File

@@ -0,0 +1,47 @@
<?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?>
<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 text="%unlock.button.unlock" defaultButton="true" GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickUnlockButton" focusTraversable="false"/>
<!-- Row 5 -->
<Label fx:id="messageLabel" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="2" />
</children>
</GridPane>

View File

@@ -0,0 +1,38 @@
<?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?>
<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="#closeVault" focusTraversable="false"/>
</children>
</GridPane>

View File

@@ -0,0 +1,26 @@
<?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?>
<AnchorPane xmlns:fx="http://javafx.com/fxml">
<children>
<Label fx:id="messageLabel" AnchorPane.leftAnchor="100.0" AnchorPane.topAnchor="50.0" text="%welcome.welcomeLabel"/>
</children>
</AnchorPane>