Bläddra i källkod

Adjusted to CryptoFS 2.0.0

Sebastian Stenzel 4 år sedan
förälder
incheckning
c0a9a95e4f

+ 12 - 0
main/commons/src/main/java/org/cryptomator/common/CommonsModule.java

@@ -25,6 +25,8 @@ import javafx.beans.binding.Binding;
 import javafx.beans.binding.Bindings;
 import javafx.collections.ObservableList;
 import java.net.InetSocketAddress;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
 import java.util.Comparator;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -53,6 +55,16 @@ public abstract class CommonsModule {
 				+ "r0DzRyj4ixPIt38CQB8=";
 	}
 
+	@Provides
+	@Singleton
+	static SecureRandom provideCSPRNG() {
+		try {
+			return SecureRandom.getInstanceStrong();
+		} catch (NoSuchAlgorithmException e) {
+			throw new IllegalStateException("A strong algorithm must exist in every Java platform.", e);
+		}
+	}
+
 	@Provides
 	@Singleton
 	@Named("SemVer")

+ 2 - 0
main/commons/src/main/java/org/cryptomator/common/Constants.java

@@ -3,5 +3,7 @@ package org.cryptomator.common;
 public interface Constants {
 
 	String MASTERKEY_FILENAME = "masterkey.cryptomator";
+	String VAULTCONFIG_FILENAME = "vault.cryptomator";
+	byte[] PEPPER = new byte[0];
 
 }

+ 13 - 8
main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java

@@ -21,6 +21,7 @@ import org.cryptomator.cryptofs.common.Constants;
 import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
 import org.cryptomator.cryptolib.api.CryptoException;
 import org.cryptomator.cryptolib.api.InvalidPassphraseException;
+import org.cryptomator.cryptolib.common.MasterkeyFile;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -45,6 +46,7 @@ import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
 
 import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
+import static org.cryptomator.common.Constants.PEPPER;
 
 @PerVault
 public class Vault {
@@ -111,14 +113,17 @@ public class Vault {
 			LOG.info("Storing file name length limit of {}", limit);
 		}
 		assert vaultSettings.filenameLengthLimit().get() > 0;
-		CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
-				.withPassphrase(passphrase) //
-				.withFlags(flags) //
-				.withMasterkeyFilename(MASTERKEY_FILENAME) //
-				.withMaxPathLength(vaultSettings.filenameLengthLimit().get() + Constants.MAX_ADDITIONAL_PATH_LENGTH) //
-				.withMaxNameLength(vaultSettings.filenameLengthLimit().get()) //
-				.build();
-		return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
+
+		Path masterkeyPath = getPath().resolve(MASTERKEY_FILENAME);
+		try (var keyLoader = MasterkeyFile.withContentFromFile(masterkeyPath).unlock(passphrase, PEPPER, Optional.empty())) {
+			CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
+					.withKeyLoader(keyLoader) //
+					.withFlags(flags) //
+					.withMaxPathLength(vaultSettings.filenameLengthLimit().get() + Constants.MAX_ADDITIONAL_PATH_LENGTH) //
+					.withMaxNameLength(vaultSettings.filenameLengthLimit().get()) //
+					.build();
+			return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
+		}
 	}
 
 	public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, VolumeException, InvalidMountPointException {

+ 4 - 3
main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java

@@ -28,6 +28,7 @@ import java.util.ResourceBundle;
 import java.util.stream.Collectors;
 
 import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
+import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
 
 @Singleton
 public class VaultListManager {
@@ -54,7 +55,7 @@ public class VaultListManager {
 
 	public Vault add(Path pathToVault) throws NoSuchFileException {
 		Path normalizedPathToVault = pathToVault.normalize().toAbsolutePath();
-		if (!CryptoFileSystemProvider.containsVault(normalizedPathToVault, MASTERKEY_FILENAME)) {
+		if (!CryptoFileSystemProvider.containsVault(normalizedPathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME)) {
 			throw new NoSuchFileException(normalizedPathToVault.toString(), null, "Not a vault directory");
 		}
 		Optional<Vault> alreadyExistingVault = get(normalizedPathToVault);
@@ -124,9 +125,9 @@ public class VaultListManager {
 	}
 
 	private static VaultState determineVaultState(Path pathToVault) throws IOException {
-		if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) {
+		if (!CryptoFileSystemProvider.containsVault(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME)) {
 			return VaultState.MISSING;
-		} else if (Migrators.get().needsMigration(pathToVault, MASTERKEY_FILENAME)) {
+		} else if (Migrators.get().needsMigration(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME)) {
 			return VaultState.NEEDS_MIGRATION;
 		} else {
 			return VaultState.LOCKED;

+ 1 - 1
main/pom.xml

@@ -24,7 +24,7 @@
 		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 
 		<!-- cryptomator dependencies -->
-		<cryptomator.cryptofs.version>1.9.13</cryptomator.cryptofs.version>
+		<cryptomator.cryptofs.version>2.0.0-beta1</cryptomator.cryptofs.version>
 		<cryptomator.integrations.version>0.1.6</cryptomator.integrations.version>
 		<cryptomator.integrations.win.version>0.1.0-beta1</cryptomator.integrations.win.version>
 		<cryptomator.integrations.mac.version>0.1.0-beta3</cryptomator.integrations.mac.version>

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 36 - 12
main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java


+ 26 - 5
main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java

@@ -3,7 +3,10 @@ package org.cryptomator.ui.changepassword;
 import org.cryptomator.common.keychain.KeychainManager;
 import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.cryptofs.CryptoFileSystemProvider;
+import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
+import org.cryptomator.cryptolib.api.CryptoException;
 import org.cryptomator.cryptolib.api.InvalidPassphraseException;
+import org.cryptomator.cryptolib.common.MasterkeyFile;
 import org.cryptomator.integrations.keychain.KeychainAccessException;
 import org.cryptomator.ui.common.Animations;
 import org.cryptomator.ui.common.ErrorComponent;
@@ -23,6 +26,12 @@ import javafx.scene.control.CheckBox;
 import javafx.stage.Stage;
 import java.io.IOException;
 import java.nio.CharBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
+import java.security.SecureRandom;
+import java.text.Normalizer;
 import java.util.Optional;
 
 import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
@@ -31,24 +40,27 @@ import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
 public class ChangePasswordController implements FxController {
 
 	private static final Logger LOG = LoggerFactory.getLogger(ChangePasswordController.class);
+	private static final String MASTERKEY_BACKUP_SUFFIX = ".bkup";
 
 	private final Stage window;
 	private final Vault vault;
 	private final ObjectProperty<CharSequence> newPassword;
 	private final ErrorComponent.Builder errorComponent;
 	private final KeychainManager keychain;
+	private final SecureRandom csprng;
 
 	public NiceSecurePasswordField oldPasswordField;
 	public CheckBox finalConfirmationCheckbox;
 	public Button finishButton;
 
 	@Inject
-	public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty<CharSequence> newPassword, ErrorComponent.Builder errorComponent, KeychainManager keychain) {
+	public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty<CharSequence> newPassword, ErrorComponent.Builder errorComponent, KeychainManager keychain, SecureRandom csprng) {
 		this.window = window;
 		this.vault = vault;
 		this.newPassword = newPassword;
 		this.errorComponent = errorComponent;
 		this.keychain = keychain;
+		this.csprng = csprng;
 	}
 
 	@FXML
@@ -67,17 +79,26 @@ public class ChangePasswordController implements FxController {
 	@FXML
 	public void finish() {
 		try {
-			CryptoFileSystemProvider.changePassphrase(vault.getPath(), MASTERKEY_FILENAME, oldPasswordField.getCharacters(), newPassword.get());
+			//String normalizedOldPassphrase = Normalizer.normalize(oldPasswordField.getCharacters(), Normalizer.Form.NFC);
+			//String normalizedNewPassphrase = Normalizer.normalize(newPassword.get(), Normalizer.Form.NFC);
+			CharSequence oldPassphrase = oldPasswordField.getCharacters(); // TODO verify: is this already NFC-normalized?
+			CharSequence newPassphrase = newPassword.get(); // TODO verify: is this already NFC-normalized?
+			Path masterkeyPath = vault.getPath().resolve(MASTERKEY_FILENAME);
+			byte[] oldMasterkeyBytes = Files.readAllBytes(masterkeyPath);
+			byte[] newMasterkeyBytes = MasterkeyFile.changePassphrase(oldMasterkeyBytes, oldPassphrase, newPassphrase, new byte[0], csprng);
+			Path backupKeyPath = vault.getPath().resolve(MASTERKEY_FILENAME + MasterkeyBackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + MASTERKEY_BACKUP_SUFFIX);
+			Files.move(masterkeyPath, backupKeyPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
+			Files.write(masterkeyPath, newMasterkeyBytes, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
 			LOG.info("Successfully changed password for {}", vault.getDisplayName());
 			window.close();
 			updatePasswordInSystemkeychain();
-		} catch (IOException e) {
-			LOG.error("IO error occured during password change. Unable to perform operation.", e);
-			errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
 		} catch (InvalidPassphraseException e) {
 			Animations.createShakeWindowAnimation(window).play();
 			oldPasswordField.selectAll();
 			oldPasswordField.requestFocus();
+		} catch (IOException | CryptoException e) {
+			LOG.error("Password change failed. Unable to perform operation.", e);
+			errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
 		}
 	}
 

+ 4 - 3
main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java

@@ -27,6 +27,7 @@ import java.util.Set;
 import java.util.stream.Collectors;
 
 import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
+import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
 
 @MainWindowScoped
 public class MainWindowController implements FxController {
@@ -91,9 +92,9 @@ public class MainWindowController implements FxController {
 	}
 
 	private boolean containsVault(Path path) {
-		if (path.getFileName().toString().equals(MASTERKEY_FILENAME)) {
+		if (path.getFileName().toString().equals(VAULTCONFIG_FILENAME)) {
 			return true;
-		} else if (Files.isDirectory(path) && Files.exists(path.resolve(MASTERKEY_FILENAME))) {
+		} else if (Files.isDirectory(path) && Files.exists(path.resolve(VAULTCONFIG_FILENAME))) {
 			return true;
 		} else {
 			return false;
@@ -102,7 +103,7 @@ public class MainWindowController implements FxController {
 
 	private void addVault(Path pathToVault) {
 		try {
-			if (pathToVault.getFileName().toString().equals(MASTERKEY_FILENAME)) {
+			if (pathToVault.getFileName().toString().equals(VAULTCONFIG_FILENAME)) {
 				vaultListManager.add(pathToVault.getParent());
 			} else {
 				vaultListManager.add(pathToVault);

+ 3 - 2
main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java

@@ -43,6 +43,7 @@ import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 
 import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
+import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
 
 @MigrationScoped
 public class MigrationRunController implements FxController {
@@ -110,8 +111,8 @@ public class MigrationRunController implements FxController {
 		}, 0, MIGRATION_PROGRESS_UPDATE_MILLIS, TimeUnit.MILLISECONDS);
 		Tasks.create(() -> {
 			Migrators migrators = Migrators.get();
-			migrators.migrate(vault.getPath(), MASTERKEY_FILENAME, password, this::migrationProgressChanged, this::migrationRequiresInput);
-			return migrators.needsMigration(vault.getPath(), MASTERKEY_FILENAME);
+			migrators.migrate(vault.getPath(), VAULTCONFIG_FILENAME, MASTERKEY_FILENAME, password, this::migrationProgressChanged, this::migrationRequiresInput);
+			return migrators.needsMigration(vault.getPath(), VAULTCONFIG_FILENAME, MASTERKEY_FILENAME);
 		}).onSuccess(needsAnotherMigration -> {
 			if (needsAnotherMigration) {
 				LOG.info("Migration of '{}' succeeded, but another migration is required.", vault.getDisplayName());

+ 2 - 1
main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java

@@ -2,6 +2,7 @@ package org.cryptomator.ui.recoverykey;
 
 import dagger.Lazy;
 import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.cryptolib.api.CryptoException;
 import org.cryptomator.cryptolib.api.InvalidPassphraseException;
 import org.cryptomator.ui.common.Animations;
 import org.cryptomator.ui.common.ErrorComponent;
@@ -80,7 +81,7 @@ public class RecoveryKeyCreationController implements FxController {
 		}
 
 		@Override
-		protected String call() throws IOException {
+		protected String call() throws IOException, CryptoException {
 			return recoveryKeyFactory.createRecoveryKey(vault.getPath(), passwordField.getCharacters());
 		}
 

+ 29 - 8
main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java

@@ -2,28 +2,39 @@ 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.cryptofs.common.MasterkeyBackupHelper;
+import org.cryptomator.cryptolib.api.CryptoException;
 import org.cryptomator.cryptolib.api.InvalidPassphraseException;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.common.MasterkeyFile;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
 import java.io.IOException;
+import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
+import java.security.SecureRandom;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Optional;
 
 import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
+import static org.cryptomator.common.Constants.PEPPER;
 
 @Singleton
 public class RecoveryKeyFactory {
 
-	private static final byte[] PEPPER = new byte[0];
+	private static final String MASTERKEY_BACKUP_SUFFIX = ".bkup";
 
 	private final WordEncoder wordEncoder;
+	private final SecureRandom csprng;
 
 	@Inject
-	public RecoveryKeyFactory(WordEncoder wordEncoder) {
+	public RecoveryKeyFactory(WordEncoder wordEncoder, SecureRandom csprng) {
 		this.wordEncoder = wordEncoder;
+		this.csprng = csprng;
 	}
 
 	public Collection<String> getDictionary() {
@@ -36,11 +47,14 @@ public class RecoveryKeyFactory {
 	 * @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
+	 * @throws CryptoException In case of other cryptographic errors
 	 * @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, PEPPER, password);
-		try {
+	public String createRecoveryKey(Path vaultPath, CharSequence password) throws IOException, InvalidPassphraseException, CryptoException {
+		Path masterkeyPath = vaultPath.resolve(MASTERKEY_FILENAME);
+		byte[] rawKey = new byte[0];
+		try (var masterkey = MasterkeyFile.withContentFromFile(masterkeyPath).unlock(password, PEPPER, Optional.empty()).loadKeyAndClose()) {
+			rawKey = masterkey.getEncoded();
 			return createRecoveryKey(rawKey);
 		} finally {
 			Arrays.fill(rawKey, (byte) 0x00);
@@ -72,8 +86,15 @@ public class RecoveryKeyFactory {
 	 */
 	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);
+		try (var masterkey = Masterkey.createFromRaw(rawKey)) {
+			byte[] restoredKey = MasterkeyFile.lock(masterkey, newPassword, PEPPER, 999, csprng);
+			Path masterkeyPath = vaultPath.resolve(MASTERKEY_FILENAME);
+			if (Files.exists(masterkeyPath)) {
+				byte[] oldMasterkeyBytes = Files.readAllBytes(masterkeyPath);
+				Path backupKeyPath = vaultPath.resolve(MASTERKEY_FILENAME + MasterkeyBackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + MASTERKEY_BACKUP_SUFFIX);
+				Files.move(masterkeyPath, backupKeyPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
+			}
+			Files.write(masterkeyPath, restoredKey, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
 		} finally {
 			Arrays.fill(rawKey, (byte) 0x00);
 		}

+ 3 - 2
main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java

@@ -7,6 +7,7 @@ import org.cryptomator.common.vaults.MountPointRequirement;
 import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.common.vaults.VaultState;
 import org.cryptomator.common.vaults.Volume.VolumeException;
+import org.cryptomator.cryptolib.api.CryptoException;
 import org.cryptomator.cryptolib.api.InvalidPassphraseException;
 import org.cryptomator.integrations.keychain.KeychainAccessException;
 import org.cryptomator.ui.common.Animations;
@@ -85,7 +86,7 @@ public class UnlockWorkflow extends Task<Boolean> {
 	}
 
 	@Override
-	protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException {
+	protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException, CryptoException {
 		try {
 			if (attemptUnlock()) {
 				handleSuccess();
@@ -100,7 +101,7 @@ public class UnlockWorkflow extends Task<Boolean> {
 		}
 	}
 
-	private boolean attemptUnlock() throws InterruptedException, IOException, VolumeException, InvalidMountPointException {
+	private boolean attemptUnlock() throws InterruptedException, IOException, VolumeException, InvalidMountPointException, CryptoException {
 		boolean proceed = password.get() != null || askForPassword(false) == PasswordEntry.PASSWORD_ENTERED;
 		while (proceed) {
 			try {

+ 20 - 3
main/ui/src/test/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactoryTest.java

@@ -2,23 +2,40 @@ package org.cryptomator.ui.recoverykey;
 
 import com.google.common.base.Splitter;
 import org.cryptomator.cryptofs.CryptoFileSystemProvider;
+import org.cryptomator.cryptolib.api.CryptoException;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.common.MasterkeyFile;
+import org.cryptomator.cryptolib.common.MasterkeyFileLoader;
 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 org.mockito.MockedStatic;
+import org.mockito.Mockito;
 
 import java.io.IOException;
 import java.nio.file.Path;
+import java.security.SecureRandom;
 
 class RecoveryKeyFactoryTest {
 
 	private WordEncoder wordEncoder = new WordEncoder();
-	private RecoveryKeyFactory inTest = new RecoveryKeyFactory(wordEncoder);
+	private SecureRandom csprng = Mockito.mock(SecureRandom.class);
+	private RecoveryKeyFactory inTest = new RecoveryKeyFactory(wordEncoder, csprng);
 
 	@Test
 	@DisplayName("createRecoveryKey() creates 44 words")
-	public void testCreateRecoveryKey(@TempDir Path pathToVault) throws IOException {
-		CryptoFileSystemProvider.initialize(pathToVault, "masterkey.cryptomator", "asd");
+	public void testCreateRecoveryKey() throws IOException, CryptoException {
+		Path pathToVault = Path.of("path/to/vault");
+		MockedStatic<MasterkeyFile> masterkeyFileClass = Mockito.mockStatic(MasterkeyFile.class);
+		MasterkeyFile masterkeyFile = Mockito.mock(MasterkeyFile.class);
+		MasterkeyFileLoader keyLoader = Mockito.mock(MasterkeyFileLoader.class);
+		Masterkey masterkey = Mockito.mock(Masterkey.class);
+		masterkeyFileClass.when(() -> MasterkeyFile.withContentFromFile(Path.of("path/to/vault/masterkey.cryptomator"))).thenReturn(masterkeyFile);
+		Mockito.when(masterkeyFile.unlock(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(keyLoader);
+		Mockito.when(keyLoader.loadKeyAndClose()).thenReturn(masterkey);
+		Mockito.when(masterkey.getEncoded()).thenReturn(new byte[64]);
+
 		String recoveryKey = inTest.createRecoveryKey(pathToVault, "asd");
 		Assertions.assertNotNull(recoveryKey);
 		Assertions.assertEquals(44, Splitter.on(' ').splitToList(recoveryKey).size()); // 66 bytes encoded as 44 words

+ 1 - 0
main/ui/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker

@@ -0,0 +1 @@
+mock-maker-inline