Browse Source

Externalized logic of recovery key creation to reusable utility class

Sebastian Stenzel 5 năm trước cách đây
mục cha
commit
08d9beb6b8

+ 0 - 2
main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java

@@ -26,8 +26,6 @@ import org.slf4j.LoggerFactory;
 import javax.inject.Inject;
 import java.awt.desktop.QuitResponse;
 import java.util.Optional;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionStage;
 
 @FxApplicationScoped
 public class FxApplication extends Application {

+ 4 - 15
main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java

@@ -24,23 +24,22 @@ import java.util.concurrent.ExecutorService;
 public class RecoveryKeyCreationController implements FxController {
 
 	private static final Logger LOG = LoggerFactory.getLogger(RecoveryKeyCreationController.class);
-	private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes
 
 	private final Stage window;
 	private final Vault vault;
 	private final ExecutorService executor;
 	private final CharSequence prefilledPassword;
-	private final WordEncoder wordEncoder;
+	private final RecoveryKeyFactory recoveryKeyFactory;
 	private final StringProperty recoveryKey;
 	public NiceSecurePasswordField passwordField;
 
 	@Inject
-	public RecoveryKeyCreationController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, ExecutorService executor, @Nullable CharSequence prefilledPassword) {
+	public RecoveryKeyCreationController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @Nullable CharSequence prefilledPassword) {
 		this.window = window;
 		this.vault = vault;
 		this.executor = executor;
 		this.prefilledPassword = prefilledPassword;
-		this.wordEncoder = new WordEncoder();
+		this.recoveryKeyFactory = recoveryKeyFactory;
 		this.recoveryKey = new SimpleStringProperty();
 	}
 	
@@ -54,17 +53,7 @@ public class RecoveryKeyCreationController implements FxController {
 	@FXML
 	public void createRecoveryKey() {
 		Tasks.create(() -> {
-			byte[] rawKey = CryptoFileSystemProvider.exportRawKey(vault.getPath(), MASTERKEY_FILENAME, new byte[0], passwordField.getCharacters());
-			assert rawKey.length == 64;
-			byte[] paddedKey = Arrays.copyOf(rawKey, 66);
-			// TODO add two-byte CRC
-
-			try {
-				return wordEncoder.encodePadded(paddedKey);
-			} finally {
-				Arrays.fill(rawKey, (byte) 0x00);
-				Arrays.fill(paddedKey, (byte) 0x00);
-			}
+			return recoveryKeyFactory.createRecoveryKey(vault.getPath(), passwordField.getCharacters());
 		}).onSuccess(result -> {
 			recoveryKey.set(result);
 		}).onError(IOException.class, e -> {

+ 78 - 0
main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java

@@ -0,0 +1,78 @@
+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.cryptolib.api.InvalidPassphraseException;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Arrays;
+
+@Singleton
+public class RecoveryKeyFactory {
+
+	private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes
+	
+	private final WordEncoder wordEncoder;
+	
+	@Inject
+	public RecoveryKeyFactory(WordEncoder wordEncoder) {
+		this.wordEncoder = wordEncoder;
+	}
+
+	/**
+	 * @param vaultPath Path to the storage location of a vault
+	 * @param password The vault's password
+	 * @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
+	 * @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);
+		try {
+			return createRecoveryKey(rawKey);
+		} finally {
+			Arrays.fill(rawKey, (byte) 0x00);
+		}
+	}
+
+	// visible for testing
+	String createRecoveryKey(byte[] rawKey) {
+		Preconditions.checkArgument(rawKey.length == 64, "key should be 64 bytes");
+		byte[] paddedKey = Arrays.copyOf(rawKey, 66);
+		try {
+			// copy 16 most significant bits of CRC32(rawKey) to the end of paddedKey:
+			Hashing.crc32().hashBytes(rawKey).writeBytesTo(paddedKey, 64, 2);
+			return wordEncoder.encodePadded(paddedKey);
+		} finally {
+			Arrays.fill(paddedKey, (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);
+		} catch (IllegalArgumentException e) {
+			return false;
+		}
+		if (paddedKey.length != 66) {
+			return false;
+		}
+		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);
+	}
+
+}

+ 6 - 1
main/ui/src/main/java/org/cryptomator/ui/recoverykey/WordEncoder.java

@@ -3,6 +3,8 @@ package org.cryptomator.ui.recoverykey;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Splitter;
 
+import javax.inject.Inject;
+import javax.inject.Singleton;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
@@ -14,16 +16,19 @@ import java.util.Map;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 
+@Singleton
 class WordEncoder {
 
+	private static final String DEFAULT_WORD_FILE = "/i18n/4096words_en.txt";
 	private static final int WORD_COUNT = 4096;
 	private static final char DELIMITER = ' ';
 	
 	private final List<String> words;
 	private final Map<String, Integer> indices;
 	
+	@Inject
 	public WordEncoder() {
-		this("/i18n/4096words_en.txt");
+		this(DEFAULT_WORD_FILE);
 	}
 	
 	public WordEncoder(String wordFile) {

+ 59 - 0
main/ui/src/test/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactoryTest.java

@@ -0,0 +1,59 @@
+package org.cryptomator.ui.recoverykey;
+
+import com.google.common.base.Splitter;
+import org.cryptomator.cryptofs.CryptoFileSystemProvider;
+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 java.io.IOException;
+import java.nio.file.Path;
+
+class RecoveryKeyFactoryTest {
+
+	private WordEncoder wordEncoder = new WordEncoder();
+	private RecoveryKeyFactory inTest = new RecoveryKeyFactory(wordEncoder);
+	
+	@Test
+	@DisplayName("createRecoveryKey() creates 44 words")
+	public void testCreateRecoveryKey(@TempDir Path pathToVault) throws IOException {
+		CryptoFileSystemProvider.initialize(pathToVault, "masterkey.cryptomator", "asd");
+		String recoveryKey = inTest.createRecoveryKey(pathToVault, "asd");
+		Assertions.assertNotNull(recoveryKey);
+		Assertions.assertEquals(44, Splitter.on(' ').splitToList(recoveryKey).size()); // 66 bytes encoded as 44 words
+	}
+
+	@Test
+	@DisplayName("validateRecoveryKey() with garbage input")
+	public void testValidateValidateRecoveryKeyWithGarbageInput() {
+		boolean result = inTest.validateRecoveryKey("löl");
+		Assertions.assertFalse(result);
+	}
+
+	@Test
+	@DisplayName("validateRecoveryKey() with too short input")
+	public void testValidateValidateRecoveryKeyWithTooShortInput() {
+		boolean result = inTest.validateRecoveryKey("them circumstances");
+		Assertions.assertFalse(result);
+	}
+
+	@Test
+	@DisplayName("validateRecoveryKey() with invalid crc32/16")
+	public void testValidateValidateRecoveryKeyWithInvalidCrc() {
+		boolean result = inTest.validateRecoveryKey("them circumstances conduct providing have gesture aged extraordinary generally silently" +
+				" beasts rights sit country highest career wrought silently liberal altogether capacity David conscious word issue" +
+				" ancient directed solitary how spain look smile see won't although dying obtain vol with c. asleep along listen circumstances");
+		Assertions.assertFalse(result);
+	}
+	
+	@Test
+	@DisplayName("validateRecoveryKey() with valid key")
+	public void testValidateValidateRecoveryKeyWithValidKey() {
+		boolean result = inTest.validateRecoveryKey("them circumstances conduct providing have gesture aged extraordinary generally silently" +
+				" beasts rights sit country highest career wrought silently liberal altogether capacity David conscious word issue" +
+				" ancient directed solitary how spain look smile see won't although dying obtain vol with c. asleep along listen riding");
+		Assertions.assertTrue(result);
+	}
+
+}