pass a single keyloader to cryptofs

This commit is contained in:
Sebastian Stenzel
2021-04-23 09:55:08 +02:00
parent 58b45bacd1
commit 69c63702f2
11 changed files with 172 additions and 207 deletions
@@ -123,7 +123,7 @@ public class Vault {
}
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
.withKeyLoaders(keyLoader) //
.withKeyLoader(keyLoader) //
.withFlags(flags) //
.withMaxCleartextNameLength(vaultSettings.maxCleartextFilenameLength().get()) //
.build();
+1 -1
View File
@@ -25,7 +25,7 @@
<project.jdk.version>16</project.jdk.version>
<!-- cryptomator dependencies -->
<cryptomator.cryptofs.version>2.0.0-beta7</cryptomator.cryptofs.version>
<cryptomator.cryptofs.version>2.0.0-beta8</cryptomator.cryptofs.version>
<cryptomator.integrations.version>1.0.0-beta2</cryptomator.integrations.version>
<cryptomator.integrations.win.version>1.0.0-beta2</cryptomator.integrations.win.version>
<cryptomator.integrations.mac.version>1.0.0-beta2</cryptomator.integrations.mac.version>
@@ -8,13 +8,14 @@ import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptofs.VaultCipherCombo;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.cryptolib.common.MasterkeyFileLoader;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.Tasks;
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingStrategy;
import org.cryptomator.ui.recoverykey.RecoveryKeyFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -36,6 +37,7 @@ import javafx.scene.control.ToggleGroup;
import javafx.stage.Stage;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.channels.WritableByteChannel;
import java.nio.file.FileSystem;
import java.nio.file.Files;
@@ -53,6 +55,7 @@ import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
public class CreateNewVaultPasswordController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultPasswordController.class);
private static final URI DEFAULT_KEY_ID = URI.create(MasterkeyFileLoadingStrategy.SCHEME + ":" + MASTERKEY_FILENAME); // TODO better place?
private final Stage window;
private final Lazy<Scene> chooseLocationScene;
@@ -174,23 +177,22 @@ public class CreateNewVaultPasswordController implements FxController {
Path masterkeyFilePath = path.resolve(MASTERKEY_FILENAME);
try (Masterkey masterkey = Masterkey.generate(csprng)) {
masterkeyFileAccess.persist(masterkey, masterkeyFilePath, passphrase);
}
// 2. initialize vault:
var context = new StaticMasterkeyFileLoaderContext(masterkeyFilePath, passphrase);
var loader = masterkeyFileAccess.keyLoader(path, context);
try {
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(VaultCipherCombo.SIV_CTRMAC).withKeyLoaders(loader).build();
CryptoFileSystemProvider.initialize(path, fsProps, MasterkeyFileLoader.keyId(MASTERKEY_FILENAME));
// 2. initialize vault:
try {
MasterkeyLoader loader = ignored -> masterkey;
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(VaultCipherCombo.SIV_CTRMAC).withKeyLoader(loader).build();
CryptoFileSystemProvider.initialize(path, fsProps, DEFAULT_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()));
// 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);
}
} catch (CryptoException e) {
throw new IOException("Failed initialize vault.", e);
}
// 4. write vault-external readme file:
@@ -1,28 +0,0 @@
package org.cryptomator.ui.addvaultwizard;
import com.google.common.base.Preconditions;
import org.cryptomator.cryptolib.common.MasterkeyFileLoaderContext;
import java.nio.file.Path;
class StaticMasterkeyFileLoaderContext implements MasterkeyFileLoaderContext {
private final Path masterkeyFilePath;
private final CharSequence passphrase;
StaticMasterkeyFileLoaderContext(Path masterkeyFilePath, CharSequence passphrase) {
this.masterkeyFilePath = masterkeyFilePath;
this.passphrase = passphrase;
}
@Override
public Path getCorrectMasterkeyFilePath(String s) {
return masterkeyFilePath;
}
@Override
public CharSequence getPassphrase(Path path) {
Preconditions.checkArgument(masterkeyFilePath.equals(path));
return passphrase;
}
}
@@ -3,8 +3,11 @@ package org.cryptomator.ui.keyloading;
import dagger.BindsInstance;
import dagger.Subcomponent;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import javafx.stage.Stage;
import java.util.Map;
import java.util.function.Supplier;
@KeyLoadingScoped
@Subcomponent(modules = {KeyLoadingModule.class})
@@ -35,13 +35,13 @@ abstract class KeyLoadingModule {
@Provides
@KeyLoading
@KeyLoadingScoped
static KeyLoadingStrategy provideKeyLoaderProvider(@KeyLoading Optional<URI> keyId, Map<String, KeyLoadingStrategy> strategies) {
static KeyLoadingStrategy provideKeyLoaderProvider(@KeyLoading Optional<URI> keyId, Map<String, Provider<KeyLoadingStrategy>> strategies) {
if (keyId.isEmpty()) {
return KeyLoadingStrategy.failed(new IllegalArgumentException("No key id provided"));
} else {
String scheme = keyId.get().getScheme();
var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme));
return strategies.getOrDefault(scheme, fallback);
return strategies.getOrDefault(scheme, () -> fallback).get();
}
}
@@ -1,21 +1,34 @@
package org.cryptomator.ui.keyloading;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
public interface KeyLoadingStrategy {
import java.net.URI;
/**
* A reusable, stateful {@link MasterkeyLoader}, that can deal with certain exceptions.
*/
@FunctionalInterface
public interface KeyLoadingStrategy extends MasterkeyLoader {
/**
* @return A reusable masterkey loader, preconfigured with the vault of the current unlock process
* @throws MasterkeyLoadingFailedException If unable to provide the masterkey loader
*/
MasterkeyLoader masterkeyLoader() throws MasterkeyLoadingFailedException;
/**
* Allows the component to try and recover from an exception thrown while loading a masterkey.
* Loads a master key. This might be a long-running operation, as it may require user input or expensive computations.
* <p>
* If loading fails exceptionally, this strategy might be able to {@link #recoverFromException(MasterkeyLoadingFailedException) recover from this exception}, so it can be used in a further attempt.
*
* @param exception An exception thrown either by {@link #masterkeyLoader()} or by the returned {@link MasterkeyLoader}.
* @return <code>true</code> if this component was able to handle the exception and another attempt should be made to load a masterkey
* @param keyId An URI uniquely identifying the source and identity of the key
* @return The raw key bytes. Must not be null
* @throws MasterkeyLoadingFailedException Thrown when it is impossible to fulfill the request
*/
@Override
Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException;
/**
* Allows the loader to try and recover from an exception thrown during the last attempt.
*
* @param exception An exception thrown by {@link #loadKey(URI)}.
* @return <code>true</code> if this component was able to handle the exception and another attempt can be made to load a masterkey
*/
default boolean recoverFromException(MasterkeyLoadingFailedException exception) {
return false;
@@ -38,7 +51,7 @@ public interface KeyLoadingStrategy {
* @return A new KeyLoadingStrategy that will always fail with an {@link MasterkeyLoadingFailedException}.
*/
static KeyLoadingStrategy failed(Exception exception) {
return () -> {
return keyid -> {
if (exception instanceof MasterkeyLoadingFailedException e) {
throw e;
} else {
@@ -1,129 +0,0 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import dagger.Lazy;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.cryptomator.cryptolib.common.MasterkeyFileLoaderContext;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.cryptomator.ui.unlock.UnlockCancelledException;
import javax.inject.Inject;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.Window;
import java.nio.CharBuffer;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicReference;
@KeyLoadingScoped
class MasterkeyFileLoadingContext implements MasterkeyFileLoaderContext {
private final Stage window;
private final Lazy<Scene> passphraseEntryScene;
private final Lazy<Scene> selectMasterkeyFileScene;
private final UserInteractionLock<MasterkeyFileLoadingModule.PasswordEntry> passwordEntryLock;
private final UserInteractionLock<MasterkeyFileLoadingModule.MasterkeyFileProvision> masterkeyFileProvisionLock;
private final AtomicReference<char[]> password;
private final AtomicReference<Path> filePath;
private boolean wrongPassword;
@Inject
public MasterkeyFileLoadingContext(@KeyLoading Stage window, @FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD) Lazy<Scene> passphraseEntryScene, @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) Lazy<Scene> selectMasterkeyFileScene, UserInteractionLock<MasterkeyFileLoadingModule.PasswordEntry> passwordEntryLock, UserInteractionLock<MasterkeyFileLoadingModule.MasterkeyFileProvision> masterkeyFileProvisionLock, AtomicReference<char[]> password, AtomicReference<Path> filePath) {
this.window = window;
this.passphraseEntryScene = passphraseEntryScene;
this.selectMasterkeyFileScene = selectMasterkeyFileScene;
this.passwordEntryLock = passwordEntryLock;
this.masterkeyFileProvisionLock = masterkeyFileProvisionLock;
this.password = password;
this.filePath = filePath;
}
@Override
public Path getCorrectMasterkeyFilePath(String masterkeyFilePath) {
if (filePath.get() != null) { // e.g. already chosen on previous attempt with wrong password
return filePath.get();
}
assert filePath.get() == null;
try {
if (askForCorrectMasterkeyFile() == MasterkeyFileLoadingModule.MasterkeyFileProvision.MASTERKEYFILE_PROVIDED) {
return filePath.get();
} else {
throw new UnlockCancelledException("Choosing masterkey file cancelled.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new UnlockCancelledException("Choosing masterkey file interrupted", e);
}
}
private MasterkeyFileLoadingModule.MasterkeyFileProvision askForCorrectMasterkeyFile() throws InterruptedException {
Platform.runLater(() -> {
window.setScene(selectMasterkeyFileScene.get());
window.show();
Window owner = window.getOwner();
if (owner != null) {
window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2);
window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2);
} else {
window.centerOnScreen();
}
});
return masterkeyFileProvisionLock.awaitInteraction();
}
@Override
public CharSequence getPassphrase(Path path) throws UnlockCancelledException {
if (password.get() != null) { // e.g. pre-filled from keychain
return CharBuffer.wrap(password.get());
}
assert password.get() == null;
try {
if (askForPassphrase() == MasterkeyFileLoadingModule.PasswordEntry.PASSWORD_ENTERED) {
assert password.get() != null;
return CharBuffer.wrap(password.get());
} else {
throw new UnlockCancelledException("Password entry cancelled.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new UnlockCancelledException("Password entry interrupted", e);
}
}
private MasterkeyFileLoadingModule.PasswordEntry askForPassphrase() throws InterruptedException {
Platform.runLater(() -> {
window.setScene(passphraseEntryScene.get());
window.show();
Window owner = window.getOwner();
if (owner != null) {
window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2);
window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2);
} else {
window.centerOnScreen();
}
if (wrongPassword) {
Animations.createShakeWindowAnimation(window).play();
}
});
return passwordEntryLock.awaitInteraction();
}
public boolean recoverFromException(MasterkeyLoadingFailedException exception) {
if (exception instanceof InvalidPassphraseException) {
this.wrongPassword = true;
password.set(null);
return true; // reattempting key load
} else {
return false; // nothing we can do
}
}
}
@@ -7,8 +7,6 @@ import dagger.multibindings.IntoMap;
import dagger.multibindings.StringKey;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.cryptolib.common.MasterkeyFileLoader;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
@@ -45,12 +43,6 @@ public abstract class MasterkeyFileLoadingModule {
CANCELED
}
@Provides
@KeyLoadingScoped
static MasterkeyFileLoader provideMasterkeyFileLoader(MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Vault vault, MasterkeyFileLoadingContext context) {
return masterkeyFileAccess.keyLoader(vault.getPath(), context);
}
@Provides
@KeyLoadingScoped
static UserInteractionLock<PasswordEntry> providePasswordEntryLock() {
@@ -125,7 +117,7 @@ public abstract class MasterkeyFileLoadingModule {
@Binds
@IntoMap
@KeyLoadingScoped
@StringKey("masterkeyfile")
@StringKey(MasterkeyFileLoadingStrategy.SCHEME)
abstract KeyLoadingStrategy bindMasterkeyFileLoadingStrategy(MasterkeyFileLoadingStrategy strategy);
}
@@ -1,38 +1,150 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import com.google.common.base.Preconditions;
import dagger.Lazy;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.cryptomator.cryptolib.common.MasterkeyFileLoader;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.cryptomator.ui.unlock.UnlockCancelledException;
import javax.inject.Inject;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.Window;
import java.net.URI;
import java.nio.CharBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicReference;
@KeyLoading
class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
private final MasterkeyFileLoader masterkeyFileLoader;
private final MasterkeyFileLoadingContext context;
public static final String SCHEME = "masterkeyfile";
private final Vault vault;
private final MasterkeyFileAccess masterkeyFileAcccess;
private final Stage window;
private final Lazy<Scene> passphraseEntryScene;
private final Lazy<Scene> selectMasterkeyFileScene;
private final UserInteractionLock<MasterkeyFileLoadingModule.PasswordEntry> passwordEntryLock;
private final UserInteractionLock<MasterkeyFileLoadingModule.MasterkeyFileProvision> masterkeyFileProvisionLock;
private final AtomicReference<char[]> password;
private final AtomicReference<Path> filePath;
private final MasterkeyFileLoadingFinisher finisher;
private boolean wrongPassword;
@Inject
public MasterkeyFileLoadingStrategy(MasterkeyFileLoader masterkeyFileLoader, MasterkeyFileLoadingContext context, MasterkeyFileLoadingFinisher finisher) {
this.masterkeyFileLoader = masterkeyFileLoader;
this.context = context;
public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAcccess, @KeyLoading Stage window, @FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD) Lazy<Scene> passphraseEntryScene, @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) Lazy<Scene> selectMasterkeyFileScene, UserInteractionLock<MasterkeyFileLoadingModule.PasswordEntry> passwordEntryLock, UserInteractionLock<MasterkeyFileLoadingModule.MasterkeyFileProvision> masterkeyFileProvisionLock, AtomicReference<char[]> password, AtomicReference<Path> filePath, MasterkeyFileLoadingFinisher finisher) {
this.vault = vault;
this.masterkeyFileAcccess = masterkeyFileAcccess;
this.window = window;
this.passphraseEntryScene = passphraseEntryScene;
this.selectMasterkeyFileScene = selectMasterkeyFileScene;
this.passwordEntryLock = passwordEntryLock;
this.masterkeyFileProvisionLock = masterkeyFileProvisionLock;
this.password = password;
this.filePath = filePath;
this.finisher = finisher;
}
@Override
public MasterkeyFileLoader masterkeyLoader() {
return masterkeyFileLoader;
public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException {
Preconditions.checkArgument(SCHEME.equalsIgnoreCase(keyId.getScheme()), "Only supports keys with scheme " + SCHEME);
try {
Path filePath = vault.getPath().resolve(keyId.getSchemeSpecificPart());
if (!Files.exists(filePath)) {
filePath = getAlternateMasterkeyFilePath();
}
CharSequence passphrase = getPassphrase();
return masterkeyFileAcccess.load(filePath, passphrase);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new UnlockCancelledException("Unlock interrupted", e);
}
}
@Override
public boolean recoverFromException(MasterkeyLoadingFailedException exception) {
return context.recoverFromException(exception);
if (exception instanceof InvalidPassphraseException) {
this.wrongPassword = true;
password.set(null);
return true; // reattempting key load
} else {
return false; // nothing we can do
}
}
@Override
public void cleanup(boolean unlockedSuccessfully) {
finisher.cleanup(unlockedSuccessfully);
}
private Path getAlternateMasterkeyFilePath() throws UnlockCancelledException, InterruptedException {
if (filePath == null) {
return switch (askUserForMasterkeyFilePath()) {
case MASTERKEYFILE_PROVIDED -> filePath.get();
case CANCELED -> throw new UnlockCancelledException("Choosing masterkey file cancelled.");
};
} else {
return filePath.get();
}
}
private MasterkeyFileLoadingModule.MasterkeyFileProvision askUserForMasterkeyFilePath() throws InterruptedException {
Platform.runLater(() -> {
window.setScene(selectMasterkeyFileScene.get());
window.show();
Window owner = window.getOwner();
if (owner != null) {
window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2);
window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2);
} else {
window.centerOnScreen();
}
});
return masterkeyFileProvisionLock.awaitInteraction();
}
private CharSequence getPassphrase() throws UnlockCancelledException, InterruptedException {
if (password.get() == null) {
return switch (askForPassphrase()) {
case PASSWORD_ENTERED -> CharBuffer.wrap(password.get());
case CANCELED -> throw new UnlockCancelledException("Password entry cancelled.");
};
} else {
// e.g. pre-filled from keychain or previous unlock attempt
return CharBuffer.wrap(password.get());
}
}
private MasterkeyFileLoadingModule.PasswordEntry askForPassphrase() throws InterruptedException {
Platform.runLater(() -> {
window.setScene(passphraseEntryScene.get());
window.show();
Window owner = window.getOwner();
if (owner != null) {
window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2);
window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2);
} else {
window.centerOnScreen();
}
if (wrongPassword) {
Animations.createShakeWindowAnimation(window).play();
}
});
return passwordEntryLock.awaitInteraction();
}
}
@@ -70,7 +70,7 @@ public class UnlockWorkflow extends Task<Boolean> {
private void attemptUnlock() throws IOException, VolumeException, InvalidMountPointException, CryptoException {
boolean success = false;
try {
vault.unlock(keyLoadingStrategy.masterkeyLoader());
vault.unlock(keyLoadingStrategy);
success = true;
} catch (MasterkeyLoadingFailedException e) {
if (keyLoadingStrategy.recoverFromException(e)) {