Skip to content

Commit

Permalink
Merge branch 'release/2.4.2'
Browse files Browse the repository at this point in the history
  • Loading branch information
infeo committed May 13, 2022
2 parents 62fd056 + 0983ed5 commit c5432f8
Show file tree
Hide file tree
Showing 16 changed files with 714 additions and 102 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>cryptofs</artifactId>
<version>2.4.1</version>
<version>2.4.2</version>
<name>Cryptomator Crypto Filesystem</name>
<description>This library provides the Java filesystem provider used by Cryptomator.</description>
<url>https://github.com/cryptomator/cryptofs</url>
Expand Down
47 changes: 39 additions & 8 deletions src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileStore;
import java.nio.file.FileSystemException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
Expand All @@ -59,6 +60,7 @@
import java.util.Collections;
import java.util.EnumSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

Expand All @@ -68,7 +70,7 @@

@CryptoFileSystemScoped
class CryptoFileSystemImpl extends CryptoFileSystem {

private final CryptoFileSystemProvider provider;
private final CryptoFileSystems cryptoFileSystems;
private final Path pathToVault;
Expand All @@ -80,6 +82,7 @@ class CryptoFileSystemImpl extends CryptoFileSystem {
private final PathMatcherFactory pathMatcherFactory;
private final DirectoryStreamFactory directoryStreamFactory;
private final DirectoryIdProvider dirIdProvider;
private final DirectoryIdBackup dirIdBackup;
private final AttributeProvider fileAttributeProvider;
private final AttributeByNameProvider fileAttributeByNameProvider;
private final AttributeViewProvider fileAttributeViewProvider;
Expand All @@ -98,7 +101,7 @@ class CryptoFileSystemImpl extends CryptoFileSystem {
@Inject
public CryptoFileSystemImpl(CryptoFileSystemProvider provider, CryptoFileSystems cryptoFileSystems, @PathToVault Path pathToVault, Cryptor cryptor,
CryptoFileStore fileStore, CryptoFileSystemStats stats, CryptoPathMapper cryptoPathMapper, CryptoPathFactory cryptoPathFactory,
PathMatcherFactory pathMatcherFactory, DirectoryStreamFactory directoryStreamFactory, DirectoryIdProvider dirIdProvider,
PathMatcherFactory pathMatcherFactory, DirectoryStreamFactory directoryStreamFactory, DirectoryIdProvider dirIdProvider, DirectoryIdBackup dirIdBackup,
AttributeProvider fileAttributeProvider, AttributeByNameProvider fileAttributeByNameProvider, AttributeViewProvider fileAttributeViewProvider,
OpenCryptoFiles openCryptoFiles, Symlinks symlinks, FinallyUtil finallyUtil, CiphertextDirectoryDeleter ciphertextDirDeleter, ReadonlyFlag readonlyFlag,
CryptoFileSystemProperties fileSystemProperties) {
Expand All @@ -113,6 +116,7 @@ public CryptoFileSystemImpl(CryptoFileSystemProvider provider, CryptoFileSystems
this.pathMatcherFactory = pathMatcherFactory;
this.directoryStreamFactory = directoryStreamFactory;
this.dirIdProvider = dirIdProvider;
this.dirIdBackup = dirIdBackup;
this.fileAttributeProvider = fileAttributeProvider;
this.fileAttributeByNameProvider = fileAttributeByNameProvider;
this.fileAttributeViewProvider = fileAttributeViewProvider;
Expand Down Expand Up @@ -235,13 +239,17 @@ <A extends BasicFileAttributes> A readAttributes(CryptoPath cleartextPath, Class

/**
* @param cleartextPath the path to the file
* @param type the Class object corresponding to the file attribute view
* @param options future use
* @param type the Class object corresponding to the file attribute view
* @param options future use
* @return a file attribute view of the specified type, or <code>null</code> if the attribute view type is not available
* @see AttributeViewProvider#getAttributeView(CryptoPath, Class, LinkOption...)
*/
<V extends FileAttributeView> V getFileAttributeView(CryptoPath cleartextPath, Class<V> type, LinkOption... options) {
return fileAttributeViewProvider.getAttributeView(cleartextPath, type, options);
if (fileStore.supportsFileAttributeView(type)) {
return fileAttributeViewProvider.getAttributeView(cleartextPath, type, options);
} else {
return null;
}
}

void checkAccess(CryptoPath cleartextPath, AccessMode... modes) throws IOException {
Expand Down Expand Up @@ -282,6 +290,10 @@ boolean isHidden(CryptoPath cleartextPath) throws IOException {
void createDirectory(CryptoPath cleartextDir, FileAttribute<?>... attrs) throws IOException {
readonlyFlag.assertWritable();
assertCleartextNameLengthAllowed(cleartextDir);
if (rootPath.equals(cleartextDir)) {
throw new FileAlreadyExistsException(rootPath.toString());
}

CryptoPath cleartextParentDir = cleartextDir.getParent();
if (cleartextParentDir == null) {
return;
Expand All @@ -302,6 +314,7 @@ void createDirectory(CryptoPath cleartextDir, FileAttribute<?>... attrs) throws
// 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);
dirIdBackup.execute(ciphertextDir);
ciphertextPath.persistLongFileName();
} catch (IOException e) {
// make sure there is no orphan dir file:
Expand Down Expand Up @@ -375,14 +388,17 @@ private FileChannel newFileChannelFromFile(CryptoPath cleartextFilePath, Effecti
stats.incrementAccessesRead();
}
return ch;
} catch (Exception e){
} catch (Exception e) {
ch.close();
throw e;
}
}

void delete(CryptoPath cleartextPath) throws IOException {
readonlyFlag.assertWritable();
if (rootPath.equals(cleartextPath)) {
throw new FileSystemException("The filesystem root cannot be deleted.");
}
CiphertextFileType ciphertextFileType = cryptoPathMapper.getCiphertextFileType(cleartextPath);
CiphertextFilePath ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextPath);
switch (ciphertextFileType) {
Expand Down Expand Up @@ -414,6 +430,11 @@ void copy(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption...
if (cleartextSource.equals(cleartextTarget)) {
return;
}

if (rootPath.equals(cleartextTarget) && ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING)) {
throw new FileSystemException("The filesystem root cannot be replaced.");
}

CiphertextFileType ciphertextFileType = cryptoPathMapper.getCiphertextFileType(cleartextSource);
if (!ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING)) {
cryptoPathMapper.assertNonExisting(cleartextTarget);
Expand Down Expand Up @@ -508,6 +529,16 @@ private void copyAttributes(Path src, Path dst) throws IOException {
void move(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption... options) throws IOException {
readonlyFlag.assertWritable();
assertCleartextNameLengthAllowed(cleartextTarget);

if (rootPath.equals(cleartextSource)) {
throw new FileSystemException("Filesystem root cannot be moved.");

}

if (rootPath.equals(cleartextTarget)) {
throw new FileAlreadyExistsException(rootPath.toString());
}

if (cleartextSource.equals(cleartextTarget)) {
return;
}
Expand Down Expand Up @@ -582,7 +613,7 @@ private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge
}
Files.walkFileTree(ciphertextTarget.getRawPath(), DeletingFileVisitor.INSTANCE);
}

// no exceptions until this point, so MOVE:
Files.move(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath(), options);
if (ciphertextTarget.isShortened()) {
Expand Down Expand Up @@ -621,7 +652,7 @@ CryptoPath getEmptyPath() {
}

void assertCleartextNameLengthAllowed(CryptoPath cleartextPath) throws FileNameTooLongException {
String filename = cleartextPath.getFileName().toString();
String filename = Optional.ofNullable(cleartextPath.getFileName()).map(CryptoPath::toString).orElse(""); //fs root has no explicit name
if (filename.length() > fileSystemProperties.maxCleartextNameLength()) {
throw new FileNameTooLongException(cleartextPath.toString(), fileSystemProperties.maxCleartextNameLength());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ public class CryptoFileSystemProperties extends AbstractMap<String, Object> {
* Key identifying the name of the masterkey file located inside the vault directory.
*
* @since 1.1.0
* @deprecated Replaced with {@link #PROPERTY_KEYLOADER external keyloader} API
*/
@Deprecated
public static final String PROPERTY_MASTERKEY_FILENAME = "masterkeyFilename";

static final String DEFAULT_MASTERKEY_FILENAME = "masterkey.cryptomator";
Expand Down Expand Up @@ -128,6 +130,7 @@ String vaultConfigFilename() {
return (String) get(PROPERTY_VAULTCONFIG_FILENAME);
}

@Deprecated
String masterkeyFilename() {
return (String) get(PROPERTY_MASTERKEY_FILENAME);
}
Expand Down Expand Up @@ -295,7 +298,9 @@ public Builder withVaultConfigFilename(String vaultConfigFilename) {
* @param masterkeyFilename the filename of the json file containing configuration to decrypt the masterkey
* @return this
* @since 1.1.0
* @deprecated Supply a {@link #withKeyLoader(MasterkeyLoader) keyloader} instead.
*/
@Deprecated
public Builder withMasterkeyFilename(String masterkeyFilename) {
this.masterkeyFilename = masterkeyFilename;
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ public static void initialize(Path pathToVault, CryptoFileSystemProperties prope
} finally {
Arrays.fill(rawKey, (byte) 0x00);
}
assert checkDirStructureForVault(pathToVault, properties.vaultConfigFilename(), properties.masterkeyFilename()) == DirStructure.VAULT;
assert checkDirStructureForVault(pathToVault, properties.vaultConfigFilename(), null) == DirStructure.VAULT;
}

/**
Expand Down
7 changes: 4 additions & 3 deletions src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,10 @@ private String readVaultConfigFile(Path pathToVault, CryptoFileSystemProperties
try {
return Files.readString(vaultConfigFile, StandardCharsets.US_ASCII);
} catch (NoSuchFileException e) {
Path masterkeyPath = pathToVault.resolve(properties.masterkeyFilename());
if (Files.exists(masterkeyPath)) {
LOG.warn("Failed to read {}, but found {}}", vaultConfigFile, masterkeyPath);
// TODO: remove this check and tell downstream users to check the vault dir structure before creating a CryptoFileSystemImpl
@SuppressWarnings("deprecation") var masterkeyFilename = properties.masterkeyFilename();
if (masterkeyFilename != null && Files.exists(pathToVault.resolve(masterkeyFilename))) {
LOG.warn("Failed to read {}, but found {}}", vaultConfigFile, masterkeyFilename);
throw new FileSystemNeedsMigrationException(pathToVault);
} else {
throw e;
Expand Down
8 changes: 3 additions & 5 deletions src/main/java/org/cryptomator/cryptofs/DirStructure.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,19 @@ public enum DirStructure {
*
* @param pathToVault A directory path
* @param vaultConfigFilename Name of the vault config file
* @param masterkeyFilename Name of the masterkey file
* @param masterkeyFilename Name of the masterkey file (may be null to skip detection of legacy vaults)
* @return enum indicating what this directory might be
* @throws IOException if the provided path is not a directory, does not exist or cannot be read
*/
public static DirStructure checkDirStructure(Path pathToVault, String vaultConfigFilename, String masterkeyFilename) throws IOException {
if(! Files.readAttributes(pathToVault, BasicFileAttributes.class).isDirectory()) {
throw new NotDirectoryException(pathToVault.toString());
}
Path vaultConfigPath = pathToVault.resolve(vaultConfigFilename);
Path masterkeyPath = pathToVault.resolve(masterkeyFilename);
Path dataDirPath = pathToVault.resolve(Constants.DATA_DIR_NAME);
if (Files.isDirectory(dataDirPath)) {
if (Files.isReadable(vaultConfigPath)) {
if (Files.isReadable(pathToVault.resolve(vaultConfigFilename))) {
return VAULT;
} else if (Files.isReadable(masterkeyPath)) {
} else if (masterkeyFilename != null && Files.isReadable(pathToVault.resolve(masterkeyFilename))) {
return MAYBE_LEGACY;
}
}
Expand Down
58 changes: 58 additions & 0 deletions src/main/java/org/cryptomator/cryptofs/DirectoryIdBackup.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.cryptomator.cryptofs;

import org.cryptomator.cryptofs.common.Constants;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel;

import javax.inject.Inject;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;

/**
* Single purpose class to backup the directory id of an encrypted directory when it is created.
*/
@CryptoFileSystemScoped
public class DirectoryIdBackup {

private Cryptor cryptor;

@Inject
public DirectoryIdBackup(Cryptor cryptor) {
this.cryptor = cryptor;
}

/**
* Performs the backup operation for the given {@link CryptoPathMapper.CiphertextDirectory} object.
* <p>
* The directory id is written via an encrypting channel to the file {@link CryptoPathMapper.CiphertextDirectory#path}/{@value Constants#DIR_ID_FILE}.
*
* @param ciphertextDirectory The cipher dir object containing the dir id and the encrypted content root
* @throws IOException if an IOException is raised during the write operation
*/
public void execute(CryptoPathMapper.CiphertextDirectory ciphertextDirectory) throws IOException {
try (var channel = Files.newByteChannel(ciphertextDirectory.path.resolve(Constants.DIR_ID_FILE), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); //
var encryptingChannel = wrapEncryptionAround(channel, cryptor)) {
encryptingChannel.write(ByteBuffer.wrap(ciphertextDirectory.dirId.getBytes(StandardCharsets.US_ASCII)));
}
}

/**
* Static method to explicitly backup the directory id for a specified ciphertext directory.
*
* @param cryptor The cryptor to be used
* @param ciphertextDirectory A {@link org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory} for which the dirId should be back up'd.
* @throws IOException when the dirId file already exists, or it cannot be written to.
*/
public static void backupManually(Cryptor cryptor, CryptoPathMapper.CiphertextDirectory ciphertextDirectory) throws IOException {
new DirectoryIdBackup(cryptor).execute(ciphertextDirectory);
}


static EncryptingWritableByteChannel wrapEncryptionAround(ByteChannel channel, Cryptor cryptor) {
return new EncryptingWritableByteChannel(channel, cryptor);
}
}
4 changes: 3 additions & 1 deletion src/main/java/org/cryptomator/cryptofs/common/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ private Constants() {
public static final int DEFAULT_SHORTENING_THRESHOLD = 220;
public static final int MAX_SYMLINK_LENGTH = 32767; // max path length on NTFS and FAT32: 32k-1
public static final int MAX_DIR_FILE_LENGTH = 36; // UUIDv4: hex-encoded 16 byte int + 4 hyphens = 36 ASCII chars
public static final int MIN_CIPHER_NAME_LENGTH = 26; //rounded up base64url encoded (16 bytes IV + 0 bytes empty string) + file suffix = 26 ASCII chars

public static final String SEPARATOR = "/";
public static final String RECOVERY_DIR_NAME = "CRYPTOMATOR_RECOVERY";
public static final String RECOVERY_DIR_NAME = "LOST+FOUND";
public static final String DIR_ID_FILE = "dirid.c9r";
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.cryptomator.cryptofs.CryptoPath;
import org.cryptomator.cryptofs.CryptoPathMapper;
import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory;
import org.cryptomator.cryptofs.common.Constants;

import javax.inject.Inject;
import java.io.IOException;
Expand All @@ -13,15 +14,14 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

@CryptoFileSystemScoped
public class DirectoryStreamFactory {

private final CryptoPathMapper cryptoPathMapper;
private final DirectoryStreamComponent.Builder directoryStreamComponentBuilder; // sharing reusable builder via synchronized
private final Map<CryptoDirectoryStream, DirectoryStream> streams = new HashMap<>();
private final Map<CryptoDirectoryStream, DirectoryStream<Path>> streams = new HashMap<>();

private volatile boolean closed = false;

Expand All @@ -36,7 +36,8 @@ public synchronized CryptoDirectoryStream newDirectoryStream(CryptoPath cleartex
throw new ClosedFileSystemException();
}
CiphertextDirectory ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextDir);
DirectoryStream<Path> ciphertextDirStream = Files.newDirectoryStream(ciphertextDir.path);
//TODO: use HealthCheck with warning and suggest fix to create one
DirectoryStream<Path> ciphertextDirStream = Files.newDirectoryStream(ciphertextDir.path, this::matchesEncryptedContentPattern);
CryptoDirectoryStream cleartextDirStream = directoryStreamComponentBuilder //
.dirId(ciphertextDir.dirId) //
.ciphertextDirectoryStream(ciphertextDirStream) //
Expand All @@ -49,12 +50,19 @@ public synchronized CryptoDirectoryStream newDirectoryStream(CryptoPath cleartex
return cleartextDirStream;
}

//visible for testing
boolean matchesEncryptedContentPattern(Path path) {
var tmp = path.getFileName().toString();
return tmp.length() >= Constants.MIN_CIPHER_NAME_LENGTH //
&& (tmp.endsWith(Constants.CRYPTOMATOR_FILE_SUFFIX) || tmp.endsWith(Constants.DEFLATED_FILE_SUFFIX));
}

public synchronized void close() throws IOException {
closed = true;
IOException exception = new IOException("Close failed");
Iterator<Map.Entry<CryptoDirectoryStream, DirectoryStream>> iter = streams.entrySet().iterator();
var iter = streams.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<CryptoDirectoryStream, DirectoryStream> entry = iter.next();
var entry = iter.next();
iter.remove();
try {
entry.getKey().close();
Expand Down
Loading

0 comments on commit c5432f8

Please sign in to comment.