Browse Source

- First public version

Sebastian Stenzel 11 years ago
parent
commit
8740e43b96
40 changed files with 3802 additions and 0 deletions
  1. 6 0
      .gitignore
  2. 54 0
      oce-main/oce-crypto/pom.xml
  3. 21 0
      oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/Cryptor.java
  4. 32 0
      oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/FilenamePseudonymizing.java
  5. 91 0
      oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/StorageCrypting.java
  6. 31 0
      oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/TransactionAwareFileAccess.java
  7. 585 0
      oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/AesCryptor.java
  8. 105 0
      oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Keys.java
  9. 79 0
      oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Metadata.java
  10. 157 0
      oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cache/PseudonymRepository.java
  11. 31 0
      oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cleartext/Metadata.java
  12. 246 0
      oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cleartext/NoCryptor.java
  13. 92 0
      oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/AesCryptorTest.java
  14. 88 0
      oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/FilenamePseudonymizerTest.java
  15. 50 0
      oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/PseudonymRepositoryTest.java
  16. 91 0
      oce-main/oce-ui/pom.xml
  17. 148 0
      oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/AccessController.java
  18. 76 0
      oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/AdvancedController.java
  19. 124 0
      oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/InitializeController.java
  20. 47 0
      oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/MainApplication.java
  21. 51 0
      oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/MainController.java
  22. 44 0
      oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/controls/SecPasswordField.java
  23. 213 0
      oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/controls/SecurePasswordField.java
  24. 117 0
      oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/settings/Settings.java
  25. 51 0
      oce-main/oce-ui/src/main/resources/access.fxml
  26. 38 0
      oce-main/oce-ui/src/main/resources/advanced.fxml
  27. 55 0
      oce-main/oce-ui/src/main/resources/initialize.fxml
  28. 35 0
      oce-main/oce-ui/src/main/resources/localization.properties
  29. 40 0
      oce-main/oce-ui/src/main/resources/main.css
  30. 42 0
      oce-main/oce-ui/src/main/resources/main.fxml
  31. 39 0
      oce-main/oce-ui/src/main/resources/panels.css
  32. 77 0
      oce-main/oce-webdav/pom.xml
  33. 39 0
      oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/EnhancedWebDavServlet.java
  34. 120 0
      oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/EnhancedWebdavStore.java
  35. 183 0
      oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/FsWebdavCryptoAdapter.java
  36. 228 0
      oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/FsWebdavResourceHandler.java
  37. 40 0
      oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/FsWebdavTransaction.java
  38. 73 0
      oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/WebDAVServer.java
  39. 32 0
      oce-main/oce-webdav/src/main/resources/log4j.xml
  40. 131 0
      oce-main/pom.xml

+ 6 - 0
.gitignore

@@ -4,3 +4,9 @@
 *.jar
 *.war
 *.ear
+
+# Eclipse Settings Files #
+.settings
+.project
+.classpath
+target/

+ 54 - 0
oce-main/oce-crypto/pom.xml

@@ -0,0 +1,54 @@
+<?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">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>de.sebastianstenzel.oce</groupId>
+		<artifactId>oce-main</artifactId>
+		<version>0.0.1-SNAPSHOT</version>
+	</parent>
+	<artifactId>oce-crypto</artifactId>
+	<name>Open Cloud Encryptor Cryptographic module</name>
+	<description>Provides stream ciphers and filename pseudonymization functions.</description>
+
+	<dependencies>
+		<!-- Logging -->
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>slf4j-log4j12</artifactId>
+		</dependency>
+
+		<!-- Commons -->
+		<dependency>
+			<groupId>commons-io</groupId>
+			<artifactId>commons-io</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.commons</groupId>
+			<artifactId>commons-collections4</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.commons</groupId>
+			<artifactId>commons-lang3</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>
+</project>

+ 21 - 0
oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/Cryptor.java

@@ -0,0 +1,21 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.crypto;
+
+import de.sebastianstenzel.oce.crypto.aes256.AesCryptor;
+
+public abstract class Cryptor implements FilenamePseudonymizing, StorageCrypting {
+	
+	private static final Cryptor DEFAULT_CRYPTOR = new AesCryptor();
+	
+	public static Cryptor getDefaultCryptor() {
+		return DEFAULT_CRYPTOR;
+	}
+
+}

+ 32 - 0
oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/FilenamePseudonymizing.java

@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.crypto;
+
+import java.io.IOException;
+
+public interface FilenamePseudonymizing {
+	
+	/**
+	 * Pseudonymizes and caches the given URI. If the doesn't exist yet, the new pseudonyms and its corresponding directory structure is created.
+	 * @return Pseudonymized URI for the provided cleartext URI.
+	 */
+	String createPseudonym(String cleartextUri, TransactionAwareFileAccess accessor) throws IOException;
+	
+	/**
+	 * Looks up the corresponding cleartext names for a given pseudonymized path.
+	 * @return Cleartext URI for the provided pseudonym URI. Returns <code>null</code>, if the pseudonym can't be resolved.
+	 */
+	String uncoverPseudonym(String pseudonymizedUri, TransactionAwareFileAccess accessor) throws IOException;
+	
+	/**
+	 * Deletes a pair of cleartext/pseudonym file name from the cache and metadata file.
+	 */
+	void deletePseudonym(String pseudonymizedUri, TransactionAwareFileAccess accessor) throws IOException;
+
+}

+ 91 - 0
oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/StorageCrypting.java

@@ -0,0 +1,91 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.crypto;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+
+public interface StorageCrypting {
+	
+	/**
+	 * Closes the given InputStream, when all content is encrypted.
+	 */
+	long encryptFile(String pseudonymizedUri, InputStream content, TransactionAwareFileAccess accessor) throws IOException;
+	
+	InputStream decryptFile(String pseudonymizedUri, TransactionAwareFileAccess accessor) throws IOException;
+	
+	long getDecryptedContentLength(String pseudonymizedUri, TransactionAwareFileAccess accessor) throws IOException;
+	
+	boolean isStorage(Path path);
+	
+	void initializeStorage(Path path, CharSequence password) throws AlreadyInitializedException, IOException;
+	
+	void unlockStorage(Path path, CharSequence password) throws InvalidStorageLocationException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException;
+	
+	void swipeSensitiveData();
+	
+	/* Exceptions */
+	
+	class StorageCryptingException extends Exception {
+		private static final long serialVersionUID = -6622699014483319376L;
+		
+		public StorageCryptingException(String string) {
+			super(string);
+		}
+		
+		public StorageCryptingException(String string, Throwable t) {
+			super(string, t);
+		}
+	}
+
+	class AlreadyInitializedException extends StorageCryptingException {
+		private static final long serialVersionUID = -8928660250898037968L;
+		
+		public AlreadyInitializedException(Path path) {
+			super(path.toString() + " already contains a vault.");
+		}
+	}
+	
+	class InvalidStorageLocationException extends StorageCryptingException {
+		private static final long serialVersionUID = -967813718181720188L;
+
+		public InvalidStorageLocationException(Path path) {
+			super("Can't read vault in path " + path.toString());
+		}
+	}
+
+	class WrongPasswordException extends StorageCryptingException {
+		private static final long serialVersionUID = -602047799678568780L;
+
+		public WrongPasswordException() {
+			super("Wrong password.");
+		}
+	}
+	
+	class DecryptFailedException extends StorageCryptingException {
+		private static final long serialVersionUID = -3855673600374897828L;
+
+		public DecryptFailedException(Throwable t) {
+			super("Decryption failed.", t);
+		}
+	}
+	
+	class UnsupportedKeyLengthException extends StorageCryptingException {
+		private static final long serialVersionUID = 8114147446419390179L;
+		
+		public UnsupportedKeyLengthException(int length, int maxLength) {
+			super(String.format("Key length (%i) exceeds policy maximum (%i).", length, maxLength));
+		}
+		
+	}
+
+}
+
+

+ 31 - 0
oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/TransactionAwareFileAccess.java

@@ -0,0 +1,31 @@
+/*******************************************************************************
+ * Copyright (c) 2014 Sebastian Stenzel
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ * 
+ * Contributors:
+ *     Sebastian Stenzel - initial API and implementation
+ ******************************************************************************/
+package de.sebastianstenzel.oce.crypto;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Path;
+
+/**
+ * IoC for I/O streams. The streams provied by these methods are closed by the caller. Thus the callee implementing this interface must not
+ * close the streams again.
+ */
+public interface TransactionAwareFileAccess {
+	
+	/**
+	 * @return Path relative to the current working directory, regardless of leading slashes.
+	 */
+	Path resolveUri(String uri);
+
+	InputStream openFileForRead(Path path) throws IOException;
+
+	OutputStream openFileForWrite(Path path) throws IOException;
+
+}

+ 585 - 0
oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/AesCryptor.java

@@ -0,0 +1,585 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.crypto.aes256;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.commons.io.Charsets;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import de.sebastianstenzel.oce.crypto.Cryptor;
+import de.sebastianstenzel.oce.crypto.TransactionAwareFileAccess;
+import de.sebastianstenzel.oce.crypto.cache.PseudonymRepository;
+
+/**
+ * Default cryptor using PBKDF2 to derive an AES user key of up to 256 bit length.
+ * This user key is used to decrypt the masterkey, which is a secure random chunk of data.
+ * The masterkey in turn is used to decrypt all files in the secure storage location.
+ */
+public class AesCryptor extends Cryptor {
+
+	private static final Logger LOG = LoggerFactory.getLogger(AesCryptor.class);
+	private static final String METADATA_FILENAME = "metadata.json";
+	private static final String KEYS_FILENAME = "keys.json";
+	private static final char URI_PATH_SEP = '/';
+	
+	/**
+	 * PRNG for cryptographically secure random numbers.
+	 * Defaults to SHA1-based number generator.
+	 * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#SecureRandom
+	 */
+	private static final SecureRandom SECURE_PRNG;
+	
+	/**
+	 * Factory for deriveing keys.
+	 * Defaults to PBKDF2/HMAC-SHA1.
+	 * @see PKCS #5, defined in RFC 2898
+	 * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#SecretKeyFactory
+	 */
+	private static final SecretKeyFactory PBKDF2_FACTORY;
+	
+	/**
+	 * Number of bytes used as seed for the PRNG.
+	 */
+	private static final 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.
+	 */
+	private static final int MASTER_KEY_LENGTH = 512;
+	
+	/**
+	 * Number of bytes used as salt, where needed.
+	 */
+	private static final int SALT_LENGTH = 8;
+	
+	/**
+	 * Our cryptographic algorithm.
+	 * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#AlgorithmParameters
+	 */
+	private static final String ALGORITHM = "AES";
+	
+	/**
+	 * More detailed specification for {@link #ALGORITHM}.
+	 * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher
+	 */
+	private static final String CIPHER = "AES/CBC/PKCS5Padding";
+	
+	/**
+	 * AES block size is 128 bit or 16 bytes.
+	 */
+	private static final int AES_BLOCK_LENGTH = 16;
+	
+	/**
+	 * Defined in static initializer.
+	 * Defaults to 256, but falls back to maximum value possible, if JCE isn't installed.
+	 * JCE can be installed from here: http://www.oracle.com/technetwork/java/javase/downloads/.
+	 */
+	private static final int AES_KEY_LENGTH;
+	
+	/**
+	 * Number of iterations for key derived from user pw.
+	 * High iteration count for better resistance to bruteforcing.
+	 */
+	private static final int PBKDF2_PW_ITERATIONS = 1000;
+	
+	/**
+	 * Number of iterations for key derived from masterkey.
+	 * Low iteration count for better performance.
+	 * No additional security is added by high values.
+	 */
+	private static final int PBKDF2_MASTERKEY_ITERATIONS = 1;
+	
+	/**
+	 * Jackson JSON-Mapper.
+	 */
+	private final ObjectMapper objectMapper = new ObjectMapper();
+	
+	/**
+	 * The decrypted master key.
+	 * Its lifecycle starts with {@link #unlockStorage(Path, CharSequence)} or {@link #initializeStorage(Path, CharSequence)}.
+	 * Its lifecycle ends with {@link #swipeSensitiveData()}.
+	 */
+	private final byte[] masterKey = new byte[MASTER_KEY_LENGTH];
+	
+	static {
+		final String keyFactoryName = "PBKDF2WithHmacSHA1";
+		final String prngName = "SHA1PRNG";
+		try {
+			PBKDF2_FACTORY = SecretKeyFactory.getInstance(keyFactoryName);
+			SECURE_PRNG = SecureRandom.getInstance(prngName);
+			final int maxKeyLen = Cipher.getMaxAllowedKeyLength(ALGORITHM);
+			AES_KEY_LENGTH = (maxKeyLen >= 256) ? 256 : maxKeyLen;
+		} catch (NoSuchAlgorithmException e) {
+			throw new IllegalStateException("Algorithm should exist.", e);
+		}
+	}
+	
+	@Override
+	public boolean isStorage(Path path) {
+		try {
+			final Path keysPath = path.resolve(KEYS_FILENAME);
+			return Files.isReadable(keysPath);
+		} catch(SecurityException ex) {
+			return false;
+		}
+	}
+	
+	@Override
+	public void initializeStorage(Path path, CharSequence password) throws AlreadyInitializedException, IOException {
+		final Path keysPath = path.resolve(KEYS_FILENAME);
+		if (Files.exists(keysPath)) {
+			throw new AlreadyInitializedException(path);
+		}
+		try {
+			// generate new masterkey:
+			randomMasterKey();
+			
+			// derive key:
+			final byte[] userSalt = randomData(SALT_LENGTH);
+			final SecretKey userKey = pbkdf2(password, userSalt, PBKDF2_PW_ITERATIONS, AES_KEY_LENGTH);
+			
+			// encrypt:
+			final byte[] iv = randomData(AES_BLOCK_LENGTH);
+			final Cipher encCipher = this.cipher(userKey, iv, Cipher.ENCRYPT_MODE);
+			byte[] encryptedUserKey = encCipher.doFinal(userKey.getEncoded());
+			byte[] encryptedMasterKey = encCipher.doFinal(this.masterKey);
+			
+			// save encrypted masterkey:
+			final Keys keys = new Keys();
+			final Keys.Key ownerKey = new Keys.Key();
+			ownerKey.setIterations(PBKDF2_PW_ITERATIONS);
+			ownerKey.setIv(iv);
+			ownerKey.setKeyLength(AES_KEY_LENGTH);
+			ownerKey.setMasterkey(encryptedMasterKey);
+			ownerKey.setSalt(userSalt);
+			ownerKey.setPwVerification(encryptedUserKey);
+			keys.setOwnerKey(ownerKey);
+			this.saveKeys(keys, keysPath);
+		} catch (IllegalBlockSizeException | BadPaddingException ex) {
+			throw new IllegalStateException("Block size hard coded. Padding irrelevant in ENCRYPT_MODE. IV must exist in CBC mode.", ex);
+		}
+	}
+	
+	@Override
+	public void unlockStorage(Path path, CharSequence password) throws InvalidStorageLocationException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException {
+		final Path keysPath = path.resolve("keys.json");
+		if (!this.isStorage(path)) {
+			throw new InvalidStorageLocationException(path);
+		}
+		byte[] decrypted = new byte[0];
+		try {
+			// load encrypted masterkey:
+			final Keys keys = this.loadKeys(keysPath);
+			final Keys.Key ownerKey = keys.getOwnerKey();
+			
+			// check, whether the key length is supported:
+			final int maxKeyLen = Cipher.getMaxAllowedKeyLength(ALGORITHM);
+			if (ownerKey.getKeyLength() > maxKeyLen) {
+				throw new UnsupportedKeyLengthException(ownerKey.getKeyLength(), maxKeyLen);
+			}
+			
+			// derive key:
+			final SecretKey userKey = pbkdf2(password, ownerKey.getSalt(), ownerKey.getIterations(), ownerKey.getKeyLength());
+			
+			// check password:
+			final Cipher encCipher = this.cipher(userKey, ownerKey.getIv(), Cipher.ENCRYPT_MODE);
+			byte[] encryptedUserKey = encCipher.doFinal(userKey.getEncoded());
+			if (!Arrays.equals(ownerKey.getPwVerification(), encryptedUserKey)) {
+				throw new WrongPasswordException();
+			}
+
+			// decrypt:
+			final Cipher decCipher = this.cipher(userKey, ownerKey.getIv(), Cipher.DECRYPT_MODE);
+			decrypted = decCipher.doFinal(ownerKey.getMasterkey());
+			
+			// everything ok, move decrypted data to masterkey:
+			final ByteBuffer masterKeyBuffer = ByteBuffer.wrap(this.masterKey);
+			masterKeyBuffer.put(decrypted);
+		} catch (IllegalBlockSizeException | BadPaddingException | BufferOverflowException ex) {
+			throw new DecryptFailedException(ex);
+		} catch (NoSuchAlgorithmException ex) {
+			throw new IllegalStateException("Algorithm should exist.", ex);
+		} finally {
+			Arrays.fill(decrypted, (byte) 0);
+		}
+	}
+	
+	@Override
+	public long encryptFile(String pseudonymizedUri, InputStream in, TransactionAwareFileAccess accessor) throws IOException {
+		final Path path = accessor.resolveUri(pseudonymizedUri);
+		OutputStream out = null;
+		try {
+			// unencrypted output stream:
+			final byte[] salt = this.randomData(SALT_LENGTH);
+			final byte[] iv = this.randomData(AES_BLOCK_LENGTH);
+			out = accessor.openFileForWrite(path);
+			out.write(salt, 0, salt.length);
+			out.write(iv, 0, iv.length);
+			
+			// turn outputstream into an encrypting output stream:
+			final SecretKey key = this.pbkdf2(masterKey, salt, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
+			final Cipher encCipher = this.cipher(key, iv, Cipher.ENCRYPT_MODE);
+			out = new CipherOutputStream(out, encCipher);
+			
+			// write payload to encrypted out:
+			final long decryptedFilesize = IOUtils.copyLarge(in, out);
+			
+			// save filesize to metadata:
+			final String folderUri = FilenameUtils.getPath(pseudonymizedUri);
+			final String pseudonym = FilenameUtils.getName(pseudonymizedUri);
+			final Metadata metadata = loadOrCreateMetadata(accessor, folderUri);
+			metadata.getFilesizes().put(pseudonym, decryptedFilesize);
+			saveMetadata(metadata, accessor, folderUri);
+			
+			return decryptedFilesize;
+		} finally {
+			in.close();
+			if (out != null) {
+				out.close();
+			}
+		}
+	}
+	
+	@Override
+	public InputStream decryptFile(String pseudonymizedUri, TransactionAwareFileAccess accessor) throws IOException {
+		// plain input stream:
+		final Path path = accessor.resolveUri(pseudonymizedUri);
+		final InputStream in = accessor.openFileForRead(path);
+		final byte[] salt = new byte[SALT_LENGTH];
+		final byte[] iv = new byte[AES_BLOCK_LENGTH];
+		in.read(salt, 0, salt.length);
+		in.read(iv, 0, iv.length);
+		
+		// deecrypting input stream:
+		final SecretKey key = this.pbkdf2(masterKey, salt, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
+		final Cipher decCipher = this.cipher(key, iv, Cipher.DECRYPT_MODE);
+		return new CipherInputStream(in, decCipher);
+	}
+	
+	@Override
+	public long getDecryptedContentLength(String pseudonymizedUri, TransactionAwareFileAccess accessor) throws IOException {
+		final String folderUri = FilenameUtils.getPath(pseudonymizedUri);
+		final String pseudonym = FilenameUtils.getName(pseudonymizedUri);
+		final Metadata metadata = loadOrCreateMetadata(accessor, folderUri);
+		if (metadata.getFilesizes().containsKey(pseudonym)) {
+			return metadata.getFilesizes().get(pseudonym);
+		} else {
+			return -1;
+		}
+	}
+	
+	/**
+	 * Overwrites the {@link #masterKey} with zeros.
+	 * As masterKey is a final field, this operation is ensured to work on its actual data.
+	 * Otherwise developers could accidentally just assign a new object to the variable.
+	 */
+	@Override
+	public void swipeSensitiveData() {
+		Arrays.fill(this.masterKey, (byte) 0);
+	}
+	
+	private Cipher cipher(SecretKey key, byte[] iv, int cipherMode) {
+		try {
+			final Cipher cipher = Cipher.getInstance(CIPHER);
+			cipher.init(cipherMode, key, new IvParameterSpec(iv));
+			return cipher;
+		} catch (InvalidKeyException ex) {
+			throw new IllegalArgumentException("Invalid key.", ex);
+		} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException ex) {
+			throw new IllegalStateException("Algorithm/Padding should exist and accept an IV.", ex);
+		}
+	}
+	
+	private byte[] randomData(int length) {
+		final byte[] result = new byte[length];
+		SECURE_PRNG.setSeed(SECURE_PRNG.generateSeed(PRNG_SEED_LENGTH));
+		SECURE_PRNG.nextBytes(result);
+		return result;
+	}
+	
+	private void randomMasterKey() {
+		SECURE_PRNG.setSeed(SECURE_PRNG.generateSeed(PRNG_SEED_LENGTH));
+		SECURE_PRNG.nextBytes(this.masterKey);
+	}
+	
+	private SecretKey pbkdf2(byte[] password, byte[] salt, int iterations, int keyLength) {
+		final char[] pw = new char[password.length];
+		try {
+			byteToChar(password, pw);
+			return pbkdf2(CharBuffer.wrap(pw), salt, iterations, keyLength);
+		} finally {
+			Arrays.fill(pw, (char) 0);
+		}
+	}
+	
+	private SecretKey pbkdf2(CharSequence password, byte[] salt, int iterations, int keyLength) {
+		final int pwLen = password.length();
+		final char[] pw = new char[pwLen];
+		CharBuffer.wrap(password).get(pw, 0, pwLen);
+		try {
+			final KeySpec specs = new PBEKeySpec(pw, salt, iterations, keyLength);
+			final SecretKey pbkdf2Key = PBKDF2_FACTORY.generateSecret(specs);
+			final SecretKey aesKey = new SecretKeySpec(pbkdf2Key.getEncoded(), ALGORITHM);
+			return aesKey;
+		} catch (InvalidKeySpecException ex) {
+			throw new IllegalStateException("Specs are hard-coded.", ex);
+		} finally {
+			Arrays.fill(pw, (char) 0);
+		}
+	}
+	
+	private void byteToChar(byte[] source, char[] destination) {
+		if (source.length != destination.length) {
+			throw new IllegalArgumentException("char[] needs to be the same length as byte[]");
+		}
+		for (int i = 0; i < source.length; i++) {
+			destination[i] = (char) (source[i] & 0xFF);
+		}
+	}
+	
+	private Keys loadKeys(Path keysFile) throws IOException {
+		InputStream in = null;
+		try {
+			in = Files.newInputStream(keysFile, StandardOpenOption.READ);
+			return objectMapper.readValue(in, Keys.class);
+		} finally {
+			if (in != null) {
+				in.close();
+			}
+		}
+	}
+
+	private void saveKeys(Keys keys, Path keysFile) throws IOException {
+		OutputStream out = null;
+		try {
+			out = Files.newOutputStream(keysFile, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.SYNC, StandardOpenOption.CREATE);
+			objectMapper.writeValue(out, keys);
+		} finally {
+			if (out != null) {
+				out.close();
+			}
+		}
+	}
+	
+	/* Pseudonymizing */
+
+	@Override
+	public String createPseudonym(String cleartextUri, TransactionAwareFileAccess access) throws IOException {
+		final List<String> cleartextUriComps = this.splitUri(cleartextUri);
+		final List<String> pseudonymUriComps = PseudonymRepository.pseudonymizedPathComponents(cleartextUriComps);
+
+		// return immediately if path is already known:
+		if (pseudonymUriComps.size() == cleartextUriComps.size()) {
+			return concatUri(pseudonymUriComps);
+		}
+
+		// append further path components otherwise:
+		for (int i = pseudonymUriComps.size(); i < cleartextUriComps.size(); i++) {
+			final String currentFolder = concatUri(pseudonymUriComps);
+			final String cleartext = cleartextUriComps.get(i);
+			String pseudonym = readPseudonymFromMetadata(access, currentFolder, cleartext);
+			if (pseudonym == null) {
+				pseudonym = UUID.randomUUID().toString();
+				this.addToMetadata(access, currentFolder, cleartext, pseudonym);
+			}
+			pseudonymUriComps.add(pseudonym);
+		}
+		PseudonymRepository.registerPath(cleartextUriComps, pseudonymUriComps);
+
+		return concatUri(pseudonymUriComps);
+	}
+
+	@Override
+	public String uncoverPseudonym(String pseudonymizedUri, TransactionAwareFileAccess access) throws IOException {
+		final List<String> pseudonymUriComps = this.splitUri(pseudonymizedUri);
+		final List<String> cleartextUriComps = PseudonymRepository.cleartextPathComponents(pseudonymUriComps);
+
+		// return immediately if path is already known:
+		if (cleartextUriComps.size() == pseudonymUriComps.size()) {
+			return concatUri(cleartextUriComps);
+		}
+
+		// append further path components otherwise:
+		for (int i = cleartextUriComps.size(); i < pseudonymUriComps.size(); i++) {
+			final String currentFolder = concatUri(pseudonymUriComps.subList(0, i));
+			final String pseudonym = pseudonymUriComps.get(i);
+			try {
+				final String cleartext = this.readCleartextFromMetadata(access, currentFolder, pseudonym);
+				if (cleartext == null) {
+					return null;
+				}
+				cleartextUriComps.add(cleartext);
+			} catch (IOException ex) {
+				LOG.warn("Unresolvable pseudonym: " + currentFolder + "/" + pseudonym);
+				return null;
+			}
+		}
+		PseudonymRepository.registerPath(cleartextUriComps, pseudonymUriComps);
+
+		return concatUri(cleartextUriComps);
+	}
+
+	@Override
+	public void deletePseudonym(String pseudonymizedUri, TransactionAwareFileAccess access) throws IOException {
+		// find parent folder:
+		final int lastPathSeparator = pseudonymizedUri.lastIndexOf(URI_PATH_SEP);
+		final String parentUri;
+		if (lastPathSeparator > 0) {
+			parentUri = pseudonymizedUri.substring(0, lastPathSeparator);
+		} else {
+			parentUri = "/";
+		}
+
+		// delete from metadata file:
+		final String pseudonym = pseudonymizedUri.substring(lastPathSeparator + 1);
+		final Metadata metadata = this.loadOrCreateMetadata(access, parentUri);
+		metadata.getFilenames().remove(pseudonym);
+		metadata.getFilesizes().remove(pseudonym);
+		this.saveMetadata(metadata, access, parentUri);
+
+		// delete from cache:
+		final List<String> pseudonymUriComps = this.splitUri(pseudonymizedUri);
+		PseudonymRepository.unregisterPath(pseudonymUriComps);
+	}
+
+	/* Metadata load & save */
+
+	private String readPseudonymFromMetadata(TransactionAwareFileAccess access, String parentFolder, String cleartext) throws IOException {
+		final Metadata metadata = loadOrCreateMetadata(access, parentFolder);
+		return metadata.getFilenames().getKey(cleartext);
+	}
+
+	private String readCleartextFromMetadata(TransactionAwareFileAccess access, String parentFolder, String pseudonym) throws IOException {
+		final Metadata metadata = loadOrCreateMetadata(access, parentFolder);
+		final byte[] encryptedFilename = metadata.getFilenames().get(pseudonym);
+		if (encryptedFilename == null) {
+			return null;
+		}
+		try {
+			// decrypt filename:
+			final SecretKey key = this.pbkdf2(masterKey, metadata.getSalt(), PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
+			final Cipher decCipher = this.cipher(key, metadata.getIv(), Cipher.DECRYPT_MODE);
+			byte[] decryptedFilename = decCipher.doFinal(encryptedFilename);
+			return new String(decryptedFilename, Charsets.UTF_8);
+		} catch (IllegalBlockSizeException | BadPaddingException ex) {
+			LOG.error("Can't decrypt filename " + pseudonym + " in folder " + parentFolder, ex);
+			return null;
+		}
+	}
+
+	private void addToMetadata(TransactionAwareFileAccess access, String parentFolder, String cleartext, String pseudonym) throws IOException {
+		final Metadata metadata = loadOrCreateMetadata(access, parentFolder);
+		try {
+			// encrypt filename:
+			final SecretKey key = this.pbkdf2(masterKey, metadata.getSalt(), PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
+			final Cipher encCipher = this.cipher(key, metadata.getIv(), Cipher.ENCRYPT_MODE);
+			byte[] encryptedFilename = encCipher.doFinal(cleartext.getBytes(Charsets.UTF_8));
+			
+			// save metadata
+			metadata.getFilenames().put(pseudonym, encryptedFilename);
+			saveMetadata(metadata, access, parentFolder);
+		} catch (IllegalBlockSizeException | BadPaddingException ex) {
+			LOG.error("Can't encrypt filename " + pseudonym + " (" + cleartext + ") in folder " + parentFolder, ex);
+		}
+	}
+
+	private Metadata loadOrCreateMetadata(TransactionAwareFileAccess access, String parentFolder) throws IOException {
+		InputStream in = null;
+		try {
+			final Path path = access.resolveUri(parentFolder).resolve(METADATA_FILENAME);
+			in = access.openFileForRead(path);
+			return objectMapper.readValue(in, Metadata.class);
+		} catch (IOException ex) {
+			final byte[] salt = randomData(SALT_LENGTH);
+			final byte[] iv = randomData(AES_BLOCK_LENGTH);
+			return new Metadata(iv, salt);
+		} finally {
+			if (in != null) {
+				in.close();
+			}
+		}
+	}
+
+	private void saveMetadata(Metadata metadata, TransactionAwareFileAccess access, String parentFolder) throws IOException {
+		OutputStream out = null;
+		try {
+			final Path path = access.resolveUri(parentFolder).resolve(METADATA_FILENAME);
+			out = access.openFileForWrite(path);
+			objectMapper.writeValue(out, metadata);
+		} finally {
+			if (out != null) {
+				out.close();
+			}
+		}
+	}
+
+	/* utility stuff */
+
+	private String concatUri(final List<String> uriComponents) {
+		final StringBuilder sb = new StringBuilder();
+		for (final String comp : uriComponents) {
+			sb.append(URI_PATH_SEP).append(comp);
+		}
+		return sb.toString();
+	}
+
+	private List<String> splitUri(final String uri) {
+		final List<String> result = new ArrayList<>();
+		int begin = 0;
+		int end = 0;
+		do {
+			end = uri.indexOf(URI_PATH_SEP, begin);
+			end = (end == -1) ? uri.length() : end;
+			if (end > begin) {
+				result.add(uri.substring(begin, end));
+			}
+			begin = end + 1;
+		} while (end < uri.length());
+		return result;
+	}
+
+}

+ 105 - 0
oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Keys.java

@@ -0,0 +1,105 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.crypto.aes256;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+
+@JsonPropertyOrder(value = { "ownerKey", "additionalKeys" })
+class Keys implements Serializable {
+
+	private static final long serialVersionUID = -19303594304327167L;
+	private Key ownerKey;
+	@JsonDeserialize(as = HashMap.class)
+	private Map<String, Key> additionalKeys;
+	
+	public Key getOwnerKey() {
+		return ownerKey;
+	}
+	
+	public void setOwnerKey(Key ownerKey) {
+		this.ownerKey = ownerKey;
+	}
+	
+	public Map<String, Key> getAdditionalKeys() {
+		return additionalKeys;
+	}
+	
+	public void setAdditionalKeys(Map<String, Key> additionalKeys) {
+		this.additionalKeys = additionalKeys;
+	}
+	
+	@JsonPropertyOrder(value = { "salt", "iv", "iterations", "keyLength", "masterkey" })
+	public static class Key implements Serializable {
+
+		private static final long serialVersionUID = 8578363158959619885L;
+		private byte[] salt;
+		private byte[] iv;
+		private int iterations;
+		private int keyLength;
+		private byte[] pwVerification;
+		private byte[] masterkey;
+		
+		public byte[] getSalt() {
+			return salt;
+		}
+		
+		public void setSalt(byte[] salt) {
+			this.salt = salt;
+		}
+
+		public byte[] getIv() {
+			return iv;
+		}
+
+		public void setIv(byte[] iv) {
+			this.iv = iv;
+		}
+
+		public int getIterations() {
+			return iterations;
+		}
+
+		public void setIterations(int iterations) {
+			this.iterations = iterations;
+		}
+
+		public int getKeyLength() {
+			return keyLength;
+		}
+
+		public void setKeyLength(int keyLength) {
+			this.keyLength = keyLength;
+		}
+
+		public byte[] getPwVerification() {
+			return pwVerification;
+		}
+
+		public void setPwVerification(byte[] pwVerification) {
+			this.pwVerification = pwVerification;
+		}
+
+		public byte[] getMasterkey() {
+			return masterkey;
+		}
+
+		public void setMasterkey(byte[] masterkey) {
+			this.masterkey = masterkey;
+		}
+
+		
+	}
+
+
+}

+ 79 - 0
oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Metadata.java

@@ -0,0 +1,79 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.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;
+	}
+
+}

+ 157 - 0
oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cache/PseudonymRepository.java

@@ -0,0 +1,157 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.crypto.cache;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+
+public final class PseudonymRepository {
+	
+	private static final Node ROOT = new Node(null, "/", "/");
+	
+	private PseudonymRepository() {
+		throw new IllegalStateException();
+	}
+	
+	/**
+	 * @return The deepest resolvable cleartext path for the requested pseudonymized path.
+	 */
+	public static List<String> cleartextPathComponents(final List<String> pseudonymizedPathComponents) {
+		final List<String> result = new ArrayList<>(pseudonymizedPathComponents.size());
+		Node node = ROOT;
+		for (final String pseudonym : pseudonymizedPathComponents) {
+			node = node.subnodesByPseudonym.get(pseudonym);
+			if (node == null) {
+				return result;
+			}
+			result.add(node.cleartext);
+		}
+		return result;
+	}
+	
+	/**
+	 * @return The deepest resolvable pseudonymized path for the requested cleartext path.
+	 */
+	public static List<String> pseudonymizedPathComponents(final List<String> cleartextPathComponents) {
+		final List<String> result = new ArrayList<>(cleartextPathComponents.size());
+		Node node = ROOT;
+		for (final String cleartext : cleartextPathComponents) {
+			Node subnode = node.subnodesByCleartext.get(cleartext);
+			if (subnode == null) {
+				return result;
+			}
+			node = subnode;
+			result.add(node.pseudonym);
+		}
+		return result;
+	}
+	
+	/**
+	 * Caches a path of cleartext/pseudonym pairs.
+	 */
+	public static void registerPath(final List<String> cleartextPathComponents, final List<String> pseudonymPathComponents) {
+		if (cleartextPathComponents.size() != pseudonymPathComponents.size()) {
+			throw new IllegalArgumentException("Cannot register pseudonymized path, that isn't matching the length of its cleartext equivalent.");
+		}
+		
+		Node node = ROOT;
+		for (int i=0; i<cleartextPathComponents.size(); i++) {
+			final String cleartextComp = cleartextPathComponents.get(i);
+			final String pseudonymComp = pseudonymPathComponents.get(i);
+			node = node.getOrCreateSubnode(cleartextComp, pseudonymComp);
+		}
+	}
+	
+	/**
+	 * Removes a path of cleartext/pseudonym pairs from the cache.
+	 */
+	public static void unregisterPath(final List<String> pseudonymPathComponents) {
+		Node node = ROOT;
+		for (final String pseudonymComp : pseudonymPathComponents) {
+			node = node.subnodesByPseudonym.get(pseudonymComp);
+		}
+		if (!ROOT.equals(node)) {
+			node.detach();
+		}
+	}
+	
+	
+	/**
+	 * Node in a tree of cleartext/pseudonym pairs, that can be traversed root to leaf. The whole tree is threadsafe.
+	 * As each node of the tree has its own synchronization, multithreaded access is balanced.
+	 */
+	private static final class Node {
+		private final Node parent;
+		private final String cleartext;
+		private final String pseudonym;
+		private final Map<String, Node> subnodesByCleartext;
+		private final Map<String, Node> subnodesByPseudonym;
+		
+		Node(Node parent, String cleartext, String pseudonym) {
+			this.parent = parent;
+			this.cleartext = cleartext;
+			this.pseudonym = pseudonym;
+			this.subnodesByCleartext = new ConcurrentHashMap<>();
+			this.subnodesByPseudonym = new ConcurrentHashMap<>();
+		}
+		
+		/**
+		 * @return New subnode attached to this.
+		 */
+		Node getOrCreateSubnode(String cleartext, String pseudonym) {
+			if (subnodesByCleartext.containsKey(cleartext) && subnodesByPseudonym.containsKey(pseudonym)) {
+				return subnodesByCleartext.get(cleartext);
+			}
+			final Node subnode = new Node(this, cleartext, pseudonym);
+			this.subnodesByCleartext.put(cleartext, subnode);
+			this.subnodesByPseudonym.put(pseudonym, subnode);
+			return subnode;
+		}
+
+		/**
+		 * Removes a node from its parent node.
+		 */
+		void detach() {
+			// the following two lines don't need to be synchronized,
+			// as inconsistencies are self-healing over the transactional metadata files.
+			this.parent.subnodesByCleartext.remove(this.cleartext);
+			this.parent.subnodesByPseudonym.remove(this.pseudonym);
+		}
+		
+		@Override
+		public int hashCode() {
+			final HashCodeBuilder hash = new HashCodeBuilder();
+			hash.append(parent);
+			hash.append(cleartext);
+			hash.append(pseudonym);
+			return hash.hashCode();
+		}
+		
+		@Override
+		public boolean equals(Object obj) {
+			if (obj instanceof Node) {
+				final Node other = (Node) obj;
+				final EqualsBuilder eq = new EqualsBuilder();
+				eq.append(this.parent, other.parent);
+				eq.append(this.cleartext, other.cleartext);
+				eq.append(this.pseudonym, other.pseudonym);
+				return eq.isEquals();
+			} else {
+				return false;
+			}
+		}
+		
+	}
+
+}

+ 31 - 0
oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cleartext/Metadata.java

@@ -0,0 +1,31 @@
+/*******************************************************************************
+ * Copyright (c) 2014 Sebastian Stenzel
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ * 
+ * Contributors:
+ *     Sebastian Stenzel - initial API and implementation
+ ******************************************************************************/
+package de.sebastianstenzel.oce.crypto.cleartext;
+
+import java.io.Serializable;
+
+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 = { "filenames" })
+class Metadata implements Serializable {
+
+	private static final long serialVersionUID = -8160643291781073247L;
+
+	@JsonDeserialize(as = DualHashBidiMap.class)
+	private final BidiMap<String, String> filenames = new DualHashBidiMap<>();
+
+	public BidiMap<String, String> getFilenames() {
+		return filenames;
+	}
+
+}

+ 246 - 0
oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cleartext/NoCryptor.java

@@ -0,0 +1,246 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.crypto.cleartext;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.apache.commons.io.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import de.sebastianstenzel.oce.crypto.Cryptor;
+import de.sebastianstenzel.oce.crypto.TransactionAwareFileAccess;
+import de.sebastianstenzel.oce.crypto.cache.PseudonymRepository;
+
+/**
+ * This Cryptor doesn't encrypting anything. It just pseudonymizes path names.
+ * @deprecated Used for testing only. Will be removed soon.
+ */
+@Deprecated
+public class NoCryptor extends Cryptor {
+
+	private static final Logger LOG = LoggerFactory.getLogger(NoCryptor.class);
+	private static String METADATA_FILENAME = "metadata.json";
+
+	private static final char URI_PATH_SEP = '/';
+	private final ObjectMapper objectMapper = new ObjectMapper();
+
+	/* Crypting */
+
+	@Override
+	public boolean isStorage(Path path) {
+		// NoCryptor doesn't depend on any special folder structure.
+		return true;
+	}
+
+	@Override
+	public void initializeStorage(Path path, CharSequence password) {
+		// Do nothing
+	}
+
+	@Override
+	public void unlockStorage(Path path, CharSequence password) {
+		// Do nothing
+	}
+
+	@Override
+	public long encryptFile(String pseudonymizedUri, InputStream in, TransactionAwareFileAccess accessor) throws IOException {
+		final Path path = accessor.resolveUri(pseudonymizedUri);
+		OutputStream out = null;
+		try {
+			out = accessor.openFileForWrite(path);
+			return IOUtils.copyLarge(in, out);
+		} finally {
+			in.close();
+			if (out != null) {
+				out.close();
+			}
+		}
+	}
+
+	@Override
+	public InputStream decryptFile(String pseudonymizedUri, TransactionAwareFileAccess accessor) throws IOException {
+		final Path path = accessor.resolveUri(pseudonymizedUri);
+		return accessor.openFileForRead(path);
+	}
+
+	@Override
+	public long getDecryptedContentLength(String pseudonymizedUri, TransactionAwareFileAccess accessor) throws IOException {
+		final Path path = accessor.resolveUri(pseudonymizedUri);
+		return Files.size(path);
+	}
+
+	@Override
+	public void swipeSensitiveData() {
+		// Do nothing
+	}
+
+	/* Pseudonymizing */
+
+	@Override
+	public String createPseudonym(String cleartextUri, TransactionAwareFileAccess access) throws IOException {
+		final List<String> cleartextUriComps = this.splitUri(cleartextUri);
+		final List<String> pseudonymUriComps = PseudonymRepository.pseudonymizedPathComponents(cleartextUriComps);
+
+		// return immediately if path is already known:
+		if (pseudonymUriComps.size() == cleartextUriComps.size()) {
+			return concatUri(pseudonymUriComps);
+		}
+
+		// append further path components otherwise:
+		for (int i = pseudonymUriComps.size(); i < cleartextUriComps.size(); i++) {
+			final String currentFolder = concatUri(pseudonymUriComps);
+			final String cleartext = cleartextUriComps.get(i);
+			String pseudonym = readPseudonymFromMetadata(access, currentFolder, cleartext);
+			if (pseudonym == null) {
+				pseudonym = UUID.randomUUID().toString();
+				this.addToMetadata(access, currentFolder, cleartext, pseudonym);
+			}
+			pseudonymUriComps.add(pseudonym);
+		}
+		PseudonymRepository.registerPath(cleartextUriComps, pseudonymUriComps);
+
+		return concatUri(pseudonymUriComps);
+	}
+
+	@Override
+	public String uncoverPseudonym(String pseudonymizedUri, TransactionAwareFileAccess access) throws IOException {
+		final List<String> pseudonymUriComps = this.splitUri(pseudonymizedUri);
+		final List<String> cleartextUriComps = PseudonymRepository.cleartextPathComponents(pseudonymUriComps);
+
+		// return immediately if path is already known:
+		if (cleartextUriComps.size() == pseudonymUriComps.size()) {
+			return concatUri(cleartextUriComps);
+		}
+
+		// append further path components otherwise:
+		for (int i = cleartextUriComps.size(); i < pseudonymUriComps.size(); i++) {
+			final String currentFolder = concatUri(pseudonymUriComps.subList(0, i));
+			final String pseudonym = pseudonymUriComps.get(i);
+			try {
+				final String cleartext = this.readCleartextFromMetadata(access, currentFolder, pseudonym);
+				if (cleartext == null) {
+					return null;
+				}
+				cleartextUriComps.add(cleartext);
+			} catch (IOException ex) {
+				LOG.warn("Unresolvable pseudonym: " + currentFolder + "/" + pseudonym);
+				return null;
+			}
+		}
+		PseudonymRepository.registerPath(cleartextUriComps, pseudonymUriComps);
+
+		return concatUri(cleartextUriComps);
+	}
+
+	@Override
+	public void deletePseudonym(String pseudonymizedUri, TransactionAwareFileAccess access) throws IOException {
+		// find parent folder:
+		final int lastPathSeparator = pseudonymizedUri.lastIndexOf(URI_PATH_SEP);
+		final String parentUri;
+		if (lastPathSeparator > 0) {
+			parentUri = pseudonymizedUri.substring(0, lastPathSeparator);
+		} else {
+			parentUri = "/";
+		}
+
+		// delete from metadata file:
+		final String pseudonym = pseudonymizedUri.substring(lastPathSeparator + 1);
+		final Metadata metadata = this.loadOrCreateMetadata(access, parentUri);
+		metadata.getFilenames().remove(pseudonym);
+		this.saveMetadata(metadata, access, parentUri);
+
+		// delete from cache:
+		final List<String> pseudonymUriComps = this.splitUri(pseudonymizedUri);
+		PseudonymRepository.unregisterPath(pseudonymUriComps);
+	}
+
+	/* Metadata load & save */
+
+	private String readPseudonymFromMetadata(TransactionAwareFileAccess access, String parentFolder, String cleartext) throws IOException {
+		final Metadata metadata = loadOrCreateMetadata(access, parentFolder);
+		return metadata.getFilenames().getKey(cleartext);
+	}
+
+	private String readCleartextFromMetadata(TransactionAwareFileAccess access, String parentFolder, String pseudonym) throws IOException {
+		final Metadata metadata = loadOrCreateMetadata(access, parentFolder);
+		return metadata.getFilenames().get(pseudonym);
+	}
+
+	private void addToMetadata(TransactionAwareFileAccess access, String parentFolder, String cleartext, String pseudonym) throws IOException {
+		final Metadata metadata = loadOrCreateMetadata(access, parentFolder);
+		if (!pseudonym.equals(metadata.getFilenames().getKey(cleartext))) {
+			metadata.getFilenames().put(pseudonym, cleartext);
+			saveMetadata(metadata, access, parentFolder);
+		}
+	}
+
+	private Metadata loadOrCreateMetadata(TransactionAwareFileAccess access, String parentFolder) throws IOException {
+		InputStream in = null;
+		try {
+			final Path path = access.resolveUri(parentFolder).resolve(METADATA_FILENAME);
+			in = access.openFileForRead(path);
+			return objectMapper.readValue(in, Metadata.class);
+		} catch (IOException ex) {
+			return new Metadata();
+		} finally {
+			if (in != null) {
+				in.close();
+			}
+		}
+	}
+
+	private void saveMetadata(Metadata metadata, TransactionAwareFileAccess access, String parentFolder) throws IOException {
+		OutputStream out = null;
+		try {
+			final Path path = access.resolveUri(parentFolder).resolve(METADATA_FILENAME);
+			out = access.openFileForWrite(path);
+			objectMapper.writeValue(out, metadata);
+		} finally {
+			if (out != null) {
+				out.close();
+			}
+		}
+	}
+
+	/* utility stuff */
+
+	private String concatUri(final List<String> uriComponents) {
+		final StringBuilder sb = new StringBuilder();
+		for (final String comp : uriComponents) {
+			sb.append(URI_PATH_SEP).append(comp);
+		}
+		return sb.toString();
+	}
+
+	private List<String> splitUri(final String uri) {
+		final List<String> result = new ArrayList<>();
+		int begin = 0;
+		int end = 0;
+		do {
+			end = uri.indexOf(URI_PATH_SEP, begin);
+			end = (end == -1) ? uri.length() : end;
+			if (end > begin) {
+				result.add(uri.substring(begin, end));
+			}
+			begin = end + 1;
+		} while (end < uri.length());
+		return result;
+	}
+
+}

+ 92 - 0
oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/AesCryptorTest.java

@@ -0,0 +1,92 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.crypto.test;
+
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import de.sebastianstenzel.oce.crypto.StorageCrypting;
+import de.sebastianstenzel.oce.crypto.StorageCrypting.AlreadyInitializedException;
+import de.sebastianstenzel.oce.crypto.StorageCrypting.DecryptFailedException;
+import de.sebastianstenzel.oce.crypto.StorageCrypting.InvalidStorageLocationException;
+import de.sebastianstenzel.oce.crypto.StorageCrypting.UnsupportedKeyLengthException;
+import de.sebastianstenzel.oce.crypto.StorageCrypting.WrongPasswordException;
+import de.sebastianstenzel.oce.crypto.aes256.AesCryptor;
+
+public class AesCryptorTest {
+
+	private Path workingDir;
+
+	@Before
+	public void prepareTmpDir() throws IOException {
+		final String tmpDirName = (String) System.getProperties().get("java.io.tmpdir");
+		final Path path = FileSystems.getDefault().getPath(tmpDirName);
+		workingDir = Files.createTempDirectory(path, "oce-crypto-test");
+	}
+
+	@Test
+	public void testCorrectPassword() throws IOException, AlreadyInitializedException, InvalidStorageLocationException, WrongPasswordException, DecryptFailedException, UnsupportedKeyLengthException {
+		final String pw = "asd";
+		final StorageCrypting encryptor = new AesCryptor();
+		encryptor.initializeStorage(workingDir, pw);
+		encryptor.swipeSensitiveData();
+
+		final StorageCrypting decryptor = new AesCryptor();
+		decryptor.unlockStorage(workingDir, pw);
+	}
+
+	@Test(expected=WrongPasswordException.class)
+	public void testWrongPassword() throws IOException, AlreadyInitializedException, InvalidStorageLocationException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
+		final String pw = "asd";
+		final StorageCrypting encryptor = new AesCryptor();
+		encryptor.initializeStorage(workingDir, pw);
+		encryptor.swipeSensitiveData();
+
+		final String wrongPw = "foo";
+		final StorageCrypting decryptor = new AesCryptor();
+		decryptor.unlockStorage(workingDir, wrongPw);
+	}
+	
+	@Test(expected=InvalidStorageLocationException.class)
+	public void testWrongLocation() throws IOException, AlreadyInitializedException, InvalidStorageLocationException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
+		final String pw = "asd";
+		final StorageCrypting encryptor = new AesCryptor();
+		encryptor.initializeStorage(workingDir, pw);
+		encryptor.swipeSensitiveData();
+
+		final Path wrongWorkginDir = workingDir.resolve("wrongSubResource");
+		final StorageCrypting decryptor = new AesCryptor();
+		decryptor.unlockStorage(wrongWorkginDir, pw);
+	}
+	
+	@Test(expected=AlreadyInitializedException.class)
+	public void testReInitialization() throws IOException, AlreadyInitializedException {
+		final String pw = "asd";
+		final StorageCrypting encryptor1 = new AesCryptor();
+		encryptor1.initializeStorage(workingDir, pw);
+		encryptor1.swipeSensitiveData();
+
+		final StorageCrypting encryptor2 = new AesCryptor();
+		encryptor2.initializeStorage(workingDir, pw);
+		encryptor2.swipeSensitiveData();
+	}
+
+	@After
+	public void dropTmpDir() throws IOException {
+		FileUtils.deleteDirectory(workingDir.toFile());
+	}
+
+}

+ 88 - 0
oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/FilenamePseudonymizerTest.java

@@ -0,0 +1,88 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.crypto.test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import de.sebastianstenzel.oce.crypto.Cryptor;
+import de.sebastianstenzel.oce.crypto.FilenamePseudonymizing;
+import de.sebastianstenzel.oce.crypto.TransactionAwareFileAccess;
+
+public class FilenamePseudonymizerTest {
+	
+	private final FilenamePseudonymizing pseudonymizer = Cryptor.getDefaultCryptor();
+	private Path workingDir;
+	
+	@Before
+	public void prepareTmpDir() throws IOException {
+		final String tmpDirName = (String) System.getProperties().get("java.io.tmpdir");
+		final Path path = FileSystems.getDefault().getPath(tmpDirName);
+		workingDir = Files.createTempDirectory(path, "oce-crypto-test");
+	}
+	
+	@Test
+	public void testCreatePseudonym() throws IOException {
+		final Accessor accessor = new Accessor();
+		final String originalCleartextUri = "/foo/bar/test.txt";
+		
+		final String pseudonym = pseudonymizer.createPseudonym(originalCleartextUri, accessor);
+		Assert.assertNotNull(pseudonym);
+		
+		final String cleartext = pseudonymizer.uncoverPseudonym(pseudonym, accessor);
+		Assert.assertEquals(originalCleartextUri, cleartext);
+	}
+	
+	@After
+	public void dropTmpDir() throws IOException {
+		FileUtils.deleteDirectory(workingDir.toFile());
+	}
+	
+	private class Accessor implements TransactionAwareFileAccess {
+
+		@Override
+		public OutputStream openFileForWrite(final Path path) throws IOException {
+			Files.createDirectories(path.getParent());
+			return Files.newOutputStream(path, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
+		}
+		
+		@Override
+		public InputStream openFileForRead(final Path path) throws IOException {
+			return Files.newInputStream(path, StandardOpenOption.READ);
+		}
+
+		@Override
+		public Path resolveUri(String uri) {
+			return workingDir.resolve(removeLeadingSlash(uri));
+		}
+		
+		private String removeLeadingSlash(String path) {
+			if (path.length() == 0) {
+				return path;
+			} else if (path.charAt(0) == '/') {
+				return path.substring(1);
+			} else {
+				return path;
+			}
+		}
+		
+	}
+
+}

+ 50 - 0
oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/PseudonymRepositoryTest.java

@@ -0,0 +1,50 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.crypto.test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import de.sebastianstenzel.oce.crypto.cache.PseudonymRepository;
+
+public class PseudonymRepositoryTest {
+	
+	@Test
+	public void testPseudonymRepos() {
+		// register first pair:
+		final List<String> clear1 = Arrays.asList("foo", "bar", "baz", "info.txt");
+		final List<String> pseudo1 = Arrays.asList("frog", "bear", "bear", "iguana");
+		PseudonymRepository.registerPath(clear1, pseudo1);
+		
+		// get pseudonymized path:
+		final List<String> result1 = PseudonymRepository.pseudonymizedPathComponents(clear1);
+		Assert.assertEquals(pseudo1, result1);
+		
+		// get cleartext path:
+		final List<String> result2 = PseudonymRepository.cleartextPathComponents(pseudo1);
+		Assert.assertEquals(clear1, result2);
+		
+		// register additional path:
+		final List<String> clear2 = Arrays.asList("foo", "bar", "zab", "info.txt");
+		final List<String> pseudo2 = Arrays.asList("frog", "bear", "zebra", "iguana");
+		PseudonymRepository.registerPath(clear2, pseudo2);
+
+		// get pseudonymized path:
+		final List<String> result3 = PseudonymRepository.pseudonymizedPathComponents(clear2);
+		Assert.assertEquals(pseudo2, result3);
+		
+		// get cleartext path:
+		final List<String> result4 = PseudonymRepository.cleartextPathComponents(pseudo2);
+		Assert.assertEquals(clear2, result4);
+	}
+
+}

+ 91 - 0
oce-main/oce-ui/pom.xml

@@ -0,0 +1,91 @@
+<?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">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>de.sebastianstenzel.oce</groupId>
+		<artifactId>oce-main</artifactId>
+		<version>0.0.1-SNAPSHOT</version>
+	</parent>
+	<artifactId>oce-ui</artifactId>
+	<name>Open Cloud Encryptor GUI</name>
+
+	<properties>
+		<exec.mainClass>de.sebastianstenzel.oce.ui.MainApplication</exec.mainClass>
+	</properties>
+
+	<dependencies>
+		<dependency>
+			<groupId>de.sebastianstenzel.oce</groupId>
+			<artifactId>oce-webdav</artifactId>
+			<version>${project.parent.version}</version>
+		</dependency>
+
+		<!-- JSON -->
+		<dependency>
+			<groupId>com.fasterxml.jackson.core</groupId>
+			<artifactId>jackson-databind</artifactId>
+		</dependency>
+
+		<!-- JavaFX 2 -->
+		<dependency>
+			<groupId>com.oracle</groupId>
+			<artifactId>javafx</artifactId>
+		</dependency>
+<!-- 		<dependency> -->
+<!-- 			<groupId>com.aquafx-project</groupId> -->
+<!-- 			<artifactId>aquafx</artifactId> -->
+<!-- 			<version>0.1</version> -->
+<!-- 		</dependency> -->
+	</dependencies>
+
+
+	<build>
+		<plugins>
+			<!-- allows building using the maven goal "com.zenjava:javafx-maven-plugin:jar" -->
+			<!-- Java < 8: invoke this before your first build: http://zenjava.com/javafx/maven/fix-classpath.html -->
+			<plugin>
+				<groupId>com.zenjava</groupId>
+				<artifactId>javafx-maven-plugin</artifactId>
+				<version>2.0</version>
+				<configuration>
+					<mainClass>de.sebastianstenzel.oce.ui.MainWindow</mainClass>
+				</configuration>
+			</plugin>
+
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-assembly-plugin</artifactId>
+				<configuration>
+					<archive>
+						<manifestEntries>
+							<JavaFX-Version>${javafx.version}</JavaFX-Version>
+							<JavaFX-Application-Class>${exec.mainClass}</JavaFX-Application-Class>
+							<Main-Class>com/javafx/main/Main</Main-Class>
+						</manifestEntries>
+					</archive>
+					<descriptorRefs>
+						<descriptorRef>jar-with-dependencies</descriptorRef>
+					</descriptorRefs>
+				</configuration>
+				<executions>
+					<execution>
+						<id>assemble-all</id>
+						<phase>package</phase>
+						<goals>
+							<goal>single</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+	</build>
+
+</project>

+ 148 - 0
oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/AccessController.java

@@ -0,0 +1,148 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.ui;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.FileSystems;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.util.ResourceBundle;
+
+import javafx.event.ActionEvent;
+import javafx.fxml.FXML;
+import javafx.fxml.Initializable;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.GridPane;
+import javafx.stage.DirectoryChooser;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import de.sebastianstenzel.oce.crypto.Cryptor;
+import de.sebastianstenzel.oce.crypto.StorageCrypting.DecryptFailedException;
+import de.sebastianstenzel.oce.crypto.StorageCrypting.InvalidStorageLocationException;
+import de.sebastianstenzel.oce.crypto.StorageCrypting.UnsupportedKeyLengthException;
+import de.sebastianstenzel.oce.crypto.StorageCrypting.WrongPasswordException;
+import de.sebastianstenzel.oce.ui.controls.SecPasswordField;
+import de.sebastianstenzel.oce.ui.settings.Settings;
+import de.sebastianstenzel.oce.webdav.WebDAVServer;
+
+public class AccessController implements Initializable {
+	
+	private static final Logger LOG = LoggerFactory.getLogger(AccessController.class);
+	
+	private ResourceBundle localization;
+	@FXML private GridPane rootGridPane;
+	@FXML private TextField workDirTextField;
+	@FXML private SecPasswordField passwordField;
+	@FXML private Button startServerButton;
+	@FXML private Label messageLabel;
+	
+	@Override
+	public void initialize(URL url, ResourceBundle rb) {
+		this.localization = rb;
+		workDirTextField.setText(Settings.load().getWebdavWorkDir());
+		determineStorageValidity();
+	}
+	
+	@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) {
+			// dialog canceled
+			return;
+		} else if (file.canWrite()) {
+			workDirTextField.setText(file.getPath());
+			Settings.load().setWebdavWorkDir(file.getPath());
+			Settings.save();
+		} else {
+			messageLabel.setText(localization.getString("access.messageLabel.invalidStorageLocation"));
+		}
+		determineStorageValidity();
+	}
+	
+	private void determineStorageValidity() {
+		boolean storageLocationValid;
+		try {
+			final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText());
+			storageLocationValid = Cryptor.getDefaultCryptor().isStorage(storagePath);
+		} catch(InvalidPathException ex) {
+			LOG.trace("Invalid path: " + workDirTextField.getText(), ex);
+			storageLocationValid = false;
+		}
+		passwordField.setDisable(!storageLocationValid);
+		startServerButton.setDisable(!storageLocationValid);
+	}
+	
+	@FXML
+	protected void startStopServer(ActionEvent event) {
+		messageLabel.setText(null);
+		if (WebDAVServer.getInstance().isRunning()) {
+			this.tryStop();
+			Cryptor.getDefaultCryptor().swipeSensitiveData();
+		} else if (this.unlockStorage()) {
+			this.tryStart();
+		}
+	}
+	
+	private boolean unlockStorage() {
+		final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText());
+		final CharSequence password = passwordField.getCharacters();
+		try {
+			Cryptor.getDefaultCryptor().unlockStorage(storagePath, password);
+			return true;
+		} catch (InvalidStorageLocationException 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();
+		}
+		return false;
+	}
+	
+	private void tryStart() {
+		try {
+			final Settings settings = Settings.load();
+			if (WebDAVServer.getInstance().start(settings.getWebdavWorkDir(), settings.getPort())) {
+				startServerButton.setText(localization.getString("access.button.stopServer"));
+				passwordField.setDisable(true);
+			}
+		} catch (NumberFormatException ex) {
+			LOG.error("Invalid port", ex);
+		}
+	}
+	
+	private void tryStop() {
+		if (WebDAVServer.getInstance().stop()) {
+			startServerButton.setText(localization.getString("access.button.startServer"));
+			passwordField.setDisable(false);
+		}
+	}
+
+}

+ 76 - 0
oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/AdvancedController.java

@@ -0,0 +1,76 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.ui;
+
+import java.net.URL;
+import java.util.ResourceBundle;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.event.EventHandler;
+import javafx.fxml.FXML;
+import javafx.fxml.Initializable;
+import javafx.scene.control.TextField;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.layout.GridPane;
+import de.sebastianstenzel.oce.ui.settings.Settings;
+
+public class AdvancedController implements Initializable {
+	
+	private static final Logger LOG = LoggerFactory.getLogger(AdvancedController.class);
+
+	@FXML
+	private GridPane rootGridPane;
+
+	@FXML
+	private TextField portTextField;
+
+	@Override
+	public void initialize(URL url, ResourceBundle rb) {
+		portTextField.setText(String.valueOf(Settings.load().getPort()));
+		portTextField.addEventFilter(KeyEvent.KEY_TYPED, new NumericKeyTypeEventFilter());
+		portTextField.focusedProperty().addListener(new PortTextFieldFocusListener());
+	}
+	
+	/**
+	 * Consumes key events, if typed key is not 0-9.
+	 */
+	private static final class NumericKeyTypeEventFilter implements EventHandler<KeyEvent> {
+		public void handle(KeyEvent t) {
+			if (t.getCharacter() == null || t.getCharacter().length() == 0) {
+				return;
+			}
+			char c = t.getCharacter().charAt(0);
+			if (!(c >= '0' && c <= '9')) {
+				t.consume();
+			}
+		}
+	}
+	
+	/**
+	 * Saves port settings, when textfield loses focus.
+	 */
+	private class PortTextFieldFocusListener implements ChangeListener<Boolean> {
+		@Override
+		public void changed(ObservableValue<? extends Boolean> property, Boolean wasFocused, Boolean isFocused) {
+			final Settings settings = Settings.load();
+			try {
+				int port = Integer.valueOf(portTextField.getText());
+				settings.setPort(port);
+			} catch (NumberFormatException ex) {
+				LOG.warn("Invalid port " + portTextField.getText());
+				portTextField.setText(String.valueOf(settings.getPort()));
+			}
+		}
+	}
+
+}

+ 124 - 0
oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/InitializeController.java

@@ -0,0 +1,124 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.ui;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.FileSystems;
+import java.nio.file.InvalidPathException;
+import java.util.ResourceBundle;
+
+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.Label;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.GridPane;
+import javafx.stage.DirectoryChooser;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import de.sebastianstenzel.oce.crypto.Cryptor;
+import de.sebastianstenzel.oce.crypto.StorageCrypting.AlreadyInitializedException;
+import de.sebastianstenzel.oce.ui.controls.SecPasswordField;
+
+public class InitializeController implements Initializable {
+	
+	private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class);
+	
+	private ResourceBundle localization;
+	@FXML private GridPane rootGridPane;
+	@FXML private TextField workDirTextField;
+	@FXML private SecPasswordField passwordField;
+	@FXML private SecPasswordField retypePasswordField;
+	@FXML private Button initWorkDirButton;
+	@FXML private Label messageLabel;
+	
+	@Override
+	public void initialize(URL url, ResourceBundle rb) {
+		this.localization = rb;
+		passwordField.textProperty().addListener(new PasswordChangeListener());
+		retypePasswordField.textProperty().addListener(new RetypePasswordChangeListener());
+	}
+	
+	/**
+	 * Step 1: Choose a directory, that shall be encrypted.
+	 * On success, step 2 will be enabled.
+	 */
+	@FXML
+	protected void chooseWorkDir(ActionEvent event) {
+		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 && file.canWrite()) {
+			workDirTextField.setText(file.getPath());
+			passwordField.setDisable(false);
+			passwordField.selectAll();
+			passwordField.requestFocus();
+		}
+	}
+	
+	/**
+	 * Step 2: 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 3: 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(newValue);
+			initWorkDirButton.setDisable(!passwordsAreEqual);
+		}
+	}
+	
+	/**
+	 * Step 4: Generate master password file in working directory.
+	 * On success, print success message.
+	 */
+	@FXML
+	protected void initWorkDir(ActionEvent event) {
+		try {
+			Cryptor.getDefaultCryptor().initializeStorage(FileSystems.getDefault().getPath(workDirTextField.getText()), passwordField.getText());
+			Cryptor.getDefaultCryptor().swipeSensitiveData();
+		} catch (AlreadyInitializedException ex) {
+			messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
+		} catch(InvalidPathException ex) {
+			messageLabel.setText(localization.getString("initialize.messageLabel.invalidPath"));
+		} catch (IOException ex) {
+			LOG.error("I/O Exception", ex);
+		} finally {
+			swipePasswordFields();
+		}
+	}
+	
+	private void swipePasswordFields() {
+		passwordField.swipe();
+		retypePasswordField.swipe();
+	}
+
+	
+}

+ 47 - 0
oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/MainApplication.java

@@ -0,0 +1,47 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.ui;
+
+import java.io.IOException;
+import java.util.ResourceBundle;
+
+import javafx.application.Application;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+import de.sebastianstenzel.oce.ui.settings.Settings;
+import de.sebastianstenzel.oce.webdav.WebDAVServer;
+
+public class MainApplication extends Application {
+	
+	public static void main(String[] args) {
+		launch(args);
+	}
+
+	@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 Scene scene = new Scene(root);
+		primaryStage.setTitle("Open Cloud Encryptor");
+		primaryStage.setScene(scene);
+		primaryStage.sizeToScene();
+		primaryStage.setResizable(false);
+		primaryStage.show();
+	}
+	
+	@Override
+	public void stop() throws Exception {
+		WebDAVServer.getInstance().stop();
+		Settings.save();
+		super.stop();
+	}
+
+}

+ 51 - 0
oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/MainController.java

@@ -0,0 +1,51 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.ui;
+
+import javafx.event.ActionEvent;
+import javafx.fxml.FXML;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.VBox;
+
+public class MainController {
+	
+	@FXML
+	private VBox rootVBox;
+	
+	@FXML
+	private Pane initializePanel;
+	
+	@FXML
+	private Pane accessPanel;
+	
+	@FXML
+	private Pane advancedPanel;
+	
+	@FXML
+	protected void showInitializePane(ActionEvent event) {
+		showPanel(initializePanel);
+	}
+	
+	@FXML
+	protected void showAccessPane(ActionEvent event) {
+		showPanel(accessPanel);
+	}
+	
+	@FXML
+	protected void showAdvancedPane(ActionEvent event) {
+		showPanel(advancedPanel);
+	}
+	
+	private void showPanel(Pane panel) {
+		rootVBox.getChildren().remove(1);
+		rootVBox.getChildren().add(panel);
+		rootVBox.getScene().getWindow().sizeToScene();
+	}
+
+}

+ 44 - 0
oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/controls/SecPasswordField.java

@@ -0,0 +1,44 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.ui.controls;
+
+import java.util.Arrays;
+
+import javafx.scene.control.PasswordField;
+
+/**
+ * Compromise in security. While the text can be swiped, any access to the {@link #getText()} method will create a copy of the String in the heap.
+ */
+public class SecPasswordField extends PasswordField {
+	
+	private static final char SWIPE_CHAR = ' ';
+	
+	/**
+	 * {@link #getContent()} uses a StringBuilder, which in turn is backed by a char[].
+	 * The delete operation of AbstractStringBuilder closes the gap, that forms by deleting chars, by moving up the following chars.
+	 * <br/>
+	 * Imagine the following example with <code>pass</code> being the password, <code>x</code> being the swipe char and <code>'</code> being the offset of the char array:
+	 * <ol>
+	 * 	<li>Append filling chars to the end of the password: <code>passxxxx'</code></li>
+	 * 	<li>Delete first 4 chars. Internal implementation will then copy the following chars to the position, where the deletion occured: <code>xxxx'xxxx</code></li>
+	 * 	<li>Delete first 4 chars again, as we appended 4 chars in step 1: <code>'xxxxxx</code></li>
+	 * </ol>
+	 */
+	public void swipe() {
+		final int pwLength = this.getContent().length();
+		final char[] fillingChars = new char[pwLength];
+		Arrays.fill(fillingChars, SWIPE_CHAR);
+		this.getContent().insert(pwLength, new String(fillingChars), false);
+		this.getContent().delete(0, pwLength, true);
+		this.getContent().delete(0, pwLength, true);
+	}
+	
+	
+
+}

+ 213 - 0
oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/controls/SecurePasswordField.java

@@ -0,0 +1,213 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.ui.controls;
+
+import java.nio.CharBuffer;
+import java.util.Arrays;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.value.ChangeListener;
+import javafx.scene.control.TextInputControl;
+
+import com.sun.javafx.binding.ExpressionHelper;
+
+/**
+ * Don't use, won't work.
+ * Just an experiment. Will be moved to a separate branch, when I have some time for cleanup stuff.
+ */
+@Deprecated
+public class SecurePasswordField extends TextInputControl {
+
+	public SecurePasswordField() {
+		this("");
+	}
+
+	public SecurePasswordField(String text) {
+		super(new SecureContent());
+		getStyleClass().add("password-field");
+		this.setText(text);
+	}
+
+	public void swipe() {
+		final Content content = this.getContent();
+		if (content instanceof SecureContent) {
+			final SecureContent secureContent = (SecureContent) content;
+			secureContent.swipe();
+		}
+	}
+	
+	@Override
+	public void cut() {
+		// No-op
+	}
+	
+	@Override
+	public void copy() {
+		// No-op
+	}
+
+	/**
+	 * Content based on a CharBuffer, whose backing char[] can be swiped on demand.
+	 */
+	private static final class SecureContent implements Content {
+		private static final int INITIAL_BUFFER_LENGTH = 64;
+		private static final int BUFFER_GROWTH_FACTOR = 2;
+
+		private ExpressionHelper<String> helper = null;
+		private CharBuffer buffer = CharBuffer.allocate(INITIAL_BUFFER_LENGTH);
+
+		public void swipe() {
+			assert (buffer.hasArray());
+			Arrays.fill(buffer.array(), (char) 0);
+			buffer.position(0);
+		}
+
+		@Override
+		public String get() {
+			return buffer.toString();
+		}
+
+		@Override
+		public void addListener(ChangeListener<? super String> changeListener) {
+			helper = ExpressionHelper.addListener(helper, this, changeListener);
+
+		}
+
+		@Override
+		public String getValue() {
+			return get();
+		}
+
+		@Override
+		public void removeListener(ChangeListener<? super String> changeListener) {
+			helper = ExpressionHelper.removeListener(helper, changeListener);
+
+		}
+
+		@Override
+		public void addListener(InvalidationListener listener) {
+			helper = ExpressionHelper.addListener(helper, this, listener);
+
+		}
+
+		@Override
+		public void removeListener(InvalidationListener listener) {
+			helper = ExpressionHelper.removeListener(helper, listener);
+
+		}
+
+		@Override
+		public void delete(int start, int end, boolean notifyListeners) {
+			final int delLen = end - start;
+			final int pos = buffer.position();
+			if (delLen <= 0 || end > pos) {
+				return;
+			}
+			final char[] followingChars = new char[pos - end];
+			try {
+				// save follow-up chars:
+				buffer.get(followingChars, end, buffer.position() - end);
+				// close gap:
+				buffer.put(followingChars, start, followingChars.length);
+				// zeroing out freed space at end of buffer
+				final char[] zeros = new char[delLen];
+				buffer.put(zeros, pos - delLen, delLen);
+				// adjust length:
+				buffer.position(pos - delLen);
+				if (notifyListeners) {
+					ExpressionHelper.fireValueChangedEvent(helper);
+				}
+			} finally {
+				// swipe tmp variable
+				Arrays.fill(followingChars, (char) 0);
+			}
+		}
+
+		@Override
+		public String get(int start, int end) {
+			final char[] tmp = new char[end - start];
+			try {
+				buffer.get(tmp, start, end - start);
+				return new String(tmp);
+			} finally {
+				Arrays.fill(tmp, (char) 0);
+			}
+		}
+
+		@Override
+		public void insert(int index, String text, boolean notifyListeners) {
+			if (text.isEmpty()) {
+				return;
+			}
+			final String filteredInput;
+			if (SecurePasswordField.containsIllegalChars(text)) {
+				filteredInput = SecurePasswordField.removeIllegalChars(text);
+			} else {
+				filteredInput = text;
+			}
+			while (filteredInput.length() > buffer.remaining()) {
+				extendBuffer();
+			}
+			final int pos = buffer.position();
+			final char[] followingChars = new char[pos - index];
+			try {
+				// create empty gap for new text:
+				buffer.get(followingChars, index, followingChars.length);
+				// insert text at index:
+				buffer.put(filteredInput, index, filteredInput.length() - index);
+				// insert chars previously at this position afterwards
+				final int posAfterNewText = index + filteredInput.length();
+				buffer.put(followingChars, posAfterNewText, followingChars.length - posAfterNewText);
+				// adjust length:
+				buffer.position(pos + filteredInput.length());
+				if (notifyListeners) {
+					ExpressionHelper.fireValueChangedEvent(helper);
+				}
+			} finally {
+				// swipe tmp variable
+				Arrays.fill(followingChars, (char) 0);
+			}
+		}
+
+		private void extendBuffer() {
+			int currentCapacity = buffer.capacity();
+			buffer = CharBuffer.allocate(currentCapacity * BUFFER_GROWTH_FACTOR);
+		}
+
+		@Override
+		public int length() {
+			return buffer.length();
+		}
+
+	}
+
+	static boolean containsIllegalChars(String string) {
+		for (char c : string.toCharArray()) {
+			if (SecurePasswordField.isIllegalChar(c)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	static String removeIllegalChars(String string) {
+		final StringBuilder sb = new StringBuilder(string.length());
+		for (char c : string.toCharArray()) {
+			if (!SecurePasswordField.isIllegalChar(c)) {
+				sb.append(c);
+			}
+		}
+		return sb.toString();
+	}
+
+	static boolean isIllegalChar(char c) {
+		return (c == 0x7F || c == 0x0A || c == 0x09 || c < 0x20);
+	}
+
+}

+ 117 - 0
oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/settings/Settings.java

@@ -0,0 +1,117 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.ui.settings;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Serializable;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+@JsonPropertyOrder(value = { "webdavWorkDir" })
+public class Settings implements Serializable {
+
+	private static final long serialVersionUID = 7609959894417878744L;
+	private static final Logger LOG = LoggerFactory.getLogger(Settings.class);
+	private static final Path SETTINGS_DIR;
+	private static final String SETTINGS_FILE = "settings.json";
+	private static final ObjectMapper JSON_OM = new ObjectMapper();
+	private static Settings INSTANCE = null;
+	
+	static {
+		final String home = System.getProperty("user.home", ".");
+		final String appdata = System.getenv("APPDATA");
+		final String os = System.getProperty("os.name").toLowerCase();
+		final FileSystem fs = FileSystems.getDefault();
+
+		if (os.contains("win") && appdata != null) {
+			SETTINGS_DIR = fs.getPath(appdata, "opencloudencryptor");
+		} else if (os.contains("win") && appdata == null) {
+			SETTINGS_DIR = fs.getPath(home, ".opencloudencryptor");
+		} else if (os.contains("mac")) {
+			SETTINGS_DIR = fs.getPath(home, "Library/Application Support/opencloudencryptor");
+		} else {
+			// (os.contains("solaris") || os.contains("sunos") || os.contains("linux") || os.contains("unix"))
+			SETTINGS_DIR = fs.getPath(home, ".opencloudencryptor");
+		}
+	}
+	
+	
+	private String webdavWorkDir;
+	private int port;
+	
+	
+	private Settings() {
+		// private constructor
+	}
+	
+	public static synchronized Settings load() {
+		if (INSTANCE == null) {
+			try {
+				Files.createDirectories(SETTINGS_DIR);
+				final Path settingsFile = SETTINGS_DIR.resolve(SETTINGS_FILE);
+				final InputStream in = Files.newInputStream(settingsFile, StandardOpenOption.READ);
+				INSTANCE = JSON_OM.readValue(in, Settings.class);
+				return INSTANCE;
+			} catch (IOException e) {
+				LOG.warn("Failed to load settings, creating new one.");
+				INSTANCE = Settings.defaultSettings();
+			}
+		}
+		return INSTANCE;
+	}
+	
+	public static synchronized void save() {
+		if (INSTANCE != null) {
+			try {
+				Files.createDirectories(SETTINGS_DIR);
+				final Path settingsFile = SETTINGS_DIR.resolve(SETTINGS_FILE);
+				final OutputStream out = Files.newOutputStream(settingsFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
+				JSON_OM.writeValue(out, INSTANCE);
+			} catch (IOException e) {
+				LOG.error("Failed to save settings.", e);
+			}
+		}
+	}
+	
+	private static Settings defaultSettings() {
+		final Settings result = new Settings();
+		result.setWebdavWorkDir(System.getProperty("user.home", "."));
+		return result;
+	}
+	
+	/* Getter/Setter */
+
+	public String getWebdavWorkDir() {
+		return webdavWorkDir;
+	}
+
+	public void setWebdavWorkDir(String webdavWorkDir) {
+		this.webdavWorkDir = webdavWorkDir;
+	}
+
+	public int getPort() {
+		return port;
+	}
+
+	public void setPort(int port) {
+		this.port = port;
+	}
+
+}

+ 51 - 0
oce-main/oce-ui/src/main/resources/access.fxml

@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Copyright (c) 2014 Sebastian Stenzel
+  This file is licensed under the terms of the MIT license.
+  See the LICENSE.txt file for more info.
+  
+  Contributors:
+      Sebastian Stenzel - initial API and implementation
+-->
+<?import java.net.*?>
+<?import javafx.geometry.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.layout.*?>
+<?import javafx.scene.text.*?>
+<?import de.sebastianstenzel.oce.ui.controls.*?>
+
+
+<GridPane fx:id="rootGridPane" fx:controller="de.sebastianstenzel.oce.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" />
+		<TextField fx:id="workDirTextField" GridPane.rowIndex="0" GridPane.columnIndex="1" editable="true" />
+		<Button fx:id="chooseWorkDirButton" GridPane.rowIndex="0" GridPane.columnIndex="2" text="%access.button.chooseWorkDir" onAction="#chooseWorkDir" />
+		
+		<!-- Row 1 -->
+		<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%access.label.password" />
+		<SecPasswordField fx:id="passwordField" GridPane.rowIndex="1" GridPane.columnIndex="1" disable="true" />
+		
+		<!-- Row 2 -->
+		<Button fx:id="startServerButton" text="%access.button.startServer" GridPane.rowIndex="2" GridPane.columnIndex="1" disable="true" onAction="#startStopServer" />
+		
+		<!-- Row 3 -->
+		<Label fx:id="messageLabel" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="3" textAlignment="CENTER" />
+	</children>
+</GridPane>
+
+

+ 38 - 0
oce-main/oce-ui/src/main/resources/advanced.fxml

@@ -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.*?>
+
+
+<GridPane fx:id="rootGridPane" fx:controller="de.sebastianstenzel.oce.ui.AdvancedController" 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="250" hgrow="ALWAYS" />
+	</columnConstraints>
+
+	<children>
+		<!-- Row 0 -->
+		<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%advanced.label.port" />
+		<TextField fx:id="portTextField" GridPane.rowIndex="0" GridPane.columnIndex="1" />
+	</children>
+</GridPane>
+
+

+ 55 - 0
oce-main/oce-ui/src/main/resources/initialize.fxml

@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Copyright (c) 2014 Sebastian Stenzel
+  This file is licensed under the terms of the MIT license.
+  See the LICENSE.txt file for more info.
+  
+  Contributors:
+      Sebastian Stenzel - initial API and implementation
+-->
+<?import java.net.*?>
+<?import javafx.geometry.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.layout.*?>
+<?import javafx.scene.text.*?>
+<?import de.sebastianstenzel.oce.ui.controls.*?>
+
+
+<GridPane fx:id="rootGridPane" fx:controller="de.sebastianstenzel.oce.ui.InitializeController" xmlns:fx="http://javafx.com/fxml" styleClass="root" gridLinesVisible="false" vgap="5" hgap="5" prefWidth="480">
+	<stylesheets>
+		<URL value="@panels.css" />
+	</stylesheets>
+
+	<padding>
+		<Insets top="10" right="10" bottom="10" left="10" />
+	</padding>
+
+	<columnConstraints>
+		<ColumnConstraints minWidth="150" maxWidth="150" hgrow="NEVER" />
+		<ColumnConstraints minWidth="200" hgrow="ALWAYS" />
+		<ColumnConstraints minWidth="50" maxWidth="120" hgrow="NEVER" />
+	</columnConstraints>
+
+	<children>
+		<!-- Row 0 -->
+		<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%initialize.label.workDir" />
+		<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" />
+		
+		<!-- Row 1 -->
+		<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%initialize.label.password" />
+		<SecPasswordField fx:id="passwordField" GridPane.rowIndex="1" GridPane.columnIndex="1" disable="true" />
+		
+		<!-- Row 2 -->
+		<Label GridPane.rowIndex="2" GridPane.columnIndex="0" text="%initialize.label.retypePassword" />
+		<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="2" GridPane.columnIndex="1" disable="true" />
+		
+		<!-- Row 3 -->
+		<Button fx:id="initWorkDirButton" text="%initialize.button.initWorkDir" GridPane.rowIndex="3" GridPane.columnIndex="1"  onAction="#initWorkDir" disable="true"/>
+		
+		<!-- Row 4 -->
+		<Label fx:id="messageLabel" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="3" textAlignment="CENTER" />
+	</children>
+</GridPane>
+
+

+ 35 - 0
oce-main/oce-ui/src/main/resources/localization.properties

@@ -0,0 +1,35 @@
+#-------------------------------------------------------------------------------
+# 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
+#-------------------------------------------------------------------------------
+# main.fxml
+toolbarbutton.initialize=Initialize Vault
+toolbarbutton.access=Access Vault
+toolbarbutton.advanced=Advanced Settings
+
+# initialize.fxml
+initialize.label.workDir=New vault location
+initialize.button.chooseWorkDir=Choose...
+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.
+
+# access.fxml
+access.label.workDir=Vault location
+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.
+
+# advanced.fxml
+advanced.label.port=WebDAV Port

+ 40 - 0
oce-main/oce-ui/src/main/resources/main.css

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

+ 42 - 0
oce-main/oce-ui/src/main/resources/main.fxml

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Copyright (c) 2014 Sebastian Stenzel
+  This file is licensed under the terms of the MIT license.
+  See the LICENSE.txt file for more info.
+  
+  Contributors:
+      Sebastian Stenzel - initial API and implementation
+-->
+<?import java.net.*?>
+<?import javafx.geometry.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.layout.*?>
+<?import javafx.scene.text.*?>
+
+<VBox fx:id="rootVBox" fx:controller="de.sebastianstenzel.oce.ui.MainController" xmlns:fx="http://javafx.com/fxml">
+	<stylesheets>
+		<URL value="@main.css" />
+	</stylesheets>
+
+	<fx:define>
+		<fx:include fx:id="initializePanel" source="initialize.fxml" />
+		<fx:include fx:id="accessPanel" source="access.fxml" />
+		<fx:include fx:id="advancedPanel" source="advanced.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" />
+				<ToggleButton text="%toolbarbutton.advanced" toggleGroup="$toolbarButtonGroup" onAction="#showAdvancedPane" />
+			</items>
+		</ToolBar>
+		<fx:reference source="accessPanel"/>
+	</children>
+</VBox>
+
+

+ 39 - 0
oce-main/oce-ui/src/main/resources/panels.css

@@ -0,0 +1,39 @@
+#-------------------------------------------------------------------------------
+# 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
+#-------------------------------------------------------------------------------
+@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 {
+	-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;
+}
+
+.button:armed,
+.button:selected {
+	-fx-background-color: linear-gradient(to bottom, #DDDDDD, #CCCCCC 30%, #EEEEEE);
+}

+ 77 - 0
oce-main/oce-webdav/pom.xml

@@ -0,0 +1,77 @@
+<?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">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>de.sebastianstenzel.oce</groupId>
+		<artifactId>oce-main</artifactId>
+		<version>0.0.1-SNAPSHOT</version>
+	</parent>
+	<artifactId>oce-webdav</artifactId>
+	<name>Open Cloud Encryptor WebDAV module</name>
+
+	<properties>
+		<jetty.version>9.1.0.v20131115</jetty.version>
+		<webdavservlet.version>2.0</webdavservlet.version>
+		<commons.transaction.version>1.2</commons.transaction.version>
+		<jta.version>1.1</jta.version>
+	</properties>
+
+	<dependencies>
+		<dependency>
+			<groupId>de.sebastianstenzel.oce</groupId>
+			<artifactId>oce-crypto</artifactId>
+			<version>${project.parent.version}</version>
+		</dependency>
+
+		<!-- Logging -->
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>slf4j-log4j12</artifactId>
+		</dependency>
+
+		<!-- Jetty (Servlet Container) -->
+		<dependency>
+			<groupId>org.eclipse.jetty</groupId>
+			<artifactId>jetty-server</artifactId>
+			<version>${jetty.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.eclipse.jetty</groupId>
+			<artifactId>jetty-webapp</artifactId>
+			<version>${jetty.version}</version>
+		</dependency>
+
+		<!-- WebDAV Servlet -->
+		<dependency>
+			<groupId>net.sf.webdav-servlet</groupId>
+			<artifactId>webdav-servlet</artifactId>
+			<version>${webdavservlet.version}</version>
+		</dependency>
+
+		<!-- I/O -->
+		<dependency>
+			<groupId>commons-io</groupId>
+			<artifactId>commons-io</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>net.java.xadisk</groupId>
+			<artifactId>xadisk</artifactId>
+			<version>1.2.2</version>
+		</dependency>
+		
+		<!-- JEE 6 implementation used by XADisk -->
+		<dependency>
+			<groupId>org.apache.openejb</groupId>
+			<artifactId>javaee-api</artifactId>
+			<version>6.0-5</version>
+		</dependency>
+	</dependencies>
+</project>

+ 39 - 0
oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/EnhancedWebDavServlet.java

@@ -0,0 +1,39 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.webdav;
+
+import java.io.File;
+
+import net.sf.webdav.IWebdavStore;
+import net.sf.webdav.WebdavServlet;
+
+public class EnhancedWebDavServlet extends WebdavServlet {
+	
+	private static final long serialVersionUID = 7198160595132838601L;
+	
+	private EnhancedWebdavStore<?> enhancedStore;
+	
+	@Override
+	protected IWebdavStore constructStore(String clazzName, File root) {
+		final IWebdavStore store = super.constructStore(clazzName, root);
+		if (store instanceof EnhancedWebdavStore) {
+			this.enhancedStore = (EnhancedWebdavStore<?>) store;
+		}
+		return store;
+	}
+	
+	@Override
+	public void destroy() {
+		if (this.enhancedStore != null) {
+			this.enhancedStore.destroy();
+		}
+		super.destroy();
+	}
+
+}

+ 120 - 0
oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/EnhancedWebdavStore.java

@@ -0,0 +1,120 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.webdav;
+
+import java.io.InputStream;
+import java.security.Principal;
+
+import net.sf.webdav.ITransaction;
+import net.sf.webdav.IWebdavStore;
+import net.sf.webdav.StoredObject;
+
+public abstract class EnhancedWebdavStore <T extends ITransaction> implements IWebdavStore {
+	
+	private final Class<T> transactionClass;
+	
+	protected EnhancedWebdavStore(final Class<T> transactionClass) {
+		this.transactionClass = transactionClass;
+	}
+	
+	private T cast(final ITransaction transaction) {
+		if (transactionClass.isAssignableFrom(transaction.getClass())) {
+			return transactionClass.cast(transaction);
+		} else {
+			throw new IllegalStateException("transaction "  + transaction + " is not of type " + transactionClass.getName());
+		}
+	}
+	
+	abstract void destroy();
+
+	@Override
+	public final ITransaction begin(Principal principal) {
+		return beginTransactionInternal(principal);
+	}
+	
+	protected abstract T beginTransactionInternal(Principal principal);
+
+	@Override
+	public final void checkAuthentication(ITransaction transaction) {
+		checkAuthenticationInternal(cast(transaction));
+	}
+	
+	protected abstract void checkAuthenticationInternal(T transaction);
+
+	@Override
+	public void commit(ITransaction transaction) {
+		commitInternal(cast(transaction));
+	}
+	
+	protected abstract void commitInternal(T transaction);
+
+	@Override
+	public void rollback(ITransaction transaction) {
+		rollbackInternal(cast(transaction));
+	}
+	
+	protected abstract void rollbackInternal(T transaction);
+
+	@Override
+	public void createFolder(ITransaction transaction, String folderUri) {
+		createFolderInternal(cast(transaction), folderUri);
+	}
+	
+	protected abstract void createFolderInternal(T transaction, String folderUri);
+
+	@Override
+	public void createResource(ITransaction transaction, String resourceUri) {
+		createResourceInternal(cast(transaction), resourceUri);
+	}
+	
+	protected abstract void createResourceInternal(T transaction, String resourceUri);
+
+	@Override
+	public InputStream getResourceContent(ITransaction transaction, String resourceUri) {
+		return getResourceContentInternal(cast(transaction), resourceUri);
+	}
+	
+	protected abstract InputStream getResourceContentInternal(T transaction, String resourceUri);
+
+	@Override
+	public long setResourceContent(ITransaction transaction, String resourceUri, InputStream content, String contentType, String characterEncoding) {
+		return setResourceContentInternal(cast(transaction), resourceUri, content, contentType, characterEncoding);
+	}
+	
+	protected abstract long setResourceContentInternal(T transaction, String resourceUri, InputStream content, String contentType, String characterEncoding);
+
+	@Override
+	public String[] getChildrenNames(ITransaction transaction, String folderUri) {
+		return getChildrenNamesInternal(cast(transaction), folderUri);
+	}
+	
+	protected abstract String[] getChildrenNamesInternal(T transaction, String folderUri);
+
+	@Override
+	public long getResourceLength(ITransaction transaction, String path) {
+		return getResourceLengthInternal(cast(transaction), path);
+	}
+	
+	protected abstract long getResourceLengthInternal(T transaction, String path);
+
+	@Override
+	public void removeObject(ITransaction transaction, String uri) {
+		removeObjectInternal(cast(transaction), uri);
+	}
+	
+	protected abstract void removeObjectInternal(T transaction, String uri);
+
+	@Override
+	public StoredObject getStoredObject(ITransaction transaction, String uri) {
+		return getStoredObjectInternal(cast(transaction), uri);
+	}
+	
+	protected abstract StoredObject getStoredObjectInternal(T transaction, String uri);
+
+}

+ 183 - 0
oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/FsWebdavCryptoAdapter.java

@@ -0,0 +1,183 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.webdav;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.io.FilenameUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.xadisk.additional.XAFileInputStreamWrapper;
+import org.xadisk.additional.XAFileOutputStreamWrapper;
+import org.xadisk.bridge.proxies.interfaces.Session;
+import org.xadisk.filesystem.exceptions.NoTransactionAssociatedException;
+import org.xadisk.filesystem.exceptions.XAApplicationException;
+
+import de.sebastianstenzel.oce.crypto.Cryptor;
+import de.sebastianstenzel.oce.crypto.TransactionAwareFileAccess;
+import de.sebastianstenzel.oce.crypto.aes256.AesCryptor;
+
+final class FsWebdavCryptoAdapter {
+
+	private static final Logger LOG = LoggerFactory.getLogger(FsWebdavCryptoAdapter.class);
+	private final Cryptor cryptor = new AesCryptor();
+	private final Path workDir;
+
+	public FsWebdavCryptoAdapter(final String workingDirectory) {
+		this.workDir = FileSystems.getDefault().getPath(workingDirectory);
+	}
+
+	/**
+	 * Creates a new folder and initializes its metadata file.
+	 * 
+	 * @return The pseudonymized URI of the created folder.
+	 */
+	public String initializeNewFolder(final Session session, final String clearUri) throws IOException {
+		final String pseudonymized = this.pseudonymizedUri(session, clearUri);
+		final TransactionAwareFileAccess accessor = new FileLoader(session);
+		final File folder = accessor.resolveUri(pseudonymized).toFile();
+		try {
+			if (!session.fileExistsAndIsDirectory(folder)) {
+				session.createFile(folder, true);
+			}
+		} catch (NoTransactionAssociatedException ex) {
+			throw new IllegalStateException("Session closed.", ex);
+		} catch (XAApplicationException | InterruptedException ex) {
+			throw new IOException(ex);
+		}
+		return pseudonymized;
+	}
+
+	/**
+	 * @return List of all cleartext child resource names for the directory with
+	 *         the given URI.
+	 */
+	public String[] uncoveredChildrenNames(final Session session, final String pseudonymizedUri) throws IOException {
+		try {
+			final TransactionAwareFileAccess accessor = new FileLoader(session);
+			final File file = accessor.resolveUri(pseudonymizedUri).toFile();
+			final List<String> result = new ArrayList<>();
+			if (file.isDirectory()) {
+				String[] children = session.listFiles(file);
+				for (final String child : children) {
+					final String pseudonym = FilenameUtils.concat(pseudonymizedUri, child);
+					final String cleartext = cryptor.uncoverPseudonym(pseudonym, accessor);
+					if (cleartext != null) {
+						result.add(FilenameUtils.getName(cleartext));
+					}
+				}
+			}
+			return result.toArray(new String[result.size()]);
+		} catch (XAApplicationException | InterruptedException e) {
+			throw new IOException(e);
+		}
+	}
+
+	/**
+	 * @return The pseudonyimzed URI for the given clear URI.
+	 */
+	public String pseudonymizedUri(final Session session, final String clearUri) throws IOException {
+		final TransactionAwareFileAccess fileLoader = new FileLoader(session);
+		return cryptor.createPseudonym(clearUri, fileLoader);
+	}
+
+	/**
+	 * Deletes a pseudonym.
+	 */
+	public void deletePseudonym(final Session session, final String pseudonymizedUri) throws IOException {
+		final TransactionAwareFileAccess fileLoader = new FileLoader(session);
+		cryptor.deletePseudonym(pseudonymizedUri, fileLoader);
+	}
+	
+	public InputStream decryptResource(Session session, String pseudonymized) throws IOException {
+		final TransactionAwareFileAccess accessor = new FileLoader(session);
+		return cryptor.decryptFile(pseudonymized, accessor);
+	}
+	
+	public long encryptResource(Session session, String pseudonymized, InputStream in) throws IOException {
+		final TransactionAwareFileAccess accessor = new FileLoader(session);
+		return cryptor.encryptFile(pseudonymized, in, accessor);
+	}
+	
+
+	public long getDecryptedFileLength(Session session, String pseudonymized) throws IOException {
+		final TransactionAwareFileAccess accessor = new FileLoader(session);
+		return cryptor.getDecryptedContentLength(pseudonymized, accessor);
+	}
+
+
+	/**
+	 * Transaction-aware implementation of MetadataLoading.
+	 */
+	private class FileLoader implements TransactionAwareFileAccess {
+
+		private final Session session;
+
+		private FileLoader(final Session session) {
+			this.session = session;
+		}
+
+		@Override
+		public InputStream openFileForRead(Path path) throws IOException {
+			try {
+				final File file = path.toFile();
+				if (!session.fileExists(file)) {
+					session.createFile(file, false);
+				}
+				return new XAFileInputStreamWrapper(session.createXAFileInputStream(file));
+			} catch (XAApplicationException | InterruptedException ex) {
+				LOG.error("Failed to open resource for reading: " + path.toString(), ex);
+				throw new IOException("Failed to open resource for reading: " + path.toString(), ex);
+			}
+		}
+
+		@Override
+		public OutputStream openFileForWrite(Path path) throws IOException {
+			try {
+				final File file = path.toFile();
+				if (!session.fileExists(file)) {
+					session.createFile(file, false);
+				} else {
+					session.truncateFile(file, 0);
+				}
+				return new XAFileOutputStreamWrapper(session.createXAFileOutputStream(file, false));
+			} catch (NoTransactionAssociatedException ex) {
+				LOG.error("Session closed.", ex);
+				throw new IllegalStateException("Session closed.", ex);
+			} catch (XAApplicationException | InterruptedException ex) {
+				LOG.error("Failed to open resource for writing: " + path.toString(), ex);
+				throw new IOException("Failed to open resource for writing: " + path.toString(), ex);
+			}
+		}
+		
+		@Override
+		public Path resolveUri(String uri) {
+			return workDir.resolve(removeLeadingSlash(uri));
+		}
+		
+		private String removeLeadingSlash(String path) {
+			if (path.length() == 0) {
+				return path;
+			} else if (path.charAt(0) == '/') {
+				return path.substring(1);
+			} else {
+				return path;
+			}
+		}
+
+	}
+
+}

+ 228 - 0
oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/FsWebdavResourceHandler.java

@@ -0,0 +1,228 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.webdav;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.Principal;
+import java.util.Date;
+
+import net.sf.webdav.StoredObject;
+import net.sf.webdav.exceptions.WebdavException;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.FilenameUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.xadisk.bridge.proxies.interfaces.Session;
+import org.xadisk.bridge.proxies.interfaces.XAFileSystem;
+import org.xadisk.bridge.proxies.interfaces.XAFileSystemProxy;
+import org.xadisk.filesystem.exceptions.NoTransactionAssociatedException;
+import org.xadisk.filesystem.exceptions.XAApplicationException;
+import org.xadisk.filesystem.standalone.StandaloneFileSystemConfiguration;
+
+public class FsWebdavResourceHandler extends EnhancedWebdavStore<FsWebdavTransaction> {
+
+	private static final Logger LOG = LoggerFactory.getLogger(FsWebdavResourceHandler.class);
+	private static final String XA_SYS_DIR_PREFIX = "oce-webdav";
+	private static final Path XA_SYS_DIR;
+	
+	static {
+		final String tmpDirName = (String) System.getProperties().get("java.io.tmpdir");
+		final Path tmpDirPath = FileSystems.getDefault().getPath(tmpDirName);
+		try {
+			XA_SYS_DIR = Files.createTempDirectory(tmpDirPath, XA_SYS_DIR_PREFIX);
+		} catch (IOException e) {
+			throw new IllegalStateException("Can't create tmp directory at " + tmpDirPath.toString());
+		}
+	}
+
+	private final XAFileSystem xafs;
+	private final String workingDirectory;
+	private final FsWebdavCryptoAdapter cryptoAdapter;
+	
+	public FsWebdavResourceHandler(final File root) {
+		super(FsWebdavTransaction.class);
+		this.workingDirectory = FilenameUtils.normalizeNoEndSeparator(root.getAbsolutePath());
+		
+		final StandaloneFileSystemConfiguration configuration = new StandaloneFileSystemConfiguration(XA_SYS_DIR.toString(), "test");
+        this.xafs = XAFileSystemProxy.bootNativeXAFileSystem(configuration);
+        this.cryptoAdapter = new FsWebdavCryptoAdapter(this.workingDirectory);
+		
+        try {
+        	this.xafs.waitForBootup(1000L);
+        	LOG.info("Started XADisk at " + XA_SYS_DIR.toString());
+        	
+        	final Session session = xafs.createSessionForLocalTransaction();
+        	cryptoAdapter.initializeNewFolder(session, "/");
+        	session.commit();
+		} catch (IOException | XAApplicationException | InterruptedException ex) {
+			throw new IllegalStateException("Could not initialize I/O components.", ex);
+		}
+	}
+	
+	private File getFileInWorkDir(final String relativeUri) {
+		final String fullPath = this.workingDirectory.concat(relativeUri);
+		return new File(FilenameUtils.normalize(fullPath));
+	}
+	
+	@Override
+	public void destroy() {
+		try {
+			this.xafs.shutdown();
+			FileUtils.deleteDirectory(XA_SYS_DIR.toFile());
+		} catch (IOException e) {
+			LOG.warn("Failed to shutdown normally", e);
+		}
+	}
+
+	@Override
+	public FsWebdavTransaction beginTransactionInternal(Principal principal) {
+		final Session session = this.xafs.createSessionForLocalTransaction();
+		LOG.trace("started transaction " + session);
+		return new FsWebdavTransaction(principal, session);
+	}
+
+	@Override
+	public void checkAuthenticationInternal(FsWebdavTransaction transaction) {
+		// TODO Auto-generated method stub
+	}
+
+	@Override
+	public void commitInternal(FsWebdavTransaction transaction) {
+		try {
+			transaction.getSession().commit();
+			LOG.trace("committed transaction " + transaction.getSession());
+		} catch (NoTransactionAssociatedException e) {
+			throw new WebdavException("Error committing transaction " + transaction.getSession(), e);
+		}
+	}
+
+	@Override
+	public void rollbackInternal(FsWebdavTransaction transaction) {
+		try {
+			transaction.getSession().rollback();
+			LOG.warn("rolled back transaction " + transaction.getSession());
+		} catch (NoTransactionAssociatedException e) {
+			throw new WebdavException("Error rolling back transaction " + transaction.getSession(), e);
+		}
+	}
+
+	@Override
+	public void createFolderInternal(FsWebdavTransaction transaction, String folderUri) {
+		try {
+			cryptoAdapter.initializeNewFolder(transaction.getSession(), folderUri);
+		} catch (IOException e) {
+			throw new WebdavException(e);
+		}
+	}
+
+	@Override
+	public void createResourceInternal(FsWebdavTransaction transaction, String resourceUri) {
+		try {			
+			final String pseudonymized = cryptoAdapter.pseudonymizedUri(transaction.getSession(), resourceUri);
+			final File file = getFileInWorkDir(pseudonymized);
+			transaction.getSession().createFile(file, false);
+		} catch (IOException | XAApplicationException | InterruptedException e) {
+			throw new WebdavException(e);
+		}
+	}
+
+	@Override
+	public InputStream getResourceContentInternal(FsWebdavTransaction transaction, String resourceUri) {
+		try {
+			// Note: The requesting entity is in charge of closing the stream.
+			final String pseudonymized = cryptoAdapter.pseudonymizedUri(transaction.getSession(), resourceUri);
+			return cryptoAdapter.decryptResource(transaction.getSession(), pseudonymized);
+		} catch (IOException e) {
+			throw new WebdavException(e);
+		}
+	}
+
+	@Override
+	public long setResourceContentInternal(FsWebdavTransaction transaction, String resourceUri, InputStream in, String contentType, String characterEncoding) {
+		try {
+			final String pseudonymized = cryptoAdapter.pseudonymizedUri(transaction.getSession(), resourceUri);
+			return cryptoAdapter.encryptResource(transaction.getSession(), pseudonymized, in);
+		} catch (IOException  e) {
+			throw new WebdavException(e);
+		}
+	}
+	
+	@Override
+	public String[] getChildrenNamesInternal(FsWebdavTransaction transaction, String folderUri) {
+		try {
+			final String pseudonymized = cryptoAdapter.pseudonymizedUri(transaction.getSession(), folderUri);
+			return cryptoAdapter.uncoveredChildrenNames(transaction.getSession(), pseudonymized);
+		} catch (IOException e) {
+			throw new WebdavException(e);
+		}
+	}
+
+	@Override
+	public long getResourceLengthInternal(FsWebdavTransaction transaction, String uri) {
+		try {
+			final String pseudonymized = cryptoAdapter.pseudonymizedUri(transaction.getSession(), uri);
+			return cryptoAdapter.getDecryptedFileLength(transaction.getSession(), pseudonymized);
+		} catch (IOException e) {
+			throw new WebdavException(e);
+		}
+	}
+
+	@Override
+	public void removeObjectInternal(FsWebdavTransaction transaction, String uri) {
+		try {
+			final String pseudonymized = cryptoAdapter.pseudonymizedUri(transaction.getSession(), uri);
+			final File file = getFileInWorkDir(pseudonymized);
+			deleteRecursively(transaction.getSession(), file);
+			cryptoAdapter.deletePseudonym(transaction.getSession(), pseudonymized);
+		} catch (IOException | XAApplicationException | InterruptedException e) {
+			LOG.error("removeObject" + uri + " failed", e);
+			throw new WebdavException(e);
+		}
+	}
+	
+	private void deleteRecursively(Session session, File file) throws XAApplicationException, InterruptedException {
+		if (file.isDirectory()) {
+			final String[] children = session.listFiles(file);
+			for (final String childName : children) {
+				final File childFile = new File(file, childName);
+				deleteRecursively(session, childFile);
+			}
+		}
+		session.deleteFile(file);
+	}
+
+	@Override
+	public StoredObject getStoredObjectInternal(FsWebdavTransaction transaction, String uri) {
+		try {
+			final String pseudonymized = cryptoAdapter.pseudonymizedUri(transaction.getSession(), uri);
+			final File file = getFileInWorkDir(pseudonymized);
+			if (transaction.getSession().fileExists(file)) {
+				final StoredObject so = new StoredObject();
+				so.setFolder(file.isDirectory());
+				so.setLastModified(new Date(file.lastModified()));
+				so.setCreationDate(new Date(file.lastModified()));
+				if (!file.isDirectory()) {
+					so.setResourceLength(transaction.getSession().getFileLength(file));
+				}
+				return so;
+			} else {
+				return null;
+			}
+		} catch (IOException | XAApplicationException | InterruptedException e) {
+			throw new WebdavException(e);
+		}
+	}
+
+}

+ 40 - 0
oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/FsWebdavTransaction.java

@@ -0,0 +1,40 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.webdav;
+
+import java.security.Principal;
+
+import org.xadisk.bridge.proxies.interfaces.Session;
+
+import net.sf.webdav.ITransaction;
+
+public class FsWebdavTransaction implements ITransaction {
+	
+	private final Principal principal;
+	private final Session session;
+	
+	/**
+	 * @param principal WebDAV User
+	 * @param session XADisk Session
+	 */
+	FsWebdavTransaction(final Principal principal, final Session session) {
+		this.principal = principal;
+		this.session = session;
+	}
+
+	@Override
+	public Principal getPrincipal() {
+		return principal;
+	}
+
+	public Session getSession() {
+		return session;
+	}
+
+}

+ 73 - 0
oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/WebDAVServer.java

@@ -0,0 +1,73 @@
+/*******************************************************************************
+ * 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 de.sebastianstenzel.oce.webdav;
+
+import org.eclipse.jetty.server.Connector;
+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.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 final Server server = new Server();
+
+	private WebDAVServer() {
+		// make constructor private
+	}
+
+	public static WebDAVServer getInstance() {
+		return INSTANCE;
+	}
+
+	public boolean start(final String workDir, final int port) {
+		final ServerConnector connector = new ServerConnector(server);
+		connector.setHost("127.0.0.1");
+		connector.setPort(port);
+		server.setConnectors(new Connector[] { connector });
+
+		final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
+		context.setContextPath("/");
+		context.addServlet(getWebDAVServletHolder(workDir), "/*");
+		server.setHandler(context);
+
+		try {
+			server.start();
+		} catch (Exception ex) {
+			LOG.error("Server couldn't be started", ex);
+		}
+		
+		return server.isStarted();
+	}
+	
+	public boolean isRunning() {
+		return server.isRunning();
+	}
+	
+	public boolean stop() {
+		try {
+			server.stop();
+		} catch (Exception ex) {
+			LOG.error("Server couldn't be stopped", ex);
+		}
+		return server.isStopped();
+	}
+
+	private ServletHolder getWebDAVServletHolder(final String rootpath) {
+		final ServletHolder result = new ServletHolder("OCE-WebdavServlet", EnhancedWebDavServlet.class);
+		result.setInitParameter("ResourceHandlerImplementation", FsWebdavResourceHandler.class.getName());
+		result.setInitParameter("rootpath", rootpath);
+		return result;
+	}
+
+}

+ 32 - 0
oce-main/oce-webdav/src/main/resources/log4j.xml

@@ -0,0 +1,32 @@
+<?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>
+	</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="INFO" />
+		<appender-ref ref="console" />
+	</root>
+</log4j:configuration>

+ 131 - 0
oce-main/pom.xml

@@ -0,0 +1,131 @@
+<?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">
+	<modelVersion>4.0.0</modelVersion>
+	<groupId>de.sebastianstenzel.oce</groupId>
+	<artifactId>oce-main</artifactId>
+	<version>0.0.1-SNAPSHOT</version>
+	<packaging>pom</packaging>
+	<name>Open Cloud Encryptor</name>
+	<organization>
+		<name>sebastianstenzel.de</name>
+	</organization>
+
+	<properties>
+		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+		<project.java.version>1.7</project.java.version>
+
+		<!-- dependency versions -->
+		<log4j.version>1.2.16</log4j.version>
+		<slf4j.version>1.7.5</slf4j.version>
+		<junit.version>4.11</junit.version>
+		<commons-io.version>2.4</commons-io.version>
+		<commons-collections.version>4.0</commons-collections.version>
+		<commons-lang.version>3.1</commons-lang.version>
+
+		<!-- Will be included in Java 8. Until then we need this dependency -->
+		<javafx.version>2.2</javafx.version>
+		<jdk.home>/Library/Java/JavaVirtualMachines/jdk1.7.0_45.jdk/Contents/Home</jdk.home>
+		<javafx.runtime.lib.jar>${jdk.home}/jre/lib/jfxrt.jar</javafx.runtime.lib.jar>
+		<javafx.tools.ant.jar>${jdk.home}/lib/ant-javafx.jar</javafx.tools.ant.jar>
+	</properties>
+
+	<developers>
+		<developer>
+			<name>Sebastian Stenzel</name>
+			<email>mail@sebastianstenzel.de</email>
+		</developer>
+	</developers>
+
+	<dependencyManagement>
+		<dependencies>
+			<!-- Logging -->
+			<dependency>
+				<groupId>log4j</groupId>
+				<artifactId>log4j</artifactId>
+				<version>${log4j.version}</version>
+			</dependency>
+			<dependency>
+				<groupId>org.slf4j</groupId>
+				<artifactId>slf4j-api</artifactId>
+				<version>${slf4j.version}</version>
+			</dependency>
+			<dependency>
+				<groupId>org.slf4j</groupId>
+				<artifactId>slf4j-log4j12</artifactId>
+				<version>${slf4j.version}</version>
+			</dependency>
+
+			<!-- commons -->
+			<dependency>
+				<groupId>commons-io</groupId>
+				<artifactId>commons-io</artifactId>
+				<version>${commons-io.version}</version>
+			</dependency>
+			<dependency>
+				<groupId>org.apache.commons</groupId>
+				<artifactId>commons-collections4</artifactId>
+				<version>${commons-collections.version}</version>
+			</dependency>
+			<dependency>
+				<groupId>org.apache.commons</groupId>
+				<artifactId>commons-lang3</artifactId>
+				<version>${commons-lang.version}</version>
+			</dependency>
+
+			<!-- JSON -->
+			<dependency>
+				<groupId>com.fasterxml.jackson.core</groupId>
+				<artifactId>jackson-databind</artifactId>
+				<version>2.3.0</version>
+			</dependency>
+
+			<!-- JavaFX -->
+			<dependency>
+				<groupId>com.oracle</groupId>
+				<artifactId>javafx</artifactId>
+				<version>${javafx.version}</version>
+				<systemPath>${javafx.runtime.lib.jar}</systemPath>
+				<scope>system</scope>
+			</dependency>
+
+			<!-- JUnit -->
+			<dependency>
+				<groupId>junit</groupId>
+				<artifactId>junit</artifactId>
+				<version>4.11</version>
+				<scope>test</scope>
+			</dependency>
+		</dependencies>
+	</dependencyManagement>
+
+	<modules>
+		<module>oce-webdav</module>
+		<module>oce-ui</module>
+		<module>oce-crypto</module>
+	</modules>
+
+	<build>
+		<pluginManagement>
+			<plugins>
+				<plugin>
+					<groupId>org.apache.maven.plugins</groupId>
+					<artifactId>maven-compiler-plugin</artifactId>
+					<version>3.0</version>
+					<configuration>
+						<source>${project.java.version}</source>
+						<target>${project.java.version}</target>
+					</configuration>
+				</plugin>
+			</plugins>
+		</pluginManagement>
+	</build>
+
+</project>