Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
14 changes: 14 additions & 0 deletions src/main/java/org/cryptomator/cryptofs/CipherDir.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.cryptomator.cryptofs;

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

//own file due to dagger
public record CipherDir(String dirId, Path contentDirPath) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

  • make package-private
  • rename contentDirPath to c9rDirPath for consistency?

@infeo infeo Oct 29, 2024

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

i'll revert it to path for a small diff and consistency.

c9rDirPath is, from my point of view, the most misleading, because the directory with the c9r suffix contains the dir.c9r file with the dir-id. For a cleartext directory /foo/bar there are always two directories on the cipher side:

  • the "linking" directory with the encrypted dir name, the c9r suffix and the dir-id as content
  • the "content" directory with a bas64-encoded hash as name and with all encrypted files and other "linking" directories

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Also, this class cannot be package private. It is used in different packages, e.g. DirectoryStreamFactory or MissingContentDir.


public CipherDir(String dirId, Path contentDirPath) {
this.dirId = Objects.requireNonNull(dirId);
this.contentDirPath = Objects.requireNonNull(contentDirPath);
}

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

import java.util.Objects;

//own file due to dagger
public record CipherNodeNameParameters(String dirId, String clearNodeName) {
Comment thread
infeo marked this conversation as resolved.
Outdated

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

}
79 changes: 79 additions & 0 deletions src/main/java/org/cryptomator/cryptofs/ClearToCipherDirCache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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;

public class ClearToCipherDirCache {
Comment thread
infeo marked this conversation as resolved.
Outdated

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

private final AsyncCache<CryptoPath, CipherDir> 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));
}
Comment thread
infeo marked this conversation as resolved.

/**
* 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()));
}

Comment thread
infeo marked this conversation as resolved.

/**
* 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 CipherDir}, containing the dirId and the ciphertext content directory path
* @throws IOException if the loading function throws an IOExcecption
*/
Comment thread
infeo marked this conversation as resolved.
Outdated
CipherDir get(CryptoPath cleartextPath, CipherDirLoader ifAbsent) throws IOException {
var futureMapping = new CompletableFuture<CipherDir>();
var currentMapping = ciphertextDirectories.asMap().putIfAbsent(cleartextPath, futureMapping);
if (currentMapping != null) {
return currentMapping.join();
} else {
futureMapping.complete(ifAbsent.load());
return futureMapping.join();
}
}

Comment thread
infeo marked this conversation as resolved.
Outdated
@FunctionalInterface
interface CipherDirLoader {

CipherDir load() throws IOException;
}

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

}

}
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).contentDirPath();
}
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).contentDirPath();
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.contentDirPath());
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).contentDirPath();
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).contentDirPath();
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).contentDirPath();
Path ciphertextTargetDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).contentDirPath();
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).contentDirPath();
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 CipherDir(Constants.ROOT_DIR_ID, vaultCipherRootPath));
} finally {
Arrays.fill(rawKey, (byte) 0x00);
}
Expand Down
119 changes: 30 additions & 89 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,18 +35,16 @@ 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 LoadingCache<CipherNodeNameParameters, String> ciphertextNames;
private final ClearToCipherDirCache clearToCipherDirCache;

private final CiphertextDirectory rootDirectory;
private final CipherDir rootDirectory;

@Inject
CryptoPathMapper(@PathToVault Path pathToVault, Cryptor cryptor, DirectoryIdProvider dirIdProvider, LongFileNameProvider longFileNameProvider, VaultConfig vaultConfig) {
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.clearToCipherDirCache = new ClearToCipherDirCache();
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 @@ -119,13 +113,13 @@ public CiphertextFilePath getCiphertextFilePath(CryptoPath cleartextPath) throws
if (parentPath == null) {
throw new IllegalArgumentException("Invalid file path (must have a parent): " + cleartextPath);
}
CiphertextDirectory parent = getCiphertextDir(parentPath);
CipherDir parent = getCiphertextDir(parentPath);
String cleartextName = cleartextPath.getFileName().toString();
return getCiphertextFilePath(parent.path, parent.dirId, cleartextName);
return getCiphertextFilePath(parent.contentDirPath(), parent.dirId(), cleartextName);
}

public CiphertextFilePath getCiphertextFilePath(Path parentCiphertextDir, String parentDirId, String cleartextName) {
String ciphertextName = ciphertextNames.get(new DirIdAndName(parentDirId, cleartextName));
String ciphertextName = ciphertextNames.get(new CipherNodeNameParameters(parentDirId, cleartextName));
Path c9rPath = parentCiphertextDir.resolve(ciphertextName);
if (ciphertextName.length() > vaultConfig.getShorteningThreshold()) {
LongFileNameProvider.DeflatedFileName deflatedFileName = longFileNameProvider.deflate(c9rPath);
Expand All @@ -135,101 +129,48 @@ 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;
private String getCiphertextFileName(CipherNodeNameParameters dirIdAndName) {
return cryptor.fileNameCryptor().encryptFilename(BaseEncoding.base64Url(), dirIdAndName.clearNodeName(), dirIdAndName.dirId().getBytes(StandardCharsets.UTF_8)) + Constants.CRYPTOMATOR_FILE_SUFFIX;
Comment thread
infeo marked this conversation as resolved.
}

/**
* 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);
clearToCipherDirCache.removeAllKeysWithPrefix(cleartextPath);
Comment thread
infeo marked this conversation as resolved.
Outdated
}

/**
* 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);
}
clearToCipherDirCache.recomputeAllKeysWithPrefix(cleartextSrc, cleartextDst);
Comment thread
infeo marked this conversation as resolved.
Outdated
}

public CiphertextDirectory getCiphertextDir(CryptoPath cleartextPath) throws IOException {
public CipherDir 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 {
ClearToCipherDirCache.CipherDirLoader cipherDirLoaderIfAbsent = () -> {
Path dirFile = getCiphertextFilePath(cleartextPath).getDirFilePath();
CiphertextDirectory cipherDir = resolveDirectory(dirFile);
lazyEntry.complete(cipherDir);
return cipherDir;
}
return resolveDirectory(dirFile);
};
return clearToCipherDirCache.get(cleartextPath, cipherDirLoaderIfAbsent);
}
}

public CiphertextDirectory resolveDirectory(Path directoryFile) throws IOException {
public CipherDir resolveDirectory(Path directoryFile) throws IOException {
String dirId = dirIdProvider.load(directoryFile);
return resolveDirectory(dirId);
}

private CiphertextDirectory resolveDirectory(String dirId) {
private CipherDir resolveDirectory(String dirId) {
String dirHash = cryptor.fileNameCryptor().hashDirectoryId(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;
}
}
return new CipherDir(dirId, dirPath);
}

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;
}
}
}

}
Loading