diff --git a/.gitignore b/.gitignore
index 0f182a034..6d21fb45d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,9 @@
*.jar
*.war
*.ear
+
+# Eclipse Settings Files #
+.settings
+.project
+.classpath
+target/
diff --git a/oce-main/oce-crypto/pom.xml b/oce-main/oce-crypto/pom.xml
new file mode 100644
index 000000000..d563a5d80
--- /dev/null
+++ b/oce-main/oce-crypto/pom.xml
@@ -0,0 +1,54 @@
+
+
+
+ 4.0.0
+
+ de.sebastianstenzel.oce
+ oce-main
+ 0.0.1-SNAPSHOT
+
+ oce-crypto
+ Open Cloud Encryptor Cryptographic module
+ Provides stream ciphers and filename pseudonymization functions.
+
+
+
+
+ org.slf4j
+ slf4j-log4j12
+
+
+
+
+ commons-io
+ commons-io
+
+
+ org.apache.commons
+ commons-collections4
+
+
+ org.apache.commons
+ commons-lang3
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
+
+ junit
+ junit
+
+
+
diff --git a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/Cryptor.java b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/Cryptor.java
new file mode 100644
index 000000000..ca5da3af7
--- /dev/null
+++ b/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;
+ }
+
+}
diff --git a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/FilenamePseudonymizing.java b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/FilenamePseudonymizing.java
new file mode 100644
index 000000000..a72ac951c
--- /dev/null
+++ b/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 null, 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;
+
+}
diff --git a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/StorageCrypting.java b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/StorageCrypting.java
new file mode 100644
index 000000000..12feae128
--- /dev/null
+++ b/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));
+ }
+
+ }
+
+}
+
+
diff --git a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/TransactionAwareFileAccess.java b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/TransactionAwareFileAccess.java
new file mode 100644
index 000000000..e2313bfe5
--- /dev/null
+++ b/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;
+
+}
diff --git a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/AesCryptor.java b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/AesCryptor.java
new file mode 100644
index 000000000..871e3a1dd
--- /dev/null
+++ b/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 cleartextUriComps = this.splitUri(cleartextUri);
+ final List 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 pseudonymUriComps = this.splitUri(pseudonymizedUri);
+ final List 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 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 uriComponents) {
+ final StringBuilder sb = new StringBuilder();
+ for (final String comp : uriComponents) {
+ sb.append(URI_PATH_SEP).append(comp);
+ }
+ return sb.toString();
+ }
+
+ private List splitUri(final String uri) {
+ final List 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;
+ }
+
+}
diff --git a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Keys.java b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Keys.java
new file mode 100644
index 000000000..ee2b4828e
--- /dev/null
+++ b/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 additionalKeys;
+
+ public Key getOwnerKey() {
+ return ownerKey;
+ }
+
+ public void setOwnerKey(Key ownerKey) {
+ this.ownerKey = ownerKey;
+ }
+
+ public Map getAdditionalKeys() {
+ return additionalKeys;
+ }
+
+ public void setAdditionalKeys(Map 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;
+ }
+
+
+ }
+
+
+}
diff --git a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Metadata.java b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Metadata.java
new file mode 100644
index 000000000..9e69c784f
--- /dev/null
+++ b/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 filenames;
+ private Map 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 getFilenames() {
+ if (filenames == null) {
+ filenames = new DualHashBidiMap<>();
+ }
+ return filenames;
+ }
+
+ public void setFilenames(BidiMap filesnames) {
+ this.filenames = filesnames;
+ }
+
+ public Map getFilesizes() {
+ if (filesizes == null) {
+ filesizes = new HashMap<>();
+ }
+ return filesizes;
+ }
+
+ public void setFilesizes(Map filesizes) {
+ this.filesizes = filesizes;
+ }
+
+}
diff --git a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cache/PseudonymRepository.java b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cache/PseudonymRepository.java
new file mode 100644
index 000000000..a1663e0e8
--- /dev/null
+++ b/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 cleartextPathComponents(final List pseudonymizedPathComponents) {
+ final List 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 pseudonymizedPathComponents(final List cleartextPathComponents) {
+ final List 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 cleartextPathComponents, final List 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 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 subnodesByCleartext;
+ private final Map 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;
+ }
+ }
+
+ }
+
+}
diff --git a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cleartext/Metadata.java b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cleartext/Metadata.java
new file mode 100644
index 000000000..2fbacbf39
--- /dev/null
+++ b/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 filenames = new DualHashBidiMap<>();
+
+ public BidiMap getFilenames() {
+ return filenames;
+ }
+
+}
diff --git a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cleartext/NoCryptor.java b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cleartext/NoCryptor.java
new file mode 100644
index 000000000..f7388c1f1
--- /dev/null
+++ b/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 cleartextUriComps = this.splitUri(cleartextUri);
+ final List 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 pseudonymUriComps = this.splitUri(pseudonymizedUri);
+ final List 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 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 uriComponents) {
+ final StringBuilder sb = new StringBuilder();
+ for (final String comp : uriComponents) {
+ sb.append(URI_PATH_SEP).append(comp);
+ }
+ return sb.toString();
+ }
+
+ private List splitUri(final String uri) {
+ final List 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;
+ }
+
+}
diff --git a/oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/AesCryptorTest.java b/oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/AesCryptorTest.java
new file mode 100644
index 000000000..9ffd1f1f9
--- /dev/null
+++ b/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());
+ }
+
+}
diff --git a/oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/FilenamePseudonymizerTest.java b/oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/FilenamePseudonymizerTest.java
new file mode 100644
index 000000000..d658ea72b
--- /dev/null
+++ b/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;
+ }
+ }
+
+ }
+
+}
diff --git a/oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/PseudonymRepositoryTest.java b/oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/PseudonymRepositoryTest.java
new file mode 100644
index 000000000..70b1df838
--- /dev/null
+++ b/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 clear1 = Arrays.asList("foo", "bar", "baz", "info.txt");
+ final List pseudo1 = Arrays.asList("frog", "bear", "bear", "iguana");
+ PseudonymRepository.registerPath(clear1, pseudo1);
+
+ // get pseudonymized path:
+ final List result1 = PseudonymRepository.pseudonymizedPathComponents(clear1);
+ Assert.assertEquals(pseudo1, result1);
+
+ // get cleartext path:
+ final List result2 = PseudonymRepository.cleartextPathComponents(pseudo1);
+ Assert.assertEquals(clear1, result2);
+
+ // register additional path:
+ final List clear2 = Arrays.asList("foo", "bar", "zab", "info.txt");
+ final List pseudo2 = Arrays.asList("frog", "bear", "zebra", "iguana");
+ PseudonymRepository.registerPath(clear2, pseudo2);
+
+ // get pseudonymized path:
+ final List result3 = PseudonymRepository.pseudonymizedPathComponents(clear2);
+ Assert.assertEquals(pseudo2, result3);
+
+ // get cleartext path:
+ final List result4 = PseudonymRepository.cleartextPathComponents(pseudo2);
+ Assert.assertEquals(clear2, result4);
+ }
+
+}
diff --git a/oce-main/oce-ui/pom.xml b/oce-main/oce-ui/pom.xml
new file mode 100644
index 000000000..ebc6d348f
--- /dev/null
+++ b/oce-main/oce-ui/pom.xml
@@ -0,0 +1,91 @@
+
+
+
+ 4.0.0
+
+ de.sebastianstenzel.oce
+ oce-main
+ 0.0.1-SNAPSHOT
+
+ oce-ui
+ Open Cloud Encryptor GUI
+
+
+ de.sebastianstenzel.oce.ui.MainApplication
+
+
+
+
+ de.sebastianstenzel.oce
+ oce-webdav
+ ${project.parent.version}
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
+
+ com.oracle
+ javafx
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.zenjava
+ javafx-maven-plugin
+ 2.0
+
+ de.sebastianstenzel.oce.ui.MainWindow
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+
+
+ ${javafx.version}
+ ${exec.mainClass}
+ com/javafx/main/Main
+
+
+
+ jar-with-dependencies
+
+
+
+
+ assemble-all
+ package
+
+ single
+
+
+
+
+
+
+
+
diff --git a/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/AccessController.java b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/AccessController.java
new file mode 100644
index 000000000..51467f003
--- /dev/null
+++ b/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);
+ }
+ }
+
+}
diff --git a/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/AdvancedController.java b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/AdvancedController.java
new file mode 100644
index 000000000..a169e8f36
--- /dev/null
+++ b/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 {
+ 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 {
+ @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()));
+ }
+ }
+ }
+
+}
diff --git a/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/InitializeController.java b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/InitializeController.java
new file mode 100644
index 000000000..5ec0af236
--- /dev/null
+++ b/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 {
+ @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 {
+ @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();
+ }
+
+
+}
diff --git a/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/MainApplication.java b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/MainApplication.java
new file mode 100644
index 000000000..d941f2a08
--- /dev/null
+++ b/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();
+ }
+
+}
diff --git a/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/MainController.java b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/MainController.java
new file mode 100644
index 000000000..a482a347b
--- /dev/null
+++ b/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();
+ }
+
+}
diff --git a/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/controls/SecPasswordField.java b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/controls/SecPasswordField.java
new file mode 100644
index 000000000..1bc6808fa
--- /dev/null
+++ b/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.
+ *
+ * Imagine the following example with pass being the password, x being the swipe char and ' being the offset of the char array:
+ *
+ * - Append filling chars to the end of the password:
passxxxx'
+ * - Delete first 4 chars. Internal implementation will then copy the following chars to the position, where the deletion occured:
xxxx'xxxx
+ * - Delete first 4 chars again, as we appended 4 chars in step 1:
'xxxxxx
+ *
+ */
+ 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);
+ }
+
+
+
+}
diff --git a/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/controls/SecurePasswordField.java b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/controls/SecurePasswordField.java
new file mode 100644
index 000000000..4dcbd6a69
--- /dev/null
+++ b/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 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);
+ }
+
+}
diff --git a/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/settings/Settings.java b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/settings/Settings.java
new file mode 100644
index 000000000..932d0bf21
--- /dev/null
+++ b/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;
+ }
+
+}
diff --git a/oce-main/oce-ui/src/main/resources/access.fxml b/oce-main/oce-ui/src/main/resources/access.fxml
new file mode 100644
index 000000000..5dfa0cc50
--- /dev/null
+++ b/oce-main/oce-ui/src/main/resources/access.fxml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/oce-main/oce-ui/src/main/resources/advanced.fxml b/oce-main/oce-ui/src/main/resources/advanced.fxml
new file mode 100644
index 000000000..83a4a4848
--- /dev/null
+++ b/oce-main/oce-ui/src/main/resources/advanced.fxml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/oce-main/oce-ui/src/main/resources/initialize.fxml b/oce-main/oce-ui/src/main/resources/initialize.fxml
new file mode 100644
index 000000000..cacf34019
--- /dev/null
+++ b/oce-main/oce-ui/src/main/resources/initialize.fxml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/oce-main/oce-ui/src/main/resources/localization.properties b/oce-main/oce-ui/src/main/resources/localization.properties
new file mode 100644
index 000000000..1ff02f80c
--- /dev/null
+++ b/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
diff --git a/oce-main/oce-ui/src/main/resources/main.css b/oce-main/oce-ui/src/main/resources/main.css
new file mode 100644
index 000000000..13d209f5b
--- /dev/null
+++ b/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;
+}
diff --git a/oce-main/oce-ui/src/main/resources/main.fxml b/oce-main/oce-ui/src/main/resources/main.fxml
new file mode 100644
index 000000000..2670cc0b1
--- /dev/null
+++ b/oce-main/oce-ui/src/main/resources/main.fxml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/oce-main/oce-ui/src/main/resources/panels.css b/oce-main/oce-ui/src/main/resources/panels.css
new file mode 100644
index 000000000..c0d4e4917
--- /dev/null
+++ b/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);
+}
diff --git a/oce-main/oce-webdav/pom.xml b/oce-main/oce-webdav/pom.xml
new file mode 100644
index 000000000..8acd8cbde
--- /dev/null
+++ b/oce-main/oce-webdav/pom.xml
@@ -0,0 +1,77 @@
+
+
+
+ 4.0.0
+
+ de.sebastianstenzel.oce
+ oce-main
+ 0.0.1-SNAPSHOT
+
+ oce-webdav
+ Open Cloud Encryptor WebDAV module
+
+
+ 9.1.0.v20131115
+ 2.0
+ 1.2
+ 1.1
+
+
+
+
+ de.sebastianstenzel.oce
+ oce-crypto
+ ${project.parent.version}
+
+
+
+
+ org.slf4j
+ slf4j-log4j12
+
+
+
+
+ org.eclipse.jetty
+ jetty-server
+ ${jetty.version}
+
+
+ org.eclipse.jetty
+ jetty-webapp
+ ${jetty.version}
+
+
+
+
+ net.sf.webdav-servlet
+ webdav-servlet
+ ${webdavservlet.version}
+
+
+
+
+ commons-io
+ commons-io
+
+
+ net.java.xadisk
+ xadisk
+ 1.2.2
+
+
+
+
+ org.apache.openejb
+ javaee-api
+ 6.0-5
+
+
+
diff --git a/oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/EnhancedWebDavServlet.java b/oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/EnhancedWebDavServlet.java
new file mode 100644
index 000000000..bc54c93d1
--- /dev/null
+++ b/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();
+ }
+
+}
diff --git a/oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/EnhancedWebdavStore.java b/oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/EnhancedWebdavStore.java
new file mode 100644
index 000000000..a47457308
--- /dev/null
+++ b/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 implements IWebdavStore {
+
+ private final Class transactionClass;
+
+ protected EnhancedWebdavStore(final Class 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);
+
+}
diff --git a/oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/FsWebdavCryptoAdapter.java b/oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/FsWebdavCryptoAdapter.java
new file mode 100644
index 000000000..4f2093259
--- /dev/null
+++ b/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 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;
+ }
+ }
+
+ }
+
+}
diff --git a/oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/FsWebdavResourceHandler.java b/oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/FsWebdavResourceHandler.java
new file mode 100644
index 000000000..62bb92bc9
--- /dev/null
+++ b/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 {
+
+ 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);
+ }
+ }
+
+}
diff --git a/oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/FsWebdavTransaction.java b/oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/FsWebdavTransaction.java
new file mode 100644
index 000000000..cf60e56e3
--- /dev/null
+++ b/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;
+ }
+
+}
diff --git a/oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/WebDAVServer.java b/oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/WebDAVServer.java
new file mode 100644
index 000000000..b31a6106e
--- /dev/null
+++ b/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;
+ }
+
+}
diff --git a/oce-main/oce-webdav/src/main/resources/log4j.xml b/oce-main/oce-webdav/src/main/resources/log4j.xml
new file mode 100644
index 000000000..ecac2310e
--- /dev/null
+++ b/oce-main/oce-webdav/src/main/resources/log4j.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/oce-main/pom.xml b/oce-main/pom.xml
new file mode 100644
index 000000000..700e5bcbe
--- /dev/null
+++ b/oce-main/pom.xml
@@ -0,0 +1,131 @@
+
+
+
+ 4.0.0
+ de.sebastianstenzel.oce
+ oce-main
+ 0.0.1-SNAPSHOT
+ pom
+ Open Cloud Encryptor
+
+ sebastianstenzel.de
+
+
+
+ UTF-8
+ 1.7
+
+
+ 1.2.16
+ 1.7.5
+ 4.11
+ 2.4
+ 4.0
+ 3.1
+
+
+ 2.2
+ /Library/Java/JavaVirtualMachines/jdk1.7.0_45.jdk/Contents/Home
+ ${jdk.home}/jre/lib/jfxrt.jar
+ ${jdk.home}/lib/ant-javafx.jar
+
+
+
+
+ Sebastian Stenzel
+ mail@sebastianstenzel.de
+
+
+
+
+
+
+
+ log4j
+ log4j
+ ${log4j.version}
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+
+
+ org.slf4j
+ slf4j-log4j12
+ ${slf4j.version}
+
+
+
+
+ commons-io
+ commons-io
+ ${commons-io.version}
+
+
+ org.apache.commons
+ commons-collections4
+ ${commons-collections.version}
+
+
+ org.apache.commons
+ commons-lang3
+ ${commons-lang.version}
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.3.0
+
+
+
+
+ com.oracle
+ javafx
+ ${javafx.version}
+ ${javafx.runtime.lib.jar}
+ system
+
+
+
+
+ junit
+ junit
+ 4.11
+ test
+
+
+
+
+
+ oce-webdav
+ oce-ui
+ oce-crypto
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.0
+
+ ${project.java.version}
+ ${project.java.version}
+
+
+
+
+
+
+