Bläddra i källkod

supporting change password again - now via CryptoFileSystemFactory

Sebastian Stenzel 9 år sedan
förälder
incheckning
a972480e72

+ 4 - 1
main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/Cryptor.java

@@ -21,8 +21,11 @@ public interface Cryptor extends Destroyable {
 
 	void randomizeMasterkey();
 
-	boolean readKeysFromMasterkeyFile(byte[] masterkeyFileContents, CharSequence passphrase);
+	void readKeysFromMasterkeyFile(byte[] masterkeyFileContents, CharSequence passphrase) throws InvalidPassphraseException;
 
 	byte[] writeKeysToMasterkeyFile(CharSequence passphrase);
 
+	@Override
+	void destroy();
+
 }

+ 8 - 5
main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java

@@ -24,6 +24,7 @@ import org.cryptomator.common.LazyInitializer;
 import org.cryptomator.crypto.engine.Cryptor;
 import org.cryptomator.crypto.engine.FileContentCryptor;
 import org.cryptomator.crypto.engine.FilenameCryptor;
+import org.cryptomator.crypto.engine.InvalidPassphraseException;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -86,7 +87,7 @@ public class CryptorImpl implements Cryptor {
 	}
 
 	@Override
-	public boolean readKeysFromMasterkeyFile(byte[] masterkeyFileContents, CharSequence passphrase) {
+	public void readKeysFromMasterkeyFile(byte[] masterkeyFileContents, CharSequence passphrase) {
 		final KeyFile keyFile;
 		try {
 			final ObjectMapper om = new ObjectMapper();
@@ -107,9 +108,8 @@ public class CryptorImpl implements Cryptor {
 			final SecretKey kek = new SecretKeySpec(kekBytes, ENCRYPTION_ALG);
 			this.encryptionKey = AesKeyWrap.unwrap(kek, keyFile.getEncryptionMasterKey(), ENCRYPTION_ALG);
 			this.macKey = AesKeyWrap.unwrap(kek, keyFile.getMacMasterKey(), MAC_ALG);
-			return true;
 		} catch (InvalidKeyException e) {
-			return false;
+			throw new InvalidPassphraseException();
 		} catch (NoSuchAlgorithmException e) {
 			throw new IllegalStateException("Hard-coded algorithm doesn't exist.", e);
 		} finally {
@@ -152,17 +152,20 @@ public class CryptorImpl implements Cryptor {
 	/* ======================= destruction ======================= */
 
 	@Override
-	public void destroy() throws DestroyFailedException {
+	public void destroy() {
 		destroyQuietly(encryptionKey);
 		destroyQuietly(macKey);
 	}
 
 	@Override
 	public boolean isDestroyed() {
-		return encryptionKey.isDestroyed() && macKey.isDestroyed();
+		return (encryptionKey == null || encryptionKey.isDestroyed()) && (macKey == null || macKey.isDestroyed());
 	}
 
 	private void destroyQuietly(Destroyable d) {
+		if (d == null) {
+			return;
+		}
 		try {
 			d.destroy();
 		} catch (DestroyFailedException e) {

+ 3 - 37
main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystem.java

@@ -16,60 +16,26 @@ import org.cryptomator.crypto.engine.InvalidPassphraseException;
 import org.cryptomator.filesystem.File;
 import org.cryptomator.filesystem.FileSystem;
 import org.cryptomator.filesystem.Folder;
-import org.cryptomator.filesystem.ReadableFile;
 import org.cryptomator.filesystem.WritableFile;
 
 public class CryptoFileSystem extends CryptoFolder implements FileSystem {
 
 	private static final String DATA_ROOT_DIR = "d";
 	private static final String ROOT_DIR_FILE = "root";
-	private static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
-	private static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup";
 
 	private final Folder physicalRoot;
 	private final CryptoFileSystemDelegate delegate;
 
 	public CryptoFileSystem(Folder physicalRoot, Cryptor cryptor, CryptoFileSystemDelegate delegate, CharSequence passphrase) throws InvalidPassphraseException {
 		super(null, "", cryptor);
+		if (cryptor.isDestroyed()) {
+			throw new IllegalArgumentException("Cryptor's keys must not be destroyed.");
+		}
 		this.physicalRoot = physicalRoot;
 		this.delegate = delegate;
-		final File masterkeyFile = physicalRoot.file(MASTERKEY_FILENAME);
-		if (masterkeyFile.exists()) {
-			final boolean unlocked = decryptMasterKeyFile(cryptor, masterkeyFile, passphrase);
-			if (!unlocked) {
-				throw new InvalidPassphraseException();
-			}
-		} else {
-			cryptor.randomizeMasterkey();
-			encryptMasterKeyFile(cryptor, masterkeyFile, passphrase);
-		}
-		assert masterkeyFile.exists() : "A CryptoFileSystem can not exist without a masterkey file.";
-		final File backupFile = physicalRoot.file(MASTERKEY_BACKUP_FILENAME);
-		masterkeyFile.copyTo(backupFile);
 		create();
 	}
 
-	private static boolean decryptMasterKeyFile(Cryptor cryptor, File masterkeyFile, CharSequence passphrase) {
-		try (ReadableFile file = masterkeyFile.openReadable()) {
-			// TODO we need to read the whole file but can not be sure about the
-			// buffer size:
-			final ByteBuffer bigEnoughBuffer = ByteBuffer.allocate(500);
-			file.read(bigEnoughBuffer);
-			bigEnoughBuffer.flip();
-			assert bigEnoughBuffer.remaining() < bigEnoughBuffer.capacity() : "The buffer wasn't big enough.";
-			final byte[] fileContents = new byte[bigEnoughBuffer.remaining()];
-			bigEnoughBuffer.get(fileContents);
-			return cryptor.readKeysFromMasterkeyFile(fileContents, passphrase);
-		}
-	}
-
-	private static void encryptMasterKeyFile(Cryptor cryptor, File masterkeyFile, CharSequence passphrase) {
-		try (WritableFile file = masterkeyFile.openWritable()) {
-			final byte[] fileContents = cryptor.writeKeysToMasterkeyFile(passphrase);
-			file.write(ByteBuffer.wrap(fileContents));
-		}
-	}
-
 	CryptoFileSystemDelegate delegate() {
 		return delegate;
 	}

+ 26 - 7
main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystemFactory.java

@@ -1,7 +1,8 @@
 package org.cryptomator.filesystem.crypto;
 
+import java.io.UncheckedIOException;
+
 import javax.inject.Inject;
-import javax.inject.Provider;
 import javax.inject.Singleton;
 
 import org.cryptomator.crypto.engine.Cryptor;
@@ -14,20 +15,38 @@ import org.cryptomator.filesystem.shortening.ShorteningFileSystemFactory;
 @Singleton
 public class CryptoFileSystemFactory {
 
-	private final Provider<Cryptor> cryptorProvider;
+	private final Masterkeys masterkeys;
 	private final ShorteningFileSystemFactory shorteningFileSystemFactory;
 	private final BlockAlignedFileSystemFactory blockAlignedFileSystemFactory;
 
 	@Inject
-	public CryptoFileSystemFactory(Provider<Cryptor> cryptorProvider, ShorteningFileSystemFactory shorteningFileSystemFactory, BlockAlignedFileSystemFactory blockAlignedFileSystemFactory) {
-		this.cryptorProvider = cryptorProvider;
+	public CryptoFileSystemFactory(Masterkeys masterkeys, ShorteningFileSystemFactory shorteningFileSystemFactory, BlockAlignedFileSystemFactory blockAlignedFileSystemFactory) {
+		this.masterkeys = masterkeys;
 		this.shorteningFileSystemFactory = shorteningFileSystemFactory;
 		this.blockAlignedFileSystemFactory = blockAlignedFileSystemFactory;
 	}
 
-	public FileSystem get(Folder root, CharSequence passphrase, CryptoFileSystemDelegate delegate) throws InvalidPassphraseException {
-		final FileSystem nameShorteningFs = shorteningFileSystemFactory.get(root);
-		final FileSystem cryptoFs = new CryptoFileSystem(nameShorteningFs, cryptorProvider.get(), delegate, passphrase);
+	public void initializeNew(Folder vaultLocation, CharSequence passphrase) {
+		masterkeys.initialize(vaultLocation, passphrase);
+	}
+
+	public FileSystem unlockExisting(Folder vaultLocation, CharSequence passphrase, CryptoFileSystemDelegate delegate) throws InvalidPassphraseException {
+		final Cryptor cryptor = masterkeys.decrypt(vaultLocation, passphrase);
+		masterkeys.backup(vaultLocation);
+		final FileSystem nameShorteningFs = shorteningFileSystemFactory.get(vaultLocation);
+		final FileSystem cryptoFs = new CryptoFileSystem(nameShorteningFs, cryptor, delegate, passphrase);
 		return blockAlignedFileSystemFactory.get(cryptoFs);
 	}
+
+	public void changePassphrase(Folder vaultLocation, CharSequence oldPassphrase, CharSequence newPassphrase) throws InvalidPassphraseException {
+		masterkeys.backup(vaultLocation);
+		try {
+			masterkeys.changePassphrase(vaultLocation, oldPassphrase, newPassphrase);
+			// At this point the backup is still using the old password.
+			// It will be changed as soon as the user unlocks the vault the next time.
+			// This way he can still restore the old password, if he doesn't remember the new one.
+		} catch (UncheckedIOException e) {
+			masterkeys.restoreBackup(vaultLocation);
+		}
+	}
 }

+ 96 - 0
main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/Masterkeys.java

@@ -0,0 +1,96 @@
+package org.cryptomator.filesystem.crypto;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+
+import org.apache.commons.io.IOUtils;
+import org.cryptomator.crypto.engine.Cryptor;
+import org.cryptomator.crypto.engine.InvalidPassphraseException;
+import org.cryptomator.filesystem.File;
+import org.cryptomator.filesystem.Folder;
+import org.cryptomator.filesystem.WritableFile;
+
+@Singleton
+class Masterkeys {
+
+	private static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
+	private static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup";
+
+	private final Provider<Cryptor> cryptorProvider;
+
+	@Inject
+	public Masterkeys(Provider<Cryptor> cryptorProvider) {
+		this.cryptorProvider = cryptorProvider;
+	}
+
+	public void initialize(Folder vaultLocation, CharSequence passphrase) {
+		File masterkeyFile = vaultLocation.file(MASTERKEY_FILENAME);
+		Cryptor cryptor = cryptorProvider.get();
+		try {
+			cryptor.randomizeMasterkey();
+			writeMasterKey(masterkeyFile, cryptor, passphrase);
+		} finally {
+			cryptor.destroy();
+		}
+	}
+
+	public Cryptor decrypt(Folder vaultLocation, CharSequence passphrase) throws InvalidPassphraseException {
+		File masterkeyFile = vaultLocation.file(MASTERKEY_FILENAME);
+		Cryptor cryptor = cryptorProvider.get();
+		try {
+			readMasterKey(masterkeyFile, cryptor, passphrase);
+		} catch (UncheckedIOException e) {
+			cryptor.destroy();
+		}
+		return cryptor;
+	}
+
+	public void changePassphrase(Folder vaultLocation, CharSequence oldPassphrase, CharSequence newPassphrase) throws InvalidPassphraseException {
+		File masterkeyFile = vaultLocation.file(MASTERKEY_FILENAME);
+		Cryptor cryptor = cryptorProvider.get();
+		try {
+			readMasterKey(masterkeyFile, cryptor, oldPassphrase);
+			writeMasterKey(masterkeyFile, cryptor, newPassphrase);
+		} finally {
+			cryptor.destroy();
+		}
+	}
+
+	public void backup(Folder vaultLocation) {
+		File masterkeyFile = vaultLocation.file(MASTERKEY_FILENAME);
+		File backupFile = vaultLocation.file(MASTERKEY_BACKUP_FILENAME);
+		masterkeyFile.copyTo(backupFile);
+	}
+
+	public void restoreBackup(Folder vaultLocation) {
+		File backupFile = vaultLocation.file(MASTERKEY_BACKUP_FILENAME);
+		File masterkeyFile = vaultLocation.file(MASTERKEY_FILENAME);
+		backupFile.copyTo(masterkeyFile);
+	}
+
+	/* I/O */
+
+	private static void readMasterKey(File file, Cryptor cryptor, CharSequence passphrase) throws UncheckedIOException, InvalidPassphraseException {
+		try (InputStream in = Channels.newInputStream(file.openReadable())) {
+			final byte[] fileContents = IOUtils.toByteArray(in);
+			cryptor.readKeysFromMasterkeyFile(fileContents, passphrase);
+		} catch (IOException e) {
+			throw new UncheckedIOException(e);
+		}
+	}
+
+	private static void writeMasterKey(File file, Cryptor cryptor, CharSequence passphrase) throws UncheckedIOException {
+		try (WritableFile writable = file.openWritable()) {
+			final byte[] fileContents = cryptor.writeKeysToMasterkeyFile(passphrase);
+			writable.write(ByteBuffer.wrap(fileContents));
+		}
+	}
+
+}

+ 6 - 2
main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/NoCryptor.java

@@ -29,9 +29,8 @@ public class NoCryptor implements Cryptor {
 	}
 
 	@Override
-	public boolean readKeysFromMasterkeyFile(byte[] masterkeyFileContents, CharSequence passphrase) {
+	public void readKeysFromMasterkeyFile(byte[] masterkeyFileContents, CharSequence passphrase) {
 		// thanks, but I don't need a key, if I'm not encryption anything...
-		return true;
 	}
 
 	@Override
@@ -40,4 +39,9 @@ public class NoCryptor implements Cryptor {
 		return new byte[0];
 	}
 
+	@Override
+	public void destroy() {
+		// no-op
+	}
+
 }

+ 12 - 3
main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/CryptorImplTest.java

@@ -11,19 +11,28 @@ package org.cryptomator.crypto.engine.impl;
 import java.io.IOException;
 
 import org.cryptomator.crypto.engine.Cryptor;
+import org.cryptomator.crypto.engine.InvalidPassphraseException;
 import org.junit.Assert;
 import org.junit.Test;
 
 public class CryptorImplTest {
 
 	@Test
-	public void testMasterkeyDecryption() throws IOException {
+	public void testMasterkeyDecryptionWithCorrectPassphrase() throws IOException {
 		final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
 				+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
 				+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"}";
 		final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
-		Assert.assertFalse(cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "qwe"));
-		Assert.assertTrue(cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd"));
+		cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
+	}
+
+	@Test(expected = InvalidPassphraseException.class)
+	public void testMasterkeyDecryptionWithWrongPassphrase() throws IOException {
+		final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+				+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+				+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"}";
+		final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
+		cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "qwe");
 	}
 
 	@Test

+ 27 - 1
main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/CryptoFileSystemComponentIntegrationTest.java

@@ -1,6 +1,7 @@
 package org.cryptomator.filesystem.crypto;
 
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
 import java.util.Arrays;
 
@@ -33,10 +34,35 @@ public class CryptoFileSystemComponentIntegrationTest {
 	public void setupFileSystems() {
 		cryptoDelegate = Mockito.mock(CryptoFileSystemDelegate.class);
 		ciphertextFs = new InMemoryFileSystem();
-		cleartextFs = cryptoFsComp.cryptoFileSystemFactory().get(ciphertextFs, "TopSecret", cryptoDelegate);
+		cryptoFsComp.cryptoFileSystemFactory().initializeNew(ciphertextFs, "TopSecret");
+		cleartextFs = cryptoFsComp.cryptoFileSystemFactory().unlockExisting(ciphertextFs, "TopSecret", cryptoDelegate);
 		cleartextFs.create();
 	}
 
+	@Test(timeout = 1000)
+	public void testVaultStructureInitializationAndBackupBehaviour() throws UncheckedIOException, IOException {
+		final FileSystem physicalFs = new InMemoryFileSystem();
+		final File masterkeyFile = physicalFs.file("masterkey.cryptomator");
+		final File masterkeyBkupFile = physicalFs.file("masterkey.cryptomator.bkup");
+		final Folder physicalDataRoot = physicalFs.folder("d");
+		Assert.assertFalse(masterkeyFile.exists());
+		Assert.assertFalse(masterkeyBkupFile.exists());
+		Assert.assertFalse(physicalDataRoot.exists());
+
+		cryptoFsComp.cryptoFileSystemFactory().initializeNew(physicalFs, "asd");
+		Assert.assertTrue(masterkeyFile.exists());
+		Assert.assertFalse(masterkeyBkupFile.exists());
+		Assert.assertFalse(physicalDataRoot.exists());
+
+		@SuppressWarnings("unused")
+		final FileSystem cryptoFs = cryptoFsComp.cryptoFileSystemFactory().unlockExisting(physicalFs, "asd", cryptoDelegate);
+		Assert.assertTrue(masterkeyBkupFile.exists());
+		Assert.assertTrue(physicalDataRoot.exists());
+		Assert.assertEquals(3, physicalFs.children().count()); // d + masterkey.cryptomator + masterkey.cryptomator.bkup
+		Assert.assertEquals(1, physicalDataRoot.files().count()); // ROOT file
+		Assert.assertEquals(1, physicalDataRoot.folders().count()); // ROOT directory
+	}
+
 	@Test
 	public void testEncryptionOfLongFolderNames() {
 		final String shortName = "normal folder name";

+ 0 - 54
main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/CryptoFileSystemTest.java

@@ -13,13 +13,11 @@ import static org.cryptomator.filesystem.FileSystemVisitor.fileSystemVisitor;
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
-import java.time.Instant;
 import java.util.Arrays;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.cryptomator.crypto.engine.Cryptor;
 import org.cryptomator.crypto.engine.NoCryptor;
-import org.cryptomator.filesystem.File;
 import org.cryptomator.filesystem.FileSystem;
 import org.cryptomator.filesystem.Folder;
 import org.cryptomator.filesystem.ReadableFile;
@@ -31,58 +29,6 @@ import org.mockito.Mockito;
 
 public class CryptoFileSystemTest {
 
-	@Test(timeout = 1000)
-	public void testVaultStructureInitialization() throws UncheckedIOException, IOException {
-		// mock cryptor:
-		final Cryptor cryptor = new NoCryptor();
-
-		// some mock fs:
-		final FileSystem physicalFs = new InMemoryFileSystem();
-		final File masterkeyFile = physicalFs.file("masterkey.cryptomator");
-		final File masterkeyBkupFile = physicalFs.file("masterkey.cryptomator.bkup");
-		final Folder physicalDataRoot = physicalFs.folder("d");
-		Assert.assertFalse(masterkeyFile.exists());
-		Assert.assertFalse(masterkeyBkupFile.exists());
-		Assert.assertFalse(physicalDataRoot.exists());
-
-		// init crypto fs:
-		final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor, Mockito.mock(CryptoFileSystemDelegate.class), "foo");
-		Assert.assertTrue(masterkeyFile.exists());
-		Assert.assertTrue(masterkeyBkupFile.exists());
-		fs.create();
-		Assert.assertTrue(physicalDataRoot.exists());
-		Assert.assertEquals(3, physicalFs.children().count()); // d + masterkey.cryptomator + masterkey.cryptomator.bkup
-		Assert.assertEquals(1, physicalDataRoot.files().count()); // ROOT file
-		Assert.assertEquals(1, physicalDataRoot.folders().count()); // ROOT directory
-	}
-
-	@Test(timeout = 1000)
-	public void testMasterkeyBackupBehaviour() throws InterruptedException {
-		// mock cryptor:
-		final Cryptor cryptor = new NoCryptor();
-
-		// some mock fs:
-		final FileSystem physicalFs = new InMemoryFileSystem();
-		final File masterkeyBkupFile = physicalFs.file("masterkey.cryptomator.bkup");
-		Assert.assertFalse(masterkeyBkupFile.exists());
-
-		// first initialization:
-		new CryptoFileSystem(physicalFs, cryptor, Mockito.mock(CryptoFileSystemDelegate.class), "foo");
-		Assert.assertTrue(masterkeyBkupFile.exists());
-		final Instant bkupDateT0 = masterkeyBkupFile.lastModified();
-
-		// make sure some time passes, as the resolution of last modified date
-		// is not in nanos:
-		Thread.sleep(1);
-
-		// second initialization:
-		new CryptoFileSystem(physicalFs, cryptor, Mockito.mock(CryptoFileSystemDelegate.class), "foo");
-		Assert.assertTrue(masterkeyBkupFile.exists());
-		final Instant bkupDateT1 = masterkeyBkupFile.lastModified();
-
-		Assert.assertTrue(bkupDateT1.isAfter(bkupDateT0));
-	}
-
 	@Test(timeout = 1000)
 	public void testDirectoryCreation() throws UncheckedIOException, IOException {
 		// mock stuff and prepare crypto FS:

+ 9 - 2
main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileSystemFactories.java

@@ -10,6 +10,7 @@ import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
 
+import org.cryptomator.crypto.engine.Cryptor;
 import org.cryptomator.crypto.engine.impl.CryptorImpl;
 import org.cryptomator.filesystem.FileSystem;
 import org.cryptomator.filesystem.crypto.CryptoFileSystem;
@@ -50,11 +51,17 @@ class FileSystemFactories implements Iterable<FileSystemFactory> {
 	}
 
 	private FileSystem createCryptoFileSystemInMemory() {
-		return new CryptoFileSystem(createInMemoryFileSystem(), new CryptorImpl(RANDOM_MOCK), Mockito.mock(CryptoFileSystemDelegate.class), "aPassphrase");
+		return new CryptoFileSystem(createInMemoryFileSystem(), createCryptor(), Mockito.mock(CryptoFileSystemDelegate.class), "aPassphrase");
 	}
 
 	private FileSystem createCryptoFileSystemNio() {
-		return new CryptoFileSystem(createNioFileSystem(), new CryptorImpl(RANDOM_MOCK), Mockito.mock(CryptoFileSystemDelegate.class), "aPassphrase");
+		return new CryptoFileSystem(createNioFileSystem(), createCryptor(), Mockito.mock(CryptoFileSystemDelegate.class), "aPassphrase");
+	}
+
+	private Cryptor createCryptor() {
+		Cryptor cryptor = new CryptorImpl(RANDOM_MOCK);
+		cryptor.randomizeMasterkey();
+		return cryptor;
 	}
 
 	private void add(String name, FileSystemFactory factory) {

+ 35 - 74
main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java

@@ -1,17 +1,20 @@
 package org.cryptomator.ui.controllers;
 
+import java.io.IOException;
 import java.net.URL;
 import java.util.ResourceBundle;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
 
+import org.cryptomator.crypto.engine.InvalidPassphraseException;
 import org.cryptomator.ui.controls.SecPasswordField;
 import org.cryptomator.ui.model.Vault;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javafx.application.Application;
+import javafx.application.Platform;
 import javafx.beans.value.ObservableValue;
 import javafx.event.ActionEvent;
 import javafx.fxml.FXML;
@@ -96,80 +99,38 @@ public class ChangePasswordController extends AbstractFXMLViewController {
 
 	@FXML
 	private void didClickChangePasswordButton(ActionEvent event) {
-		throw new UnsupportedOperationException("TODO");
-		/*
-		 * downloadsPageLink.setVisible(false);
-		 * final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE);
-		 * final Path masterKeyBackupPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_BACKUP_FILE);
-		 * 
-		 * // decrypt with old password:
-		 * final CharSequence oldPassword = oldPasswordField.getCharacters();
-		 * try (final InputStream masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ)) {
-		 * vault.getCryptor().decryptMasterKey(masterKeyInputStream, oldPassword);
-		 * Files.copy(masterKeyPath, masterKeyBackupPath, StandardCopyOption.REPLACE_EXISTING);
-		 * } catch (IOException ex) {
-		 * messageText.setText(resourceBundle.getString("changePassword.errorMessage.decryptionFailed"));
-		 * LOG.error("Decryption failed for technical reasons.", ex);
-		 * newPasswordField.swipe();
-		 * retypePasswordField.swipe();
-		 * return;
-		 * } catch (WrongPasswordException e) {
-		 * messageText.setText(resourceBundle.getString("changePassword.errorMessage.wrongPassword"));
-		 * newPasswordField.swipe();
-		 * retypePasswordField.swipe();
-		 * Platform.runLater(oldPasswordField::requestFocus);
-		 * return;
-		 * } catch (UnsupportedKeyLengthException ex) {
-		 * messageText.setText(resourceBundle.getString("changePassword.errorMessage.unsupportedKeyLengthInstallJCE"));
-		 * LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex);
-		 * newPasswordField.swipe();
-		 * retypePasswordField.swipe();
-		 * return;
-		 * } catch (UnsupportedVaultException e) {
-		 * downloadsPageLink.setVisible(true);
-		 * if (e.isVaultOlderThanSoftware()) {
-		 * messageText.setText(resourceBundle.getString("changePassword.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
-		 * } else if (e.isSoftwareOlderThanVault()) {
-		 * messageText.setText(resourceBundle.getString("changePassword.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
-		 * }
-		 * newPasswordField.swipe();
-		 * retypePasswordField.swipe();
-		 * return;
-		 * } finally {
-		 * oldPasswordField.swipe();
-		 * }
-		 * 
-		 * // when we reach this line, decryption was successful.
-		 * 
-		 * // encrypt with new password:
-		 * final CharSequence newPassword = newPasswordField.getCharacters();
-		 * try (final OutputStream masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.SYNC)) {
-		 * vault.getCryptor().encryptMasterKey(masterKeyOutputStream, newPassword);
-		 * messageText.setText(resourceBundle.getString("changePassword.infoMessage.success"));
-		 * Platform.runLater(this::didChangePassword);
-		 * // At this point the backup is still using the old password.
-		 * // It will be changed as soon as the user unlocks the vault the next time.
-		 * // This way he can still restore the old password, if he doesn't remember the new one.
-		 * } catch (IOException ex) {
-		 * LOG.error("Re-encryption failed for technical reasons. Restoring Backup.", ex);
-		 * this.restoreBackupQuietly();
-		 * } finally {
-		 * newPasswordField.swipe();
-		 * retypePasswordField.swipe();
-		 * }
-		 */
-	}
-
-	private void restoreBackupQuietly() {
-		/*
-		 * final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE);
-		 * final Path masterKeyBackupPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_BACKUP_FILE);
-		 * try {
-		 * Files.copy(masterKeyBackupPath, masterKeyPath, StandardCopyOption.REPLACE_EXISTING);
-		 * } catch (IOException ex) {
-		 * LOG.error("Restoring Backup failed.", ex);
-		 * }
-		 */
+		downloadsPageLink.setVisible(false);
+		try {
+			vault.changePassphrase(oldPasswordField.getCharacters(), newPasswordField.getCharacters());
+			messageText.setText(resourceBundle.getString("changePassword.infoMessage.success"));
+			Platform.runLater(this::didChangePassword);
+		} catch (InvalidPassphraseException e) {
+			messageText.setText(resourceBundle.getString("changePassword.errorMessage.wrongPassword"));
+			newPasswordField.swipe();
+			retypePasswordField.swipe();
+			Platform.runLater(oldPasswordField::requestFocus);
+			return;
+		} catch (IOException ex) {
+			messageText.setText(resourceBundle.getString("changePassword.errorMessage.decryptionFailed"));
+			LOG.error("Decryption failed for technical reasons.", ex);
+			newPasswordField.swipe();
+			retypePasswordField.swipe();
+			return;
+			// } catch (UnsupportedVaultException e) {
+			// downloadsPageLink.setVisible(true);
+			// if (e.isVaultOlderThanSoftware()) {
+			// messageText.setText(resourceBundle.getString("changePassword.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
+			// } else if (e.isSoftwareOlderThanVault()) {
+			// messageText.setText(resourceBundle.getString("changePassword.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
+			// }
+			// newPasswordField.swipe();
+			// retypePasswordField.swipe();
+			// return;
+		} finally {
+			oldPasswordField.swipe();
+			newPasswordField.swipe();
+			retypePasswordField.swipe();
+		}
 	}
 
 	private void didChangePassword() {

+ 12 - 6
main/ui/src/main/java/org/cryptomator/ui/model/Vault.java

@@ -16,6 +16,7 @@ import java.util.Set;
 import org.apache.commons.lang3.CharUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.cryptomator.common.Optionals;
+import org.cryptomator.crypto.engine.InvalidPassphraseException;
 import org.cryptomator.filesystem.FileSystem;
 import org.cryptomator.filesystem.crypto.CryptoFileSystemDelegate;
 import org.cryptomator.filesystem.crypto.CryptoFileSystemFactory;
@@ -90,7 +91,16 @@ public class Vault implements Serializable, CryptoFileSystemDelegate {
 			if (fs.children().count() > 0) {
 				throw new FileAlreadyExistsException(null, null, "Vault location not empty.");
 			}
-			cryptoFileSystemFactory.get(fs, passphrase, this);
+			cryptoFileSystemFactory.initializeNew(fs, passphrase);
+		} catch (UncheckedIOException e) {
+			throw new IOException(e);
+		}
+	}
+
+	public void changePassphrase(CharSequence oldPassphrase, CharSequence newPassphrase) throws IOException, InvalidPassphraseException {
+		try {
+			FileSystem fs = NioFileSystem.rootedAt(path);
+			cryptoFileSystemFactory.changePassphrase(fs, oldPassphrase, newPassphrase);
 		} catch (UncheckedIOException e) {
 			throw new IOException(e);
 		}
@@ -99,7 +109,7 @@ public class Vault implements Serializable, CryptoFileSystemDelegate {
 	public synchronized void activateFrontend(CharSequence passphrase) throws FrontendCreationFailedException {
 		try {
 			FileSystem fs = NioFileSystem.rootedAt(path);
-			FileSystem cryptoFs = cryptoFileSystemFactory.get(fs, passphrase, this);
+			FileSystem cryptoFs = cryptoFileSystemFactory.unlockExisting(fs, passphrase, this);
 			String contextPath = StringUtils.prependIfMissing(mountName, "/");
 			Frontend frontend = frontendFactory.get().create(cryptoFs, contextPath);
 			filesystemFrontend = closer.closeLater(frontend);
@@ -165,10 +175,6 @@ public class Vault implements Serializable, CryptoFileSystemDelegate {
 		return StringUtils.removeEnd(path.getFileName().toString(), VAULT_FILE_EXTENSION);
 	}
 
-	// public Cryptor getCryptor() {
-	// return cryptor;
-	// }
-
 	public ObjectProperty<Boolean> unlockedProperty() {
 		return unlocked;
 	}