- 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:
Sebastian Stenzel 2014-09-27 23:49:41 +02:00
parent 318eb24d64
commit 9988ec6c0b
55 changed files with 2267 additions and 2157 deletions

View File

@ -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
View File

@ -0,0 +1 @@
/target/

View File

@ -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>

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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.
}
}

View File

@ -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());
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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);
}
};
}
}

View File

@ -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;
}

View File

@ -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 + "}");
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -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.");
}
}

View File

@ -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
View File

@ -0,0 +1 @@
/target/

View 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>

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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.
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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));
}
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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"));

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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>