Skip to content

Commit

Permalink
Merge branch 'release/1.8.5'
Browse files Browse the repository at this point in the history
  • Loading branch information
overheadhunter committed May 22, 2019
2 parents e2edee0 + b1c723d commit 8b9d605
Show file tree
Hide file tree
Showing 23 changed files with 272 additions and 161 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
target/
test-output/

# Maven #
target/
pom.xml.versionsBackup

# IntelliJ Settings Files #
.idea/
out/
Expand Down
2 changes: 1 addition & 1 deletion 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.8.4</version>
<version>1.8.5</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 Down
4 changes: 3 additions & 1 deletion src/main/java/org/cryptomator/cryptofs/ConflictResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,9 @@ private Path renameConflictingFile(Path canonicalPath, Path conflictingPath, Str
alternativePath = canonicalPath.resolveSibling(alternativeCiphertextFileName);
}
LOG.info("Moving conflicting file {} to {}", conflictingPath, alternativePath);
return Files.move(conflictingPath, alternativePath, StandardCopyOption.ATOMIC_MOVE);
Path resolved = Files.move(conflictingPath, alternativePath, StandardCopyOption.ATOMIC_MOVE);
longFileNameProvider.persistCachedIfDeflated(resolved);
return resolved;
} 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);
Expand Down
16 changes: 14 additions & 2 deletions src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class CryptoFileSystemImpl extends CryptoFileSystem {
private final CryptoFileStore fileStore;
private final CryptoFileSystemStats stats;
private final CryptoPathMapper cryptoPathMapper;
private final LongFileNameProvider longFileNameProvider;
private final CryptoPathFactory cryptoPathFactory;
private final PathMatcherFactory pathMatcherFactory;
private final DirectoryStreamFactory directoryStreamFactory;
Expand All @@ -95,7 +96,7 @@ class CryptoFileSystemImpl extends CryptoFileSystem {

@Inject
public CryptoFileSystemImpl(CryptoFileSystemProvider provider, CryptoFileSystems cryptoFileSystems, @PathToVault Path pathToVault, Cryptor cryptor,
CryptoFileStore fileStore, CryptoFileSystemStats stats, CryptoPathMapper cryptoPathMapper, CryptoPathFactory cryptoPathFactory,
CryptoFileStore fileStore, CryptoFileSystemStats stats, CryptoPathMapper cryptoPathMapper, LongFileNameProvider longFileNameProvider, CryptoPathFactory cryptoPathFactory,
PathMatcherFactory pathMatcherFactory, DirectoryStreamFactory directoryStreamFactory, DirectoryIdProvider dirIdProvider,
AttributeProvider fileAttributeProvider, AttributeByNameProvider fileAttributeByNameProvider, AttributeViewProvider fileAttributeViewProvider,
OpenCryptoFiles openCryptoFiles, Symlinks symlinks, FinallyUtil finallyUtil, CiphertextDirectoryDeleter ciphertextDirDeleter, ReadonlyFlag readonlyFlag, RootDirectoryInitializer rootDirectoryInitializer) {
Expand All @@ -106,6 +107,7 @@ public CryptoFileSystemImpl(CryptoFileSystemProvider provider, CryptoFileSystems
this.fileStore = fileStore;
this.stats = stats;
this.cryptoPathMapper = cryptoPathMapper;
this.longFileNameProvider = longFileNameProvider;
this.cryptoPathFactory = cryptoPathFactory;
this.pathMatcherFactory = pathMatcherFactory;
this.directoryStreamFactory = directoryStreamFactory;
Expand Down Expand Up @@ -302,6 +304,7 @@ void createDirectory(CryptoPath cleartextDir, FileAttribute<?>... attrs) throws
// create dir if and only if the dirFile has been created right now (not if it has been created before):
try {
Files.createDirectories(ciphertextDir.path);
longFileNameProvider.persistCachedIfDeflated(ciphertextDirFile);
} catch (IOException e) {
// make sure there is no orphan dir file:
Files.delete(ciphertextDirFile);
Expand Down Expand Up @@ -351,7 +354,9 @@ private FileChannel newFileChannel(CryptoPath cleartextFilePath, EffectiveOpenOp
throw new FileAlreadyExistsException(cleartextFilePath.toString());
} else {
// might also throw FileAlreadyExists:
return openCryptoFiles.getOrCreate(ciphertextPath).newFileChannel(options);
FileChannel ch = openCryptoFiles.getOrCreate(ciphertextPath).newFileChannel(options);
longFileNameProvider.persistCachedIfDeflated(ciphertextPath);
return ch;
}
}

Expand Down Expand Up @@ -419,6 +424,7 @@ private void copySymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget,
Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget, CiphertextFileType.SYMLINK);
CopyOption[] resolvedOptions = ArrayUtils.without(options, LinkOption.NOFOLLOW_LINKS).toArray(CopyOption[]::new);
Files.copy(ciphertextSourceFile, ciphertextTargetFile, resolvedOptions);
longFileNameProvider.persistCachedIfDeflated(ciphertextTargetFile);
} else {
CryptoPath resolvedSource = symlinks.resolveRecursively(cleartextSource);
CryptoPath resolvedTarget = symlinks.resolveRecursively(cleartextTarget);
Expand All @@ -431,6 +437,7 @@ private void copyFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, Co
Path ciphertextSourceFile = cryptoPathMapper.getCiphertextFilePath(cleartextSource, CiphertextFileType.FILE);
Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget, CiphertextFileType.FILE);
Files.copy(ciphertextSourceFile, ciphertextTargetFile, options);
longFileNameProvider.persistCachedIfDeflated(ciphertextTargetFile);
}

private void copyDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption[] options) throws IOException {
Expand All @@ -439,6 +446,7 @@ private void copyDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge
if (Files.notExists(ciphertextTargetDirFile)) {
// create new:
createDirectory(cleartextTarget);
longFileNameProvider.persistCachedIfDeflated(ciphertextTargetDirFile);
} else if (ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING)) {
// keep existing (if empty):
Path ciphertextTargetDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path;
Expand Down Expand Up @@ -517,6 +525,7 @@ private void moveSymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget,
Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget, CiphertextFileType.SYMLINK);
try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSourceFile, ciphertextTargetFile)) {
Files.move(ciphertextSourceFile, ciphertextTargetFile, options);
longFileNameProvider.persistCachedIfDeflated(ciphertextTargetFile);
twoPhaseMove.commit();
}
}
Expand All @@ -528,6 +537,7 @@ private void moveFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, Co
Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget, CiphertextFileType.FILE);
try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSourceFile, ciphertextTargetFile)) {
Files.move(ciphertextSourceFile, ciphertextTargetFile, options);
longFileNameProvider.persistCachedIfDeflated(ciphertextTargetFile);
twoPhaseMove.commit();
}
}
Expand All @@ -540,6 +550,7 @@ private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge
if (!ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING)) {
// try to move, don't replace:
Files.move(ciphertextSourceDirFile, ciphertextTargetDirFile, options);
longFileNameProvider.persistCachedIfDeflated(ciphertextTargetDirFile);
} else if (ArrayUtils.contains(options, StandardCopyOption.ATOMIC_MOVE)) {
// replace atomically (impossible):
assert ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING);
Expand All @@ -558,6 +569,7 @@ private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge
Files.delete(ciphertextTargetDir);
}
Files.move(ciphertextSourceDirFile, ciphertextTargetDirFile, options);
longFileNameProvider.persistCachedIfDeflated(ciphertextTargetDirFile);
}
dirIdProvider.move(ciphertextSourceDirFile, ciphertextTargetDirFile);
cryptoPathMapper.invalidatePathMapping(cleartextSource);
Expand Down
62 changes: 40 additions & 22 deletions src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*******************************************************************************/
package org.cryptomator.cryptofs;

import com.google.common.base.Throwables;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
Expand All @@ -24,6 +25,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Duration;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;

Expand All @@ -34,16 +36,18 @@
class LongFileNameProvider {

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

private final Path metadataRoot;
private final ReadonlyFlag readonlyFlag;
private final LoadingCache<String, String> longNames;

@Inject
public LongFileNameProvider(@PathToVault Path pathToVault) {
public LongFileNameProvider(@PathToVault Path pathToVault, ReadonlyFlag readonlyFlag) {
this.metadataRoot = pathToVault.resolve(METADATA_DIR_NAME);
this.longNames = CacheBuilder.newBuilder().maximumSize(MAX_CACHE_SIZE).build(new Loader());
this.readonlyFlag = readonlyFlag;
this.longNames = CacheBuilder.newBuilder().expireAfterAccess(MAX_CACHE_AGE).build(new Loader());
}

private class Loader extends CacheLoader<String, String> {
Expand All @@ -64,36 +68,50 @@ public String inflate(String shortFileName) throws IOException {
try {
return longNames.get(shortFileName);
} catch (ExecutionException e) {
if (e.getCause() instanceof IOException || e.getCause() instanceof UncheckedIOException) {
throw new IOException(e);
} else {
throw new UncheckedExecutionException("Unexpected exception", e);
}
Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
throw new IllegalStateException("Unexpected exception", e);
}
}

public String deflate(String longFileName) throws IOException {
public String deflate(String longFileName) {
byte[] longFileNameBytes = longFileName.getBytes(UTF_8);
byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFileNameBytes);
String shortName = BASE32.encode(hash) + LONG_NAME_FILE_EXT;
if (longNames.getIfPresent(shortName) == null) {
String cachedLongName = longNames.getIfPresent(shortName);
if (cachedLongName == null) {
longNames.put(shortName, longFileName);
// TODO markuskreusch, overheadhunter: do we really want to persist this at this point?...
// ...maybe the caller only wanted to know if a file exists without creating anything.
Path file = resolveMetadataFile(shortName);
Path fileDir = file.getParent();
assert fileDir != null : "resolveMetadataFile returned path to a file";
Files.createDirectories(fileDir);
try (WritableByteChannel ch = Files.newByteChannel(file, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) {
ch.write(ByteBuffer.wrap(longFileNameBytes));
} catch (FileAlreadyExistsException e) {
// no-op: if the file already exists, we assume its content to be what we want (or we found a SHA1 collision ;-))
assert Arrays.equals(Files.readAllBytes(file), longFileNameBytes);
}
} else {
assert cachedLongName.equals(longFileName);
}
return shortName;
}

public void persistCachedIfDeflated(Path ciphertextFile) throws IOException {
String filename = ciphertextFile.getFileName().toString();
if (isDeflated(filename)) {
persistCached(filename);
}
}

// visible for testing
void persistCached(String shortName) throws IOException {
readonlyFlag.assertWritable();
String longName = longNames.getIfPresent(shortName);
if (longName == null) {
throw new IllegalStateException("Long name for " + shortName + " has not been shortened within the last " + MAX_CACHE_AGE);
}
Path file = resolveMetadataFile(shortName);
Path fileDir = file.getParent();
assert fileDir != null : "resolveMetadataFile returned path to a file";
Files.createDirectories(fileDir);
try (WritableByteChannel ch = Files.newByteChannel(file, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) {
ch.write(UTF_8.encode(longName));
} catch (FileAlreadyExistsException e) {
// no-op: if the file already exists, we assume its content to be what we want (or we found a SHA1 collision ;-))
assert Arrays.equals(Files.readAllBytes(file), longName.getBytes(UTF_8));
}
}

private Path resolveMetadataFile(String shortName) {
return metadataRoot.resolve(shortName.substring(0, 2)).resolve(shortName.substring(2, 4)).resolve(shortName);
}
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/org/cryptomator/cryptofs/Symlinks.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@
public class Symlinks {

private final CryptoPathMapper cryptoPathMapper;
private final LongFileNameProvider longFileNameProvider;
private final OpenCryptoFiles openCryptoFiles;
private final ReadonlyFlag readonlyFlag;

@Inject
Symlinks(CryptoPathMapper cryptoPathMapper, OpenCryptoFiles openCryptoFiles, ReadonlyFlag readonlyFlag) {
Symlinks(CryptoPathMapper cryptoPathMapper, LongFileNameProvider longFileNameProvider, OpenCryptoFiles openCryptoFiles, ReadonlyFlag readonlyFlag) {
this.cryptoPathMapper = cryptoPathMapper;
this.longFileNameProvider = longFileNameProvider;
this.openCryptoFiles = openCryptoFiles;
this.readonlyFlag = readonlyFlag;
}
Expand All @@ -41,6 +43,7 @@ public void createSymbolicLink(CryptoPath cleartextPath, Path target, FileAttrib
EffectiveOpenOptions openOptions = EffectiveOpenOptions.from(EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW), readonlyFlag);
ByteBuffer content = UTF_8.encode(target.toString());
openCryptoFiles.writeCiphertextFile(ciphertextSymlinkFile, openOptions, content);
longFileNameProvider.persistCachedIfDeflated(ciphertextSymlinkFile);
}

public CryptoPath readSymbolicLink(CryptoPath cleartextPath) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import dagger.BindsInstance;
import dagger.Subcomponent;
import org.cryptomator.cryptofs.EffectiveOpenOptions;
import org.cryptomator.cryptolib.api.FileHeader;

import java.nio.channels.FileChannel;

Expand All @@ -24,6 +25,12 @@ interface Builder {
@BindsInstance
Builder ciphertextChannel(FileChannel ciphertextChannel);

@BindsInstance
Builder mustWriteHeader(@MustWriteHeader boolean mustWriteHeader);

@BindsInstance
Builder fileHeader(FileHeader fileHeader);

ChannelComponent build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
import org.cryptomator.cryptofs.fh.ChunkCache;
import org.cryptomator.cryptofs.fh.ChunkData;
import org.cryptomator.cryptofs.fh.ExceptionsDuringWrite;
import org.cryptomator.cryptofs.fh.FileHeaderLoader;
import org.cryptomator.cryptofs.fh.OpenFileModifiedDate;
import org.cryptomator.cryptofs.fh.OpenFileSize;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.FileHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -40,7 +40,7 @@ public class CleartextFileChannel extends AbstractFileChannel {
private static final Logger LOG = LoggerFactory.getLogger(CleartextFileChannel.class);

private final FileChannel ciphertextFileChannel;
private final FileHeaderLoader fileHeaderLoader;
private final FileHeader fileHeader;
private final Cryptor cryptor;
private final ChunkCache chunkCache;
private final EffectiveOpenOptions options;
Expand All @@ -50,13 +50,13 @@ public class CleartextFileChannel extends AbstractFileChannel {
private final ExceptionsDuringWrite exceptionsDuringWrite;
private final ChannelCloseListener closeListener;
private final CryptoFileSystemStats stats;
private boolean headerWritten;
private boolean mustWriteHeader;

@Inject
public CleartextFileChannel(FileChannel ciphertextFileChannel, FileHeaderLoader fileHeaderLoader, ReadWriteLock readWriteLock, Cryptor cryptor, ChunkCache chunkCache, EffectiveOpenOptions options, @OpenFileSize AtomicLong fileSize, @OpenFileModifiedDate AtomicReference<Instant> lastModified, Supplier<BasicFileAttributeView> attrViewProvider, ExceptionsDuringWrite exceptionsDuringWrite, ChannelCloseListener closeListener, CryptoFileSystemStats stats) {
public CleartextFileChannel(FileChannel ciphertextFileChannel, FileHeader fileHeader, @MustWriteHeader boolean mustWriteHeader, ReadWriteLock readWriteLock, Cryptor cryptor, ChunkCache chunkCache, EffectiveOpenOptions options, @OpenFileSize AtomicLong fileSize, @OpenFileModifiedDate AtomicReference<Instant> lastModified, Supplier<BasicFileAttributeView> attrViewProvider, ExceptionsDuringWrite exceptionsDuringWrite, ChannelCloseListener closeListener, CryptoFileSystemStats stats) {
super(readWriteLock);
this.ciphertextFileChannel = ciphertextFileChannel;
this.fileHeaderLoader = fileHeaderLoader;
this.fileHeader = fileHeader;
this.cryptor = cryptor;
this.chunkCache = chunkCache;
this.options = options;
Expand All @@ -69,7 +69,7 @@ public CleartextFileChannel(FileChannel ciphertextFileChannel, FileHeaderLoader
if (options.append()) {
position = fileSize.get();
}
this.headerWritten = !options.writable();
this.mustWriteHeader = mustWriteHeader;
if (options.createNew() || options.create()) {
lastModified.compareAndSet(Instant.EPOCH, Instant.now());
}
Expand Down Expand Up @@ -174,10 +174,11 @@ private long writeLockedInternal(ByteSource src, long position) throws IOExcepti
}

private void writeHeaderIfNeeded() throws IOException {
if (!headerWritten) {
if (mustWriteHeader) {
LOG.trace("{} - Writing file header.", this);
ciphertextFileChannel.write(cryptor.fileHeaderCryptor().encryptHeader(fileHeaderLoader.get()), 0);
headerWritten = true;
ByteBuffer encryptedHeader = cryptor.fileHeaderCryptor().encryptHeader(fileHeader);
ciphertextFileChannel.write(encryptedHeader, 0);
mustWriteHeader = false; // write the header only once!
}
}

Expand Down
13 changes: 13 additions & 0 deletions src/main/java/org/cryptomator/cryptofs/ch/MustWriteHeader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.cryptomator.cryptofs.ch;

import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Qualifier
@Documented
@Retention(RUNTIME)
@interface MustWriteHeader {
}
4 changes: 0 additions & 4 deletions src/main/java/org/cryptomator/cryptofs/fh/ChunkIO.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,6 @@ public void unregisterChannel(FileChannel channel) {
writableChannels.remove(channel);
}

long size() throws IOException {
return getReadableChannel().size();
}

int read(ByteBuffer dst, long position) throws IOException {
return getReadableChannel().read(dst, position);
}
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/org/cryptomator/cryptofs/fh/ChunkLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ class ChunkLoader {

private final Cryptor cryptor;
private final ChunkIO ciphertext;
private final FileHeaderLoader headerLoader;
private final FileHeaderHolder headerHolder;
private final CryptoFileSystemStats stats;

@Inject
public ChunkLoader(Cryptor cryptor, ChunkIO ciphertext, FileHeaderLoader headerLoader, CryptoFileSystemStats stats) {
public ChunkLoader(Cryptor cryptor, ChunkIO ciphertext, FileHeaderHolder headerHolder, CryptoFileSystemStats stats) {
this.cryptor = cryptor;
this.ciphertext = ciphertext;
this.headerLoader = headerLoader;
this.headerHolder = headerHolder;
this.stats = stats;
}

Expand All @@ -35,7 +35,7 @@ public ChunkData load(Long chunkIndex) throws IOException {
return ChunkData.emptyWithSize(payloadSize);
} else {
ciphertextBuf.flip();
ByteBuffer cleartextBuf = cryptor.fileContentCryptor().decryptChunk(ciphertextBuf, chunkIndex, headerLoader.get(), true);
ByteBuffer cleartextBuf = cryptor.fileContentCryptor().decryptChunk(ciphertextBuf, chunkIndex, headerHolder.get(), true);
stats.addBytesDecrypted(cleartextBuf.remaining());
ByteBuffer cleartextBufWhichCanHoldFullChunk;
if (cleartextBuf.capacity() < payloadSize) {
Expand Down
Loading

0 comments on commit 8b9d605

Please sign in to comment.