diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 36fde6cc..37afd896 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-java@v1 with: - java-version: 11 + java-version: 16 - uses: actions/cache@v2 with: path: ~/.m2/repository diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 51683060..d100a440 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,7 +20,7 @@ jobs: fetch-depth: 2 - uses: actions/setup-java@v1 with: - java-version: 11 + java-version: 16 - uses: actions/cache@v2 with: path: ~/.m2/repository diff --git a/.github/workflows/publish-central.yml b/.github/workflows/publish-central.yml index f22b7a96..93b31389 100644 --- a/.github/workflows/publish-central.yml +++ b/.github/workflows/publish-central.yml @@ -15,7 +15,7 @@ jobs: ref: "refs/tags/${{ github.event.inputs.tag }}" - uses: actions/setup-java@v1 with: - java-version: 11 + java-version: 16 server-id: ossrh # Value of the distributionManagement/repository/id field of the pom.xml server-username: MAVEN_USERNAME # env variable for username in deploy server-password: MAVEN_PASSWORD # env variable for token in deploy @@ -32,6 +32,11 @@ jobs: - name: Deploy run: mvn deploy -B -DskipTests -Psign,deploy-central --no-transfer-progress env: + MAVEN_OPTS: > + --add-opens=java.base/java.util=ALL-UNNAMED + --add-opens=java.base/java.lang.reflect=ALL-UNNAMED + --add-opens=java.base/java.text=ALL-UNNAMED + --add-opens=java.desktop/java.awt.font=ALL-UNNAMED MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} \ No newline at end of file diff --git a/.github/workflows/publish-github.yml b/.github/workflows/publish-github.yml index 32a3041b..3923b47f 100644 --- a/.github/workflows/publish-github.yml +++ b/.github/workflows/publish-github.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-java@v1 with: - java-version: 11 + java-version: 16 gpg-private-key: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import gpg-passphrase: MAVEN_GPG_PASSPHRASE # env variable for GPG private key passphrase - uses: actions/cache@v2 @@ -35,6 +35,6 @@ jobs: SLACK_ICON_EMOJI: ':bot:' SLACK_CHANNEL: 'cryptomator-desktop' SLACK_TITLE: "Published ${{ github.event.repository.name }} ${{ github.event.release.tag_name }}" - SLACK_MESSAGE: "Ready to ." + SLACK_MESSAGE: "Ready to ." SLACK_FOOTER: - MSG_MINIMAL: true \ No newline at end of file + MSG_MINIMAL: true diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..d361191e --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,78 @@ + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..79ee123c --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 2fc03d02..d7e555a5 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -7,5 +7,5 @@ - + \ No newline at end of file diff --git a/README.md b/README.md index cf1af941..ccd045d0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ ![cryptomator](cryptomator.png) [![Build](https://github.com/cryptomator/cryptofs/workflows/Build/badge.svg)](https://github.com/cryptomator/cryptofs/actions?query=workflow%3ABuild) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/7248ca7d466843f785f79f33374302c2)](https://www.codacy.com/app/cryptomator/cryptofs) -[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/7248ca7d466843f785f79f33374302c2)](https://www.codacy.com/app/cryptomator/cryptofs?utm_source=github.com&utm_medium=referral&utm_content=cryptomator/cryptofs&utm_campaign=Badge_Coverage) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/7248ca7d466843f785f79f33374302c2)](https://www.codacy.com/gh/cryptomator/cryptofs/dashboard) +[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/7248ca7d466843f785f79f33374302c2)](https://www.codacy.com/gh/cryptomator/cryptofs/dashboard) [![Known Vulnerabilities](https://snyk.io/test/github/cryptomator/cryptofs/badge.svg)](https://snyk.io/test/github/cryptomator/cryptofs) **CryptoFS:** Implementation of the [Cryptomator](https://github.com/cryptomator/cryptomator) encryption scheme. @@ -93,7 +93,7 @@ For more details on how to use the constructed `FileSystem`, you may consult the ### Dependencies -* Java 11 +* Java 16 (will be updated to 17 in late 2021) * Maven 3 ### Run Maven diff --git a/pom.xml b/pom.xml index 7d0b80f6..62515786 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.cryptomator cryptofs - 1.9.14 + 2.0.0 Cryptomator Crypto Filesystem This library provides the Java filesystem provider used by Cryptomator. https://github.com/cryptomator/cryptofs @@ -15,17 +15,24 @@ UTF-8 + 16 - 1.4.1 - 2.29.1 - 30.0-jre - 1.7.30 + 2.0.0 + 3.18.1 + 2.37 + 30.1.1-jre + 1.7.31 - 5.6.2 - 3.3.3 + 5.7.2 + 3.11.2 2.2 + + + 6.2.2 + 0.8.7 + 1.6.8 @@ -59,26 +66,27 @@ cryptolib ${cryptolib.version} + + com.auth0 + java-jwt + ${jwt.version} + + + com.google.dagger + dagger + ${dagger.version} + com.google.guava guava ${guava.version} - - org.slf4j slf4j-api ${slf4j.version} - - - com.google.dagger - dagger - ${dagger.version} - - org.junit.jupiter @@ -114,33 +122,11 @@ - - org.apache.maven.plugins - maven-enforcer-plugin - 3.0.0-M3 - - - enforce-java - - enforce - - - - - You need at least JDK 11.0.3 to build this project. - [11.0.3,) - - - - - - org.apache.maven.plugins maven-compiler-plugin 3.8.1 - 8 true @@ -154,19 +140,12 @@ org.apache.maven.plugins maven-surefire-plugin - 2.22.2 + 3.0.0-M5 org.apache.maven.plugins maven-jar-plugin 3.2.0 - - - - org.cryptomator.cryptofs - - - maven-source-plugin @@ -182,7 +161,7 @@ maven-javadoc-plugin - 3.2.0 + 3.3.0 attach-javadocs @@ -217,14 +196,6 @@ serialData see - - - - javax.annotation - jsr250-api - 1.0 - - @@ -238,7 +209,7 @@ org.owasp dependency-check-maven - 6.1.0 + ${dependency-check.version} 24 0 @@ -265,7 +236,7 @@ org.jacoco jacoco-maven-plugin - 0.8.6 + ${jacoco.version} prepare-agent @@ -291,7 +262,7 @@ maven-gpg-plugin - 1.6 + 3.0.1 sign-artifacts @@ -326,7 +297,7 @@ org.sonatype.plugins nexus-staging-maven-plugin - 1.6.8 + ${nexus-staging.version} true ossrh diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java new file mode 100644 index 00000000..c645dcc5 --- /dev/null +++ b/src/main/java/module-info.java @@ -0,0 +1,24 @@ +import org.cryptomator.cryptofs.CryptoFileSystemProvider; + +import java.nio.file.spi.FileSystemProvider; + +module org.cryptomator.cryptofs { + requires transitive org.cryptomator.cryptolib; + requires com.google.common; + requires org.slf4j; + requires dagger; + requires com.auth0.jwt; + + // filename-based module required by dagger + // we will probably need to live with this for a while: + // https://github.com/javax-inject/javax-inject/issues/33 + // May be provided by another lib during runtime + requires static javax.inject; + + exports org.cryptomator.cryptofs; + exports org.cryptomator.cryptofs.common; + exports org.cryptomator.cryptofs.migration; + exports org.cryptomator.cryptofs.migration.api; + + provides FileSystemProvider with CryptoFileSystemProvider; +} \ No newline at end of file diff --git a/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java b/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java index fdd2c2ed..e655861a 100644 --- a/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java +++ b/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java @@ -47,8 +47,7 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (obj instanceof CiphertextFilePath) { - CiphertextFilePath other = (CiphertextFilePath) obj; + if (obj instanceof CiphertextFilePath other) { return this.path.equals(other.path) && this.deflatedFileName.equals(other.deflatedFileName); } else { return false; diff --git a/src/main/java/org/cryptomator/cryptofs/ContentRootMissingException.java b/src/main/java/org/cryptomator/cryptofs/ContentRootMissingException.java new file mode 100644 index 00000000..d74a2f99 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/ContentRootMissingException.java @@ -0,0 +1,10 @@ +package org.cryptomator.cryptofs; + +import java.nio.file.NoSuchFileException; + +public class ContentRootMissingException extends NoSuchFileException { + + public ContentRootMissingException(String msg) { + super(msg); + } +} diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemComponent.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemComponent.java index 28ab66f4..d82b2675 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemComponent.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemComponent.java @@ -2,13 +2,9 @@ import dagger.BindsInstance; import dagger.Subcomponent; -import org.cryptomator.cryptofs.attr.AttributeViewComponent; -import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker; -import org.cryptomator.cryptofs.dir.DirectoryStreamComponent; -import org.cryptomator.cryptofs.fh.OpenCryptoFileComponent; +import org.cryptomator.cryptolib.api.Cryptor; import java.nio.file.Path; -import java.util.Set; @CryptoFileSystemScoped @Subcomponent(modules = {CryptoFileSystemModule.class}) @@ -19,6 +15,12 @@ public interface CryptoFileSystemComponent { @Subcomponent.Builder interface Builder { + @BindsInstance + Builder cryptor(Cryptor cryptor); + + @BindsInstance + Builder vaultConfig(VaultConfig vaultConfig); + @BindsInstance Builder provider(CryptoFileSystemProvider provider); diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java index fb10f3ef..5da6de99 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java @@ -21,12 +21,9 @@ import org.cryptomator.cryptofs.dir.DirectoryStreamFactory; import org.cryptomator.cryptofs.fh.OpenCryptoFiles; import org.cryptomator.cryptolib.api.Cryptor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.io.IOException; -import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.AccessDeniedException; import java.nio.file.AccessMode; @@ -38,7 +35,6 @@ import java.nio.file.DirectoryStream.Filter; import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileStore; -import java.nio.file.FileSystemException; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; @@ -105,7 +101,7 @@ public CryptoFileSystemImpl(CryptoFileSystemProvider provider, CryptoFileSystems PathMatcherFactory pathMatcherFactory, DirectoryStreamFactory directoryStreamFactory, DirectoryIdProvider dirIdProvider, AttributeProvider fileAttributeProvider, AttributeByNameProvider fileAttributeByNameProvider, AttributeViewProvider fileAttributeViewProvider, OpenCryptoFiles openCryptoFiles, Symlinks symlinks, FinallyUtil finallyUtil, CiphertextDirectoryDeleter ciphertextDirDeleter, ReadonlyFlag readonlyFlag, - CryptoFileSystemProperties fileSystemProperties, RootDirectoryInitializer rootDirectoryInitializer) { + CryptoFileSystemProperties fileSystemProperties) { this.provider = provider; this.cryptoFileSystems = cryptoFileSystems; this.pathToVault = pathToVault; @@ -129,8 +125,6 @@ public CryptoFileSystemImpl(CryptoFileSystemProvider provider, CryptoFileSystems this.rootPath = cryptoPathFactory.rootFor(this); this.emptyPath = cryptoPathFactory.emptyFor(this); - - rootDirectoryInitializer.initialize(rootPath); } @Override @@ -269,16 +263,11 @@ void checkAccess(CryptoPath cleartextPath, AccessMode... modes) throws IOExcepti } private boolean hasAccess(Set permissions, AccessMode accessMode) { - switch (accessMode) { - case READ: - return permissions.contains(PosixFilePermission.OWNER_READ); - case WRITE: - return permissions.contains(PosixFilePermission.OWNER_WRITE); - case EXECUTE: - return permissions.contains(PosixFilePermission.OWNER_EXECUTE); - default: - throw new UnsupportedOperationException("AccessMode " + accessMode + " not supported."); - } + return switch (accessMode) { + case READ -> permissions.contains(PosixFilePermission.OWNER_READ); + case WRITE -> permissions.contains(PosixFilePermission.OWNER_WRITE); + case EXECUTE -> permissions.contains(PosixFilePermission.OWNER_EXECUTE); + }; } boolean isHidden(CryptoPath cleartextPath) throws IOException { @@ -292,6 +281,7 @@ boolean isHidden(CryptoPath cleartextPath) throws IOException { void createDirectory(CryptoPath cleartextDir, FileAttribute... attrs) throws IOException { readonlyFlag.assertWritable(); + assertCleartextNameLengthAllowed(cleartextDir); CryptoPath cleartextParentDir = cleartextDir.getParent(); if (cleartextParentDir == null) { return; @@ -303,7 +293,6 @@ void createDirectory(CryptoPath cleartextDir, FileAttribute... attrs) throws cryptoPathMapper.assertNonExisting(cleartextDir); CiphertextFilePath ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextDir); Path ciphertextDirFile = ciphertextPath.getDirFilePath(); - assertCiphertextPathLengthMeetsLimitations(ciphertextDirFile); CiphertextDirectory ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextDir); // atomically check for FileAlreadyExists and create otherwise: Files.createDirectory(ciphertextPath.getRawPath()); @@ -342,25 +331,28 @@ FileChannel newFileChannel(CryptoPath cleartextPath, Set o throw e; } } - switch (ciphertextFileType) { - case SYMLINK: - if (options.noFollowLinks()) { - throw new UnsupportedOperationException("Unsupported OpenOption LinkOption.NOFOLLOW_LINKS. Can not create file channel for symbolic link."); - } else { - CryptoPath resolvedPath = symlinks.resolveRecursively(cleartextPath); - return newFileChannel(resolvedPath, options, attrs); - } - case FILE: - return newFileChannel(cleartextPath, options, attrs); - default: - throw new UnsupportedOperationException("Can not create file channel for " + ciphertextFileType.name()); + return switch (ciphertextFileType) { + case SYMLINK -> newFileChannelFromSymlink(cleartextPath, options, attrs); + case FILE -> newFileChannelFromFile(cleartextPath, options, attrs); + case DIRECTORY -> throw new UnsupportedOperationException("Can not create file channel for " + ciphertextFileType.name()); + }; + } + + private FileChannel newFileChannelFromSymlink(CryptoPath cleartextPath, EffectiveOpenOptions options, FileAttribute... attrs) throws IOException { + if (options.noFollowLinks()) { + throw new UnsupportedOperationException("Unsupported OpenOption LinkOption.NOFOLLOW_LINKS. Can not create file channel for symbolic link."); + } else { + CryptoPath resolvedPath = symlinks.resolveRecursively(cleartextPath); + return newFileChannelFromFile(resolvedPath, options, attrs); } } - private FileChannel newFileChannel(CryptoPath cleartextFilePath, EffectiveOpenOptions options, FileAttribute... attrs) throws IOException { + private FileChannel newFileChannelFromFile(CryptoPath cleartextFilePath, EffectiveOpenOptions options, FileAttribute... attrs) throws IOException { + if (options.create() || options.createNew()) { + assertCleartextNameLengthAllowed(cleartextFilePath); + } CiphertextFilePath ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextFilePath); Path ciphertextFilePath = ciphertextPath.getFilePath(); - assertCiphertextPathLengthMeetsLimitations(ciphertextFilePath); if (options.createNew() && openCryptoFiles.get(ciphertextFilePath).isPresent()) { throw new FileAlreadyExistsException(cleartextFilePath.toString()); } else { @@ -386,12 +378,8 @@ void delete(CryptoPath cleartextPath) throws IOException { CiphertextFileType ciphertextFileType = cryptoPathMapper.getCiphertextFileType(cleartextPath); CiphertextFilePath ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextPath); switch (ciphertextFileType) { - case DIRECTORY: - deleteDirectory(cleartextPath, ciphertextPath); - return; - default: - Files.walkFileTree(ciphertextPath.getRawPath(), DeletingFileVisitor.INSTANCE); - return; + case DIRECTORY -> deleteDirectory(cleartextPath, ciphertextPath); + case FILE, SYMLINK -> Files.walkFileTree(ciphertextPath.getRawPath(), DeletingFileVisitor.INSTANCE); } } @@ -414,6 +402,7 @@ private void deleteDirectory(CryptoPath cleartextPath, CiphertextFilePath cipher void copy(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption... options) throws IOException { readonlyFlag.assertWritable(); + assertCleartextNameLengthAllowed(cleartextTarget); if (cleartextSource.equals(cleartextTarget)) { return; } @@ -422,17 +411,9 @@ void copy(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption... cryptoPathMapper.assertNonExisting(cleartextTarget); } switch (ciphertextFileType) { - case SYMLINK: - copySymlink(cleartextSource, cleartextTarget, options); - return; - case FILE: - copyFile(cleartextSource, cleartextTarget, options); - return; - case DIRECTORY: - copyDirectory(cleartextSource, cleartextTarget, options); - return; - default: - throw new UnsupportedOperationException("Unhandled node type " + ciphertextFileType); + case SYMLINK -> copySymlink(cleartextSource, cleartextTarget, options); + case FILE -> copyFile(cleartextSource, cleartextTarget, options); + case DIRECTORY -> copyDirectory(cleartextSource, cleartextTarget, options); } } @@ -440,7 +421,6 @@ private void copySymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget, if (ArrayUtils.contains(options, LinkOption.NOFOLLOW_LINKS)) { CiphertextFilePath ciphertextSourceFile = cryptoPathMapper.getCiphertextFilePath(cleartextSource); CiphertextFilePath ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); - assertCiphertextPathLengthMeetsLimitations(ciphertextTargetFile.getSymlinkFilePath()); CopyOption[] resolvedOptions = ArrayUtils.without(options, LinkOption.NOFOLLOW_LINKS).toArray(CopyOption[]::new); Files.createDirectories(ciphertextTargetFile.getRawPath()); Files.copy(ciphertextSourceFile.getSymlinkFilePath(), ciphertextTargetFile.getSymlinkFilePath(), resolvedOptions); @@ -456,7 +436,6 @@ private void copySymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget, private void copyFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption[] options) throws IOException { CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource); CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); - assertCiphertextPathLengthMeetsLimitations(ciphertextTarget.getFilePath()); if (ciphertextTarget.isShortened()) { Files.createDirectories(ciphertextTarget.getRawPath()); } @@ -520,6 +499,7 @@ private void copyAttributes(Path src, Path dst) throws IOException { void move(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption... options) throws IOException { readonlyFlag.assertWritable(); + assertCleartextNameLengthAllowed(cleartextTarget); if (cleartextSource.equals(cleartextTarget)) { return; } @@ -528,17 +508,9 @@ void move(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption... cryptoPathMapper.assertNonExisting(cleartextTarget); } switch (ciphertextFileType) { - case SYMLINK: - moveSymlink(cleartextSource, cleartextTarget, options); - return; - case FILE: - moveFile(cleartextSource, cleartextTarget, options); - return; - case DIRECTORY: - moveDirectory(cleartextSource, cleartextTarget, options); - return; - default: - throw new UnsupportedOperationException("Unhandled node type " + ciphertextFileType); + case SYMLINK -> moveSymlink(cleartextSource, cleartextTarget, options); + case FILE -> moveFile(cleartextSource, cleartextTarget, options); + case DIRECTORY -> moveDirectory(cleartextSource, cleartextTarget, options); } } @@ -547,7 +519,6 @@ private void moveSymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget, // "the symbolic link itself, not the target of the link, is moved" CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource); CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); - assertCiphertextPathLengthMeetsLimitations(ciphertextTarget.getSymlinkFilePath()); try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath())) { Files.move(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath(), options); if (ciphertextTarget.isShortened()) { @@ -564,7 +535,6 @@ private void moveFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, Co // we need to re-map the OpenCryptoFile entry. CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource); CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); - assertCiphertextPathLengthMeetsLimitations(ciphertextTarget.getFilePath()); try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath())) { if (ciphertextTarget.isShortened()) { Files.createDirectory(ciphertextTarget.getRawPath()); @@ -583,7 +553,6 @@ private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge // Hence there is no need to re-map OpenCryptoFile entries. CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource); CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); - assertCiphertextPathLengthMeetsLimitations(ciphertextTarget.getDirFilePath()); if (ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING)) { // check if not attempting to move atomically: if (ArrayUtils.contains(options, StandardCopyOption.ATOMIC_MOVE)) { @@ -623,8 +592,8 @@ CryptoFileStore getFileStore() { void createSymbolicLink(CryptoPath cleartextPath, Path target, FileAttribute... attrs) throws IOException { assertOpen(); - CiphertextFilePath ciphertextFilePath = cryptoPathMapper.getCiphertextFilePath(cleartextPath); - assertCiphertextPathLengthMeetsLimitations(ciphertextFilePath.getSymlinkFilePath()); + readonlyFlag.assertWritable(); + assertCleartextNameLengthAllowed(cleartextPath); symlinks.createSymbolicLink(cleartextPath, target, attrs); } @@ -642,13 +611,11 @@ CryptoPath getRootPath() { CryptoPath getEmptyPath() { return emptyPath; } - - void assertCiphertextPathLengthMeetsLimitations(Path cdrFilePath) throws FileNameTooLongException { - Path vaultRelativePath = pathToVault.relativize(cdrFilePath); - String fileName = vaultRelativePath.getName(3).toString(); // fourth path element (d/xx/yyyyy/file.c9r/symlink.c9r) - String path = vaultRelativePath.toString(); - if (fileName.length() > fileSystemProperties.maxNameLength() || path.length() > fileSystemProperties.maxPathLength()) { - throw new FileNameTooLongException(path, fileSystemProperties.maxPathLength(), fileSystemProperties.maxNameLength()); + + void assertCleartextNameLengthAllowed(CryptoPath cleartextPath) throws FileNameTooLongException { + String filename = cleartextPath.getFileName().toString(); + if (filename.length() > fileSystemProperties.maxCleartextNameLength()) { + throw new FileNameTooLongException(cleartextPath.toString(), fileSystemProperties.maxCleartextNameLength()); } } diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java index 2ccb7f48..eacc1972 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java @@ -9,18 +9,12 @@ import dagger.Provides; import org.cryptomator.cryptofs.attr.AttributeComponent; import org.cryptomator.cryptofs.attr.AttributeViewComponent; -import org.cryptomator.cryptofs.common.Constants; -import org.cryptomator.cryptofs.common.MasterkeyBackupHelper; import org.cryptomator.cryptofs.dir.DirectoryStreamComponent; import org.cryptomator.cryptofs.fh.OpenCryptoFileComponent; -import org.cryptomator.cryptolib.api.Cryptor; -import org.cryptomator.cryptolib.api.CryptorProvider; -import org.cryptomator.cryptolib.api.KeyFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.io.UncheckedIOException; import java.nio.file.FileStore; import java.nio.file.Files; import java.nio.file.Path; @@ -31,23 +25,6 @@ class CryptoFileSystemModule { private static final Logger LOG = LoggerFactory.getLogger(CryptoFileSystemModule.class); - @Provides - @CryptoFileSystemScoped - public Cryptor provideCryptor(CryptorProvider cryptorProvider, @PathToVault Path pathToVault, CryptoFileSystemProperties properties, ReadonlyFlag readonlyFlag) { - try { - Path masterKeyPath = pathToVault.resolve(properties.masterkeyFilename()); - assert Files.exists(masterKeyPath); // since 1.3.0 a file system can only be created for existing vaults. initialization is done before. - byte[] keyFileContents = Files.readAllBytes(masterKeyPath); - Cryptor cryptor = cryptorProvider.createFromKeyFile(KeyFile.parse(keyFileContents), properties.passphrase(), properties.pepper(), Constants.VAULT_VERSION); - if (!readonlyFlag.isSet()) { - MasterkeyBackupHelper.attemptMasterKeyBackup(masterKeyPath); - } - return cryptor; - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - @Provides @CryptoFileSystemScoped public Optional provideNativeFileStore(@PathToVault Path pathToVault) { diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java index 7329773d..d9e1b944 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java @@ -8,24 +8,21 @@ *******************************************************************************/ package org.cryptomator.cryptofs; -import static java.util.Arrays.asList; -import static java.util.Collections.unmodifiableSet; +import com.google.common.base.Strings; +import org.cryptomator.cryptolib.api.CryptorProvider; +import org.cryptomator.cryptolib.api.MasterkeyLoader; import java.net.URI; import java.nio.file.FileSystems; import java.nio.file.Path; -import java.text.Normalizer; -import java.text.Normalizer.Form; import java.util.AbstractMap; import java.util.Collection; import java.util.EnumSet; -import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.function.Consumer; -import com.google.common.base.Strings; -import org.cryptomator.cryptofs.common.Constants; +import static java.util.Arrays.asList; /** * Properties to pass to @@ -33,46 +30,39 @@ *
  • {@link FileSystems#newFileSystem(URI, Map)} or *
  • {@link CryptoFileSystemProvider#newFileSystem(Path, CryptoFileSystemProperties)}. * - * + * * @author Markus Kreusch */ public class CryptoFileSystemProperties extends AbstractMap { /** - * Key identifying the passphrase for an encrypted vault. - */ - public static final String PROPERTY_PASSPHRASE = "passphrase"; - - /** - * Maximum ciphertext path length. + * Maximum cleartext filename length. * - * @since 1.9.8 + * @since 2.0.0 */ - public static final String PROPERTY_MAX_PATH_LENGTH = "maxPathLength"; + public static final String PROPERTY_MAX_CLEARTEXT_NAME_LENGTH = "maxCleartextNameLength"; - static final int DEFAULT_MAX_PATH_LENGTH = Constants.MAX_CIPHERTEXT_PATH_LENGTH; + static final int DEFAULT_MAX_CLEARTEXT_NAME_LENGTH = LongFileNameProvider.MAX_FILENAME_BUFFER_SIZE; /** - * Maximum filename length of .c9r files. + * Key identifying the key loader used during initialization. * - * @since 1.9.9 + * @since 2.0.0 */ - public static final String PROPERTY_MAX_NAME_LENGTH = "maxNameLength"; - - static final int DEFAULT_MAX_NAME_LENGTH = Constants.MAX_CIPHERTEXT_NAME_LENGTH; + public static final String PROPERTY_KEYLOADER = "keyLoader"; /** - * Key identifying the pepper used during key derivation. - * - * @since 1.3.2 + * Key identifying the name of the vault config file located inside the vault directory. + * + * @since 2.0.0 */ - public static final String PROPERTY_PEPPER = "pepper"; + public static final String PROPERTY_VAULTCONFIG_FILENAME = "vaultConfigFilename"; - static final byte[] DEFAULT_PEPPER = new byte[0]; + static final String DEFAULT_VAULTCONFIG_FILENAME = "vault.cryptomator"; /** * Key identifying the name of the masterkey file located inside the vault directory. - * + * * @since 1.1.0 */ public static final String PROPERTY_MASTERKEY_FILENAME = "masterkeyFilename"; @@ -81,69 +71,48 @@ public class CryptoFileSystemProperties extends AbstractMap { /** * Key identifying the filesystem flags. - * + * * @since 1.3.0 */ public static final String PROPERTY_FILESYSTEM_FLAGS = "flags"; - static final Set DEFAULT_FILESYSTEM_FLAGS = unmodifiableSet(EnumSet.of(FileSystemFlags.MIGRATE_IMPLICITLY, FileSystemFlags.INIT_IMPLICITLY)); + static final Set DEFAULT_FILESYSTEM_FLAGS = EnumSet.noneOf(FileSystemFlags.class); public enum FileSystemFlags { /** * If present, the vault is opened in read-only mode. - *

    - * This flag can not be set together with {@link #INIT_IMPLICITLY} or {@link #MIGRATE_IMPLICITLY}. */ READONLY, + } - /** - * If present, the vault gets automatically migrated during file system creation, which might become significantly slower. - * If absent, a {@link FileSystemNeedsMigrationException} will get thrown during the attempt to open a vault that needs migration. - *

    - * This flag can not be set together with {@link #READONLY}. - * - * @since 1.4.0 - */ - MIGRATE_IMPLICITLY, - - /** - * If present, the vault structure will implicitly get initialized upon filesystem creation. - *

    - * This flag can not be set together with {@link #READONLY}. - * - * @deprecated Will get removed in version 2.0.0. Use {@link CryptoFileSystemProvider#initialize(Path, String, CharSequence)} explicitly. - */ - @Deprecated INIT_IMPLICITLY, + /** + * Key identifying the combination of ciphers to use in a vault. Only meaningful during vault initialization. + * + * @since 2.0.0 + */ + public static final String PROPERTY_CIPHER_COMBO = "cipherCombo"; - /** - * If present, the maximum ciphertext path length (beginning from the root of the vault directory). - *

    - * If exceeding the limit during a file operation, an exception is thrown. - * - * @since 1.9.8 - */ - MAX_PATH_LENGTH, - }; + static final CryptorProvider.Scheme DEFAULT_CIPHER_COMBO = CryptorProvider.Scheme.SIV_GCM; private final Set> entries; private CryptoFileSystemProperties(Builder builder) { - this.entries = unmodifiableSet(new HashSet<>(asList( // - entry(PROPERTY_PASSPHRASE, builder.passphrase), // - entry(PROPERTY_PEPPER, builder.pepper), // - entry(PROPERTY_FILESYSTEM_FLAGS, builder.flags), // - entry(PROPERTY_MASTERKEY_FILENAME, builder.masterkeyFilename), // - entry(PROPERTY_MAX_PATH_LENGTH, builder.maxPathLength), // - entry(PROPERTY_MAX_NAME_LENGTH, builder.maxNameLength) // - ))); + this.entries = Set.of( // + Map.entry(PROPERTY_KEYLOADER, builder.keyLoader), // + Map.entry(PROPERTY_FILESYSTEM_FLAGS, builder.flags), // + Map.entry(PROPERTY_VAULTCONFIG_FILENAME, builder.vaultConfigFilename), // + Map.entry(PROPERTY_MASTERKEY_FILENAME, builder.masterkeyFilename), // + Map.entry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, builder.maxCleartextNameLength), // + Map.entry(PROPERTY_CIPHER_COMBO, builder.cipherCombo) // + ); } - CharSequence passphrase() { - return (CharSequence) get(PROPERTY_PASSPHRASE); + MasterkeyLoader keyLoader() { + return (MasterkeyLoader) get(PROPERTY_KEYLOADER); } - byte[] pepper() { - return (byte[]) get(PROPERTY_PEPPER); + public CryptorProvider.Scheme cipherCombo() { + return (CryptorProvider.Scheme) get(PROPERTY_CIPHER_COMBO); } @SuppressWarnings("unchecked") @@ -155,24 +124,16 @@ public boolean readonly() { return flags().contains(FileSystemFlags.READONLY); } - boolean migrateImplicitly() { - return flags().contains(FileSystemFlags.MIGRATE_IMPLICITLY); - } - - boolean initializeImplicitly() { - return flags().contains(FileSystemFlags.INIT_IMPLICITLY); + String vaultConfigFilename() { + return (String) get(PROPERTY_VAULTCONFIG_FILENAME); } String masterkeyFilename() { return (String) get(PROPERTY_MASTERKEY_FILENAME); } - int maxPathLength() { - return (int) get(PROPERTY_MAX_PATH_LENGTH); - } - - int maxNameLength() { - return (int) get(PROPERTY_MAX_NAME_LENGTH); + int maxCleartextNameLength() { + return (int) get(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH); } @Override @@ -180,49 +141,18 @@ public Set> entrySet() { return entries; } - private static Entry entry(String key, Object value) { - return new Entry() { - @Override - public String getKey() { - return key; - } - - @Override - public Object getValue() { - return value; - } - - @Override - public Object setValue(Object value) { - throw new UnsupportedOperationException(); - } - }; - } - /** * Starts construction of {@code CryptoFileSystemProperties} - * + * * @return a {@link Builder} which can be used to construct {@code CryptoFileSystemProperties} */ public static Builder cryptoFileSystemProperties() { return new Builder(); } - /** - * Starts construction of {@code CryptoFileSystemProperties}. - * Convenience function for cryptoFileSystemProperties().withPassphrase(passphrase). - * - * @param passphrase the passphrase to use - * @return a {@link Builder} which can be used to construct {@code CryptoFileSystemProperties} - * @since 1.4.0 - */ - public static Builder withPassphrase(CharSequence passphrase) { - return new Builder().withPassphrase(passphrase); - } - /** * Starts construction of {@code CryptoFileSystemProperties} - * + * * @param properties a {@link Map} containing properties used to initialize the builder * @return a {@link Builder} which can be used to construct {@code CryptoFileSystemProperties} and has been initialized with the values from properties */ @@ -232,14 +162,14 @@ public static Builder cryptoFileSystemPropertiesFrom(Map properties) /** * Constructs {@code CryptoFileSystemProperties} from a {@link Map}. - * + * * @param properties the {@code Map} to convert * @return the passed in {@code Map} if already of type {@code CryptoFileSystemProperties} or a new {@code CryptoFileSystemProperties} instance holding the values from the {@code Map} * @throws IllegalArgumentException if a value in the {@code Map} does not have the expected type or if a required value is missing */ public static CryptoFileSystemProperties wrap(Map properties) { - if (properties instanceof CryptoFileSystemProperties) { - return (CryptoFileSystemProperties) properties; + if (properties instanceof CryptoFileSystemProperties p) { + return p; } else { try { return cryptoFileSystemPropertiesFrom(properties).build(); @@ -254,23 +184,23 @@ public static CryptoFileSystemProperties wrap(Map properties) { */ public static class Builder { - private CharSequence passphrase; - public byte[] pepper = DEFAULT_PEPPER; + public CryptorProvider.Scheme cipherCombo = DEFAULT_CIPHER_COMBO; + private MasterkeyLoader keyLoader = null; private final Set flags = EnumSet.copyOf(DEFAULT_FILESYSTEM_FLAGS); + private String vaultConfigFilename = DEFAULT_VAULTCONFIG_FILENAME; private String masterkeyFilename = DEFAULT_MASTERKEY_FILENAME; - private int maxPathLength = DEFAULT_MAX_PATH_LENGTH; - private int maxNameLength = DEFAULT_MAX_NAME_LENGTH; + private int maxCleartextNameLength = DEFAULT_MAX_CLEARTEXT_NAME_LENGTH; private Builder() { } private Builder(Map properties) { - checkedSet(CharSequence.class, PROPERTY_PASSPHRASE, properties, this::withPassphrase); - checkedSet(byte[].class, PROPERTY_PEPPER, properties, this::withPepper); + checkedSet(MasterkeyLoader.class, PROPERTY_KEYLOADER, properties, this::withKeyLoader); + checkedSet(String.class, PROPERTY_VAULTCONFIG_FILENAME, properties, this::withVaultConfigFilename); checkedSet(String.class, PROPERTY_MASTERKEY_FILENAME, properties, this::withMasterkeyFilename); checkedSet(Set.class, PROPERTY_FILESYSTEM_FLAGS, properties, this::withFlags); - checkedSet(Integer.class, PROPERTY_MAX_PATH_LENGTH, properties, this::withMaxPathLength); - checkedSet(Integer.class, PROPERTY_MAX_NAME_LENGTH, properties, this::withMaxNameLength); + checkedSet(Integer.class, PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, properties, this::withMaxCleartextNameLength); + checkedSet(CryptorProvider.Scheme.class, PROPERTY_CIPHER_COMBO, properties, this::withCipherCombo); } private void checkedSet(Class type, String key, Map properties, Consumer setter) { @@ -285,55 +215,47 @@ private void checkedSet(Class type, String key, Map properties } /** - * Sets the passphrase to use for a CryptoFileSystem. - * - * @param passphrase the passphrase to use - * @return this - */ - public Builder withPassphrase(CharSequence passphrase) { - this.passphrase = Normalizer.normalize(passphrase, Form.NFC); - return this; - } - - /** - * Sets the maximum ciphertext path length for a CryptoFileSystem. + * Sets the maximum cleartext filename length for a CryptoFileSystem. This value is checked during write + * operations. Read access to nodes with longer names should be unaffected. Setting this value to {@code 0} or + * a negative value effectively disables write access. * - * @param maxPathLength The maximum ciphertext path length allowed + * @param maxCleartextNameLength The maximum cleartext filename length allowed * @return this - * @since 1.9.8 + * @since 2.0.0 */ - public Builder withMaxPathLength(int maxPathLength) { - this.maxPathLength = maxPathLength; + public Builder withMaxCleartextNameLength(int maxCleartextNameLength) { + this.maxCleartextNameLength = maxCleartextNameLength; return this; } + /** - * Sets the maximum ciphertext filename length for a CryptoFileSystem. + * Sets the cipher combo used during vault initialization. * - * @param maxNameLength The maximum ciphertext filename length allowed + * @param cipherCombo The cipher combo * @return this - * @since 1.9.9 + * @since 2.0.0 */ - public Builder withMaxNameLength(int maxNameLength) { - this.maxNameLength = maxNameLength; + public Builder withCipherCombo(CryptorProvider.Scheme cipherCombo) { + this.cipherCombo = cipherCombo; return this; } /** - * Sets the pepper for a CryptoFileSystem. - * - * @param pepper A pepper used during key derivation + * Sets the keyloader for a CryptoFileSystem. + * + * @param keyLoader A factory creating a {@link MasterkeyLoader} capable of handling the given {@code scheme}. * @return this - * @since 1.3.2 + * @since 2.0.0 */ - public Builder withPepper(byte[] pepper) { - this.pepper = pepper; + public Builder withKeyLoader(MasterkeyLoader keyLoader) { + this.keyLoader = keyLoader; return this; } /** * Sets the flags for a CryptoFileSystem. - * + * * @param flags File system flags * @return this * @since 1.3.1 @@ -344,46 +266,32 @@ public Builder withFlags(FileSystemFlags... flags) { /** * Sets the flags for a CryptoFileSystem. - * + * * @param flags collection of file system flags * @return this * @since 1.3.0 */ public Builder withFlags(Collection flags) { - validate(flags); this.flags.clear(); this.flags.addAll(flags); return this; } - private void validate(Collection flags) { - if (flags.contains(FileSystemFlags.READONLY)) { - if (flags.contains(FileSystemFlags.INIT_IMPLICITLY)) { - throw new IllegalStateException("Can not set flag INIT_IMPLICITLY in conjunction with flag READONLY."); - } - if (flags.contains(FileSystemFlags.MIGRATE_IMPLICITLY)) { - throw new IllegalStateException("Can not set flag MIGRATE_IMPLICITLY in conjunction with flag READONLY."); - } - } - } - /** - * Sets the readonly flag for a CryptoFileSystem. - * + * Sets the name of the vault config file located inside the vault directory. + * + * @param vaultConfigFilename the filename of the jwt file containing the vault configuration * @return this - * @deprecated Will be removed in 2.0.0. Use {@link #withFlags(FileSystemFlags...) withFlags(FileSystemFlags.READONLY)} + * @since 2.0.0 */ - @Deprecated - public Builder withReadonlyFlag() { - flags.add(FileSystemFlags.READONLY); - flags.remove(FileSystemFlags.INIT_IMPLICITLY); - flags.remove(FileSystemFlags.MIGRATE_IMPLICITLY); + public Builder withVaultConfigFilename(String vaultConfigFilename) { + this.vaultConfigFilename = vaultConfigFilename; return this; } /** * Sets the name of the masterkey file located inside the vault directory. - * + * * @param masterkeyFilename the filename of the json file containing configuration to decrypt the masterkey * @return this * @since 1.1.0 @@ -395,7 +303,7 @@ public Builder withMasterkeyFilename(String masterkeyFilename) { /** * Validates the values and creates new {@link CryptoFileSystemProperties}. - * + * * @return a new {@code CryptoFileSystemProperties} with the values from this builder * @throws IllegalStateException if a required value was not set on this {@code Builder} */ @@ -405,8 +313,8 @@ public CryptoFileSystemProperties build() { } private void validate() { - if (passphrase == null) { - throw new IllegalStateException("passphrase is required"); + if (keyLoader == null) { + throw new IllegalStateException("keyLoader is required"); } if (Strings.nullToEmpty(masterkeyFilename).trim().isEmpty()) { throw new IllegalStateException("masterkeyFilename is required"); diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java index 9ae7236e..d6d32188 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java @@ -11,19 +11,17 @@ import org.cryptomator.cryptofs.ch.AsyncDelegatingFileChannel; import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker; -import org.cryptomator.cryptofs.common.MasterkeyBackupHelper; -import org.cryptomator.cryptofs.migration.Migrators; -import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener.ContinuationResult; -import org.cryptomator.cryptolib.Cryptors; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.CryptorProvider; -import org.cryptomator.cryptolib.api.InvalidPassphraseException; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import java.io.IOException; import java.net.URI; import java.nio.channels.AsynchronousFileChannel; import java.nio.channels.FileChannel; import java.nio.channels.SeekableByteChannel; +import java.nio.charset.StandardCharsets; import java.nio.file.AccessMode; import java.nio.file.CopyOption; import java.nio.file.DirectoryStream; @@ -33,7 +31,6 @@ import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.LinkOption; -import java.nio.file.NoSuchFileException; import java.nio.file.NotDirectoryException; import java.nio.file.OpenOption; import java.nio.file.Path; @@ -45,14 +42,11 @@ import java.nio.file.spi.FileSystemProvider; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -import java.text.Normalizer; -import java.text.Normalizer.Form; +import java.util.Arrays; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutorService; -import static java.nio.file.StandardCopyOption.ATOMIC_MOVE; -import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import static java.nio.file.StandardOpenOption.CREATE_NEW; import static java.nio.file.StandardOpenOption.WRITE; @@ -82,7 +76,6 @@ *

    * Afterwards you can use the created {@code FileSystem} to create paths, do directory listings, create files and so on. *

    - *

    * To create a new FileSystem from a URI using {@link FileSystems#newFileSystem(URI, Map)} you may have a look at {@link CryptoFileSystemUri}. * * @see CryptoFileSystemUri @@ -92,14 +85,20 @@ */ public class CryptoFileSystemProvider extends FileSystemProvider { - private static final CryptorProvider CRYPTOR_PROVIDER = Cryptors.version1(strongSecureRandom()); - private final CryptoFileSystems fileSystems; private final MoveOperation moveOperation; private final CopyOperation copyOperation; public CryptoFileSystemProvider() { - this(DaggerCryptoFileSystemProviderComponent.builder().cryptorProvider(CRYPTOR_PROVIDER).build()); + this(DaggerCryptoFileSystemProviderComponent.builder().csprng(strongSecureRandom()).build()); + } + + private static SecureRandom strongSecureRandom() { + try { + return SecureRandom.getInstanceStrong(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("A strong algorithm must exist in every Java platform.", e); + } } /** @@ -111,25 +110,18 @@ public CryptoFileSystemProvider() { this.copyOperation = component.copyOperation(); } - private static SecureRandom strongSecureRandom() { - try { - return SecureRandom.getInstanceStrong(); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("A strong algorithm must exist in every Java platform.", e); - } - } - /** * Typesafe alternative to {@link FileSystems#newFileSystem(URI, Map)}. Default way to retrieve a CryptoFS instance. * * @param pathToVault Path to this vault's storage location - * @param properties Parameters used during initialization of the file system + * @param properties Parameters used during initialization of the file system * @return a new file system - * @throws FileSystemNeedsMigrationException if the vault format needs to get updated and properties did not contain a flag for implicit migration. + * @throws FileSystemNeedsMigrationException if the vault format needs to get updated and properties did not contain a flag for implicit migration. * @throws FileSystemCapabilityChecker.MissingCapabilityException If the underlying filesystem lacks features required to store a vault - * @throws IOException if an I/O error occurs creating the file system + * @throws IOException if an I/O error occurs creating the file system + * @throws MasterkeyLoadingFailedException if the masterkey for this vault could not be loaded */ - public static CryptoFileSystem newFileSystem(Path pathToVault, CryptoFileSystemProperties properties) throws FileSystemNeedsMigrationException, IOException { + public static CryptoFileSystem newFileSystem(Path pathToVault, CryptoFileSystemProperties properties) throws FileSystemNeedsMigrationException, IOException, MasterkeyLoadingFailedException { URI uri = CryptoFileSystemUri.create(pathToVault.toAbsolutePath()); return (CryptoFileSystem) FileSystems.newFileSystem(uri, properties); } @@ -138,142 +130,48 @@ public static CryptoFileSystem newFileSystem(Path pathToVault, CryptoFileSystemP * Creates a new vault at the given directory path. * * @param pathToVault Path to an existing directory - * @param masterkeyFilename Name of the masterkey file - * @param passphrase Passphrase that should be used to unlock the vault - * @throws NotDirectoryException If the given path is not an existing directory. - * @throws FileSystemCapabilityChecker.MissingCapabilityException If the underlying filesystem lacks features required to store a vault - * @throws IOException If the vault structure could not be initialized due to I/O errors - * @since 1.3.0 + * @param properties Parameters to use when writing the vault configuration + * @param keyId ID of the master key to use for this vault + * @throws NotDirectoryException If the given path is not an existing directory. + * @throws IOException If the vault structure could not be initialized due to I/O errors + * @throws MasterkeyLoadingFailedException If thrown by the supplied keyLoader + * @since 2.0.0 */ - public static void initialize(Path pathToVault, String masterkeyFilename, CharSequence passphrase) throws NotDirectoryException, IOException { - initialize(pathToVault, masterkeyFilename, new byte[0], passphrase); - } - - /** - * Creates a new vault at the given directory path. - * - * @param pathToVault Path to an existing directory - * @param masterkeyFilename Name of the masterkey file - * @param pepper Application-specific pepper used during key derivation - * @param passphrase Passphrase that should be used to unlock the vault - * @throws NotDirectoryException If the given path is not an existing directory. - * @throws FileSystemCapabilityChecker.MissingCapabilityException If the underlying filesystem lacks features required to store a vault - * @throws IOException If the vault structure could not be initialized due to I/O errors - * @since 1.3.2 - */ - public static void initialize(Path pathToVault, String masterkeyFilename, byte[] pepper, CharSequence passphrase) throws NotDirectoryException, IOException { + public static void initialize(Path pathToVault, CryptoFileSystemProperties properties, URI keyId) throws NotDirectoryException, IOException, MasterkeyLoadingFailedException { if (!Files.isDirectory(pathToVault)) { throw new NotDirectoryException(pathToVault.toString()); } - try (Cryptor cryptor = CRYPTOR_PROVIDER.createNew()) { - // save masterkey file: - Path masterKeyPath = pathToVault.resolve(masterkeyFilename); - byte[] keyFileContents = cryptor.writeKeysToMasterkeyFile(Normalizer.normalize(passphrase, Form.NFC), pepper, Constants.VAULT_VERSION).serialize(); - Files.write(masterKeyPath, keyFileContents, CREATE_NEW, WRITE); - // create "d/RO/OTDIRECTORY": - String rootDirHash = cryptor.fileNameCryptor().hashDirectoryId(Constants.ROOT_DIR_ID); - Path rootDirPath = pathToVault.resolve(Constants.DATA_DIR_NAME).resolve(rootDirHash.substring(0, 2)).resolve(rootDirHash.substring(2)); - Files.createDirectories(rootDirPath); + byte[] rawKey = new byte[0]; + var config = VaultConfig.createNew().cipherCombo(properties.cipherCombo()).shorteningThreshold(Constants.DEFAULT_SHORTENING_THRESHOLD).build(); + try (Masterkey key = properties.keyLoader().loadKey(keyId); + Cryptor cryptor = CryptorProvider.forScheme(config.getCipherCombo()).provide(key, strongSecureRandom())) { + rawKey = key.getEncoded(); + // save vault config: + Path vaultConfigPath = pathToVault.resolve(properties.vaultConfigFilename()); + var token = config.toToken(keyId.toString(), rawKey); + Files.writeString(vaultConfigPath, token, StandardCharsets.US_ASCII, WRITE, CREATE_NEW); + // create "d" dir and root: + String dirHash = cryptor.fileNameCryptor().hashDirectoryId(Constants.ROOT_DIR_ID); + Path vaultCipherRootPath = pathToVault.resolve(Constants.DATA_DIR_NAME).resolve(dirHash.substring(0, 2)).resolve(dirHash.substring(2)); + Files.createDirectories(vaultCipherRootPath); + } finally { + Arrays.fill(rawKey, (byte) 0x00); } - assert containsVault(pathToVault, masterkeyFilename); + assert checkDirStructureForVault(pathToVault, properties.vaultConfigFilename(), properties.masterkeyFilename()) == DirStructure.VAULT; } /** - * Checks if the folder represented by the given path exists and contains a valid vault structure. + * Delegate to {@link DirStructure#checkDirStructure(Path, String, String)}. * - * @param pathToVault A directory path - * @param masterkeyFilename Name of the masterkey file - * @return true if the directory seems to contain a vault. - * @since 1.1.0 + * @param pathToAssumedVault + * @param vaultConfigFilename + * @param masterkeyFilename + * @return a {@link DirStructure} object + * @throws IOException + * @since 2.0.0 */ - public static boolean containsVault(Path pathToVault, String masterkeyFilename) { - Path masterKeyPath = pathToVault.resolve(masterkeyFilename); - Path dataDirPath = pathToVault.resolve(Constants.DATA_DIR_NAME); - return Files.isReadable(masterKeyPath) && Files.isDirectory(dataDirPath); - } - - /** - * Changes the passphrase of a vault at the given path. - * - * @param pathToVault Vault directory - * @param masterkeyFilename Name of the masterkey file - * @param oldPassphrase Current passphrase - * @param newPassphrase Future passphrase - * @throws InvalidPassphraseException If oldPassphrase can not be used to unlock the vault. - * @throws FileSystemNeedsMigrationException if the vault format needs to get updated. - * @throws IOException If the masterkey could not be read or written. - * @see #changePassphrase(Path, String, byte[], CharSequence, CharSequence) - * @since 1.1.0 - */ - public static void changePassphrase(Path pathToVault, String masterkeyFilename, CharSequence oldPassphrase, CharSequence newPassphrase) - throws InvalidPassphraseException, FileSystemNeedsMigrationException, IOException { - changePassphrase(pathToVault, masterkeyFilename, new byte[0], oldPassphrase, newPassphrase); - } - - /** - * Changes the passphrase of a vault at the given path. - * - * @param pathToVault Vault directory - * @param masterkeyFilename Name of the masterkey file - * @param pepper An application-specific pepper added to the salt during key-derivation (if applicable) - * @param oldPassphrase Current passphrase - * @param newPassphrase Future passphrase - * @throws InvalidPassphraseException If oldPassphrase can not be used to unlock the vault. - * @throws FileSystemNeedsMigrationException if the vault format needs to get updated. - * @throws IOException If the masterkey could not be read or written. - * @since 1.4.0 - */ - public static void changePassphrase(Path pathToVault, String masterkeyFilename, byte[] pepper, CharSequence oldPassphrase, CharSequence newPassphrase) - throws InvalidPassphraseException, FileSystemNeedsMigrationException, IOException { - if (Migrators.get().needsMigration(pathToVault, masterkeyFilename)) { - throw new FileSystemNeedsMigrationException(pathToVault); - } - String normalizedOldPassphrase = Normalizer.normalize(oldPassphrase, Form.NFC); - String normalizedNewPassphrase = Normalizer.normalize(newPassphrase, Form.NFC); - Path masterKeyPath = pathToVault.resolve(masterkeyFilename); - byte[] oldMasterkeyBytes = Files.readAllBytes(masterKeyPath); - byte[] newMasterkeyBytes = Cryptors.changePassphrase(CRYPTOR_PROVIDER, oldMasterkeyBytes, pepper, normalizedOldPassphrase, normalizedNewPassphrase); - Path backupKeyPath = pathToVault.resolve(masterkeyFilename + MasterkeyBackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + Constants.MASTERKEY_BACKUP_SUFFIX); - Files.move(masterKeyPath, backupKeyPath, REPLACE_EXISTING, ATOMIC_MOVE); - Files.write(masterKeyPath, newMasterkeyBytes, CREATE_NEW, WRITE); - } - - /** - * Exports the raw key for backup purposes or external key management. - * - * @param pathToVault Vault directory - * @param masterkeyFilename Name of the masterkey file - * @param pepper An application-specific pepper added to the salt during key-derivation (if applicable) - * @param passphrase Current passphrase - * @return A 64 byte array consisting of 32 byte aes key and 32 byte mac key - * @since 1.9.0 - */ - public static byte[] exportRawKey(Path pathToVault, String masterkeyFilename, byte[] pepper, CharSequence passphrase) throws InvalidPassphraseException, IOException { - String normalizedPassphrase = Normalizer.normalize(passphrase, Form.NFC); - Path masterKeyPath = pathToVault.resolve(masterkeyFilename); - byte[] masterKeyBytes = Files.readAllBytes(masterKeyPath); - return Cryptors.exportRawKey(CRYPTOR_PROVIDER, masterKeyBytes, pepper, normalizedPassphrase); - } - - /** - * Imports a raw key from backup or external key management. - * - * @param pathToVault Vault directory - * @param masterkeyFilename Name of the masterkey file - * @param pepper An application-specific pepper added to the salt during key-derivation (if applicable) - * @param passphrase Future passphrase - * @since 1.9.0 - */ - public static void restoreRawKey(Path pathToVault, String masterkeyFilename, byte[] rawKey, byte[] pepper, CharSequence passphrase) throws InvalidPassphraseException, IOException { - String normalizedPassphrase = Normalizer.normalize(passphrase, Form.NFC); - byte[] masterKeyBytes = Cryptors.restoreRawKey(CRYPTOR_PROVIDER, rawKey, pepper, normalizedPassphrase, Constants.VAULT_VERSION); - Path masterKeyPath = pathToVault.resolve(masterkeyFilename); - if (Files.exists(masterKeyPath)) { - byte[] oldMasterkeyBytes = Files.readAllBytes(masterKeyPath); - Path backupKeyPath = pathToVault.resolve(masterkeyFilename + MasterkeyBackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + Constants.MASTERKEY_BACKUP_SUFFIX); - Files.move(masterKeyPath, backupKeyPath, REPLACE_EXISTING, ATOMIC_MOVE); - } - Files.write(masterKeyPath, masterKeyBytes, CREATE_NEW, WRITE); + public static DirStructure checkDirStructureForVault(Path pathToAssumedVault, String vaultConfigFilename, String masterkeyFilename) throws IOException { + return DirStructure.checkDirStructure(pathToAssumedVault, vaultConfigFilename, masterkeyFilename); } /** @@ -290,39 +188,12 @@ public String getScheme() { } @Override - public CryptoFileSystem newFileSystem(URI uri, Map rawProperties) throws IOException { + public CryptoFileSystem newFileSystem(URI uri, Map rawProperties) throws IOException, MasterkeyLoadingFailedException { CryptoFileSystemUri parsedUri = CryptoFileSystemUri.parse(uri); CryptoFileSystemProperties properties = CryptoFileSystemProperties.wrap(rawProperties); - - // TODO remove implicit initialization in 2.0.0 - initializeFileSystemIfRequired(parsedUri, properties); - migrateFileSystemIfRequired(parsedUri, properties); - return fileSystems.create(this, parsedUri.pathToVault(), properties); } - @Deprecated - private void migrateFileSystemIfRequired(CryptoFileSystemUri parsedUri, CryptoFileSystemProperties properties) throws IOException, FileSystemNeedsMigrationException { - if (Migrators.get().needsMigration(parsedUri.pathToVault(), properties.masterkeyFilename())) { - if (properties.migrateImplicitly()) { - Migrators.get().migrate(parsedUri.pathToVault(), properties.masterkeyFilename(), properties.passphrase(), (state, progress) -> {}, (event) -> ContinuationResult.CANCEL); - } else { - throw new FileSystemNeedsMigrationException(parsedUri.pathToVault()); - } - } - } - - @Deprecated - private void initializeFileSystemIfRequired(CryptoFileSystemUri parsedUri, CryptoFileSystemProperties properties) throws NotDirectoryException, IOException, NoSuchFileException { - if (!CryptoFileSystemProvider.containsVault(parsedUri.pathToVault(), properties.masterkeyFilename())) { - if (properties.initializeImplicitly()) { - CryptoFileSystemProvider.initialize(parsedUri.pathToVault(), properties.masterkeyFilename(), properties.passphrase()); - } else { - throw new NoSuchFileException(parsedUri.pathToVault().toString(), null, "Vault not initialized."); - } - } - } - @Override public CryptoFileSystem getFileSystem(URI uri) { CryptoFileSystemUri parsedUri = CryptoFileSystemUri.parse(uri); @@ -394,7 +265,7 @@ public boolean isHidden(Path cleartextPath) throws IOException { } @Override - public FileStore getFileStore(Path cleartextPath) throws IOException { + public FileStore getFileStore(Path cleartextPath) { return fileSystem(cleartextPath).getFileStore(); } diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderComponent.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderComponent.java index ce52f6e6..78d47e44 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderComponent.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderComponent.java @@ -2,9 +2,9 @@ import dagger.BindsInstance; import dagger.Component; -import org.cryptomator.cryptolib.api.CryptorProvider; import javax.inject.Singleton; +import java.security.SecureRandom; @Singleton @Component(modules = {CryptoFileSystemProviderModule.class}) @@ -19,7 +19,7 @@ interface CryptoFileSystemProviderComponent { @Component.Builder interface Builder { @BindsInstance - Builder cryptorProvider(CryptorProvider cryptorProvider); + Builder csprng(SecureRandom csprng); CryptoFileSystemProviderComponent build(); } diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderModule.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderModule.java index 9bd7b33e..782137f4 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderModule.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderModule.java @@ -2,17 +2,12 @@ import dagger.Module; import dagger.Provides; -import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker; import javax.inject.Singleton; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; @Module(subcomponents = {CryptoFileSystemComponent.class}) public class CryptoFileSystemProviderModule { - - @Provides - @Singleton - public FileSystemCapabilityChecker provideFileSystemCapabilityChecker() { - return new FileSystemCapabilityChecker(); - } - + } diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemUri.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemUri.java index 15f4299f..fbf866bc 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemUri.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemUri.java @@ -69,6 +69,7 @@ static Path uncCompatibleUriToPath(URI uri) { * * @param pathToVault path to the vault * @param pathComponentsInsideVault path components to node inside the vault + * @return An URI pointing to the root of the CryptoFileSystem */ public static URI create(Path pathToVault, String... pathComponentsInsideVault) { try { diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java index d2332dab..81f422bf 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java @@ -1,16 +1,24 @@ package org.cryptomator.cryptofs; +import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.CryptorProvider; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Singleton; import java.io.IOException; -import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; import java.nio.file.FileSystemAlreadyExistsException; import java.nio.file.FileSystemNotFoundException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.security.SecureRandom; import java.util.EnumSet; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -20,40 +28,97 @@ @Singleton class CryptoFileSystems { - + private static final Logger LOG = LoggerFactory.getLogger(CryptoFileSystems.class); private final ConcurrentMap fileSystems = new ConcurrentHashMap<>(); private final CryptoFileSystemComponent.Builder cryptoFileSystemComponentBuilder; // sharing reusable builder via synchronized private final FileSystemCapabilityChecker capabilityChecker; + private final SecureRandom csprng; @Inject - public CryptoFileSystems(CryptoFileSystemComponent.Builder cryptoFileSystemComponentBuilder, FileSystemCapabilityChecker capabilityChecker) { + public CryptoFileSystems(CryptoFileSystemComponent.Builder cryptoFileSystemComponentBuilder, FileSystemCapabilityChecker capabilityChecker, SecureRandom csprng) { this.cryptoFileSystemComponentBuilder = cryptoFileSystemComponentBuilder; this.capabilityChecker = capabilityChecker; + this.csprng = csprng; + } + + public CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathToVault, CryptoFileSystemProperties properties) throws IOException, MasterkeyLoadingFailedException { + Path normalizedPathToVault = pathToVault.normalize(); + var token = readVaultConfigFile(normalizedPathToVault, properties); + + var configLoader = VaultConfig.decode(token); + var keyId = configLoader.getKeyId(); + try (Masterkey key = properties.keyLoader().loadKey(keyId)) { + var config = configLoader.verify(key.getEncoded(), Constants.VAULT_VERSION); + var adjustedProperties = adjustForCapabilities(pathToVault, properties); + var cryptor = CryptorProvider.forScheme(config.getCipherCombo()).provide(key.clone(), csprng); + try { + checkVaultRootExistence(pathToVault, cryptor); + return fileSystems.compute(normalizedPathToVault, (path, fs) -> { + if (fs == null) { + return create(provider, normalizedPathToVault, adjustedProperties, cryptor, config); + } else { + throw new FileSystemAlreadyExistsException(); + } + }); + } catch (Exception e) { //on any exception, destroy the cryptor + cryptor.destroy(); + throw e; + } + } } - public synchronized CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathToVault, CryptoFileSystemProperties properties) throws IOException { + /** + * Checks if the vault has a content root folder. If not, an exception is raised. + * @param pathToVault Path to the vault root + * @param cryptor Cryptor object initialized with the correct masterkey + * @throws ContentRootMissingException If the existence of encrypted vault content root cannot be ensured + */ + private void checkVaultRootExistence(Path pathToVault, Cryptor cryptor) throws ContentRootMissingException { + String dirHash = cryptor.fileNameCryptor().hashDirectoryId(Constants.ROOT_DIR_ID); + Path vaultCipherRootPath = pathToVault.resolve(Constants.DATA_DIR_NAME).resolve(dirHash.substring(0, 2)).resolve(dirHash.substring(2)); + if (!Files.exists(vaultCipherRootPath)) { + throw new ContentRootMissingException("The encrypted root directory of the vault " + pathToVault + " is missing."); + } + } + + // synchronized access to non-threadsafe cryptoFileSystemComponentBuilder required + private synchronized CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathToVault, CryptoFileSystemProperties properties, Cryptor cryptor, VaultConfig config) { + return cryptoFileSystemComponentBuilder // + .cryptor(cryptor) // + .vaultConfig(config) // + .pathToVault(pathToVault) // + .properties(properties) // + .provider(provider) // + .build() // + .cryptoFileSystem(); + } + + /** + * Attempts to read a vault config file + * + * @param pathToVault path to the vault's root + * @param properties properties used when attempting to construct a fs for this vault + * @return The contents of the file decoded in ASCII + * @throws IOException If the file could not be read + * @throws FileSystemNeedsMigrationException If the file doesn't exists, but a legacy masterkey file was found instead + */ + private String readVaultConfigFile(Path pathToVault, CryptoFileSystemProperties properties) throws IOException, FileSystemNeedsMigrationException { + Path vaultConfigFile = pathToVault.resolve(properties.vaultConfigFilename()); try { - Path normalizedPathToVault = pathToVault.normalize(); - CryptoFileSystemProperties adjustedProperties = adjustForCapabilities(normalizedPathToVault, properties); - return fileSystems.compute(normalizedPathToVault, (key, value) -> { - if (value == null) { - return cryptoFileSystemComponentBuilder // - .pathToVault(key) // - .properties(adjustedProperties) // - .provider(provider) // - .build() // - .cryptoFileSystem(); - } else { - throw new FileSystemAlreadyExistsException(); - } - }); - } catch (UncheckedIOException e) { - throw new IOException("Error during file system creation.", e); + return Files.readString(vaultConfigFile, StandardCharsets.US_ASCII); + } catch (NoSuchFileException e) { + Path masterkeyPath = pathToVault.resolve(properties.masterkeyFilename()); + if (Files.exists(masterkeyPath)) { + LOG.warn("Failed to read {}, but found {}}", vaultConfigFile, masterkeyPath); + throw new FileSystemNeedsMigrationException(pathToVault); + } else { + throw e; + } } } - + private CryptoFileSystemProperties adjustForCapabilities(Path pathToVault, CryptoFileSystemProperties originalProperties) throws FileSystemCapabilityChecker.MissingCapabilityException { if (!originalProperties.readonly()) { try { @@ -83,7 +148,7 @@ public CryptoFileSystemImpl get(Path pathToVault) { Path normalizedPathToVault = pathToVault.normalize(); CryptoFileSystemImpl fs = fileSystems.get(normalizedPathToVault); if (fs == null) { - throw new FileSystemNotFoundException(format("CryptoFileSystem at %s not initialized", pathToVault)); + throw new FileSystemNotFoundException(format("CryptoFileSystem at %s not initialized", normalizedPathToVault)); } else { return fs; } diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoPath.java b/src/main/java/org/cryptomator/cryptofs/CryptoPath.java index 044a0cdc..e6165aa8 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoPath.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoPath.java @@ -55,10 +55,9 @@ static CryptoPath castAndAssertAbsolute(Path path) { } static CryptoPath cast(Path path) { - if (path instanceof CryptoPath) { - CryptoPath cryptoPath = (CryptoPath) path; - cryptoPath.getFileSystem().assertOpen(); - return cryptoPath; + if (path instanceof CryptoPath p) { + p.getFileSystem().assertOpen(); + return p; } else { throw new ProviderMismatchException("Used a path from different provider: " + path); } @@ -351,8 +350,7 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (obj instanceof CryptoPath) { - CryptoPath other = (CryptoPath) obj; + if (obj instanceof CryptoPath other) { return this.fileSystem.equals(other.fileSystem) // && this.compareTo(other) == 0; } else { diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java index fbb8688e..c32cc6cd 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java @@ -48,17 +48,19 @@ public class CryptoPathMapper { private final Path dataRoot; private final DirectoryIdProvider dirIdProvider; private final LongFileNameProvider longFileNameProvider; + private final VaultConfig vaultConfig; private final LoadingCache ciphertextNames; private final Cache ciphertextDirectories; private final CiphertextDirectory rootDirectory; @Inject - CryptoPathMapper(@PathToVault Path pathToVault, Cryptor cryptor, DirectoryIdProvider dirIdProvider, LongFileNameProvider longFileNameProvider) { + CryptoPathMapper(@PathToVault Path pathToVault, Cryptor cryptor, DirectoryIdProvider dirIdProvider, LongFileNameProvider longFileNameProvider, VaultConfig vaultConfig) { this.dataRoot = pathToVault.resolve(DATA_DIR_NAME); this.cryptor = cryptor; this.dirIdProvider = dirIdProvider; this.longFileNameProvider = longFileNameProvider; + this.vaultConfig = vaultConfig; this.ciphertextNames = CacheBuilder.newBuilder().maximumSize(MAX_CACHED_CIPHERTEXT_NAMES).build(CacheLoader.from(this::getCiphertextFileName)); this.ciphertextDirectories = CacheBuilder.newBuilder().maximumSize(MAX_CACHED_DIR_PATHS).expireAfterWrite(MAX_CACHE_AGE).build(); this.rootDirectory = resolveDirectory(Constants.ROOT_DIR_ID); @@ -127,7 +129,7 @@ public CiphertextFilePath getCiphertextFilePath(CryptoPath cleartextPath) throws public CiphertextFilePath getCiphertextFilePath(Path parentCiphertextDir, String parentDirId, String cleartextName) { String ciphertextName = ciphertextNames.getUnchecked(new DirIdAndName(parentDirId, cleartextName)); Path c9rPath = parentCiphertextDir.resolve(ciphertextName); - if (ciphertextName.length() > Constants.MAX_CIPHERTEXT_NAME_LENGTH) { + if (ciphertextName.length() > vaultConfig.getShorteningThreshold()) { LongFileNameProvider.DeflatedFileName deflatedFileName = longFileNameProvider.deflate(c9rPath); return new CiphertextFilePath(deflatedFileName.c9sPath, Optional.of(deflatedFileName)); } else { @@ -198,8 +200,7 @@ public int hashCode() { public boolean equals(Object obj) { if (obj == this) { return true; - } else if (obj instanceof CiphertextDirectory) { - CiphertextDirectory other = (CiphertextDirectory) obj; + } else if (obj instanceof CiphertextDirectory other) { return this.dirId.equals(other.dirId) && this.path.equals(other.path); } else { return false; @@ -225,8 +226,7 @@ public int hashCode() { public boolean equals(Object obj) { if (obj == this) { return true; - } else if (obj instanceof DirIdAndName) { - DirIdAndName other = (DirIdAndName) obj; + } else if (obj instanceof DirIdAndName other) { return this.dirId.equals(other.dirId) && this.name.equals(other.name); } else { return false; diff --git a/src/main/java/org/cryptomator/cryptofs/DirStructure.java b/src/main/java/org/cryptomator/cryptofs/DirStructure.java new file mode 100644 index 00000000..daff7fb4 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/DirStructure.java @@ -0,0 +1,69 @@ +package org.cryptomator.cryptofs; + +import org.cryptomator.cryptofs.common.Constants; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.NotDirectoryException; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; + +/** + * Enumeration of the vault directory structure resemblances. + *

    + * A valid vault must contain a `d` directory. + * Beginning with vault format 8, it must also contain a vault config file. + * If the vault format is lower than 8, it must instead contain a masterkey file. + *

    + * In the latter case, to distinguish between a damaged vault 8 directory and a legacy vault the masterkey file must be read. + * For efficiency reasons, this class only checks for existence/readability of the above elements. + * Hence, if the result of {@link #checkDirStructure(Path, String, String)} is {@link #MAYBE_LEGACY}, one needs to parse + * the masterkey file and read out the vault version to determine this case. + * + * @since 2.0.0 + */ +public enum DirStructure { + + /** + * Dir contains a d dir as well as a vault config file. + */ + VAULT, + + /** + * Dir contains a d dir and a masterkey file, but misses a vault config file. + * Either needs migration to a newer format or damaged. + */ + MAYBE_LEGACY, + + /** + * Dir does not qualify as vault. + */ + UNRELATED; + + + /** + * Analyzes the structure of the given directory under certain vault existence criteria. + * + * @param pathToVault A directory path + * @param vaultConfigFilename Name of the vault config file + * @param masterkeyFilename Name of the masterkey file + * @return enum indicating what this directory might be + * @throws IOException if the provided path is not a directory, does not exist or cannot be read + */ + public static DirStructure checkDirStructure(Path pathToVault, String vaultConfigFilename, String masterkeyFilename) throws IOException { + if(! Files.readAttributes(pathToVault, BasicFileAttributes.class).isDirectory()) { + throw new NotDirectoryException(pathToVault.toString()); + } + Path vaultConfigPath = pathToVault.resolve(vaultConfigFilename); + Path masterkeyPath = pathToVault.resolve(masterkeyFilename); + Path dataDirPath = pathToVault.resolve(Constants.DATA_DIR_NAME); + if (Files.isDirectory(dataDirPath)) { + if (Files.isReadable(vaultConfigPath)) { + return VAULT; + } else if (Files.isReadable(masterkeyPath)) { + return MAYBE_LEGACY; + } + } + return UNRELATED; + } +} diff --git a/src/main/java/org/cryptomator/cryptofs/FileNameTooLongException.java b/src/main/java/org/cryptomator/cryptofs/FileNameTooLongException.java index 127798db..cdcdc598 100644 --- a/src/main/java/org/cryptomator/cryptofs/FileNameTooLongException.java +++ b/src/main/java/org/cryptomator/cryptofs/FileNameTooLongException.java @@ -6,13 +6,13 @@ /** * Indicates that an operation failed, as it would result in a ciphertext path that is too long for the underlying file system. * - * @see org.cryptomator.cryptofs.common.FileSystemCapabilityChecker#determineSupportedFileNameLength(Path) - * @since 1.9.8 + * @see org.cryptomator.cryptofs.common.FileSystemCapabilityChecker#determineSupportedCleartextFileNameLength(Path) + * @since 2.0.0 */ public class FileNameTooLongException extends FileSystemException { - public FileNameTooLongException(String c9rPathRelativeToVaultRoot, int maxPathLength, int maxNameLength) { - super(c9rPathRelativeToVaultRoot, null, "File name or path too long. Max ciphertext path name length is " + maxPathLength + ". Max ciphertext name is " + maxNameLength); + public FileNameTooLongException(String path, int maxNameLength) { + super(path, null, "File name or path too long. Max cleartext filename name length is " + maxNameLength); } } diff --git a/src/main/java/org/cryptomator/cryptofs/FileSystemInitializationFailedException.java b/src/main/java/org/cryptomator/cryptofs/FileSystemInitializationFailedException.java new file mode 100644 index 00000000..442c7eb3 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/FileSystemInitializationFailedException.java @@ -0,0 +1,14 @@ +package org.cryptomator.cryptofs; + +import java.io.IOException; + +public class FileSystemInitializationFailedException extends IOException { + + public FileSystemInitializationFailedException(String message, Throwable cause) { + super(message, cause); + } + + public FileSystemInitializationFailedException(String message) { + super(message); + } +} diff --git a/src/main/java/org/cryptomator/cryptofs/FilesWrapper.java b/src/main/java/org/cryptomator/cryptofs/FilesWrapper.java deleted file mode 100644 index 05e8fe90..00000000 --- a/src/main/java/org/cryptomator/cryptofs/FilesWrapper.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.cryptomator.cryptofs; - -import javax.inject.Inject; -import javax.inject.Singleton; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.attribute.FileAttribute; - -/** - * Mockable wrapper around {@link Files} operations. - * - * @author Markus Kreusch - */ -@Singleton -class FilesWrapper { - - @Inject - public FilesWrapper() { - } - - public Path createDirectories(Path dir, FileAttribute... attrs) throws IOException { - return Files.createDirectories(dir, attrs); - } - -} diff --git a/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java b/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java index 0ebab05e..e427a2b0 100644 --- a/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java @@ -36,7 +36,8 @@ @CryptoFileSystemScoped public class LongFileNameProvider { - private static final int MAX_FILENAME_BUFFER_SIZE = 10 * 1024; // no sane person gives a file a 10kb long name. + public static final int MAX_FILENAME_BUFFER_SIZE = 10 * 1024; // no sane person gives a file a 10kb long name. + private static final BaseEncoding BASE64 = BaseEncoding.base64Url(); private static final Duration MAX_CACHE_AGE = Duration.ofMinutes(1); diff --git a/src/main/java/org/cryptomator/cryptofs/RootDirectoryInitializer.java b/src/main/java/org/cryptomator/cryptofs/RootDirectoryInitializer.java deleted file mode 100644 index 1b3a69cb..00000000 --- a/src/main/java/org/cryptomator/cryptofs/RootDirectoryInitializer.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.cryptomator.cryptofs; - -import javax.inject.Inject; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.Path; - -@CryptoFileSystemScoped -class RootDirectoryInitializer { - - private final CryptoPathMapper cryptoPathMapper; - private final ReadonlyFlag readonlyFlag; - private final FilesWrapper files; - - @Inject - public RootDirectoryInitializer(CryptoPathMapper cryptoPathMapper, ReadonlyFlag readonlyFlag, FilesWrapper files) { - this.cryptoPathMapper = cryptoPathMapper; - this.readonlyFlag = readonlyFlag; - this.files = files; - } - - public void initialize(CryptoPath cleartextRoot) { - if (readonlyFlag.isSet()) { - return; - } - try { - Path ciphertextRoot = cryptoPathMapper.getCiphertextDir(cleartextRoot).path; - files.createDirectories(ciphertextRoot); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - -} diff --git a/src/main/java/org/cryptomator/cryptofs/VaultConfig.java b/src/main/java/org/cryptomator/cryptofs/VaultConfig.java new file mode 100644 index 00000000..95e63312 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/VaultConfig.java @@ -0,0 +1,207 @@ +package org.cryptomator.cryptofs; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.InvalidClaimException; +import com.auth0.jwt.exceptions.JWTDecodeException; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.exceptions.SignatureVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; +import org.cryptomator.cryptofs.common.Constants; +import org.cryptomator.cryptolib.api.CryptorProvider; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.MasterkeyLoader; +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; + +import java.net.URI; +import java.util.Arrays; +import java.util.UUID; + +/** + * Typesafe representation of vault configuration files. + *

    + * To prevent config tampering, such as downgrade attacks, vault configurations are cryptographically signed using HMAC-256 + * with the vault's 64 byte master key. + *

    + * If the signature could be successfully verified, the configuration can be assumed valid and the masterkey can be assumed + * eligible for the vault. + *

    + * When {@link #load(String, MasterkeyLoader, int) loading} a vault configuration, a key must be provided and the signature is checked. + * It is impossible to create an instance of this class from an existing configuration without signature verification. + */ +public class VaultConfig { + + private static final String JSON_KEY_VAULTVERSION = "format"; + private static final String JSON_KEY_CIPHERCONFIG = "cipherCombo"; + private static final String JSON_KEY_SHORTENING_THRESHOLD = "shorteningThreshold"; + + private final String id; + private final int vaultVersion; + private final CryptorProvider.Scheme cipherCombo; + private final int shorteningThreshold; + + private VaultConfig(DecodedJWT verifiedConfig) { + this.id = verifiedConfig.getId(); + this.vaultVersion = verifiedConfig.getClaim(JSON_KEY_VAULTVERSION).asInt(); + this.cipherCombo = CryptorProvider.Scheme.valueOf(verifiedConfig.getClaim(JSON_KEY_CIPHERCONFIG).asString()); + this.shorteningThreshold = verifiedConfig.getClaim(JSON_KEY_SHORTENING_THRESHOLD).asInt(); + } + + private VaultConfig(VaultConfigBuilder builder) { + this.id = builder.id; + this.vaultVersion = builder.vaultVersion; + this.cipherCombo = builder.cipherCombo; + this.shorteningThreshold = builder.shorteningThreshold; + } + + public String getId() { + return id; + } + + public int getVaultVersion() { + return vaultVersion; + } + + public CryptorProvider.Scheme getCipherCombo() { + return cipherCombo; + } + + public int getShorteningThreshold() { + return shorteningThreshold; + } + + public String toToken(String keyId, byte[] rawKey) { + return JWT.create() // + .withKeyId(keyId) // + .withJWTId(id) // + .withClaim(JSON_KEY_VAULTVERSION, vaultVersion) // + .withClaim(JSON_KEY_CIPHERCONFIG, cipherCombo.name()) // + .withClaim(JSON_KEY_SHORTENING_THRESHOLD, shorteningThreshold) // + .sign(Algorithm.HMAC256(rawKey)); + } + + /** + * Convenience wrapper for {@link #decode(String)} and {@link UnverifiedVaultConfig#verify(byte[], int)} + * + * @param token The token + * @param keyLoader A key loader capable of providing a key for this token + * @param expectedVaultVersion The vault version this token should contain + * @return The decoded configuration + * @throws MasterkeyLoadingFailedException If the key loader was unable to provide a key for this vault configuration + * @throws VaultConfigLoadException When loading the configuration fails + */ + public static VaultConfig load(String token, MasterkeyLoader keyLoader, int expectedVaultVersion) throws MasterkeyLoadingFailedException, VaultConfigLoadException { + var configLoader = decode(token); + byte[] rawKey = new byte[0]; + try (Masterkey key = keyLoader.loadKey(configLoader.getKeyId())) { + rawKey = key.getEncoded(); + return configLoader.verify(rawKey, expectedVaultVersion); + } finally { + Arrays.fill(rawKey, (byte) 0x00); + } + } + + /** + * Decodes a vault configuration stored in JWT format to load it + * + * @param token The token + * @return A loader object that allows loading the configuration (if providing the required key) + * @throws VaultConfigLoadException When parsing the token failed + */ + public static UnverifiedVaultConfig decode(String token) throws VaultConfigLoadException { + try { + return new UnverifiedVaultConfig(JWT.decode(token)); + } catch (JWTDecodeException e) { + throw new VaultConfigLoadException("Failed to parse config: " + token); + } + } + + /** + * Create a new configuration object for a new vault. + * + * @return A new configuration builder + */ + public static VaultConfigBuilder createNew() { + return new VaultConfigBuilder(); + } + + public static class UnverifiedVaultConfig { + + private final DecodedJWT unverifiedConfig; + + private UnverifiedVaultConfig(DecodedJWT unverifiedConfig) { + this.unverifiedConfig = unverifiedConfig; + } + + /** + * @return The ID of the key required to {@link #verify(byte[], int) load} this config + */ + public URI getKeyId() { + return URI.create(unverifiedConfig.getKeyId()); + } + + /** + * @return The unverified vault version (signature not verified) + */ + public int allegedVaultVersion() { + return unverifiedConfig.getClaim(JSON_KEY_VAULTVERSION).asInt(); + } + + /** + * @return The unverified shortening threshold (signature not verified) + */ + public int allegedShorteningThreshold() { + return unverifiedConfig.getClaim(JSON_KEY_SHORTENING_THRESHOLD).asInt(); + } + + /** + * Decodes a vault configuration stored in JWT format. + * + * @param rawKey The key matching the id in {@link #getKeyId()} + * @param expectedVaultVersion The vault version this token should contain + * @return The decoded configuration + * @throws VaultKeyInvalidException If the provided key was invalid + * @throws VaultVersionMismatchException If the token did not match the expected vault version + * @throws VaultConfigLoadException Generic parse error + */ + public VaultConfig verify(byte[] rawKey, int expectedVaultVersion) throws VaultKeyInvalidException, VaultVersionMismatchException, VaultConfigLoadException { + try { + var verifier = JWT.require(Algorithm.HMAC256(rawKey)) // + .withClaim(JSON_KEY_VAULTVERSION, expectedVaultVersion) // + .build(); + var verifiedConfig = verifier.verify(unverifiedConfig); + return new VaultConfig(verifiedConfig); + } catch (SignatureVerificationException e) { + throw new VaultKeyInvalidException(); + } catch (InvalidClaimException e) { + throw new VaultVersionMismatchException("Vault config not for version " + expectedVaultVersion); + } catch (JWTVerificationException e) { + throw new VaultConfigLoadException("Failed to verify vault config: " + unverifiedConfig.getToken()); + } + } + } + + public static class VaultConfigBuilder { + + private final String id = UUID.randomUUID().toString(); + private final int vaultVersion = Constants.VAULT_VERSION; + private CryptorProvider.Scheme cipherCombo; + private int shorteningThreshold; + + public VaultConfigBuilder cipherCombo(CryptorProvider.Scheme cipherCombo) { + this.cipherCombo = cipherCombo; + return this; + } + + public VaultConfigBuilder shorteningThreshold(int shorteningThreshold) { + this.shorteningThreshold = shorteningThreshold; + return this; + } + + public VaultConfig build() { + return new VaultConfig(this); + } + + } + +} diff --git a/src/main/java/org/cryptomator/cryptofs/VaultConfigLoadException.java b/src/main/java/org/cryptomator/cryptofs/VaultConfigLoadException.java new file mode 100644 index 00000000..ac0f6bf8 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/VaultConfigLoadException.java @@ -0,0 +1,12 @@ +package org.cryptomator.cryptofs; + +/** + * Failed to parse or verify vault config token. + */ +public class VaultConfigLoadException extends FileSystemInitializationFailedException { + + public VaultConfigLoadException(String message) { + super(message); + } + +} diff --git a/src/main/java/org/cryptomator/cryptofs/VaultKeyInvalidException.java b/src/main/java/org/cryptomator/cryptofs/VaultKeyInvalidException.java new file mode 100644 index 00000000..ba6396e6 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/VaultKeyInvalidException.java @@ -0,0 +1,12 @@ +package org.cryptomator.cryptofs; + +/** + * An attempt was made to verify the signature of a vault config token using an invalid key. + */ +public class VaultKeyInvalidException extends VaultConfigLoadException { + + public VaultKeyInvalidException() { + super("Failed to verify vault config signature using the provided key."); + } + +} diff --git a/src/main/java/org/cryptomator/cryptofs/VaultVersionMismatchException.java b/src/main/java/org/cryptomator/cryptofs/VaultVersionMismatchException.java new file mode 100644 index 00000000..6c429063 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/VaultVersionMismatchException.java @@ -0,0 +1,9 @@ +package org.cryptomator.cryptofs; + +public class VaultVersionMismatchException extends VaultConfigLoadException { + + public VaultVersionMismatchException(String message) { + super(message); + } + +} diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java b/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java index 6e45aeaf..abd91859 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java @@ -8,13 +8,13 @@ *******************************************************************************/ package org.cryptomator.cryptofs.attr; -import org.cryptomator.cryptofs.common.ArrayUtils; -import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptofs.CryptoPath; import org.cryptomator.cryptofs.CryptoPathMapper; -import org.cryptomator.cryptofs.fh.OpenCryptoFiles; import org.cryptomator.cryptofs.Symlinks; +import org.cryptomator.cryptofs.common.ArrayUtils; +import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptofs.fh.OpenCryptoFile; +import org.cryptomator.cryptofs.fh.OpenCryptoFiles; import java.io.IOException; import java.nio.file.LinkOption; @@ -50,19 +50,19 @@ protected Optional getOpenCryptoFile() throws IOException { private Path getCiphertextPath(CryptoPath path) throws IOException { CiphertextFileType type = pathMapper.getCiphertextFileType(path); - switch (type) { + return switch (type) { case SYMLINK: if (ArrayUtils.contains(linkOptions, LinkOption.NOFOLLOW_LINKS)) { - return pathMapper.getCiphertextFilePath(path).getSymlinkFilePath(); + yield pathMapper.getCiphertextFilePath(path).getSymlinkFilePath(); } else { CryptoPath resolved = symlinks.resolveRecursively(path); - return getCiphertextPath(resolved); + yield getCiphertextPath(resolved); } case DIRECTORY: - return pathMapper.getCiphertextDir(path).path; - default: - return pathMapper.getCiphertextFilePath(path).getFilePath(); - } + yield pathMapper.getCiphertextDir(path).path; + case FILE: + yield pathMapper.getCiphertextFilePath(path).getFilePath(); + }; } } diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AttributeModule.java b/src/main/java/org/cryptomator/cryptofs/attr/AttributeModule.java index 52fc9ca3..46b70c0d 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/AttributeModule.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/AttributeModule.java @@ -28,8 +28,8 @@ public static Optional provideOpenCryptoFile(OpenCryptoFiles ope @Provides @AttributeScoped public static PosixFileAttributes providePosixFileAttributes(BasicFileAttributes ciphertextAttributes) { - if (ciphertextAttributes instanceof PosixFileAttributes) { - return (PosixFileAttributes) ciphertextAttributes; + if (ciphertextAttributes instanceof PosixFileAttributes attr) { + return attr; } else { throw new IllegalStateException("Attempted to inject instance of type " + ciphertextAttributes.getClass() + " but expected PosixFileAttributes."); } @@ -38,8 +38,8 @@ public static PosixFileAttributes providePosixFileAttributes(BasicFileAttributes @Provides @AttributeScoped public static DosFileAttributes provideDosFileAttributes(BasicFileAttributes ciphertextAttributes) { - if (ciphertextAttributes instanceof DosFileAttributes) { - return (DosFileAttributes) ciphertextAttributes; + if (ciphertextAttributes instanceof DosFileAttributes attr) { + return attr; } else { throw new IllegalStateException("Attempted to inject instance of type " + ciphertextAttributes.getClass() + " but expected DosFileAttributes."); } diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java b/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java index 6a7c0db6..d9c445bb 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java @@ -62,16 +62,11 @@ public A readAttributes(CryptoPath cleartextPath } private Path getCiphertextPath(CryptoPath path, CiphertextFileType type) throws IOException { - switch (type) { - case SYMLINK: - return pathMapper.getCiphertextFilePath(path).getSymlinkFilePath(); - case DIRECTORY: - return pathMapper.getCiphertextDir(path).path; - case FILE: - return pathMapper.getCiphertextFilePath(path).getFilePath(); - default: - throw new UnsupportedOperationException("Unhandled node type " + type); - } + return switch (type) { + case SYMLINK -> pathMapper.getCiphertextFilePath(path).getSymlinkFilePath(); + case DIRECTORY -> pathMapper.getCiphertextDir(path).path; + case FILE -> pathMapper.getCiphertextFilePath(path).getFilePath(); + }; } } diff --git a/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributes.java b/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributes.java index 3f84ec1c..252a9a3f 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributes.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributes.java @@ -10,7 +10,6 @@ import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptofs.fh.OpenCryptoFile; -import org.cryptomator.cryptolib.Cryptors; import org.cryptomator.cryptolib.api.Cryptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,17 +36,10 @@ class CryptoBasicFileAttributes implements BasicFileAttributes { @Inject public CryptoBasicFileAttributes(BasicFileAttributes delegate, CiphertextFileType ciphertextFileType, Path ciphertextPath, Cryptor cryptor, Optional openCryptoFile) { this.ciphertextFileType = ciphertextFileType; - switch (ciphertextFileType) { - case SYMLINK: - case DIRECTORY: - this.size = delegate.size(); - break; - case FILE: - this.size = getPlaintextFileSize(ciphertextPath, delegate.size(), openCryptoFile, cryptor); - break; - default: - throw new IllegalArgumentException("Unsupported ciphertext file type: " + ciphertextFileType); - } + this.size = switch (ciphertextFileType) { + case SYMLINK, DIRECTORY -> delegate.size(); + case FILE -> getPlaintextFileSize(ciphertextPath, delegate.size(), openCryptoFile, cryptor); + }; this.lastModifiedTime = openCryptoFile.map(OpenCryptoFile::getLastModifiedTime).orElseGet(delegate::lastModifiedTime); this.lastAccessTime = openCryptoFile.map(openFile -> FileTime.from(Instant.now())).orElseGet(delegate::lastAccessTime); this.creationTime = delegate.creationTime(); @@ -60,7 +52,8 @@ private static long getPlaintextFileSize(Path ciphertextPath, long size, Optiona private static long calculatePlaintextFileSize(Path ciphertextPath, long size, Cryptor cryptor) { try { - return Cryptors.cleartextSize(size - cryptor.fileHeaderCryptor().headerSize(), cryptor); + long payloadSize = size - cryptor.fileHeaderCryptor().headerSize(); + return cryptor.fileContentCryptor().cleartextSize(payloadSize); } catch (IllegalArgumentException e) { LOG.warn("Unable to calculate cleartext file size for {}. Ciphertext size (including header): {}", ciphertextPath, size); return 0l; diff --git a/src/main/java/org/cryptomator/cryptofs/ch/CleartextFileChannel.java b/src/main/java/org/cryptomator/cryptofs/ch/CleartextFileChannel.java index 75abe860..b41b626d 100644 --- a/src/main/java/org/cryptomator/cryptofs/ch/CleartextFileChannel.java +++ b/src/main/java/org/cryptomator/cryptofs/ch/CleartextFileChannel.java @@ -32,7 +32,6 @@ import static java.lang.Math.max; import static java.lang.Math.min; -import static org.cryptomator.cryptolib.Cryptors.ciphertextSize; @ChannelScoped public class CleartextFileChannel extends AbstractFileChannel { @@ -192,7 +191,7 @@ protected void truncateLocked(long newSize) throws IOException { if (sizeOfIncompleteChunk > 0) { chunkCache.get(indexOfLastChunk).truncate(sizeOfIncompleteChunk); } - long ciphertextFileSize = cryptor.fileHeaderCryptor().headerSize() + ciphertextSize(newSize, cryptor); + long ciphertextFileSize = cryptor.fileHeaderCryptor().headerSize() + cryptor.fileContentCryptor().ciphertextSize(newSize); chunkCache.invalidateAll(); // make sure no chunks _after_ newSize exist that would otherwise be written during the next cache eviction ciphertextFileChannel.truncate(ciphertextFileSize); position = min(newSize, position); diff --git a/src/main/java/org/cryptomator/cryptofs/common/Constants.java b/src/main/java/org/cryptomator/cryptofs/common/Constants.java index 0ad683f8..c9085c44 100644 --- a/src/main/java/org/cryptomator/cryptofs/common/Constants.java +++ b/src/main/java/org/cryptomator/cryptofs/common/Constants.java @@ -10,7 +10,10 @@ public final class Constants { - public static final int VAULT_VERSION = 7; + private Constants() { + } + + public static final int VAULT_VERSION = 8; public static final String MASTERKEY_BACKUP_SUFFIX = ".bkup"; public static final String DATA_DIR_NAME = "d"; public static final String ROOT_DIR_ID = ""; @@ -21,14 +24,9 @@ public final class Constants { public static final String CONTENTS_FILE_NAME = "contents.c9r"; public static final String INFLATED_FILE_NAME = "name.c9s"; - public static final int MAX_CIPHERTEXT_NAME_LENGTH = 220; // inclusive. calculations done in https://github.com/cryptomator/cryptofs/issues/60#issuecomment-523238303 - public static final int MIN_CIPHERTEXT_NAME_LENGTH = 28; // base64(iv).c9r - public static final int MAX_CLEARTEXT_NAME_LENGTH = 146; // inclusive. calculations done in https://github.com/cryptomator/cryptofs/issues/60#issuecomment-523238303 - public static final int MAX_ADDITIONAL_PATH_LENGTH = 48; // beginning at d/... see https://github.com/cryptomator/cryptofs/issues/77 - public static final int MAX_CIPHERTEXT_PATH_LENGTH = MAX_CIPHERTEXT_NAME_LENGTH + MAX_ADDITIONAL_PATH_LENGTH; + public static final int DEFAULT_SHORTENING_THRESHOLD = 220; public static final int MAX_SYMLINK_LENGTH = 32767; // max path length on NTFS and FAT32: 32k-1 public static final int MAX_DIR_FILE_LENGTH = 36; // UUIDv4: hex-encoded 16 byte int + 4 hyphens = 36 ASCII chars public static final String SEPARATOR = "/"; - } diff --git a/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java b/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java index 30ff3f6c..f6162bfd 100644 --- a/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java +++ b/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java @@ -7,6 +7,8 @@ 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; @@ -15,9 +17,13 @@ import java.nio.file.Files; import java.nio.file.Path; +@Singleton public 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 { /** @@ -35,6 +41,10 @@ public enum Capability { WRITE_ACCESS, } + @Inject + public FileSystemCapabilityChecker() { + } + /** * Checks whether the underlying filesystem has all required capabilities. * @@ -83,16 +93,24 @@ public void assertWriteAccess(Path pathToVault) throws MissingCapabilityExceptio } } + public int determineSupportedCleartextFileNameLength(Path pathToVault) throws IOException { + int maxCiphertextLen = determineSupportedCiphertextFileNameLength(pathToVault); + assert maxCiphertextLen >= MIN_CIPHERTEXT_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; + } + /** * Determinse the number of chars a ciphertext filename (including its extension) is allowed to have inside a vault's d/XX/YYYYYYYYYYYYYYYYYYYYYYYYYYYYYY/ directory. - * + * * @param pathToVault Path to the vault * @return Number of chars a .c9r file is allowed to have * @throws IOException If unable to perform this check */ - public int determineSupportedFileNameLength(Path pathToVault) throws IOException { - int subPathLength = Constants.MAX_ADDITIONAL_PATH_LENGTH - 2; // subtract "c/" - return determineSupportedFileNameLength(pathToVault.resolve("c"), subPathLength, Constants.MIN_CIPHERTEXT_NAME_LENGTH, Constants.MAX_CIPHERTEXT_NAME_LENGTH); + 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); } /** @@ -105,7 +123,7 @@ public int determineSupportedFileNameLength(Path pathToVault) throws IOException * @return The supported filename length inside a subdirectory of dir with subPathLength chars * @throws IOException If unable to perform this check */ - public int determineSupportedFileNameLength(Path dir, int subPathLength, int minFileNameLength, int maxFileNameLength) throws IOException { + public 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); @@ -120,13 +138,13 @@ public int determineSupportedFileNameLength(Path dir, int subPathLength, int min throw new IOException("Unable to read dir"); } // perform actual check: - return determineSupportedFileNameLength(fillerDir, minFileNameLength, maxFileNameLength + 1); + return determineSupportedCiphertextFileNameLength(fillerDir, minFileNameLength, maxFileNameLength + 1); } finally { deleteRecursivelySilently(fillerDir); } } - private int determineSupportedFileNameLength(Path p, int lowerBoundIncl, int upperBoundExcl) { + private int determineSupportedCiphertextFileNameLength(Path p, int lowerBoundIncl, int upperBoundExcl) { assert lowerBoundIncl < upperBoundExcl; int mid = (lowerBoundIncl + upperBoundExcl) / 2; assert mid < upperBoundExcl; @@ -135,9 +153,9 @@ private int determineSupportedFileNameLength(Path p, int lowerBoundIncl, int upp } assert lowerBoundIncl < mid; if (canHandleFileNameLength(p, mid)) { - return determineSupportedFileNameLength(p, mid, upperBoundExcl); + return determineSupportedCiphertextFileNameLength(p, mid, upperBoundExcl); } else { - return determineSupportedFileNameLength(p, lowerBoundIncl, mid); + return determineSupportedCiphertextFileNameLength(p, lowerBoundIncl, mid); } } diff --git a/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java b/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java index dc34068d..33570fd1 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java @@ -4,6 +4,7 @@ import com.google.common.io.BaseEncoding; import com.google.common.io.MoreFiles; import com.google.common.io.RecursiveDeleteOption; +import org.cryptomator.cryptofs.VaultConfig; import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptolib.api.Cryptor; import org.slf4j.Logger; @@ -23,8 +24,6 @@ import java.util.stream.Stream; import static org.cryptomator.cryptofs.common.Constants.DIR_FILE_NAME; -import static org.cryptomator.cryptofs.common.Constants.MAX_CIPHERTEXT_NAME_LENGTH; -import static org.cryptomator.cryptofs.common.Constants.MAX_CLEARTEXT_NAME_LENGTH; import static org.cryptomator.cryptofs.common.Constants.MAX_DIR_FILE_LENGTH; import static org.cryptomator.cryptofs.common.Constants.MAX_SYMLINK_LENGTH; import static org.cryptomator.cryptofs.common.Constants.SYMLINK_FILE_NAME; @@ -36,11 +35,15 @@ class C9rConflictResolver { private final Cryptor cryptor; private final byte[] dirId; + private final int maxC9rFileNameLength; + private final int maxCleartextFileNameLength; @Inject - public C9rConflictResolver(Cryptor cryptor, @Named("dirId") String dirId) { + public C9rConflictResolver(Cryptor cryptor, @Named("dirId") String dirId, VaultConfig vaultConfig) { this.cryptor = cryptor; this.dirId = dirId.getBytes(StandardCharsets.US_ASCII); + this.maxC9rFileNameLength = vaultConfig.getShorteningThreshold(); + this.maxCleartextFileNameLength = (maxC9rFileNameLength - 4) / 4 * 3 - 16; // math from FileSystemCapabilityChecker.determineSupportedCleartextFileNameLength() } public Stream process(Node node) { @@ -93,7 +96,7 @@ private Node renameConflictingFile(Path canonicalPath, Path conflictingPath, Str final int beginOfFileExtension = cleartext.lastIndexOf('.'); final String fileExtension = (beginOfFileExtension > 0) ? cleartext.substring(beginOfFileExtension) : ""; final String basename = (beginOfFileExtension > 0) ? cleartext.substring(0, beginOfFileExtension) : cleartext; - final String lengthRestrictedBasename = basename.substring(0, Math.min(basename.length(), MAX_CLEARTEXT_NAME_LENGTH - fileExtension.length() - 5)); // 5 chars for conflict suffix " (42)" + final String lengthRestrictedBasename = basename.substring(0, Math.min(basename.length(), maxCleartextFileNameLength - fileExtension.length() - 5)); // 5 chars for conflict suffix " (42)" String alternativeCleartext; String alternativeCiphertext; String alternativeCiphertextName; @@ -105,7 +108,7 @@ private Node renameConflictingFile(Path canonicalPath, Path conflictingPath, Str alternativeCiphertextName = alternativeCiphertext + Constants.CRYPTOMATOR_FILE_SUFFIX; alternativePath = canonicalPath.resolveSibling(alternativeCiphertextName); } while (Files.exists(alternativePath)); - assert alternativeCiphertextName.length() <= MAX_CIPHERTEXT_NAME_LENGTH; + assert alternativeCiphertextName.length() <= maxC9rFileNameLength; LOG.info("Moving conflicting file {} to {}", conflictingPath, alternativePath); Files.move(conflictingPath, alternativePath, StandardCopyOption.ATOMIC_MOVE); Node node = new Node(alternativePath); diff --git a/src/main/java/org/cryptomator/cryptofs/dir/CiphertextDirectoryDeleter.java b/src/main/java/org/cryptomator/cryptofs/dir/CiphertextDirectoryDeleter.java index 1efdcc4f..acb1adeb 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/CiphertextDirectoryDeleter.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/CiphertextDirectoryDeleter.java @@ -45,13 +45,8 @@ public void deleteCiphertextDirIncludingNonCiphertextFiles(Path ciphertextDir, C * case 2 is true. */ switch (deleteNonCiphertextFiles(ciphertextDir, cleartextDir)) { - case NO_FILES_DELETED: - throw e; - case SOME_FILES_DELETED: - Files.delete(ciphertextDir); - break; - default: - throw new IllegalStateException("Unexpected enum constant"); + case NO_FILES_DELETED -> throw e; + case SOME_FILES_DELETED-> Files.delete(ciphertextDir); } } } diff --git a/src/main/java/org/cryptomator/cryptofs/fh/ChunkCache.java b/src/main/java/org/cryptomator/cryptofs/fh/ChunkCache.java index 67f6419e..2963af85 100644 --- a/src/main/java/org/cryptomator/cryptofs/fh/ChunkCache.java +++ b/src/main/java/org/cryptomator/cryptofs/fh/ChunkCache.java @@ -1,5 +1,7 @@ package org.cryptomator.cryptofs.fh; +import com.google.common.base.Throwables; +import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; @@ -21,7 +23,7 @@ public class ChunkCache { private final ChunkLoader chunkLoader; private final ChunkSaver chunkSaver; private final CryptoFileSystemStats stats; - private final LoadingCache chunks; + private final Cache chunks; @Inject public ChunkCache(ChunkLoader chunkLoader, ChunkSaver chunkSaver, CryptoFileSystemStats stats) { @@ -31,15 +33,16 @@ public ChunkCache(ChunkLoader chunkLoader, ChunkSaver chunkSaver, CryptoFileSyst this.chunks = CacheBuilder.newBuilder() // .maximumSize(MAX_CACHED_CLEARTEXT_CHUNKS) // .removalListener(this::removeChunk) // - .build(CacheLoader.from(this::loadChunk)); + .build(); } - private ChunkData loadChunk(Long chunkIndex) { + private ChunkData loadChunk(long chunkIndex) throws IOException { + stats.addChunkCacheMiss(); try { - stats.addChunkCacheMiss(); return chunkLoader.load(chunkIndex); - } catch (IOException e) { - throw new UncheckedIOException(e); + } catch (AuthenticationFailedException e) { + // TODO provide means to pass an AuthenticationFailedException handler using an OpenOption + throw new IOException("Unauthentic ciphertext in chunk " + chunkIndex, e); } } @@ -54,20 +57,10 @@ private void removeChunk(RemovalNotification removal) { public ChunkData get(long chunkIndex) throws IOException { try { stats.addChunkCacheAccess(); - return chunks.get(chunkIndex); + return chunks.get(chunkIndex, () -> loadChunk(chunkIndex)); } catch (ExecutionException e) { - assert e.getCause() != null; // no exception in ChunkLoader -> no executionException during chunk loading ;-) + assert e.getCause() instanceof IOException; // the only checked exception thrown by #loadChunk(long) throw (IOException) e.getCause(); - } catch (UncheckedExecutionException e) { - if (e.getCause() instanceof UncheckedIOException) { - UncheckedIOException uioe = (UncheckedIOException) e.getCause(); - throw uioe.getCause(); - } else if (e.getCause() instanceof AuthenticationFailedException) { - // TODO provide means to pass an AuthenticationFailedException handler using an OpenOption - throw new IOException(e.getCause()); - } else { - throw e; - } } } diff --git a/src/main/java/org/cryptomator/cryptofs/fh/ChunkLoader.java b/src/main/java/org/cryptomator/cryptofs/fh/ChunkLoader.java index fb7e9157..278fa68b 100644 --- a/src/main/java/org/cryptomator/cryptofs/fh/ChunkLoader.java +++ b/src/main/java/org/cryptomator/cryptofs/fh/ChunkLoader.java @@ -1,6 +1,7 @@ package org.cryptomator.cryptofs.fh; import org.cryptomator.cryptofs.CryptoFileSystemStats; +import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; import javax.inject.Inject; @@ -23,7 +24,7 @@ public ChunkLoader(Cryptor cryptor, ChunkIO ciphertext, FileHeaderHolder headerH this.stats = stats; } - public ChunkData load(Long chunkIndex) throws IOException { + public ChunkData load(Long chunkIndex) throws IOException, AuthenticationFailedException { stats.addChunkCacheMiss(); int payloadSize = cryptor.fileContentCryptor().cleartextChunkSize(); int chunkSize = cryptor.fileContentCryptor().ciphertextChunkSize(); diff --git a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java index d9352fa6..98b22756 100644 --- a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java +++ b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java @@ -11,7 +11,6 @@ import org.cryptomator.cryptofs.EffectiveOpenOptions; import org.cryptomator.cryptofs.ch.ChannelComponent; import org.cryptomator.cryptofs.ch.CleartextFileChannel; -import org.cryptomator.cryptolib.Cryptors; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileHeader; import org.slf4j.Logger; @@ -135,7 +134,8 @@ private void initFileSize(FileChannel ciphertextFileChannel) throws IOException try { long ciphertextSize = ciphertextFileChannel.size(); if (ciphertextSize > 0l) { - cleartextSize = Cryptors.cleartextSize(ciphertextSize - cryptor.fileHeaderCryptor().headerSize(), cryptor); + long payloadSize = ciphertextSize - cryptor.fileHeaderCryptor().headerSize(); + cleartextSize = cryptor.fileContentCryptor().cleartextSize(payloadSize); } } catch (IllegalArgumentException e) { LOG.warn("Invalid cipher text file size. Assuming empty file.", e); diff --git a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFileModule.java b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFileModule.java index 281d8e7f..81cac714 100644 --- a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFileModule.java +++ b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFileModule.java @@ -18,8 +18,6 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Supplier; -import static org.cryptomator.cryptolib.Cryptors.cleartextSize; - @Module public class OpenCryptoFileModule { diff --git a/src/main/java/org/cryptomator/cryptofs/migration/Migration.java b/src/main/java/org/cryptomator/cryptofs/migration/Migration.java index c214ca2d..3180e37e 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/Migration.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/Migration.java @@ -17,9 +17,14 @@ enum Migration { FIVE_TO_SIX(5), /** - * Migrates vault format 5 to 6. + * Migrates vault format 6 to 7. + */ + SIX_TO_SEVEN(6), + + /** + * Migrates vault format 7 to 8 */ - SIX_TO_SEVEN(6); + SEVEN_TO_EIGHT(7); private final int applicableVersion; diff --git a/src/main/java/org/cryptomator/cryptofs/migration/MigrationComponent.java b/src/main/java/org/cryptomator/cryptofs/migration/MigrationComponent.java index 35165dde..ce4126fb 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/MigrationComponent.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/MigrationComponent.java @@ -5,11 +5,23 @@ *******************************************************************************/ package org.cryptomator.cryptofs.migration; +import dagger.BindsInstance; import dagger.Component; +import java.security.SecureRandom; + @Component(modules = {MigrationModule.class}) interface MigrationComponent { Migrators migrators(); + @Component.Builder + interface Builder { + + @BindsInstance + Builder csprng(SecureRandom csprng); + + MigrationComponent build(); + } + } diff --git a/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java b/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java index 0d700add..fcc9ba7e 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java @@ -5,10 +5,6 @@ *******************************************************************************/ package org.cryptomator.cryptofs.migration; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - import dagger.MapKey; import dagger.Module; import dagger.Provides; @@ -17,7 +13,11 @@ import org.cryptomator.cryptofs.migration.api.Migrator; import org.cryptomator.cryptofs.migration.v6.Version6Migrator; import org.cryptomator.cryptofs.migration.v7.Version7Migrator; -import org.cryptomator.cryptolib.api.CryptorProvider; +import org.cryptomator.cryptofs.migration.v8.Version8Migrator; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; @@ -25,17 +25,6 @@ @Module class MigrationModule { - private final CryptorProvider version1Cryptor; - - MigrationModule(CryptorProvider version1Cryptor) { - this.version1Cryptor = version1Cryptor; - } - - @Provides - CryptorProvider provideVersion1CryptorProvider() { - return version1Cryptor; - } - @Provides FileSystemCapabilityChecker provideFileSystemCapabilityChecker() { return new FileSystemCapabilityChecker(); @@ -55,6 +44,13 @@ Migrator provideVersion7Migrator(Version7Migrator migrator) { return migrator; } + @Provides + @IntoMap + @MigratorKey(Migration.SEVEN_TO_EIGHT) + Migrator provideVersion8Migrator(Version8Migrator migrator) { + return migrator; + } + @Documented @Target(METHOD) @Retention(RUNTIME) diff --git a/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java index a0456fb0..09a43b95 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java @@ -5,47 +5,45 @@ *******************************************************************************/ package org.cryptomator.cryptofs.migration; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.Map; -import java.util.Optional; - -import javax.inject.Inject; - +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; import org.cryptomator.cryptofs.migration.api.NoApplicableMigratorException; -import org.cryptomator.cryptolib.Cryptors; +import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.InvalidPassphraseException; -import org.cryptomator.cryptolib.api.KeyFile; import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Map; +import java.util.Optional; /** * Used to perform migration from an older vault format to a newer one. *

    * Example Usage: - * + * *

      * 
    - * if (Migrators.get().{@link #needsMigration(Path, String) needsMigration(pathToVault, masterkeyFileName)}) {
    - * 	Migrators.get().{@link #migrate(Path, String, CharSequence, MigrationProgressListener, MigrationContinuationListener) migrate(pathToVault, masterkeyFileName, passphrase, progressListener, continuationListener)};
    + * if (Migrators.get().{@link #needsMigration(Path, String, String)} needsMigration(pathToVault, vaultConfigFilename, masterkeyFileName)}) {
    + * 	Migrators.get().{@link #migrate(Path, String, String, CharSequence, MigrationProgressListener, MigrationContinuationListener) migrate(pathToVault, masterkeyFileName, passphrase, progressListener, continuationListener)};
      * }
      * 
      * 
    - * + * * @since 1.4.0 */ public class Migrators { - private static final MigrationComponent COMPONENT = DaggerMigrationComponent.builder() // - .migrationModule(new MigrationModule(Cryptors.version1(strongSecureRandom()))) // - .build(); + private static final MigrationComponent COMPONENT = DaggerMigrationComponent.builder().csprng(strongSecureRandom()).build(); private final Map migrators; private final FileSystemCapabilityChecker fsCapabilityChecker; @@ -70,52 +68,57 @@ 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 masterkeyFilename Name of the masterkey file located in the vault + * + * @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 * @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 */ - public boolean needsMigration(Path pathToVault, String masterkeyFilename) throws IOException { - Path masterKeyPath = pathToVault.resolve(masterkeyFilename); - byte[] keyFileContents = Files.readAllBytes(masterKeyPath); - try { - KeyFile keyFile = KeyFile.parse(keyFileContents); - return keyFile.getVersion() < Constants.VAULT_VERSION; - } catch (IllegalArgumentException e) { - throw new IOException("Malformed masterkey file " + masterKeyPath, e); - } + public boolean needsMigration(Path pathToVault, String vaultConfigFilename, String masterkeyFilename) throws IOException { + int vaultVersion = determineVaultVersion(pathToVault, vaultConfigFilename, masterkeyFilename); + return vaultVersion < Constants.VAULT_VERSION; } /** * Performs the actual migration. This task may take a while and this method will block. - * - * @param pathToVault Path to the vault's root - * @param masterkeyFilename Name of the masterkey file located in the vault - * @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 masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws NoApplicableMigratorException, InvalidPassphraseException, IOException { + public void migrate(Path pathToVault, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws NoApplicableMigratorException, CryptoException, IOException { fsCapabilityChecker.assertAllCapabilities(pathToVault); - - Path masterKeyPath = pathToVault.resolve(masterkeyFilename); - byte[] keyFileContents = Files.readAllBytes(masterKeyPath); - KeyFile keyFile = KeyFile.parse(keyFileContents); - + int vaultVersion = determineVaultVersion(pathToVault, vaultConfigFilename, masterkeyFilename); try { - Migrator migrator = findApplicableMigrator(keyFile.getVersion()).orElseThrow(NoApplicableMigratorException::new); - migrator.migrate(pathToVault, masterkeyFilename, passphrase, progressListener, continuationListener); + Migrator migrator = findApplicableMigrator(vaultVersion).orElseThrow(NoApplicableMigratorException::new); + migrator.migrate(pathToVault, vaultConfigFilename, masterkeyFilename, passphrase, progressListener, continuationListener); } catch (UnsupportedVaultFormatException e) { // might be a tampered masterkey file, as this exception is also thrown if the vault version MAC is not authentic. throw new IllegalStateException("Vault version checked beforehand but not supported by migrator."); } } + private int determineVaultVersion(Path pathToVault, String vaultConfigFilename, String masterkeyFilename) throws IOException { + Path vaultConfigPath = pathToVault.resolve(vaultConfigFilename); + Path masterKeyPath = pathToVault.resolve(masterkeyFilename); + if (Files.exists(vaultConfigPath)) { + var jwt = Files.readString(vaultConfigPath); + return VaultConfig.decode(jwt).allegedVaultVersion(); + } else if (Files.exists(masterKeyPath)) { + return MasterkeyFileAccess.readAllegedVaultVersion(Files.readAllBytes(masterKeyPath)); + } else { + throw new IOException("Did not find " + vaultConfigFilename + " nor " + masterkeyFilename); + } + } + private Optional findApplicableMigrator(int version) { return migrators.entrySet().stream().filter(entry -> entry.getKey().isApplicable(version)).map(Map.Entry::getValue).findAny(); } diff --git a/src/main/java/org/cryptomator/cryptofs/migration/api/MigrationContinuationListener.java b/src/main/java/org/cryptomator/cryptofs/migration/api/MigrationContinuationListener.java index a531565f..4f08321e 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/api/MigrationContinuationListener.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/api/MigrationContinuationListener.java @@ -3,6 +3,8 @@ @FunctionalInterface public interface MigrationContinuationListener { + MigrationContinuationListener CANCEL_ALWAYS = event -> ContinuationResult.CANCEL; + /** * Invoked when the migration requires action. *

    diff --git a/src/main/java/org/cryptomator/cryptofs/migration/api/MigrationProgressListener.java b/src/main/java/org/cryptomator/cryptofs/migration/api/MigrationProgressListener.java index 9fe9ef68..c8cc82e9 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/api/MigrationProgressListener.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/api/MigrationProgressListener.java @@ -3,6 +3,8 @@ @FunctionalInterface public interface MigrationProgressListener { + MigrationProgressListener IGNORE = (state, progress) -> {}; + /** * Called on every step during migration that might change the progress. * diff --git a/src/main/java/org/cryptomator/cryptofs/migration/api/Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/api/Migrator.java index 6973e8ac..e933234c 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/api/Migrator.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/api/Migrator.java @@ -8,6 +8,7 @@ import java.io.IOException; import java.nio.file.Path; +import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; @@ -20,43 +21,49 @@ public interface Migrator { * Performs the migration this migrator is built for. * * @param vaultRoot + * @param vaultConfigFilename * @param masterkeyFilename * @param passphrase * @throws InvalidPassphraseException * @throws UnsupportedVaultFormatException + * @throws CryptoException * @throws IOException */ - default void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passphrase) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException { - migrate(vaultRoot, masterkeyFilename, passphrase, (state, progress) -> {}); + default void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase) throws InvalidPassphraseException, UnsupportedVaultFormatException, CryptoException, IOException { + migrate(vaultRoot, vaultConfigFilename, masterkeyFilename, passphrase, (state, progress) -> {}); } /** * Performs the migration this migrator is built for. * * @param vaultRoot + * @param vaultConfigFilename * @param masterkeyFilename * @param passphrase * @param progressListener * @throws InvalidPassphraseException * @throws UnsupportedVaultFormatException + * @throws CryptoException * @throws IOException */ - default void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException { - migrate(vaultRoot, masterkeyFilename, passphrase, progressListener, (event) -> MigrationContinuationListener.ContinuationResult.CANCEL); + default void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, CryptoException, IOException { + migrate(vaultRoot, vaultConfigFilename, masterkeyFilename, passphrase, progressListener, (event) -> MigrationContinuationListener.ContinuationResult.CANCEL); } /** * Performs the migration this migrator is built for. * * @param vaultRoot + * @param vaultConfigFilename * @param masterkeyFilename * @param passphrase * @param progressListener * @param continuationListener * @throws InvalidPassphraseException * @throws UnsupportedVaultFormatException + * @throws CryptoException * @throws IOException */ - void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException; + void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, CryptoException, IOException; } diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java index 8c39a1c0..e07e6bc8 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java @@ -5,46 +5,48 @@ *******************************************************************************/ package org.cryptomator.cryptofs.migration.v6; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.text.Normalizer; -import java.text.Normalizer.Form; - -import javax.inject.Inject; - import org.cryptomator.cryptofs.common.MasterkeyBackupHelper; import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener; import org.cryptomator.cryptofs.migration.api.MigrationProgressListener; import org.cryptomator.cryptofs.migration.api.Migrator; -import org.cryptomator.cryptolib.api.Cryptor; -import org.cryptomator.cryptolib.api.CryptorProvider; -import org.cryptomator.cryptolib.api.InvalidPassphraseException; -import org.cryptomator.cryptolib.api.KeyFile; -import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; +import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.SecureRandom; +import java.text.Normalizer; +import java.text.Normalizer.Form; + +/** + * Updates masterkey.cryptomator: + * + * Version 6 encodes the passphrase in Unicode NFC. + */ public class Version6Migrator implements Migrator { private static final Logger LOG = LoggerFactory.getLogger(Version6Migrator.class); - private final CryptorProvider cryptorProvider; + private final SecureRandom csprng; @Inject - public Version6Migrator(CryptorProvider cryptorProvider) { - this.cryptorProvider = cryptorProvider; + public Version6Migrator(SecureRandom csprng) { + this.csprng = csprng; } @Override - public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException { + public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws CryptoException, IOException { LOG.info("Upgrading {} from version 5 to version 6.", vaultRoot); progressListener.update(MigrationProgressListener.ProgressState.INITIALIZING, 0.0); Path masterkeyFile = vaultRoot.resolve(masterkeyFilename); byte[] fileContentsBeforeUpgrade = Files.readAllBytes(masterkeyFile); - KeyFile keyFile = KeyFile.parse(fileContentsBeforeUpgrade); - try (Cryptor cryptor = cryptorProvider.createFromKeyFile(keyFile, passphrase, 5)) { + MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(new byte[0], csprng); + try (Masterkey masterkey = masterkeyFileAccess.load(masterkeyFile, passphrase)) { // create backup, as soon as we know the password was correct: Path masterkeyBackupFile = MasterkeyBackupHelper.attemptMasterKeyBackup(masterkeyFile); LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName()); @@ -52,8 +54,7 @@ public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passp progressListener.update(MigrationProgressListener.ProgressState.FINALIZING, 0.0); // rewrite masterkey file with normalized passphrase: - byte[] fileContentsAfterUpgrade = cryptor.writeKeysToMasterkeyFile(Normalizer.normalize(passphrase, Form.NFC), 6).serialize(); - Files.write(masterkeyFile, fileContentsAfterUpgrade, StandardOpenOption.TRUNCATE_EXISTING); + masterkeyFileAccess.persist(masterkey, masterkeyFile, Normalizer.normalize(passphrase, Form.NFC), 6); LOG.info("Updated masterkey."); } LOG.info("Upgraded {} from version 5 to version 6.", vaultRoot); 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 b4f5bc5b..e61c3cab 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java @@ -14,11 +14,9 @@ import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener.ContinuationResult; import org.cryptomator.cryptofs.migration.api.MigrationProgressListener; import org.cryptomator.cryptofs.migration.api.Migrator; -import org.cryptomator.cryptolib.api.Cryptor; -import org.cryptomator.cryptolib.api.CryptorProvider; -import org.cryptomator.cryptolib.api.InvalidPassphraseException; -import org.cryptomator.cryptolib.api.KeyFile; -import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; +import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,34 +25,48 @@ import java.nio.file.FileVisitOption; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardOpenOption; +import java.security.SecureRandom; import java.util.EnumSet; +/** + * Renames ciphertext names: + * + *

      + *
    • Files: BASE32== → base64==.c9r
    • + *
    • Dirs: 0BASE32== → base64==.c9r/dir.c9r
    • + *
    • Symlinks: 1SBASE32== → base64.c9r/symlink.c9r
    • + *
    + *

    + * Shortened names: + *

      + *
    • shortened.lng → shortened.c9s
    • + *
    • m/shortened.lng → shortened.c9s/contents.c9r
    • + *
    + */ public class Version7Migrator implements Migrator { private static final Logger LOG = LoggerFactory.getLogger(Version7Migrator.class); - private final CryptorProvider cryptorProvider; + private final SecureRandom csprng; @Inject - public Version7Migrator(CryptorProvider cryptorProvider) { - this.cryptorProvider = cryptorProvider; + public Version7Migrator(SecureRandom csprng) { + this.csprng = csprng; } @Override - public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException { + public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws CryptoException, IOException { LOG.info("Upgrading {} from version 6 to version 7.", vaultRoot); progressListener.update(MigrationProgressListener.ProgressState.INITIALIZING, 0.0); Path masterkeyFile = vaultRoot.resolve(masterkeyFilename); - byte[] fileContentsBeforeUpgrade = Files.readAllBytes(masterkeyFile); - KeyFile keyFile = KeyFile.parse(fileContentsBeforeUpgrade); - try (Cryptor cryptor = cryptorProvider.createFromKeyFile(keyFile, passphrase, 6)) { + MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(new byte[0], csprng); + try (Masterkey masterkey = masterkeyFileAccess.load(masterkeyFile, passphrase)) { // create backup, as soon as we know the password was correct: Path masterkeyBackupFile = MasterkeyBackupHelper.attemptMasterKeyBackup(masterkeyFile); LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName()); - + // check file system capabilities: - int filenameLengthLimit = new FileSystemCapabilityChecker().determineSupportedFileNameLength(vaultRoot.resolve("c"), 46, 28, 220); + int filenameLengthLimit = new FileSystemCapabilityChecker().determineSupportedCiphertextFileNameLength(vaultRoot.resolve("c"), 46, 28, 220); int pathLengthLimit = filenameLengthLimit + 48; // TODO PreMigrationVisitor preMigrationVisitor; if (filenameLengthLimit >= 220) { @@ -82,13 +94,13 @@ public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passp // fail if ciphertext paths are too long: if (preMigrationVisitor.getMaxCiphertextPathLength() > pathLengthLimit) { LOG.error("Migration aborted due to unsupported path length (required {}) of underlying file system (supports {}). Vault is unchanged.", preMigrationVisitor.getMaxCiphertextPathLength(), pathLengthLimit); - throw new FileNameTooLongException(preMigrationVisitor.getLongestPath().toString(), pathLengthLimit, filenameLengthLimit); + throw new FileNameTooLongException(preMigrationVisitor.getLongestPath().toString(), filenameLengthLimit); } // fail if ciphertext names are too long: if (preMigrationVisitor.getMaxCiphertextNameLength() > filenameLengthLimit) { LOG.error("Migration aborted due to unsupported filename length (required {}) of underlying file system (supports {}). Vault is unchanged.", preMigrationVisitor.getMaxCiphertextNameLength(), filenameLengthLimit); - throw new FileNameTooLongException(preMigrationVisitor.getPathWithLongestName().toString(), pathLengthLimit, filenameLengthLimit); + throw new FileNameTooLongException(preMigrationVisitor.getPathWithLongestName().toString(), filenameLengthLimit); } // start migration: @@ -103,8 +115,7 @@ public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passp Files.walkFileTree(vaultRoot.resolve("m"), DeletingFileVisitor.INSTANCE); // rewrite masterkey file with normalized passphrase: - byte[] fileContentsAfterUpgrade = cryptor.writeKeysToMasterkeyFile(passphrase, 7).serialize(); - Files.write(masterkeyFile, fileContentsAfterUpgrade, StandardOpenOption.TRUNCATE_EXISTING); + masterkeyFileAccess.persist(masterkey, masterkeyFile, passphrase, 7); LOG.info("Updated masterkey."); } LOG.info("Upgraded {} from version 6 to version 7.", vaultRoot); diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java new file mode 100644 index 00000000..afa56b10 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the accompanying LICENSE file. + *******************************************************************************/ +package org.cryptomator.cryptofs.migration.v8; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import org.cryptomator.cryptofs.common.MasterkeyBackupHelper; +import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener; +import org.cryptomator.cryptofs.migration.api.MigrationProgressListener; +import org.cryptomator.cryptofs.migration.api.Migrator; +import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.UUID; + +/** + * Splits up masterkey.cryptomator: + * + *
      + *
    • vault.cryptomator contains vault version and vault-specific metadata
    • + *
    • masterkey.cryptomator contains KDF params and may become obsolete when other key sources are supported
    • + *
    + */ +public class Version8Migrator implements Migrator { + + private static final Logger LOG = LoggerFactory.getLogger(Version8Migrator.class); + + private final SecureRandom csprng; + + @Inject + public Version8Migrator(SecureRandom csprng) { + this.csprng = csprng; + } + + @Override + public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws CryptoException, IOException { + LOG.info("Upgrading {} from version 7 to version 8.", vaultRoot); + progressListener.update(MigrationProgressListener.ProgressState.INITIALIZING, 0.0); + Path masterkeyFile = vaultRoot.resolve(masterkeyFilename); + Path vaultConfigFile = vaultRoot.resolve(vaultConfigFilename); + byte[] rawKey = new byte[0]; + MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(new byte[0], csprng); + try (Masterkey masterkey = masterkeyFileAccess.load(masterkeyFile, passphrase)) { + // create backup, as soon as we know the password was correct: + Path masterkeyBackupFile = MasterkeyBackupHelper.attemptMasterKeyBackup(masterkeyFile); + LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName()); + + // create vaultconfig.cryptomator + rawKey = masterkey.getEncoded(); + Algorithm algorithm = Algorithm.HMAC256(rawKey); + var config = JWT.create() // + .withJWTId(UUID.randomUUID().toString()) // + .withKeyId("masterkeyfile:masterkey.cryptomator") // + .withClaim("format", 8) // + .withClaim("cipherCombo", "SIV_CTRMAC") // + .withClaim("shorteningThreshold", 220) // + .sign(algorithm); + Files.writeString(vaultConfigFile, config, StandardCharsets.US_ASCII, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW); + LOG.info("Wrote vault config to {}.", vaultConfigFile); + + progressListener.update(MigrationProgressListener.ProgressState.FINALIZING, 0.0); + + // rewrite masterkey file with normalized passphrase: + masterkeyFileAccess.persist(masterkey, masterkeyFile, passphrase, 999); + LOG.info("Updated masterkey."); + } finally { + Arrays.fill(rawKey, (byte) 0x00); + } + LOG.info("Upgraded {} from version 7 to version 8.", vaultRoot); + } + +} diff --git a/src/test/java/org/cryptomator/cryptofs/CopyOperationTest.java b/src/test/java/org/cryptomator/cryptofs/CopyOperationTest.java index 283149e4..bd0c71aa 100644 --- a/src/test/java/org/cryptomator/cryptofs/CopyOperationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CopyOperationTest.java @@ -25,6 +25,7 @@ import static org.cryptomator.cryptofs.util.ByteBuffers.repeat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; @@ -54,7 +55,7 @@ public void setup() { public void testCopyWithEqualPathDoesNothing() throws IOException { inTest.copy(aPathFromFsA, aPathFromFsA); - verifyZeroInteractions(aPathFromFsA); + verifyNoInteractions(aPathFromFsA); } @Test diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java index 538d488f..3b12c4c0 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java @@ -9,6 +9,9 @@ package org.cryptomator.cryptofs; import com.google.common.jimfs.Jimfs; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.MasterkeyLoader; +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; @@ -22,8 +25,10 @@ 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.net.URI; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; @@ -59,8 +64,12 @@ public class Windows { private FileSystem fileSystem; @BeforeAll - public void setupClass(@TempDir Path tmpDir) throws IOException { - fileSystem = new CryptoFileSystemProvider().newFileSystem(create(tmpDir), cryptoFileSystemProperties().withPassphrase("asd").build()); + public void setupClass(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException { + MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class); + Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64])); + CryptoFileSystemProperties properties = cryptoFileSystemProperties().withKeyLoader(keyLoader).build(); + CryptoFileSystemProvider.initialize(tmpDir, properties, URI.create("test:key")); + fileSystem = CryptoFileSystemProvider.newFileSystem(tmpDir, properties); } // tests https://github.com/cryptomator/cryptofs/issues/69 @@ -89,21 +98,21 @@ public void testLastModifiedIsPreservedOverSeveralOperations() throws IOExceptio try (FileChannel ch = FileChannel.open(file, CREATE_NEW, WRITE)) { t1 = Files.getLastModifiedTime(file).toInstant().truncatedTo(ChronoUnit.MILLIS); - Thread.currentThread().sleep(50); + Thread.sleep(50); ch.write(data); ch.force(true); - Thread.currentThread().sleep(50); + Thread.sleep(50); t2 = Files.getLastModifiedTime(file).toInstant().truncatedTo(ChronoUnit.MILLIS); Files.setLastModifiedTime(file, FileTime.from(t0)); ch.force(true); - Thread.currentThread().sleep(50); + Thread.sleep(50); t3 = Files.getLastModifiedTime(file).toInstant().truncatedTo(ChronoUnit.MILLIS); ch.write(data); ch.force(true); - Thread.currentThread().sleep(1000); + Thread.sleep(1000); t4 = Files.getLastModifiedTime(file).toInstant().truncatedTo(ChronoUnit.SECONDS); } @@ -127,12 +136,15 @@ public class PlatformIndependent { private Path file; @BeforeAll - public void beforeAll() throws IOException { + public void beforeAll() throws IOException, MasterkeyLoadingFailedException { inMemoryFs = Jimfs.newFileSystem(); Path vaultPath = inMemoryFs.getPath("vault"); Files.createDirectories(vaultPath); - CryptoFileSystemProvider.initialize(vaultPath, "masterkey.cryptomator", "asd"); - fileSystem = new CryptoFileSystemProvider().newFileSystem(vaultPath, cryptoFileSystemProperties().withPassphrase("asd").withFlags().build()); + MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class); + Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64])); + var properties = cryptoFileSystemProperties().withKeyLoader(keyLoader).build(); + CryptoFileSystemProvider.initialize(vaultPath, properties, URI.create("test:key")); + fileSystem = new CryptoFileSystemProvider().newFileSystem(vaultPath, properties); file = fileSystem.getPath("/test.txt"); } diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileStoreTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileStoreTest.java index a5b5ba7a..bee34ccb 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileStoreTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileStoreTest.java @@ -38,7 +38,7 @@ public class CryptoFileStoreTest { @Nested @DisplayName("with delegate present") - class DelegatingCryptoFileStoreTest { + public class DelegatingCryptoFileStoreTest { private final FileStore delegate = mock(FileStore.class); private CryptoFileStore cryptoFileStore; @@ -128,7 +128,7 @@ public void testGetAttribute() { @Nested @DisplayName("with delegate absent") - class FallbackCryptoFileStoreTest { + public class FallbackCryptoFileStoreTest { private CryptoFileStore cryptoFileStore; diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java index 670694d3..cc75fdea 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java @@ -19,6 +19,7 @@ import org.hamcrest.CoreMatchers; import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -26,10 +27,8 @@ import org.mockito.Mockito; import java.io.IOException; -import java.lang.reflect.Field; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; -import java.nio.channels.spi.AbstractInterruptibleChannel; import java.nio.file.AccessDeniedException; import java.nio.file.AccessMode; import java.nio.file.AtomicMoveNotSupportedException; @@ -74,8 +73,8 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.mockito.internal.verification.VerificationModeFactory.atLeast; @@ -96,7 +95,6 @@ public class CryptoFileSystemImplTest { private final PathMatcherFactory pathMatcherFactory = mock(PathMatcherFactory.class); private final CryptoPathFactory cryptoPathFactory = mock(CryptoPathFactory.class); private final CryptoFileSystemStats stats = mock(CryptoFileSystemStats.class); - private final RootDirectoryInitializer rootDirectoryInitializer = mock(RootDirectoryInitializer.class); private final DirectoryStreamFactory directoryStreamFactory = mock(DirectoryStreamFactory.class); private final FinallyUtil finallyUtil = mock(FinallyUtil.class); private final CiphertextDirectoryDeleter ciphertextDirDeleter = mock(CiphertextDirectoryDeleter.class); @@ -117,15 +115,14 @@ public void setup() { return other; }); - when(fileSystemProperties.maxPathLength()).thenReturn(Constants.MAX_CIPHERTEXT_PATH_LENGTH); - when(fileSystemProperties.maxNameLength()).thenReturn(Constants.MAX_CIPHERTEXT_NAME_LENGTH); + when(fileSystemProperties.maxCleartextNameLength()).thenReturn(32768); inTest = new CryptoFileSystemImpl(provider, cryptoFileSystems, pathToVault, cryptor, fileStore, stats, cryptoPathMapper, cryptoPathFactory, pathMatcherFactory, directoryStreamFactory, dirIdProvider, fileAttributeProvider, fileAttributeByNameProvider, fileAttributeViewProvider, openCryptoFiles, symlinks, finallyUtil, ciphertextDirDeleter, readonlyFlag, - fileSystemProperties, rootDirectoryInitializer); + fileSystemProperties); } @Test @@ -362,6 +359,7 @@ public class NewFileChannel { @BeforeEach public void setup() throws IOException { + when(cleartextPath.getFileName()).thenReturn(cleartextPath); when(cryptoPathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.FILE); when(cryptoPathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextPath); when(ciphertextPath.getFilePath()).thenReturn(ciphertextFilePath); @@ -370,6 +368,50 @@ public void setup() throws IOException { when(openCryptoFile.newFileChannel(any())).thenReturn(fileChannel); } + @Nested + public class LimitedCleartextNameLength { + + @BeforeEach + public void setup() throws IOException { + Assumptions.assumeTrue(cleartextPath.getFileName().toString().length() == 9); + } + + @Test + @DisplayName("read-only always works") + public void testNewFileChannelReadOnlyDespiteMaxName() throws IOException { + Mockito.doReturn(0).when(fileSystemProperties).maxCleartextNameLength(); + + FileChannel ch = inTest.newFileChannel(cleartextPath, EnumSet.of(StandardOpenOption.READ)); + + Assertions.assertSame(fileChannel, ch); + verify(readonlyFlag, Mockito.never()).assertWritable(); + } + + @Test + @DisplayName("create new fails when exceeding limit") + public void testNewFileChannelCreate1() { + Mockito.doReturn(0).when(fileSystemProperties).maxCleartextNameLength(); + + Assertions.assertThrows(FileNameTooLongException.class, () -> { + inTest.newFileChannel(cleartextPath, EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE)); + }); + + verifyNoInteractions(openCryptoFiles); + } + + @Test + @DisplayName("create new succeeds when within limit") + public void testNewFileChannelCreate2() throws IOException { + Mockito.doReturn(10).when(fileSystemProperties).maxCleartextNameLength(); + + FileChannel ch = inTest.newFileChannel(cleartextPath, EnumSet.of(StandardOpenOption.READ)); + + Assertions.assertSame(fileChannel, ch); + verify(readonlyFlag, Mockito.never()).assertWritable(); + } + + } + @Test @DisplayName("newFileChannel read-only") public void testNewFileChannelReadOnly() throws IOException { @@ -511,6 +553,7 @@ public class CopyAndMove { @BeforeEach public void setup() throws IOException { + when(cleartextDestination.getFileName()).thenReturn(cleartextDestination); when(ciphertextSource.getRawPath()).thenReturn(ciphertextSourceFile); when(ciphertextSource.getFilePath()).thenReturn(ciphertextSourceFile); when(ciphertextSource.getSymlinkFilePath()).thenReturn(ciphertextSourceFile); @@ -547,10 +590,12 @@ public class Move { @Test public void moveFileToItselfDoesNothing() throws IOException { + when(cleartextSource.getFileName()).thenReturn(cleartextSource); + inTest.move(cleartextSource, cleartextSource); verify(readonlyFlag).assertWritable(); - verifyZeroInteractions(cleartextSource); + verifyNoInteractions(cryptoPathMapper); } @Test @@ -699,17 +744,16 @@ public void setup() throws IOException, ReflectiveOperationException { when(cryptoPathMapper.getCiphertextDir(cleartextTargetParent)).thenReturn(new CiphertextDirectory("41", ciphertextTargetParent)); when(cryptoPathMapper.getCiphertextDir(cleartextDestination)).thenReturn(new CiphertextDirectory("42", ciphertextDestinationDir)); when(physicalFsProv.newFileChannel(Mockito.same(ciphertextDestinationDirFile), Mockito.anySet(), Mockito.any())).thenReturn(ciphertextTargetDirFileChannel); - Field closeLockField = AbstractInterruptibleChannel.class.getDeclaredField("closeLock"); - closeLockField.setAccessible(true); - closeLockField.set(ciphertextTargetDirFileChannel, new Object()); } @Test public void copyFileToItselfDoesNothing() throws IOException { + when(cleartextSource.getFileName()).thenReturn(cleartextSource); + inTest.copy(cleartextSource, cleartextSource); verify(readonlyFlag).assertWritable(); - verifyZeroInteractions(cleartextSource); + verifyNoInteractions(cryptoPathMapper); } @Test @@ -749,6 +793,7 @@ public void copySymlink() throws IOException { public void copySymlinkTarget() throws IOException { when(cryptoPathMapper.getCiphertextFileType(cleartextSource)).thenReturn(CiphertextFileType.SYMLINK); when(cryptoPathMapper.getCiphertextFileType(cleartextDestination)).thenThrow(NoSuchFileException.class); + when(destinationLinkTarget.getFileName()).thenReturn(destinationLinkTarget); CopyOption option1 = mock(CopyOption.class); CopyOption option2 = mock(CopyOption.class); @@ -937,30 +982,30 @@ public class CreateDirectory { private final CryptoFileSystemProvider provider = mock(CryptoFileSystemProvider.class); private final CryptoFileSystemImpl fileSystem = mock(CryptoFileSystemImpl.class); + private final CryptoPath path = mock(CryptoPath.class, "path"); + private final CryptoPath parent = mock(CryptoPath.class, "parent"); @BeforeEach public void setup() { when(fileSystem.provider()).thenReturn(provider); + when(path.getFileName()).thenReturn(path); + when(path.getParent()).thenReturn(parent); } @Test public void createDirectoryIfPathHasNoParentDoesNothing() throws IOException { - CryptoPath path = mock(CryptoPath.class); when(path.getParent()).thenReturn(null); inTest.createDirectory(path); verify(readonlyFlag).assertWritable(); verify(path).getParent(); - verifyNoMoreInteractions(path); + verifyNoMoreInteractions(cryptoPathMapper); } @Test public void createDirectoryIfPathsParentDoesNotExistsThrowsNoSuchFileException() throws IOException { - CryptoPath path = mock(CryptoPath.class); - CryptoPath parent = mock(CryptoPath.class); Path ciphertextParent = mock(Path.class); - when(path.getParent()).thenReturn(parent); when(cryptoPathMapper.getCiphertextDir(parent)).thenReturn(new CiphertextDirectory("foo", ciphertextParent)); when(ciphertextParent.getFileSystem()).thenReturn(fileSystem); doThrow(NoSuchFileException.class).when(provider).checkAccess(ciphertextParent); @@ -972,11 +1017,8 @@ public void createDirectoryIfPathsParentDoesNotExistsThrowsNoSuchFileException() } @Test - public void createDirectoryIfPathCyphertextFileDoesExistThrowsFileAlreadyException() throws IOException { - CryptoPath path = mock(CryptoPath.class); - CryptoPath parent = mock(CryptoPath.class); + public void createDirectoryIfPathCiphertextFileDoesExistThrowsFileAlreadyException() throws IOException { Path ciphertextParent = mock(Path.class); - when(path.getParent()).thenReturn(parent); when(cryptoPathMapper.getCiphertextDir(parent)).thenReturn(new CiphertextDirectory("foo", ciphertextParent)); when(ciphertextParent.getFileSystem()).thenReturn(fileSystem); doThrow(new FileAlreadyExistsException(path.toString())).when(cryptoPathMapper).assertNonExisting(path); @@ -989,8 +1031,6 @@ public void createDirectoryIfPathCyphertextFileDoesExistThrowsFileAlreadyExcepti @Test public void createDirectoryCreatesDirectoryIfConditonsAreMet() throws IOException { - CryptoPath path = mock(CryptoPath.class, "path"); - CryptoPath parent = mock(CryptoPath.class, "parent"); Path ciphertextParent = mock(Path.class, "ciphertextParent"); Path ciphertextRawPath = mock(Path.class, "d/00/00/path.c9r"); Path ciphertextDirFile = mock(Path.class, "d/00/00/path.c9r/dir.c9r"); @@ -998,7 +1038,6 @@ public void createDirectoryCreatesDirectoryIfConditonsAreMet() throws IOExceptio CiphertextFilePath ciphertextPath = mock(CiphertextFilePath.class, "ciphertext"); String dirId = "DirId1234ABC"; FileChannelMock channel = new FileChannelMock(100); - when(path.getParent()).thenReturn(parent); when(ciphertextRawPath.resolve("dir.c9r")).thenReturn(ciphertextDirFile); when(cryptoPathMapper.getCiphertextFilePath(path)).thenReturn(ciphertextPath); when(cryptoPathMapper.getCiphertextDir(path)).thenReturn(new CiphertextDirectory(dirId, ciphertextDirPath)); @@ -1021,8 +1060,6 @@ public void createDirectoryCreatesDirectoryIfConditonsAreMet() throws IOExceptio @Test public void createDirectoryClearsDirIdAndDeletesDirFileIfCreatingDirFails() throws IOException { - CryptoPath path = mock(CryptoPath.class, "path"); - CryptoPath parent = mock(CryptoPath.class, "parent"); Path ciphertextParent = mock(Path.class, "ciphertextParent"); Path ciphertextRawPath = mock(Path.class, "d/00/00/path.c9r"); Path ciphertextDirFile = mock(Path.class, "d/00/00/path.c9r/dir.c9r"); @@ -1030,7 +1067,6 @@ public void createDirectoryClearsDirIdAndDeletesDirFileIfCreatingDirFails() thro CiphertextFilePath ciphertextPath = mock(CiphertextFilePath.class, "ciphertext"); String dirId = "DirId1234ABC"; FileChannelMock channel = new FileChannelMock(100); - when(path.getParent()).thenReturn(parent); when(ciphertextRawPath.resolve("dir.c9r")).thenReturn(ciphertextDirFile); when(cryptoPathMapper.getCiphertextFilePath(path)).thenReturn(ciphertextPath); when(cryptoPathMapper.getCiphertextDir(path)).thenReturn(new CiphertextDirectory(dirId, ciphertextDirPath)); diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java index c556af23..185dbc08 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java @@ -1,37 +1,30 @@ package org.cryptomator.cryptofs; -import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags; +import org.cryptomator.cryptolib.api.MasterkeyLoader; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.MatcherAssert; import org.hamcrest.TypeSafeDiagnosingMatcher; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; -import java.nio.charset.StandardCharsets; +import java.util.Collection; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; -import static org.cryptomator.cryptofs.CryptoFileSystemProperties.DEFAULT_MASTERKEY_FILENAME; -import static org.cryptomator.cryptofs.CryptoFileSystemProperties.DEFAULT_MAX_NAME_LENGTH; -import static org.cryptomator.cryptofs.CryptoFileSystemProperties.DEFAULT_MAX_PATH_LENGTH; -import static org.cryptomator.cryptofs.CryptoFileSystemProperties.DEFAULT_PEPPER; -import static org.cryptomator.cryptofs.CryptoFileSystemProperties.PROPERTY_FILESYSTEM_FLAGS; -import static org.cryptomator.cryptofs.CryptoFileSystemProperties.PROPERTY_MASTERKEY_FILENAME; -import static org.cryptomator.cryptofs.CryptoFileSystemProperties.PROPERTY_MAX_NAME_LENGTH; -import static org.cryptomator.cryptofs.CryptoFileSystemProperties.PROPERTY_MAX_PATH_LENGTH; -import static org.cryptomator.cryptofs.CryptoFileSystemProperties.PROPERTY_PASSPHRASE; -import static org.cryptomator.cryptofs.CryptoFileSystemProperties.PROPERTY_PEPPER; -import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemProperties; -import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemPropertiesFrom; +import static org.cryptomator.cryptofs.CryptoFileSystemProperties.*; import static org.hamcrest.CoreMatchers.sameInstance; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.is; public class CryptoFileSystemPropertiesTest { + private final MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class); + @Test public void testSetNoPassphrase() { Assertions.assertThrows(IllegalStateException.class, () -> { @@ -40,167 +33,94 @@ public void testSetNoPassphrase() { } @Test - @SuppressWarnings({"unchecked", "deprecation"}) - public void testSetOnlyPassphrase() { - String passphrase = "aPassphrase"; - CryptoFileSystemProperties inTest = cryptoFileSystemProperties() // - .withPassphrase(passphrase) // - .build(); - - MatcherAssert.assertThat(inTest.passphrase(), is(passphrase)); - MatcherAssert.assertThat(inTest.masterkeyFilename(), is(DEFAULT_MASTERKEY_FILENAME)); - MatcherAssert.assertThat(inTest.readonly(), is(false)); - MatcherAssert.assertThat(inTest.initializeImplicitly(), is(true)); - MatcherAssert.assertThat(inTest.migrateImplicitly(), is(true)); - MatcherAssert.assertThat(inTest.entrySet(), - containsInAnyOrder( // - anEntry(PROPERTY_PASSPHRASE, passphrase), // - anEntry(PROPERTY_PEPPER, DEFAULT_PEPPER), // - anEntry(PROPERTY_MASTERKEY_FILENAME, DEFAULT_MASTERKEY_FILENAME), // - anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), // - anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), // - anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.INIT_IMPLICITLY, FileSystemFlags.MIGRATE_IMPLICITLY)))); - } - - @Test - @SuppressWarnings({"unchecked", "deprecation"}) - public void testSetPassphraseAndReadonlyFlag() { - String passphrase = "aPassphrase"; - CryptoFileSystemProperties inTest = cryptoFileSystemProperties() // - .withPassphrase(passphrase) // - .withReadonlyFlag() // - .build(); - - MatcherAssert.assertThat(inTest.passphrase(), is(passphrase)); - MatcherAssert.assertThat(inTest.masterkeyFilename(), is(DEFAULT_MASTERKEY_FILENAME)); - MatcherAssert.assertThat(inTest.readonly(), is(true)); - MatcherAssert.assertThat(inTest.initializeImplicitly(), is(false)); - MatcherAssert.assertThat(inTest.migrateImplicitly(), is(false)); - MatcherAssert.assertThat(inTest.entrySet(), - containsInAnyOrder( // - anEntry(PROPERTY_PASSPHRASE, passphrase), // - anEntry(PROPERTY_PEPPER, DEFAULT_PEPPER), // - anEntry(PROPERTY_MASTERKEY_FILENAME, DEFAULT_MASTERKEY_FILENAME), // - anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), // - anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), // - anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY)))); - } - - @Test - @SuppressWarnings({"unchecked", "deprecation"}) - public void testSetPassphraseAndMasterkeyFilenameAndReadonlyFlag() { - String passphrase = "aPassphrase"; + public void testSetMasterkeyFilenameAndReadonlyFlag() { String masterkeyFilename = "aMasterkeyFilename"; CryptoFileSystemProperties inTest = cryptoFileSystemProperties() // - .withPassphrase(passphrase) // + .withKeyLoader(keyLoader) // .withMasterkeyFilename(masterkeyFilename) // - .withReadonlyFlag() // + .withFlags(FileSystemFlags.READONLY) .build(); - MatcherAssert.assertThat(inTest.passphrase(), is(passphrase)); MatcherAssert.assertThat(inTest.masterkeyFilename(), is(masterkeyFilename)); MatcherAssert.assertThat(inTest.readonly(), is(true)); - MatcherAssert.assertThat(inTest.initializeImplicitly(), is(false)); - MatcherAssert.assertThat(inTest.migrateImplicitly(), is(false)); MatcherAssert.assertThat(inTest.entrySet(), containsInAnyOrder( // - anEntry(PROPERTY_PASSPHRASE, passphrase), // - anEntry(PROPERTY_PEPPER, DEFAULT_PEPPER), // + anEntry(PROPERTY_KEYLOADER, keyLoader), // + anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), // anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), // - anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), // - anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), // + anEntry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, DEFAULT_MAX_CLEARTEXT_NAME_LENGTH), // + anEntry(PROPERTY_CIPHER_COMBO, DEFAULT_CIPHER_COMBO), // anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY)))); } @Test - @SuppressWarnings({"unchecked"}) public void testFromMap() { Map map = new HashMap<>(); - String passphrase = "aPassphrase"; - byte[] pepper = "aPepper".getBytes(StandardCharsets.US_ASCII); String masterkeyFilename = "aMasterkeyFilename"; - map.put(PROPERTY_PASSPHRASE, passphrase); - map.put(PROPERTY_PEPPER, pepper); + map.put(PROPERTY_KEYLOADER, keyLoader); map.put(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename); - map.put(PROPERTY_MAX_PATH_LENGTH, 1000); - map.put(PROPERTY_MAX_NAME_LENGTH, 255); + map.put(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, 255); map.put(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY)); CryptoFileSystemProperties inTest = cryptoFileSystemPropertiesFrom(map).build(); - MatcherAssert.assertThat(inTest.passphrase(), is(passphrase)); MatcherAssert.assertThat(inTest.masterkeyFilename(), is(masterkeyFilename)); MatcherAssert.assertThat(inTest.readonly(), is(true)); - MatcherAssert.assertThat(inTest.initializeImplicitly(), is(false)); - MatcherAssert.assertThat(inTest.maxPathLength(), is(1000)); - MatcherAssert.assertThat(inTest.maxNameLength(), is(255)); + MatcherAssert.assertThat(inTest.maxCleartextNameLength(), is(255)); MatcherAssert.assertThat(inTest.entrySet(), containsInAnyOrder( // - anEntry(PROPERTY_PASSPHRASE, passphrase), // - anEntry(PROPERTY_PEPPER, pepper), // + anEntry(PROPERTY_KEYLOADER, keyLoader), // + anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), // anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), // - anEntry(PROPERTY_MAX_PATH_LENGTH, 1000), // - anEntry(PROPERTY_MAX_NAME_LENGTH, 255), // + anEntry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, 255), // + anEntry(PROPERTY_CIPHER_COMBO, DEFAULT_CIPHER_COMBO), // anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY)))); } @Test - @SuppressWarnings("unchecked") public void testWrapMapWithTrueReadonly() { Map map = new HashMap<>(); - String passphrase = "aPassphrase"; - byte[] pepper = "aPepper".getBytes(StandardCharsets.US_ASCII); String masterkeyFilename = "aMasterkeyFilename"; - map.put(PROPERTY_PASSPHRASE, passphrase); - map.put(PROPERTY_PEPPER, pepper); + map.put(PROPERTY_KEYLOADER, keyLoader); map.put(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename); map.put(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY)); CryptoFileSystemProperties inTest = CryptoFileSystemProperties.wrap(map); - MatcherAssert.assertThat(inTest.passphrase(), is(passphrase)); MatcherAssert.assertThat(inTest.masterkeyFilename(), is(masterkeyFilename)); MatcherAssert.assertThat(inTest.readonly(), is(true)); - MatcherAssert.assertThat(inTest.initializeImplicitly(), is(false)); MatcherAssert.assertThat(inTest.entrySet(), containsInAnyOrder( // - anEntry(PROPERTY_PASSPHRASE, passphrase), // - anEntry(PROPERTY_PEPPER, pepper), // + anEntry(PROPERTY_KEYLOADER, keyLoader), // + anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), // anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), // - anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), // - anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), // + anEntry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, DEFAULT_MAX_CLEARTEXT_NAME_LENGTH), // + anEntry(PROPERTY_CIPHER_COMBO, DEFAULT_CIPHER_COMBO), // anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY)))); } @Test - @SuppressWarnings("unchecked") public void testWrapMapWithFalseReadonly() { Map map = new HashMap<>(); - String passphrase = "aPassphrase"; - byte[] pepper = "aPepper".getBytes(StandardCharsets.US_ASCII); String masterkeyFilename = "aMasterkeyFilename"; - map.put(PROPERTY_PASSPHRASE, passphrase); - map.put(PROPERTY_PEPPER, pepper); + map.put(PROPERTY_KEYLOADER, keyLoader); map.put(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename); map.put(PROPERTY_FILESYSTEM_FLAGS, EnumSet.noneOf(FileSystemFlags.class)); CryptoFileSystemProperties inTest = CryptoFileSystemProperties.wrap(map); - MatcherAssert.assertThat(inTest.passphrase(), is(passphrase)); MatcherAssert.assertThat(inTest.masterkeyFilename(), is(masterkeyFilename)); MatcherAssert.assertThat(inTest.readonly(), is(false)); - MatcherAssert.assertThat(inTest.initializeImplicitly(), is(false)); MatcherAssert.assertThat(inTest.entrySet(), containsInAnyOrder( // - anEntry(PROPERTY_PASSPHRASE, passphrase), // - anEntry(PROPERTY_PEPPER, pepper), // + anEntry(PROPERTY_KEYLOADER, keyLoader), // + anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), // anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), // - anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), // - anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), // + anEntry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, DEFAULT_MAX_CLEARTEXT_NAME_LENGTH), // + anEntry(PROPERTY_CIPHER_COMBO, DEFAULT_CIPHER_COMBO), // anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.noneOf(FileSystemFlags.class)))); } @Test public void testWrapMapWithInvalidFilesystemFlags() { Map map = new HashMap<>(); - map.put(PROPERTY_PASSPHRASE, "any"); map.put(PROPERTY_MASTERKEY_FILENAME, "any"); map.put(PROPERTY_FILESYSTEM_FLAGS, "invalidType"); @@ -212,7 +132,6 @@ public void testWrapMapWithInvalidFilesystemFlags() { @Test public void testWrapMapWithInvalidMasterkeyFilename() { Map map = new HashMap<>(); - map.put(PROPERTY_PASSPHRASE, "any"); map.put(PROPERTY_MASTERKEY_FILENAME, ""); map.put(PROPERTY_FILESYSTEM_FLAGS, EnumSet.noneOf(FileSystemFlags.class)); @@ -224,7 +143,6 @@ public void testWrapMapWithInvalidMasterkeyFilename() { @Test public void testWrapMapWithInvalidPassphrase() { Map map = new HashMap<>(); - map.put(PROPERTY_PASSPHRASE, new Object()); map.put(PROPERTY_MASTERKEY_FILENAME, "any"); map.put(PROPERTY_FILESYSTEM_FLAGS, EnumSet.noneOf(FileSystemFlags.class)); @@ -234,28 +152,23 @@ public void testWrapMapWithInvalidPassphrase() { } @Test - @SuppressWarnings({"unchecked", "deprecation"}) public void testWrapMapWithoutReadonly() { Map map = new HashMap<>(); - String passphrase = "aPassphrase"; - byte[] pepper = "aPepper".getBytes(StandardCharsets.US_ASCII); - map.put(PROPERTY_PASSPHRASE, passphrase); - map.put(PROPERTY_PEPPER, pepper); + map.put(PROPERTY_KEYLOADER, keyLoader); CryptoFileSystemProperties inTest = CryptoFileSystemProperties.wrap(map); - MatcherAssert.assertThat(inTest.passphrase(), is(passphrase)); MatcherAssert.assertThat(inTest.masterkeyFilename(), is(DEFAULT_MASTERKEY_FILENAME)); MatcherAssert.assertThat(inTest.readonly(), is(false)); - MatcherAssert.assertThat(inTest.initializeImplicitly(), is(true)); - MatcherAssert.assertThat(inTest.migrateImplicitly(), is(true)); MatcherAssert.assertThat(inTest.entrySet(), containsInAnyOrder( // - anEntry(PROPERTY_PASSPHRASE, passphrase), // - anEntry(PROPERTY_PEPPER, pepper), // + anEntry(PROPERTY_KEYLOADER, keyLoader), // + anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), // anEntry(PROPERTY_MASTERKEY_FILENAME, DEFAULT_MASTERKEY_FILENAME), // - anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), // - anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), // - anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.INIT_IMPLICITLY, FileSystemFlags.MIGRATE_IMPLICITLY)))); + anEntry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, DEFAULT_MAX_CLEARTEXT_NAME_LENGTH), // + anEntry(PROPERTY_CIPHER_COMBO, DEFAULT_CIPHER_COMBO), // + anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.noneOf(FileSystemFlags.class)) + ) + ); } @Test @@ -267,9 +180,7 @@ public void testWrapMapWithoutPassphrase() { @Test public void testWrapCryptoFileSystemProperties() { - CryptoFileSystemProperties inTest = cryptoFileSystemProperties() // - .withPassphrase("any") // - .build(); + CryptoFileSystemProperties inTest = cryptoFileSystemProperties().withKeyLoader(keyLoader).build(); MatcherAssert.assertThat(CryptoFileSystemProperties.wrap(inTest), is(sameInstance(inTest))); } @@ -278,7 +189,7 @@ public void testWrapCryptoFileSystemProperties() { public void testMapIsImmutable() { Assertions.assertThrows(UnsupportedOperationException.class, () -> { cryptoFileSystemProperties() // - .withPassphrase("irrelevant") // + .withKeyLoader(keyLoader) // .build() // .put("test", "test"); }); @@ -288,7 +199,7 @@ public void testMapIsImmutable() { public void testEntrySetIsImmutable() { Assertions.assertThrows(UnsupportedOperationException.class, () -> { cryptoFileSystemProperties() // - .withPassphrase("irrelevant") // + .withKeyLoader(keyLoader) // .build() // .entrySet() // .add(null); @@ -299,7 +210,7 @@ public void testEntrySetIsImmutable() { public void testEntryIsImmutable() { Assertions.assertThrows(UnsupportedOperationException.class, () -> { cryptoFileSystemProperties() // - .withPassphrase("irrelevant") // + .withKeyLoader(keyLoader) // .build() // .entrySet() // .iterator().next() // @@ -308,7 +219,7 @@ public void testEntryIsImmutable() { } private Matcher> anEntry(K key, V value) { - return new TypeSafeDiagnosingMatcher>(Map.Entry.class) { + return new TypeSafeDiagnosingMatcher<>(Map.Entry.class) { @Override public void describeTo(Description description) { description.appendText("an entry ").appendValue(key).appendText(" = ").appendValue(value); @@ -324,11 +235,20 @@ protected boolean matchesSafely(Entry item, Description mismatchDescriptio } private boolean keyMatches(K itemKey) { - return key == null ? itemKey == null : key.equals(itemKey); + return Objects.equals(key, itemKey); } private boolean valueMatches(V itemValue) { - return value == null ? itemValue == null : value.equals(itemValue); + if (value instanceof Collection v && itemValue instanceof Collection c) { + return valuesMatch(v, c); + } else { + return Objects.equals(value, itemValue); + } + } + + @SuppressWarnings("rawtypes") + private boolean valuesMatch(Collection value, Collection itemValue) { + return value.containsAll(itemValue) && itemValue.containsAll(value); } }; } diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java index 571bcdff..549edc6f 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java @@ -12,7 +12,9 @@ import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Jimfs; import org.cryptomator.cryptofs.ch.CleartextFileChannel; -import org.cryptomator.cryptolib.api.InvalidPassphraseException; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.MasterkeyLoader; +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterAll; @@ -34,6 +36,7 @@ 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.net.URI; @@ -48,18 +51,17 @@ import java.nio.channels.WritableByteChannel; import java.nio.charset.StandardCharsets; import java.nio.file.AccessDeniedException; -import java.nio.file.CopyOption; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.DosFileAttributeView; +import java.util.Arrays; import java.util.EnumSet; import static java.nio.file.Files.readAllBytes; @@ -72,25 +74,27 @@ public class CryptoFileSystemProviderIntegrationTest { @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) - class WithLimitedPaths { + public class WithLimitedPaths { + private MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class); private CryptoFileSystem fs; private Path shortFilePath; private Path shortSymlinkPath; private Path shortDirPath; @BeforeAll - public void setup(@TempDir Path tmpDir) throws IOException { - CryptoFileSystemProvider.initialize(tmpDir, "masterkey.cryptomator", "asd"); + public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException { + Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64])); CryptoFileSystemProperties properties = cryptoFileSystemProperties() // .withFlags() // .withMasterkeyFilename("masterkey.cryptomator") // - .withPassphrase("asd") // - .withMaxPathLength(100) + .withKeyLoader(keyLoader) // + .withMaxCleartextNameLength(50) .build(); + CryptoFileSystemProvider.initialize(tmpDir, properties, URI.create("test:key")); fs = CryptoFileSystemProvider.newFileSystem(tmpDir, properties); } - + @BeforeEach public void setupEach() throws IOException { shortFilePath = fs.getPath("/short-enough.txt"); @@ -100,7 +104,7 @@ public void setupEach() throws IOException { Files.createDirectory(shortDirPath); Files.createSymbolicLink(shortSymlinkPath, shortFilePath); } - + @AfterEach public void tearDownEach() throws IOException { Files.deleteIfExists(shortFilePath); @@ -111,7 +115,7 @@ public void tearDownEach() throws IOException { @DisplayName("expect create file to fail with FileNameTooLongException") @Test public void testCreateFileExceedingPathLengthLimit() { - Path p = fs.getPath("/this-should-result-in-ciphertext-path-longer-than-100"); + Path p = fs.getPath("/this-cleartext-filename-is-longer-than-50-characters"); Assertions.assertThrows(FileNameTooLongException.class, () -> { Files.createFile(p); }); @@ -120,7 +124,7 @@ public void testCreateFileExceedingPathLengthLimit() { @DisplayName("expect create directory to fail with FileNameTooLongException") @Test public void testCreateDirExceedingPathLengthLimit() { - Path p = fs.getPath("/this-should-result-in-ciphertext-path-longer-than-100"); + Path p = fs.getPath("/this-cleartext-filename-is-longer-than-50-characters"); Assertions.assertThrows(FileNameTooLongException.class, () -> { Files.createDirectory(p); }); @@ -129,18 +133,18 @@ public void testCreateDirExceedingPathLengthLimit() { @DisplayName("expect create symlink to fail with FileNameTooLongException") @Test public void testCreateSymlinkExceedingPathLengthLimit() { - Path p = fs.getPath("/this-should-result-in-ciphertext-path-longer-than-100"); + Path p = fs.getPath("/this-cleartext-filename-is-longer-than-50-characters"); Assertions.assertThrows(FileNameTooLongException.class, () -> { Files.createSymbolicLink(p, shortFilePath); }); } @DisplayName("expect move to fail with FileNameTooLongException") - @ParameterizedTest(name = "move {0} -> this-should-result-in-ciphertext-path-longer-than-100") + @ParameterizedTest(name = "move {0} -> this-cleartext-filename-is-longer-than-50-characters") @ValueSource(strings = {"/short-enough.txt", "/short-enough-dir", "/symlink.txt"}) public void testMoveExceedingPathLengthLimit(String path) { Path src = fs.getPath(path); - Path dst = fs.getPath("/this-should-result-in-ciphertext-path-longer-than-100"); + Path dst = fs.getPath("/this-cleartext-filename-is-longer-than-50-characters"); Assertions.assertThrows(FileNameTooLongException.class, () -> { Files.move(src, dst); }); @@ -149,42 +153,52 @@ public void testMoveExceedingPathLengthLimit(String path) { } @DisplayName("expect copy to fail with FileNameTooLongException") - @ParameterizedTest(name = "copy {0} -> this-should-result-in-ciphertext-path-longer-than-100") + @ParameterizedTest(name = "copy {0} -> this-cleartext-filename-is-longer-than-50-characters") @ValueSource(strings = {"/short-enough.txt", "/short-enough-dir", "/symlink.txt"}) public void testCopyExceedingPathLengthLimit(String path) { Path src = fs.getPath(path); - Path dst = fs.getPath("/this-should-result-in-ciphertext-path-longer-than-100"); + Path dst = fs.getPath("/this-cleartext-filename-is-longer-than-50-characters"); Assertions.assertThrows(FileNameTooLongException.class, () -> { Files.copy(src, dst, LinkOption.NOFOLLOW_LINKS); }); Assertions.assertTrue(Files.exists(src)); Assertions.assertTrue(Files.notExists(dst)); } - + } @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) - class InMemory { + public class InMemory { private FileSystem tmpFs; + private MasterkeyLoader keyLoader1; + private MasterkeyLoader keyLoader2; private Path pathToVault1; private Path pathToVault2; - private Path masterkeyFile1; - private Path masterkeyFile2; + private Path vaultConfigFile1; + private Path vaultConfigFile2; private FileSystem fs1; private FileSystem fs2; @BeforeAll - public void setup() throws IOException { + public void setup() throws IOException, MasterkeyLoadingFailedException { tmpFs = Jimfs.newFileSystem(Configuration.unix()); + byte[] key1 = new byte[64]; + byte[] key2 = new byte[64]; + Arrays.fill(key1, (byte) 0x55); + Arrays.fill(key2, (byte) 0x77); + keyLoader1 = Mockito.mock(MasterkeyLoader.class); + keyLoader2 = Mockito.mock(MasterkeyLoader.class); + Mockito.when(keyLoader1.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(key1)); + Mockito.when(keyLoader2.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(key2)); pathToVault1 = tmpFs.getPath("/vaultDir1"); pathToVault2 = tmpFs.getPath("/vaultDir2"); Files.createDirectory(pathToVault1); Files.createDirectory(pathToVault2); - masterkeyFile1 = pathToVault1.resolve("masterkey.cryptomator"); - masterkeyFile2 = pathToVault2.resolve("masterkey.cryptomator"); + vaultConfigFile1 = pathToVault1.resolve("vault.cryptomator"); + vaultConfigFile2 = pathToVault2.resolve("vault.cryptomator"); } @AfterAll @@ -198,106 +212,67 @@ public void teardown() throws IOException { public void initializeVaults() { Assertions.assertAll( () -> { - CryptoFileSystemProvider.initialize(pathToVault1, "masterkey.cryptomator", "asd"); + var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader1).build(); + CryptoFileSystemProvider.initialize(pathToVault1, properties, URI.create("test:key")); Assertions.assertTrue(Files.isDirectory(pathToVault1.resolve("d"))); - Assertions.assertTrue(Files.isRegularFile(masterkeyFile1)); + Assertions.assertTrue(Files.isRegularFile(vaultConfigFile1)); }, () -> { - byte[] pepper = "pepper".getBytes(StandardCharsets.US_ASCII); - CryptoFileSystemProvider.initialize(pathToVault2, "masterkey.cryptomator", pepper, "asd"); + var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader2).build(); + CryptoFileSystemProvider.initialize(pathToVault2, properties, URI.create("test:key")); Assertions.assertTrue(Files.isDirectory(pathToVault2.resolve("d"))); - Assertions.assertTrue(Files.isRegularFile(masterkeyFile2)); + Assertions.assertTrue(Files.isRegularFile(vaultConfigFile2)); }); } @Test @Order(2) @DisplayName("get filesystem with incorrect credentials") - public void testGetFsWithWrongCredentials() { - Assumptions.assumeTrue(Files.exists(masterkeyFile1)); - Assumptions.assumeTrue(Files.exists(masterkeyFile2)); + public void testGetFsWithWrongCredentials() throws IOException { + Assumptions.assumeTrue(CryptoFileSystemProvider.checkDirStructureForVault(pathToVault1, "vault.cryptomator", "masterkey.cryptomator") == DirStructure.VAULT); + Assumptions.assumeTrue(CryptoFileSystemProvider.checkDirStructureForVault(pathToVault2, "vault.cryptomator", "masterkey.cryptomator") == DirStructure.VAULT); Assertions.assertAll( () -> { URI fsUri = CryptoFileSystemUri.create(pathToVault1); CryptoFileSystemProperties properties = cryptoFileSystemProperties() // .withFlags() // .withMasterkeyFilename("masterkey.cryptomator") // - .withPassphrase("qwe") // + .withKeyLoader(keyLoader2) // .build(); - Assertions.assertThrows(InvalidPassphraseException.class, () -> { + Assertions.assertThrows(VaultKeyInvalidException.class, () -> { FileSystems.newFileSystem(fsUri, properties); }); }, () -> { - byte[] pepper = "salt".getBytes(StandardCharsets.US_ASCII); URI fsUri = CryptoFileSystemUri.create(pathToVault2); CryptoFileSystemProperties properties = cryptoFileSystemProperties() // .withFlags() // .withMasterkeyFilename("masterkey.cryptomator") // - .withPassphrase("qwe") // - .withPepper(pepper) + .withKeyLoader(keyLoader1) // .build(); - Assertions.assertThrows(InvalidPassphraseException.class, () -> { + Assertions.assertThrows(VaultKeyInvalidException.class, () -> { FileSystems.newFileSystem(fsUri, properties); }); }); } - @Test - @Order(3) - @DisplayName("change password") - public void testChangePassword() { - Assumptions.assumeTrue(Files.exists(masterkeyFile1)); - Assumptions.assumeTrue(Files.exists(masterkeyFile2)); - Assertions.assertAll( - () -> { - Path pathToVault = tmpFs.getPath("/tmpVault"); - Files.createDirectory(pathToVault); - Path masterkeyFile = pathToVault.resolve("masterkey.cryptomator"); - Files.write(masterkeyFile, "{\"version\": 0}".getBytes(StandardCharsets.US_ASCII)); - Assertions.assertThrows(FileSystemNeedsMigrationException.class, () -> { - CryptoFileSystemProvider.changePassphrase(pathToVault, "masterkey.cryptomator", "asd", "qwe"); - }); - }, - () -> { - Assertions.assertThrows(InvalidPassphraseException.class, () -> { - CryptoFileSystemProvider.changePassphrase(pathToVault1, "masterkey.cryptomator", "WRONG", "qwe"); - }); - }, - () -> { - CryptoFileSystemProvider.changePassphrase(pathToVault1, "masterkey.cryptomator", "asd", "qwe"); - }, - () -> { - byte[] pepper = "salt".getBytes(StandardCharsets.US_ASCII); - Assertions.assertThrows(InvalidPassphraseException.class, () -> { - CryptoFileSystemProvider.changePassphrase(pathToVault2, "masterkey.cryptomator", pepper, "asd", "qwe"); - }); - }, - () -> { - byte[] pepper = "pepper".getBytes(StandardCharsets.US_ASCII); - CryptoFileSystemProvider.changePassphrase(pathToVault2, "masterkey.cryptomator", pepper, "asd", "qwe"); - } - ); - } - @Test @Order(4) @DisplayName("get filesystem with correct credentials") public void testGetFsViaNioApi() { - Assumptions.assumeTrue(Files.exists(masterkeyFile1)); - Assumptions.assumeTrue(Files.exists(masterkeyFile2)); + Assumptions.assumeTrue(Files.exists(vaultConfigFile1)); + Assumptions.assumeTrue(Files.exists(vaultConfigFile2)); Assertions.assertAll( () -> { URI fsUri = CryptoFileSystemUri.create(pathToVault1); - fs1 = FileSystems.newFileSystem(fsUri, cryptoFileSystemProperties().withPassphrase("qwe").build()); + fs1 = FileSystems.newFileSystem(fsUri, cryptoFileSystemProperties().withKeyLoader(keyLoader1).build()); Assertions.assertTrue(fs1 instanceof CryptoFileSystemImpl); FileSystem sameFs = FileSystems.getFileSystem(fsUri); Assertions.assertSame(fs1, sameFs); }, () -> { - byte[] pepper = "pepper".getBytes(StandardCharsets.US_ASCII); URI fsUri = CryptoFileSystemUri.create(pathToVault2); - fs2 = FileSystems.newFileSystem(fsUri, cryptoFileSystemProperties().withPassphrase("qwe").withPepper(pepper).build()); + fs2 = FileSystems.newFileSystem(fsUri, cryptoFileSystemProperties().withKeyLoader(keyLoader2).build()); Assertions.assertTrue(fs2 instanceof CryptoFileSystemImpl); FileSystem sameFs = FileSystems.getFileSystem(fsUri); @@ -548,21 +523,24 @@ public void testMoveFileFromOneCryptoFileSystemToAnother() throws IOException { @EnabledOnOs({OS.MAC, OS.LINUX}) @TestInstance(TestInstance.Lifecycle.PER_CLASS) @DisplayName("On POSIX Systems") - class PosixTests { + public class PosixTests { private FileSystem fs; @BeforeAll - public void setup(@TempDir Path tmpDir) throws IOException { + public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException { Path pathToVault = tmpDir.resolve("vaultDir1"); Files.createDirectories(pathToVault); - CryptoFileSystemProvider.initialize(pathToVault, "masterkey.cryptomator", "asd"); - fs = CryptoFileSystemProvider.newFileSystem(pathToVault, cryptoFileSystemProperties().withPassphrase("asd").build()); + MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class); + Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64])); + var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build(); + CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key")); + fs = CryptoFileSystemProvider.newFileSystem(pathToVault, properties); } @Nested @DisplayName("File Locks") - class FileLockTests { + public class FileLockTests { private Path file = fs.getPath("/lock.txt"); @@ -637,16 +615,19 @@ public void testOverlappingLocks(boolean shared) throws IOException { @EnabledOnOs(OS.WINDOWS) @TestInstance(TestInstance.Lifecycle.PER_CLASS) @DisplayName("On Windows Systems") - class WindowsTests { + public class WindowsTests { private FileSystem fs; @BeforeAll - public void setup(@TempDir Path tmpDir) throws IOException { + public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException { Path pathToVault = tmpDir.resolve("vaultDir1"); Files.createDirectories(pathToVault); - CryptoFileSystemProvider.initialize(pathToVault, "masterkey.cryptomator", "asd"); - fs = CryptoFileSystemProvider.newFileSystem(pathToVault, cryptoFileSystemProperties().withPassphrase("asd").build()); + MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class); + Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64])); + var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build(); + CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key")); + fs = CryptoFileSystemProvider.newFileSystem(pathToVault, properties); } @Test @@ -680,7 +661,7 @@ public void testDosFileAttributes() throws IOException { @Nested @DisplayName("read-only file") - class OnReadOnlyFile { + public class OnReadOnlyFile { private Path file = fs.getPath("/readonly.txt"); private DosFileAttributeView attrView; diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java index 4ba4731e..cba944b0 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java @@ -2,15 +2,17 @@ import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Jimfs; -import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags; import org.cryptomator.cryptofs.ch.AsyncDelegatingFileChannel; -import org.cryptomator.cryptolib.api.InvalidPassphraseException; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.MasterkeyLoader; +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; import java.io.IOException; import java.net.URI; @@ -25,7 +27,6 @@ import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.LinkOption; -import java.nio.file.NoSuchFileException; import java.nio.file.NotDirectoryException; import java.nio.file.OpenOption; import java.nio.file.Path; @@ -36,6 +37,7 @@ import java.nio.file.spi.FileSystemProvider; import java.util.HashSet; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.stream.Stream; @@ -44,18 +46,15 @@ import static java.nio.file.StandardOpenOption.APPEND; import static java.util.Arrays.asList; import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemProperties; -import static org.cryptomator.cryptofs.CryptoFileSystemProvider.containsVault; -import static org.cryptomator.cryptofs.CryptoFileSystemProvider.newFileSystem; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class CryptoFileSystemProviderTest { + private final MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class); private final CryptoFileSystems fileSystems = mock(CryptoFileSystems.class); private final CryptoPath cryptoPath = mock(CryptoPath.class); @@ -71,7 +70,7 @@ public class CryptoFileSystemProviderTest { private CryptoFileSystemProvider inTest; - private static final Stream shouldFailWithProviderMismatch() { + public static Stream shouldFailWithProviderMismatch() { return Stream.of( // invocation("newAsynchronousFileChannel", (inTest, path) -> inTest.newAsynchronousFileChannel(path, new HashSet<>(), mock(ExecutorService.class))), // invocation("newFileChannel", (inTest, path) -> inTest.newFileChannel(path, new HashSet<>())), // @@ -92,7 +91,7 @@ private static final Stream shouldFailWithProviderMis } @SuppressWarnings("unchecked") - private static final Stream shouldFailWithRelativePath() { + public static Stream shouldFailWithRelativePath() { return Stream.of( // invocation("newAsynchronousFileChannel", (inTest, path) -> inTest.newAsynchronousFileChannel(path, new HashSet<>(), mock(ExecutorService.class))), // invocation("newFileChannel", (inTest, path) -> inTest.newFileChannel(path, new HashSet<>())), // @@ -113,7 +112,9 @@ private static final Stream shouldFailWithRelativePat @BeforeEach @SuppressWarnings("deprecation") - public void setup() { + public void setup() throws MasterkeyLoadingFailedException { + when(keyLoader.loadKey(Mockito.any())).thenReturn(new Masterkey(new byte[64])); + CryptoFileSystemProviderComponent component = mock(CryptoFileSystemProviderComponent.class); when(component.fileSystems()).thenReturn(fileSystems); when(component.copyOperation()).thenReturn(copyOperation); @@ -165,167 +166,48 @@ public void testGetSchemeReturnsCryptomatorScheme() { public void testInitializeFailWithNotDirectoryException() { FileSystem fs = Jimfs.newFileSystem(Configuration.unix()); Path pathToVault = fs.getPath("/vaultDir"); + var properties = cryptoFileSystemProperties().withKeyLoader(keyLoader).build(); Assertions.assertThrows(NotDirectoryException.class, () -> { - CryptoFileSystemProvider.initialize(pathToVault, "irrelevant.txt", "asd"); + CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key")); }); } @Test - public void testInitialize() throws IOException { + public void testInitialize() throws IOException, MasterkeyLoadingFailedException { FileSystem fs = Jimfs.newFileSystem(Configuration.unix()); Path pathToVault = fs.getPath("/vaultDir"); - Path masterkeyFile = pathToVault.resolve("masterkey.cryptomator"); + Path vaultConfigFile = pathToVault.resolve("vault.cryptomator"); Path dataDir = pathToVault.resolve("d"); + var properties = cryptoFileSystemProperties().withKeyLoader(keyLoader).build(); Files.createDirectory(pathToVault); - CryptoFileSystemProvider.initialize(pathToVault, "masterkey.cryptomator", "asd"); + CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key")); Assertions.assertTrue(Files.isDirectory(dataDir)); - Assertions.assertTrue(Files.isRegularFile(masterkeyFile)); - } + Assertions.assertTrue(Files.isRegularFile(vaultConfigFile)); - @Test - public void testNoImplicitInitialization() throws IOException { - FileSystem fs = Jimfs.newFileSystem(Configuration.unix()); - Path pathToVault = fs.getPath("/vaultDir"); - Path masterkeyFile = pathToVault.resolve("masterkey.cryptomator"); - Path dataDir = pathToVault.resolve("d"); - - Files.createDirectory(pathToVault); - URI uri = CryptoFileSystemUri.create(pathToVault); + Optional preRootDir = Files.list(dataDir).findFirst(); + Assertions.assertTrue(preRootDir.isPresent()); + Assertions.assertTrue(Files.isDirectory(preRootDir.get())); - CryptoFileSystemProperties properties = cryptoFileSystemProperties() // - .withFlags() // - .withMasterkeyFilename("masterkey.cryptomator") // - .withPassphrase("asd") // - .build(); - - NoSuchFileException e = Assertions.assertThrows(NoSuchFileException.class, () -> { - inTest.newFileSystem(uri, properties); - }); - MatcherAssert.assertThat(e.getMessage(), containsString("Vault not initialized")); - Assertions.assertTrue(Files.notExists(dataDir)); - Assertions.assertTrue(Files.notExists(masterkeyFile)); + Optional rootDir = Files.list(preRootDir.get()).findFirst(); + Assertions.assertTrue(rootDir.isPresent()); + Assertions.assertTrue(Files.isDirectory(rootDir.get())); } @Test - @SuppressWarnings("deprecation") - public void testImplicitInitialization() throws IOException { - FileSystem fs = Jimfs.newFileSystem(Configuration.unix()); - Path pathToVault = fs.getPath("/vaultDir"); - Path masterkeyFile = pathToVault.resolve("masterkey.cryptomator"); - Path dataDir = pathToVault.resolve("d"); - - Files.createDirectory(pathToVault); + public void testNewFileSystem() throws IOException, MasterkeyLoadingFailedException { + Path pathToVault = Path.of("/vaultDir"); URI uri = CryptoFileSystemUri.create(pathToVault); - CryptoFileSystemProperties properties = cryptoFileSystemProperties() // - .withFlags(FileSystemFlags.INIT_IMPLICITLY) // - .withMasterkeyFilename("masterkey.cryptomator") // - .withPassphrase("asd") // + .withFlags() // + .withKeyLoader(keyLoader) // .build(); - when(fileSystems.create(eq(inTest), eq(pathToVault), eq(properties))).thenReturn(cryptoFileSystem); - FileSystem result = inTest.newFileSystem(uri, properties); - verify(fileSystems).create(eq(inTest), eq(pathToVault), eq(properties)); - - Assertions.assertSame(cryptoFileSystem, result); - Assertions.assertTrue(Files.isDirectory(dataDir)); - Assertions.assertTrue(Files.isRegularFile(masterkeyFile)); - } - - @Test - public void testContainsVaultReturnsTrueIfDirectoryContainsMasterkeyFileAndDataDir() throws IOException { - FileSystem fs = Jimfs.newFileSystem(Configuration.unix()); - String masterkeyFilename = "masterkey.foo.baz"; - Path pathToVault = fs.getPath("/vaultDir"); - - Path masterkeyFile = pathToVault.resolve(masterkeyFilename); - Path dataDir = pathToVault.resolve("d"); - Files.createDirectories(dataDir); - Files.write(masterkeyFile, new byte[0]); + inTest.newFileSystem(uri, properties); - Assertions.assertTrue(containsVault(pathToVault, masterkeyFilename)); - } - - @Test - public void testContainsVaultReturnsFalseIfDirectoryContainsNoMasterkeyFileButDataDir() throws IOException { - FileSystem fs = Jimfs.newFileSystem(Configuration.unix()); - - String masterkeyFilename = "masterkey.foo.baz"; - Path pathToVault = fs.getPath("/vaultDir"); - - Path dataDir = pathToVault.resolve("d"); - Files.createDirectories(dataDir); - - Assertions.assertFalse(containsVault(pathToVault, masterkeyFilename)); - } - - @Test - public void testContainsVaultReturnsFalseIfDirectoryContainsMasterkeyFileButNoDataDir() throws IOException { - FileSystem fs = Jimfs.newFileSystem(Configuration.unix()); - - String masterkeyFilename = "masterkey.foo.baz"; - Path pathToVault = fs.getPath("/vaultDir"); - - Path masterkeyFile = pathToVault.resolve(masterkeyFilename); - Files.createDirectories(pathToVault); - Files.write(masterkeyFile, new byte[0]); - - Assertions.assertFalse(containsVault(pathToVault, masterkeyFilename)); - } - - @Test - public void testVaultWithChangedPassphraseCanBeOpenedWithNewPassphrase() throws IOException { - String oldPassphrase = "oldPassphrase838283"; - String newPassphrase = "newPassphrase954810921"; - String masterkeyFilename = "masterkey.foo.baz"; - FileSystem fs = Jimfs.newFileSystem(Configuration.unix()); - Path pathToVault = fs.getPath("/vaultDir"); - Files.createDirectory(pathToVault); - newFileSystem( // - pathToVault, // - cryptoFileSystemProperties() // - .withMasterkeyFilename(masterkeyFilename) // - .withPassphrase(oldPassphrase) // - .build()).close(); - - CryptoFileSystemProvider.changePassphrase(pathToVault, masterkeyFilename, oldPassphrase, newPassphrase); - - newFileSystem( // - pathToVault, // - cryptoFileSystemProperties() // - .withMasterkeyFilename(masterkeyFilename) // - .withPassphrase(newPassphrase) // - .build()).close(); - } - - @Test - public void testVaultWithChangedPassphraseCanNotBeOpenedWithOldPassphrase() throws IOException { - String oldPassphrase = "oldPassphrase838283"; - String newPassphrase = "newPassphrase954810921"; - String masterkeyFilename = "masterkey.foo.baz"; - FileSystem fs = Jimfs.newFileSystem(Configuration.unix()); - Path pathToVault = fs.getPath("/vaultDir"); - Files.createDirectory(pathToVault); - newFileSystem( // - pathToVault, // - cryptoFileSystemProperties() // - .withMasterkeyFilename(masterkeyFilename) // - .withPassphrase(oldPassphrase) // - .build()).close(); - - CryptoFileSystemProvider.changePassphrase(pathToVault, masterkeyFilename, oldPassphrase, newPassphrase); - - Assertions.assertThrows(InvalidPassphraseException.class, () -> { - newFileSystem( // - pathToVault, // - cryptoFileSystemProperties() // - .withMasterkeyFilename(masterkeyFilename) // - .withPassphrase(oldPassphrase) // - .build()); - }); + Mockito.verify(fileSystems).create(Mockito.same(inTest), Mockito.eq(pathToVault.toAbsolutePath()), Mockito.eq(properties)); } @Test @@ -370,7 +252,7 @@ public void testNewAsyncFileChannelReturnsAsyncDelegatingFileChannel() throws IO when(cryptoFileSystem.newFileChannel(cryptoPath, options)).thenReturn(channel); AsynchronousFileChannel result = inTest.newAsynchronousFileChannel(cryptoPath, options, executor); - + MatcherAssert.assertThat(result, instanceOf(AsyncDelegatingFileChannel.class)); } @@ -491,7 +373,7 @@ public void testCheckAccessDelegatesToFileSystem() throws IOException { } @Test - public void testGetFileStoreDelegatesToFileSystem() throws IOException { + public void testGetFileStoreDelegatesToFileSystem() { CryptoFileStore fileStore = mock(CryptoFileStore.class); when(cryptoFileSystem.getFileStore()).thenReturn(fileStore); diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java index fa7cbd71..d6d186b5 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java @@ -1,8 +1,12 @@ package org.cryptomator.cryptofs; import org.cryptomator.cryptofs.common.DeletingFileVisitor; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.MasterkeyLoader; +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import java.io.IOException; import java.net.URI; @@ -13,7 +17,6 @@ import java.nio.file.Paths; import static java.nio.file.Files.createTempDirectory; -import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemProperties; public class CryptoFileSystemUriTest { @@ -68,10 +71,14 @@ public void testCreateWithPathComponents() throws URISyntaxException { } @Test - public void testCreateWithPathToVaultFromNonDefaultProvider() throws IOException { + public void testCreateWithPathToVaultFromNonDefaultProvider() throws IOException, MasterkeyLoadingFailedException { Path tempDir = createTempDirectory("CryptoFileSystemUrisTest").toAbsolutePath(); try { - FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(tempDir, cryptoFileSystemProperties().withPassphrase("asd").build()); + MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class); + Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64])); + CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build(); + CryptoFileSystemProvider.initialize(tempDir, properties, URI.create("test:key")); + FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(tempDir, properties); Path absolutePathToVault = fileSystem.getPath("a").toAbsolutePath(); URI uri = CryptoFileSystemUri.create(absolutePathToVault, "a", "b"); diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java index 56687d7b..37dd143b 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java @@ -1,15 +1,29 @@ package org.cryptomator.cryptofs; +import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.CryptorProvider; +import org.cryptomator.cryptolib.api.FileNameCryptor; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.MasterkeyLoader; +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.hamcrest.MatcherAssert; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; import java.nio.file.FileSystemAlreadyExistsException; import java.nio.file.FileSystemNotFoundException; +import java.nio.file.Files; import java.nio.file.Path; +import java.security.SecureRandom; import static org.hamcrest.Matchers.containsString; import static org.mockito.ArgumentMatchers.any; @@ -19,26 +33,78 @@ public class CryptoFileSystemsTest { - private final Path path = mock(Path.class); - private final Path normalizedPath = mock(Path.class); + private final Path pathToVault = mock(Path.class, "vaultPath"); + private final Path normalizedPathToVault = mock(Path.class, "normalizedVaultPath"); + private final Path configFilePath = mock(Path.class, "normalizedVaultPath/vault.cryptomator"); + 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); private final CryptoFileSystemImpl cryptoFileSystem = mock(CryptoFileSystemImpl.class); - + private final VaultConfig.UnverifiedVaultConfig configLoader = mock(VaultConfig.UnverifiedVaultConfig.class); + private final MasterkeyLoader keyLoader = mock(MasterkeyLoader.class); + private final Masterkey masterkey = mock(Masterkey.class); + private final Masterkey clonedMasterkey = Mockito.mock(Masterkey.class); + private final byte[] rawKey = new byte[64]; + private final VaultConfig vaultConfig = mock(VaultConfig.class); + private final CryptorProvider.Scheme cipherCombo = mock(CryptorProvider.Scheme.class); + private final SecureRandom csprng = Mockito.mock(SecureRandom.class); + private final CryptorProvider cryptorProvider = mock(CryptorProvider.class); + private final Cryptor cryptor = mock(Cryptor.class); + private final FileNameCryptor fileNameCryptor = mock(FileNameCryptor.class); private final CryptoFileSystemComponent.Builder cryptoFileSystemComponentBuilder = mock(CryptoFileSystemComponent.Builder.class); - private final FileSystemCapabilityChecker capabilityChecker = mock(FileSystemCapabilityChecker.class); - private final CryptoFileSystems inTest = new CryptoFileSystems(cryptoFileSystemComponentBuilder, capabilityChecker); + + private MockedStatic vaultConficClass; + private MockedStatic filesClass; + private MockedStatic cryptorProviderClass; + + private final CryptoFileSystems inTest = new CryptoFileSystems(cryptoFileSystemComponentBuilder, capabilityChecker, csprng); @BeforeEach - public void setup() { - when(cryptoFileSystemComponentBuilder.provider(any())).thenReturn(cryptoFileSystemComponentBuilder); + public void setup() throws IOException, MasterkeyLoadingFailedException { + vaultConficClass = Mockito.mockStatic(VaultConfig.class); + filesClass = Mockito.mockStatic(Files.class); + cryptorProviderClass = Mockito.mockStatic(CryptorProvider.class); + + when(pathToVault.normalize()).thenReturn(normalizedPathToVault); + when(normalizedPathToVault.resolve("vault.cryptomator")).thenReturn(configFilePath); + when(properties.vaultConfigFilename()).thenReturn("vault.cryptomator"); + when(properties.keyLoader()).thenReturn(keyLoader); + filesClass.when(() -> Files.readString(configFilePath, StandardCharsets.US_ASCII)).thenReturn("jwt-vault-config"); + vaultConficClass.when(() -> VaultConfig.decode("jwt-vault-config")).thenReturn(configLoader); + cryptorProviderClass.when(() -> CryptorProvider.forScheme(cipherCombo)).thenReturn(cryptorProvider); + when(VaultConfig.decode("jwt-vault-config")).thenReturn(configLoader); + when(configLoader.getKeyId()).thenReturn(URI.create("test:key")); + when(keyLoader.loadKey(Mockito.any())).thenReturn(masterkey); + when(masterkey.getEncoded()).thenReturn(rawKey); + when(masterkey.clone()).thenReturn(clonedMasterkey); + when(configLoader.verify(rawKey, Constants.VAULT_VERSION)).thenReturn(vaultConfig); + when(cryptorProvider.provide(clonedMasterkey, csprng)).thenReturn(cryptor); + when(vaultConfig.getCipherCombo()).thenReturn(cipherCombo); + when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor); + when(fileNameCryptor.hashDirectoryId("")).thenReturn("ABCDEFGHIJKLMNOP"); + when(pathToVault.resolve(Constants.DATA_DIR_NAME)).thenReturn(dataDirPath); + when(dataDirPath.resolve("AB")).thenReturn(preContenRootPath); + when(preContenRootPath.resolve("CDEFGHIJKLMNOP")).thenReturn(contenRootPath); + filesClass.when(() -> Files.exists(contenRootPath)).thenReturn(true); + when(cryptoFileSystemComponentBuilder.cryptor(any())).thenReturn(cryptoFileSystemComponentBuilder); + when(cryptoFileSystemComponentBuilder.vaultConfig(any())).thenReturn(cryptoFileSystemComponentBuilder); when(cryptoFileSystemComponentBuilder.pathToVault(any())).thenReturn(cryptoFileSystemComponentBuilder); when(cryptoFileSystemComponentBuilder.properties(any())).thenReturn(cryptoFileSystemComponentBuilder); + when(cryptoFileSystemComponentBuilder.provider(any())).thenReturn(cryptoFileSystemComponentBuilder); when(cryptoFileSystemComponentBuilder.build()).thenReturn(cryptoFileSystemComponent); when(cryptoFileSystemComponent.cryptoFileSystem()).thenReturn(cryptoFileSystem); - when(path.normalize()).thenReturn(normalizedPath); + } + + @AfterEach + public void tearDown() { + vaultConficClass.close(); + filesClass.close(); + cryptorProviderClass.close(); } @Test @@ -47,47 +113,60 @@ public void testContainsReturnsFalseForNonContainedFileSystem() { } @Test - public void testContainsReturnsTrueForContainedFileSystem() throws IOException { - CryptoFileSystemImpl impl = inTest.create(provider, path, properties); + public void testContainsReturnsTrueForContainedFileSystem() throws IOException, MasterkeyLoadingFailedException { + CryptoFileSystemImpl impl = inTest.create(provider, pathToVault, properties); Assertions.assertSame(cryptoFileSystem, impl); Assertions.assertTrue(inTest.contains(cryptoFileSystem)); - verify(cryptoFileSystemComponentBuilder).provider(provider); + verify(cryptoFileSystemComponentBuilder).cryptor(cryptor); + verify(cryptoFileSystemComponentBuilder).vaultConfig(vaultConfig); + verify(cryptoFileSystemComponentBuilder).pathToVault(normalizedPathToVault); verify(cryptoFileSystemComponentBuilder).properties(properties); - verify(cryptoFileSystemComponentBuilder).pathToVault(normalizedPath); + verify(cryptoFileSystemComponentBuilder).provider(provider); verify(cryptoFileSystemComponentBuilder).build(); } @Test - public void testCreateThrowsFileSystemAlreadyExistsExceptionIfInvokedWithSamePathTwice() throws IOException { - inTest.create(provider, path, properties); + public void testCreateThrowsFileSystemAlreadyExistsExceptionIfInvokedWithSamePathTwice() throws IOException, MasterkeyLoadingFailedException { + inTest.create(provider, pathToVault, properties); Assertions.assertThrows(FileSystemAlreadyExistsException.class, () -> { - inTest.create(provider, path, properties); + inTest.create(provider, pathToVault, properties); }); } @Test - public void testCreateDoesNotThrowFileSystemAlreadyExistsExceptionIfFileSystemIsRemovedBefore() throws IOException { - CryptoFileSystemImpl fileSystem = inTest.create(provider, path, properties); - inTest.remove(fileSystem); + public void testCreateDoesNotThrowFileSystemAlreadyExistsExceptionIfFileSystemIsRemovedBefore() throws IOException, MasterkeyLoadingFailedException { + CryptoFileSystemImpl fileSystem1 = inTest.create(provider, pathToVault, properties); + Assertions.assertTrue(inTest.contains(fileSystem1)); + inTest.remove(fileSystem1); + Assertions.assertFalse(inTest.contains(fileSystem1)); + + CryptoFileSystemImpl fileSystem2 = inTest.create(provider, pathToVault, properties); + Assertions.assertTrue(inTest.contains(fileSystem2)); + } + + @Test + public void testCreateThrowsIOExceptionIfContentRootExistenceCheckFails() { + filesClass.when(() -> Files.exists(contenRootPath)).thenReturn(false); - inTest.create(provider, path, properties); + Assertions.assertThrows(IOException.class, () -> inTest.create(provider, pathToVault, properties)); } @Test - public void testGetReturnsFileSystemForPathIfItExists() throws IOException { - inTest.create(provider, path, properties); + public void testGetReturnsFileSystemForPathIfItExists() throws IOException, MasterkeyLoadingFailedException { + CryptoFileSystemImpl fileSystem = inTest.create(provider, pathToVault, properties); - Assertions.assertSame(cryptoFileSystem, inTest.get(path)); + Assertions.assertTrue(inTest.contains(fileSystem)); + Assertions.assertSame(cryptoFileSystem, inTest.get(pathToVault)); } @Test public void testThrowsFileSystemNotFoundExceptionIfItDoesNotExists() { FileSystemNotFoundException e = Assertions.assertThrows(FileSystemNotFoundException.class, () -> { - inTest.get(path); + inTest.get(pathToVault); }); - MatcherAssert.assertThat(e.getMessage(), containsString(path.toString())); + MatcherAssert.assertThat(e.getMessage(), containsString(normalizedPathToVault.toString())); } } diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java index 9384fce0..76ac28f1 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java @@ -35,6 +35,7 @@ public class CryptoPathMapperTest { private final FileNameCryptor fileNameCryptor = Mockito.mock(FileNameCryptor.class); private final DirectoryIdProvider dirIdProvider = Mockito.mock(DirectoryIdProvider.class); private final LongFileNameProvider longFileNameProvider = Mockito.mock(LongFileNameProvider.class); + private final VaultConfig vaultConfig = Mockito.mock(VaultConfig.class); private final Symlinks symlinks = Mockito.mock(Symlinks.class); private final CryptoFileSystemImpl fileSystem = Mockito.mock(CryptoFileSystemImpl.class); @@ -45,6 +46,7 @@ public void setup() { CryptoPath empty = cryptoPathFactory.emptyFor(fileSystem); Mockito.when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor); Mockito.when(pathToVault.resolve("d")).thenReturn(dataRoot); + Mockito.when(vaultConfig.getShorteningThreshold()).thenReturn(220); Mockito.when(fileSystem.getPath(ArgumentMatchers.anyString(), ArgumentMatchers.any())).thenAnswer(invocation -> { String first = invocation.getArgument(0); if (invocation.getArguments().length == 1) { @@ -68,7 +70,7 @@ public void testPathEncryptionForRoot() throws IOException { Path d0000 = Mockito.mock(Path.class); Mockito.when(d00.resolve("00")).thenReturn(d0000); - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); Path path = mapper.getCiphertextDir(fileSystem.getRootPath()).path; Assertions.assertEquals(d0000, path); } @@ -92,7 +94,7 @@ public void testPathEncryptionForFoo() throws IOException { Path d0001 = Mockito.mock(Path.class); Mockito.when(d00.resolve("01")).thenReturn(d0001); - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); Path path = mapper.getCiphertextDir(fileSystem.getPath("/foo")).path; Assertions.assertEquals(d0001, path); } @@ -126,7 +128,7 @@ public void testPathEncryptionForFooBar() throws IOException { Path d0002 = Mockito.mock(Path.class); Mockito.when(d00.resolve("02")).thenReturn(d0002); - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); Path path = mapper.getCiphertextDir(fileSystem.getPath("/foo/bar")).path; Assertions.assertEquals(d0002, path); } @@ -163,7 +165,7 @@ public void testPathEncryptionForFooBarBaz() throws IOException { Mockito.when(d0002.resolve("zab.c9r")).thenReturn(d0002zab); Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.eq("baz"), Mockito.any())).thenReturn("zab"); - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); Path path = mapper.getCiphertextFilePath(fileSystem.getPath("/foo/bar/baz")).getRawPath(); Assertions.assertEquals(d0002zab, path); } @@ -211,7 +213,7 @@ public void setup() throws IOException { @Test public void testGetCiphertextFileTypeOfRootPath() throws IOException { - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); CiphertextFileType type = mapper.getCiphertextFileType(fileSystem.getRootPath()); Assertions.assertEquals(CiphertextFileType.DIRECTORY, type); } @@ -220,7 +222,7 @@ public void testGetCiphertextFileTypeOfRootPath() throws IOException { public void testGetCiphertextFileTypeForNonexistingFile() throws IOException { Mockito.when(underlyingFileSystemProvider.readAttributes(c9rPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(NoSuchFileException.class); - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); CryptoPath path = fileSystem.getPath("/CLEAR"); Assertions.assertThrows(NoSuchFileException.class, () -> { @@ -233,7 +235,7 @@ public void testGetCiphertextFileTypeForFile() throws IOException { Mockito.when(underlyingFileSystemProvider.readAttributes(c9rPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(c9rAttrs); Mockito.when(c9rAttrs.isDirectory()).thenReturn(false); - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); CryptoPath path = fileSystem.getPath("/CLEAR"); CiphertextFileType type = mapper.getCiphertextFileType(path); @@ -248,7 +250,7 @@ public void testGetCiphertextFileTypeForDirectory() throws IOException { Mockito.when(underlyingFileSystemProvider.readAttributes(symlinkFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(NoSuchFileException.class); Mockito.when(underlyingFileSystemProvider.readAttributes(contentsFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(NoSuchFileException.class); - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); CryptoPath path = fileSystem.getPath("/CLEAR"); CiphertextFileType type = mapper.getCiphertextFileType(path); @@ -263,7 +265,7 @@ public void testGetCiphertextFileTypeForSymlink() throws IOException { Mockito.when(underlyingFileSystemProvider.readAttributes(symlinkFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(Mockito.mock(BasicFileAttributes.class)); Mockito.when(underlyingFileSystemProvider.readAttributes(contentsFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(NoSuchFileException.class); - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); CryptoPath path = fileSystem.getPath("/CLEAR"); CiphertextFileType type = mapper.getCiphertextFileType(path); @@ -280,7 +282,7 @@ public void testGetCiphertextFileTypeForShortenedFile() throws IOException { Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.eq("LONGCLEAR"), Mockito.any())).thenReturn(Strings.repeat("A", 1000)); Mockito.when(longFileNameProvider.deflate(Mockito.any())).thenReturn(new LongFileNameProvider.DeflatedFileName(c9rPath, null, null)); - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); CryptoPath path = fileSystem.getPath("/LONGCLEAR"); CiphertextFileType type = mapper.getCiphertextFileType(path); diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoPathTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoPathTest.java index 8340d524..6475fd38 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoPathTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoPathTest.java @@ -35,6 +35,7 @@ import static org.hamcrest.Matchers.lessThan; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; @@ -268,7 +269,7 @@ public void testToRealPathDoesNotResolveSymlinksWhenNotFollowingLinks() throws I Path normalizedAndAbsolute = new CryptoPath(fileSystem, symlinks, asList("a", "b"), true); Assertions.assertEquals(normalizedAndAbsolute, inTest.toRealPath(LinkOption.NOFOLLOW_LINKS)); - verifyZeroInteractions(symlinks); + verifyNoInteractions(symlinks); } @Test diff --git a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java index d8d47c54..21754ca2 100644 --- a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java @@ -10,26 +10,27 @@ import com.google.common.base.Strings; import org.cryptomator.cryptofs.common.Constants; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.MasterkeyLoader; +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; +import java.net.URI; import java.nio.file.DirectoryNotEmptyException; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; -import java.util.stream.Collectors; -import java.util.stream.IntStream; import java.util.stream.Stream; import static java.nio.file.StandardOpenOption.CREATE_NEW; -import static org.cryptomator.cryptofs.common.Constants.MAX_CIPHERTEXT_NAME_LENGTH; -import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemProperties; import static org.cryptomator.cryptofs.CryptoFileSystemUri.create; /** @@ -41,10 +42,14 @@ public class DeleteNonEmptyCiphertextDirectoryIntegrationTest { private static FileSystem fileSystem; @BeforeAll - public static void setupClass(@TempDir Path tmpDir) throws IOException { + public static void setupClass(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException { pathToVault = tmpDir.resolve("vault"); Files.createDirectory(pathToVault); - fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withPassphrase("asd").build()); + MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class); + Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64])); + CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build(); + CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key")); + fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), properties); } @Test @@ -53,7 +58,7 @@ public void testDeleteCiphertextDirectoryContainingNonCryptoFile() throws IOExce Files.createDirectory(cleartextDirectory); Path ciphertextDirectory = firstEmptyCiphertextDirectory(); - createFile(ciphertextDirectory, "foo01234.txt", new byte[] {65}); + createFile(ciphertextDirectory, "foo01234.txt", new byte[]{65}); Files.delete(cleartextDirectory); } @@ -72,9 +77,9 @@ public void testDeleteCiphertextDirectoryContainingDirectories() throws IOExcept // .... text.data Path foo0123 = createFolder(ciphertextDirectory, "foo0123"); Path foobar = createFolder(foo0123, "foobar"); - createFile(foo0123, "test.txt", new byte[] {65}); - createFile(foo0123, "text.data", new byte[] {65}); - createFile(foobar, "test.baz", new byte[] {65}); + createFile(foo0123, "test.txt", new byte[]{65}); + createFile(foo0123, "text.data", new byte[]{65}); + createFile(foobar, "test.baz", new byte[]{65}); Files.delete(cleartextDirectory); } @@ -87,7 +92,7 @@ public void testDeleteDirectoryContainingLongNameFileWithoutMetadata() throws IO Path ciphertextDirectory = firstEmptyCiphertextDirectory(); Path longNameDir = createFolder(ciphertextDirectory, "HHEZJURE.c9s"); - createFile(longNameDir, Constants.CONTENTS_FILE_NAME, new byte[] {65}); + createFile(longNameDir, Constants.CONTENTS_FILE_NAME, new byte[]{65}); Files.delete(cleartextDirectory); } @@ -101,7 +106,7 @@ public void testDeleteDirectoryContainingUnauthenticLongNameDirectoryFile() thro Path ciphertextDirectory = firstEmptyCiphertextDirectory(); Path longNameDir = createFolder(ciphertextDirectory, "HHEZJURE.c9s"); createFile(longNameDir, Constants.INFLATED_FILE_NAME, "HHEZJUREHHEZJUREHHEZJURE".getBytes()); - createFile(longNameDir, Constants.CONTENTS_FILE_NAME, new byte[] {65}); + createFile(longNameDir, Constants.CONTENTS_FILE_NAME, new byte[]{65}); Files.delete(cleartextDirectory); } @@ -110,7 +115,7 @@ public void testDeleteDirectoryContainingUnauthenticLongNameDirectoryFile() thro public void testDeleteNonEmptyDir() throws IOException { Path cleartextDirectory = fileSystem.getPath("/d"); Files.createDirectory(cleartextDirectory); - createFile(cleartextDirectory, "test", new byte[] {65}); + createFile(cleartextDirectory, "test", new byte[]{65}); Assertions.assertThrows(DirectoryNotEmptyException.class, () -> { Files.delete(cleartextDirectory); @@ -118,14 +123,13 @@ public void testDeleteNonEmptyDir() throws IOException { } @Test - @Disabled // c9s not yet implemented public void testDeleteDirectoryContainingLongNamedDirectory() throws IOException { Path cleartextDirectory = fileSystem.getPath("/e"); Files.createDirectory(cleartextDirectory); // a // .. LongNameaaa... - String name = "LongName" + Strings.repeat("a", MAX_CIPHERTEXT_NAME_LENGTH); + String name = "LongName" + Strings.repeat("a", Constants.DEFAULT_SHORTENING_THRESHOLD); createFolder(cleartextDirectory, name); Assertions.assertThrows(DirectoryNotEmptyException.class, () -> { diff --git a/src/test/java/org/cryptomator/cryptofs/DirStructureTest.java b/src/test/java/org/cryptomator/cryptofs/DirStructureTest.java new file mode 100644 index 00000000..2cb27ddb --- /dev/null +++ b/src/test/java/org/cryptomator/cryptofs/DirStructureTest.java @@ -0,0 +1,75 @@ +package org.cryptomator.cryptofs; + +import org.cryptomator.cryptofs.common.Constants; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +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.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +public class DirStructureTest { + + private static final String KEY = "key"; + private static final String CONFIG = "config"; + + @TempDir + public Path vaultPath; + + @Test + public void testNonExistingVaultPathThrowsIOException() { + Path vaultPath = Path.of("this/certainly/does/not/exist"); + Assumptions.assumeTrue(Files.notExists(vaultPath)); + + Assertions.assertThrows(IOException.class, () -> DirStructure.checkDirStructure(vaultPath, CONFIG, KEY)); + } + + @Test + public void testNonDirectoryVaultPathThrowsIOException() throws IOException { + Path tmp = vaultPath.resolve("this"); + Files.createFile(tmp); + Assumptions.assumeTrue(Files.exists(tmp)); + + Assertions.assertThrows(IOException.class, () -> DirStructure.checkDirStructure(tmp, CONFIG, KEY)); + } + + @ParameterizedTest(name = "Testing all combinations of data dir, config and masterkey file existence.") + @MethodSource("provideAllCases") + public void testAllCombosOfDataAndConfigAndKey(boolean createDataDir, boolean createConfig, boolean createKey, DirStructure expectedResult) throws IOException { + Path keyPath = vaultPath.resolve(KEY); + Path configPath = vaultPath.resolve(CONFIG); + Path dataDir = vaultPath.resolve(Constants.DATA_DIR_NAME); + + if (createDataDir) { + Files.createDirectory(dataDir); + } + if (createConfig) { + Files.createFile(configPath); + } + if (createKey) { + Files.createFile(keyPath); + } + + Assertions.assertEquals(expectedResult, DirStructure.checkDirStructure(vaultPath, CONFIG, KEY)); + } + + public static Stream provideAllCases() { + return Stream.of( + Arguments.of(true, true, true, DirStructure.VAULT), + Arguments.of(true, true, false, DirStructure.VAULT), + Arguments.of(true, false, true, DirStructure.MAYBE_LEGACY), + Arguments.of(true, false, false, DirStructure.UNRELATED), + Arguments.of(false, false, false, DirStructure.UNRELATED), + Arguments.of(false, false, true, DirStructure.UNRELATED), + Arguments.of(false, true, false, DirStructure.UNRELATED), + Arguments.of(false, true, true, DirStructure.UNRELATED) + ); + } + +} diff --git a/src/test/java/org/cryptomator/cryptofs/DirectoryIdLoaderTest.java b/src/test/java/org/cryptomator/cryptofs/DirectoryIdLoaderTest.java index 036f2412..3943207a 100644 --- a/src/test/java/org/cryptomator/cryptofs/DirectoryIdLoaderTest.java +++ b/src/test/java/org/cryptomator/cryptofs/DirectoryIdLoaderTest.java @@ -7,10 +7,8 @@ import org.mockito.Mockito; import java.io.IOException; -import java.lang.reflect.Field; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; -import java.nio.channels.spi.AbstractInterruptibleChannel; import java.nio.file.FileSystem; import java.nio.file.NoSuchFileException; import java.nio.file.Path; @@ -62,10 +60,10 @@ public void testDirectoryIdForNonExistingFileIsNotEmpty() throws IOException { } @Test - public void testDirectoryIdIsReadFromExistingFile() throws IOException, ReflectiveOperationException { + public void testDirectoryIdIsReadFromExistingFile() throws IOException { String expectedId = "asdüßT°z¬╚‗"; byte[] expectedIdBytes = expectedId.getBytes(UTF_8); - FileChannel channel = createFileChannelMock(); + FileChannel channel = Mockito.mock(FileChannel.class); when(provider.newFileChannel(eq(dirFilePath), any())).thenReturn(channel); when(channel.size()).thenReturn((long) expectedIdBytes.length); when(channel.read(any(ByteBuffer.class))).then(invocation -> { @@ -80,8 +78,8 @@ public void testDirectoryIdIsReadFromExistingFile() throws IOException, Reflecti } @Test - public void testIOExceptionWhenExistingFileIsEmpty() throws IOException, ReflectiveOperationException { - FileChannel channel = createFileChannelMock(); + public void testIOExceptionWhenExistingFileIsEmpty() throws IOException { + FileChannel channel = Mockito.mock(FileChannel.class); when(provider.newFileChannel(eq(dirFilePath), any())).thenReturn(channel); when(channel.size()).thenReturn(0l); @@ -92,8 +90,8 @@ public void testIOExceptionWhenExistingFileIsEmpty() throws IOException, Reflect } @Test - public void testIOExceptionWhenExistingFileIsTooLarge() throws IOException, ReflectiveOperationException { - FileChannel channel = createFileChannelMock(); + public void testIOExceptionWhenExistingFileIsTooLarge() throws IOException { + FileChannel channel = Mockito.mock(FileChannel.class); when(provider.newFileChannel(eq(dirFilePath), any())).thenReturn(channel); when(channel.size()).thenReturn((long) Integer.MAX_VALUE); @@ -103,26 +101,4 @@ public void testIOExceptionWhenExistingFileIsTooLarge() throws IOException, Refl MatcherAssert.assertThat(e.getMessage(), containsString("Unexpectedly large directory file")); } - private FileChannel createFileChannelMock() throws ReflectiveOperationException { - FileChannel channel = Mockito.mock(FileChannel.class); - try { - Field channelOpenField = AbstractInterruptibleChannel.class.getDeclaredField("open"); - channelOpenField.setAccessible(true); - channelOpenField.set(channel, true); - } catch (NoSuchFieldException e) { - // field only declared in jdk8 - } - try { - Field channelClosedField = AbstractInterruptibleChannel.class.getDeclaredField("closed"); - channelClosedField.setAccessible(true); - channelClosedField.set(channel, false); - } catch (NoSuchFieldException e) { - // field only declared in jdk 9 - } - Field channelCloseLockField = AbstractInterruptibleChannel.class.getDeclaredField("closeLock"); - channelCloseLockField.setAccessible(true); - channelCloseLockField.set(channel, new Object()); - return channel; - } - } diff --git a/src/test/java/org/cryptomator/cryptofs/MoveOperationTest.java b/src/test/java/org/cryptomator/cryptofs/MoveOperationTest.java index 1ec8cb53..4744bb43 100644 --- a/src/test/java/org/cryptomator/cryptofs/MoveOperationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/MoveOperationTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; @@ -54,7 +55,7 @@ public void setup() { public void testMoveWithEqualPathDoesNothing() throws IOException { inTest.move(aPathFromFsA, aPathFromFsA); - verifyZeroInteractions(aPathFromFsA); + verifyNoInteractions(aPathFromFsA); } @Test diff --git a/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java b/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java index 9b7cf232..fc0037e5 100644 --- a/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java +++ b/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java @@ -8,9 +8,13 @@ *******************************************************************************/ package org.cryptomator.cryptofs; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.MasterkeyLoader; +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; import java.io.IOException; import java.net.URI; @@ -25,16 +29,24 @@ public class ReadmeCodeSamplesTest { @Test - public void testReadmeCodeSampleUsingFileSystemConstructionMethodA(@TempDir Path storageLocation) throws IOException { - FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(storageLocation, CryptoFileSystemProperties.cryptoFileSystemProperties().withPassphrase("password").build()); + public void testReadmeCodeSampleUsingFileSystemConstructionMethodA(@TempDir Path storageLocation) throws IOException, MasterkeyLoadingFailedException { + MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class); + Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64])); + CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build(); + CryptoFileSystemProvider.initialize(storageLocation, properties, URI.create("test:key")); + FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(storageLocation, properties); runCodeSample(fileSystem); } @Test - public void testReadmeCodeSampleUsingFileSystemConstructionMethodB(@TempDir Path storageLocation) throws IOException { + public void testReadmeCodeSampleUsingFileSystemConstructionMethodB(@TempDir Path storageLocation) throws IOException, MasterkeyLoadingFailedException { URI uri = CryptoFileSystemUri.create(storageLocation); - FileSystem fileSystem = FileSystems.newFileSystem(uri, CryptoFileSystemProperties.cryptoFileSystemProperties().withPassphrase("password").build()); + MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class); + Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64])); + CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build(); + CryptoFileSystemProvider.initialize(storageLocation, properties, URI.create("test:key")); + FileSystem fileSystem = FileSystems.newFileSystem(uri, properties); runCodeSample(fileSystem); } diff --git a/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java index 8cd282b1..6706ddf3 100644 --- a/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java @@ -8,12 +8,17 @@ *******************************************************************************/ package org.cryptomator.cryptofs; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.MasterkeyLoader; +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; import java.io.IOException; +import java.net.URI; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; @@ -29,10 +34,14 @@ public class RealFileSystemIntegrationTest { private static FileSystem fileSystem; @BeforeAll - public static void setupClass(@TempDir Path tmpDir) throws IOException { + public static void setupClass(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException { pathToVault = tmpDir.resolve("vault"); Files.createDirectory(pathToVault); - fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withPassphrase("asd").build()); + MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class); + Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64])); + CryptoFileSystemProperties properties = cryptoFileSystemProperties().withKeyLoader(keyLoader).build(); + CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key")); + fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), properties); } @Test diff --git a/src/test/java/org/cryptomator/cryptofs/RootDirectoryInitializerTest.java b/src/test/java/org/cryptomator/cryptofs/RootDirectoryInitializerTest.java deleted file mode 100644 index 02fd7364..00000000 --- a/src/test/java/org/cryptomator/cryptofs/RootDirectoryInitializerTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.cryptomator.cryptofs; - -import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.nio.file.Path; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; - -public class RootDirectoryInitializerTest { - - private final CryptoPathMapper cryptoPathMapper = mock(CryptoPathMapper.class); - private final ReadonlyFlag readonlyFlag = mock(ReadonlyFlag.class); - private final FilesWrapper filesWrapper = mock(FilesWrapper.class); - - private final CryptoPath cleartextRoot = mock(CryptoPath.class); - private final Path ciphertextRoot = mock(Path.class); - - private RootDirectoryInitializer inTest = new RootDirectoryInitializer(cryptoPathMapper, readonlyFlag, filesWrapper); - - @BeforeEach - public void setup() throws IOException { - when(cryptoPathMapper.getCiphertextDir(cleartextRoot)).thenReturn(new CiphertextDirectory("", ciphertextRoot)); - } - - @Test - public void testInitializeCreatesRootDirectoryIfReadonlyFlagIsNotSet() throws IOException { - when(readonlyFlag.isSet()).thenReturn(false); - - inTest.initialize(cleartextRoot); - - verify(filesWrapper).createDirectories(ciphertextRoot); - } - - @Test - public void testInitializeDoesNotCreateRootDirectoryIfReadonlyFlagIsSet() throws IOException { - when(readonlyFlag.isSet()).thenReturn(true); - - inTest.initialize(cleartextRoot); - - verifyZeroInteractions(filesWrapper); - } - -} diff --git a/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java b/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java new file mode 100644 index 00000000..a7abbda7 --- /dev/null +++ b/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java @@ -0,0 +1,118 @@ +package org.cryptomator.cryptofs; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.interfaces.Claim; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.interfaces.Verification; +import org.cryptomator.cryptofs.common.Constants; +import org.cryptomator.cryptolib.api.CryptorProvider; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.MasterkeyLoader; +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; + +import java.util.Arrays; + +public class VaultConfigTest { + + private MasterkeyLoader masterkeyLoader = Mockito.mock(MasterkeyLoader.class); + private byte[] rawKey = new byte[64]; + private Masterkey key = Mockito.mock(Masterkey.class); + + @BeforeEach + public void setup() throws MasterkeyLoadingFailedException { + Arrays.fill(rawKey, (byte) 0x55); + Mockito.when(masterkeyLoader.loadKey(Mockito.any())).thenReturn(key); + Mockito.when(key.getEncoded()).thenReturn(rawKey); + } + + @Test + public void testLoadMalformedToken() { + Assertions.assertThrows(VaultConfigLoadException.class, () -> { + VaultConfig.load("hello world", masterkeyLoader, 42); + }); + } + + @Nested + public class WithValidToken { + + private VaultConfig originalConfig; + private String token; + + @BeforeEach + public void setup() throws MasterkeyLoadingFailedException { + originalConfig = VaultConfig.createNew().cipherCombo(CryptorProvider.Scheme.SIV_CTRMAC).shorteningThreshold(220).build(); + token = originalConfig.toToken("TEST_KEY", rawKey); + } + + @Test + public void testSuccessfulLoad() throws VaultConfigLoadException, MasterkeyLoadingFailedException { + var loaded = VaultConfig.load(token, masterkeyLoader, originalConfig.getVaultVersion()); + + Assertions.assertEquals(originalConfig.getId(), loaded.getId()); + Assertions.assertEquals(originalConfig.getVaultVersion(), loaded.getVaultVersion()); + Assertions.assertEquals(originalConfig.getCipherCombo(), loaded.getCipherCombo()); + Assertions.assertEquals(originalConfig.getShorteningThreshold(), loaded.getShorteningThreshold()); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3, 10, 20, 30, 63}) + public void testLoadWithInvalidKey(int pos) { + rawKey[pos] = (byte) 0x77; + Mockito.when(key.getEncoded()).thenReturn(rawKey); + + Assertions.assertThrows(VaultKeyInvalidException.class, () -> { + VaultConfig.load(token, masterkeyLoader, originalConfig.getVaultVersion()); + }); + } + + } + + @Test + public void testCreateNew() { + var config = VaultConfig.createNew().cipherCombo(CryptorProvider.Scheme.SIV_CTRMAC).shorteningThreshold(220).build(); + + Assertions.assertNotNull(config.getId()); + Assertions.assertEquals(Constants.VAULT_VERSION, config.getVaultVersion()); + Assertions.assertEquals(CryptorProvider.Scheme.SIV_CTRMAC, config.getCipherCombo()); + Assertions.assertEquals(220, config.getShorteningThreshold()); + } + + @Test + public void testLoadExisting() throws VaultConfigLoadException, MasterkeyLoadingFailedException { + var decodedJwt = Mockito.mock(DecodedJWT.class); + var formatClaim = Mockito.mock(Claim.class); + var cipherComboClaim = Mockito.mock(Claim.class); + var maxFilenameLenClaim = Mockito.mock(Claim.class); + var key = Mockito.mock(Masterkey.class); + var verification = Mockito.mock(Verification.class); + var verifier = Mockito.mock(JWTVerifier.class); + Mockito.when(decodedJwt.getKeyId()).thenReturn("test:key"); + Mockito.when(decodedJwt.getClaim("format")).thenReturn(formatClaim); + Mockito.when(decodedJwt.getClaim("cipherCombo")).thenReturn(cipherComboClaim); + Mockito.when(decodedJwt.getClaim("shorteningThreshold")).thenReturn(maxFilenameLenClaim); + Mockito.when(key.getEncoded()).thenReturn(new byte[64]); + Mockito.when(verification.withClaim("format", 42)).thenReturn(verification); + Mockito.when(verification.build()).thenReturn(verifier); + Mockito.when(verifier.verify(decodedJwt)).thenReturn(decodedJwt); + Mockito.when(formatClaim.asInt()).thenReturn(42); + Mockito.when(cipherComboClaim.asString()).thenReturn("SIV_CTRMAC"); + Mockito.when(maxFilenameLenClaim.asInt()).thenReturn(220); + try (var jwtMock = Mockito.mockStatic(JWT.class)) { + jwtMock.when(() -> JWT.decode("jwt-vault-config")).thenReturn(decodedJwt); + jwtMock.when(() -> JWT.require(Mockito.any())).thenReturn(verification); + + var config = VaultConfig.load("jwt-vault-config", masterkeyLoader, 42); + Assertions.assertNotNull(config); + Assertions.assertEquals(42, config.getVaultVersion()); + } + } + +} \ No newline at end of file diff --git a/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java b/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java index ec60d06f..2e39d116 100644 --- a/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java +++ b/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java @@ -1,11 +1,16 @@ package org.cryptomator.cryptofs; import com.google.common.jimfs.Jimfs; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.MasterkeyLoader; +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import java.io.IOException; +import java.net.URI; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.FileSystem; @@ -27,11 +32,15 @@ public class WriteFileWhileReadonlyChannelIsOpenTest { private Path root; @BeforeEach - public void setup() throws IOException { + public void setup() throws IOException, MasterkeyLoadingFailedException { inMemoryFs = Jimfs.newFileSystem(); Path pathToVault = inMemoryFs.getRootDirectories().iterator().next().resolve("vault"); Files.createDirectory(pathToVault); - fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withPassphrase("asd").build()); + MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class); + Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64])); + CryptoFileSystemProperties properties = cryptoFileSystemProperties().withKeyLoader(keyLoader).build(); + CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key")); + fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), properties); root = fileSystem.getPath("/"); } diff --git a/src/test/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributesTest.java b/src/test/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributesTest.java index 87af1128..abda39d7 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributesTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributesTest.java @@ -40,6 +40,7 @@ public void setup() { Mockito.when(cryptor.fileContentCryptor()).thenReturn(contentCryptor); Mockito.when(contentCryptor.cleartextChunkSize()).thenReturn(32 * 1024); Mockito.when(contentCryptor.ciphertextChunkSize()).thenReturn(16 + 32 * 1024 + 32); + Mockito.doCallRealMethod().when(contentCryptor).cleartextSize(Mockito.anyLong()); ciphertextFilePath = Mockito.mock(Path.class, "ciphertextFile"); delegateAttr = Mockito.mock(BasicFileAttributes.class); diff --git a/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributesTest.java b/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributesTest.java index 05d6c2e8..31c3ffd5 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributesTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributesTest.java @@ -45,7 +45,7 @@ public void setup() { @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) @DisplayName("on read-write filesystem") - class ReadWriteFileSystem { + public class ReadWriteFileSystem { private CryptoDosFileAttributes inTest; @@ -96,7 +96,7 @@ public void testIsSystemDelegates(boolean value) { @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) @DisplayName("on read-only filesystem") - class ReadOnlyFileSystem { + public class ReadOnlyFileSystem { private CryptoDosFileAttributes inTest; diff --git a/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java index 70eb5834..ffb26afa 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java @@ -9,7 +9,11 @@ package org.cryptomator.cryptofs.attr; import com.google.common.jimfs.Jimfs; +import org.cryptomator.cryptofs.CryptoFileSystemProperties; import org.cryptomator.cryptofs.CryptoFileSystemProvider; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.MasterkeyLoader; +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.hamcrest.Matcher; @@ -19,8 +23,10 @@ import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import java.io.IOException; +import java.net.URI; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.FileSystem; @@ -39,7 +45,6 @@ import static java.lang.Boolean.TRUE; import static java.lang.System.currentTimeMillis; import static java.nio.file.Files.readAttributes; -import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemProperties; import static org.cryptomator.cryptofs.CryptoFileSystemUri.create; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.greaterThan; @@ -52,11 +57,15 @@ public class FileAttributeIntegrationTest { private static FileSystem fileSystem; @BeforeAll - public static void setupClass() throws IOException { + public static void setupClass() throws IOException, MasterkeyLoadingFailedException { inMemoryFs = Jimfs.newFileSystem(); pathToVault = inMemoryFs.getRootDirectories().iterator().next().resolve("vault"); Files.createDirectory(pathToVault); - fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withPassphrase("asd").build()); + MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class); + Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64])); + CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build(); + CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key")); + fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), properties); } @AfterAll @@ -187,12 +196,11 @@ public void testFileAttributeViewUpdatesAfterMove() throws IOException { } private static Matcher isAfter(FileTime previousFileTime) { - return new BaseMatcher() { + return new BaseMatcher<>() { @Override public boolean matches(Object item) { - if (item instanceof FileTime) { - FileTime subject = (FileTime) item; - return subject.compareTo(previousFileTime) > 0; + if (item instanceof FileTime ft) { + return ft.compareTo(previousFileTime) > 0; } else { return false; } diff --git a/src/test/java/org/cryptomator/cryptofs/ch/AsyncDelegatingFileChannelTest.java b/src/test/java/org/cryptomator/cryptofs/ch/AsyncDelegatingFileChannelTest.java index ce452646..9d10cea3 100644 --- a/src/test/java/org/cryptomator/cryptofs/ch/AsyncDelegatingFileChannelTest.java +++ b/src/test/java/org/cryptomator/cryptofs/ch/AsyncDelegatingFileChannelTest.java @@ -8,7 +8,6 @@ *******************************************************************************/ package org.cryptomator.cryptofs.ch; -import org.cryptomator.cryptofs.ch.AsyncDelegatingFileChannel; import org.hamcrest.CoreMatchers; import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; @@ -16,17 +15,13 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; import java.io.IOException; -import java.lang.reflect.Field; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.CompletionHandler; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; -import java.nio.channels.spi.AbstractInterruptibleChannel; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -44,38 +39,22 @@ public class AsyncDelegatingFileChannelTest { @BeforeEach public void setup() throws ReflectiveOperationException { channel = Mockito.mock(FileChannel.class); - try { - Field channelOpenField = AbstractInterruptibleChannel.class.getDeclaredField("open"); - channelOpenField.setAccessible(true); - channelOpenField.set(channel, true); - } catch (NoSuchFieldException e) { - // field only declared in jdk8 - } - try { - Field channelClosedField = AbstractInterruptibleChannel.class.getDeclaredField("closed"); - channelClosedField.setAccessible(true); - channelClosedField.set(channel, false); - } catch (NoSuchFieldException e) { - // field only declared in jdk 9 - } - Field channelCloseLockField = AbstractInterruptibleChannel.class.getDeclaredField("closeLock"); - channelCloseLockField.setAccessible(true); - channelCloseLockField.set(channel, new Object()); asyncChannel = new AsyncDelegatingFileChannel(channel, executor); } @Test - public void testIsOpen() throws IOException { + public void testIsOpen() { + Mockito.when(channel.isOpen()).thenReturn(true); Assertions.assertTrue(asyncChannel.isOpen()); - channel.close(); + + Mockito.when(channel.isOpen()).thenReturn(false); Assertions.assertFalse(asyncChannel.isOpen()); } @Test public void testClose() throws IOException { - Assertions.assertTrue(asyncChannel.isOpen()); asyncChannel.close(); - Assertions.assertFalse(asyncChannel.isOpen()); + Mockito.verify(channel).close(); } @Test @@ -112,21 +91,19 @@ public void testTryLock() throws IOException { public class LockTest { @Test - public void testSuccess() throws IOException, InterruptedException, ExecutionException { + public void testSuccess() throws IOException, InterruptedException { + Mockito.when(channel.isOpen()).thenReturn(true); final FileLock lock = Mockito.mock(FileLock.class); - Mockito.when(channel.lock(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyBoolean())).thenAnswer(new Answer() { - @Override - public FileLock answer(InvocationOnMock invocation) throws Throwable { - Thread.sleep(100); - return lock; - } + Mockito.when(channel.lock(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyBoolean())).thenAnswer(invocation -> { + Thread.sleep(100); + return lock; }); CountDownLatch cdl = new CountDownLatch(1); AtomicReference result = new AtomicReference<>(); AtomicReference attachment = new AtomicReference<>(); AtomicReference exception = new AtomicReference<>(); - asyncChannel.lock(123l, 234l, true, "bam", new CompletionHandler() { + asyncChannel.lock(123l, 234l, true, "bam", new CompletionHandler<>() { @Override public void completed(FileLock r, String a) { @@ -162,18 +139,16 @@ public void testClosed() throws Throwable { @Test public void testExecutionException() throws Throwable { - Mockito.when(channel.lock(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyBoolean())).thenAnswer(new Answer() { - @Override - public FileLock answer(InvocationOnMock invocation) throws Throwable { - throw new java.lang.ArithmeticException("fail"); - } + Mockito.when(channel.isOpen()).thenReturn(true); + Mockito.when(channel.lock(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyBoolean())).thenAnswer(invocation -> { + throw new ArithmeticException("fail"); }); CountDownLatch cdl = new CountDownLatch(1); AtomicReference result = new AtomicReference<>(); AtomicReference attachment = new AtomicReference<>(); AtomicReference exception = new AtomicReference<>(); - asyncChannel.lock(123l, 234l, true, "bam", new CompletionHandler() { + asyncChannel.lock(123l, 234l, true, "bam", new CompletionHandler<>() { @Override public void completed(FileLock r, String a) { @@ -204,16 +179,14 @@ public void failed(Throwable e, String a) { public class ReadTest { @Test - public void testSuccess() throws IOException, InterruptedException, ExecutionException { - Mockito.when(channel.read(Mockito.any(), Mockito.anyLong())).thenAnswer(new Answer() { - @Override - public Integer answer(InvocationOnMock invocation) throws Throwable { - ByteBuffer dst = invocation.getArgument(0); - Thread.sleep(100); - int read = dst.remaining(); - dst.position(dst.position() + read); - return read; - } + public void testSuccess() throws IOException, InterruptedException { + Mockito.when(channel.isOpen()).thenReturn(true); + Mockito.when(channel.read(Mockito.any(), Mockito.anyLong())).thenAnswer(invocation -> { + ByteBuffer dst = invocation.getArgument(0); + Thread.sleep(100); + int read = dst.remaining(); + dst.position(dst.position() + read); + return read; }); CountDownLatch cdl = new CountDownLatch(1); @@ -221,7 +194,7 @@ public Integer answer(InvocationOnMock invocation) throws Throwable { AtomicReference attachment = new AtomicReference<>(); AtomicReference exception = new AtomicReference<>(); ByteBuffer buf = ByteBuffer.allocate(42); - asyncChannel.read(buf, 0l, "bam", new CompletionHandler() { + asyncChannel.read(buf, 0l, "bam", new CompletionHandler<>() { @Override public void completed(Integer r, String a) { @@ -261,16 +234,14 @@ public void testClosed() throws Throwable { public class WriteTest { @Test - public void testSuccess() throws IOException, InterruptedException, ExecutionException { - Mockito.when(channel.write(Mockito.any(), Mockito.anyLong())).thenAnswer(new Answer() { - @Override - public Integer answer(InvocationOnMock invocation) throws Throwable { - ByteBuffer dst = invocation.getArgument(0); - Thread.sleep(100); - int read = dst.remaining(); - dst.position(dst.position() + read); - return read; - } + public void testSuccess() throws IOException, InterruptedException { + Mockito.when(channel.isOpen()).thenReturn(true); + Mockito.when(channel.write(Mockito.any(), Mockito.anyLong())).thenAnswer(invocation -> { + ByteBuffer dst = invocation.getArgument(0); + Thread.sleep(100); + int read = dst.remaining(); + dst.position(dst.position() + read); + return read; }); CountDownLatch cdl = new CountDownLatch(1); @@ -278,7 +249,7 @@ public Integer answer(InvocationOnMock invocation) throws Throwable { AtomicReference attachment = new AtomicReference<>(); AtomicReference exception = new AtomicReference<>(); ByteBuffer buf = ByteBuffer.allocate(42); - asyncChannel.write(buf, 0l, "bam", new CompletionHandler() { + asyncChannel.write(buf, 0l, "bam", new CompletionHandler<>() { @Override public void completed(Integer r, String a) { diff --git a/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java b/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java index b38f4458..2856e31f 100644 --- a/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java +++ b/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java @@ -252,7 +252,7 @@ public void testMapThrowsUnsupportedOperationException() throws IOException { } @Nested - class Locking { + public class Locking { private FileLock delegate = Mockito.mock(FileLock.class); diff --git a/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileLockTest.java b/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileLockTest.java index 64ba9ed4..e439be59 100644 --- a/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileLockTest.java +++ b/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileLockTest.java @@ -21,16 +21,19 @@ public class CleartextFileLockTest { @BeforeEach public void setup() { - channel = Mockito.spy(new DummyFileChannel()); + channel = Mockito.mock(FileChannel.class); + delegate = Mockito.mock(FileLock.class); + Mockito.when(channel.isOpen()).thenReturn(true); } @Nested @DisplayName("Shared Locks") - class ValidSharedLockTests { + public class SharedLockTests { @BeforeEach public void setup() { - delegate = Mockito.spy(new FileLockMock(channel, position, size, true)); + Mockito.when(delegate.isValid()).thenReturn(true); + Mockito.when(delegate.isShared()).thenReturn(true); inTest = new CleartextFileLock(channel, delegate, position, size); } @@ -72,11 +75,12 @@ public void testIsValid() { @Nested @DisplayName("After releasing the lock") - class ReleasedLock { + public class ReleasedLock { @BeforeEach public void setup() throws IOException { inTest.release(); + Mockito.when(delegate.isValid()).thenReturn(false); } @Test @@ -95,11 +99,11 @@ public void testReleaseDelegate() throws IOException { @Nested @DisplayName("After closing the channel") - class ClosedChannel { + public class ClosedChannel { @BeforeEach public void setup() throws IOException { - channel.close(); + Mockito.when(channel.isOpen()).thenReturn(false); } @Test @@ -114,11 +118,12 @@ public void testIsValid() { @Nested @DisplayName("Exclusive Locks") - class InvalidSharedLockTests { + public class ExclusiveLockTests { @BeforeEach public void setup() { - delegate = Mockito.spy(new FileLockMock(channel, position, size, false)); + Mockito.when(delegate.isValid()).thenReturn(true); + Mockito.when(delegate.isShared()).thenReturn(false); inTest = new CleartextFileLock(channel, delegate, position, size); } @@ -160,11 +165,12 @@ public void testIsValid() { @Nested @DisplayName("After releasing the lock") - class ReleasedLock { + public class ReleasedLock { @BeforeEach public void setup() throws IOException { inTest.release(); + Mockito.when(delegate.isValid()).thenReturn(false); } @Test @@ -183,11 +189,11 @@ public void testReleaseDelegate() throws IOException { @Nested @DisplayName("After closing the channel") - class ClosedChannel { + public class ClosedChannel { @BeforeEach public void setup() throws IOException { - channel.close(); + Mockito.when(channel.isOpen()).thenReturn(false); } @Test @@ -200,24 +206,4 @@ public void testIsValid() { } - private static class FileLockMock extends FileLock { - - private boolean valid; - - protected FileLockMock(FileChannel channel, long position, long size, boolean shared) { - super(channel, position, size, shared); - this.valid = true; - } - - @Override - public boolean isValid() { - return valid; - } - - @Override - public void release() { - valid = false; - } - } - } diff --git a/src/test/java/org/cryptomator/cryptofs/ch/DummyFileChannel.java b/src/test/java/org/cryptomator/cryptofs/ch/DummyFileChannel.java deleted file mode 100644 index ab9c5abc..00000000 --- a/src/test/java/org/cryptomator/cryptofs/ch/DummyFileChannel.java +++ /dev/null @@ -1,97 +0,0 @@ -package org.cryptomator.cryptofs.ch; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; -import java.nio.channels.ReadableByteChannel; -import java.nio.channels.WritableByteChannel; - -class DummyFileChannel extends FileChannel { - - @Override - public int read(ByteBuffer dst) throws IOException { - return 0; - } - - @Override - public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { - return 0; - } - - @Override - public int write(ByteBuffer src) throws IOException { - return 0; - } - - @Override - public long write(ByteBuffer[] srcs, int offset, int length) throws IOException { - return 0; - } - - @Override - public long position() throws IOException { - return 0; - } - - @Override - public FileChannel position(long newPosition) throws IOException { - return null; - } - - @Override - public long size() throws IOException { - return 0; - } - - @Override - public FileChannel truncate(long size) throws IOException { - return null; - } - - @Override - public void force(boolean metaData) throws IOException { - } - - @Override - public long transferTo(long position, long count, WritableByteChannel target) throws IOException { - return 0; - } - - @Override - public long transferFrom(ReadableByteChannel src, long position, long count) throws IOException { - return 0; - } - - @Override - public int read(ByteBuffer dst, long position) throws IOException { - return 0; - } - - @Override - public int write(ByteBuffer src, long position) throws IOException { - return 0; - } - - @Override - public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException { - return null; - } - - @Override - public FileLock lock(long position, long size, boolean shared) throws IOException { - return null; - } - - @Override - public FileLock tryLock(long position, long size, boolean shared) throws IOException { - return null; - } - - @Override - protected void implCloseChannel() throws IOException { - } - -} - diff --git a/src/test/java/org/cryptomator/cryptofs/common/FileSystemCapabilityCheckerTest.java b/src/test/java/org/cryptomator/cryptofs/common/FileSystemCapabilityCheckerTest.java index 3a58f8f0..1c363eaf 100644 --- a/src/test/java/org/cryptomator/cryptofs/common/FileSystemCapabilityCheckerTest.java +++ b/src/test/java/org/cryptomator/cryptofs/common/FileSystemCapabilityCheckerTest.java @@ -3,12 +3,13 @@ import org.cryptomator.cryptofs.mocks.DirectoryStreamMock; import org.cryptomator.cryptofs.mocks.SeekableByteChannelMock; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; 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.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.mockito.Mockito; import java.io.IOException; @@ -18,11 +19,11 @@ import java.nio.file.spi.FileSystemProvider; import java.util.Collections; -class FileSystemCapabilityCheckerTest { +public class FileSystemCapabilityCheckerTest { @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) - class PathLengthLimits { + public class PathLengthLimits { private Path pathToVault = Mockito.mock(Path.class); private Path cDir = Mockito.mock(Path.class); @@ -62,7 +63,7 @@ public void testUnlimitedLength() throws IOException { return checkDirMock; }); - int determinedLimit = new FileSystemCapabilityChecker().determineSupportedFileNameLength(pathToVault); + int determinedLimit = new FileSystemCapabilityChecker().determineSupportedCiphertextFileNameLength(pathToVault); Assertions.assertEquals(220, determinedLimit); } @@ -95,7 +96,7 @@ public void testLimitedLengthDuringDirListing() throws IOException { return checkDirMock; }); - int determinedLimit = new FileSystemCapabilityChecker().determineSupportedFileNameLength(pathToVault); + int determinedLimit = new FileSystemCapabilityChecker().determineSupportedCiphertextFileNameLength(pathToVault); Assertions.assertEquals(limit, determinedLimit); } @@ -125,10 +126,23 @@ public void testLimitedLengthDuringFileCreation() throws IOException { return checkDirMock; }); - int determinedLimit = new FileSystemCapabilityChecker().determineSupportedFileNameLength(pathToVault); + int determinedLimit = new FileSystemCapabilityChecker().determineSupportedCiphertextFileNameLength(pathToVault); Assertions.assertEquals(limit, determinedLimit); } + + @DisplayName("determineSupportedCleartextFileNameLength(...)") + @ParameterizedTest(name = "ciphertext length {0} -> cleartext length {1}") + @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); + } } diff --git a/src/test/java/org/cryptomator/cryptofs/common/MasterkeyBackupHelperTest.java b/src/test/java/org/cryptomator/cryptofs/common/MasterkeyBackupHelperTest.java index f4b4c15a..20467f00 100644 --- a/src/test/java/org/cryptomator/cryptofs/common/MasterkeyBackupHelperTest.java +++ b/src/test/java/org/cryptomator/cryptofs/common/MasterkeyBackupHelperTest.java @@ -15,7 +15,7 @@ import java.util.Random; import java.util.stream.Stream; -class MasterkeyBackupHelperTest { +public class MasterkeyBackupHelperTest { @EnabledOnOs({OS.LINUX, OS.MAC}) @ParameterizedTest @@ -47,7 +47,7 @@ public void testBackupFileWin(byte[] contents, @TempDir Path tmp) throws IOExcep Assertions.assertEquals(backupFile, backupFile2); } - static Stream createRandomBytes() { + public static Stream createRandomBytes() { Random rnd = new Random(42l); return Stream.generate(() -> { byte[] bytes = new byte[100]; diff --git a/src/test/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilterTest.java b/src/test/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilterTest.java index 1741b73b..e18810e1 100644 --- a/src/test/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilterTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilterTest.java @@ -11,7 +11,7 @@ import java.nio.file.Path; import java.util.stream.Stream; -class BrokenDirectoryFilterTest { +public class BrokenDirectoryFilterTest { private CryptoPathMapper cryptoPathMapper = Mockito.mock(CryptoPathMapper.class); private BrokenDirectoryFilter brokenDirectoryFilter = new BrokenDirectoryFilter(cryptoPathMapper); diff --git a/src/test/java/org/cryptomator/cryptofs/dir/C9SInflatorTest.java b/src/test/java/org/cryptomator/cryptofs/dir/C9SInflatorTest.java index 1c771b0c..e0ec9c00 100644 --- a/src/test/java/org/cryptomator/cryptofs/dir/C9SInflatorTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/C9SInflatorTest.java @@ -13,7 +13,7 @@ import java.nio.file.Paths; import java.util.stream.Stream; -class C9SInflatorTest { +public class C9SInflatorTest { private LongFileNameProvider longFileNameProvider; private Cryptor cryptor; @@ -30,7 +30,7 @@ public void setup() { } @Test - public void inflateDeflated() throws IOException { + public void inflateDeflated() throws IOException, AuthenticationFailedException { Node deflated = new Node(Paths.get("foo.c9s")); Mockito.when(longFileNameProvider.inflate(deflated.ciphertextPath)).thenReturn("foo.c9r"); Mockito.when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.eq("foo"), Mockito.any())).thenReturn("hello world.txt"); @@ -52,7 +52,7 @@ public void inflateUninflatableDueToIOException() throws IOException { } @Test - public void inflateUninflatableDueToInvalidCiphertext() throws IOException { + public void inflateUninflatableDueToInvalidCiphertext() throws IOException, AuthenticationFailedException { Node deflated = new Node(Paths.get("foo.c9s")); Mockito.when(longFileNameProvider.inflate(deflated.ciphertextPath)).thenReturn("foo.c9r"); Mockito.when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.eq("foo"), Mockito.any())).thenThrow(new AuthenticationFailedException("peng!")); diff --git a/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java b/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java index 2e3944ea..fc868950 100644 --- a/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java @@ -1,5 +1,6 @@ package org.cryptomator.cryptofs.dir; +import org.cryptomator.cryptofs.VaultConfig; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileNameCryptor; import org.junit.jupiter.api.Assertions; @@ -7,6 +8,7 @@ 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; @@ -16,18 +18,21 @@ import java.nio.file.Paths; import java.util.stream.Stream; -class C9rConflictResolverTest { +public class C9rConflictResolverTest { private Cryptor cryptor; private FileNameCryptor fileNameCryptor; + private VaultConfig vaultConfig; private C9rConflictResolver conflictResolver; @BeforeEach public void setup() { cryptor = Mockito.mock(Cryptor.class); fileNameCryptor = Mockito.mock(FileNameCryptor.class); + vaultConfig = Mockito.mock(VaultConfig.class); Mockito.when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor); - conflictResolver = new C9rConflictResolver(cryptor, "foo"); + Mockito.when(vaultConfig.getShorteningThreshold()).thenReturn(44); // results in max cleartext size = 14 + conflictResolver = new C9rConflictResolver(cryptor, "foo", vaultConfig); } @Test @@ -72,6 +77,25 @@ public void testResolveConflictingFileByChoosingNewName(@TempDir Path dir) throw Assertions.assertFalse(Files.exists(unresolved.ciphertextPath)); } + @Test + public void testResolveConflictingFileByChoosingNewLengthLimitedName(@TempDir Path dir) throws IOException { + Files.createFile(dir.resolve("foo (1).c9r")); + Files.createFile(dir.resolve("foo.c9r")); + Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn("baz"); + Node unresolved = new Node(dir.resolve("foo (1).c9r")); + unresolved.cleartextName = "hello world.txt"; + unresolved.extractedCiphertext = "foo"; + + Stream result = conflictResolver.process(unresolved); + Node resolved = result.findAny().get(); + + Assertions.assertNotEquals(unresolved, resolved); + Assertions.assertEquals("baz.c9r", resolved.fullCiphertextFileName); + Assertions.assertEquals("hello (1).txt", resolved.cleartextName); + Assertions.assertTrue(Files.exists(resolved.ciphertextPath)); + Assertions.assertFalse(Files.exists(unresolved.ciphertextPath)); + } + @Test public void testResolveConflictingFileTrivially(@TempDir Path dir) throws IOException { Files.createFile(dir.resolve("foo (1).c9r")); diff --git a/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java b/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java index 3c1e0ba6..05455820 100644 --- a/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java @@ -15,7 +15,7 @@ import java.util.Optional; import java.util.stream.Stream; -class C9rDecryptorTest { +public class C9rDecryptorTest { private Cryptor cryptor; private FileNameCryptor fileNameCryptor; @@ -58,7 +58,7 @@ public void testInvalidBase64Pattern(String input) { @Test @DisplayName("process canonical filename") - public void testProcessFullMatch() { + public void testProcessFullMatch() throws AuthenticationFailedException { Mockito.when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn("helloWorld.txt"); Node input = new Node(Paths.get("aaaaBBBBccccDDDDeeeeFFFF.c9r")); @@ -81,7 +81,7 @@ public void testProcessFullMatch() { "foo_aaaaBBBBcccc_--_11112222_foo.c9r", "aaaaBBBBccccDDDDeeeeFFFF___aaaaBBBBcccc_--_11112222----aaaaBBBBccccDDDDeeeeFFFF.c9r", }) - public void testProcessPartialMatch(String filename) { + public void testProcessPartialMatch(String filename) throws AuthenticationFailedException { Mockito.when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).then(invocation -> { String ciphertext = invocation.getArgument(1); if (ciphertext.equals("aaaaBBBBcccc_--_11112222")) { @@ -107,7 +107,7 @@ public void testProcessPartialMatch(String filename) { "aaaaBBBB$$$$DDDDeeeeFFFF.c9r", "aaaaBBBBxxxxDDDDeeeeFFFF.c9r", }) - public void testProcessNoMatch(String filename) { + public void testProcessNoMatch(String filename) throws AuthenticationFailedException { Mockito.when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenThrow(new AuthenticationFailedException("Invalid ciphertext.")); Node input = new Node(Paths.get(filename)); diff --git a/src/test/java/org/cryptomator/cryptofs/fh/ChunkCacheTest.java b/src/test/java/org/cryptomator/cryptofs/fh/ChunkCacheTest.java index a15bf342..2e5bc541 100644 --- a/src/test/java/org/cryptomator/cryptofs/fh/ChunkCacheTest.java +++ b/src/test/java/org/cryptomator/cryptofs/fh/ChunkCacheTest.java @@ -2,16 +2,12 @@ import org.cryptomator.cryptofs.CryptoFileSystemStats; import org.cryptomator.cryptolib.api.AuthenticationFailedException; -import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import java.io.IOException; -import java.util.List; -import static java.util.Arrays.asList; -import static org.hamcrest.Matchers.containsInAnyOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -26,7 +22,7 @@ public class ChunkCacheTest { private final ChunkCache inTest = new ChunkCache(chunkLoader, chunkSaver, stats); @Test - public void testGetInvokesLoaderIfEntryNotInCache() throws IOException { + public void testGetInvokesLoaderIfEntryNotInCache() throws IOException, AuthenticationFailedException { long index = 42L; ChunkData data = mock(ChunkData.class); when(chunkLoader.load(index)).thenReturn(data); @@ -36,7 +32,7 @@ public void testGetInvokesLoaderIfEntryNotInCache() throws IOException { } @Test - public void testGetDoesNotInvokeLoaderIfEntryInCacheFromPreviousGet() throws IOException { + public void testGetDoesNotInvokeLoaderIfEntryInCacheFromPreviousGet() throws IOException, AuthenticationFailedException { long index = 42L; ChunkData data = mock(ChunkData.class); when(chunkLoader.load(index)).thenReturn(data); @@ -58,7 +54,7 @@ public void testGetDoesNotInvokeLoaderIfEntryInCacheFromPreviousSet() throws IOE } @Test - public void testGetInvokesSaverIfMaxEntriesInCacheAreReachedAndAnEntryNotInCacheIsRequested() throws IOException { + public void testGetInvokesSaverIfMaxEntriesInCacheAreReachedAndAnEntryNotInCacheIsRequested() throws IOException, AuthenticationFailedException { long firstIndex = 42L; long indexNotInCache = 40L; ChunkData firstData = mock(ChunkData.class); @@ -108,7 +104,7 @@ public void testGetInvokesSaverIfMaxEntriesInCacheAreReachedAndAnEntryInCacheIsS } @Test - public void testGetRethrowsAuthenticationFailedExceptionFromLoader() throws IOException { + public void testGetRethrowsAuthenticationFailedExceptionFromLoader() throws IOException, AuthenticationFailedException { long index = 42L; AuthenticationFailedException authenticationFailedException = new AuthenticationFailedException("Foo"); when(chunkLoader.load(index)).thenThrow(authenticationFailedException); @@ -120,7 +116,7 @@ public void testGetRethrowsAuthenticationFailedExceptionFromLoader() throws IOEx } @Test - public void testGetThrowsUncheckedExceptionFromLoader() throws IOException { + public void testGetThrowsUncheckedExceptionFromLoader() throws IOException, AuthenticationFailedException { long index = 42L; RuntimeException uncheckedException = new RuntimeException(); when(chunkLoader.load(index)).thenThrow(uncheckedException); @@ -132,7 +128,7 @@ public void testGetThrowsUncheckedExceptionFromLoader() throws IOException { } @Test - public void testInvalidateAllInvokesSaverForAllEntriesInCache() throws IOException { + public void testInvalidateAllInvokesSaverForAllEntriesInCache() throws IOException, AuthenticationFailedException { long index = 42L; long index2 = 43L; ChunkData data = mock(ChunkData.class); @@ -148,16 +144,7 @@ public void testInvalidateAllInvokesSaverForAllEntriesInCache() throws IOExcepti } @Test - @SuppressWarnings("unchecked") - public void testLoaderThrowsOnlyIOException() throws NoSuchMethodException { - List> exceptionsThrownByLoader = asList(ChunkLoader.class.getMethod("load", Long.class).getExceptionTypes()); - - // INFO: when adding exception types here add a corresponding test like testGetRethrowsIOExceptionFromLoader - MatcherAssert.assertThat(exceptionsThrownByLoader, containsInAnyOrder(IOException.class)); - } - - @Test - public void testGetRethrowsIOExceptionFromLoader() throws IOException { + public void testGetRethrowsIOExceptionFromLoader() throws IOException, AuthenticationFailedException { long index = 42L; IOException ioException = new IOException(); when(chunkLoader.load(index)).thenThrow(ioException); diff --git a/src/test/java/org/cryptomator/cryptofs/fh/ChunkLoaderTest.java b/src/test/java/org/cryptomator/cryptofs/fh/ChunkLoaderTest.java index 498e1b24..0705da4c 100644 --- a/src/test/java/org/cryptomator/cryptofs/fh/ChunkLoaderTest.java +++ b/src/test/java/org/cryptomator/cryptofs/fh/ChunkLoaderTest.java @@ -1,6 +1,7 @@ package org.cryptomator.cryptofs.fh; import org.cryptomator.cryptofs.CryptoFileSystemStats; +import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileContentCryptor; import org.cryptomator.cryptolib.api.FileHeader; @@ -49,7 +50,7 @@ public void setup() throws IOException { } @Test - public void testChunkLoaderReturnsEmptyDataOfChunkAfterEndOfFile() throws IOException { + public void testChunkLoaderReturnsEmptyDataOfChunkAfterEndOfFile() throws IOException, AuthenticationFailedException { long chunkIndex = 482L; long chunkOffset = chunkIndex * CIPHERTEXT_CHUNK_SIZE + HEADER_SIZE; when(chunkIO.read(argThat(hasAtLeastRemaining(CIPHERTEXT_CHUNK_SIZE)), eq(chunkOffset))).thenReturn(-1); @@ -63,7 +64,7 @@ public void testChunkLoaderReturnsEmptyDataOfChunkAfterEndOfFile() throws IOExce } @Test - public void testChunkLoaderReturnsDecryptedDataOfChunkInsideFile() throws IOException { + public void testChunkLoaderReturnsDecryptedDataOfChunkInsideFile() throws IOException, AuthenticationFailedException { long chunkIndex = 482L; long chunkOffset = chunkIndex * CIPHERTEXT_CHUNK_SIZE + HEADER_SIZE; Supplier decryptedData = () -> repeat(9).times(CLEARTEXT_CHUNK_SIZE).asByteBuffer(); @@ -81,7 +82,7 @@ public void testChunkLoaderReturnsDecryptedDataOfChunkInsideFile() throws IOExce } @Test - public void testChunkLoaderReturnsDecrytedDataOfChunkContainingEndOfFile() throws IOException { + public void testChunkLoaderReturnsDecrytedDataOfChunkContainingEndOfFile() throws IOException, AuthenticationFailedException { long chunkIndex = 482L; long chunkOffset = chunkIndex * CIPHERTEXT_CHUNK_SIZE + HEADER_SIZE; Supplier decryptedData = () -> repeat(9).times(CLEARTEXT_CHUNK_SIZE - 3).asByteBuffer(); diff --git a/src/test/java/org/cryptomator/cryptofs/fh/ChunkSaverTest.java b/src/test/java/org/cryptomator/cryptofs/fh/ChunkSaverTest.java index 3b8b2830..c2a669ae 100644 --- a/src/test/java/org/cryptomator/cryptofs/fh/ChunkSaverTest.java +++ b/src/test/java/org/cryptomator/cryptofs/fh/ChunkSaverTest.java @@ -18,6 +18,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.mockito.hamcrest.MockitoHamcrest.argThat; @@ -87,8 +88,8 @@ public void testChunkThatWasNotWrittenIsNotWritten() throws IOException { inTest.save(chunkIndex, chunkData); - verifyZeroInteractions(chunkIO); - verifyZeroInteractions(stats); + verifyNoInteractions(chunkIO); + verifyNoInteractions(stats); } @Test diff --git a/src/test/java/org/cryptomator/cryptofs/fh/FileHeaderHolderTest.java b/src/test/java/org/cryptomator/cryptofs/fh/FileHeaderHolderTest.java index 1f22043f..bad4f2cd 100644 --- a/src/test/java/org/cryptomator/cryptofs/fh/FileHeaderHolderTest.java +++ b/src/test/java/org/cryptomator/cryptofs/fh/FileHeaderHolderTest.java @@ -1,5 +1,6 @@ package org.cryptomator.cryptofs.fh; +import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileHeader; import org.cryptomator.cryptolib.api.FileHeaderCryptor; @@ -23,7 +24,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -class FileHeaderHolderTest { +public class FileHeaderHolderTest { static { System.setProperty("org.slf4j.simpleLogger.log.org.cryptomator.cryptofs.ch.FileHeaderHolder", "trace"); @@ -45,13 +46,13 @@ public void setup() throws IOException { @Nested @DisplayName("existing header") - class ExistingHeader { + public class ExistingHeader { private FileHeader headerToLoad = Mockito.mock(FileHeader.class); private FileChannel channel = Mockito.mock(FileChannel.class); @BeforeEach - public void setup() throws IOException { + public void setup() throws IOException, AuthenticationFailedException { byte[] headerBytes = "leHeader".getBytes(StandardCharsets.US_ASCII); when(fileHeaderCryptor.headerSize()).thenReturn(headerBytes.length); when(channel.read(Mockito.any(ByteBuffer.class), Mockito.eq(0l))).thenAnswer(invocation -> { @@ -65,7 +66,7 @@ public void setup() throws IOException { @Test @DisplayName("load") - public void testLoadExisting() throws IOException { + public void testLoadExisting() throws IOException, AuthenticationFailedException { FileHeader loadedHeader1 = inTest.loadExisting(channel); FileHeader loadedHeader2 = inTest.get(); FileHeader loadedHeader3 = inTest.get(); @@ -80,7 +81,7 @@ public void testLoadExisting() throws IOException { @Nested @DisplayName("new header") - class NewHeader { + public class NewHeader { private FileHeader headerToCreate = Mockito.mock(FileHeader.class); @@ -90,7 +91,7 @@ public void setup() throws IOException { } @AfterEach - public void tearDown() { + public void tearDown() throws AuthenticationFailedException { verify(fileHeaderCryptor, Mockito.never()).decryptHeader(Mockito.any()); } diff --git a/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java b/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java index f424d770..13873697 100644 --- a/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java +++ b/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java @@ -90,7 +90,7 @@ public void testCloseImmediatelyIfOpeningFirstChannelFails() { @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @DisplayName("FileChannels") - class FileChannelFactoryTest { + public class FileChannelFactoryTest { private OpenCryptoFile openCryptoFile; private CleartextFileChannel cleartextFileChannel; diff --git a/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFilesTest.java b/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFilesTest.java index c49de017..a60e65af 100644 --- a/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFilesTest.java +++ b/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFilesTest.java @@ -9,10 +9,8 @@ import javax.inject.Provider; import java.io.IOException; -import java.lang.reflect.Field; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; -import java.nio.channels.spi.AbstractInterruptibleChannel; import java.nio.charset.StandardCharsets; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Path; @@ -40,9 +38,6 @@ public void setup() throws IOException, ReflectiveOperationException { Mockito.when(openCryptoFileComponentBuilder.build()).thenReturn(subComponent); Mockito.when(file.newFileChannel(Mockito.any())).thenReturn(ciphertextFileChannel); - Field closeLockField = AbstractInterruptibleChannel.class.getDeclaredField("closeLock"); - closeLockField.setAccessible(true); - closeLockField.set(ciphertextFileChannel, new Object()); inTest = new OpenCryptoFiles(openCryptoFileComponentBuilderProvider); } diff --git a/src/test/java/org/cryptomator/cryptofs/migration/MigrationComponentTest.java b/src/test/java/org/cryptomator/cryptofs/migration/MigrationComponentTest.java deleted file mode 100644 index fb43df89..00000000 --- a/src/test/java/org/cryptomator/cryptofs/migration/MigrationComponentTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.cryptomator.cryptofs.migration; - -import org.cryptomator.cryptofs.mocks.NullSecureRandom; -import org.cryptomator.cryptolib.Cryptors; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.security.NoSuchAlgorithmException; - -public class MigrationComponentTest { - - @Test - public void testAvailableMigrators() throws NoSuchAlgorithmException { - MigrationModule migrationModule = new MigrationModule(Cryptors.version1(NullSecureRandom.INSTANCE)); - TestMigrationComponent comp = DaggerTestMigrationComponent.builder().migrationModule(migrationModule).build(); - Assertions.assertFalse(comp.availableMigrators().isEmpty()); - } - -} diff --git a/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java b/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java index e4391c22..5cb2295e 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java @@ -5,115 +5,233 @@ *******************************************************************************/ package org.cryptomator.cryptofs.migration; +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.MigrationContinuationListener.ContinuationResult; import org.cryptomator.cryptofs.migration.api.MigrationProgressListener; import org.cryptomator.cryptofs.migration.api.Migrator; import org.cryptomator.cryptofs.migration.api.NoApplicableMigratorException; -import org.cryptomator.cryptolib.api.InvalidPassphraseException; +import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; 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; -import java.nio.ByteBuffer; -import java.nio.channels.SeekableByteChannel; 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.HashMap; +import java.util.Map; public class MigratorsTest { - private ByteBuffer keyFile; private Path pathToVault; + private Path vaultConfigPath; + private Path masterkeyPath; private FileSystemCapabilityChecker fsCapabilityChecker; @BeforeEach - public void setup() throws IOException { - keyFile = StandardCharsets.UTF_8.encode("{\"version\": 0000}"); - pathToVault = Mockito.mock(Path.class); + public void setup(@TempDir Path tmpDir) { + pathToVault = tmpDir; + vaultConfigPath = tmpDir.resolve("vault.cryptomator"); + masterkeyPath = tmpDir.resolve("masterkey.cryptomator"); fsCapabilityChecker = Mockito.mock(FileSystemCapabilityChecker.class); - - Path pathToMasterkey = Mockito.mock(Path.class); - FileSystem fs = Mockito.mock(FileSystem.class); - FileSystemProvider provider = Mockito.mock(FileSystemProvider.class); - SeekableByteChannel sbc = Mockito.mock(SeekableByteChannel.class); - - Mockito.when(pathToVault.resolve("masterkey.cryptomator")).thenReturn(pathToMasterkey); - Mockito.when(pathToMasterkey.getFileSystem()).thenReturn(fs); - Mockito.when(fs.provider()).thenReturn(provider); - Mockito.when(provider.newByteChannel(Mockito.eq(pathToMasterkey), Mockito.any(), Mockito.any())).thenReturn(sbc); - Mockito.when(sbc.size()).thenReturn((long) keyFile.remaining()); - Mockito.when(sbc.read(Mockito.any())).then(invocation -> { - ByteBuffer dst = invocation.getArgument(0); - int n = Math.min(keyFile.remaining(), dst.remaining()); - byte[] tmp = new byte[n]; - keyFile.get(tmp); - dst.put(tmp); - return n; - }); } @Test - public void testNeedsMigration() throws IOException { + @DisplayName("can't determine vault version without masterkey.cryptomator or vault.cryptomator") + public void throwsExceptionIfNeitherMasterkeyNorVaultConfigExists() { Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker); - boolean result = migrators.needsMigration(pathToVault, "masterkey.cryptomator"); - Assertions.assertTrue(result); + IOException thrown = Assertions.assertThrows(IOException.class, () -> { + migrators.needsMigration(pathToVault, "vault.cryptomator", "masterkey.cryptomator"); + }); + MatcherAssert.assertThat(thrown.getMessage(), CoreMatchers.containsString("Did not find vault.cryptomator nor masterkey.cryptomator")); } - @Test - public void testNeedsNoMigration() throws IOException { - keyFile = StandardCharsets.UTF_8.encode("{\"version\": 9999}"); + @Nested + public class WithExistingVaultConfig { - Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker); - boolean result = migrators.needsMigration(pathToVault, "masterkey.cryptomator"); + private MockedStatic vaultConfigClass; + private VaultConfig.UnverifiedVaultConfig unverifiedVaultConfig; - Assertions.assertFalse(result); - } + @BeforeEach + public void setup() throws IOException { + vaultConfigClass = Mockito.mockStatic(VaultConfig.class); + unverifiedVaultConfig = Mockito.mock(VaultConfig.UnverifiedVaultConfig.class); + + Files.write(vaultConfigPath, "vault-config".getBytes(StandardCharsets.UTF_8)); + Assumptions.assumeTrue(Files.exists(vaultConfigPath)); + Assumptions.assumeFalse(Files.exists(masterkeyPath)); + + vaultConfigClass.when(() -> VaultConfig.decode("vault-config")).thenReturn(unverifiedVaultConfig); + } + + @AfterEach + public void tearDown() { + vaultConfigClass.close(); + } + + @Test + @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); + + boolean result = migrators.needsMigration(pathToVault, "vault.cryptomator", "masterkey.cryptomator"); + + Assertions.assertTrue(result); + } + + @Test + @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); + + boolean result = migrators.needsMigration(pathToVault, "vault.cryptomator", "masterkey.cryptomator"); + + Assertions.assertFalse(result); + } + + @Test + @DisplayName("throws NoApplicableMigratorException if no migrator was found") + public void testMigrateWithoutMigrators() { + Mockito.when(unverifiedVaultConfig.allegedVaultVersion()).thenReturn(42); + + Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker); + Assertions.assertThrows(NoApplicableMigratorException.class, () -> { + migrators.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", MigrationProgressListener.IGNORE, MigrationContinuationListener.CANCEL_ALWAYS); + }); + } + + @Test + @DisplayName("migrate successfully") + @SuppressWarnings("deprecation") + public void testMigrate() throws NoApplicableMigratorException, CryptoException, IOException { + MigrationProgressListener progressListener = Mockito.mock(MigrationProgressListener.class); + 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.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", progressListener, continuationListener); + + Mockito.verify(migrator).migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", progressListener, continuationListener); + } + + @Test + @DisplayName("migrate throws UnsupportedVaultFormatException") + @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); + 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()); + + Assertions.assertThrows(IllegalStateException.class, () -> { + migrators.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", MigrationProgressListener.IGNORE, MigrationContinuationListener.CANCEL_ALWAYS); + }); + } - @Test - public void testMigrateWithoutMigrators() throws IOException { - Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker); - Assertions.assertThrows(NoApplicableMigratorException.class, () -> { - migrators.migrate(pathToVault, "masterkey.cryptomator", "secret", (state, progress) -> {}, (event) -> ContinuationResult.CANCEL); - }); - } - - @Test - @SuppressWarnings("deprecation") - public void testMigrate() throws NoApplicableMigratorException, InvalidPassphraseException, IOException { - MigrationProgressListener progressListener = Mockito.mock(MigrationProgressListener.class); - MigrationContinuationListener continuationListener = Mockito.mock(MigrationContinuationListener.class); - Migrator migrator = Mockito.mock(Migrator.class); - Migrators migrators = new Migrators(new HashMap() { - { - put(Migration.ZERO_TO_ONE, migrator); - } - }, fsCapabilityChecker); - migrators.migrate(pathToVault, "masterkey.cryptomator", "secret", progressListener, continuationListener); - Mockito.verify(migrator).migrate(pathToVault, "masterkey.cryptomator", "secret", progressListener, continuationListener); } - @Test - @SuppressWarnings("deprecation") - public void testMigrateUnsupportedVaultFormat() throws NoApplicableMigratorException, InvalidPassphraseException, IOException { - Migrator migrator = Mockito.mock(Migrator.class); - Migrators migrators = new Migrators(new HashMap() { - { - put(Migration.ZERO_TO_ONE, migrator); - } - }, fsCapabilityChecker); - Mockito.doThrow(new UnsupportedVaultFormatException(Integer.MAX_VALUE, 1)).when(migrator).migrate(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); - Assertions.assertThrows(IllegalStateException.class, () -> { - migrators.migrate(pathToVault, "masterkey.cryptomator", "secret", (state, progress) -> {}, (event) -> ContinuationResult.CANCEL); - }); + @Nested + public class WithExistingMasterkeyFile { + + private MockedStatic masterkeyFileAccessClass; + + @BeforeEach + public void setup() throws IOException { + masterkeyFileAccessClass = Mockito.mockStatic(MasterkeyFileAccess.class); + Files.createFile(masterkeyPath); + Assumptions.assumeFalse(Files.exists(vaultConfigPath)); + Assumptions.assumeTrue(Files.exists(masterkeyPath)); + } + + @AfterEach + public void tearDown() { + masterkeyFileAccessClass.close(); + } + + @Test + @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); + + boolean result = migrators.needsMigration(pathToVault, "vault.cryptomator", "masterkey.cryptomator"); + + Assertions.assertTrue(result); + } + + @Test + @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); + + boolean result = migrators.needsMigration(pathToVault, "vault.cryptomator", "masterkey.cryptomator"); + + Assertions.assertFalse(result); + } + + @Test + @DisplayName("throws NoApplicableMigratorException if no migrator was found") + public void testMigrateWithoutMigrators() { + masterkeyFileAccessClass.when(() -> MasterkeyFileAccess.readAllegedVaultVersion(Mockito.any())).thenReturn(1337); + + Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker); + Assertions.assertThrows(NoApplicableMigratorException.class, () -> { + migrators.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", MigrationProgressListener.IGNORE, MigrationContinuationListener.CANCEL_ALWAYS); + }); + } + + @Test + @DisplayName("migrate successfully") + @SuppressWarnings("deprecation") + public void testMigrate() throws NoApplicableMigratorException, CryptoException, IOException { + MigrationProgressListener progressListener = Mockito.mock(MigrationProgressListener.class); + 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.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", progressListener, continuationListener); + + Mockito.verify(migrator).migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", progressListener, continuationListener); + } + + @Test + @DisplayName("migrate throws UnsupportedVaultFormatException") + @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); + 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()); + + Assertions.assertThrows(IllegalStateException.class, () -> { + migrators.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", MigrationProgressListener.IGNORE, MigrationContinuationListener.CANCEL_ALWAYS); + }); + } + } } diff --git a/src/test/java/org/cryptomator/cryptofs/migration/TestMigrationComponent.java b/src/test/java/org/cryptomator/cryptofs/migration/TestMigrationComponent.java deleted file mode 100644 index 40089d2f..00000000 --- a/src/test/java/org/cryptomator/cryptofs/migration/TestMigrationComponent.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.cryptomator.cryptofs.migration; - -import java.util.Map; - -import org.cryptomator.cryptofs.migration.api.Migrator; - -import dagger.Component; - -@Component(modules = {MigrationModule.class}) -interface TestMigrationComponent extends MigrationComponent { - - Map availableMigrators(); - -} diff --git a/src/test/java/org/cryptomator/cryptofs/migration/api/SimpleMigrationContinuationListenerTest.java b/src/test/java/org/cryptomator/cryptofs/migration/api/SimpleMigrationContinuationListenerTest.java index 3e2738d9..7db601f9 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/api/SimpleMigrationContinuationListenerTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/api/SimpleMigrationContinuationListenerTest.java @@ -9,7 +9,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -class SimpleMigrationContinuationListenerTest { +public class SimpleMigrationContinuationListenerTest { @Test public void testConcurrency() { diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java index 1d3620a9..b099554e 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java @@ -2,23 +2,26 @@ import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Jimfs; -import org.cryptomator.cryptofs.common.MasterkeyBackupHelper; import org.cryptomator.cryptofs.common.Constants; +import org.cryptomator.cryptofs.common.MasterkeyBackupHelper; import org.cryptomator.cryptofs.migration.api.Migrator; import org.cryptomator.cryptofs.mocks.NullSecureRandom; -import org.cryptomator.cryptolib.Cryptors; -import org.cryptomator.cryptolib.api.Cryptor; -import org.cryptomator.cryptolib.api.CryptorProvider; -import org.cryptomator.cryptolib.api.KeyFile; +import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; +import java.security.SecureRandom; import java.text.Normalizer; import java.text.Normalizer.Form; @@ -27,11 +30,10 @@ public class Version6MigratorTest { private FileSystem fs; private Path pathToVault; private Path masterkeyFile; - private CryptorProvider cryptorProvider; + private SecureRandom csprng = NullSecureRandom.INSTANCE; @BeforeEach public void setup() throws IOException { - cryptorProvider = Cryptors.version1(NullSecureRandom.INSTANCE); fs = Jimfs.newFileSystem(Configuration.unix()); pathToVault = fs.getPath("/vaultDir"); masterkeyFile = pathToVault.resolve("masterkey.cryptomator"); @@ -45,28 +47,33 @@ public void teardown() throws IOException { } @Test - public void testMigrate() throws IOException { + public void testMigrate() throws IOException, CryptoException { String oldPassword = Normalizer.normalize("ä", Form.NFD); String newPassword = Normalizer.normalize("ä", Form.NFC); Assertions.assertNotEquals(oldPassword, newPassword); - KeyFile beforeMigration = cryptorProvider.createNew().writeKeysToMasterkeyFile(oldPassword, 5); - Assertions.assertEquals(5, beforeMigration.getVersion()); - Files.write(masterkeyFile, beforeMigration.serialize()); - Path masterkeyBackupFile = pathToVault.resolve("masterkey.cryptomator" + MasterkeyBackupHelper.generateFileIdSuffix(beforeMigration.serialize()) + Constants.MASTERKEY_BACKUP_SUFFIX); + Masterkey masterkey = Masterkey.generate(csprng); + MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(new byte[0], csprng); + masterkeyFileAccess.persist(masterkey, masterkeyFile, oldPassword, 5); + byte[] beforeMigration = Files.readAllBytes(masterkeyFile); + + Files.write(masterkeyFile, beforeMigration); + Path masterkeyBackupFile = pathToVault.resolve("masterkey.cryptomator" + MasterkeyBackupHelper.generateFileIdSuffix(beforeMigration) + Constants.MASTERKEY_BACKUP_SUFFIX); + + Migrator migrator = new Version6Migrator(csprng); + migrator.migrate(pathToVault, null, "masterkey.cryptomator", oldPassword); - Migrator migrator = new Version6Migrator(cryptorProvider); - migrator.migrate(pathToVault, "masterkey.cryptomator", oldPassword); + byte[] afterMigration = Files.readAllBytes(masterkeyFile); + String afterMigrationJson = new String(afterMigration, StandardCharsets.UTF_8); + MatcherAssert.assertThat(afterMigrationJson, CoreMatchers.containsString("\"version\": 6")); - KeyFile afterMigration = KeyFile.parse(Files.readAllBytes(masterkeyFile)); - Assertions.assertEquals(6, afterMigration.getVersion()); - try (Cryptor cryptor = cryptorProvider.createFromKeyFile(afterMigration, newPassword, 6)) { - Assertions.assertNotNull(cryptor); + try (var key = masterkeyFileAccess.load(masterkeyFile, newPassword)) { + Assertions.assertNotNull(key); } Assertions.assertTrue(Files.exists(masterkeyBackupFile)); - KeyFile backupKey = KeyFile.parse(Files.readAllBytes(masterkeyBackupFile)); - Assertions.assertEquals(5, backupKey.getVersion()); + String backedUpJson = Files.readString(masterkeyBackupFile, StandardCharsets.UTF_8); + MatcherAssert.assertThat(backedUpJson, CoreMatchers.containsString("\"version\": 5")); } } diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java index 120592bb..9d0cb5a0 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java @@ -145,7 +145,7 @@ public void testGetTargetPath(String oldCanonicalName, String attemptSuffix, Str @DisplayName("FilePathMigration.parse(...)") @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) - class Parsing { + public class Parsing { private FileSystem fs; private Path vaultRoot; @@ -275,7 +275,7 @@ public void testParseShortenedFile(String oldPath, String metadataFilePath, Stri @DisplayName("FilePathMigration.parse(...).get().migrate(...)") @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) - class Migrating { + public class Migrating { private FileSystem fs; private Path vaultRoot; diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java index f7fbeaae..63b8ce50 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java @@ -4,19 +4,22 @@ import com.google.common.jimfs.Jimfs; import org.cryptomator.cryptofs.migration.api.Migrator; import org.cryptomator.cryptofs.mocks.NullSecureRandom; -import org.cryptomator.cryptolib.Cryptors; -import org.cryptomator.cryptolib.api.Cryptor; -import org.cryptomator.cryptolib.api.CryptorProvider; -import org.cryptomator.cryptolib.api.KeyFile; +import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; +import java.security.SecureRandom; public class Version7MigratorTest { @@ -25,11 +28,10 @@ public class Version7MigratorTest { private Path dataDir; private Path metaDir; private Path masterkeyFile; - private CryptorProvider cryptorProvider; + private SecureRandom csprng = NullSecureRandom.INSTANCE; @BeforeEach public void setup() throws IOException { - cryptorProvider = Cryptors.version1(NullSecureRandom.INSTANCE); fs = Jimfs.newFileSystem(Configuration.unix()); vaultRoot = fs.getPath("/vaultDir"); dataDir = vaultRoot.resolve("d"); @@ -38,10 +40,10 @@ public void setup() throws IOException { Files.createDirectory(vaultRoot); Files.createDirectory(dataDir); Files.createDirectory(metaDir); - try (Cryptor cryptor = cryptorProvider.createNew()) { - KeyFile keyFile = cryptor.writeKeysToMasterkeyFile("test", 6); - Files.write(masterkeyFile, keyFile.serialize()); - } + + Masterkey masterkey = Masterkey.generate(csprng); + MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(new byte[0], csprng); + masterkeyFileAccess.persist(masterkey, masterkeyFile, "test", 6); } @AfterEach @@ -50,22 +52,19 @@ public void teardown() throws IOException { } @Test - public void testKeyfileGetsUpdates() throws IOException { - KeyFile beforeMigration = KeyFile.parse(Files.readAllBytes(masterkeyFile)); - Assertions.assertEquals(6, beforeMigration.getVersion()); - - Migrator migrator = new Version7Migrator(cryptorProvider); - migrator.migrate(vaultRoot, "masterkey.cryptomator", "test"); + public void testKeyfileGetsUpdates() throws CryptoException, IOException { + Migrator migrator = new Version7Migrator(csprng); + migrator.migrate(vaultRoot, null, "masterkey.cryptomator", "test"); - KeyFile afterMigration = KeyFile.parse(Files.readAllBytes(masterkeyFile)); - Assertions.assertEquals(7, afterMigration.getVersion()); + String migrated = Files.readString(masterkeyFile, StandardCharsets.UTF_8); + MatcherAssert.assertThat(migrated, CoreMatchers.containsString("\"version\": 7")); } @Test - public void testMDirectoryGetsDeleted() throws IOException { - Migrator migrator = new Version7Migrator(cryptorProvider); - migrator.migrate(vaultRoot, "masterkey.cryptomator", "test"); - + public void testMDirectoryGetsDeleted() throws CryptoException, IOException { + Migrator migrator = new Version7Migrator(csprng); + migrator.migrate(vaultRoot, null, "masterkey.cryptomator", "test"); + Assertions.assertFalse(Files.exists(metaDir)); } @@ -76,54 +75,54 @@ public void testMigrationFailsIfEncounteringUnsyncediCloudContent() throws IOExc Path fileBeforeMigration = dir.resolve("MZUWYZLOMFWWK===.icloud"); Files.createFile(fileBeforeMigration); - Migrator migrator = new Version7Migrator(cryptorProvider); - + Migrator migrator = new Version7Migrator(csprng); + IOException e = Assertions.assertThrows(PreMigrationVisitor.PreMigrationChecksFailedException.class, () -> { - migrator.migrate(vaultRoot, "masterkey.cryptomator", "test"); + migrator.migrate(vaultRoot, null, "masterkey.cryptomator", "test"); }); Assertions.assertTrue(e.getMessage().contains("MZUWYZLOMFWWK===.icloud")); } @Test - public void testMigrationOfNormalFile() throws IOException { + public void testMigrationOfNormalFile() throws CryptoException, IOException { Path dir = dataDir.resolve("AA/BBBBBCCCCCDDDDDEEEEEFFFFFGGGGG"); Files.createDirectories(dir); Path fileBeforeMigration = dir.resolve("MZUWYZLOMFWWK==="); Path fileAfterMigration = dir.resolve("ZmlsZW5hbWU=.c9r"); Files.createFile(fileBeforeMigration); - Migrator migrator = new Version7Migrator(cryptorProvider); - migrator.migrate(vaultRoot, "masterkey.cryptomator", "test"); + Migrator migrator = new Version7Migrator(csprng); + migrator.migrate(vaultRoot, null, "masterkey.cryptomator", "test"); Assertions.assertFalse(Files.exists(fileBeforeMigration)); Assertions.assertTrue(Files.exists(fileAfterMigration)); } @Test - public void testMigrationOfNormalDirectory() throws IOException { + public void testMigrationOfNormalDirectory() throws CryptoException, IOException { Path dir = dataDir.resolve("AA/BBBBBCCCCCDDDDDEEEEEFFFFFGGGGG"); Files.createDirectories(dir); Path fileBeforeMigration = dir.resolve("0MZUWYZLOMFWWK==="); Path fileAfterMigration = dir.resolve("ZmlsZW5hbWU=.c9r/dir.c9r"); Files.createFile(fileBeforeMigration); - Migrator migrator = new Version7Migrator(cryptorProvider); - migrator.migrate(vaultRoot, "masterkey.cryptomator", "test"); + Migrator migrator = new Version7Migrator(csprng); + migrator.migrate(vaultRoot, null, "masterkey.cryptomator", "test"); Assertions.assertFalse(Files.exists(fileBeforeMigration)); Assertions.assertTrue(Files.exists(fileAfterMigration)); } @Test - public void testMigrationOfNormalSymlink() throws IOException { + public void testMigrationOfNormalSymlink() throws CryptoException, IOException { Path dir = dataDir.resolve("AA/BBBBBCCCCCDDDDDEEEEEFFFFFGGGGG"); Files.createDirectories(dir); Path fileBeforeMigration = dir.resolve("1SMZUWYZLOMFWWK==="); Path fileAfterMigration = dir.resolve("ZmlsZW5hbWU=.c9r/symlink.c9r"); Files.createFile(fileBeforeMigration); - Migrator migrator = new Version7Migrator(cryptorProvider); - migrator.migrate(vaultRoot, "masterkey.cryptomator", "test"); + Migrator migrator = new Version7Migrator(csprng); + migrator.migrate(vaultRoot, null, "masterkey.cryptomator", "test"); Assertions.assertFalse(Files.exists(fileBeforeMigration)); Assertions.assertTrue(Files.exists(fileAfterMigration)); diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java new file mode 100644 index 00000000..cd15fe73 --- /dev/null +++ b/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java @@ -0,0 +1,70 @@ +package org.cryptomator.cryptofs.migration.v8; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.google.common.jimfs.Configuration; +import com.google.common.jimfs.Jimfs; +import org.cryptomator.cryptofs.migration.api.Migrator; +import org.cryptomator.cryptofs.mocks.NullSecureRandom; +import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.SecureRandom; + +public class Version8MigratorTest { + + private FileSystem fs; + private Path pathToVault; + private Path masterkeyFile; + private Path vaultConfigFile; + private SecureRandom csprng = NullSecureRandom.INSTANCE; + + @BeforeEach + public void setup() throws IOException { + fs = Jimfs.newFileSystem(Configuration.unix()); + pathToVault = fs.getPath("/vaultDir"); + masterkeyFile = pathToVault.resolve("masterkey.cryptomator"); + vaultConfigFile = pathToVault.resolve("vault.cryptomator"); + Files.createDirectory(pathToVault); + } + + @AfterEach + public void teardown() throws IOException { + fs.close(); + } + + @Test + public void testMigrate() throws CryptoException, IOException { + Masterkey masterkey = Masterkey.generate(csprng); + MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(new byte[0], csprng); + masterkeyFileAccess.persist(masterkey, masterkeyFile, "topsecret", 7); + Assumptions.assumeFalse(Files.exists(vaultConfigFile)); + + Migrator migrator = new Version8Migrator(csprng); + migrator.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "topsecret"); + + String migrated = Files.readString(masterkeyFile, StandardCharsets.UTF_8); + MatcherAssert.assertThat(migrated, CoreMatchers.containsString("\"version\": 999")); + Assertions.assertTrue(Files.exists(vaultConfigFile)); + DecodedJWT token = JWT.decode(Files.readString(vaultConfigFile)); + Assertions.assertNotNull(token.getId()); + Assertions.assertEquals("masterkeyfile:masterkey.cryptomator", token.getKeyId()); + Assertions.assertEquals(8, token.getClaim("format").asInt()); + Assertions.assertEquals("SIV_CTRMAC", token.getClaim("cipherCombo").asString()); + Assertions.assertEquals(220, token.getClaim("shorteningThreshold").asInt()); + } + +} \ No newline at end of file diff --git a/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 00000000..ca6ee9ce --- /dev/null +++ b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/suppression.xml b/suppression.xml index 9edca825..cc17d63b 100644 --- a/suppression.xml +++ b/suppression.xml @@ -1,9 +1,4 @@ - - - org.slf4j:slf4j-api:1.7.25 - CVE-2018-8088 - \ No newline at end of file