From caa8c84d8ae6f796931a31edd2d17ccb5f13e5ee Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 20 Feb 2020 15:31:22 +0100 Subject: [PATCH] Can now use a recovery key to reset a vault's password --- .../org/cryptomator/ui/common/FxmlFile.java | 2 +- .../RecoveryKeyCreationController.java | 38 +++++--- .../ui/recoverykey/RecoveryKeyFactory.java | 48 ++++++++-- .../ui/recoverykey/RecoveryKeyModule.java | 33 ++++++- .../RecoveryKeyRecoverController.java | 16 +++- .../RecoveryKeyResetPasswordController.java | 94 +++++++++++++++++++ .../resources/fxml/recoverykey_recover.fxml | 1 - .../fxml/recoverykey_reset_password.fxml | 33 +++++++ 8 files changed, 237 insertions(+), 28 deletions(-) create mode 100644 main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java create mode 100644 main/ui/src/main/resources/fxml/recoverykey_reset_password.fxml diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java index 4c07de9fe..998bea98e 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -21,8 +21,8 @@ public enum FxmlFile { QUIT("/fxml/quit.fxml"), // RECOVERYKEY_CREATE("/fxml/recoverykey_create.fxml"), // RECOVERYKEY_RECOVER("/fxml/recoverykey_recover.fxml"), // + RECOVERYKEY_RESET_PASSWORD("/fxml/recoverykey_reset_password.fxml"), // RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), // - RECOVER_VAULT("/fxml/recovervault.fxml"),// TODO REMOVE_VAULT("/fxml/remove_vault.fxml"), // UNLOCK("/fxml/unlock.fxml"), UNLOCK_GENERIC_ERROR("/fxml/unlock_generic_error.fxml"), // 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 d39aee58e..6bb6a9f80 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 javafx.beans.property.StringProperty; +import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.scene.Scene; import javafx.stage.Stage; @@ -11,7 +12,6 @@ import org.cryptomator.ui.common.Animations; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; -import org.cryptomator.ui.common.Tasks; import org.cryptomator.ui.controls.NiceSecurePasswordField; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,19 +42,26 @@ public class RecoveryKeyCreationController implements FxController { this.recoveryKeyFactory = recoveryKeyFactory; this.recoveryKeyProperty = recoveryKey; } - + @FXML public void createRecoveryKey() { - Tasks.create(() -> { - return recoveryKeyFactory.createRecoveryKey(vault.getPath(), passwordField.getCharacters()); - }).onSuccess(result -> { - recoveryKeyProperty.set(result); + Task task = new RecoveryKeyCreationTask(); + task.setOnScheduled(event -> { + LOG.debug("Creating recovery key for {}.", vault.getDisplayablePath()); + }); + task.setOnSucceeded(event -> { + String recoveryKey = task.getValue(); + recoveryKeyProperty.set(recoveryKey); window.setScene(successScene.get()); - }).onError(IOException.class, e -> { - LOG.error("Creation of recovery key failed.", e); - }).onError(InvalidPassphraseException.class, e -> { - Animations.createShakeWindowAnimation(window).play(); - }).runOnce(executor); + }); + task.setOnFailed(event -> { + if (task.getException() instanceof InvalidPassphraseException) { + Animations.createShakeWindowAnimation(window).play(); + } else { + LOG.error("Creation of recovery key failed.", task.getException()); + } + }); + executor.submit(task); } @FXML @@ -62,6 +69,15 @@ public class RecoveryKeyCreationController implements FxController { window.close(); } + private class RecoveryKeyCreationTask extends Task { + + @Override + protected String call() throws IOException { + return recoveryKeyFactory.createRecoveryKey(vault.getPath(), passwordField.getCharacters()); + } + + } + /* Getter/Setter */ public Vault getVault() { 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 30e53c53c..25374a7f2 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 @@ -16,6 +16,7 @@ import java.util.Collection; public class RecoveryKeyFactory { private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes + private static final byte[] PEPPER = new byte[0]; private final WordEncoder wordEncoder; @@ -37,7 +38,7 @@ public class RecoveryKeyFactory { * @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, new byte[0], password); + byte[] rawKey = CryptoFileSystemProvider.exportRawKey(vaultPath, MASTERKEY_FILENAME, PEPPER, password); try { return createRecoveryKey(rawKey); } finally { @@ -58,26 +59,53 @@ public class RecoveryKeyFactory { } } + /** + * Creates a completely new masterkey using a recovery key. + * @param vaultPath Path to the storage location of a vault + * @param recoveryKey A recovery key for this vault + * @param newPassword The new password used to encrypt the keys + * @throws IOException If the masterkey file could not be written + * @throws IllegalArgumentException If the recoveryKey is invalid + * @apiNote This is a long-running operation and should be invoked in a background thread + */ + 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); + } finally { + Arrays.fill(rawKey, (byte) 0x00); + } + } + /** * Checks whether a String is a syntactically correct recovery key with a valid checksum * @param recoveryKey A word sequence which might be a recovery key * @return true if this seems to be a legitimate recovery key */ public boolean validateRecoveryKey(String recoveryKey) { - final byte[] paddedKey; try { - paddedKey = wordEncoder.decode(recoveryKey); + byte[] key = decodeRecoveryKey(recoveryKey); + Arrays.fill(key, (byte) 0x00); + return true; } catch (IllegalArgumentException e) { return false; } - if (paddedKey.length != 66) { - return false; + } + + private byte[] decodeRecoveryKey(String recoveryKey) throws IllegalArgumentException { + byte[] paddedKey = new byte[0]; + try { + paddedKey = wordEncoder.decode(recoveryKey); + Preconditions.checkArgument(paddedKey.length == 66, "Recovery key doesn't consist of 66 bytes."); + byte[] rawKey = Arrays.copyOf(paddedKey, 64); + byte[] expectedCrc16 = Arrays.copyOfRange(paddedKey, 64, 66); + byte[] actualCrc32 = Hashing.crc32().hashBytes(rawKey).asBytes(); + byte[] actualCrc16 = Arrays.copyOf(actualCrc32, 2); + Preconditions.checkArgument(Arrays.equals(expectedCrc16, actualCrc16), "Recovery key has invalid CRC."); + return rawKey; + } finally { + Arrays.fill(paddedKey, (byte) 0x00); } - byte[] rawKey = Arrays.copyOf(paddedKey, 64); - byte[] expectedCrc16 = Arrays.copyOfRange(paddedKey, 64, 66); - byte[] actualCrc32 = Hashing.crc32().hashBytes(rawKey).asBytes(); - byte[] actualCrc16 = Arrays.copyOf(actualCrc32, 2); - return Arrays.equals(expectedCrc16, actualCrc16); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java index 3eb1d3baf..ffa87b2ca 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java @@ -4,6 +4,8 @@ import dagger.Binds; import dagger.Module; import dagger.Provides; import dagger.multibindings.IntoMap; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.scene.Scene; @@ -17,6 +19,8 @@ import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxControllerKey; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; +import org.cryptomator.ui.common.NewPasswordController; +import org.cryptomator.ui.common.PasswordStrengthUtil; import javax.inject.Named; import javax.inject.Provider; @@ -53,7 +57,15 @@ abstract class RecoveryKeyModule { static StringProperty provideRecoveryKeyProperty() { return new SimpleStringProperty(); } - + + @Provides + @RecoveryKeyScoped + @Named("newPassword") + static ObjectProperty provideNewPasswordProperty() { + return new SimpleObjectProperty<>(""); + } + + // ------------------ @Provides @@ -77,6 +89,13 @@ abstract class RecoveryKeyModule { return fxmlLoaders.createScene("/fxml/recoverykey_recover.fxml"); } + @Provides + @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) + @RecoveryKeyScoped + static Scene provideRecoveryKeyResetPasswordScene(@RecoveryKeyWindow FXMLLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene("/fxml/recoverykey_reset_password.fxml"); + } + // ------------------ @Binds @@ -100,5 +119,17 @@ abstract class RecoveryKeyModule { @IntoMap @FxControllerKey(RecoveryKeySuccessController.class) abstract FxController bindRecoveryKeySuccessController(RecoveryKeySuccessController controller); + + @Binds + @IntoMap + @FxControllerKey(RecoveryKeyResetPasswordController.class) + abstract FxController bindRecoveryKeyResetPasswordController(RecoveryKeyResetPasswordController controller); + + @Provides + @IntoMap + @FxControllerKey(NewPasswordController.class) + static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty password) { + return new NewPasswordController(resourceBundle, strengthRater, password); + } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java index 7a212d443..8b9e1cdfb 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java @@ -2,10 +2,12 @@ package org.cryptomator.ui.recoverykey; import com.google.common.base.CharMatcher; import com.google.common.base.Strings; +import dagger.Lazy; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.beans.property.StringProperty; import javafx.fxml.FXML; +import javafx.scene.Scene; import javafx.scene.control.TextArea; import javafx.scene.control.TextFormatter; import javafx.scene.input.KeyCode; @@ -13,6 +15,8 @@ import javafx.scene.input.KeyEvent; import javafx.stage.Stage; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; import javax.inject.Inject; import java.util.Optional; @@ -27,17 +31,19 @@ public class RecoveryKeyRecoverController implements FxController { private final StringProperty recoveryKey; private final RecoveryKeyFactory recoveryKeyFactory; private final BooleanBinding validRecoveryKey; + private final Lazy resetPasswordScene; private final AutoCompleter autoCompleter; public TextArea textarea; @Inject - public RecoveryKeyRecoverController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, @RecoveryKeyWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory) { + public RecoveryKeyRecoverController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, @RecoveryKeyWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory, @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) Lazy resetPasswordScene) { this.window = window; this.vault = vault; this.recoveryKey = recoveryKey; this.recoveryKeyFactory = recoveryKeyFactory; this.validRecoveryKey = Bindings.createBooleanBinding(this::isValidRecoveryKey, recoveryKey); + this.resetPasswordScene = resetPasswordScene; this.autoCompleter = new AutoCompleter(recoveryKeyFactory.getDictionary()); } @@ -72,9 +78,11 @@ public class RecoveryKeyRecoverController implements FxController { @FXML public void onKeyPressed(KeyEvent keyEvent) { - if (keyEvent.getCode() == KeyCode.TAB) { + if (keyEvent.getCode() == KeyCode.TAB && textarea.getAnchor() > textarea.getCaretPosition()) { // apply autocompletion: - textarea.positionCaret(textarea.getAnchor()); + int pos = textarea.getAnchor(); + textarea.insertText(pos, " "); + textarea.positionCaret(pos + 1); } } @@ -85,7 +93,7 @@ public class RecoveryKeyRecoverController implements FxController { @FXML public void recover() { - recoveryKeyFactory.validateRecoveryKey(textarea.getText()); + window.setScene(resetPasswordScene.get()); } /* Getter/Setter */ diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java new file mode 100644 index 000000000..7bda40364 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java @@ -0,0 +1,94 @@ +package org.cryptomator.ui.recoverykey; + +import dagger.Lazy; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.StringProperty; +import javafx.concurrent.Task; +import javafx.fxml.FXML; +import javafx.scene.Scene; +import javafx.stage.Stage; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.cryptolib.api.InvalidPassphraseException; +import org.cryptomator.ui.common.Animations; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Named; +import java.io.IOException; +import java.util.concurrent.ExecutorService; + +@RecoveryKeyScoped +public class RecoveryKeyResetPasswordController implements FxController { + + private static final Logger LOG = LoggerFactory.getLogger(RecoveryKeyResetPasswordController.class); + + private final Stage window; + private final Vault vault; + private final RecoveryKeyFactory recoveryKeyFactory; + private final ExecutorService executor; + private final StringProperty recoveryKey; + private final ObjectProperty newPassword; + private final Lazy recoverScene; + private final BooleanBinding invalidNewPassword; + + @Inject + public RecoveryKeyResetPasswordController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, @Named("newPassword")ObjectProperty newPassword, @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy recoverScene) { + this.window = window; + this.vault = vault; + this.recoveryKeyFactory = recoveryKeyFactory; + this.executor = executor; + this.recoveryKey = recoveryKey; + this.newPassword = newPassword; + this.recoverScene = recoverScene; + this.invalidNewPassword = Bindings.createBooleanBinding(this::isInvalidNewPassword, newPassword); + } + + @FXML + public void back() { + window.setScene(recoverScene.get()); + } + + @FXML + public void done() { + Task task = new ResetPasswordTask(); + task.setOnScheduled(event -> { + LOG.debug("Using recovery key to reset password for {}.", vault.getDisplayablePath()); + }); + task.setOnSucceeded(event -> { + LOG.info("Used recovery key to reset password for {}.", vault.getDisplayablePath()); + // TODO show success screen + window.close(); + }); + task.setOnFailed(event -> { + // TODO show generic error screen + LOG.error("Creation of recovery key failed.", task.getException()); + }); + executor.submit(task); + } + + private class ResetPasswordTask extends Task { + + @Override + protected Void call() throws IOException, IllegalArgumentException { + recoveryKeyFactory.resetPasswordWithRecoveryKey(vault.getPath(), recoveryKey.get(), newPassword.get()); + return null; + } + + } + + /* Getter/Setter */ + + public BooleanBinding invalidNewPasswordProperty() { + return invalidNewPassword; + } + + public boolean isInvalidNewPassword() { + return newPassword.get() == null || newPassword.get().length() == 0; + } +} diff --git a/main/ui/src/main/resources/fxml/recoverykey_recover.fxml b/main/ui/src/main/resources/fxml/recoverykey_recover.fxml index 74f9f617c..d77ac29e8 100644 --- a/main/ui/src/main/resources/fxml/recoverykey_recover.fxml +++ b/main/ui/src/main/resources/fxml/recoverykey_recover.fxml @@ -9,7 +9,6 @@ - + + + + + + + + + + + + + + + + + + +