From 67ae1cd829c96d1bcb4371b10cf675e3679c6b4d Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 19 Jun 2017 01:20:26 +0200 Subject: [PATCH 1/9] Updated vault version to 6, added migration capabilities, now normalizing passwords with unicode normalization form C --- .../org/cryptomator/cryptofs/Constants.java | 21 ++-- .../cryptofs/CryptoFileSystemProperties.java | 4 +- .../cryptofs/CryptoFileSystemProvider.java | 34 +++++-- .../cryptofs/migration/Migration.java | 29 ++++++ .../migration/MigrationComponent.java | 15 +++ .../cryptofs/migration/MigrationModule.java | 56 +++++++++++ .../cryptofs/migration/Migrators.java | 97 +++++++++++++++++++ .../cryptofs/migration/api/Migrator.java | 44 +++++++++ .../api/NoApplicableMigratorException.java | 10 ++ .../migration/v6/Version6Migrator.java | 60 ++++++++++++ .../cryptofs/migration/MigratorsTest.java | 93 ++++++++++++++++++ 11 files changed, 442 insertions(+), 21 deletions(-) create mode 100644 src/main/java/org/cryptomator/cryptofs/migration/Migration.java create mode 100644 src/main/java/org/cryptomator/cryptofs/migration/MigrationComponent.java create mode 100644 src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java create mode 100644 src/main/java/org/cryptomator/cryptofs/migration/Migrators.java create mode 100644 src/main/java/org/cryptomator/cryptofs/migration/api/Migrator.java create mode 100644 src/main/java/org/cryptomator/cryptofs/migration/api/NoApplicableMigratorException.java create mode 100644 src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java create mode 100644 src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java diff --git a/src/main/java/org/cryptomator/cryptofs/Constants.java b/src/main/java/org/cryptomator/cryptofs/Constants.java index 0049bd5b..aed2bb0b 100644 --- a/src/main/java/org/cryptomator/cryptofs/Constants.java +++ b/src/main/java/org/cryptomator/cryptofs/Constants.java @@ -1,23 +1,24 @@ /******************************************************************************* - * Copyright (c) 2016 Sebastian Stenzel and others. + * Copyright (c) 2016, 2017 Sebastian Stenzel and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the accompanying LICENSE.txt. * * Contributors: * Sebastian Stenzel - initial API and implementation - *******************************************************************************/ + *******************************************************************************/ package org.cryptomator.cryptofs; -final class Constants { +public final class Constants { + public static final int VAULT_VERSION = 6; public static final String MASTERKEY_BACKUP_SUFFIX = ".bkup"; - public static final String DATA_DIR_NAME = "d"; - public static final String METADATA_DIR_NAME = "m"; - public static final String DIR_PREFIX = "0"; - public static final int NAME_SHORTENING_THRESHOLD = 129; - public static final int VAULT_VERSION = 5; - public static final String ROOT_DIR_ID = ""; - public static final String SEPARATOR = "/"; + static final String DATA_DIR_NAME = "d"; + static final String METADATA_DIR_NAME = "m"; + static final String DIR_PREFIX = "0"; + static final int NAME_SHORTENING_THRESHOLD = 129; + static final String ROOT_DIR_ID = ""; + + static final String SEPARATOR = "/"; } diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java index 64cad408..25849e58 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java @@ -14,6 +14,8 @@ 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; @@ -217,7 +219,7 @@ private void checkedSet(Class type, String key, Map properties * @return this */ public Builder withPassphrase(CharSequence passphrase) { - this.passphrase = passphrase; + this.passphrase = Normalizer.normalize(passphrase, Form.NFC); return this; } diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java index 86427f40..cbde10ed 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java @@ -39,6 +39,8 @@ 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.Map; import java.util.Set; import java.util.concurrent.ExecutorService; @@ -98,6 +100,22 @@ public CryptoFileSystemProvider() { this.moveOperation = component.moveOperation(); } + 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 + * @return a new file system + * @throws IOException if an I/O error occurs creating the file system + */ public static CryptoFileSystem newFileSystem(Path pathToVault, CryptoFileSystemProperties properties) throws IOException { return (CryptoFileSystem) FileSystems.newFileSystem(create(pathToVault.toAbsolutePath()), properties); } @@ -134,7 +152,7 @@ public static void initialize(Path pathToVault, String masterkeyFilename, byte[] try (Cryptor cryptor = CRYPTOR_PROVIDER.createNew()) { // save masterkey file: Path masterKeyPath = pathToVault.resolve(masterkeyFilename); - byte[] keyFileContents = cryptor.writeKeysToMasterkeyFile(passphrase, pepper, Constants.VAULT_VERSION).serialize(); + 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); @@ -170,22 +188,16 @@ public static boolean containsVault(Path pathToVault, String masterkeyFilename) * @since 1.1.0 */ public static void changePassphrase(Path pathToVault, String masterkeyFilename, CharSequence oldPassphrase, CharSequence newPassphrase) throws InvalidPassphraseException, IOException { + String normalizedOldPassphrase = Normalizer.normalize(oldPassphrase, Form.NFC); + String normalizedNewPassphrase = Normalizer.normalize(newPassphrase, Form.NFC); Path masterKeyPath = pathToVault.resolve(masterkeyFilename); Path backupKeyPath = pathToVault.resolve(masterkeyFilename + Constants.MASTERKEY_BACKUP_SUFFIX); byte[] oldMasterkeyBytes = Files.readAllBytes(masterKeyPath); - byte[] newMasterkeyBytes = Cryptors.changePassphrase(CRYPTOR_PROVIDER, oldMasterkeyBytes, oldPassphrase, newPassphrase); + byte[] newMasterkeyBytes = Cryptors.changePassphrase(CRYPTOR_PROVIDER, oldMasterkeyBytes, normalizedOldPassphrase, normalizedNewPassphrase); Files.move(masterKeyPath, backupKeyPath, REPLACE_EXISTING, ATOMIC_MOVE); Files.write(masterKeyPath, newMasterkeyBytes, CREATE_NEW, WRITE); } - private static SecureRandom strongSecureRandom() { - try { - return SecureRandom.getInstanceStrong(); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("A strong algorithm must exist in every Java platform.", e); - } - } - /** * @deprecated only for testing */ @@ -219,6 +231,8 @@ public CryptoFileSystem newFileSystem(URI uri, Map rawProperties) thr CryptoFileSystemProvider.initialize(parsedUri.pathToVault(), properties.masterkeyFilename(), properties.passphrase()); } + // TODO overheadhunter: check for compatibility, expose and document specific exception if vault needs to get migrated + return fileSystems.create(parsedUri.pathToVault(), properties); } 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..d976c8ce --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * 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; + +/** + * @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) { + 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/migration/MigratorsTest.java b/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java new file mode 100644 index 00000000..db646529 --- /dev/null +++ b/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * 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.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"); + } + +} From bcbeae89ca0528dadc15f44bdf181c97ebe5aa81 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 19 Jun 2017 11:09:11 +0200 Subject: [PATCH 2/9] added migration tests --- .../cryptofs/CryptoFileSystemProvider.java | 7 +- .../migration/MigrationComponentTest.java | 19 +++++ .../cryptofs/migration/MigratorsTest.java | 14 ++++ .../migration/TestMigrationComponent.java | 14 ++++ .../migration/v6/Version6MigratorTest.java | 71 +++++++++++++++++++ 5 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 src/test/java/org/cryptomator/cryptofs/migration/MigrationComponentTest.java create mode 100644 src/test/java/org/cryptomator/cryptofs/migration/TestMigrationComponent.java create mode 100644 src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java index cbde10ed..9ea02808 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java @@ -12,7 +12,6 @@ import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import static java.nio.file.StandardOpenOption.CREATE_NEW; import static java.nio.file.StandardOpenOption.WRITE; -import static org.cryptomator.cryptofs.CryptoFileSystemUri.create; import java.io.IOException; import java.net.URI; @@ -68,7 +67,8 @@ * storageLocation, * {@link CryptoFileSystemProperties cryptoFileSystemProperties()} * .withPassword("password") - * .withReadonlyFlag().build()); + * .withFlags(FileSystemFlags.READONLY) + * .build()); * * * @@ -117,7 +117,8 @@ private static SecureRandom strongSecureRandom() { * @throws IOException if an I/O error occurs creating the file system */ public static CryptoFileSystem newFileSystem(Path pathToVault, CryptoFileSystemProperties properties) throws IOException { - return (CryptoFileSystem) FileSystems.newFileSystem(create(pathToVault.toAbsolutePath()), properties); + URI uri = CryptoFileSystemUri.create(pathToVault.toAbsolutePath()); + return (CryptoFileSystem) FileSystems.newFileSystem(uri, properties); } /** 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 index db646529..2ff16976 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java @@ -18,6 +18,7 @@ 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; @@ -90,4 +91,17 @@ public void testMigrate() throws NoApplicableMigratorException, InvalidPassphras 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()); + } + +} From 67e0ec8975151565b6e7960452cbafb60f45c13d Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 19 Jun 2017 13:06:14 +0200 Subject: [PATCH 3/9] Added vault migration options to CryptoFS --- .../cryptofs/CryptoFileSystemProperties.java | 26 +++++++- .../cryptofs/CryptoFileSystemProvider.java | 19 +++++- .../FileSystemNeedsMigrationException.java | 20 ++++++ .../cryptofs/migration/Migrators.java | 13 ++++ ...yptoFileSystemProviderIntegrationTest.java | 62 +++++++++++++------ 5 files changed, 116 insertions(+), 24 deletions(-) create mode 100644 src/main/java/org/cryptomator/cryptofs/FileSystemNeedsMigrationException.java diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java index 25849e58..929aa6de 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java @@ -67,7 +67,7 @@ public class CryptoFileSystemProperties extends AbstractMap { */ public static final String PROPERTY_FILESYSTEM_FLAGS = "flags"; - static final Set DEFAULT_FILESYSTEM_FLAGS = unmodifiableSet(EnumSet.of(FileSystemFlags.INIT_IMPLICITLY)); + static final Set DEFAULT_FILESYSTEM_FLAGS = unmodifiableSet(EnumSet.of(FileSystemFlags.MIGRATE_IMPLICITLY, FileSystemFlags.INIT_IMPLICITLY)); public enum FileSystemFlags { /** @@ -75,6 +75,14 @@ public enum FileSystemFlags { */ 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. + * + * @since 1.4.0 + */ + MIGRATE_IMPLICITLY, + /** * If present, the vault structure will implicitly get initialized upon filesystem creation. * @@ -111,6 +119,10 @@ boolean readonly() { return flags().contains(FileSystemFlags.READONLY); } + boolean migrateImplicitly() { + return flags().contains(FileSystemFlags.MIGRATE_IMPLICITLY); + } + boolean initializeImplicitly() { return flags().contains(FileSystemFlags.INIT_IMPLICITLY); } @@ -152,6 +164,18 @@ 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} * diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java index 9ea02808..25aece90 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java @@ -44,6 +44,7 @@ import java.util.Set; import java.util.concurrent.ExecutorService; +import org.cryptomator.cryptofs.migration.Migrators; import org.cryptomator.cryptolib.Cryptors; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.CryptorProvider; @@ -114,9 +115,10 @@ private static SecureRandom strongSecureRandom() { * @param pathToVault Path to this vault's storage location * @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 IOException if an I/O error occurs creating the file system */ - public static CryptoFileSystem newFileSystem(Path pathToVault, CryptoFileSystemProperties properties) throws IOException { + public static CryptoFileSystem newFileSystem(Path pathToVault, CryptoFileSystemProperties properties) throws FileSystemNeedsMigrationException, IOException { URI uri = CryptoFileSystemUri.create(pathToVault.toAbsolutePath()); return (CryptoFileSystem) FileSystems.newFileSystem(uri, properties); } @@ -185,10 +187,15 @@ public static boolean containsVault(Path pathToVault, String masterkeyFilename) * @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.1.0 */ - public static void changePassphrase(Path pathToVault, String masterkeyFilename, CharSequence oldPassphrase, CharSequence newPassphrase) throws InvalidPassphraseException, IOException { + public static void changePassphrase(Path pathToVault, String masterkeyFilename, 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); @@ -232,7 +239,13 @@ public CryptoFileSystem newFileSystem(URI uri, Map rawProperties) thr CryptoFileSystemProvider.initialize(parsedUri.pathToVault(), properties.masterkeyFilename(), properties.passphrase()); } - // TODO overheadhunter: check for compatibility, expose and document specific exception if vault needs to get migrated + 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/Migrators.java b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java index d976c8ce..78f2939a 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java @@ -24,6 +24,18 @@ 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 { @@ -91,6 +103,7 @@ public void migrate(Path pathToVault, String masterkeyFilename, CharSequence pas } 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/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); } } From e5e683198bd6fb67256f7ae25428e6e20b4e7f6a Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 19 Jun 2017 13:30:05 +0200 Subject: [PATCH 4/9] fixed tests --- .../cryptofs/CryptoFileSystemProvider.java | 11 ++++-- .../CryptoFileSystemPropertiesTest.java | 22 +++++++----- .../CryptoFileSystemProviderTest.java | 34 +++++++------------ 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java index 25aece90..a08877cb 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java @@ -27,6 +27,7 @@ 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; @@ -234,9 +235,13 @@ public CryptoFileSystem newFileSystem(URI uri, Map 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())) { 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/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()); From b53e7ce85f57b35041d8933d95c8da9332467723 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 19 Jun 2017 13:58:33 +0200 Subject: [PATCH 5/9] fixes coverity CID 1424365 --- .../java/org/cryptomator/cryptofs/ConflictResolver.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java b/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java index 6ca2cd9a..361f093b 100644 --- a/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java +++ b/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java @@ -164,11 +164,11 @@ private boolean hasSameDirFileContent(Path conflictingPath, Path canonicalPath) ReadableByteChannel in2 = Files.newByteChannel(canonicalPath, StandardOpenOption.READ)) { ByteBuffer buf1 = ByteBuffer.allocate(MAX_DIR_FILE_SIZE); ByteBuffer buf2 = ByteBuffer.allocate(MAX_DIR_FILE_SIZE); - in1.read(buf1); - in2.read(buf2); + int read1 = in1.read(buf1); + int read2 = in2.read(buf2); buf1.flip(); buf2.flip(); - return buf1.compareTo(buf2) == 0; + return read1 == read2 && buf1.compareTo(buf2) == 0; } } From c24779c833cea46612a50af70febf007012a1d11 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 19 Jun 2017 17:02:22 +0200 Subject: [PATCH 6/9] Added pepper support to changePassphrase --- pom.xml | 2 +- .../cryptofs/CryptoFileSystemProvider.java | 21 ++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 55d520e6..b3ecaf5b 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ 1.8 - 1.1.2 + 1.1.4 2.11 22.0 3.5 diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java index a08877cb..d94df6f6 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java @@ -191,9 +191,28 @@ public static boolean containsVault(Path pathToVault, String masterkeyFilename) * @throws FileSystemNeedsMigrationException if the vault format needs to get updated. * @throws IOException If the masterkey could not be read or written. * @since 1.1.0 + * @see #changePassphrase(Path, String, byte[], CharSequence, CharSequence) */ 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); } @@ -202,7 +221,7 @@ public static void changePassphrase(Path pathToVault, String masterkeyFilename, Path masterKeyPath = pathToVault.resolve(masterkeyFilename); Path backupKeyPath = pathToVault.resolve(masterkeyFilename + Constants.MASTERKEY_BACKUP_SUFFIX); byte[] oldMasterkeyBytes = Files.readAllBytes(masterKeyPath); - byte[] newMasterkeyBytes = Cryptors.changePassphrase(CRYPTOR_PROVIDER, oldMasterkeyBytes, normalizedOldPassphrase, normalizedNewPassphrase); + byte[] newMasterkeyBytes = Cryptors.changePassphrase(CRYPTOR_PROVIDER, oldMasterkeyBytes, pepper, normalizedOldPassphrase, normalizedNewPassphrase); Files.move(masterKeyPath, backupKeyPath, REPLACE_EXISTING, ATOMIC_MOVE); Files.write(masterKeyPath, newMasterkeyBytes, CREATE_NEW, WRITE); } From 1bcbfb933393ee988b1e3380526bb51936a39236 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 19 Jun 2017 17:03:42 +0200 Subject: [PATCH 7/9] preparing release 1.4.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b3ecaf5b..39bd8eb3 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.cryptomator cryptofs - 1.4.0-SNAPSHOT + 1.4.0 Cryptomator Crypto Filesystem This library provides the Java filesystem provider used by Cryptomator. https://github.com/cryptomator/cryptofs From 7d97fda686b1a98e823470c2b9d2129b2ae16cce Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 19 Jun 2017 17:10:39 +0200 Subject: [PATCH 8/9] updated dependencies --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 39bd8eb3..8f1a0177 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ 1.1.4 2.11 22.0 - 3.5 + 3.6 1.7.25 UTF-8 @@ -108,7 +108,7 @@ org.mockito mockito-core - 2.8.9 + 2.8.47 test From 3b49095ee133c67db00029ff154781dfefe1e0d1 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 19 Jun 2017 21:14:34 +0200 Subject: [PATCH 9/9] Updated CryptoLib --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 8f1a0177..85ed078f 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ 1.8 - 1.1.4 + 1.1.5 2.11 22.0 3.6 @@ -34,14 +34,14 @@ Sebastian Stenzel - sebastian.stenzel@gmail.com + sebastian.stenzel@skymatic.de +1 cryptomator.org http://cryptomator.org Markus Kreusch - mail@markuskreusch.de + markus.kreusch@skymatic.de +1 cryptomator.org http://cryptomator.org