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

This commit is contained in:
Sebastian Stenzel
2020-02-20 15:31:22 +01:00
parent e14fc56b37
commit caa8c84d8a
8 changed files with 237 additions and 28 deletions

View File

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

View File

@@ -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() {

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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 */

View File

@@ -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;
}
}

View File

@@ -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"

View File

@@ -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>