Compare commits

..

88 Commits
1.5.1 ... 1.5.5

Author SHA1 Message Date
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
Sebastian Stenzel
94af8bd15a Merge branch 'release/1.5.2' 2020-04-29 16:47:22 +02:00
Sebastian Stenzel
4712c4f593 preparing 1.5.2 2020-04-29 16:34:06 +02:00
Sebastian Stenzel
42c856fc6f New Crowdin translations (#1135)
[ci skip]
2020-04-29 16:15:24 +02:00
Sebastian Stenzel
3618c4b8db Merge branch 'feature/NewMigrationFlow' into develop 2020-04-29 16:14:44 +02:00
Sebastian Stenzel
bb681fa6d9 fixes #1116, at least properly handle errors if the underlying file system doesn't support migration 2020-04-29 14:59:16 +02:00
Armin Schrenk
22e3840caa Update README.md 2020-04-29 12:02:15 +02:00
Sebastian Stenzel
461ec3ca43 Simpler solution to 8aa3da1 and 1702c6a2 2020-04-28 21:17:41 +02:00
Armin Schrenk
8aa3da14a4 fixing bug where unlock dialog is not shown anymore 2020-04-28 19:43:10 +02:00
Sebastian Stenzel
670d4a165c fixes #1134, fixes #1116
references #1144
2020-04-28 18:58:25 +02:00
Tobias Hagemann
9a9b19e6e2 fixed error message and removed wrong comment [ci skip] 2020-04-28 17:48:48 +02:00
Tobias Hagemann
206ba4c69a improved migration impossible 2020-04-28 17:46:45 +02:00
Tobias Hagemann
1702c6a243 hide/disable interactive elements when vault is processing 2020-04-28 17:05:00 +02:00
Armin Schrenk
8d2fe2fc03 Improving vault migration impossible dialogue 2020-04-28 12:26:38 +02:00
Armin Schrenk
4864eb3204 integrate migratiion impossible scene to migration workflow 2020-04-28 12:16:07 +02:00
Sebastian Stenzel
5721b63135 fixes #1115 2020-04-28 10:36:35 +02:00
Sebastian Stenzel
c99e0ea656 Determine path length limitations during unlock 2020-04-28 09:48:25 +02:00
Armin Schrenk
b1dc983d6b adding cryptofs to pom and add implementation notes to migrate dialogue 2020-04-27 18:06:37 +02:00
Armin Schrenk
808223d58e refined migration not possible dialogue 2020-04-27 16:59:09 +02:00
Armin Schrenk
4d5f6cbb52 update migration dialogue 2020-04-27 13:17:31 +02:00
Armin Schrenk
fcdbc7a6cc Add migration impossible dialogue 2020-04-27 11:58:45 +02:00
Armin Schrenk
113b745050 add missing translation keys 2020-04-27 10:55:34 +02:00
Sebastian Stenzel
e4cde7f66f Merge pull request #1143 from jellemdekker/feature/keycombo_clear_password_filed
Allow password field to be cleared with Ctrl/Command + backspace, fixes #885
2020-04-24 15:27:07 +02:00
jellemdekker
f6c834fee2 Merged if-statements. 2020-04-24 15:23:08 +02:00
jellemdekker
96990788d9 Indent using tabs instead of spaces. 2020-04-24 15:22:33 +02:00
jellemdekker
c204ed2601 Allow password field to be cleared with Ctrl/Command + backspace, implements #885. 2020-04-24 14:30:19 +02:00
Tobias Hagemann
b882296c19 fixes #1123 2020-04-24 11:21:59 +02:00
Tobias Hagemann
186ed5c115 set title to vault name in unlock screen for improved compatibility with password managers 2020-04-23 22:16:23 +02:00
Sebastian Stenzel
db29513376 one more switch expression 2020-04-23 14:36:41 +02:00
Sebastian Stenzel
0469e99baa Updated build dependencies 2020-04-23 14:22:24 +02:00
Sebastian Stenzel
37fcae8f0e replaced some "old" switch statements by switch expressions 2020-04-23 14:21:41 +02:00
Sebastian Stenzel
2902479fc1 Speed up build, make cache more efficient 2020-04-21 14:27:42 +02:00
Sebastian Stenzel
5ef7a3e76d Updated Build Badge 2020-04-21 14:15:56 +02:00
Sebastian Stenzel
041ce2504c Merge branch 'master' into develop 2020-04-21 14:12:00 +02:00
160 changed files with 2485 additions and 987 deletions

View File

@@ -4,25 +4,24 @@ on:
[push]
jobs:
test:
name: Run Tests
build:
name: Build and Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 14
uses: actions/setup-java@v1
- uses: actions/setup-java@v1
with:
java-version: 14
- uses: actions/cache@v1
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ github.run_id }}
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Ensure to use tagged version
run: mvn versions:set --file main/pom.xml -DnewVersion=${GITHUB_REF##*/} # use shell parameter expansion to strip of 'refs/tags'
if: startsWith(github.ref, 'refs/tags/')
- name: Build with Maven
- name: Build and Test
run: mvn -B install --file main/pom.xml -Pcoverage
- name: Run Codacy Coverage Reporter
run: |
@@ -34,25 +33,7 @@ jobs:
$JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar final
env:
CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
assemble-build-kit:
name: Assemble Build Kit
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v2
- name: Set up JDK 14
uses: actions/setup-java@v1
with:
java-version: 14
- uses: actions/cache@v1
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ github.run_id }}
- name: Ensure to use tagged version
run: mvn versions:set --file main/pom.xml -DnewVersion=${GITHUB_REF##*/} # use shell parameter expansion to strip of 'refs/tags'
if: startsWith(github.ref, 'refs/tags/')
- name: Build with Maven
- name: Assemble Buildkit
run: mvn -B package -DskipTests --file main/pom.xml --resume-from=buildkit -Prelease
- name: Upload buildkit-linux.zip
uses: actions/upload-artifact@v1
@@ -70,10 +51,10 @@ jobs:
name: buildkit-win.zip
path: main/buildkit/target/buildkit-win.zip
github-release:
release:
name: Draft a Release on GitHub Releases
runs-on: ubuntu-latest
needs: assemble-build-kit
needs: build
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Download buildkit-linux.zip
@@ -91,7 +72,8 @@ jobs:
with:
name: buildkit-win.zip
path: .
- id: create_release
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -99,9 +81,9 @@ jobs:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
body: |
TODO
:construction: Work in Progress
draft: true
prerelease: true
prerelease: false
- name: Upload buildkit-linux.zip to GitHub Releases
uses: actions/upload-release-asset@v1.0.1
env:

2
.idea/compiler.xml generated
View File

@@ -32,9 +32,9 @@
<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

@@ -1,5 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Cryptomator macOS" type="Application" factoryName="Application">
<envs>
<env name="LD_LIBRARY_PATH" value="/usr/local/lib" />
</envs>
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="launcher" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />

View File

@@ -1,6 +1,6 @@
[![cryptomator](cryptomator.png)](https://cryptomator.org/)
[![Build Status](https://travis-ci.org/cryptomator/cryptomator.svg?branch=master)](https://travis-ci.org/cryptomator/cryptomator)
[![Build](https://github.com/cryptomator/cryptomator/workflows/Build/badge.svg)](https://github.com/cryptomator/cryptomator/actions?query=workflow%3ABuild)
[![Known Vulnerabilities](https://snyk.io/test/github/cryptomator/cryptomator/badge.svg?targetFile=main%2Fpom.xml)](https://snyk.io/test/github/cryptomator/cryptomator?targetFile=main%2Fpom.xml)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/2a0adf3cec6a4143b91035d3924178f1)](https://www.codacy.com/app/cryptomator/cryptomator?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=cryptomator/cryptomator&amp;utm_campaign=Badge_Grade)
[![Twitter](https://img.shields.io/badge/twitter-@Cryptomator-blue.svg?style=flat)](http://twitter.com/Cryptomator)
@@ -41,7 +41,7 @@ Download native binaries of Cryptomator on [cryptomator.org](https://cryptomator
- File names get encrypted
- Folder structure gets obfuscated
- Use as many vaults in your Dropbox as you want, each having individual passwords
- One thousand commits for the security of your data!! :tada:
- Two thousand commits for the security of your data!! :tada:
### Privacy
@@ -65,7 +65,7 @@ For more information on the security details visit [cryptomator.org](https://doc
### Dependencies
* JDK 11 (we recommend to use the latest version)
* JDK 14 (e.g. adoptopenjdk)
* Maven 3
* Optional: OS-dependent build tools for native packaging (see [Windows](https://github.com/cryptomator/cryptomator-win), [OS X](https://github.com/cryptomator/cryptomator-osx), [Linux](https://github.com/cryptomator/builder-containers))

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.5.1</version>
<version>1.5.5</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.1</version>
<version>1.5.5</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,8 +1,8 @@
package org.cryptomator.common.settings;
public enum UiTheme {
LIGHT("Light"),
DARK("Dark");
LIGHT("preferences.general.theme.light"),
DARK("preferences.general.theme.dark");
// CUSTOM("Custom (%s)");
private String displayName;

View File

@@ -9,27 +9,23 @@ import com.google.common.base.Strings;
import com.google.common.io.BaseEncoding;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.apache.commons.lang3.StringUtils;
import org.fxmisc.easybind.EasyBind;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Base64;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
import java.util.UUID;
/**
* The settings specific to a single vault.
* TODO: Change the name of individualMountPath and its derivatives to customMountPath
*/
public class VaultSettings {
@@ -38,6 +34,8 @@ 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 int DEFAULT_FILENAME_LENGTH_LIMIT = -1;
public static final WhenUnlocked DEFAULT_ACTION_AFTER_UNLOCK = WhenUnlocked.ASK;
private static final Random RNG = new Random();
@@ -47,10 +45,12 @@ 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);
@@ -59,7 +59,7 @@ public class VaultSettings {
}
Observable[] observables() {
return new Observable[]{path, mountName, winDriveLetter, unlockAfterStartup, revealAfterMount, usesIndividualMountPath, individualMountPath, usesReadOnlyMode, mountFlags};
return new Observable[]{path, mountName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, filenameLengthLimit, actionAfterUnlock};
}
private void deriveMountNameFromPath(Path path) {
@@ -123,17 +123,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();
}
@@ -146,6 +146,18 @@ 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 */

View File

@@ -25,10 +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();
}
@@ -36,52 +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;
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();
@@ -92,11 +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

@@ -21,6 +21,8 @@ import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptofs.common.Constants;
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.slf4j.Logger;
@@ -101,16 +103,25 @@ public class Vault {
if (vaultSettings.usesReadOnlyMode().get()) {
flags.add(FileSystemFlags.READONLY);
}
if (vaultSettings.filenameLengthLimit().get() == -1) {
LOG.debug("Determining file name length limitations...");
int limit = new FileSystemCapabilityChecker().determineSupportedFileNameLength(getPath());
vaultSettings.filenameLengthLimit().set(limit);
LOG.info("Storing file name length limit of {}", limit);
}
assert vaultSettings.filenameLengthLimit().get() > 0;
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
.withPassphrase(passphrase) //
.withFlags(flags) //
.withMasterkeyFilename(MASTERKEY_FILENAME) //
.withMaxPathLength(vaultSettings.filenameLengthLimit().get() + Constants.MAX_ADDITIONAL_PATH_LENGTH) //
.withMaxNameLength(vaultSettings.filenameLengthLimit().get()) //
.build();
return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
}
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

@@ -51,15 +51,16 @@ public class VaultListManager {
}
public Vault add(Path pathToVault) throws NoSuchFileException {
if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) {
throw new NoSuchFileException(pathToVault.toString(), null, "Not a vault directory");
Path normalizedPathToVault = pathToVault.normalize().toAbsolutePath();
if (!CryptoFileSystemProvider.containsVault(normalizedPathToVault, MASTERKEY_FILENAME)) {
throw new NoSuchFileException(normalizedPathToVault.toString(), null, "Not a vault directory");
}
Optional<Vault> alreadyExistingVault = get(pathToVault);
Optional<Vault> alreadyExistingVault = get(normalizedPathToVault);
if (alreadyExistingVault.isPresent()) {
return alreadyExistingVault.get();
} else {
VaultSettings vaultSettings = VaultSettings.withRandomId();
vaultSettings.path().set(pathToVault);
vaultSettings.path().set(normalizedPathToVault);
Vault newVault = create(vaultSettings);
vaultList.add(newVault);
return newVault;
@@ -72,13 +73,11 @@ public class VaultListManager {
}
private Optional<Vault> get(Path vaultPath) {
return vaultList.stream().filter(v -> {
try {
return Files.isSameFile(vaultPath, v.getPath());
} catch (IOException e) {
return false;
}
}).findAny();
assert vaultPath.isAbsolute();
assert vaultPath.normalize().equals(vaultPath);
return vaultList.stream() //
.filter(v -> vaultPath.equals(v.getPath())) //
.findAny();
}
private Vault create(VaultSettings vaultSettings) {
@@ -93,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

@@ -41,16 +41,13 @@ public class VaultStats {
}
private void vaultStateChanged(@SuppressWarnings("unused") Observable observable) {
switch (state.get()) {
case UNLOCKED:
assert fs.get() != null;
LOG.debug("start recording stats");
updateService.restart();
break;
default:
LOG.debug("stop recording stats");
updateService.cancel();
break;
if (VaultState.UNLOCKED.equals(state.get())) {
assert fs.get() != null;
LOG.debug("start recording stats");
updateService.restart();
} else {
LOG.debug("stop recording stats");
updateService.cancel();
}
}

View File

@@ -43,17 +43,10 @@ public interface Volume {
}
static VolumeImpl[] getCurrentSupportedAdapters() {
return Stream.of(VolumeImpl.values()).filter(impl -> {
switch (impl) {
case WEBDAV:
return WebDavVolume.isSupportedStatic();
case DOKANY:
return DokanyVolume.isSupportedStatic();
case FUSE:
return FuseVolume.isSupportedStatic();
default:
return false;//throw new IllegalStateException("Adapter not implemented.");
}
return Stream.of(VolumeImpl.values()).filter(impl -> switch (impl) {
case WEBDAV -> WebDavVolume.isSupportedStatic();
case DOKANY -> DokanyVolume.isSupportedStatic();
case FUSE -> FuseVolume.isSupportedStatic();
}).toArray(VolumeImpl[]::new);
}

View File

@@ -7,8 +7,6 @@ package org.cryptomator.common.vaults;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.SystemUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -22,12 +20,11 @@ import java.util.stream.StreamSupport;
@Singleton
public final class WindowsDriveLetters {
private static final Logger LOG = LoggerFactory.getLogger(WindowsDriveLetters.class);
private static final Set<String> A_TO_Z;
private static final Set<String> C_TO_Z;
static {
try (IntStream stream = IntStream.rangeClosed('A', 'Z')) {
A_TO_Z = stream.mapToObj(i -> String.valueOf((char) i)).collect(Collectors.toSet());
try (IntStream stream = IntStream.rangeClosed('C', 'Z')) {
C_TO_Z = stream.mapToObj(i -> String.valueOf((char) i)).collect(Collectors.toSet());
}
}
@@ -36,7 +33,7 @@ public final class WindowsDriveLetters {
}
public Set<String> getAllDriveLetters() {
return A_TO_Z;
return C_TO_Z;
}
public Set<String> getOccupiedDriveLetters() {
@@ -44,12 +41,12 @@ public final class WindowsDriveLetters {
return Set.of();
} else {
Iterable<Path> rootDirs = FileSystems.getDefault().getRootDirectories();
return StreamSupport.stream(rootDirs.spliterator(), false).map(p -> p.toString().substring(0,1)).collect(Collectors.toSet());
return StreamSupport.stream(rootDirs.spliterator(), false).map(p -> p.toString().substring(0, 1)).collect(Collectors.toSet());
}
}
public Set<String> getAvailableDriveLetters() {
return Sets.difference(A_TO_Z, getOccupiedDriveLetters());
return Sets.difference(C_TO_Z, getOccupiedDriveLetters());
}
}

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.1</version>
<version>1.5.5</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.1</version>
<version>1.5.5</version>
</parent>
<artifactId>launcher</artifactId>
<name>Cryptomator Launcher</name>

View File

@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.5.1</version>
<version>1.5.5</version>
<packaging>pom</packaging>
<name>Cryptomator</name>
@@ -24,15 +24,16 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- cryptomator dependencies -->
<cryptomator.cryptofs.version>1.9.7</cryptomator.cryptofs.version>
<cryptomator.cryptofs.version>1.9.11</cryptomator.cryptofs.version>
<cryptomator.jni.version>2.2.2</cryptomator.jni.version>
<cryptomator.fuse.version>1.2.3</cryptomator.fuse.version>
<cryptomator.dokany.version>1.1.13</cryptomator.dokany.version>
<cryptomator.webdav.version>1.0.10</cryptomator.webdav.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>
<secret-service.version>1.0.0</secret-service.version>
<jwt.version>3.10.2</jwt.version>
<easybind.version>1.0.3</easybind.version>
<guava.version>28.2-jre</guava.version>
@@ -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,6 +161,13 @@
<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>

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.5.1</version>
<version>1.5.5</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,17 +36,19 @@ 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
@@ -69,8 +67,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,6 +80,17 @@ 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() {

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

@@ -13,6 +13,7 @@ public enum FxmlFile {
FORGET_PASSWORD("/fxml/forget_password.fxml"), //
MAIN_WINDOW("/fxml/main_window.fxml"), //
MIGRATION_CAPABILITY_ERROR("/fxml/migration_capability_error.fxml"), //
MIGRATION_IMPOSSIBLE("/fxml/migration_impossible.fxml"),
MIGRATION_RUN("/fxml/migration_run.fxml"), //
MIGRATION_START("/fxml/migration_start.fxml"), //
MIGRATION_SUCCESS("/fxml/migration_success.fxml"), //

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

@@ -26,6 +26,7 @@ public enum FontAwesome5Icon {
INFO_CIRCLE("\uF05A"), //
KEY("\uF084"), //
LINK("\uF0C1"), //
UNLINK("\uf127"),
LOCK("\uF023"), //
LOCK_OPEN("\uF3C1"), //
MAGIC("\uF0D0"), //

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

@@ -22,6 +22,8 @@ import javafx.scene.control.TextField;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.TransferMode;
@@ -38,11 +40,12 @@ 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 = "";
private static final String STYLE_CLASS = "secure-password-field";
private static final KeyCodeCombination SHORTCUT_BACKSPACE = new KeyCodeCombination(KeyCode.BACK_SPACE, KeyCombination.SHORTCUT_DOWN);
private final String placeholderChar;
private final BooleanProperty capsLocked = new SimpleBooleanProperty();
@@ -74,12 +77,10 @@ public class SecurePasswordField extends TextField {
}
public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
switch(attribute) {
case TEXT:
return null;
default:
return super.queryAccessibleAttribute(attribute, parameters);
}
return switch (attribute) {
case TEXT -> null;
default -> super.queryAccessibleAttribute(attribute, parameters);
};
}
private void handleDragOver(DragEvent event) {
@@ -101,6 +102,8 @@ public class SecurePasswordField extends TextField {
private void handleKeyEvent(KeyEvent e) {
if (e.getCode() == KeyCode.CAPS) {
updateCapsLocked();
} else if (SHORTCUT_BACKSPACE.match(e)) {
wipe();
}
}
@@ -186,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;
}
}
@@ -198,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() {
@@ -217,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);
}
/**
@@ -228,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;
@@ -239,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

@@ -27,6 +27,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 +39,24 @@ 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;
@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 +74,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 +84,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");
});
}
@@ -129,15 +121,14 @@ public class FxApplication extends Application {
private void loadSelectedStyleSheet(UiTheme desiredTheme) {
UiTheme theme = licenseHolder.isValidLicense() ? desiredTheme : UiTheme.LIGHT;
switch (theme) {
case DARK:
case DARK -> {
Application.setUserAgentStylesheet(getClass().getResource("/css/dark_theme.css").toString());
macFunctions.map(MacFunctions::uiAppearance).ifPresent(JniException.ignore(MacApplicationUiAppearance::setToDarkAqua));
break;
case LIGHT:
default:
}
case LIGHT -> {
Application.setUserAgentStylesheet(getClass().getResource("/css/light_theme.css").toString());
macFunctions.map(MacFunctions::uiAppearance).ifPresent(JniException.ignore(MacApplicationUiAppearance::setToAqua));
break;
}
}
}

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

@@ -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

@@ -31,16 +31,12 @@ public class VaultDetailController implements FxController {
}
private FontAwesome5Icon getGlyphForVaultState(VaultState state) {
switch (state) {
case LOCKED:
return FontAwesome5Icon.LOCK;
case PROCESSING:
return FontAwesome5Icon.SPINNER;
case UNLOCKED:
return FontAwesome5Icon.LOCK_OPEN;
default:
return FontAwesome5Icon.EXCLAMATION_TRIANGLE;
}
return switch (state) {
case LOCKED -> FontAwesome5Icon.LOCK;
case PROCESSING -> FontAwesome5Icon.SPINNER;
case UNLOCKED -> FontAwesome5Icon.LOCK_OPEN;
case NEEDS_MIGRATION, MISSING, ERROR -> FontAwesome5Icon.EXCLAMATION_TRIANGLE;
};
}
@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

@@ -3,9 +3,9 @@ package org.cryptomator.ui.mainwindow;
import javafx.beans.binding.Binding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.controls.FontAwesome5Icon;
import org.fxmisc.easybind.EasyBind;
@@ -23,16 +23,12 @@ public class VaultListCellController implements FxController {
}
private FontAwesome5Icon getGlyphForVaultState(VaultState state) {
switch (state) {
case LOCKED:
return FontAwesome5Icon.LOCK;
case PROCESSING:
return FontAwesome5Icon.SPINNER;
case UNLOCKED:
return FontAwesome5Icon.LOCK_OPEN;
default:
return FontAwesome5Icon.EXCLAMATION_TRIANGLE;
}
return switch (state) {
case LOCKED -> FontAwesome5Icon.LOCK;
case PROCESSING -> FontAwesome5Icon.SPINNER;
case UNLOCKED -> FontAwesome5Icon.LOCK_OPEN;
case NEEDS_MIGRATION, MISSING, ERROR -> FontAwesome5Icon.EXCLAMATION_TRIANGLE;
};
}
/* Getter/Setter */

View File

@@ -64,26 +64,7 @@ public class VaultListController implements FxController {
if (newValue == null) {
return;
}
VaultState reportedState = newValue.getState();
switch (reportedState) {
case LOCKED:
case NEEDS_MIGRATION:
case 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:
case UNLOCKED:
case PROCESSING:
default:
// 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

@@ -0,0 +1,46 @@
package org.cryptomator.ui.migration;
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 javax.inject.Inject;
public class MigrationImpossibleController implements FxController {
private static final String HELP_URI = "https://docs.cryptomator.org/en/1.5/help/manual-migration/";
private final FxApplication fxApplication;
private final Stage window;
private final Vault vault;
@Inject
MigrationImpossibleController(FxApplication fxApplication, @MigrationWindow Stage window, @MigrationWindow Vault vault) {
this.fxApplication = fxApplication;
this.window = window;
this.vault = vault;
}
@FXML
public void close() {
window.close();
}
@FXML
public void getMigrationHelp() {
fxApplication.getHostServices().showDocument(HELP_URI);
}
/* Getter/Setters */
public Vault getVault() {
return vault;
}
public String getHelpUri() {
return HELP_URI;
}
}

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;
}
@@ -83,6 +83,13 @@ abstract class MigrationModule {
return fxmlLoaders.createScene("/fxml/migration_capability_error.fxml");
}
@Provides
@FxmlScene(FxmlFile.MIGRATION_IMPOSSIBLE)
@MigrationScoped
static Scene provideMigrationImpossibleScene(@MigrationWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/migration_impossible.fxml");
}
// ------------------
@Binds
@@ -104,5 +111,9 @@ abstract class MigrationModule {
@IntoMap
@FxControllerKey(MigrationCapabilityErrorController.class)
abstract FxController bindMigrationCapabilityErrorController(MigrationCapabilityErrorController controller);
@Binds
@IntoMap
@FxControllerKey(MigrationImpossibleController.class)
abstract FxController bindMigrationImpossibleController(MigrationImpossibleController controller);
}

View File

@@ -16,12 +16,14 @@ import javafx.scene.control.ContentDisplay;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.cryptofs.FileNameTooLongException;
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
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;
@@ -53,11 +55,12 @@ 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;
private final Lazy<Scene> successScene;
private final Lazy<Scene> impossibleScene;
private final ObjectBinding<ContentDisplay> migrateButtonContentDisplay;
private final Lazy<Scene> capabilityErrorScene;
private final BooleanProperty migrationButtonDisabled;
@@ -66,12 +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, 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;
@@ -80,10 +84,11 @@ public class MigrationRunController implements FxController {
this.capabilityErrorScene = capabilityErrorScene;
this.migrationButtonDisabled = new SimpleBooleanProperty();
this.migrationProgress = new SimpleDoubleProperty(volatileMigrationProgress);
this.impossibleScene = impossibleScene;
}
public void initialize() {
if (keychainAccess.isPresent()) {
if (keychain.isPresent()) {
loadStoredPassword();
}
migrationButtonDisabled.bind(vault.stateProperty().isNotEqualTo(VaultState.NEEDS_MIGRATION).or(passwordField.textProperty().isEmpty()));
@@ -107,7 +112,7 @@ public class MigrationRunController implements FxController {
}, 0, MIGRATION_PROGRESS_UPDATE_MILLIS, TimeUnit.MILLISECONDS);
Tasks.create(() -> {
Migrators migrators = Migrators.get();
migrators.migrate(vault.getPath(), MASTERKEY_FILENAME, password, this::migrationProgressChanged);
migrators.migrate(vault.getPath(), MASTERKEY_FILENAME, password, this::migrationProgressChanged, this::migrationRequiresInput);
return migrators.needsMigration(vault.getPath(), MASTERKEY_FILENAME);
}).onSuccess(needsAnotherMigration -> {
if (needsAnotherMigration) {
@@ -116,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 -> {
@@ -127,9 +132,14 @@ public class MigrationRunController implements FxController {
vault.setState(VaultState.NEEDS_MIGRATION);
}).onError(FileSystemCapabilityChecker.MissingCapabilityException.class, e -> {
LOG.error("Underlying file system not supported.", e);
vault.setState(VaultState.ERROR);
vault.setState(VaultState.NEEDS_MIGRATION);
missingCapability.set(e.getMissingCapability());
window.setScene(capabilityErrorScene.get());
}).onError(FileNameTooLongException.class, e -> {
LOG.error("Migration failed because the underlying file system does not support long filenames.", e);
vault.setState(VaultState.NEEDS_MIGRATION);
errorComponent.cause(e).window(window).returnToScene(startScene.get()).build().showErrorScene();
window.setScene(impossibleScene.get());
}).onError(Exception.class, e -> { // including RuntimeExceptions
LOG.error("Migration failed for technical reasons.", e);
vault.setState(VaultState.NEEDS_MIGRATION);
@@ -142,26 +152,25 @@ public class MigrationRunController implements FxController {
// Called by a background task. We can not directly modify observable properties from here
private void migrationProgressChanged(MigrationProgressListener.ProgressState state, double progress) {
switch (state) {
case INITIALIZING:
volatileMigrationProgress = -1.0;
break;
case MIGRATING:
volatileMigrationProgress = progress;
break;
case FINALIZING:
volatileMigrationProgress = 1.0;
break;
default:
throw new IllegalStateException("Unexpted state " + state);
}
volatileMigrationProgress = switch (state) {
case INITIALIZING -> -1.0;
case MIGRATING -> progress;
case FINALIZING -> 1.0;
};
}
private MigrationContinuationListener.ContinuationResult migrationRequiresInput(MigrationContinuationListener.ContinuationEvent event) {
//TODO: creating a new scene seems a little over the top, maybe stick to this scene
// my suggestion is to make this quick and dirty by setting some elements unmanaged and invisible and afterwards activate them again
// otherwise: We need a more abstract runController which has two subviews (run and halted), see mainWindow for example
return MigrationContinuationListener.ContinuationResult.PROCEED;
}
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);
@@ -194,12 +203,10 @@ public class MigrationRunController implements FxController {
}
public ContentDisplay getMigrateButtonContentDisplay() {
switch (vault.getState()) {
case PROCESSING:
return ContentDisplay.LEFT;
default:
return ContentDisplay.TEXT_ONLY;
}
return switch (vault.getState()) {
case PROCESSING -> ContentDisplay.LEFT;
default -> ContentDisplay.TEXT_ONLY;
};
}
public ReadOnlyDoubleProperty migrationProgressProperty() {

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

@@ -12,9 +12,11 @@ import javafx.scene.control.RadioButton;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.util.StringConverter;
import javafx.application.Application;
import org.cryptomator.common.LicenseHolder;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.UiTheme;
import org.cryptomator.common.Environment;
import org.cryptomator.ui.common.FxController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -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,23 @@ 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.valueProperty().bindBidirectional(settings.theme());
themeChoiceBox.setConverter(new UiThemeConverter());
themeChoiceBox.setConverter(new UiThemeConverter(resourceBundle));
startHiddenCheckbox.selectedProperty().bindBidirectional(settings.startHidden());
@@ -112,13 +121,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

@@ -49,21 +49,14 @@ public class PreferencesController implements FxController {
}
private Tab getTabToSelect(SelectedPreferencesTab selectedTab) {
switch (selectedTab) {
case UPDATES:
return updatesTab;
case VOLUME:
return volumeTab;
case DONATION_KEY:
return donationKeyTab;
case GENERAL:
return generalTab;
case ABOUT:
return aboutTab;
case ANY:
default:
return updateAvailable.get() ? updatesTab : generalTab;
}
return switch (selectedTab) {
case UPDATES -> updatesTab;
case VOLUME -> volumeTab;
case DONATION_KEY -> donationKeyTab;
case GENERAL -> generalTab;
case ABOUT -> aboutTab;
case ANY -> updateAvailable.get() ? updatesTab : generalTab;
};
}
private void selectedTabChanged() {

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

@@ -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

@@ -29,16 +29,14 @@ class TrayImageFactory {
MacApplicationUiInterfaceStyle interfaceStyle = macFunctions.map(MacFunctions::uiAppearance) //
.map(MacApplicationUiAppearance::getCurrentInterfaceStyle) //
.orElse(MacApplicationUiInterfaceStyle.LIGHT);
switch (interfaceStyle) {
case DARK:
return "/tray_icon_mac_white.png";
default:
return "/tray_icon_mac_black.png";
}
return switch (interfaceStyle) {
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,39 @@
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.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.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,124 +42,136 @@ 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;
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.window.setOnCloseRequest(windowEvent -> cancel());
}
@FXML
public void initialize() {
if (keychainAccess.isPresent()) {
loadStoredPassword();
} else {
savePassword.setDisable(true);
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));
}
}
@@ -169,17 +181,20 @@ public class UnlockController implements FxController {
return vault;
}
public ObjectBinding<ContentDisplay> unlockButtonStateProperty() {
return unlockButtonState;
public ObjectBinding<ContentDisplay> unlockButtonContentDisplayProperty() {
return unlockButtonContentDisplay;
}
public ContentDisplay getUnlockButtonState() {
switch (vault.getState()) {
case PROCESSING:
return ContentDisplay.LEFT;
default:
return 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,4 +204,8 @@ public class UnlockController implements FxController {
public boolean isUnlockButtonDisabled() {
return unlockButtonDisabled.get();
}
public boolean isKeychainAccessAvailable() {
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,26 +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
@@ -35,12 +80,16 @@ abstract class UnlockModule {
@Provides
@UnlockWindow
@UnlockScoped
static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("unlock.title"));
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

@@ -631,6 +631,10 @@
-fx-background-color: TEXT_FILL;
}
.check-box:selected:disabled > .box > .mark {
-fx-background-color: TEXT_FILL_MUTED;
}
/*******************************************************************************
* *
* RadioButton *
@@ -854,7 +858,7 @@
.progress-bar > .bar {
-fx-background-color: CONTROL_PRIMARY_BG_NORMAL;
-fx-background-radius: 4px;
-fx-padding: 0.5em;
-fx-padding: 1em 0.5em;
}
.progress-bar:indeterminate > .bar {

View File

@@ -630,6 +630,10 @@
-fx-background-color: TEXT_FILL;
}
.check-box:selected:disabled > .box > .mark {
-fx-background-color: TEXT_FILL_MUTED;
}
/*******************************************************************************
* *
* RadioButton *
@@ -853,7 +857,7 @@
.progress-bar > .bar {
-fx-background-color: CONTROL_PRIMARY_BG_NORMAL;
-fx-background-radius: 4px;
-fx-padding: 0.5em;
-fx-padding: 1em 0.5em;
}
.progress-bar:indeterminate > .bar {

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

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Hyperlink?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<?import javafx.scene.text.Text?>
<?import javafx.scene.text.TextFlow?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.migration.MigrationImpossibleController"
minWidth="400"
maxWidth="400"
minHeight="145"
spacing="12">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<HBox spacing="12" alignment="CENTER_LEFT" VBox.vgrow="ALWAYS">
<StackPane alignment="CENTER" HBox.hgrow="NEVER">
<Circle styleClass="glyph-icon-red" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="TIMES" glyphSize="24"/>
</StackPane>
<VBox spacing="6">
<Label styleClass="label-large" text="%migration.impossible.heading"/>
<Label text="%migration.impossible.reason" wrapText="true" HBox.hgrow="ALWAYS"/>
<TextFlow>
<Text text="%migration.impossible.moreInfo"/>
<Text text=" "/>
<Hyperlink styleClass="hyperlink-underline" text="docs.cryptomator.org" wrapText="true" onAction="#getMigrationHelp"/>
<Text text="."/>
</TextFlow>
</VBox>
</HBox>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<ButtonBar buttonMinWidth="120" buttonOrder="+C">
<buttons>
<!-- Button text="Try again" ButtonBar.buttonData="to do" onAction="#retry" / also add button to button bar order-->
<Button text="%generic.button.close" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
</buttons>
</ButtonBar>
</VBox>
</VBox>

View File

@@ -3,6 +3,7 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.control.ProgressIndicator?>
<?import javafx.scene.layout.VBox?>
@@ -19,17 +20,20 @@
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<VBox spacing="6">
<VBox spacing="6" visible="${!controller.vault.processing}" managed="${!controller.vault.processing}">
<FormattedLabel format="%migration.run.enterPassword" arg1="${controller.vault.displayableName}" wrapText="true"/>
<NiceSecurePasswordField fx:id="passwordField"/>
</VBox>
<ProgressBar progress="${controller.migrationProgress}" prefWidth="Infinity" visible="${controller.vault.processing}"/>
<VBox spacing="6" visible="${controller.vault.processing}" managed="${controller.vault.processing}">
<Label text="%migration.run.progressHint" wrapText="true"/>
<ProgressBar progress="${controller.migrationProgress}" prefWidth="Infinity"/>
</VBox>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<ButtonBar buttonMinWidth="120" buttonOrder="B+X">
<buttons>
<Button text="%generic.button.back" ButtonBar.buttonData="BACK_PREVIOUS" cancelButton="true" onAction="#back"/>
<Button text="%generic.button.back" ButtonBar.buttonData="BACK_PREVIOUS" cancelButton="true" onAction="#back" disable="${controller.vault.processing}"/>
<Button text="%migration.run.startMigrationBtn" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#migrate" contentDisplay="${controller.migrateButtonContentDisplay}"
disable="${controller.migrationButtonDisabled}">
<graphic>

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

@@ -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"/>
</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.vault.displayableName}" 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"/>
<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>

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