diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java
index 311f5746e..a1180dd9f 100644
--- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java
+++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java
@@ -7,6 +7,7 @@ import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
+import org.jetbrains.annotations.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -16,6 +17,7 @@ import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.Collection;
+import java.util.function.Predicate;
import static org.cryptomator.common.Constants.MASTERKEY_BACKUP_SUFFIX;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
@@ -102,12 +104,29 @@ public class RecoveryKeyFactory {
* @return true if this seems to be a legitimate recovery key
*/
public boolean validateRecoveryKey(String recoveryKey) {
+ return validateRecoveryKey(recoveryKey, null);
+ }
+
+ /**
+ * Checks whether a String is a syntactically correct recovery key with a valid checksum and passes the extended validation.
+ *
+ * @param recoveryKey A word sequence which might be a recovery key
+ * @param extendedValidation Additional verification of the decoded key (optional)
+ * @return true if this seems to be a legitimate recovery key and passes the extended validation
+ */
+ public boolean validateRecoveryKey(String recoveryKey, @Nullable Predicate extendedValidation) {
+ byte[] key = new byte[0];
try {
- byte[] key = decodeRecoveryKey(recoveryKey);
- Arrays.fill(key, (byte) 0x00);
- return true;
+ key = decodeRecoveryKey(recoveryKey);
+ if (extendedValidation != null) {
+ return extendedValidation.test(key);
+ } else {
+ return true;
+ }
} catch (IllegalArgumentException e) {
return false;
+ } finally {
+ Arrays.fill(key, (byte) 0x00);
}
}
diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java
index b30167e73..0522e3a8d 100644
--- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java
+++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java
@@ -4,7 +4,9 @@ import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
+import org.cryptomator.common.Nullable;
import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
@@ -22,12 +24,25 @@ import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage;
+import java.io.IOException;
import java.util.Map;
import java.util.ResourceBundle;
@Module
abstract class RecoveryKeyModule {
+ @Provides
+ @Nullable
+ @RecoveryKeyWindow
+ @RecoveryKeyScoped
+ static VaultConfig.UnverifiedVaultConfig vaultConfig(@RecoveryKeyWindow Vault vault) {
+ try {
+ return vault.getVaultConfigCache().get();
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
@Provides
@RecoveryKeyWindow
@RecoveryKeyScoped
diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java
index 7e40a833b..e05a73169 100644
--- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java
+++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java
@@ -3,10 +3,16 @@ package org.cryptomator.ui.recoverykey;
import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
import dagger.Lazy;
+import org.cryptomator.common.Nullable;
import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.cryptofs.VaultConfig;
+import org.cryptomator.cryptofs.VaultConfigLoadException;
+import org.cryptomator.cryptofs.VaultKeyInvalidException;
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 javafx.beans.binding.Bindings;
@@ -24,10 +30,12 @@ import java.util.Optional;
@RecoveryKeyScoped
public class RecoveryKeyRecoverController implements FxController {
- private final static CharMatcher ALLOWED_CHARS = CharMatcher.inRange('a', 'z').or(CharMatcher.is(' '));
+ private static final Logger LOG = LoggerFactory.getLogger(RecoveryKeyCreationController.class);
+ private static final CharMatcher ALLOWED_CHARS = CharMatcher.inRange('a', 'z').or(CharMatcher.is(' '));
private final Stage window;
private final Vault vault;
+ private final VaultConfig.UnverifiedVaultConfig unverifiedVaultConfig;
private final StringProperty recoveryKey;
private final RecoveryKeyFactory recoveryKeyFactory;
private final BooleanBinding validRecoveryKey;
@@ -37,9 +45,10 @@ public class RecoveryKeyRecoverController implements FxController {
public TextArea textarea;
@Inject
- public RecoveryKeyRecoverController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, @RecoveryKeyWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory, @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) Lazy resetPasswordScene) {
+ public RecoveryKeyRecoverController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, @RecoveryKeyWindow @Nullable VaultConfig.UnverifiedVaultConfig unverifiedVaultConfig, @RecoveryKeyWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory, @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) Lazy resetPasswordScene) {
this.window = window;
this.vault = vault;
+ this.unverifiedVaultConfig = unverifiedVaultConfig;
this.recoveryKey = recoveryKey;
this.recoveryKeyFactory = recoveryKeyFactory;
this.validRecoveryKey = Bindings.createBooleanBinding(this::isValidRecoveryKey, recoveryKey);
@@ -96,6 +105,20 @@ public class RecoveryKeyRecoverController implements FxController {
window.setScene(resetPasswordScene.get());
}
+ private boolean checkKeyAgainstVaultConfig(byte[] key) {
+ try {
+ var config = unverifiedVaultConfig.verify(key, unverifiedVaultConfig.allegedVaultVersion());
+ LOG.info("Provided recovery key matches vault config signature for vault {}", config.getId());
+ return true;
+ } catch (VaultKeyInvalidException e) {
+ LOG.debug("Provided recovery key does not match vault config signature.");
+ return false;
+ } catch (VaultConfigLoadException e) {
+ LOG.error("Failed to parse vault config", e);
+ return false;
+ }
+ }
+
/* Getter/Setter */
public Vault getVault() {
@@ -107,7 +130,11 @@ public class RecoveryKeyRecoverController implements FxController {
}
public boolean isValidRecoveryKey() {
- return recoveryKeyFactory.validateRecoveryKey(recoveryKey.get());
+ if (unverifiedVaultConfig != null) {
+ return recoveryKeyFactory.validateRecoveryKey(recoveryKey.get(), this::checkKeyAgainstVaultConfig);
+ } else {
+ return recoveryKeyFactory.validateRecoveryKey(recoveryKey.get());
+ }
}
public TextFormatter getRecoveryKeyTextFormatter() {
diff --git a/src/test/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactoryTest.java b/src/test/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactoryTest.java
index c9061451e..82928f11c 100644
--- a/src/test/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactoryTest.java
+++ b/src/test/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactoryTest.java
@@ -7,10 +7,13 @@ import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito;
import java.io.IOException;
import java.nio.file.Path;
+import java.util.function.Predicate;
public class RecoveryKeyFactoryTest {
@@ -75,4 +78,19 @@ public class RecoveryKeyFactoryTest {
Assertions.assertTrue(result);
}
+ @ParameterizedTest(name = "passing validation = {0}")
+ @DisplayName("validateRecoveryKey() with extended validation")
+ @ValueSource(booleans = {true, false})
+ public void testValidateValidateRecoveryKeyWithValidKey(boolean extendedValidationResult) {
+ Predicate validator = Mockito.mock(Predicate.class);
+ Mockito.doReturn(extendedValidationResult).when(validator).test(Mockito.any());
+ boolean result = inTest.validateRecoveryKey("""
+ pathway lift abuse plenty export texture gentleman landscape beyond ceiling around leaf cafe charity \
+ border breakdown victory surely computer cat linger restrict infer crowd live computer true written amazed \
+ investor boot depth left theory snow whereby terminal weekly reject happiness circuit partial cup ad \
+ """, validator);
+ Mockito.verify(validator).test(Mockito.any());
+ Assertions.assertEquals(extendedValidationResult, result);
+ }
+
}
\ No newline at end of file