Skip to content

Commit

Permalink
Merge branch 'release/1.2.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
overheadhunter committed Apr 4, 2017
2 parents 186968f + d86da82 commit 831cc0a
Show file tree
Hide file tree
Showing 9 changed files with 422 additions and 40 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
![cryptomator](cryptomator.png)

[![Build Status](https://travis-ci.org/cryptomator/cryptofs.svg?branch=develop)](https://travis-ci.org/cryptomator/cryptofs)
[![codecov](https://codecov.io/gh/cryptomator/cryptofs/branch/develop/graph/badge.svg)](https://codecov.io/gh/cryptomator/cryptofs)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/7248ca7d466843f785f79f33374302c2)](https://www.codacy.com/app/cryptomator/cryptofs)
[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/7248ca7d466843f785f79f33374302c2)](https://www.codacy.com/app/cryptomator/cryptofs?utm_source=github.com&utm_medium=referral&utm_content=cryptomator/cryptofs&utm_campaign=Badge_Coverage)
[![Coverity Scan Build Status](https://scan.coverity.com/projects/10006/badge.svg)](https://scan.coverity.com/projects/cryptomator-cryptofs)

**CryptoFS** - Implementation of the [Cryptomator](https://github.com/cryptomator/cryptomator) encryption scheme.
Expand Down
10 changes: 5 additions & 5 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>cryptofs</artifactId>
<version>1.1.2</version>
<version>1.2.0</version>
<name>Cryptomator Crypto Filesystem</name>
<description>This library provides the Java filesystem provider used by Cryptomator.</description>
<url>https://github.com/cryptomator/cryptofs</url>
Expand All @@ -16,10 +16,10 @@
<properties>
<java.version>1.8</java.version>
<cryptolib.version>1.1.1</cryptolib.version>
<dagger.version>2.9</dagger.version>
<dagger.version>2.10</dagger.version>
<guava.version>21.0</guava.version>
<commons.lang.version>3.5</commons.lang.version>
<slf4j.version>1.7.24</slf4j.version>
<slf4j.version>1.7.25</slf4j.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

Expand Down Expand Up @@ -114,7 +114,7 @@
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.7.12</version>
<version>2.7.21</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down Expand Up @@ -218,7 +218,7 @@
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.5.0</version>
<version>1.6.0</version>
<executions>
<execution>
<phase>verify</phase>
Expand Down
175 changes: 175 additions & 0 deletions src/main/java/org/cryptomator/cryptofs/ConflictResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package org.cryptomator.cryptofs;

import static org.cryptomator.cryptofs.Constants.DIR_PREFIX;
import static org.cryptomator.cryptofs.Constants.NAME_SHORTENING_THRESHOLD;
import static org.cryptomator.cryptofs.LongFileNameProvider.LONG_NAME_FILE_EXT;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.inject.Inject;

import org.apache.commons.lang3.StringUtils;
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.Cryptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@PerFileSystem
class ConflictResolver {

private static final Logger LOG = LoggerFactory.getLogger(ConflictResolver.class);
private static final Pattern BASE32_PATTERN = Pattern.compile("0?(([A-Z2-7]{8})*[A-Z2-7=]{8})");
private static final int MAX_DIR_FILE_SIZE = 87; // "normal" file header has 88 bytes

private final LongFileNameProvider longFileNameProvider;
private final Cryptor cryptor;

@Inject
public ConflictResolver(LongFileNameProvider longFileNameProvider, Cryptor cryptor) {
this.longFileNameProvider = longFileNameProvider;
this.cryptor = cryptor;
}

/**
* Checks if the name of the file represented by the given ciphertextPath is a valid ciphertext name without any additional chars.
* If any unexpected chars are found on the name but it still contains an authentic ciphertext, it is considered a conflicting file.
* Conflicting files will be given a new name. The caller must use the path returned by this function after invoking it, as the given ciphertextPath might be no longer valid.
*
* @param ciphertextPath The path to a file to check.
* @param dirId The directory id of the file's parent directory.
* @return Either the original name if no unexpected chars have been found or a completely new path.
* @throws IOException
*/
public Path resolveConflictsIfNecessary(Path ciphertextPath, String dirId) throws IOException {
String ciphertextFileName = ciphertextPath.getFileName().toString();
String basename = StringUtils.removeEnd(ciphertextFileName, LONG_NAME_FILE_EXT);
Matcher m = BASE32_PATTERN.matcher(basename);
if (!m.matches() && m.find(0)) {
// no full match, but still contains base32 -> partial match
return resolveConflict(ciphertextPath, m.group(1), dirId);
} else {
// full match or no match at all -> nothing to resolve
return ciphertextPath;
}
}

/**
* Resolves a conflict.
*
* @param conflictingPath The path of a file containing a valid base 32 part.
* @param base32match The base32 part inside the filename of the conflicting file.
* @param dirId The directory id of the file's parent directory.
* @return The new path of the conflicting file after the conflict has been resolved.
* @throws IOException
*/
private Path resolveConflict(Path conflictingPath, String base32match, String dirId) throws IOException {
final Path directory = conflictingPath.getParent();
final String originalFileName = conflictingPath.getFileName().toString();
final String ciphertext;
final boolean isDirectory;
final String dirPrefix;
final Path canonicalPath;
if (LongFileNameProvider.isDeflated(originalFileName)) {
String inflated = longFileNameProvider.inflate(base32match + LONG_NAME_FILE_EXT);
ciphertext = StringUtils.removeStart(inflated, DIR_PREFIX);
isDirectory = inflated.startsWith(DIR_PREFIX);
dirPrefix = isDirectory ? DIR_PREFIX : "";
canonicalPath = directory.resolve(base32match + LONG_NAME_FILE_EXT);
} else {
ciphertext = base32match;
isDirectory = originalFileName.startsWith(DIR_PREFIX);
dirPrefix = isDirectory ? DIR_PREFIX : "";
canonicalPath = directory.resolve(dirPrefix + ciphertext);
}

if (isDirectory && resolveDirectoryConflictTrivially(canonicalPath, conflictingPath)) {
return canonicalPath;
} else {
return renameConflictingFile(canonicalPath, conflictingPath, ciphertext, dirId, dirPrefix);
}
}

/**
* Resolves a conflict by renaming the conflicting file.
*
* @param canonicalPath The path to the original (conflict-free) file.
* @param conflictingPath The path to the potentially conflicting file.
* @param ciphertext The (previously inflated) ciphertext name of the file without any preceeding directory prefix.
* @param dirId The directory id of the file's parent directory.
* @param dirPrefix The directory prefix (if the conflicting file is a directory file) or an empty string.
* @return The new path after renaming the conflicting file.
* @throws IOException
*/
private Path renameConflictingFile(Path canonicalPath, Path conflictingPath, String ciphertext, String dirId, String dirPrefix) throws IOException {
try {
String cleartext = cryptor.fileNameCryptor().decryptFilename(ciphertext, dirId.getBytes(StandardCharsets.UTF_8));
Path alternativePath = canonicalPath;
for (int i = 1; Files.exists(alternativePath); i++) {
String alternativeCleartext = cleartext + " (Conflict " + i + ")";
String alternativeCiphertext = cryptor.fileNameCryptor().encryptFilename(alternativeCleartext, dirId.getBytes(StandardCharsets.UTF_8));
String alternativeCiphertextFileName = dirPrefix + alternativeCiphertext;
if (alternativeCiphertextFileName.length() >= NAME_SHORTENING_THRESHOLD) {
alternativeCiphertextFileName = longFileNameProvider.deflate(alternativeCiphertextFileName);
}
alternativePath = canonicalPath.resolveSibling(alternativeCiphertextFileName);
}
LOG.info("Moving conflicting file {} to {}", conflictingPath, alternativePath);
return Files.move(conflictingPath, alternativePath, StandardCopyOption.ATOMIC_MOVE);
} catch (AuthenticationFailedException e) {
// not decryptable, no need to resolve any kind of conflict
LOG.info("Found valid Base32 string, which is an unauthentic ciphertext: {}", conflictingPath);
return conflictingPath;
}
}

/**
* Tries to resolve a conflicting directory file without renaming the file. If successful, only the file with the canonical path will exist afterwards.
*
* @param canonicalPath The path to the original (conflict-free) directory file (must not exist).
* @param conflictingPath The path to the potentially conflicting file (known to exist).
* @return <code>true</code> if the conflict has been resolved.
* @throws IOException
*/
private boolean resolveDirectoryConflictTrivially(Path canonicalPath, Path conflictingPath) throws IOException {
if (!Files.exists(canonicalPath)) {
Files.move(conflictingPath, canonicalPath, StandardCopyOption.ATOMIC_MOVE);
return true;
} else if (hasSameDirFileContent(conflictingPath, canonicalPath)) {
// there must not be two directories pointing to the same dirId.
LOG.info("Removing conflicting directory file {} (identical to {})", conflictingPath, canonicalPath);
Files.deleteIfExists(conflictingPath);
return true;
} else {
return false;
}
}

/**
* @param conflictingPath Path to a potentially conflicting file supposedly containing a directory id
* @param canonicalPath Path to the canonical file containing a directory id
* @return <code>true</code> if the first {@value #MAX_DIR_FILE_SIZE} bytes are equal in both files.
* @throws IOException If an I/O exception occurs while reading either file.
*/
private boolean hasSameDirFileContent(Path conflictingPath, Path canonicalPath) throws IOException {
try (ReadableByteChannel in1 = Files.newByteChannel(conflictingPath, StandardOpenOption.READ); //
ReadableByteChannel in2 = Files.newByteChannel(canonicalPath, StandardOpenOption.READ)) {
ByteBuffer buf1 = ByteBuffer.allocate(MAX_DIR_FILE_SIZE);
ByteBuffer buf2 = ByteBuffer.allocate(MAX_DIR_FILE_SIZE);
in1.read(buf1);
in2.read(buf2);
buf1.flip();
buf2.flip();
return buf1.compareTo(buf2) == 0;
}
}

}
31 changes: 21 additions & 10 deletions src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.cryptomator.cryptofs.CryptoPathMapper.Directory;
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.FileNameCryptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Iterators;

class CryptoDirectoryStream implements DirectoryStream<Path> {

private static final Pattern BASE32_PATTERN = Pattern.compile("^0?(([A-Z2-7]{8})*[A-Z2-7=]{8})");
Expand All @@ -38,12 +38,13 @@ class CryptoDirectoryStream implements DirectoryStream<Path> {
private final Path cleartextDir;
private final FileNameCryptor filenameCryptor;
private final LongFileNameProvider longFileNameProvider;
private final ConflictResolver conflictResolver;
private final DirectoryStream.Filter<? super Path> filter;
private final Consumer<CryptoDirectoryStream> onClose;
private final FinallyUtil finallyUtil;

public CryptoDirectoryStream(Directory ciphertextDir, Path cleartextDir, FileNameCryptor filenameCryptor, LongFileNameProvider longFileNameProvider, DirectoryStream.Filter<? super Path> filter,
Consumer<CryptoDirectoryStream> onClose, FinallyUtil finallyUtil) throws IOException {
public CryptoDirectoryStream(Directory ciphertextDir, Path cleartextDir, FileNameCryptor filenameCryptor, LongFileNameProvider longFileNameProvider, ConflictResolver conflictResolver,
DirectoryStream.Filter<? super Path> filter, Consumer<CryptoDirectoryStream> onClose, FinallyUtil finallyUtil) throws IOException {
this.onClose = onClose;
this.finallyUtil = finallyUtil;
this.directoryId = ciphertextDir.dirId;
Expand All @@ -52,17 +53,27 @@ public CryptoDirectoryStream(Directory ciphertextDir, Path cleartextDir, FileNam
this.cleartextDir = cleartextDir;
this.filenameCryptor = filenameCryptor;
this.longFileNameProvider = longFileNameProvider;
this.conflictResolver = conflictResolver;
this.filter = filter;
}

@Override
public Iterator<Path> iterator() {
Iterator<Path> ciphertextPathIter = ciphertextDirStream.iterator();
Iterator<Path> longCiphertextPathOrNullIter = Iterators.transform(ciphertextPathIter, this::inflateIfNeeded);
Iterator<Path> longCiphertextPathIter = Iterators.filter(longCiphertextPathOrNullIter, Objects::nonNull);
Iterator<Path> cleartextPathOrNullIter = Iterators.transform(longCiphertextPathIter, this::decrypt);
Iterator<Path> cleartextPathIter = Iterators.filter(cleartextPathOrNullIter, Objects::nonNull);
return Iterators.filter(cleartextPathIter, this::isAcceptableByFilter);
Stream<Path> pathIter = StreamSupport.stream(ciphertextDirStream.spliterator(), false);
Stream<Path> resolved = pathIter.map(this::resolveConflictingFileIfNeeded).filter(Objects::nonNull);
Stream<Path> inflated = resolved.map(this::inflateIfNeeded).filter(Objects::nonNull);
Stream<Path> decrypted = inflated.map(this::decrypt).filter(Objects::nonNull);
Stream<Path> filtered = decrypted.filter(this::isAcceptableByFilter);
return filtered.iterator();
}

private Path resolveConflictingFileIfNeeded(Path potentiallyConflictingPath) {
try {
return conflictResolver.resolveConflictsIfNecessary(potentiallyConflictingPath, directoryId);
} catch (IOException e) {
LOG.warn("I/O exception while finding potentially conflicting file versions for {}.", potentiallyConflictingPath);
return null;
}
}

private Path inflateIfNeeded(Path ciphertextPath) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class DirectoryStreamFactory {

private final Cryptor cryptor;
private final LongFileNameProvider longFileNameProvider;
private final ConflictResolver conflictResolver;
private final CryptoPathMapper cryptoPathMapper;
private final FinallyUtil finallyUtil;

Expand All @@ -26,9 +27,10 @@ class DirectoryStreamFactory {
private volatile boolean closed = false;

@Inject
public DirectoryStreamFactory(Cryptor cryptor, LongFileNameProvider longFileNameProvider, CryptoPathMapper cryptoPathMapper, FinallyUtil finallyUtil) {
public DirectoryStreamFactory(Cryptor cryptor, LongFileNameProvider longFileNameProvider, ConflictResolver conflictResolver, CryptoPathMapper cryptoPathMapper, FinallyUtil finallyUtil) {
this.cryptor = cryptor;
this.longFileNameProvider = longFileNameProvider;
this.conflictResolver = conflictResolver;
this.cryptoPathMapper = cryptoPathMapper;
this.finallyUtil = finallyUtil;
}
Expand All @@ -40,6 +42,7 @@ public DirectoryStream<Path> newDirectoryStream(CryptoPath cleartextDir, Filter<
cleartextDir, //
cryptor.fileNameCryptor(), //
longFileNameProvider, //
conflictResolver, //
filter, //
closed -> streams.remove(closed), //
finallyUtil);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class LongFileNameProvider {

private static final BaseEncoding BASE32 = BaseEncoding.base32();
private static final int MAX_CACHE_SIZE = 5000;
private static final String LONG_NAME_FILE_EXT = ".lng";
public static final String LONG_NAME_FILE_EXT = ".lng";

private final Path metadataRoot;
private final LoadingCache<String, String> ids;
Expand Down
Loading

0 comments on commit 831cc0a

Please sign in to comment.