mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-09-19 14:34:20 +00:00
Externalized logic of recovery key creation to reusable utility class
This commit is contained in:
@@ -26,8 +26,6 @@ import org.slf4j.LoggerFactory;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import java.awt.desktop.QuitResponse;
|
import java.awt.desktop.QuitResponse;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.CompletionStage;
|
|
||||||
|
|
||||||
@FxApplicationScoped
|
@FxApplicationScoped
|
||||||
public class FxApplication extends Application {
|
public class FxApplication extends Application {
|
||||||
|
|||||||
+4
-15
@@ -24,23 +24,22 @@ import java.util.concurrent.ExecutorService;
|
|||||||
public class RecoveryKeyCreationController implements FxController {
|
public class RecoveryKeyCreationController implements FxController {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(RecoveryKeyCreationController.class);
|
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 Stage window;
|
||||||
private final Vault vault;
|
private final Vault vault;
|
||||||
private final ExecutorService executor;
|
private final ExecutorService executor;
|
||||||
private final CharSequence prefilledPassword;
|
private final CharSequence prefilledPassword;
|
||||||
private final WordEncoder wordEncoder;
|
private final RecoveryKeyFactory recoveryKeyFactory;
|
||||||
private final StringProperty recoveryKey;
|
private final StringProperty recoveryKey;
|
||||||
public NiceSecurePasswordField passwordField;
|
public NiceSecurePasswordField passwordField;
|
||||||
|
|
||||||
@Inject
|
@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.window = window;
|
||||||
this.vault = vault;
|
this.vault = vault;
|
||||||
this.executor = executor;
|
this.executor = executor;
|
||||||
this.prefilledPassword = prefilledPassword;
|
this.prefilledPassword = prefilledPassword;
|
||||||
this.wordEncoder = new WordEncoder();
|
this.recoveryKeyFactory = recoveryKeyFactory;
|
||||||
this.recoveryKey = new SimpleStringProperty();
|
this.recoveryKey = new SimpleStringProperty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,17 +53,7 @@ public class RecoveryKeyCreationController implements FxController {
|
|||||||
@FXML
|
@FXML
|
||||||
public void createRecoveryKey() {
|
public void createRecoveryKey() {
|
||||||
Tasks.create(() -> {
|
Tasks.create(() -> {
|
||||||
byte[] rawKey = CryptoFileSystemProvider.exportRawKey(vault.getPath(), MASTERKEY_FILENAME, new byte[0], passwordField.getCharacters());
|
return recoveryKeyFactory.createRecoveryKey(vault.getPath(), 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);
|
|
||||||
}
|
|
||||||
}).onSuccess(result -> {
|
}).onSuccess(result -> {
|
||||||
recoveryKey.set(result);
|
recoveryKey.set(result);
|
||||||
}).onError(IOException.class, e -> {
|
}).onError(IOException.class, e -> {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ package org.cryptomator.ui.recoverykey;
|
|||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.base.Splitter;
|
import com.google.common.base.Splitter;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@@ -14,16 +16,19 @@ import java.util.Map;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
class WordEncoder {
|
class WordEncoder {
|
||||||
|
|
||||||
|
private static final String DEFAULT_WORD_FILE = "/i18n/4096words_en.txt";
|
||||||
private static final int WORD_COUNT = 4096;
|
private static final int WORD_COUNT = 4096;
|
||||||
private static final char DELIMITER = ' ';
|
private static final char DELIMITER = ' ';
|
||||||
|
|
||||||
private final List<String> words;
|
private final List<String> words;
|
||||||
private final Map<String, Integer> indices;
|
private final Map<String, Integer> indices;
|
||||||
|
|
||||||
|
@Inject
|
||||||
public WordEncoder() {
|
public WordEncoder() {
|
||||||
this("/i18n/4096words_en.txt");
|
this(DEFAULT_WORD_FILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public WordEncoder(String wordFile) {
|
public WordEncoder(String wordFile) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user