Parcourir la source

Can now use a recovery key to reset a vault's password

Sebastian Stenzel il y a 5 ans
Parent
commit
caa8c84d8a

+ 1 - 1
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"), //

+ 27 - 11
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<String> 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<String> {
+
+		@Override
+		protected String call() throws IOException {
+			return recoveryKeyFactory.createRecoveryKey(vault.getPath(), passwordField.getCharacters());
+		}
+
+	}
+
 	/* Getter/Setter */
 
 	public Vault getVault() {

+ 38 - 10
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 <code>true</code> 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);
 	}
 
 }

+ 32 - 1
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<CharSequence> 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<CharSequence> password) {
+		return new NewPasswordController(resourceBundle, strengthRater, password);
+	}
 	
 }

+ 12 - 4
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<Scene> 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<Scene> 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 */

+ 94 - 0
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<CharSequence> newPassword;
+	private final Lazy<Scene> recoverScene;
+	private final BooleanBinding invalidNewPassword;
+
+	@Inject
+	public RecoveryKeyResetPasswordController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, @Named("newPassword")ObjectProperty<CharSequence> newPassword, @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy<Scene> 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<Void> 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<Void> {
+
+		@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;
+	}
+}

+ 0 - 1
main/ui/src/main/resources/fxml/recoverykey_recover.fxml

@@ -9,7 +9,6 @@
 <?import javafx.geometry.Insets?>
 <?import javafx.scene.control.Label?>
 <?import org.cryptomator.ui.controls.FontAwesome5IconView?>
-<?import javafx.scene.control.TextFormatter?>
 <VBox xmlns="http://javafx.com/javafx"
 	  xmlns:fx="http://javafx.com/fxml"
 	  fx:controller="org.cryptomator.ui.recoverykey.RecoveryKeyRecoverController"

+ 33 - 0
main/ui/src/main/resources/fxml/recoverykey_reset_password.fxml

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import javafx.geometry.Insets?>
+<?import javafx.scene.control.Button?>
+<?import javafx.scene.control.ButtonBar?>
+<?import javafx.scene.layout.Region?>
+<?import javafx.scene.layout.VBox?>
+<VBox xmlns="http://javafx.com/javafx"
+	  xmlns:fx="http://javafx.com/fxml"
+	  fx:controller="org.cryptomator.ui.recoverykey.RecoveryKeyResetPasswordController"
+	  minWidth="400"
+	  maxWidth="400"
+	  minHeight="145"
+	  spacing="12"
+	  alignment="TOP_CENTER">
+	<padding>
+		<Insets topRightBottomLeft="12"/>
+	</padding>
+	<children>
+		<fx:include source="/fxml/new_password.fxml"/>
+
+		<Region VBox.vgrow="ALWAYS"/>
+
+		<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
+			<ButtonBar buttonMinWidth="120" buttonOrder="B+I">
+				<buttons>
+					<Button text="%generic.button.back" ButtonBar.buttonData="BACK_PREVIOUS" cancelButton="true" onAction="#back" />
+					<Button text="%generic.button.next" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#done" disable="${controller.invalidNewPassword}"/>
+				</buttons>
+			</ButtonBar>
+		</VBox>
+	</children>
+</VBox>