rawProperties) thr
CryptoFileSystemUri parsedUri = CryptoFileSystemUri.parse(uri);
CryptoFileSystemProperties properties = CryptoFileSystemProperties.wrap(rawProperties);
- // TODO remove implicit initialization in 2.0.0:
- if (properties.initializeImplicitly() && !CryptoFileSystemProvider.containsVault(parsedUri.pathToVault(), properties.masterkeyFilename())) {
- CryptoFileSystemProvider.initialize(parsedUri.pathToVault(), properties.masterkeyFilename(), properties.passphrase());
+ if (!CryptoFileSystemProvider.containsVault(parsedUri.pathToVault(), properties.masterkeyFilename())) {
+ // TODO remove implicit initialization in 2.0.0:
+ if (properties.initializeImplicitly()) {
+ CryptoFileSystemProvider.initialize(parsedUri.pathToVault(), properties.masterkeyFilename(), properties.passphrase());
+ } else {
+ throw new NoSuchFileException(parsedUri.pathToVault().toString(), null, "Vault not initialized.");
+ }
+ }
+
+ if (Migrators.get().needsMigration(parsedUri.pathToVault(), properties.masterkeyFilename())) {
+ if (properties.migrateImplicitly()) {
+ Migrators.get().migrate(parsedUri.pathToVault(), properties.masterkeyFilename(), properties.passphrase());
+ } else {
+ throw new FileSystemNeedsMigrationException(parsedUri.pathToVault());
+ }
}
return fileSystems.create(parsedUri.pathToVault(), properties);
diff --git a/src/main/java/org/cryptomator/cryptofs/FileSystemNeedsMigrationException.java b/src/main/java/org/cryptomator/cryptofs/FileSystemNeedsMigrationException.java
new file mode 100644
index 00000000..f8c4da98
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/FileSystemNeedsMigrationException.java
@@ -0,0 +1,20 @@
+package org.cryptomator.cryptofs;
+
+import java.nio.file.FileSystemException;
+import java.nio.file.Path;
+
+import org.cryptomator.cryptofs.migration.Migrators;
+
+/**
+ * Indicates that no file system for a given vault can be created, because the vault has been created with an older version of this library.
+ *
+ * @see Migrators
+ * @since 1.4.0
+ */
+public class FileSystemNeedsMigrationException extends FileSystemException {
+
+ public FileSystemNeedsMigrationException(Path pathToVault) {
+ super(pathToVault.toString(), null, "File system needs migration to a newer format.");
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/Migration.java b/src/main/java/org/cryptomator/cryptofs/migration/Migration.java
new file mode 100644
index 00000000..932506c8
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/migration/Migration.java
@@ -0,0 +1,29 @@
+/*******************************************************************************
+ * 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;
+
+enum Migration {
+ /**
+ * @deprecated for testing only
+ */
+ @Deprecated ZERO_TO_ONE(0),
+
+ /**
+ * Migrates vault format 5 to 6.
+ */
+ FIVE_TO_SIX(5);
+
+ private final int applicableVersion;
+
+ private Migration(int applicableVersion) {
+ this.applicableVersion = applicableVersion;
+ }
+
+ public boolean isApplicable(int version) {
+ return version == applicableVersion;
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/MigrationComponent.java b/src/main/java/org/cryptomator/cryptofs/migration/MigrationComponent.java
new file mode 100644
index 00000000..35165dde
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/migration/MigrationComponent.java
@@ -0,0 +1,15 @@
+/*******************************************************************************
+ * 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;
+
+import dagger.Component;
+
+@Component(modules = {MigrationModule.class})
+interface MigrationComponent {
+
+ Migrators migrators();
+
+}
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java b/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java
new file mode 100644
index 00000000..36a47b7b
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java
@@ -0,0 +1,56 @@
+/*******************************************************************************
+ * 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;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.cryptomator.cryptofs.migration.api.Migrator;
+import org.cryptomator.cryptofs.migration.v6.Version6Migrator;
+import org.cryptomator.cryptolib.CryptoLibModule;
+
+import dagger.MapKey;
+import dagger.Module;
+import dagger.Provides;
+import dagger.multibindings.IntoMap;
+
+@Module(includes = {CryptoLibModule.class})
+class MigrationModule {
+
+ @Provides
+ @IntoMap
+ @MigratorKey(Migration.FIVE_TO_SIX)
+ Migrator provideVersion6Migrator(Version6Migrator migrator) {
+ return migrator;
+ }
+
+ // @Provides
+ // @IntoMap
+ // @MigratorKey(Migration.SIX_TO_SEVEN)
+ // Migrator provideVersion7Migrator(Version7Migrator migrator) {
+ // return migrator;
+ // }
+ //
+ // @Provides
+ // @IntoMap
+ // @MigratorKey(Migration.FIVE_TO_SEVEN)
+ // Migrator provideVersion7Migrator(Version6Migrator v6Migrator, Version7Migrator v7Migrator) {
+ // return v6Migrator.andThen(v7Migrator);
+ // }
+
+ @Documented
+ @Target(METHOD)
+ @Retention(RUNTIME)
+ @MapKey
+ public @interface MigratorKey {
+ Migration value();
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java
new file mode 100644
index 00000000..78f2939a
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java
@@ -0,0 +1,110 @@
+/*******************************************************************************
+ * 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;
+
+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.Constants;
+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.KeyFile;
+import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
+import org.cryptomator.cryptolib.common.SecureRandomModule;
+
+/**
+ * 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) migrate(pathToVault, masterkeyFileName, passphrase)};
+ * }
+ *
+ *
+ *
+ * @since 1.4.0
+ */
+public class Migrators {
+
+ private static final MigrationComponent COMPONENT = DaggerMigrationComponent.builder() //
+ .secureRandomModule(new SecureRandomModule(strongSecureRandom())) //
+ .build();
+
+ private final Map migrators;
+
+ @Inject
+ Migrators(Map migrators) {
+ this.migrators = migrators;
+ }
+
+ private static SecureRandom strongSecureRandom() {
+ try {
+ return SecureRandom.getInstanceStrong();
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("A strong algorithm must exist in every Java platform.", e);
+ }
+ }
+
+ public static Migrators get() {
+ return COMPONENT.migrators();
+ }
+
+ /**
+ * 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
+ * @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);
+ KeyFile keyFile = KeyFile.parse(keyFileContents);
+ return keyFile.getVersion() < 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
+ * @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 IOException if an I/O error occurs migrating the vault
+ */
+ public void migrate(Path pathToVault, String masterkeyFilename, CharSequence passphrase) throws NoApplicableMigratorException, InvalidPassphraseException, IOException {
+ Path masterKeyPath = pathToVault.resolve(masterkeyFilename);
+ byte[] keyFileContents = Files.readAllBytes(masterKeyPath);
+ KeyFile keyFile = KeyFile.parse(keyFileContents);
+
+ try {
+ Migrator migrator = findApplicableMigrator(keyFile.getVersion()).orElseThrow(NoApplicableMigratorException::new);
+ migrator.migrate(pathToVault, masterkeyFilename, passphrase);
+ } 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 Optional findApplicableMigrator(int version) {
+ // TODO return "5->6->7" instead of "5->6" and "6->7", if possible
+ 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/Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/api/Migrator.java
new file mode 100644
index 00000000..0319e00f
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/migration/api/Migrator.java
@@ -0,0 +1,44 @@
+/*******************************************************************************
+ * 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.api;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+import org.cryptomator.cryptolib.api.InvalidPassphraseException;
+import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
+
+/**
+ * @since 1.4.0
+ */
+public interface Migrator {
+
+ /**
+ * Performs the migration this migrator is built for.
+ *
+ * @param vaultRoot
+ * @param masterkeyFilename
+ * @param passphrase
+ * @throws InvalidPassphraseException
+ * @throws UnsupportedVaultFormatException
+ * @throws IOException
+ */
+ void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passphrase) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException;
+
+ /**
+ * Chains this migrator with a consecutive migrator.
+ *
+ * @param nextMigration The next migrator able to read the vault format created by this migrator.
+ * @return A combined migrator performing both steps in order.
+ */
+ default Migrator andThen(Migrator nextMigration) {
+ return (Path vaultRoot, String masterkeyFilename, CharSequence passphrase) -> {
+ migrate(vaultRoot, masterkeyFilename, passphrase);
+ nextMigration.migrate(vaultRoot, masterkeyFilename, passphrase);
+ };
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/api/NoApplicableMigratorException.java b/src/main/java/org/cryptomator/cryptofs/migration/api/NoApplicableMigratorException.java
new file mode 100644
index 00000000..4cbf321d
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/migration/api/NoApplicableMigratorException.java
@@ -0,0 +1,10 @@
+/*******************************************************************************
+ * 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.api;
+
+public class NoApplicableMigratorException extends IllegalStateException {
+
+}
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java
new file mode 100644
index 00000000..5f6dc4d2
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java
@@ -0,0 +1,60 @@
+/*******************************************************************************
+ * 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.v6;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
+import java.text.Normalizer;
+import java.text.Normalizer.Form;
+
+import javax.inject.Inject;
+
+import org.cryptomator.cryptofs.Constants;
+import org.cryptomator.cryptofs.migration.api.Migrator;
+import org.cryptomator.cryptolib.api.CryptoLibVersion;
+import org.cryptomator.cryptolib.api.CryptoLibVersion.Version;
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Version6Migrator implements Migrator {
+
+ private static final Logger LOG = LoggerFactory.getLogger(Version6Migrator.class);
+
+ private final CryptorProvider cryptorProvider;
+
+ @Inject
+ public Version6Migrator(@CryptoLibVersion(Version.ONE) CryptorProvider cryptorProvider) {
+ this.cryptorProvider = cryptorProvider;
+ }
+
+ @Override
+ public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passphrase) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException {
+ LOG.info("Upgrading {} from version 5 to version 6.", vaultRoot);
+ Path masterkeyFile = vaultRoot.resolve(masterkeyFilename);
+ byte[] fileContentsBeforeUpgrade = Files.readAllBytes(masterkeyFile);
+ KeyFile keyFile = KeyFile.parse(fileContentsBeforeUpgrade);
+ try (Cryptor cryptor = cryptorProvider.createFromKeyFile(keyFile, passphrase, 5)) {
+ // create backup, as soon as we know the password was correct:
+ Path masterkeyBackupFile = vaultRoot.resolve(masterkeyFilename + Constants.MASTERKEY_BACKUP_SUFFIX);
+ Files.copy(masterkeyFile, masterkeyBackupFile, StandardCopyOption.REPLACE_EXISTING);
+ LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName());
+ // rewrite masterkey file with normalized passphrase:
+ byte[] fileContentsAfterUpgrade = cryptor.writeKeysToMasterkeyFile(Normalizer.normalize(passphrase, Form.NFC), 6).serialize();
+ Files.write(masterkeyFile, fileContentsAfterUpgrade, StandardOpenOption.TRUNCATE_EXISTING);
+ LOG.info("Updated masterkey.");
+ }
+ LOG.info("Upgraded {} from version 5 to version 6.", vaultRoot);
+ }
+
+}
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
index 89f12f7c..39585eff 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
@@ -40,7 +40,7 @@ public void testSetNoPassphrase() {
}
@Test
- @SuppressWarnings({ "unchecked", "deprecation" })
+ @SuppressWarnings({"unchecked", "deprecation"})
public void testSetOnlyPassphrase() {
String passphrase = "aPassphrase";
CryptoFileSystemProperties inTest = cryptoFileSystemProperties() //
@@ -51,16 +51,17 @@ public void testSetOnlyPassphrase() {
assertThat(inTest.masterkeyFilename(), is(DEFAULT_MASTERKEY_FILENAME));
assertThat(inTest.readonly(), is(false));
assertThat(inTest.initializeImplicitly(), is(true));
+ assertThat(inTest.migrateImplicitly(), is(true));
assertThat(inTest.entrySet(),
containsInAnyOrder( //
anEntry(PROPERTY_PASSPHRASE, passphrase), //
anEntry(PROPERTY_PEPPER, DEFAULT_PEPPER), //
anEntry(PROPERTY_MASTERKEY_FILENAME, DEFAULT_MASTERKEY_FILENAME), //
- anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.INIT_IMPLICITLY))));
+ anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.INIT_IMPLICITLY, FileSystemFlags.MIGRATE_IMPLICITLY))));
}
@Test
- @SuppressWarnings({ "unchecked", "deprecation" })
+ @SuppressWarnings({"unchecked", "deprecation"})
public void testSetPassphraseAndReadonlyFlag() {
String passphrase = "aPassphrase";
CryptoFileSystemProperties inTest = cryptoFileSystemProperties() //
@@ -72,16 +73,17 @@ public void testSetPassphraseAndReadonlyFlag() {
assertThat(inTest.masterkeyFilename(), is(DEFAULT_MASTERKEY_FILENAME));
assertThat(inTest.readonly(), is(true));
assertThat(inTest.initializeImplicitly(), is(true));
+ assertThat(inTest.migrateImplicitly(), is(true));
assertThat(inTest.entrySet(),
containsInAnyOrder( //
anEntry(PROPERTY_PASSPHRASE, passphrase), //
anEntry(PROPERTY_PEPPER, DEFAULT_PEPPER), //
anEntry(PROPERTY_MASTERKEY_FILENAME, DEFAULT_MASTERKEY_FILENAME), //
- anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY, FileSystemFlags.INIT_IMPLICITLY))));
+ anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY, FileSystemFlags.INIT_IMPLICITLY, FileSystemFlags.MIGRATE_IMPLICITLY))));
}
@Test
- @SuppressWarnings({ "unchecked", "deprecation" })
+ @SuppressWarnings({"unchecked", "deprecation"})
public void testSetPassphraseAndMasterkeyFilenameAndReadonlyFlag() {
String passphrase = "aPassphrase";
String masterkeyFilename = "aMasterkeyFilename";
@@ -95,16 +97,17 @@ public void testSetPassphraseAndMasterkeyFilenameAndReadonlyFlag() {
assertThat(inTest.masterkeyFilename(), is(masterkeyFilename));
assertThat(inTest.readonly(), is(true));
assertThat(inTest.initializeImplicitly(), is(true));
+ assertThat(inTest.migrateImplicitly(), is(true));
assertThat(inTest.entrySet(),
containsInAnyOrder( //
anEntry(PROPERTY_PASSPHRASE, passphrase), //
anEntry(PROPERTY_PEPPER, DEFAULT_PEPPER), //
anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
- anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY, FileSystemFlags.INIT_IMPLICITLY))));
+ anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY, FileSystemFlags.INIT_IMPLICITLY, FileSystemFlags.MIGRATE_IMPLICITLY))));
}
@Test
- @SuppressWarnings({ "unchecked" })
+ @SuppressWarnings({"unchecked"})
public void testFromMap() {
Map map = new HashMap<>();
String passphrase = "aPassphrase";
@@ -212,7 +215,7 @@ public void testWrapMapWithInvalidPassphrase() {
}
@Test
- @SuppressWarnings({ "unchecked", "deprecation" })
+ @SuppressWarnings({"unchecked", "deprecation"})
public void testWrapMapWithoutReadonly() {
Map map = new HashMap<>();
String passphrase = "aPassphrase";
@@ -225,12 +228,13 @@ public void testWrapMapWithoutReadonly() {
assertThat(inTest.masterkeyFilename(), is(DEFAULT_MASTERKEY_FILENAME));
assertThat(inTest.readonly(), is(false));
assertThat(inTest.initializeImplicitly(), is(true));
+ assertThat(inTest.migrateImplicitly(), is(true));
assertThat(inTest.entrySet(),
containsInAnyOrder( //
anEntry(PROPERTY_PASSPHRASE, passphrase), //
anEntry(PROPERTY_PEPPER, pepper), //
anEntry(PROPERTY_MASTERKEY_FILENAME, DEFAULT_MASTERKEY_FILENAME), //
- anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.INIT_IMPLICITLY))));
+ anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.INIT_IMPLICITLY, FileSystemFlags.MIGRATE_IMPLICITLY))));
}
@Test
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
index 3be36b17..6b6ab6b4 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
@@ -35,6 +35,8 @@
import org.junit.Test;
import org.junit.rules.ExpectedException;
+import com.google.common.io.MoreFiles;
+import com.google.common.io.RecursiveDeleteOption;
import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
@@ -43,38 +45,39 @@ public class CryptoFileSystemProviderIntegrationTest {
@Rule
public ExpectedException thrown = ExpectedException.none();
- private Path tmpPath;
+ private FileSystem tmpFs;
+ private Path pathToVault;
+ private Path masterkeyFile;
@Before
public void setup() throws IOException {
- tmpPath = Files.createTempDirectory("unit-tests");
+ tmpFs = Jimfs.newFileSystem(Configuration.unix());
+ pathToVault = tmpFs.getPath("/vaultDir");
+ masterkeyFile = pathToVault.resolve("masterkey.cryptomator");
+ Files.createDirectory(pathToVault);
}
@After
public void teardown() throws IOException {
- Files.walkFileTree(tmpPath, new DeletingFileVisitor());
+ tmpFs.close();
}
@Test
public void testGetFsViaNioApi() throws IOException {
- URI fsUri = CryptoFileSystemUri.create(tmpPath);
+ URI fsUri = CryptoFileSystemUri.create(pathToVault);
FileSystem fs = FileSystems.newFileSystem(fsUri, cryptoFileSystemProperties().withPassphrase("asd").build());
Assert.assertTrue(fs instanceof CryptoFileSystemImpl);
- Assert.assertTrue(Files.exists(tmpPath.resolve("masterkey.cryptomator")));
+ Assert.assertTrue(Files.exists(masterkeyFile));
FileSystem fs2 = FileSystems.getFileSystem(fsUri);
Assert.assertSame(fs, fs2);
}
@Test
public void testInitAndOpenFsWithPepper() throws IOException {
- FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
- Path pathToVault = fs.getPath("/vaultDir");
- Path masterkeyFile = pathToVault.resolve("masterkey.cryptomator");
Path dataDir = pathToVault.resolve("d");
byte[] pepper = "pepper".getBytes(StandardCharsets.US_ASCII);
// Initialize vault:
- Files.createDirectory(pathToVault);
CryptoFileSystemProvider.initialize(pathToVault, "masterkey.cryptomator", pepper, "asd");
Assert.assertTrue(Files.isDirectory(dataDir));
Assert.assertTrue(Files.isRegularFile(masterkeyFile));
@@ -100,9 +103,25 @@ public void testInitAndOpenFsWithPepper() throws IOException {
CryptoFileSystemProvider.newFileSystem(pathToVault, wrongProperties);
}
+ @Test
+ public void testChangePassphraseWithUnsupportedVersion() throws IOException {
+ Files.write(masterkeyFile, "{\"version\": 0}".getBytes(StandardCharsets.US_ASCII));
+ thrown.expect(FileSystemNeedsMigrationException.class);
+ CryptoFileSystemProvider.changePassphrase(pathToVault, "masterkey.cryptomator", "foo", "bar");
+ }
+
+ @Test
+ public void testChangePassphrase() throws IOException {
+ CryptoFileSystemProvider.initialize(pathToVault, "masterkey.cryptomator", "foo");
+ CryptoFileSystemProvider.changePassphrase(pathToVault, "masterkey.cryptomator", "foo", "bar");
+ try (FileSystem fs = CryptoFileSystemProvider.newFileSystem(pathToVault, CryptoFileSystemProperties.withPassphrase("bar").build())) {
+ Assert.assertNotNull(fs);
+ }
+ }
+
@Test
public void testOpenAndCloseFileChannel() throws IOException {
- FileSystem fs = CryptoFileSystemProvider.newFileSystem(tmpPath, cryptoFileSystemProperties().withPassphrase("asd").build());
+ FileSystem fs = CryptoFileSystemProvider.newFileSystem(pathToVault, cryptoFileSystemProperties().withPassphrase("asd").build());
try (FileChannel ch = FileChannel.open(fs.getPath("/foo"), EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW))) {
Assert.assertTrue(ch instanceof CryptoFileChannel);
}
@@ -110,10 +129,10 @@ public void testOpenAndCloseFileChannel() throws IOException {
@Test
public void testCopyFileFromOneCryptoFileSystemToAnother() throws IOException {
- byte[] data = new byte[] { 1, 2, 3, 4, 5, 6, 7 };
+ byte[] data = new byte[] {1, 2, 3, 4, 5, 6, 7};
- Path fs1Location = tmpPath.resolve("foo");
- Path fs2Location = tmpPath.resolve("bar");
+ Path fs1Location = pathToVault.resolve("foo");
+ Path fs2Location = pathToVault.resolve("bar");
Files.createDirectories(fs1Location);
Files.createDirectories(fs2Location);
FileSystem fs1 = CryptoFileSystemProvider.newFileSystem(fs1Location, cryptoFileSystemProperties().withPassphrase("asd").build());
@@ -132,11 +151,11 @@ public void testCopyFileFromOneCryptoFileSystemToAnother() throws IOException {
@Test
public void testCopyFileByRelacingExistingFromOneCryptoFileSystemToAnother() throws IOException {
- byte[] data = new byte[] { 1, 2, 3, 4, 5, 6, 7 };
- byte[] data2 = new byte[] { 10, 11, 12 };
+ byte[] data = new byte[] {1, 2, 3, 4, 5, 6, 7};
+ byte[] data2 = new byte[] {10, 11, 12};
- Path fs1Location = tmpPath.resolve("foo");
- Path fs2Location = tmpPath.resolve("bar");
+ Path fs1Location = pathToVault.resolve("foo");
+ Path fs2Location = pathToVault.resolve("bar");
Files.createDirectories(fs1Location);
Files.createDirectories(fs2Location);
FileSystem fs1 = CryptoFileSystemProvider.newFileSystem(fs1Location, cryptoFileSystemProperties().withPassphrase("asd").build());
@@ -156,10 +175,10 @@ public void testCopyFileByRelacingExistingFromOneCryptoFileSystemToAnother() thr
@Test
public void testMoveFileFromOneCryptoFileSystemToAnother() throws IOException {
- byte[] data = new byte[] { 1, 2, 3, 4, 5, 6, 7 };
+ byte[] data = new byte[] {1, 2, 3, 4, 5, 6, 7};
- Path fs1Location = tmpPath.resolve("foo");
- Path fs2Location = tmpPath.resolve("bar");
+ Path fs1Location = pathToVault.resolve("foo");
+ Path fs2Location = pathToVault.resolve("bar");
Files.createDirectories(fs1Location);
Files.createDirectories(fs2Location);
FileSystem fs1 = CryptoFileSystemProvider.newFileSystem(fs1Location, cryptoFileSystemProperties().withPassphrase("asd").build());
@@ -180,6 +199,7 @@ public void testMoveFileFromOneCryptoFileSystemToAnother() throws IOException {
public void testDosFileAttributes() throws IOException {
Assume.assumeTrue(IS_OS_WINDOWS);
+ Path tmpPath = Files.createTempDirectory("unit-tests");
FileSystem fs = CryptoFileSystemProvider.newFileSystem(tmpPath, cryptoFileSystemProperties().withPassphrase("asd").build());
Path file = fs.getPath("/test");
Files.write(file, new byte[1]);
@@ -193,6 +213,8 @@ public void testDosFileAttributes() throws IOException {
assertThat(Files.getAttribute(file, "dos:system"), is(true));
assertThat(Files.getAttribute(file, "dos:archive"), is(true));
assertThat(Files.getAttribute(file, "dos:readOnly"), is(true));
+
+ MoreFiles.deleteRecursively(tmpPath, RecursiveDeleteOption.ALLOW_INSECURE);
}
}
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
index 7dd60d3b..94b8df4a 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
@@ -29,6 +29,7 @@
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;
@@ -147,8 +148,7 @@ public void setup() {
}
@Theory
- public void testInvocationsWithPathFromOtherProviderFailWithProviderMismatchException(@FromDataPoints("shouldFailWithProviderMismatch") InvocationWhichShouldFail shouldFailWithProviderMismatch)
- throws IOException {
+ public void testInvocationsWithPathFromOtherProviderFailWithProviderMismatchException(@FromDataPoints("shouldFailWithProviderMismatch") InvocationWhichShouldFail shouldFailWithProviderMismatch) throws IOException {
thrown.expect(ProviderMismatchException.class);
shouldFailWithProviderMismatch.invoke(inTest, otherPath);
@@ -210,11 +210,14 @@ public void testNoImplicitInitialization() throws IOException {
.withMasterkeyFilename("masterkey.cryptomator") //
.withPassphrase("asd") //
.build();
- inTest.newFileSystem(uri, properties);
- verify(fileSystems).create(eq(pathToVault), eq(properties));
-
- Assert.assertTrue(Files.notExists(dataDir));
- Assert.assertTrue(Files.notExists(masterkeyFile));
+ try {
+ thrown.expect(NoSuchFileException.class);
+ thrown.expectMessage("Vault not initialized");
+ inTest.newFileSystem(uri, properties);
+ } finally {
+ Assert.assertTrue(Files.notExists(dataDir));
+ Assert.assertTrue(Files.notExists(masterkeyFile));
+ }
}
@Test
@@ -233,26 +236,15 @@ public void testImplicitInitialization() throws IOException {
.withMasterkeyFilename("masterkey.cryptomator") //
.withPassphrase("asd") //
.build();
- inTest.newFileSystem(uri, properties);
+ when(fileSystems.create(eq(pathToVault), eq(properties))).thenReturn(cryptoFileSystem);
+ FileSystem result = inTest.newFileSystem(uri, properties);
verify(fileSystems).create(eq(pathToVault), eq(properties));
+ Assert.assertThat(result, is(cryptoFileSystem));
Assert.assertTrue(Files.isDirectory(dataDir));
Assert.assertTrue(Files.isRegularFile(masterkeyFile));
}
- @Test
- public void testNewFileSystemInvokesFileSystemsCreate() throws IOException {
- Path pathToVault = get("a").toAbsolutePath();
-
- URI uri = CryptoFileSystemUri.create(pathToVault);
- CryptoFileSystemProperties properties = cryptoFileSystemProperties().withPassphrase("asd").withFlags().build();
- when(fileSystems.create(eq(pathToVault), eq(properties))).thenReturn(cryptoFileSystem);
-
- FileSystem result = inTest.newFileSystem(uri, properties);
-
- assertThat(result, is(cryptoFileSystem));
- }
-
@Test
public void testContainsVaultReturnsTrueIfDirectoryContainsMasterkeyFileAndDataDir() throws IOException {
FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/MigrationComponentTest.java b/src/test/java/org/cryptomator/cryptofs/migration/MigrationComponentTest.java
new file mode 100644
index 00000000..ff150644
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptofs/migration/MigrationComponentTest.java
@@ -0,0 +1,19 @@
+package org.cryptomator.cryptofs.migration;
+
+import java.security.NoSuchAlgorithmException;
+
+import org.cryptomator.cryptofs.mocks.NullSecureRandom;
+import org.cryptomator.cryptolib.common.SecureRandomModule;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class MigrationComponentTest {
+
+ @Test
+ public void testAvailableMigrators() throws NoSuchAlgorithmException {
+ SecureRandomModule secRndModule = new SecureRandomModule(new NullSecureRandom());
+ TestMigrationComponent comp = DaggerTestMigrationComponent.builder().secureRandomModule(secRndModule).build();
+ Assert.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
new file mode 100644
index 00000000..2ff16976
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java
@@ -0,0 +1,107 @@
+/*******************************************************************************
+ * 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;
+
+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.Path;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.Collections;
+import java.util.HashMap;
+
+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.UnsupportedVaultFormatException;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class MigratorsTest {
+
+ private ByteBuffer keyFile;
+ private Path pathToVault;
+
+ @Before
+ public void setup() throws IOException {
+ keyFile = StandardCharsets.UTF_8.encode("{\"version\": 0000}");
+ pathToVault = Mockito.mock(Path.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 {
+ Migrators migrators = new Migrators(Collections.emptyMap());
+ boolean result = migrators.needsMigration(pathToVault, "masterkey.cryptomator");
+
+ Assert.assertTrue(result);
+ }
+
+ @Test
+ public void testNeedsNoMigration() throws IOException {
+ keyFile = StandardCharsets.UTF_8.encode("{\"version\": 9999}");
+
+ Migrators migrators = new Migrators(Collections.emptyMap());
+ boolean result = migrators.needsMigration(pathToVault, "masterkey.cryptomator");
+
+ Assert.assertFalse(result);
+ }
+
+ @Test(expected = NoApplicableMigratorException.class)
+ public void testMigrateWithoutMigrators() throws IOException {
+ Migrators migrators = new Migrators(Collections.emptyMap());
+ migrators.migrate(pathToVault, "masterkey.cryptomator", "secret");
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void testMigrate() throws NoApplicableMigratorException, InvalidPassphraseException, IOException {
+ Migrator migrator = Mockito.mock(Migrator.class);
+ Migrators migrators = new Migrators(new HashMap() {
+ {
+ put(Migration.ZERO_TO_ONE, migrator);
+ }
+ });
+ migrators.migrate(pathToVault, "masterkey.cryptomator", "secret");
+ Mockito.verify(migrator).migrate(pathToVault, "masterkey.cryptomator", "secret");
+ }
+
+ @Test(expected = IllegalStateException.class)
+ @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);
+ }
+ });
+ Mockito.doThrow(new UnsupportedVaultFormatException(Integer.MAX_VALUE, 1)).when(migrator).migrate(pathToVault, "masterkey.cryptomator", "secret");
+ migrators.migrate(pathToVault, "masterkey.cryptomator", "secret");
+ }
+
+}
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/TestMigrationComponent.java b/src/test/java/org/cryptomator/cryptofs/migration/TestMigrationComponent.java
new file mode 100644
index 00000000..40089d2f
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptofs/migration/TestMigrationComponent.java
@@ -0,0 +1,14 @@
+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/v6/Version6MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java
new file mode 100644
index 00000000..bb4b5246
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java
@@ -0,0 +1,71 @@
+package org.cryptomator.cryptofs.migration.v6;
+
+import java.io.IOException;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.text.Normalizer;
+import java.text.Normalizer.Form;
+
+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.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.jimfs.Configuration;
+import com.google.common.jimfs.Jimfs;
+
+public class Version6MigratorTest {
+
+ private FileSystem fs;
+ private Path pathToVault;
+ private Path masterkeyFile;
+ private Path masterkeyBackupFile;
+ private CryptorProvider cryptorProvider;
+
+ @Before
+ public void setup() throws IOException {
+ cryptorProvider = Cryptors.version1(new NullSecureRandom());
+ fs = Jimfs.newFileSystem(Configuration.unix());
+ pathToVault = fs.getPath("/vaultDir");
+ masterkeyFile = pathToVault.resolve("masterkey.cryptomator");
+ masterkeyBackupFile = pathToVault.resolve("masterkey.cryptomator.bkup");
+ Files.createDirectory(pathToVault);
+ }
+
+ @After
+ public void teardown() throws IOException {
+ fs.close();
+ }
+
+ @Test
+ public void testMigrate() throws IOException {
+ String oldPassword = Normalizer.normalize("ä", Form.NFD);
+ String newPassword = Normalizer.normalize("ä", Form.NFC);
+ Assert.assertNotEquals(oldPassword, newPassword);
+
+ KeyFile beforeMigration = cryptorProvider.createNew().writeKeysToMasterkeyFile(oldPassword, 5);
+ Assert.assertEquals(5, beforeMigration.getVersion());
+ Files.write(masterkeyFile, beforeMigration.serialize());
+
+ Migrator migrator = new Version6Migrator(cryptorProvider);
+ migrator.migrate(pathToVault, "masterkey.cryptomator", oldPassword);
+
+ KeyFile afterMigration = KeyFile.parse(Files.readAllBytes(masterkeyFile));
+ Assert.assertEquals(6, afterMigration.getVersion());
+ try (Cryptor cryptor = cryptorProvider.createFromKeyFile(afterMigration, newPassword, 6)) {
+ Assert.assertNotNull(cryptor);
+ }
+
+ Assert.assertTrue(Files.exists(masterkeyBackupFile));
+ KeyFile backupKey = KeyFile.parse(Files.readAllBytes(masterkeyBackupFile));
+ Assert.assertEquals(5, backupKey.getVersion());
+ }
+
+}