diff --git a/main/core/pom.xml b/main/core/pom.xml index c7b66fa5a..85fd9f6a1 100644 --- a/main/core/pom.xml +++ b/main/core/pom.xml @@ -64,9 +64,5 @@ org.apache.commons commons-lang3 - - org.apache.commons - commons-collections4 - diff --git a/main/core/src/main/java/org/cryptomator/webdav/exceptions/IORuntimeException.java b/main/core/src/main/java/org/cryptomator/webdav/exceptions/IORuntimeException.java index 210524596..df5e57c77 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/exceptions/IORuntimeException.java +++ b/main/core/src/main/java/org/cryptomator/webdav/exceptions/IORuntimeException.java @@ -14,8 +14,8 @@ public class IORuntimeException extends RuntimeException { private static final long serialVersionUID = -4713080133052143303L; - public IORuntimeException(IOException ioException) { - super(ioException); + public IORuntimeException(IOException cause) { + super(cause); } @Override diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/AbstractEncryptedNode.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/AbstractEncryptedNode.java index f760c445b..1715ed810 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/AbstractEncryptedNode.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/AbstractEncryptedNode.java @@ -9,11 +9,9 @@ package org.cryptomator.webdav.jackrabbit; import java.io.IOException; -import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.FileTime; import java.util.List; @@ -21,7 +19,6 @@ 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; @@ -46,14 +43,14 @@ 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 CryptoResourceFactory factory; + protected final CryptoLocator 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) { + protected AbstractEncryptedNode(CryptoResourceFactory factory, CryptoLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) { this.factory = factory; this.locator = locator; this.session = session; @@ -63,6 +60,8 @@ abstract class AbstractEncryptedNode implements DavResource { this.determineProperties(); } + protected abstract Path getPhysicalPath(); + @Override public String getComplianceClass() { return DAV_COMPLIANCE_CLASSES; @@ -75,8 +74,7 @@ abstract class AbstractEncryptedNode implements DavResource { @Override public boolean exists() { - final Path path = ResourcePathUtils.getPhysicalPath(this); - return Files.exists(path); + return Files.exists(getPhysicalPath()); } @Override @@ -91,7 +89,7 @@ abstract class AbstractEncryptedNode implements DavResource { } @Override - public DavResourceLocator getLocator() { + public CryptoLocator getLocator() { return locator; } @@ -107,9 +105,8 @@ abstract class AbstractEncryptedNode implements DavResource { @Override public long getModificationTime() { - final Path path = ResourcePathUtils.getPhysicalPath(this); try { - return Files.getLastModifiedTime(path).toMillis(); + return Files.getLastModifiedTime(getPhysicalPath()).toMillis(); } catch (IOException e) { return -1; } @@ -139,7 +136,7 @@ abstract class AbstractEncryptedNode implements DavResource { LOG.info("Set property {}", property.getName()); try { - final Path path = ResourcePathUtils.getPhysicalPath(this); + final Path path = getPhysicalPath(); if (DavPropertyName.CREATIONDATE.equals(property.getName()) && property.getValue() instanceof String) { final String createDateStr = (String) property.getValue(); final FileTime createTime = FileTimeUtils.fromRfc1123String(createDateStr); @@ -196,49 +193,37 @@ abstract class AbstractEncryptedNode implements DavResource { } @Override - public void move(DavResource dest) throws DavException { - final Path src = ResourcePathUtils.getPhysicalPath(this); - final Path dst = ResourcePathUtils.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: + public final void move(DavResource dest) throws DavException { + if (dest instanceof AbstractEncryptedNode) { try { - Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); - } catch (AtomicMoveNotSupportedException e) { - Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING); + this.move((AbstractEncryptedNode) dest); + } catch (IOException e) { + LOG.error("Error moving file from " + this.getResourcePath() + " to " + dest.getResourcePath()); + throw new IORuntimeException(e); } - } catch (IOException e) { - LOG.error("Error moving file from " + src.toString() + " to " + dst.toString()); - throw new IORuntimeException(e); + } else { + throw new IllegalArgumentException("Unsupported resource type: " + dest.getClass().getName()); } } + public abstract void move(AbstractEncryptedNode dest) throws DavException, IOException; + @Override - public void copy(DavResource dest, boolean shallow) throws DavException { - final Path src = ResourcePathUtils.getPhysicalPath(this); - final Path dst = ResourcePathUtils.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: + public final void copy(DavResource dest, boolean shallow) throws DavException { + if (dest instanceof AbstractEncryptedNode) { 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); + this.copy((AbstractEncryptedNode) dest, shallow); + } catch (IOException e) { + LOG.error("Error copying file from " + this.getResourcePath() + " to " + dest.getResourcePath()); + throw new IORuntimeException(e); } - } catch (IOException e) { - LOG.error("Error copying file from " + src.toString() + " to " + dst.toString()); - throw new IORuntimeException(e); + } else { + throw new IllegalArgumentException("Unsupported resource type: " + dest.getClass().getName()); } } + public abstract void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException; + @Override public boolean isLockable(Type type, Scope scope) { return true; @@ -281,7 +266,7 @@ abstract class AbstractEncryptedNode implements DavResource { } @Override - public DavResourceFactory getFactory() { + public CryptoResourceFactory getFactory() { return factory; } diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/BidiLRUMap.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/BidiLRUMap.java deleted file mode 100644 index dc8761d1f..000000000 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/BidiLRUMap.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.cryptomator.webdav.jackrabbit; - -import java.util.Map; - -import org.apache.commons.collections4.BidiMap; -import org.apache.commons.collections4.bidimap.AbstractDualBidiMap; -import org.apache.commons.collections4.map.LRUMap; - -final class BidiLRUMap extends AbstractDualBidiMap { - - BidiLRUMap(int maxSize) { - super(new LRUMap(maxSize), new LRUMap(maxSize)); - } - - protected BidiLRUMap(final Map normalMap, final Map reverseMap, final BidiMap inverseBidiMap) { - super(normalMap, reverseMap, inverseBidiMap); - } - - @Override - protected BidiMap createBidiMap(Map normalMap, Map reverseMap, BidiMap inverseMap) { - return new BidiLRUMap(normalMap, reverseMap, inverseMap); - } - -} diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoLocator.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoLocator.java new file mode 100644 index 000000000..12cf9a761 --- /dev/null +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoLocator.java @@ -0,0 +1,166 @@ +package org.cryptomator.webdav.jackrabbit; + +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Path; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.util.EncodeUtil; +import org.apache.logging.log4j.util.Strings; +import org.cryptomator.crypto.Cryptor; +import org.cryptomator.webdav.exceptions.IORuntimeException; + +class CryptoLocator implements DavResourceLocator { + + private final CryptoLocatorFactory factory; + private final Cryptor cryptor; + private final Path rootPath; + private final String prefix; + private final String resourcePath; + + public CryptoLocator(CryptoLocatorFactory factory, Cryptor cryptor, Path rootPath, String prefix, String resourcePath) { + this.factory = factory; + this.cryptor = cryptor; + this.rootPath = rootPath; + this.prefix = prefix; + this.resourcePath = FilenameUtils.normalizeNoEndSeparator(resourcePath, true); + } + + /* path variants */ + + /** + * Returns the decrypted path without any trailing slash. + * + * @see #getHref(boolean) + * @return Plaintext resource path. + */ + @Override + public String getResourcePath() { + return resourcePath; + } + + /** + * Returns the decrypted path and adds URL-encoding. + * + * @param isCollection If true, a trailing slash will be appended. + * @see #getResourcePath() + * @return URL-encoded plaintext resource path. + */ + @Override + public String getHref(boolean isCollection) { + final String encodedResourcePath = EncodeUtil.escapePath(getResourcePath()); + final String href = getPrefix().concat(encodedResourcePath); + assert !href.endsWith("/"); + if (isCollection) { + return href.concat("/"); + } else { + return href; + } + } + + /** + * Returns the encrypted, absolute path on the local filesystem. + * + * @return Absolute, encrypted path as string (use {@link #getEncryptedFilePath()} for {@link Path}s). + */ + @Override + public String getRepositoryPath() { + if (isRootLocation()) { + return getDirectoryPath(); + } + try { + final String plaintextPath = getResourcePath(); + final String plaintextDir = FilenameUtils.getPathNoEndSeparator(plaintextPath); + final String plaintextFilename = FilenameUtils.getName(plaintextPath); + final String ciphertextDir = cryptor.encryptDirectoryPath(plaintextDir, FileSystems.getDefault().getSeparator()); + final String ciphertextFilename = cryptor.encryptFilename(plaintextFilename, factory); + final String ciphertextPath = ciphertextDir + FileSystems.getDefault().getSeparator() + ciphertextFilename; + return rootPath.resolve(ciphertextPath).toString(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * Returns the encrypted, absolute path on the local filesystem to the directory represented by this locator. + * + * @return Absolute, encrypted path as string (use {@link #getEncryptedDirectoryPath()} for {@link Path}s). + */ + public String getDirectoryPath() { + final String ciphertextPath = cryptor.encryptDirectoryPath(getResourcePath(), FileSystems.getDefault().getSeparator()); + return rootPath.resolve(ciphertextPath).toString(); + } + + public Path getEncryptedFilePath() { + return FileSystems.getDefault().getPath(getRepositoryPath()); + } + + public Path getEncryptedDirectoryPath() { + return FileSystems.getDefault().getPath(getDirectoryPath()); + } + + /* other stuff */ + + @Override + public String getPrefix() { + return prefix; + } + + @Override + public String getWorkspacePath() { + return isRootLocation() ? null : ""; + } + + @Override + public String getWorkspaceName() { + return getPrefix(); + } + + @Override + public boolean isSameWorkspace(DavResourceLocator locator) { + return (locator == null) ? false : isSameWorkspace(locator.getWorkspaceName()); + } + + @Override + public boolean isSameWorkspace(String workspaceName) { + return getWorkspaceName().equals(workspaceName); + } + + @Override + public boolean isRootLocation() { + return Strings.isEmpty(getResourcePath()); + } + + @Override + public CryptoLocatorFactory getFactory() { + return factory; + } + + /* hashcode and equals */ + + @Override + public int hashCode() { + final HashCodeBuilder builder = new HashCodeBuilder(); + builder.append(prefix); + builder.append(resourcePath); + return builder.toHashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof CryptoLocator) { + final CryptoLocator other = (CryptoLocator) obj; + final EqualsBuilder builder = new EqualsBuilder(); + builder.append(this.factory, other.factory); + builder.append(this.prefix, other.prefix); + builder.append(this.resourcePath, other.resourcePath); + return builder.isEquals(); + } else { + return false; + } + } + +} diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoLocatorFactory.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoLocatorFactory.java new file mode 100644 index 000000000..7e910de80 --- /dev/null +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoLocatorFactory.java @@ -0,0 +1,102 @@ +package org.cryptomator.webdav.jackrabbit; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +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.FilenameUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.jackrabbit.webdav.DavLocatorFactory; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.util.EncodeUtil; +import org.cryptomator.crypto.Cryptor; +import org.cryptomator.crypto.CryptorMetadataSupport; +import org.cryptomator.crypto.exceptions.DecryptFailedException; +import org.cryptomator.webdav.exceptions.DecryptFailedRuntimeException; +import org.cryptomator.webdav.exceptions.IORuntimeException; + +class CryptoLocatorFactory implements DavLocatorFactory, CryptorMetadataSupport { + + private final Path dataRoot; + private final Path metadataRoot; + private final Cryptor cryptor; + + CryptoLocatorFactory(String fsRoot, Cryptor cryptor) { + this.dataRoot = FileSystems.getDefault().getPath(fsRoot).resolve("d"); + this.metadataRoot = FileSystems.getDefault().getPath(fsRoot).resolve("m"); + this.cryptor = cryptor; + } + + @Override + public CryptoLocator createResourceLocator(String prefix, String href) { + final String fullPrefix = prefix.endsWith("/") ? prefix : prefix + "/"; + final String relativeHref = StringUtils.removeStart(href, fullPrefix); + + final String resourcePath = EncodeUtil.unescape(StringUtils.removeStart(relativeHref, "/")); + return new CryptoLocator(this, cryptor, dataRoot, fullPrefix, resourcePath); + } + + /** + * @throws DecryptFailedRuntimeException, which should be a checked exception, but Jackrabbit doesn't allow that. + */ + @Override + public CryptoLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) { + if (!isResourcePath) { + throw new UnsupportedOperationException("Can not decrypt " + path + " without knowing plaintext parent path."); + } + final String fullPrefix = prefix.endsWith("/") ? prefix : prefix + "/"; + return new CryptoLocator(this, cryptor, dataRoot, fullPrefix, path); + } + + @Override + public CryptoLocator createResourceLocator(String prefix, String workspacePath, String resourcePath) { + try { + return createResourceLocator(prefix, workspacePath, resourcePath, true); + } catch (DecryptFailedRuntimeException e) { + throw new IllegalStateException("Tried to decrypt resourcePath. Only repositoryPaths can be encrypted.", e); + } + } + + public DavResourceLocator createSubresourceLocator(CryptoLocator parentResource, String ciphertextChildName) { + try { + final String plaintextFilename = cryptor.decryptFilename(ciphertextChildName, this); + final String plaintextPath = FilenameUtils.concat(parentResource.getResourcePath(), plaintextFilename); + return createResourceLocator(parentResource.getPrefix(), parentResource.getWorkspacePath(), plaintextPath); + } catch (IOException e) { + throw new IORuntimeException(e); + } catch (DecryptFailedException e) { + throw new DecryptFailedRuntimeException(e); + } + } + + /* metadata storage */ + + @Override + public void writeMetadata(String metadataGroup, byte[] encryptedMetadata) throws IOException { + final Path metadataDir = metadataRoot.resolve(metadataGroup.substring(0, 2)); + Files.createDirectories(metadataDir); + final Path metadataFile = metadataDir.resolve(metadataGroup.substring(2)); + try (final FileChannel c = FileChannel.open(metadataFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC); final FileLock lock = c.lock()) { + c.write(ByteBuffer.wrap(encryptedMetadata)); + } + } + + @Override + public byte[] readMetadata(String metadataGroup) throws IOException { + final Path metadataDir = metadataRoot.resolve(metadataGroup.substring(0, 2)); + final Path metadataFile = metadataDir.resolve(metadataGroup.substring(2)); + if (!Files.isReadable(metadataFile)) { + return null; + } + try (final FileChannel c = FileChannel.open(metadataFile, StandardOpenOption.READ, StandardOpenOption.DSYNC); final FileLock lock = c.lock(0L, Long.MAX_VALUE, true)) { + final ByteBuffer buffer = ByteBuffer.allocate((int) c.size()); + c.read(buffer); + return buffer.array(); + } + } +} diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoResourceFactory.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoResourceFactory.java new file mode 100644 index 000000000..6a965f5e4 --- /dev/null +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoResourceFactory.java @@ -0,0 +1,99 @@ +package org.cryptomator.webdav.jackrabbit; + +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.ExecutorService; + +import org.apache.commons.httpclient.HttpStatus; +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 org.cryptomator.crypto.Cryptor; +import org.eclipse.jetty.http.HttpHeader; + +public class CryptoResourceFactory implements DavResourceFactory { + + private final LockManager lockManager = new SimpleLockManager(); + private final Cryptor cryptor; + private final CryptoWarningHandler cryptoWarningHandler; + private final ExecutorService backgroundTaskExecutor; + + CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, ExecutorService backgroundTaskExecutor) { + this.cryptor = cryptor; + this.cryptoWarningHandler = cryptoWarningHandler; + this.backgroundTaskExecutor = backgroundTaskExecutor; + } + + @Override + public final DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException { + if (locator instanceof CryptoLocator) { + return createResource((CryptoLocator) locator, request, response); + } else { + throw new IllegalArgumentException("Unsupported resource locator of type " + locator.getClass().getName()); + } + } + + @Override + public final DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException { + if (locator instanceof CryptoLocator) { + return createResource((CryptoLocator) locator, session); + } else { + throw new IllegalArgumentException("Unsupported resource locator of type " + locator.getClass().getName()); + } + } + + private DavResource createResource(CryptoLocator locator, DavServletRequest request, DavServletResponse response) throws DavException { + final Path filepath = FileSystems.getDefault().getPath(locator.getRepositoryPath()); + final Path dirpath = FileSystems.getDefault().getPath(locator.getDirectoryPath()); + final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString()); + + if (Files.isDirectory(dirpath) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) { + return createDirectory(locator, request.getDavSession()); + } else if (Files.isRegularFile(filepath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) { + response.setStatus(HttpStatus.SC_PARTIAL_CONTENT); + return createFilePart(locator, request.getDavSession(), request); + } else if (Files.isRegularFile(filepath) || DavMethods.METHOD_PUT.equals(request.getMethod())) { + return createFile(locator, request.getDavSession()); + } else { + return createNonExisting(locator, request.getDavSession()); + } + } + + private DavResource createResource(CryptoLocator locator, DavSession session) throws DavException { + final Path filepath = FileSystems.getDefault().getPath(locator.getRepositoryPath()); + final Path dirpath = FileSystems.getDefault().getPath(locator.getDirectoryPath()); + + if (Files.isDirectory(dirpath)) { + return createDirectory(locator, session); + } else if (Files.isRegularFile(filepath)) { + return createFile(locator, session); + } else { + return createNonExisting(locator, session); + } + } + + private EncryptedFile createFilePart(CryptoLocator locator, DavSession session, DavServletRequest request) { + return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, cryptoWarningHandler, backgroundTaskExecutor); + } + + private EncryptedFile createFile(CryptoLocator locator, DavSession session) { + return new EncryptedFile(this, locator, session, lockManager, cryptor, cryptoWarningHandler); + } + + private EncryptedDir createDirectory(CryptoLocator locator, DavSession session) { + return new EncryptedDir(this, locator, session, lockManager, cryptor); + } + + private NonExistingNode createNonExisting(CryptoLocator locator, DavSession session) { + return new NonExistingNode(this, locator, session, lockManager, cryptor); + } + +} diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/DavLocatorFactoryImpl.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/DavLocatorFactoryImpl.java deleted file mode 100644 index 71ca8e476..000000000 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/DavLocatorFactoryImpl.java +++ /dev/null @@ -1,242 +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 org.cryptomator.webdav.jackrabbit; - -import java.io.IOException; -import java.nio.file.FileSystems; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; - -import org.apache.commons.collections4.BidiMap; -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.builder.EqualsBuilder; -import org.apache.commons.lang3.builder.HashCodeBuilder; -import org.apache.jackrabbit.webdav.DavLocatorFactory; -import org.apache.jackrabbit.webdav.DavResourceLocator; -import org.apache.jackrabbit.webdav.util.EncodeUtil; -import org.cryptomator.crypto.Cryptor; -import org.cryptomator.crypto.CryptorIOSupport; -import org.cryptomator.crypto.SensitiveDataSwipeListener; -import org.cryptomator.crypto.exceptions.DecryptFailedException; -import org.cryptomator.webdav.exceptions.DecryptFailedRuntimeException; - -class DavLocatorFactoryImpl implements DavLocatorFactory, SensitiveDataSwipeListener, CryptorIOSupport { - - private static final int MAX_CACHED_PATHS = 10000; - private final Path fsRoot; - private final Cryptor cryptor; - private final BidiMap pathCache = new BidiLRUMap<>(MAX_CACHED_PATHS); // - - DavLocatorFactoryImpl(String fsRoot, Cryptor cryptor) { - this.fsRoot = FileSystems.getDefault().getPath(fsRoot); - this.cryptor = cryptor; - cryptor.addSensitiveDataSwipeListener(this); - } - - /* DavLocatorFactory */ - - @Override - public DavResourceLocator createResourceLocator(String prefix, String href) { - final String fullPrefix = prefix.endsWith("/") ? prefix : prefix + "/"; - final String relativeHref = StringUtils.removeStart(href, fullPrefix); - - final String resourcePath = EncodeUtil.unescape(StringUtils.removeStart(relativeHref, "/")); - return new DavResourceLocatorImpl(fullPrefix, resourcePath); - } - - /** - * @throws DecryptFailedRuntimeException, which should a checked exception, but Jackrabbit doesn't allow that. - */ - @Override - public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) { - final String fullPrefix = prefix.endsWith("/") ? prefix : prefix + "/"; - - try { - final String resourcePath = (isResourcePath) ? path : getResourcePath(path); - return new DavResourceLocatorImpl(fullPrefix, resourcePath); - } catch (DecryptFailedException e) { - throw new DecryptFailedRuntimeException(e); - } - } - - @Override - public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String resourcePath) { - try { - return createResourceLocator(prefix, workspacePath, resourcePath, true); - } catch (DecryptFailedRuntimeException e) { - throw new IllegalStateException("Tried to decrypt resourcePath. Only repositoryPaths can be encrypted.", e); - } - } - - /* Encryption/Decryption */ - - /** - * @return Encrypted absolute paths on the file system. - */ - private String getRepositoryPath(String resourcePath) { - String encryptedPath = pathCache.get(resourcePath); - if (encryptedPath == null) { - encryptedPath = encryptRepositoryPath(resourcePath); - pathCache.put(resourcePath, encryptedPath); - } - return encryptedPath; - } - - private String encryptRepositoryPath(String resourcePath) { - if (resourcePath == null) { - return fsRoot.toString(); - } - final String encryptedRepoPath = cryptor.encryptPath(resourcePath, FileSystems.getDefault().getSeparator().charAt(0), '/', this); - return fsRoot.resolve(encryptedRepoPath).toString(); - } - - /** - * @return Decrypted path for use in URIs. - */ - private String getResourcePath(String repositoryPath) throws DecryptFailedException { - String decryptedPath = pathCache.getKey(repositoryPath); - if (decryptedPath == null) { - decryptedPath = decryptResourcePath(repositoryPath); - pathCache.put(decryptedPath, repositoryPath); - } - return decryptedPath; - } - - private String decryptResourcePath(String repositoryPath) throws DecryptFailedException { - 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), '/', this); - return resourcePath; - } - } - - /* CryptorIOSupport */ - - @Override - public void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) throws IOException { - final Path metaDataFile = fsRoot.resolve(encryptedPath); - Files.write(metaDataFile, encryptedMetadata, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC); - } - - @Override - public byte[] readPathSpecificMetadata(String encryptedPath) throws IOException { - final Path metaDataFile = fsRoot.resolve(encryptedPath); - if (!Files.isReadable(metaDataFile)) { - return null; - } else { - return Files.readAllBytes(metaDataFile); - } - } - - /* SensitiveDataSwipeListener */ - - @Override - public void swipeSensitiveData() { - pathCache.clear(); - } - - /* Locator */ - - private class DavResourceLocatorImpl implements DavResourceLocator { - - private final String prefix; - private final String resourcePath; - - private DavResourceLocatorImpl(String prefix, String resourcePath) { - this.prefix = prefix; - this.resourcePath = FilenameUtils.normalizeNoEndSeparator(resourcePath, true); - } - - @Override - public String getPrefix() { - return prefix; - } - - @Override - public String getResourcePath() { - return resourcePath; - } - - @Override - public String getWorkspacePath() { - return isRootLocation() ? null : ""; - } - - @Override - public String getWorkspaceName() { - return getPrefix(); - } - - @Override - public boolean isSameWorkspace(DavResourceLocator locator) { - return (locator == null) ? false : isSameWorkspace(locator.getWorkspaceName()); - } - - @Override - public boolean isSameWorkspace(String workspaceName) { - return getWorkspaceName().equals(workspaceName); - } - - @Override - public String getHref(boolean isCollection) { - final String encodedResourcePath = EncodeUtil.escapePath(getResourcePath()); - final String href = getPrefix().concat(encodedResourcePath); - if (isCollection && !href.endsWith("/")) { - return href.concat("/"); - } else if (!isCollection && href.endsWith("/")) { - return href.substring(0, href.length() - 1); - } else { - return href; - } - } - - @Override - public boolean isRootLocation() { - return getResourcePath() == null; - } - - @Override - public DavLocatorFactory getFactory() { - return DavLocatorFactoryImpl.this; - } - - @Override - public String getRepositoryPath() { - return DavLocatorFactoryImpl.this.getRepositoryPath(getResourcePath()); - } - - @Override - public int hashCode() { - final HashCodeBuilder builder = new HashCodeBuilder(); - builder.append(prefix); - builder.append(resourcePath); - return builder.toHashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof DavResourceLocatorImpl) { - final DavResourceLocatorImpl other = (DavResourceLocatorImpl) obj; - final EqualsBuilder builder = new EqualsBuilder(); - builder.append(this.prefix, other.prefix); - builder.append(this.resourcePath, other.resourcePath); - return builder.isEquals(); - } else { - return false; - } - } - - } - -} diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/DavResourceFactoryImpl.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/DavResourceFactoryImpl.java deleted file mode 100644 index fac7e9072..000000000 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/DavResourceFactoryImpl.java +++ /dev/null @@ -1,88 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2014 Sebastian Stenzel - * This file is licensed under the terms of the MIT license. - * See the LICENSE.txt file for more info. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - ******************************************************************************/ -package org.cryptomator.webdav.jackrabbit; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.concurrent.ExecutorService; - -import org.apache.commons.httpclient.HttpStatus; -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 org.cryptomator.crypto.Cryptor; -import org.eclipse.jetty.http.HttpHeader; - -class DavResourceFactoryImpl implements DavResourceFactory { - - private final LockManager lockManager = new SimpleLockManager(); - private final Cryptor cryptor; - private final CryptoWarningHandler cryptoWarningHandler; - private final ExecutorService backgroundTaskExecutor; - - DavResourceFactoryImpl(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, ExecutorService backgroundTaskExecutor) { - this.cryptor = cryptor; - this.cryptoWarningHandler = cryptoWarningHandler; - this.backgroundTaskExecutor = backgroundTaskExecutor; - } - - @Override - public DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException { - final Path path = ResourcePathUtils.getPhysicalPath(locator); - final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString()); - - if (Files.isRegularFile(path) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) { - response.setStatus(HttpStatus.SC_PARTIAL_CONTENT); - return createFilePart(locator, request.getDavSession(), request); - } else if (Files.isRegularFile(path) || DavMethods.METHOD_PUT.equals(request.getMethod())) { - return createFile(locator, request.getDavSession()); - } else if (Files.isDirectory(path) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) { - return createDirectory(locator, request.getDavSession()); - } else { - return createNonExisting(locator, request.getDavSession()); - } - } - - @Override - public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException { - final Path path = ResourcePathUtils.getPhysicalPath(locator); - - if (path != null && Files.isRegularFile(path)) { - return createFile(locator, session); - } else if (path != null && Files.isDirectory(path)) { - return createDirectory(locator, session); - } else { - return createNonExisting(locator, session); - } - } - - private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, DavServletRequest request) { - return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, cryptoWarningHandler, backgroundTaskExecutor); - } - - private EncryptedFile createFile(DavResourceLocator locator, DavSession session) { - return new EncryptedFile(this, locator, session, lockManager, cryptor, cryptoWarningHandler); - } - - private EncryptedDir createDirectory(DavResourceLocator locator, DavSession session) { - return new EncryptedDir(this, locator, session, lockManager, cryptor); - } - - private NonExistingNode createNonExisting(DavResourceLocator locator, DavSession session) { - return new NonExistingNode(this, locator, session, lockManager, cryptor); - } - -} diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedDir.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedDir.java index 925953123..aec5a60e8 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedDir.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedDir.java @@ -10,11 +10,11 @@ package org.cryptomator.webdav.jackrabbit; import java.io.IOException; import java.nio.channels.SeekableByteChannel; +import java.nio.file.AtomicMoveNotSupportedException; 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.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; @@ -23,7 +23,6 @@ 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; @@ -48,28 +47,55 @@ 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) { + public EncryptedDir(CryptoResourceFactory factory, CryptoLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) { super(factory, locator, session, lockManager, cryptor); } + @Override + protected Path getPhysicalPath() { + return locator.getEncryptedDirectoryPath(); + } + @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); + public boolean exists() { + return Files.isDirectory(locator.getEncryptedDirectoryPath()); + } + + @Override + public long getModificationTime() { + try { + return Files.getLastModifiedTime(locator.getEncryptedDirectoryPath()).toMillis(); + } catch (IOException e) { + return -1; } } - private void addMemberDir(DavResource resource, InputContext inputContext) throws DavException { - final Path childPath = ResourcePathUtils.getPhysicalPath(resource); + @Override + public void addMember(DavResource resource, InputContext inputContext) throws DavException { + if (resource instanceof AbstractEncryptedNode) { + addMember((AbstractEncryptedNode) resource, inputContext); + } else { + throw new IllegalArgumentException("Unsupported resource type: " + resource.getClass().getName()); + } + } + + private void addMember(AbstractEncryptedNode childResource, InputContext inputContext) throws DavException { + if (childResource.isCollection()) { + this.addMemberDir(childResource.getLocator(), inputContext); + } else { + this.addMemberFile(childResource.getLocator(), inputContext); + } + } + + private void addMemberDir(CryptoLocator childLocator, InputContext inputContext) throws DavException { try { - Files.createDirectories(childPath); + Files.createDirectories(childLocator.getEncryptedFilePath()); + Files.createDirectories(childLocator.getEncryptedDirectoryPath()); } catch (SecurityException e) { throw new DavException(DavServletResponse.SC_FORBIDDEN, e); } catch (IOException e) { @@ -78,9 +104,8 @@ class EncryptedDir extends AbstractEncryptedNode { } } - private void addMemberFile(DavResource resource, InputContext inputContext) throws DavException { - final Path childPath = ResourcePathUtils.getPhysicalPath(resource); - try (final SeekableByteChannel channel = Files.newByteChannel(childPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { + private void addMemberFile(CryptoLocator childLocator, InputContext inputContext) throws DavException { + try (final SeekableByteChannel channel = Files.newByteChannel(childLocator.getEncryptedFilePath(), StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { cryptor.encryptFile(inputContext.getInputStream(), channel); } catch (SecurityException e) { throw new DavException(DavServletResponse.SC_FORBIDDEN, e); @@ -100,14 +125,15 @@ class EncryptedDir extends AbstractEncryptedNode { @Override public DavResourceIterator getMembers() { - final Path dir = ResourcePathUtils.getPhysicalPath(this); try { - final DirectoryStream directoryStream = Files.newDirectoryStream(dir, cryptor.getPayloadFilesFilter()); + final DirectoryStream directoryStream = Files.newDirectoryStream(locator.getEncryptedDirectoryPath(), cryptor.getPayloadFilesFilter()); final List result = new ArrayList<>(); for (final Path childPath : directoryStream) { try { - final DavResourceLocator childLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), childPath.toString(), false); + final DavResourceLocator childLocator = locator.getFactory().createSubresourceLocator(locator, childPath.getFileName().toString()); + // final DavResourceLocator childLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), + // locator.getWorkspacePath(), childPath.toString(), false); final DavResource resource = factory.createResource(childLocator, session); result.add(resource); } catch (DecryptFailedRuntimeException e) { @@ -126,19 +152,80 @@ class EncryptedDir extends AbstractEncryptedNode { } @Override - public void removeMember(DavResource member) throws DavException { - final Path memberPath = ResourcePathUtils.getPhysicalPath(member); + public void removeMember(DavResource member) { + if (member instanceof AbstractEncryptedNode) { + removeMember((AbstractEncryptedNode) member); + } else { + throw new IllegalArgumentException("Unsupported resource type: " + member.getClass().getName()); + } + } + + private void removeMember(AbstractEncryptedNode member) { try { - if (Files.exists(memberPath)) { - Files.walkFileTree(memberPath, new DeletingFileVisitor()); + if (member.isCollection()) { + member.getMembers().forEachRemaining(m -> securelyRemoveMemberOfCollection(member, m)); + Files.deleteIfExists(member.getLocator().getEncryptedDirectoryPath()); } - } catch (SecurityException e) { - throw new DavException(DavServletResponse.SC_FORBIDDEN, e); + Files.deleteIfExists(member.getLocator().getEncryptedFilePath()); } catch (IOException e) { throw new IORuntimeException(e); } } + private void securelyRemoveMemberOfCollection(DavResource collection, DavResource member) { + try { + collection.removeMember(member); + } catch (DavException e) { + throw new IllegalStateException("DavException should not be thrown by collections of type EncryptedDir. Collections is of type " + collection.getClass().getName()); + } + } + + @Override + public void move(AbstractEncryptedNode dest) throws DavException, IOException { + final Path srcDir = this.locator.getEncryptedDirectoryPath(); + final Path dstDir = dest.locator.getEncryptedDirectoryPath(); + final Path srcFile = this.locator.getEncryptedFilePath(); + final Path dstFile = dest.locator.getEncryptedFilePath(); + + // check for conflicts: + if (Files.exists(dstDir) && Files.getLastModifiedTime(dstDir).toMillis() > Files.getLastModifiedTime(dstDir).toMillis()) { + throw new DavException(DavServletResponse.SC_CONFLICT, "Directory at destination already exists: " + dstDir.toString()); + } + + // move: + Files.createDirectories(dstDir); + try { + Files.move(srcDir, dstDir, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + Files.move(srcFile, dstFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.move(srcDir, dstDir, StandardCopyOption.REPLACE_EXISTING); + Files.move(srcFile, dstFile, StandardCopyOption.REPLACE_EXISTING); + } + } + + @Override + public void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException { + final Path srcDir = this.locator.getEncryptedDirectoryPath(); + final Path dstDir = dest.locator.getEncryptedDirectoryPath(); + final Path srcFile = this.locator.getEncryptedFilePath(); + final Path dstFile = dest.locator.getEncryptedFilePath(); + + // check for conflicts: + if (Files.exists(dstDir) && Files.getLastModifiedTime(dstDir).toMillis() > Files.getLastModifiedTime(dstDir).toMillis()) { + throw new DavException(DavServletResponse.SC_CONFLICT, "Directory at destination already exists: " + dstDir.toString()); + } + + // copy: + Files.createDirectories(dstDir); + try { + Files.copy(srcDir, dstDir, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + Files.copy(srcFile, dstFile, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.copy(srcDir, dstDir, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); + Files.copy(srcFile, dstFile, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); + } + } + @Override public void spool(OutputContext outputContext) throws IOException { // do nothing @@ -146,7 +233,7 @@ class EncryptedDir extends AbstractEncryptedNode { @Override protected void determineProperties() { - final Path path = ResourcePathUtils.getPhysicalPath(this); + final Path path = locator.getEncryptedDirectoryPath(); properties.add(new ResourceType(ResourceType.COLLECTION)); properties.add(new DefaultDavProperty(DavPropertyName.ISCOLLECTION, 1)); if (Files.exists(path)) { @@ -161,31 +248,4 @@ class EncryptedDir extends AbstractEncryptedNode { } } - /** - * Deletes all files and folders, it visits. - */ - private static class DeletingFileVisitor extends SimpleFileVisitor { - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException { - if (attributes.isRegularFile()) { - Files.delete(file); - } - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { - Files.delete(dir); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { - LOG.error("Failed to delete file " + file.toString(), exc); - return FileVisitResult.TERMINATE; - } - - } - } diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java index 09905a0b6..30a3cd160 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java @@ -11,16 +11,17 @@ package org.cryptomator.webdav.jackrabbit; import java.io.EOFException; import java.io.IOException; import java.nio.channels.SeekableByteChannel; +import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; 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.DavServletResponse; import org.apache.jackrabbit.webdav.DavSession; import org.apache.jackrabbit.webdav.io.InputContext; import org.apache.jackrabbit.webdav.io.OutputContext; @@ -42,11 +43,16 @@ class EncryptedFile extends AbstractEncryptedNode { protected final CryptoWarningHandler cryptoWarningHandler; - public EncryptedFile(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler) { + public EncryptedFile(CryptoResourceFactory factory, CryptoLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler) { super(factory, locator, session, lockManager, cryptor); this.cryptoWarningHandler = cryptoWarningHandler; } + @Override + protected Path getPhysicalPath() { + return locator.getEncryptedFilePath(); + } + @Override public boolean isCollection() { return false; @@ -69,7 +75,7 @@ class EncryptedFile extends AbstractEncryptedNode { @Override public void spool(OutputContext outputContext) throws IOException { - final Path path = ResourcePathUtils.getPhysicalPath(this); + final Path path = locator.getEncryptedFilePath(); if (Files.isRegularFile(path)) { outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis()); outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString()); @@ -93,7 +99,7 @@ class EncryptedFile extends AbstractEncryptedNode { @Override protected void determineProperties() { - final Path path = ResourcePathUtils.getPhysicalPath(this); + final Path path = locator.getEncryptedFilePath(); if (Files.exists(path)) { try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) { final Long contentLength = cryptor.decryptedContentLength(channel); @@ -101,6 +107,9 @@ class EncryptedFile extends AbstractEncryptedNode { } catch (IOException e) { LOG.error("Error reading filesize " + path.toString(), e); throw new IORuntimeException(e); + } catch (MacAuthenticationFailedException e) { + LOG.warn("Content length couldn't be determined due to MAC authentication violation."); + // don't add content length DAV property } try { @@ -115,4 +124,40 @@ class EncryptedFile extends AbstractEncryptedNode { } } + @Override + public void move(AbstractEncryptedNode dest) throws DavException, IOException { + final Path src = this.locator.getEncryptedFilePath(); + final Path dst = dest.locator.getEncryptedFilePath(); + + // 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); + } + } + + @Override + public void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException { + final Path src = this.locator.getEncryptedFilePath(); + final Path dst = dest.locator.getEncryptedFilePath(); + + // 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); + } + } + } diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java index dfc73d78c..697a4422b 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java @@ -16,7 +16,6 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.MutablePair; import org.apache.commons.lang3.tuple.Pair; -import org.apache.jackrabbit.webdav.DavResourceFactory; import org.apache.jackrabbit.webdav.DavResourceLocator; import org.apache.jackrabbit.webdav.DavServletRequest; import org.apache.jackrabbit.webdav.DavSession; @@ -56,7 +55,7 @@ class EncryptedFilePart extends EncryptedFile { private final Set> requestedContentRanges = new HashSet>(); - public EncryptedFilePart(DavResourceFactory factory, DavResourceLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, + public EncryptedFilePart(CryptoResourceFactory factory, CryptoLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, ExecutorService backgroundTaskExecutor) { super(factory, locator, session, lockManager, cryptor, cryptoWarningHandler); final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString()); @@ -126,7 +125,7 @@ class EncryptedFilePart extends EncryptedFile { @Override public void spool(OutputContext outputContext) throws IOException { - final Path path = ResourcePathUtils.getPhysicalPath(this); + final Path path = locator.getEncryptedFilePath(); if (Files.isRegularFile(path)) { outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis()); try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) { @@ -154,9 +153,9 @@ class EncryptedFilePart extends EncryptedFile { private class MacAuthenticationJob implements Runnable { - private final DavResourceLocator locator; + private final CryptoLocator locator; - public MacAuthenticationJob(final DavResourceLocator locator) { + public MacAuthenticationJob(final CryptoLocator locator) { if (locator == null) { throw new IllegalArgumentException("locator must not be null."); } @@ -165,7 +164,7 @@ class EncryptedFilePart extends EncryptedFile { @Override public void run() { - final Path path = ResourcePathUtils.getPhysicalPath(locator); + final Path path = locator.getEncryptedFilePath(); if (Files.isRegularFile(path) && Files.isReadable(path)) { try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) { final boolean authentic = cryptor.isAuthentic(channel); diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/NonExistingNode.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/NonExistingNode.java index f58f20a2c..859cd943c 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/NonExistingNode.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/NonExistingNode.java @@ -9,12 +9,11 @@ package org.cryptomator.webdav.jackrabbit; import java.io.IOException; +import java.nio.file.Path; 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; @@ -23,10 +22,15 @@ import org.cryptomator.crypto.Cryptor; class NonExistingNode extends AbstractEncryptedNode { - public NonExistingNode(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) { + public NonExistingNode(CryptoResourceFactory factory, CryptoLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) { super(factory, locator, session, lockManager, cryptor); } + @Override + protected Path getPhysicalPath() { + throw new UnsupportedOperationException("Resource doesn't exist."); + } + @Override public boolean exists() { return false; @@ -37,6 +41,11 @@ class NonExistingNode extends AbstractEncryptedNode { return false; } + @Override + public long getModificationTime() { + return -1; + } + @Override public void spool(OutputContext outputContext) throws IOException { throw new UnsupportedOperationException("Resource doesn't exist."); @@ -62,4 +71,14 @@ class NonExistingNode extends AbstractEncryptedNode { // do nothing. } + @Override + public void move(AbstractEncryptedNode destination) throws DavException { + throw new UnsupportedOperationException("Resource doesn't exist."); + } + + @Override + public void copy(AbstractEncryptedNode destination, boolean shallow) throws DavException { + throw new UnsupportedOperationException("Resource doesn't exist."); + } + } diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/ResourcePathUtils.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/ResourcePathUtils.java index 71f65fcd3..6f77bff1f 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/ResourcePathUtils.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/ResourcePathUtils.java @@ -11,21 +11,18 @@ package org.cryptomator.webdav.jackrabbit; import java.nio.file.FileSystems; import java.nio.file.Path; -import org.apache.jackrabbit.webdav.DavResource; -import org.apache.jackrabbit.webdav.DavResourceLocator; - final class ResourcePathUtils { private ResourcePathUtils() { throw new IllegalStateException("not instantiable"); } - public static Path getPhysicalPath(DavResource resource) { - return getPhysicalPath(resource.getLocator()); - } - - public static Path getPhysicalPath(DavResourceLocator locator) { + public static Path getPhysicalFilePath(CryptoLocator locator) { return FileSystems.getDefault().getPath(locator.getRepositoryPath()); } + public static Path getPhysicalDirectoryPath(CryptoLocator locator) { + return FileSystems.getDefault().getPath(locator.getDirectoryPath()); + } + } diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java index 48752c20c..8b5fbc86f 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java @@ -47,8 +47,8 @@ public class WebDavServlet extends AbstractWebdavServlet { final String fsRoot = config.getInitParameter(CFG_FS_ROOT); backgroundTaskExecutor = Executors.newCachedThreadPool(); davSessionProvider = new DavSessionProviderImpl(); - davLocatorFactory = new DavLocatorFactoryImpl(fsRoot, cryptor); - davResourceFactory = new DavResourceFactoryImpl(cryptor, cryptoWarningHandler, backgroundTaskExecutor); + davLocatorFactory = new CryptoLocatorFactory(fsRoot, cryptor); + davResourceFactory = new CryptoResourceFactory(cryptor, cryptoWarningHandler, backgroundTaskExecutor); } @Override diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java index fcc0e2ebd..b33a344bc 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java @@ -22,9 +22,7 @@ import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import java.util.UUID; import javax.crypto.BadPaddingException; @@ -44,14 +42,15 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.NullOutputStream; import org.apache.commons.lang3.StringUtils; import org.bouncycastle.crypto.generators.SCrypt; -import org.cryptomator.crypto.AbstractCryptor; -import org.cryptomator.crypto.CryptorIOSupport; +import org.cryptomator.crypto.Cryptor; +import org.cryptomator.crypto.CryptorMetadataSupport; import org.cryptomator.crypto.aes256.CounterAwareInputStream.CounterAwareInputLimitReachedException; import org.cryptomator.crypto.exceptions.CounterOverflowException; import org.cryptomator.crypto.exceptions.DecryptFailedException; import org.cryptomator.crypto.exceptions.EncryptFailedException; import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException; import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; +import org.cryptomator.crypto.exceptions.UnsupportedVaultException; import org.cryptomator.crypto.exceptions.WrongPasswordException; import org.cryptomator.crypto.io.SeekableByteChannelInputStream; import org.cryptomator.crypto.io.SeekableByteChannelOutputStream; @@ -59,11 +58,11 @@ import org.cryptomator.crypto.io.SeekableByteChannelOutputStream; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicConfiguration, FileNamingConventions { +public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, FileNamingConventions { /** - * Defined in static initializer. Defaults to 256, but falls back to maximum value possible, if JCE Unlimited Strength Jurisdiction - * Policy Files isn't installed. Those files can be downloaded here: http://www.oracle.com/technetwork/java/javase/downloads/. + * Defined in static initializer. Defaults to 256, but falls back to maximum value possible, if JCE Unlimited Strength Jurisdiction Policy Files isn't installed. Those files can be downloaded + * here: http://www.oracle.com/technetwork/java/javase/downloads/. */ private static final int AES_KEY_LENGTH_IN_BITS; @@ -80,8 +79,8 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo private final ObjectMapper objectMapper = new ObjectMapper(); /** - * The decrypted master key. Its lifecycle starts with the construction of an Aes256Cryptor instance or - * {@link #decryptMasterKey(InputStream, CharSequence)}. Its lifecycle ends with {@link #swipeSensitiveData()}. + * The decrypted master key. Its lifecycle starts with the construction of an Aes256Cryptor instance or {@link #decryptMasterKey(InputStream, CharSequence)}. Its lifecycle ends with + * {@link #swipeSensitiveData()}. */ private SecretKey primaryMasterKey; @@ -135,6 +134,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo // save encrypted masterkey: final KeyFile keyfile = new KeyFile(); + keyfile.setVersion(KeyFile.CURRENT_VERSION); keyfile.setScryptSalt(kekSalt); keyfile.setScryptCostParam(SCRYPT_COST_PARAM); keyfile.setScryptBlockSize(SCRYPT_BLOCK_SIZE); @@ -151,17 +151,21 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo * Reads the encrypted masterkey from the given input stream and decrypts it with the given password. * * @throws DecryptFailedException If the decryption failed for various reasons (including wrong password). - * @throws WrongPasswordException If the provided password was wrong. Note: Sometimes the algorithm itself fails due to a wrong - * password. In this case a DecryptFailedException will be thrown. - * @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In - * this case Java JCE needs to be installed. + * @throws WrongPasswordException If the provided password was wrong. Note: Sometimes the algorithm itself fails due to a wrong password. In this case a DecryptFailedException will be thrown. + * @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In this case Java JCE needs to be installed. + * @throws UnsupportedVaultException If the masterkey file is too old or too modern. */ @Override - public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException { + public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException, UnsupportedVaultException { try { // load encrypted masterkey: final KeyFile keyfile = objectMapper.readValue(in, KeyFile.class); + // check version + if (keyfile.getVersion() != KeyFile.CURRENT_VERSION) { + throw new UnsupportedVaultException(keyfile.getVersion(), KeyFile.CURRENT_VERSION); + } + // check, whether the key length is supported: final int maxKeyLen = Cipher.getMaxAllowedKeyLength(AES_KEY_ALGORITHM); if (keyfile.getKeyLength() > maxKeyLen) { @@ -187,7 +191,12 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo } @Override - public void swipeSensitiveDataInternal() { + public boolean isDestroyed() { + return primaryMasterKey.isDestroyed() && hMacMasterKey.isDestroyed(); + } + + @Override + public void destroy() { destroyQuietly(primaryMasterKey); destroyQuietly(hMacMasterKey); } @@ -224,17 +233,16 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo } } - private Cipher aesEcbCipher(SecretKey key, int cipherMode) { + private Cipher aesCbcCipher(SecretKey key, byte[] iv, int cipherMode) { try { - final Cipher cipher = Cipher.getInstance(AES_ECB_CIPHER); - cipher.init(cipherMode, key); + final Cipher cipher = Cipher.getInstance(AES_CBC_CIPHER); + cipher.init(cipherMode, key, new IvParameterSpec(iv)); return cipher; } catch (InvalidKeyException ex) { throw new IllegalArgumentException("Invalid key.", ex); - } catch (NoSuchAlgorithmException | NoSuchPaddingException ex) { - throw new AssertionError("Every implementation of the Java platform is required to support AES/ECB/PKCS5Padding.", ex); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException ex) { + throw new AssertionError("Every implementation of the Java platform is required to support AES/CBC/PKCS5Padding, which accepts an IV", ex); } - } private Mac hmacSha256(SecretKey key) { @@ -249,6 +257,14 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo } } + private MessageDigest sha256() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError("Every implementation of the Java platform is required to support Sha-256"); + } + } + private byte[] randomData(int length) { final byte[] result = new byte[length]; securePrng.nextBytes(result); @@ -272,48 +288,38 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo } @Override - public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) { - try { - final String[] cleartextPathComps = StringUtils.split(cleartextPath, cleartextPathSep); - final List encryptedPathComps = new ArrayList<>(cleartextPathComps.length); - for (final String cleartext : cleartextPathComps) { - final String encrypted = encryptPathComponent(cleartext, primaryMasterKey, hMacMasterKey, ioSupport); - encryptedPathComps.add(encrypted); - } - return StringUtils.join(encryptedPathComps, encryptedPathSep); - } catch (InvalidKeyException | IOException e) { - throw new IllegalStateException("Unable to encrypt path: " + cleartextPath, e); - } + public String encryptDirectoryPath(String cleartextPath, String nativePathSep) { + final byte[] cleartextBytes = cleartextPath.getBytes(StandardCharsets.UTF_8); + byte[] encryptedBytes = AesSivCipherUtil.sivEncrypt(primaryMasterKey, hMacMasterKey, cleartextBytes); + final byte[] hashed = sha256().digest(encryptedBytes); + final String encryptedThenHashedPath = ENCRYPTED_FILENAME_CODEC.encodeAsString(hashed); + return encryptedThenHashedPath.substring(0, 2) + nativePathSep + encryptedThenHashedPath.substring(2); } /** * Each path component, i.e. file or directory name separated by path separators, gets encrypted for its own.
- * Encryption will blow up the filename length due to aes block sizes and base32 encoding. The result may be too long for some old file - * systems.
- * This means that we need a workaround for filenames longer than the limit defined in - * {@link FileNamingConventions#ENCRYPTED_FILENAME_LENGTH_LIMIT}.
+ * Encryption will blow up the filename length due to aes block sizes, IVs and base32 encoding. The result may be too long for some old file systems.
+ * This means that we need a workaround for filenames longer than the limit defined in {@link FileNamingConventions#ENCRYPTED_FILENAME_LENGTH_LIMIT}.
*
- * In any case we will create the encrypted filename normally. For those, that are too long, we calculate a checksum. No - * cryptographically secure hash is needed here. We just want an uniform distribution for better load balancing. All encrypted filenames - * with the same checksum will then share a metadata file, in which a lookup map between encrypted filenames and short unique + * In any case we will create the encrypted filename normally. For those, that are too long, we calculate a checksum. No cryptographically secure hash is needed here. We just want an uniform + * distribution for better load balancing. All encrypted filenames with the same checksum will then share a metadata file, in which a lookup map between encrypted filenames and short unique * alternative names are stored.
*
- * These alternative names consist of the checksum, a unique id and a special file extension defined in - * {@link FileNamingConventions#LONG_NAME_FILE_EXT}. + * These alternative names consist of the checksum, a unique id and a special file extension defined in {@link FileNamingConventions#LONG_NAME_FILE_EXT}. */ - private String encryptPathComponent(final String cleartext, final SecretKey aesKey, final SecretKey macKey, CryptorIOSupport ioSupport) throws IOException, InvalidKeyException { - final byte[] cleartextBytes = cleartext.getBytes(StandardCharsets.UTF_8); + @Override + public String encryptFilename(String cleartextName, CryptorMetadataSupport ioSupport) throws IOException { + final byte[] cleartextBytes = cleartextName.getBytes(StandardCharsets.UTF_8); // encrypt: - final byte[] encryptedBytes = AesSivCipherUtil.sivEncrypt(aesKey, macKey, cleartextBytes); + final byte[] encryptedBytes = AesSivCipherUtil.sivEncrypt(primaryMasterKey, hMacMasterKey, cleartextBytes); final String ivAndCiphertext = ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes); if (ivAndCiphertext.length() + BASIC_FILE_EXT.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) { - final String groupPrefix = ivAndCiphertext.substring(0, LONG_NAME_PREFIX_LENGTH); - final String metadataFilename = groupPrefix + METADATA_FILE_EXT; - final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataFilename); - final String alternativeFileName = groupPrefix + metadata.getOrCreateUuidForEncryptedFilename(ivAndCiphertext).toString() + LONG_NAME_FILE_EXT; - this.storeMetadata(ioSupport, metadataFilename, metadata); + final String metadataGroup = ivAndCiphertext.substring(0, LONG_NAME_PREFIX_LENGTH); + final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataGroup); + final String alternativeFileName = metadataGroup + metadata.getOrCreateUuidForEncryptedFilename(ivAndCiphertext).toString() + LONG_NAME_FILE_EXT; + this.storeMetadata(ioSupport, metadataGroup, metadata); return alternativeFileName; } else { return ivAndCiphertext + BASIC_FILE_EXT; @@ -321,47 +327,29 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo } @Override - public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) throws DecryptFailedException { - try { - final String[] encryptedPathComps = StringUtils.split(encryptedPath, encryptedPathSep); - final List cleartextPathComps = new ArrayList<>(encryptedPathComps.length); - for (final String encrypted : encryptedPathComps) { - final String cleartext = decryptPathComponent(encrypted, primaryMasterKey, hMacMasterKey, ioSupport); - cleartextPathComps.add(new String(cleartext)); - } - return StringUtils.join(cleartextPathComps, cleartextPathSep); - } catch (InvalidKeyException | IOException e) { - throw new IllegalStateException("Unable to decrypt path: " + encryptedPath, e); - } - } - - /** - * @see #encryptPathComponent(String, SecretKey, CryptorIOSupport) - */ - private String decryptPathComponent(final String encrypted, final SecretKey aesKey, final SecretKey macKey, CryptorIOSupport ioSupport) throws IOException, InvalidKeyException, DecryptFailedException { + public String decryptFilename(String ciphertextName, CryptorMetadataSupport ioSupport) throws DecryptFailedException, IOException { final String ciphertext; - if (encrypted.endsWith(LONG_NAME_FILE_EXT)) { - final String basename = StringUtils.removeEnd(encrypted, LONG_NAME_FILE_EXT); - final String groupPrefix = basename.substring(0, LONG_NAME_PREFIX_LENGTH); + if (ciphertextName.endsWith(LONG_NAME_FILE_EXT)) { + final String basename = StringUtils.removeEnd(ciphertextName, LONG_NAME_FILE_EXT); + final String metadataGroup = basename.substring(0, LONG_NAME_PREFIX_LENGTH); final String uuid = basename.substring(LONG_NAME_PREFIX_LENGTH); - final String metadataFilename = groupPrefix + METADATA_FILE_EXT; - final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataFilename); + final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataGroup); ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid)); - } else if (encrypted.endsWith(BASIC_FILE_EXT)) { - ciphertext = StringUtils.removeEndIgnoreCase(encrypted, BASIC_FILE_EXT); + } else if (ciphertextName.endsWith(BASIC_FILE_EXT)) { + ciphertext = StringUtils.removeEndIgnoreCase(ciphertextName, BASIC_FILE_EXT); } else { - throw new IllegalArgumentException("Unsupported path component: " + encrypted); + throw new IllegalArgumentException("Unsupported path component: " + ciphertextName); } // decrypt: final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(ciphertext); - final byte[] cleartextBytes = AesSivCipherUtil.sivDecrypt(aesKey, macKey, encryptedBytes); + final byte[] cleartextBytes = AesSivCipherUtil.sivDecrypt(primaryMasterKey, hMacMasterKey, encryptedBytes); return new String(cleartextBytes, StandardCharsets.UTF_8); } - private LongFilenameMetadata getMetadata(CryptorIOSupport ioSupport, String metadataFile) throws IOException { - final byte[] fileContent = ioSupport.readPathSpecificMetadata(metadataFile); + private LongFilenameMetadata getMetadata(CryptorMetadataSupport ioSupport, String metadataGroup) throws IOException { + final byte[] fileContent = ioSupport.readMetadata(metadataGroup); if (fileContent == null) { return new LongFilenameMetadata(); } else { @@ -369,28 +357,54 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo } } - private void storeMetadata(CryptorIOSupport ioSupport, String metadataFile, LongFilenameMetadata metadata) throws JsonProcessingException, IOException { - ioSupport.writePathSpecificMetadata(metadataFile, objectMapper.writeValueAsBytes(metadata)); + private void storeMetadata(CryptorMetadataSupport ioSupport, String metadataGroup, LongFilenameMetadata metadata) throws JsonProcessingException, IOException { + ioSupport.writeMetadata(metadataGroup, objectMapper.writeValueAsBytes(metadata)); } @Override - public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException { - // skip 128bit IV + 256 bit MAC: - encryptedFile.position(48); - - // read encrypted value: - final ByteBuffer encryptedFileSizeBuffer = ByteBuffer.allocate(AES_BLOCK_LENGTH); - final int numFileSizeBytesRead = encryptedFile.read(encryptedFileSizeBuffer); - - // return "unknown" value, if EOF - if (numFileSizeBytesRead != encryptedFileSizeBuffer.capacity()) { + public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException { + // read header: + encryptedFile.position(0); + final ByteBuffer headerBuf = ByteBuffer.allocate(64); + final int headerBytesRead = encryptedFile.read(headerBuf); + if (headerBytesRead != headerBuf.capacity()) { return null; } - // decrypt size: + // read iv: + final byte[] iv = new byte[AES_BLOCK_LENGTH]; + headerBuf.position(0); + headerBuf.get(iv); + + // read content length: + final byte[] encryptedContentLengthBytes = new byte[AES_BLOCK_LENGTH]; + headerBuf.position(16); + headerBuf.get(encryptedContentLengthBytes); + final Long fileSize = decryptContentLength(encryptedContentLengthBytes, iv); + + // read stored header mac: + final byte[] storedHeaderMac = new byte[32]; + headerBuf.position(32); + headerBuf.get(storedHeaderMac); + + // calculate mac over first 32 bytes of header: + final Mac headerMac = this.hmacSha256(hMacMasterKey); + headerBuf.rewind(); + headerBuf.limit(32); + headerMac.update(headerBuf); + + final boolean macMatches = MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal()); + if (!macMatches) { + throw new MacAuthenticationFailedException("MAC authentication failed."); + } + + return fileSize; + } + + private long decryptContentLength(byte[] encryptedContentLengthBytes, byte[] iv) { try { - final Cipher sizeCipher = aesEcbCipher(primaryMasterKey, Cipher.DECRYPT_MODE); - final byte[] decryptedFileSize = sizeCipher.doFinal(encryptedFileSizeBuffer.array()); + final Cipher sizeCipher = aesCbcCipher(primaryMasterKey, iv, Cipher.DECRYPT_MODE); + final byte[] decryptedFileSize = sizeCipher.doFinal(encryptedContentLengthBytes); final ByteBuffer fileSizeBuffer = ByteBuffer.wrap(decryptedFileSize); return fileSizeBuffer.getLong(); } catch (IllegalBlockSizeException | BadPaddingException e) { @@ -398,85 +412,93 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo } } - private void encryptedContentLength(SeekableByteChannel encryptedFile, Long contentLength) throws IOException { - final ByteBuffer encryptedFileSizeBuffer; - - // encrypt content length in ECB mode (content length is less than one block): + private byte[] encryptContentLength(long contentLength, byte[] iv) { try { final ByteBuffer fileSizeBuffer = ByteBuffer.allocate(Long.BYTES); fileSizeBuffer.putLong(contentLength); - final Cipher sizeCipher = aesEcbCipher(primaryMasterKey, Cipher.ENCRYPT_MODE); - final byte[] encryptedFileSize = sizeCipher.doFinal(fileSizeBuffer.array()); - encryptedFileSizeBuffer = ByteBuffer.wrap(encryptedFileSize); + final Cipher sizeCipher = aesCbcCipher(primaryMasterKey, iv, Cipher.ENCRYPT_MODE); + return sizeCipher.doFinal(fileSizeBuffer.array()); } catch (IllegalBlockSizeException | BadPaddingException e) { throw new IllegalStateException("Block size must be valid, as padding is requested. BadPaddingException not possible in encrypt mode.", e); } - - // skip 128bit IV + 256 bit MAC: - encryptedFile.position(48); - - // write result: - encryptedFile.write(encryptedFileSizeBuffer); } @Override public boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException { - // init mac: - final Mac calculatedMac = this.hmacSha256(hMacMasterKey); - - // read stored mac: - encryptedFile.position(16); - final ByteBuffer storedMac = ByteBuffer.allocate(calculatedMac.getMacLength()); - final int numMacBytesRead = encryptedFile.read(storedMac); - - // check validity of header: - if (numMacBytesRead != calculatedMac.getMacLength()) { + // read header: + encryptedFile.position(0l); + final ByteBuffer headerBuf = ByteBuffer.allocate(96); + final int headerBytesRead = encryptedFile.read(headerBuf); + if (headerBytesRead != headerBuf.capacity()) { throw new IOException("Failed to read file header."); } - // go to begin of content: - encryptedFile.position(64); + // read header mac: + final byte[] storedHeaderMac = new byte[32]; + headerBuf.position(32); + headerBuf.get(storedHeaderMac); - // calculated MAC + // read content mac: + final byte[] storedContentMac = new byte[32]; + headerBuf.position(64); + headerBuf.get(storedContentMac); + + // calculate mac over first 32 bytes of header: + final Mac headerMac = this.hmacSha256(hMacMasterKey); + headerBuf.position(0); + headerBuf.limit(32); + headerMac.update(headerBuf); + + // calculate mac over content: + encryptedFile.position(96l); + final Mac contentMac = this.hmacSha256(hMacMasterKey); final InputStream in = new SeekableByteChannelInputStream(encryptedFile); - final InputStream macIn = new MacInputStream(in, calculatedMac); + final InputStream macIn = new MacInputStream(in, contentMac); IOUtils.copyLarge(macIn, new NullOutputStream()); // compare (in constant time): - return MessageDigest.isEqual(storedMac.array(), calculatedMac.doFinal()); + final boolean headerMacMatches = MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal()); + final boolean contentMacMatches = MessageDigest.isEqual(storedContentMac, contentMac.doFinal()); + return headerMacMatches && contentMacMatches; } @Override public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException { - // read iv: - encryptedFile.position(0); - final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH); - final int numIvBytesRead = encryptedFile.read(countingIv); - - // init mac: - final Mac calculatedMac = this.hmacSha256(hMacMasterKey); - - // read stored mac: - final ByteBuffer storedMac = ByteBuffer.allocate(calculatedMac.getMacLength()); - final int numMacBytesRead = encryptedFile.read(storedMac); - - // read file size: - final Long fileSize = decryptedContentLength(encryptedFile); - - // check validity of header: - if (numIvBytesRead != AES_BLOCK_LENGTH || numMacBytesRead != calculatedMac.getMacLength() || fileSize == null) { + // read header: + encryptedFile.position(0l); + final ByteBuffer headerBuf = ByteBuffer.allocate(96); + final int headerBytesRead = encryptedFile.read(headerBuf); + if (headerBytesRead != headerBuf.capacity()) { throw new IOException("Failed to read file header."); } - // go to begin of content: - encryptedFile.position(64); + // read iv: + final byte[] iv = new byte[AES_BLOCK_LENGTH]; + headerBuf.position(0); + headerBuf.get(iv); - // generate cipher: - final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.DECRYPT_MODE); + // read content length: + final byte[] encryptedContentLengthBytes = new byte[AES_BLOCK_LENGTH]; + headerBuf.position(16); + headerBuf.get(encryptedContentLengthBytes); + final Long fileSize = decryptContentLength(encryptedContentLengthBytes, iv); - // read content + // read header mac: + final byte[] headerMac = new byte[32]; + headerBuf.position(32); + headerBuf.get(headerMac); + + // read content mac: + final byte[] contentMac = new byte[32]; + headerBuf.position(64); + headerBuf.get(contentMac); + + // decrypt content + encryptedFile.position(96l); + final Mac calculatedContentMac = this.hmacSha256(hMacMasterKey); + final Cipher cipher = this.aesCtrCipher(primaryMasterKey, iv, Cipher.DECRYPT_MODE); final InputStream in = new SeekableByteChannelInputStream(encryptedFile); - final InputStream macIn = new MacInputStream(in, calculatedMac); + final InputStream macIn = new MacInputStream(in, calculatedContentMac); final InputStream cipheredIn = new CipherInputStream(macIn, cipher); final long bytesDecrypted = IOUtils.copyLarge(cipheredIn, plaintextFile, 0, fileSize); @@ -484,7 +506,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo IOUtils.copyLarge(macIn, new NullOutputStream()); // compare (in constant time): - final boolean macMatches = MessageDigest.isEqual(storedMac.array(), calculatedMac.doFinal()); + final boolean macMatches = MessageDigest.isEqual(contentMac, calculatedContentMac.doFinal()); if (!macMatches) { // This exception will be thrown AFTER we sent the decrypted content to the user. // This has two advantages: @@ -500,7 +522,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo @Override public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException { // read iv: - encryptedFile.position(0); + encryptedFile.position(0l); final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH); final int numIvBytesRead = encryptedFile.read(countingIv); @@ -516,7 +538,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo countingIv.putInt(AES_BLOCK_LENGTH - Integer.BYTES, (int) firstRelevantBlock); // int-cast is possible, as max file size is 64GiB // fast forward stream: - encryptedFile.position(64l + beginOfFirstRelevantBlock); + encryptedFile.position(96l + beginOfFirstRelevantBlock); // generate cipher: final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.DECRYPT_MODE); @@ -527,30 +549,33 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo return IOUtils.copyLarge(cipheredIn, plaintextFile, offsetInsideFirstRelevantBlock, length); } + /** + * header = {16 byte iv, 16 byte filesize, 32 byte headerMac, 32 byte contentMac} + */ @Override public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException { // truncate file - encryptedFile.truncate(0); + encryptedFile.truncate(0l); // use an IV, whose last 8 bytes store a long used in counter mode and write initial value to file. - final ByteBuffer countingIv = ByteBuffer.wrap(randomData(AES_BLOCK_LENGTH)); - countingIv.putInt(AES_BLOCK_LENGTH - Integer.BYTES, 0); - encryptedFile.write(countingIv); + final ByteBuffer ivBuf = ByteBuffer.wrap(randomData(AES_BLOCK_LENGTH)); + ivBuf.putInt(AES_BLOCK_LENGTH - Integer.BYTES, 0); + final byte[] iv = ivBuf.array(); - // init crypto stuff: - final Mac mac = this.hmacSha256(hMacMasterKey); - final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.ENCRYPT_MODE); + // 96 byte header buffer (16 IV, 16 size, 32 headerMac, 32 contentMac) + // prefilled with "zero" content length for impatient processes, which want to know the size, before file has been completely written: + final ByteBuffer headerBuf = ByteBuffer.allocate(96); + headerBuf.position(16); + headerBuf.put(encryptContentLength(0l, iv)); + headerBuf.flip(); + headerBuf.limit(96); + encryptedFile.write(headerBuf); - // init mac buffer and skip 32 bytes - final ByteBuffer macBuffer = ByteBuffer.allocate(mac.getMacLength()); - encryptedFile.write(macBuffer); - - // encrypt and write "zero length" as a placeholder, which will be read by concurrent requests, as long as encryption didn't finish: - encryptedContentLength(encryptedFile, 0l); - - // write content: + // content encryption: + final Cipher cipher = this.aesCtrCipher(primaryMasterKey, iv, Cipher.ENCRYPT_MODE); + final Mac contentMac = this.hmacSha256(hMacMasterKey); final OutputStream out = new SeekableByteChannelOutputStream(encryptedFile); - final OutputStream macOut = new MacOutputStream(out, mac); + final OutputStream macOut = new MacOutputStream(out, contentMac); final OutputStream cipheredOut = new CipherOutputStream(macOut, cipher); final OutputStream blockSizeBufferedOut = new BufferedOutputStream(cipheredOut, AES_BLOCK_LENGTH); final InputStream lengthLimitingIn = new CounterAwareInputStream(plaintextFile); @@ -558,36 +583,35 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo try { plaintextSize = IOUtils.copyLarge(lengthLimitingIn, blockSizeBufferedOut); } catch (CounterAwareInputLimitReachedException ex) { - encryptedFile.truncate(64l + CounterAwareInputStream.SIXTY_FOUR_GIGABYE); - encryptedContentLength(encryptedFile, CounterAwareInputStream.SIXTY_FOUR_GIGABYE); - // no additional padding needed here, as 64GiB is a multiple of 128bit + encryptedFile.truncate(0l); throw new CounterOverflowException("File size exceeds limit (64Gib). Aborting to prevent counter overflow."); } - // ensure total byte count is a multiple of the block size, in CTR mode: - final int remainderToFillLastBlock = AES_BLOCK_LENGTH - (int) (plaintextSize % AES_BLOCK_LENGTH); - blockSizeBufferedOut.write(new byte[remainderToFillLastBlock]); - - // for filesizes of up to 16GiB: append a few blocks of fake data: - if (plaintextSize < (long) (Integer.MAX_VALUE / 4) * AES_BLOCK_LENGTH) { - final int numberOfPlaintextBlocks = (int) Math.ceil(plaintextSize / AES_BLOCK_LENGTH); - final int upToTenPercentFakeBlocks = (int) Math.ceil(Math.random() * 0.1 * numberOfPlaintextBlocks); - final byte[] emptyBytes = this.randomData(AES_BLOCK_LENGTH); - for (int i = 0; i < upToTenPercentFakeBlocks; i += AES_BLOCK_LENGTH) { - blockSizeBufferedOut.write(emptyBytes); - } + // add random length padding to obfuscate file length: + final long numberOfPlaintextBlocks = (int) Math.ceil(plaintextSize / AES_BLOCK_LENGTH); + final long minAdditionalBlocks = 4; + final long maxAdditionalBlocks = Math.min(numberOfPlaintextBlocks >> 3, 1024 * 1024); // 12,5% of original blocks, but not more than 1M blocks (16MiBs) + final long availableBlocks = (1l << 32) - numberOfPlaintextBlocks; // before reaching limit of 2^32 blocks + final long additionalBlocks = (long) Math.min(Math.random() * Math.max(minAdditionalBlocks, maxAdditionalBlocks), availableBlocks); + final byte[] randomPadding = this.randomData(AES_BLOCK_LENGTH); + for (int i = 0; i < additionalBlocks; i += AES_BLOCK_LENGTH) { + blockSizeBufferedOut.write(randomPadding); } blockSizeBufferedOut.flush(); - // write MAC of total ciphertext: - macBuffer.clear(); - macBuffer.put(mac.doFinal()); - macBuffer.flip(); - encryptedFile.position(16); // right behind the IV - encryptedFile.write(macBuffer); // 256 bit MAC - - // encrypt and write plaintextSize: - encryptedContentLength(encryptedFile, plaintextSize); + // create and write header: + headerBuf.clear(); + headerBuf.put(iv); + headerBuf.put(encryptContentLength(plaintextSize, iv)); + headerBuf.flip(); + final Mac headerMac = this.hmacSha256(hMacMasterKey); + headerMac.update(headerBuf); + headerBuf.limit(96); + headerBuf.put(headerMac.doFinal()); + headerBuf.put(contentMac.doFinal()); + headerBuf.flip(); + encryptedFile.position(0); + encryptedFile.write(headerBuf); return plaintextSize; } @@ -597,7 +621,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo return new Filter() { @Override public boolean accept(Path entry) throws IOException { - return ENCRYPTED_FILE_GLOB_MATCHER.matches(entry); + return ENCRYPTED_FILE_MATCHER.matches(entry); } }; } diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesCryptographicConfiguration.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesCryptographicConfiguration.java index 4bd81f93f..852248b9b 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesCryptographicConfiguration.java +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesCryptographicConfiguration.java @@ -26,14 +26,14 @@ interface AesCryptographicConfiguration { int SCRYPT_BLOCK_SIZE = 8; /** - * Number of bytes of the master key. Should be the maximum possible AES key length to provide best security. + * Preferred number of bytes of the master key. */ int PREF_MASTER_KEY_LENGTH_IN_BITS = 256; /** * Number of bytes used as seed for the PRNG. */ - int PRNG_SEED_LENGTH = 32; + int PRNG_SEED_LENGTH = 16; /** * Algorithm used for random number generation. @@ -60,30 +60,22 @@ interface AesCryptographicConfiguration { String AES_KEYWRAP_CIPHER = "AESWrap"; /** - * Cipher specs for file name and file content encryption. Using CTR-mode for random access.
- * Important: As JCE doesn't support a padding, input must be a multiple of the block size. + * Cipher specs for file content encryption. Using CTR-mode for random access.
* * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher */ String AES_CTR_CIPHER = "AES/CTR/NoPadding"; /** - * Cipher specs for single block encryption (like file size). + * Cipher specs for file header encryption (fixed-length block cipher).
* * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#impl */ - String AES_ECB_CIPHER = "AES/ECB/PKCS5Padding"; + String AES_CBC_CIPHER = "AES/CBC/PKCS5Padding"; /** * AES block size is 128 bit or 16 bytes. */ int AES_BLOCK_LENGTH = 16; - /** - * Number of non-zero bytes in the IV used for file name encryption. Less means shorter encrypted filenames, more means higher entropy. - * Maximum length is {@value #AES_BLOCK_LENGTH}. Even the shortest base32 (see {@link FileNamingConventions#ENCRYPTED_FILENAME_CODEC}) - * encoded byte array will need 8 chars. The maximum number of bytes that fit in 8 base32 chars is 5. Thus 5 is the ideal length. - */ - int FILE_NAME_IV_LENGTH = 5; - } diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesSivCipherUtil.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesSivCipherUtil.java index 4a4d9b3f1..023f28767 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesSivCipherUtil.java +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesSivCipherUtil.java @@ -33,7 +33,7 @@ final class AesSivCipherUtil { private static final byte[] BYTES_ZERO = new byte[16]; private static final byte DOUBLING_CONST = (byte) 0x87; - static byte[] sivEncrypt(SecretKey aesKey, SecretKey macKey, byte[] plaintext, byte[]... additionalData) throws InvalidKeyException { + static byte[] sivEncrypt(SecretKey aesKey, SecretKey macKey, byte[] plaintext, byte[]... additionalData) { final byte[] aesKeyBytes = aesKey.getEncoded(); final byte[] macKeyBytes = macKey.getEncoded(); if (aesKeyBytes == null || macKeyBytes == null) { @@ -41,6 +41,8 @@ final class AesSivCipherUtil { } try { return sivEncrypt(aesKeyBytes, macKeyBytes, plaintext, additionalData); + } catch (InvalidKeyException ex) { + throw new IllegalArgumentException(ex); } finally { Arrays.fill(aesKeyBytes, (byte) 0); Arrays.fill(macKeyBytes, (byte) 0); @@ -78,7 +80,7 @@ final class AesSivCipherUtil { return ArrayUtils.addAll(iv, ciphertext); } - static byte[] sivDecrypt(SecretKey aesKey, SecretKey macKey, byte[] plaintext, byte[]... additionalData) throws InvalidKeyException, DecryptFailedException { + static byte[] sivDecrypt(SecretKey aesKey, SecretKey macKey, byte[] plaintext, byte[]... additionalData) throws DecryptFailedException { final byte[] aesKeyBytes = aesKey.getEncoded(); final byte[] macKeyBytes = macKey.getEncoded(); if (aesKeyBytes == null || macKeyBytes == null) { @@ -86,6 +88,8 @@ final class AesSivCipherUtil { } try { return sivDecrypt(aesKeyBytes, macKeyBytes, plaintext, additionalData); + } catch (InvalidKeyException ex) { + throw new IllegalArgumentException(ex); } finally { Arrays.fill(aesKeyBytes, (byte) 0); Arrays.fill(macKeyBytes, (byte) 0); diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/CounterAwareInputStream.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/CounterAwareInputStream.java index ede51e3f8..a4cea2bf8 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/CounterAwareInputStream.java +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/CounterAwareInputStream.java @@ -5,20 +5,18 @@ import java.io.IOException; import java.io.InputStream; import java.util.concurrent.atomic.AtomicLong; -import javax.crypto.Mac; - /** - * Updates a {@link Mac} with the bytes read from this stream. + * Throws an exception, if more than (2^32)-1 16 byte blocks will be encrypted (would result in an counter overflow).
+ * From https://tools.ietf.org/html/rfc3686: Using the encryption process described in section 2.1, this construction permits each packet to consist of up to: (2^32)-1 blocks */ class CounterAwareInputStream extends FilterInputStream { - static final long SIXTY_FOUR_GIGABYE = 1024l * 1024l * 1024l * 64l; + static final long SIXTY_FOUR_GIGABYE = ((1l << 32) - 1) * 16; private final AtomicLong counter; /** * @param in Stream from which to read contents, which will update the Mac. - * @param mac Mac to be updated during writes. */ public CounterAwareInputStream(InputStream in) { super(in); diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/FileNamingConventions.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/FileNamingConventions.java index 82e6b6aa7..ca6da8eea 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/FileNamingConventions.java +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/FileNamingConventions.java @@ -8,11 +8,13 @@ ******************************************************************************/ package org.cryptomator.crypto.aes256; -import java.nio.file.FileSystems; +import java.nio.file.Path; import java.nio.file.PathMatcher; +import java.util.regex.Pattern; import org.apache.commons.codec.binary.Base32; import org.apache.commons.codec.binary.BaseNCodec; +import org.apache.commons.lang3.StringUtils; interface FileNamingConventions { @@ -22,21 +24,17 @@ interface FileNamingConventions { BaseNCodec ENCRYPTED_FILENAME_CODEC = new Base32(); /** - * Maximum length possible on file systems with a filename limit of 255 chars.
- * Also we would need a few chars for our file extension, so lets use {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT}. + * Maximum path length on some file systems or cloud storage providers is restricted.
+ * Parent folder path uses up to 58 chars (sha256 -> 32 bytes base32 encoded to 56 bytes + two slashes). That in mind we don't want the total path to be longer than 255 chars.
+ * 128 chars would be enought for up to 80 plaintext chars. Also we need up to 8 chars for our file extension. So lets use {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT}. */ - int ENCRYPTED_FILENAME_LENGTH_LIMIT = 250; + int ENCRYPTED_FILENAME_LENGTH_LIMIT = 136; /** * For plaintext file names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars. */ String BASIC_FILE_EXT = ".aes"; - /** - * Prefix in front of the actual encrypted file name used as IV. - */ - String IV_PREFIX_SEPARATOR = "_"; - /** * For plaintext file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars. */ @@ -48,14 +46,27 @@ interface FileNamingConventions { int LONG_NAME_PREFIX_LENGTH = 8; /** - * For metadata files for a certain group of files. The cryptor may decide what files to assign to the same group; hopefully using some - * kind of uniform distribution for better load balancing. + * Matches valid encrypted filenames (both normal and long filenames - see {@link #ENCRYPTED_FILENAME_LENGTH_LIMIT}). */ - String METADATA_FILE_EXT = ".meta"; + PathMatcher ENCRYPTED_FILE_MATCHER = new PathMatcher() { - /** - * 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 + "}"); + private final Pattern BASIC_NAME_PATTERN = Pattern.compile("^[a-z2-7]+=*$", Pattern.CASE_INSENSITIVE); + private final Pattern LONG_NAME_PATTERN = Pattern.compile("^[a-z2-7]{8}[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", Pattern.CASE_INSENSITIVE); + + @Override + public boolean matches(Path path) { + final String filename = path.getFileName().toString(); + if (StringUtils.endsWithIgnoreCase(filename, LONG_NAME_FILE_EXT)) { + final String basename = StringUtils.removeEndIgnoreCase(filename, LONG_NAME_FILE_EXT); + return LONG_NAME_PATTERN.matcher(basename).matches(); + } else if (StringUtils.endsWithIgnoreCase(filename, BASIC_FILE_EXT)) { + final String basename = StringUtils.removeEndIgnoreCase(filename, BASIC_FILE_EXT); + return BASIC_NAME_PATTERN.matcher(basename).matches(); + } else { + return false; + } + } + + }; } diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/KeyFile.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/KeyFile.java index 330730d6f..dec5d477a 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/KeyFile.java +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/KeyFile.java @@ -4,10 +4,13 @@ import java.io.Serializable; import com.fasterxml.jackson.annotation.JsonPropertyOrder; -@JsonPropertyOrder(value = {"scryptSalt", "scryptCostParam", "scryptBlockSize", "keyLength", "primaryMasterKey", "hMacMasterKey"}) +@JsonPropertyOrder(value = {"version", "scryptSalt", "scryptCostParam", "scryptBlockSize", "keyLength", "primaryMasterKey", "hMacMasterKey"}) public class KeyFile implements Serializable { + static final Integer CURRENT_VERSION = 1; private static final long serialVersionUID = 8578363158959619885L; + + private Integer version; private byte[] scryptSalt; private int scryptCostParam; private int scryptBlockSize; @@ -15,6 +18,14 @@ public class KeyFile implements Serializable { private byte[] primaryMasterKey; private byte[] hMacMasterKey; + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + public byte[] getScryptSalt() { return scryptSalt; } diff --git a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java b/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java index 0ee932d81..a04303fb2 100644 --- a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java +++ b/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java @@ -18,11 +18,14 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import javax.security.auth.DestroyFailedException; + import org.apache.commons.io.IOUtils; -import org.cryptomator.crypto.CryptorIOSupport; +import org.cryptomator.crypto.CryptorMetadataSupport; import org.cryptomator.crypto.exceptions.DecryptFailedException; import org.cryptomator.crypto.exceptions.EncryptFailedException; import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; +import org.cryptomator.crypto.exceptions.UnsupportedVaultException; import org.cryptomator.crypto.exceptions.WrongPasswordException; import org.junit.Assert; import org.junit.Test; @@ -30,12 +33,12 @@ import org.junit.Test; public class Aes256CryptorTest { @Test - public void testCorrectPassword() throws IOException, WrongPasswordException, DecryptFailedException, UnsupportedKeyLengthException { + public void testCorrectPassword() throws IOException, WrongPasswordException, DecryptFailedException, UnsupportedKeyLengthException, DestroyFailedException, UnsupportedVaultException { final String pw = "asd"; final Aes256Cryptor cryptor = new Aes256Cryptor(); final ByteArrayOutputStream out = new ByteArrayOutputStream(); cryptor.encryptMasterKey(out, pw); - cryptor.swipeSensitiveData(); + cryptor.destroy(); final Aes256Cryptor decryptor = new Aes256Cryptor(); final InputStream in = new ByteArrayInputStream(out.toByteArray()); @@ -46,12 +49,12 @@ public class Aes256CryptorTest { } @Test - public void testWrongPassword() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException { + public void testWrongPassword() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, DestroyFailedException, UnsupportedVaultException { final String pw = "asd"; final Aes256Cryptor cryptor = new Aes256Cryptor(); final ByteArrayOutputStream out = new ByteArrayOutputStream(); cryptor.encryptMasterKey(out, pw); - cryptor.swipeSensitiveData(); + cryptor.destroy(); IOUtils.closeQuietly(out); // all these passwords are expected to fail. @@ -80,7 +83,7 @@ public class Aes256CryptorTest { final Aes256Cryptor cryptor = new Aes256Cryptor(); // encrypt: - final ByteBuffer encryptedData = ByteBuffer.allocate(96); + final ByteBuffer encryptedData = ByteBuffer.allocate(256); final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData); cryptor.encryptFile(plaintextIn, encryptedOut); IOUtils.closeQuietly(plaintextIn); @@ -112,7 +115,7 @@ public class Aes256CryptorTest { final Aes256Cryptor cryptor = new Aes256Cryptor(); // encrypt: - final ByteBuffer encryptedData = ByteBuffer.allocate(96); + final ByteBuffer encryptedData = ByteBuffer.allocate(256); final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData); cryptor.encryptFile(plaintextIn, encryptedOut); IOUtils.closeQuietly(plaintextIn); @@ -144,7 +147,7 @@ public class Aes256CryptorTest { final Aes256Cryptor cryptor = new Aes256Cryptor(); // encrypt: - final ByteBuffer encryptedData = ByteBuffer.allocate(96); + final ByteBuffer encryptedData = ByteBuffer.allocate(256); final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData); cryptor.encryptFile(plaintextIn, encryptedOut); IOUtils.closeQuietly(plaintextIn); @@ -183,7 +186,7 @@ public class Aes256CryptorTest { final Aes256Cryptor cryptor = new Aes256Cryptor(); // encrypt: - final ByteBuffer encryptedData = ByteBuffer.allocate((int) (64 + plaintextData.length * 1.2)); + final ByteBuffer encryptedData = ByteBuffer.allocate((int) (96 + plaintextData.length * 1.2)); final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData); cryptor.encryptFile(plaintextIn, encryptedOut); IOUtils.closeQuietly(plaintextIn); @@ -207,47 +210,45 @@ public class Aes256CryptorTest { @Test public void testEncryptionOfFilenames() throws IOException, DecryptFailedException { - final CryptorIOSupport ioSupportMock = new CryptoIOSupportMock(); + final CryptorMetadataSupport ioSupportMock = new CryptoIOSupportMock(); final Aes256Cryptor cryptor = new Aes256Cryptor(); - // short path components + // directory paths final String originalPath1 = "foo/bar/baz"; - final String encryptedPath1a = cryptor.encryptPath(originalPath1, '/', '/', ioSupportMock); - final String encryptedPath1b = cryptor.encryptPath(originalPath1, '/', '/', ioSupportMock); + final String encryptedPath1a = cryptor.encryptDirectoryPath(originalPath1, "/"); + final String encryptedPath1b = cryptor.encryptDirectoryPath(originalPath1, "/"); Assert.assertEquals(encryptedPath1a, encryptedPath1b); - final String decryptedPath1 = cryptor.decryptPath(encryptedPath1a, '/', '/', ioSupportMock); - Assert.assertEquals(originalPath1, decryptedPath1); - // long path components + // long file names final String str50chars = "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee"; - final String originalPath2 = "foo/" + str50chars + str50chars + str50chars + str50chars + str50chars + "/baz"; - final String encryptedPath2a = cryptor.encryptPath(originalPath2, '/', '/', ioSupportMock); - final String encryptedPath2b = cryptor.encryptPath(originalPath2, '/', '/', ioSupportMock); + final String originalPath2 = str50chars + str50chars + str50chars + str50chars + str50chars + "_isLongerThan255Chars.txt"; + final String encryptedPath2a = cryptor.encryptFilename(originalPath2, ioSupportMock); + final String encryptedPath2b = cryptor.encryptFilename(originalPath2, ioSupportMock); Assert.assertEquals(encryptedPath2a, encryptedPath2b); - final String decryptedPath2 = cryptor.decryptPath(encryptedPath2a, '/', '/', ioSupportMock); + final String decryptedPath2 = cryptor.decryptFilename(encryptedPath2a, ioSupportMock); Assert.assertEquals(originalPath2, decryptedPath2); - // block size length path components + // block size length file names final String originalPath3 = "aaaabbbbccccdddd"; - final String encryptedPath3a = cryptor.encryptPath(originalPath3, '/', '/', ioSupportMock); - final String encryptedPath3b = cryptor.encryptPath(originalPath3, '/', '/', ioSupportMock); + final String encryptedPath3a = cryptor.encryptFilename(originalPath3, ioSupportMock); + final String encryptedPath3b = cryptor.encryptFilename(originalPath3, ioSupportMock); Assert.assertEquals(encryptedPath3a, encryptedPath3b); - final String decryptedPath3 = cryptor.decryptPath(encryptedPath3a, '/', '/', ioSupportMock); + final String decryptedPath3 = cryptor.decryptFilename(encryptedPath3a, ioSupportMock); Assert.assertEquals(originalPath3, decryptedPath3); } - private static class CryptoIOSupportMock implements CryptorIOSupport { + private static class CryptoIOSupportMock implements CryptorMetadataSupport { private final Map map = new HashMap<>(); @Override - public void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) { - map.put(encryptedPath, encryptedMetadata); + public void writeMetadata(String metadataGroup, byte[] encryptedMetadata) { + map.put(metadataGroup, encryptedMetadata); } @Override - public byte[] readPathSpecificMetadata(String encryptedPath) { - return map.get(encryptedPath); + public byte[] readMetadata(String metadataGroup) { + return map.get(metadataGroup); } } diff --git a/main/crypto-api/pom.xml b/main/crypto-api/pom.xml index bd0a115c3..3a8c08e03 100644 --- a/main/crypto-api/pom.xml +++ b/main/crypto-api/pom.xml @@ -27,5 +27,9 @@ org.apache.commons commons-lang3 + + org.apache.commons + commons-collections4 + \ No newline at end of file diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptor.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptor.java deleted file mode 100644 index d51ad70c3..000000000 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptor.java +++ /dev/null @@ -1,38 +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 org.cryptomator.crypto; - -import java.util.HashSet; -import java.util.Set; - -public abstract class AbstractCryptor implements Cryptor { - - private final Set swipeListeners = new HashSet<>(); - - @Override - public final void swipeSensitiveData() { - this.swipeSensitiveDataInternal(); - for (final SensitiveDataSwipeListener sensitiveDataSwipeListener : swipeListeners) { - sensitiveDataSwipeListener.swipeSensitiveData(); - } - } - - protected abstract void swipeSensitiveDataInternal(); - - @Override - public final void addSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) { - this.swipeListeners.add(listener); - } - - @Override - public final void removeSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) { - this.swipeListeners.remove(listener); - } - -} diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptorDecorator.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptorDecorator.java new file mode 100644 index 000000000..ab44886f0 --- /dev/null +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptorDecorator.java @@ -0,0 +1,92 @@ +package org.cryptomator.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 javax.security.auth.DestroyFailedException; + +import org.cryptomator.crypto.exceptions.DecryptFailedException; +import org.cryptomator.crypto.exceptions.EncryptFailedException; +import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException; +import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; +import org.cryptomator.crypto.exceptions.UnsupportedVaultException; +import org.cryptomator.crypto.exceptions.WrongPasswordException; + +public class AbstractCryptorDecorator implements Cryptor { + + protected final Cryptor cryptor; + + public AbstractCryptorDecorator(Cryptor cryptor) { + this.cryptor = cryptor; + } + + @Override + public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException { + cryptor.encryptMasterKey(out, password); + } + + @Override + public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException, UnsupportedVaultException { + cryptor.decryptMasterKey(in, password); + } + + @Override + public String encryptDirectoryPath(String cleartextPath, String nativePathSep) { + return cryptor.encryptDirectoryPath(cleartextPath, nativePathSep); + } + + @Override + public String encryptFilename(String cleartextName, CryptorMetadataSupport ioSupport) throws IOException { + return cryptor.encryptFilename(cleartextName, ioSupport); + } + + @Override + public String decryptFilename(String ciphertextName, CryptorMetadataSupport ioSupport) throws IOException, DecryptFailedException { + return cryptor.decryptFilename(ciphertextName, ioSupport); + } + + @Override + public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException { + return cryptor.decryptedContentLength(encryptedFile); + } + + @Override + public boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException { + return cryptor.isAuthentic(encryptedFile); + } + + @Override + public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException { + return cryptor.decryptFile(encryptedFile, plaintextFile); + } + + @Override + public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException { + return cryptor.decryptRange(encryptedFile, plaintextFile, pos, length); + } + + @Override + public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException { + return cryptor.encryptFile(plaintextFile, encryptedFile); + } + + @Override + public Filter getPayloadFilesFilter() { + return cryptor.getPayloadFilesFilter(); + } + + @Override + public void destroy() throws DestroyFailedException { + cryptor.destroy(); + } + + @Override + public boolean isDestroyed() { + return cryptor.isDestroyed(); + } + +} diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java index bcf28cf90..bf7a5f8ba 100644 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java @@ -15,15 +15,19 @@ import java.nio.channels.SeekableByteChannel; import java.nio.file.DirectoryStream.Filter; import java.nio.file.Path; +import javax.security.auth.Destroyable; + import org.cryptomator.crypto.exceptions.DecryptFailedException; import org.cryptomator.crypto.exceptions.EncryptFailedException; +import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException; import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; +import org.cryptomator.crypto.exceptions.UnsupportedVaultException; import org.cryptomator.crypto.exceptions.WrongPasswordException; /** * Provides access to cryptographic functions. All methods are threadsafe. */ -public interface Cryptor extends SensitiveDataSwipeListener { +public interface Cryptor extends Destroyable { /** * Encrypts the current masterKey with the given password and writes the result to the given output stream. @@ -34,47 +38,48 @@ public interface Cryptor extends SensitiveDataSwipeListener { * Reads the encrypted masterkey from the given input stream and decrypts it with the given password. * * @throws DecryptFailedException If the decryption failed for various reasons (including wrong password). - * @throws WrongPasswordException If the provided password was wrong. Note: Sometimes the algorithm itself fails due to a wrong - * password. In this case a DecryptFailedException will be thrown. - * @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In - * this case Java JCE needs to be installed. + * @throws WrongPasswordException If the provided password was wrong. Note: Sometimes the algorithm itself fails due to a wrong password. In this case a DecryptFailedException will be thrown. + * @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In this case Java JCE needs to be installed. + * @throws UnsupportedVaultException If the masterkey file is too old or too modern. */ - void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException; + void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException, UnsupportedVaultException; /** - * Encrypts each plaintext path component for its own. + * Encrypts a given plaintext path representing a directory structure. See {@link #encryptFilename(String, CryptorMetadataSupport)} for contents inside directories. * - * @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. + * @param cleartextPath A relative path (UTF-8 encoded), whose path components are separated by '/' + * @param nativePathSep Path separator like "/" used on local file system. Must not be null, even if cleartextPath is a sole file name without any path separators. + * @return Encrypted path. */ - String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport); + String encryptDirectoryPath(String cleartextPath, String nativePathSep); /** - * Decrypts each encrypted path component for its own. + * Encrypts the name of a file. See {@link #encryptDirectoryPath(String, char)} for parent dir. * - * @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. + * @param cleartextName A plaintext filename without any preceeding directory paths. + * @param ioSupport Support object allowing the Cryptor to read and write its own metadata to a storage space associated with this support object. + * @return Encrypted filename. + * @throws IOException If ioSupport throws an IOException + */ + String encryptFilename(String cleartextName, CryptorMetadataSupport ioSupport) throws IOException; + + /** + * Decrypts the name of a file. + * + * @param ciphertextName A ciphertext filename without any preceeding directory paths. + * @param ioSupport Support object allowing the Cryptor to read and write its own metadata to a storage space associated with this support object. + * @return Decrypted filename. * @throws DecryptFailedException If the decryption failed for various reasons (including wrong password). + * @throws IOException If ioSupport throws an IOException */ - String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) throws DecryptFailedException; + String decryptFilename(String ciphertextName, CryptorMetadataSupport ioSupport) throws IOException, DecryptFailedException; /** * @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file. * @return Content length of the decrypted file or null if unknown. + * @throws MacAuthenticationFailedException If the MAC auth failed. */ - Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException; + Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException; /** * @return true, if the stored MAC matches the calculated one. @@ -101,13 +106,8 @@ public interface Cryptor extends SensitiveDataSwipeListener { Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException; /** - * @return A filter, that returns true for encrypted files, i.e. if the file is an actual user payload and not a supporting - * metadata file of the {@link Cryptor}. + * @return A filter, that returns true for encrypted files, i.e. if the file is an actual user payload and not a supporting metadata file of the {@link Cryptor}. */ Filter getPayloadFilesFilter(); - void addSensitiveDataSwipeListener(SensitiveDataSwipeListener listener); - - void removeSensitiveDataSwipeListener(SensitiveDataSwipeListener listener); - } diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/CryptorIOSupport.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/CryptorMetadataSupport.java similarity index 56% rename from main/crypto-api/src/main/java/org/cryptomator/crypto/CryptorIOSupport.java rename to main/crypto-api/src/main/java/org/cryptomator/crypto/CryptorMetadataSupport.java index 2709441bc..c3564fb32 100644 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/CryptorIOSupport.java +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/CryptorMetadataSupport.java @@ -13,19 +13,19 @@ import java.io.IOException; /** * Methods that may be called by the Cryptor when accessing a path. */ -public interface CryptorIOSupport { +public interface CryptorMetadataSupport { /** - * Persists encryptedMetadata to the given encryptedPath. + * Persists encryptedMetadata in a metadata group. * - * @param encryptedPath A relative path + * @param metadataFilename File relative to * @throws IOException */ - void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) throws IOException; + void writeMetadata(String metadataGroup, byte[] encryptedMetadata) throws IOException; /** - * @return Previously written encryptedMetadata stored at the given encryptedPath or null if no such file exists. + * @return Previously written metadata stored in the given metadata group or null if no such group exists. */ - byte[] readPathSpecificMetadata(String encryptedPath) throws IOException; + byte[] readMetadata(String metadataGroup) throws IOException; } \ No newline at end of file diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/PathCachingCryptorDecorator.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/PathCachingCryptorDecorator.java new file mode 100644 index 000000000..ba955a799 --- /dev/null +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/PathCachingCryptorDecorator.java @@ -0,0 +1,79 @@ +package org.cryptomator.crypto; + +import java.io.IOException; +import java.util.Map; + +import org.apache.commons.collections4.BidiMap; +import org.apache.commons.collections4.bidimap.AbstractDualBidiMap; +import org.apache.commons.collections4.map.LRUMap; +import org.cryptomator.crypto.exceptions.DecryptFailedException; + +public class PathCachingCryptorDecorator extends AbstractCryptorDecorator { + + private static final int MAX_CACHED_PATHS = 5000; + private static final int MAX_CACHED_NAMES = 5000; + + private final Map pathCache = new LRUMap<>(MAX_CACHED_PATHS); // + private final BidiMap nameCache = new BidiLRUMap<>(MAX_CACHED_NAMES); // + + private PathCachingCryptorDecorator(Cryptor cryptor) { + super(cryptor); + } + + public static Cryptor decorate(Cryptor cryptor) { + return new PathCachingCryptorDecorator(cryptor); + } + + /* Cryptor */ + + @Override + public String encryptDirectoryPath(String cleartextPath, String nativePathSep) { + if (pathCache.containsKey(cleartextPath)) { + return pathCache.get(cleartextPath); + } else { + final String ciphertextPath = cryptor.encryptDirectoryPath(cleartextPath, nativePathSep); + pathCache.put(cleartextPath, ciphertextPath); + return ciphertextPath; + } + } + + @Override + public String encryptFilename(String cleartextName, CryptorMetadataSupport ioSupport) throws IOException { + if (nameCache.containsKey(cleartextName)) { + return nameCache.get(cleartextName); + } else { + final String ciphertextName = cryptor.encryptFilename(cleartextName, ioSupport); + nameCache.put(cleartextName, ciphertextName); + return ciphertextName; + } + } + + @Override + public String decryptFilename(String ciphertextName, CryptorMetadataSupport ioSupport) throws IOException, DecryptFailedException { + if (nameCache.containsValue(ciphertextName)) { + return nameCache.getKey(ciphertextName); + } else { + final String cleartextName = cryptor.decryptFilename(ciphertextName, ioSupport); + nameCache.put(cleartextName, ciphertextName); + return ciphertextName; + } + } + + private static class BidiLRUMap extends AbstractDualBidiMap { + + BidiLRUMap(int maxSize) { + super(new LRUMap(maxSize), new LRUMap(maxSize)); + } + + protected BidiLRUMap(final Map normalMap, final Map reverseMap, final BidiMap inverseBidiMap) { + super(normalMap, reverseMap, inverseBidiMap); + } + + @Override + protected BidiMap createBidiMap(Map normalMap, Map reverseMap, BidiMap inverseMap) { + return new BidiLRUMap(normalMap, reverseMap, inverseMap); + } + + } + +} diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingDecorator.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingCryptorDecorator.java similarity index 56% rename from main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingDecorator.java rename to main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingCryptorDecorator.java index a6461373a..8f056054b 100644 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingDecorator.java +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingCryptorDecorator.java @@ -4,35 +4,24 @@ 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 java.util.concurrent.atomic.AtomicLong; -import org.apache.commons.lang3.StringUtils; import org.cryptomator.crypto.exceptions.DecryptFailedException; import org.cryptomator.crypto.exceptions.EncryptFailedException; -import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; -import org.cryptomator.crypto.exceptions.WrongPasswordException; -public class SamplingDecorator implements Cryptor, CryptorIOSampling { +public class SamplingCryptorDecorator extends AbstractCryptorDecorator implements CryptorIOSampling { - private final Cryptor cryptor; private final AtomicLong encryptedBytes; private final AtomicLong decryptedBytes; - private SamplingDecorator(Cryptor cryptor) { - this.cryptor = cryptor; + private SamplingCryptorDecorator(Cryptor cryptor) { + super(cryptor); encryptedBytes = new AtomicLong(); decryptedBytes = new AtomicLong(); } public static Cryptor decorate(Cryptor cryptor) { - return new SamplingDecorator(cryptor); - } - - @Override - public void swipeSensitiveData() { - cryptor.swipeSensitiveData(); + return new SamplingCryptorDecorator(cryptor); } @Override @@ -55,38 +44,6 @@ public class SamplingDecorator implements Cryptor, CryptorIOSampling { /* Cryptor */ - @Override - public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException { - cryptor.encryptMasterKey(out, password); - } - - @Override - public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException { - cryptor.decryptMasterKey(in, password); - } - - @Override - public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) { - encryptedBytes.addAndGet(StringUtils.length(cleartextPath)); - return cryptor.encryptPath(cleartextPath, encryptedPathSep, cleartextPathSep, ioSupport); - } - - @Override - public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) throws DecryptFailedException { - decryptedBytes.addAndGet(StringUtils.length(encryptedPath)); - return cryptor.decryptPath(encryptedPath, encryptedPathSep, cleartextPathSep, ioSupport); - } - - @Override - public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException { - return cryptor.decryptedContentLength(encryptedFile); - } - - @Override - public boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException { - return cryptor.isAuthentic(encryptedFile); - } - @Override public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException { final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile); @@ -105,21 +62,6 @@ public class SamplingDecorator implements Cryptor, CryptorIOSampling { return cryptor.encryptFile(countingInputStream, encryptedFile); } - @Override - public Filter getPayloadFilesFilter() { - return cryptor.getPayloadFilesFilter(); - } - - @Override - public void addSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) { - cryptor.addSensitiveDataSwipeListener(listener); - } - - @Override - public void removeSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) { - cryptor.removeSensitiveDataSwipeListener(listener); - } - private class CountingInputStream extends InputStream { private final InputStream in; diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/SensitiveDataSwipeListener.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/SensitiveDataSwipeListener.java deleted file mode 100644 index c98cceb99..000000000 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/SensitiveDataSwipeListener.java +++ /dev/null @@ -1,19 +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 org.cryptomator.crypto; - -public interface SensitiveDataSwipeListener { - - /** - * Removes sensitive data from memory. Depending on the data (e.g. for passwords) it might be necessary to overwrite the memory before - * freeing the object. - */ - void swipeSensitiveData(); - -} diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/UnsupportedVaultException.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/UnsupportedVaultException.java new file mode 100644 index 000000000..b2a69ac54 --- /dev/null +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/UnsupportedVaultException.java @@ -0,0 +1,32 @@ +package org.cryptomator.crypto.exceptions; + +public class UnsupportedVaultException extends Exception { + + private static final long serialVersionUID = -5147549533387945622L; + + private final Integer detectedVersion; + private final Integer supportedVersion; + + public UnsupportedVaultException(Integer detectedVersion, Integer supportedVersion) { + super("Tried to open vault of version " + detectedVersion + ", but can only handle version " + supportedVersion); + this.detectedVersion = detectedVersion; + this.supportedVersion = supportedVersion; + } + + public Integer getDetectedVersion() { + return detectedVersion; + } + + public Integer getSupportedVersion() { + return supportedVersion; + } + + public boolean isVaultOlderThanSoftware() { + return detectedVersion == null || detectedVersion < supportedVersion; + } + + public boolean isSoftwareOlderThanVault() { + return detectedVersion > supportedVersion; + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/MainModule.java b/main/ui/src/main/java/org/cryptomator/ui/MainModule.java index bfa815eab..30f3d78bf 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/MainModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/MainModule.java @@ -19,7 +19,7 @@ import javax.inject.Named; import javax.inject.Singleton; import org.cryptomator.crypto.Cryptor; -import org.cryptomator.crypto.SamplingDecorator; +import org.cryptomator.crypto.SamplingCryptorDecorator; import org.cryptomator.crypto.aes256.Aes256Cryptor; import org.cryptomator.ui.MainApplication.MainApplicationReference; import org.cryptomator.ui.model.VaultFactory; @@ -88,7 +88,7 @@ public class MainModule extends AbstractModule { @Provides Cryptor getCryptor() { - return SamplingDecorator.decorate(new Aes256Cryptor()); + return SamplingCryptorDecorator.decorate(new Aes256Cryptor()); } @Provides diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java index 91fa9df66..d13ae94fa 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java @@ -10,16 +10,19 @@ import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.util.ResourceBundle; +import javafx.application.Application; import javafx.application.Platform; import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.Button; -import javafx.scene.control.Label; +import javafx.scene.control.Hyperlink; +import javafx.scene.text.Text; import org.cryptomator.crypto.exceptions.DecryptFailedException; import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; +import org.cryptomator.crypto.exceptions.UnsupportedVaultException; import org.cryptomator.crypto.exceptions.WrongPasswordException; import org.cryptomator.ui.controls.SecPasswordField; import org.cryptomator.ui.model.Vault; @@ -49,11 +52,17 @@ public class ChangePasswordController implements Initializable { private Button changePasswordButton; @FXML - private Label messageLabel; + private Text messageText; + + @FXML + private Hyperlink downloadsPageLink; + + private final Application app; @Inject - public ChangePasswordController() { + public ChangePasswordController(Application app) { super(); + this.app = app; } @Override @@ -76,12 +85,22 @@ public class ChangePasswordController implements Initializable { changePasswordButton.setDisable(oldPasswordIsEmpty || newPasswordIsEmpty || !passwordsAreEqual); } + // **************************************** + // Downloads link + // **************************************** + + @FXML + public void didClickDownloadsLink(ActionEvent event) { + app.getHostServices().showDocument("https://cryptomator.org/downloads/"); + } + // **************************************** // Change password button // **************************************** @FXML private void didClickChangePasswordButton(ActionEvent event) { + downloadsPageLink.setVisible(false); final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE); final Path masterKeyBackupPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_BACKUP_FILE); @@ -91,23 +110,33 @@ public class ChangePasswordController implements Initializable { vault.getCryptor().decryptMasterKey(masterKeyInputStream, oldPassword); Files.copy(masterKeyPath, masterKeyBackupPath, StandardCopyOption.REPLACE_EXISTING); } catch (DecryptFailedException | IOException ex) { - messageLabel.setText(rb.getString("changePassword.errorMessage.decryptionFailed")); + messageText.setText(rb.getString("changePassword.errorMessage.decryptionFailed")); LOG.error("Decryption failed for technical reasons.", ex); newPasswordField.swipe(); retypePasswordField.swipe(); return; } catch (WrongPasswordException e) { - messageLabel.setText(rb.getString("changePassword.errorMessage.wrongPassword")); + messageText.setText(rb.getString("changePassword.errorMessage.wrongPassword")); newPasswordField.swipe(); retypePasswordField.swipe(); Platform.runLater(oldPasswordField::requestFocus); return; } catch (UnsupportedKeyLengthException ex) { - messageLabel.setText(rb.getString("changePassword.errorMessage.unsupportedKeyLengthInstallJCE")); + messageText.setText(rb.getString("changePassword.errorMessage.unsupportedKeyLengthInstallJCE")); LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex); newPasswordField.swipe(); retypePasswordField.swipe(); return; + } catch (UnsupportedVaultException e) { + downloadsPageLink.setVisible(true); + if (e.isVaultOlderThanSoftware()) { + messageText.setText(rb.getString("changePassword.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " "); + } else if (e.isSoftwareOlderThanVault()) { + messageText.setText(rb.getString("changePassword.errorMessage.unsupportedVersion.softwareOlderThanVault") + " "); + } + newPasswordField.swipe(); + retypePasswordField.swipe(); + return; } finally { oldPasswordField.swipe(); } @@ -118,7 +147,7 @@ public class ChangePasswordController implements Initializable { final CharSequence newPassword = newPasswordField.getCharacters(); try (final OutputStream masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.SYNC)) { vault.getCryptor().encryptMasterKey(masterKeyOutputStream, newPassword); - messageLabel.setText(rb.getString("changePassword.infoMessage.success")); + messageText.setText(rb.getString("changePassword.infoMessage.success")); Platform.runLater(this::didChangePassword); // At this point the backup is still using the old password. // It will be changed as soon as the user unlocks the vault the next time. diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java index 0d392b4db..a7fa2a0a4 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java @@ -12,6 +12,7 @@ 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; @@ -78,6 +79,11 @@ public class InitializeController implements Initializable { final CharSequence password = passwordField.getCharacters(); try (OutputStream masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) { vault.getCryptor().encryptMasterKey(masterKeyOutputStream, password); + final String dataRootDir = vault.getCryptor().encryptDirectoryPath("", FileSystems.getDefault().getSeparator()); + final Path dataRootPath = vault.getPath().resolve("d").resolve(dataRootDir); + final Path metadataPath = vault.getPath().resolve("m"); + Files.createDirectories(dataRootPath); + Files.createDirectories(metadataPath); if (listener != null) { listener.didInitialize(this); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java index 1a708e30f..9155128c9 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java @@ -19,20 +19,25 @@ import java.util.ResourceBundle; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; +import javafx.application.Application; import javafx.application.Platform; import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.Button; -import javafx.scene.control.Label; +import javafx.scene.control.Hyperlink; import javafx.scene.control.ProgressIndicator; import javafx.scene.control.TextField; import javafx.scene.input.KeyEvent; +import javafx.scene.text.Text; + +import javax.security.auth.DestroyFailedException; import org.apache.commons.lang3.CharUtils; import org.cryptomator.crypto.exceptions.DecryptFailedException; import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; +import org.cryptomator.crypto.exceptions.UnsupportedVaultException; import org.cryptomator.crypto.exceptions.WrongPasswordException; import org.cryptomator.ui.controls.SecPasswordField; import org.cryptomator.ui.model.Vault; @@ -63,13 +68,18 @@ public class UnlockController implements Initializable { private ProgressIndicator progressIndicator; @FXML - private Label messageLabel; + private Text messageText; + + @FXML + private Hyperlink downloadsPageLink; private final ExecutorService exec; + private final Application app; @Inject - public UnlockController(ExecutorService exec) { + public UnlockController(Application app, ExecutorService exec) { super(); + this.app = app; this.exec = exec; } @@ -91,6 +101,15 @@ public class UnlockController implements Initializable { unlockButton.setDisable(passwordIsEmpty); } + // **************************************** + // Downloads link + // **************************************** + + @FXML + public void didClickDownloadsLink(ActionEvent event) { + app.getHostServices().showDocument("https://cryptomator.org/downloads/"); + } + // **************************************** // Unlock button // **************************************** @@ -99,14 +118,15 @@ public class UnlockController implements Initializable { private void didClickUnlockButton(ActionEvent event) { setControlsDisabled(true); progressIndicator.setVisible(true); + downloadsPageLink.setVisible(false); final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE); final Path masterKeyBackupPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_BACKUP_FILE); final CharSequence password = passwordField.getCharacters(); try (final InputStream masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ)) { vault.getCryptor().decryptMasterKey(masterKeyInputStream, password); if (!vault.startServer()) { - messageLabel.setText(rb.getString("unlock.messageLabel.startServerFailed")); - vault.getCryptor().swipeSensitiveData(); + messageText.setText(rb.getString("unlock.messageLabel.startServerFailed")); + vault.getCryptor().destroy(); return; } // at this point we know for sure, that the masterkey can be decrypted, so lets make a backup: @@ -117,18 +137,31 @@ public class UnlockController implements Initializable { } catch (DecryptFailedException | IOException ex) { setControlsDisabled(false); progressIndicator.setVisible(false); - messageLabel.setText(rb.getString("unlock.errorMessage.decryptionFailed")); + messageText.setText(rb.getString("unlock.errorMessage.decryptionFailed")); LOG.error("Decryption failed for technical reasons.", ex); } catch (WrongPasswordException e) { setControlsDisabled(false); progressIndicator.setVisible(false); - messageLabel.setText(rb.getString("unlock.errorMessage.wrongPassword")); + messageText.setText(rb.getString("unlock.errorMessage.wrongPassword")); Platform.runLater(passwordField::requestFocus); } catch (UnsupportedKeyLengthException ex) { setControlsDisabled(false); progressIndicator.setVisible(false); - messageLabel.setText(rb.getString("unlock.errorMessage.unsupportedKeyLengthInstallJCE")); + messageText.setText(rb.getString("unlock.errorMessage.unsupportedKeyLengthInstallJCE")); LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex); + } catch (UnsupportedVaultException e) { + setControlsDisabled(false); + progressIndicator.setVisible(false); + downloadsPageLink.setVisible(true); + if (e.isVaultOlderThanSoftware()) { + messageText.setText(rb.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " "); + } else if (e.isSoftwareOlderThanVault()) { + messageText.setText(rb.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " "); + } + } catch (DestroyFailedException e) { + setControlsDisabled(false); + progressIndicator.setVisible(false); + LOG.error("Destruction of cryptor threw an exception.", e); } finally { passwordField.swipe(); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java index f07363605..0b3f39d49 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java @@ -34,12 +34,16 @@ import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.cookie.CookiePolicy; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.lang3.SystemUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; public class WelcomeController implements Initializable { + private static final Logger LOG = LoggerFactory.getLogger(WelcomeController.class); + @FXML private ImageView botImageView; @@ -97,6 +101,7 @@ public class WelcomeController implements Initializable { return; } final String currentVersion = WelcomeController.class.getPackage().getImplementationVersion(); + LOG.debug("Current version: {}, lastest version: {}", currentVersion, latestVersion); if (currentVersion != null && semVerComparator.compare(currentVersion, latestVersion) < 0) { final String msg = String.format(rb.getString("welcome.newVersionMessage"), latestVersion, currentVersion); Platform.runLater(() -> { diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java index 9b20e48cc..2a65ad2aa 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java @@ -13,6 +13,8 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableSet; +import javax.security.auth.DestroyFailedException; + import org.apache.commons.lang3.StringUtils; import org.cryptomator.crypto.Cryptor; import org.cryptomator.ui.util.DeferredClosable; @@ -94,7 +96,11 @@ public class Vault implements Serializable { LOG.warn("Unmounting failed. Locking anyway...", e); } webDavServlet.close(); - cryptor.swipeSensitiveData(); + try { + cryptor.destroy(); + } catch (DestroyFailedException e) { + LOG.error("Destruction of cryptor throw an exception.", e); + } setUnlocked(false); namesOfResourcesWithInvalidMac.clear(); } diff --git a/main/ui/src/main/resources/fxml/change_password.fxml b/main/ui/src/main/resources/fxml/change_password.fxml index f29b657ea..36db54e94 100644 --- a/main/ui/src/main/resources/fxml/change_password.fxml +++ b/main/ui/src/main/resources/fxml/change_password.fxml @@ -17,6 +17,9 @@ + + + @@ -43,10 +46,15 @@ -