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
13 changes: 8 additions & 5 deletions src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@

@CryptoFileSystemScoped
class CryptoFileSystemImpl extends CryptoFileSystem {

private final CryptoFileSystemProvider provider;
private final CryptoFileSystems cryptoFileSystems;
private final Path pathToVault;
Expand All @@ -80,6 +80,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 +99,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 +114,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,8 +237,8 @@ <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...)
*/
Expand Down Expand Up @@ -302,6 +304,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 @@ -582,7 +585,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
46 changes: 46 additions & 0 deletions src/main/java/org/cryptomator/cryptofs/DirectoryIdBackup.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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.UTF_8)));
}
}

static EncryptingWritableByteChannel wrapEncryptionAround(ByteChannel channel, Cryptor cryptor) {
return new EncryptingWritableByteChannel(channel, cryptor);
}
}
2 changes: 2 additions & 0 deletions 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 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
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public class CryptoFileSystemImplTest {
private final Symlinks symlinks = mock(Symlinks.class);
private final CryptoPathMapper cryptoPathMapper = mock(CryptoPathMapper.class);
private final DirectoryIdProvider dirIdProvider = mock(DirectoryIdProvider.class);
private final DirectoryIdBackup dirIdBackup = mock(DirectoryIdBackup.class);
private final AttributeProvider fileAttributeProvider = mock(AttributeProvider.class);
private final AttributeByNameProvider fileAttributeByNameProvider = mock(AttributeByNameProvider.class);
private final AttributeViewProvider fileAttributeViewProvider = mock(AttributeViewProvider.class);
Expand Down Expand Up @@ -118,7 +119,7 @@ public void setup() {

inTest = new CryptoFileSystemImpl(provider, cryptoFileSystems, pathToVault, cryptor,
fileStore, stats, cryptoPathMapper, cryptoPathFactory,
pathMatcherFactory, directoryStreamFactory, dirIdProvider,
pathMatcherFactory, directoryStreamFactory, dirIdProvider, dirIdBackup,
fileAttributeProvider, fileAttributeByNameProvider, fileAttributeViewProvider,
openCryptoFiles, symlinks, finallyUtil, ciphertextDirDeleter, readonlyFlag,
fileSystemProperties);
Expand Down Expand Up @@ -1103,6 +1104,37 @@ public void createDirectoryClearsDirIdAndDeletesDirFileIfCreatingDirFails() thro
verify(cryptoPathMapper).invalidatePathMapping(path);
}

@Test
public void createDirectoryBackupsDirIdInCiphertextDirPath() throws IOException {
Path ciphertextParent = mock(Path.class, "ciphertextParent");
Path ciphertextRawPath = mock(Path.class, "d/00/00/path.c9r");
Path ciphertextDirFile = mock(Path.class, "d/00/00/path.c9r/dir.c9r");
Path ciphertextDirPath = mock(Path.class, "d/FF/FF/");
CiphertextFilePath ciphertextPath = mock(CiphertextFilePath.class, "ciphertext");
String dirId = "DirId1234ABC";
CiphertextDirectory cipherDirObject = new CiphertextDirectory(dirId, ciphertextDirPath);
FileChannelMock channel = new FileChannelMock(100);
when(ciphertextRawPath.resolve("dir.c9r")).thenReturn(ciphertextDirFile);
when(cryptoPathMapper.getCiphertextFilePath(path)).thenReturn(ciphertextPath);
when(cryptoPathMapper.getCiphertextDir(path)).thenReturn(cipherDirObject);
when(cryptoPathMapper.getCiphertextDir(parent)).thenReturn(new CiphertextDirectory("parentDirId", ciphertextDirPath));
when(cryptoPathMapper.getCiphertextFileType(path)).thenThrow(NoSuchFileException.class);
when(ciphertextPath.getRawPath()).thenReturn(ciphertextRawPath);
when(ciphertextPath.getDirFilePath()).thenReturn(ciphertextDirFile);
when(ciphertextParent.getFileSystem()).thenReturn(fileSystem);
when(ciphertextRawPath.getFileSystem()).thenReturn(fileSystem);
when(ciphertextDirFile.getFileSystem()).thenReturn(fileSystem);
when(ciphertextDirPath.getFileSystem()).thenReturn(fileSystem);
when(ciphertextDirFile.getName(3)).thenReturn(mock(Path.class, "path.c9r"));
when(provider.newFileChannel(ciphertextDirFile, EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE))).thenReturn(channel);

inTest.createDirectory(path);

verify(readonlyFlag).assertWritable();
verify(dirIdBackup, Mockito.times(1)).execute(cipherDirObject);
}


}

@Nested
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mockito;
Expand All @@ -28,6 +29,7 @@
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.Predicate;
import java.util.stream.Stream;

import static java.nio.file.StandardOpenOption.CREATE_NEW;
Expand Down Expand Up @@ -159,21 +161,35 @@ private Path firstEmptyCiphertextDirectory() throws IOException {
try (Stream<Path> allFilesInVaultDir = Files.walk(pathToVault)) {
return allFilesInVaultDir //
.filter(Files::isDirectory) //
.filter(this::isEmptyDirectory) //
.filter(this::isEmptyCryptoFsDirectory) //
.filter(this::isEncryptedDirectory) //
.findFirst() //
.get();
}
}

private boolean isEmptyDirectory(Path path) {
private boolean isEmptyCryptoFsDirectory(Path path) {
Predicate<Path> isIgnoredFile = p -> Constants.DIR_ID_FILE.equals(p.getFileName().toString());
try (Stream<Path> files = Files.list(path)) {
return files.count() == 0;
return files.noneMatch(isIgnoredFile.negate());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

@Test
@DisplayName("Tests internal cryptofs directory emptiness definition")
public void testCryptoFsDirEmptiness() throws IOException {
var emptiness = pathToVault.getParent().resolve("emptiness");
var ignoredFile = emptiness.resolve(Constants.DIR_ID_FILE);
Files.createDirectory(emptiness);
Files.createFile(ignoredFile);

boolean result = isEmptyCryptoFsDirectory(emptiness);

Assertions.assertTrue(result, "Ciphertext directory containing only dirId-file should be accepted as an empty dir");
}

private boolean isEncryptedDirectory(Path pathInVault) {
Path relativePath = pathToVault.relativize(pathInVault);
String relativePathAsString = relativePath.toString().replace(File.separatorChar, '/');
Expand Down
67 changes: 67 additions & 0 deletions src/test/java/org/cryptomator/cryptofs/DirectoryIdBackupTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.cryptomator.cryptofs;

import org.cryptomator.cryptofs.common.Constants;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.MockedStatic;
import org.mockito.Mockito;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class DirectoryIdBackupTest {

@TempDir
Path contentPath;

private String dirId = "12345678";
private CryptoPathMapper.CiphertextDirectory cipherDirObject;
private EncryptingWritableByteChannel encChannel;
private Cryptor cryptor;

private DirectoryIdBackup dirIdBackup;


@BeforeEach
public void init() {
cipherDirObject = new CryptoPathMapper.CiphertextDirectory(dirId, contentPath);
cryptor = Mockito.mock(Cryptor.class);
encChannel = Mockito.mock(EncryptingWritableByteChannel.class);

dirIdBackup = new DirectoryIdBackup(cryptor);
}

@Test
public void testIdFileCreated() throws IOException {
try (MockedStatic<DirectoryIdBackup> backupMock = Mockito.mockStatic(DirectoryIdBackup.class)) {
backupMock.when(() -> DirectoryIdBackup.wrapEncryptionAround(Mockito.any(), Mockito.eq(cryptor))).thenReturn(encChannel);
Mockito.when(encChannel.write(Mockito.any())).thenReturn(0);

dirIdBackup.execute(cipherDirObject);

Assertions.assertTrue(Files.exists(contentPath.resolve(Constants.DIR_ID_FILE)));
}
}

@Test
public void testContentIsWritten() throws IOException {
Mockito.when(encChannel.write(Mockito.any())).thenReturn(0);
var expectedWrittenContent = ByteBuffer.wrap(dirId.getBytes(StandardCharsets.UTF_8));

try (MockedStatic<DirectoryIdBackup> backupMock = Mockito.mockStatic(DirectoryIdBackup.class)) {
backupMock.when(() -> DirectoryIdBackup.wrapEncryptionAround(Mockito.any(), Mockito.eq(cryptor))).thenReturn(encChannel);

dirIdBackup.execute(cipherDirObject);

Mockito.verify(encChannel, Mockito.times(1)).write(Mockito.argThat(b -> b.equals(expectedWrittenContent)));
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatcher;
import org.mockito.Mockito;

import java.io.IOException;
Expand All @@ -32,7 +31,7 @@ public class CryptoDirectoryStreamTest {
private static final Consumer<CryptoDirectoryStream> DO_NOTHING_ON_CLOSE = ignored -> {
};
private static final Filter<? super Path> ACCEPT_ALL = ignored -> true;

private NodeProcessor nodeProcessor;
private DirectoryStream<Path> dirStream;

Expand Down Expand Up @@ -70,7 +69,7 @@ public void testDirListing() throws IOException {
Mockito.doAnswer(invocation -> {
return Stream.empty();
}).when(nodeProcessor).process(Mockito.argThat(node -> node.fullCiphertextFileName.equals("invalidCiphertext")));

try (CryptoDirectoryStream stream = new CryptoDirectoryStream("foo", dirStream, cleartextPath, ACCEPT_ALL, DO_NOTHING_ON_CLOSE, nodeProcessor)) {
Iterator<Path> iter = stream.iterator();
Assertions.assertTrue(iter.hasNext());
Expand Down
Loading