mirror of
https://github.com/cryptomator/cryptomator.git
synced 2024-11-23 03:59:51 +00:00
- 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
This commit is contained in:
parent
318eb24d64
commit
9988ec6c0b
17
README.md
17
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
|
||||
|
||||
|
1
oce-main/oce-core/.gitignore
vendored
Normal file
1
oce-main/oce-core/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target/
|
@ -1,25 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright (c) 2014 Sebastian Stenzel
|
||||
This file is licensed under the terms of the MIT license.
|
||||
See the LICENSE.txt file for more info.
|
||||
|
||||
Contributors:
|
||||
Sebastian Stenzel - initial API and implementation
|
||||
-->
|
||||
<!-- Copyright (c) 2014 Sebastian Stenzel This file is licensed under the terms of the MIT license. See the LICENSE.txt file for more info. Contributors: Sebastian Stenzel - initial API and implementation -->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>de.sebastianstenzel.oce</groupId>
|
||||
<artifactId>oce-main</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>oce-webdav</artifactId>
|
||||
<name>Open Cloud Encryptor WebDAV module</name>
|
||||
<artifactId>oce-core</artifactId>
|
||||
<name>Open Cloud Encryptor core I/O module</name>
|
||||
|
||||
<properties>
|
||||
<jetty.version>9.1.0.v20131115</jetty.version>
|
||||
<webdavservlet.version>2.0</webdavservlet.version>
|
||||
<milton.version>2.6.2.4</milton.version>
|
||||
<jackrabbit.version>2.9.0</jackrabbit.version>
|
||||
<commons.transaction.version>1.2</commons.transaction.version>
|
||||
<jta.version>1.1</jta.version>
|
||||
</properties>
|
||||
@ -27,7 +21,7 @@
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>de.sebastianstenzel.oce</groupId>
|
||||
<artifactId>oce-crypto</artifactId>
|
||||
<artifactId>oce-crypto-api</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
|
||||
@ -49,11 +43,11 @@
|
||||
<version>${jetty.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- WebDAV Servlet -->
|
||||
<!-- Jackrabbit -->
|
||||
<dependency>
|
||||
<groupId>net.sf.webdav-servlet</groupId>
|
||||
<artifactId>webdav-servlet</artifactId>
|
||||
<version>${webdavservlet.version}</version>
|
||||
<groupId>org.apache.jackrabbit</groupId>
|
||||
<artifactId>jackrabbit-webdav</artifactId>
|
||||
<version>${jackrabbit.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- I/O -->
|
||||
@ -62,16 +56,8 @@
|
||||
<artifactId>commons-io</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.java.xadisk</groupId>
|
||||
<artifactId>xadisk</artifactId>
|
||||
<version>1.2.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JEE 6 implementation used by XADisk -->
|
||||
<dependency>
|
||||
<groupId>org.apache.openejb</groupId>
|
||||
<artifactId>javaee-api</artifactId>
|
||||
<version>6.0-5</version>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,270 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package de.sebastianstenzel.oce.webdav.jackrabbit.resources;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.DavServletResponse;
|
||||
import org.apache.jackrabbit.webdav.DavSession;
|
||||
import org.apache.jackrabbit.webdav.MultiStatusResponse;
|
||||
import org.apache.jackrabbit.webdav.lock.ActiveLock;
|
||||
import org.apache.jackrabbit.webdav.lock.LockInfo;
|
||||
import org.apache.jackrabbit.webdav.lock.LockManager;
|
||||
import org.apache.jackrabbit.webdav.lock.Scope;
|
||||
import org.apache.jackrabbit.webdav.lock.Type;
|
||||
import org.apache.jackrabbit.webdav.property.DavProperty;
|
||||
import org.apache.jackrabbit.webdav.property.DavPropertyName;
|
||||
import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
|
||||
import org.apache.jackrabbit.webdav.property.DavPropertySet;
|
||||
import org.apache.jackrabbit.webdav.property.PropEntry;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import de.sebastianstenzel.oce.crypto.Cryptor;
|
||||
import de.sebastianstenzel.oce.webdav.exceptions.IORuntimeException;
|
||||
|
||||
public abstract class AbstractEncryptedNode implements DavResource {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractEncryptedNode.class);
|
||||
private static final String DAV_COMPLIANCE_CLASSES = "1, 2";
|
||||
|
||||
protected final DavResourceFactory factory;
|
||||
protected final DavResourceLocator locator;
|
||||
protected final DavSession session;
|
||||
protected final LockManager lockManager;
|
||||
protected final Cryptor cryptor;
|
||||
protected final DavPropertySet properties;
|
||||
|
||||
protected AbstractEncryptedNode(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
|
||||
this.factory = factory;
|
||||
this.locator = locator;
|
||||
this.session = session;
|
||||
this.lockManager = lockManager;
|
||||
this.cryptor = cryptor;
|
||||
this.properties = new DavPropertySet();
|
||||
this.determineProperties();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getComplianceClass() {
|
||||
return DAV_COMPLIANCE_CLASSES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSupportedMethods() {
|
||||
return METHODS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
final Path path = PathUtils.getPhysicalPath(this);
|
||||
return Files.exists(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
final String resourcePath = getResourcePath();
|
||||
final int lastSlash = resourcePath.lastIndexOf('/');
|
||||
if (lastSlash == -1) {
|
||||
return resourcePath;
|
||||
} else {
|
||||
return resourcePath.substring(lastSlash);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceLocator getLocator() {
|
||||
return locator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getResourcePath() {
|
||||
return locator.getResourcePath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHref() {
|
||||
return locator.getHref(this.isCollection());
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getModificationTime() {
|
||||
final Path path = PathUtils.getPhysicalPath(this);
|
||||
try {
|
||||
return Files.getLastModifiedTime(path).toMillis();
|
||||
} catch (IOException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void determineProperties();
|
||||
|
||||
@Override
|
||||
public DavPropertyName[] getPropertyNames() {
|
||||
return getProperties().getPropertyNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavProperty<?> getProperty(DavPropertyName name) {
|
||||
return getProperties().get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavPropertySet getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProperty(DavProperty<?> property) throws DavException {
|
||||
getProperties().add(property);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeProperty(DavPropertyName propertyName) throws DavException {
|
||||
getProperties().remove(propertyName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultiStatusResponse alterProperties(List<? extends PropEntry> changeList) throws DavException {
|
||||
final DavPropertyNameSet names = new DavPropertyNameSet();
|
||||
for (final PropEntry entry : changeList) {
|
||||
if (entry instanceof DavProperty) {
|
||||
final DavProperty<?> prop = (DavProperty<?>) entry;
|
||||
this.setProperty(prop);
|
||||
names.add(prop.getName());
|
||||
} else if (entry instanceof DavPropertyName) {
|
||||
final DavPropertyName name = (DavPropertyName) entry;
|
||||
this.removeProperty(name);
|
||||
names.add(name);
|
||||
}
|
||||
}
|
||||
return new MultiStatusResponse(this, names);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResource getCollection() {
|
||||
if (locator.isRootLocation()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String parentResource = FilenameUtils.getPath(locator.getResourcePath());
|
||||
final DavResourceLocator parentLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), parentResource);
|
||||
try {
|
||||
return getFactory().createResource(parentLocator, session);
|
||||
} catch (DavException e) {
|
||||
throw new IllegalStateException("Unable to get parent resource with path " + parentLocator.getResourcePath(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void move(DavResource dest) throws DavException {
|
||||
final Path src = PathUtils.getPhysicalPath(this);
|
||||
final Path dst = PathUtils.getPhysicalPath(dest);
|
||||
try {
|
||||
// check for conflicts:
|
||||
if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) {
|
||||
throw new DavException(DavServletResponse.SC_CONFLICT, "File at destination already exists: " + dst.toString());
|
||||
}
|
||||
|
||||
// move:
|
||||
try {
|
||||
Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error moving file from " + src.toString() + " to " + dst.toString());
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copy(DavResource dest, boolean shallow) throws DavException {
|
||||
final Path src = PathUtils.getPhysicalPath(this);
|
||||
final Path dst = PathUtils.getPhysicalPath(dest);
|
||||
try {
|
||||
// check for conflicts:
|
||||
if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) {
|
||||
throw new DavException(DavServletResponse.SC_CONFLICT, "File at destination already exists: " + dst.toString());
|
||||
}
|
||||
|
||||
// copy:
|
||||
try {
|
||||
Files.copy(src, dst, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.copy(src, dst, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error copying file from " + src.toString() + " to " + dst.toString());
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLockable(Type type, Scope scope) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasLock(Type type, Scope scope) {
|
||||
return lockManager.getLock(type, scope, this) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActiveLock getLock(Type type, Scope scope) {
|
||||
return lockManager.getLock(type, scope, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActiveLock[] getLocks() {
|
||||
final ActiveLock exclusiveWriteLock = getLock(Type.WRITE, Scope.EXCLUSIVE);
|
||||
return new ActiveLock[] {exclusiveWriteLock};
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActiveLock lock(LockInfo reqLockInfo) throws DavException {
|
||||
return lockManager.createLock(reqLockInfo, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActiveLock refreshLock(LockInfo reqLockInfo, String lockToken) throws DavException {
|
||||
return lockManager.refreshLock(reqLockInfo, lockToken, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unlock(String lockToken) throws DavException {
|
||||
lockManager.releaseLock(lockToken, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addLockManager(LockManager lockmgr) {
|
||||
throw new UnsupportedOperationException("Locks are managed");
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceFactory getFactory() {
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavSession getSession() {
|
||||
return session;
|
||||
}
|
||||
|
||||
}
|
@ -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<Path> directoryStream = Files.newDirectoryStream(dir, cryptor.getPayloadFilesFilter());
|
||||
final List<DavResource> 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<Integer>(DavPropertyName.ISCOLLECTION, 1));
|
||||
if (Files.exists(path)) {
|
||||
try {
|
||||
final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
|
||||
properties.add(new DefaultDavProperty<Long>(DavPropertyName.CREATIONDATE, attrs.creationTime().toMillis()));
|
||||
properties.add(new DefaultDavProperty<Long>(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<Path> {
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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<Long>(DavPropertyName.GETCONTENTLENGTH, contentLength));
|
||||
|
||||
final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
|
||||
properties.add(new DefaultDavProperty<Long>(DavPropertyName.CREATIONDATE, attrs.creationTime().toMillis()));
|
||||
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETLASTMODIFIED, attrs.lastModifiedTime().toMillis()));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error determining metadata " + path.toString(), e);
|
||||
throw new IORuntimeException(e);
|
||||
} finally {
|
||||
IOUtils.closeQuietly(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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.
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -10,8 +10,21 @@
|
||||
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
|
||||
|
||||
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
|
||||
|
||||
<appender name="console" class="org.apache.log4j.ConsoleAppender">
|
||||
<param name="Target" value="System.out"/>
|
||||
<param name="Target" value="System.out"/>
|
||||
<layout class="org.apache.log4j.PatternLayout">
|
||||
<param name="ConversionPattern" value="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
</layout>
|
||||
<filter class="org.apache.log4j.varia.LevelRangeFilter">
|
||||
<param name="LevelMin" value="debug" />
|
||||
<param name="LevelMax" value="info" />
|
||||
</filter>
|
||||
</appender>
|
||||
|
||||
<appender name="stderr" class="org.apache.log4j.ConsoleAppender">
|
||||
<param name="Target" value="System.err"/>
|
||||
<param name="threshold" value="warn" />
|
||||
<layout class="org.apache.log4j.PatternLayout">
|
||||
<param name="ConversionPattern" value="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
</layout>
|
||||
@ -26,7 +39,10 @@
|
||||
</appender>
|
||||
|
||||
<root>
|
||||
<priority value="INFO" />
|
||||
<priority value="DEBUG" />
|
||||
<appender-ref ref="console" />
|
||||
<appender-ref ref="stderr" />
|
||||
</root>
|
||||
|
||||
|
||||
</log4j:configuration>
|
@ -1,24 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright (c) 2014 Sebastian Stenzel
|
||||
This file is licensed under the terms of the MIT license.
|
||||
See the LICENSE.txt file for more info.
|
||||
|
||||
Contributors:
|
||||
Sebastian Stenzel - initial API and implementation
|
||||
-->
|
||||
<!-- Copyright (c) 2014 Sebastian Stenzel This file is licensed under the terms of the MIT license. See the LICENSE.txt file for more info. Contributors: Sebastian Stenzel - initial API and implementation -->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>de.sebastianstenzel.oce</groupId>
|
||||
<artifactId>oce-main</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>oce-crypto</artifactId>
|
||||
<name>Open Cloud Encryptor Cryptographic module</name>
|
||||
<artifactId>oce-crypto-aes</artifactId>
|
||||
<name>Open Cloud Encryptor cryptographic module (AES)</name>
|
||||
<description>Provides stream ciphers and filename pseudonymization functions.</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>de.sebastianstenzel.oce</groupId>
|
||||
<artifactId>oce-crypto-api</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Logging -->
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
@ -38,6 +37,11 @@
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- JSON -->
|
||||
<dependency>
|
@ -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<String> 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<String> 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<Path> getPayloadFilesFilter() {
|
||||
return new Filter<Path>() {
|
||||
@Override
|
||||
public boolean accept(Path entry) throws IOException {
|
||||
return ENCRYPTED_FILE_GLOB_MATCHER.matches(entry);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
@ -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.<br/>
|
||||
* 144 and 160 are multiples of 16 (128bit aes block size).<br/>
|
||||
* 144 * 8/5 (base32) = 230,..<br/>
|
||||
* 160 * 8/5 = 256<br/>
|
||||
* Base 64 isn't supported on case-insensitive file systems.<br/>
|
||||
*/
|
||||
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 + "}");
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
@ -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.");
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
1
oce-main/oce-crypto-api/.gitignore
vendored
Normal file
1
oce-main/oce-crypto-api/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target/
|
18
oce-main/oce-crypto-api/pom.xml
Normal file
18
oce-main/oce-crypto-api/pom.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>de.sebastianstenzel.oce</groupId>
|
||||
<artifactId>oce-main</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>oce-crypto-api</artifactId>
|
||||
<name>Open Cloud Encryptor cryptographic module API</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -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 <code>null</code> 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 <code>true</code> for encrypted files, i.e. if the file is an actual user payload and not a supporting
|
||||
* metadata file of the {@link Cryptor}.
|
||||
*/
|
||||
Filter<Path> getPayloadFilesFilter();
|
||||
|
||||
void swipeSensitiveData();
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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<Path> getPayloadFilesFilter() {
|
||||
return new Filter<Path>() {
|
||||
|
||||
@Override
|
||||
public boolean accept(Path entry) throws IOException {
|
||||
/* all files are "encrypted" */
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void swipeSensitiveData() {
|
||||
// do nothing.
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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 <code>null</code>, if the pseudonym can't be resolved.
|
||||
*/
|
||||
String uncoverPseudonym(String pseudonymizedUri, TransactionAwareFileAccess accessor) throws IOException;
|
||||
|
||||
/**
|
||||
* Deletes a pair of cleartext/pseudonym file name from the cache and metadata file.
|
||||
*/
|
||||
void deletePseudonym(String pseudonymizedUri, TransactionAwareFileAccess accessor) throws IOException;
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
}
|
@ -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<String> cleartextUriComps = this.splitUri(cleartextUri);
|
||||
final List<String> pseudonymUriComps = PseudonymRepository.pseudonymizedPathComponents(cleartextUriComps);
|
||||
|
||||
// return immediately if path is already known:
|
||||
if (pseudonymUriComps.size() == cleartextUriComps.size()) {
|
||||
return concatUri(pseudonymUriComps);
|
||||
}
|
||||
|
||||
// append further path components otherwise:
|
||||
for (int i = pseudonymUriComps.size(); i < cleartextUriComps.size(); i++) {
|
||||
final String currentFolder = concatUri(pseudonymUriComps);
|
||||
final String cleartext = cleartextUriComps.get(i);
|
||||
String pseudonym = readPseudonymFromMetadata(access, currentFolder, cleartext);
|
||||
if (pseudonym == null) {
|
||||
pseudonym = UUID.randomUUID().toString();
|
||||
this.addToMetadata(access, currentFolder, cleartext, pseudonym);
|
||||
}
|
||||
pseudonymUriComps.add(pseudonym);
|
||||
}
|
||||
PseudonymRepository.registerPath(cleartextUriComps, pseudonymUriComps);
|
||||
|
||||
return concatUri(pseudonymUriComps);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uncoverPseudonym(String pseudonymizedUri, TransactionAwareFileAccess access) throws IOException {
|
||||
final List<String> pseudonymUriComps = this.splitUri(pseudonymizedUri);
|
||||
final List<String> cleartextUriComps = PseudonymRepository.cleartextPathComponents(pseudonymUriComps);
|
||||
|
||||
// return immediately if path is already known:
|
||||
if (cleartextUriComps.size() == pseudonymUriComps.size()) {
|
||||
return concatUri(cleartextUriComps);
|
||||
}
|
||||
|
||||
// append further path components otherwise:
|
||||
for (int i = cleartextUriComps.size(); i < pseudonymUriComps.size(); i++) {
|
||||
final String currentFolder = concatUri(pseudonymUriComps.subList(0, i));
|
||||
final String pseudonym = pseudonymUriComps.get(i);
|
||||
try {
|
||||
final String cleartext = this.readCleartextFromMetadata(access, currentFolder, pseudonym);
|
||||
if (cleartext == null) {
|
||||
return null;
|
||||
}
|
||||
cleartextUriComps.add(cleartext);
|
||||
} catch (IOException ex) {
|
||||
LOG.warn("Unresolvable pseudonym: " + currentFolder + "/" + pseudonym);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
PseudonymRepository.registerPath(cleartextUriComps, pseudonymUriComps);
|
||||
|
||||
return concatUri(cleartextUriComps);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deletePseudonym(String pseudonymizedUri, TransactionAwareFileAccess access) throws IOException {
|
||||
// find parent folder:
|
||||
final int lastPathSeparator = pseudonymizedUri.lastIndexOf(URI_PATH_SEP);
|
||||
final String parentUri;
|
||||
if (lastPathSeparator > 0) {
|
||||
parentUri = pseudonymizedUri.substring(0, lastPathSeparator);
|
||||
} else {
|
||||
parentUri = "/";
|
||||
}
|
||||
|
||||
// delete from metadata file:
|
||||
final String pseudonym = pseudonymizedUri.substring(lastPathSeparator + 1);
|
||||
final Metadata metadata = this.loadOrCreateMetadata(access, parentUri);
|
||||
metadata.getFilenames().remove(pseudonym);
|
||||
metadata.getFilesizes().remove(pseudonym);
|
||||
this.saveMetadata(metadata, access, parentUri);
|
||||
|
||||
// delete from cache:
|
||||
final List<String> pseudonymUriComps = this.splitUri(pseudonymizedUri);
|
||||
PseudonymRepository.unregisterPath(pseudonymUriComps);
|
||||
}
|
||||
|
||||
/* Metadata load & save */
|
||||
|
||||
private String readPseudonymFromMetadata(TransactionAwareFileAccess access, String parentFolder, String cleartext) throws IOException {
|
||||
final Metadata metadata = loadOrCreateMetadata(access, parentFolder);
|
||||
return metadata.getFilenames().getKey(cleartext);
|
||||
}
|
||||
|
||||
private String readCleartextFromMetadata(TransactionAwareFileAccess access, String parentFolder, String pseudonym) throws IOException {
|
||||
final Metadata metadata = loadOrCreateMetadata(access, parentFolder);
|
||||
final byte[] encryptedFilename = metadata.getFilenames().get(pseudonym);
|
||||
if (encryptedFilename == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
// decrypt filename:
|
||||
final SecretKey key = this.pbkdf2(masterKey, metadata.getSalt(), PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
|
||||
final Cipher decCipher = this.cipher(key, metadata.getIv(), Cipher.DECRYPT_MODE);
|
||||
byte[] decryptedFilename = decCipher.doFinal(encryptedFilename);
|
||||
return new String(decryptedFilename, Charsets.UTF_8);
|
||||
} catch (IllegalBlockSizeException | BadPaddingException ex) {
|
||||
LOG.error("Can't decrypt filename " + pseudonym + " in folder " + parentFolder, ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void addToMetadata(TransactionAwareFileAccess access, String parentFolder, String cleartext, String pseudonym) throws IOException {
|
||||
final Metadata metadata = loadOrCreateMetadata(access, parentFolder);
|
||||
try {
|
||||
// encrypt filename:
|
||||
final SecretKey key = this.pbkdf2(masterKey, metadata.getSalt(), PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
|
||||
final Cipher encCipher = this.cipher(key, metadata.getIv(), Cipher.ENCRYPT_MODE);
|
||||
byte[] encryptedFilename = encCipher.doFinal(cleartext.getBytes(Charsets.UTF_8));
|
||||
|
||||
// save metadata
|
||||
metadata.getFilenames().put(pseudonym, encryptedFilename);
|
||||
saveMetadata(metadata, access, parentFolder);
|
||||
} catch (IllegalBlockSizeException | BadPaddingException ex) {
|
||||
LOG.error("Can't encrypt filename " + pseudonym + " (" + cleartext + ") in folder " + parentFolder, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Metadata loadOrCreateMetadata(TransactionAwareFileAccess access, String parentFolder) throws IOException {
|
||||
InputStream in = null;
|
||||
try {
|
||||
final Path path = access.resolveUri(parentFolder).resolve(METADATA_FILENAME);
|
||||
in = access.openFileForRead(path);
|
||||
return objectMapper.readValue(in, Metadata.class);
|
||||
} catch (IOException ex) {
|
||||
final byte[] salt = randomData(SALT_LENGTH);
|
||||
final byte[] iv = randomData(AES_BLOCK_LENGTH);
|
||||
return new Metadata(iv, salt);
|
||||
} finally {
|
||||
if (in != null) {
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveMetadata(Metadata metadata, TransactionAwareFileAccess access, String parentFolder) throws IOException {
|
||||
OutputStream out = null;
|
||||
try {
|
||||
final Path path = access.resolveUri(parentFolder).resolve(METADATA_FILENAME);
|
||||
out = access.openFileForWrite(path);
|
||||
objectMapper.writeValue(out, metadata);
|
||||
} finally {
|
||||
if (out != null) {
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* utility stuff */
|
||||
|
||||
private String concatUri(final List<String> uriComponents) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
for (final String comp : uriComponents) {
|
||||
sb.append(URI_PATH_SEP).append(comp);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private List<String> splitUri(final String uri) {
|
||||
final List<String> result = new ArrayList<>();
|
||||
int begin = 0;
|
||||
int end = 0;
|
||||
do {
|
||||
end = uri.indexOf(URI_PATH_SEP, begin);
|
||||
end = (end == -1) ? uri.length() : end;
|
||||
if (end > begin) {
|
||||
result.add(uri.substring(begin, end));
|
||||
}
|
||||
begin = end + 1;
|
||||
} while (end < uri.length());
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
@ -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<String> cleartextPathComponents(final List<String> pseudonymizedPathComponents) {
|
||||
final List<String> result = new ArrayList<>(pseudonymizedPathComponents.size());
|
||||
Node node = ROOT;
|
||||
for (final String pseudonym : pseudonymizedPathComponents) {
|
||||
node = node.subnodesByPseudonym.get(pseudonym);
|
||||
if (node == null) {
|
||||
return result;
|
||||
}
|
||||
result.add(node.cleartext);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The deepest resolvable pseudonymized path for the requested cleartext path.
|
||||
*/
|
||||
public static List<String> pseudonymizedPathComponents(final List<String> cleartextPathComponents) {
|
||||
final List<String> result = new ArrayList<>(cleartextPathComponents.size());
|
||||
Node node = ROOT;
|
||||
for (final String cleartext : cleartextPathComponents) {
|
||||
Node subnode = node.subnodesByCleartext.get(cleartext);
|
||||
if (subnode == null) {
|
||||
return result;
|
||||
}
|
||||
node = subnode;
|
||||
result.add(node.pseudonym);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches a path of cleartext/pseudonym pairs.
|
||||
*/
|
||||
public static void registerPath(final List<String> cleartextPathComponents, final List<String> pseudonymPathComponents) {
|
||||
if (cleartextPathComponents.size() != pseudonymPathComponents.size()) {
|
||||
throw new IllegalArgumentException("Cannot register pseudonymized path, that isn't matching the length of its cleartext equivalent.");
|
||||
}
|
||||
|
||||
Node node = ROOT;
|
||||
for (int i=0; i<cleartextPathComponents.size(); i++) {
|
||||
final String cleartextComp = cleartextPathComponents.get(i);
|
||||
final String pseudonymComp = pseudonymPathComponents.get(i);
|
||||
node = node.getOrCreateSubnode(cleartextComp, pseudonymComp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a path of cleartext/pseudonym pairs from the cache.
|
||||
*/
|
||||
public static void unregisterPath(final List<String> pseudonymPathComponents) {
|
||||
Node node = ROOT;
|
||||
for (final String pseudonymComp : pseudonymPathComponents) {
|
||||
node = node.subnodesByPseudonym.get(pseudonymComp);
|
||||
}
|
||||
if (!ROOT.equals(node)) {
|
||||
node.detach();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Node in a tree of cleartext/pseudonym pairs, that can be traversed root to leaf. The whole tree is threadsafe.
|
||||
* As each node of the tree has its own synchronization, multithreaded access is balanced.
|
||||
*/
|
||||
private static final class Node {
|
||||
private final Node parent;
|
||||
private final String cleartext;
|
||||
private final String pseudonym;
|
||||
private final Map<String, Node> subnodesByCleartext;
|
||||
private final Map<String, Node> subnodesByPseudonym;
|
||||
|
||||
Node(Node parent, String cleartext, String pseudonym) {
|
||||
this.parent = parent;
|
||||
this.cleartext = cleartext;
|
||||
this.pseudonym = pseudonym;
|
||||
this.subnodesByCleartext = new ConcurrentHashMap<>();
|
||||
this.subnodesByPseudonym = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return New subnode attached to this.
|
||||
*/
|
||||
Node getOrCreateSubnode(String cleartext, String pseudonym) {
|
||||
if (subnodesByCleartext.containsKey(cleartext) && subnodesByPseudonym.containsKey(pseudonym)) {
|
||||
return subnodesByCleartext.get(cleartext);
|
||||
}
|
||||
final Node subnode = new Node(this, cleartext, pseudonym);
|
||||
this.subnodesByCleartext.put(cleartext, subnode);
|
||||
this.subnodesByPseudonym.put(pseudonym, subnode);
|
||||
return subnode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a node from its parent node.
|
||||
*/
|
||||
void detach() {
|
||||
// the following two lines don't need to be synchronized,
|
||||
// as inconsistencies are self-healing over the transactional metadata files.
|
||||
this.parent.subnodesByCleartext.remove(this.cleartext);
|
||||
this.parent.subnodesByPseudonym.remove(this.pseudonym);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final HashCodeBuilder hash = new HashCodeBuilder();
|
||||
hash.append(parent);
|
||||
hash.append(cleartext);
|
||||
hash.append(pseudonym);
|
||||
return hash.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Node) {
|
||||
final Node other = (Node) obj;
|
||||
final EqualsBuilder eq = new EqualsBuilder();
|
||||
eq.append(this.parent, other.parent);
|
||||
eq.append(this.cleartext, other.cleartext);
|
||||
eq.append(this.pseudonym, other.pseudonym);
|
||||
return eq.isEquals();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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<String, String> filenames = new DualHashBidiMap<>();
|
||||
|
||||
public BidiMap<String, String> getFilenames() {
|
||||
return filenames;
|
||||
}
|
||||
|
||||
}
|
@ -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<String> cleartextUriComps = this.splitUri(cleartextUri);
|
||||
final List<String> pseudonymUriComps = PseudonymRepository.pseudonymizedPathComponents(cleartextUriComps);
|
||||
|
||||
// return immediately if path is already known:
|
||||
if (pseudonymUriComps.size() == cleartextUriComps.size()) {
|
||||
return concatUri(pseudonymUriComps);
|
||||
}
|
||||
|
||||
// append further path components otherwise:
|
||||
for (int i = pseudonymUriComps.size(); i < cleartextUriComps.size(); i++) {
|
||||
final String currentFolder = concatUri(pseudonymUriComps);
|
||||
final String cleartext = cleartextUriComps.get(i);
|
||||
String pseudonym = readPseudonymFromMetadata(access, currentFolder, cleartext);
|
||||
if (pseudonym == null) {
|
||||
pseudonym = UUID.randomUUID().toString();
|
||||
this.addToMetadata(access, currentFolder, cleartext, pseudonym);
|
||||
}
|
||||
pseudonymUriComps.add(pseudonym);
|
||||
}
|
||||
PseudonymRepository.registerPath(cleartextUriComps, pseudonymUriComps);
|
||||
|
||||
return concatUri(pseudonymUriComps);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uncoverPseudonym(String pseudonymizedUri, TransactionAwareFileAccess access) throws IOException {
|
||||
final List<String> pseudonymUriComps = this.splitUri(pseudonymizedUri);
|
||||
final List<String> cleartextUriComps = PseudonymRepository.cleartextPathComponents(pseudonymUriComps);
|
||||
|
||||
// return immediately if path is already known:
|
||||
if (cleartextUriComps.size() == pseudonymUriComps.size()) {
|
||||
return concatUri(cleartextUriComps);
|
||||
}
|
||||
|
||||
// append further path components otherwise:
|
||||
for (int i = cleartextUriComps.size(); i < pseudonymUriComps.size(); i++) {
|
||||
final String currentFolder = concatUri(pseudonymUriComps.subList(0, i));
|
||||
final String pseudonym = pseudonymUriComps.get(i);
|
||||
try {
|
||||
final String cleartext = this.readCleartextFromMetadata(access, currentFolder, pseudonym);
|
||||
if (cleartext == null) {
|
||||
return null;
|
||||
}
|
||||
cleartextUriComps.add(cleartext);
|
||||
} catch (IOException ex) {
|
||||
LOG.warn("Unresolvable pseudonym: " + currentFolder + "/" + pseudonym);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
PseudonymRepository.registerPath(cleartextUriComps, pseudonymUriComps);
|
||||
|
||||
return concatUri(cleartextUriComps);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deletePseudonym(String pseudonymizedUri, TransactionAwareFileAccess access) throws IOException {
|
||||
// find parent folder:
|
||||
final int lastPathSeparator = pseudonymizedUri.lastIndexOf(URI_PATH_SEP);
|
||||
final String parentUri;
|
||||
if (lastPathSeparator > 0) {
|
||||
parentUri = pseudonymizedUri.substring(0, lastPathSeparator);
|
||||
} else {
|
||||
parentUri = "/";
|
||||
}
|
||||
|
||||
// delete from metadata file:
|
||||
final String pseudonym = pseudonymizedUri.substring(lastPathSeparator + 1);
|
||||
final Metadata metadata = this.loadOrCreateMetadata(access, parentUri);
|
||||
metadata.getFilenames().remove(pseudonym);
|
||||
this.saveMetadata(metadata, access, parentUri);
|
||||
|
||||
// delete from cache:
|
||||
final List<String> pseudonymUriComps = this.splitUri(pseudonymizedUri);
|
||||
PseudonymRepository.unregisterPath(pseudonymUriComps);
|
||||
}
|
||||
|
||||
/* Metadata load & save */
|
||||
|
||||
private String readPseudonymFromMetadata(TransactionAwareFileAccess access, String parentFolder, String cleartext) throws IOException {
|
||||
final Metadata metadata = loadOrCreateMetadata(access, parentFolder);
|
||||
return metadata.getFilenames().getKey(cleartext);
|
||||
}
|
||||
|
||||
private String readCleartextFromMetadata(TransactionAwareFileAccess access, String parentFolder, String pseudonym) throws IOException {
|
||||
final Metadata metadata = loadOrCreateMetadata(access, parentFolder);
|
||||
return metadata.getFilenames().get(pseudonym);
|
||||
}
|
||||
|
||||
private void addToMetadata(TransactionAwareFileAccess access, String parentFolder, String cleartext, String pseudonym) throws IOException {
|
||||
final Metadata metadata = loadOrCreateMetadata(access, parentFolder);
|
||||
if (!pseudonym.equals(metadata.getFilenames().getKey(cleartext))) {
|
||||
metadata.getFilenames().put(pseudonym, cleartext);
|
||||
saveMetadata(metadata, access, parentFolder);
|
||||
}
|
||||
}
|
||||
|
||||
private Metadata loadOrCreateMetadata(TransactionAwareFileAccess access, String parentFolder) throws IOException {
|
||||
InputStream in = null;
|
||||
try {
|
||||
final Path path = access.resolveUri(parentFolder).resolve(METADATA_FILENAME);
|
||||
in = access.openFileForRead(path);
|
||||
return objectMapper.readValue(in, Metadata.class);
|
||||
} catch (IOException ex) {
|
||||
return new Metadata();
|
||||
} finally {
|
||||
if (in != null) {
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveMetadata(Metadata metadata, TransactionAwareFileAccess access, String parentFolder) throws IOException {
|
||||
OutputStream out = null;
|
||||
try {
|
||||
final Path path = access.resolveUri(parentFolder).resolve(METADATA_FILENAME);
|
||||
out = access.openFileForWrite(path);
|
||||
objectMapper.writeValue(out, metadata);
|
||||
} finally {
|
||||
if (out != null) {
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* utility stuff */
|
||||
|
||||
private String concatUri(final List<String> uriComponents) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
for (final String comp : uriComponents) {
|
||||
sb.append(URI_PATH_SEP).append(comp);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private List<String> splitUri(final String uri) {
|
||||
final List<String> result = new ArrayList<>();
|
||||
int begin = 0;
|
||||
int end = 0;
|
||||
do {
|
||||
end = uri.indexOf(URI_PATH_SEP, begin);
|
||||
end = (end == -1) ? uri.length() : end;
|
||||
if (end > begin) {
|
||||
result.add(uri.substring(begin, end));
|
||||
}
|
||||
begin = end + 1;
|
||||
} while (end < uri.length());
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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<String> clear1 = Arrays.asList("foo", "bar", "baz", "info.txt");
|
||||
final List<String> pseudo1 = Arrays.asList("frog", "bear", "bear", "iguana");
|
||||
PseudonymRepository.registerPath(clear1, pseudo1);
|
||||
|
||||
// get pseudonymized path:
|
||||
final List<String> result1 = PseudonymRepository.pseudonymizedPathComponents(clear1);
|
||||
Assert.assertEquals(pseudo1, result1);
|
||||
|
||||
// get cleartext path:
|
||||
final List<String> result2 = PseudonymRepository.cleartextPathComponents(pseudo1);
|
||||
Assert.assertEquals(clear1, result2);
|
||||
|
||||
// register additional path:
|
||||
final List<String> clear2 = Arrays.asList("foo", "bar", "zab", "info.txt");
|
||||
final List<String> pseudo2 = Arrays.asList("frog", "bear", "zebra", "iguana");
|
||||
PseudonymRepository.registerPath(clear2, pseudo2);
|
||||
|
||||
// get pseudonymized path:
|
||||
final List<String> result3 = PseudonymRepository.pseudonymizedPathComponents(clear2);
|
||||
Assert.assertEquals(pseudo2, result3);
|
||||
|
||||
// get cleartext path:
|
||||
final List<String> result4 = PseudonymRepository.cleartextPathComponents(pseudo2);
|
||||
Assert.assertEquals(clear2, result4);
|
||||
}
|
||||
|
||||
}
|
@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>de.sebastianstenzel.oce</groupId>
|
||||
<artifactId>oce-main</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>oce-ui</artifactId>
|
||||
<name>Open Cloud Encryptor GUI</name>
|
||||
@ -25,7 +25,12 @@
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>de.sebastianstenzel.oce</groupId>
|
||||
<artifactId>oce-webdav</artifactId>
|
||||
<artifactId>oce-core</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.sebastianstenzel.oce</groupId>
|
||||
<artifactId>oce-crypto-aes</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
|
||||
@ -34,6 +39,12 @@
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- apache commons -->
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
|
@ -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"));
|
||||
|
@ -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<String> {
|
||||
@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<String> {
|
||||
@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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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 <T extends ITransaction> implements IWebdavStore {
|
||||
|
||||
private final Class<T> transactionClass;
|
||||
|
||||
protected EnhancedWebdavStore(final Class<T> transactionClass) {
|
||||
this.transactionClass = transactionClass;
|
||||
}
|
||||
|
||||
private T cast(final ITransaction transaction) {
|
||||
if (transactionClass.isAssignableFrom(transaction.getClass())) {
|
||||
return transactionClass.cast(transaction);
|
||||
} else {
|
||||
throw new IllegalStateException("transaction " + transaction + " is not of type " + transactionClass.getName());
|
||||
}
|
||||
}
|
||||
|
||||
abstract void destroy();
|
||||
|
||||
@Override
|
||||
public final ITransaction begin(Principal principal) {
|
||||
return beginTransactionInternal(principal);
|
||||
}
|
||||
|
||||
protected abstract T beginTransactionInternal(Principal principal);
|
||||
|
||||
@Override
|
||||
public final void checkAuthentication(ITransaction transaction) {
|
||||
checkAuthenticationInternal(cast(transaction));
|
||||
}
|
||||
|
||||
protected abstract void checkAuthenticationInternal(T transaction);
|
||||
|
||||
@Override
|
||||
public void commit(ITransaction transaction) {
|
||||
commitInternal(cast(transaction));
|
||||
}
|
||||
|
||||
protected abstract void commitInternal(T transaction);
|
||||
|
||||
@Override
|
||||
public void rollback(ITransaction transaction) {
|
||||
rollbackInternal(cast(transaction));
|
||||
}
|
||||
|
||||
protected abstract void rollbackInternal(T transaction);
|
||||
|
||||
@Override
|
||||
public void createFolder(ITransaction transaction, String folderUri) {
|
||||
createFolderInternal(cast(transaction), folderUri);
|
||||
}
|
||||
|
||||
protected abstract void createFolderInternal(T transaction, String folderUri);
|
||||
|
||||
@Override
|
||||
public void createResource(ITransaction transaction, String resourceUri) {
|
||||
createResourceInternal(cast(transaction), resourceUri);
|
||||
}
|
||||
|
||||
protected abstract void createResourceInternal(T transaction, String resourceUri);
|
||||
|
||||
@Override
|
||||
public InputStream getResourceContent(ITransaction transaction, String resourceUri) {
|
||||
return getResourceContentInternal(cast(transaction), resourceUri);
|
||||
}
|
||||
|
||||
protected abstract InputStream getResourceContentInternal(T transaction, String resourceUri);
|
||||
|
||||
@Override
|
||||
public long setResourceContent(ITransaction transaction, String resourceUri, InputStream content, String contentType, String characterEncoding) {
|
||||
return setResourceContentInternal(cast(transaction), resourceUri, content, contentType, characterEncoding);
|
||||
}
|
||||
|
||||
protected abstract long setResourceContentInternal(T transaction, String resourceUri, InputStream content, String contentType, String characterEncoding);
|
||||
|
||||
@Override
|
||||
public String[] getChildrenNames(ITransaction transaction, String folderUri) {
|
||||
return getChildrenNamesInternal(cast(transaction), folderUri);
|
||||
}
|
||||
|
||||
protected abstract String[] getChildrenNamesInternal(T transaction, String folderUri);
|
||||
|
||||
@Override
|
||||
public long getResourceLength(ITransaction transaction, String path) {
|
||||
return getResourceLengthInternal(cast(transaction), path);
|
||||
}
|
||||
|
||||
protected abstract long getResourceLengthInternal(T transaction, String path);
|
||||
|
||||
@Override
|
||||
public void removeObject(ITransaction transaction, String uri) {
|
||||
removeObjectInternal(cast(transaction), uri);
|
||||
}
|
||||
|
||||
protected abstract void removeObjectInternal(T transaction, String uri);
|
||||
|
||||
@Override
|
||||
public StoredObject getStoredObject(ITransaction transaction, String uri) {
|
||||
return getStoredObjectInternal(cast(transaction), uri);
|
||||
}
|
||||
|
||||
protected abstract StoredObject getStoredObjectInternal(T transaction, String uri);
|
||||
|
||||
}
|
@ -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<String> result = new ArrayList<>();
|
||||
if (file.isDirectory()) {
|
||||
String[] children = session.listFiles(file);
|
||||
for (final String child : children) {
|
||||
final String pseudonym = FilenameUtils.concat(pseudonymizedUri, child);
|
||||
final String cleartext = cryptor.uncoverPseudonym(pseudonym, accessor);
|
||||
if (cleartext != null) {
|
||||
result.add(FilenameUtils.getName(cleartext));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toArray(new String[result.size()]);
|
||||
} catch (XAApplicationException | InterruptedException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The pseudonyimzed URI for the given clear URI.
|
||||
*/
|
||||
public String pseudonymizedUri(final Session session, final String clearUri) throws IOException {
|
||||
final TransactionAwareFileAccess fileLoader = new FileLoader(session);
|
||||
return cryptor.createPseudonym(clearUri, fileLoader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a pseudonym.
|
||||
*/
|
||||
public void deletePseudonym(final Session session, final String pseudonymizedUri) throws IOException {
|
||||
final TransactionAwareFileAccess fileLoader = new FileLoader(session);
|
||||
cryptor.deletePseudonym(pseudonymizedUri, fileLoader);
|
||||
}
|
||||
|
||||
public InputStream decryptResource(Session session, String pseudonymized) throws IOException {
|
||||
final TransactionAwareFileAccess accessor = new FileLoader(session);
|
||||
return cryptor.decryptFile(pseudonymized, accessor);
|
||||
}
|
||||
|
||||
public long encryptResource(Session session, String pseudonymized, InputStream in) throws IOException {
|
||||
final TransactionAwareFileAccess accessor = new FileLoader(session);
|
||||
return cryptor.encryptFile(pseudonymized, in, accessor);
|
||||
}
|
||||
|
||||
|
||||
public long getDecryptedFileLength(Session session, String pseudonymized) throws IOException {
|
||||
final TransactionAwareFileAccess accessor = new FileLoader(session);
|
||||
return cryptor.getDecryptedContentLength(pseudonymized, accessor);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Transaction-aware implementation of MetadataLoading.
|
||||
*/
|
||||
private class FileLoader implements TransactionAwareFileAccess {
|
||||
|
||||
private final Session session;
|
||||
|
||||
private FileLoader(final Session session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream openFileForRead(Path path) throws IOException {
|
||||
try {
|
||||
final File file = path.toFile();
|
||||
if (!session.fileExists(file)) {
|
||||
session.createFile(file, false);
|
||||
}
|
||||
return new XAFileInputStreamWrapper(session.createXAFileInputStream(file));
|
||||
} catch (XAApplicationException | InterruptedException ex) {
|
||||
LOG.error("Failed to open resource for reading: " + path.toString(), ex);
|
||||
throw new IOException("Failed to open resource for reading: " + path.toString(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream openFileForWrite(Path path) throws IOException {
|
||||
try {
|
||||
final File file = path.toFile();
|
||||
if (!session.fileExists(file)) {
|
||||
session.createFile(file, false);
|
||||
} else {
|
||||
session.truncateFile(file, 0);
|
||||
}
|
||||
return new XAFileOutputStreamWrapper(session.createXAFileOutputStream(file, false));
|
||||
} catch (NoTransactionAssociatedException ex) {
|
||||
LOG.error("Session closed.", ex);
|
||||
throw new IllegalStateException("Session closed.", ex);
|
||||
} catch (XAApplicationException | InterruptedException ex) {
|
||||
LOG.error("Failed to open resource for writing: " + path.toString(), ex);
|
||||
throw new IOException("Failed to open resource for writing: " + path.toString(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path resolveUri(String uri) {
|
||||
return workDir.resolve(removeLeadingSlash(uri));
|
||||
}
|
||||
|
||||
private String removeLeadingSlash(String path) {
|
||||
if (path.length() == 0) {
|
||||
return path;
|
||||
} else if (path.charAt(0) == '/') {
|
||||
return path.substring(1);
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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<FsWebdavTransaction> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FsWebdavResourceHandler.class);
|
||||
private static final String XA_SYS_DIR_PREFIX = "oce-webdav";
|
||||
private static final Path XA_SYS_DIR;
|
||||
|
||||
static {
|
||||
final String tmpDirName = (String) System.getProperties().get("java.io.tmpdir");
|
||||
final Path tmpDirPath = FileSystems.getDefault().getPath(tmpDirName);
|
||||
try {
|
||||
XA_SYS_DIR = Files.createTempDirectory(tmpDirPath, XA_SYS_DIR_PREFIX);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Can't create tmp directory at " + tmpDirPath.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private final XAFileSystem xafs;
|
||||
private final String workingDirectory;
|
||||
private final FsWebdavCryptoAdapter cryptoAdapter;
|
||||
|
||||
public FsWebdavResourceHandler(final File root) {
|
||||
super(FsWebdavTransaction.class);
|
||||
this.workingDirectory = FilenameUtils.normalizeNoEndSeparator(root.getAbsolutePath());
|
||||
|
||||
final StandaloneFileSystemConfiguration configuration = new StandaloneFileSystemConfiguration(XA_SYS_DIR.toString(), "test");
|
||||
this.xafs = XAFileSystemProxy.bootNativeXAFileSystem(configuration);
|
||||
this.cryptoAdapter = new FsWebdavCryptoAdapter(this.workingDirectory);
|
||||
|
||||
try {
|
||||
this.xafs.waitForBootup(1000L);
|
||||
LOG.info("Started XADisk at " + XA_SYS_DIR.toString());
|
||||
|
||||
final Session session = xafs.createSessionForLocalTransaction();
|
||||
cryptoAdapter.initializeNewFolder(session, "/");
|
||||
session.commit();
|
||||
} catch (IOException | XAApplicationException | InterruptedException ex) {
|
||||
throw new IllegalStateException("Could not initialize I/O components.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private File getFileInWorkDir(final String relativeUri) {
|
||||
final String fullPath = this.workingDirectory.concat(relativeUri);
|
||||
return new File(FilenameUtils.normalize(fullPath));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
try {
|
||||
this.xafs.shutdown();
|
||||
FileUtils.deleteDirectory(XA_SYS_DIR.toFile());
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Failed to shutdown normally", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FsWebdavTransaction beginTransactionInternal(Principal principal) {
|
||||
final Session session = this.xafs.createSessionForLocalTransaction();
|
||||
LOG.trace("started transaction " + session);
|
||||
return new FsWebdavTransaction(principal, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkAuthenticationInternal(FsWebdavTransaction transaction) {
|
||||
// TODO Auto-generated method stub
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commitInternal(FsWebdavTransaction transaction) {
|
||||
try {
|
||||
transaction.getSession().commit();
|
||||
LOG.trace("committed transaction " + transaction.getSession());
|
||||
} catch (NoTransactionAssociatedException e) {
|
||||
throw new WebdavException("Error committing transaction " + transaction.getSession(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollbackInternal(FsWebdavTransaction transaction) {
|
||||
try {
|
||||
transaction.getSession().rollback();
|
||||
LOG.warn("rolled back transaction " + transaction.getSession());
|
||||
} catch (NoTransactionAssociatedException e) {
|
||||
throw new WebdavException("Error rolling back transaction " + transaction.getSession(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createFolderInternal(FsWebdavTransaction transaction, String folderUri) {
|
||||
try {
|
||||
cryptoAdapter.initializeNewFolder(transaction.getSession(), folderUri);
|
||||
} catch (IOException e) {
|
||||
throw new WebdavException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createResourceInternal(FsWebdavTransaction transaction, String resourceUri) {
|
||||
try {
|
||||
final String pseudonymized = cryptoAdapter.pseudonymizedUri(transaction.getSession(), resourceUri);
|
||||
final File file = getFileInWorkDir(pseudonymized);
|
||||
transaction.getSession().createFile(file, false);
|
||||
} catch (IOException | XAApplicationException | InterruptedException e) {
|
||||
throw new WebdavException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getResourceContentInternal(FsWebdavTransaction transaction, String resourceUri) {
|
||||
try {
|
||||
// Note: The requesting entity is in charge of closing the stream.
|
||||
final String pseudonymized = cryptoAdapter.pseudonymizedUri(transaction.getSession(), resourceUri);
|
||||
return cryptoAdapter.decryptResource(transaction.getSession(), pseudonymized);
|
||||
} catch (IOException e) {
|
||||
throw new WebdavException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long setResourceContentInternal(FsWebdavTransaction transaction, String resourceUri, InputStream in, String contentType, String characterEncoding) {
|
||||
try {
|
||||
final String pseudonymized = cryptoAdapter.pseudonymizedUri(transaction.getSession(), resourceUri);
|
||||
return cryptoAdapter.encryptResource(transaction.getSession(), pseudonymized, in);
|
||||
} catch (IOException e) {
|
||||
throw new WebdavException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getChildrenNamesInternal(FsWebdavTransaction transaction, String folderUri) {
|
||||
try {
|
||||
final String pseudonymized = cryptoAdapter.pseudonymizedUri(transaction.getSession(), folderUri);
|
||||
return cryptoAdapter.uncoveredChildrenNames(transaction.getSession(), pseudonymized);
|
||||
} catch (IOException e) {
|
||||
throw new WebdavException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getResourceLengthInternal(FsWebdavTransaction transaction, String uri) {
|
||||
try {
|
||||
final String pseudonymized = cryptoAdapter.pseudonymizedUri(transaction.getSession(), uri);
|
||||
return cryptoAdapter.getDecryptedFileLength(transaction.getSession(), pseudonymized);
|
||||
} catch (IOException e) {
|
||||
throw new WebdavException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeObjectInternal(FsWebdavTransaction transaction, String uri) {
|
||||
try {
|
||||
final String pseudonymized = cryptoAdapter.pseudonymizedUri(transaction.getSession(), uri);
|
||||
final File file = getFileInWorkDir(pseudonymized);
|
||||
deleteRecursively(transaction.getSession(), file);
|
||||
cryptoAdapter.deletePseudonym(transaction.getSession(), pseudonymized);
|
||||
} catch (IOException | XAApplicationException | InterruptedException e) {
|
||||
LOG.error("removeObject" + uri + " failed", e);
|
||||
throw new WebdavException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteRecursively(Session session, File file) throws XAApplicationException, InterruptedException {
|
||||
if (file.isDirectory()) {
|
||||
final String[] children = session.listFiles(file);
|
||||
for (final String childName : children) {
|
||||
final File childFile = new File(file, childName);
|
||||
deleteRecursively(session, childFile);
|
||||
}
|
||||
}
|
||||
session.deleteFile(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoredObject getStoredObjectInternal(FsWebdavTransaction transaction, String uri) {
|
||||
try {
|
||||
final String pseudonymized = cryptoAdapter.pseudonymizedUri(transaction.getSession(), uri);
|
||||
final File file = getFileInWorkDir(pseudonymized);
|
||||
if (transaction.getSession().fileExists(file)) {
|
||||
final StoredObject so = new StoredObject();
|
||||
so.setFolder(file.isDirectory());
|
||||
so.setLastModified(new Date(file.lastModified()));
|
||||
so.setCreationDate(new Date(file.lastModified()));
|
||||
if (!file.isDirectory()) {
|
||||
so.setResourceLength(transaction.getSession().getFileLength(file));
|
||||
}
|
||||
return so;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (IOException | XAApplicationException | InterruptedException e) {
|
||||
throw new WebdavException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -1,17 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright (c) 2014 Sebastian Stenzel
|
||||
This file is licensed under the terms of the MIT license.
|
||||
See the LICENSE.txt file for more info.
|
||||
|
||||
Contributors:
|
||||
Sebastian Stenzel - initial API and implementation
|
||||
-->
|
||||
<!-- Copyright (c) 2014 Sebastian Stenzel This file is licensed under the terms of the MIT license. See the LICENSE.txt file for more info. Contributors: Sebastian Stenzel - initial API and implementation -->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>de.sebastianstenzel.oce</groupId>
|
||||
<artifactId>oce-main</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>Open Cloud Encryptor</name>
|
||||
<organization>
|
||||
@ -29,6 +22,7 @@
|
||||
<commons-io.version>2.4</commons-io.version>
|
||||
<commons-collections.version>4.0</commons-collections.version>
|
||||
<commons-lang.version>3.1</commons-lang.version>
|
||||
<commons-codec.version>1.9</commons-codec.version>
|
||||
</properties>
|
||||
|
||||
<developers>
|
||||
@ -73,12 +67,17 @@
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>${commons-lang.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<version>${commons-codec.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>2.3.0</version>
|
||||
<version>2.4.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JUnit -->
|
||||
@ -92,9 +91,10 @@
|
||||
</dependencyManagement>
|
||||
|
||||
<modules>
|
||||
<module>oce-webdav</module>
|
||||
<module>oce-crypto-api</module>
|
||||
<module>oce-crypto-aes</module>
|
||||
<module>oce-core</module>
|
||||
<module>oce-ui</module>
|
||||
<module>oce-crypto</module>
|
||||
</modules>
|
||||
|
||||
<build>
|
||||
|
Loading…
Reference in New Issue
Block a user