From c0a9a95e4fcf6870c1492ffba630040ee93a07b2 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 8 Dec 2020 14:39:46 +0100 Subject: [PATCH] Adjusted to CryptoFS 2.0.0 --- .../org/cryptomator/common/CommonsModule.java | 12 +++++ .../org/cryptomator/common/Constants.java | 2 + .../org/cryptomator/common/vaults/Vault.java | 21 ++++---- .../common/vaults/VaultListManager.java | 7 +-- main/pom.xml | 2 +- .../CreateNewVaultPasswordController.java | 48 ++++++++++++++----- .../ChangePasswordController.java | 31 ++++++++++-- .../ui/mainwindow/MainWindowController.java | 7 +-- .../ui/migration/MigrationRunController.java | 5 +- .../RecoveryKeyCreationController.java | 3 +- .../ui/recoverykey/RecoveryKeyFactory.java | 37 ++++++++++---- .../cryptomator/ui/unlock/UnlockWorkflow.java | 5 +- .../recoverykey/RecoveryKeyFactoryTest.java | 23 +++++++-- .../org.mockito.plugins.MockMaker | 1 + 14 files changed, 156 insertions(+), 48 deletions(-) create mode 100644 main/ui/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java b/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java index ed278e1ab..0ea3428ed 100644 --- a/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java +++ b/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java @@ -25,6 +25,8 @@ import javafx.beans.binding.Binding; import javafx.beans.binding.Bindings; import javafx.collections.ObservableList; import java.net.InetSocketAddress; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.util.Comparator; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -53,6 +55,16 @@ public abstract class CommonsModule { + "r0DzRyj4ixPIt38CQB8="; } + @Provides + @Singleton + static SecureRandom provideCSPRNG() { + try { + return SecureRandom.getInstanceStrong(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("A strong algorithm must exist in every Java platform.", e); + } + } + @Provides @Singleton @Named("SemVer") diff --git a/main/commons/src/main/java/org/cryptomator/common/Constants.java b/main/commons/src/main/java/org/cryptomator/common/Constants.java index 432cd08e5..cbde9642a 100644 --- a/main/commons/src/main/java/org/cryptomator/common/Constants.java +++ b/main/commons/src/main/java/org/cryptomator/common/Constants.java @@ -3,5 +3,7 @@ package org.cryptomator.common; public interface Constants { String MASTERKEY_FILENAME = "masterkey.cryptomator"; + String VAULTCONFIG_FILENAME = "vault.cryptomator"; + byte[] PEPPER = new byte[0]; } diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java b/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java index 730f0fb6d..c27920b20 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java @@ -21,6 +21,7 @@ import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker; import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.InvalidPassphraseException; +import org.cryptomator.cryptolib.common.MasterkeyFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,6 +46,7 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; +import static org.cryptomator.common.Constants.PEPPER; @PerVault public class Vault { @@ -111,14 +113,17 @@ public class Vault { LOG.info("Storing file name length limit of {}", limit); } assert vaultSettings.filenameLengthLimit().get() > 0; - CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() // - .withPassphrase(passphrase) // - .withFlags(flags) // - .withMasterkeyFilename(MASTERKEY_FILENAME) // - .withMaxPathLength(vaultSettings.filenameLengthLimit().get() + Constants.MAX_ADDITIONAL_PATH_LENGTH) // - .withMaxNameLength(vaultSettings.filenameLengthLimit().get()) // - .build(); - return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps); + + Path masterkeyPath = getPath().resolve(MASTERKEY_FILENAME); + try (var keyLoader = MasterkeyFile.withContentFromFile(masterkeyPath).unlock(passphrase, PEPPER, Optional.empty())) { + CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() // + .withKeyLoader(keyLoader) // + .withFlags(flags) // + .withMaxPathLength(vaultSettings.filenameLengthLimit().get() + Constants.MAX_ADDITIONAL_PATH_LENGTH) // + .withMaxNameLength(vaultSettings.filenameLengthLimit().get()) // + .build(); + return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps); + } } public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, VolumeException, InvalidMountPointException { diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java index 984008ba9..cbf196d8a 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java @@ -28,6 +28,7 @@ import java.util.ResourceBundle; import java.util.stream.Collectors; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; +import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME; @Singleton public class VaultListManager { @@ -54,7 +55,7 @@ public class VaultListManager { public Vault add(Path pathToVault) throws NoSuchFileException { Path normalizedPathToVault = pathToVault.normalize().toAbsolutePath(); - if (!CryptoFileSystemProvider.containsVault(normalizedPathToVault, MASTERKEY_FILENAME)) { + if (!CryptoFileSystemProvider.containsVault(normalizedPathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME)) { throw new NoSuchFileException(normalizedPathToVault.toString(), null, "Not a vault directory"); } Optional alreadyExistingVault = get(normalizedPathToVault); @@ -124,9 +125,9 @@ public class VaultListManager { } private static VaultState determineVaultState(Path pathToVault) throws IOException { - if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) { + if (!CryptoFileSystemProvider.containsVault(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME)) { return VaultState.MISSING; - } else if (Migrators.get().needsMigration(pathToVault, MASTERKEY_FILENAME)) { + } else if (Migrators.get().needsMigration(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME)) { return VaultState.NEEDS_MIGRATION; } else { return VaultState.LOCKED; diff --git a/main/pom.xml b/main/pom.xml index 77690a641..20587a864 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -24,7 +24,7 @@ UTF-8 - 1.9.13 + 2.0.0-beta1 0.1.6 0.1.0-beta1 0.1.0-beta3 diff --git a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java index c4e226359..25cb45395 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java @@ -5,6 +5,13 @@ import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.cryptofs.CryptoFileSystemProperties; import org.cryptomator.cryptofs.CryptoFileSystemProvider; +import org.cryptomator.cryptofs.VaultCipherCombo; +import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.InvalidPassphraseException; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; +import org.cryptomator.cryptolib.common.MasterkeyFile; +import org.cryptomator.cryptolib.common.MasterkeyFileLoader; import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxmlFile; @@ -29,6 +36,7 @@ import javafx.scene.control.ContentDisplay; import javafx.scene.control.Toggle; import javafx.scene.control.ToggleGroup; import javafx.stage.Stage; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.channels.WritableByteChannel; @@ -37,12 +45,15 @@ import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.security.SecureRandom; import java.util.Collections; +import java.util.Optional; import java.util.ResourceBundle; import java.util.concurrent.ExecutorService; import static java.nio.charset.StandardCharsets.US_ASCII; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; +import static org.cryptomator.common.Constants.PEPPER; @AddVaultWizardScoped public class CreateNewVaultPasswordController implements FxController { @@ -64,6 +75,7 @@ public class CreateNewVaultPasswordController implements FxController { private final ResourceBundle resourceBundle; private final ObjectProperty password; private final ReadmeGenerator readmeGenerator; + private final SecureRandom csprng; private final BooleanProperty processing; private final BooleanProperty readyToCreateVault; private final ObjectBinding createVaultButtonState; @@ -73,7 +85,7 @@ public class CreateNewVaultPasswordController implements FxController { public Toggle skipRecoveryKey; @Inject - CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy successScene, ErrorComponent.Builder errorComponent, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty vaultPath, @AddVaultWizardWindow ObjectProperty vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, @Named("newPassword") ObjectProperty password, ReadmeGenerator readmeGenerator) { + CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy successScene, ErrorComponent.Builder errorComponent, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty vaultPath, @AddVaultWizardWindow ObjectProperty vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, @Named("newPassword") ObjectProperty password, ReadmeGenerator readmeGenerator, SecureRandom csprng) { this.window = window; this.chooseLocationScene = chooseLocationScene; this.recoveryKeyScene = recoveryKeyScene; @@ -89,6 +101,7 @@ public class CreateNewVaultPasswordController implements FxController { this.resourceBundle = resourceBundle; this.password = password; this.readmeGenerator = readmeGenerator; + this.csprng = csprng; this.processing = new SimpleBooleanProperty(); this.readyToCreateVault = new SimpleBooleanProperty(); this.createVaultButtonState = Bindings.createObjectBinding(this::getCreateVaultButtonState, processing); @@ -161,23 +174,34 @@ public class CreateNewVaultPasswordController implements FxController { } private void initializeVault(Path path, CharSequence passphrase) throws IOException { - CryptoFileSystemProvider.initialize(path, MASTERKEY_FILENAME, passphrase); - CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() // - .withPassphrase(passphrase) // - .withFlags(Collections.emptySet()) // - .withMasterkeyFilename(MASTERKEY_FILENAME) // - .build(); - - String vaultReadmeFileName = resourceBundle.getString("addvault.new.readme.accessLocation.fileName"); - try (FileSystem fs = CryptoFileSystemProvider.newFileSystem(path, fsProps); // - WritableByteChannel ch = Files.newByteChannel(fs.getPath("/", vaultReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { - ch.write(US_ASCII.encode(readmeGenerator.createVaultAccessLocationReadmeRtf())); + // 1. write masterkey: + Path masterkeyFilePath = path.resolve(MASTERKEY_FILENAME); + try (Masterkey masterkey = Masterkey.createNew(csprng)) { + byte[] serialized = MasterkeyFile.lock(masterkey, passphrase, PEPPER, 999, csprng); + Files.write(masterkeyFilePath, serialized, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); } + // 2. verify masterkey and initialize vault: + try (var loader = MasterkeyFile.withContentFromFile(masterkeyFilePath).unlock(passphrase, PEPPER, Optional.of(999))) { + CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(loader).build(); + CryptoFileSystemProvider.initialize(path, fsProps, MasterkeyFileLoader.KEY_ID); + + // 3. write vault-internal readme file: + String vaultReadmeFileName = resourceBundle.getString("addvault.new.readme.accessLocation.fileName"); + try (FileSystem fs = CryptoFileSystemProvider.newFileSystem(path, fsProps); // + WritableByteChannel ch = Files.newByteChannel(fs.getPath("/", vaultReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { + ch.write(US_ASCII.encode(readmeGenerator.createVaultAccessLocationReadmeRtf())); + } + } catch (CryptoException e) { + throw new IOException("Failed initialize vault.", e); + } + + // 4. write vault-external readme file: String storagePathReadmeFileName = resourceBundle.getString("addvault.new.readme.storageLocation.fileName"); try (WritableByteChannel ch = Files.newByteChannel(path.resolve(storagePathReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { ch.write(US_ASCII.encode(readmeGenerator.createVaultStorageLocationReadmeRtf())); } + LOG.info("Created vault at {}", path); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java index 52cfe2b81..436103436 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java @@ -3,7 +3,10 @@ package org.cryptomator.ui.changepassword; import org.cryptomator.common.keychain.KeychainManager; import org.cryptomator.common.vaults.Vault; import org.cryptomator.cryptofs.CryptoFileSystemProvider; +import org.cryptomator.cryptofs.common.MasterkeyBackupHelper; +import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.InvalidPassphraseException; +import org.cryptomator.cryptolib.common.MasterkeyFile; import org.cryptomator.integrations.keychain.KeychainAccessException; import org.cryptomator.ui.common.Animations; import org.cryptomator.ui.common.ErrorComponent; @@ -23,6 +26,12 @@ import javafx.scene.control.CheckBox; import javafx.stage.Stage; import java.io.IOException; import java.nio.CharBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.security.SecureRandom; +import java.text.Normalizer; import java.util.Optional; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; @@ -31,24 +40,27 @@ import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; public class ChangePasswordController implements FxController { private static final Logger LOG = LoggerFactory.getLogger(ChangePasswordController.class); + private static final String MASTERKEY_BACKUP_SUFFIX = ".bkup"; private final Stage window; private final Vault vault; private final ObjectProperty newPassword; private final ErrorComponent.Builder errorComponent; private final KeychainManager keychain; + private final SecureRandom csprng; public NiceSecurePasswordField oldPasswordField; public CheckBox finalConfirmationCheckbox; public Button finishButton; @Inject - public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty newPassword, ErrorComponent.Builder errorComponent, KeychainManager keychain) { + public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty newPassword, ErrorComponent.Builder errorComponent, KeychainManager keychain, SecureRandom csprng) { this.window = window; this.vault = vault; this.newPassword = newPassword; this.errorComponent = errorComponent; this.keychain = keychain; + this.csprng = csprng; } @FXML @@ -67,17 +79,26 @@ public class ChangePasswordController implements FxController { @FXML public void finish() { try { - CryptoFileSystemProvider.changePassphrase(vault.getPath(), MASTERKEY_FILENAME, oldPasswordField.getCharacters(), newPassword.get()); + //String normalizedOldPassphrase = Normalizer.normalize(oldPasswordField.getCharacters(), Normalizer.Form.NFC); + //String normalizedNewPassphrase = Normalizer.normalize(newPassword.get(), Normalizer.Form.NFC); + CharSequence oldPassphrase = oldPasswordField.getCharacters(); // TODO verify: is this already NFC-normalized? + CharSequence newPassphrase = newPassword.get(); // TODO verify: is this already NFC-normalized? + Path masterkeyPath = vault.getPath().resolve(MASTERKEY_FILENAME); + byte[] oldMasterkeyBytes = Files.readAllBytes(masterkeyPath); + byte[] newMasterkeyBytes = MasterkeyFile.changePassphrase(oldMasterkeyBytes, oldPassphrase, newPassphrase, new byte[0], csprng); + Path backupKeyPath = vault.getPath().resolve(MASTERKEY_FILENAME + MasterkeyBackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + MASTERKEY_BACKUP_SUFFIX); + Files.move(masterkeyPath, backupKeyPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + Files.write(masterkeyPath, newMasterkeyBytes, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); LOG.info("Successfully changed password for {}", vault.getDisplayName()); window.close(); updatePasswordInSystemkeychain(); - } catch (IOException e) { - LOG.error("IO error occured during password change. Unable to perform operation.", e); - errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene(); } catch (InvalidPassphraseException e) { Animations.createShakeWindowAnimation(window).play(); oldPasswordField.selectAll(); oldPasswordField.requestFocus(); + } catch (IOException | CryptoException e) { + LOG.error("Password change failed. Unable to perform operation.", e); + errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene(); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java index 65c3a8fcf..5fd4cc62d 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java @@ -27,6 +27,7 @@ import java.util.Set; import java.util.stream.Collectors; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; +import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME; @MainWindowScoped public class MainWindowController implements FxController { @@ -91,9 +92,9 @@ public class MainWindowController implements FxController { } private boolean containsVault(Path path) { - if (path.getFileName().toString().equals(MASTERKEY_FILENAME)) { + if (path.getFileName().toString().equals(VAULTCONFIG_FILENAME)) { return true; - } else if (Files.isDirectory(path) && Files.exists(path.resolve(MASTERKEY_FILENAME))) { + } else if (Files.isDirectory(path) && Files.exists(path.resolve(VAULTCONFIG_FILENAME))) { return true; } else { return false; @@ -102,7 +103,7 @@ public class MainWindowController implements FxController { private void addVault(Path pathToVault) { try { - if (pathToVault.getFileName().toString().equals(MASTERKEY_FILENAME)) { + if (pathToVault.getFileName().toString().equals(VAULTCONFIG_FILENAME)) { vaultListManager.add(pathToVault.getParent()); } else { vaultListManager.add(pathToVault); diff --git a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java index 830e15819..ff18510d9 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java @@ -43,6 +43,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; +import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME; @MigrationScoped public class MigrationRunController implements FxController { @@ -110,8 +111,8 @@ public class MigrationRunController implements FxController { }, 0, MIGRATION_PROGRESS_UPDATE_MILLIS, TimeUnit.MILLISECONDS); Tasks.create(() -> { Migrators migrators = Migrators.get(); - migrators.migrate(vault.getPath(), MASTERKEY_FILENAME, password, this::migrationProgressChanged, this::migrationRequiresInput); - return migrators.needsMigration(vault.getPath(), MASTERKEY_FILENAME); + migrators.migrate(vault.getPath(), VAULTCONFIG_FILENAME, MASTERKEY_FILENAME, password, this::migrationProgressChanged, this::migrationRequiresInput); + return migrators.needsMigration(vault.getPath(), VAULTCONFIG_FILENAME, MASTERKEY_FILENAME); }).onSuccess(needsAnotherMigration -> { if (needsAnotherMigration) { LOG.info("Migration of '{}' succeeded, but another migration is required.", vault.getDisplayName()); diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java index 902f2cd50..104794604 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java @@ -2,6 +2,7 @@ package org.cryptomator.ui.recoverykey; import dagger.Lazy; import org.cryptomator.common.vaults.Vault; +import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.ui.common.Animations; import org.cryptomator.ui.common.ErrorComponent; @@ -80,7 +81,7 @@ public class RecoveryKeyCreationController implements FxController { } @Override - protected String call() throws IOException { + protected String call() throws IOException, CryptoException { return recoveryKeyFactory.createRecoveryKey(vault.getPath(), passwordField.getCharacters()); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java index 15d4e651e..a53113020 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java +++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java @@ -2,28 +2,39 @@ package org.cryptomator.ui.recoverykey; import com.google.common.base.Preconditions; import com.google.common.hash.Hashing; -import org.cryptomator.cryptofs.CryptoFileSystemProvider; +import org.cryptomator.cryptofs.common.MasterkeyBackupHelper; +import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.InvalidPassphraseException; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.common.MasterkeyFile; import javax.inject.Inject; import javax.inject.Singleton; 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.security.SecureRandom; import java.util.Arrays; import java.util.Collection; +import java.util.Optional; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; +import static org.cryptomator.common.Constants.PEPPER; @Singleton public class RecoveryKeyFactory { - private static final byte[] PEPPER = new byte[0]; + private static final String MASTERKEY_BACKUP_SUFFIX = ".bkup"; private final WordEncoder wordEncoder; + private final SecureRandom csprng; @Inject - public RecoveryKeyFactory(WordEncoder wordEncoder) { + public RecoveryKeyFactory(WordEncoder wordEncoder, SecureRandom csprng) { this.wordEncoder = wordEncoder; + this.csprng = csprng; } public Collection getDictionary() { @@ -36,11 +47,14 @@ public class RecoveryKeyFactory { * @return The recovery key of the vault at the given path * @throws IOException If the masterkey file could not be read * @throws InvalidPassphraseException If the provided password is wrong + * @throws CryptoException In case of other cryptographic errors * @apiNote This is a long-running operation and should be invoked in a background thread */ - public String createRecoveryKey(Path vaultPath, CharSequence password) throws IOException, InvalidPassphraseException { - byte[] rawKey = CryptoFileSystemProvider.exportRawKey(vaultPath, MASTERKEY_FILENAME, PEPPER, password); - try { + public String createRecoveryKey(Path vaultPath, CharSequence password) throws IOException, InvalidPassphraseException, CryptoException { + Path masterkeyPath = vaultPath.resolve(MASTERKEY_FILENAME); + byte[] rawKey = new byte[0]; + try (var masterkey = MasterkeyFile.withContentFromFile(masterkeyPath).unlock(password, PEPPER, Optional.empty()).loadKeyAndClose()) { + rawKey = masterkey.getEncoded(); return createRecoveryKey(rawKey); } finally { Arrays.fill(rawKey, (byte) 0x00); @@ -72,8 +86,15 @@ public class RecoveryKeyFactory { */ public void resetPasswordWithRecoveryKey(Path vaultPath, String recoveryKey, CharSequence newPassword) throws IOException, IllegalArgumentException { final byte[] rawKey = decodeRecoveryKey(recoveryKey); - try { - CryptoFileSystemProvider.restoreRawKey(vaultPath, MASTERKEY_FILENAME, rawKey, PEPPER, newPassword); + try (var masterkey = Masterkey.createFromRaw(rawKey)) { + byte[] restoredKey = MasterkeyFile.lock(masterkey, newPassword, PEPPER, 999, csprng); + Path masterkeyPath = vaultPath.resolve(MASTERKEY_FILENAME); + if (Files.exists(masterkeyPath)) { + byte[] oldMasterkeyBytes = Files.readAllBytes(masterkeyPath); + Path backupKeyPath = vaultPath.resolve(MASTERKEY_FILENAME + MasterkeyBackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + MASTERKEY_BACKUP_SUFFIX); + Files.move(masterkeyPath, backupKeyPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } + Files.write(masterkeyPath, restoredKey, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); } finally { Arrays.fill(rawKey, (byte) 0x00); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java index 8cb7e0752..475ba92a5 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java @@ -7,6 +7,7 @@ import org.cryptomator.common.vaults.MountPointRequirement; import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.VaultState; import org.cryptomator.common.vaults.Volume.VolumeException; +import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.integrations.keychain.KeychainAccessException; import org.cryptomator.ui.common.Animations; @@ -85,7 +86,7 @@ public class UnlockWorkflow extends Task { } @Override - protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException { + protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException, CryptoException { try { if (attemptUnlock()) { handleSuccess(); @@ -100,7 +101,7 @@ public class UnlockWorkflow extends Task { } } - private boolean attemptUnlock() throws InterruptedException, IOException, VolumeException, InvalidMountPointException { + private boolean attemptUnlock() throws InterruptedException, IOException, VolumeException, InvalidMountPointException, CryptoException { boolean proceed = password.get() != null || askForPassword(false) == PasswordEntry.PASSWORD_ENTERED; while (proceed) { try { diff --git a/main/ui/src/test/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactoryTest.java b/main/ui/src/test/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactoryTest.java index 6ca1fd891..a8c7d4844 100644 --- a/main/ui/src/test/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactoryTest.java +++ b/main/ui/src/test/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactoryTest.java @@ -2,23 +2,40 @@ package org.cryptomator.ui.recoverykey; import com.google.common.base.Splitter; import org.cryptomator.cryptofs.CryptoFileSystemProvider; +import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.common.MasterkeyFile; +import org.cryptomator.cryptolib.common.MasterkeyFileLoader; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import java.io.IOException; import java.nio.file.Path; +import java.security.SecureRandom; class RecoveryKeyFactoryTest { private WordEncoder wordEncoder = new WordEncoder(); - private RecoveryKeyFactory inTest = new RecoveryKeyFactory(wordEncoder); + private SecureRandom csprng = Mockito.mock(SecureRandom.class); + private RecoveryKeyFactory inTest = new RecoveryKeyFactory(wordEncoder, csprng); @Test @DisplayName("createRecoveryKey() creates 44 words") - public void testCreateRecoveryKey(@TempDir Path pathToVault) throws IOException { - CryptoFileSystemProvider.initialize(pathToVault, "masterkey.cryptomator", "asd"); + public void testCreateRecoveryKey() throws IOException, CryptoException { + Path pathToVault = Path.of("path/to/vault"); + MockedStatic masterkeyFileClass = Mockito.mockStatic(MasterkeyFile.class); + MasterkeyFile masterkeyFile = Mockito.mock(MasterkeyFile.class); + MasterkeyFileLoader keyLoader = Mockito.mock(MasterkeyFileLoader.class); + Masterkey masterkey = Mockito.mock(Masterkey.class); + masterkeyFileClass.when(() -> MasterkeyFile.withContentFromFile(Path.of("path/to/vault/masterkey.cryptomator"))).thenReturn(masterkeyFile); + Mockito.when(masterkeyFile.unlock(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(keyLoader); + Mockito.when(keyLoader.loadKeyAndClose()).thenReturn(masterkey); + Mockito.when(masterkey.getEncoded()).thenReturn(new byte[64]); + String recoveryKey = inTest.createRecoveryKey(pathToVault, "asd"); Assertions.assertNotNull(recoveryKey); Assertions.assertEquals(44, Splitter.on(' ').splitToList(recoveryKey).size()); // 66 bytes encoded as 44 words diff --git a/main/ui/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/main/ui/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000..ca6ee9cea --- /dev/null +++ b/main/ui/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file