mirror of
https://github.com/cryptomator/cryptomator.git
synced 2024-11-23 03:59:51 +00:00
Merge branch 'develop' into feature/gtk2-launcher
This commit is contained in:
commit
67e1626de0
6
.github/workflows/appimage.yml
vendored
6
.github/workflows/appimage.yml
vendored
@ -149,6 +149,6 @@ jobs:
|
||||
fail_on_unmatched_files: true
|
||||
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
|
||||
files: |
|
||||
*.AppImage
|
||||
*.zsync
|
||||
*.asc
|
||||
cryptomator-*.AppImage
|
||||
cryptomator-*.zsync
|
||||
cryptomator-*.asc
|
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@ -32,6 +32,7 @@ jobs:
|
||||
restore-keys: ${{ runner.os }}-sonar
|
||||
- name: Build and Test
|
||||
run: >
|
||||
xvfb-run
|
||||
mvn -B verify
|
||||
jacoco:report
|
||||
org.sonarsource.scanner.maven:sonar-maven-plugin:sonar
|
||||
|
4
.github/workflows/mac-dmg.yml
vendored
4
.github/workflows/mac-dmg.yml
vendored
@ -226,7 +226,7 @@ jobs:
|
||||
fail_on_unmatched_files: true
|
||||
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
|
||||
files: |
|
||||
*.dmg
|
||||
*.asc
|
||||
Cryptomator-*.dmg
|
||||
Cryptomator-*.asc
|
||||
|
||||
|
||||
|
25
.github/workflows/win-exe.yml
vendored
25
.github/workflows/win-exe.yml
vendored
@ -217,7 +217,26 @@ jobs:
|
||||
run: >
|
||||
"${WIX}/bin/light.exe" -b dist/win/ dist/win/bundle/bundleWithWinfsp.wixobj
|
||||
-ext WixBalExtension
|
||||
-out installer/Cryptomator.exe
|
||||
-out installer/unsigned/Cryptomator.exe
|
||||
- name: Detach burn engine in preparation to sign
|
||||
run: >
|
||||
"${WIX}/bin/insignia.exe"
|
||||
-ib installer/unsigned/Cryptomator.exe
|
||||
-o tmp/engine.exe
|
||||
- name: Codesign burn engine
|
||||
uses: skymatic/code-sign-action@v1
|
||||
with:
|
||||
certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }}
|
||||
password: ${{ secrets.WIN_CODESIGN_P12_PW }}
|
||||
certificatesha1: FF52240075AD7D14AF25629FDF69635357C7D14B
|
||||
description: Cryptomator Installer
|
||||
timestampUrl: 'http://timestamp.digicert.com'
|
||||
folder: tmp
|
||||
- name: Reattach signed burn engine to installer
|
||||
run : >
|
||||
"${WIX}/bin/insignia.exe"
|
||||
-ab tmp/engine.exe installer/unsigned/Cryptomator.exe
|
||||
-o installer/Cryptomator.exe
|
||||
- name: Codesign EXE
|
||||
uses: skymatic/code-sign-action@v1
|
||||
with:
|
||||
@ -251,5 +270,5 @@ jobs:
|
||||
fail_on_unmatched_files: true
|
||||
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
|
||||
files: |
|
||||
*.exe
|
||||
*.asc
|
||||
Cryptomator-*.exe
|
||||
Cryptomator-*.asc
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -21,7 +21,6 @@ pom.xml.versionsBackup
|
||||
.idea/dictionaries/**
|
||||
!.idea/dictionaries/dict_*
|
||||
.idea/compiler.xml
|
||||
.idea/encodings.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/uiDesigner.xml
|
||||
.idea/**/libraries/
|
||||
|
8
.idea/encodings.xml
Normal file
8
.idea/encodings.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" defaultCharsetForPropertiesFiles="UTF-8">
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||
<file url="PROJECT" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
3
dist/mac/dmg/.gitignore
vendored
3
dist/mac/dmg/.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
# created during build
|
||||
Cryptomator.app/
|
||||
runtime/
|
||||
dmg/
|
||||
*.dmg
|
||||
*.dmg
|
18
pom.xml
18
pom.xml
@ -34,23 +34,23 @@
|
||||
<cryptomator.integrations.linux.version>1.0.1</cryptomator.integrations.linux.version>
|
||||
<cryptomator.fuse.version>1.3.3</cryptomator.fuse.version>
|
||||
<cryptomator.dokany.version>1.3.3</cryptomator.dokany.version>
|
||||
<cryptomator.webdav.version>1.2.6</cryptomator.webdav.version>
|
||||
<cryptomator.webdav.version>1.2.7</cryptomator.webdav.version>
|
||||
|
||||
<!-- 3rd party dependencies -->
|
||||
<javafx.version>17.0.2</javafx.version>
|
||||
<javafx.version>18</javafx.version>
|
||||
<commons-lang3.version>3.12.0</commons-lang3.version>
|
||||
<jwt.version>3.18.3</jwt.version>
|
||||
<jwt.version>3.19.0</jwt.version>
|
||||
<easybind.version>2.2</easybind.version>
|
||||
<guava.version>31.0-jre</guava.version>
|
||||
<dagger.version>2.40.3</dagger.version>
|
||||
<gson.version>2.8.9</gson.version>
|
||||
<guava.version>31.1-jre</guava.version>
|
||||
<dagger.version>2.41</dagger.version>
|
||||
<gson.version>2.9.0</gson.version>
|
||||
<zxcvbn.version>1.5.2</zxcvbn.version>
|
||||
<slf4j.version>1.7.32</slf4j.version>
|
||||
<logback.version>1.2.9</logback.version>
|
||||
<slf4j.version>1.7.36</slf4j.version>
|
||||
<logback.version>1.2.11</logback.version>
|
||||
|
||||
<!-- test dependencies -->
|
||||
<junit.jupiter.version>5.8.1</junit.jupiter.version>
|
||||
<mockito.version>3.12.4</mockito.version>
|
||||
<mockito.version>4.4.0</mockito.version>
|
||||
<hamcrest.version>2.2</hamcrest.version>
|
||||
|
||||
<!-- build plugin dependencies -->
|
||||
|
108
src/main/java/org/cryptomator/common/Passphrase.java
Normal file
108
src/main/java/org/cryptomator/common/Passphrase.java
Normal 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;
|
||||
}
|
||||
}
|
@ -42,7 +42,7 @@ public enum FxmlFile {
|
||||
this.ressourcePathString = ressourcePathString;
|
||||
}
|
||||
|
||||
String getRessourcePathString() {
|
||||
public String getRessourcePathString() {
|
||||
return ressourcePathString;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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));
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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 {
|
||||
|
||||
}
|
@ -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, ' ');
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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) {
|
||||
|
||||
}
|
@ -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 {
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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 -----
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -91,6 +91,8 @@ class TrayMenuController {
|
||||
unlockItem.addActionListener(createActionListenerForVault(vault, this::unlockVault));
|
||||
submenu.add(unlockItem);
|
||||
} else if (vault.isUnlocked()) {
|
||||
submenu.setLabel("* ".concat(submenu.getLabel()));
|
||||
|
||||
MenuItem lockItem = new MenuItem(resourceBundle.getString("traymenu.vault.lock"));
|
||||
lockItem.addActionListener(createActionListenerForVault(vault, this::lockVault));
|
||||
submenu.add(lockItem);
|
||||
|
@ -24,6 +24,7 @@ import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.StringConverter;
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ResourceBundle;
|
||||
@ -122,8 +123,11 @@ public class MountOptionsController implements FxController {
|
||||
DirectoryChooser directoryChooser = new DirectoryChooser();
|
||||
directoryChooser.setTitle(resourceBundle.getString("vaultOptions.mount.mountPoint.directoryPickerTitle"));
|
||||
try {
|
||||
var initialDir = vault.getVaultSettings().getCustomMountPath().orElse(System.getProperty("user.home"));
|
||||
directoryChooser.setInitialDirectory(Path.of(initialDir).toFile());
|
||||
var initialDir = Path.of(vault.getVaultSettings().getCustomMountPath().orElse(System.getProperty("user.home")));
|
||||
|
||||
if(Files.exists(initialDir)) {
|
||||
directoryChooser.setInitialDirectory(initialDir.toFile());
|
||||
}
|
||||
} catch (InvalidPathException e) {
|
||||
// no-op
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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.
|
||||
|
@ -17,9 +17,9 @@ Cryptomator uses 40 third-party dependencies under the following licenses:
|
||||
- jnr-a64asm (com.github.jnr:jnr-a64asm:1.0.0 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-a64asm)
|
||||
- jnr-constants (com.github.jnr:jnr-constants:0.10.2 - http://github.com/jnr/jnr-constants)
|
||||
- jnr-ffi (com.github.jnr:jnr-ffi:2.2.7 - http://github.com/jnr/jnr-ffi)
|
||||
- Dagger (com.google.dagger:dagger:2.40.3 - https://github.com/google/dagger)
|
||||
- Dagger (com.google.dagger:dagger:2.41 - https://github.com/google/dagger)
|
||||
- Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.1 - https://github.com/google/guava/failureaccess)
|
||||
- Guava: Google Core Libraries for Java (com.google.guava:guava:31.0-jre - https://github.com/google/guava)
|
||||
- Guava: Google Core Libraries for Java (com.google.guava:guava:31.1-jre - https://github.com/google/guava)
|
||||
- Apache Commons CLI (commons-cli:commons-cli:1.4 - http://commons.apache.org/proper/commons-cli/)
|
||||
- javax.inject (javax.inject:javax.inject:1 - http://code.google.com/p/atinject/)
|
||||
- Apache Commons Lang (org.apache.commons:commons-lang3:3.12.0 - https://commons.apache.org/proper/commons-lang/)
|
||||
@ -33,7 +33,7 @@ Cryptomator uses 40 third-party dependencies under the following licenses:
|
||||
- Jetty :: Utilities (org.eclipse.jetty:jetty-util:10.0.6 - https://eclipse.org/jetty/jetty-util)
|
||||
- Jetty :: Servlet API and Schemas for JPMS and OSGi (org.eclipse.jetty.toolchain:jetty-servlet-api:4.0.6 - https://eclipse.org/jetty/jetty-servlet-api)
|
||||
Apache-2.0:
|
||||
- Gson (com.google.code.gson:gson:2.8.9 - https://github.com/google/gson/gson)
|
||||
- Gson (com.google.code.gson:gson:2.9.0 - https://github.com/google/gson/gson)
|
||||
- Java Native Access (net.java.dev.jna:jna:5.9.0 - https://github.com/java-native-access/jna)
|
||||
- Java Native Access Platform (net.java.dev.jna:jna-platform:5.9.0 - https://github.com/java-native-access/jna)
|
||||
BSD-3-Clause:
|
||||
@ -52,31 +52,31 @@ Cryptomator uses 40 third-party dependencies under the following licenses:
|
||||
- Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:10.0.6 - https://eclipse.org/jetty/jetty-servlet)
|
||||
- Jetty :: Utilities (org.eclipse.jetty:jetty-util:10.0.6 - https://eclipse.org/jetty/jetty-util)
|
||||
Eclipse Public License - v 1.0:
|
||||
- Logback Classic Module (ch.qos.logback:logback-classic:1.2.9 - http://logback.qos.ch/logback-classic)
|
||||
- Logback Core Module (ch.qos.logback:logback-core:1.2.9 - http://logback.qos.ch/logback-core)
|
||||
- Logback Classic Module (ch.qos.logback:logback-classic:1.2.11 - http://logback.qos.ch/logback-classic)
|
||||
- Logback Core Module (ch.qos.logback:logback-core:1.2.11 - http://logback.qos.ch/logback-core)
|
||||
Eclipse Public License - v 2.0:
|
||||
- jnr-posix (com.github.jnr:jnr-posix:3.1.10 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix)
|
||||
GNU Lesser General Public License:
|
||||
- Logback Classic Module (ch.qos.logback:logback-classic:1.2.9 - http://logback.qos.ch/logback-classic)
|
||||
- Logback Core Module (ch.qos.logback:logback-core:1.2.9 - http://logback.qos.ch/logback-core)
|
||||
- Logback Classic Module (ch.qos.logback:logback-classic:1.2.11 - http://logback.qos.ch/logback-classic)
|
||||
- Logback Core Module (ch.qos.logback:logback-core:1.2.11 - http://logback.qos.ch/logback-core)
|
||||
GPLv2:
|
||||
- jnr-posix (com.github.jnr:jnr-posix:3.1.10 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix)
|
||||
GPLv2+CE:
|
||||
- javafx-base (org.openjfx:javafx-base:17.0.2 - https://openjdk.java.net/projects/openjfx/javafx-base/)
|
||||
- javafx-controls (org.openjfx:javafx-controls:17.0.2 - https://openjdk.java.net/projects/openjfx/javafx-controls/)
|
||||
- javafx-fxml (org.openjfx:javafx-fxml:17.0.2 - https://openjdk.java.net/projects/openjfx/javafx-fxml/)
|
||||
- javafx-graphics (org.openjfx:javafx-graphics:17.0.2 - https://openjdk.java.net/projects/openjfx/javafx-graphics/)
|
||||
- javafx-base (org.openjfx:javafx-base:18 - https://openjdk.java.net/projects/openjfx/javafx-base/)
|
||||
- javafx-controls (org.openjfx:javafx-controls:18 - https://openjdk.java.net/projects/openjfx/javafx-controls/)
|
||||
- javafx-fxml (org.openjfx:javafx-fxml:18 - https://openjdk.java.net/projects/openjfx/javafx-fxml/)
|
||||
- javafx-graphics (org.openjfx:javafx-graphics:18 - https://openjdk.java.net/projects/openjfx/javafx-graphics/)
|
||||
LGPL 2.1:
|
||||
- jnr-posix (com.github.jnr:jnr-posix:3.1.10 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix)
|
||||
LGPL-2.1-or-later:
|
||||
- Java Native Access (net.java.dev.jna:jna:5.9.0 - https://github.com/java-native-access/jna)
|
||||
- Java Native Access Platform (net.java.dev.jna:jna-platform:5.9.0 - https://github.com/java-native-access/jna)
|
||||
MIT License:
|
||||
- java jwt (com.auth0:java-jwt:3.18.2 - https://github.com/auth0/java-jwt)
|
||||
- java jwt (com.auth0:java-jwt:3.19.0 - https://github.com/auth0/java-jwt)
|
||||
- jnr-x86asm (com.github.jnr:jnr-x86asm:1.0.2 - http://github.com/jnr/jnr-x86asm)
|
||||
- jnr-fuse (com.github.serceman:jnr-fuse:0.5.7 - https://github.com/SerCeMan/jnr-fuse)
|
||||
- zxcvbn4j (com.nulab-inc:zxcvbn:1.5.2 - https://github.com/nulab/zxcvbn4j)
|
||||
- SLF4J API Module (org.slf4j:slf4j-api:1.7.32 - http://www.slf4j.org)
|
||||
- SLF4J API Module (org.slf4j:slf4j-api:1.7.36 - http://www.slf4j.org)
|
||||
The BSD 2-Clause License:
|
||||
- EasyBind (com.tobiasdiez:easybind:2.2 - https://github.com/tobiasdiez/EasyBind)
|
||||
|
||||
|
129
src/test/java/org/cryptomator/common/PassphraseTest.java
Normal file
129
src/test/java/org/cryptomator/common/PassphraseTest.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -2,7 +2,9 @@ package org.cryptomator.common.keychain;
|
||||
|
||||
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
@ -32,7 +34,8 @@ public class KeychainManagerTest {
|
||||
public static void startup() throws InterruptedException {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
Platform.startup(latch::countDown);
|
||||
latch.await(5, TimeUnit.SECONDS);
|
||||
var javafxStarted = latch.await(5, TimeUnit.SECONDS);
|
||||
Assumptions.assumeTrue(javafxStarted);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -31,8 +31,8 @@ public class LaunchBasedTriggeringPolicyTest {
|
||||
triggered = policy.isTriggeringEvent(activeFile, event);
|
||||
Assertions.assertFalse(triggered);
|
||||
|
||||
Mockito.verifyZeroInteractions(activeFile);
|
||||
Mockito.verifyZeroInteractions(event);
|
||||
Mockito.verifyNoInteractions(activeFile);
|
||||
Mockito.verifyNoInteractions(event);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.cryptomator.ui.controls;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
@ -8,7 +9,6 @@ import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@ -18,13 +18,10 @@ public class SecurePasswordFieldTest {
|
||||
|
||||
@BeforeAll
|
||||
public static void initJavaFx() throws InterruptedException {
|
||||
Assumptions.assumeFalse(GraphicsEnvironment.isHeadless());
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
Platform.startup(latch::countDown);
|
||||
|
||||
if (!latch.await(5L, TimeUnit.SECONDS)) {
|
||||
throw new ExceptionInInitializerError();
|
||||
}
|
||||
var javafxStarted = latch.await(5, TimeUnit.SECONDS);
|
||||
Assumptions.assumeTrue(javafxStarted);
|
||||
}
|
||||
|
||||
@Nested
|
||||
|
@ -11,13 +11,12 @@ import org.mockito.Mockito;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class RecoveryKeyFactoryTest {
|
||||
|
||||
private WordEncoder wordEncoder = new WordEncoder();
|
||||
private MasterkeyFileAccess masterkeyFileAccess = Mockito.mock(MasterkeyFileAccess.class);
|
||||
private RecoveryKeyFactory inTest = new RecoveryKeyFactory(wordEncoder, masterkeyFileAccess);
|
||||
private final WordEncoder wordEncoder = new WordEncoder();
|
||||
private final MasterkeyFileAccess masterkeyFileAccess = Mockito.mock(MasterkeyFileAccess.class);
|
||||
private final RecoveryKeyFactory inTest = new RecoveryKeyFactory(wordEncoder, masterkeyFileAccess);
|
||||
|
||||
@Test
|
||||
@DisplayName("createRecoveryKey() creates 44 words")
|
||||
|
Loading…
Reference in New Issue
Block a user