diff --git a/.github/workflows/publish-central.yml b/.github/workflows/publish-central.yml index 2e2882c9..073a7426 100644 --- a/.github/workflows/publish-central.yml +++ b/.github/workflows/publish-central.yml @@ -36,4 +36,4 @@ jobs: MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} MAVEN_GPG_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import - MAVEN_GPG_KEY_FINGERPRINT: "58117AFA1F85B3EEC154677D615D449FE6E6A235" \ No newline at end of file + MAVEN_GPG_KEY_FINGERPRINT: ${{ vars.RELEASES_GPG_KEY_FINGERPRINT }} \ No newline at end of file diff --git a/.github/workflows/publish-github.yml b/.github/workflows/publish-github.yml index 5980df5c..170a583d 100644 --- a/.github/workflows/publish-github.yml +++ b/.github/workflows/publish-github.yml @@ -23,7 +23,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} MAVEN_GPG_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import - MAVEN_GPG_KEY_FINGERPRINT: "58117AFA1F85B3EEC154677D615D449FE6E6A235" + MAVEN_GPG_KEY_FINGERPRINT: ${{ vars.RELEASES_GPG_KEY_FINGERPRINT }} - name: Slack Notification uses: rtCamp/action-slack-notify@v2 env: diff --git a/pom.xml b/pom.xml index 79aaa787..6ad3747e 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.cryptomator cryptofs - 2.7.1 + 2.7.2 Cryptomator Crypto Filesystem This library provides the Java filesystem provider used by Cryptomator. https://github.com/cryptomator/cryptofs diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystem.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystem.java index 7f82e2d6..de268656 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystem.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystem.java @@ -1,5 +1,7 @@ package org.cryptomator.cryptofs; +import org.cryptomator.cryptofs.common.Constants; + import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.Files; @@ -41,6 +43,26 @@ public abstract class CryptoFileSystem extends FileSystem { */ public abstract Path getCiphertextPath(Path cleartextPath) throws IOException; + /** + * Computes from a valid,encrypted node (file or folder) its cleartext name. + *

+ * Due to the structure of a vault, an encrypted node is valid if: + *

+ * + * @param ciphertextNode path to the ciphertext file or directory + * @return the cleartext name of the ciphertext file or directory + * @throws java.nio.file.NoSuchFileException if the ciphertextFile does not exist + * @throws IOException if an I/O error occurs reading the ciphertext files + * @throws IllegalArgumentException if {@param ciphertextNode} is not a valid ciphertext content node of the vault + * @throws UnsupportedOperationException if the directory containing the {@param ciphertextNode} does not have a {@value Constants#DIR_ID_BACKUP_FILE_NAME} file + */ + public abstract String getCleartextName(Path ciphertextNode) throws IOException, UnsupportedOperationException; + /** * Provides file system performance statistics. * diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java index b0c67943..905e508d 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java @@ -17,8 +17,8 @@ import org.cryptomator.cryptofs.common.DeletingFileVisitor; import org.cryptomator.cryptofs.common.FinallyUtil; import org.cryptomator.cryptofs.dir.CiphertextDirectoryDeleter; -import org.cryptomator.cryptofs.dir.DirectoryStreamFilters; import org.cryptomator.cryptofs.dir.DirectoryStreamFactory; +import org.cryptomator.cryptofs.dir.DirectoryStreamFilters; import org.cryptomator.cryptofs.fh.OpenCryptoFiles; import org.cryptomator.cryptolib.api.Cryptor; @@ -95,16 +95,17 @@ class CryptoFileSystemImpl extends CryptoFileSystem { private final CryptoPath rootPath; private final CryptoPath emptyPath; + private final FileNameDecryptor fileNameDecryptor; private volatile boolean open = true; @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, DirectoryIdBackup dirIdBackup, - AttributeProvider fileAttributeProvider, AttributeByNameProvider fileAttributeByNameProvider, AttributeViewProvider fileAttributeViewProvider, - OpenCryptoFiles openCryptoFiles, Symlinks symlinks, FinallyUtil finallyUtil, CiphertextDirectoryDeleter ciphertextDirDeleter, ReadonlyFlag readonlyFlag, - CryptoFileSystemProperties fileSystemProperties) { + 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, DirectoryIdBackup dirIdBackup, // + AttributeProvider fileAttributeProvider, AttributeByNameProvider fileAttributeByNameProvider, AttributeViewProvider fileAttributeViewProvider, // + OpenCryptoFiles openCryptoFiles, Symlinks symlinks, FinallyUtil finallyUtil, CiphertextDirectoryDeleter ciphertextDirDeleter, ReadonlyFlag readonlyFlag, // + CryptoFileSystemProperties fileSystemProperties, FileNameDecryptor fileNameDecryptor) { this.provider = provider; this.cryptoFileSystems = cryptoFileSystems; this.pathToVault = pathToVault; @@ -129,6 +130,7 @@ public CryptoFileSystemImpl(CryptoFileSystemProvider provider, CryptoFileSystems this.rootPath = cryptoPathFactory.rootFor(this); this.emptyPath = cryptoPathFactory.emptyFor(this); + this.fileNameDecryptor = fileNameDecryptor; } @Override @@ -151,6 +153,11 @@ public Path getCiphertextPath(Path cleartextPath) throws IOException { } } + @Override + public String getCleartextName(Path ciphertextNode) throws IOException, UnsupportedOperationException { + return fileNameDecryptor.decryptFilename(ciphertextNode); + } + @Override public CryptoFileSystemStats getStats() { return stats; @@ -331,7 +338,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: diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java index 1360d6d7..6d4abd45 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java @@ -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); } diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java index e64ab36a..1efd9988 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java @@ -34,13 +34,11 @@ class CryptoFileSystems { private final ConcurrentMap fileSystems = new ConcurrentHashMap<>(); private final CryptoFileSystemComponent.Factory cryptoFileSystemComponentFactory; - private final FileSystemCapabilityChecker capabilityChecker; private final SecureRandom csprng; @Inject - public CryptoFileSystems(CryptoFileSystemComponent.Factory cryptoFileSystemComponentFactory, FileSystemCapabilityChecker capabilityChecker, SecureRandom csprng) { + public CryptoFileSystems(CryptoFileSystemComponent.Factory cryptoFileSystemComponentFactory, SecureRandom csprng) { this.cryptoFileSystemComponentFactory = cryptoFileSystemComponentFactory; - this.capabilityChecker = capabilityChecker; this.csprng = csprng; } @@ -53,13 +51,12 @@ public CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathT try (Masterkey key = properties.keyLoader().loadKey(keyId)) { var config = configLoader.verify(key.getEncoded(), Constants.VAULT_VERSION); backupVaultConfigFile(normalizedPathToVault, properties); - var adjustedProperties = adjustForCapabilities(pathToVault, properties); var cryptor = CryptorProvider.forScheme(config.getCipherCombo()).provide(key.copy(), csprng); try { checkVaultRootExistence(pathToVault, cryptor); return fileSystems.compute(normalizedPathToVault, (path, fs) -> { if (fs == null) { - return cryptoFileSystemComponentFactory.create(cryptor, config, provider, normalizedPathToVault, adjustedProperties).cryptoFileSystem(); + return cryptoFileSystemComponentFactory.create(cryptor, config, provider, normalizedPathToVault, properties).cryptoFileSystem(); } else { throw new FileSystemAlreadyExistsException(); } @@ -123,23 +120,6 @@ private void backupVaultConfigFile(Path pathToVault, CryptoFileSystemProperties BackupHelper.attemptBackup(vaultConfigFile); } - private CryptoFileSystemProperties adjustForCapabilities(Path pathToVault, CryptoFileSystemProperties originalProperties) throws FileSystemCapabilityChecker.MissingCapabilityException { - if (!originalProperties.readonly()) { - try { - capabilityChecker.assertWriteAccess(pathToVault); - return originalProperties; - } catch (FileSystemCapabilityChecker.MissingCapabilityException e) { - capabilityChecker.assertReadAccess(pathToVault); - LOG.warn("No write access to vault. Fallback to read-only access."); - Set flags = EnumSet.copyOf(originalProperties.flags()); - flags.add(CryptoFileSystemProperties.FileSystemFlags.READONLY); - return CryptoFileSystemProperties.cryptoFileSystemPropertiesFrom(originalProperties).withFlags(flags).build(); - } - } else { - return originalProperties; - } - } - public void remove(CryptoFileSystemImpl cryptoFileSystem) { fileSystems.values().remove(cryptoFileSystem); } diff --git a/src/main/java/org/cryptomator/cryptofs/DirectoryIdBackup.java b/src/main/java/org/cryptomator/cryptofs/DirectoryIdBackup.java index 6dc2f7fe..5be4f60f 100644 --- a/src/main/java/org/cryptomator/cryptofs/DirectoryIdBackup.java +++ b/src/main/java/org/cryptomator/cryptofs/DirectoryIdBackup.java @@ -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; @@ -10,15 +12,16 @@ 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) { @@ -26,15 +29,15 @@ public DirectoryIdBackup(Cryptor cryptor) { } /** - * Performs the backup operation for the given {@link CiphertextDirectory} object. + * Writes the dirId backup file for the {@link CiphertextDirectory} object. *

- * 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))); } @@ -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 content 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 content 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); } } diff --git a/src/main/java/org/cryptomator/cryptofs/FileNameDecryptor.java b/src/main/java/org/cryptomator/cryptofs/FileNameDecryptor.java new file mode 100644 index 00000000..ff312c8a --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/FileNameDecryptor.java @@ -0,0 +1,99 @@ +package org.cryptomator.cryptofs; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.io.BaseEncoding; +import org.cryptomator.cryptofs.common.Constants; +import org.cryptomator.cryptofs.common.StringUtils; +import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.FileNameCryptor; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.FileSystemException; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.stream.Stream; + +/** + * @see CryptoFileSystem#getCleartextName(Path) + */ +@CryptoFileSystemScoped +class FileNameDecryptor { + + private final DirectoryIdBackup dirIdBackup; + private final LongFileNameProvider longFileNameProvider; + private final Path vaultPath; + private final FileNameCryptor fileNameCryptor; + + @Inject + public FileNameDecryptor(@PathToVault Path vaultPath, Cryptor cryptor, DirectoryIdBackup dirIdBackup, LongFileNameProvider longFileNameProvider) { + this.vaultPath = vaultPath; + this.fileNameCryptor = cryptor.fileNameCryptor(); + this.dirIdBackup = dirIdBackup; + this.longFileNameProvider = longFileNameProvider; + } + + public String decryptFilename(Path ciphertextNode) throws IOException, UnsupportedOperationException { + validatePath(ciphertextNode.toAbsolutePath()); + return decryptFilenameInternal(ciphertextNode); + } + + @VisibleForTesting + String decryptFilenameInternal(Path ciphertextNode) throws IOException, UnsupportedOperationException { + byte[] dirId = null; + try { + dirId = dirIdBackup.read(ciphertextNode); + } catch (NoSuchFileException e) { + throw new UnsupportedOperationException("Directory does not have a " + Constants.DIR_ID_BACKUP_FILE_NAME + " file."); + } catch (CryptoException | IllegalStateException e) { + throw new FileSystemException(ciphertextNode.toString(), null, "Decryption of dirId backup file failed:" + e); + } + var fullCipherNodeName = ciphertextNode.getFileName().toString(); + var cipherNodeExtension = fullCipherNodeName.substring(fullCipherNodeName.length() - 4); + + String actualEncryptedName = switch (cipherNodeExtension) { + case Constants.CRYPTOMATOR_FILE_SUFFIX -> StringUtils.removeEnd(fullCipherNodeName, Constants.CRYPTOMATOR_FILE_SUFFIX); + case Constants.DEFLATED_FILE_SUFFIX -> longFileNameProvider.inflate(ciphertextNode); + default -> throw new IllegalStateException("SHOULD NOT REACH HERE"); + }; + try { + return fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), actualEncryptedName, dirId); + } catch (CryptoException e) { + throw new FileSystemException(ciphertextNode.toString(), null, "Filname decryption failed:" + e); + } + } + + @VisibleForTesting + void validatePath(Path absolutePath) { + if (!belongsToVault(absolutePath)) { + throw new IllegalArgumentException("Node %s is not a part of vault %s".formatted(absolutePath, vaultPath)); + } + if (!isAtCipherNodeLevel(absolutePath)) { + throw new IllegalArgumentException("Node %s is not located at depth 4 from vault storage root".formatted(absolutePath)); + } + if (!(hasCipherNodeExtension(absolutePath) && hasMinimumFileNameLength(absolutePath))) { + throw new IllegalArgumentException("Node %s does not end with %s or %s or filename is shorter than %d characters.".formatted(absolutePath, Constants.CRYPTOMATOR_FILE_SUFFIX, Constants.DEFLATED_FILE_SUFFIX, Constants.MIN_CIPHER_NAME_LENGTH)); + } + } + + boolean hasCipherNodeExtension(Path p) { + var name = p.getFileName(); + return name != null && Stream.of(Constants.CRYPTOMATOR_FILE_SUFFIX, Constants.DEFLATED_FILE_SUFFIX).anyMatch(name.toString()::endsWith); + } + + boolean isAtCipherNodeLevel(Path absolutPah) { + if (!absolutPah.isAbsolute()) { + throw new IllegalArgumentException("Path " + absolutPah + "must be absolute"); + } + return absolutPah.subpath(vaultPath.getNameCount(), absolutPah.getNameCount()).getNameCount() == 4; + } + + boolean hasMinimumFileNameLength(Path p) { + return p.getFileName().toString().length() >= Constants.MIN_CIPHER_NAME_LENGTH; + } + + boolean belongsToVault(Path p) { + return p.startsWith(vaultPath); + } +} diff --git a/src/main/java/org/cryptomator/cryptofs/ch/ChannelCloseListener.java b/src/main/java/org/cryptomator/cryptofs/ch/ChannelCloseListener.java deleted file mode 100644 index 3ee1c008..00000000 --- a/src/main/java/org/cryptomator/cryptofs/ch/ChannelCloseListener.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.cryptomator.cryptofs.ch; - - -@FunctionalInterface -public interface ChannelCloseListener { - - void closed(CleartextFileChannel channel); - -} \ No newline at end of file diff --git a/src/main/java/org/cryptomator/cryptofs/ch/ChannelComponent.java b/src/main/java/org/cryptomator/cryptofs/ch/ChannelComponent.java index a22b0fa4..2447c10a 100644 --- a/src/main/java/org/cryptomator/cryptofs/ch/ChannelComponent.java +++ b/src/main/java/org/cryptomator/cryptofs/ch/ChannelComponent.java @@ -5,6 +5,7 @@ import org.cryptomator.cryptofs.EffectiveOpenOptions; import java.nio.channels.FileChannel; +import java.util.function.Consumer; @ChannelScoped @Subcomponent @@ -17,7 +18,7 @@ interface Factory { ChannelComponent create(@BindsInstance FileChannel ciphertextChannel, // @BindsInstance EffectiveOpenOptions options, // - @BindsInstance ChannelCloseListener listener); // + @BindsInstance Consumer closeListener); // } } diff --git a/src/main/java/org/cryptomator/cryptofs/ch/CleartextFileChannel.java b/src/main/java/org/cryptomator/cryptofs/ch/CleartextFileChannel.java index 44d165a8..f40a34d7 100644 --- a/src/main/java/org/cryptomator/cryptofs/ch/CleartextFileChannel.java +++ b/src/main/java/org/cryptomator/cryptofs/ch/CleartextFileChannel.java @@ -34,6 +34,7 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReadWriteLock; +import java.util.function.Consumer; import static java.lang.Math.max; import static java.lang.Math.min; @@ -53,11 +54,11 @@ public class CleartextFileChannel extends AbstractFileChannel { private final AtomicLong fileSize; private final AtomicReference lastModified; private final ExceptionsDuringWrite exceptionsDuringWrite; - private final ChannelCloseListener closeListener; + private final Consumer closeListener; private final CryptoFileSystemStats stats; @Inject - public CleartextFileChannel(FileChannel ciphertextFileChannel, FileHeaderHolder fileHeaderHolder, ReadWriteLock readWriteLock, Cryptor cryptor, ChunkCache chunkCache, BufferPool bufferPool, EffectiveOpenOptions options, @OpenFileSize AtomicLong fileSize, @OpenFileModifiedDate AtomicReference lastModified, @CurrentOpenFilePath AtomicReference currentPath, ExceptionsDuringWrite exceptionsDuringWrite, ChannelCloseListener closeListener, CryptoFileSystemStats stats) { + public CleartextFileChannel(FileChannel ciphertextFileChannel, FileHeaderHolder fileHeaderHolder, ReadWriteLock readWriteLock, Cryptor cryptor, ChunkCache chunkCache, BufferPool bufferPool, EffectiveOpenOptions options, @OpenFileSize AtomicLong fileSize, @OpenFileModifiedDate AtomicReference lastModified, @CurrentOpenFilePath AtomicReference currentPath, ExceptionsDuringWrite exceptionsDuringWrite, Consumer closeListener, CryptoFileSystemStats stats) { super(readWriteLock); this.ciphertextFileChannel = ciphertextFileChannel; this.fileHeaderHolder = fileHeaderHolder; @@ -327,7 +328,7 @@ long beginOfChunk(long cleartextPos) { protected void implCloseChannel() throws IOException { var closeActions = List.of(this::flush, // super::implCloseChannel, // - () -> closeListener.closed(this), // + () -> closeListener.accept(ciphertextFileChannel), ciphertextFileChannel::close, // this::tryPersistLastModified); tryAll(closeActions.iterator()); diff --git a/src/main/java/org/cryptomator/cryptofs/common/Constants.java b/src/main/java/org/cryptomator/cryptofs/common/Constants.java index 2df84326..291eaf22 100644 --- a/src/main/java/org/cryptomator/cryptofs/common/Constants.java +++ b/src/main/java/org/cryptomator/cryptofs/common/Constants.java @@ -25,11 +25,13 @@ 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 MIN_CIPHER_NAME_LENGTH = 26; //rounded up base64url encoded (16 bytes IV + 0 bytes empty string) + file suffix = 26 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 MAX_CIPHER_NAME_LENGTH = 220; // calculations done in https://github.com/cryptomator/cryptofs/issues/60#issuecomment-523238303 + public static final int MIN_CIPHER_NAME_LENGTH = 28; //rounded up base64url encoded (16 bytes IV + 0 bytes empty string) + file suffix = 28 ASCII chars + public static final int MAX_ADDITIONAL_PATH_LENGTH = 48; // beginning at d/... see https://github.com/cryptomator/cryptofs/issues/77 public static final String SEPARATOR = "/"; public static final String RECOVERY_DIR_NAME = "LOST+FOUND"; diff --git a/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java b/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java index 7b2cdc47..f976e578 100644 --- a/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java +++ b/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java @@ -7,8 +7,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.inject.Inject; -import javax.inject.Singleton; import java.io.IOException; import java.nio.file.DirectoryIteratorException; import java.nio.file.DirectoryStream; @@ -17,13 +15,9 @@ import java.nio.file.Files; import java.nio.file.Path; -@Singleton -public class FileSystemCapabilityChecker { +public final class FileSystemCapabilityChecker { private static final Logger LOG = LoggerFactory.getLogger(FileSystemCapabilityChecker.class); - private static final int MAX_CIPHERTEXT_NAME_LENGTH = 220; // inclusive. calculations done in https://github.com/cryptomator/cryptofs/issues/60#issuecomment-523238303 - private static final int MIN_CIPHERTEXT_NAME_LENGTH = 28; // base64(iv).c9r - private static final int MAX_ADDITIONAL_PATH_LENGTH = 48; // beginning at d/... see https://github.com/cryptomator/cryptofs/issues/77 public enum Capability { /** @@ -41,8 +35,8 @@ public enum Capability { WRITE_ACCESS, } - @Inject - public FileSystemCapabilityChecker() { + private FileSystemCapabilityChecker() { + } /** @@ -53,7 +47,7 @@ public FileSystemCapabilityChecker() { * @implNote Only short-running tests with constant time are performed * @since 1.9.2 */ - public void assertAllCapabilities(Path pathToVault) throws MissingCapabilityException { + public static void assertAllCapabilities(Path pathToVault) throws MissingCapabilityException { assertReadAccess(pathToVault); assertWriteAccess(pathToVault); } @@ -65,7 +59,7 @@ public void assertAllCapabilities(Path pathToVault) throws MissingCapabilityExce * @throws MissingCapabilityException if the check fails * @since 1.9.3 */ - public void assertReadAccess(Path pathToVault) throws MissingCapabilityException { + public static void assertReadAccess(Path pathToVault) throws MissingCapabilityException { try (DirectoryStream ds = Files.newDirectoryStream(pathToVault)) { assert ds != null; } catch (IOException e) { @@ -80,7 +74,7 @@ public void assertReadAccess(Path pathToVault) throws MissingCapabilityException * @throws MissingCapabilityException if the check fails * @since 1.9.3 */ - public void assertWriteAccess(Path pathToVault) throws MissingCapabilityException { + public static void assertWriteAccess(Path pathToVault) throws MissingCapabilityException { Path checkDir = pathToVault.resolve("c"); try { Files.createDirectories(checkDir); @@ -93,9 +87,9 @@ public void assertWriteAccess(Path pathToVault) throws MissingCapabilityExceptio } } - public int determineSupportedCleartextFileNameLength(Path pathToVault) throws IOException { + public static int determineSupportedCleartextFileNameLength(Path pathToVault) throws IOException { int maxCiphertextLen = determineSupportedCiphertextFileNameLength(pathToVault); - assert maxCiphertextLen >= MIN_CIPHERTEXT_NAME_LENGTH; + assert maxCiphertextLen >= Constants.MIN_CIPHER_NAME_LENGTH; // math explained in https://github.com/cryptomator/cryptofs/issues/60#issuecomment-523238303; // subtract 4 for file extension, base64-decode, subtract 16 for IV return (maxCiphertextLen - 4) / 4 * 3 - 16; @@ -108,22 +102,22 @@ public int determineSupportedCleartextFileNameLength(Path pathToVault) throws IO * @return Number of chars a .c9r file is allowed to have * @throws IOException If unable to perform this check */ - public int determineSupportedCiphertextFileNameLength(Path pathToVault) throws IOException { - int subPathLength = MAX_ADDITIONAL_PATH_LENGTH - 2; // subtract "c/" - return determineSupportedCiphertextFileNameLength(pathToVault.resolve("c"), subPathLength, MIN_CIPHERTEXT_NAME_LENGTH, MAX_CIPHERTEXT_NAME_LENGTH); + public static int determineSupportedCiphertextFileNameLength(Path pathToVault) throws IOException { + int subPathLength = Constants.MAX_ADDITIONAL_PATH_LENGTH - 2; // subtract "c/" + return determineSupportedCiphertextFileNameLength(pathToVault.resolve("c"), subPathLength, Constants.MIN_CIPHER_NAME_LENGTH, Constants.MAX_CIPHER_NAME_LENGTH); } /** * Determines the number of chars a filename is allowed to have inside of subdirectories of dir by running an experiment. * - * @param dir Path to a directory where to conduct the experiment (e.g. /path/to/vault/c) - * @param subPathLength Defines the combined number of chars of the subdirectories inside dir, including slashes but excluding the leading slash. Must be a minimum of 6 + * @param dir Path to a directory where to conduct the experiment (e.g. /path/to/vault/c) + * @param subPathLength Defines the combined number of chars of the subdirectories inside dir, including slashes but excluding the leading slash. Must be a minimum of 6 * @param minFileNameLength The minimum filename length to check * @param maxFileNameLength The maximum filename length to check * @return The supported filename length inside a subdirectory of dir with subPathLength chars * @throws IOException If unable to perform this check */ - public int determineSupportedCiphertextFileNameLength(Path dir, int subPathLength, int minFileNameLength, int maxFileNameLength) throws IOException { + public static int determineSupportedCiphertextFileNameLength(Path dir, int subPathLength, int minFileNameLength, int maxFileNameLength) throws IOException { Preconditions.checkArgument(subPathLength >= 6, "subPathLength must be larger than charcount(a/nnn/)"); Preconditions.checkArgument(minFileNameLength > 0); Preconditions.checkArgument(maxFileNameLength <= 999); @@ -144,7 +138,7 @@ public int determineSupportedCiphertextFileNameLength(Path dir, int subPathLengt } } - private int determineSupportedCiphertextFileNameLength(Path p, int lowerBoundIncl, int upperBoundExcl) { + private static int determineSupportedCiphertextFileNameLength(Path p, int lowerBoundIncl, int upperBoundExcl) { assert lowerBoundIncl < upperBoundExcl; int mid = (lowerBoundIncl + upperBoundExcl) / 2; assert mid < upperBoundExcl; @@ -159,7 +153,7 @@ private int determineSupportedCiphertextFileNameLength(Path p, int lowerBoundInc } } - private boolean canHandleFileNameLength(Path parent, int nameLength) { + private static boolean canHandleFileNameLength(Path parent, int nameLength) { Path checkDir = parent.resolve(String.format("%03d", nameLength)); Path checkFile = checkDir.resolve(Strings.repeat("a", nameLength)); try { @@ -178,7 +172,7 @@ private boolean canHandleFileNameLength(Path parent, int nameLength) { } } - private boolean canListDir(Path dir) { + private static boolean canListDir(Path dir) { try (DirectoryStream ds = Files.newDirectoryStream(dir)) { ds.iterator().hasNext(); // throws DirectoryIteratorException on Windows if child path too long return true; @@ -187,7 +181,7 @@ private boolean canListDir(Path dir) { } } - private void deleteSilently(Path path) { + private static void deleteSilently(Path path) { try { Files.delete(path); } catch (IOException e) { @@ -195,7 +189,7 @@ private void deleteSilently(Path path) { } } - private void deleteRecursivelySilently(Path dir) { + private static void deleteRecursivelySilently(Path dir) { try { if (Files.exists(dir)) { MoreFiles.deleteRecursively(dir, RecursiveDeleteOption.ALLOW_INSECURE); diff --git a/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java b/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java index 957ad555..4a7db946 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java @@ -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; @@ -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; diff --git a/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFilters.java b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFilters.java index 13f87c11..413601d0 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFilters.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFilters.java @@ -7,6 +7,6 @@ public interface DirectoryStreamFilters { - static DirectoryStream.Filter EXCLUDE_DIR_ID_BACKUP = p -> !p.equals(p.resolveSibling(Constants.DIR_BACKUP_FILE_NAME)); + static DirectoryStream.Filter EXCLUDE_DIR_ID_BACKUP = p -> !p.equals(p.resolveSibling(Constants.DIR_ID_BACKUP_FILE_NAME)); } diff --git a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java index 14e5d072..ca507291 100644 --- a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java +++ b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java @@ -1,11 +1,3 @@ -/******************************************************************************* - * 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.fh; import org.cryptomator.cryptofs.EffectiveOpenOptions; @@ -23,8 +15,7 @@ import java.nio.file.attribute.FileTime; import java.time.Instant; import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -35,19 +26,20 @@ public class OpenCryptoFile implements Closeable { private final FileCloseListener listener; private final AtomicReference lastModified; - private final ChunkCache chunkCache; private final Cryptor cryptor; private final FileHeaderHolder headerHolder; private final ChunkIO chunkIO; private final AtomicReference currentFilePath; private final AtomicLong fileSize; private final OpenCryptoFileComponent component; - private final ConcurrentMap openChannels = new ConcurrentHashMap<>(); + + private final AtomicInteger openChannelsCount = new AtomicInteger(0); @Inject - public OpenCryptoFile(FileCloseListener listener, ChunkCache chunkCache, Cryptor cryptor, FileHeaderHolder headerHolder, ChunkIO chunkIO, @CurrentOpenFilePath AtomicReference currentFilePath, @OpenFileSize AtomicLong fileSize, @OpenFileModifiedDate AtomicReference lastModified, OpenCryptoFileComponent component) { + public OpenCryptoFile(FileCloseListener listener, Cryptor cryptor, FileHeaderHolder headerHolder, ChunkIO chunkIO, // + @CurrentOpenFilePath AtomicReference currentFilePath, @OpenFileSize AtomicLong fileSize, // + @OpenFileModifiedDate AtomicReference lastModified, OpenCryptoFileComponent component) { this.listener = listener; - this.chunkCache = chunkCache; this.cryptor = cryptor; this.headerHolder = headerHolder; this.chunkIO = chunkIO; @@ -71,30 +63,26 @@ public synchronized FileChannel newFileChannel(EffectiveOpenOptions options, Fil } FileChannel ciphertextFileChannel = null; CleartextFileChannel cleartextFileChannel = null; + + openChannelsCount.incrementAndGet(); // synchronized context, hence we can proactively increase the number try { ciphertextFileChannel = path.getFileSystem().provider().newFileChannel(path, options.createOpenOptionsForEncryptedFile(), attrs); initFileHeader(options, ciphertextFileChannel); - if (options.truncateExisting()) { - chunkCache.invalidateStale(); - ciphertextFileChannel.truncate(cryptor.fileHeaderCryptor().headerSize()); - fileSize.set(0); - } initFileSize(ciphertextFileChannel); cleartextFileChannel = component.newChannelComponent() // - .create(ciphertextFileChannel, options, this::channelClosed) // + .create(ciphertextFileChannel, options, this::cleartextChannelClosed) // .channel(); + if (options.truncateExisting()) { + cleartextFileChannel.truncate(0); + } } finally { if (cleartextFileChannel == null) { // i.e. something didn't work + cleartextChannelClosed(ciphertextFileChannel); closeQuietly(ciphertextFileChannel); - // is this the first file channel to be opened? - if (openChannels.isEmpty()) { - close(); // then also close the file again. - } } } assert cleartextFileChannel != null; // otherwise there would have been an exception - openChannels.put(cleartextFileChannel, ciphertextFileChannel); chunkIO.registerChannel(ciphertextFileChannel, options.writable()); return cleartextFileChannel; } @@ -183,12 +171,11 @@ public void updateCurrentFilePath(Path newFilePath) { currentFilePath.updateAndGet(p -> p == null ? null : newFilePath); } - private synchronized void channelClosed(CleartextFileChannel cleartextFileChannel) { - FileChannel ciphertextFileChannel = openChannels.remove(cleartextFileChannel); + private synchronized void cleartextChannelClosed(FileChannel ciphertextFileChannel) { if (ciphertextFileChannel != null) { chunkIO.unregisterChannel(ciphertextFileChannel); } - if (openChannels.isEmpty()) { + if (openChannelsCount.decrementAndGet() == 0) { close(); } } diff --git a/src/main/java/org/cryptomator/cryptofs/health/dirid/DirIdCheck.java b/src/main/java/org/cryptomator/cryptofs/health/dirid/DirIdCheck.java index f283d312..f553a9f5 100644 --- a/src/main/java/org/cryptomator/cryptofs/health/dirid/DirIdCheck.java +++ b/src/main/java/org/cryptomator/cryptofs/health/dirid/DirIdCheck.java @@ -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)); @@ -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) { diff --git a/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingContentDir.java b/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingContentDir.java index 28cd3614..0f9df968 100644 --- a/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingContentDir.java +++ b/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingContentDir.java @@ -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 diff --git a/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackup.java b/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackup.java index 9002ba94..9512d5e6 100644 --- a/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackup.java +++ b/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackup.java @@ -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 { @@ -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 diff --git a/src/main/java/org/cryptomator/cryptofs/health/dirid/ObeseDirFile.java b/src/main/java/org/cryptomator/cryptofs/health/dirid/ObeseDirFile.java index c6b019b0..de6edbaa 100644 --- a/src/main/java/org/cryptomator/cryptofs/health/dirid/ObeseDirFile.java +++ b/src/main/java/org/cryptomator/cryptofs/health/dirid/ObeseDirFile.java @@ -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 diff --git a/src/main/java/org/cryptomator/cryptofs/health/dirid/OrphanContentDir.java b/src/main/java/org/cryptomator/cryptofs/health/dirid/OrphanContentDir.java index 350616a9..d8907406 100644 --- a/src/main/java/org/cryptomator/cryptofs/health/dirid/OrphanContentDir.java +++ b/src/main/java/org/cryptomator/cryptofs/health/dirid/OrphanContentDir.java @@ -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; @@ -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 dirId = retrieveDirId(orphanedDir, cryptor); + Optional dirId = retrieveDirId(orphanedDir, cryptor); try (var orphanedContentStream = Files.newDirectoryStream(orphanedDir, this::matchesEncryptedContentPattern)) { for (Path orphanedResource : orphanedContentStream) { @@ -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); @@ -172,7 +170,7 @@ 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 } @@ -180,29 +178,18 @@ CiphertextDirectory prepareStepParent(Path dataDir, Path cipherRecoveryDir, Cryp } //visible for testing - Optional 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 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)); @@ -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 diff --git a/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java b/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java index fcc9ba7e..4025765e 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java @@ -25,11 +25,6 @@ @Module class MigrationModule { - @Provides - FileSystemCapabilityChecker provideFileSystemCapabilityChecker() { - return new FileSystemCapabilityChecker(); - } - @Provides @IntoMap @MigratorKey(Migration.FIVE_TO_SIX) diff --git a/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java index 09a43b95..3f891af2 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java @@ -46,12 +46,10 @@ public class Migrators { private static final MigrationComponent COMPONENT = DaggerMigrationComponent.builder().csprng(strongSecureRandom()).build(); private final Map migrators; - private final FileSystemCapabilityChecker fsCapabilityChecker; @Inject - Migrators(Map migrators, FileSystemCapabilityChecker fsCapabilityChecker) { + Migrators(Map migrators) { this.migrators = migrators; - this.fsCapabilityChecker = fsCapabilityChecker; } private static SecureRandom strongSecureRandom() { @@ -69,9 +67,9 @@ public static Migrators get() { /** * Inspects the vault and checks if it is supported by this library. * - * @param pathToVault Path to the vault's root + * @param pathToVault Path to the vault's root * @param vaultConfigFilename Name of the vault config file located in the vault - * @param masterkeyFilename Name of the masterkey file optionally located in the vault + * @param masterkeyFilename Name of the masterkey file optionally located in the vault * @return true if the vault at the given path is of an older format than supported by this library * @throws IOException if an I/O error occurs parsing the masterkey file */ @@ -83,19 +81,19 @@ public boolean needsMigration(Path pathToVault, String vaultConfigFilename, Stri /** * Performs the actual migration. This task may take a while and this method will block. * - * @param pathToVault Path to the vault's root - * @param vaultConfigFilename Name of the vault config file located inside pathToVault - * @param masterkeyFilename Name of the masterkey file located inside pathToVault - * @param passphrase The passphrase needed to unlock the vault - * @param progressListener Listener that will get notified of progress updates + * @param pathToVault Path to the vault's root + * @param vaultConfigFilename Name of the vault config file located inside pathToVault + * @param masterkeyFilename Name of the masterkey file located inside pathToVault + * @param passphrase The passphrase needed to unlock the vault + * @param progressListener Listener that will get notified of progress updates * @param continuationListener Listener that will get asked if there are events that require feedback - * @throws NoApplicableMigratorException If the vault can not be migrated, because no migrator could be found - * @throws InvalidPassphraseException If the passphrase could not be used to unlock the vault + * @throws NoApplicableMigratorException If the vault can not be migrated, because no migrator could be found + * @throws InvalidPassphraseException If the passphrase could not be used to unlock the vault * @throws FileSystemCapabilityChecker.MissingCapabilityException If the underlying filesystem lacks features required to store a vault - * @throws IOException if an I/O error occurs migrating the vault + * @throws IOException if an I/O error occurs migrating the vault */ public void migrate(Path pathToVault, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws NoApplicableMigratorException, CryptoException, IOException { - fsCapabilityChecker.assertAllCapabilities(pathToVault); + FileSystemCapabilityChecker.assertAllCapabilities(pathToVault); int vaultVersion = determineVaultVersion(pathToVault, vaultConfigFilename, masterkeyFilename); try { Migrator migrator = findApplicableMigrator(vaultVersion).orElseThrow(NoApplicableMigratorException::new); diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java index 86a3fdb8..510c4d99 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java @@ -66,7 +66,7 @@ public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkey LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName()); // check file system capabilities: - int filenameLengthLimit = new FileSystemCapabilityChecker().determineSupportedCiphertextFileNameLength(vaultRoot.resolve("c"), 46, 28, 220); + int filenameLengthLimit = FileSystemCapabilityChecker.determineSupportedCiphertextFileNameLength(vaultRoot.resolve("c"), 46, 28, 220); int pathLengthLimit = filenameLengthLimit + 48; // TODO PreMigrationVisitor preMigrationVisitor; if (filenameLengthLimit >= 220) { diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java index 2bcbaf42..949ae7fc 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java @@ -50,6 +50,8 @@ import java.util.List; import java.util.Random; import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -617,11 +619,13 @@ public void testWriteThenDeleteThenRead() throws IOException { } @RepeatedTest(50) - public void testConcurrentWriteAndTruncate() throws IOException { + public void testConcurrentWriteAndTruncate() throws IOException, InterruptedException { AtomicBoolean keepWriting = new AtomicBoolean(true); - ByteBuffer buf = ByteBuffer.wrap("the quick brown fox jumps over the lazy dog".getBytes(StandardCharsets.UTF_8)); - var executor = Executors.newCachedThreadPool(); - try (FileChannel writingChannel = FileChannel.open(file, WRITE, CREATE)) { + ByteBuffer buf = ByteBuffer.allocate(50_000); // 50 kiB + + try (ExecutorService executor = Executors.newCachedThreadPool(); + FileChannel writingChannel = FileChannel.open(file, WRITE, CREATE)) { + var cdl = new CountDownLatch(3); executor.submit(() -> { while (keepWriting.get()) { try { @@ -630,20 +634,22 @@ public void testConcurrentWriteAndTruncate() throws IOException { throw new UncheckedIOException(e); } buf.flip(); + cdl.countDown(); } }); + cdl.await(); try (FileChannel truncatingChannel = FileChannel.open(file, WRITE, TRUNCATE_EXISTING)) { keepWriting.set(false); } - executor.shutdown(); } - Assertions.assertDoesNotThrow(() -> { - try (FileChannel readingChannel = FileChannel.open(file, READ)) { - var dst = ByteBuffer.allocate(buf.capacity()); + + try (FileChannel readingChannel = FileChannel.open(file, READ)) { + var dst = ByteBuffer.allocate(buf.capacity()); + Assertions.assertDoesNotThrow(() -> { readingChannel.read(dst); - } - }); + }); + } } } diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java index 3b7d8462..5343dfc7 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java @@ -105,6 +105,7 @@ public class CryptoFileSystemImplTest { private final CiphertextDirectoryDeleter ciphertextDirDeleter = mock(CiphertextDirectoryDeleter.class); private final ReadonlyFlag readonlyFlag = mock(ReadonlyFlag.class); private final CryptoFileSystemProperties fileSystemProperties = mock(CryptoFileSystemProperties.class); + private final FileNameDecryptor filenameDecryptor = mock(FileNameDecryptor.class); private final CryptoPath root = mock(CryptoPath.class); private final CryptoPath empty = mock(CryptoPath.class); @@ -127,7 +128,7 @@ public void setup() { pathMatcherFactory, directoryStreamFactory, dirIdProvider, dirIdBackup, // fileAttributeProvider, fileAttributeByNameProvider, fileAttributeViewProvider, // openCryptoFiles, symlinks, finallyUtil, ciphertextDirDeleter, readonlyFlag, // - fileSystemProperties); + fileSystemProperties, filenameDecryptor); } @Test @@ -1277,7 +1278,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); } diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java index a1dc036d..ab8f8bea 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java @@ -198,7 +198,7 @@ public void testInitialize() throws IOException, MasterkeyLoadingFailedException Optional 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 diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java index df120142..7e646e57 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java @@ -41,7 +41,6 @@ public class CryptoFileSystemsTest { private final Path dataDirPath = mock(Path.class, "normalizedVaultPath/d"); private final Path preContenRootPath = mock(Path.class, "normalizedVaultPath/d/AB"); private final Path contenRootPath = mock(Path.class, "normalizedVaultPath/d/AB/CDEFGHIJKLMNOP"); - private final FileSystemCapabilityChecker capabilityChecker = mock(FileSystemCapabilityChecker.class); private final CryptoFileSystemProvider provider = mock(CryptoFileSystemProvider.class); private final CryptoFileSystemProperties properties = mock(CryptoFileSystemProperties.class); private final CryptoFileSystemComponent cryptoFileSystemComponent = mock(CryptoFileSystemComponent.class); @@ -65,7 +64,7 @@ public class CryptoFileSystemsTest { private MockedStatic cryptorProviderClass; private MockedStatic backupHelperClass; - private final CryptoFileSystems inTest = new CryptoFileSystems(cryptoFileSystemComponentFactory, capabilityChecker, csprng); + private final CryptoFileSystems inTest = new CryptoFileSystems(cryptoFileSystemComponentFactory, csprng); @BeforeEach public void setup() throws IOException, MasterkeyLoadingFailedException { diff --git a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java index 12e090a2..713dc7f9 100644 --- a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java @@ -169,7 +169,7 @@ private Path firstEmptyCiphertextDirectory() throws IOException { } private boolean isEmptyCryptoFsDirectory(Path path) { - Predicate isIgnoredFile = p -> Constants.DIR_BACKUP_FILE_NAME.equals(p.getFileName().toString()); + Predicate isIgnoredFile = p -> Constants.DIR_ID_BACKUP_FILE_NAME.equals(p.getFileName().toString()); try (Stream files = Files.list(path)) { return files.noneMatch(isIgnoredFile.negate()); } catch (IOException e) { @@ -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); diff --git a/src/test/java/org/cryptomator/cryptofs/DirectoryIdBackupTest.java b/src/test/java/org/cryptomator/cryptofs/DirectoryIdBackupTest.java index 58739e90..fb120e4b 100644 --- a/src/test/java/org/cryptomator/cryptofs/DirectoryIdBackupTest.java +++ b/src/test/java/org/cryptomator/cryptofs/DirectoryIdBackupTest.java @@ -1,13 +1,18 @@ package org.cryptomator.cryptofs; import org.cryptomator.cryptofs.common.Constants; +import org.cryptomator.cryptofs.health.dirid.OrphanContentDirTest; +import org.cryptomator.cryptofs.util.TestCryptoException; +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 org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; 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; @@ -15,6 +20,10 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; public class DirectoryIdBackupTest { @@ -22,8 +31,6 @@ public class DirectoryIdBackupTest { Path contentPath; private String dirId = "12345678"; - private CiphertextDirectory ciphertextDirectoryObject; - private EncryptingWritableByteChannel encChannel; private Cryptor cryptor; private DirectoryIdBackup dirIdBackup; @@ -31,37 +38,108 @@ public class DirectoryIdBackupTest { @BeforeEach public void init() { - ciphertextDirectoryObject = new 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 backupMock = Mockito.mockStatic(DirectoryIdBackup.class)) { - backupMock.when(() -> DirectoryIdBackup.wrapEncryptionAround(Mockito.any(), Mockito.eq(cryptor))).thenReturn(encChannel); + @Nested + public class Write { + + private CiphertextDirectory ciphertextDirectoryObject; + private EncryptingWritableByteChannel encChannel; + + @BeforeEach + public void beforeEachWriteTest() { + ciphertextDirectoryObject = new CiphertextDirectory(dirId, contentPath); + encChannel = Mockito.mock(EncryptingWritableByteChannel.class); + } + + @Test + public void testIdFileCreated() throws IOException { + var dirIdBackupSpy = spy(dirIdBackup); + Mockito.doReturn(encChannel).when(dirIdBackupSpy).wrapEncryptionAround(Mockito.any(), Mockito.eq(cryptor)); + Mockito.when(encChannel.write(Mockito.any())).thenReturn(0); + + dirIdBackupSpy.write(ciphertextDirectoryObject); + + Assertions.assertTrue(Files.exists(contentPath.resolve(Constants.DIR_ID_BACKUP_FILE_NAME))); + } + + @Test + public void testContentIsWritten() throws IOException { + var dirIdBackupSpy = spy(dirIdBackup); + Mockito.doReturn(encChannel).when(dirIdBackupSpy).wrapEncryptionAround(Mockito.any(), Mockito.eq(cryptor)); Mockito.when(encChannel.write(Mockito.any())).thenReturn(0); + var expectedWrittenContent = ByteBuffer.wrap(dirId.getBytes(StandardCharsets.US_ASCII)); - dirIdBackup.execute(ciphertextDirectoryObject); + dirIdBackupSpy.write(ciphertextDirectoryObject); - Assertions.assertTrue(Files.exists(contentPath.resolve(Constants.DIR_BACKUP_FILE_NAME))); + Mockito.verify(encChannel, Mockito.times(1)).write(Mockito.argThat(b -> b.equals(expectedWrittenContent))); } + //TODO: test, what happens if file already exists? } - @Test - public void testContentIsWritten() throws IOException { - Mockito.when(encChannel.write(Mockito.any())).thenReturn(0); - var expectedWrittenContent = ByteBuffer.wrap(dirId.getBytes(StandardCharsets.US_ASCII)); + @Nested + public class Read { - try (MockedStatic backupMock = Mockito.mockStatic(DirectoryIdBackup.class)) { - backupMock.when(() -> DirectoryIdBackup.wrapEncryptionAround(Mockito.any(), Mockito.eq(cryptor))).thenReturn(encChannel); + private DecryptingReadableByteChannel decChannel; - dirIdBackup.execute(ciphertextDirectoryObject); + @BeforeEach + public void beforeEachRead() throws IOException { + var backupFile = contentPath.resolve(Constants.DIR_ID_BACKUP_FILE_NAME); + Files.writeString(backupFile, dirId, StandardCharsets.US_ASCII, StandardOpenOption.CREATE, StandardOpenOption.WRITE); - Mockito.verify(encChannel, Mockito.times(1)).write(Mockito.argThat(b -> b.equals(expectedWrittenContent))); + decChannel = mock(DecryptingReadableByteChannel.class); + } + + @Test + @DisplayName("If the directory id is longer than 36 characters, throw IllegalStateException") + public void contentLongerThan36Chars() throws IOException { + var dirIdBackupSpy = spy(dirIdBackup); + Mockito.when(dirIdBackupSpy.wrapDecryptionAround(Mockito.any(), Mockito.eq(cryptor))).thenReturn(decChannel); + Mockito.when(decChannel.read(Mockito.any())).thenReturn(Constants.MAX_DIR_ID_LENGTH + 1); + Assertions.assertThrows(IllegalStateException.class, () -> dirIdBackupSpy.read(contentPath)); + } + + @Test + @DisplayName("If the backup file cannot be decrypted, a CryptoException is thrown") + public void invalidEncryptionThrowsCryptoException() throws IOException { + var dirIdBackupSpy = spy(dirIdBackup); + var expectedException = new TestCryptoException(); + Mockito.when(dirIdBackupSpy.wrapDecryptionAround(Mockito.any(), Mockito.eq(cryptor))).thenReturn(decChannel); + Mockito.when(decChannel.read(Mockito.any())).thenThrow(expectedException); + var actual = Assertions.assertThrows(CryptoException.class, () -> dirIdBackupSpy.read(contentPath)); + Assertions.assertEquals(expectedException, actual); + } + + @Test + @DisplayName("IOException accessing the file is rethrown") + public void ioException() throws IOException { + var dirIdBackupSpy = spy(dirIdBackup); + var expectedException = new IOException("my oh my"); + Mockito.when(dirIdBackupSpy.wrapDecryptionAround(Mockito.any(), Mockito.eq(cryptor))).thenReturn(decChannel); + Mockito.when(decChannel.read(Mockito.any())).thenThrow(expectedException); + var actual = Assertions.assertThrows(IOException.class, () -> dirIdBackupSpy.read(contentPath)); + Assertions.assertEquals(expectedException, actual); + } + + @Test + @DisplayName("Valid dir id is read from the backup file") + public void success() throws IOException { + var dirIdBackupSpy = spy(dirIdBackup); + var expectedArray = dirId.getBytes(StandardCharsets.US_ASCII); + + Mockito.when(dirIdBackupSpy.wrapDecryptionAround(Mockito.any(), Mockito.eq(cryptor))).thenReturn(decChannel); + Mockito.doAnswer(invocationOnMock -> { + var buf = (ByteBuffer) invocationOnMock.getArgument(0); + buf.put(expectedArray); + return expectedArray.length; + }).when(decChannel).read(Mockito.any()); + + var readDirId = dirIdBackupSpy.read(contentPath); + Assertions.assertArrayEquals(expectedArray, readDirId); } } + } diff --git a/src/test/java/org/cryptomator/cryptofs/FileNameDecryptorTest.java b/src/test/java/org/cryptomator/cryptofs/FileNameDecryptorTest.java new file mode 100644 index 00000000..c5874d8b --- /dev/null +++ b/src/test/java/org/cryptomator/cryptofs/FileNameDecryptorTest.java @@ -0,0 +1,209 @@ +package org.cryptomator.cryptofs; + +import org.cryptomator.cryptofs.common.Constants; +import org.cryptomator.cryptofs.util.TestCryptoException; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.FileNameCryptor; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; + +import java.io.IOException; +import java.nio.file.FileSystemException; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class FileNameDecryptorTest { + + @TempDir + Path tmpPath; + Path vaultPath = mock(Path.class); + DirectoryIdBackup dirIdBackup = mock(DirectoryIdBackup.class); + LongFileNameProvider longFileNameProvider = mock(LongFileNameProvider.class); + FileNameCryptor fileNameCryptor = mock(FileNameCryptor.class); + FileNameDecryptor testObj; + FileNameDecryptor testObjSpy; + + @BeforeEach + public void beforeEach() { + var cryptor = mock(Cryptor.class); + when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor); + testObj = new FileNameDecryptor(vaultPath, cryptor, dirIdBackup, longFileNameProvider); + testObjSpy = Mockito.spy(testObj); + } + + @ParameterizedTest + @DisplayName("Given a ciphertextNode, it's clearname is returned") + @ValueSource(strings = {Constants.DEFLATED_FILE_SUFFIX, Constants.CRYPTOMATOR_FILE_SUFFIX}) + public void success(String fileExtension) throws IOException { + var ciphertextNodeNameName = "someFile"; + var ciphertextNode = tmpPath.resolve(ciphertextNodeNameName + fileExtension); + var dirId = new byte[]{'f', 'o', 'o', 'b', 'a', 'r'}; + var expectedClearName = "veryClearText"; + when(dirIdBackup.read(ciphertextNode)).thenReturn(dirId); + when(longFileNameProvider.inflate(ciphertextNode)).thenReturn(ciphertextNodeNameName); + when(fileNameCryptor.decryptFilename(any(), eq(ciphertextNodeNameName), eq(dirId))).thenReturn(expectedClearName); + + var result = testObjSpy.decryptFilenameInternal(ciphertextNode); + verify(fileNameCryptor).decryptFilename(any(), eq(ciphertextNodeNameName), eq(dirId)); + Assertions.assertEquals(expectedClearName, result); + } + + @Test + @DisplayName("Path is validated before computation") + public void validatePath() throws IOException { + var ciphertextNode = tmpPath.resolve("someFile.c9r"); + Mockito.doNothing().when(testObjSpy).validatePath(any()); + Mockito.doReturn("veryClearName").when(testObjSpy).decryptFilenameInternal(any()); + + var actual = testObjSpy.decryptFilename(ciphertextNode); + Assertions.assertEquals("veryClearName", actual); + } + + @Test + @DisplayName("If the dirId backup file does not exists, throw UnsupportedOperationException") + public void notExistingDirIdFile() throws IOException { + var ciphertextNode = tmpPath.resolve("toDecrypt.c9r"); + when(dirIdBackup.read(ciphertextNode)).thenThrow(NoSuchFileException.class); + + Assertions.assertThrows(UnsupportedOperationException.class, () -> testObjSpy.decryptFilenameInternal(ciphertextNode)); + } + + @Test + @DisplayName("If the dirId cannot be read, throw FileSystemException") + public void notReadableDirIdFile() throws IOException { + var ciphertextNode = tmpPath.resolve("toDecrypt.c9r"); + when(dirIdBackup.read(ciphertextNode)) // + .thenThrow(TestCryptoException.class) // + .thenThrow(IllegalStateException.class); + Assertions.assertThrows(FileSystemException.class, () -> testObjSpy.decryptFilenameInternal(ciphertextNode)); + Assertions.assertThrows(FileSystemException.class, () -> testObjSpy.decryptFilenameInternal(ciphertextNode)); + } + + @Test + @DisplayName("If the ciphertextName cannot be decrypted, throw FileSystemException") + public void notDecryptableCiphertext() throws IOException { + var name = "toDecrypt"; + var ciphertextNode = tmpPath.resolve(name + ".c9s"); + var dirId = new byte[]{'f', 'o', 'o', 'b', 'a', 'r'}; + var expectedException = new IOException("Inflation failed"); + when(dirIdBackup.read(ciphertextNode)).thenReturn(dirId); + when(longFileNameProvider.inflate(ciphertextNode)).thenThrow(expectedException); + + var actual = Assertions.assertThrows(IOException.class, () -> testObjSpy.decryptFilenameInternal(ciphertextNode)); + Assertions.assertEquals(expectedException, actual); + } + + @Test + @DisplayName("If inflating the shortened Name throws exception, it is rethrown") + public void inflateThrows() throws IOException { + var name = "toDecrypt"; + var ciphertextNode = tmpPath.resolve(name + ".c9r"); + var dirId = new byte[]{'f', 'o', 'o', 'b', 'a', 'r'}; + when(dirIdBackup.read(ciphertextNode)).thenReturn(dirId); + when(fileNameCryptor.decryptFilename(any(), eq(name), eq(dirId))).thenThrow(TestCryptoException.class); + + Assertions.assertThrows(FileSystemException.class, () -> testObjSpy.decryptFilenameInternal(ciphertextNode)); + verify(fileNameCryptor).decryptFilename(any(), eq(name), eq(dirId)); + } + + @Nested + public class TestValidation { + + Path p = mock(Path.class, "/absolute/path/to/ciphertext.c9r"); + + @BeforeEach + public void beforeEach() { + doReturn(true).when(testObjSpy).belongsToVault(p); + doReturn(true).when(testObjSpy).isAtCipherNodeLevel(p); + doReturn(true).when(testObjSpy).hasCipherNodeExtension(p); + doReturn(true).when(testObjSpy).hasMinimumFileNameLength(p); + } + + @Test + @DisplayName("If node is not part of the vault, validation fails") + public void validateNotVaultFile() { + doReturn(false).when(testObjSpy).belongsToVault(p); + Assertions.assertThrows(IllegalArgumentException.class, () -> testObjSpy.validatePath(p)); + verify(testObjSpy).belongsToVault(any()); + } + + @Test + @DisplayName("If node is on the wrong level, validation fails") + public void validateWrongLevel() { + doReturn(false).when(testObjSpy).isAtCipherNodeLevel(p); + Assertions.assertThrows(IllegalArgumentException.class, () -> testObjSpy.validatePath(p)); + verify(testObjSpy).isAtCipherNodeLevel(any()); + } + + + @Test + @DisplayName("If node has wrong file extension, validation fails") + public void validateWrongExtension() { + doReturn(false).when(testObjSpy).hasCipherNodeExtension(p); + Assertions.assertThrows(IllegalArgumentException.class, () -> testObjSpy.validatePath(p)); + verify(testObjSpy).hasCipherNodeExtension(any()); + } + + @Test + @DisplayName("If filename is too short, validation fails") + public void validateTooShort() { + doReturn(false).when(testObjSpy).hasMinimumFileNameLength(p); + Assertions.assertThrows(IllegalArgumentException.class, () -> testObjSpy.validatePath(p)); + verify(testObjSpy).hasMinimumFileNameLength(any()); + } + } + + @Nested + public class IsAtCipherNodeLevel { + + @TempDir + Path tmpDir; + + @Test + @DisplayName("cipherNodeLevel test requires an absolute path") + public void requiresAbsolutePath() { + var relativePath = Path.of("relative/path"); + Assertions.assertThrows(IllegalArgumentException.class, () -> testObj.isAtCipherNodeLevel(relativePath)); + } + + @Test + public void success() { + when(vaultPath.getNameCount()).thenReturn(tmpDir.getNameCount()); + var p = tmpDir.resolve("d/AA/BBBBBBBBBBBBBBB/encrypted.file"); + Assertions.assertTrue(testObj.isAtCipherNodeLevel(p)); + } + + @Test + public void failure() { + when(vaultPath.getNameCount()).thenReturn(tmpDir.getNameCount()); + var p = tmpDir.resolve("d/AA/other.file"); + Assertions.assertFalse(testObj.isAtCipherNodeLevel(p)); + } + } + + @ParameterizedTest + @DisplayName("Only c9r and c9s are accepted file extensions") + @CsvSource(value = {"file.c9r,true", "file.c9s,true", "filec9r,false", "file.c9l,false",}) + public void testHasCipherNodeExtension(String filename, boolean expected) { + var p = Path.of(filename); + var result = testObj.hasCipherNodeExtension(p); + Assertions.assertEquals(expected, result, "The filename %s is WRONGLY %s".formatted(filename, result ? "accepted" : "rejected")); + } + + +} diff --git a/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java b/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java index a19e508d..e203b16b 100644 --- a/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java +++ b/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java @@ -43,6 +43,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; +import java.util.function.Consumer; import static org.hamcrest.CoreMatchers.is; import static org.mockito.ArgumentMatchers.any; @@ -76,7 +77,7 @@ public class CleartextFileChannelTest { private AtomicReference lastModified = new AtomicReference<>(Instant.ofEpochMilli(0)); private BasicFileAttributeView attributeView = mock(BasicFileAttributeView.class); private ExceptionsDuringWrite exceptionsDuringWrite = mock(ExceptionsDuringWrite.class); - private ChannelCloseListener closeListener = mock(ChannelCloseListener.class); + private Consumer closeListener = mock(Consumer.class); private CryptoFileSystemStats stats = mock(CryptoFileSystemStats.class); private CleartextFileChannel inTest; @@ -242,11 +243,22 @@ public void testCloseIoExceptionFlush() throws IOException { Assertions.assertThrows(IOException.class, () -> inSpy.implCloseChannel()); - verify(closeListener).closed(inSpy); + verify(closeListener).accept(ciphertextFileChannel); verify(ciphertextFileChannel).close(); verify(inSpy).persistLastModified(); } + @Test + @DisplayName("On close, first flush channel, then unregister") + public void testCloseCipherChannelFlushBeforeUnregister() throws IOException { + var inSpy = spy(inTest); + inSpy.implCloseChannel(); + + var ordering = inOrder(inSpy, closeListener); + ordering.verify(inSpy).flush(); + verify(closeListener).accept(ciphertextFileChannel); + } + @Test @DisplayName("On close, first close channel, then persist lastModified") public void testCloseCipherChannelCloseBeforePersist() throws IOException { @@ -278,8 +290,8 @@ public void testCloseExceptionOnLastModifiedPersistenceIgnored() throws IOExcept var inSpy = Mockito.spy(inTest); Mockito.doThrow(IOException.class).when(inSpy).persistLastModified(); - Assertions.assertDoesNotThrow(() -> inSpy.implCloseChannel()); - verify(closeListener).closed(inSpy); + Assertions.assertDoesNotThrow(inSpy::implCloseChannel); + verify(closeListener).accept(ciphertextFileChannel); verify(ciphertextFileChannel).close(); } diff --git a/src/test/java/org/cryptomator/cryptofs/common/FileSystemCapabilityCheckerTest.java b/src/test/java/org/cryptomator/cryptofs/common/FileSystemCapabilityCheckerTest.java index 1c363eaf..4bb1ccb6 100644 --- a/src/test/java/org/cryptomator/cryptofs/common/FileSystemCapabilityCheckerTest.java +++ b/src/test/java/org/cryptomator/cryptofs/common/FileSystemCapabilityCheckerTest.java @@ -20,18 +20,18 @@ import java.util.Collections; public class FileSystemCapabilityCheckerTest { - + @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class PathLengthLimits { - + private Path pathToVault = Mockito.mock(Path.class); private Path cDir = Mockito.mock(Path.class); private Path fillerDir = Mockito.mock(Path.class); private Path nnnDir = Mockito.mock(Path.class); private FileSystem fileSystem = Mockito.mock(FileSystem.class); private FileSystemProvider fileSystemProvider = Mockito.mock(FileSystemProvider.class); - + @BeforeEach public void setup() throws IOException { Mockito.when(pathToVault.getFileSystem()).thenReturn(fileSystem); @@ -55,15 +55,14 @@ public void testUnlimitedLength() throws IOException { String checkFileStr = invocation.getArgument(0); Path checkFileMock = Mockito.mock(Path.class, checkFileStr); Mockito.when(checkFileMock.getFileSystem()).thenReturn(fileSystem); - Mockito.when(fileSystemProvider.newByteChannel(Mockito.eq(checkFileMock), Mockito.any())) - .thenReturn(new SeekableByteChannelMock(0)); + Mockito.when(fileSystemProvider.newByteChannel(Mockito.eq(checkFileMock), Mockito.any())).thenReturn(new SeekableByteChannelMock(0)); return checkFileMock; }); Mockito.when(fileSystemProvider.newDirectoryStream(Mockito.eq(checkDirMock), Mockito.any())).thenReturn(DirectoryStreamMock.empty()); return checkDirMock; }); - int determinedLimit = new FileSystemCapabilityChecker().determineSupportedCiphertextFileNameLength(pathToVault); + int determinedLimit = FileSystemCapabilityChecker.determineSupportedCiphertextFileNameLength(pathToVault); Assertions.assertEquals(220, determinedLimit); } @@ -80,13 +79,12 @@ public void testLimitedLengthDuringDirListing() throws IOException { String checkFileStr = invocation.getArgument(0); Path checkFileMock = Mockito.mock(Path.class, checkFileStr); Mockito.when(checkFileMock.getFileSystem()).thenReturn(fileSystem); - Mockito.when(fileSystemProvider.newByteChannel(Mockito.eq(checkFileMock), Mockito.any())) - .thenReturn(new SeekableByteChannelMock(0)); + Mockito.when(fileSystemProvider.newByteChannel(Mockito.eq(checkFileMock), Mockito.any())).thenReturn(new SeekableByteChannelMock(0)); return checkFileMock; }); Mockito.when(fileSystemProvider.newDirectoryStream(Mockito.eq(checkDirMock), Mockito.any())).then(invocation3 -> { Iterable iterable = Mockito.mock(Iterable.class); - if (Integer.valueOf(checkDirStr) > limit) { + if (Integer.parseInt(checkDirStr) > limit) { Mockito.when(iterable.iterator()).thenThrow(new DirectoryIteratorException(new IOException("path too long"))); } else { Mockito.when(iterable.iterator()).thenReturn(Collections.emptyIterator()); @@ -96,8 +94,8 @@ public void testLimitedLengthDuringDirListing() throws IOException { return checkDirMock; }); - int determinedLimit = new FileSystemCapabilityChecker().determineSupportedCiphertextFileNameLength(pathToVault); - + int determinedLimit = FileSystemCapabilityChecker.determineSupportedCiphertextFileNameLength(pathToVault); + Assertions.assertEquals(limit, determinedLimit); } @@ -113,12 +111,10 @@ public void testLimitedLengthDuringFileCreation() throws IOException { String checkFileStr = invocation.getArgument(0); Path checkFileMock = Mockito.mock(Path.class, checkFileStr); Mockito.when(checkFileMock.getFileSystem()).thenReturn(fileSystem); - if (Integer.valueOf(checkDirStr) > limit) { - Mockito.when(fileSystemProvider.newByteChannel(Mockito.eq(checkFileMock), Mockito.any())) - .thenThrow(new IOException("name too long")); + if (Integer.parseInt(checkDirStr) > limit) { + Mockito.when(fileSystemProvider.newByteChannel(Mockito.eq(checkFileMock), Mockito.any())).thenThrow(new IOException("name too long")); } else { - Mockito.when(fileSystemProvider.newByteChannel(Mockito.eq(checkFileMock), Mockito.any())) - .thenReturn(new SeekableByteChannelMock(0)); + Mockito.when(fileSystemProvider.newByteChannel(Mockito.eq(checkFileMock), Mockito.any())).thenReturn(new SeekableByteChannelMock(0)); } return checkFileMock; }); @@ -126,7 +122,7 @@ public void testLimitedLengthDuringFileCreation() throws IOException { return checkDirMock; }); - int determinedLimit = new FileSystemCapabilityChecker().determineSupportedCiphertextFileNameLength(pathToVault); + int determinedLimit = FileSystemCapabilityChecker.determineSupportedCiphertextFileNameLength(pathToVault); Assertions.assertEquals(limit, determinedLimit); } @@ -136,14 +132,14 @@ public void testLimitedLengthDuringFileCreation() throws IOException { @CsvSource({"220, 146", "219, 143", "218, 143", "217, 143", "216, 143", "215, 140"}) public void testDetermineSupportedCleartextFileNameLength(int ciphertextLimit, int expectedCleartextLimit) throws IOException { Path path = Mockito.mock(Path.class); - FileSystemCapabilityChecker checker = Mockito.spy(new FileSystemCapabilityChecker()); - Mockito.doReturn(ciphertextLimit).when(checker).determineSupportedCiphertextFileNameLength(path); - - int result = checker.determineSupportedCleartextFileNameLength(path); - - Assertions.assertEquals(expectedCleartextLimit, result); + try (var staticCheckerMock = Mockito.mockStatic(FileSystemCapabilityChecker.class)) { + staticCheckerMock.when(() -> FileSystemCapabilityChecker.determineSupportedCiphertextFileNameLength(path)).thenReturn(ciphertextLimit); + staticCheckerMock.when(() -> FileSystemCapabilityChecker.determineSupportedCleartextFileNameLength(path)).thenCallRealMethod(); + int result = FileSystemCapabilityChecker.determineSupportedCleartextFileNameLength(path); + Assertions.assertEquals(expectedCleartextLimit, result); + } } - + } } \ No newline at end of file diff --git a/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java b/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java index eada83a8..b9fce578 100644 --- a/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java @@ -3,6 +3,7 @@ import org.cryptomator.cryptofs.CiphertextDirectory; import org.cryptomator.cryptofs.CryptoPath; import org.cryptomator.cryptofs.CryptoPathMapper; +import org.cryptomator.cryptofs.common.Constants; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -118,10 +119,10 @@ public void testCiphertextDirStreamFilter(String fileName, boolean expected) { private static Stream provideFilterExamples() { return Stream.of( // - Arguments.of("foo25____25chars_____.c9r", false), // - Arguments.of("bar25____25chars_____.c9s", false), // - Arguments.of("foo26____26chars______.c9r", true), // - Arguments.of("bar26____26chars______.c9s", true)); + Arguments.of("b".repeat(Constants.MIN_CIPHER_NAME_LENGTH - 5)+".c9r", false), // + Arguments.of("b".repeat(Constants.MIN_CIPHER_NAME_LENGTH - 5)+".c9s", false), // + Arguments.of("a".repeat(Constants.MIN_CIPHER_NAME_LENGTH - 4)+".c9r", true), // + Arguments.of("a".repeat(Constants.MIN_CIPHER_NAME_LENGTH - 4)+".c9s", true)); } } diff --git a/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java b/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java index 3de6a2f9..e50b7d57 100644 --- a/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java +++ b/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java @@ -4,7 +4,6 @@ import com.google.common.jimfs.Jimfs; import org.cryptomator.cryptofs.EffectiveOpenOptions; import org.cryptomator.cryptofs.ReadonlyFlag; -import org.cryptomator.cryptofs.ch.ChannelCloseListener; import org.cryptomator.cryptofs.ch.ChannelComponent; import org.cryptomator.cryptofs.ch.CleartextFileChannel; import org.cryptomator.cryptolib.api.Cryptor; @@ -27,7 +26,6 @@ import java.io.UncheckedIOException; import java.nio.channels.FileChannel; import java.nio.file.FileSystem; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.PosixFilePermissions; @@ -35,6 +33,7 @@ import java.util.EnumSet; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -48,7 +47,6 @@ public class OpenCryptoFileTest { private static AtomicReference CURRENT_FILE_PATH; private ReadonlyFlag readonlyFlag = mock(ReadonlyFlag.class); private FileCloseListener closeListener = mock(FileCloseListener.class); - private ChunkCache chunkCache = mock(ChunkCache.class); private Cryptor cryptor = mock(Cryptor.class); private FileHeaderCryptor fileHeaderCryptor = mock(FileHeaderCryptor.class); private FileHeaderHolder headerHolder = mock(FileHeaderHolder.class); @@ -72,7 +70,7 @@ public static void tearDown() throws IOException { @Test public void testCloseTriggersCloseListener() { - OpenCryptoFile openCryptoFile = new OpenCryptoFile(closeListener, chunkCache, cryptor, headerHolder, chunkIO, CURRENT_FILE_PATH, fileSize, lastModified, openCryptoFileComponent); + OpenCryptoFile openCryptoFile = new OpenCryptoFile(closeListener, cryptor, headerHolder, chunkIO, CURRENT_FILE_PATH, fileSize, lastModified, openCryptoFileComponent); openCryptoFile.close(); verify(closeListener).close(CURRENT_FILE_PATH.get(), openCryptoFile); } @@ -83,7 +81,7 @@ public void testCloseImmediatelyIfOpeningFirstChannelFails() { UncheckedIOException expectedException = new UncheckedIOException(new IOException("fail!")); EffectiveOpenOptions options = Mockito.mock(EffectiveOpenOptions.class); Mockito.when(options.createOpenOptionsForEncryptedFile()).thenThrow(expectedException); - OpenCryptoFile openCryptoFile = new OpenCryptoFile(closeListener, chunkCache, cryptor, headerHolder, chunkIO, CURRENT_FILE_PATH, fileSize, lastModified, openCryptoFileComponent); + OpenCryptoFile openCryptoFile = new OpenCryptoFile(closeListener, cryptor, headerHolder, chunkIO, CURRENT_FILE_PATH, fileSize, lastModified, openCryptoFileComponent); UncheckedIOException exception = Assertions.assertThrows(UncheckedIOException.class, () -> { openCryptoFile.newFileChannel(options); @@ -93,19 +91,20 @@ public void testCloseImmediatelyIfOpeningFirstChannelFails() { } @Test - @DisplayName("Opening a file channel with TRUNCATE_EXISTING sets the file size to 0") - public void testFileSizeZerodOnTruncateExisting() throws IOException { + @DisplayName("Opening a file channel with TRUNCATE_EXISTING calls truncate(0) on the cleartextChannel") + public void testCleartextChannelTruncateCalledOnTruncateExisting() throws IOException { EffectiveOpenOptions options = EffectiveOpenOptions.from(EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING), readonlyFlag); + var cleartextChannel = mock(CleartextFileChannel.class); Mockito.when(headerHolder.get()).thenReturn(Mockito.mock(FileHeader.class)); Mockito.when(cryptor.fileHeaderCryptor()).thenReturn(fileHeaderCryptor); Mockito.when(fileHeaderCryptor.headerSize()).thenReturn(42); Mockito.when(openCryptoFileComponent.newChannelComponent()).thenReturn(channelComponentFactory); Mockito.when(channelComponentFactory.create(any(), any(), any())).thenReturn(channelComponent); - Mockito.when(channelComponent.channel()).thenReturn(mock(CleartextFileChannel.class)); - OpenCryptoFile openCryptoFile = new OpenCryptoFile(closeListener, chunkCache, cryptor, headerHolder, chunkIO, CURRENT_FILE_PATH, fileSize, lastModified, openCryptoFileComponent); + Mockito.when(channelComponent.channel()).thenReturn(cleartextChannel); + OpenCryptoFile openCryptoFile = new OpenCryptoFile(closeListener, cryptor, headerHolder, chunkIO, CURRENT_FILE_PATH, fileSize, lastModified, openCryptoFileComponent); openCryptoFile.newFileChannel(options); - verify(fileSize).set(0L); + verify(cleartextChannel).truncate(0L); } @Nested @@ -114,7 +113,7 @@ public class InitFilHeaderTests { EffectiveOpenOptions options = Mockito.mock(EffectiveOpenOptions.class); FileChannel cipherFileChannel = Mockito.mock(FileChannel.class, "cipherFilechannel"); - OpenCryptoFile inTest = new OpenCryptoFile(closeListener, chunkCache, cryptor, headerHolder, chunkIO, CURRENT_FILE_PATH, fileSize, lastModified, openCryptoFileComponent); + OpenCryptoFile inTest = new OpenCryptoFile(closeListener, cryptor, headerHolder, chunkIO, CURRENT_FILE_PATH, fileSize, lastModified, openCryptoFileComponent); @Test @DisplayName("Skip file header init, if the file header already exists in memory") @@ -191,14 +190,14 @@ public class FileChannelFactoryTest { private final AtomicLong realFileSize = new AtomicLong(-1L); private OpenCryptoFile openCryptoFile; private CleartextFileChannel cleartextFileChannel; - private AtomicReference listener; + private AtomicReference> listener; private AtomicReference ciphertextChannel; @BeforeAll public void setup() throws IOException { FS = Jimfs.newFileSystem("OpenCryptoFileTest.FileChannelFactoryTest", Configuration.unix().toBuilder().setAttributeViews("basic", "posix").build()); CURRENT_FILE_PATH = new AtomicReference<>(FS.getPath("currentFile")); - openCryptoFile = new OpenCryptoFile(closeListener, chunkCache, cryptor, headerHolder, chunkIO, CURRENT_FILE_PATH, realFileSize, lastModified, openCryptoFileComponent); + openCryptoFile = new OpenCryptoFile(closeListener,cryptor, headerHolder, chunkIO, CURRENT_FILE_PATH, realFileSize, lastModified, openCryptoFileComponent); cleartextFileChannel = mock(CleartextFileChannel.class); listener = new AtomicReference<>(); ciphertextChannel = new AtomicReference<>(); @@ -260,19 +259,6 @@ public void testGetSizeAfterCreatingSecondFileChannel() { Assertions.assertEquals(0l, openCryptoFile.size().get()); } - - @Test - @Order(20) - @DisplayName("TRUNCATE_EXISTING leads to chunk cache invalidation") - public void testTruncateExistingInvalidatesChunkCache() throws IOException { - Mockito.when(cryptor.fileHeaderCryptor()).thenReturn(fileHeaderCryptor); - Mockito.when(fileHeaderCryptor.headerSize()).thenReturn(43); - Files.write(CURRENT_FILE_PATH.get(), new byte[0]); - EffectiveOpenOptions options = EffectiveOpenOptions.from(EnumSet.of(StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE), readonlyFlag); - openCryptoFile.newFileChannel(options); - verify(chunkCache).invalidateStale(); - } - @Test @Order(100) @DisplayName("closeListener triggers chunkIO.unregisterChannel()") @@ -280,7 +266,7 @@ public void triggerCloseListener() throws IOException { Assumptions.assumeTrue(listener.get() != null); Assumptions.assumeTrue(ciphertextChannel.get() != null); - listener.get().closed(cleartextFileChannel); + listener.get().accept(ciphertextChannel.get()); verify(chunkIO).unregisterChannel(ciphertextChannel.get()); } diff --git a/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingContentDirTest.java b/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingContentDirTest.java index 10e9ba35..240fe666 100644 --- a/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingContentDirTest.java +++ b/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingContentDirTest.java @@ -55,13 +55,13 @@ public void testFix() throws IOException { var dirIdHash = "ridiculous-32-char-pseudo-hashhh"; Mockito.doReturn(dirIdHash).when(fileNameCryptor).hashDirectoryId(dirId); try (var dirIdBackupMock = Mockito.mockStatic(DirectoryIdBackup.class)) { - dirIdBackupMock.when(() -> DirectoryIdBackup.backupManually(Mockito.any(), Mockito.any())).thenAnswer(Answers.RETURNS_SMART_NULLS); + dirIdBackupMock.when(() -> DirectoryIdBackup.write(Mockito.any(), Mockito.any())).thenAnswer(Answers.RETURNS_SMART_NULLS); result.fix(pathToVault, cryptor); var expectedPath = pathToVault.resolve("d/ri/diculous-32-char-pseudo-hashhh"); ArgumentMatcher cipherDirMatcher = obj -> obj.dirId().equals(dirId) && obj.path().endsWith(expectedPath); - dirIdBackupMock.verify(() -> DirectoryIdBackup.backupManually(Mockito.eq(cryptor), Mockito.argThat(cipherDirMatcher)), Mockito.times(1)); + dirIdBackupMock.verify(() -> DirectoryIdBackup.write(Mockito.eq(cryptor), Mockito.argThat(cipherDirMatcher)), Mockito.times(1)); var attr = Assertions.assertDoesNotThrow(() -> Files.readAttributes(expectedPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)); Assertions.assertTrue(attr.isDirectory()); } @@ -73,7 +73,7 @@ public void testFixFailsOnFailingDirIdFile() throws IOException { var dirIdHash = "ridiculous-32-char-pseudo-hashhh"; try (var dirIdBackupMock = Mockito.mockStatic(DirectoryIdBackup.class)) { Mockito.doReturn(dirIdHash).when(fileNameCryptor).hashDirectoryId(dirId); - dirIdBackupMock.when(() -> DirectoryIdBackup.backupManually(Mockito.any(), Mockito.any())).thenThrow(new IOException("Access denied")); + dirIdBackupMock.when(() -> DirectoryIdBackup.write(Mockito.any(), Mockito.any())).thenThrow(new IOException("Access denied")); Assertions.assertThrows(IOException.class, () -> result.fix(pathToVault, cryptor)); } diff --git a/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackupTest.java b/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackupTest.java index cb97d0b7..6d173b49 100644 --- a/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackupTest.java +++ b/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackupTest.java @@ -36,7 +36,7 @@ public void testFix() throws IOException { Path cipherDir = Path.of("d/ri/diculous-30-char-pseudo-hash"); String dirId = "1234-456789-1234"; try (var dirIdBackupMock = Mockito.mockStatic(DirectoryIdBackup.class)) { - dirIdBackupMock.when(() -> DirectoryIdBackup.backupManually(Mockito.any(), Mockito.any())).thenAnswer(Answers.RETURNS_SMART_NULLS); + dirIdBackupMock.when(() -> DirectoryIdBackup.write(Mockito.any(), Mockito.any())).thenAnswer(Answers.RETURNS_SMART_NULLS); Cryptor cryptor = Mockito.mock(Cryptor.class); result = new MissingDirIdBackup(dirId, cipherDir); @@ -44,7 +44,7 @@ public void testFix() throws IOException { var expectedPath = pathToVault.resolve(cipherDir); ArgumentMatcher cipherDirMatcher = obj -> obj.dirId().equals(dirId) && obj.path().isAbsolute() && obj.path().equals(expectedPath); - dirIdBackupMock.verify(() -> DirectoryIdBackup.backupManually(Mockito.eq(cryptor), Mockito.argThat(cipherDirMatcher)), Mockito.times(1)); + dirIdBackupMock.verify(() -> DirectoryIdBackup.write(Mockito.eq(cryptor), Mockito.argThat(cipherDirMatcher)), Mockito.times(1)); } } } diff --git a/src/test/java/org/cryptomator/cryptofs/health/dirid/OrphanDirTest.java b/src/test/java/org/cryptomator/cryptofs/health/dirid/OrphanContentDirTest.java similarity index 88% rename from src/test/java/org/cryptomator/cryptofs/health/dirid/OrphanDirTest.java rename to src/test/java/org/cryptomator/cryptofs/health/dirid/OrphanContentDirTest.java index 9f9c93b2..934e1f58 100644 --- a/src/test/java/org/cryptomator/cryptofs/health/dirid/OrphanDirTest.java +++ b/src/test/java/org/cryptomator/cryptofs/health/dirid/OrphanContentDirTest.java @@ -5,34 +5,34 @@ import org.cryptomator.cryptofs.DirectoryIdBackup; import org.cryptomator.cryptofs.VaultConfig; import org.cryptomator.cryptofs.common.Constants; +import org.cryptomator.cryptofs.util.TestCryptoException; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileNameCryptor; import org.cryptomator.cryptolib.api.Masterkey; -import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.FieldSource; import org.mockito.Mockito; import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.SeekableByteChannel; import java.nio.charset.StandardCharsets; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.security.MessageDigest; +import java.util.List; import java.util.Optional; import java.util.UUID; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -public class OrphanDirTest { +public class OrphanContentDirTest { @TempDir public Path pathToVault; @@ -162,11 +162,11 @@ public void testPrepareStepParent() throws IOException { UUID uuid = Mockito.mock(UUID.class); uuidClass.when(UUID::randomUUID).thenReturn(uuid); Mockito.doReturn("aaaaaa").when(uuid).toString(); - dirIdBackupClass.when(() -> DirectoryIdBackup.backupManually(Mockito.eq(cryptor), Mockito.any())).thenAnswer(invocation -> null); + dirIdBackupClass.when(() -> DirectoryIdBackup.write(Mockito.eq(cryptor), Mockito.any())).thenAnswer(invocation -> null); result.prepareStepParent(dataDir, cipherRecovery, cryptor, clearStepParentName); - dirIdBackupClass.verify(() -> DirectoryIdBackup.backupManually(Mockito.eq(cryptor), Mockito.any()), Mockito.times(1)); + dirIdBackupClass.verify(() -> DirectoryIdBackup.write(Mockito.eq(cryptor), Mockito.any()), Mockito.times(1)); } Assertions.assertEquals("aaaaaa", Files.readString(cipherRecovery.resolve("2.c9r/dir.c9r"), StandardCharsets.UTF_8)); Assertions.assertTrue(Files.isDirectory(pathToVault.resolve("d/22/2222"))); @@ -186,11 +186,11 @@ public void testPrepareStepParentExistingStepParentDir() throws IOException { UUID uuid = Mockito.mock(UUID.class); uuidClass.when(UUID::randomUUID).thenReturn(uuid); Mockito.doReturn("aaaaaa").when(uuid).toString(); - dirIdBackupClass.when(() -> DirectoryIdBackup.backupManually(Mockito.eq(cryptor), Mockito.any())).thenThrow(new FileAlreadyExistsException("dirId file exists")); + dirIdBackupClass.when(() -> DirectoryIdBackup.write(Mockito.eq(cryptor), Mockito.any())).thenThrow(new FileAlreadyExistsException("dirId file exists")); result.prepareStepParent(dataDir, cipherRecovery, cryptor, clearStepParentName); - dirIdBackupClass.verify(() -> DirectoryIdBackup.backupManually(Mockito.eq(cryptor), Mockito.any()), Mockito.times(1)); + dirIdBackupClass.verify(() -> DirectoryIdBackup.write(Mockito.eq(cryptor), Mockito.any()), Mockito.times(1)); } Assertions.assertEquals("aaaaaa", Files.readString(cipherRecovery.resolve("2.c9r/dir.c9r"), StandardCharsets.UTF_8)); Assertions.assertTrue(Files.isDirectory(pathToVault.resolve("d/22/2222"))); @@ -210,11 +210,11 @@ public void testPrepareStepParentOrphanedStepParentDir() throws IOException { UUID uuid = Mockito.mock(UUID.class); uuidClass.when(UUID::randomUUID).thenReturn(uuid); Mockito.doReturn("aaaaaa").when(uuid).toString(); - dirIdBackupClass.when(() -> DirectoryIdBackup.backupManually(Mockito.eq(cryptor), Mockito.any())).thenAnswer(invocation -> null); + dirIdBackupClass.when(() -> DirectoryIdBackup.write(Mockito.eq(cryptor), Mockito.any())).thenAnswer(invocation -> null); result.prepareStepParent(dataDir, cipherRecovery, cryptor, clearStepParentName); - dirIdBackupClass.verify(() -> DirectoryIdBackup.backupManually(Mockito.eq(cryptor), Mockito.any()), Mockito.times(1)); + dirIdBackupClass.verify(() -> DirectoryIdBackup.write(Mockito.eq(cryptor), Mockito.any()), Mockito.times(1)); } Assertions.assertEquals("aaaaaa", Files.readString(cipherRecovery.resolve("2.c9r/dir.c9r"), StandardCharsets.UTF_8)); Assertions.assertTrue(Files.isDirectory(pathToVault.resolve("d/22/2222"))); @@ -227,46 +227,34 @@ class RetrieveDirIdTests { private OrphanContentDir resultSpy; + static List expectedExceptions = List.of(new IOException(), new IllegalStateException(), new TestCryptoException()); + @BeforeEach public void init() { resultSpy = Mockito.spy(result); } @Test - @DisplayName("retrieveDirId extracts directory id of cipher-dir/dirId.c9r") - public void testRetrieveDirIdSuccess() throws IOException { - var dirIdFile = cipherOrphan.resolve(Constants.DIR_BACKUP_FILE_NAME); - var dirId = "random-uuid-with-at-most-36chars"; - - Files.writeString(dirIdFile, dirId, StandardCharsets.US_ASCII, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); - DecryptingReadableByteChannel dirIdReadChannel = Mockito.mock(DecryptingReadableByteChannel.class); - - Mockito.doReturn(dirIdReadChannel).when(resultSpy).createDecryptingReadableByteChannel(Mockito.any(), Mockito.eq(cryptor)); - AtomicInteger readBytesInMockedChannel = new AtomicInteger(0); - //in every invocation the channel position is updated, simulating a stateful channel - Mockito.doAnswer(invocationOnMock -> { - ByteBuffer buf = invocationOnMock.getArgument(0); - try (SeekableByteChannel channel = Files.newByteChannel(dirIdFile, StandardOpenOption.READ)) { - channel.position(readBytesInMockedChannel.get()); - readBytesInMockedChannel.getAndSet(channel.read(buf)); - return readBytesInMockedChannel.get(); - } - }).when(dirIdReadChannel).read(Mockito.any()); - - Mockito.when(fileNameCryptor.hashDirectoryId(dirId)).thenReturn("333333"); - - var maybeDirId = resultSpy.retrieveDirId(cipherOrphan, cryptor); - - Assertions.assertTrue(maybeDirId.isPresent()); - Assertions.assertEquals(dirId, maybeDirId.get()); + @DisplayName("Successful reading dirId from backup file") + public void success() { + var dirId = new byte[]{'f', 'o', 'o'}; + try (var dirIdBackupMock = Mockito.mockStatic(DirectoryIdBackup.class)) { + dirIdBackupMock.when(() -> DirectoryIdBackup.read(cryptor, cipherOrphan)).thenReturn(dirId); + var result = resultSpy.retrieveDirId(cipherOrphan, cryptor); + Assertions.assertTrue(result.isPresent()); + Assertions.assertArrayEquals(dirId, result.get()); + } } - @Test - @DisplayName("retrieveDirId returns an empty optional if cipher-dir/dirId.c9r cannot be read") - public void testRetrieveDirIdIOExceptionReadingFile() throws IOException { - var notExistingResult = resultSpy.retrieveDirId(cipherOrphan, cryptor); - - Assertions.assertTrue(notExistingResult.isEmpty()); + @ParameterizedTest + @DisplayName("retrieveDirId returns an empty optional on any exception") + @FieldSource("expectedExceptions") + public void testRetrieveDirIdIOExceptionReadingFile(Throwable t) throws IOException { + try (var dirIdBackupMock = Mockito.mockStatic(DirectoryIdBackup.class)) { + dirIdBackupMock.when(() -> DirectoryIdBackup.read(cryptor, cipherOrphan)).thenThrow(t); + var notExistingResult = resultSpy.retrieveDirId(cipherOrphan, cryptor); + Assertions.assertTrue(notExistingResult.isEmpty()); + } } } @@ -283,7 +271,7 @@ void testRestoreFilenameNormalSuccess() throws IOException { //by using Mockito.eq() in filename parameter Mockito.verfiy() not necessary Mockito.when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.eq("orphan"), Mockito.any())).thenReturn("theTrueName.txt"); - String decryptedFile = result.decryptFileName(oldCipherPath, false, "someDirId", fileNameCryptor); + String decryptedFile = result.decryptFileName(oldCipherPath, false, new byte[]{}, fileNameCryptor); Assertions.assertEquals("theTrueName.txt", decryptedFile); } @@ -299,7 +287,7 @@ void testRestoreFilenameShortenedSuccess() throws IOException { //by using Mockito.eq() in filename parameter Mockito.verfiy() not necessary Mockito.when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.eq("OrphanWithLongestName"), Mockito.any())).thenReturn("theRealLongName.txt"); - String decryptedFile = result.decryptFileName(oldCipherPath, true, "someDirId", fileNameCryptor); + String decryptedFile = result.decryptFileName(oldCipherPath, true, new byte[]{}, fileNameCryptor); Assertions.assertEquals("theRealLongName.txt", decryptedFile); } @@ -310,7 +298,7 @@ void testRestoreFilenameShortenedIOException() throws IOException { Path oldCipherPath = cipherOrphan.resolve("hashOfOrphanWithLongestName.c9r"); Files.createDirectory(oldCipherPath); - Assertions.assertThrows(IOException.class, () -> result.decryptFileName(oldCipherPath, true, "someDirId", fileNameCryptor)); + Assertions.assertThrows(IOException.class, () -> result.decryptFileName(oldCipherPath, true, new byte[]{}, fileNameCryptor)); } } @@ -438,9 +426,9 @@ public void testFixContinuesOnNotRecoverableFilename() throws IOException { Path orphan2 = cipherOrphan.resolve("orphan2_with_at_least_26chars.c9s"); Files.createFile(orphan1); Files.createDirectories(orphan2); - Files.createFile(cipherOrphan.resolve(Constants.DIR_BACKUP_FILE_NAME)); + Files.createFile(cipherOrphan.resolve(Constants.DIR_ID_BACKUP_FILE_NAME)); - var dirId = Optional.of("trololo-id"); + var dirId = Optional.of(new byte[]{'t', 'r', 'o', 'l', 'o', 'l', 'o'}); CiphertextDirectory stepParentDir = new CiphertextDirectory("aaaaaa", dataDir.resolve("22/2222")); @@ -477,9 +465,9 @@ public void testFixWithDirId() throws IOException { Path orphan2 = cipherOrphan.resolve("orphan2_with_at_least_26chars.c9s"); Files.createFile(orphan1); Files.createDirectories(orphan2); - Files.createFile(cipherOrphan.resolve(Constants.DIR_BACKUP_FILE_NAME)); + Files.createFile(cipherOrphan.resolve(Constants.DIR_ID_BACKUP_FILE_NAME)); - var dirId = Optional.of("trololo-id"); + var dirId = Optional.of(new byte[]{'t', 'r', 'o', 'l', 'o', 'l', 'o'}); CiphertextDirectory stepParentDir = new CiphertextDirectory("aaaaaa", dataDir.resolve("22/2222")); @@ -500,7 +488,7 @@ public void testFixWithDirId() throws IOException { resultSpy.fix(pathToVault, config, masterkey, cryptor); - Mockito.verify(resultSpy, Mockito.never()).adoptOrphanedResource(Mockito.eq(cipherOrphan.resolve(Constants.DIR_BACKUP_FILE_NAME)), Mockito.any(), Mockito.anyBoolean(), Mockito.eq(stepParentDir), Mockito.eq(fileNameCryptor), Mockito.any()); + Mockito.verify(resultSpy, Mockito.never()).adoptOrphanedResource(Mockito.eq(cipherOrphan.resolve(Constants.DIR_ID_BACKUP_FILE_NAME)), Mockito.any(), Mockito.anyBoolean(), Mockito.eq(stepParentDir), Mockito.eq(fileNameCryptor), Mockito.any()); Mockito.verify(resultSpy, Mockito.times(1)).adoptOrphanedResource(Mockito.eq(orphan1), Mockito.eq(lostName1), Mockito.anyBoolean(), Mockito.eq(stepParentDir), Mockito.eq(fileNameCryptor), Mockito.any()); Mockito.verify(resultSpy, Mockito.times(1)).adoptOrphanedResource(Mockito.eq(orphan2), Mockito.eq(lostName2), Mockito.anyBoolean(), Mockito.eq(stepParentDir), Mockito.eq(fileNameCryptor), Mockito.any()); Assertions.assertTrue(Files.notExists(cipherOrphan)); @@ -520,9 +508,9 @@ public void testFixWithNonCryptomatorFiles() throws IOException { Files.createFile(orphan1); Files.createDirectories(orphan2); Files.createFile(unrelated); - Files.createFile(cipherOrphan.resolve(Constants.DIR_BACKUP_FILE_NAME)); + Files.createFile(cipherOrphan.resolve(Constants.DIR_ID_BACKUP_FILE_NAME)); - var dirId = Optional.of("trololo-id"); + var dirId = Optional.of(new byte[]{'t', 'r', 'o', 'l', 'o', 'l', 'o'}); CiphertextDirectory stepParentDir = new CiphertextDirectory("aaaaaa", dataDir.resolve("22/2222")); Files.createDirectories(stepParentDir.path()); //needs to be created here, otherwise the Files.move(non-crypto-resource, stepparent) will fail @@ -605,5 +593,4 @@ public void testFixOrphanedRecoveryDir() throws IOException { Mockito.verify(resultSpy, Mockito.never()).adoptOrphanedResource(Mockito.any(), Mockito.any(), Mockito.anyBoolean(), Mockito.any(), Mockito.eq(fileNameCryptor), Mockito.any()); Mockito.verify(resultSpy).prepareRecoveryDir(pathToVault, fileNameCryptor); } - } diff --git a/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java b/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java index 5cb2295e..4bb03835 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java @@ -7,7 +7,6 @@ import org.cryptomator.cryptofs.VaultConfig; import org.cryptomator.cryptofs.common.Constants; -import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker; import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener; import org.cryptomator.cryptofs.migration.api.MigrationProgressListener; import org.cryptomator.cryptofs.migration.api.Migrator; @@ -30,12 +29,8 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.nio.file.FileSystem; import java.nio.file.Files; -import java.nio.file.NoSuchFileException; import java.nio.file.Path; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.spi.FileSystemProvider; import java.util.Collections; import java.util.Map; @@ -44,20 +39,18 @@ public class MigratorsTest { private Path pathToVault; private Path vaultConfigPath; private Path masterkeyPath; - private FileSystemCapabilityChecker fsCapabilityChecker; @BeforeEach public void setup(@TempDir Path tmpDir) { pathToVault = tmpDir; vaultConfigPath = tmpDir.resolve("vault.cryptomator"); masterkeyPath = tmpDir.resolve("masterkey.cryptomator"); - fsCapabilityChecker = Mockito.mock(FileSystemCapabilityChecker.class); } @Test @DisplayName("can't determine vault version without masterkey.cryptomator or vault.cryptomator") public void throwsExceptionIfNeitherMasterkeyNorVaultConfigExists() { - Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker); + Migrators migrators = new Migrators(Collections.emptyMap()); IOException thrown = Assertions.assertThrows(IOException.class, () -> { migrators.needsMigration(pathToVault, "vault.cryptomator", "masterkey.cryptomator"); @@ -92,7 +85,7 @@ public void tearDown() { @DisplayName("needs migration if vault version < Constants.VAULT_VERSION") public void testNeedsMigration() throws IOException { Mockito.when(unverifiedVaultConfig.allegedVaultVersion()).thenReturn(Constants.VAULT_VERSION - 1); - Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker); + Migrators migrators = new Migrators(Collections.emptyMap()); boolean result = migrators.needsMigration(pathToVault, "vault.cryptomator", "masterkey.cryptomator"); @@ -103,7 +96,7 @@ public void testNeedsMigration() throws IOException { @DisplayName("needs no migration if vault version >= Constants.VAULT_VERSION") public void testNeedsNoMigration() throws IOException { Mockito.when(unverifiedVaultConfig.allegedVaultVersion()).thenReturn(Constants.VAULT_VERSION); - Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker); + Migrators migrators = new Migrators(Collections.emptyMap()); boolean result = migrators.needsMigration(pathToVault, "vault.cryptomator", "masterkey.cryptomator"); @@ -115,7 +108,7 @@ public void testNeedsNoMigration() throws IOException { public void testMigrateWithoutMigrators() { Mockito.when(unverifiedVaultConfig.allegedVaultVersion()).thenReturn(42); - Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker); + Migrators migrators = new Migrators(Collections.emptyMap()); Assertions.assertThrows(NoApplicableMigratorException.class, () -> { migrators.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", MigrationProgressListener.IGNORE, MigrationContinuationListener.CANCEL_ALWAYS); }); @@ -129,7 +122,7 @@ public void testMigrate() throws NoApplicableMigratorException, CryptoException, MigrationContinuationListener continuationListener = Mockito.mock(MigrationContinuationListener.class); Migrator migrator = Mockito.mock(Migrator.class); Mockito.when(unverifiedVaultConfig.allegedVaultVersion()).thenReturn(0); - Migrators migrators = new Migrators(Map.of(Migration.ZERO_TO_ONE, migrator), fsCapabilityChecker); + Migrators migrators = new Migrators(Map.of(Migration.ZERO_TO_ONE, migrator)); migrators.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", progressListener, continuationListener); @@ -141,7 +134,7 @@ public void testMigrate() throws NoApplicableMigratorException, CryptoException, @SuppressWarnings("deprecation") public void testMigrateUnsupportedVaultFormat() throws NoApplicableMigratorException, CryptoException, IOException { Migrator migrator = Mockito.mock(Migrator.class); - Migrators migrators = new Migrators(Map.of(Migration.ZERO_TO_ONE, migrator), fsCapabilityChecker); + Migrators migrators = new Migrators(Map.of(Migration.ZERO_TO_ONE, migrator)); Mockito.when(unverifiedVaultConfig.allegedVaultVersion()).thenReturn(0); Mockito.doThrow(new UnsupportedVaultFormatException(Integer.MAX_VALUE, 1)).when(migrator).migrate(Mockito.any(), Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any()); @@ -174,7 +167,7 @@ public void tearDown() { @DisplayName("needs migration if vault version < Constants.VAULT_VERSION") public void testNeedsMigration() throws IOException { masterkeyFileAccessClass.when(() -> MasterkeyFileAccess.readAllegedVaultVersion(Mockito.any())).thenReturn(Constants.VAULT_VERSION - 1); - Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker); + Migrators migrators = new Migrators(Collections.emptyMap()); boolean result = migrators.needsMigration(pathToVault, "vault.cryptomator", "masterkey.cryptomator"); @@ -185,7 +178,7 @@ public void testNeedsMigration() throws IOException { @DisplayName("needs no migration if vault version >= Constants.VAULT_VERSION") public void testNeedsNoMigration() throws IOException { masterkeyFileAccessClass.when(() -> MasterkeyFileAccess.readAllegedVaultVersion(Mockito.any())).thenReturn(Constants.VAULT_VERSION); - Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker); + Migrators migrators = new Migrators(Collections.emptyMap()); boolean result = migrators.needsMigration(pathToVault, "vault.cryptomator", "masterkey.cryptomator"); @@ -197,7 +190,7 @@ public void testNeedsNoMigration() throws IOException { public void testMigrateWithoutMigrators() { masterkeyFileAccessClass.when(() -> MasterkeyFileAccess.readAllegedVaultVersion(Mockito.any())).thenReturn(1337); - Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker); + Migrators migrators = new Migrators(Collections.emptyMap()); Assertions.assertThrows(NoApplicableMigratorException.class, () -> { migrators.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", MigrationProgressListener.IGNORE, MigrationContinuationListener.CANCEL_ALWAYS); }); @@ -211,7 +204,7 @@ public void testMigrate() throws NoApplicableMigratorException, CryptoException, MigrationContinuationListener continuationListener = Mockito.mock(MigrationContinuationListener.class); Migrator migrator = Mockito.mock(Migrator.class); masterkeyFileAccessClass.when(() -> MasterkeyFileAccess.readAllegedVaultVersion(Mockito.any())).thenReturn(0); - Migrators migrators = new Migrators(Map.of(Migration.ZERO_TO_ONE, migrator), fsCapabilityChecker); + Migrators migrators = new Migrators(Map.of(Migration.ZERO_TO_ONE, migrator)); migrators.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", progressListener, continuationListener); @@ -223,7 +216,7 @@ public void testMigrate() throws NoApplicableMigratorException, CryptoException, @SuppressWarnings("deprecation") public void testMigrateUnsupportedVaultFormat() throws NoApplicableMigratorException, CryptoException, IOException { Migrator migrator = Mockito.mock(Migrator.class); - Migrators migrators = new Migrators(Map.of(Migration.ZERO_TO_ONE, migrator), fsCapabilityChecker); + Migrators migrators = new Migrators(Map.of(Migration.ZERO_TO_ONE, migrator)); masterkeyFileAccessClass.when(() -> MasterkeyFileAccess.readAllegedVaultVersion(Mockito.any())).thenReturn(0); Mockito.doThrow(new UnsupportedVaultFormatException(Integer.MAX_VALUE, 1)).when(migrator).migrate(Mockito.any(), Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any()); diff --git a/src/test/java/org/cryptomator/cryptofs/util/TestCryptoException.java b/src/test/java/org/cryptomator/cryptofs/util/TestCryptoException.java new file mode 100644 index 00000000..7197ef6e --- /dev/null +++ b/src/test/java/org/cryptomator/cryptofs/util/TestCryptoException.java @@ -0,0 +1,11 @@ +package org.cryptomator.cryptofs.util; + +import org.cryptomator.cryptolib.api.CryptoException; + +public class TestCryptoException extends CryptoException { + + public TestCryptoException() { + super(); + } + +}