Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ private FileChannel newFileChannelFromFile(CryptoPath cleartextFilePath, Effecti
Files.createDirectories(ciphertextPath.getRawPath()); // suppresses FileAlreadyExists
}

FileChannel ch = openCryptoFiles.getOrCreate(ciphertextFilePath).newFileChannel(options, attrs); // might throw FileAlreadyExists
FileChannel ch = openCryptoFiles.getOrCreate(cleartextFilePath, ciphertextFilePath).newFileChannel(options, attrs); // might throw FileAlreadyExists
try {
if (options.writable()) {
ciphertextPath.persistLongFileName();
Expand Down Expand Up @@ -588,7 +588,7 @@ private void moveSymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget,
// "the symbolic link itself, not the target of the link, is moved"
CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource);
CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget);
try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath())) {
try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSource.getRawPath(), cleartextTarget, ciphertextTarget.getRawPath())) {
Files.move(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath(), options);
if (ciphertextTarget.isShortened()) {
ciphertextTarget.persistLongFileName();
Expand All @@ -604,7 +604,7 @@ private void moveFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, Co
// we need to re-map the OpenCryptoFile entry.
CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource);
CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget);
try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath())) {
try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSource.getRawPath(), cleartextTarget, ciphertextTarget.getRawPath())) {
if (ciphertextTarget.isShortened()) {
Files.createDirectories(ciphertextTarget.getRawPath());
ciphertextTarget.persistLongFileName();
Expand Down
8 changes: 3 additions & 5 deletions src/main/java/org/cryptomator/cryptofs/Symlinks.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,12 @@
public class Symlinks {

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

@Inject
Symlinks(CryptoPathMapper cryptoPathMapper, LongFileNameProvider longFileNameProvider, OpenCryptoFiles openCryptoFiles, ReadonlyFlag readonlyFlag) {
Symlinks(CryptoPathMapper cryptoPathMapper, OpenCryptoFiles openCryptoFiles, ReadonlyFlag readonlyFlag) {
this.cryptoPathMapper = cryptoPathMapper;
this.longFileNameProvider = longFileNameProvider;
this.openCryptoFiles = openCryptoFiles;
this.readonlyFlag = readonlyFlag;
}
Expand All @@ -48,7 +46,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());
Files.createDirectory(ciphertextFilePath.getRawPath());
openCryptoFiles.writeCiphertextFile(ciphertextFilePath.getSymlinkFilePath(), openOptions, content);
openCryptoFiles.writeCiphertextFile(cleartextPath, ciphertextFilePath.getSymlinkFilePath(), openOptions, content);
ciphertextFilePath.persistLongFileName();
}

Expand All @@ -57,7 +55,7 @@ public CryptoPath readSymbolicLink(CryptoPath cleartextPath) throws IOException
EffectiveOpenOptions openOptions = EffectiveOpenOptions.from(EnumSet.of(StandardOpenOption.READ), readonlyFlag);
assertIsSymlink(cleartextPath, ciphertextSymlinkFile);
try {
ByteBuffer content = openCryptoFiles.readCiphertextFile(ciphertextSymlinkFile, openOptions, Constants.MAX_SYMLINK_LENGTH);
ByteBuffer content = openCryptoFiles.readCiphertextFile(cleartextPath, ciphertextSymlinkFile, openOptions, Constants.MAX_SYMLINK_LENGTH);
return cleartextPath.getFileSystem().getPath(UTF_8.decode(content).toString());
} catch (BufferUnderflowException e) {
throw new NotLinkException(cleartextPath.toString(), null, "Unreasonably large symlink file");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
import org.cryptomator.cryptofs.fh.BufferPool;
import org.cryptomator.cryptofs.fh.Chunk;
import org.cryptomator.cryptofs.fh.ChunkCache;
import org.cryptomator.cryptofs.fh.CurrentOpenFilePath;
import org.cryptomator.cryptofs.fh.ClearAndCipherPath;
import org.cryptomator.cryptofs.fh.CurrentOpenFilePaths;
import org.cryptomator.cryptofs.fh.ExceptionsDuringWrite;
import org.cryptomator.cryptofs.fh.FileHeaderHolder;
import org.cryptomator.cryptofs.fh.OpenFileModifiedDate;
Expand All @@ -25,7 +26,6 @@
import java.nio.channels.NonReadableChannelException;
import java.nio.channels.NonWritableChannelException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
Expand All @@ -50,23 +50,23 @@ public class CleartextFileChannel extends AbstractFileChannel {
private final ChunkCache chunkCache;
private final BufferPool bufferPool;
private final EffectiveOpenOptions options;
private final AtomicReference<Path> currentFilePath;
private final AtomicReference<ClearAndCipherPath> currentFilePaths;
private final AtomicLong fileSize;
private final AtomicReference<Instant> lastModified;
private final ExceptionsDuringWrite exceptionsDuringWrite;
private final Consumer<FileChannel> closeListener;
private final CryptoFileSystemStats stats;

@Inject
public CleartextFileChannel(FileChannel ciphertextFileChannel, FileHeaderHolder fileHeaderHolder, ReadWriteLock readWriteLock, Cryptor cryptor, ChunkCache chunkCache, BufferPool bufferPool, EffectiveOpenOptions options, @OpenFileSize AtomicLong fileSize, @OpenFileModifiedDate AtomicReference<Instant> lastModified, @CurrentOpenFilePath AtomicReference<Path> currentPath, ExceptionsDuringWrite exceptionsDuringWrite, Consumer<FileChannel> closeListener, CryptoFileSystemStats stats) {
public CleartextFileChannel(FileChannel ciphertextFileChannel, FileHeaderHolder fileHeaderHolder, ReadWriteLock readWriteLock, Cryptor cryptor, ChunkCache chunkCache, BufferPool bufferPool, EffectiveOpenOptions options, @OpenFileSize AtomicLong fileSize, @OpenFileModifiedDate AtomicReference<Instant> lastModified, @CurrentOpenFilePaths AtomicReference<ClearAndCipherPath> currentPaths, ExceptionsDuringWrite exceptionsDuringWrite, Consumer<FileChannel> closeListener, CryptoFileSystemStats stats) {
super(readWriteLock);
this.ciphertextFileChannel = ciphertextFileChannel;
this.fileHeaderHolder = fileHeaderHolder;
this.cryptor = cryptor;
this.chunkCache = chunkCache;
this.bufferPool = bufferPool;
this.options = options;
this.currentFilePath = currentPath;
this.currentFilePaths = currentPaths;
this.fileSize = fileSize;
this.lastModified = lastModified;
this.exceptionsDuringWrite = exceptionsDuringWrite;
Expand Down Expand Up @@ -254,10 +254,11 @@ void flush() throws IOException {
void persistLastModified() throws IOException {
FileTime lastModifiedTime = isWritable() ? FileTime.from(lastModified.get()) : null;
FileTime lastAccessTime = FileTime.from(Instant.now());
var p = currentFilePath.get();
if (p != null) {
p.getFileSystem().provider()//
.getFileAttributeView(p, BasicFileAttributeView.class)
var ps = currentFilePaths.get();
if (ps != null) {
var ciphertextPath = ps.ciphertextPath();
ciphertextPath.getFileSystem().provider()//
.getFileAttributeView(ciphertextPath, BasicFileAttributeView.class)
.setTimes(lastModifiedTime, lastAccessTime, null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
/**
* Emitted, if a decryption operation fails.
*
* @param cleartextPath path within the cryptographic filesystem
* @param ciphertextPath path to the encrypted resource
* @param e thrown exception
*/
public record DecryptionFailedEvent(Path ciphertextPath, AuthenticationFailedException e) implements FilesystemEvent {
public record DecryptionFailedEvent(Path cleartextPath, Path ciphertextPath, AuthenticationFailedException e) implements FilesystemEvent {

}
11 changes: 5 additions & 6 deletions src/main/java/org/cryptomator/cryptofs/fh/ChunkLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,26 @@
import org.cryptomator.cryptolib.api.Cryptor;

import javax.inject.Inject;
import javax.inject.Named;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;

@OpenFileScoped
class ChunkLoader {

private final Consumer<FilesystemEvent> eventConsumer;
private final AtomicReference<Path> path;
private final AtomicReference<ClearAndCipherPath> paths;
private final Cryptor cryptor;
private final ChunkIO ciphertext;
private final FileHeaderHolder headerHolder;
private final CryptoFileSystemStats stats;
private final BufferPool bufferPool;

@Inject
public ChunkLoader(Consumer<FilesystemEvent> eventConsumer, @CurrentOpenFilePath AtomicReference<Path> path, Cryptor cryptor, ChunkIO ciphertext, FileHeaderHolder headerHolder, CryptoFileSystemStats stats, BufferPool bufferPool) {
public ChunkLoader(Consumer<FilesystemEvent> eventConsumer, @CurrentOpenFilePaths AtomicReference<ClearAndCipherPath> paths, Cryptor cryptor, ChunkIO ciphertext, FileHeaderHolder headerHolder, CryptoFileSystemStats stats, BufferPool bufferPool) {
this.eventConsumer = eventConsumer;
this.path = path;
this.paths = paths;
this.cryptor = cryptor;
this.ciphertext = ciphertext;
this.headerHolder = headerHolder;
Expand All @@ -53,7 +51,8 @@ public ByteBuffer load(Long chunkIndex) throws IOException, AuthenticationFailed
}
return cleartextBuf;
} catch (AuthenticationFailedException e) {
eventConsumer.accept(new DecryptionFailedEvent(path.get(), e));
var tmp = paths.get();
eventConsumer.accept(new DecryptionFailedEvent(tmp.cleartextPath(), tmp.ciphertextPath(), e));
throw e;
} finally {
bufferPool.recycle(ciphertextBuf);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.cryptomator.cryptofs.fh;

import org.cryptomator.cryptofs.CryptoPath;

import java.nio.file.Path;

public record ClearAndCipherPath(CryptoPath cleartextPath, Path ciphertextPath) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@

/**
* The current Path of an OpenCryptoFile.
* @see OriginalOpenFilePath
* @see OriginalOpenFilePaths
*/
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface CurrentOpenFilePath {
public @interface CurrentOpenFilePaths {
}
16 changes: 8 additions & 8 deletions src/main/java/org/cryptomator/cryptofs/fh/FileHeaderHolder.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
Expand All @@ -25,16 +24,16 @@ public class FileHeaderHolder {

private final Consumer<FilesystemEvent> eventConsumer;
private final Cryptor cryptor;
private final AtomicReference<Path> path;
private final AtomicReference<ClearAndCipherPath> paths;
private final AtomicReference<FileHeader> header = new AtomicReference<>();
private final AtomicReference<ByteBuffer> encryptedHeader = new AtomicReference<>();
private final AtomicBoolean isPersisted = new AtomicBoolean();

@Inject
public FileHeaderHolder(Consumer<FilesystemEvent> eventConsumer, Cryptor cryptor, @CurrentOpenFilePath AtomicReference<Path> path) {
public FileHeaderHolder(Consumer<FilesystemEvent> eventConsumer, Cryptor cryptor, @CurrentOpenFilePaths AtomicReference<ClearAndCipherPath> paths) {
this.eventConsumer = eventConsumer;
this.cryptor = cryptor;
this.path = path;
this.paths = paths;
}

public FileHeader get() {
Expand All @@ -54,7 +53,7 @@ public ByteBuffer getEncrypted() {
}

FileHeader createNew() {
LOG.trace("Generating file header for {}", path.get());
LOG.trace("Generating file header for {}", paths.get().ciphertextPath());
FileHeader newHeader = cryptor.fileHeaderCryptor().create();
encryptedHeader.set(cryptor.fileHeaderCryptor().encryptHeader(newHeader).asReadOnlyBuffer()); //to prevent NONCE reuse, we already encrypt the header and cache it
header.set(newHeader);
Expand All @@ -70,7 +69,7 @@ FileHeader createNew() {
* @throws IOException if the file header cannot be read or decrypted
*/
FileHeader loadExisting(FileChannel ch) throws IOException {
LOG.trace("Reading file header from {}", path.get());
LOG.trace("Reading file header from {}", paths.get().cleartextPath());
ByteBuffer existingHeaderBuf = ByteBuffer.allocate(cryptor.fileHeaderCryptor().headerSize());
ch.read(existingHeaderBuf, 0);
existingHeaderBuf.flip();
Expand All @@ -81,10 +80,11 @@ FileHeader loadExisting(FileChannel ch) throws IOException {
isPersisted.set(true);
return existingHeader;
} catch (IllegalArgumentException | CryptoException e) {
var ps = paths.get();
if (e instanceof AuthenticationFailedException afe) {
eventConsumer.accept(new DecryptionFailedEvent(path.get(), afe));
eventConsumer.accept(new DecryptionFailedEvent(ps.cleartextPath(), ps.ciphertextPath(), afe));
}
throw new IOException("Unable to decrypt header of file " + path.get(), e);
throw new IOException("Unable to decrypt header of file " + ps.ciphertextPath(), e);
}
}

Expand Down
34 changes: 18 additions & 16 deletions src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import java.io.Closeable;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
Expand All @@ -29,21 +28,21 @@ public class OpenCryptoFile implements Closeable {
private final Cryptor cryptor;
private final FileHeaderHolder headerHolder;
private final ChunkIO chunkIO;
private final AtomicReference<Path> currentFilePath;
private final AtomicReference<ClearAndCipherPath> currentFilePaths;
private final AtomicLong fileSize;
private final OpenCryptoFileComponent component;

private final AtomicInteger openChannelsCount = new AtomicInteger(0);

@Inject
public OpenCryptoFile(FileCloseListener listener, Cryptor cryptor, FileHeaderHolder headerHolder, ChunkIO chunkIO, //
@CurrentOpenFilePath AtomicReference<Path> currentFilePath, @OpenFileSize AtomicLong fileSize, //
@CurrentOpenFilePaths AtomicReference<ClearAndCipherPath> currentFilePaths, @OpenFileSize AtomicLong fileSize, //
@OpenFileModifiedDate AtomicReference<Instant> lastModified, OpenCryptoFileComponent component) {
this.listener = listener;
this.cryptor = cryptor;
this.headerHolder = headerHolder;
this.chunkIO = chunkIO;
this.currentFilePath = currentFilePath;
this.currentFilePaths = currentFilePaths;
this.fileSize = fileSize;
this.component = component;
this.lastModified = lastModified;
Expand All @@ -57,16 +56,17 @@ public OpenCryptoFile(FileCloseListener listener, Cryptor cryptor, FileHeaderHol
* @throws IOException
*/
public synchronized FileChannel newFileChannel(EffectiveOpenOptions options, FileAttribute<?>... attrs) throws IOException {
Path path = currentFilePath.get();
if (path == null) {
var paths = currentFilePaths.get();
if (paths == null) {
throw new IllegalStateException("Cannot create file channel to deleted file");
}
FileChannel ciphertextFileChannel = null;
CleartextFileChannel cleartextFileChannel = null;

openChannelsCount.incrementAndGet(); // synchronized context, hence we can proactively increase the number
try {
ciphertextFileChannel = path.getFileSystem().provider().newFileChannel(path, options.createOpenOptionsForEncryptedFile(), attrs);
var ciphertextPath = paths.ciphertextPath();
ciphertextFileChannel = ciphertextPath.getFileSystem().provider().newFileChannel(ciphertextPath, options.createOpenOptionsForEncryptedFile(), attrs);
initFileHeader(options, ciphertextFileChannel);
initFileSize(ciphertextFileChannel);
cleartextFileChannel = component.newChannelComponent() //
Expand Down Expand Up @@ -159,16 +159,17 @@ public void setLastModifiedTime(FileTime lastModifiedTime) {
lastModified.set(lastModifiedTime.toInstant());
}

public Path getCurrentFilePath() {
return currentFilePath.get();
public ClearAndCipherPath getCurrentFilePaths() {
return currentFilePaths.get();
}

/**
* Updates the current ciphertext file path, if it is not already set to null (i.e., the openCryptoFile is deleted)
* @param newFilePath new ciphertext path
*
* @param newPaths the new clear- & ciphertext paths
*/
public void updateCurrentFilePath(Path newFilePath) {
currentFilePath.updateAndGet(p -> p == null ? null : newFilePath);
public void updateCurrentFilePath(ClearAndCipherPath newPaths) {
currentFilePaths.updateAndGet(p -> p == null ? null : newPaths);
}

private synchronized void cleartextChannelClosed(FileChannel ciphertextFileChannel) {
Expand All @@ -182,14 +183,15 @@ private synchronized void cleartextChannelClosed(FileChannel ciphertextFileChann

@Override
public void close() {
var p = currentFilePath.get();
if(p != null) {
listener.close(p, this);
var p = currentFilePaths.get();
if (p != null) {
listener.close(p.ciphertextPath(), this);
}
}

@Override
public String toString() {
return "OpenCryptoFile(path=" + currentFilePath.toString() + ")";
var paths = currentFilePaths.get();
return "OpenCryptoFile(path=" + (paths != null ? paths.ciphertextPath().toString() : "[deleted]") + ")";
}
Comment on lines 193 to 196
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add null check before accessing ciphertextPath.

The toString method might throw a NPE if paths is null and ciphertextPath() is called.

Apply this diff to add a null check:

-    return "OpenCryptoFile(path=" + (paths != null ? paths.ciphertextPath().toString() : "[deleted]") + ")";
+    return "OpenCryptoFile(path=" + (paths != null && paths.ciphertextPath() != null ? paths.ciphertextPath().toString() : "[deleted]") + ")";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public String toString() {
return "OpenCryptoFile(path=" + currentFilePath.toString() + ")";
var paths = currentFilePaths.get();
return "OpenCryptoFile(path=" + (paths != null ? paths.ciphertextPath().toString() : "[deleted]") + ")";
}
public String toString() {
var paths = currentFilePaths.get();
return "OpenCryptoFile(path=" + (paths != null && paths.ciphertextPath() != null ? paths.ciphertextPath().toString() : "[deleted]") + ")";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public interface OpenCryptoFileComponent {
@Subcomponent.Factory
interface Factory {

OpenCryptoFileComponent create(@BindsInstance @OriginalOpenFilePath Path path, //
OpenCryptoFileComponent create(@BindsInstance @OriginalOpenFilePaths ClearAndCipherPath clearAndCipherPath, //
@BindsInstance FileCloseListener onCloseListener);
}

Expand Down
Loading