From 9988ec6c0bd0f45ec0812bffd6624a0753ce2d76 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sat, 27 Sep 2014 23:49:41 +0200 Subject: [PATCH] - replaced webdav implementation (webdav-servlet -> milton -> jackrabbit): faster, better, harder, stronger and much more space for future improvements - more lightweight filename encryption (no more metadata for filenames < 144 chars), thus less filehandles, less blocking I/O - vastly refactored project structure --- README.md | 17 +- oce-main/oce-core/.gitignore | 1 + oce-main/{oce-webdav => oce-core}/pom.xml | 40 +- .../oce/webdav/WebDAVServer.java | 27 +- .../exceptions/DavRuntimeException.java | 31 + .../webdav/exceptions/IORuntimeException.java | 31 + .../jackrabbit/WebDavLocatorFactory.java | 69 +++ .../jackrabbit/WebDavResourceFactory.java | 80 +++ .../oce/webdav/jackrabbit/WebDavServlet.java | 87 +++ .../oce/webdav/jackrabbit/WebDavSession.java | 45 ++ .../jackrabbit/WebDavSessionProvider.java | 29 + .../resources/AbstractEncryptedNode.java | 270 ++++++++ .../jackrabbit/resources/EncryptedDir.java | 179 ++++++ .../jackrabbit/resources/EncryptedFile.java | 111 ++++ .../jackrabbit/resources/NonExistingNode.java | 66 ++ .../jackrabbit/resources/PathUtils.java | 31 + .../src/main/resources/log4j.xml | 20 +- .../{oce-crypto => oce-crypto-aes}/pom.xml | 26 +- .../oce/crypto/aes256/Aes256Cryptor.java | 390 ++++++++++++ .../aes256/AesCryptographicConfiguration.java | 93 +++ .../crypto/aes256/FileNamingConventions.java | 53 ++ .../oce/crypto/aes256/Keys.java | 0 .../oce/crypto/aes256/Metadata.java | 0 .../exceptions/DecryptFailedException.java | 9 + .../exceptions/StorageCryptingException.java | 13 + .../UnsupportedKeyLengthException.java | 10 + .../exceptions/WrongPasswordException.java | 9 + .../oce/crypto/aes256/Aes256CryptorTest.java | 104 ++++ oce-main/oce-crypto-api/.gitignore | 1 + oce-main/oce-crypto-api/pom.xml | 18 + .../sebastianstenzel/oce/crypto/Cryptor.java | 75 +++ .../oce/crypto/MetadataSupport.java | 53 ++ .../oce/crypto/NotACryptor.java | 73 +++ .../io/SeekableByteChannelInputStream.java | 90 +++ .../io/SeekableByteChannelOutputStream.java | 64 ++ .../sebastianstenzel/oce/crypto/Cryptor.java | 21 - .../oce/crypto/FilenamePseudonymizing.java | 32 - .../oce/crypto/StorageCrypting.java | 91 --- .../crypto/TransactionAwareFileAccess.java | 31 - .../oce/crypto/aes256/AesCryptor.java | 585 ------------------ .../oce/crypto/cache/PseudonymRepository.java | 157 ----- .../oce/crypto/cleartext/Metadata.java | 31 - .../oce/crypto/cleartext/NoCryptor.java | 246 -------- .../oce/crypto/test/AesCryptorTest.java | 92 --- .../test/FilenamePseudonymizerTest.java | 88 --- .../crypto/test/PseudonymRepositoryTest.java | 50 -- oce-main/oce-ui/pom.xml | 15 +- .../oce/ui/AccessController.java | 65 +- .../oce/ui/InitializeController.java | 71 ++- .../oce/webdav/EnhancedWebDavServlet.java | 39 -- .../oce/webdav/EnhancedWebdavStore.java | 120 ---- .../oce/webdav/FsWebdavCryptoAdapter.java | 183 ------ .../oce/webdav/FsWebdavResourceHandler.java | 228 ------- .../oce/webdav/FsWebdavTransaction.java | 40 -- oce-main/pom.xml | 24 +- 55 files changed, 2267 insertions(+), 2157 deletions(-) create mode 100644 oce-main/oce-core/.gitignore rename oce-main/{oce-webdav => oce-core}/pom.xml (60%) rename oce-main/{oce-webdav => oce-core}/src/main/java/de/sebastianstenzel/oce/webdav/WebDAVServer.java (69%) create mode 100644 oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/exceptions/DavRuntimeException.java create mode 100644 oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/exceptions/IORuntimeException.java create mode 100644 oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/WebDavLocatorFactory.java create mode 100644 oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/WebDavResourceFactory.java create mode 100644 oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/WebDavServlet.java create mode 100644 oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/WebDavSession.java create mode 100644 oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/WebDavSessionProvider.java create mode 100644 oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/resources/AbstractEncryptedNode.java create mode 100644 oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/resources/EncryptedDir.java create mode 100644 oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/resources/EncryptedFile.java create mode 100644 oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/resources/NonExistingNode.java create mode 100644 oce-main/oce-core/src/main/java/de/sebastianstenzel/oce/webdav/jackrabbit/resources/PathUtils.java rename oce-main/{oce-webdav => oce-core}/src/main/resources/log4j.xml (62%) rename oce-main/{oce-crypto => oce-crypto-aes}/pom.xml (66%) create mode 100644 oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Aes256Cryptor.java create mode 100644 oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/AesCryptographicConfiguration.java create mode 100644 oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/FileNamingConventions.java rename oce-main/{oce-crypto => oce-crypto-aes}/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Keys.java (100%) rename oce-main/{oce-crypto => oce-crypto-aes}/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Metadata.java (100%) create mode 100644 oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/exceptions/DecryptFailedException.java create mode 100644 oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/exceptions/StorageCryptingException.java create mode 100644 oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/exceptions/UnsupportedKeyLengthException.java create mode 100644 oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/exceptions/WrongPasswordException.java create mode 100644 oce-main/oce-crypto-aes/src/test/java/de/sebastianstenzel/oce/crypto/aes256/Aes256CryptorTest.java create mode 100644 oce-main/oce-crypto-api/.gitignore create mode 100644 oce-main/oce-crypto-api/pom.xml create mode 100644 oce-main/oce-crypto-api/src/main/java/de/sebastianstenzel/oce/crypto/Cryptor.java create mode 100644 oce-main/oce-crypto-api/src/main/java/de/sebastianstenzel/oce/crypto/MetadataSupport.java create mode 100644 oce-main/oce-crypto-api/src/main/java/de/sebastianstenzel/oce/crypto/NotACryptor.java create mode 100644 oce-main/oce-crypto-api/src/main/java/de/sebastianstenzel/oce/crypto/io/SeekableByteChannelInputStream.java create mode 100644 oce-main/oce-crypto-api/src/main/java/de/sebastianstenzel/oce/crypto/io/SeekableByteChannelOutputStream.java delete mode 100644 oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/Cryptor.java delete mode 100644 oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/FilenamePseudonymizing.java delete mode 100644 oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/StorageCrypting.java delete mode 100644 oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/TransactionAwareFileAccess.java delete mode 100644 oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/AesCryptor.java delete mode 100644 oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cache/PseudonymRepository.java delete mode 100644 oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cleartext/Metadata.java delete mode 100644 oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cleartext/NoCryptor.java delete mode 100644 oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/AesCryptorTest.java delete mode 100644 oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/FilenamePseudonymizerTest.java delete mode 100644 oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/PseudonymRepositoryTest.java delete mode 100644 oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/EnhancedWebDavServlet.java delete mode 100644 oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/EnhancedWebdavStore.java delete mode 100644 oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/FsWebdavCryptoAdapter.java delete mode 100644 oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/FsWebdavResourceHandler.java delete mode 100644 oce-main/oce-webdav/src/main/java/de/sebastianstenzel/oce/webdav/FsWebdavTransaction.java 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 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