mirror of
https://github.com/cryptomator/cryptomator.git
synced 2025-02-17 00:29:00 +00:00
Merge pull request #54 from cryptomator/flatDirectoryStructure
Flat directory structure
This commit is contained in:
commit
f36a61df1c
@ -64,9 +64,5 @@
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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<K, V> extends AbstractDualBidiMap<K, V> {
|
||||
|
||||
BidiLRUMap(int maxSize) {
|
||||
super(new LRUMap<K, V>(maxSize), new LRUMap<V, K>(maxSize));
|
||||
}
|
||||
|
||||
protected BidiLRUMap(final Map<K, V> normalMap, final Map<V, K> reverseMap, final BidiMap<V, K> inverseBidiMap) {
|
||||
super(normalMap, reverseMap, inverseBidiMap);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BidiMap<V, K> createBidiMap(Map<V, K> normalMap, Map<K, V> reverseMap, BidiMap<K, V> inverseMap) {
|
||||
return new BidiLRUMap<V, K>(normalMap, reverseMap, inverseMap);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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<String, String> pathCache = new BidiLRUMap<>(MAX_CACHED_PATHS); // <decryptedPath, encryptedPath>
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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<Path> directoryStream = Files.newDirectoryStream(dir, cryptor.getPayloadFilesFilter());
|
||||
final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(locator.getEncryptedDirectoryPath(), cryptor.getPayloadFilesFilter());
|
||||
final List<DavResource> 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<Integer>(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<Path> {
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
|
||||
if (attributes.isRegularFile()) {
|
||||
Files.delete(file);
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
Files.delete(dir);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
|
||||
LOG.error("Failed to delete file " + file.toString(), exc);
|
||||
return FileVisitResult.TERMINATE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<Pair<Long, Long>> requestedContentRanges = new HashSet<Pair<Long, Long>>();
|
||||
|
||||
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);
|
||||
|
@ -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.");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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<String> 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.<br/>
|
||||
* 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.<br/>
|
||||
* This means that we need a workaround for filenames longer than the limit defined in
|
||||
* {@link FileNamingConventions#ENCRYPTED_FILENAME_LENGTH_LIMIT}.<br/>
|
||||
* 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.<br/>
|
||||
* This means that we need a workaround for filenames longer than the limit defined in {@link FileNamingConventions#ENCRYPTED_FILENAME_LENGTH_LIMIT}.<br/>
|
||||
* <br/>
|
||||
* 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.<br/>
|
||||
* <br/>
|
||||
* 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<String> 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<Path>() {
|
||||
@Override
|
||||
public boolean accept(Path entry) throws IOException {
|
||||
return ENCRYPTED_FILE_GLOB_MATCHER.matches(entry);
|
||||
return ENCRYPTED_FILE_MATCHER.matches(entry);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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.<br/>
|
||||
* <strong>Important</strong>: 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.<br/>
|
||||
*
|
||||
* @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).<br/>
|
||||
*
|
||||
* @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;
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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).<br/>
|
||||
* From https://tools.ietf.org/html/rfc3686: <cite> Using the encryption process described in section 2.1, this construction permits each packet to consist of up to: (2^32)-1 blocks</cite>
|
||||
*/
|
||||
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);
|
||||
|
@ -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.<br/>
|
||||
* 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.<br/>
|
||||
* 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.<br/>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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<String, byte[]> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -27,5 +27,9 @@
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
@ -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<SensitiveDataSwipeListener> 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);
|
||||
}
|
||||
|
||||
}
|
@ -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<Path> getPayloadFilesFilter() {
|
||||
return cryptor.getPayloadFilesFilter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws DestroyFailedException {
|
||||
cryptor.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
return cryptor.isDestroyed();
|
||||
}
|
||||
|
||||
}
|
@ -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 <code>null</code> 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 <code>true</code> for encrypted files, i.e. if the file is an actual user payload and not a supporting
|
||||
* metadata file of the {@link Cryptor}.
|
||||
* @return A filter, that returns <code>true</code> for encrypted files, i.e. if the file is an actual user payload and not a supporting metadata file of the {@link Cryptor}.
|
||||
*/
|
||||
Filter<Path> getPayloadFilesFilter();
|
||||
|
||||
void addSensitiveDataSwipeListener(SensitiveDataSwipeListener listener);
|
||||
|
||||
void removeSensitiveDataSwipeListener(SensitiveDataSwipeListener listener);
|
||||
|
||||
}
|
||||
|
@ -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 <code>null</code> if no such file exists.
|
||||
* @return Previously written metadata stored in the given metadata group or <code>null</code> if no such group exists.
|
||||
*/
|
||||
byte[] readPathSpecificMetadata(String encryptedPath) throws IOException;
|
||||
byte[] readMetadata(String metadataGroup) throws IOException;
|
||||
|
||||
}
|
@ -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<String, String> pathCache = new LRUMap<>(MAX_CACHED_PATHS); // <cleartextPath, ciphertextPath>
|
||||
private final BidiMap<String, String> nameCache = new BidiLRUMap<>(MAX_CACHED_NAMES); // <cleartextName, ciphertextName>
|
||||
|
||||
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<K, V> extends AbstractDualBidiMap<K, V> {
|
||||
|
||||
BidiLRUMap(int maxSize) {
|
||||
super(new LRUMap<K, V>(maxSize), new LRUMap<V, K>(maxSize));
|
||||
}
|
||||
|
||||
protected BidiLRUMap(final Map<K, V> normalMap, final Map<V, K> reverseMap, final BidiMap<V, K> inverseBidiMap) {
|
||||
super(normalMap, reverseMap, inverseBidiMap);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BidiMap<V, K> createBidiMap(Map<V, K> normalMap, Map<K, V> reverseMap, BidiMap<K, V> inverseMap) {
|
||||
return new BidiLRUMap<V, K>(normalMap, reverseMap, inverseMap);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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<Path> 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;
|
@ -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();
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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(() -> {
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -17,6 +17,9 @@
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.layout.ColumnConstraints?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.text.TextFlow?>
|
||||
<?import javafx.scene.control.Hyperlink?>
|
||||
<?import javafx.scene.text.Text?>
|
||||
|
||||
|
||||
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.controllers.ChangePasswordController" xmlns:fx="http://javafx.com/fxml">
|
||||
@ -43,10 +46,15 @@
|
||||
<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="2" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
|
||||
|
||||
<!-- Row 3 -->
|
||||
<Button fx:id="changePasswordButton" text="%unlock.button.unlock" defaultButton="true" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickChangePasswordButton" disable="true"/>
|
||||
<Button fx:id="changePasswordButton" text="%changePassword.button.change" defaultButton="true" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickChangePasswordButton" disable="true"/>
|
||||
|
||||
<!-- Row 4 -->
|
||||
<Label fx:id="messageLabel" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" />
|
||||
<TextFlow GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2">
|
||||
<children>
|
||||
<Text fx:id="messageText" />
|
||||
<Hyperlink fx:id="downloadsPageLink" text="%changePassword.label.downloadsPageLink" visible="false" onAction="#didClickDownloadsLink" />
|
||||
</children>
|
||||
</TextFlow>
|
||||
</children>
|
||||
</GridPane>
|
||||
|
||||
|
@ -18,6 +18,9 @@
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.layout.ColumnConstraints?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.text.TextFlow?>
|
||||
<?import javafx.scene.control.Hyperlink?>
|
||||
<?import javafx.scene.text.Text?>
|
||||
|
||||
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.controllers.UnlockController" xmlns:fx="http://javafx.com/fxml">
|
||||
<padding>
|
||||
@ -45,7 +48,12 @@
|
||||
<ProgressIndicator progress="-1" fx:id="progressIndicator" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="CENTER" visible="false"/>
|
||||
|
||||
<!-- Row 4 -->
|
||||
<Label fx:id="messageLabel" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" />
|
||||
<TextFlow GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" >
|
||||
<children>
|
||||
<Text fx:id="messageText" />
|
||||
<Hyperlink fx:id="downloadsPageLink" text="%unlock.label.downloadsPageLink" visible="false" onAction="#didClickDownloadsLink" />
|
||||
</children>
|
||||
</TextFlow>
|
||||
</children>
|
||||
</GridPane>
|
||||
|
||||
|
@ -28,20 +28,26 @@ initialize.button.ok=Create vault
|
||||
# unlock.fxml
|
||||
unlock.label.password=Password
|
||||
unlock.label.mountName=Drive name
|
||||
unlock.label.downloadsPageLink=All Cryptomator versions
|
||||
unlock.button.unlock=Unlock vault
|
||||
unlock.errorMessage.wrongPassword=Wrong password.
|
||||
unlock.errorMessage.decryptionFailed=Decryption failed.
|
||||
unlock.errorMessage.unsupportedKeyLengthInstallJCE=Decryption failed. Please install Oracle JCE Unlimited Strength Policy.
|
||||
unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware=Unsupported vault. This vault has been created with an older version of Cryptomator.
|
||||
unlock.errorMessage.unsupportedVersion.softwareOlderThanVault=Unsupported vault. This vault has been created with a newer version of Cryptomator.
|
||||
unlock.messageLabel.startServerFailed=Starting WebDAV server failed.
|
||||
|
||||
# change_password.fxml
|
||||
changePassword.label.oldPassword=Old password
|
||||
changePassword.label.newPassword=New password
|
||||
changePassword.label.retypePassword=Retype password
|
||||
changePassword.button.unlock=Change password
|
||||
changePassword.label.downloadsPageLink=All Cryptomator versions
|
||||
changePassword.button.change=Change password
|
||||
changePassword.errorMessage.wrongPassword=Wrong password.
|
||||
changePassword.errorMessage.decryptionFailed=Decryption failed.
|
||||
changePassword.errorMessage.unsupportedKeyLengthInstallJCE=Decryption failed. Please install Oracle JCE Unlimited Strength Policy.
|
||||
changePassword.errorMessage.unsupportedVersion.vaultOlderThanSoftware=Unsupported vault. This vault has been created with an older version of Cryptomator.
|
||||
changePassword.errorMessage.unsupportedVersion.softwareOlderThanVault=Unsupported vault. This vault has been created with a newer version of Cryptomator.
|
||||
changePassword.infoMessage.success=Password changed.
|
||||
|
||||
# unlocked.fxml
|
||||
|
Loading…
x
Reference in New Issue
Block a user