mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-19 19:21:27 +00:00
Adjusted to CryptoFS 2.0.0
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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];
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,6 +5,13 @@ import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultListManager;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||
import org.cryptomator.cryptofs.VaultCipherCombo;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
|
||||
import org.cryptomator.cryptolib.common.MasterkeyFile;
|
||||
import org.cryptomator.cryptolib.common.MasterkeyFileLoader;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
@@ -29,6 +36,7 @@ import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Toggle;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.stage.Stage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
@@ -37,12 +45,15 @@ import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
||||
import static org.cryptomator.common.Constants.PEPPER;
|
||||
|
||||
@AddVaultWizardScoped
|
||||
public class CreateNewVaultPasswordController implements FxController {
|
||||
@@ -64,6 +75,7 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
private final ResourceBundle resourceBundle;
|
||||
private final ObjectProperty<CharSequence> password;
|
||||
private final ReadmeGenerator readmeGenerator;
|
||||
private final SecureRandom csprng;
|
||||
private final BooleanProperty processing;
|
||||
private final BooleanProperty readyToCreateVault;
|
||||
private final ObjectBinding<ContentDisplay> createVaultButtonState;
|
||||
@@ -73,7 +85,7 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
public Toggle skipRecoveryKey;
|
||||
|
||||
@Inject
|
||||
CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy<Scene> recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, ErrorComponent.Builder errorComponent, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, @Named("newPassword") ObjectProperty<CharSequence> password, ReadmeGenerator readmeGenerator) {
|
||||
CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy<Scene> recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, ErrorComponent.Builder errorComponent, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, @Named("newPassword") ObjectProperty<CharSequence> password, ReadmeGenerator readmeGenerator, SecureRandom csprng) {
|
||||
this.window = window;
|
||||
this.chooseLocationScene = chooseLocationScene;
|
||||
this.recoveryKeyScene = recoveryKeyScene;
|
||||
@@ -89,6 +101,7 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
this.resourceBundle = resourceBundle;
|
||||
this.password = password;
|
||||
this.readmeGenerator = readmeGenerator;
|
||||
this.csprng = csprng;
|
||||
this.processing = new SimpleBooleanProperty();
|
||||
this.readyToCreateVault = new SimpleBooleanProperty();
|
||||
this.createVaultButtonState = Bindings.createObjectBinding(this::getCreateVaultButtonState, processing);
|
||||
@@ -161,23 +174,34 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
}
|
||||
|
||||
private void initializeVault(Path path, CharSequence passphrase) throws IOException {
|
||||
CryptoFileSystemProvider.initialize(path, MASTERKEY_FILENAME, passphrase);
|
||||
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
|
||||
.withPassphrase(passphrase) //
|
||||
.withFlags(Collections.emptySet()) //
|
||||
.withMasterkeyFilename(MASTERKEY_FILENAME) //
|
||||
.build();
|
||||
|
||||
String vaultReadmeFileName = resourceBundle.getString("addvault.new.readme.accessLocation.fileName");
|
||||
try (FileSystem fs = CryptoFileSystemProvider.newFileSystem(path, fsProps); //
|
||||
WritableByteChannel ch = Files.newByteChannel(fs.getPath("/", vaultReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
||||
ch.write(US_ASCII.encode(readmeGenerator.createVaultAccessLocationReadmeRtf()));
|
||||
// 1. write masterkey:
|
||||
Path masterkeyFilePath = path.resolve(MASTERKEY_FILENAME);
|
||||
try (Masterkey masterkey = Masterkey.createNew(csprng)) {
|
||||
byte[] serialized = MasterkeyFile.lock(masterkey, passphrase, PEPPER, 999, csprng);
|
||||
Files.write(masterkeyFilePath, serialized, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
|
||||
}
|
||||
|
||||
// 2. verify masterkey and initialize vault:
|
||||
try (var loader = MasterkeyFile.withContentFromFile(masterkeyFilePath).unlock(passphrase, PEPPER, Optional.of(999))) {
|
||||
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(loader).build();
|
||||
CryptoFileSystemProvider.initialize(path, fsProps, MasterkeyFileLoader.KEY_ID);
|
||||
|
||||
// 3. write vault-internal readme file:
|
||||
String vaultReadmeFileName = resourceBundle.getString("addvault.new.readme.accessLocation.fileName");
|
||||
try (FileSystem fs = CryptoFileSystemProvider.newFileSystem(path, fsProps); //
|
||||
WritableByteChannel ch = Files.newByteChannel(fs.getPath("/", vaultReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
||||
ch.write(US_ASCII.encode(readmeGenerator.createVaultAccessLocationReadmeRtf()));
|
||||
}
|
||||
} catch (CryptoException e) {
|
||||
throw new IOException("Failed initialize vault.", e);
|
||||
}
|
||||
|
||||
// 4. write vault-external readme file:
|
||||
String storagePathReadmeFileName = resourceBundle.getString("addvault.new.readme.storageLocation.fileName");
|
||||
try (WritableByteChannel ch = Files.newByteChannel(path.resolve(storagePathReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
||||
ch.write(US_ASCII.encode(readmeGenerator.createVaultStorageLocationReadmeRtf()));
|
||||
}
|
||||
|
||||
LOG.info("Created vault at {}", path);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
mock-maker-inline
|
||||
Reference in New Issue
Block a user