org.cryptomator.cryptofs.CryptoPathMapper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of cryptofs Show documentation
Show all versions of cryptofs Show documentation
This library provides the Java filesystem provider used by Cryptomator.
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE.txt.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
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;
import org.cryptomator.cryptofs.common.CiphertextFileType;
import org.cryptomator.cryptofs.common.Constants;
import org.cryptomator.cryptolib.api.Cryptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
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;
@CryptoFileSystemScoped
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 ciphertextNames;
private final AsyncCache ciphertextDirectories;
private final CiphertextDirectory rootDirectory;
@Inject
CryptoPathMapper(@PathToVault Path pathToVault, Cryptor cryptor, DirectoryIdProvider dirIdProvider, LongFileNameProvider longFileNameProvider, VaultConfig vaultConfig) {
this.dataRoot = pathToVault.resolve(DATA_DIR_NAME);
this.cryptor = cryptor;
this.dirIdProvider = dirIdProvider;
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.rootDirectory = resolveDirectory(Constants.ROOT_DIR_ID);
}
/**
* Verifies that no node exists for the given path. Otherwise a {@link FileAlreadyExistsException} will be thrown.
*
* @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
*/
public void assertNonExisting(CryptoPath cleartextPath) throws FileAlreadyExistsException, IOException {
try {
CiphertextFilePath ciphertextPath = getCiphertextFilePath(cleartextPath);
BasicFileAttributes attr = Files.readAttributes(ciphertextPath.getRawPath(), BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
if (attr != null) {
throw new FileAlreadyExistsException(cleartextPath.toString());
}
} catch (NoSuchFileException e) {
// good!
}
}
/**
* @param cleartextPath A path
* @return The file type for the given path (if it exists)
* @throws NoSuchFileException If no node exists at the given path for any known type
* @throws IOException
*/
public CiphertextFileType getCiphertextFileType(CryptoPath cleartextPath) throws NoSuchFileException, IOException {
CryptoPath parentPath = cleartextPath.getParent();
if (parentPath == null) {
return CiphertextFileType.DIRECTORY; // ROOT
} else {
CiphertextFilePath ciphertextPath = getCiphertextFilePath(cleartextPath);
BasicFileAttributes attr = Files.readAttributes(ciphertextPath.getRawPath(), BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
if (attr.isDirectory()) {
if (Files.exists(ciphertextPath.getDirFilePath(), LinkOption.NOFOLLOW_LINKS)) {
return CiphertextFileType.DIRECTORY;
} else if (Files.exists(ciphertextPath.getSymlinkFilePath(), LinkOption.NOFOLLOW_LINKS)) {
return CiphertextFileType.SYMLINK;
} else if (ciphertextPath.isShortened() && Files.exists(ciphertextPath.getFilePath(), LinkOption.NOFOLLOW_LINKS)) {
return CiphertextFileType.FILE;
} else {
LOG.warn("Did not find valid content inside of {}", ciphertextPath.getRawPath());
throw new NoSuchFileException(cleartextPath.toString(), null, "Could not determine type of file " + ciphertextPath.getRawPath());
}
} else {
// assume "file" if not a directory (even if it isn't a "regular" file, see issue #81):
return CiphertextFileType.FILE;
}
}
}
public CiphertextFilePath getCiphertextFilePath(CryptoPath cleartextPath) throws IOException {
CryptoPath parentPath = cleartextPath.getParent();
if (parentPath == null) {
throw new IllegalArgumentException("Invalid file path (must have a parent): " + cleartextPath);
}
CiphertextDirectory parent = getCiphertextDir(parentPath);
String cleartextName = cleartextPath.getFileName().toString();
return getCiphertextFilePath(parent.path, parent.dirId, cleartextName);
}
public CiphertextFilePath getCiphertextFilePath(Path parentCiphertextDir, String parentDirId, String cleartextName) {
String ciphertextName = ciphertextNames.get(new DirIdAndName(parentDirId, cleartextName));
Path c9rPath = parentCiphertextDir.resolve(ciphertextName);
if (ciphertextName.length() > vaultConfig.getShorteningThreshold()) {
LongFileNameProvider.DeflatedFileName deflatedFileName = longFileNameProvider.deflate(c9rPath);
return new CiphertextFilePath(deflatedFileName.c9sPath, Optional.of(deflatedFileName));
} else {
return new CiphertextFilePath(c9rPath, Optional.empty());
}
}
private String getCiphertextFileName(DirIdAndName dirIdAndName) {
return cryptor.fileNameCryptor().encryptFilename(BaseEncoding.base64Url(), dirIdAndName.name, dirIdAndName.dirId.getBytes(StandardCharsets.UTF_8)) + Constants.CRYPTOMATOR_FILE_SUFFIX;
}
public void invalidatePathMapping(CryptoPath cleartextPath) {
ciphertextDirectories.asMap().remove(cleartextPath);
}
public void movePathMapping(CryptoPath cleartextSrc, CryptoPath cleartextDst) {
var cachedValue = ciphertextDirectories.asMap().remove(cleartextSrc);
if (cachedValue != null) {
ciphertextDirectories.put(cleartextDst, cachedValue);
}
}
public CiphertextDirectory getCiphertextDir(CryptoPath cleartextPath) throws IOException {
assert cleartextPath.isAbsolute();
CryptoPath parentPath = cleartextPath.getParent();
if (parentPath == null) {
return rootDirectory;
} else {
var lazyEntry = new CompletableFuture();
var priorEntry = ciphertextDirectories.asMap().putIfAbsent(cleartextPath, lazyEntry);
if (priorEntry != null) {
return priorEntry.join();
} else {
Path dirFile = getCiphertextFilePath(cleartextPath).getDirFilePath();
CiphertextDirectory cipherDir = resolveDirectory(dirFile);
lazyEntry.complete(cipherDir);
return cipherDir;
}
}
}
public CiphertextDirectory resolveDirectory(Path directoryFile) throws IOException {
String dirId = dirIdProvider.load(directoryFile);
return resolveDirectory(dirId);
}
private CiphertextDirectory 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;
}
}
}
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;
}
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy