Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions src/main/java/org/cryptomator/cryptofs/CiphertextDirCache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package org.cryptomator.cryptofs;

import com.github.benmanes.caffeine.cache.AsyncCache;
import com.github.benmanes.caffeine.cache.Caffeine;

import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.concurrent.CompletableFuture;

/**
* Caches for the cleartext path of a directory its ciphertext path to the content directory.
*/
public class CiphertextDirCache {

private static final int MAX_CACHED_PATHS = 5000;
private static final Duration MAX_CACHE_AGE = Duration.ofSeconds(20);

private final AsyncCache<CryptoPath, CiphertextDirectory> ciphertextDirectories = Caffeine.newBuilder() //
.maximumSize(MAX_CACHED_PATHS) //
.expireAfterWrite(MAX_CACHE_AGE) //
.buildAsync();

/**
* Removes all (key,value) entries, where {@code key.startsWith(oldPrefix) == true}.
*
* @param basePrefix The prefix key which the keys are checked against
*/
void removeAllKeysWithPrefix(CryptoPath basePrefix) {
ciphertextDirectories.asMap().keySet().removeIf(p -> p.startsWith(basePrefix));
}

/**
* Remaps all (key,value) entries, where {@code key.startsWith(oldPrefix) == true}.
* The new key is computed by replacing the oldPrefix with the newPrefix.
*
* @param oldPrefix the prefix key which the keys are checked against
* @param newPrefix the prefix key which replaces {@code oldPrefix}
*/
void recomputeAllKeysWithPrefix(CryptoPath oldPrefix, CryptoPath newPrefix) {
var remappedEntries = new ArrayList<CacheEntry>();
ciphertextDirectories.asMap().entrySet().removeIf(e -> {
if (e.getKey().startsWith(oldPrefix)) {
var remappedPath = newPrefix.resolve(oldPrefix.relativize(e.getKey()));
return remappedEntries.add(new CacheEntry(remappedPath, e.getValue()));
} else {
return false;
}
});
remappedEntries.forEach(e -> ciphertextDirectories.put(e.clearPath(), e.cipherDir()));
}


/**
* Gets the cipher directory for the given cleartext path. If a cache miss occurs, the mapping is loaded with the {@code ifAbsent} function.
* @param cleartextPath Cleartext path key
* @param ifAbsent Function to compute the (cleartextPath, cipherDir) mapping on a cache miss.
* @return a {@link CiphertextDirectory}, containing the dirId and the ciphertext content directory path
* @throws IOException if the loading function throws an IOException
*/
CiphertextDirectory get(CryptoPath cleartextPath, CipherDirLoader ifAbsent) throws IOException {
var futureMapping = new CompletableFuture<CiphertextDirectory>();
var currentMapping = ciphertextDirectories.asMap().putIfAbsent(cleartextPath, futureMapping);
if (currentMapping != null) {
return currentMapping.join();
} else {
futureMapping.complete(ifAbsent.load());
return futureMapping.join();
}
}

@FunctionalInterface
interface CipherDirLoader {

CiphertextDirectory load() throws IOException;
}

private record CacheEntry(CryptoPath clearPath, CompletableFuture<CiphertextDirectory> cipherDir) {

}

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

import java.nio.file.Path;
import java.util.Objects;

//own file due to dagger

/**
* Represents a ciphertext directory without it's mount point in the virtual filesystem.
*
* @param dirId The (ciphertext) dir id (not encrypted, just a uuid)
* @param path The path to content directory (which contains the actual encrypted files and links to subdirectories)
*/
public record CiphertextDirectory(String dirId, Path path) {

public CiphertextDirectory(String dirId, Path path) {
this.dirId = Objects.requireNonNull(dirId);
this.path = Objects.requireNonNull(path);
}

Comment on lines +14 to +20
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding format validation for dirId.

While null checks are good, consider adding validation for the dirId format since it's mentioned to be a UUID in the documentation.

Here's a suggested implementation:

 public record CiphertextDirectory(String dirId, Path path) {
+    private static final Pattern UUID_PATTERN = 
+        Pattern.compile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$");
 
     public CiphertextDirectory(String dirId, Path path) {
         this.dirId = Objects.requireNonNull(dirId);
+        if (!UUID_PATTERN.matcher(dirId).matches()) {
+            throw new IllegalArgumentException("dirId must be a valid UUID");
+        }
         this.path = Objects.requireNonNull(path);
     }
 }

Don't forget to add the import:

import java.util.regex.Pattern;

}
21 changes: 10 additions & 11 deletions src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
*******************************************************************************/
package org.cryptomator.cryptofs;

import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory;
import org.cryptomator.cryptofs.attr.AttributeByNameProvider;
import org.cryptomator.cryptofs.attr.AttributeProvider;
import org.cryptomator.cryptofs.attr.AttributeViewProvider;
Expand Down Expand Up @@ -142,7 +141,7 @@ public Path getCiphertextPath(Path cleartextPath) throws IOException {
var p = CryptoPath.castAndAssertAbsolute(cleartextPath);
var nodeType = cryptoPathMapper.getCiphertextFileType(p);
if (nodeType == CiphertextFileType.DIRECTORY) {
return cryptoPathMapper.getCiphertextDir(p).path;
return cryptoPathMapper.getCiphertextDir(p).path();
}
var cipherFile = cryptoPathMapper.getCiphertextFilePath(p);
if (nodeType == CiphertextFileType.SYMLINK) {
Expand Down Expand Up @@ -316,22 +315,22 @@ void createDirectory(CryptoPath cleartextDir, FileAttribute<?>... attrs) throws
if (cleartextParentDir == null) {
return;
}
Path ciphertextParentDir = cryptoPathMapper.getCiphertextDir(cleartextParentDir).path;
Path ciphertextParentDir = cryptoPathMapper.getCiphertextDir(cleartextParentDir).path();
if (!Files.exists(ciphertextParentDir)) {
throw new NoSuchFileException(cleartextParentDir.toString());
}
cryptoPathMapper.assertNonExisting(cleartextDir);
CiphertextFilePath ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextDir);
Path ciphertextDirFile = ciphertextPath.getDirFilePath();
CiphertextDirectory ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextDir);
var ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextDir);
// atomically check for FileAlreadyExists and create otherwise:
Files.createDirectory(ciphertextPath.getRawPath());
try (FileChannel channel = FileChannel.open(ciphertextDirFile, EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE), attrs)) {
channel.write(UTF_8.encode(ciphertextDir.dirId));
channel.write(UTF_8.encode(ciphertextDir.dirId()));
}
// 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);
Files.createDirectories(ciphertextDir.path());
dirIdBackup.execute(ciphertextDir);
ciphertextPath.persistLongFileName();
} catch (IOException e) {
Expand Down Expand Up @@ -432,7 +431,7 @@ private void deleteFileOrSymlink(CiphertextFilePath ciphertextPath) throws IOExc
}

private void deleteDirectory(CryptoPath cleartextPath, CiphertextFilePath ciphertextPath) throws IOException {
Path ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextPath).path;
Path ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextPath).path();
Path ciphertextDirFile = ciphertextPath.getDirFilePath();
try {
ciphertextDirDeleter.deleteCiphertextDirIncludingNonCiphertextFiles(ciphertextDir, cleartextPath);
Expand Down Expand Up @@ -505,7 +504,7 @@ private void copyDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge
ciphertextTarget.persistLongFileName();
} else if (ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING)) {
// keep existing (if empty):
Path ciphertextTargetDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path;
Path ciphertextTargetDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path();
try (DirectoryStream<Path> ds = Files.newDirectoryStream(ciphertextTargetDir)) {
if (ds.iterator().hasNext()) {
throw new DirectoryNotEmptyException(cleartextTarget.toString());
Expand All @@ -515,8 +514,8 @@ private void copyDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge
throw new FileAlreadyExistsException(cleartextTarget.toString(), null, "Ciphertext file already exists: " + ciphertextTarget);
}
if (ArrayUtils.contains(options, StandardCopyOption.COPY_ATTRIBUTES)) {
Path ciphertextSourceDir = cryptoPathMapper.getCiphertextDir(cleartextSource).path;
Path ciphertextTargetDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path;
Path ciphertextSourceDir = cryptoPathMapper.getCiphertextDir(cleartextSource).path();
Path ciphertextTargetDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path();
copyAttributes(ciphertextSourceDir, ciphertextTargetDir);
}
}
Expand Down Expand Up @@ -622,7 +621,7 @@ private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge
throw new AtomicMoveNotSupportedException(cleartextSource.toString(), cleartextTarget.toString(), "Replacing directories during move requires non-atomic status checks.");
}
// check if dir is empty:
Path targetCiphertextDirContentDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path;
Path targetCiphertextDirContentDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path();
boolean targetCiphertextDirExists = true;
try (DirectoryStream<Path> ds = Files.newDirectoryStream(targetCiphertextDirContentDir, DirectoryStreamFilters.EXCLUDE_DIR_ID_BACKUP)) {
if (ds.iterator().hasNext()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ public static void initialize(Path pathToVault, CryptoFileSystemProperties prope
Path vaultCipherRootPath = pathToVault.resolve(Constants.DATA_DIR_NAME).resolve(dirHash.substring(0, 2)).resolve(dirHash.substring(2));
Files.createDirectories(vaultCipherRootPath);
// create dirId backup:
DirectoryIdBackup.backupManually(cryptor, new CryptoPathMapper.CiphertextDirectory(Constants.ROOT_DIR_ID, vaultCipherRootPath));
DirectoryIdBackup.backupManually(cryptor, new CiphertextDirectory(Constants.ROOT_DIR_ID, vaultCipherRootPath));
} finally {
Arrays.fill(rawKey, (byte) 0x00);
}
Expand Down
101 changes: 21 additions & 80 deletions src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
*******************************************************************************/
package org.cryptomator.cryptofs;

import com.github.benmanes.caffeine.cache.AsyncCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.io.BaseEncoding;
Expand All @@ -27,10 +26,7 @@
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Duration;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;

import static org.cryptomator.cryptofs.common.Constants.DATA_DIR_NAME;

Expand All @@ -39,16 +35,14 @@ public class CryptoPathMapper {

private static final Logger LOG = LoggerFactory.getLogger(CryptoPathMapper.class);
private static final int MAX_CACHED_CIPHERTEXT_NAMES = 5000;
private static final int MAX_CACHED_DIR_PATHS = 5000;
private static final Duration MAX_CACHE_AGE = Duration.ofSeconds(20);

private final Cryptor cryptor;
private final Path dataRoot;
private final DirectoryIdProvider dirIdProvider;
private final LongFileNameProvider longFileNameProvider;
private final VaultConfig vaultConfig;
private final LoadingCache<DirIdAndName, String> ciphertextNames;
private final AsyncCache<CryptoPath, CiphertextDirectory> ciphertextDirectories;
private final CiphertextDirCache ciphertextDirCache;

private final CiphertextDirectory rootDirectory;

Expand All @@ -60,7 +54,7 @@ public class CryptoPathMapper {
this.longFileNameProvider = longFileNameProvider;
this.vaultConfig = vaultConfig;
this.ciphertextNames = Caffeine.newBuilder().maximumSize(MAX_CACHED_CIPHERTEXT_NAMES).build(this::getCiphertextFileName);
this.ciphertextDirectories = Caffeine.newBuilder().maximumSize(MAX_CACHED_DIR_PATHS).expireAfterWrite(MAX_CACHE_AGE).buildAsync();
this.ciphertextDirCache = new CiphertextDirCache();
this.rootDirectory = resolveDirectory(Constants.ROOT_DIR_ID);
}

Expand All @@ -69,7 +63,7 @@ public class CryptoPathMapper {
*
* @param cleartextPath A path
* @throws FileAlreadyExistsException If the node exists
* @throws IOException If any I/O error occurs while attempting to resolve the ciphertext path
* @throws IOException If any I/O error occurs while attempting to resolve the ciphertext path
*/
public void assertNonExisting(CryptoPath cleartextPath) throws FileAlreadyExistsException, IOException {
try {
Expand Down Expand Up @@ -121,7 +115,7 @@ public CiphertextFilePath getCiphertextFilePath(CryptoPath cleartextPath) throws
}
CiphertextDirectory parent = getCiphertextDir(parentPath);
String cleartextName = cleartextPath.getFileName().toString();
return getCiphertextFilePath(parent.path, parent.dirId, cleartextName);
return getCiphertextFilePath(parent.path(), parent.dirId(), cleartextName);
}

public CiphertextFilePath getCiphertextFilePath(Path parentCiphertextDir, String parentDirId, String cleartextName) {
Expand All @@ -136,36 +130,36 @@ public CiphertextFilePath getCiphertextFilePath(Path parentCiphertextDir, String
}

private String getCiphertextFileName(DirIdAndName dirIdAndName) {
return cryptor.fileNameCryptor().encryptFilename(BaseEncoding.base64Url(), dirIdAndName.name, dirIdAndName.dirId.getBytes(StandardCharsets.UTF_8)) + Constants.CRYPTOMATOR_FILE_SUFFIX;
return cryptor.fileNameCryptor().encryptFilename(BaseEncoding.base64Url(), dirIdAndName.clearNodeName(), dirIdAndName.dirId().getBytes(StandardCharsets.UTF_8)) + Constants.CRYPTOMATOR_FILE_SUFFIX;
}

/**
* Removes the given cleartext path and all cached child paths from the dir cache
* @param cleartextPath the root cleartext path, for which all mappings starting with it will be removed
*/
public void invalidatePathMapping(CryptoPath cleartextPath) {
ciphertextDirectories.asMap().remove(cleartextPath);
ciphertextDirCache.removeAllKeysWithPrefix(cleartextPath);
}

/**
* Moves the given cleartext path and all cached child paths in the dir cache
* @param cleartextSrc the root cleartext path, for which alle mappings starting with it will be moved
* @param cleartextDst the destination cleartext path. The path itself and all childs will be adjusted to start with cleartextDst.
*/
public void movePathMapping(CryptoPath cleartextSrc, CryptoPath cleartextDst) {
var cachedValue = ciphertextDirectories.asMap().remove(cleartextSrc);
if (cachedValue != null) {
ciphertextDirectories.put(cleartextDst, cachedValue);
}
ciphertextDirCache.recomputeAllKeysWithPrefix(cleartextSrc, cleartextDst);
}

public CiphertextDirectory getCiphertextDir(CryptoPath cleartextPath) throws IOException {
assert cleartextPath.isAbsolute();
CryptoPath parentPath = cleartextPath.getParent();
if (parentPath == null) {
if (cleartextPath.getParent() == null) {
return rootDirectory;
} else {
var lazyEntry = new CompletableFuture<CiphertextDirectory>();
var priorEntry = ciphertextDirectories.asMap().putIfAbsent(cleartextPath, lazyEntry);
if (priorEntry != null) {
return priorEntry.join();
} else {
CiphertextDirCache.CipherDirLoader cipherDirLoaderIfAbsent = () -> {
Path dirFile = getCiphertextFilePath(cleartextPath).getDirFilePath();
CiphertextDirectory cipherDir = resolveDirectory(dirFile);
lazyEntry.complete(cipherDir);
return cipherDir;
}
return resolveDirectory(dirFile);
};
return ciphertextDirCache.get(cleartextPath, cipherDirLoaderIfAbsent);
}
}

Expand All @@ -179,57 +173,4 @@ private CiphertextDirectory resolveDirectory(String dirId) {
Path dirPath = dataRoot.resolve(dirHash.substring(0, 2)).resolve(dirHash.substring(2));
return new CiphertextDirectory(dirId, dirPath);
}

public static class CiphertextDirectory {
public final String dirId;
public final Path path;

public CiphertextDirectory(String dirId, Path path) {
this.dirId = Objects.requireNonNull(dirId);
this.path = Objects.requireNonNull(path);
}

@Override
public int hashCode() {
return Objects.hash(dirId, path);
}

@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
} else if (obj instanceof CiphertextDirectory other) {
return this.dirId.equals(other.dirId) && this.path.equals(other.path);
} else {
return false;
}
}
}

private static class DirIdAndName {
public final String dirId;
public final String name;

public DirIdAndName(String dirId, String name) {
this.dirId = Objects.requireNonNull(dirId);
this.name = Objects.requireNonNull(name);
}

@Override
public int hashCode() {
return Objects.hash(dirId, name);
}

@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
} else if (obj instanceof DirIdAndName other) {
return this.dirId.equals(other.dirId) && this.name.equals(other.name);
} else {
return false;
}
}
}

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

import java.util.Objects;

//own file due to dagger

/**
* Helper object to store the dir id of a directory along with its cleartext name (aka, the last element in the cleartext path)
* @param dirId
* @param clearNodeName
*/
record DirIdAndName(String dirId, String clearNodeName) {

public DirIdAndName(String dirId, String clearNodeName) {
this.dirId = Objects.requireNonNull(dirId);
this.clearNodeName = Objects.requireNonNull(clearNodeName);
}

}
Loading