diff --git a/README.md b/README.md
index c5f4e1805..5efb9d94e 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,8 @@ Multiplatform transparent client-side encryption of your files in the cloud. You
## Features
- Totally transparent: Just work on the encrypted volume, as if it was an USB drive
-- Works with Dropbox, Skydrive, Google Drive and any other cloud storage, that syncs with a local directory
+- Works with Dropbox, OneDrive (Skydrive), Google Drive and any other cloud storage, that syncs with a local directory
+- In fact it works with any directory. You can use it to encrypt as many folders as you like
- AES encryption with up to 256 bit key length
- Client-side. No accounts, no data shared with any online service
- Filenames get encrypted too
@@ -23,17 +24,25 @@ Multiplatform transparent client-side encryption of your files in the cloud. You
## Consistency
- I/O operations are transactional and atomic, if the file systems supports it
-- Metadata is stored per-folder, so it's not a SPOF
+- ~~Metadata is stored per-folder, so it's not a SPOF~~
+- *NEW:* No Metadata at all. Encrypted files can be decrypted even on completely shuffled file systems (if their contents are undamaged).
## Dependencies
-- Java 8
+- Java 8 (for UI only - runs headless on Java 7)
- Maven
+- Awesome 3rd party open source libraries (Apache Commons, Apache Jackrabbit, Jetty, Jackson, ...)
## TODO
+
+### Core
+- WebDAV Session handling
+- Java NIO file locking
+- Support for HTTP range requests
+
+### UI
- Automount of WebDAV volumes for Win/Mac/Tux
- App icon and drive icons in WebDAV volumes
- Change password functionality
-- Replace WebDAV implementation by more efficient and robust solution
- CRC32 checksums for decrypted files
- Better explanations on UI
diff --git a/oce-main/oce-core/.gitignore b/oce-main/oce-core/.gitignore
new file mode 100644
index 000000000..b83d22266
--- /dev/null
+++ b/oce-main/oce-core/.gitignore
@@ -0,0 +1 @@
+/target/
diff --git a/oce-main/oce-webdav/pom.xml b/oce-main/oce-core/pom.xml
similarity index 60%
rename from oce-main/oce-webdav/pom.xml
rename to oce-main/oce-core/pom.xml
index 8acd8cbde..111c1fdd2 100644
--- a/oce-main/oce-webdav/pom.xml
+++ b/oce-main/oce-core/pom.xml
@@ -1,25 +1,19 @@
-
+
4.0.0
de.sebastianstenzel.oce
oce-main
- 0.0.1-SNAPSHOT
+ 0.1.0-SNAPSHOT
- oce-webdav
- Open Cloud Encryptor WebDAV module
+ oce-core
+ Open Cloud Encryptor core I/O module
9.1.0.v20131115
- 2.0
+ 2.6.2.4
+ 2.9.0
1.2
1.1
@@ -27,7 +21,7 @@
de.sebastianstenzel.oce
- oce-crypto
+ oce-crypto-api
${project.parent.version}
@@ -49,11 +43,11 @@
${jetty.version}
-
+
- net.sf.webdav-servlet
- webdav-servlet
- ${webdavservlet.version}
+ org.apache.jackrabbit
+ jackrabbit-webdav
+ ${jackrabbit.version}
@@ -62,16 +56,8 @@
commons-io
- net.java.xadisk
- xadisk
- 1.2.2
-
-
-
-
- org.apache.openejb
- javaee-api
- 6.0-5
+ org.apache.commons
+ commons-lang3
diff --git a/oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/WebDAVServer.java b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/WebDAVServer.java
similarity index 69%
rename from oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/WebDAVServer.java
rename to oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/WebDAVServer.java
index b31a6106e..d6dd54de6 100644
--- a/oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/WebDAVServer.java
+++ b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/WebDAVServer.java
@@ -16,6 +16,9 @@ import org.eclipse.jetty.servlet.ServletHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import de.sebastianstenzel.oce.crypto.Cryptor;
+import de.sebastianstenzel.oce.webdav.jackrabbit.WebDavServlet;
+
public final class WebDAVServer {
private static final Logger LOG = LoggerFactory.getLogger(WebDAVServer.class);
@@ -30,15 +33,17 @@ public final class WebDAVServer {
return INSTANCE;
}
- public boolean start(final String workDir, final int port) {
+ public boolean start(final String workDir, final int port, final Cryptor cryptor) {
final ServerConnector connector = new ServerConnector(server);
connector.setHost("127.0.0.1");
connector.setPort(port);
- server.setConnectors(new Connector[] { connector });
+ server.setConnectors(new Connector[] {connector});
+
+ final String contextPath = "/";
final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
- context.setContextPath("/");
- context.addServlet(getWebDAVServletHolder(workDir), "/*");
+ context.addServlet(getMiltonServletHolder(workDir, contextPath, cryptor), "/*");
+ context.setContextPath(contextPath);
server.setHandler(context);
try {
@@ -46,14 +51,14 @@ public final class WebDAVServer {
} 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();
@@ -63,10 +68,10 @@ public final class WebDAVServer {
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);
+ private ServletHolder getMiltonServletHolder(final String workDir, final String contextPath, final Cryptor cryptor) {
+ final ServletHolder result = new ServletHolder("OCE-WebDAV-Servlet", new WebDavServlet(cryptor));
+ result.setInitParameter(WebDavServlet.CFG_FS_ROOT, workDir);
+ result.setInitParameter(WebDavServlet.CFG_HTTP_ROOT, contextPath);
return result;
}
diff --git a/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/exceptions/DavRuntimeException.java b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/exceptions/DavRuntimeException.java
new file mode 100644
index 000000000..483cea4e2
--- /dev/null
+++ b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/exceptions/DavRuntimeException.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.webdav.exceptions;
+
+import org.apache.jackrabbit.webdav.DavException;
+
+public class DavRuntimeException extends RuntimeException {
+
+ private static final long serialVersionUID = -4713080133052143303L;
+
+ public DavRuntimeException(DavException davException) {
+ super(davException);
+ }
+
+ @Override
+ public String getMessage() {
+ return getCause().getMessage();
+ }
+
+ @Override
+ public String getLocalizedMessage() {
+ return getCause().getLocalizedMessage();
+ }
+
+}
diff --git a/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/exceptions/IORuntimeException.java b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/exceptions/IORuntimeException.java
new file mode 100644
index 000000000..716ed7606
--- /dev/null
+++ b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/exceptions/IORuntimeException.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.webdav.exceptions;
+
+import java.io.IOException;
+
+public class IORuntimeException extends RuntimeException {
+
+ private static final long serialVersionUID = -4713080133052143303L;
+
+ public IORuntimeException(IOException ioException) {
+ super(ioException);
+ }
+
+ @Override
+ public String getMessage() {
+ return getCause().getMessage();
+ }
+
+ @Override
+ public String getLocalizedMessage() {
+ return getCause().getLocalizedMessage();
+ }
+
+}
diff --git a/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/WebDavLocatorFactory.java b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/WebDavLocatorFactory.java
new file mode 100644
index 000000000..d80fe7977
--- /dev/null
+++ b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/WebDavLocatorFactory.java
@@ -0,0 +1,69 @@
+/*******************************************************************************
+ * 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.jackrabbit;
+
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+
+import org.apache.jackrabbit.webdav.AbstractLocatorFactory;
+import org.apache.jackrabbit.webdav.DavResourceLocator;
+
+import de.sebastianstenzel.oce.crypto.Cryptor;
+
+public class WebDavLocatorFactory extends AbstractLocatorFactory {
+
+ private final Path fsRoot;
+ private final Cryptor cryptor;
+
+ public WebDavLocatorFactory(String fsRoot, String httpRoot, Cryptor cryptor) {
+ super(httpRoot);
+ this.fsRoot = FileSystems.getDefault().getPath(fsRoot);
+ this.cryptor = cryptor;
+ }
+
+ /**
+ * @return Encrypted absolute paths on the file system.
+ */
+ @Override
+ protected String getRepositoryPath(String resourcePath, String wspPath) {
+ if (resourcePath == null) {
+ return fsRoot.toString();
+ }
+ final String encryptedRepoPath = cryptor.encryptPath(resourcePath, FileSystems.getDefault().getSeparator().charAt(0), '/', null);
+ return fsRoot.resolve(encryptedRepoPath).toString();
+ }
+
+ /**
+ * @return Decrypted path for use in URIs.
+ */
+ @Override
+ protected String getResourcePath(String repositoryPath, String wspPath) {
+ final Path absRepoPath = FileSystems.getDefault().getPath(repositoryPath);
+ if (fsRoot.equals(absRepoPath)) {
+ return null;
+ } else {
+ final Path relativeRepositoryPath = fsRoot.relativize(absRepoPath);
+ final String resourcePath = cryptor.decryptPath(relativeRepositoryPath.toString(), FileSystems.getDefault().getSeparator().charAt(0), '/', null);
+ return resourcePath;
+ }
+ }
+
+ @Override
+ public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) {
+ // we don't support workspaces
+ return super.createResourceLocator(prefix, "", path, isResourcePath);
+ }
+
+ @Override
+ public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String resourcePath) {
+ // we don't support workspaces
+ return super.createResourceLocator(prefix, "", resourcePath);
+ }
+
+}
diff --git a/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/WebDavResourceFactory.java b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/WebDavResourceFactory.java
new file mode 100644
index 000000000..6c45c949e
--- /dev/null
+++ b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/WebDavResourceFactory.java
@@ -0,0 +1,80 @@
+/*******************************************************************************
+ * 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.jackrabbit;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.jackrabbit.webdav.DavException;
+import org.apache.jackrabbit.webdav.DavMethods;
+import org.apache.jackrabbit.webdav.DavResource;
+import org.apache.jackrabbit.webdav.DavResourceFactory;
+import org.apache.jackrabbit.webdav.DavResourceLocator;
+import org.apache.jackrabbit.webdav.DavServletRequest;
+import org.apache.jackrabbit.webdav.DavServletResponse;
+import org.apache.jackrabbit.webdav.DavSession;
+import org.apache.jackrabbit.webdav.lock.LockManager;
+import org.apache.jackrabbit.webdav.lock.SimpleLockManager;
+
+import de.sebastianstenzel.oce.crypto.Cryptor;
+import de.sebastianstenzel.oce.webdav.jackrabbit.resources.EncryptedDir;
+import de.sebastianstenzel.oce.webdav.jackrabbit.resources.EncryptedFile;
+import de.sebastianstenzel.oce.webdav.jackrabbit.resources.NonExistingNode;
+import de.sebastianstenzel.oce.webdav.jackrabbit.resources.PathUtils;
+
+public class WebDavResourceFactory implements DavResourceFactory {
+
+ private final LockManager lockManager = new SimpleLockManager();
+ private final Cryptor cryptor;
+
+ public WebDavResourceFactory(Cryptor cryptor) {
+ this.cryptor = cryptor;
+ }
+
+ @Override
+ public DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
+ final Path path = PathUtils.getPhysicalPath(locator);
+
+ if (Files.exists(path)) {
+ return createResource(locator, request.getDavSession());
+ } else if (DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
+ return createDirectory(locator, request.getDavSession());
+ } else if (DavMethods.METHOD_PUT.equals(request.getMethod())) {
+ return createFile(locator, request.getDavSession());
+ } else {
+ return createNonExisting(locator, request.getDavSession());
+ }
+ }
+
+ @Override
+ public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
+ final Path path = PathUtils.getPhysicalPath(locator);
+
+ if (Files.isDirectory(path)) {
+ return createDirectory(locator, session);
+ } else if (Files.isRegularFile(path)) {
+ return createFile(locator, session);
+ } else {
+ return createNonExisting(locator, session);
+ }
+ }
+
+ private EncryptedFile createFile(DavResourceLocator locator, DavSession session) {
+ return new EncryptedFile(this, locator, session, lockManager, cryptor);
+ }
+
+ private EncryptedDir createDirectory(DavResourceLocator locator, DavSession session) {
+ return new EncryptedDir(this, locator, session, lockManager, cryptor);
+ }
+
+ private NonExistingNode createNonExisting(DavResourceLocator locator, DavSession session) {
+ return new NonExistingNode(this, locator, session, lockManager, cryptor);
+ }
+
+}
diff --git a/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/WebDavServlet.java b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/WebDavServlet.java
new file mode 100644
index 000000000..ca56e12a4
--- /dev/null
+++ b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/WebDavServlet.java
@@ -0,0 +1,87 @@
+/*******************************************************************************
+ * 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.jackrabbit;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+
+import org.apache.jackrabbit.webdav.DavLocatorFactory;
+import org.apache.jackrabbit.webdav.DavResource;
+import org.apache.jackrabbit.webdav.DavResourceFactory;
+import org.apache.jackrabbit.webdav.DavSessionProvider;
+import org.apache.jackrabbit.webdav.WebdavRequest;
+import org.apache.jackrabbit.webdav.server.AbstractWebdavServlet;
+
+import de.sebastianstenzel.oce.crypto.Cryptor;
+
+public class WebDavServlet extends AbstractWebdavServlet {
+
+ private static final long serialVersionUID = 7965170007048673022L;
+ public static final String CFG_FS_ROOT = "oce.fs.root";
+ public static final String CFG_HTTP_ROOT = "oce.http.root";
+ private DavSessionProvider davSessionProvider;
+ private DavLocatorFactory davLocatorFactory;
+ private DavResourceFactory davResourceFactory;
+ private final Cryptor cryptor;
+
+ public WebDavServlet(final Cryptor cryptor) {
+ super();
+ this.cryptor = cryptor;
+ }
+
+ @Override
+ public void init(ServletConfig config) throws ServletException {
+ super.init(config);
+
+ davSessionProvider = new WebDavSessionProvider();
+
+ final String fsRoot = config.getInitParameter(CFG_FS_ROOT);
+ final String httpRoot = config.getInitParameter(CFG_HTTP_ROOT);
+ this.davLocatorFactory = new WebDavLocatorFactory(fsRoot, httpRoot, cryptor);
+
+ this.davResourceFactory = new WebDavResourceFactory(cryptor);
+ }
+
+ @Override
+ protected boolean isPreconditionValid(WebdavRequest request, DavResource resource) {
+ // TODO Auto-generated method stub
+ return true;
+ }
+
+ @Override
+ public DavSessionProvider getDavSessionProvider() {
+ return davSessionProvider;
+ }
+
+ @Override
+ public void setDavSessionProvider(DavSessionProvider davSessionProvider) {
+ this.davSessionProvider = davSessionProvider;
+ }
+
+ @Override
+ public DavLocatorFactory getLocatorFactory() {
+ return davLocatorFactory;
+ }
+
+ @Override
+ public void setLocatorFactory(DavLocatorFactory locatorFactory) {
+ this.davLocatorFactory = locatorFactory;
+ }
+
+ @Override
+ public DavResourceFactory getResourceFactory() {
+ return davResourceFactory;
+ }
+
+ @Override
+ public void setResourceFactory(DavResourceFactory resourceFactory) {
+ this.davResourceFactory = resourceFactory;
+ }
+
+}
diff --git a/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/WebDavSession.java b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/WebDavSession.java
new file mode 100644
index 000000000..e551cb0cc
--- /dev/null
+++ b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/WebDavSession.java
@@ -0,0 +1,45 @@
+/*******************************************************************************
+ * 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.jackrabbit;
+
+import org.apache.jackrabbit.webdav.DavSession;
+
+public class WebDavSession implements DavSession {
+
+ @Override
+ public void addReference(Object reference) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void removeReference(Object reference) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void addLockToken(String token) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public String[] getLockTokens() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public void removeLockToken(String token) {
+ // TODO Auto-generated method stub
+
+ }
+
+}
diff --git a/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/WebDavSessionProvider.java b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/WebDavSessionProvider.java
new file mode 100644
index 000000000..38b53bfaf
--- /dev/null
+++ b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/WebDavSessionProvider.java
@@ -0,0 +1,29 @@
+/*******************************************************************************
+ * 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.jackrabbit;
+
+import org.apache.jackrabbit.webdav.DavException;
+import org.apache.jackrabbit.webdav.DavSessionProvider;
+import org.apache.jackrabbit.webdav.WebdavRequest;
+
+public class WebDavSessionProvider implements DavSessionProvider {
+
+ @Override
+ public boolean attachSession(WebdavRequest request) throws DavException {
+ // every user gets a session
+ request.setDavSession(new WebDavSession());
+ return true;
+ }
+
+ @Override
+ public void releaseSession(WebdavRequest request) {
+ // do nothing
+ }
+
+}
diff --git a/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/resources/AbstractEncryptedNode.java b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/resources/AbstractEncryptedNode.java
new file mode 100644
index 000000000..b33f5abc3
--- /dev/null
+++ b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/resources/AbstractEncryptedNode.java
@@ -0,0 +1,270 @@
+/*******************************************************************************
+ * 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.jackrabbit.resources;
+
+import java.io.IOException;
+import java.nio.file.AtomicMoveNotSupportedException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.List;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.jackrabbit.webdav.DavException;
+import org.apache.jackrabbit.webdav.DavResource;
+import org.apache.jackrabbit.webdav.DavResourceFactory;
+import org.apache.jackrabbit.webdav.DavResourceLocator;
+import org.apache.jackrabbit.webdav.DavServletResponse;
+import org.apache.jackrabbit.webdav.DavSession;
+import org.apache.jackrabbit.webdav.MultiStatusResponse;
+import org.apache.jackrabbit.webdav.lock.ActiveLock;
+import org.apache.jackrabbit.webdav.lock.LockInfo;
+import org.apache.jackrabbit.webdav.lock.LockManager;
+import org.apache.jackrabbit.webdav.lock.Scope;
+import org.apache.jackrabbit.webdav.lock.Type;
+import org.apache.jackrabbit.webdav.property.DavProperty;
+import org.apache.jackrabbit.webdav.property.DavPropertyName;
+import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
+import org.apache.jackrabbit.webdav.property.DavPropertySet;
+import org.apache.jackrabbit.webdav.property.PropEntry;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import de.sebastianstenzel.oce.crypto.Cryptor;
+import de.sebastianstenzel.oce.webdav.exceptions.IORuntimeException;
+
+public abstract class AbstractEncryptedNode implements DavResource {
+
+ private static final Logger LOG = LoggerFactory.getLogger(AbstractEncryptedNode.class);
+ private static final String DAV_COMPLIANCE_CLASSES = "1, 2";
+
+ protected final DavResourceFactory factory;
+ protected final DavResourceLocator locator;
+ protected final DavSession session;
+ protected final LockManager lockManager;
+ protected final Cryptor cryptor;
+ protected final DavPropertySet properties;
+
+ protected AbstractEncryptedNode(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
+ this.factory = factory;
+ this.locator = locator;
+ this.session = session;
+ this.lockManager = lockManager;
+ this.cryptor = cryptor;
+ this.properties = new DavPropertySet();
+ this.determineProperties();
+ }
+
+ @Override
+ public String getComplianceClass() {
+ return DAV_COMPLIANCE_CLASSES;
+ }
+
+ @Override
+ public String getSupportedMethods() {
+ return METHODS;
+ }
+
+ @Override
+ public boolean exists() {
+ final Path path = PathUtils.getPhysicalPath(this);
+ return Files.exists(path);
+ }
+
+ @Override
+ public String getDisplayName() {
+ final String resourcePath = getResourcePath();
+ final int lastSlash = resourcePath.lastIndexOf('/');
+ if (lastSlash == -1) {
+ return resourcePath;
+ } else {
+ return resourcePath.substring(lastSlash);
+ }
+ }
+
+ @Override
+ public DavResourceLocator getLocator() {
+ return locator;
+ }
+
+ @Override
+ public String getResourcePath() {
+ return locator.getResourcePath();
+ }
+
+ @Override
+ public String getHref() {
+ return locator.getHref(this.isCollection());
+ }
+
+ @Override
+ public long getModificationTime() {
+ final Path path = PathUtils.getPhysicalPath(this);
+ try {
+ return Files.getLastModifiedTime(path).toMillis();
+ } catch (IOException e) {
+ return -1;
+ }
+ }
+
+ protected abstract void determineProperties();
+
+ @Override
+ public DavPropertyName[] getPropertyNames() {
+ return getProperties().getPropertyNames();
+ }
+
+ @Override
+ public DavProperty> getProperty(DavPropertyName name) {
+ return getProperties().get(name);
+ }
+
+ @Override
+ public DavPropertySet getProperties() {
+ return properties;
+ }
+
+ @Override
+ public void setProperty(DavProperty> property) throws DavException {
+ getProperties().add(property);
+ }
+
+ @Override
+ public void removeProperty(DavPropertyName propertyName) throws DavException {
+ getProperties().remove(propertyName);
+ }
+
+ @Override
+ public MultiStatusResponse alterProperties(List extends PropEntry> changeList) throws DavException {
+ final DavPropertyNameSet names = new DavPropertyNameSet();
+ for (final PropEntry entry : changeList) {
+ if (entry instanceof DavProperty) {
+ final DavProperty> prop = (DavProperty>) entry;
+ this.setProperty(prop);
+ names.add(prop.getName());
+ } else if (entry instanceof DavPropertyName) {
+ final DavPropertyName name = (DavPropertyName) entry;
+ this.removeProperty(name);
+ names.add(name);
+ }
+ }
+ return new MultiStatusResponse(this, names);
+ }
+
+ @Override
+ public DavResource getCollection() {
+ if (locator.isRootLocation()) {
+ return null;
+ }
+
+ final String parentResource = FilenameUtils.getPath(locator.getResourcePath());
+ final DavResourceLocator parentLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), parentResource);
+ try {
+ return getFactory().createResource(parentLocator, session);
+ } catch (DavException e) {
+ throw new IllegalStateException("Unable to get parent resource with path " + parentLocator.getResourcePath(), e);
+ }
+ }
+
+ @Override
+ public void move(DavResource dest) throws DavException {
+ final Path src = PathUtils.getPhysicalPath(this);
+ final Path dst = PathUtils.getPhysicalPath(dest);
+ try {
+ // check for conflicts:
+ if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) {
+ throw new DavException(DavServletResponse.SC_CONFLICT, "File at destination already exists: " + dst.toString());
+ }
+
+ // move:
+ try {
+ Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
+ } catch (AtomicMoveNotSupportedException e) {
+ Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING);
+ }
+ } catch (IOException e) {
+ LOG.error("Error moving file from " + src.toString() + " to " + dst.toString());
+ throw new IORuntimeException(e);
+ }
+ }
+
+ @Override
+ public void copy(DavResource dest, boolean shallow) throws DavException {
+ final Path src = PathUtils.getPhysicalPath(this);
+ final Path dst = PathUtils.getPhysicalPath(dest);
+ try {
+ // check for conflicts:
+ if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) {
+ throw new DavException(DavServletResponse.SC_CONFLICT, "File at destination already exists: " + dst.toString());
+ }
+
+ // copy:
+ try {
+ Files.copy(src, dst, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
+ } catch (AtomicMoveNotSupportedException e) {
+ Files.copy(src, dst, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
+ }
+ } catch (IOException e) {
+ LOG.error("Error copying file from " + src.toString() + " to " + dst.toString());
+ throw new IORuntimeException(e);
+ }
+ }
+
+ @Override
+ public boolean isLockable(Type type, Scope scope) {
+ return true;
+ }
+
+ @Override
+ public boolean hasLock(Type type, Scope scope) {
+ return lockManager.getLock(type, scope, this) != null;
+ }
+
+ @Override
+ public ActiveLock getLock(Type type, Scope scope) {
+ return lockManager.getLock(type, scope, this);
+ }
+
+ @Override
+ public ActiveLock[] getLocks() {
+ final ActiveLock exclusiveWriteLock = getLock(Type.WRITE, Scope.EXCLUSIVE);
+ return new ActiveLock[] {exclusiveWriteLock};
+ }
+
+ @Override
+ public ActiveLock lock(LockInfo reqLockInfo) throws DavException {
+ return lockManager.createLock(reqLockInfo, this);
+ }
+
+ @Override
+ public ActiveLock refreshLock(LockInfo reqLockInfo, String lockToken) throws DavException {
+ return lockManager.refreshLock(reqLockInfo, lockToken, this);
+ }
+
+ @Override
+ public void unlock(String lockToken) throws DavException {
+ lockManager.releaseLock(lockToken, this);
+ }
+
+ @Override
+ public void addLockManager(LockManager lockmgr) {
+ throw new UnsupportedOperationException("Locks are managed");
+ }
+
+ @Override
+ public DavResourceFactory getFactory() {
+ return factory;
+ }
+
+ @Override
+ public DavSession getSession() {
+ return session;
+ }
+
+}
diff --git a/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/resources/EncryptedDir.java b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/resources/EncryptedDir.java
new file mode 100644
index 000000000..35cfe6d4a
--- /dev/null
+++ b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/resources/EncryptedDir.java
@@ -0,0 +1,179 @@
+/*******************************************************************************
+ * 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.jackrabbit.resources;
+
+import java.io.IOException;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.StandardOpenOption;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.jackrabbit.webdav.DavException;
+import org.apache.jackrabbit.webdav.DavResource;
+import org.apache.jackrabbit.webdav.DavResourceFactory;
+import org.apache.jackrabbit.webdav.DavResourceIterator;
+import org.apache.jackrabbit.webdav.DavResourceIteratorImpl;
+import org.apache.jackrabbit.webdav.DavResourceLocator;
+import org.apache.jackrabbit.webdav.DavServletResponse;
+import org.apache.jackrabbit.webdav.DavSession;
+import org.apache.jackrabbit.webdav.io.InputContext;
+import org.apache.jackrabbit.webdav.io.OutputContext;
+import org.apache.jackrabbit.webdav.lock.LockManager;
+import org.apache.jackrabbit.webdav.property.DavPropertyName;
+import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
+import org.apache.jackrabbit.webdav.property.ResourceType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import de.sebastianstenzel.oce.crypto.Cryptor;
+import de.sebastianstenzel.oce.webdav.exceptions.DavRuntimeException;
+import de.sebastianstenzel.oce.webdav.exceptions.IORuntimeException;
+
+public class EncryptedDir extends AbstractEncryptedNode {
+
+ private static final Logger LOG = LoggerFactory.getLogger(EncryptedDir.class);
+
+ public EncryptedDir(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
+ super(factory, locator, session, lockManager, cryptor);
+ }
+
+ @Override
+ public boolean isCollection() {
+ return true;
+ }
+
+ @Override
+ public void addMember(DavResource resource, InputContext inputContext) throws DavException {
+ if (resource.isCollection()) {
+ this.addMemberDir(resource, inputContext);
+ } else {
+ this.addMemberFile(resource, inputContext);
+ }
+ }
+
+ private void addMemberDir(DavResource resource, InputContext inputContext) throws DavException {
+ final Path childPath = PathUtils.getPhysicalPath(resource);
+ try {
+ Files.createDirectories(childPath);
+ } catch (SecurityException e) {
+ throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
+ } catch (IOException e) {
+ LOG.error("Failed to create subdirectory.", e);
+ throw new IORuntimeException(e);
+ }
+ }
+
+ private void addMemberFile(DavResource resource, InputContext inputContext) throws DavException {
+ final Path childPath = PathUtils.getPhysicalPath(resource);
+ SeekableByteChannel channel = null;
+ try {
+ channel = Files.newByteChannel(childPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
+ cryptor.encryptFile(inputContext.getInputStream(), channel);
+ } catch (SecurityException e) {
+ throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
+ } catch (IOException e) {
+ LOG.error("Failed to create file.", e);
+ throw new IORuntimeException(e);
+ } finally {
+ IOUtils.closeQuietly(channel);
+ IOUtils.closeQuietly(inputContext.getInputStream());
+ }
+ }
+
+ @Override
+ public DavResourceIterator getMembers() {
+ final Path dir = PathUtils.getPhysicalPath(this);
+ try {
+ final DirectoryStream directoryStream = Files.newDirectoryStream(dir, cryptor.getPayloadFilesFilter());
+ final List result = new ArrayList<>();
+
+ for (final Path childPath : directoryStream) {
+ final DavResourceLocator childLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), childPath.toString(), false);
+ final DavResource resource = factory.createResource(childLocator, session);
+ result.add(resource);
+ }
+ return new DavResourceIteratorImpl(result);
+ } catch (IOException e) {
+ LOG.error("Exception during getMembers.", e);
+ throw new IORuntimeException(e);
+ } catch (DavException e) {
+ LOG.error("Exception during getMembers.", e);
+ throw new DavRuntimeException(e);
+ }
+ }
+
+ @Override
+ public void removeMember(DavResource member) throws DavException {
+ final Path memberPath = PathUtils.getPhysicalPath(member);
+ try {
+ Files.walkFileTree(memberPath, new DeletingFileVisitor());
+ } catch (SecurityException e) {
+ throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
+ } catch (IOException e) {
+ throw new IORuntimeException(e);
+ }
+ }
+
+ @Override
+ public void spool(OutputContext outputContext) throws IOException {
+ // do nothing
+ }
+
+ @Override
+ protected void determineProperties() {
+ final Path path = PathUtils.getPhysicalPath(this);
+ properties.add(new ResourceType(ResourceType.COLLECTION));
+ properties.add(new DefaultDavProperty(DavPropertyName.ISCOLLECTION, 1));
+ if (Files.exists(path)) {
+ try {
+ final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
+ properties.add(new DefaultDavProperty(DavPropertyName.CREATIONDATE, attrs.creationTime().toMillis()));
+ properties.add(new DefaultDavProperty(DavPropertyName.GETLASTMODIFIED, attrs.lastModifiedTime().toMillis()));
+ } catch (IOException e) {
+ LOG.error("Error determining metadata " + path.toString(), e);
+ throw new IORuntimeException(e);
+ }
+ }
+ }
+
+ /**
+ * Deletes all files and folders, it visits.
+ */
+ private static class DeletingFileVisitor extends SimpleFileVisitor {
+
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
+ if (attributes.isRegularFile()) {
+ Files.delete(file);
+ }
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+ Files.delete(dir);
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
+ LOG.error("Failed to delete file " + file.toString(), exc);
+ return FileVisitResult.TERMINATE;
+ }
+
+ }
+
+}
diff --git a/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/resources/EncryptedFile.java b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/resources/EncryptedFile.java
new file mode 100644
index 000000000..8d7ca8d51
--- /dev/null
+++ b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/resources/EncryptedFile.java
@@ -0,0 +1,111 @@
+/*******************************************************************************
+ * 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.jackrabbit.resources;
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.nio.file.attribute.BasicFileAttributes;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.jackrabbit.webdav.DavException;
+import org.apache.jackrabbit.webdav.DavResource;
+import org.apache.jackrabbit.webdav.DavResourceFactory;
+import org.apache.jackrabbit.webdav.DavResourceIterator;
+import org.apache.jackrabbit.webdav.DavResourceLocator;
+import org.apache.jackrabbit.webdav.DavSession;
+import org.apache.jackrabbit.webdav.io.InputContext;
+import org.apache.jackrabbit.webdav.io.OutputContext;
+import org.apache.jackrabbit.webdav.lock.LockManager;
+import org.apache.jackrabbit.webdav.property.DavPropertyName;
+import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import de.sebastianstenzel.oce.crypto.Cryptor;
+import de.sebastianstenzel.oce.webdav.exceptions.IORuntimeException;
+
+
+public class EncryptedFile extends AbstractEncryptedNode {
+
+ private static final Logger LOG = LoggerFactory.getLogger(EncryptedFile.class);
+
+ public EncryptedFile(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
+ super(factory, locator, session, lockManager, cryptor);
+ }
+
+ @Override
+ public boolean isCollection() {
+ return false;
+ }
+
+ @Override
+ public void addMember(DavResource resource, InputContext inputContext) throws DavException {
+ throw new UnsupportedOperationException("Can not add member to file.");
+ }
+
+ @Override
+ public DavResourceIterator getMembers() {
+ throw new UnsupportedOperationException("Can not list members of file.");
+ }
+
+ @Override
+ public void removeMember(DavResource member) throws DavException {
+ throw new UnsupportedOperationException("Can not remove member to file.");
+ }
+
+ @Override
+ public void spool(OutputContext outputContext) throws IOException {
+ final Path path = PathUtils.getPhysicalPath(this);
+ if (Files.exists(path)) {
+ outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
+ SeekableByteChannel channel = null;
+ try {
+ channel = Files.newByteChannel(path, StandardOpenOption.READ);
+ outputContext.setContentLength(cryptor.decryptedContentLength(channel, null));
+ if (outputContext.hasStream()) {
+ cryptor.decryptedFile(channel, outputContext.getOutputStream());
+ }
+ } catch (EOFException e) {
+ LOG.warn("Unexpected end of stream (possibly client hung up).");
+ } catch (IOException e) {
+ LOG.error("Error reading file " + path.toString(), e);
+ throw new IORuntimeException(e);
+ } finally {
+ IOUtils.closeQuietly(channel);
+ }
+
+ }
+ }
+
+ @Override
+ protected void determineProperties() {
+ final Path path = PathUtils.getPhysicalPath(this);
+ if (Files.exists(path)) {
+ SeekableByteChannel channel = null;
+ try {
+ channel = Files.newByteChannel(path, StandardOpenOption.READ);
+ final Long contentLength = cryptor.decryptedContentLength(channel, null);
+ properties.add(new DefaultDavProperty(DavPropertyName.GETCONTENTLENGTH, contentLength));
+
+ final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
+ properties.add(new DefaultDavProperty(DavPropertyName.CREATIONDATE, attrs.creationTime().toMillis()));
+ properties.add(new DefaultDavProperty(DavPropertyName.GETLASTMODIFIED, attrs.lastModifiedTime().toMillis()));
+ } catch (IOException e) {
+ LOG.error("Error determining metadata " + path.toString(), e);
+ throw new IORuntimeException(e);
+ } finally {
+ IOUtils.closeQuietly(channel);
+ }
+ }
+ }
+
+}
diff --git a/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/resources/NonExistingNode.java b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/resources/NonExistingNode.java
new file mode 100644
index 000000000..10a8e22c8
--- /dev/null
+++ b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/resources/NonExistingNode.java
@@ -0,0 +1,66 @@
+/*******************************************************************************
+ * 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.jackrabbit.resources;
+
+import java.io.IOException;
+
+import org.apache.jackrabbit.webdav.DavException;
+import org.apache.jackrabbit.webdav.DavResource;
+import org.apache.jackrabbit.webdav.DavResourceFactory;
+import org.apache.jackrabbit.webdav.DavResourceIterator;
+import org.apache.jackrabbit.webdav.DavResourceLocator;
+import org.apache.jackrabbit.webdav.DavSession;
+import org.apache.jackrabbit.webdav.io.InputContext;
+import org.apache.jackrabbit.webdav.io.OutputContext;
+import org.apache.jackrabbit.webdav.lock.LockManager;
+
+import de.sebastianstenzel.oce.crypto.Cryptor;
+
+public class NonExistingNode extends AbstractEncryptedNode {
+
+ public NonExistingNode(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
+ super(factory, locator, session, lockManager, cryptor);
+ }
+
+ @Override
+ public boolean exists() {
+ return false;
+ }
+
+ @Override
+ public boolean isCollection() {
+ throw new UnsupportedOperationException("Resource doesn't exist.");
+ }
+
+ @Override
+ public void spool(OutputContext outputContext) throws IOException {
+ throw new UnsupportedOperationException("Resource doesn't exist.");
+ }
+
+ @Override
+ public void addMember(DavResource resource, InputContext inputContext) throws DavException {
+ throw new UnsupportedOperationException("Resource doesn't exist.");
+ }
+
+ @Override
+ public DavResourceIterator getMembers() {
+ throw new UnsupportedOperationException("Resource doesn't exist.");
+ }
+
+ @Override
+ public void removeMember(DavResource member) throws DavException {
+ throw new UnsupportedOperationException("Resource doesn't exist.");
+ }
+
+ @Override
+ protected void determineProperties() {
+ // do nothing.
+ }
+
+}
diff --git a/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/resources/PathUtils.java b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/resources/PathUtils.java
new file mode 100644
index 000000000..9665b2057
--- /dev/null
+++ b/oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/resources/PathUtils.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.webdav.jackrabbit.resources;
+
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+
+import org.apache.jackrabbit.webdav.DavResource;
+import org.apache.jackrabbit.webdav.DavResourceLocator;
+
+public final class PathUtils {
+
+ private PathUtils() {
+ throw new IllegalStateException("not instantiable");
+ }
+
+ public static Path getPhysicalPath(DavResource resource) {
+ return getPhysicalPath(resource.getLocator());
+ }
+
+ public static Path getPhysicalPath(DavResourceLocator locator) {
+ return FileSystems.getDefault().getPath(locator.getRepositoryPath());
+ }
+
+}
diff --git a/oce-main/oce-webdav/src/main/resources/log4j.xml b/oce-main/oce-core/src/main/resources/log4j.xml
similarity index 62%
rename from oce-main/oce-webdav/src/main/resources/log4j.xml
rename to oce-main/oce-core/src/main/resources/log4j.xml
index ecac2310e..9583a53e2 100644
--- a/oce-main/oce-webdav/src/main/resources/log4j.xml
+++ b/oce-main/oce-core/src/main/resources/log4j.xml
@@ -10,8 +10,21 @@
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -26,7 +39,10 @@
-
+
+
+
+
diff --git a/oce-main/oce-crypto/pom.xml b/oce-main/oce-crypto-aes/pom.xml
similarity index 66%
rename from oce-main/oce-crypto/pom.xml
rename to oce-main/oce-crypto-aes/pom.xml
index d563a5d80..775f93170 100644
--- a/oce-main/oce-crypto/pom.xml
+++ b/oce-main/oce-crypto-aes/pom.xml
@@ -1,24 +1,23 @@
-
+
4.0.0
de.sebastianstenzel.oce
oce-main
- 0.0.1-SNAPSHOT
+ 0.1.0-SNAPSHOT
- oce-crypto
- Open Cloud Encryptor Cryptographic module
+ oce-crypto-aes
+ Open Cloud Encryptor cryptographic module (AES)
Provides stream ciphers and filename pseudonymization functions.
+
+ de.sebastianstenzel.oce
+ oce-crypto-api
+ ${project.parent.version}
+
+
org.slf4j
@@ -38,6 +37,11 @@
org.apache.commons
commons-lang3
+
+ commons-codec
+ commons-codec
+
+
diff --git a/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Aes256Cryptor.java b/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Aes256Cryptor.java
new file mode 100644
index 000000000..30aa589a7
--- /dev/null
+++ b/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Aes256Cryptor.java
@@ -0,0 +1,390 @@
+/*******************************************************************************
+ * 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.channels.SeekableByteChannel;
+import java.nio.file.DirectoryStream.Filter;
+import java.nio.file.Path;
+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 javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+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.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import de.sebastianstenzel.oce.crypto.Cryptor;
+import de.sebastianstenzel.oce.crypto.MetadataSupport;
+import de.sebastianstenzel.oce.crypto.exceptions.DecryptFailedException;
+import de.sebastianstenzel.oce.crypto.exceptions.UnsupportedKeyLengthException;
+import de.sebastianstenzel.oce.crypto.exceptions.WrongPasswordException;
+import de.sebastianstenzel.oce.crypto.io.SeekableByteChannelInputStream;
+import de.sebastianstenzel.oce.crypto.io.SeekableByteChannelOutputStream;
+
+public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, FileNamingConventions {
+
+ /**
+ * 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;
+
+ /**
+ * 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;
+
+ /**
+ * 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];
+
+ private static final int SIZE_OF_LONG = Long.SIZE / Byte.SIZE;
+ private static final int SIZE_OF_INT = Integer.SIZE / Byte.SIZE;
+
+ static {
+ try {
+ PBKDF2_FACTORY = SecretKeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
+ SECURE_PRNG = SecureRandom.getInstance(PRNG_ALGORITHM);
+ final int maxKeyLen = Cipher.getMaxAllowedKeyLength(CRYPTO_ALGORITHM);
+ AES_KEY_LENGTH = (maxKeyLen >= 256) ? 256 : maxKeyLen;
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("Algorithm should exist.", e);
+ }
+ }
+
+ public void initializeStorage(OutputStream masterkey, CharSequence password) throws IOException {
+ 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(MASTERKEY_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);
+ objectMapper.writeValue(masterkey, keys);
+ } catch (IllegalBlockSizeException | BadPaddingException ex) {
+ throw new IllegalStateException("Block size hard coded. Padding irrelevant in ENCRYPT_MODE. IV must exist in CBC mode.", ex);
+ }
+ }
+
+ public void unlockStorage(InputStream masterkey, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException {
+ byte[] decrypted = new byte[0];
+ try {
+ // load encrypted masterkey:
+ final Keys keys = objectMapper.readValue(masterkey, Keys.class);
+ ;
+ final Keys.Key ownerKey = keys.getOwnerKey();
+
+ // check, whether the key length is supported:
+ final int maxKeyLen = Cipher.getMaxAllowedKeyLength(CRYPTO_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(MASTERKEY_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(MASTERKEY_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);
+ }
+ }
+
+ /**
+ * 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(String cipherTransformation, SecretKey key, byte[] iv, int cipherMode) {
+ try {
+ final Cipher cipher = Cipher.getInstance(cipherTransformation);
+ 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(), CRYPTO_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);
+ }
+ }
+
+ @Override
+ public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, MetadataSupport metadataSupport) {
+ try {
+ final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
+ final String[] cleartextPathComps = StringUtils.split(cleartextPath, cleartextPathSep);
+ final List encryptedPathComps = new ArrayList<>(cleartextPathComps.length);
+ for (final String cleartext : cleartextPathComps) {
+ final String encrypted = encryptPathComponent(cleartext, key);
+ encryptedPathComps.add(encrypted);
+ }
+ return StringUtils.join(encryptedPathComps, encryptedPathSep);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ throw new IllegalStateException("Unable to encrypt path: " + cleartextPath, e);
+ }
+ }
+
+ private String encryptPathComponent(final String cleartext, final SecretKey key) throws IllegalBlockSizeException, BadPaddingException {
+ if (cleartext.length() > PLAINTEXT_FILENAME_LENGTH_LIMIT) {
+ return encryptLongPathComponent(cleartext, key);
+ } else {
+ return encryptShortPathComponent(cleartext, key);
+ }
+ }
+
+ private String encryptShortPathComponent(final String cleartext, final SecretKey key) throws IllegalBlockSizeException, BadPaddingException {
+ final Cipher cipher = this.cipher(FILE_NAME_CIPHER, key, EMPTY_IV, Cipher.ENCRYPT_MODE);
+ final byte[] encryptedBytes = cipher.doFinal(cleartext.getBytes(Charsets.UTF_8));
+ return ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes) + BASIC_FILE_EXT;
+ }
+
+ private String encryptLongPathComponent(String cleartext, SecretKey key) {
+ throw new UnsupportedOperationException("not yet implemented");
+ }
+
+ @Override
+ public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, MetadataSupport metadataSupport) {
+ try {
+ final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
+ final String[] encryptedPathComps = StringUtils.split(encryptedPath, encryptedPathSep);
+ final List cleartextPathComps = new ArrayList<>(encryptedPathComps.length);
+ for (final String encrypted : encryptedPathComps) {
+ final String cleartext = decryptPathComponent(encrypted, key);
+ cleartextPathComps.add(new String(cleartext));
+ }
+ return StringUtils.join(cleartextPathComps, cleartextPathSep);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ throw new IllegalStateException("Unable to decrypt path: " + encryptedPath, e);
+ }
+ }
+
+ private String decryptPathComponent(final String encrypted, final SecretKey key) throws IllegalBlockSizeException, BadPaddingException {
+ if (encrypted.endsWith(LONG_NAME_FILE_EXT)) {
+ return decryptLongPathComponent(encrypted, key);
+ } else if (encrypted.endsWith(BASIC_FILE_EXT)) {
+ return decryptShortPathComponent(encrypted, key);
+ } else {
+ throw new IllegalArgumentException("Unsupported path component: " + encrypted);
+ }
+ }
+
+ private String decryptShortPathComponent(final String encrypted, final SecretKey key) throws IllegalBlockSizeException, BadPaddingException {
+ final String basename = StringUtils.removeEndIgnoreCase(encrypted, BASIC_FILE_EXT);
+ final Cipher cipher = this.cipher(FILE_NAME_CIPHER, key, EMPTY_IV, Cipher.DECRYPT_MODE);
+ final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(basename);
+ final byte[] cleartextBytes = cipher.doFinal(encryptedBytes);
+ return new String(cleartextBytes, Charsets.UTF_8);
+ }
+
+ private String decryptLongPathComponent(final String encrypted, final SecretKey key) {
+ throw new UnsupportedOperationException("not yet implemented");
+ }
+
+ @Override
+ public Long decryptedContentLength(SeekableByteChannel encryptedFile, MetadataSupport metadataSupport) throws IOException {
+ final ByteBuffer sizeBuffer = ByteBuffer.allocate(SIZE_OF_LONG);
+ final int read = encryptedFile.read(sizeBuffer);
+ if (read == SIZE_OF_LONG) {
+ return sizeBuffer.getLong(0);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException {
+ // skip content size:
+ encryptedFile.position(SIZE_OF_LONG);
+
+ // read iv:
+ final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
+ final int read = encryptedFile.read(countingIv);
+ if (read != AES_BLOCK_LENGTH) {
+ throw new IOException("Failed to read encrypted file header.");
+ }
+
+ // derive secret key and generate cipher:
+ final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
+ final Cipher cipher = this.cipher(FILE_CONTENT_CIPHER, key, countingIv.array(), Cipher.DECRYPT_MODE);
+
+ // read content
+ final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
+ final OutputStream cipheredOut = new CipherOutputStream(plaintextFile, cipher);
+ return IOUtils.copyLarge(in, cipheredOut);
+ }
+
+ @Override
+ public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException {
+ // truncate file
+ encryptedFile.truncate(0);
+
+ // use an IV, whose last 4 bytes store an integer used in counter mode and write initial value to file.
+ final ByteBuffer countingIv = ByteBuffer.wrap(randomData(AES_BLOCK_LENGTH));
+ countingIv.putInt(AES_BLOCK_LENGTH - SIZE_OF_INT, 0);
+
+ // derive secret key and generate cipher:
+ final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
+ final Cipher cipher = this.cipher(FILE_CONTENT_CIPHER, key, countingIv.array(), Cipher.ENCRYPT_MODE);
+
+ // skip 8 bytes (reserved for file size):
+ encryptedFile.position(SIZE_OF_LONG);
+
+ // write iv:
+ encryptedFile.write(countingIv);
+
+ // write content:
+ final OutputStream out = new SeekableByteChannelOutputStream(encryptedFile);
+ final OutputStream cipheredOut = new CipherOutputStream(out, cipher);
+ final Long actualSize = IOUtils.copyLarge(plaintextFile, cipheredOut);
+
+ // write filesize
+ final ByteBuffer actualSizeBuffer = ByteBuffer.allocate(SIZE_OF_LONG);
+ actualSizeBuffer.putLong(actualSize);
+ actualSizeBuffer.position(0);
+ encryptedFile.position(0);
+ encryptedFile.write(actualSizeBuffer);
+
+ return actualSize;
+ }
+
+ @Override
+ public Filter getPayloadFilesFilter() {
+ return new Filter() {
+ @Override
+ public boolean accept(Path entry) throws IOException {
+ return ENCRYPTED_FILE_GLOB_MATCHER.matches(entry);
+ }
+ };
+ }
+}
diff --git a/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/AesCryptographicConfiguration.java b/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/AesCryptographicConfiguration.java
new file mode 100644
index 000000000..0fd66c3a2
--- /dev/null
+++ b/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/AesCryptographicConfiguration.java
@@ -0,0 +1,93 @@
+/*******************************************************************************
+ * 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;
+
+interface AesCryptographicConfiguration {
+
+ /**
+ * Number of bytes used as seed for the PRNG.
+ */
+ 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.
+ */
+ int MASTER_KEY_LENGTH = 512;
+
+ /**
+ * Number of bytes used as salt, where needed.
+ */
+ int SALT_LENGTH = 8;
+
+ /**
+ * 0-filled salt.
+ */
+ byte[] EMPTY_SALT = new byte[SALT_LENGTH];
+
+ /**
+ * Algorithm used for key derivation.
+ */
+ String KEY_FACTORY_ALGORITHM = "PBKDF2WithHmacSHA1";
+
+ /**
+ * Algorithm used for random number generation.
+ */
+ String PRNG_ALGORITHM = "SHA1PRNG";
+
+ /**
+ * Algorithm used for en/decryption.
+ *
+ * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#AlgorithmParameters
+ */
+ String CRYPTO_ALGORITHM = "AES";
+
+ /**
+ * Cipher specs for masterkey encryption.
+ *
+ * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher
+ */
+ String MASTERKEY_CIPHER = "AES/CBC/PKCS5Padding";
+
+ /**
+ * Cipher specs for file name encryption.
+ *
+ * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher
+ */
+ String FILE_NAME_CIPHER = "AES/CBC/PKCS5Padding";
+
+ /**
+ * Cipher specs for content encryption. Using CTR-mode for random access.
+ *
+ * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher
+ */
+ String FILE_CONTENT_CIPHER = "AES/CTR/NoPadding";
+
+ /**
+ * AES block size is 128 bit or 16 bytes.
+ */
+ int AES_BLOCK_LENGTH = 16;
+
+ /**
+ * 0-filled initialization vector.
+ */
+ byte[] EMPTY_IV = new byte[AES_BLOCK_LENGTH];
+
+ /**
+ * Number of iterations for key derived from user pw. High iteration count for better resistance to bruteforcing.
+ */
+ 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.
+ */
+ int PBKDF2_MASTERKEY_ITERATIONS = 1;
+
+}
diff --git a/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/FileNamingConventions.java b/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/FileNamingConventions.java
new file mode 100644
index 000000000..665c81efd
--- /dev/null
+++ b/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/FileNamingConventions.java
@@ -0,0 +1,53 @@
+/*******************************************************************************
+ * 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.nio.file.FileSystems;
+import java.nio.file.PathMatcher;
+
+import org.apache.commons.codec.binary.Base32;
+import org.apache.commons.codec.binary.BaseNCodec;
+
+
+interface FileNamingConventions {
+
+ /**
+ * Name of the masterkey file inside the root directory of the encrypted storage.
+ */
+ String MASTERKEY_FILENAME = "masterkey.json";
+
+ /**
+ * How to encode the encrypted file names safely.
+ */
+ BaseNCodec ENCRYPTED_FILENAME_CODEC = new Base32();
+
+ /**
+ * Maximum length possible on file systems with a filename limit of 255 chars.
+ * 144 and 160 are multiples of 16 (128bit aes block size).
+ * 144 * 8/5 (base32) = 230,..
+ * 160 * 8/5 = 256
+ * Base 64 isn't supported on case-insensitive file systems.
+ */
+ int PLAINTEXT_FILENAME_LENGTH_LIMIT = 144;
+
+ /**
+ * For plaintext file names <= {@value #PLAINTEXT_FILENAME_LENGTH_LIMIT} chars.
+ */
+ String BASIC_FILE_EXT = ".aes";
+
+ /**
+ * For plaintext file names > {@value #PLAINTEXT_FILENAME_LENGTH_LIMIT} chars.
+ */
+ String LONG_NAME_FILE_EXT = ".lng.aes";
+
+ /**
+ * Matches both, {@value #BASIC_FILE_EXT} and {@value #LONG_NAME_FILE_EXT} files.
+ */
+ PathMatcher ENCRYPTED_FILE_GLOB_MATCHER = FileSystems.getDefault().getPathMatcher("glob:**/*{" + BASIC_FILE_EXT + "," + LONG_NAME_FILE_EXT + "}");
+
+}
diff --git a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Keys.java b/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Keys.java
similarity index 100%
rename from oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Keys.java
rename to oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Keys.java
diff --git a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Metadata.java b/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Metadata.java
similarity index 100%
rename from oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Metadata.java
rename to oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Metadata.java
diff --git a/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/exceptions/DecryptFailedException.java b/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/exceptions/DecryptFailedException.java
new file mode 100644
index 000000000..10c01308b
--- /dev/null
+++ b/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/exceptions/DecryptFailedException.java
@@ -0,0 +1,9 @@
+package de.sebastianstenzel.oce.crypto.exceptions;
+
+public class DecryptFailedException extends StorageCryptingException {
+ private static final long serialVersionUID = -3855673600374897828L;
+
+ public DecryptFailedException(Throwable t) {
+ super("Decryption failed.", t);
+ }
+}
\ No newline at end of file
diff --git a/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/exceptions/StorageCryptingException.java b/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/exceptions/StorageCryptingException.java
new file mode 100644
index 000000000..5777e8ab5
--- /dev/null
+++ b/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/exceptions/StorageCryptingException.java
@@ -0,0 +1,13 @@
+package de.sebastianstenzel.oce.crypto.exceptions;
+
+public 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);
+ }
+}
\ No newline at end of file
diff --git a/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/exceptions/UnsupportedKeyLengthException.java b/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/exceptions/UnsupportedKeyLengthException.java
new file mode 100644
index 000000000..839a1afa7
--- /dev/null
+++ b/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/exceptions/UnsupportedKeyLengthException.java
@@ -0,0 +1,10 @@
+package de.sebastianstenzel.oce.crypto.exceptions;
+
+public 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));
+ }
+
+}
\ No newline at end of file
diff --git a/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/exceptions/WrongPasswordException.java b/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/exceptions/WrongPasswordException.java
new file mode 100644
index 000000000..50358ee3b
--- /dev/null
+++ b/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/exceptions/WrongPasswordException.java
@@ -0,0 +1,9 @@
+package de.sebastianstenzel.oce.crypto.exceptions;
+
+public class WrongPasswordException extends StorageCryptingException {
+ private static final long serialVersionUID = -602047799678568780L;
+
+ public WrongPasswordException() {
+ super("Wrong password.");
+ }
+}
\ No newline at end of file
diff --git a/oce-main/oce-crypto-aes/src/test/java/de/sebastianstenzel/oce/crypto/aes256/Aes256CryptorTest.java b/oce-main/oce-crypto-aes/src/test/java/de/sebastianstenzel/oce/crypto/aes256/Aes256CryptorTest.java
new file mode 100644
index 000000000..e2e48a81d
--- /dev/null
+++ b/oce-main/oce-crypto-aes/src/test/java/de/sebastianstenzel/oce/crypto/aes256/Aes256CryptorTest.java
@@ -0,0 +1,104 @@
+/*******************************************************************************
+ * 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.file.FileAlreadyExistsException;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import de.sebastianstenzel.oce.crypto.exceptions.DecryptFailedException;
+import de.sebastianstenzel.oce.crypto.exceptions.UnsupportedKeyLengthException;
+import de.sebastianstenzel.oce.crypto.exceptions.WrongPasswordException;
+
+public class Aes256CryptorTest {
+
+ private Path tmpDir;
+ private Path masterKey;
+
+ @Before
+ public void prepareTmpDir() throws IOException {
+ final String tmpDirName = (String) System.getProperties().get("java.io.tmpdir");
+ final Path path = FileSystems.getDefault().getPath(tmpDirName);
+ tmpDir = Files.createTempDirectory(path, "oce-crypto-test");
+ masterKey = tmpDir.resolve(Aes256Cryptor.MASTERKEY_FILENAME);
+ }
+
+ @After
+ public void dropTmpDir() throws IOException {
+ FileUtils.deleteDirectory(tmpDir.toFile());
+ }
+
+ /* ------------------------------------------------------------------------------- */
+
+ @Test
+ public void testCorrectPassword() throws IOException, WrongPasswordException, DecryptFailedException, UnsupportedKeyLengthException {
+ final String pw = "asd";
+ final Aes256Cryptor cryptor = new Aes256Cryptor();
+ final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ cryptor.initializeStorage(out, pw);
+ cryptor.swipeSensitiveData();
+
+ final Aes256Cryptor decryptor = new Aes256Cryptor();
+ final InputStream in = Files.newInputStream(masterKey, StandardOpenOption.READ);
+ decryptor.unlockStorage(in, pw);
+ }
+
+ @Test(expected = WrongPasswordException.class)
+ public void testWrongPassword() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
+ final String pw = "asd";
+ final Aes256Cryptor cryptor = new Aes256Cryptor();
+ final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ cryptor.initializeStorage(out, pw);
+ cryptor.swipeSensitiveData();
+
+ final String wrongPw = "foo";
+ final Aes256Cryptor decryptor = new Aes256Cryptor();
+ final InputStream in = Files.newInputStream(masterKey, StandardOpenOption.READ);
+ decryptor.unlockStorage(in, wrongPw);
+ }
+
+ @Test(expected = NoSuchFileException.class)
+ public void testWrongLocation() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
+ final String pw = "asd";
+ final Aes256Cryptor cryptor = new Aes256Cryptor();
+ final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ cryptor.initializeStorage(out, pw);
+ cryptor.swipeSensitiveData();
+
+ final Path wrongMasterKey = tmpDir.resolve("notExistingMasterKey.json");
+ final Aes256Cryptor decryptor = new Aes256Cryptor();
+ final InputStream in = Files.newInputStream(wrongMasterKey, StandardOpenOption.READ);
+ decryptor.unlockStorage(in, pw);
+ }
+
+ @Test(expected = FileAlreadyExistsException.class)
+ public void testReInitialization() throws IOException {
+ final String pw = "asd";
+ final Aes256Cryptor cryptor = new Aes256Cryptor();
+ final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ cryptor.initializeStorage(out, pw);
+ cryptor.swipeSensitiveData();
+
+ final OutputStream outAgain = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
+ cryptor.initializeStorage(outAgain, pw);
+ cryptor.swipeSensitiveData();
+ }
+
+}
diff --git a/oce-main/oce-crypto-api/.gitignore b/oce-main/oce-crypto-api/.gitignore
new file mode 100644
index 000000000..b83d22266
--- /dev/null
+++ b/oce-main/oce-crypto-api/.gitignore
@@ -0,0 +1 @@
+/target/
diff --git a/oce-main/oce-crypto-api/pom.xml b/oce-main/oce-crypto-api/pom.xml
new file mode 100644
index 000000000..c3173e991
--- /dev/null
+++ b/oce-main/oce-crypto-api/pom.xml
@@ -0,0 +1,18 @@
+
+ 4.0.0
+
+ de.sebastianstenzel.oce
+ oce-main
+ 0.1.0-SNAPSHOT
+
+ oce-crypto-api
+ Open Cloud Encryptor cryptographic module API
+
+
+
+ commons-io
+ commons-io
+
+
+
+
\ No newline at end of file
diff --git a/oce-main/oce-crypto-api/src/main/java/de/sebastianstenzel/oce/crypto/Cryptor.java b/oce-main/oce-crypto-api/src/main/java/de/sebastianstenzel/oce/crypto/Cryptor.java
new file mode 100644
index 000000000..a2053b1ae
--- /dev/null
+++ b/oce-main/oce-crypto-api/src/main/java/de/sebastianstenzel/oce/crypto/Cryptor.java
@@ -0,0 +1,75 @@
+/*******************************************************************************
+ * 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.channels.SeekableByteChannel;
+import java.nio.file.DirectoryStream.Filter;
+import java.nio.file.Path;
+
+/**
+ * Provides access to cryptographic functions. All methods are threadsafe.
+ */
+public interface Cryptor {
+
+ /**
+ * Encrypts each plaintext path component for its own.
+ *
+ * @param cleartextPath A relative path (UTF-8 encoded)
+ * @param encryptedPathSep Path separator char like '/' used on local file system. Must not be null, even if cleartextPath is a sole
+ * file name without any path separators.
+ * @param cleartextPathSep Path separator char like '/' used in webdav URIs. Must not be null, even if cleartextPath is a sole file name
+ * without any path separators.
+ * @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
+ * @return Encrypted path components concatenated by the given encryptedPathSep. Must not start with encryptedPathSep, unless the
+ * encrypted path is explicitly absolute.
+ */
+ String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, MetadataSupport metadataSupport);
+
+ /**
+ * Decrypts each encrypted path component for its own.
+ *
+ * @param encryptedPath A relative path (UTF-8 encoded)
+ * @param encryptedPathSep Path separator char like '/' used on local file system. Must not be null, even if encryptedPath is a sole
+ * file name without any path separators.
+ * @param cleartextPathSep Path separator char like '/' used in webdav URIs. Must not be null, even if encryptedPath is a sole file name
+ * without any path separators.
+ * @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
+ * @return Decrypted path components concatenated by the given cleartextPathSep. Must not start with cleartextPathSep, unless the
+ * cleartext path is explicitly absolute.
+ */
+ String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, MetadataSupport metadataSupport);
+
+ /**
+ * @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
+ * @return Content length of the decrypted file or null if unknown.
+ */
+ Long decryptedContentLength(SeekableByteChannel encryptedFile, MetadataSupport metadataSupport) throws IOException;
+
+ /**
+ * @return Number of decrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.
+ */
+ Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException;
+
+ /**
+ * @return Number of encrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.
+ */
+ Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException;
+
+ /**
+ * @return A filter, that returns true for encrypted files, i.e. if the file is an actual user payload and not a supporting
+ * metadata file of the {@link Cryptor}.
+ */
+ Filter getPayloadFilesFilter();
+
+ void swipeSensitiveData();
+
+}
diff --git a/oce-main/oce-crypto-api/src/main/java/de/sebastianstenzel/oce/crypto/MetadataSupport.java b/oce-main/oce-crypto-api/src/main/java/de/sebastianstenzel/oce/crypto/MetadataSupport.java
new file mode 100644
index 000000000..f42533525
--- /dev/null
+++ b/oce-main/oce-crypto-api/src/main/java/de/sebastianstenzel/oce/crypto/MetadataSupport.java
@@ -0,0 +1,53 @@
+/*******************************************************************************
+ * 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.NoSuchFileException;
+
+@Deprecated
+public interface MetadataSupport {
+
+ /**
+ * Opens the file with the given name for writing. Overwrites existing files.
+ *
+ * @return Outputstream ready to write to. Must be closed by caller.
+ * @throws IOException
+ */
+ OutputStream openMetadataForWrite(String filename, Level location) throws IOException;
+
+ /**
+ * Opens the file with the given name without locking it.
+ *
+ * @return InputStream ready to read from. Must be closed by caller.
+ * @throws NoSuchFileException
+ * @throws IOException
+ */
+ InputStream openMetadataForRead(String filename, Level location) throws NoSuchFileException, IOException;
+
+ enum Level {
+ /**
+ * Root folder of the encrypted store.
+ */
+ ROOT_FOLDER,
+
+ /**
+ * Parent folder of the current object.
+ */
+ PARENT_FOLDER,
+
+ /**
+ * If the current object is a folder, its own location. If the current object is a file, behaves the same as {@link #PARENT_FOLDER}.
+ */
+ CURRENT_FOLDER;
+ }
+
+}
diff --git a/oce-main/oce-crypto-api/src/main/java/de/sebastianstenzel/oce/crypto/NotACryptor.java b/oce-main/oce-crypto-api/src/main/java/de/sebastianstenzel/oce/crypto/NotACryptor.java
new file mode 100644
index 000000000..102d42fc3
--- /dev/null
+++ b/oce-main/oce-crypto-api/src/main/java/de/sebastianstenzel/oce/crypto/NotACryptor.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.crypto;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.DirectoryStream.Filter;
+import java.nio.file.Path;
+
+import org.apache.commons.io.IOUtils;
+
+import de.sebastianstenzel.oce.crypto.io.SeekableByteChannelInputStream;
+import de.sebastianstenzel.oce.crypto.io.SeekableByteChannelOutputStream;
+
+/**
+ * @deprecated Just for test purposes.
+ */
+@Deprecated
+public class NotACryptor implements Cryptor {
+
+ @Override
+ public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, MetadataSupport metadataSupport) {
+ return cleartextPath;
+ }
+
+ @Override
+ public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, MetadataSupport metadataSupport) {
+ return encryptedPath;
+ }
+
+ @Override
+ public Long decryptedContentLength(SeekableByteChannel encryptedFile, MetadataSupport metadataSupport) throws IOException {
+ return encryptedFile.size();
+ }
+
+ @Override
+ public Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException {
+ final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
+ return IOUtils.copyLarge(in, plaintextFile);
+ }
+
+ @Override
+ public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException {
+ final OutputStream out = new SeekableByteChannelOutputStream(encryptedFile);
+ return IOUtils.copyLarge(plaintextFile, out);
+ }
+
+ @Override
+ public Filter getPayloadFilesFilter() {
+ return new Filter() {
+
+ @Override
+ public boolean accept(Path entry) throws IOException {
+ /* all files are "encrypted" */
+ return true;
+ }
+ };
+ }
+
+ @Override
+ public void swipeSensitiveData() {
+ // do nothing.
+ }
+
+}
diff --git a/oce-main/oce-crypto-api/src/main/java/de/sebastianstenzel/oce/crypto/io/SeekableByteChannelInputStream.java b/oce-main/oce-crypto-api/src/main/java/de/sebastianstenzel/oce/crypto/io/SeekableByteChannelInputStream.java
new file mode 100644
index 000000000..db4421a93
--- /dev/null
+++ b/oce-main/oce-crypto-api/src/main/java/de/sebastianstenzel/oce/crypto/io/SeekableByteChannelInputStream.java
@@ -0,0 +1,90 @@
+/*******************************************************************************
+ * 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.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.SeekableByteChannel;
+
+public class SeekableByteChannelInputStream extends InputStream {
+ private final SeekableByteChannel channel;
+ private volatile long markedPos = 0;
+
+ public SeekableByteChannelInputStream(SeekableByteChannel channel) {
+ this.channel = channel;
+ }
+
+ @Override
+ public int read() throws IOException {
+ final ByteBuffer buffer = ByteBuffer.allocate(1);
+ final int read = channel.read(buffer);
+ if (read == 1) {
+ return buffer.get(0);
+ } else {
+ return -1;
+ }
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ final ByteBuffer buffer = ByteBuffer.wrap(b, off, len);
+ return channel.read(buffer);
+ }
+
+ @Override
+ public int available() throws IOException {
+ long available = channel.size() - channel.position();
+ if (available > Integer.MAX_VALUE) {
+ return Integer.MAX_VALUE;
+ } else {
+ return (int) available;
+ }
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ final long pos = channel.position();
+ final long max = channel.size();
+ final long maxSkip = max - pos;
+ final long actualSkip = Math.min(n, maxSkip);
+ channel.position(channel.position() + actualSkip);
+ return actualSkip;
+ }
+
+ @Override
+ public void close() throws IOException {
+ channel.close();
+ super.close();
+ }
+
+ @Override
+ public synchronized void mark(int readlimit) {
+ try {
+ markedPos = channel.position();
+ } catch (IOException e) {
+ markedPos = 0;
+ }
+ }
+
+ @Override
+ public synchronized void reset() throws IOException {
+ channel.position(markedPos);
+ }
+
+ public synchronized void resetTo(long position) throws IOException {
+ channel.position(position);
+ }
+
+ @Override
+ public boolean markSupported() {
+ return true;
+ }
+
+}
\ No newline at end of file
diff --git a/oce-main/oce-crypto-api/src/main/java/de/sebastianstenzel/oce/crypto/io/SeekableByteChannelOutputStream.java b/oce-main/oce-crypto-api/src/main/java/de/sebastianstenzel/oce/crypto/io/SeekableByteChannelOutputStream.java
new file mode 100644
index 000000000..14ecca572
--- /dev/null
+++ b/oce-main/oce-crypto-api/src/main/java/de/sebastianstenzel/oce/crypto/io/SeekableByteChannelOutputStream.java
@@ -0,0 +1,64 @@
+/*******************************************************************************
+ * 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.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.SeekableByteChannel;
+
+public class SeekableByteChannelOutputStream extends OutputStream {
+
+ private final SeekableByteChannel channel;
+
+ public SeekableByteChannelOutputStream(SeekableByteChannel channel) {
+ this.channel = channel;
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ final byte actualByte = (byte) (b & 0x000000FF);
+ final ByteBuffer buffer = ByteBuffer.allocate(1);
+ buffer.put(actualByte);
+ channel.write(buffer);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ final ByteBuffer buffer = ByteBuffer.wrap(b, off, len);
+ channel.write(buffer);
+ }
+
+ @Override
+ public void close() throws IOException {
+ channel.close();
+ }
+
+ /**
+ * @see SeekableByteChannel#truncate(long)
+ */
+ public void truncate(long size) throws IOException {
+ channel.truncate(size);
+ }
+
+ /**
+ * @see SeekableByteChannel#position()
+ */
+ public long position() throws IOException {
+ return channel.position();
+ }
+
+ /**
+ * @see SeekableByteChannel#position(long)
+ */
+ public void position(long newPosition) throws IOException {
+ channel.position(newPosition);
+ }
+
+}
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
deleted file mode 100644
index ca5da3af7..000000000
--- a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/Cryptor.java
+++ /dev/null
@@ -1,21 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2014 Sebastian Stenzel
- * This file is licensed under the terms of the MIT license.
- * See the LICENSE.txt file for more info.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- ******************************************************************************/
-package 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
deleted file mode 100644
index a72ac951c..000000000
--- a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/FilenamePseudonymizing.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2014 Sebastian Stenzel
- * This file is licensed under the terms of the MIT license.
- * See the LICENSE.txt file for more info.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- ******************************************************************************/
-package 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
deleted file mode 100644
index 12feae128..000000000
--- a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/StorageCrypting.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2014 Sebastian Stenzel
- * This file is licensed under the terms of the MIT license.
- * See the LICENSE.txt file for more info.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- ******************************************************************************/
-package 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
deleted file mode 100644
index e2313bfe5..000000000
--- a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/TransactionAwareFileAccess.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2014 Sebastian Stenzel
- * This file is licensed under the terms of the MIT license.
- * See the LICENSE.txt file for more info.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- ******************************************************************************/
-package 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
deleted file mode 100644
index 871e3a1dd..000000000
--- a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/AesCryptor.java
+++ /dev/null
@@ -1,585 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2014 Sebastian Stenzel
- * This file is licensed under the terms of the MIT license.
- * See the LICENSE.txt file for more info.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- ******************************************************************************/
-package 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/cache/PseudonymRepository.java b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cache/PseudonymRepository.java
deleted file mode 100644
index a1663e0e8..000000000
--- a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cache/PseudonymRepository.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2014 Sebastian Stenzel
- * This file is licensed under the terms of the MIT license.
- * See the LICENSE.txt file for more info.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- ******************************************************************************/
-package 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
deleted file mode 100644
index 2fbacbf39..000000000
--- a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cleartext/Metadata.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2014 Sebastian Stenzel
- * This file is licensed under the terms of the MIT license.
- * See the LICENSE.txt file for more info.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- ******************************************************************************/
-package 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
deleted file mode 100644
index f7388c1f1..000000000
--- a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cleartext/NoCryptor.java
+++ /dev/null
@@ -1,246 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2014 Sebastian Stenzel
- * This file is licensed under the terms of the MIT license.
- * See the LICENSE.txt file for more info.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- ******************************************************************************/
-package 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
deleted file mode 100644
index 9ffd1f1f9..000000000
--- a/oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/AesCryptorTest.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2014 Sebastian Stenzel
- * This file is licensed under the terms of the MIT license.
- * See the LICENSE.txt file for more info.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- ******************************************************************************/
-package 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
deleted file mode 100644
index d658ea72b..000000000
--- a/oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/FilenamePseudonymizerTest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2014 Sebastian Stenzel
- * This file is licensed under the terms of the MIT license.
- * See the LICENSE.txt file for more info.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- ******************************************************************************/
-package 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
deleted file mode 100644
index 70b1df838..000000000
--- a/oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/PseudonymRepositoryTest.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2014 Sebastian Stenzel
- * This file is licensed under the terms of the MIT license.
- * See the LICENSE.txt file for more info.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- ******************************************************************************/
-package 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
index 145bb8ad4..0de6f4400 100644
--- a/oce-main/oce-ui/pom.xml
+++ b/oce-main/oce-ui/pom.xml
@@ -12,7 +12,7 @@
de.sebastianstenzel.oce
oce-main
- 0.0.1-SNAPSHOT
+ 0.1.0-SNAPSHOT
oce-ui
Open Cloud Encryptor GUI
@@ -25,7 +25,12 @@
de.sebastianstenzel.oce
- oce-webdav
+ oce-core
+ ${project.parent.version}
+
+
+ de.sebastianstenzel.oce
+ oce-crypto-aes
${project.parent.version}
@@ -34,6 +39,12 @@
com.fasterxml.jackson.core
jackson-databind
+
+
+
+ commons-io
+ commons-io
+
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
index 51467f003..41334d8f9 100644
--- 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
@@ -10,10 +10,14 @@ package de.sebastianstenzel.oce.ui;
import java.io.File;
import java.io.IOException;
+import java.io.InputStream;
import java.net.URL;
import java.nio.file.FileSystems;
+import java.nio.file.Files;
import java.nio.file.InvalidPathException;
+import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
@@ -25,36 +29,42 @@ import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.stage.DirectoryChooser;
+import org.apache.commons.io.IOUtils;
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.crypto.aes256.Aes256Cryptor;
+import de.sebastianstenzel.oce.crypto.exceptions.DecryptFailedException;
+import de.sebastianstenzel.oce.crypto.exceptions.UnsupportedKeyLengthException;
+import de.sebastianstenzel.oce.crypto.exceptions.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 final Aes256Cryptor cryptor = new Aes256Cryptor();
private ResourceBundle localization;
- @FXML private GridPane rootGridPane;
- @FXML private TextField workDirTextField;
- @FXML private SecPasswordField passwordField;
- @FXML private Button startServerButton;
- @FXML private Label messageLabel;
-
+ @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);
@@ -76,38 +86,42 @@ public class AccessController implements Initializable {
}
determineStorageValidity();
}
-
+
private void determineStorageValidity() {
boolean storageLocationValid;
try {
final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText());
- storageLocationValid = Cryptor.getDefaultCryptor().isStorage(storagePath);
- } catch(InvalidPathException ex) {
+ final Path masterKeyPath = storagePath.resolve(Aes256Cryptor.MASTERKEY_FILENAME);
+ storageLocationValid = Files.exists(masterKeyPath);
+ } 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();
+ cryptor.swipeSensitiveData();
} else if (this.unlockStorage()) {
this.tryStart();
}
}
-
+
private boolean unlockStorage() {
final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText());
+ final Path masterKeyPath = storagePath.resolve(Aes256Cryptor.MASTERKEY_FILENAME);
final CharSequence password = passwordField.getCharacters();
+ InputStream masterKeyInputStream = null;
try {
- Cryptor.getDefaultCryptor().unlockStorage(storagePath, password);
+ masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ);
+ cryptor.unlockStorage(masterKeyInputStream, password);
return true;
- } catch (InvalidStorageLocationException e) {
+ } catch (NoSuchFileException e) {
messageLabel.setText(localization.getString("access.messageLabel.invalidStorageLocation"));
LOG.warn("Invalid path: " + storagePath.toString());
} catch (DecryptFailedException ex) {
@@ -122,14 +136,15 @@ public class AccessController implements Initializable {
LOG.error("I/O Exception", ex);
} finally {
passwordField.swipe();
+ IOUtils.closeQuietly(masterKeyInputStream);
}
return false;
}
-
+
private void tryStart() {
try {
final Settings settings = Settings.load();
- if (WebDAVServer.getInstance().start(settings.getWebdavWorkDir(), settings.getPort())) {
+ if (WebDAVServer.getInstance().start(settings.getWebdavWorkDir(), settings.getPort(), cryptor)) {
startServerButton.setText(localization.getString("access.button.stopServer"));
passwordField.setDisable(true);
}
@@ -137,7 +152,7 @@ public class AccessController implements Initializable {
LOG.error("Invalid port", ex);
}
}
-
+
private void tryStop() {
if (WebDAVServer.getInstance().stop()) {
startServerButton.setText(localization.getString("access.button.startServer"));
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
index 5ec0af236..0ae0a9f2c 100644
--- 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
@@ -10,9 +10,14 @@ package de.sebastianstenzel.oce.ui;
import java.io.File;
import java.io.IOException;
+import java.io.OutputStream;
import java.net.URL;
+import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystems;
+import java.nio.file.Files;
import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
import java.util.ResourceBundle;
import javafx.beans.value.ChangeListener;
@@ -26,35 +31,40 @@ import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.stage.DirectoryChooser;
+import org.apache.commons.io.IOUtils;
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.crypto.aes256.Aes256Cryptor;
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;
-
+ @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.
+ * Step 1: Choose a directory, that shall be encrypted. On success, step 2 will be enabled.
*/
@FXML
protected void chooseWorkDir(ActionEvent event) {
@@ -71,10 +81,9 @@ public class InitializeController implements Initializable {
passwordField.requestFocus();
}
}
-
+
/**
- * Step 2: Defina a password.
- * On success, step 3 will be enabled.
+ * Step 2: Defina a password. On success, step 3 will be enabled.
*/
private final class PasswordChangeListener implements ChangeListener {
@Override
@@ -82,10 +91,9 @@ public class InitializeController implements Initializable {
retypePasswordField.setDisable(newValue.isEmpty());
}
}
-
+
/**
- * Step 3: Retype the password.
- * On success, step 4 will be enabled.
+ * Step 3: Retype the password. On success, step 4 will be enabled.
*/
private final class RetypePasswordChangeListener implements ChangeListener {
@Override
@@ -94,31 +102,36 @@ public class InitializeController implements Initializable {
initWorkDirButton.setDisable(!passwordsAreEqual);
}
}
-
+
/**
- * Step 4: Generate master password file in working directory.
- * On success, print success message.
+ * Step 4: Generate master password file in working directory. On success, print success message.
*/
@FXML
protected void initWorkDir(ActionEvent event) {
+ final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText());
+ final Path masterKeyPath = storagePath.resolve(Aes256Cryptor.MASTERKEY_FILENAME);
+ final Aes256Cryptor cryptor = new Aes256Cryptor();
+ final CharSequence password = passwordField.getCharacters();
+ OutputStream masterKeyOutputStream = null;
try {
- Cryptor.getDefaultCryptor().initializeStorage(FileSystems.getDefault().getPath(workDirTextField.getText()), passwordField.getText());
- Cryptor.getDefaultCryptor().swipeSensitiveData();
- } catch (AlreadyInitializedException ex) {
+ masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
+ cryptor.initializeStorage(masterKeyOutputStream, password);
+ cryptor.swipeSensitiveData();
+ } catch (FileAlreadyExistsException ex) {
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
- } catch(InvalidPathException ex) {
+ } catch (InvalidPathException ex) {
messageLabel.setText(localization.getString("initialize.messageLabel.invalidPath"));
} catch (IOException ex) {
LOG.error("I/O Exception", ex);
} finally {
swipePasswordFields();
+ IOUtils.closeQuietly(masterKeyOutputStream);
}
}
-
+
private void swipePasswordFields() {
passwordField.swipe();
retypePasswordField.swipe();
}
-
}
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
deleted file mode 100644
index bc54c93d1..000000000
--- a/oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/EnhancedWebDavServlet.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2014 Sebastian Stenzel
- * This file is licensed under the terms of the MIT license.
- * See the LICENSE.txt file for more info.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- ******************************************************************************/
-package 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
deleted file mode 100644
index a47457308..000000000
--- a/oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/EnhancedWebdavStore.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2014 Sebastian Stenzel
- * This file is licensed under the terms of the MIT license.
- * See the LICENSE.txt file for more info.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- ******************************************************************************/
-package 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
deleted file mode 100644
index 4f2093259..000000000
--- a/oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/FsWebdavCryptoAdapter.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2014 Sebastian Stenzel
- * This file is licensed under the terms of the MIT license.
- * See the LICENSE.txt file for more info.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- ******************************************************************************/
-package 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
deleted file mode 100644
index 62bb92bc9..000000000
--- a/oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/FsWebdavResourceHandler.java
+++ /dev/null
@@ -1,228 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2014 Sebastian Stenzel
- * This file is licensed under the terms of the MIT license.
- * See the LICENSE.txt file for more info.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- ******************************************************************************/
-package 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
deleted file mode 100644
index cf60e56e3..000000000
--- a/oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/FsWebdavTransaction.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2014 Sebastian Stenzel
- * This file is licensed under the terms of the MIT license.
- * See the LICENSE.txt file for more info.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- ******************************************************************************/
-package 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/pom.xml b/oce-main/pom.xml
index a618e9512..bcb2db965 100644
--- a/oce-main/pom.xml
+++ b/oce-main/pom.xml
@@ -1,17 +1,10 @@
-
+
4.0.0
de.sebastianstenzel.oce
oce-main
- 0.0.1-SNAPSHOT
+ 0.1.0-SNAPSHOT
pom
Open Cloud Encryptor
@@ -29,6 +22,7 @@
2.4
4.0
3.1
+ 1.9
@@ -73,12 +67,17 @@
commons-lang3
${commons-lang.version}
+
+ commons-codec
+ commons-codec
+ ${commons-codec.version}
+
com.fasterxml.jackson.core
jackson-databind
- 2.3.0
+ 2.4.2
@@ -92,9 +91,10 @@
- oce-webdav
+ oce-crypto-api
+ oce-crypto-aes
+ oce-core
oce-ui
- oce-crypto