mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-15 09:11:29 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc15f2cdb4 | ||
|
|
b6546f24d5 | ||
|
|
5fe54634a9 | ||
|
|
2fdf9be017 | ||
|
|
1de2d9d2da | ||
|
|
3a5917ef53 | ||
|
|
d0f0c09585 | ||
|
|
884b894e04 | ||
|
|
ebb3207854 | ||
|
|
8abd5ebc01 | ||
|
|
ce197b3314 | ||
|
|
8ae7e95c41 | ||
|
|
6830861346 | ||
|
|
696b3412f2 | ||
|
|
e7ba6f5c92 | ||
|
|
8031b0c516 | ||
|
|
047e1fe1d6 | ||
|
|
75f49b88d6 | ||
|
|
891e79cdae | ||
|
|
b2f20f9a15 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,3 +10,4 @@
|
||||
.project
|
||||
.classpath
|
||||
target/
|
||||
test-output/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
60
main/pom.xml
60
main/pom.xml
@@ -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>
|
||||
|
||||
BIN
main/ui/package/linux/Cryptomator.png
Normal file
BIN
main/ui/package/linux/Cryptomator.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
BIN
main/ui/package/macosx/Cryptomator-background.png
Normal file
BIN
main/ui/package/macosx/Cryptomator-background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
BIN
main/ui/package/macosx/Cryptomator.icns
Normal file
BIN
main/ui/package/macosx/Cryptomator.icns
Normal file
Binary file not shown.
BIN
main/ui/package/windows/Cryptomator.ico
Normal file
BIN
main/ui/package/windows/Cryptomator.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 361 KiB |
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
156
main/ui/src/main/java/org/cryptomator/ui/UnlockController.java
Normal file
156
main/ui/src/main/java/org/cryptomator/ui/UnlockController.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
144
main/ui/src/main/java/org/cryptomator/ui/model/Directory.java
Normal file
144
main/ui/src/main/java/org/cryptomator/ui/model/Directory.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
119
main/ui/src/main/java/org/cryptomator/ui/util/TrayIconUtil.java
Normal file
119
main/ui/src/main/java/org/cryptomator/ui/util/TrayIconUtil.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
33
main/ui/src/main/resources/log4j2.xml
Normal file
33
main/ui/src/main/resources/log4j2.xml
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
BIN
main/ui/src/main/resources/tray_icon.png
Normal file
BIN
main/ui/src/main/resources/tray_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
47
main/ui/src/main/resources/unlock.fxml
Normal file
47
main/ui/src/main/resources/unlock.fxml
Normal 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>
|
||||
|
||||
|
||||
38
main/ui/src/main/resources/unlocked.fxml
Normal file
38
main/ui/src/main/resources/unlocked.fxml
Normal 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>
|
||||
|
||||
|
||||
26
main/ui/src/main/resources/welcome.fxml
Normal file
26
main/ui/src/main/resources/welcome.fxml
Normal 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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user