Externalized logic of recovery key creation to reusable utility class

This commit is contained in:
Sebastian Stenzel
2019-10-08 14:58:55 +02:00
parent 5808239416
commit 08d9beb6b8
5 changed files with 147 additions and 18 deletions

View File

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

View File

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

View File

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

View File

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

View File

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