diff --git a/pom.xml b/pom.xml
index 3d1c550a..1f92d11b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
4.0.0
org.cryptomator
cryptofs
- 1.9.1
+ 1.9.2
Cryptomator Crypto Filesystem
This library provides the Java filesystem provider used by Cryptomator.
https://github.com/cryptomator/cryptofs
@@ -15,11 +15,11 @@
1.3.0
- 2.25.4
+ 2.26
28.2-jre
1.7.30
- 5.5.2
+ 5.6.0
3.2.4
2.2
UTF-8
@@ -239,6 +239,31 @@
+
+
+
+ apiNote
+ a
+ API Note:
+
+
+ implSpec
+ a
+ Implementation Requirements:
+
+
+ implNote
+ a
+ Implementation Note:
+
+ param
+ return
+ throws
+ since
+ version
+ serialData
+ see
+
javax.annotation
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
index 7285f69d..9cc9de44 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
@@ -10,6 +10,7 @@
import org.cryptomator.cryptofs.ch.AsyncDelegatingFileChannel;
import org.cryptomator.cryptofs.common.Constants;
+import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
import org.cryptomator.cryptofs.common.MasterkeyBackupFileHasher;
import org.cryptomator.cryptofs.migration.Migrators;
import org.cryptomator.cryptolib.Cryptors;
@@ -124,6 +125,7 @@ private static SecureRandom strongSecureRandom() {
* @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 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
*/
public static CryptoFileSystem newFileSystem(Path pathToVault, CryptoFileSystemProperties properties) throws FileSystemNeedsMigrationException, IOException {
@@ -138,6 +140,7 @@ public static CryptoFileSystem newFileSystem(Path pathToVault, CryptoFileSystemP
* @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
*/
@@ -153,6 +156,7 @@ public static void initialize(Path pathToVault, String masterkeyFilename, CharSe
* @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
*/
@@ -160,6 +164,7 @@ public static void initialize(Path pathToVault, String masterkeyFilename, byte[]
if (!Files.isDirectory(pathToVault)) {
throw new NotDirectoryException(pathToVault.toString());
}
+ new FileSystemCapabilityChecker().checkCapabilities(pathToVault);
try (Cryptor cryptor = CRYPTOR_PROVIDER.createNew()) {
// save masterkey file:
Path masterKeyPath = pathToVault.resolve(masterkeyFilename);
@@ -288,6 +293,8 @@ public String getScheme() {
public CryptoFileSystem newFileSystem(URI uri, Map rawProperties) throws IOException {
CryptoFileSystemUri parsedUri = CryptoFileSystemUri.parse(uri);
CryptoFileSystemProperties properties = CryptoFileSystemProperties.wrap(rawProperties);
+
+ new FileSystemCapabilityChecker().checkCapabilities(parsedUri.pathToVault());
// TODO remove implicit initialization in 2.0.0
initializeFileSystemIfRequired(parsedUri, properties);
diff --git a/src/main/java/org/cryptomator/cryptofs/ch/CleartextFileChannel.java b/src/main/java/org/cryptomator/cryptofs/ch/CleartextFileChannel.java
index ccf205c6..75abe860 100644
--- a/src/main/java/org/cryptomator/cryptofs/ch/CleartextFileChannel.java
+++ b/src/main/java/org/cryptomator/cryptofs/ch/CleartextFileChannel.java
@@ -211,9 +211,7 @@ private void forceInternal(boolean metaData) throws IOException {
flush();
ciphertextFileChannel.force(metaData);
if (metaData) {
- FileTime lastModifiedTime = isWritable() ? FileTime.from(lastModified.get()) : null;
- FileTime lastAccessTime = FileTime.from(Instant.now());
- attrViewProvider.get().setTimes(lastModifiedTime, lastAccessTime, null);
+ persistLastModified();
}
}
@@ -229,6 +227,17 @@ private void flush() throws IOException {
}
}
+ /**
+ * Corrects the last modified and access date due to possible cache invalidation (i.e. write operation!)
+ *
+ * @throws IOException
+ */
+ private void persistLastModified() throws IOException {
+ FileTime lastModifiedTime = isWritable() ? FileTime.from(lastModified.get()) : null;
+ FileTime lastAccessTime = FileTime.from(Instant.now());
+ attrViewProvider.get().setTimes(lastModifiedTime, lastAccessTime, null);
+ }
+
@Override
public MappedByteBuffer map(MapMode mode, long position, long size) {
throw new UnsupportedOperationException();
@@ -294,6 +303,7 @@ long beginOfChunk(long cleartextPos) {
protected void implCloseChannel() throws IOException {
try {
flush();
+ persistLastModified();
} finally {
super.implCloseChannel();
closeListener.closed(this);
diff --git a/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java b/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java
new file mode 100644
index 00000000..d9531a6d
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java
@@ -0,0 +1,88 @@
+package org.cryptomator.cryptofs.common;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.common.io.MoreFiles;
+import com.google.common.io.RecursiveDeleteOption;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.file.FileSystemException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class FileSystemCapabilityChecker {
+
+ private static final Logger LOG = LoggerFactory.getLogger(FileSystemCapabilityChecker.class);
+
+ public enum Capability {
+ /**
+ * File system supports filenames with ≥ 230 chars.
+ */
+ LONG_FILENAMES,
+
+ /**
+ * File system supports paths with ≥ 400 chars.
+ */
+ LONG_PATHS,
+ }
+
+ /**
+ * Checks whether the underlying filesystem has all required capabilities.
+ *
+ * @param pathToVault Path to a vault's storage location
+ * @throws MissingCapabilityException if any check fails
+ * @implNote Only short-running tests with constant time are performed
+ * @since 1.9.2
+ */
+ public void checkCapabilities(Path pathToVault) throws MissingCapabilityException {
+ Path checkDir = pathToVault.resolve("c");
+ try {
+ checkLongFilenames(checkDir);
+ checkLongFilePaths(checkDir);
+ } finally {
+ try {
+ MoreFiles.deleteRecursively(checkDir, RecursiveDeleteOption.ALLOW_INSECURE);
+ } catch (IOException e) {
+ LOG.warn("Failed to clean up " + checkDir, e);
+ }
+ }
+ }
+
+ private void checkLongFilenames(Path checkDir) throws MissingCapabilityException {
+ String longFileName = Strings.repeat("a", 226) + ".c9r";
+ Path p = checkDir.resolve(longFileName);
+ try {
+ Files.createDirectories(p);
+ } catch (IOException e) {
+ throw new MissingCapabilityException(p, Capability.LONG_FILENAMES);
+ }
+ }
+
+ private void checkLongFilePaths(Path checkDir) throws MissingCapabilityException {
+ String longFileName = Strings.repeat("a", 96) + ".c9r";
+ String longPath = Joiner.on('/').join(longFileName, longFileName, longFileName, longFileName);
+ Path p = checkDir.resolve(longPath);
+ try {
+ Files.createDirectories(p);
+ } catch (IOException e) {
+ throw new MissingCapabilityException(p, Capability.LONG_PATHS);
+ }
+ }
+
+ public static class MissingCapabilityException extends FileSystemException {
+
+ private final Capability missingCapability;
+
+ public MissingCapabilityException(Path path, Capability missingCapability) {
+ super(path.toString(), null, "Filesystem doesn't support " + missingCapability);
+ this.missingCapability = missingCapability;
+ }
+
+ public Capability getMissingCapability() {
+ return missingCapability;
+ }
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java b/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java
index d69c2efc..0d700add 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java
@@ -13,6 +13,7 @@
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
+import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
import org.cryptomator.cryptofs.migration.api.Migrator;
import org.cryptomator.cryptofs.migration.v6.Version6Migrator;
import org.cryptomator.cryptofs.migration.v7.Version7Migrator;
@@ -34,6 +35,11 @@ class MigrationModule {
CryptorProvider provideVersion1CryptorProvider() {
return version1Cryptor;
}
+
+ @Provides
+ FileSystemCapabilityChecker provideFileSystemCapabilityChecker() {
+ return new FileSystemCapabilityChecker();
+ }
@Provides
@IntoMap
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java
index e8506250..8937eb60 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java
@@ -16,6 +16,7 @@
import javax.inject.Inject;
import org.cryptomator.cryptofs.common.Constants;
+import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
import org.cryptomator.cryptofs.migration.api.MigrationProgressListener;
import org.cryptomator.cryptofs.migration.api.Migrator;
import org.cryptomator.cryptofs.migration.api.NoApplicableMigratorException;
@@ -46,10 +47,12 @@ public class Migrators {
.build();
private final Map migrators;
+ private final FileSystemCapabilityChecker fsCapabilityChecker;
@Inject
- Migrators(Map migrators) {
+ Migrators(Map migrators, FileSystemCapabilityChecker fsCapabilityChecker) {
this.migrators = migrators;
+ this.fsCapabilityChecker = fsCapabilityChecker;
}
private static SecureRandom strongSecureRandom() {
@@ -87,9 +90,12 @@ public boolean needsMigration(Path pathToVault, String masterkeyFilename) throws
* @param passphrase The passphrase needed 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
*/
public void migrate(Path pathToVault, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener) throws NoApplicableMigratorException, InvalidPassphraseException, IOException {
+ fsCapabilityChecker.checkCapabilities(pathToVault);
+
Path masterKeyPath = pathToVault.resolve(masterkeyFilename);
byte[] keyFileContents = Files.readAllBytes(masterKeyPath);
KeyFile keyFile = KeyFile.parse(keyFileContents);
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
index 5f4aae6c..538d488f 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
@@ -63,9 +63,9 @@ public void setupClass(@TempDir Path tmpDir) throws IOException {
fileSystem = new CryptoFileSystemProvider().newFileSystem(create(tmpDir), cryptoFileSystemProperties().withPassphrase("asd").build());
}
- // tests https://github.com/cryptomator/cryptofs/issues/56
+ // tests https://github.com/cryptomator/cryptofs/issues/69
@Test
- public void testForceDoesntBumpModifiedDate() throws IOException {
+ public void testCloseDoesNotBumpModifiedDate() throws IOException {
Path file = fileSystem.getPath("/file.txt");
Instant t0, t1;
@@ -76,7 +76,43 @@ public void testForceDoesntBumpModifiedDate() throws IOException {
}
t1 = Files.getLastModifiedTime(file).toInstant().truncatedTo(ChronoUnit.SECONDS);
- Assertions.assertTrue(t1.equals(t0));
+ Assertions.assertEquals(t0, t1);
+ }
+
+ @Test
+ public void testLastModifiedIsPreservedOverSeveralOperations() throws IOException, InterruptedException {
+ Path file = fileSystem.getPath("/file2.txt");
+
+ Instant t0, t1, t2, t3, t4, t5;
+ t0 = Instant.ofEpochSecond(123456789).truncatedTo(ChronoUnit.SECONDS);
+ ByteBuffer data = ByteBuffer.wrap("CryptoFS".getBytes());
+
+ try (FileChannel ch = FileChannel.open(file, CREATE_NEW, WRITE)) {
+ t1 = Files.getLastModifiedTime(file).toInstant().truncatedTo(ChronoUnit.MILLIS);
+ Thread.currentThread().sleep(50);
+
+ ch.write(data);
+ ch.force(true);
+ Thread.currentThread().sleep(50);
+ t2 = Files.getLastModifiedTime(file).toInstant().truncatedTo(ChronoUnit.MILLIS);
+
+ Files.setLastModifiedTime(file, FileTime.from(t0));
+ ch.force(true);
+ Thread.currentThread().sleep(50);
+ t3 = Files.getLastModifiedTime(file).toInstant().truncatedTo(ChronoUnit.MILLIS);
+
+ ch.write(data);
+ ch.force(true);
+ Thread.currentThread().sleep(1000);
+ t4 = Files.getLastModifiedTime(file).toInstant().truncatedTo(ChronoUnit.SECONDS);
+
+ }
+
+ t5 = Files.getLastModifiedTime(file).toInstant().truncatedTo(ChronoUnit.SECONDS);
+ Assertions.assertNotEquals(t1, t2);
+ Assertions.assertEquals(t0, t3);
+ Assertions.assertNotEquals(t4, t3);
+ Assertions.assertEquals(t4, t5);
}
}
diff --git a/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java b/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java
index ac41721e..b38f4458 100644
--- a/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java
@@ -222,6 +222,26 @@ public void testCloseTriggersCloseListener() throws IOException {
verify(closeListener).closed(inTest);
}
+
+ @Test
+ public void testCloseUpdatesLastModifiedTimeIfWriteable() throws IOException {
+ when(options.writable()).thenReturn(true);
+ lastModified.set(Instant.ofEpochMilli(123456789000l));
+ FileTime fileTime = FileTime.from(lastModified.get());
+
+ inTest.implCloseChannel();
+
+ verify(attributeView).setTimes(Mockito.eq(fileTime), Mockito.any(), Mockito.isNull());
+ }
+
+ @Test
+ public void testCloseDoesNotUpdateLastModifiedTimeIfReadOnly() throws IOException {
+ when(options.writable()).thenReturn(false);
+
+ inTest.implCloseChannel();
+
+ verify(attributeView).setTimes(Mockito.isNull(), Mockito.any(), Mockito.isNull());
+ }
}
@Test
@@ -267,7 +287,7 @@ public void testTryLockReturnsNullIfDelegateReturnsNull() throws IOException {
@Test
@DisplayName("successful tryLock()")
public void testTryLockReturnsCryptoFileLockWrappingDelegate() throws IOException {
- when(ciphertextFileChannel.tryLock(380l, 4670l+110l-380l, true)).thenReturn(delegate);
+ when(ciphertextFileChannel.tryLock(380l, 4670l + 110l - 380l, true)).thenReturn(delegate);
FileLock result = inTest.tryLock(372l, 3828l, true);
@@ -283,7 +303,7 @@ public void testTryLockReturnsCryptoFileLockWrappingDelegate() throws IOExceptio
@Test
@DisplayName("successful lock()")
public void testLockReturnsCryptoFileLockWrappingDelegate() throws IOException {
- when(ciphertextFileChannel.lock(380l, 4670l+110l-380l, true)).thenReturn(delegate);
+ when(ciphertextFileChannel.lock(380l, 4670l + 110l - 380l, true)).thenReturn(delegate);
FileLock result = inTest.lock(372l, 3828l, true);
@@ -472,7 +492,7 @@ public void testDontRewriteHeader() throws IOException {
inTest = new CleartextFileChannel(ciphertextFileChannel, header, false, readWriteLock, cryptor, chunkCache, options, fileSize, lastModified, attributeViewSupplier, exceptionsDuringWrite, closeListener, stats);
inTest.force(true);
-
+
Mockito.verify(ciphertextFileChannel, Mockito.never()).write(Mockito.any(), Mockito.eq(0l));
}
diff --git a/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java b/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java
index 1d214212..3c1e0ba6 100644
--- a/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java
@@ -104,7 +104,7 @@ public void testProcessPartialMatch(String filename) {
@ValueSource(strings = {
"foo.bar",
"foo.c9r",
- "aaaaBBBB????DDDDeeeeFFFF.c9r",
+ "aaaaBBBB$$$$DDDDeeeeFFFF.c9r",
"aaaaBBBBxxxxDDDDeeeeFFFF.c9r",
})
public void testProcessNoMatch(String filename) {
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java b/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java
index df8f3e29..f15ae16f 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java
@@ -5,6 +5,7 @@
*******************************************************************************/
package org.cryptomator.cryptofs.migration;
+import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
import org.cryptomator.cryptofs.migration.api.MigrationProgressListener;
import org.cryptomator.cryptofs.migration.api.Migrator;
import org.cryptomator.cryptofs.migration.api.NoApplicableMigratorException;
@@ -29,11 +30,13 @@ public class MigratorsTest {
private ByteBuffer keyFile;
private Path pathToVault;
+ private FileSystemCapabilityChecker fsCapabilityChecker;
@BeforeEach
public void setup() throws IOException {
keyFile = StandardCharsets.UTF_8.encode("{\"version\": 0000}");
pathToVault = Mockito.mock(Path.class);
+ fsCapabilityChecker = Mockito.mock(FileSystemCapabilityChecker.class);
Path pathToMasterkey = Mockito.mock(Path.class);
FileSystem fs = Mockito.mock(FileSystem.class);
@@ -57,7 +60,7 @@ public void setup() throws IOException {
@Test
public void testNeedsMigration() throws IOException {
- Migrators migrators = new Migrators(Collections.emptyMap());
+ Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker);
boolean result = migrators.needsMigration(pathToVault, "masterkey.cryptomator");
Assertions.assertTrue(result);
@@ -67,7 +70,7 @@ public void testNeedsMigration() throws IOException {
public void testNeedsNoMigration() throws IOException {
keyFile = StandardCharsets.UTF_8.encode("{\"version\": 9999}");
- Migrators migrators = new Migrators(Collections.emptyMap());
+ Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker);
boolean result = migrators.needsMigration(pathToVault, "masterkey.cryptomator");
Assertions.assertFalse(result);
@@ -75,12 +78,25 @@ public void testNeedsNoMigration() throws IOException {
@Test
public void testMigrateWithoutMigrators() throws IOException {
- Migrators migrators = new Migrators(Collections.emptyMap());
+ Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker);
Assertions.assertThrows(NoApplicableMigratorException.class, () -> {
migrators.migrate(pathToVault, "masterkey.cryptomator", "secret", (state, progress) -> {});
});
}
+ @Test
+ public void testMigrateWithFailingCapabilitiesCheck() throws IOException {
+ Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker);
+
+ Exception expected = new FileSystemCapabilityChecker.MissingCapabilityException(pathToVault, FileSystemCapabilityChecker.Capability.LONG_FILENAMES);
+ Mockito.doThrow(expected).when(fsCapabilityChecker).checkCapabilities(pathToVault);
+
+ Exception thrown = Assertions.assertThrows(FileSystemCapabilityChecker.MissingCapabilityException.class, () -> {
+ migrators.migrate(pathToVault, "masterkey.cryptomator", "secret", (state, progress) -> {});
+ });
+ Assertions.assertEquals(expected, thrown);
+ }
+
@Test
@SuppressWarnings("deprecation")
public void testMigrate() throws NoApplicableMigratorException, InvalidPassphraseException, IOException {
@@ -90,7 +106,7 @@ public void testMigrate() throws NoApplicableMigratorException, InvalidPassphras
{
put(Migration.ZERO_TO_ONE, migrator);
}
- });
+ }, fsCapabilityChecker);
migrators.migrate(pathToVault, "masterkey.cryptomator", "secret", listener);
Mockito.verify(migrator).migrate(pathToVault, "masterkey.cryptomator", "secret", listener);
}
@@ -103,7 +119,7 @@ public void testMigrateUnsupportedVaultFormat() throws NoApplicableMigratorExcep
{
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());
Assertions.assertThrows(IllegalStateException.class, () -> {
migrators.migrate(pathToVault, "masterkey.cryptomator", "secret", (state, progress) -> {});