Compare commits

...

83 Commits
1.5.2 ... 1.5.7

Author SHA1 Message Date
Tobias Hagemann
1c45455cb2 Merge branch 'release/1.5.7' 2020-08-14 12:58:26 +02:00
Tobias Hagemann
411aca8695 preparing 1.5.7 2020-08-14 12:55:46 +02:00
Sebastian Stenzel
3de69021df New Crowdin updates (#1255)
[ci skip]
2020-08-14 12:49:55 +02:00
Tobias Hagemann
8418038736 dependency update 2020-08-14 12:47:11 +02:00
Tobias Hagemann
69f8c46f15 ui adjustments in vault detail missing 2020-08-12 17:35:11 +02:00
Tobias Hagemann
25195fffe2 added icon to password strength label in new password controller 2020-08-07 17:47:49 +02:00
Tobias Hagemann
0e784a6ffc also disable finish button in change password if old password field is empty 2020-08-07 17:09:55 +02:00
Tobias Hagemann
351f96fa8b switch to light theme if donation key has changed and is invalid 2020-08-07 09:30:22 +02:00
Tobias Hagemann
703fb4fb51 fixed 0091c401df (r40816364) 2020-08-06 18:32:57 +02:00
Tobias Hagemann
a975df6f8c added "Automatic" theme on macOS 2020-08-06 18:30:34 +02:00
Martin Beyer
3bd2a2f911 Fixes #1262 by adding new buttons, if a vault cannot be found 2020-07-29 10:21:16 +02:00
Martin Beyer
0091c401df Fixes #1267 by binding the settings.preferredVolumeImpl to VolumeImpl.WEBDAV, if preferredVolimeImpl isn't available 2020-07-22 15:26:21 +02:00
Armin Schrenk
4ab7bfaa20 reformat 2020-07-17 11:58:11 +02:00
Armin Schrenk
7e0ffb43a6 fixing bug where the mountName of the vault is initially set as the default one 2020-07-17 11:57:53 +02:00
Sebastian Stenzel
d3063c8117 Make sure not to launch the FX app more than once. 2020-07-07 16:01:32 +02:00
Armin Schrenk
cd1ed088c3 Update bug.md
Change bullet point "Drive" to "Volume type" such that users are less confused by this field. Also added where this info can be found.
2020-07-03 10:37:02 +02:00
Martin Beyer
e831debb94 fixes #1244 by renaming the refresh button and adding a method call 2020-06-30 09:36:46 +02:00
Martin
8d15a6612f Implements #1244 by adding a small button to regain focus 2020-06-28 11:05:27 +02:00
Sebastian Stenzel
330b1cbf7d derived resources after dependency update
[ci skip]
2020-06-25 17:15:07 +02:00
Sebastian Stenzel
c925727269 Merge branch 'master' into develop 2020-06-22 17:23:58 +02:00
Sebastian Stenzel
ac2dc06bd6 Merge branch 'release/1.5.6' 2020-06-22 17:23:27 +02:00
Sebastian Stenzel
80b83adb53 Preparing 1.5.6 2020-06-22 17:08:48 +02:00
Sebastian Stenzel
ddc31ac2bb New Crowdin translations (#1227)
[ci skip]
2020-06-22 17:08:09 +02:00
Sebastian Stenzel
eb7c5d0b2b Dependency update 2020-06-22 10:21:44 +02:00
Armin Schrenk
e5ce3cb62d closes #1254 2020-06-19 15:57:12 +02:00
Sebastian Stenzel
d3a6964d8f fixes #1249 2020-06-16 15:02:38 +02:00
Armin Schrenk
d80605532f closes #1141 2020-05-28 16:26:22 +02:00
Sebastian Stenzel
8d5fb14b50 Merge branch 'master' into develop 2020-05-27 12:43:56 +02:00
Sebastian Stenzel
570286c7df Merge branch 'release/1.5.5' 2020-05-27 12:43:35 +02:00
Sebastian Stenzel
11e3ee8d90 formatted code
[ci skip]
2020-05-27 12:43:18 +02:00
Sebastian Stenzel
0664c670f7 Merge branch 'develop' into release/1.5.5 2020-05-27 12:33:55 +02:00
Martin Beyer
f36f9d412c Fixes #1203 2020-05-27 12:04:34 +02:00
Sebastian Stenzel
bd734f1960 preparing 1.5.5 2020-05-27 11:23:09 +02:00
Sebastian Stenzel
675a0b1a73 New Crowdin translations (#1194)
[ci skip]
2020-05-26 16:50:01 +02:00
Martin Beyer
a2816277bf Refactor unlock Window in preparation of #1203 2020-05-26 14:00:25 +02:00
Tobias Hagemann
e37f1f914b some layout adjustments in vault detail 2020-05-26 13:20:47 +02:00
Sebastian Stenzel
adf7694308 fixes #1021, fixes #1170, fixes #1180 2020-05-26 10:04:32 +02:00
Sebastian Stenzel
c13449c6ad fixes #1214 2020-05-26 09:21:18 +02:00
Armin Schrenk
f72035210c fixing error where vault stays in processing state if unlock is canceld via system close button (window decoration bar) 2020-05-25 13:51:14 +02:00
Martin Beyer
89ef5238ea Fixes #1218 by setting the maxWidth of each button to Infinity 2020-05-24 12:41:45 +02:00
Armin Schrenk
8198f66c1f fixes #1208 2020-05-22 12:39:41 +02:00
Armin Schrenk
eb5aa4ee44 remove unnecessary binding 2020-05-22 11:14:35 +02:00
Tobias Hagemann
adb09a0efe proofread strings 2020-05-20 17:45:54 +02:00
Martin Beyer
241eb8bed5 Implements #1182 By adding a button to forget the saved password 2020-05-20 15:04:33 +02:00
Martin Beyer
842a0d6ff3 Implements #1183 Saved Passwords are indicated 2020-05-20 15:02:53 +02:00
Martin Beyer
3200917df2 Merge remote-tracking branch 'origin/develop' into develop 2020-05-20 14:51:57 +02:00
Martin Beyer
12dcf0647d Reverted small error in 22859c9ffa to fix#1082 2020-05-20 14:49:12 +02:00
Martin Beyer
22859c9ffa Fixes #1082 Illegal mount options cannot be selected (WebDav + Windows)
By adding the visible and managed flags to the corresponding fxml tags, it is no longer possible to select illegal combinations.
2020-05-20 13:02:14 +02:00
Sebastian Stenzel
29182156df fixes #1196 2020-05-19 11:21:20 +02:00
Sebastian Stenzel
357d63f398 Added animation to unlock dialog 2020-05-14 10:37:51 +02:00
Sebastian Stenzel
e594bf208d moved images to subdir 2020-05-14 07:53:51 +02:00
Sebastian Stenzel
2f0de3520a Refactored KeychainAccess in preparation of #1183, #1182 2020-05-13 19:59:32 +02:00
Sebastian Stenzel
8def68eb02 Merge branch 'master' into develop 2020-05-12 16:27:14 +02:00
Sebastian Stenzel
aef33dc864 Merge branch 'release/1.5.4' 2020-05-12 16:26:44 +02:00
Sebastian Stenzel
06d2f2d9e9 Preparing 1.5.4 2020-05-12 16:21:50 +02:00
Sebastian Stenzel
dad0ad76fb New Crowdin translations (#1166)
[ci skip]
2020-05-12 16:20:49 +02:00
Sebastian Stenzel
99fa8e7c8e fixes #1171 (or rather applying a workaround until we get an upstream fix) 2020-05-12 16:18:11 +02:00
Martin Beyer
ab0f175edf Implemented hyperlink in preferences to reveal log files (#1184) 2020-05-12 11:59:22 +02:00
Ralph Plawetzki
8cb9728565 Implement changePassphrase from the secret-service API (#1191)
Fixes #1189
2020-05-12 07:41:10 +02:00
Ralph Plawetzki
d91d27f2a4 Close unlockScene after entering the phrase and unlocking the vault (#1186) 2020-05-12 07:40:38 +02:00
Sebastian Stenzel
b2a6e038ae Updated secret-service lib to 1.0.0
references #1169
2020-05-11 17:34:12 +02:00
Sebastian Stenzel
75f360903c recheck vault state when focusing window
fixes #1190
fixes #1110
fixes #1139
2020-05-11 08:08:15 +02:00
Sebastian Stenzel
79c3137b90 no need to be application-scoped 2020-05-11 07:58:29 +02:00
Sebastian Stenzel
d2189d379c Using switch expressions 2020-05-11 07:47:15 +02:00
Sebastian Stenzel
49aead7323 Merge branch 'feature/refactored-unlock' into develop 2020-05-08 15:08:46 +02:00
Sebastian Stenzel
7bd610563f exception not thrown here
[ci skip]
2020-05-08 09:30:21 +02:00
Sebastian Stenzel
5c1a1ad162 respect choice made in #1083 2020-05-07 16:57:57 +02:00
Sebastian Stenzel
86906d0049 fixes #1083 2020-05-07 16:45:24 +02:00
Sebastian Stenzel
93011dc754 cleanup 2020-05-07 16:33:05 +02:00
Sebastian Stenzel
b084b651af wipe memory when setting a new password 2020-05-07 15:57:56 +02:00
Sebastian Stenzel
fecf9c0423 Fixes #1088 2020-05-07 14:18:09 +02:00
Sebastian Stenzel
117fe78a4a Refactored stage creation 2020-05-07 12:35:01 +02:00
Tobias Hagemann
153d43573a Merge branch 'master' into develop 2020-04-30 16:51:48 +02:00
Tobias Hagemann
9145f5d2f8 Merge branch 'release/1.5.3' 2020-04-30 16:51:01 +02:00
Tobias Hagemann
835ea3b640 preparing 1.5.3 2020-04-30 16:50:16 +02:00
Sebastian Stenzel
1e7eb23d1b New Crowdin translations (#1164)
[ci skip]
2020-04-30 16:48:22 +02:00
Sebastian Stenzel
cfe25d0bf5 updating siv-mode for twice as fast filename encryption/decryption 2020-04-30 16:37:07 +02:00
Sebastian Stenzel
b14939bd77 New Crowdin translations (#1159)
[ci skip]
2020-04-30 16:33:33 +02:00
Tobias Hagemann
26e140ee22 fixed checkmark color if it's selected and disabled 2020-04-30 16:31:25 +02:00
Tobias Hagemann
1c5ecf8c01 centered main.vaultlist.emptyList.onboardingInstruction 2020-04-30 14:52:16 +02:00
Tobias Hagemann
9b528a05b5 localized display name of ui themes, now actually use vaultOptions.mount.winDriveLetterOccupied localizations 2020-04-30 14:42:51 +02:00
Sebastian Stenzel
3a7aa6d64f fixes #1163, fixes #1131 2020-04-30 13:41:18 +02:00
Sebastian Stenzel
55820e47f9 Merge branch 'master' into develop 2020-04-29 16:47:55 +02:00
159 changed files with 2657 additions and 925 deletions

View File

@@ -23,7 +23,7 @@ Of course, we also expect you to search for existing similar issues first! ;) ht
* Operating system and version: [Windows/macOS/Linux + Version]
* Cryptomator version: [Shown in the settings]
* Drive: [Dokany/FUSE/WebDAV]
* Volume type: [Dokany/FUSE/WebDAV, shown in the settings]
### Steps to Reproduce
@@ -58,4 +58,4 @@ Log file location:
- macOS: ~/Library/Logs/Cryptomator
- Linux: ~/.local/share/Cryptomator/logs
-->
-->

28
.idea/compiler.xml generated
View File

@@ -7,34 +7,26 @@
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<processorPath useClasspath="false">
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-compiler/2.27/dagger-compiler-2.27.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger/2.27/dagger-2.27.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-compiler/2.22/dagger-compiler-2.22.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger/2.22/dagger-2.22.jar" />
<entry name="$MAVEN_REPOSITORY$/javax/inject/javax.inject/1/javax.inject-1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-producers/2.27/dagger-producers-2.27.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/guava/27.1-jre/guava-27.1-jre.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/code/findbugs/jsr305/3.0.1/jsr305-3.0.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-qual/2.5.2/checker-qual-2.5.2.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotations/2.2.0/error_prone_annotations-2.2.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/j2objc/j2objc-annotations/1.1/j2objc-annotations-1.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/codehaus/mojo/animal-sniffer-annotations/1.17/animal-sniffer-annotations-1.17.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-producers/2.22/dagger-producers-2.22.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/guava/25.0-jre/guava-25.0-jre.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/code/findbugs/jsr305/1.3.9/jsr305-1.3.9.jar" />
<entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-compat-qual/2.5.3/checker-compat-qual-2.5.3.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-spi/2.27/dagger-spi-2.27.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotations/2.1.3/error_prone_annotations-2.1.3.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/j2objc/j2objc-annotations/1.1/j2objc-annotations-1.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/codehaus/mojo/animal-sniffer-annotations/1.14/animal-sniffer-annotations-1.14.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-spi/2.22/dagger-spi-2.22.jar" />
<entry name="$MAVEN_REPOSITORY$/com/squareup/javapoet/1.11.1/javapoet-1.11.1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/googlejavaformat/google-java-format/1.5/google-java-format-1.5.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/javac-shaded/9-dev-r4023-3/javac-shaded-9-dev-r4023-3.jar" />
<entry name="$MAVEN_REPOSITORY$/javax/annotation/jsr250-api/1.0/jsr250-api-1.0.jar" />
<entry name="$MAVEN_REPOSITORY$/net/ltgt/gradle/incap/incap/0.2/incap-0.2.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.3.50/kotlin-stdlib-1.3.50.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.3.50/kotlin-stdlib-common-1.3.50.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-metadata-jvm/0.1.0/kotlinx-metadata-jvm-0.1.0.jar" />
</processorPath>
<module name="keychain" />
<module name="launcher" />
<module name="commons" />
<module name="ui" />
<module name="launcher" />
</profile>
</annotationProcessing>
</component>

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.5.2</version>
<version>1.5.7</version>
</parent>
<artifactId>buildkit</artifactId>
<packaging>pom</packaging>

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.5.2</version>
<version>1.5.7</version>
</parent>
<artifactId>commons</artifactId>
<name>Cryptomator Commons</name>

View File

@@ -58,46 +58,22 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
while (in.hasNext()) {
String name = in.nextName();
switch (name) {
case "directories":
settings.getDirectories().addAll(readVaultSettingsArray(in));
break;
case "askedForUpdateCheck":
settings.askedForUpdateCheck().set(in.nextBoolean());
break;
case "checkForUpdatesEnabled":
settings.checkForUpdates().set(in.nextBoolean());
break;
case "startHidden":
settings.startHidden().set(in.nextBoolean());
break;
case "port":
settings.port().set(in.nextInt());
break;
case "numTrayNotifications":
settings.numTrayNotifications().set(in.nextInt());
break;
case "preferredGvfsScheme":
settings.preferredGvfsScheme().set(parseWebDavUrlSchemePrefix(in.nextString()));
break;
case "debugMode":
settings.debugMode().set(in.nextBoolean());
break;
case "preferredVolumeImpl":
settings.preferredVolumeImpl().set(parsePreferredVolumeImplName(in.nextString()));
break;
case "theme":
settings.theme().set(parseUiTheme(in.nextString()));
break;
case "uiOrientation":
settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
break;
case "licenseKey":
settings.licenseKey().set(in.nextString());
break;
default:
case "directories" -> settings.getDirectories().addAll(readVaultSettingsArray(in));
case "askedForUpdateCheck" -> settings.askedForUpdateCheck().set(in.nextBoolean());
case "checkForUpdatesEnabled" -> settings.checkForUpdates().set(in.nextBoolean());
case "startHidden" -> settings.startHidden().set(in.nextBoolean());
case "port" -> settings.port().set(in.nextInt());
case "numTrayNotifications" -> settings.numTrayNotifications().set(in.nextInt());
case "preferredGvfsScheme" -> settings.preferredGvfsScheme().set(parseWebDavUrlSchemePrefix(in.nextString()));
case "debugMode" -> settings.debugMode().set(in.nextBoolean());
case "preferredVolumeImpl" -> settings.preferredVolumeImpl().set(parsePreferredVolumeImplName(in.nextString()));
case "theme" -> settings.theme().set(parseUiTheme(in.nextString()));
case "uiOrientation" -> settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
case "licenseKey" -> settings.licenseKey().set(in.nextString());
default -> {
LOG.warn("Unsupported vault setting found in JSON: " + name);
in.skipValue();
break;
}
}
}
in.endObject();

View File

@@ -1,11 +1,21 @@
package org.cryptomator.common.settings;
public enum UiTheme {
LIGHT("Light"),
DARK("Dark");
// CUSTOM("Custom (%s)");
import org.apache.commons.lang3.SystemUtils;
private String displayName;
public enum UiTheme {
LIGHT("preferences.general.theme.light"), //
DARK("preferences.general.theme.dark"), //
AUTOMATIC("preferences.general.theme.automatic");
public static UiTheme[] applicableValues() {
if (SystemUtils.IS_OS_MAC) {
return values();
} else {
return new UiTheme[]{LIGHT, DARK};
}
}
private final String displayName;
UiTheme(String displayName) {
this.displayName = displayName;

View File

@@ -26,7 +26,6 @@ import java.util.Random;
/**
* The settings specific to a single vault.
* TODO: Change the name of individualMountPath and its derivatives to customMountPath
*/
public class VaultSettings {
@@ -35,9 +34,11 @@ public class VaultSettings {
public static final boolean DEFAULT_USES_INDIVIDUAL_MOUNTPATH = false;
public static final boolean DEFAULT_USES_READONLY_MODE = false;
public static final String DEFAULT_MOUNT_FLAGS = "";
public static final String DEFAULT_MOUNT_NAME = "Vault";
public static final int DEFAULT_FILENAME_LENGTH_LIMIT = -1;
private static final Random RNG = new Random();
public static final WhenUnlocked DEFAULT_ACTION_AFTER_UNLOCK = WhenUnlocked.ASK;
private static final Random RNG = new Random();
private final String id;
private final ObjectProperty<Path> path = new SimpleObjectProperty();
@@ -45,25 +46,36 @@ public class VaultSettings {
private final StringProperty winDriveLetter = new SimpleStringProperty();
private final BooleanProperty unlockAfterStartup = new SimpleBooleanProperty(DEFAULT_UNLOCK_AFTER_STARTUP);
private final BooleanProperty revealAfterMount = new SimpleBooleanProperty(DEFAULT_REAVEAL_AFTER_MOUNT);
private final BooleanProperty usesIndividualMountPath = new SimpleBooleanProperty(DEFAULT_USES_INDIVIDUAL_MOUNTPATH);
private final StringProperty individualMountPath = new SimpleStringProperty();
private final BooleanProperty useCustomMountPath = new SimpleBooleanProperty(DEFAULT_USES_INDIVIDUAL_MOUNTPATH);
private final StringProperty customMountPath = new SimpleStringProperty();
private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE);
private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS);
private final IntegerProperty filenameLengthLimit = new SimpleIntegerProperty(DEFAULT_FILENAME_LENGTH_LIMIT);
private final ObjectProperty<WhenUnlocked> actionAfterUnlock = new SimpleObjectProperty<>(DEFAULT_ACTION_AFTER_UNLOCK);
public VaultSettings(String id) {
this.id = Objects.requireNonNull(id);
EasyBind.subscribe(path, this::deriveMountNameFromPath);
EasyBind.subscribe(path, this::deriveMountNameFromPathOrUseDefault);
}
Observable[] observables() {
return new Observable[]{path, mountName, winDriveLetter, unlockAfterStartup, revealAfterMount, usesIndividualMountPath, individualMountPath, usesReadOnlyMode, mountFlags, filenameLengthLimit};
return new Observable[]{path, mountName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, filenameLengthLimit, actionAfterUnlock};
}
private void deriveMountNameFromPath(Path path) {
if (path != null && StringUtils.isBlank(mountName.get())) {
mountName.set(normalizeMountName(path.getFileName().toString()));
private void deriveMountNameFromPathOrUseDefault(Path newPath) {
final boolean mountNameSet = !StringUtils.isBlank(mountName.get());
final boolean dirnameExists = (newPath != null) && (newPath.getFileName() != null);
if (!mountNameSet && dirnameExists) {
mountName.set(normalizeMountName(newPath.getFileName().toString()));
} else if (!mountNameSet && !dirnameExists) {
mountName.set(DEFAULT_MOUNT_NAME + id);
} else if (mountNameSet && dirnameExists) {
if (mountName.get().equals(DEFAULT_MOUNT_NAME + id)) {
//this is okay, since this function is only executed if the path changes (aka, the vault is created or added)
mountName.set(newPath.getFileName().toString());
}
}
}
@@ -122,17 +134,17 @@ public class VaultSettings {
return revealAfterMount;
}
public BooleanProperty usesIndividualMountPath() {
return usesIndividualMountPath;
public BooleanProperty useCustomMountPath() {
return useCustomMountPath;
}
public StringProperty individualMountPath() {
return individualMountPath;
public StringProperty customMountPath() {
return customMountPath;
}
public Optional<String> getIndividualMountPath() {
if (usesIndividualMountPath.get()) {
return Optional.ofNullable(Strings.emptyToNull(individualMountPath.get()));
public Optional<String> getCustomMountPath() {
if (useCustomMountPath.get()) {
return Optional.ofNullable(Strings.emptyToNull(customMountPath.get()));
} else {
return Optional.empty();
}
@@ -145,11 +157,19 @@ public class VaultSettings {
public StringProperty mountFlags() {
return mountFlags;
}
public IntegerProperty filenameLengthLimit() {
return filenameLengthLimit;
}
public ObjectProperty<WhenUnlocked> actionAfterUnlock() {
return actionAfterUnlock;
}
public WhenUnlocked getActionAfterUnlock() {
return actionAfterUnlock.get();
}
/* Hashcode/Equals */
@Override

View File

@@ -25,11 +25,12 @@ class VaultSettingsJsonAdapter {
out.name("winDriveLetter").value(value.winDriveLetter().get());
out.name("unlockAfterStartup").value(value.unlockAfterStartup().get());
out.name("revealAfterMount").value(value.revealAfterMount().get());
out.name("usesIndividualMountPath").value(value.usesIndividualMountPath().get());
out.name("individualMountPath").value(value.individualMountPath().get());
out.name("useCustomMountPath").value(value.useCustomMountPath().get());
out.name("customMountPath").value(value.customMountPath().get());
out.name("usesReadOnlyMode").value(value.usesReadOnlyMode().get());
out.name("mountFlags").value(value.mountFlags().get());
out.name("filenameLengthLimit").value(value.filenameLengthLimit().get());
out.name("actionAfterUnlock").value(value.actionAfterUnlock().get().name());
out.endObject();
}
@@ -37,56 +38,36 @@ class VaultSettingsJsonAdapter {
String id = null;
String path = null;
String mountName = null;
String individualMountPath = null;
String customMountPath = null;
String winDriveLetter = null;
boolean unlockAfterStartup = VaultSettings.DEFAULT_UNLOCK_AFTER_STARTUP;
boolean revealAfterMount = VaultSettings.DEFAULT_REAVEAL_AFTER_MOUNT;
boolean usesIndividualMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH;
boolean useCustomMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH;
boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE;
String mountFlags = VaultSettings.DEFAULT_MOUNT_FLAGS;
int filenameLengthLimit = VaultSettings.DEFAULT_FILENAME_LENGTH_LIMIT;
WhenUnlocked actionAfterUnlock = VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
switch (name) {
case "id":
id = in.nextString();
break;
case "path":
path = in.nextString();
break;
case "mountName":
mountName = in.nextString();
break;
case "winDriveLetter":
winDriveLetter = in.nextString();
break;
case "unlockAfterStartup":
unlockAfterStartup = in.nextBoolean();
break;
case "revealAfterMount":
revealAfterMount = in.nextBoolean();
break;
case "usesIndividualMountPath":
usesIndividualMountPath = in.nextBoolean();
break;
case "individualMountPath":
individualMountPath = in.nextString();
break;
case "usesReadOnlyMode":
usesReadOnlyMode = in.nextBoolean();
break;
case "mountFlags":
mountFlags = in.nextString();
break;
case "filenameLengthLimit":
filenameLengthLimit = in.nextInt();
break;
default:
case "id" -> id = in.nextString();
case "path" -> path = in.nextString();
case "mountName" -> mountName = in.nextString();
case "winDriveLetter" -> winDriveLetter = in.nextString();
case "unlockAfterStartup" -> unlockAfterStartup = in.nextBoolean();
case "revealAfterMount" -> revealAfterMount = in.nextBoolean();
case "usesIndividualMountPath", "useCustomMountPath" -> useCustomMountPath = in.nextBoolean();
case "individualMountPath", "customMountPath" -> customMountPath = in.nextString();
case "usesReadOnlyMode" -> usesReadOnlyMode = in.nextBoolean();
case "mountFlags" -> mountFlags = in.nextString();
case "filenameLengthLimit" -> filenameLengthLimit = in.nextInt();
case "actionAfterUnlock" -> actionAfterUnlock = parseActionAfterUnlock(in.nextString());
default -> {
LOG.warn("Unsupported vault setting found in JSON: " + name);
in.skipValue();
break;
}
}
}
in.endObject();
@@ -97,12 +78,22 @@ class VaultSettingsJsonAdapter {
vaultSettings.winDriveLetter().set(winDriveLetter);
vaultSettings.unlockAfterStartup().set(unlockAfterStartup);
vaultSettings.revealAfterMount().set(revealAfterMount);
vaultSettings.usesIndividualMountPath().set(usesIndividualMountPath);
vaultSettings.individualMountPath().set(individualMountPath);
vaultSettings.useCustomMountPath().set(useCustomMountPath);
vaultSettings.customMountPath().set(customMountPath);
vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode);
vaultSettings.mountFlags().set(mountFlags);
vaultSettings.filenameLengthLimit().set(filenameLengthLimit);
vaultSettings.actionAfterUnlock().set(actionAfterUnlock);
return vaultSettings;
}
private WhenUnlocked parseActionAfterUnlock(String actionAfterUnlockName) {
try {
return WhenUnlocked.valueOf(actionAfterUnlockName.toUpperCase());
} catch (IllegalArgumentException e) {
LOG.warn("Invalid action after unlock {}. Defaulting to {}.", actionAfterUnlockName, VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK);
return VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;
}
}
}

View File

@@ -1,7 +1,5 @@
package org.cryptomator.common.settings;
import java.util.Arrays;
public enum VolumeImpl {
WEBDAV("WebDAV"),
FUSE("FUSE"),
@@ -17,18 +15,4 @@ public enum VolumeImpl {
return displayName;
}
/**
* Finds a VolumeImpl by display name.
*
* @param displayName Display name of the VolumeImpl
* @return VolumeImpl with the given <code>displayName</code>.
* @throws IllegalArgumentException if not volumeImpl with the given <code>displayName</code> was found.
*/
public static VolumeImpl forDisplayName(String displayName) throws IllegalArgumentException {
return Arrays.stream(values()) //
.filter(impl -> impl.displayName.equals(displayName)) //
.findAny() //
.orElseThrow(IllegalArgumentException::new);
}
}

View File

@@ -1,7 +1,5 @@
package org.cryptomator.common.settings;
import java.util.Arrays;
public enum WebDavUrlScheme {
DAV("dav", "dav:// (Gnome, Nautilus, ...)"),
WEBDAV("webdav", "webdav:// (KDE, Dolphin, ...)");
@@ -20,18 +18,4 @@ public enum WebDavUrlScheme {
public String getDisplayName() {
return displayName;
}
/**
* Finds a WebDavUrlScheme by prefix.
*
* @param prefix Prefix of the WebDavUrlScheme
* @return WebDavUrlScheme with the given <code>prefix</code>.
* @throws IllegalArgumentException if not WebDavUrlScheme with the given <code>prefix</code> was found.
*/
public static WebDavUrlScheme forPrefix(String prefix) throws IllegalArgumentException {
return Arrays.stream(values()) //
.filter(impl -> impl.prefix.equals(prefix)) //
.findAny() //
.orElseThrow(IllegalArgumentException::new);
}
}

View File

@@ -0,0 +1,17 @@
package org.cryptomator.common.settings;
public enum WhenUnlocked {
IGNORE("vaultOptions.general.actionAfterUnlock.ignore"),
REVEAL("vaultOptions.general.actionAfterUnlock.reveal"),
ASK("vaultOptions.general.actionAfterUnlock.ask");
private String displayName;
WhenUnlocked(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}

View File

@@ -51,7 +51,7 @@ public class DokanyVolume implements Volume {
try {
this.mount = mountFactory.mount(fs.getPath("/"), mountPoint, mountName, FS_TYPE_NAME, mountFlags.strip());
} catch (MountFailedException e) {
if (vaultSettings.getIndividualMountPath().isPresent()) {
if (vaultSettings.getCustomMountPath().isPresent()) {
LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPoint);
}
throw new VolumeException("Unable to mount Filesystem", e);
@@ -59,7 +59,7 @@ public class DokanyVolume implements Volume {
}
private Path determineMountPoint() throws VolumeException, IOException {
Optional<String> optionalCustomMountPoint = vaultSettings.getIndividualMountPath();
Optional<String> optionalCustomMountPoint = vaultSettings.getCustomMountPath();
if (optionalCustomMountPoint.isPresent()) {
Path customMountPoint = Paths.get(optionalCustomMountPoint.get());
checkProvidedMountPoint(customMountPoint);

View File

@@ -45,7 +45,7 @@ public class FuseVolume implements Volume {
@Override
public void mount(CryptoFileSystem fs, String mountFlags) throws IOException, FuseNotSupportedException, VolumeException {
Optional<String> optionalCustomMountPoint = vaultSettings.getIndividualMountPath();
Optional<String> optionalCustomMountPoint = vaultSettings.getCustomMountPath();
if (optionalCustomMountPoint.isPresent()) {
Path customMountPoint = Paths.get(optionalCustomMountPoint.get());
checkProvidedMountPoint(customMountPoint);

View File

@@ -121,7 +121,7 @@ public class Vault {
}
public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, Volume.VolumeException {
if (vaultSettings.usesIndividualMountPath().get() && Strings.isNullOrEmpty(vaultSettings.individualMountPath().get())) {
if (vaultSettings.useCustomMountPath().get() && Strings.isNullOrEmpty(vaultSettings.customMountPath().get())) {
throw new NotDirectoryException("");
}
CryptoFileSystem fs = getCryptoFileSystem(passphrase);

View File

@@ -92,8 +92,27 @@ public class VaultListManager {
}
return compBuilder.build().vault();
}
public static VaultState redetermineVaultState(Vault vault) {
VaultState previousState = vault.getState();
return switch (previousState) {
case LOCKED, NEEDS_MIGRATION, MISSING -> {
try {
VaultState determinedState = determineVaultState(vault.getPath());
vault.setState(determinedState);
yield determinedState;
} catch (IOException e) {
LOG.warn("Failed to determine vault state for " + vault.getPath(), e);
vault.setState(VaultState.ERROR);
vault.setLastKnownException(e);
yield VaultState.ERROR;
}
}
case ERROR, UNLOCKED, PROCESSING -> previousState;
};
}
public static VaultState determineVaultState(Path pathToVault) throws IOException {
private static VaultState determineVaultState(Path pathToVault) throws IOException {
if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) {
return VaultState.MISSING;
} else if (Migrators.get().needsMigration(pathToVault, MASTERKEY_FILENAME)) {

View File

@@ -16,7 +16,6 @@ import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.file.Paths;
import java.util.Arrays;
public class VaultSettingsJsonAdapterTest {
@@ -32,7 +31,7 @@ public class VaultSettingsJsonAdapterTest {
Assertions.assertEquals(Paths.get("/foo/bar"), vaultSettings.path().get());
Assertions.assertEquals("test", vaultSettings.mountName().get());
Assertions.assertEquals("X", vaultSettings.winDriveLetter().get());
Assertions.assertEquals("/home/test/crypto", vaultSettings.individualMountPath().get());
Assertions.assertEquals("/home/test/crypto", vaultSettings.customMountPath().get());
Assertions.assertEquals("--foo --bar", vaultSettings.mountFlags().get());

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.5.2</version>
<version>1.5.7</version>
</parent>
<artifactId>keychain</artifactId>
<name>System Keychain Access</name>
@@ -15,16 +15,27 @@
<artifactId>commons</artifactId>
</dependency>
<!-- JavaFx -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-base</artifactId>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
</dependency>
<!-- Apache -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- Google -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<!-- Google -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
@@ -40,7 +51,6 @@
<dependency>
<groupId>de.swiesend</groupId>
<artifactId>secret-service</artifactId>
<version>1.0.0-RC.3</version>
</dependency>
<!-- Logging -->

View File

@@ -1,31 +0,0 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.keychain;
public interface KeychainAccess {
/**
* Associates a passphrase with a given key.
*
* @param key Key used to retrieve the passphrase via {@link #loadPassphrase(String)}.
* @param passphrase The secret to store in this keychain.
*/
void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException;
/**
* @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
* @return The stored passphrase for the given key or <code>null</code> if no value for the given key could be found.
*/
char[] loadPassphrase(String key) throws KeychainAccessException;
/**
* Deletes a passphrase with a given key.
*
* @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
*/
void deletePassphrase(String key) throws KeychainAccessException;
}

View File

@@ -5,7 +5,36 @@
*******************************************************************************/
package org.cryptomator.keychain;
interface KeychainAccessStrategy extends KeychainAccess {
interface KeychainAccessStrategy {
/**
* Associates a passphrase with a given key.
*
* @param key Key used to retrieve the passphrase via {@link #loadPassphrase(String)}.
* @param passphrase The secret to store in this keychain.
*/
void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException;
/**
* @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
* @return The stored passphrase for the given key or <code>null</code> if no value for the given key could be found.
*/
char[] loadPassphrase(String key) throws KeychainAccessException;
/**
* Deletes a passphrase with a given key.
*
* @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
*/
void deletePassphrase(String key) throws KeychainAccessException;
/**
* Updates a passphrase with a given key. Noop, if there is no item for the given key.
*
* @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
* @param passphrase The secret to be updated in this keychain.
*/
void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException;
/**
* @return <code>true</code> if this KeychainAccessStrategy works on the current machine.

View File

@@ -0,0 +1,117 @@
package org.cryptomator.keychain;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
public class KeychainManager implements KeychainAccessStrategy {
private static final Logger LOG = LoggerFactory.getLogger(KeychainManager.class);
private final KeychainAccessStrategy keychain;
private LoadingCache<String, BooleanProperty> passphraseStoredProperties;
KeychainManager(KeychainAccessStrategy keychain) {
assert keychain.isSupported();
this.keychain = keychain;
this.passphraseStoredProperties = CacheBuilder.newBuilder() //
.weakValues() //
.build(CacheLoader.from(this::createStoredPassphraseProperty));
}
@Override
public void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
keychain.storePassphrase(key, passphrase);
setPassphraseStored(key, true);
}
@Override
public char[] loadPassphrase(String key) throws KeychainAccessException {
char[] passphrase = keychain.loadPassphrase(key);
setPassphraseStored(key, passphrase != null);
return passphrase;
}
@Override
public void deletePassphrase(String key) throws KeychainAccessException {
keychain.deletePassphrase(key);
setPassphraseStored(key, false);
}
@Override
public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
keychain.changePassphrase(key, passphrase);
setPassphraseStored(key, true);
}
@Override
public boolean isSupported() {
return true;
}
/**
* Checks if the keychain knows a passphrase for the given key.
* <p>
* Expensive operation. If possible, use {@link #getPassphraseStoredProperty(String)} instead.
*
* @param key The key to look up
* @return <code>true</code> if a password for <code>key</code> is stored.
* @throws KeychainAccessException
*/
public boolean isPassphraseStored(String key) throws KeychainAccessException {
char[] storedPw = null;
try {
storedPw = keychain.loadPassphrase(key);
return storedPw != null;
} finally {
if (storedPw != null) {
Arrays.fill(storedPw, ' ');
}
}
}
private void setPassphraseStored(String key, boolean value) {
BooleanProperty property = passphraseStoredProperties.getIfPresent(key);
if (property != null) {
if (Platform.isFxApplicationThread()) {
property.set(value);
} else {
LOG.warn("");
Platform.runLater(() -> property.set(value));
}
}
}
/**
* Returns an observable property for use in the UI that tells whether a passphrase is stored for the given key.
* <p>
* Assuming that this process is the only process modifying Cryptomator-related items in the system keychain, this
* property stays in memory in an attempt to avoid unnecessary calls to the system keychain. Note that due to this
* fact the value stored in the returned property is not 100% reliable. Code defensively!
*
* @param key The key to look up
* @return An observable property which is <code>true</code> when it almost certain that a password for <code>key</code> is stored.
* @see #isPassphraseStored(String)
*/
public ReadOnlyBooleanProperty getPassphraseStoredProperty(String key) {
return passphraseStoredProperties.getUnchecked(key);
}
private BooleanProperty createStoredPassphraseProperty(String key) {
try {
LOG.warn("LOAD"); // TODO remove
return new SimpleBooleanProperty(isPassphraseStored(key));
} catch (KeychainAccessException e) {
return new SimpleBooleanProperty(false);
}
}
}

View File

@@ -5,10 +5,10 @@
*******************************************************************************/
package org.cryptomator.keychain;
import com.google.common.collect.Sets;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.ElementsIntoSet;
import dagger.multibindings.IntoSet;
import org.cryptomator.common.JniModule;
import javax.inject.Singleton;
@@ -16,18 +16,30 @@ import java.util.Optional;
import java.util.Set;
@Module(includes = {JniModule.class})
public class KeychainModule {
public abstract class KeychainModule {
@Provides
@ElementsIntoSet
Set<KeychainAccessStrategy> provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain, LinuxSecretServiceKeychainAccess linKeychain) {
return Sets.newHashSet(macKeychain, winKeychain, linKeychain);
}
@Binds
@IntoSet
abstract KeychainAccessStrategy bindMacSystemKeychainAccess(MacSystemKeychainAccess keychainAccessStrategy);
@Binds
@IntoSet
abstract KeychainAccessStrategy bindWindowsProtectedKeychainAccess(WindowsProtectedKeychainAccess keychainAccessStrategy);
@Binds
@IntoSet
abstract KeychainAccessStrategy bindLinuxSecretServiceKeychainAccess(LinuxSecretServiceKeychainAccess keychainAccessStrategy);
@Provides
@Singleton
public Optional<KeychainAccess> provideSupportedKeychain(Set<KeychainAccessStrategy> keychainAccessStrategies) {
return keychainAccessStrategies.stream().filter(KeychainAccessStrategy::isSupported).map(KeychainAccess.class::cast).findFirst();
static Optional<KeychainAccessStrategy> provideSupportedKeychain(Set<KeychainAccessStrategy> keychainAccessStrategies) {
return keychainAccessStrategies.stream().filter(KeychainAccessStrategy::isSupported).findFirst();
}
@Provides
@Singleton
public static Optional<KeychainManager> provideKeychainManager(Optional<KeychainAccessStrategy> keychainAccess) {
return keychainAccess.map(KeychainManager::new);
}
}

View File

@@ -3,11 +3,13 @@ package org.cryptomator.keychain;
import org.apache.commons.lang3.SystemUtils;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Optional;
/**
* A facade to LinuxSecretServiceKeychainAccessImpl that doesn't depend on libraries that are unavailable on Mac and Windows.
*/
@Singleton
public class LinuxSecretServiceKeychainAccess implements KeychainAccessStrategy {
// the actual implementation is hidden in this delegate object which is loaded via reflection,
@@ -48,4 +50,9 @@ public class LinuxSecretServiceKeychainAccess implements KeychainAccessStrategy
public void deletePassphrase(String key) throws KeychainAccessException {
delegate.orElseThrow(IllegalStateException::new).deletePassphrase(key);
}
@Override
public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
delegate.orElseThrow(IllegalStateException::new).changePassphrase(key, passphrase);
}
}

View File

@@ -9,6 +9,8 @@ import java.util.Map;
class LinuxSecretServiceKeychainAccessImpl implements KeychainAccessStrategy {
private final String LABEL_FOR_SECRET_IN_KEYRING = "Cryptomator";
@Override
public boolean isSupported() {
try (@SuppressWarnings("unused") SimpleCollection keyring = new SimpleCollection()) {
@@ -24,7 +26,7 @@ class LinuxSecretServiceKeychainAccessImpl implements KeychainAccessStrategy {
try (SimpleCollection keyring = new SimpleCollection()) {
List<String> list = keyring.getItems(createAttributes(key));
if (list == null) {
keyring.createItem("Cryptomator", passphrase, createAttributes(key));
keyring.createItem(LABEL_FOR_SECRET_IN_KEYRING, passphrase, createAttributes(key));
}
} catch (IOException e) {
throw new KeychainAccessException(e);
@@ -57,6 +59,18 @@ class LinuxSecretServiceKeychainAccessImpl implements KeychainAccessStrategy {
}
}
@Override
public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
try (SimpleCollection keyring = new SimpleCollection()) {
List<String> list = keyring.getItems(createAttributes(key));
if (list != null) {
keyring.updateItem(list.get(0), LABEL_FOR_SECRET_IN_KEYRING, passphrase, createAttributes(key));
}
} catch (IOException e) {
throw new KeychainAccessException(e);
}
}
private Map<String, String> createAttributes(String key) {
Map<String, String> attributes = new HashMap();
attributes.put("Vault", key);

View File

@@ -8,11 +8,13 @@ package org.cryptomator.keychain;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.jni.MacFunctions;
import org.cryptomator.jni.MacKeychainAccess;
@Singleton
class MacSystemKeychainAccess implements KeychainAccessStrategy {
private final Optional<MacFunctions> macFunctions;
@@ -46,4 +48,11 @@ class MacSystemKeychainAccess implements KeychainAccessStrategy {
keychain().deletePassword(key);
}
@Override
public void changePassphrase(String key, CharSequence passphrase) {
if (keychain().deletePassword(key)) {
keychain().storePassword(key, passphrase);
}
}
}

View File

@@ -25,6 +25,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -50,6 +51,7 @@ import java.util.stream.Collectors;
import static java.nio.charset.StandardCharsets.UTF_8;
@Singleton
class WindowsProtectedKeychainAccess implements KeychainAccessStrategy {
private static final Logger LOG = LoggerFactory.getLogger(WindowsProtectedKeychainAccess.class);
@@ -112,6 +114,14 @@ class WindowsProtectedKeychainAccess implements KeychainAccessStrategy {
saveKeychainEntries();
}
@Override
public void changePassphrase(String key, CharSequence passphrase) {
loadKeychainEntriesIfNeeded();
if (keychainEntries.remove(key) != null) {
storePassphrase(key, passphrase);
}
}
@Override
public boolean isSupported() {
return SystemUtils.IS_OS_WINDOWS && winFunctions.isPresent() && !keychainPaths.isEmpty();

View File

@@ -0,0 +1,55 @@
package org.cryptomator.keychain;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyBooleanProperty;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
class KeychainManagerTest {
@Test
public void testStoreAndLoad() throws KeychainAccessException {
KeychainManager keychainManager = new KeychainManager(new MapKeychainAccess());
keychainManager.storePassphrase("test", "asd");
Assertions.assertArrayEquals("asd".toCharArray(), keychainManager.loadPassphrase("test"));
}
@Nested
public static class WhenObservingProperties {
@BeforeAll
public static void startup() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
Platform.startup(latch::countDown);
latch.await(5, TimeUnit.SECONDS);
}
@Test
public void testPropertyChangesWhenStoringPassword() throws KeychainAccessException, InterruptedException {
KeychainManager keychainManager = new KeychainManager(new MapKeychainAccess());
ReadOnlyBooleanProperty property = keychainManager.getPassphraseStoredProperty("test");
Assertions.assertEquals(false, property.get());
keychainManager.storePassphrase("test", "bar");
AtomicBoolean result = new AtomicBoolean(false);
CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(() -> {
result.set(property.get());
latch.countDown();
});
latch.await(1, TimeUnit.SECONDS);
Assertions.assertEquals(true, result.get());
}
}
}

View File

@@ -1,24 +0,0 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.keychain;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Optional;
public class KeychainModuleTest {
@Test
public void testGetKeychain() throws KeychainAccessException {
Optional<KeychainAccess> keychainAccess = DaggerTestKeychainComponent.builder().keychainModule(new TestKeychainModule()).build().keychainAccess();
Assertions.assertTrue(keychainAccess.isPresent());
Assertions.assertTrue(keychainAccess.get() instanceof MapKeychainAccess);
keychainAccess.get().storePassphrase("test", "asd");
Assertions.assertArrayEquals("asd".toCharArray(), keychainAccess.get().loadPassphrase("test"));
}
}

View File

@@ -31,6 +31,12 @@ class MapKeychainAccess implements KeychainAccessStrategy {
map.remove(key);
}
@Override
public void changePassphrase(String key, CharSequence passphrase) {
map.get(key);
storePassphrase(key, passphrase);
}
@Override
public boolean isSupported() {
return true;

View File

@@ -1,19 +0,0 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.keychain;
import dagger.Component;
import javax.inject.Singleton;
import java.util.Optional;
@Singleton
@Component(modules = KeychainModule.class)
interface TestKeychainComponent {
Optional<KeychainAccess> keychainAccess();
}

View File

@@ -1,17 +0,0 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.keychain;
import java.util.Set;
public class TestKeychainModule extends KeychainModule {
@Override
Set<KeychainAccessStrategy> provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain, LinuxSecretServiceKeychainAccess linKeychain) {
return Set.of(new MapKeychainAccess());
}
}

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.5.2</version>
<version>1.5.7</version>
</parent>
<artifactId>launcher</artifactId>
<name>Cryptomator Launcher</name>

View File

@@ -0,0 +1,30 @@
package org.cryptomator.logging;
import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy;
import ch.qos.logback.core.rolling.TriggeringPolicyBase;
import ch.qos.logback.core.util.FileSize;
import java.io.File;
/**
* Triggers a roll-over either on the first log event or if watched log file reaches a certain size
*
* @param <E> Event type the policy possibly reacts to
*/
public class LaunchAndSizeBasedTriggerinPolicy<E> extends TriggeringPolicyBase<E> {
LaunchBasedTriggeringPolicy<E> launchBasedTriggeringPolicy;
SizeBasedTriggeringPolicy<E> sizeBasedTriggeringPolicy;
public LaunchAndSizeBasedTriggerinPolicy(FileSize threshold) {
this.launchBasedTriggeringPolicy = new LaunchBasedTriggeringPolicy<>();
this.sizeBasedTriggeringPolicy = new SizeBasedTriggeringPolicy<>();
sizeBasedTriggeringPolicy.setMaxFileSize(threshold);
}
@Override
public boolean isTriggeringEvent(File activeFile, E event) {
return launchBasedTriggeringPolicy.isTriggeringEvent(activeFile, event) || sizeBasedTriggeringPolicy.isTriggeringEvent(activeFile, event);
}
}

View File

@@ -9,10 +9,9 @@ import ch.qos.logback.core.Appender;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.FileAppender;
import ch.qos.logback.core.helpers.NOPAppender;
import ch.qos.logback.core.hook.DelayingShutdownHook;
import ch.qos.logback.core.rolling.FixedWindowRollingPolicy;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.util.Duration;
import ch.qos.logback.core.util.FileSize;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.common.Environment;
@@ -33,6 +32,8 @@ public class LoggerModule {
private static final int LOGFILE_ROLLING_MIN = 1;
private static final int LOGFILE_ROLLING_MAX = 9;
private static final String LOG_PATTERN = "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n";
private static final String LOG_MAX_SIZE = "100mb";
static final Map<String, Level> DEFAULT_LOG_LEVELS = Map.of( //
Logger.ROOT_LOGGER_NAME, Level.INFO, //
"org.cryptomator", Level.INFO //
@@ -84,7 +85,7 @@ public class LoggerModule {
appender.setContext(context);
appender.setFile(logDir.resolve(LOGFILE_NAME).toString());
appender.setEncoder(encoder);
LaunchBasedTriggeringPolicy triggeringPolicy = new LaunchBasedTriggeringPolicy();
LaunchAndSizeBasedTriggerinPolicy triggeringPolicy = new LaunchAndSizeBasedTriggerinPolicy(FileSize.valueOf(LOG_MAX_SIZE));
triggeringPolicy.setContext(context);
triggeringPolicy.start();
appender.setTriggeringPolicy(triggeringPolicy);

View File

@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.5.2</version>
<version>1.5.7</version>
<packaging>pom</packaging>
<name>Cryptomator</name>
@@ -24,25 +24,26 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- cryptomator dependencies -->
<cryptomator.cryptofs.version>1.9.9</cryptomator.cryptofs.version>
<cryptomator.jni.version>2.2.2</cryptomator.jni.version>
<cryptomator.cryptofs.version>1.9.12</cryptomator.cryptofs.version>
<cryptomator.jni.version>2.2.3</cryptomator.jni.version>
<cryptomator.fuse.version>1.2.3</cryptomator.fuse.version>
<cryptomator.dokany.version>1.1.14</cryptomator.dokany.version>
<cryptomator.dokany.version>1.1.15</cryptomator.dokany.version>
<cryptomator.webdav.version>1.0.11</cryptomator.webdav.version>
<!-- 3rd party dependencies -->
<javafx.version>14</javafx.version>
<commons-lang3.version>3.9</commons-lang3.version>
<jwt.version>3.10.2</jwt.version>
<commons-lang3.version>3.11</commons-lang3.version>
<secret-service.version>1.0.0</secret-service.version>
<jwt.version>3.10.3</jwt.version>
<easybind.version>1.0.3</easybind.version>
<guava.version>28.2-jre</guava.version>
<dagger.version>2.27</dagger.version>
<guava.version>29.0-jre</guava.version>
<dagger.version>2.22</dagger.version>
<gson.version>2.8.6</gson.version>
<slf4j.version>1.7.30</slf4j.version>
<logback.version>1.2.3</logback.version>
<!-- test dependencies -->
<junit.jupiter.version>5.6.1</junit.jupiter.version>
<junit.jupiter.version>5.6.2</junit.jupiter.version>
<mockito.version>3.3.3</mockito.version>
<hamcrest.version>2.2</hamcrest.version>
</properties>
@@ -79,6 +80,11 @@
</dependency>
<!-- Cryptomator Libs -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>siv-mode</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>cryptofs</artifactId>
@@ -155,7 +161,14 @@
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<!-- Linux System Keychain -->
<dependency>
<groupId>de.swiesend</groupId>
<artifactId>secret-service</artifactId>
<version>${secret-service.version}</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>com.auth0</groupId>

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.5.2</version>
<version>1.5.7</version>
</parent>
<artifactId>ui</artifactId>
<name>Cryptomator GUI</name>

View File

@@ -21,6 +21,7 @@ import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindow;
import org.cryptomator.ui.recoverykey.RecoveryKeyDisplayController;
@@ -51,13 +52,12 @@ public abstract class AddVaultModule {
@Provides
@AddVaultWizardWindow
@AddVaultWizardScoped
static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle) {
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("addvaultwizard.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.getIcons().addAll(windowIcons);
return stage;
}

View File

@@ -9,6 +9,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplication;
import javax.inject.Inject;
import java.util.Optional;
@AddVaultWizardScoped
public class AddVaultSuccessController implements FxController {
@@ -27,7 +28,7 @@ public class AddVaultSuccessController implements FxController {
@FXML
public void unlockAndClose() {
close();
fxApplication.showUnlockWindow(vault.get());
fxApplication.startUnlockWorkflow(vault.get(), Optional.of(window));
}
@FXML

View File

@@ -18,7 +18,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -57,7 +56,7 @@ public class ChooseExistingVaultController implements FxController {
@FXML
public void initialize() {
final String resource = SystemUtils.IS_OS_MAC ? "/select-masterkey-mac.png" : "/select-masterkey-win.png";
final String resource = SystemUtils.IS_OS_MAC ? "/img/select-masterkey-mac.png" : "/img/select-masterkey-win.png";
try (InputStream in = getClass().getResourceAsStream(resource)) {
this.screenshot = new Image(in);
} catch (IOException e) {

View File

@@ -29,6 +29,7 @@ import java.io.File;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ResourceBundle;
@@ -48,7 +49,6 @@ public class CreateNewVaultLocationController implements FxController {
private final StringProperty vaultName;
private final ResourceBundle resourceBundle;
private final BooleanBinding validVaultPath;
private final BooleanBinding invalidVaultPath;
private final BooleanProperty usePresetPath;
private final StringProperty warningText;
@@ -71,7 +71,6 @@ public class CreateNewVaultLocationController implements FxController {
this.vaultName = vaultName;
this.resourceBundle = resourceBundle;
this.validVaultPath = Bindings.createBooleanBinding(this::isValidVaultPath, vaultPath);
this.invalidVaultPath = validVaultPath.not();
this.usePresetPath = new SimpleBooleanProperty();
this.warningText = new SimpleStringProperty();
}
@@ -125,6 +124,9 @@ public class CreateNewVaultLocationController implements FxController {
} catch (FileAlreadyExistsException e) {
LOG.warn("Can not use already existing vault path {}", vaultPath.get());
warningText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
} catch (NoSuchFileException e) {
LOG.warn("At least one path component does not exist of path {}", vaultPath.get());
warningText.set(resourceBundle.getString("addvaultwizard.new.locationDoesNotExist"));
} catch (IOException e) {
LOG.error("Failed to create and delete directory at chosen vault path.", e);
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
@@ -135,7 +137,11 @@ public class CreateNewVaultLocationController implements FxController {
public void chooseCustomVaultPath() {
DirectoryChooser directoryChooser = new DirectoryChooser();
directoryChooser.setTitle(resourceBundle.getString("addvaultwizard.new.directoryPickerTitle"));
directoryChooser.setInitialDirectory(customVaultPath.toFile());
if (Files.exists(customVaultPath)) {
directoryChooser.setInitialDirectory(customVaultPath.toFile());
} else {
directoryChooser.setInitialDirectory(DEFAULT_CUSTOM_VAULT_PATH.toFile());
}
final File file = directoryChooser.showDialog(window);
if (file != null) {
customVaultPath = file.toPath().toAbsolutePath();
@@ -153,12 +159,12 @@ public class CreateNewVaultLocationController implements FxController {
return vaultPath;
}
public BooleanBinding invalidVaultPathProperty() {
return invalidVaultPath;
public BooleanBinding validVaultPathProperty() {
return validVaultPath;
}
public Boolean getInvalidVaultPath() {
return invalidVaultPath.get();
public Boolean getValidVaultPath() {
return validVaultPath.get();
}
public LocationPresets getLocationPresets() {

View File

@@ -2,32 +2,28 @@ package org.cryptomator.ui.changepassword;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.keychain.KeychainManager;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.IOException;
import java.util.ResourceBundle;
import java.nio.CharBuffer;
import java.util.Optional;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
@@ -40,24 +36,27 @@ public class ChangePasswordController implements FxController {
private final Vault vault;
private final ObjectProperty<CharSequence> newPassword;
private final ErrorComponent.Builder errorComponent;
private final Optional<KeychainManager> keychain;
public NiceSecurePasswordField oldPasswordField;
public CheckBox finalConfirmationCheckbox;
public Button finishButton;
@Inject
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty<CharSequence> newPassword, ErrorComponent.Builder errorComponent) {
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty<CharSequence> newPassword, ErrorComponent.Builder errorComponent, Optional<KeychainManager> keychain) {
this.window = window;
this.vault = vault;
this.newPassword = newPassword;
this.errorComponent = errorComponent;
this.keychain = keychain;
}
@FXML
public void initialize() {
BooleanBinding hasNotConfirmedCheckbox = finalConfirmationCheckbox.selectedProperty().not();
BooleanBinding isInvalidNewPassword = Bindings.createBooleanBinding(() -> newPassword.get() == null || newPassword.get().length() == 0, newPassword);
finishButton.disableProperty().bind(hasNotConfirmedCheckbox.or(isInvalidNewPassword));
BooleanBinding checkboxNotConfirmed = finalConfirmationCheckbox.selectedProperty().not();
BooleanBinding oldPasswordFieldEmpty = oldPasswordField.textProperty().isEmpty();
BooleanBinding newPasswordInvalid = Bindings.createBooleanBinding(() -> newPassword.get() == null || newPassword.get().length() == 0, newPassword);
finishButton.disableProperty().bind(checkboxNotConfirmed.or(oldPasswordFieldEmpty).or(newPasswordInvalid));
}
@FXML
@@ -69,8 +68,9 @@ public class ChangePasswordController implements FxController {
public void finish() {
try {
CryptoFileSystemProvider.changePassphrase(vault.getPath(), MASTERKEY_FILENAME, oldPasswordField.getCharacters(), newPassword.get());
LOG.info("Successful changed password for {}", vault.getDisplayableName());
LOG.info("Successfully changed password for {}", vault.getDisplayableName());
window.close();
updatePasswordInSystemkeychain();
} catch (IOException e) {
LOG.error("IO error occured during password change. Unable to perform operation.", e);
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
@@ -81,10 +81,21 @@ public class ChangePasswordController implements FxController {
}
}
private void updatePasswordInSystemkeychain() {
if (keychain.isPresent()) {
try {
keychain.get().changePassphrase(vault.getId(), CharBuffer.wrap(newPassword.get()));
LOG.info("Successfully updated password in system keychain for {}", vault.getDisplayableName());
} catch (KeychainAccessException e) {
LOG.error("Failed to update password in system keychain.", e);
}
}
}
/* Getter/Setter */
public Vault getVault() {
return vault;
}
}

View File

@@ -18,6 +18,7 @@ import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import org.cryptomator.ui.common.StageFactory;
import javax.inject.Named;
import javax.inject.Provider;
@@ -46,13 +47,12 @@ abstract class ChangePasswordModule {
@Provides
@ChangePasswordWindow
@ChangePasswordScoped
static Stage provideStage(@Named("changePasswordOwner") Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
static Stage provideStage(StageFactory factory, @Named("changePasswordOwner") Stage owner, ResourceBundle resourceBundle) {
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("changepassword.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.getIcons().addAll(windowIcons);
return stage;
}

View File

@@ -26,6 +26,7 @@ public class NewPasswordController implements FxController {
public Label passwordStrengthLabel;
public Label passwordMatchLabel;
public FontAwesome5IconView checkmark;
public FontAwesome5IconView warning;
public FontAwesome5IconView cross;
public NewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, ObjectProperty<CharSequence> password) {
@@ -36,11 +37,13 @@ public class NewPasswordController implements FxController {
@FXML
public void initialize() {
passwordStrength.bind(Bindings.createIntegerBinding(() -> strengthRater.computeRate(passwordField.getCharacters()), passwordField.textProperty()));
passwordStrengthLabel.graphicProperty().bind(Bindings.createObjectBinding(this::getIconViewForPasswordStrengthLabel, passwordField.textProperty(), passwordStrength));
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
BooleanBinding passwordsMatch = Bindings.createBooleanBinding(this::hasSamePasswordInBothFields, passwordField.textProperty(), reenterField.textProperty());
BooleanBinding reenterFieldNotEmpty = reenterField.textProperty().isNotEmpty();
passwordStrength.bind(Bindings.createIntegerBinding(() -> strengthRater.computeRate(passwordField.getCharacters()), passwordField.textProperty()));
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
passwordMatchLabel.visibleProperty().bind(reenterFieldNotEmpty);
passwordMatchLabel.graphicProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(checkmark).otherwise(cross));
passwordMatchLabel.textProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(resourceBundle.getString("newPassword.passwordsMatch")).otherwise(resourceBundle.getString("newPassword.passwordsDoNotMatch")));
@@ -49,6 +52,18 @@ public class NewPasswordController implements FxController {
reenterField.textProperty().addListener(this::passwordsDidChange);
}
private FontAwesome5IconView getIconViewForPasswordStrengthLabel() {
if (passwordField.getCharacters().length() == 0) {
return null;
} else if (passwordStrength.intValue() <= -1) {
return cross;
} else if (passwordStrength.intValue() < 3) {
return warning;
} else {
return checkmark;
}
}
private void passwordsDidChange(@SuppressWarnings("unused") Observable observable) {
if (hasSamePasswordInBothFields() && strengthRater.fulfillsMinimumRequirements(passwordField.getCharacters())) {
password.set(passwordField.getCharacters());

View File

@@ -0,0 +1,26 @@
package org.cryptomator.ui.common;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import java.util.function.Consumer;
public class StageFactory {
private final Consumer<Stage> initializer;
public StageFactory(Consumer<Stage> initializer) {
this.initializer = initializer;
}
public Stage create() {
return create(StageStyle.DECORATED);
}
public Stage create(StageStyle stageStyle) {
Stage stage = new Stage(stageStyle);
initializer.accept(stage);
return stage;
}
}

View File

@@ -0,0 +1,51 @@
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.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class UserInteractionLock<E extends Enum> {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private final BooleanProperty awaitingInteraction = new SimpleBooleanProperty();
private volatile E state;
public UserInteractionLock(E initialValue) {
state = initialValue;
}
public void interacted(E result) {
assert Platform.isFxApplicationThread();
lock.lock();
try {
state = result;
awaitingInteraction.set(false);
condition.signal();
} finally {
lock.unlock();
}
}
public E awaitInteraction() throws InterruptedException {
assert !Platform.isFxApplicationThread();
lock.lock();
try {
Platform.runLater(() -> awaitingInteraction.set(true));
condition.await();
return state;
} finally {
lock.unlock();
}
}
public ReadOnlyBooleanProperty awaitingInteraction() {
return awaitingInteraction;
}
}

View File

@@ -4,16 +4,13 @@ import javafx.concurrent.Task;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.keychain.KeychainManager;
import org.cryptomator.ui.fxapp.FxApplicationScoped;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.nio.CharBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
@@ -28,10 +25,10 @@ public class VaultService {
private static final Logger LOG = LoggerFactory.getLogger(VaultService.class);
private final ExecutorService executorService;
private final Optional<KeychainAccess> keychain;
private final Optional<KeychainManager> keychain;
@Inject
public VaultService(ExecutorService executorService, Optional<KeychainAccess> keychain) {
public VaultService(ExecutorService executorService, Optional<KeychainManager> keychain) {
this.executorService = executorService;
this.keychain = keychain;
}
@@ -52,62 +49,6 @@ public class VaultService {
return task;
}
/**
* Attempts to unlock all given vaults in a background thread using passwords stored in the system keychain.
*
* @param vaults The vaults to unlock
* @implNote No-op if no system keychain is present
*/
public void attemptAutoUnlock(Collection<Vault> vaults) {
if (!keychain.isPresent()) {
LOG.debug("No system keychain found. Unable to auto unlock without saved passwords.");
} else {
List<Task<Vault>> unlockTasks = vaults.stream().map(v -> createAutoUnlockTask(v, keychain.get())).collect(Collectors.toList());
Task<Collection<Vault>> runSequentiallyTask = new RunSequentiallyTask(unlockTasks);
executorService.execute(runSequentiallyTask);
}
}
/**
* Creates but doesn't start an auto-unlock task.
*
* @param vault The vault to unlock
* @param keychainAccess The system keychain holding the passphrase for the vault
* @return The task
*/
public Task<Vault> createAutoUnlockTask(Vault vault, KeychainAccess keychainAccess) {
Task<Vault> task = new AutoUnlockVaultTask(vault, keychainAccess);
task.setOnSucceeded(evt -> LOG.info("Auto-unlocked {}", vault.getDisplayableName()));
task.setOnFailed(evt -> LOG.error("Failed to auto-unlock " + vault.getDisplayableName(), evt.getSource().getException()));
return task;
}
/**
* Unlocks a vault in a background thread
*
* @param vault The vault to unlock
* @param passphrase The password to use - wipe this param asap
* @implNote A copy of the passphrase will be made, which is wiped as soon as the task ran.
*/
public void unlock(Vault vault, CharSequence passphrase) {
executorService.execute(createUnlockTask(vault, passphrase));
}
/**
* Creates but doesn't start an unlock task.
*
* @param vault The vault to unlock
* @param passphrase The password to use - wipe this param asap
* @return The task
* @implNote A copy of the passphrase will be made, which is wiped as soon as the task ran.
*/
public Task<Vault> createUnlockTask(Vault vault, CharSequence passphrase) {
Task<Vault> task = new UnlockVaultTask(vault, passphrase);
task.setOnSucceeded(evt -> LOG.info("Unlocked {}", vault.getDisplayableName()));
task.setOnFailed(evt -> LOG.error("Failed to unlock " + vault.getDisplayableName(), evt.getSource().getException()));
return task;
}
/**
* Locks a vault in a background thread.
*
@@ -209,116 +150,6 @@ public class VaultService {
}
}
/**
* A task that runs a list of tasks in their given order
*/
private static class RunSequentiallyTask extends Task<Collection<Vault>> {
private final List<Task<Vault>> tasks;
public RunSequentiallyTask(List<Task<Vault>> tasks) {
this.tasks = List.copyOf(tasks);
}
@Override
protected List<Vault> call() throws ExecutionException, InterruptedException {
List<Vault> completed = new ArrayList<>();
for (Task<Vault> task : tasks) {
task.run();
Vault done = task.get();
completed.add(done);
}
return completed;
}
}
private static class AutoUnlockVaultTask extends Task<Vault> {
private final Vault vault;
private final KeychainAccess keychain;
public AutoUnlockVaultTask(Vault vault, KeychainAccess keychain) {
this.vault = vault;
this.keychain = keychain;
}
@Override
protected Vault call() throws Exception {
char[] storedPw = null;
try {
storedPw = keychain.loadPassphrase(vault.getId());
if (storedPw == null) {
throw new InvalidPassphraseException();
}
vault.unlock(CharBuffer.wrap(storedPw));
} finally {
if (storedPw != null) {
Arrays.fill(storedPw, ' ');
}
}
return vault;
}
@Override
protected void scheduled() {
vault.setState(VaultState.PROCESSING);
}
@Override
protected void succeeded() {
vault.setState(VaultState.UNLOCKED);
}
@Override
protected void failed() {
vault.setState(VaultState.LOCKED);
}
}
private static class UnlockVaultTask extends Task<Vault> {
private final Vault vault;
private final CharBuffer passphrase;
/**
* @param vault The vault to unlock
* @param passphrase The password to use - wipe this param asap
* @implNote A copy of the passphrase will be made, which is wiped as soon as the task ran.
*/
public UnlockVaultTask(Vault vault, CharSequence passphrase) {
this.vault = vault;
this.passphrase = CharBuffer.allocate(passphrase.length());
for (int i = 0; i < passphrase.length(); i++) {
this.passphrase.put(i, passphrase.charAt(i));
}
}
@Override
protected Vault call() throws Exception {
try {
vault.unlock(passphrase);
} finally {
Arrays.fill(passphrase.array(), ' ');
}
return vault;
}
@Override
protected void scheduled() {
vault.setState(VaultState.PROCESSING);
}
@Override
protected void succeeded() {
vault.setState(VaultState.UNLOCKED);
}
@Override
protected void failed() {
vault.setState(VaultState.LOCKED);
}
}
/**
* A task that locks a vault
*/

View File

@@ -0,0 +1,32 @@
package org.cryptomator.ui.common;
import javafx.beans.binding.StringBinding;
import javafx.beans.value.ObservableObjectValue;
/**
* Contains a variety of method to create {@link java.util.function.Function#identity() identity}-bindings
* to facilitate the Weak References used internally in JavaFX's Bindings.
*/
public final class WeakBindings {
/**
* Create a new StringBinding that listens to changes from the given observable without being strongly referenced by it.
*
* @param observable The observable
* @return a StringBinding weakly referenced from the given observable
*/
public static StringBinding bindString(ObservableObjectValue<String> observable) {
return new StringBinding() {
{
bind(observable);
}
@Override
protected String computeValue() {
return observable.get();
}
};
}
}

View File

@@ -11,6 +11,7 @@ public enum FontAwesome5Icon {
COGS("\uF085"), //
COPY("\uF0C5"), //
CROWN("\uF521"), //
EDIT("\uF044"), //
EXCLAMATION("\uF12A"), //
EXCLAMATION_CIRCLE("\uF06A"), //
EXCLAMATION_TRIANGLE("\uF071"), //
@@ -32,10 +33,13 @@ public enum FontAwesome5Icon {
PLUS("\uF067"), //
PRINT("\uF02F"), //
QUESTION("\uF128"), //
REDO("\uF01E"), //
SEARCH("\uF002"), //
SPINNER("\uF110"), //
SYNC("\uF021"), //
TIMES("\uF00D"), //
TRASH("\uF1F8"), //
UNLINK("\uf127"), //
WRENCH("\uF0AD"), //
WINDOW_MINIMIZE("\uF2D1"), //
;

View File

@@ -94,8 +94,8 @@ public class NiceSecurePasswordField extends StackPane {
passwordField.setPassword(password);
}
public void swipe() {
passwordField.swipe();
public void wipe() {
passwordField.wipe();
}
public void selectAll() {

View File

@@ -40,7 +40,7 @@ import java.util.Arrays;
*/
public class SecurePasswordField extends TextField {
private static final char SWIPE_CHAR = ' ';
private static final char WIPE_CHAR = ' ';
private static final int INITIAL_BUFFER_SIZE = 50;
private static final int GROW_BUFFER_SIZE = 50;
private static final String DEFAULT_PLACEHOLDER = "";
@@ -103,7 +103,7 @@ public class SecurePasswordField extends TextField {
if (e.getCode() == KeyCode.CAPS) {
updateCapsLocked();
} else if (SHORTCUT_BACKSPACE.match(e)) {
swipe();
wipe();
}
}
@@ -189,7 +189,7 @@ public class SecurePasswordField extends TextField {
if (length > content.length) {
char[] newContent = new char[length + GROW_BUFFER_SIZE];
System.arraycopy(content, 0, newContent, 0, content.length);
swipe(content);
wipe(content);
this.content = newContent;
}
}
@@ -201,7 +201,7 @@ public class SecurePasswordField extends TextField {
* @implNote The CharSequence will not copy the backing char[].
* Therefore any mutation to the SecurePasswordField's content will mutate or eventually swipe the returned CharSequence.
* @implSpec The CharSequence is usually in <a href="https://www.unicode.org/glossary/#normalization_form_c">NFC</a> representation (unless NFD-encoded char[] is set via {@link #setPassword(char[])}).
* @see #swipe()
* @see #wipe()
*/
@Override
public CharSequence getCharacters() {
@@ -220,7 +220,7 @@ public class SecurePasswordField extends TextField {
buf[i] = password.charAt(i);
}
setPassword(buf);
Arrays.fill(buf, SWIPE_CHAR);
Arrays.fill(buf, WIPE_CHAR);
}
/**
@@ -231,7 +231,7 @@ public class SecurePasswordField extends TextField {
* @param password
*/
public void setPassword(char[] password) {
swipe();
wipe();
content = Arrays.copyOf(password, password.length);
length = password.length;
@@ -242,14 +242,14 @@ public class SecurePasswordField extends TextField {
/**
* Destroys the stored password by overriding each character with a different character.
*/
public void swipe() {
swipe(content);
public void wipe() {
wipe(content);
length = 0;
setText(null);
}
private void swipe(char[] buffer) {
Arrays.fill(buffer, SWIPE_CHAR);
private void wipe(char[] buffer) {
Arrays.fill(buffer, WIPE_CHAR);
}
/* Observable Properties */

View File

@@ -4,8 +4,8 @@ import javafx.beans.property.BooleanProperty;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.keychain.KeychainManager;
import org.cryptomator.ui.common.FxController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -20,14 +20,14 @@ public class ForgetPasswordController implements FxController {
private final Stage window;
private final Vault vault;
private final Optional<KeychainAccess> keychainAccess;
private final Optional<KeychainManager> keychain;
private final BooleanProperty confirmedResult;
@Inject
public ForgetPasswordController(@ForgetPasswordWindow Stage window, @ForgetPasswordWindow Vault vault, Optional<KeychainAccess> keychainAccess, @ForgetPasswordWindow BooleanProperty confirmedResult) {
public ForgetPasswordController(@ForgetPasswordWindow Stage window, @ForgetPasswordWindow Vault vault, Optional<KeychainManager> keychain, @ForgetPasswordWindow BooleanProperty confirmedResult) {
this.window = window;
this.vault = vault;
this.keychainAccess = keychainAccess;
this.keychain = keychain;
this.confirmedResult = confirmedResult;
}
@@ -38,9 +38,9 @@ public class ForgetPasswordController implements FxController {
@FXML
public void finish() {
if (keychainAccess.isPresent()) {
if (keychain.isPresent()) {
try {
keychainAccess.get().deletePassphrase(vault.getId());
keychain.get().deletePassphrase(vault.getId());
LOG.debug("Forgot password for vault {}.", vault.getDisplayableName());
confirmedResult.setValue(true);
} catch (KeychainAccessException e) {

View File

@@ -17,6 +17,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import javax.inject.Named;
import javax.inject.Provider;
@@ -37,13 +38,12 @@ abstract class ForgetPasswordModule {
@Provides
@ForgetPasswordWindow
@ForgetPasswordScoped
static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons, @Named("forgetPasswordOwner") Stage owner) {
Stage stage = new Stage();
static Stage provideStage(StageFactory factory, ResourceBundle resourceBundle, @Named("forgetPasswordOwner") Stage owner) {
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("forgetPassword.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.getIcons().addAll(windowIcons);
return stage;
}

View File

@@ -6,7 +6,6 @@ import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import javafx.stage.Stage;
import org.cryptomator.common.LicenseHolder;
@@ -27,6 +26,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Provider;
import java.awt.desktop.QuitResponse;
import java.util.Optional;
@@ -38,24 +38,26 @@ public class FxApplication extends Application {
private final Settings settings;
private final Lazy<MainWindowComponent> mainWindow;
private final Lazy<PreferencesComponent> preferencesWindow;
private final UnlockComponent.Builder unlockWindowBuilder;
private final QuitComponent.Builder quitWindowBuilder;
private final Provider<UnlockComponent.Builder> unlockWindowBuilderProvider;
private final Provider<QuitComponent.Builder> quitWindowBuilderProvider;
private final Optional<MacFunctions> macFunctions;
private final VaultService vaultService;
private final LicenseHolder licenseHolder;
private final ObservableSet<Stage> visibleStages = FXCollections.observableSet();
private final BooleanBinding hasVisibleStages = Bindings.isNotEmpty(visibleStages);
private final BooleanBinding hasVisibleStages;
private Optional<String> macApperanceObserverIdentifier = Optional.empty();
@Inject
FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, UnlockComponent.Builder unlockWindowBuilder, QuitComponent.Builder quitWindowBuilder, Optional<MacFunctions> macFunctions, VaultService vaultService, LicenseHolder licenseHolder) {
FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Provider<UnlockComponent.Builder> unlockWindowBuilderProvider, Provider<QuitComponent.Builder> quitWindowBuilderProvider, Optional<MacFunctions> macFunctions, VaultService vaultService, LicenseHolder licenseHolder, ObservableSet<Stage> visibleStages) {
this.settings = settings;
this.mainWindow = mainWindow;
this.preferencesWindow = preferencesWindow;
this.unlockWindowBuilder = unlockWindowBuilder;
this.quitWindowBuilder = quitWindowBuilder;
this.unlockWindowBuilderProvider = unlockWindowBuilderProvider;
this.quitWindowBuilderProvider = quitWindowBuilderProvider;
this.macFunctions = macFunctions;
this.vaultService = vaultService;
this.licenseHolder = licenseHolder;
this.hasVisibleStages = Bindings.isNotEmpty(visibleStages);
}
public void start() {
@@ -73,11 +75,6 @@ public class FxApplication extends Application {
throw new UnsupportedOperationException("Use start() instead.");
}
private void addVisibleStage(Stage stage) {
visibleStages.add(stage);
stage.setOnHidden(evt -> visibleStages.remove(stage));
}
private void hasVisibleStagesChanged(@SuppressWarnings("unused") ObservableValue<? extends Boolean> observableValue, @SuppressWarnings("unused") boolean oldValue, boolean newValue) {
if (newValue) {
macFunctions.map(MacFunctions::uiState).ifPresent(MacApplicationUiState::transformToForegroundApplication);
@@ -88,32 +85,28 @@ public class FxApplication extends Application {
public void showPreferencesWindow(SelectedPreferencesTab selectedTab) {
Platform.runLater(() -> {
Stage stage = preferencesWindow.get().showPreferencesWindow(selectedTab);
addVisibleStage(stage);
preferencesWindow.get().showPreferencesWindow(selectedTab);
LOG.debug("Showing Preferences");
});
}
public void showMainWindow() {
Platform.runLater(() -> {
Stage stage = mainWindow.get().showMainWindow();
addVisibleStage(stage);
mainWindow.get().showMainWindow();
LOG.debug("Showing MainWindow");
});
}
public void showUnlockWindow(Vault vault) {
public void startUnlockWorkflow(Vault vault, Optional<Stage> owner) {
Platform.runLater(() -> {
Stage stage = unlockWindowBuilder.vault(vault).build().showUnlockWindow();
addVisibleStage(stage);
unlockWindowBuilderProvider.get().vault(vault).owner(owner).build().startUnlockWorkflow();
LOG.debug("Showing UnlockWindow for {}", vault.getDisplayableName());
});
}
public void showQuitWindow(QuitResponse response) {
Platform.runLater(() -> {
Stage stage = quitWindowBuilder.quitResponse(response).build().showQuitWindow();
addVisibleStage(stage);
quitWindowBuilderProvider.get().quitResponse(response).build().showQuitWindow();
LOG.debug("Showing QuitWindow");
});
}
@@ -123,22 +116,44 @@ public class FxApplication extends Application {
}
private void themeChanged(@SuppressWarnings("unused") ObservableValue<? extends UiTheme> observable, @SuppressWarnings("unused") UiTheme oldValue, UiTheme newValue) {
if (macApperanceObserverIdentifier.isPresent()) {
macFunctions.map(MacFunctions::uiAppearance).ifPresent(uiAppearance -> uiAppearance.removeListener(macApperanceObserverIdentifier.get()));
macApperanceObserverIdentifier = Optional.empty();
}
loadSelectedStyleSheet(newValue);
}
private void loadSelectedStyleSheet(UiTheme desiredTheme) {
UiTheme theme = licenseHolder.isValidLicense() ? desiredTheme : UiTheme.LIGHT;
switch (theme) {
case DARK:
Application.setUserAgentStylesheet(getClass().getResource("/css/dark_theme.css").toString());
macFunctions.map(MacFunctions::uiAppearance).ifPresent(JniException.ignore(MacApplicationUiAppearance::setToDarkAqua));
break;
case LIGHT:
default:
Application.setUserAgentStylesheet(getClass().getResource("/css/light_theme.css").toString());
macFunctions.map(MacFunctions::uiAppearance).ifPresent(JniException.ignore(MacApplicationUiAppearance::setToAqua));
break;
case LIGHT -> setToLightTheme();
case DARK -> setToDarkTheme();
case AUTOMATIC -> {
macFunctions.map(MacFunctions::uiAppearance).ifPresent(uiAppearance -> {
macApperanceObserverIdentifier = Optional.of(uiAppearance.addListener(this::macInterfaceThemeChanged));
});
macInterfaceThemeChanged();
}
}
}
private void macInterfaceThemeChanged() {
macFunctions.map(MacFunctions::uiAppearance).ifPresent(uiAppearance -> {
switch (uiAppearance.getCurrentInterfaceStyle()) {
case LIGHT -> setToLightTheme();
case DARK -> setToDarkTheme();
}
});
}
private void setToLightTheme() {
Application.setUserAgentStylesheet(getClass().getResource("/css/light_theme.css").toString());
macFunctions.map(MacFunctions::uiAppearance).ifPresent(JniException.ignore(MacApplicationUiAppearance::setToAqua));
}
private void setToDarkTheme() {
Application.setUserAgentStylesheet(getClass().getResource("/css/dark_theme.css").toString());
macFunctions.map(MacFunctions::uiAppearance).ifPresent(JniException.ignore(MacApplicationUiAppearance::setToDarkAqua));
}
}

View File

@@ -9,12 +9,13 @@ import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindowComponent;
import org.cryptomator.ui.preferences.PreferencesComponent;
import org.cryptomator.ui.quit.QuitComponent;
@@ -32,8 +33,8 @@ abstract class FxApplicationModule {
@Provides
@FxApplicationScoped
static ObjectProperty<Vault> provideSelectedVault() {
return new SimpleObjectProperty<>();
static ObservableSet<Stage> provideVisibleStages() {
return FXCollections.observableSet();
}
@Provides
@@ -43,16 +44,30 @@ abstract class FxApplicationModule {
if (SystemUtils.IS_OS_MAC) {
return Collections.emptyList();
}
try {
return List.of( //
createImageFromResource("/window_icon_32.png"), //
createImageFromResource("/window_icon_512.png") //
createImageFromResource("/img/window_icon_32.png"), //
createImageFromResource("/img/window_icon_512.png") //
);
} catch (IOException e) {
throw new UncheckedIOException("Failed to load embedded resource.", e);
}
}
@Provides
@FxApplicationScoped
static StageFactory provideStageFactory(@Named("windowIcons") List<Image> windowIcons, ObservableSet<Stage> visibleStages) {
return new StageFactory(stage -> {
stage.getIcons().addAll(windowIcons);
stage.showingProperty().addListener((observableValue, wasShowing, isShowing) -> {
if (isShowing) {
visibleStages.add(stage);
} else {
visibleStages.remove(stage);
}
});
});
}
private static Image createImageFromResource(String resourceName) throws IOException {
try (InputStream in = FxApplicationModule.class.getResourceAsStream(resourceName)) {

View File

@@ -52,19 +52,13 @@ class AppLaunchEventHandler {
private void handleLaunchEvent(boolean hasTrayIcon, AppLaunchEvent event) {
switch (event.getType()) {
case REVEAL_APP:
fxApplicationStarter.get(hasTrayIcon).thenAccept(FxApplication::showMainWindow);
break;
case OPEN_FILE:
fxApplicationStarter.get(hasTrayIcon).thenRun(() -> {
case REVEAL_APP -> fxApplicationStarter.get(hasTrayIcon).thenAccept(FxApplication::showMainWindow);
case OPEN_FILE -> fxApplicationStarter.get(hasTrayIcon).thenRun(() -> {
Platform.runLater(() -> {
event.getPathsToOpen().forEach(this::addVault);
});
});
break;
default:
LOG.warn("Unsupported event type: {}", event.getType());
break;
default -> LOG.warn("Unsupported event type: {}", event.getType());
}
}

View File

@@ -11,6 +11,7 @@ import javax.inject.Singleton;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
@Singleton
public class FxApplicationStarter {
@@ -19,17 +20,19 @@ public class FxApplicationStarter {
private final FxApplicationComponent.Builder fxAppComponent;
private final ExecutorService executor;
private final AtomicBoolean started;
private final CompletableFuture<FxApplication> future;
@Inject
public FxApplicationStarter(FxApplicationComponent.Builder fxAppComponent, ExecutorService executor) {
this.fxAppComponent = fxAppComponent;
this.executor = executor;
this.started = new AtomicBoolean();
this.future = new CompletableFuture<>();
}
public synchronized CompletionStage<FxApplication> get(boolean hasTrayIcon) {
if (!future.isDone()) {
public CompletionStage<FxApplication> get(boolean hasTrayIcon) {
if (!started.getAndSet(true)) {
start(hasTrayIcon);
}
return future;

View File

@@ -62,14 +62,22 @@ public class UiLauncher {
Desktop.getDesktop().addAppEventListener((AppReopenedListener) e -> showMainWindowAsync(hasTrayIcon));
// auto unlock
Collection<Vault> vaultsWithAutoUnlockEnabled = vaults.filtered(v -> v.getVaultSettings().unlockAfterStartup().get());
if (!vaultsWithAutoUnlockEnabled.isEmpty()) {
fxApplicationStarter.get(hasTrayIcon).thenAccept(app -> app.getVaultService().attemptAutoUnlock(vaultsWithAutoUnlockEnabled));
Collection<Vault> vaultsToAutoUnlock = vaults.filtered(this::shouldAttemptAutoUnlock);
if (!vaultsToAutoUnlock.isEmpty()) {
fxApplicationStarter.get(hasTrayIcon).thenAccept(app -> {
for (Vault vault : vaultsToAutoUnlock) {
app.startUnlockWorkflow(vault, Optional.empty());
}
});
}
launchEventHandler.startHandlingLaunchEvents(hasTrayIcon);
}
private boolean shouldAttemptAutoUnlock(Vault vault) {
return vault.isLocked() && vault.getVaultSettings().unlockAfterStartup().get();
}
private void showMainWindowAsync(boolean hasTrayIcon) {
fxApplicationStarter.get(hasTrayIcon).thenAccept(FxApplication::showMainWindow);
}

View File

@@ -1,12 +1,17 @@
package org.cryptomator.ui.mainwindow;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML;
import javafx.scene.input.DragEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent;
@@ -28,15 +33,19 @@ public class MainWindowController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(MainWindowController.class);
private final Stage window;
private final VaultListManager vaultListManager;
private final ReadOnlyObjectProperty<Vault> selectedVault;
private final WrongFileAlertComponent.Builder wrongFileAlert;
private final BooleanProperty draggingOver = new SimpleBooleanProperty();
private final BooleanProperty draggingVaultOver = new SimpleBooleanProperty();
public StackPane root;
@Inject
public MainWindowController(VaultListManager vaultListManager, WrongFileAlertComponent.Builder wrongFileAlert) {
public MainWindowController(@MainWindow Stage window, VaultListManager vaultListManager, ObjectProperty<Vault> selectedVault, WrongFileAlertComponent.Builder wrongFileAlert) {
this.window = window;
this.vaultListManager = vaultListManager;
this.selectedVault = selectedVault;
this.wrongFileAlert = wrongFileAlert;
}
@@ -50,6 +59,14 @@ public class MainWindowController implements FxController {
if (SystemUtils.IS_OS_WINDOWS) {
root.getStyleClass().add("os-windows");
}
window.focusedProperty().addListener(this::mainWindowFocusChanged);
}
private void mainWindowFocusChanged(Observable observable) {
var v = selectedVault.get();
if (v != null) {
VaultListManager.redetermineVaultState(v);
}
}
private void handleDragEvent(DragEvent event) {

View File

@@ -4,16 +4,21 @@ import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent;
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.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.fxapp.FxApplicationScoped;
import org.cryptomator.ui.migration.MigrationComponent;
import org.cryptomator.ui.removevault.RemoveVaultComponent;
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
@@ -28,6 +33,12 @@ import java.util.ResourceBundle;
@Module(subcomponents = {AddVaultWizardComponent.class, MigrationComponent.class, RemoveVaultComponent.class, VaultOptionsComponent.class, WrongFileAlertComponent.class})
abstract class MainWindowModule {
@Provides
@MainWindowScoped
static ObjectProperty<Vault> provideSelectedVault() {
return new SimpleObjectProperty<>();
}
@Provides
@MainWindow
@MainWindowScoped
@@ -38,22 +49,21 @@ abstract class MainWindowModule {
@Provides
@MainWindow
@MainWindowScoped
static Stage provideStage(@Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage(StageStyle.UNDECORATED);
static Stage provideStage(StageFactory factory) {
Stage stage = factory.create(StageStyle.UNDECORATED);
// TODO: min/max values chosen arbitrarily. We might wanna take a look at the user's resolution...
stage.setMinWidth(650);
stage.setMinHeight(440);
stage.setMaxWidth(1000);
stage.setMaxHeight(700);
stage.setTitle("Cryptomator");
stage.getIcons().addAll(windowIcons);
return stage;
}
@Provides
@FxmlScene(FxmlFile.MAIN_WINDOW)
@MainWindowScoped
static Scene provideMainScene(@MainWindow FXMLLoaderFactory fxmlLoaders, MainWindowController mainWindowController, VaultListController vaultListController) {
static Scene provideMainScene(@MainWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/main_window.fxml");
}

View File

@@ -1,14 +1,20 @@
package org.cryptomator.ui.mainwindow;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.keychain.KeychainManager;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
import org.fxmisc.easybind.EasyBind;
import javax.inject.Inject;
import java.util.Optional;
@MainWindowScoped
public class VaultDetailLockedController implements FxController {
@@ -16,24 +22,34 @@ public class VaultDetailLockedController implements FxController {
private final ReadOnlyObjectProperty<Vault> vault;
private final FxApplication application;
private final VaultOptionsComponent.Builder vaultOptionsWindow;
private final Optional<KeychainManager> keychainManagerOptional;
private final Stage mainWindow;
private final BooleanExpression passwordSaved;
@Inject
VaultDetailLockedController(ObjectProperty<Vault> vault, FxApplication application, VaultOptionsComponent.Builder vaultOptionsWindow) {
VaultDetailLockedController(ObjectProperty<Vault> vault, FxApplication application, VaultOptionsComponent.Builder vaultOptionsWindow, Optional<KeychainManager> keychainManagerOptional, @MainWindow Stage mainWindow) {
this.vault = vault;
this.application = application;
this.vaultOptionsWindow = vaultOptionsWindow;
this.keychainManagerOptional = keychainManagerOptional;
this.mainWindow = mainWindow;
if (keychainManagerOptional.isPresent()) {
this.passwordSaved = BooleanExpression.booleanExpression(EasyBind.select(vault).selectObject(v -> keychainManagerOptional.get().getPassphraseStoredProperty(v.getId())));
} else {
this.passwordSaved = new SimpleBooleanProperty(false);
}
}
@FXML
public void unlock() {
application.showUnlockWindow(vault.get());
application.startUnlockWorkflow(vault.get(), Optional.of(mainWindow));
}
@FXML
public void showVaultOptions() {
vaultOptionsWindow.vault(vault.get()).build().showVaultOptionsWindow();
}
/* Getter/Setter */
public ReadOnlyObjectProperty<Vault> vaultProperty() {
@@ -43,4 +59,14 @@ public class VaultDetailLockedController implements FxController {
public Vault getVault() {
return vault.get();
}
public BooleanExpression passwordSavedProperty() {
return passwordSaved;
}
public boolean isPasswordSaved() {
if (keychainManagerOptional.isPresent() && vault.get() != null) {
return keychainManagerOptional.get().getPassphraseStoredProperty(vault.get().getId()).get();
} else return false;
}
}

View File

@@ -1,20 +1,55 @@
package org.cryptomator.ui.mainwindow;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.fxml.FXML;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.removevault.RemoveVaultComponent;
import javax.inject.Inject;
import java.io.File;
import java.util.ResourceBundle;
@MainWindowScoped
public class VaultDetailMissingVaultController implements FxController {
private final ReadOnlyObjectProperty<Vault> vault;
private final ObjectProperty<Vault> vault;
private final RemoveVaultComponent.Builder removeVault;
private final ResourceBundle resourceBundle;
private final Stage window;
@Inject
public VaultDetailMissingVaultController(ObjectProperty<Vault> vault) {
public VaultDetailMissingVaultController(ObjectProperty<Vault> vault, RemoveVaultComponent.Builder removeVault, ResourceBundle resourceBundle, @MainWindow Stage window) {
this.vault = vault;
this.removeVault = removeVault;
this.resourceBundle = resourceBundle;
this.window = window;
}
@FXML
public void recheck() {
VaultListManager.redetermineVaultState(vault.get());
}
@FXML
void didClickRemoveVault() {
removeVault.vault(vault.get()).build().showRemoveVault();
}
@FXML
void changeLocation() {
// copied from ChooseExistingVaultController class
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(resourceBundle.getString("addvaultwizard.existing.filePickerTitle"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
File masterkeyFile = fileChooser.showOpenDialog(window);
if (masterkeyFile != null) {
vault.get().getVaultSettings().path().setValue(masterkeyFile.toPath().toAbsolutePath().getParent());
recheck();
}
}
}

View File

@@ -64,21 +64,7 @@ public class VaultListController implements FxController {
if (newValue == null) {
return;
}
VaultState reportedState = newValue.getState();
switch (reportedState) {
case LOCKED, NEEDS_MIGRATION, MISSING:
try {
VaultState determinedState = VaultListManager.determineVaultState(newValue.getPath());
newValue.setState(determinedState);
} catch (IOException e) {
LOG.warn("Failed to determine vault state for " + newValue.getPath(), e);
newValue.setState(VaultState.ERROR);
newValue.setLastKnownException(e);
}
break;
case ERROR, UNLOCKED, PROCESSING:
break; // no-op
}
VaultListManager.redetermineVaultState(newValue);
}
@FXML

View File

@@ -42,4 +42,5 @@ public class WelcomeController implements FxController {
public boolean isNoVaultPresent() {
return noVaultPresent.get();
}
}

View File

@@ -17,6 +17,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindow;
import javax.inject.Named;
@@ -38,13 +39,12 @@ abstract class MigrationModule {
@Provides
@MigrationWindow
@MigrationScoped
static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle) {
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("migration.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.getIcons().addAll(windowIcons);
return stage;
}

View File

@@ -22,8 +22,8 @@ import org.cryptomator.cryptofs.migration.Migrators;
import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener;
import org.cryptomator.cryptofs.migration.api.MigrationProgressListener;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.keychain.KeychainManager;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
@@ -55,7 +55,7 @@ public class MigrationRunController implements FxController {
private final Vault vault;
private final ExecutorService executor;
private final ScheduledExecutorService scheduler;
private final Optional<KeychainAccess> keychainAccess;
private final Optional<KeychainManager> keychain;
private final ObjectProperty<FileSystemCapabilityChecker.Capability> missingCapability;
private final ErrorComponent.Builder errorComponent;
private final Lazy<Scene> startScene;
@@ -69,13 +69,13 @@ public class MigrationRunController implements FxController {
public NiceSecurePasswordField passwordField;
@Inject
public MigrationRunController(@MigrationWindow Stage window, @MigrationWindow Vault vault, ExecutorService executor, ScheduledExecutorService scheduler, Optional<KeychainAccess> keychainAccess, @Named("capabilityErrorCause") ObjectProperty<FileSystemCapabilityChecker.Capability> missingCapability, @FxmlScene(FxmlFile.MIGRATION_START) Lazy<Scene> startScene, @FxmlScene(FxmlFile.MIGRATION_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.MIGRATION_CAPABILITY_ERROR) Lazy<Scene> capabilityErrorScene, @FxmlScene(FxmlFile.MIGRATION_IMPOSSIBLE) Lazy<Scene> impossibleScene, ErrorComponent.Builder errorComponent) {
public MigrationRunController(@MigrationWindow Stage window, @MigrationWindow Vault vault, ExecutorService executor, ScheduledExecutorService scheduler, Optional<KeychainManager> keychain, @Named("capabilityErrorCause") ObjectProperty<FileSystemCapabilityChecker.Capability> missingCapability, @FxmlScene(FxmlFile.MIGRATION_START) Lazy<Scene> startScene, @FxmlScene(FxmlFile.MIGRATION_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.MIGRATION_CAPABILITY_ERROR) Lazy<Scene> capabilityErrorScene, @FxmlScene(FxmlFile.MIGRATION_IMPOSSIBLE) Lazy<Scene> impossibleScene, ErrorComponent.Builder errorComponent) {
this.window = window;
this.vault = vault;
this.executor = executor;
this.scheduler = scheduler;
this.keychainAccess = keychainAccess;
this.keychain = keychain;
this.missingCapability = missingCapability;
this.errorComponent = errorComponent;
this.startScene = startScene;
@@ -88,7 +88,7 @@ public class MigrationRunController implements FxController {
}
public void initialize() {
if (keychainAccess.isPresent()) {
if (keychain.isPresent()) {
loadStoredPassword();
}
migrationButtonDisabled.bind(vault.stateProperty().isNotEqualTo(VaultState.NEEDS_MIGRATION).or(passwordField.textProperty().isEmpty()));
@@ -121,7 +121,7 @@ public class MigrationRunController implements FxController {
} else {
LOG.info("Migration of '{}' succeeded.", vault.getDisplayableName());
vault.setState(VaultState.LOCKED);
passwordField.swipe();
passwordField.wipe();
window.setScene(successScene.get());
}
}).onError(InvalidPassphraseException.class, e -> {
@@ -167,10 +167,10 @@ public class MigrationRunController implements FxController {
}
private void loadStoredPassword() {
assert keychainAccess.isPresent();
assert keychain.isPresent();
char[] storedPw = null;
try {
storedPw = keychainAccess.get().loadPassphrase(vault.getId());
storedPw = keychain.get().loadPassphrase(vault.getId());
if (storedPw != null) {
passwordField.setPassword(storedPw);
passwordField.selectRange(storedPw.length, storedPw.length);

View File

@@ -1,15 +1,14 @@
package org.cryptomator.ui.migration;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.mainwindow.MainWindow;
import javax.inject.Inject;
import java.util.Optional;
@MigrationScoped
public class MigrationSuccessController implements FxController {
@@ -17,18 +16,20 @@ public class MigrationSuccessController implements FxController {
private final FxApplication fxApplication;
private final Stage window;
private final Vault vault;
private final Stage mainWindow;
@Inject
MigrationSuccessController(FxApplication fxApplication, @MigrationWindow Stage window, @MigrationWindow Vault vault) {
MigrationSuccessController(FxApplication fxApplication, @MigrationWindow Stage window, @MigrationWindow Vault vault, @MainWindow Stage mainWindow) {
this.fxApplication = fxApplication;
this.window = window;
this.vault = vault;
this.mainWindow = mainWindow;
}
@FXML
public void unlockAndClose() {
close();
fxApplication.showUnlockWindow(vault);
fxApplication.startUnlockWorkflow(vault, Optional.of(mainWindow));
}
@FXML

View File

@@ -5,23 +5,27 @@ import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.control.TextArea;
import org.cryptomator.common.LicenseHolder;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.UiTheme;
import org.cryptomator.ui.common.FxController;
import javax.inject.Inject;
@PreferencesScoped
public class DonationKeyPreferencesController implements FxController {
private static final String DONATION_URI = "https://store.cryptomator.org/desktop";
private final Application application;
private final LicenseHolder licenseHolder;
private final Settings settings;
public TextArea donationKeyField;
@Inject
DonationKeyPreferencesController(Application application, LicenseHolder licenseHolder) {
DonationKeyPreferencesController(Application application, LicenseHolder licenseHolder, Settings settings) {
this.application = application;
this.licenseHolder = licenseHolder;
this.settings = settings;
}
@FXML
@@ -32,6 +36,9 @@ public class DonationKeyPreferencesController implements FxController {
private void registrationKeyChanged(@SuppressWarnings("unused") ObservableValue<? extends String> observable, @SuppressWarnings("unused") String oldValue, String newValue) {
licenseHolder.validateAndStoreLicense(newValue);
if (!licenseHolder.isValidLicense()) {
settings.theme().set(UiTheme.LIGHT);
}
}
@FXML

View File

@@ -1,5 +1,6 @@
package org.cryptomator.ui.preferences;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue;
@@ -12,6 +13,7 @@ import javafx.scene.control.RadioButton;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.util.StringConverter;
import org.cryptomator.common.Environment;
import org.cryptomator.common.LicenseHolder;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.UiTheme;
@@ -22,6 +24,7 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
@PreferencesScoped
@@ -35,6 +38,9 @@ public class GeneralPreferencesController implements FxController {
private final ObjectProperty<SelectedPreferencesTab> selectedTabProperty;
private final LicenseHolder licenseHolder;
private final ExecutorService executor;
private final ResourceBundle resourceBundle;
private final Application application;
private final Environment environment;
public ChoiceBox<UiTheme> themeChoiceBox;
public CheckBox startHiddenCheckbox;
public CheckBox debugModeCheckbox;
@@ -44,20 +50,26 @@ public class GeneralPreferencesController implements FxController {
public RadioButton nodeOrientationRtl;
@Inject
GeneralPreferencesController(Settings settings, @Named("trayMenuSupported") boolean trayMenuSupported, Optional<AutoStartStrategy> autoStartStrategy, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ExecutorService executor) {
GeneralPreferencesController(Settings settings, @Named("trayMenuSupported") boolean trayMenuSupported, Optional<AutoStartStrategy> autoStartStrategy, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ExecutorService executor, ResourceBundle resourceBundle, Application application, Environment environment) {
this.settings = settings;
this.trayMenuSupported = trayMenuSupported;
this.autoStartStrategy = autoStartStrategy;
this.selectedTabProperty = selectedTabProperty;
this.licenseHolder = licenseHolder;
this.executor = executor;
this.resourceBundle = resourceBundle;
this.application = application;
this.environment = environment;
}
@FXML
public void initialize() {
themeChoiceBox.getItems().addAll(UiTheme.values());
themeChoiceBox.getItems().addAll(UiTheme.applicableValues());
if (!themeChoiceBox.getItems().contains(settings.theme().get())) {
settings.theme().set(UiTheme.LIGHT);
}
themeChoiceBox.valueProperty().bindBidirectional(settings.theme());
themeChoiceBox.setConverter(new UiThemeConverter());
themeChoiceBox.setConverter(new UiThemeConverter(resourceBundle));
startHiddenCheckbox.selectedProperty().bindBidirectional(settings.startHidden());
@@ -112,13 +124,24 @@ public class GeneralPreferencesController implements FxController {
selectedTabProperty.set(SelectedPreferencesTab.DONATION_KEY);
}
@FXML
public void showLogfileDirectory() {
environment.getLogDir().ifPresent(logDirPath -> application.getHostServices().showDocument(logDirPath.toUri().toString()));
}
/* Helper classes */
private static class UiThemeConverter extends StringConverter<UiTheme> {
private final ResourceBundle resourceBundle;
UiThemeConverter(ResourceBundle resourceBundle) {
this.resourceBundle = resourceBundle;
}
@Override
public String toString(UiTheme impl) {
return impl.getDisplayName();
return resourceBundle.getString(impl.getDisplayName());
}
@Override

View File

@@ -15,6 +15,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import javax.inject.Named;
import javax.inject.Provider;
@@ -41,11 +42,10 @@ abstract class PreferencesModule {
@Provides
@PreferencesWindow
@PreferencesScoped
static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
static Stage provideStage(StageFactory factory, ResourceBundle resourceBundle) {
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("preferences.title"));
stage.setResizable(false);
stage.getIcons().addAll(windowIcons);
return stage;
}

View File

@@ -25,7 +25,7 @@ public class VolumePreferencesController implements FxController {
private final Settings settings;
private final BooleanBinding showWebDavSettings;
private final BooleanBinding showWebDavScheme;
public ChoiceBox<VolumeImpl> volumeTypeChoicBox;
public ChoiceBox<VolumeImpl> volumeTypeChoiceBox;
public TextField webDavPortField;
public Button changeWebDavPortButton;
public ChoiceBox<WebDavUrlScheme> webDavUrlSchemeChoiceBox;
@@ -38,9 +38,12 @@ public class VolumePreferencesController implements FxController {
}
public void initialize() {
volumeTypeChoicBox.getItems().addAll(Volume.getCurrentSupportedAdapters());
volumeTypeChoicBox.valueProperty().bindBidirectional(settings.preferredVolumeImpl());
volumeTypeChoicBox.setConverter(new VolumeImplConverter());
volumeTypeChoiceBox.getItems().addAll(Volume.getCurrentSupportedAdapters());
if (!volumeTypeChoiceBox.getItems().contains(settings.preferredVolumeImpl().get())) {
settings.preferredVolumeImpl().set(VolumeImpl.WEBDAV);
}
volumeTypeChoiceBox.valueProperty().bindBidirectional(settings.preferredVolumeImpl());
volumeTypeChoiceBox.setConverter(new VolumeImplConverter());
webDavPortField.setText(String.valueOf(settings.port().get()));
changeWebDavPortButton.visibleProperty().bind(settings.port().asString().isNotEqualTo(webDavPortField.textProperty()));

View File

@@ -17,6 +17,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import javax.inject.Named;
import javax.inject.Provider;
@@ -37,12 +38,11 @@ abstract class QuitModule {
@Provides
@QuitWindow
@QuitScoped
static Stage provideStage(@Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
static Stage provideStage(StageFactory factory) {
Stage stage = factory.create();
stage.setMinWidth(300);
stage.setMinHeight(100);
stage.initModality(Modality.APPLICATION_MODAL);
stage.getIcons().addAll(windowIcons);
return stage;
}

View File

@@ -21,6 +21,7 @@ import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import org.cryptomator.ui.common.StageFactory;
import javax.inject.Named;
import javax.inject.Provider;
@@ -41,13 +42,12 @@ abstract class RecoveryKeyModule {
@Provides
@RecoveryKeyWindow
@RecoveryKeyScoped
static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons, @Named("keyRecoveryOwner") Stage owner) {
Stage stage = new Stage();
static Stage provideStage(StageFactory factory, ResourceBundle resourceBundle, @Named("keyRecoveryOwner") Stage owner) {
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("recoveryKey.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.getIcons().addAll(windowIcons);
return stage;
}

View File

@@ -17,6 +17,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindow;
import javax.inject.Named;
@@ -38,13 +39,12 @@ abstract class RemoveVaultModule {
@Provides
@RemoveVaultWindow
@RemoveVaultScoped
static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle) {
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("removeVault.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.getIcons().addAll(windowIcons);
return stage;
}

View File

@@ -47,7 +47,7 @@ public class TrayIconController {
trayMenuController.initTrayMenu();
}
public void macInterfaceThemeChanged() {
private void macInterfaceThemeChanged() {
trayIcon.setImage(imageFactory.loadImage());
}

View File

@@ -30,13 +30,13 @@ class TrayImageFactory {
.map(MacApplicationUiAppearance::getCurrentInterfaceStyle) //
.orElse(MacApplicationUiInterfaceStyle.LIGHT);
return switch (interfaceStyle) {
case DARK -> "/tray_icon_mac_white.png";
case LIGHT -> "/tray_icon_mac_black.png";
case DARK -> "/img/tray_icon_mac_white.png";
case LIGHT -> "/img/tray_icon_mac_black.png";
};
}
private String getWinOrLinuxResourceName() {
return "/tray_icon.png";
return "/img/tray_icon.png";
}
}

View File

@@ -15,6 +15,7 @@ import java.awt.PopupMenu;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.EventObject;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.function.Consumer;
@@ -103,7 +104,7 @@ class TrayMenuController {
}
private void unlockVault(Vault vault) {
fxApplicationStarter.get(true).thenAccept(app -> app.showUnlockWindow(vault));
fxApplicationStarter.get(true).thenAccept(app -> app.startUnlockWorkflow(vault, Optional.empty()));
}
private void lockVault(Vault vault) {

View File

@@ -6,37 +6,38 @@
package org.cryptomator.ui.unlock;
import dagger.BindsInstance;
import dagger.Lazy;
import dagger.Subcomponent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.common.vaults.Vault;
import javax.inject.Named;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
@UnlockScoped
@Subcomponent(modules = {UnlockModule.class})
public interface UnlockComponent {
@UnlockWindow
Stage window();
ExecutorService defaultExecutorService();
@FxmlScene(FxmlFile.UNLOCK)
Lazy<Scene> scene();
UnlockWorkflow unlockWorkflow();
default Stage showUnlockWindow() {
Stage stage = window();
stage.setScene(scene().get());
stage.show();
return stage;
default Future<Boolean> startUnlockWorkflow() {
UnlockWorkflow workflow = unlockWorkflow();
defaultExecutorService().submit(workflow);
return workflow;
}
@Subcomponent.Builder
interface Builder {
@BindsInstance
Builder vault(@UnlockWindow Vault vault);
@BindsInstance
Builder owner(@Named("unlockWindowOwner") Optional<Stage> owner);
UnlockComponent build();
}

View File

@@ -1,39 +1,41 @@
package org.cryptomator.ui.unlock;
import dagger.Lazy;
import javafx.animation.Animation;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
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;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContentDisplay;
import javafx.scene.image.ImageView;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.keychain.KeychainManager;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.common.WeakBindings;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.NotDirectoryException;
import javax.inject.Named;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@UnlockScoped
public class UnlockController implements FxController {
@@ -42,142 +44,165 @@ public class UnlockController implements FxController {
private final Stage window;
private final Vault vault;
private final ExecutorService executor;
private final ObjectBinding<ContentDisplay> unlockButtonState;
private final Optional<KeychainAccess> keychainAccess;
private final VaultService vaultService;
private final Lazy<Scene> successScene;
private final Lazy<Scene> invalidMountPointScene;
private final ErrorComponent.Builder errorComponent;
private final AtomicReference<char[]> password;
private final AtomicBoolean savePassword;
private final Optional<char[]> savedPassword;
private final UserInteractionLock<UnlockModule.PasswordEntry> passwordEntryLock;
private final ForgetPasswordComponent.Builder forgetPassword;
private final Optional<KeychainManager> keychain;
private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay;
private final BooleanBinding userInteractionDisabled;
private final BooleanProperty unlockButtonDisabled;
private final StringBinding vaultName;
public NiceSecurePasswordField passwordField;
public CheckBox savePassword;
public CheckBox savePasswordCheckbox;
public ImageView face;
public ImageView leftArm;
public ImageView rightArm;
public ImageView legs;
public ImageView body;
public Animation unlockAnimation;
@Inject
public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor, Optional<KeychainAccess> keychainAccess, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, ErrorComponent.Builder errorComponent, ForgetPasswordComponent.Builder forgetPassword) {
public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<UnlockModule.PasswordEntry> passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, Optional<KeychainManager> keychain) {
this.window = window;
this.vault = vault;
this.executor = executor;
this.unlockButtonState = Bindings.createObjectBinding(this::getUnlockButtonState, vault.stateProperty());
this.keychainAccess = keychainAccess;
this.vaultService = vaultService;
this.successScene = successScene;
this.invalidMountPointScene = invalidMountPointScene;
this.errorComponent = errorComponent;
this.password = password;
this.savePassword = savePassword;
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.displayableNameProperty());
this.window.setOnCloseRequest(windowEvent -> cancel());
}
@FXML
public void initialize() {
if (keychainAccess.isPresent()) {
loadStoredPassword();
} else {
savePassword.setSelected(false);
savePasswordCheckbox.setSelected(savedPassword.isPresent());
if (password.get() != null) {
passwordField.setPassword(password.get());
}
unlockButtonDisabled.bind(vault.stateProperty().isNotEqualTo(VaultState.LOCKED).or(passwordField.textProperty().isEmpty()));
unlockButtonDisabled.bind(userInteractionDisabled.or(passwordField.textProperty().isEmpty()));
var leftArmTranslation = new Translate(24, 0);
var leftArmRotation = new Rotate(60, 16, 30, 0);
var leftArmRetracted = new KeyValue(leftArmTranslation.xProperty(), 24);
var leftArmExtended = new KeyValue(leftArmTranslation.xProperty(), 0.0);
var leftArmHorizontal = new KeyValue(leftArmRotation.angleProperty(), 60, Interpolator.EASE_OUT);
var leftArmHanging = new KeyValue(leftArmRotation.angleProperty(), 0);
leftArm.getTransforms().setAll(leftArmTranslation, leftArmRotation);
var rightArmTranslation = new Translate(-24, 0);
var rightArmRotation = new Rotate(60, 48, 30, 0);
var rightArmRetracted = new KeyValue(rightArmTranslation.xProperty(), -24);
var rightArmExtended = new KeyValue(rightArmTranslation.xProperty(), 0.0);
var rightArmHorizontal = new KeyValue(rightArmRotation.angleProperty(), -60);
var rightArmHanging = new KeyValue(rightArmRotation.angleProperty(), 0, Interpolator.EASE_OUT);
rightArm.getTransforms().setAll(rightArmTranslation, rightArmRotation);
var legsRetractedY = new KeyValue(legs.scaleYProperty(), 0);
var legsExtendedY = new KeyValue(legs.scaleYProperty(), 1, Interpolator.EASE_OUT);
var legsRetractedX = new KeyValue(legs.scaleXProperty(), 0);
var legsExtendedX = new KeyValue(legs.scaleXProperty(), 1, Interpolator.EASE_OUT);
legs.setScaleY(0);
legs.setScaleX(0);
var faceHidden = new KeyValue(face.opacityProperty(), 0.0);
var faceVisible = new KeyValue(face.opacityProperty(), 1.0, Interpolator.LINEAR);
face.setOpacity(0);
unlockAnimation = new Timeline(
new KeyFrame(Duration.ZERO, leftArmRetracted, leftArmHorizontal, rightArmRetracted, rightArmHorizontal, legsRetractedY, legsRetractedX, faceHidden),
new KeyFrame(Duration.millis(200), leftArmExtended, leftArmHorizontal, rightArmRetracted, rightArmHorizontal),
new KeyFrame(Duration.millis(400), leftArmExtended, leftArmHanging, rightArmExtended, rightArmHorizontal),
new KeyFrame(Duration.millis(600), leftArmExtended, leftArmHanging, rightArmExtended, rightArmHanging),
new KeyFrame(Duration.millis(800), legsExtendedY, legsExtendedX, faceHidden),
new KeyFrame(Duration.millis(1000), faceVisible)
);
passwordEntryLock.awaitingInteraction().addListener(observable -> stopUnlockAnimation());
}
@FXML
public void cancel() {
LOG.debug("Unlock canceled by user.");
window.close();
passwordEntryLock.interacted(UnlockModule.PasswordEntry.CANCELED);
}
@FXML
public void unlock() {
LOG.trace("UnlockController.unlock()");
CharSequence password = passwordField.getCharacters();
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(UnlockModule.PasswordEntry.PASSWORD_ENTERED);
startUnlockAnimation();
}
private void startUnlockAnimation() {
leftArm.setVisible(true);
rightArm.setVisible(true);
legs.setVisible(true);
face.setVisible(true);
unlockAnimation.playFromStart();
}
Task<Vault> task = vaultService.createUnlockTask(vault, password);
passwordField.setDisable(true);
task.setOnSucceeded(event -> {
passwordField.setDisable(false);
if (keychainAccess.isPresent() && savePassword.isSelected()) {
try {
keychainAccess.get().storePassphrase(vault.getId(), password);
} catch (KeychainAccessException e) {
LOG.error("Failed to store passphrase in system keychain.", e);
}
}
passwordField.swipe();
LOG.info("Unlock of '{}' succeeded.", vault.getDisplayableName());
window.setScene(successScene.get());
});
task.setOnFailed(event -> {
passwordField.setDisable(false);
if (task.getException() instanceof InvalidPassphraseException) {
Animations.createShakeWindowAnimation(window).play();
passwordField.selectAll();
passwordField.requestFocus();
} else if (task.getException() instanceof NotDirectoryException || task.getException() instanceof DirectoryNotEmptyException) {
LOG.error("Unlock failed. Mount point not an empty directory: {}", task.getException().getMessage());
window.setScene(invalidMountPointScene.get());
} else {
LOG.error("Unlock failed for technical reasons.", task.getException());
errorComponent.cause(task.getException()).window(window).returnToScene(window.getScene()).build().showErrorScene();
}
});
executor.execute(task);
private void stopUnlockAnimation() {
unlockAnimation.stop();
leftArm.setVisible(false);
rightArm.setVisible(false);
legs.setVisible(false);
face.setVisible(false);
}
/* Save Password */
@FXML
private void didClickSavePasswordCheckbox() {
if (!savePassword.isSelected() && hasStoredPassword()) {
forgetPassword.vault(vault).owner(window).build().showForgetPassword().thenAccept(forgotten -> savePassword.setSelected(!forgotten));
}
}
private void loadStoredPassword() {
assert keychainAccess.isPresent();
char[] storedPw = null;
try {
storedPw = keychainAccess.get().loadPassphrase(vault.getId());
if (storedPw != null) {
savePassword.setSelected(true);
passwordField.setPassword(storedPw);
passwordField.selectRange(storedPw.length, storedPw.length);
}
} catch (KeychainAccessException e) {
LOG.error("Failed to load entry from system keychain.", e);
} finally {
if (storedPw != null) {
Arrays.fill(storedPw, ' ');
}
}
}
private boolean hasStoredPassword() {
char[] storedPw = null;
try {
storedPw = keychainAccess.get().loadPassphrase(vault.getId());
return storedPw != null;
} catch (KeychainAccessException e) {
return false;
} finally {
if (storedPw != null) {
Arrays.fill(storedPw, ' ');
}
savePassword.set(savePasswordCheckbox.isSelected());
if (!savePasswordCheckbox.isSelected() && savedPassword.isPresent()) {
forgetPassword.vault(vault).owner(window).build().showForgetPassword().thenAccept(forgotten -> savePasswordCheckbox.setSelected(!forgotten));
}
}
/* Getter/Setter */
public Vault getVault() {
return vault;
public String getVaultName() {
return vaultName.get();
}
public StringBinding vaultNameProperty() {
return vaultName;
}
public ObjectBinding<ContentDisplay> unlockButtonStateProperty() {
return unlockButtonState;
public ObjectBinding<ContentDisplay> unlockButtonContentDisplayProperty() {
return unlockButtonContentDisplay;
}
public ContentDisplay getUnlockButtonState() {
return switch (vault.getState()) {
case PROCESSING -> ContentDisplay.LEFT;
default -> ContentDisplay.TEXT_ONLY;
};
public ContentDisplay getUnlockButtonContentDisplay() {
return passwordEntryLock.awaitingInteraction().get() ? ContentDisplay.TEXT_ONLY : ContentDisplay.LEFT;
}
public BooleanBinding userInteractionDisabledProperty() {
return userInteractionDisabled;
}
public boolean isUserInteractionDisabled() {
return userInteractionDisabled.get();
}
public ReadOnlyBooleanProperty unlockButtonDisabledProperty() {
@@ -189,6 +214,6 @@ public class UnlockController implements FxController {
}
public boolean isKeychainAccessAvailable() {
return keychainAccess.isPresent();
return keychain.isPresent();
}
}

View File

@@ -33,7 +33,7 @@ public class UnlockInvalidMountPointController implements FxController {
/* Getter/Setter */
public String getMountPoint() {
return vault.getVaultSettings().getIndividualMountPath().orElse("AUTO");
return vault.getVaultSettings().getCustomMountPath().orElse("AUTO");
}
}

View File

@@ -5,27 +5,71 @@ import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.keychain.KeychainManager;
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.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Named;
import javax.inject.Provider;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@Module(subcomponents = {ForgetPasswordComponent.class})
abstract class UnlockModule {
private static final Logger LOG = LoggerFactory.getLogger(UnlockModule.class);
public enum PasswordEntry {PASSWORD_ENTERED, CANCELED}
@Provides
@UnlockScoped
static UserInteractionLock<PasswordEntry> providePasswordEntryLock() {
return new UserInteractionLock<>(null);
}
@Provides
@Named("savedPassword")
@UnlockScoped
static Optional<char[]> provideStoredPassword(Optional<KeychainManager> keychain, @UnlockWindow Vault vault) {
return keychain.map(k -> {
try {
return k.loadPassphrase(vault.getId());
} catch (KeychainAccessException e) {
LOG.error("Failed to load entry from system keychain.", e);
return null;
}
});
}
@Provides
@UnlockScoped
static AtomicReference<char[]> providePassword(@Named("savedPassword") Optional<char[]> storedPassword) {
return new AtomicReference(storedPassword.orElse(null));
}
@Provides
@Named("savePassword")
@UnlockScoped
static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional<char[]> storedPassword) {
return new AtomicBoolean(storedPassword.isPresent());
}
@Provides
@UnlockWindow
@UnlockScoped
@@ -36,12 +80,16 @@ abstract class UnlockModule {
@Provides
@UnlockWindow
@UnlockScoped
static Stage provideStage(@UnlockWindow Vault vault, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
static Stage provideStage(StageFactory factory, @UnlockWindow Vault vault, @Named("unlockWindowOwner") Optional<Stage> owner) {
Stage stage = factory.create();
stage.setTitle(vault.getDisplayableName());
stage.setResizable(false);
stage.initModality(Modality.APPLICATION_MODAL);
stage.getIcons().addAll(windowIcons);
if (owner.isPresent()) {
stage.initOwner(owner.get());
stage.initModality(Modality.WINDOW_MODAL);
} else {
stage.initModality(Modality.APPLICATION_MODAL);
}
return stage;
}

View File

@@ -7,8 +7,10 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContentDisplay;
import javafx.stage.Stage;
import org.cryptomator.common.settings.WhenUnlocked;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.VaultService;
@@ -30,6 +32,8 @@ public class UnlockSuccessController implements FxController {
private final ObjectProperty<ContentDisplay> revealButtonState;
private final BooleanProperty revealButtonDisabled;
public CheckBox rememberChoiceCheckbox;
@Inject
public UnlockSuccessController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor, VaultService vaultService) {
this.window = window;
@@ -44,6 +48,9 @@ public class UnlockSuccessController implements FxController {
public void close() {
LOG.trace("UnlockSuccessController.close()");
window.close();
if (rememberChoiceCheckbox.isSelected()) {
vault.getVaultSettings().actionAfterUnlock().setValue(WhenUnlocked.IGNORE);
}
}
@FXML
@@ -64,6 +71,9 @@ public class UnlockSuccessController implements FxController {
revealButtonDisabled.set(false);
});
executor.execute(revealTask);
if (rememberChoiceCheckbox.isSelected()) {
vault.getVaultSettings().actionAfterUnlock().setValue(WhenUnlocked.REVEAL);
}
}
/* Getter/Setter */

View File

@@ -0,0 +1,198 @@
package org.cryptomator.ui.unlock;
import dagger.Lazy;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.Window;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.keychain.KeychainManager;
import org.cryptomator.ui.common.Animations;
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.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.unlock.UnlockModule.PasswordEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.IOException;
import java.nio.CharBuffer;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.FileSystemException;
import java.nio.file.NotDirectoryException;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/**
* A multi-step task that consists of background activities as well as user interaction.
* <p>
* This class runs the unlock process and controls when to display which UI.
*/
@UnlockScoped
public class UnlockWorkflow extends Task<Boolean> {
private static final Logger LOG = LoggerFactory.getLogger(UnlockWorkflow.class);
private final Stage window;
private final Vault vault;
private final VaultService vaultService;
private final AtomicReference<char[]> password;
private final AtomicBoolean savePassword;
private final Optional<char[]> savedPassword;
private final UserInteractionLock<PasswordEntry> passwordEntryLock;
private final Optional<KeychainManager> keychain;
private final Lazy<Scene> unlockScene;
private final Lazy<Scene> successScene;
private final Lazy<Scene> invalidMountPointScene;
private final ErrorComponent.Builder errorComponent;
@Inject
UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<PasswordEntry> passwordEntryLock, Optional<KeychainManager> keychain, @FxmlScene(FxmlFile.UNLOCK) Lazy<Scene> unlockScene, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, ErrorComponent.Builder errorComponent) {
this.window = window;
this.vault = vault;
this.vaultService = vaultService;
this.password = password;
this.savePassword = savePassword;
this.savedPassword = savedPassword;
this.passwordEntryLock = passwordEntryLock;
this.keychain = keychain;
this.unlockScene = unlockScene;
this.successScene = successScene;
this.invalidMountPointScene = invalidMountPointScene;
this.errorComponent = errorComponent;
}
@Override
protected Boolean call() throws InterruptedException, IOException, Volume.VolumeException {
try {
if (attemptUnlock()) {
handleSuccess();
return true;
} else {
cancel(false); // set Tasks state to cancelled
return false;
}
} catch (NotDirectoryException | DirectoryNotEmptyException e) {
handleInvalidMountPoint(e);
throw e; // rethrow to trigger correct exception handling in Task
} catch (CryptoException | Volume.VolumeException | IOException e) {
handleGenericError(e);
throw e; // rethrow to trigger correct exception handling in Task
} finally {
wipePassword(password.get());
wipePassword(savedPassword.orElse(null));
}
}
private boolean attemptUnlock() throws InterruptedException, IOException, Volume.VolumeException {
boolean proceed = password.get() != null || askForPassword(false) == PasswordEntry.PASSWORD_ENTERED;
while (proceed) {
try {
vault.unlock(CharBuffer.wrap(password.get()));
return true;
} catch (InvalidPassphraseException e) {
proceed = askForPassword(true) == PasswordEntry.PASSWORD_ENTERED;
}
}
return false;
}
private PasswordEntry askForPassword(boolean animateShake) throws InterruptedException {
Platform.runLater(() -> {
window.setScene(unlockScene.get());
window.show();
Window owner = window.getOwner();
if (owner != null) {
window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2);
window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2);
} else {
window.centerOnScreen();
}
if (animateShake) {
Animations.createShakeWindowAnimation(window).play();
}
});
return passwordEntryLock.awaitInteraction();
}
private void handleSuccess() {
LOG.info("Unlock of '{}' succeeded.", vault.getDisplayableName());
if (savePassword.get()) {
savePasswordToSystemkeychain();
}
switch (vault.getVaultSettings().actionAfterUnlock().get()) {
case ASK -> Platform.runLater(() -> {
window.setScene(successScene.get());
window.show();
});
case REVEAL -> {
Platform.runLater(window::close);
vaultService.reveal(vault);
}
case IGNORE -> Platform.runLater(window::close);
}
}
private void savePasswordToSystemkeychain() {
if (keychain.isPresent()) {
try {
keychain.get().storePassphrase(vault.getId(), CharBuffer.wrap(password.get()));
} catch (KeychainAccessException e) {
LOG.error("Failed to store passphrase in system keychain.", e);
}
}
}
private void handleInvalidMountPoint(FileSystemException e) {
LOG.error("Unlock failed. Mount point not an empty directory: {}", e.getMessage());
Platform.runLater(() -> {
window.setScene(invalidMountPointScene.get());
});
}
private void handleGenericError(Exception e) {
LOG.error("Unlock failed for technical reasons.", e);
Platform.runLater(() -> {
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
});
}
private void wipePassword(char[] pw) {
if (pw != null) {
Arrays.fill(pw, ' ');
}
}
@Override
protected void scheduled() {
vault.setState(VaultState.PROCESSING);
}
@Override
protected void succeeded() {
vault.setState(VaultState.UNLOCKED);
}
@Override
protected void failed() {
vault.setState(VaultState.LOCKED);
}
@Override
protected void cancelled() {
vault.setState(VaultState.LOCKED);
}
}

View File

@@ -2,24 +2,56 @@ package org.cryptomator.ui.vaultoptions;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox;
import javafx.util.StringConverter;
import org.cryptomator.common.settings.UiTheme;
import org.cryptomator.common.settings.WhenUnlocked;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import javax.inject.Inject;
import java.util.ResourceBundle;
@VaultOptionsScoped
public class GeneralVaultOptionsController implements FxController {
private final Vault vault;
private final ResourceBundle resourceBundle;
public CheckBox unlockOnStartupCheckbox;
public ChoiceBox<WhenUnlocked> actionAfterUnlockChoiceBox;
@Inject
GeneralVaultOptionsController(@VaultOptionsWindow Vault vault) {
GeneralVaultOptionsController(@VaultOptionsWindow Vault vault, ResourceBundle resourceBundle) {
this.vault = vault;
this.resourceBundle = resourceBundle;
}
@FXML
public void initialize() {
unlockOnStartupCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().unlockAfterStartup());
actionAfterUnlockChoiceBox.getItems().addAll(WhenUnlocked.values());
actionAfterUnlockChoiceBox.valueProperty().bindBidirectional(vault.getVaultSettings().actionAfterUnlock());
actionAfterUnlockChoiceBox.setConverter(new WhenUnlockedConverter(resourceBundle));
}
private static class WhenUnlockedConverter extends StringConverter<WhenUnlocked> {
private final ResourceBundle resourceBundle;
public WhenUnlockedConverter(ResourceBundle resourceBundle) {
this.resourceBundle = resourceBundle;
}
@Override
public String toString(WhenUnlocked obj) {
return resourceBundle.getString(obj.getDisplayName());
}
@Override
public WhenUnlocked fromString(String string) {
throw new UnsupportedOperationException();
}
}
}

View File

@@ -1,13 +1,19 @@
package org.cryptomator.ui.vaultoptions;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.keychain.KeychainManager;
import org.cryptomator.ui.changepassword.ChangePasswordComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
import javax.inject.Inject;
import java.util.Optional;
@VaultOptionsScoped
public class MasterkeyOptionsController implements FxController {
@@ -16,13 +22,20 @@ public class MasterkeyOptionsController implements FxController {
private final Stage window;
private final ChangePasswordComponent.Builder changePasswordWindow;
private final RecoveryKeyComponent.Builder recoveryKeyWindow;
private final Optional<KeychainManager> keychainManagerOptional;
private final BooleanExpression passwordSaved;
@Inject
MasterkeyOptionsController(@VaultOptionsWindow Vault vault, @VaultOptionsWindow Stage window, ChangePasswordComponent.Builder changePasswordWindow, RecoveryKeyComponent.Builder recoveryKeyWindow) {
MasterkeyOptionsController(@VaultOptionsWindow Vault vault, @VaultOptionsWindow Stage window, ChangePasswordComponent.Builder changePasswordWindow, RecoveryKeyComponent.Builder recoveryKeyWindow, Optional<KeychainManager> keychainManagerOptional) {
this.vault = vault;
this.window = window;
this.changePasswordWindow = changePasswordWindow;
this.recoveryKeyWindow = recoveryKeyWindow;
this.keychainManagerOptional = keychainManagerOptional;
if (keychainManagerOptional.isPresent()) {
this.passwordSaved = Bindings.createBooleanBinding(this::isPasswordSaved, keychainManagerOptional.get().getPassphraseStoredProperty(vault.getId()));
} else this.passwordSaved = new SimpleBooleanProperty(false);
}
@FXML
@@ -39,4 +52,20 @@ public class MasterkeyOptionsController implements FxController {
public void showRecoverVaultDialogue() {
recoveryKeyWindow.vault(vault).owner(window).build().showRecoveryKeyRecoverWindow();
}
@FXML
public void removePasswordFromKeychain() throws KeychainAccessException {
keychainManagerOptional.get().deletePassphrase(vault.getId());
window.close();
}
public BooleanExpression passwordSavedProperty() {
return passwordSaved;
}
public boolean isPasswordSaved() {
if (keychainManagerOptional.isPresent() && vault != null) {
return keychainManagerOptional.get().getPassphraseStoredProperty(vault.getId()).get();
} else return false;
}
}

View File

@@ -82,10 +82,10 @@ public class MountOptionsController implements FxController {
// mount point options:
mountPoint.selectedToggleProperty().addListener(this::toggleMountPoint);
driveLetterSelection.getItems().addAll(windowsDriveLetters.getAllDriveLetters());
driveLetterSelection.setConverter(new WinDriveLetterLabelConverter(windowsDriveLetters));
driveLetterSelection.setConverter(new WinDriveLetterLabelConverter(windowsDriveLetters, resourceBundle));
driveLetterSelection.setValue(vault.getVaultSettings().winDriveLetter().get());
if (vault.getVaultSettings().usesIndividualMountPath().get()) {
if (vault.getVaultSettings().useCustomMountPath().get()) {
mountPoint.selectToggle(mountPointCustomDir);
} else if (!Strings.isNullOrEmpty(vault.getVaultSettings().winDriveLetter().get())) {
mountPoint.selectToggle(mountPointWinDriveLetter);
@@ -93,7 +93,7 @@ public class MountOptionsController implements FxController {
mountPoint.selectToggle(mountPointAuto);
}
vault.getVaultSettings().usesIndividualMountPath().bind(mountPoint.selectedToggleProperty().isEqualTo(mountPointCustomDir));
vault.getVaultSettings().useCustomMountPath().bind(mountPoint.selectedToggleProperty().isEqualTo(mountPointCustomDir));
vault.getVaultSettings().winDriveLetter().bind( //
Bindings.when(mountPoint.selectedToggleProperty().isEqualTo(mountPointWinDriveLetter)) //
.then(driveLetterSelection.getSelectionModel().selectedItemProperty()) //
@@ -126,14 +126,14 @@ public class MountOptionsController implements FxController {
}
File file = directoryChooser.showDialog(window);
if (file != null) {
vault.getVaultSettings().individualMountPath().set(file.getAbsolutePath());
vault.getVaultSettings().customMountPath().set(file.getAbsolutePath());
} else {
vault.getVaultSettings().individualMountPath().set(null);
vault.getVaultSettings().customMountPath().set(null);
}
}
private void toggleMountPoint(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
if (mountPointCustomDir.equals(newValue) && Strings.isNullOrEmpty(vault.getVaultSettings().individualMountPath().get())) {
if (mountPointCustomDir.equals(newValue) && Strings.isNullOrEmpty(vault.getVaultSettings().customMountPath().get())) {
chooseCustomMountPoint();
}
}
@@ -144,15 +144,17 @@ public class MountOptionsController implements FxController {
private static class WinDriveLetterLabelConverter extends StringConverter<String> {
private final Set<String> occupiedDriveLetters;
private final ResourceBundle resourceBundle;
WinDriveLetterLabelConverter(WindowsDriveLetters windowsDriveLetters) {
WinDriveLetterLabelConverter(WindowsDriveLetters windowsDriveLetters, ResourceBundle resourceBundle) {
this.occupiedDriveLetters = windowsDriveLetters.getOccupiedDriveLetters();
this.resourceBundle = resourceBundle;
}
@Override
public String toString(String driveLetter) {
if (occupiedDriveLetters.contains(driveLetter)) {
return driveLetter + ": (occupied)"; // TODO localize?
return driveLetter + ": (" + resourceBundle.getString("vaultOptions.mount.winDriveLetterOccupied") + ")";
} else {
return driveLetter + ":";
}
@@ -184,11 +186,11 @@ public class MountOptionsController implements FxController {
}
public StringProperty customMountPathProperty() {
return vault.getVaultSettings().individualMountPath();
return vault.getVaultSettings().customMountPath();
}
public String getCustomMountPath() {
return vault.getVaultSettings().individualMountPath().get();
return vault.getVaultSettings().customMountPath().get();
}
}

View File

@@ -16,6 +16,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindow;
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
@@ -38,15 +39,14 @@ abstract class VaultOptionsModule {
@Provides
@VaultOptionsWindow
@VaultOptionsScoped
static Stage provideStage(@MainWindow Stage owner, @VaultOptionsWindow Vault vault, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
static Stage provideStage(StageFactory factory, @MainWindow Stage owner, @VaultOptionsWindow Vault vault) {
Stage stage = factory.create();
stage.setTitle(vault.getDisplayableName());
stage.setResizable(true);
stage.setMinWidth(400);
stage.setMinHeight(300);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.getIcons().addAll(windowIcons);
return stage;
}

View File

@@ -30,7 +30,7 @@ public class WrongFileAlertController implements FxController {
@FXML
public void initialize() {
final String resource = SystemUtils.IS_OS_MAC ? "/vault-volume-mac.png" : "/vault-volume-win.png";
final String resource = SystemUtils.IS_OS_MAC ? "/img/vault-volume-mac.png" : "/img/vault-volume-win.png";
try (InputStream in = getClass().getResourceAsStream(resource)) {
this.screenshot = new Image(in);
} catch (IOException e) {

View File

@@ -14,6 +14,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindow;
import javax.inject.Named;
@@ -35,13 +36,12 @@ abstract class WrongFileAlertModule {
@Provides
@WrongFileAlertWindow
@WrongFileAlertScoped
static Stage provideStage(@MainWindow Stage mainWindow, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
static Stage provideStage(StageFactory factory, @MainWindow Stage mainWindow, ResourceBundle resourceBundle) {
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("wrongFileAlert.title"));
stage.setResizable(false);
stage.initOwner(mainWindow);
stage.initModality(Modality.WINDOW_MODAL);
stage.getIcons().addAll(windowIcons);
return stage;
}

View File

@@ -143,6 +143,10 @@
-fx-fill: RED_5;
}
.glyph-icon-orange {
-fx-fill: ORANGE_5;
}
/*******************************************************************************
* *
* Main Window *
@@ -631,6 +635,10 @@
-fx-background-color: TEXT_FILL;
}
.check-box:selected:disabled > .box > .mark {
-fx-background-color: TEXT_FILL_MUTED;
}
/*******************************************************************************
* *
* RadioButton *

View File

@@ -143,6 +143,10 @@
-fx-fill: RED_5;
}
.glyph-icon-orange {
-fx-fill: ORANGE_5;
}
/*******************************************************************************
* *
* Main Window *
@@ -630,6 +634,10 @@
-fx-background-color: TEXT_FILL;
}
.check-box:selected:disabled > .box > .mark {
-fx-background-color: TEXT_FILL_MUTED;
}
/*******************************************************************************
* *
* RadioButton *

View File

@@ -60,7 +60,7 @@
<ButtonBar buttonMinWidth="120" buttonOrder="B+X">
<buttons>
<Button text="%generic.button.back" ButtonBar.buttonData="BACK_PREVIOUS" onAction="#back"/>
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" onAction="#next" defaultButton="true" disable="${controller.invalidVaultPath}"/>
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" onAction="#next" defaultButton="true" disable="${!controller.validVaultPath}"/>
</buttons>
</ButtonBar>
</children>

View File

@@ -21,7 +21,7 @@
<Region VBox.vgrow="ALWAYS"/>
<ImageView VBox.vgrow="ALWAYS" fitHeight="128" preserveRatio="true" smooth="true" cache="true">
<Image url="/bot.png"/>
<Image url="/img/bot/bot.png"/>
</ImageView>
<Region VBox.vgrow="ALWAYS"/>

View File

@@ -13,18 +13,19 @@
alignment="CENTER_LEFT">
<fx:define>
<FontAwesome5IconView fx:id="checkmark" styleClass="glyph-icon-primary" glyph="CHECK"/>
<FontAwesome5IconView fx:id="warning" styleClass="glyph-icon-orange" glyph="EXCLAMATION_TRIANGLE"/>
<FontAwesome5IconView fx:id="cross" styleClass="glyph-icon-red" glyph="TIMES"/>
</fx:define>
<children>
<Label text="%newPassword.promptText" labelFor="$passwordField"/>
<NiceSecurePasswordField fx:id="passwordField"/>
<PasswordStrengthIndicator spacing="6" prefHeight="6" strength="${controller.passwordStrength}"/>
<Label fx:id="passwordStrengthLabel" styleClass="label-muted" alignment="CENTER_RIGHT" maxWidth="Infinity"/>
<Label fx:id="passwordStrengthLabel" styleClass="label-muted" alignment="CENTER_RIGHT" maxWidth="Infinity" graphicTextGap="6"/>
<Region/>
<Label text="%newPassword.reenterPassword" labelFor="$reenterField"/>
<NiceSecurePasswordField fx:id="reenterField"/>
<Label fx:id="passwordMatchLabel" styleClass="label-muted" alignment="CENTER_RIGHT" maxWidth="Infinity" graphicTextGap="6" contentDisplay="LEFT" />
<Label fx:id="passwordMatchLabel" styleClass="label-muted" alignment="CENTER_RIGHT" maxWidth="Infinity" graphicTextGap="6"/>
</children>
</VBox>

View File

@@ -18,7 +18,7 @@
<children>
<HBox spacing="12" VBox.vgrow="NEVER">
<ImageView VBox.vgrow="ALWAYS" fitHeight="64" preserveRatio="true" smooth="true" cache="true">
<Image url="/bot.png"/>
<Image url="/img/bot/bot.png"/>
</ImageView>
<VBox spacing="3" HBox.hgrow="ALWAYS" alignment="CENTER_LEFT">
<FormattedLabel styleClass="label-large" format="Cryptomator %s" arg1="${controller.applicationVersion}"/>

View File

@@ -34,7 +34,10 @@
<CheckBox fx:id="startHiddenCheckbox" text="%preferences.general.startHidden" visible="${controller.trayMenuSupported}" managed="${controller.trayMenuSupported}"/>
<CheckBox fx:id="debugModeCheckbox" text="%preferences.general.debugLogging"/>
<HBox spacing="6" alignment="CENTER_LEFT">
<CheckBox fx:id="debugModeCheckbox" text="%preferences.general.debugLogging"/>
<Hyperlink styleClass="hyperlink-underline" text="%preferences.general.debugDirectory" onAction="#showLogfileDirectory"/>
</HBox>
<CheckBox fx:id="autoStartCheckbox" text="%preferences.general.autoStart" visible="${controller.autoStartSupported}" managed="${controller.autoStartSupported}" onAction="#toggleAutoStart"/>
</children>

View File

@@ -17,7 +17,7 @@
<children>
<HBox spacing="6" alignment="CENTER_LEFT">
<Label text="%preferences.volume.type"/>
<ChoiceBox fx:id="volumeTypeChoicBox"/>
<ChoiceBox fx:id="volumeTypeChoiceBox"/>
</HBox>
<HBox spacing="6" alignment="CENTER_LEFT" visible="${controller.showWebDavSettings}">

View File

@@ -5,6 +5,10 @@
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.ProgressIndicator?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import org.cryptomator.ui.controls.FormattedLabel?>
<?import org.cryptomator.ui.controls.NiceSecurePasswordField?>
@@ -19,17 +23,40 @@
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<VBox spacing="6">
<FormattedLabel format="%unlock.passwordPrompt" arg1="${controller.vault.displayableName}" wrapText="true"/>
<NiceSecurePasswordField fx:id="passwordField"/>
<CheckBox fx:id="savePassword" text="%unlock.savePassword" onAction="#didClickSavePasswordCheckbox" disable="${controller.vault.processing}" visible="${controller.keychainAccessAvailable}"/>
</VBox>
<HBox spacing="12" VBox.vgrow="ALWAYS">
<StackPane alignment="CENTER" HBox.hgrow="NEVER">
<ImageView VBox.vgrow="ALWAYS" fitWidth="64" preserveRatio="true" smooth="true" cache="true" fx:id="face" visible="false">
<Image url="/img/bot/face.png"/>
</ImageView>
<ImageView VBox.vgrow="ALWAYS" fitWidth="64" preserveRatio="true" smooth="true" cache="true" fx:id="leftArm" visible="false">
<Image url="/img/bot/arm-l.png"/>
</ImageView>
<ImageView VBox.vgrow="ALWAYS" fitWidth="64" preserveRatio="true" smooth="true" cache="true" fx:id="rightArm" visible="false">
<Image url="/img/bot/arm-r.png"/>
</ImageView>
<ImageView VBox.vgrow="ALWAYS" fitWidth="64" preserveRatio="true" smooth="true" cache="true" fx:id="legs" visible="false">
<Image url="/img/bot/legs.png"/>
</ImageView>
<ImageView VBox.vgrow="ALWAYS" fitWidth="64" preserveRatio="true" smooth="true" cache="true" fx:id="body">
<Image url="/img/bot/body.png"/>
</ImageView>
</StackPane>
<VBox spacing="6" HBox.hgrow="ALWAYS">
<FormattedLabel format="%unlock.passwordPrompt" arg1="${controller.vaultName}" wrapText="true"/>
<NiceSecurePasswordField fx:id="passwordField" disable="${controller.userInteractionDisabled}"/>
<CheckBox fx:id="savePasswordCheckbox" text="%unlock.savePassword" onAction="#didClickSavePasswordCheckbox" disable="${controller.userInteractionDisabled}" visible="${controller.keychainAccessAvailable}"/>
</VBox>
</HBox>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<ButtonBar buttonMinWidth="120" buttonOrder="+CI">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel" disable="${controller.vault.processing}"/>
<Button text="%unlock.unlockBtn" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#unlock" contentDisplay="${controller.unlockButtonState}" disable="${controller.unlockButtonDisabled}">
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel" disable="${controller.userInteractionDisabled}"/>
<Button text="%unlock.unlockBtn" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#unlock" contentDisplay="${controller.unlockButtonContentDisplay}" disable="${controller.unlockButtonDisabled}">
<graphic>
<ProgressIndicator progress="-1" prefWidth="12" prefHeight="12"/>
</graphic>

View File

@@ -10,6 +10,7 @@
<?import javafx.scene.shape.Circle?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import org.cryptomator.ui.controls.FormattedLabel?>
<?import javafx.scene.control.CheckBox?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.unlock.UnlockSuccessController"
@@ -26,7 +27,10 @@
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="CHECK" glyphSize="24"/>
</StackPane>
<FormattedLabel format="%unlock.success.message" arg1="${controller.vault.displayableName}" wrapText="true" HBox.hgrow="ALWAYS"/>
<VBox spacing="6">
<FormattedLabel format="%unlock.success.message" arg1="${controller.vault.displayableName}" wrapText="true" HBox.hgrow="ALWAYS"/>
<CheckBox text="%unlock.success.rememberChoice" fx:id="rememberChoiceCheckbox"/>
</VBox>
</HBox>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">

View File

@@ -14,7 +14,7 @@
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.mainwindow.VaultDetailController"
minWidth="300"
spacing="36">
spacing="60">
<padding>
<Insets topRightBottomLeft="24"/>
</padding>

View File

@@ -1,8 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Hyperlink?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.VBox?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<VBox xmlns="http://javafx.com/javafx"
@@ -10,11 +12,15 @@
fx:controller="org.cryptomator.ui.mainwindow.VaultDetailLockedController"
alignment="TOP_CENTER"
spacing="9">
<padding>
<Insets topRightBottomLeft="24"/>
</padding>
<children>
<Button styleClass="button-large" text="%main.vaultDetail.unlockBtn" minWidth="120" onAction="#unlock" defaultButton="${controller.vault.locked}">
<Button styleClass="button-large" text="%main.vaultDetail.unlockBtn" minWidth="120" onAction="#unlock" defaultButton="${controller.vault.locked}" visible="${!controller.passwordSaved}"
managed="${!controller.passwordSaved}">
<graphic>
<FontAwesome5IconView glyph="KEY" glyphSize="15"/>
</graphic>
</Button>
<Button styleClass="button-large" text="%main.vaultDetail.unlockNowBtn" minWidth="120" onAction="#unlock" defaultButton="${controller.vault.locked}" visible="${controller.passwordSaved}"
managed="${controller.passwordSaved}">
<graphic>
<FontAwesome5IconView glyph="KEY" glyphSize="15"/>
</graphic>
@@ -24,5 +30,13 @@
<FontAwesome5IconView glyph="COG"/>
</graphic>
</Hyperlink>
<Region VBox.vgrow="ALWAYS"/>
<HBox alignment="CENTER_RIGHT" spacing="6">
<Label styleClass="label-small,label-muted" text="%main.vaultDetail.passwordSavedInKeychain" visible="${controller.passwordSaved}">
<graphic>
<FontAwesome5IconView styleClass="glyph-icon-muted" glyph="LOCK"/>
</graphic>
</Label>
</HBox>
</children>
</VBox>

View File

@@ -1,4 +1,5 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
@@ -8,21 +9,36 @@
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.mainwindow.VaultDetailMissingVaultController"
alignment="TOP_CENTER"
spacing="9">
<padding>
<Insets topRightBottomLeft="24"/>
</padding>
spacing="24">
<children>
<StackPane alignment="CENTER">
<Circle styleClass="glyph-icon-primary" radius="48"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="FILE" glyphSize="48"/>
<FontAwesome5IconView styleClass="glyph-icon-primary" glyph="SEARCH" glyphSize="24">
<StackPane.margin>
<Insets top="12"/>
</StackPane.margin>
</FontAwesome5IconView>
</StackPane>
<Label text="%main.vaultDetail.missing.info" wrapText="true"/>
<VBox spacing="9" alignment="CENTER">
<StackPane alignment="CENTER">
<Circle styleClass="glyph-icon-primary" radius="48"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="FILE" glyphSize="48"/>
<FontAwesome5IconView styleClass="glyph-icon-primary" glyph="SEARCH" glyphSize="24">
<StackPane.margin>
<Insets top="12"/>
</StackPane.margin>
</FontAwesome5IconView>
</StackPane>
<Label text="%main.vaultDetail.missing.info" wrapText="true"/>
</VBox>
<VBox spacing="6" alignment="CENTER">
<Button text="%main.vaultDetail.missing.recheck" minWidth="120" onAction="#recheck">
<graphic>
<FontAwesome5IconView glyph="REDO"/>
</graphic>
</Button>
<Button text="%main.vaultDetail.missing.changeLocation" minWidth="120" onAction="#changeLocation">
<graphic>
<FontAwesome5IconView glyph="EDIT"/>
</graphic>
</Button>
<Button text="%main.vaultDetail.missing.remove" minWidth="120" onAction="#didClickRemoveVault">
<graphic>
<FontAwesome5IconView glyph="TRASH"/>
</graphic>
</Button>
</VBox>
</children>
</VBox>

Some files were not shown because too many files have changed in this diff Show More