From a972480e7279c660a7a4f05899c6180829380fab Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 27 Jan 2016 18:21:47 +0100 Subject: [PATCH] supporting change password again - now via CryptoFileSystemFactory --- .../cryptomator/crypto/engine/Cryptor.java | 5 +- .../crypto/engine/impl/CryptorImpl.java | 13 ++- .../filesystem/crypto/CryptoFileSystem.java | 40 +------ .../crypto/CryptoFileSystemFactory.java | 33 ++++-- .../filesystem/crypto/Masterkeys.java | 96 +++++++++++++++ .../cryptomator/crypto/engine/NoCryptor.java | 8 +- .../crypto/engine/impl/CryptorImplTest.java | 15 ++- ...ptoFileSystemComponentIntegrationTest.java | 28 ++++- .../crypto/CryptoFileSystemTest.java | 54 --------- .../invariants/FileSystemFactories.java | 11 +- .../controllers/ChangePasswordController.java | 109 ++++++------------ .../java/org/cryptomator/ui/model/Vault.java | 18 ++- 12 files changed, 238 insertions(+), 192 deletions(-) create mode 100644 main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/Masterkeys.java diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/Cryptor.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/Cryptor.java index da06cd9c7..b523d6fb5 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/Cryptor.java +++ b/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(); + } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java index 64062b8f2..c081628da 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java +++ b/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) { diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystem.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystem.java index 52c2a35d6..92fb95af9 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystem.java +++ b/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; } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystemFactory.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystemFactory.java index 56cd965c4..91cce0210 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystemFactory.java +++ b/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 cryptorProvider; + private final Masterkeys masterkeys; private final ShorteningFileSystemFactory shorteningFileSystemFactory; private final BlockAlignedFileSystemFactory blockAlignedFileSystemFactory; @Inject - public CryptoFileSystemFactory(Provider 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); + } + } } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/Masterkeys.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/Masterkeys.java new file mode 100644 index 000000000..9a60a36f2 --- /dev/null +++ b/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 cryptorProvider; + + @Inject + public Masterkeys(Provider 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)); + } + } + +} diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/NoCryptor.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/NoCryptor.java index 346ca8ce6..8bff8fdb9 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/NoCryptor.java +++ b/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 + } + } diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/CryptorImplTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/CryptorImplTest.java index d7370fcdb..747df500c 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/CryptorImplTest.java +++ b/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 diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/CryptoFileSystemComponentIntegrationTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/CryptoFileSystemComponentIntegrationTest.java index 5911797d1..665a6a981 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/CryptoFileSystemComponentIntegrationTest.java +++ b/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"; diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/CryptoFileSystemTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/CryptoFileSystemTest.java index 52536e637..e9fbb0993 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/CryptoFileSystemTest.java +++ b/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: diff --git a/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileSystemFactories.java b/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileSystemFactories.java index 9c28edcad..00372013b 100644 --- a/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileSystemFactories.java +++ b/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 { } 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) { diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java index 1673a49aa..f07cae3ae 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java +++ b/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() { diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java index 2ddf8867a..8b7984fe3 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java +++ b/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 unlockedProperty() { return unlocked; }