Merge pull request #1983 from cryptomator/feature/simplified-workflow-results

This commit is contained in:
Sebastian Stenzel
2022-03-21 21:19:32 +01:00
committed by GitHub
26 changed files with 594 additions and 442 deletions

View File

@@ -0,0 +1,108 @@
package org.cryptomator.common;
import javax.security.auth.Destroyable;
import java.util.Arrays;
/**
* A destroyable CharSequence.
*/
public class Passphrase implements Destroyable, CharSequence {
private final char[] data;
private final int offset;
private final int length;
private boolean destroyed;
/**
* Wraps (doesn't copy) the given data.
*
* @param data The wrapped data. Any changes to this will be reflected in this passphrase
*/
public Passphrase(char[] data) {
this(data, 0, data.length);
}
/**
* Wraps (doesn't copy) a subarray of the given data.
*
* @param data The wrapped data. Any changes to this will be reflected in this passphrase
* @param offset The subarray offset, i.e. the first character of this passphrase
* @param length The subarray length, i.e. the length of this passphrase
*/
public Passphrase(char[] data, int offset, int length) {
if (offset < 0 || length < 0 || offset + length > data.length) {
throw new IndexOutOfBoundsException("[%1$d %1$d + %2$d[ not within [0, %3$d[".formatted(offset, length, data.length));
}
this.data = data;
this.offset = offset;
this.length = length;
}
public static Passphrase copyOf(CharSequence cs) {
char[] result = new char[cs.length()];
for (int i = 0; i < cs.length(); i++) {
result[i] = cs.charAt(i);
}
return new Passphrase(result);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Passphrase that = (Passphrase) o;
// time-constant comparison
int diff = 0;
for (int i = 0; i < length; i++) {
diff |= charAt(i) ^ that.charAt(i);
}
return diff == 0;
}
@Override
public int hashCode() {
// basically Arrays.hashCode, but only for a certain subarray
int result = 1;
for (int i = 0; i < length; i++) {
result = 31 * result + charAt(i);
}
return result;
}
@Override
public String toString() {
return new String(data, offset, length);
}
@Override
public int length() {
return length;
}
@Override
public char charAt(int index) {
if (index < 0 || index >= length) {
throw new IndexOutOfBoundsException("%d not within [0, %d[".formatted(index, length));
}
return data[offset + index];
}
@Override
public Passphrase subSequence(int start, int end) {
if (start < 0 || end < 0 || end > length || start > end) {
throw new IndexOutOfBoundsException("[%d, %d[ not within [0, %d[".formatted(start, end, length));
}
return new Passphrase(Arrays.copyOfRange(data, offset + start, offset + end));
}
@Override
public boolean isDestroyed() {
return destroyed;
}
@Override
public void destroy() {
Arrays.fill(data, offset, offset + length, '\0');
destroyed = true;
}
}

View File

@@ -42,7 +42,7 @@ public enum FxmlFile {
this.ressourcePathString = ressourcePathString;
}
String getRessourcePathString() {
public String getRessourcePathString() {
return ressourcePathString;
}
}

View File

@@ -22,6 +22,10 @@ public class FxmlLoaderFactory {
this.resourceBundle = resourceBundle;
}
public static <T extends FxController> FxmlLoaderFactory forController(T controller, Function<Parent, Scene> sceneFactory, ResourceBundle resourceBundle) {
return new FxmlLoaderFactory(Map.of(controller.getClass(), () -> controller), sceneFactory, resourceBundle);
}
/**
* @return A new FXMLLoader instance
*/

View File

@@ -1,61 +0,0 @@
package org.cryptomator.ui.common;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class UserInteractionLock<E extends Enum<E>> {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private final BooleanProperty awaitingInteraction = new SimpleBooleanProperty();
private final AtomicBoolean interacted = new AtomicBoolean();
private final AtomicReference<E> state;
public UserInteractionLock(E initialValue) {
this.state = new AtomicReference<>(initialValue);
}
public synchronized void reset(E value) {
state.set(value);
interacted.set(false);
}
public void interacted(E result) {
assert Platform.isFxApplicationThread();
lock.lock();
try {
state.set(result);
interacted.set(true);
awaitingInteraction.set(false);
condition.signal();
} finally {
lock.unlock();
}
}
public E awaitInteraction() throws InterruptedException {
assert !Platform.isFxApplicationThread();
lock.lock();
try {
Platform.runLater(() -> awaitingInteraction.set(true));
while (!interacted.get()) {
condition.await();
}
return state.get();
} finally {
lock.unlock();
}
}
public ReadOnlyBooleanProperty awaitingInteraction() {
return awaitingInteraction;
}
}

View File

@@ -1,5 +1,7 @@
package org.cryptomator.ui.controls;
import org.cryptomator.common.Passphrase;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.StringProperty;
@@ -82,7 +84,7 @@ public class NiceSecurePasswordField extends StackPane {
return passwordField.textProperty();
}
public CharSequence getCharacters() {
public Passphrase getCharacters() {
return passwordField.getCharacters();
}

View File

@@ -9,6 +9,7 @@
package org.cryptomator.ui.controls;
import com.google.common.base.Strings;
import org.cryptomator.common.Passphrase;
import javafx.application.Platform;
import javafx.beans.NamedArg;
@@ -28,7 +29,6 @@ import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.TransferMode;
import java.nio.CharBuffer;
import java.text.Normalizer;
import java.text.Normalizer.Form;
import java.util.Arrays;
@@ -203,8 +203,8 @@ public class SecurePasswordField extends TextField {
* @see #wipe()
*/
@Override
public CharSequence getCharacters() {
return CharBuffer.wrap(content, 0, length);
public Passphrase getCharacters() {
return new Passphrase(content, 0, length);
}
/**

View File

@@ -26,7 +26,7 @@ abstract class KeyLoadingModule {
@Provides
@KeyLoading
@KeyLoadingScoped
static KeyLoadingStrategy provideKeyLoaderProvider(@KeyLoading Vault vault, Map<String, Provider<KeyLoadingStrategy>> strategies) {
static KeyLoadingStrategy provideKeyLoadingStrategy(@KeyLoading Vault vault, Map<String, Provider<KeyLoadingStrategy>> strategies) {
try {
String scheme = vault.getVaultConfigCache().get().getKeyId().getScheme();
var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme));

View File

@@ -0,0 +1,25 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import dagger.Subcomponent;
import javafx.scene.Scene;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
@ChooseMasterkeyFileScoped
@Subcomponent(modules = {ChooseMasterkeyFileModule.class})
public interface ChooseMasterkeyFileComponent {
@ChooseMasterkeyFileScoped
Scene chooseMasterkeyScene();
@ChooseMasterkeyFileScoped
CompletableFuture<Path> result();
@Subcomponent.Builder
interface Builder {
ChooseMasterkeyFileComponent build();
}
}

View File

@@ -0,0 +1,57 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import java.io.File;
import java.nio.file.Path;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
@ChooseMasterkeyFileScoped
public class ChooseMasterkeyFileController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(ChooseMasterkeyFileController.class);
private final Stage window;
private final CompletableFuture<Path> result;
private final ResourceBundle resourceBundle;
@Inject
public ChooseMasterkeyFileController(@KeyLoading Stage window, CompletableFuture<Path> result, ResourceBundle resourceBundle) {
this.window = window;
this.result = result;
this.resourceBundle = resourceBundle;
this.window.setOnHiding(this::windowClosed);
}
@FXML
public void cancel() {
window.close();
}
private void windowClosed(WindowEvent windowEvent) {
result.cancel(true);
}
@FXML
public void proceed() {
LOG.trace("proceed()");
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(resourceBundle.getString("unlock.chooseMasterkey.filePickerTitle"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
File masterkeyFile = fileChooser.showOpenDialog(window);
if (masterkeyFile != null) {
LOG.debug("Chose masterkey file: {}", masterkeyFile);
result.complete(masterkeyFile.toPath());
}
}
}

View File

@@ -0,0 +1,29 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import javafx.scene.Scene;
import java.nio.file.Path;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
@Module
interface ChooseMasterkeyFileModule {
@Provides
@ChooseMasterkeyFileScoped
static CompletableFuture<Path> provideResult() {
return new CompletableFuture<>();
}
@Provides
@ChooseMasterkeyFileScoped
static Scene provideChooseMasterkeyScene(ChooseMasterkeyFileController controller, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return FxmlLoaderFactory.forController(controller, sceneFactory, resourceBundle).createScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE);
}
}

View File

@@ -0,0 +1,13 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import javax.inject.Scope;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
@interface ChooseMasterkeyFileScoped {
}

View File

@@ -1,62 +0,0 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.nio.CharBuffer;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@KeyLoadingScoped
class MasterkeyFileLoadingFinisher {
private static final Logger LOG = LoggerFactory.getLogger(MasterkeyFileLoadingFinisher.class);
private final Vault vault;
private final Optional<char[]> storedPassword;
private final AtomicReference<char[]> enteredPassword;
private final AtomicBoolean shouldSavePassword;
private final KeychainManager keychain;
@Inject
MasterkeyFileLoadingFinisher(@KeyLoading Vault vault, @Named("savedPassword") Optional<char[]> storedPassword, AtomicReference<char[]> enteredPassword, @Named("savePassword") AtomicBoolean shouldSavePassword, KeychainManager keychain) {
this.vault = vault;
this.storedPassword = storedPassword;
this.enteredPassword = enteredPassword;
this.shouldSavePassword = shouldSavePassword;
this.keychain = keychain;
}
public void cleanup(boolean successfullyUnlocked) {
if (successfullyUnlocked && shouldSavePassword.get()) {
savePasswordToSystemkeychain();
}
wipePassword(storedPassword.orElse(null));
wipePassword(enteredPassword.getAndSet(null));
}
private void savePasswordToSystemkeychain() {
if (keychain.isSupported()) {
try {
keychain.storePassphrase(vault.getId(), vault.getDisplayName(), CharBuffer.wrap(enteredPassword.get()));
} catch (KeychainAccessException e) {
LOG.error("Failed to store passphrase in system keychain.", e);
}
}
}
private void wipePassword(char[] pw) {
if (pw != null) {
Arrays.fill(pw, ' ');
}
}
}

View File

@@ -8,54 +8,17 @@ import dagger.multibindings.StringKey;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Named;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.nio.file.Path;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@Module(subcomponents = {ForgetPasswordComponent.class})
public abstract class MasterkeyFileLoadingModule {
private static final Logger LOG = LoggerFactory.getLogger(MasterkeyFileLoadingModule.class);
public enum PasswordEntry {
PASSWORD_ENTERED,
CANCELED
}
public enum MasterkeyFileProvision {
MASTERKEYFILE_PROVIDED,
CANCELED
}
@Provides
@KeyLoadingScoped
static UserInteractionLock<PasswordEntry> providePasswordEntryLock() {
return new UserInteractionLock<>(null);
}
@Provides
@KeyLoadingScoped
static UserInteractionLock<MasterkeyFileProvision> provideMasterkeyFileProvisionLock() {
return new UserInteractionLock<>(null);
}
@Module(subcomponents = {ForgetPasswordComponent.class, PassphraseEntryComponent.class, ChooseMasterkeyFileComponent.class})
public interface MasterkeyFileLoadingModule {
@Provides
@Named("savedPassword")
@@ -67,67 +30,12 @@ public abstract class MasterkeyFileLoadingModule {
try {
return Optional.ofNullable(keychain.loadPassphrase(vault.getId()));
} catch (KeychainAccessException e) {
LOG.error("Failed to load entry from system keychain.", e);
LoggerFactory.getLogger(MasterkeyFileLoadingModule.class).error("Failed to load entry from system keychain.", e);
return Optional.empty();
}
}
}
@Provides
@KeyLoadingScoped
static AtomicReference<Path> provideUserProvidedMasterkeyPath() {
return new AtomicReference<>();
}
@Provides
@KeyLoadingScoped
static AtomicReference<char[]> providePassword(@Named("savedPassword") Optional<char[]> storedPassword) {
return new AtomicReference<>(storedPassword.orElse(null));
}
@Provides
@Named("savePassword")
@KeyLoadingScoped
static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional<char[]> storedPassword) {
return new AtomicBoolean(storedPassword.isPresent());
}
@Provides
@FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD)
@KeyLoadingScoped
static Scene provideUnlockScene(@KeyLoading FxmlLoaderFactory fxmlLoaders, @KeyLoading Stage window, @KeyLoading Vault v, ResourceBundle resourceBundle) {
var scene = fxmlLoaders.createScene(FxmlFile.UNLOCK_ENTER_PASSWORD);
scene.windowProperty().addListener((prop, oldVal, newVal) -> {
if (window.equals(newVal)) {
window.setTitle(String.format(resourceBundle.getString("unlock.title"), v.getDisplayName()));
}
});
return scene;
}
@Provides
@FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE)
@KeyLoadingScoped
static Scene provideUnlockSelectMasterkeyFileScene(@KeyLoading FxmlLoaderFactory fxmlLoaders, @KeyLoading Stage window, @KeyLoading Vault v, ResourceBundle resourceBundle) {
var scene = fxmlLoaders.createScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE);
scene.windowProperty().addListener((prop, oldVal, newVal) -> {
if (window.equals(newVal)) {
window.setTitle(String.format(resourceBundle.getString("unlock.chooseMasterkey.title"), v.getDisplayName()));
}
});
return scene;
}
@Binds
@IntoMap
@FxControllerKey(PassphraseEntryController.class)
abstract FxController bindUnlockController(PassphraseEntryController controller);
@Binds
@IntoMap
@FxControllerKey(SelectMasterkeyFileController.class)
abstract FxController bindUnlockSelectMasterkeyFileController(SelectMasterkeyFileController controller);
@Binds
@IntoMap
@KeyLoadingScoped

View File

@@ -1,32 +1,33 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import com.google.common.base.Preconditions;
import dagger.Lazy;
import org.cryptomator.common.Passphrase;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.common.BackupHelper;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.integrations.keychain.KeychainAccessException;
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 javax.inject.Named;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.Window;
import java.io.IOException;
import java.net.URI;
import java.nio.CharBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicReference;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
@KeyLoading
public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
@@ -36,28 +37,26 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
private final Vault vault;
private final MasterkeyFileAccess masterkeyFileAccess;
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 final PassphraseEntryComponent.Builder passphraseEntry;
private final ChooseMasterkeyFileComponent.Builder masterkeyFileChoice;
private final KeychainManager keychain;
private final ResourceBundle resourceBundle;
private boolean wrongPassword;
private Passphrase passphrase;
private boolean savePassphrase;
private boolean wrongPassphrase;
@Inject
public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAccess, @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) {
public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Stage window, @Named("savedPassword") Optional<char[]> savedPassphrase, PassphraseEntryComponent.Builder passphraseEntry, ChooseMasterkeyFileComponent.Builder masterkeyFileChoice, KeychainManager keychain, ResourceBundle resourceBundle) {
this.vault = vault;
this.masterkeyFileAccess = masterkeyFileAccess;
this.window = window;
this.passphraseEntryScene = passphraseEntryScene;
this.selectMasterkeyFileScene = selectMasterkeyFileScene;
this.passwordEntryLock = passwordEntryLock;
this.masterkeyFileProvisionLock = masterkeyFileProvisionLock;
this.password = password;
this.filePath = filePath;
this.finisher = finisher;
this.passphraseEntry = passphraseEntry;
this.masterkeyFileChoice = masterkeyFileChoice;
this.keychain = keychain;
this.resourceBundle = resourceBundle;
this.passphrase = savedPassphrase.map(Passphrase::new).orElse(null);
this.savePassphrase = savedPassphrase.isPresent();
}
@Override
@@ -66,9 +65,11 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
try {
Path filePath = vault.getPath().resolve(keyId.getSchemeSpecificPart());
if (!Files.exists(filePath)) {
filePath = getAlternateMasterkeyFilePath();
filePath = askUserForMasterkeyFilePath();
}
if (passphrase == null) {
askForPassphrase();
}
CharSequence passphrase = getPassphrase();
var masterkey = masterkeyFileAccess.load(filePath, passphrase);
//backup
if (filePath.startsWith(vault.getPath())) {
@@ -90,8 +91,9 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
@Override
public boolean recoverFromException(MasterkeyLoadingFailedException exception) {
if (exception instanceof InvalidPassphraseException) {
this.wrongPassword = true;
password.set(null);
this.wrongPassphrase = true;
passphrase.destroy();
this.passphrase = null;
return true; // reattempting key load
} else {
return false; // nothing we can do
@@ -100,23 +102,29 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
@Override
public void cleanup(boolean unlockedSuccessfully) {
finisher.cleanup(unlockedSuccessfully);
}
private Path getAlternateMasterkeyFilePath() throws UnlockCancelledException, InterruptedException {
if (filePath.get() == null) {
return switch (askUserForMasterkeyFilePath()) {
case MASTERKEYFILE_PROVIDED -> filePath.get();
case CANCELED -> throw new UnlockCancelledException("Choosing masterkey file cancelled.");
};
} else {
return filePath.get();
if (unlockedSuccessfully && savePassphrase) {
savePasswordToSystemkeychain(passphrase);
}
if (passphrase != null) {
passphrase.destroy();
}
}
private MasterkeyFileLoadingModule.MasterkeyFileProvision askUserForMasterkeyFilePath() throws InterruptedException {
private void savePasswordToSystemkeychain(Passphrase passphrase) {
if (keychain.isSupported()) {
try {
keychain.storePassphrase(vault.getId(), vault.getDisplayName(), passphrase);
} catch (KeychainAccessException e) {
LOG.error("Failed to store passphrase in system keychain.", e);
}
}
}
private Path askUserForMasterkeyFilePath() throws InterruptedException {
var comp = masterkeyFileChoice.build();
Platform.runLater(() -> {
window.setScene(selectMasterkeyFileScene.get());
window.setScene(comp.chooseMasterkeyScene());
window.setTitle(resourceBundle.getString("unlock.chooseMasterkey.title").formatted(vault.getDisplayName()));
window.show();
Window owner = window.getOwner();
if (owner != null) {
@@ -126,24 +134,20 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
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());
try {
return comp.result().get();
} catch (CancellationException e) {
throw new UnlockCancelledException("Choosing masterkey file cancelled.");
} catch (ExecutionException e) {
throw new MasterkeyLoadingFailedException("Failed to select masterkey file.", e);
}
}
private MasterkeyFileLoadingModule.PasswordEntry askForPassphrase() throws InterruptedException {
private void askForPassphrase() throws InterruptedException {
var comp = passphraseEntry.savedPassword(passphrase).build();
Platform.runLater(() -> {
window.setScene(passphraseEntryScene.get());
window.setScene(comp.passphraseEntryScene());
window.setTitle(resourceBundle.getString("unlock.title").formatted(vault.getDisplayName()));
window.show();
Window owner = window.getOwner();
if (owner != null) {
@@ -152,11 +156,19 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
} else {
window.centerOnScreen();
}
if (wrongPassword) {
if (wrongPassphrase) {
Animations.createShakeWindowAnimation(window).play();
}
});
return passwordEntryLock.awaitInteraction();
try {
var result = comp.result().get();
this.passphrase = result.passphrase();
this.savePassphrase = result.savePassphrase();
} catch (CancellationException e) {
throw new UnlockCancelledException("Password entry cancelled.");
} catch (ExecutionException e) {
throw new MasterkeyLoadingFailedException("Failed to ask for password.", e);
}
}
}

View File

@@ -0,0 +1,31 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import dagger.BindsInstance;
import dagger.Subcomponent;
import org.cryptomator.common.Nullable;
import org.cryptomator.common.Passphrase;
import javax.inject.Named;
import javafx.scene.Scene;
import java.util.concurrent.CompletableFuture;
@PassphraseEntryScoped
@Subcomponent(modules = {PassphraseEntryModule.class})
public interface PassphraseEntryComponent {
@PassphraseEntryScoped
Scene passphraseEntryScene();
@PassphraseEntryScoped
CompletableFuture<PassphraseEntryResult> result();
@Subcomponent.Builder
interface Builder {
@BindsInstance
PassphraseEntryComponent.Builder savedPassword(@Nullable @Named("savedPassword") Passphrase savedPassword);
PassphraseEntryComponent build();
}
}

View File

@@ -1,16 +1,14 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import org.cryptomator.common.Nullable;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.common.Passphrase;
import org.cryptomator.ui.common.WeakBindings;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule.PasswordEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -21,8 +19,8 @@ import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.BooleanProperty;
@@ -37,33 +35,27 @@ import javafx.scene.transform.Translate;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import javafx.util.Duration;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.CompletableFuture;
@KeyLoadingScoped
@PassphraseEntryScoped
public class PassphraseEntryController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(PassphraseEntryController.class);
private final Stage window;
private final Vault vault;
private final AtomicReference<char[]> password;
private final AtomicBoolean savePassword;
private final Optional<char[]> savedPassword;
private final UserInteractionLock<PasswordEntry> passwordEntryLock;
private final CompletableFuture<PassphraseEntryResult> result;
private final Passphrase savedPassword;
private final ForgetPasswordComponent.Builder forgetPassword;
private final KeychainManager keychain;
private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay;
private final BooleanBinding userInteractionDisabled;
private final BooleanProperty unlockButtonDisabled;
private final StringBinding vaultName;
private final BooleanProperty unlockInProgress = new SimpleBooleanProperty();
private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, unlockInProgress);
private final BooleanProperty unlockButtonDisabled = new SimpleBooleanProperty();
/* FXML */
public NiceSecurePasswordField passwordField;
public CheckBox savePasswordCheckbox;
public FontAwesome5IconView unlockInProgressView;
public ImageView face;
public ImageView leftArm;
public ImageView rightArm;
@@ -72,29 +64,25 @@ public class PassphraseEntryController implements FxController {
public Animation unlockAnimation;
@Inject
public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<PasswordEntry> passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) {
public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, CompletableFuture<PassphraseEntryResult> result, @Nullable @Named("savedPassword") Passphrase savedPassword, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) {
this.window = window;
this.vault = vault;
this.password = password;
this.savePassword = savePassword;
this.result = result;
this.savedPassword = savedPassword;
this.passwordEntryLock = passwordEntryLock;
this.forgetPassword = forgetPassword;
this.keychain = keychain;
this.unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, passwordEntryLock.awaitingInteraction());
this.userInteractionDisabled = passwordEntryLock.awaitingInteraction().not();
this.unlockButtonDisabled = new SimpleBooleanProperty();
this.vaultName = WeakBindings.bindString(vault.displayNameProperty());
this.window.setOnHiding(this::windowClosed);
window.setOnHiding(this::windowClosed);
result.whenCompleteAsync((r, t) -> unlockInProgress.set(false), Platform::runLater);
}
@FXML
public void initialize() {
savePasswordCheckbox.setSelected(savedPassword.isPresent());
if (password.get() != null) {
passwordField.setPassword(password.get());
if (savedPassword != null) {
savePasswordCheckbox.setSelected(true);
passwordField.setPassword(savedPassword);
}
unlockButtonDisabled.bind(userInteractionDisabled.or(passwordField.textProperty().isEmpty()));
unlockButtonDisabled.bind(unlockInProgress.or(passwordField.textProperty().isEmpty()));
var leftArmTranslation = new Translate(24, 0);
var leftArmRotation = new Rotate(60, 16, 30, 0);
@@ -132,7 +120,7 @@ public class PassphraseEntryController implements FxController {
new KeyFrame(Duration.millis(1000), faceVisible) //
);
passwordEntryLock.awaitingInteraction().addListener(observable -> stopUnlockAnimation());
result.whenCompleteAsync((r, t) -> stopUnlockAnimation());
}
@FXML
@@ -141,26 +129,17 @@ public class PassphraseEntryController implements FxController {
}
private void windowClosed(WindowEvent windowEvent) {
// if not already interacted, mark this workflow as cancelled:
if (passwordEntryLock.awaitingInteraction().get()) {
LOG.debug("Unlock canceled by user.");
passwordEntryLock.interacted(PasswordEntry.CANCELED);
}
LOG.debug("Unlock canceled by user.");
result.cancel(true);
}
@FXML
public void unlock() {
LOG.trace("UnlockController.unlock()");
unlockInProgress.set(true);
CharSequence pwFieldContents = passwordField.getCharacters();
char[] newPw = new char[pwFieldContents.length()];
for (int i = 0; i < pwFieldContents.length(); i++) {
newPw[i] = pwFieldContents.charAt(i);
}
char[] oldPw = password.getAndSet(newPw);
if (oldPw != null) {
Arrays.fill(oldPw, ' ');
}
passwordEntryLock.interacted(PasswordEntry.PASSWORD_ENTERED);
Passphrase pw = Passphrase.copyOf(pwFieldContents);
result.complete(new PassphraseEntryResult(pw, savePasswordCheckbox.isSelected()));
startUnlockAnimation();
}
@@ -184,8 +163,7 @@ public class PassphraseEntryController implements FxController {
@FXML
private void didClickSavePasswordCheckbox() {
savePassword.set(savePasswordCheckbox.isSelected());
if (!savePasswordCheckbox.isSelected() && savedPassword.isPresent()) {
if (!savePasswordCheckbox.isSelected() && savedPassword != null) {
forgetPassword.vault(vault).owner(window).build().showForgetPassword().thenAccept(forgotten -> savePasswordCheckbox.setSelected(!forgotten));
}
}
@@ -205,15 +183,15 @@ public class PassphraseEntryController implements FxController {
}
public ContentDisplay getUnlockButtonContentDisplay() {
return passwordEntryLock.awaitingInteraction().get() ? ContentDisplay.TEXT_ONLY : ContentDisplay.LEFT;
return unlockInProgress.get() ? ContentDisplay.LEFT : ContentDisplay.TEXT_ONLY;
}
public BooleanBinding userInteractionDisabledProperty() {
return userInteractionDisabled;
public ReadOnlyBooleanProperty userInteractionDisabledProperty() {
return unlockInProgress;
}
public boolean isUserInteractionDisabled() {
return userInteractionDisabled.get();
return unlockInProgress.get();
}
public ReadOnlyBooleanProperty unlockButtonDisabledProperty() {
@@ -227,4 +205,6 @@ public class PassphraseEntryController implements FxController {
public boolean isKeychainAccessAvailable() {
return keychain.isSupported();
}
}

View File

@@ -0,0 +1,28 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import javafx.scene.Scene;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
@Module
interface PassphraseEntryModule {
@Provides
@PassphraseEntryScoped
static CompletableFuture<PassphraseEntryResult> provideResult() {
return new CompletableFuture<>();
}
@Provides
@PassphraseEntryScoped
static Scene provideUnlockScene(PassphraseEntryController controller, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return FxmlLoaderFactory.forController(controller, sceneFactory, resourceBundle).createScene(FxmlFile.UNLOCK_ENTER_PASSWORD);
}
}

View File

@@ -0,0 +1,8 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import org.cryptomator.common.Passphrase;
// TODO: change to package-private, as soon as this works for Dagger -.-
public record PassphraseEntryResult(Passphrase passphrase, boolean savePassphrase) {
}

View File

@@ -0,0 +1,13 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import javax.inject.Scope;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
@interface PassphraseEntryScoped {
}

View File

@@ -1,67 +0,0 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule.MasterkeyFileProvision;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import java.io.File;
import java.nio.file.Path;
import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicReference;
@KeyLoadingScoped
public class SelectMasterkeyFileController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(SelectMasterkeyFileController.class);
private final Stage window;
private final AtomicReference<Path> masterkeyPath;
private final UserInteractionLock<MasterkeyFileProvision> masterkeyFileProvisionLock;
private final ResourceBundle resourceBundle;
@Inject
public SelectMasterkeyFileController(@KeyLoading Stage window, AtomicReference<Path> masterkeyPath, UserInteractionLock<MasterkeyFileProvision> masterkeyFileProvisionLock, ResourceBundle resourceBundle) {
this.window = window;
this.masterkeyPath = masterkeyPath;
this.masterkeyFileProvisionLock = masterkeyFileProvisionLock;
this.resourceBundle = resourceBundle;
this.window.setOnHiding(this::windowClosed);
}
@FXML
public void cancel() {
window.close();
}
private void windowClosed(WindowEvent windowEvent) {
// if not already interacted, mark this workflow as cancelled:
if (masterkeyFileProvisionLock.awaitingInteraction().get()) {
LOG.debug("Unlock canceled by user.");
masterkeyFileProvisionLock.interacted(MasterkeyFileProvision.CANCELED);
}
}
@FXML
public void proceed() {
LOG.trace("proceed()");
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(resourceBundle.getString("unlock.chooseMasterkey.filePickerTitle"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
File masterkeyFile = fileChooser.showOpenDialog(window);
if (masterkeyFile != null) {
LOG.debug("Chose masterkey file: {}", masterkeyFile);
masterkeyPath.set(masterkeyFile.toPath());
masterkeyFileProvisionLock.interacted(MasterkeyFileProvision.MASTERKEYFILE_PROVIDED);
}
}
}

View File

@@ -2,56 +2,48 @@ package org.cryptomator.ui.lock;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.UserInteractionLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
@LockScoped
public class LockForcedController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(LockForcedController.class);
private final Stage window;
private final Vault vault;
private final UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock;
private final AtomicReference<CompletableFuture<Boolean>> forceRetryDecision;
@Inject
public LockForcedController(@LockWindow Stage window, @LockWindow Vault vault, UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock) {
public LockForcedController(@LockWindow Stage window, @LockWindow Vault vault, AtomicReference<CompletableFuture<Boolean>> forceRetryDecision) {
this.window = window;
this.vault = vault;
this.forceLockDecisionLock = forceLockDecisionLock;
this.forceRetryDecision = forceRetryDecision;
this.window.setOnHiding(this::windowClosed);
}
@FXML
public void cancel() {
forceLockDecisionLock.interacted(LockModule.ForceLockDecision.CANCEL);
window.close();
}
@FXML
public void retry() {
forceLockDecisionLock.interacted(LockModule.ForceLockDecision.RETRY);
forceRetryDecision.get().complete(false);
window.close();
}
@FXML
public void force() {
forceLockDecisionLock.interacted(LockModule.ForceLockDecision.FORCE);
forceRetryDecision.get().complete(true);
window.close();
}
private void windowClosed(WindowEvent windowEvent) {
// if not already interacted, set the decision to CANCEL
if (forceLockDecisionLock.awaitingInteraction().get()) {
LOG.debug("Lock canceled in force-lock-phase by user.");
forceLockDecisionLock.interacted(LockModule.ForceLockDecision.CANCEL);
}
forceRetryDecision.get().cancel(true);
}
// ----- Getter & Setter -----

View File

@@ -6,13 +6,12 @@ import dagger.Provides;
import dagger.multibindings.IntoMap;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.common.UserInteractionLock;
import javax.inject.Named;
import javax.inject.Provider;
@@ -22,20 +21,16 @@ import javafx.stage.Stage;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
@Module
abstract class LockModule {
enum ForceLockDecision {
CANCEL,
RETRY,
FORCE;
}
@Provides
@LockScoped
static UserInteractionLock<LockModule.ForceLockDecision> provideForceLockDecisionLock() {
return new UserInteractionLock<>(null);
static AtomicReference<CompletableFuture<Boolean>> provideForceRetryDecisionRef() {
return new AtomicReference<>();
}
@Provides

View File

@@ -8,7 +8,6 @@ import org.cryptomator.common.vaults.Volume;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.UserInteractionLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -18,6 +17,10 @@ import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.Window;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicReference;
/**
* The sequence of actions performed and checked during lock of a vault.
@@ -34,43 +37,48 @@ public class LockWorkflow extends Task<Void> {
private final Stage lockWindow;
private final Vault vault;
private final UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock;
private final AtomicReference<CompletableFuture<Boolean>> forceRetryDecision;
private final Lazy<Scene> lockForcedScene;
private final Lazy<Scene> lockFailedScene;
private final ErrorComponent.Builder errorComponent;
@Inject
public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy<Scene> lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy<Scene> lockFailedScene, ErrorComponent.Builder errorComponent) {
public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, AtomicReference<CompletableFuture<Boolean>> forceRetryDecision, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy<Scene> lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy<Scene> lockFailedScene, ErrorComponent.Builder errorComponent) {
this.lockWindow = lockWindow;
this.vault = vault;
this.forceLockDecisionLock = forceLockDecisionLock;
this.forceRetryDecision = forceRetryDecision;
this.lockForcedScene = lockForcedScene;
this.lockFailedScene = lockFailedScene;
this.errorComponent = errorComponent;
}
@Override
protected Void call() throws Volume.VolumeException, InterruptedException, LockNotCompletedException {
protected Void call() throws Volume.VolumeException, InterruptedException, LockNotCompletedException, ExecutionException {
lock(false);
return null;
}
private void lock(boolean forced) throws InterruptedException {
private void lock(boolean forced) throws InterruptedException, ExecutionException {
try {
vault.lock(forced);
} catch (Volume.VolumeException | LockNotCompletedException e) {
LOG.info("Locking {} failed (forced: {}).", vault.getDisplayName(), forced, e);
var decision = askUserForAction();
switch (decision) {
case RETRY -> lock(false);
case FORCE -> lock(true);
case CANCEL -> cancel(false);
}
retryOrCancel();
}
}
private LockModule.ForceLockDecision askUserForAction() throws InterruptedException {
forceLockDecisionLock.reset(null);
private void retryOrCancel() throws ExecutionException, InterruptedException {
try {
boolean forced = askWhetherToUseTheForce().get();
lock(forced);
} catch (CancellationException e) {
cancel(false);
}
}
private CompletableFuture<Boolean> askWhetherToUseTheForce() {
var decision = new CompletableFuture<Boolean>();
forceRetryDecision.set(decision);
// show forcedLock dialogue ...
Platform.runLater(() -> {
lockWindow.setScene(lockForcedScene.get());
@@ -83,8 +91,7 @@ public class LockWorkflow extends Task<Void> {
lockWindow.centerOnScreen();
}
});
// ... and wait for answer
return forceLockDecisionLock.awaitInteraction();
return decision;
}
@Override

View File

@@ -11,7 +11,7 @@
<?import javafx.scene.shape.Circle?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.keyloading.masterkeyfile.SelectMasterkeyFileController"
fx:controller="org.cryptomator.ui.keyloading.masterkeyfile.ChooseMasterkeyFileController"
minWidth="400"
maxWidth="400"
minHeight="145"
@@ -34,7 +34,7 @@
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel"/>
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#proceed"/>
<Button text="%unlock.chooseMasterkey.chooseBtn" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#proceed"/>
</buttons>
</ButtonBar>
</VBox>

View File

@@ -106,6 +106,7 @@ unlock.unlockBtn=Unlock
## Select
unlock.chooseMasterkey.title=Select Masterkey of "%s"
unlock.chooseMasterkey.prompt=Could not find the masterkey file for this vault at its expected location. Please choose the key file manually.
unlock.chooseMasterkey.chooseBtn=Choose…
unlock.chooseMasterkey.filePickerTitle=Select Masterkey File
## Success
unlock.success.message=Unlocked "%s" successfully! Your vault is now accessible via its virtual drive.

View File

@@ -0,0 +1,129 @@
package org.cryptomator.common;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
public class PassphraseTest {
@ParameterizedTest
@CsvSource(value = {
"-1, 0",
"0, -1",
"0, 10",
"10, 0",
"10, 10"
})
public void testInvalidConstructorArgs(int offset, int length) {
char[] data = "test".toCharArray();
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> {
new Passphrase(data, offset, length);
});
}
@ParameterizedTest
@CsvSource(value = {
"0, 4",
"0, 0",
"0, 1",
"1, 1",
"2, 2"
})
public void testValidConstructorArgs(int offset, int length) {
char[] data = "test".toCharArray();
var pw = new Passphrase(data, offset, length);
Assertions.assertEquals(length, pw.length());
Assertions.assertEquals("test".substring(offset, offset + length), pw.toString());
}
@Nested
public class InstanceMethods {
private Passphrase pw1;
private Passphrase pw2;
@BeforeEach
public void setup() {
char[] foo = "test test".toCharArray();
pw1 = new Passphrase(foo, 5, 4);
pw2 = Passphrase.copyOf("test");
}
@Test
public void testToString() {
Assertions.assertEquals("test", pw1.toString());
Assertions.assertEquals("test", pw2.toString());
}
@Test
public void testEquals() {
Assertions.assertEquals(pw1, pw2);
}
@Test
public void testHashcode() {
Assertions.assertEquals(pw1.hashCode(), pw2.hashCode());
}
@Test
public void testLength() {
Assertions.assertEquals(4, pw1.length());
Assertions.assertEquals(4, pw2.length());
}
@Test
public void testCharAt() {
Assertions.assertEquals('s', pw1.charAt(2));
}
@ParameterizedTest
@ValueSource(ints = {-1, 4, 5})
public void testInvalidCharAt(int idx) {
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> pw1.charAt(idx));
}
@ParameterizedTest
@ValueSource(ints = {0, 1, 2, 3})
public void testValidCharAt(int idx) {
Assertions.assertEquals("test".charAt(idx), pw1.charAt(idx));
}
@ParameterizedTest
@CsvSource(value = {
"-1, 0",
"0, -1",
"-1, -1",
"0, 5",
"3, 2"
})
public void testInvalidSubSequence(int start, int end) {
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> pw1.subSequence(start, end));
}
@ParameterizedTest
@CsvSource(value = {
"0, 4",
"1, 4",
"0, 2",
"2, 4",
"4, 4",
})
public void testValidSubSequence(int start, int end) {
Assertions.assertEquals("test".substring(start, end), pw1.subSequence(start, end).toString());
}
@Test
public void testDestroy() {
pw2.destroy();
Assertions.assertFalse(pw1.isDestroyed());
Assertions.assertTrue(pw2.isDestroyed());
Assertions.assertNotEquals(pw1, pw2);
}
}
}