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
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,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);
dirIdBackup.write(ciphertextDir);
ciphertextPath.persistLongFileName();
} catch (IOException e) {
// make sure there is no orphan dir file:
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 CiphertextDirectory(Constants.ROOT_DIR_ID, vaultCipherRootPath));
DirectoryIdBackup.write(cryptor, new CiphertextDirectory(Constants.ROOT_DIR_ID, vaultCipherRootPath));
} finally {
Arrays.fill(rawKey, (byte) 0x00);
}
Expand Down
72 changes: 62 additions & 10 deletions src/main/java/org/cryptomator/cryptofs/DirectoryIdBackup.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.cryptomator.cryptofs;

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

import javax.inject.Inject;
Expand All @@ -10,31 +12,32 @@
import java.nio.channels.ByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

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

private Cryptor cryptor;
private final Cryptor cryptor;

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

/**
* Performs the backup operation for the given {@link CiphertextDirectory} object.
* Writes the dirId backup file for the {@link CiphertextDirectory} object.
* <p>
* The directory id is written via an encrypting channel to the file {@link CiphertextDirectory#path()} /{@value Constants#DIR_BACKUP_FILE_NAME}.
* The directory id is written via an encrypting channel to the file {@link CiphertextDirectory#path()}.resolve({@value Constants#DIR_ID_BACKUP_FILE_NAME});
*
* @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(CiphertextDirectory ciphertextDirectory) throws IOException {
try (var channel = Files.newByteChannel(ciphertextDirectory.path().resolve(Constants.DIR_BACKUP_FILE_NAME), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); //
public void write(CiphertextDirectory ciphertextDirectory) throws IOException {
try (var channel = Files.newByteChannel(getBackupFilePath(ciphertextDirectory.path()), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); //
var encryptingChannel = wrapEncryptionAround(channel, cryptor)) {
encryptingChannel.write(ByteBuffer.wrap(ciphertextDirectory.dirId().getBytes(StandardCharsets.US_ASCII)));
}
Expand All @@ -43,16 +46,65 @@ public void execute(CiphertextDirectory ciphertextDirectory) throws IOException
/**
* Static method to explicitly back up the directory id for a specified ciphertext directory.
*
* @param cryptor The cryptor to be used
* @param cryptor The cryptor to be used for encryption
* @param ciphertextDirectory A {@link 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, CiphertextDirectory ciphertextDirectory) throws IOException {
new DirectoryIdBackup(cryptor).execute(ciphertextDirectory);
public static void write(Cryptor cryptor, CiphertextDirectory ciphertextDirectory) throws IOException {
new DirectoryIdBackup(cryptor).write(ciphertextDirectory);
}


static EncryptingWritableByteChannel wrapEncryptionAround(ByteChannel channel, Cryptor cryptor) {
/**
* Reads the dirId backup file and retrieves the directory id from it.
*
* @param ciphertextContentDir path of a ciphertext <strong>content</strong> directory
* @return a byte array containing the directory id
* @throws IOException if the dirId backup file cannot be read
* @throws CryptoException if the content of dirId backup file cannot be decrypted/authenticated
* @throws IllegalStateException if the directory id exceeds {@value Constants#MAX_DIR_ID_LENGTH} chars
*/
public byte[] read(Path ciphertextContentDir) throws IOException, CryptoException, IllegalStateException {
var dirIdBackupFile = getBackupFilePath(ciphertextContentDir);
var dirIdBuffer = ByteBuffer.allocate(Constants.MAX_DIR_ID_LENGTH + 1); //a dir id contains at most 36 ascii chars, we add for security checks one more

try (var channel = Files.newByteChannel(dirIdBackupFile, StandardOpenOption.READ); //
var decryptingChannel = wrapDecryptionAround(channel, cryptor)) {
int read = decryptingChannel.read(dirIdBuffer);
if (read < 0 || read > Constants.MAX_DIR_ID_LENGTH) {
throw new IllegalStateException("Read directory id exceeds the maximum length of %d characters".formatted(Constants.MAX_DIR_ID_LENGTH));
}
}

var dirId = new byte[dirIdBuffer.position()];
dirIdBuffer.get(0, dirId);
return dirId;
}

/**
* Static method to explicitly retrieve the directory id of a ciphertext directory from the dirId backup file
*
* @param cryptor The cryptor to be used for decryption
* @param ciphertextContentDir path of a ciphertext <strong>content</strong> directory
* @return a byte array containing the directory id
* @throws IOException if the dirId backup file cannot be read
* @throws CryptoException if the content of dirId backup file cannot be decrypted/authenticated
* @throws IllegalStateException if the directory id exceeds {@value Constants#MAX_DIR_ID_LENGTH} chars
*/
public static byte[] read(Cryptor cryptor, Path ciphertextContentDir) throws IOException, CryptoException, IllegalStateException {
return new DirectoryIdBackup(cryptor).read(ciphertextContentDir);
}


private static Path getBackupFilePath(Path ciphertextContentDir) {
return ciphertextContentDir.resolve(Constants.DIR_ID_BACKUP_FILE_NAME);
}

DecryptingReadableByteChannel wrapDecryptionAround(ByteChannel channel, Cryptor cryptor) {
return new DecryptingReadableByteChannel(channel, cryptor, true);
}

EncryptingWritableByteChannel wrapEncryptionAround(ByteChannel channel, Cryptor cryptor) {
return new EncryptingWritableByteChannel(channel, cryptor);
}
}
4 changes: 2 additions & 2 deletions src/main/java/org/cryptomator/cryptofs/common/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ private Constants() {
public static final String SYMLINK_FILE_NAME = "symlink.c9r";
public static final String CONTENTS_FILE_NAME = "contents.c9r";
public static final String INFLATED_FILE_NAME = "name.c9s";
public static final String DIR_BACKUP_FILE_NAME = "dirid.c9r";
public static final String DIR_ID_BACKUP_FILE_NAME = "dirid.c9r";

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 MAX_DIR_ID_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 = "/";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import java.util.stream.Stream;

import static org.cryptomator.cryptofs.common.Constants.DIR_FILE_NAME;
import static org.cryptomator.cryptofs.common.Constants.MAX_DIR_FILE_LENGTH;
import static org.cryptomator.cryptofs.common.Constants.MAX_DIR_ID_LENGTH;
import static org.cryptomator.cryptofs.common.Constants.MAX_SYMLINK_LENGTH;
import static org.cryptomator.cryptofs.common.Constants.SYMLINK_FILE_NAME;

Expand Down Expand Up @@ -127,7 +127,7 @@ private boolean resolveConflictTrivially(Path canonicalPath, Path conflictingPat
if (!Files.exists(canonicalPath)) {
Files.move(conflictingPath, canonicalPath); // boom. conflict solved.
return true;
} else if (hasSameFileContent(conflictingPath.resolve(DIR_FILE_NAME), canonicalPath.resolve(DIR_FILE_NAME), MAX_DIR_FILE_LENGTH)) {
} else if (hasSameFileContent(conflictingPath.resolve(DIR_FILE_NAME), canonicalPath.resolve(DIR_FILE_NAME), MAX_DIR_ID_LENGTH)) {
LOG.info("Removing conflicting directory {} (identical to {})", conflictingPath, canonicalPath);
MoreFiles.deleteRecursively(conflictingPath, RecursiveDeleteOption.ALLOW_INSECURE);
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@

public interface DirectoryStreamFilters {

static DirectoryStream.Filter<Path> EXCLUDE_DIR_ID_BACKUP = p -> !p.equals(p.resolveSibling(Constants.DIR_BACKUP_FILE_NAME));
static DirectoryStream.Filter<Path> EXCLUDE_DIR_ID_BACKUP = p -> !p.equals(p.resolveSibling(Constants.DIR_ID_BACKUP_FILE_NAME));

}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public void check(Path pathToVault, VaultConfig config, Masterkey masterkey, Cry
if (foundDir) {
iter.remove();
var expectedDirVaultRel = Path.of(Constants.DATA_DIR_NAME).resolve(expectedDir);
if (Files.exists(pathToVault.resolve(expectedDirVaultRel).resolve(Constants.DIR_BACKUP_FILE_NAME))) {
if (Files.exists(pathToVault.resolve(expectedDirVaultRel).resolve(Constants.DIR_ID_BACKUP_FILE_NAME))) {
resultCollector.accept(new HealthyDir(dirId, dirFile, expectedDirVaultRel));
} else {
resultCollector.accept(new MissingDirIdBackup(dirId, expectedDirVaultRel));
Expand Down Expand Up @@ -116,7 +116,7 @@ private FileVisitResult visitDirFile(Path dirFile, BasicFileAttributes attrs) th
return FileVisitResult.CONTINUE;
}

if (attrs.size() > Constants.MAX_DIR_FILE_LENGTH) {
if (attrs.size() > Constants.MAX_DIR_ID_LENGTH) {
LOG.warn("Encountered dir.c9r file of size {}", attrs.size());
resultCollector.accept(new ObeseDirFile(dirFile, attrs.size()));
} else if (attrs.size() == 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ void fix(Path pathToVault, Cryptor cryptor) throws IOException {
var dirIdHash = cryptor.fileNameCryptor().hashDirectoryId(dirId);
Path dirPath = pathToVault.resolve(Constants.DATA_DIR_NAME).resolve(dirIdHash.substring(0, 2)).resolve(dirIdHash.substring(2, 32));
Files.createDirectories(dirPath);
DirectoryIdBackup.backupManually(cryptor, new CiphertextDirectory(dirId, dirPath));
DirectoryIdBackup.write(cryptor, new CiphertextDirectory(dirId, dirPath));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import java.util.Optional;

/**
* The dir id backup file {@value org.cryptomator.cryptofs.common.Constants#DIR_BACKUP_FILE_NAME} is missing.
* The dir id backup file {@value org.cryptomator.cryptofs.common.Constants#DIR_ID_BACKUP_FILE_NAME} is missing.
*/
public record MissingDirIdBackup(String dirId, Path contentDir) implements DiagnosticResult {

Expand All @@ -29,7 +29,7 @@ public String toString() {
//visible for testing
void fix(Path pathToVault, Cryptor cryptor) throws IOException {
Path absCipherDir = pathToVault.resolve(contentDir);
DirectoryIdBackup.backupManually(cryptor, new CiphertextDirectory(dirId, absCipherDir));
DirectoryIdBackup.write(cryptor, new CiphertextDirectory(dirId, absCipherDir));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public Severity getSeverity() {

@Override
public String toString() {
return String.format("Unexpected file size of %s: %d should be ≤ %d", dirFile, size, Constants.MAX_DIR_FILE_LENGTH);
return String.format("Unexpected file size of %s: %d should be ≤ %d", dirFile, size, Constants.MAX_DIR_ID_LENGTH);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,15 @@
import org.cryptomator.cryptofs.common.Constants;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.FileNameCryptor;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.common.ByteBuffers;
import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
Expand Down Expand Up @@ -89,7 +87,7 @@ private void fix(Path pathToVault, VaultConfig config, Cryptor cryptor) throws I
AtomicInteger dirCounter = new AtomicInteger(1);
AtomicInteger symlinkCounter = new AtomicInteger(1);
String longNameSuffix = createClearnameToBeShortened(config.getShorteningThreshold());
Optional<String> dirId = retrieveDirId(orphanedDir, cryptor);
Optional<byte[]> dirId = retrieveDirId(orphanedDir, cryptor);

try (var orphanedContentStream = Files.newDirectoryStream(orphanedDir, this::matchesEncryptedContentPattern)) {
for (Path orphanedResource : orphanedContentStream) {
Expand All @@ -112,7 +110,7 @@ private void fix(Path pathToVault, VaultConfig config, Cryptor cryptor) throws I
adoptOrphanedResource(orphanedResource, newClearName, isShortened, stepParentDir, cryptor.fileNameCryptor(), sha1);
}
}
Files.deleteIfExists(orphanedDir.resolve(Constants.DIR_BACKUP_FILE_NAME));
Files.deleteIfExists(orphanedDir.resolve(Constants.DIR_ID_BACKUP_FILE_NAME));
try (var nonCryptomatorFiles = Files.newDirectoryStream(orphanedDir)) {
for (Path p : nonCryptomatorFiles) {
Files.move(p, stepParentDir.path().resolve(p.getFileName()), LinkOption.NOFOLLOW_LINKS);
Expand Down Expand Up @@ -172,37 +170,26 @@ CiphertextDirectory prepareStepParent(Path dataDir, Path cipherRecoveryDir, Cryp
var stepParentCipherDir = new CiphertextDirectory(stepParentUUID, stepParentDir);
//only if it does not exist
try {
DirectoryIdBackup.backupManually(cryptor, stepParentCipherDir);
DirectoryIdBackup.write(cryptor, stepParentCipherDir);
} catch (FileAlreadyExistsException e) {
// already exists due to a previous recovery attempt
}
return stepParentCipherDir;
}

//visible for testing
Optional<String> retrieveDirId(Path orphanedDir, Cryptor cryptor) {
var dirIdFile = orphanedDir.resolve(Constants.DIR_BACKUP_FILE_NAME);
var dirIdBuffer = ByteBuffer.allocate(36); //a dir id contains at most 36 ascii chars

try (var channel = Files.newByteChannel(dirIdFile, StandardOpenOption.READ); //
var decryptingChannel = createDecryptingReadableByteChannel(channel, cryptor)) {
ByteBuffers.fill(decryptingChannel, dirIdBuffer);
dirIdBuffer.flip();
} catch (IOException e) {
LOG.info("Unable to read {}.", dirIdFile, e);
Optional<byte []> retrieveDirId(Path orphanedDir, Cryptor cryptor) {
try {
byte[] dirId = DirectoryIdBackup.read(cryptor, orphanedDir);
return Optional.of(dirId);
} catch (IOException | CryptoException | IllegalStateException e) {
LOG.info("Unable to retrieve directory id for directory {}", orphanedDir, e);
return Optional.empty();
}

return Optional.of(StandardCharsets.US_ASCII.decode(dirIdBuffer).toString());
}

//exists and visible for testability
DecryptingReadableByteChannel createDecryptingReadableByteChannel(ByteChannel channel, Cryptor cryptor) {
return new DecryptingReadableByteChannel(channel, cryptor, true);
}

//visible for testing
String decryptFileName(Path orphanedResource, boolean isShortened, String dirId, FileNameCryptor cryptor) throws IOException, AuthenticationFailedException {
String decryptFileName(Path orphanedResource, boolean isShortened, byte [] dirId, FileNameCryptor cryptor) throws IOException, AuthenticationFailedException {
final String filenameWithExtension;
if (isShortened) {
filenameWithExtension = Files.readString(orphanedResource.resolve(Constants.INFLATED_FILE_NAME));
Expand All @@ -211,7 +198,7 @@ String decryptFileName(Path orphanedResource, boolean isShortened, String dirId,
}

final String filename = filenameWithExtension.substring(0, filenameWithExtension.length() - Constants.CRYPTOMATOR_FILE_SUFFIX.length());
return cryptor.decryptFilename(BaseEncoding.base64Url(), filename, dirId.getBytes(StandardCharsets.UTF_8));
return cryptor.decryptFilename(BaseEncoding.base64Url(), filename, dirId);
}

// visible for testing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1277,7 +1277,7 @@ public void createDirectoryBackupsDirIdInCiphertextDirPath() throws IOException
inTest.createDirectory(path);

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


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ public void testInitialize() throws IOException, MasterkeyLoadingFailedException
Optional<Path> dirIdBackup = Files.list(rootDir.get()).findFirst();
Assertions.assertTrue(dirIdBackup.isPresent());
Assertions.assertTrue(Files.isRegularFile(dirIdBackup.get()));
Assertions.assertEquals(Constants.DIR_BACKUP_FILE_NAME, dirIdBackup.get().getFileName().toString());
Assertions.assertEquals(Constants.DIR_ID_BACKUP_FILE_NAME, dirIdBackup.get().getFileName().toString());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ private Path firstEmptyCiphertextDirectory() throws IOException {
}

private boolean isEmptyCryptoFsDirectory(Path path) {
Predicate<Path> isIgnoredFile = p -> Constants.DIR_BACKUP_FILE_NAME.equals(p.getFileName().toString());
Predicate<Path> isIgnoredFile = p -> Constants.DIR_ID_BACKUP_FILE_NAME.equals(p.getFileName().toString());
try (Stream<Path> files = Files.list(path)) {
return files.noneMatch(isIgnoredFile.negate());
} catch (IOException e) {
Expand All @@ -181,7 +181,7 @@ private boolean isEmptyCryptoFsDirectory(Path path) {
@DisplayName("Tests internal cryptofs directory emptiness definition")
public void testCryptoFsDirEmptiness() throws IOException {
var emptiness = pathToVault.getParent().resolve("emptiness");
var ignoredFile = emptiness.resolve(Constants.DIR_BACKUP_FILE_NAME);
var ignoredFile = emptiness.resolve(Constants.DIR_ID_BACKUP_FILE_NAME);
Files.createDirectory(emptiness);
Files.createFile(ignoredFile);

Expand Down
Loading
Loading