Skip to content
Closed
1 change: 1 addition & 0 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
requires java.compiler;

exports org.cryptomator.cryptofs;
exports org.cryptomator.cryptofs.event;
exports org.cryptomator.cryptofs.common;
exports org.cryptomator.cryptofs.health.api;
exports org.cryptomator.cryptofs.migration;
Expand Down
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.cryptomator.cryptofs.attr.AttributeComponent;
import org.cryptomator.cryptofs.attr.AttributeViewComponent;
import org.cryptomator.cryptofs.dir.DirectoryStreamComponent;
import org.cryptomator.cryptofs.event.FilesystemEvent;
import org.cryptomator.cryptofs.fh.OpenCryptoFileComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -19,6 +20,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.function.Consumer;

@Module(subcomponents = {AttributeComponent.class, AttributeViewComponent.class, OpenCryptoFileComponent.class, DirectoryStreamComponent.class})
class CryptoFileSystemModule {
Expand All @@ -35,4 +37,10 @@ public Optional<FileStore> provideNativeFileStore(@PathToVault Path pathToVault)
return Optional.empty();
}
}

@Provides
@CryptoFileSystemScoped
public Consumer<FilesystemEvent> provideFilesystemEventConsumer(CryptoFileSystemProperties fsProps) {
return (Consumer<FilesystemEvent>) fsProps.get(CryptoFileSystemProperties.PROPERTY_NOTIFY_METHOD);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
package org.cryptomator.cryptofs;

import com.google.common.base.Strings;
import org.cryptomator.cryptofs.event.FilesystemEvent;
import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.MasterkeyLoader;

Expand Down Expand Up @@ -80,6 +81,15 @@ public class CryptoFileSystemProperties extends AbstractMap<String, Object> {

static final String DEFAULT_MASTERKEY_FILENAME = "masterkey.cryptomator";

/**
* Key identifying the function to call for notifications.
*
* @since 2.9.0
*/
public static final String PROPERTY_NOTIFY_METHOD = "notificationConsumer";

static final Consumer<FilesystemEvent> DEFAULT_NOTIFY_METHOD = (FilesystemEvent e) -> {};

/**
* Key identifying the filesystem flags.
*
Expand Down Expand Up @@ -113,6 +123,7 @@ private CryptoFileSystemProperties(Builder builder) {
Map.entry(PROPERTY_FILESYSTEM_FLAGS, builder.flags), //
Map.entry(PROPERTY_VAULTCONFIG_FILENAME, builder.vaultConfigFilename), //
Map.entry(PROPERTY_MASTERKEY_FILENAME, builder.masterkeyFilename), //
Map.entry(PROPERTY_NOTIFY_METHOD, builder.eventConsumer), //
Map.entry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, builder.maxCleartextNameLength), //
Map.entry(PROPERTY_SHORTENING_THRESHOLD, builder.shorteningThreshold), //
Map.entry(PROPERTY_CIPHER_COMBO, builder.cipherCombo) //
Expand Down Expand Up @@ -208,6 +219,7 @@ public static class Builder {
private String masterkeyFilename = DEFAULT_MASTERKEY_FILENAME;
private int maxCleartextNameLength = DEFAULT_MAX_CLEARTEXT_NAME_LENGTH;
private int shorteningThreshold = DEFAULT_SHORTENING_THRESHOLD;
private Consumer<FilesystemEvent> eventConsumer = DEFAULT_NOTIFY_METHOD;

private Builder() {
}
Expand All @@ -220,6 +232,7 @@ private Builder(Map<String, ?> properties) {
checkedSet(Integer.class, PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, properties, this::withMaxCleartextNameLength);
checkedSet(Integer.class, PROPERTY_SHORTENING_THRESHOLD, properties, this::withShorteningThreshold);
checkedSet(CryptorProvider.Scheme.class, PROPERTY_CIPHER_COMBO, properties, this::withCipherCombo);
checkedSet(Consumer.class, PROPERTY_NOTIFY_METHOD, properties, this::withFilesystemEventConsumer);
}

private <T> void checkedSet(Class<T> type, String key, Map<String, ?> properties, Consumer<T> setter) {
Expand Down Expand Up @@ -334,6 +347,18 @@ public Builder withMasterkeyFilename(String masterkeyFilename) {
return this;
}

/**
* Sets the consumer for filesystem events
*
* @param eventConsumer the consumer to receive filesystem events
* @return this
* @since 2.8.0
*/
public Builder withFilesystemEventConsumer(Consumer<FilesystemEvent> eventConsumer) {
this.eventConsumer = eventConsumer;
return this;
}

/**
* Validates the values and creates new {@link CryptoFileSystemProperties}.
*
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 @@ -6,6 +6,9 @@
import com.google.common.io.RecursiveDeleteOption;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptofs.common.Constants;
import org.cryptomator.cryptofs.event.ConflictResolutionFailedEvent;
import org.cryptomator.cryptofs.event.ConflictResolvedEvent;
import org.cryptomator.cryptofs.event.FilesystemEvent;
import org.cryptomator.cryptolib.api.Cryptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -18,6 +21,7 @@
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.function.Consumer;
import java.util.stream.Stream;

import static org.cryptomator.cryptofs.common.Constants.DIR_FILE_NAME;
Expand All @@ -33,14 +37,18 @@ class C9rConflictResolver {
private final Cryptor cryptor;
private final byte[] dirId;
private final int maxC9rFileNameLength;
private final Path cleartextPath;
private final int maxCleartextFileNameLength;
private final Consumer<FilesystemEvent> eventConsumer;

@Inject
public C9rConflictResolver(Cryptor cryptor, @Named("dirId") String dirId, VaultConfig vaultConfig) {
public C9rConflictResolver(Cryptor cryptor, @Named("dirId") String dirId, VaultConfig vaultConfig, Consumer<FilesystemEvent> eventConsumer, @Named("cleartextPath") Path cleartextPath) {
this.cryptor = cryptor;
this.dirId = dirId.getBytes(StandardCharsets.US_ASCII);
this.maxC9rFileNameLength = vaultConfig.getShorteningThreshold();
this.cleartextPath = cleartextPath;
this.maxCleartextFileNameLength = (maxC9rFileNameLength - 4) / 4 * 3 - 16; // math from FileSystemCapabilityChecker.determineSupportedCleartextFileNameLength()
this.eventConsumer = eventConsumer;
}

public Stream<Node> process(Node node) {
Expand All @@ -61,13 +69,15 @@ public Stream<Node> process(Node node) {
Path canonicalPath = node.ciphertextPath.resolveSibling(canonicalCiphertextFileName);
return resolveConflict(node, canonicalPath);
} catch (IOException e) {
LOG.error("Failed to resolve conflict for " + node.ciphertextPath, e);
eventConsumer.accept(new ConflictResolutionFailedEvent(cleartextPath.resolve(node.cleartextName), node.ciphertextPath.resolve(node.fullCiphertextFileName), e));
LOG.error("Failed to resolve conflict for {}", node.ciphertextPath, e);
return Stream.empty();
}
}
}

private Stream<Node> resolveConflict(Node conflicting, Path canonicalPath) throws IOException {
//visible for testing
Stream<Node> resolveConflict(Node conflicting, Path canonicalPath) throws IOException {
Path conflictingPath = conflicting.ciphertextPath;
if (resolveConflictTrivially(canonicalPath, conflictingPath)) {
Node resolved = new Node(canonicalPath);
Expand Down Expand Up @@ -111,6 +121,7 @@ private Node renameConflictingFile(Path canonicalPath, Path conflictingPath, Str
Node node = new Node(alternativePath);
node.cleartextName = alternativeCleartext;
node.extractedCiphertext = alternativeCiphertext;
eventConsumer.accept(new ConflictResolvedEvent(cleartextPath.resolve(cleartext), canonicalPath, cleartextPath.resolve(alternativeCleartext), alternativePath));
return node;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.cryptomator.cryptofs.event;

import java.nio.file.Path;

/**
* Emitted, if the conflict resolution inside an encrypted directory failed
*
* @param cleartextPath path within the cryptographic filesystem
* @param ciphertextPath path to the encrypted resource with the broken filename
* @param reason exception, why the resolution failed
*/
public record ConflictResolutionFailedEvent(Path cleartextPath, Path ciphertextPath, Exception reason) implements FilesystemEvent {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.cryptomator.cryptofs.event;

import java.nio.file.Path;

/**
* Emitted, if a conflict inside an encrypted directory was resolved
*
* @param cleartextPath path within the cryptographic filesystem
* @param ciphertextPath path to the encrypted resource
* @param oldVersionCleartextPath path within the cryptographic filesystem of the renamed resource
* @param oldVersionCiphertextPath path to the renamed, encrypted resource
*/
public record ConflictResolvedEvent(Path cleartextPath, Path ciphertextPath, Path oldVersionCleartextPath, Path oldVersionCiphertextPath) implements FilesystemEvent {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.cryptomator.cryptofs.event;

import org.cryptomator.cryptolib.api.AuthenticationFailedException;

import java.nio.file.Path;

/**
* 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 cleartextPath, Path ciphertextPath, AuthenticationFailedException e) implements FilesystemEvent {

}
27 changes: 27 additions & 0 deletions src/main/java/org/cryptomator/cryptofs/event/FilesystemEvent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.cryptomator.cryptofs.event;

import java.util.function.Consumer;

/**
* Common interface for all filesystem events.
* <p>
* Events are emitted via the notification method set in the properties during filesystem creation, see {@link org.cryptomator.cryptofs.CryptoFileSystemProperties.Builder#withFilesystemEventConsumer(Consumer)}.
* <p>
* To get a specific event type, use the enhanced switch pattern or typecasting in if-instance of, e.g.
* {@code
* FilesystemEvent fse;
* switch (fse) {
* case DecryptionFailedEvent dfe -> //do stuff
* case ConflictResolvedEvent cre -> //do other stuff
* //other cases
* }
* if( fse instanceof DecryptionFailedEvent dfe) {
* //do more stuff
* }
* }.
*
* @apiNote Events might have occured a long time ago in a galaxy far, far away... therefore, any feedback method is non-blocking and might fail due to changes in the filesystem.
*/
public sealed interface FilesystemEvent permits ConflictResolutionFailedEvent, ConflictResolvedEvent, DecryptionFailedEvent {

}
Loading