Compare commits

..
118 Commits
Author SHA1 Message Date
Sebastian Stenzel c6d88f1dc4 Merge branch 'release/1.5.0-beta3' 2020-02-27 10:02:37 +01:00
Sebastian Stenzel 4a2d71405d preparing 1.5.0-beta3 2020-02-27 09:58:37 +01:00
Sebastian StenzelandGitHub f98c3aa5ca New Crowdin translations (#1064)
[ci skip]
2020-02-27 09:54:48 +01:00
Sebastian Stenzel 7d389b561d use shorter names for rtf files to make sure the name doesn't get collapsed in the file explorer 2020-02-27 09:40:52 +01:00
Sebastian Stenzel 7056d35254 refer to docs.cryptomator.org instead of google.com in README file 2020-02-27 09:39:03 +01:00
Sebastian Stenzel 6a8234ddd9 fixes #1046 2020-02-26 16:09:08 +01:00
Tobias Hagemann 3d3eed0ed2 updated logo and removed unused assets 2020-02-21 12:08:58 +01:00
Sebastian Stenzel e479b35846 typo
[ci skip]
2020-02-20 17:43:19 +01:00
Sebastian Stenzel 83c7421a96 renamed keys
[ci skip]
2020-02-20 17:19:56 +01:00
Sebastian StenzelandGitHub 116091f2e0 New Crowdin translations (#1041)
* New translations
[ci skip]
2020-02-20 17:17:49 +01:00
Sebastian Stenzel ce11017609 Moved masterkey-related vault options to its own tab
Also made remaining vault recovery labels localizable
2020-02-20 16:27:21 +01:00
Sebastian Stenzel caa8c84d8a Can now use a recovery key to reset a vault's password 2020-02-20 15:31:22 +01:00
Sebastian Stenzel e14fc56b37 fixed autocompletion for pasting text 2020-02-20 14:09:32 +01:00
Sebastian Stenzel 0d29e56948 Implemented word auto-completion for recovery key entry field 2020-02-20 11:10:42 +01:00
Sebastian Stenzel d2a27c782d No longer warn about this method invocation 2020-02-20 11:09:52 +01:00
Sebastian Stenzel 864e92ea59 updated funding.yml 2020-02-20 09:51:43 +01:00
Sebastian Stenzel 97dfe9a1d4 fixed unit test 2020-02-20 00:28:48 +01:00
Sebastian Stenzel d7edfd13a7 Began vault recovery wizard 2020-02-19 17:27:07 +01:00
Sebastian Stenzel 35207de7cc fixes #1056 2020-02-17 22:36:13 +01:00
Sebastian Stenzel adf0e3720d dedup 2020-02-17 20:34:20 +01:00
Sebastian Stenzel f73c1889b7 Show generic error screen in case of exceptions during migration (references #1056) 2020-02-17 20:23:19 +01:00
Sebastian Stenzel 18ff1d2898 fixes #986, fixes #1062 2020-02-17 18:45:31 +01:00
Sebastian Stenzel c6e2fa11a4 updated cryptofs to 1.9.2, fixes #1057, references #1056 2020-02-17 18:14:08 +01:00
Sebastian Stenzel 3fac8b7cd6 updated dependencies 2020-02-17 18:12:58 +01:00
Sebastian Stenzel f62faa72ce (Auto)Unlock via VaultService (fixes #1044) 2020-02-13 23:50:26 +01:00
Sebastian Stenzel 57256d0733 began implementation of new auto unlocker (references #1044) 2020-02-13 17:16:09 +01:00
Sebastian Stenzel e1f44fb48a App lifecycle fixes 2020-02-13 16:29:49 +01:00
Sebastian Stenzel 62676d5a83 minor ui state tweaks
[ci skip]
2020-02-13 14:32:23 +01:00
Armin Schrenk 7755256956 further additions to #1060 2020-02-12 16:19:42 +01:00
Armin Schrenk e1cf8546b7 adding recover vault dialogue stub 2020-02-12 15:02:06 +01:00
Tobias Hagemann e40837da2c updated primary_d2 color (somehow there was a small discrepancy in the corporate design manual) [ci skip] 2020-01-31 13:25:11 +01:00
Tobias Hagemann e7c3c0ab53 updated color palette 2020-01-30 15:33:54 +01:00
Sebastian Stenzel 2336b9c622 write vault migration events to additional upgrade log (references #1058) 2020-01-30 09:24:46 +01:00
Sebastian StenzelandGitHub 4e2122a64f Merge pull request #1050 from zhouer/close_request
Handle close request event for main window, fixes #1040
2020-01-16 12:22:56 +01:00
Sebastian Stenzel 833bb085e6 Using disting ExecutorService and ScheduledExecutorService since the latter doesn't scale beyond its pre-defined number of core threads
fixes #1051
2020-01-15 13:07:19 +01:00
En-Jan Chou 1ec9b0507d Prevent window close if user choose cancel in quit window 2020-01-14 17:12:36 -05:00
En-Jan Chou b6a4f70ec9 Handle close request event for main window
When receiving close request event, main window should quit program
if there is no system tray available, and just like the close button
was clicked. The window close request event happens when user close
window by pressing alt-f4 (Gnome) or when receiving WM_DELETE_WINDOW
xwindow event. Current behavior is just close the window and let the
program keep running, and there is no way to quit program without
system tray.
2020-01-14 16:54:47 -05:00
Sebastian Stenzel eb047e29a0 jcenter requires https now
[ci skip]
2020-01-14 22:06:01 +01:00
Sebastian Stenzel 5dc1d9b2e0 Fail fast (ammendment for #1047) 2020-01-14 21:46:52 +01:00
Sebastian Stenzel 362f3eac63 Certain app lifecycle event handlers are now now longer @TrayMenuScoped but rather @Singleton, so they get set up even if there is no TrayIcon (references #1035) 2020-01-14 21:43:45 +01:00
Sebastian StenzelandGitHub f0b26c60c4 Merge pull request #1045 from zhouer/minimize
Add minimize button, fixes #1035
2020-01-14 20:59:10 +01:00
Sebastian StenzelandGitHub e827d86967 Merge pull request #1047 from zhouer/icons
Add multiple icons and let system choose the best size
2020-01-14 20:54:18 +01:00
Tobias Hagemann 04533c0338 downgrading font awesome from pro to free 2020-01-14 16:57:13 +01:00
En-Jan Chou e148e39fef Rollback TrayMenuController to FxApplication code moving 2020-01-13 03:48:34 -05:00
En-Jan Chou cb97905c98 Use static emptyList and close InputStream 2020-01-12 21:17:56 -05:00
En-Jan Chou 7ec7cecbfd Add multiple icons and let system choose the best size 2020-01-12 13:42:17 -05:00
En-Jan Chou 3369401f1c Add minimize button and tray menu refactoring
* Add minimize button and change close button behavior
  * close main window (minimize to tray) if system tray available,
    quit application otherwise.
  * show minimize button if system tray unavailable
* Move some codes from TrayMenuController to FxApplication, includes:
  * Desktop integrations (shortcut, quit handlers...)
  * vaults change listener for sudden termination
  * public method showPreferenceWindow change to showPerferenceTab
    due to name conflict
  * public method quitApplication for both main window and system tray
  * shutdown hook for unmounting vaults on shutdown
* Add a new i18n string: main.minimizeBtn.tooltip
2020-01-12 05:29:56 -05:00
Sebastian Stenzel 09b9cfe027 onboarding text in empty vault list is now centered 2020-01-10 16:12:00 +01:00
Sebastian Stenzel e94ac3720b onboarding text in empty vault list is now centered 2020-01-10 14:46:25 +01:00
Sebastian Stenzel 4414d60026 Merge branch 'master' into develop 2020-01-09 17:16:40 +01:00
Sebastian Stenzel a6e680e32d Merge branch 'release/1.5.0-beta2' 2020-01-09 17:15:55 +01:00
Sebastian Stenzel d60a024c63 preparing 1.5.0-beta2 2020-01-09 16:50:38 +01:00
Sebastian StenzelandGitHub cc1b536656 New Crowdin translations (#1039)
[ci skip]
2020-01-09 16:49:39 +01:00
Sebastian Stenzel 85e773abef enable themes for donators 2020-01-09 16:34:31 +01:00
Sebastian Stenzel 4690bc56aa fixes #1038 2020-01-09 16:04:46 +01:00
Sebastian Stenzel 292c2676fd fixes #980 2020-01-09 15:44:25 +01:00
Sebastian Stenzel f7c6604c7c fixes #1029 2020-01-09 15:16:13 +01:00
Sebastian Stenzel 22310eb957 updated donation key store uri 2020-01-09 14:38:23 +01:00
Sebastian Stenzel 6d17d1298b removed unused fields 2020-01-09 13:03:22 +01:00
Sebastian Stenzel e90a680620 Split up MainWindowController, as it grew too big. The title bar is now handled by MainWindowTitleController
(@zhouer add the minimize button for #1035 to this new controller and fxml file)
2020-01-09 12:18:24 +01:00
Sebastian Stenzel 35d0ccfe89 allow resizing main window on all four corners
fixes #995
2020-01-09 12:06:12 +01:00
Tobias Hagemann 62cc454367 updated license public key 2020-01-09 10:35:27 +01:00
Tobias Hagemann 8bb277fe56 added iCloud Drive and OneDrive location presets 2020-01-09 10:27:56 +01:00
Sebastian Stenzel 67485b76af Make sure that adding a vault located at ~/foo is noop when a vault located at /home/user/foo already exists
fixes #1032
2020-01-08 16:50:08 +01:00
Sebastian Stenzel 270e60d7cf updated logging
[ci skip]
2020-01-08 15:41:26 +01:00
Sebastian Stenzel 11ffec862e Added autostart support for windows
fixes #418
2020-01-07 17:56:05 +01:00
Sebastian Stenzel d37e0dd5e1 Determine autostart state async (#418) 2020-01-07 17:55:36 +01:00
Sebastian Stenzel 960c3665e0 Localizations 2020-01-06 12:54:51 +01:00
Sebastian Stenzel 3396c79c38 updated intellij config
[ci skip]
2020-01-06 12:45:01 +01:00
Sebastian Stenzel cc1517f1b8 Added checkbox for auto start on macOS (#418) 2020-01-06 12:37:36 +01:00
Sebastian Stenzel 4f3ff55ae7 updated stale config
[ci skip]
2020-01-06 09:48:18 +01:00
Sebastian Stenzel f51362e95e cleanup
[ci skip]
2020-01-06 09:46:14 +01:00
Sebastian Stenzel 415dfece58 sames as in 9b6145ce:
We only want the user to trigger the listener
2020-01-06 09:46:01 +01:00
Sebastian StenzelandGitHub 3bdb5353da New Crowdin translations (#1015)
[ci skip]
2020-01-04 22:11:48 +01:00
Sebastian StenzelandGitHub 6ddf1b59ef Merge pull request #1031 from zhouer/hide_start_hidden
Hide startHidden setting when system tray is not supported
2019-12-29 14:18:35 +01:00
En-Jan Chou ef88f11c4f Hide startHidden setting when system tray is not supported
For platform not supporting system tray, users will have no chance to show
window and modify settings back again if the window is hidden at startup.
2019-12-26 09:40:33 -05:00
Sebastian Stenzel ec800c5439 Enforce minimum password length (amendment to #1018) 2019-12-18 15:31:52 +01:00
Sebastian Stenzel a2f3f5d254 fixes #1018, references #979 2019-12-18 14:58:36 +01:00
Sebastian Stenzel f475f70adf Updated zxcvbn4j to 1.3.0, which allows us to swipe the password from memory even after measuring its strength 2019-12-18 14:40:04 +01:00
Sebastian StenzelandGitHub 93c3da66da Merge pull request #1024 from zhouer/fix_mount_point
Initialize radio button before binding settings to it
2019-12-18 12:16:12 +01:00
En-Jan Chou 9b6145ce3f Initialize radio button before binding settings to it
The usesIndividualMountPath setting will be cleared if it bind to
an uninitialized radio button. This patch sets radio button with
current settings before binding the setting to the radio button.
2019-12-18 02:39:03 -05:00
Sebastian Stenzel 148eed172a Deduplicated UI code
Made "Enter Password" and "Reenter Password" a reusable component that is included in its original places
preparation for #1018
2019-12-17 17:35:16 +01:00
Sebastian Stenzel c97f146964 enabled "lock all" from tray menu (#297, #863) 2019-12-16 14:44:14 +01:00
Sebastian Stenzel a1034f5663 Refactored "lock all" task 2019-12-16 14:43:33 +01:00
Sebastian Stenzel 938b351f33 enabled "reveal" from tray menu (#297) 2019-12-16 13:10:43 +01:00
Sebastian Stenzel d870ecbcdb moved reveal() to VaultService 2019-12-16 13:03:28 +01:00
Sebastian Stenzel 5dff2126c4 cleanup of new VaultService 2019-12-16 11:22:30 +01:00
Sebastian Stenzel 41a6cc15e8 run "lock all" service on shared executorservice 2019-12-16 10:32:35 +01:00
Sebastian Stenzel f21b30c009 removed unused field 2019-12-16 10:31:04 +01:00
Sebastian Stenzel d69b63acc3 enabled "lock" from tray menu (#297) 2019-12-11 15:57:39 +01:00
Sebastian Stenzel f95b2baad5 "lock" deduplication 2019-12-11 15:57:14 +01:00
Sebastian Stenzel 1a4d1fffb3 shutdown logback manually 2019-12-11 13:53:32 +01:00
Sebastian Stenzel ac536ba125 Some shutdown hook refactorings for #980 2019-12-04 17:50:30 +01:00
Sebastian Stenzel 362b225d66 Fixes #939 2019-11-27 16:39:40 +01:00
Sebastian Stenzel 7d0bdc1a63 Show stack trace in case of unexpected exceptions 2019-11-27 15:44:53 +01:00
Sebastian Stenzel 94e3b21e44 Typo
[ci skip]
2019-11-27 13:20:44 +01:00
Sebastian Stenzel 23f89c1dc9 Reevaluate vault state when selecting vault 2019-11-26 17:21:28 +01:00
Sebastian Stenzel ce9c2f2c7a removed unused import
[ci skip]
2019-11-26 17:21:08 +01:00
Tobias Hagemann 44e97ab046 smaller text size for path in reveal button [ci skip] 2019-11-26 17:02:13 +01:00
Sebastian Stenzel 5d0a1fd49f tabpane styling
[ci skip]
2019-11-26 16:39:32 +01:00
Sebastian Stenzel ffc1d8dc1e simplified 2019-11-22 14:23:55 +01:00
Sebastian Stenzel 54fca93bba deduplicated code 2019-11-22 14:21:51 +01:00
Sebastian Stenzel c281687910 added copy/print buttons to recovery key dialog 2019-11-22 13:43:40 +01:00
Sebastian Stenzel 6229d7abbe Updated password and recovery key instructions 2019-11-22 13:10:50 +01:00
Sebastian Stenzel 01644eddb6 increase wizard dimensions
[ci skip]
2019-11-21 16:58:05 +01:00
Sebastian Stenzel 6e30df3796 Let the user choose whether a recovery key should be shown during vault creation 2019-11-21 16:57:43 +01:00
Sebastian Stenzel f87fa319ff renamed class 2019-11-20 17:14:31 +01:00
Sebastian Stenzel 9b019726bb show badge for missing license key in window title 2019-11-20 17:12:11 +01:00
Sebastian StenzelandGitHub cceafb76ed New Crowdin translations
New translations strings.properties
[ci skip]
2019-11-20 17:09:01 +01:00
Tobias Hagemann 39e2994c69 updated drag-n-drop-indicator styling 2019-11-20 16:53:02 +01:00
Sebastian Stenzel 1ddfcc3219 reworked drag'n'drop l&f 2019-11-20 15:03:03 +01:00
Tobias Hagemann 687f11596e added disabled styling to choice-box 2019-11-19 21:52:52 +01:00
Sebastian Stenzel b445f614fb Made mount point settings localizable
(and some fxml cleanup)
[ci skip]
2019-11-19 21:39:00 +01:00
Sebastian Stenzel c6fc1d93a0 save donation key to settings 2019-11-19 21:20:00 +01:00
Sebastian Stenzel 28d58922e3 TextArea needs a prefRowCount, otherwise it attempts to use all available space, displacing other elements.
fixes #1012
2019-11-18 10:11:20 +01:00
Sebastian Stenzel b884ea7ddc added registration key to preferences 2019-11-15 15:56:35 +01:00
Sebastian Stenzel 1717e20b61 Add JWT verifier 2019-11-14 17:02:41 +01:00
Sebastian Stenzel 6222f96b01 Merge branch 'master' into develop 2019-11-12 13:40:34 +01:00
173 changed files with 5174 additions and 1611 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# These are supported funding model platforms # These are supported funding model platforms
github: [overheadhunter, tobihagemann] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] github: [cryptomator] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username ko_fi: # Replace with a single Ko-fi username
+3 -1
View File
@@ -1,12 +1,14 @@
# Number of days of inactivity before an issue becomes stale # Number of days of inactivity before an issue becomes stale
daysUntilStale: 90 daysUntilStale: 180
# Number of days of inactivity before a stale issue is closed # Number of days of inactivity before a stale issue is closed
daysUntilClose: 30 daysUntilClose: 30
# Issues with these labels will never be considered stale # Issues with these labels will never be considered stale
exemptLabels: exemptLabels:
- type:security-issue # never close automatically - type:security-issue # never close automatically
- type:feature-request # never close automatically
- state:awaiting-response # handled by different bot - state:awaiting-response # handled by different bot
- state:blocked - state:blocked
- state:confirmed
# Set to true to ignore issues in a milestone (defaults to false) # Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true exemptMilestones: true
# Label to use when marking an issue as stale # Label to use when marking an issue as stale
+6 -6
View File
@@ -7,20 +7,20 @@
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" /> <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" /> <outputRelativeToContentRoot value="true" />
<processorPath useClasspath="false"> <processorPath useClasspath="false">
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-compiler/2.25.2/dagger-compiler-2.25.2.jar" /> <entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-compiler/2.26/dagger-compiler-2.26.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger/2.25.2/dagger-2.25.2.jar" /> <entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger/2.26/dagger-2.26.jar" />
<entry name="$MAVEN_REPOSITORY$/javax/inject/javax.inject/1/javax.inject-1.jar" /> <entry name="$MAVEN_REPOSITORY$/javax/inject/javax.inject/1/javax.inject-1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-producers/2.25.2/dagger-producers-2.25.2.jar" /> <entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-producers/2.26/dagger-producers-2.26.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar" /> <entry name="$MAVEN_REPOSITORY$/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/guava/27.1-jre/guava-27.1-jre.jar" /> <entry name="$MAVEN_REPOSITORY$/com/google/guava/guava/27.1-jre/guava-27.1-jre.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar" /> <entry name="$MAVEN_REPOSITORY$/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar" /> <entry name="$MAVEN_REPOSITORY$/com/google/code/findbugs/jsr305/3.0.1/jsr305-3.0.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-qual/2.5.2/checker-qual-2.5.2.jar" /> <entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-qual/2.5.2/checker-qual-2.5.2.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotations/2.2.0/error_prone_annotations-2.2.0.jar" /> <entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotations/2.2.0/error_prone_annotations-2.2.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/j2objc/j2objc-annotations/1.1/j2objc-annotations-1.1.jar" /> <entry name="$MAVEN_REPOSITORY$/com/google/j2objc/j2objc-annotations/1.1/j2objc-annotations-1.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/codehaus/mojo/animal-sniffer-annotations/1.17/animal-sniffer-annotations-1.17.jar" /> <entry name="$MAVEN_REPOSITORY$/org/codehaus/mojo/animal-sniffer-annotations/1.17/animal-sniffer-annotations-1.17.jar" />
<entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-compat-qual/2.5.3/checker-compat-qual-2.5.3.jar" /> <entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-compat-qual/2.5.3/checker-compat-qual-2.5.3.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-spi/2.25.2/dagger-spi-2.25.2.jar" /> <entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-spi/2.26/dagger-spi-2.26.jar" />
<entry name="$MAVEN_REPOSITORY$/com/squareup/javapoet/1.11.1/javapoet-1.11.1.jar" /> <entry name="$MAVEN_REPOSITORY$/com/squareup/javapoet/1.11.1/javapoet-1.11.1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/googlejavaformat/google-java-format/1.5/google-java-format-1.5.jar" /> <entry name="$MAVEN_REPOSITORY$/com/google/googlejavaformat/google-java-format/1.5/google-java-format-1.5.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/javac-shaded/9-dev-r4023-3/javac-shaded-9-dev-r4023-3.jar" /> <entry name="$MAVEN_REPOSITORY$/com/google/errorprone/javac-shaded/9-dev-r4023-3/javac-shaded-9-dev-r4023-3.jar" />
@@ -31,8 +31,8 @@
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar" /> <entry name="$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-metadata-jvm/0.1.0/kotlinx-metadata-jvm-0.1.0.jar" /> <entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-metadata-jvm/0.1.0/kotlinx-metadata-jvm-0.1.0.jar" />
</processorPath> </processorPath>
<module name="commons" />
<module name="keychain" /> <module name="keychain" />
<module name="commons" />
<module name="ui" /> <module name="ui" />
<module name="launcher" /> <module name="launcher" />
</profile> </profile>
+8 -6
View File
@@ -1,11 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="Encoding" addBOMForNewFiles="with NO BOM"> <component name="Encoding" defaultCharsetForPropertiesFiles="UTF-8">
<file url="file://$PROJECT_DIR$/main" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/main" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/buildkit" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/main/commons/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/commons" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/main/keychain/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/keychain" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/main/keychain/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/launcher" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/main/launcher/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/ui" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/main/ui/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/main/ui/src/main/resources" charset="UTF-8" />
<file url="PROJECT" charset="UTF-8" />
</component> </component>
</project> </project>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>org.cryptomator</groupId> <groupId>org.cryptomator</groupId>
<artifactId>main</artifactId> <artifactId>main</artifactId>
<version>1.5.0-beta1</version> <version>1.5.0-beta3</version>
</parent> </parent>
<artifactId>buildkit</artifactId> <artifactId>buildkit</artifactId>
<packaging>pom</packaging> <packaging>pom</packaging>
+7 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>org.cryptomator</groupId> <groupId>org.cryptomator</groupId>
<artifactId>main</artifactId> <artifactId>main</artifactId>
<version>1.5.0-beta1</version> <version>1.5.0-beta3</version>
</parent> </parent>
<artifactId>commons</artifactId> <artifactId>commons</artifactId>
<name>Cryptomator Commons</name> <name>Cryptomator Commons</name>
@@ -48,6 +48,12 @@
<artifactId>easybind</artifactId> <artifactId>easybind</artifactId>
</dependency> </dependency>
<!-- JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
</dependency>
<!-- Google --> <!-- Google -->
<dependency> <dependency>
<groupId>com.google.guava</groupId> <groupId>com.google.guava</groupId>
@@ -5,7 +5,6 @@
*******************************************************************************/ *******************************************************************************/
package org.cryptomator.common; package org.cryptomator.common;
import dagger.Binds;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import javafx.beans.binding.Binding; import javafx.beans.binding.Binding;
@@ -19,6 +18,8 @@ import org.cryptomator.common.vaults.VaultComponent;
import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.frontend.webdav.WebDavServer; import org.cryptomator.frontend.webdav.WebDavServer;
import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
@@ -27,13 +28,29 @@ import java.util.Comparator;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
@Module(subcomponents = {VaultComponent.class}) @Module(subcomponents = {VaultComponent.class})
public abstract class CommonsModule { public abstract class CommonsModule {
private static final int NUM_SCHEDULER_THREADS = 4; private static final Logger LOG = LoggerFactory.getLogger(CommonsModule.class);
private static final int NUM_SCHEDULER_THREADS = 2;
private static final int NUM_CORE_BG_THREADS = 6;
private static final long BG_THREAD_KEEPALIVE_SECONDS = 60l;
@Provides
@Singleton
@Named("licensePublicKey")
static String provideLicensePublicKey() {
// in PEM format without the dash-escaped begin/end lines
return "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQB7NfnqiZbg2KTmoflmZ71PbXru7oW" //
+ "fmnV2yv3eDjlDfGruBrqz9TtXBZV/eYWt31xu1osIqaT12lKBvZ511aaAkIBeOEV" //
+ "gwcBIlJr6kUw7NKzeJt7r2rrsOyQoOG2nWc/Of/NBqA3mIZRHk5Aq1YupFdD26QE" //
+ "r0DzRyj4ixPIt38CQB8=";
}
@Provides @Provides
@Singleton @Singleton
@@ -56,21 +73,41 @@ public abstract class CommonsModule {
@Provides @Provides
@Singleton @Singleton
static ScheduledExecutorService provideScheduledExecutorService(@Named("shutdownTaskScheduler") Consumer<Runnable> shutdownTaskScheduler) { static ScheduledExecutorService provideScheduledExecutorService(ShutdownHook shutdownHook) {
final AtomicInteger threadNumber = new AtomicInteger(1); final AtomicInteger threadNumber = new AtomicInteger(1);
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(NUM_SCHEDULER_THREADS, r -> { ScheduledExecutorService executorService = Executors.newScheduledThreadPool(NUM_SCHEDULER_THREADS, r -> {
String name = String.format("App Scheduled Executor %02d", threadNumber.getAndIncrement());
Thread t = new Thread(r); Thread t = new Thread(r);
t.setName("Background Thread " + threadNumber.getAndIncrement()); t.setName(name);
t.setUncaughtExceptionHandler(CommonsModule::handleUncaughtExceptionInBackgroundThread);
t.setDaemon(true); t.setDaemon(true);
LOG.debug("Starting {}", t.getName());
return t; return t;
}); });
shutdownTaskScheduler.accept(executorService::shutdown); shutdownHook.runOnShutdown(executorService::shutdown);
return executorService; return executorService;
} }
@Binds @Provides
@Singleton @Singleton
abstract ExecutorService bindExecutorService(ScheduledExecutorService executor); static ExecutorService provideExecutorService(ShutdownHook shutdownHook) {
final AtomicInteger threadNumber = new AtomicInteger(1);
ExecutorService executorService = new ThreadPoolExecutor(NUM_CORE_BG_THREADS, Integer.MAX_VALUE, BG_THREAD_KEEPALIVE_SECONDS, TimeUnit.SECONDS, new SynchronousQueue<>(), r -> {
String name = String.format("App Background Thread %03d", threadNumber.getAndIncrement());
Thread t = new Thread(r);
t.setName(name);
t.setUncaughtExceptionHandler(CommonsModule::handleUncaughtExceptionInBackgroundThread);
t.setDaemon(true);
LOG.debug("Starting {}", t.getName());
return t;
});
shutdownHook.runOnShutdown(executorService::shutdown);
return executorService;
}
private static void handleUncaughtExceptionInBackgroundThread(Thread thread, Throwable throwable) {
LOG.error("Uncaught exception in " + thread.getName(), throwable);
}
@Provides @Provides
@Singleton @Singleton
@@ -25,6 +25,7 @@ public class Environment {
private static final Path RELATIVE_HOME_DIR = Paths.get("~"); private static final Path RELATIVE_HOME_DIR = Paths.get("~");
private static final Path ABSOLUTE_HOME_DIR = Paths.get(USER_HOME); private static final Path ABSOLUTE_HOME_DIR = Paths.get(USER_HOME);
private static final char PATH_LIST_SEP = ':'; private static final char PATH_LIST_SEP = ':';
private static final int DEFAULT_MIN_PW_LENGTH = 8;
@Inject @Inject
public Environment() { public Environment() {
@@ -37,6 +38,7 @@ public class Environment {
LOG.debug("cryptomator.keychainPath: {}", System.getProperty("cryptomator.keychainPath")); LOG.debug("cryptomator.keychainPath: {}", System.getProperty("cryptomator.keychainPath"));
LOG.debug("cryptomator.logDir: {}", System.getProperty("cryptomator.logDir")); LOG.debug("cryptomator.logDir: {}", System.getProperty("cryptomator.logDir"));
LOG.debug("cryptomator.mountPointsDir: {}", System.getProperty("cryptomator.mountPointsDir")); LOG.debug("cryptomator.mountPointsDir: {}", System.getProperty("cryptomator.mountPointsDir"));
LOG.debug("cryptomator.minPwLength: {}", System.getProperty("cryptomator.minPwLength"));
} }
public boolean useCustomLogbackConfig() { public boolean useCustomLogbackConfig() {
@@ -63,6 +65,19 @@ public class Environment {
return getPath("cryptomator.mountPointsDir").map(this::replaceHomeDir); return getPath("cryptomator.mountPointsDir").map(this::replaceHomeDir);
} }
public int getMinPwLength() {
return getInt("cryptomator.minPwLength", DEFAULT_MIN_PW_LENGTH);
}
private int getInt(String propertyName, int defaultValue) {
String value = System.getProperty(propertyName);
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) { // includes "null" values
return defaultValue;
}
}
private Optional<Path> getPath(String propertyName) { private Optional<Path> getPath(String propertyName) {
String value = System.getProperty(propertyName); String value = System.getProperty(propertyName);
return Optional.ofNullable(value).map(Paths::get); return Optional.ofNullable(value).map(Paths::get);
@@ -0,0 +1,56 @@
package org.cryptomator.common;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.google.common.io.BaseEncoding;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Optional;
@Singleton
class LicenseChecker {
private final JWTVerifier verifier;
@Inject
public LicenseChecker(@Named("licensePublicKey") String pemEncodedPublicKey) {
Algorithm algorithm = Algorithm.ECDSA512(decodePublicKey(pemEncodedPublicKey), null);
this.verifier = JWT.require(algorithm).build();
}
private static ECPublicKey decodePublicKey(String pemEncodedPublicKey) {
try {
byte[] keyBytes = BaseEncoding.base64().decode(pemEncodedPublicKey);
PublicKey key = KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(keyBytes));
if (key instanceof ECPublicKey) {
return (ECPublicKey) key;
} else {
throw new IllegalStateException("Key not an EC public key.");
}
} catch (InvalidKeySpecException e) {
throw new IllegalArgumentException("Invalid license public key", e);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
public Optional<DecodedJWT> check(String licenseKey) {
try {
return Optional.of(verifier.verify(licenseKey));
} catch (JWTVerificationException exception) {
return Optional.empty();
}
}
}
@@ -0,0 +1,79 @@
package org.cryptomator.common;
import com.auth0.jwt.interfaces.DecodedJWT;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.cryptomator.common.settings.Settings;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Optional;
@Singleton
public class LicenseHolder {
private final Settings settings;
private final LicenseChecker licenseChecker;
private final ObjectProperty<DecodedJWT> validJwtClaims;
private final StringBinding licenseSubject;
private final BooleanBinding validLicenseProperty;
@Inject
public LicenseHolder(LicenseChecker licenseChecker, Settings settings) {
this.settings = settings;
this.licenseChecker = licenseChecker;
this.validJwtClaims = new SimpleObjectProperty<>();
this.licenseSubject = Bindings.createStringBinding(this::getLicenseSubject, validJwtClaims);
this.validLicenseProperty = validJwtClaims.isNotNull();
Optional<DecodedJWT> claims = licenseChecker.check(settings.licenseKey().get());
validJwtClaims.set(claims.orElse(null));
}
public boolean validateAndStoreLicense(String licenseKey) {
Optional<DecodedJWT> claims = licenseChecker.check(licenseKey);
validJwtClaims.set(claims.orElse(null));
if (claims.isPresent()) {
settings.licenseKey().set(licenseKey);
return true;
} else {
return false;
}
}
/* Observable Properties */
public Optional<String> getLicenseKey() {
DecodedJWT claims = validJwtClaims.get();
if (claims != null) {
return Optional.of(claims.getToken());
} else {
return Optional.empty();
}
}
public StringBinding licenseSubjectProperty() {
return licenseSubject;
}
public String getLicenseSubject() {
DecodedJWT claims = validJwtClaims.get();
if (claims != null) {
return claims.getSubject();
} else {
return null;
}
}
public BooleanBinding validLicenseProperty() {
return validLicenseProperty;
}
public boolean isValidLicense() {
return validLicenseProperty.get();
}
}
@@ -1,28 +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.common;
import java.util.Optional;
import java.util.function.Function;
public final class Optionals {
private Optionals() {
}
/**
* Returns a function that is equivalent to the input function but immediately gets the value of the returned optional when invoked.
*
* @param <T> the type of the input to the function
* @param <R> the type of the result of the function
* @param function An {@code Optional}-bearing input function {@code Function<Foo, Optional<Bar>>}
* @return A {@code Function<Foo, Bar>}, that may throw a NoSuchElementException, if the original function returns an empty optional.
*/
public static <T, R> Function<T, R> unwrap(Function<T, Optional<R>> function) {
return t -> function.apply(t).get();
}
}
@@ -0,0 +1,96 @@
/*******************************************************************************
* 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.common;
import com.google.common.util.concurrent.Runnables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Queue;
import java.util.concurrent.PriorityBlockingQueue;
@Singleton
public class ShutdownHook extends Thread {
private static final int PRIO_VERY_LAST = Integer.MIN_VALUE;
public static final int PRIO_LAST = PRIO_VERY_LAST + 1;
public static final int PRIO_DEFAULT = 0;
public static final int PRIO_FIRST = Integer.MAX_VALUE;
private static final Logger LOG = LoggerFactory.getLogger(ShutdownHook.class);
private static final OrderedTask POISON = new OrderedTask(PRIO_VERY_LAST, Runnables.doNothing());
private final Queue<OrderedTask> tasks = new PriorityBlockingQueue<>();
@Inject
ShutdownHook() {
super(null, null, "ShutdownTasks", 0);
Runtime.getRuntime().addShutdownHook(this);
LOG.debug("Registered shutdown hook.");
}
@Override
public void run() {
LOG.debug("Running graceful shutdown tasks...");
tasks.add(POISON);
Runnable task;
while ((task = tasks.remove()) != POISON) {
try {
task.run();
} catch (RuntimeException e) {
LOG.error("Exception while shutting down.", e);
}
}
}
/**
* Schedules a task to be run during shutdown with default order
*
* @param task The task to be scheduled
*/
public void runOnShutdown(Runnable task) {
runOnShutdown(PRIO_DEFAULT, task);
}
/**
* Schedules a task to be run with the given priority
*
* @param priority Tasks with high priority will be run before task with lower priority
* @param task The task to be scheduled
*/
public void runOnShutdown(int priority, Runnable task) {
tasks.add(new OrderedTask(priority, task));
}
private static class OrderedTask implements Comparable<OrderedTask>, Runnable {
private final int priority;
private final Runnable task;
public OrderedTask(int priority, Runnable task) {
this.priority = priority;
this.task = task;
}
@Override
public int compareTo(OrderedTask other) {
// overflow-safe signum impl:
if (this.priority > other.priority) {
return -1; // higher prio -> this before other
} else if (this.priority < other.priority) {
return +1; // lower prio -> other before this
} else {
return 0; // same prio
}
}
@Override
public void run() {
task.run();
}
}
}
@@ -15,6 +15,8 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.geometry.NodeOrientation; import javafx.geometry.NodeOrientation;
@@ -35,6 +37,7 @@ public class Settings {
public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = System.getProperty("os.name").toLowerCase().contains("windows") ? VolumeImpl.DOKANY : VolumeImpl.FUSE; public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = System.getProperty("os.name").toLowerCase().contains("windows") ? VolumeImpl.DOKANY : VolumeImpl.FUSE;
public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT; public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT;
public static final NodeOrientation DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT; public static final NodeOrientation DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT;
private static final String DEFAULT_LICENSE_KEY = "";
private final ObservableList<VaultSettings> directories = FXCollections.observableArrayList(VaultSettings::observables); private final ObservableList<VaultSettings> directories = FXCollections.observableArrayList(VaultSettings::observables);
private final BooleanProperty askedForUpdateCheck = new SimpleBooleanProperty(DEFAULT_ASKED_FOR_UPDATE_CHECK); private final BooleanProperty askedForUpdateCheck = new SimpleBooleanProperty(DEFAULT_ASKED_FOR_UPDATE_CHECK);
@@ -47,6 +50,7 @@ public class Settings {
private final ObjectProperty<VolumeImpl> preferredVolumeImpl = new SimpleObjectProperty<>(DEFAULT_PREFERRED_VOLUME_IMPL); private final ObjectProperty<VolumeImpl> preferredVolumeImpl = new SimpleObjectProperty<>(DEFAULT_PREFERRED_VOLUME_IMPL);
private final ObjectProperty<UiTheme> theme = new SimpleObjectProperty<>(DEFAULT_THEME); private final ObjectProperty<UiTheme> theme = new SimpleObjectProperty<>(DEFAULT_THEME);
private final ObjectProperty<NodeOrientation> userInterfaceOrientation = new SimpleObjectProperty<>(DEFAULT_USER_INTERFACE_ORIENTATION); private final ObjectProperty<NodeOrientation> userInterfaceOrientation = new SimpleObjectProperty<>(DEFAULT_USER_INTERFACE_ORIENTATION);
private final StringProperty licenseKey = new SimpleStringProperty(DEFAULT_LICENSE_KEY);
private Consumer<Settings> saveCmd; private Consumer<Settings> saveCmd;
@@ -65,6 +69,7 @@ public class Settings {
preferredVolumeImpl.addListener(this::somethingChanged); preferredVolumeImpl.addListener(this::somethingChanged);
theme.addListener(this::somethingChanged); theme.addListener(this::somethingChanged);
userInterfaceOrientation.addListener(this::somethingChanged); userInterfaceOrientation.addListener(this::somethingChanged);
licenseKey.addListener(this::somethingChanged);
} }
void setSaveCmd(Consumer<Settings> saveCmd) { void setSaveCmd(Consumer<Settings> saveCmd) {
@@ -126,4 +131,8 @@ public class Settings {
public ObjectProperty<NodeOrientation> userInterfaceOrientation() { public ObjectProperty<NodeOrientation> userInterfaceOrientation() {
return userInterfaceOrientation; return userInterfaceOrientation;
} }
public StringProperty licenseKey() {
return licenseKey;
}
} }
@@ -38,6 +38,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
out.name("preferredVolumeImpl").value(value.preferredVolumeImpl().get().name()); out.name("preferredVolumeImpl").value(value.preferredVolumeImpl().get().name());
out.name("theme").value(value.theme().get().name()); out.name("theme").value(value.theme().get().name());
out.name("uiOrientation").value(value.userInterfaceOrientation().get().name()); out.name("uiOrientation").value(value.userInterfaceOrientation().get().name());
out.name("licenseKey").value(value.licenseKey().get());
out.endObject(); out.endObject();
} }
@@ -90,6 +91,9 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
case "uiOrientation": case "uiOrientation":
settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString())); settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
break; break;
case "licenseKey":
settings.licenseKey().set(in.nextString());
break;
default: default:
LOG.warn("Unsupported vault setting found in JSON: " + name); LOG.warn("Unsupported vault setting found in JSON: " + name);
in.skipValue(); in.skipValue();
@@ -32,7 +32,6 @@ import java.nio.file.Path;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -46,16 +45,17 @@ public class SettingsProvider implements Supplier<Settings> {
private static final Logger LOG = LoggerFactory.getLogger(SettingsProvider.class); private static final Logger LOG = LoggerFactory.getLogger(SettingsProvider.class);
private static final long SAVE_DELAY_MS = 1000; private static final long SAVE_DELAY_MS = 1000;
private final ScheduledExecutorService saveScheduler = Executors.newSingleThreadScheduledExecutor();
private final AtomicReference<ScheduledFuture<?>> scheduledSaveCmd = new AtomicReference<>(); private final AtomicReference<ScheduledFuture<?>> scheduledSaveCmd = new AtomicReference<>();
private final AtomicReference<Settings> settings = new AtomicReference<>(); private final AtomicReference<Settings> settings = new AtomicReference<>();
private final SettingsJsonAdapter settingsJsonAdapter = new SettingsJsonAdapter(); private final SettingsJsonAdapter settingsJsonAdapter = new SettingsJsonAdapter();
private final Environment env; private final Environment env;
private final ScheduledExecutorService scheduler;
private final Gson gson; private final Gson gson;
@Inject @Inject
public SettingsProvider(Environment env) { public SettingsProvider(Environment env, ScheduledExecutorService scheduler) {
this.env = env; this.env = env;
this.scheduler = scheduler;
this.gson = new GsonBuilder() // this.gson = new GsonBuilder() //
.setPrettyPrinting().setLenient().disableHtmlEscaping() // .setPrettyPrinting().setLenient().disableHtmlEscaping() //
.registerTypeAdapter(Settings.class, settingsJsonAdapter) // .registerTypeAdapter(Settings.class, settingsJsonAdapter) //
@@ -98,7 +98,7 @@ public class SettingsProvider implements Supplier<Settings> {
final Optional<Path> settingsPath = env.getSettingsPath().findFirst(); // alway save to preferred (first) path final Optional<Path> settingsPath = env.getSettingsPath().findFirst(); // alway save to preferred (first) path
settingsPath.ifPresent(path -> { settingsPath.ifPresent(path -> {
Runnable saveCommand = () -> this.save(settings, path); Runnable saveCommand = () -> this.save(settings, path);
ScheduledFuture<?> scheduledTask = saveScheduler.schedule(saveCommand, SAVE_DELAY_MS, TimeUnit.MILLISECONDS); ScheduledFuture<?> scheduledTask = scheduler.schedule(saveCommand, SAVE_DELAY_MS, TimeUnit.MILLISECONDS);
ScheduledFuture<?> previouslyScheduledTask = scheduledSaveCmd.getAndSet(scheduledTask); ScheduledFuture<?> previouslyScheduledTask = scheduledSaveCmd.getAndSet(scheduledTask);
if (previouslyScheduledTask != null) { if (previouslyScheduledTask != null) {
previouslyScheduledTask.cancel(false); previouslyScheduledTask.cancel(false);
@@ -18,6 +18,7 @@ import org.cryptomator.cryptofs.migration.Migrators;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException; import java.nio.file.NoSuchFileException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collection; import java.util.Collection;
@@ -67,7 +68,13 @@ public class VaultListManager {
} }
private Optional<Vault> get(Path vaultPath) { private Optional<Vault> get(Path vaultPath) {
return vaultList.stream().filter(v -> v.getPath().equals(vaultPath)).findAny(); return vaultList.stream().filter(v -> {
try {
return Files.isSameFile(vaultPath, v.getPath());
} catch (IOException e) {
return false;
}
}).findAny();
} }
private Vault create(VaultSettings vaultSettings) { private Vault create(VaultSettings vaultSettings) {
@@ -76,7 +83,7 @@ public class VaultListManager {
return comp.vault(); return comp.vault();
} }
private VaultState determineVaultState(Path pathToVault) { public static VaultState determineVaultState(Path pathToVault) {
try { try {
if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) { if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) {
return VaultState.MISSING; return VaultState.MISSING;
@@ -41,7 +41,6 @@ public final class WindowsDriveLetters {
public Set<String> getOccupiedDriveLetters() { public Set<String> getOccupiedDriveLetters() {
if (!SystemUtils.IS_OS_WINDOWS) { if (!SystemUtils.IS_OS_WINDOWS) {
LOG.warn("Attempted to get occupied drive letters on non-Windows machine.");
return Set.of(); return Set.of();
} else { } else {
Iterable<Path> rootDirs = FileSystems.getDefault().getRootDirectories(); Iterable<Path> rootDirs = FileSystems.getDefault().getRootDirectories();
@@ -0,0 +1,62 @@
package org.cryptomator.common;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.cryptomator.common.LicenseChecker;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Optional;
class LicenseCheckerTest {
private static final String PUBLIC_KEY = "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBgc4HZz+/fBbC7lmEww0AO3NK9wVZ" //
+ "PDZ0VEnsaUFLEYpTzb90nITtJUcPUbvOsdZIZ1Q8fnbquAYgxXL5UgHMoywAib47" //
+ "6MkyyYgPk0BXZq3mq4zImTRNuaU9slj9TVJ3ScT3L1bXwVuPJDzpr5GOFpaj+WwM" //
+ "Al8G7CqwoJOsW7Kddns=";
private LicenseChecker licenseChecker;
@BeforeEach
public void setup() {
licenseChecker = new LicenseChecker(PUBLIC_KEY);
}
@Test
public void testCheckValidLicense() {
String license = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6InhaRGZacHJ5NFA5dlpQWnlHMmZOQlJqLTdMejVvbVZkbTd0SG9DZ1NOZlkifQ.eyJzdWIiOiJjcnlwdG9ib3RAZXhhbXBsZS5jb20iLCJpYXQiOjE1MTYyMzkwMjJ9.AQaBIKQdNCxmRJi2wLOcbagTgi39WhdWwgdpKTYSPicg-aPr_tst_RjmnqMemx3cBe0Blr4nEbj_lAtSKHz_i61fAUyI1xCIAZYbK9Q3ICHIHQl3AiuCpBwFl-k81OB4QDYiKpEc9gLN5dhW_VymJMsgOvyiC0UjC91f2AM7s46byDNj";
Optional<DecodedJWT> decoded = licenseChecker.check(license);
Assertions.assertTrue(decoded.isPresent());
Assertions.assertEquals("cryptobot@example.com", decoded.get().getSubject());
}
@Test
public void testCheckInvalidLicenseHeader() {
String license = "EyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6InhaRGZacHJ5NFA5dlpQWnlHMmZOQlJqLTdMejVvbVZkbTd0SG9DZ1NOZlkifQ.eyJzdWIiOiJjcnlwdG9ib3RAZXhhbXBsZS5jb20iLCJpYXQiOjE1MTYyMzkwMjJ9.AQaBIKQdNCxmRJi2wLOcbagTgi39WhdWwgdpKTYSPicg-aPr_tst_RjmnqMemx3cBe0Blr4nEbj_lAtSKHz_i61fAUyI1xCIAZYbK9Q3ICHIHQl3AiuCpBwFl-k81OB4QDYiKpEc9gLN5dhW_VymJMsgOvyiC0UjC91f2AM7s46byDNj";
Optional<DecodedJWT> decoded = licenseChecker.check(license);
Assertions.assertFalse(decoded.isPresent());
}
@Test
public void testCheckInvalidLicensePayload() {
String license = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6InhaRGZacHJ5NFA5dlpQWnlHMmZOQlJqLTdMejVvbVZkbTd0SG9DZ1NOZlkifQ.EyJzdWIiOiJjcnlwdG9ib3RAZXhhbXBsZS5jb20iLCJpYXQiOjE1MTYyMzkwMjJ9.AQaBIKQdNCxmRJi2wLOcbagTgi39WhdWwgdpKTYSPicg-aPr_tst_RjmnqMemx3cBe0Blr4nEbj_lAtSKHz_i61fAUyI1xCIAZYbK9Q3ICHIHQl3AiuCpBwFl-k81OB4QDYiKpEc9gLN5dhW_VymJMsgOvyiC0UjC91f2AM7s46byDNj";
Optional<DecodedJWT> decoded = licenseChecker.check(license);
Assertions.assertFalse(decoded.isPresent());
}
@Test
public void testCheckInvalidLicenseSignature() {
String license = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6InhaRGZacHJ5NFA5dlpQWnlHMmZOQlJqLTdMejVvbVZkbTd0SG9DZ1NOZlkifQ.eyJzdWIiOiJjcnlwdG9ib3RAZXhhbXBsZS5jb20iLCJpYXQiOjE1MTYyMzkwMjJ9.aQaBIKQdNCxmRJi2wLOcbagTgi39WhdWwgdpKTYSPicg-aPr_tst_RjmnqMemx3cBe0Blr4nEbj_lAtSKHz_i61fAUyI1xCIAZYbK9Q3ICHIHQl3AiuCpBwFl-k81OB4QDYiKpEc9gLN5dhW_VymJMsgOvyiC0UjC91f2AM7s46byDNj";
Optional<DecodedJWT> decoded = licenseChecker.check(license);
Assertions.assertFalse(decoded.isPresent());
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>org.cryptomator</groupId> <groupId>org.cryptomator</groupId>
<artifactId>main</artifactId> <artifactId>main</artifactId>
<version>1.5.0-beta1</version> <version>1.5.0-beta3</version>
</parent> </parent>
<artifactId>keychain</artifactId> <artifactId>keychain</artifactId>
<name>System Keychain Access</name> <name>System Keychain Access</name>
@@ -11,6 +11,7 @@ import dagger.Provides;
import dagger.multibindings.ElementsIntoSet; import dagger.multibindings.ElementsIntoSet;
import org.cryptomator.common.JniModule; import org.cryptomator.common.JniModule;
import javax.inject.Singleton;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@@ -24,6 +25,7 @@ public class KeychainModule {
} }
@Provides @Provides
@Singleton
public Optional<KeychainAccess> provideSupportedKeychain(Set<KeychainAccessStrategy> keychainAccessStrategies) { public Optional<KeychainAccess> provideSupportedKeychain(Set<KeychainAccessStrategy> keychainAccessStrategies) {
return keychainAccessStrategies.stream().filter(KeychainAccessStrategy::isSupported).map(KeychainAccess.class::cast).findFirst(); return keychainAccessStrategies.stream().filter(KeychainAccessStrategy::isSupported).map(KeychainAccess.class::cast).findFirst();
} }
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>org.cryptomator</groupId> <groupId>org.cryptomator</groupId>
<artifactId>main</artifactId> <artifactId>main</artifactId>
<version>1.5.0-beta1</version> <version>1.5.0-beta3</version>
</parent> </parent>
<artifactId>launcher</artifactId> <artifactId>launcher</artifactId>
<name>Cryptomator Launcher</name> <name>Cryptomator Launcher</name>
@@ -1,48 +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.launcher;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
class CleanShutdownPerformer extends Thread {
private static final Logger LOG = LoggerFactory.getLogger(CleanShutdownPerformer.class);
private final ConcurrentMap<Runnable, Boolean> tasks = new ConcurrentHashMap<>();
@Inject
CleanShutdownPerformer() {
super(null, null, "ShutdownTasks", 0);
}
@Override
public void run() {
LOG.debug("Running graceful shutdown tasks...");
tasks.keySet().forEach(r -> {
try {
r.run();
} catch (RuntimeException e) {
LOG.error("Exception while shutting down.", e);
}
});
tasks.clear();
}
void scheduleShutdownTask(Runnable task) {
tasks.put(task, Boolean.TRUE);
}
void registerShutdownHook() {
Runtime.getRuntime().addShutdownHook(this);
}
}
@@ -32,17 +32,15 @@ public class Cryptomator {
private final IpcFactory ipcFactory; private final IpcFactory ipcFactory;
private final Optional<String> applicationVersion; private final Optional<String> applicationVersion;
private final CountDownLatch shutdownLatch; private final CountDownLatch shutdownLatch;
private final CleanShutdownPerformer shutdownPerformer;
private final UiLauncher uiLauncher; private final UiLauncher uiLauncher;
@Inject @Inject
Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, IpcFactory ipcFactory, @Named("applicationVersion") Optional<String> applicationVersion, @Named("shutdownLatch") CountDownLatch shutdownLatch, CleanShutdownPerformer shutdownPerformer, UiLauncher uiLauncher) { Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, IpcFactory ipcFactory, @Named("applicationVersion") Optional<String> applicationVersion, @Named("shutdownLatch") CountDownLatch shutdownLatch, UiLauncher uiLauncher) {
this.logConfig = logConfig; this.logConfig = logConfig;
this.debugMode = debugMode; this.debugMode = debugMode;
this.ipcFactory = ipcFactory; this.ipcFactory = ipcFactory;
this.applicationVersion = applicationVersion; this.applicationVersion = applicationVersion;
this.shutdownLatch = shutdownLatch; this.shutdownLatch = shutdownLatch;
this.shutdownPerformer = shutdownPerformer;
this.uiLauncher = uiLauncher; this.uiLauncher = uiLauncher;
} }
@@ -90,7 +88,6 @@ public class Cryptomator {
*/ */
private int runGuiApplication() { private int runGuiApplication() {
try { try {
shutdownPerformer.registerShutdownHook();
uiLauncher.launch(); uiLauncher.launch();
shutdownLatch.await(); shutdownLatch.await();
LOG.info("UI shut down"); LOG.info("UI shut down");
@@ -7,18 +7,10 @@ import javax.inject.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.function.Consumer;
@Module @Module
class CryptomatorModule { class CryptomatorModule {
@Provides
@Singleton
@Named("shutdownTaskScheduler")
Consumer<Runnable> provideShutdownTaskScheduler(CleanShutdownPerformer shutdownPerformer) {
return shutdownPerformer::scheduleShutdownTask;
}
@Provides @Provides
@Singleton @Singleton
@Named("shutdownLatch") @Named("shutdownLatch")
@@ -21,8 +21,10 @@ import java.nio.file.FileSystems;
import java.nio.file.InvalidPathException; import java.nio.file.InvalidPathException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@Singleton @Singleton
@@ -40,7 +42,7 @@ class FileOpenRequestHandler {
} }
private void openFiles(OpenFilesEvent evt) { private void openFiles(OpenFilesEvent evt) {
Stream<Path> pathsToOpen = evt.getFiles().stream().map(File::toPath); Collection<Path> pathsToOpen = evt.getFiles().stream().map(File::toPath).collect(Collectors.toList());
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen); AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
tryToEnqueueFileOpenRequest(launchEvent); tryToEnqueueFileOpenRequest(launchEvent);
} }
@@ -51,16 +53,18 @@ class FileOpenRequestHandler {
// visible for testing // visible for testing
void handleLaunchArgs(FileSystem fs, String[] args) { void handleLaunchArgs(FileSystem fs, String[] args) {
Stream<Path> pathsToOpen = Arrays.stream(args).map(str -> { Collection<Path> pathsToOpen = Arrays.stream(args).map(str -> {
try { try {
return fs.getPath(str); return fs.getPath(str);
} catch (InvalidPathException e) { } catch (InvalidPathException e) {
LOG.trace("Argument not a valid path: {}", str); LOG.trace("Argument not a valid path: {}", str);
return null; return null;
} }
}).filter(Objects::nonNull); }).filter(Objects::nonNull).collect(Collectors.toList());
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen); if (!pathsToOpen.isEmpty()) {
tryToEnqueueFileOpenRequest(launchEvent); AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
tryToEnqueueFileOpenRequest(launchEvent);
}
} }
@@ -8,6 +8,7 @@ import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -27,7 +28,7 @@ class IpcProtocolImpl implements IpcProtocol {
@Override @Override
public void revealRunningApp() { public void revealRunningApp() {
launchEventQueue.add(new AppLaunchEvent(AppLaunchEvent.EventType.REVEAL_APP, Stream.empty())); launchEventQueue.add(new AppLaunchEvent(AppLaunchEvent.EventType.REVEAL_APP, Collections.emptyList()));
} }
@Override @Override
@@ -5,9 +5,8 @@ import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender; import ch.qos.logback.core.Appender;
import ch.qos.logback.core.hook.DelayingShutdownHook;
import ch.qos.logback.core.util.Duration;
import org.cryptomator.common.Environment; import org.cryptomator.common.Environment;
import org.cryptomator.common.ShutdownHook;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
@@ -16,26 +15,27 @@ import java.util.Map;
@Singleton @Singleton
public class LoggerConfiguration { public class LoggerConfiguration {
private static final double SHUTDOWN_DELAY_MS = 100;
private final LoggerContext context; private final LoggerContext context;
private final Environment environment; private final Environment environment;
private final Appender<ILoggingEvent> stdout; private final Appender<ILoggingEvent> stdout;
private final Appender<ILoggingEvent> upgrade; private final Appender<ILoggingEvent> upgrade;
private final Appender<ILoggingEvent> file; private final Appender<ILoggingEvent> file;
private final ShutdownHook shutdownHook;
@Inject @Inject
LoggerConfiguration(LoggerContext context, // LoggerConfiguration(LoggerContext context, //
Environment environment, // Environment environment, //
@Named("stdoutAppender") Appender<ILoggingEvent> stdout, // @Named("stdoutAppender") Appender<ILoggingEvent> stdout, //
@Named("upgradeAppender") Appender<ILoggingEvent> upgrade, // @Named("upgradeAppender") Appender<ILoggingEvent> upgrade, //
@Named("fileAppender") Appender<ILoggingEvent> file) { @Named("fileAppender") Appender<ILoggingEvent> file, //
ShutdownHook shutdownHook) {
this.context = context; this.context = context;
this.environment = environment; this.environment = environment;
this.stdout = stdout; this.stdout = stdout;
this.upgrade = upgrade; this.upgrade = upgrade;
this.file = file; this.file = file;
this.shutdownHook = shutdownHook;
} }
public void init() { public void init() {
@@ -55,16 +55,15 @@ public class LoggerConfiguration {
} }
// configure upgrade logger: // configure upgrade logger:
Logger upgrades = context.getLogger("org.cryptomator.ui.model.upgrade"); Logger upgrades = context.getLogger("org.cryptomator.cryptofs.migration");
upgrades.setLevel(Level.DEBUG); upgrades.setLevel(Level.DEBUG);
upgrades.addAppender(stdout); upgrades.addAppender(stdout);
upgrades.addAppender(upgrade); upgrades.addAppender(upgrade);
upgrades.addAppender(file);
upgrades.setAdditive(false); upgrades.setAdditive(false);
// add shutdown hook // add shutdown hook
DelayingShutdownHook shutdownHook = new DelayingShutdownHook(); shutdownHook.runOnShutdown(ShutdownHook.PRIO_LAST, context::stop);
shutdownHook.setContext(context);
shutdownHook.setDelay(Duration.buildByMilliseconds(SHUTDOWN_DELAY_MS));
} }
} }
@@ -15,16 +15,14 @@ import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.Mockito; import org.mockito.Mockito;
import java.io.IOException;
import java.nio.file.FileSystem; import java.nio.file.FileSystem;
import java.nio.file.InvalidPathException; import java.nio.file.InvalidPathException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.List; import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class FileOpenRequestHandlerTest { public class FileOpenRequestHandlerTest {
@@ -39,32 +37,30 @@ public class FileOpenRequestHandlerTest {
@Test @Test
@DisplayName("./cryptomator.exe foo bar") @DisplayName("./cryptomator.exe foo bar")
public void testOpenArgsWithCorrectPaths() throws IOException { public void testOpenArgsWithCorrectPaths() {
inTest.handleLaunchArgs(new String[]{"foo", "bar"}); inTest.handleLaunchArgs(new String[]{"foo", "bar"});
AppLaunchEvent evt = queue.poll(); AppLaunchEvent evt = queue.poll();
Assertions.assertNotNull(evt); Assertions.assertNotNull(evt);
List<Path> paths = evt.getPathsToOpen().collect(Collectors.toList()); Collection<Path> paths = evt.getPathsToOpen();
MatcherAssert.assertThat(paths, CoreMatchers.hasItems(Paths.get("foo"), Paths.get("bar"))); MatcherAssert.assertThat(paths, CoreMatchers.hasItems(Paths.get("foo"), Paths.get("bar")));
} }
@Test @Test
@DisplayName("./cryptomator.exe foo (with 'foo' being an invalid path)") @DisplayName("./cryptomator.exe foo (with 'foo' being an invalid path)")
public void testOpenArgsWithIncorrectPaths() throws IOException { public void testOpenArgsWithIncorrectPaths() {
FileSystem fs = Mockito.mock(FileSystem.class); FileSystem fs = Mockito.mock(FileSystem.class);
Mockito.when(fs.getPath("foo")).thenThrow(new InvalidPathException("foo", "foo is not a path")); Mockito.when(fs.getPath("foo")).thenThrow(new InvalidPathException("foo", "foo is not a path"));
inTest.handleLaunchArgs(fs, new String[]{"foo"}); inTest.handleLaunchArgs(fs, new String[]{"foo"});
AppLaunchEvent evt = queue.poll(); AppLaunchEvent evt = queue.poll();
Assertions.assertNotNull(evt); Assertions.assertNull(evt);
List<Path> paths = evt.getPathsToOpen().collect(Collectors.toList());
Assertions.assertTrue(paths.isEmpty());
} }
@Test @Test
@DisplayName("./cryptomator.exe foo (with full event queue)") @DisplayName("./cryptomator.exe foo (with full event queue)")
public void testOpenArgsWithFullQueue() throws IOException { public void testOpenArgsWithFullQueue() {
queue.add(new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, Stream.empty())); queue.add(new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, Collections.emptyList()));
Assumptions.assumeTrue(queue.remainingCapacity() == 0); Assumptions.assumeTrue(queue.remainingCapacity() == 0);
inTest.handleLaunchArgs(new String[]{"foo"}); inTest.handleLaunchArgs(new String[]{"foo"});
+22 -16
View File
@@ -3,13 +3,13 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId> <groupId>org.cryptomator</groupId>
<artifactId>main</artifactId> <artifactId>main</artifactId>
<version>1.5.0-beta1</version> <version>1.5.0-beta3</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>Cryptomator</name> <name>Cryptomator</name>
<organization> <organization>
<name>cryptomator.org</name> <name>cryptomator.org</name>
<url>http://cryptomator.org</url> <url>https://cryptomator.org</url>
</organization> </organization>
<developers> <developers>
@@ -23,35 +23,34 @@
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- dependency versions --> <!-- cryptomator dependencies -->
<cryptomator.cryptofs.version>1.9.0-rc2</cryptomator.cryptofs.version> <cryptomator.cryptofs.version>1.9.3</cryptomator.cryptofs.version>
<cryptomator.jni.version>2.2.1</cryptomator.jni.version> <cryptomator.jni.version>2.2.2</cryptomator.jni.version>
<cryptomator.fuse.version>1.2.1</cryptomator.fuse.version> <cryptomator.fuse.version>1.2.2</cryptomator.fuse.version>
<cryptomator.dokany.version>1.1.11</cryptomator.dokany.version> <cryptomator.dokany.version>1.1.12</cryptomator.dokany.version>
<cryptomator.webdav.version>1.0.10</cryptomator.webdav.version> <cryptomator.webdav.version>1.0.10</cryptomator.webdav.version>
<javafx.version>13.0.1</javafx.version> <!-- 3rd party dependencies -->
<javafx.version>14-ea+8</javafx.version>
<commons-lang3.version>3.9</commons-lang3.version> <commons-lang3.version>3.9</commons-lang3.version>
<jwt.version>3.8.3</jwt.version>
<easybind.version>1.0.3</easybind.version> <easybind.version>1.0.3</easybind.version>
<guava.version>28.1-jre</guava.version> <guava.version>28.1-jre</guava.version>
<dagger.version>2.25.2</dagger.version> <dagger.version>2.26</dagger.version>
<gson.version>2.8.6</gson.version> <gson.version>2.8.6</gson.version>
<slf4j.version>1.7.29</slf4j.version> <slf4j.version>1.7.29</slf4j.version>
<logback.version>1.2.3</logback.version> <logback.version>1.2.3</logback.version>
<junit.jupiter.version>5.5.2</junit.jupiter.version> <!-- test dependencies -->
<mockito.version>3.1.0</mockito.version> <junit.jupiter.version>5.6.0</junit.jupiter.version>
<mockito.version>3.2.4</mockito.version>
<hamcrest.version>2.2</hamcrest.version> <hamcrest.version>2.2</hamcrest.version>
</properties> </properties>
<repositories> <repositories>
<repository> <repository>
<id>jcenter</id> <id>jcenter</id>
<url>http://jcenter.bintray.com</url> <url>https://jcenter.bintray.com</url>
</repository> </repository>
</repositories> </repositories>
@@ -156,6 +155,13 @@
<artifactId>commons-lang3</artifactId> <artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version> <version>${commons-lang3.version}</version>
</dependency> </dependency>
<!-- JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<!-- EasyBind --> <!-- EasyBind -->
<dependency> <dependency>
+3 -3
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>org.cryptomator</groupId> <groupId>org.cryptomator</groupId>
<artifactId>main</artifactId> <artifactId>main</artifactId>
<version>1.5.0-beta1</version> <version>1.5.0-beta3</version>
</parent> </parent>
<artifactId>ui</artifactId> <artifactId>ui</artifactId>
<name>Cryptomator GUI</name> <name>Cryptomator GUI</name>
@@ -37,7 +37,7 @@
<dependency> <dependency>
<groupId>org.fxmisc.easybind</groupId> <groupId>org.fxmisc.easybind</groupId>
<artifactId>easybind</artifactId> <artifactId>easybind</artifactId>
</dependency> </dependency>
<!-- Google --> <!-- Google -->
<dependency> <dependency>
@@ -65,7 +65,7 @@
<dependency> <dependency>
<groupId>com.nulab-inc</groupId> <groupId>com.nulab-inc</groupId>
<artifactId>zxcvbn</artifactId> <artifactId>zxcvbn</artifactId>
<version>1.2.7</version> <version>1.3.0</version>
</dependency> </dependency>
<!-- Logging --> <!-- Logging -->
@@ -15,14 +15,14 @@ import javax.inject.Inject;
import java.nio.file.Path; import java.nio.file.Path;
@AddVaultWizardScoped @AddVaultWizardScoped
public class AddVaultFailureExisitingController implements FxController { public class AddVaultFailureExistingController implements FxController {
private final Stage window; private final Stage window;
private final Lazy<Scene> previousScene; private final Lazy<Scene> previousScene;
private final StringBinding vaultName; private final StringBinding vaultName;
@Inject @Inject
AddVaultFailureExisitingController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_EXISTING) Lazy<Scene> previousScene, ObjectProperty<Path> pathOfFailedVault){ AddVaultFailureExistingController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_EXISTING) Lazy<Scene> previousScene, ObjectProperty<Path> pathOfFailedVault){
this.window = window; this.window = window;
this.previousScene = previousScene; this.previousScene = previousScene;
this.vaultName = Bindings.createStringBinding(() -> pathOfFailedVault.get().getFileName().toString(),pathOfFailedVault); this.vaultName = Bindings.createStringBinding(() -> pathOfFailedVault.get().getFileName().toString(),pathOfFailedVault);
@@ -19,18 +19,28 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey; import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import org.cryptomator.ui.mainwindow.MainWindow; import org.cryptomator.ui.mainwindow.MainWindow;
import org.cryptomator.ui.recoverykey.RecoveryKeyDisplayController;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Provider; import javax.inject.Provider;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle; import java.util.ResourceBundle;
@Module @Module
public abstract class AddVaultModule { public abstract class AddVaultModule {
@Provides
@AddVaultWizardScoped
@Named("newPassword")
static ObjectProperty<CharSequence> provideNewPasswordProperty() {
return new SimpleObjectProperty<>("");
}
@Provides @Provides
@AddVaultWizardWindow @AddVaultWizardWindow
@AddVaultWizardScoped @AddVaultWizardScoped
@@ -41,13 +51,13 @@ public abstract class AddVaultModule {
@Provides @Provides
@AddVaultWizardWindow @AddVaultWizardWindow
@AddVaultWizardScoped @AddVaultWizardScoped
static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcon") Optional<Image> windowIcon) { static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage(); Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("addvaultwizard.title")); stage.setTitle(resourceBundle.getString("addvaultwizard.title"));
stage.setResizable(false); stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL); stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner); stage.initOwner(owner);
windowIcon.ifPresent(stage.getIcons()::add); stage.getIcons().addAll(windowIcons);
return stage; return stage;
} }
@@ -150,8 +160,8 @@ public abstract class AddVaultModule {
@Binds @Binds
@IntoMap @IntoMap
@FxControllerKey(AddVaultFailureExisitingController.class) @FxControllerKey(AddVaultFailureExistingController.class)
abstract FxController bindAddVaultFailureExistingController(AddVaultFailureExisitingController controller); abstract FxController bindAddVaultFailureExistingController(AddVaultFailureExistingController controller);
@Binds @Binds
@IntoMap @IntoMap
@@ -168,13 +178,27 @@ public abstract class AddVaultModule {
@FxControllerKey(CreateNewVaultPasswordController.class) @FxControllerKey(CreateNewVaultPasswordController.class)
abstract FxController bindCreateNewVaultPasswordController(CreateNewVaultPasswordController controller); abstract FxController bindCreateNewVaultPasswordController(CreateNewVaultPasswordController controller);
@Binds @Provides
@IntoMap @IntoMap
@FxControllerKey(AddVaultSuccessController.class) @FxControllerKey(NewPasswordController.class)
abstract FxController bindAddVaultSuccessController(AddVaultSuccessController controller); static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty<CharSequence> password) {
return new NewPasswordController(resourceBundle, strengthRater, password);
}
@Binds @Binds
@IntoMap @IntoMap
@FxControllerKey(CreateNewVaultRecoveryKeyController.class) @FxControllerKey(CreateNewVaultRecoveryKeyController.class)
abstract FxController bindCreateNewVaultRecoveryKeyController(CreateNewVaultRecoveryKeyController controller); abstract FxController bindCreateNewVaultRecoveryKeyController(CreateNewVaultRecoveryKeyController controller);
@Provides
@IntoMap
@FxControllerKey(RecoveryKeyDisplayController.class)
static FxController provideRecoveryKeyDisplayController(@AddVaultWizardWindow Stage window, @Named("vaultName") StringProperty vaultName, @Named("recoveryKey") StringProperty recoveryKey, ResourceBundle localization) {
return new RecoveryKeyDisplayController(window, vaultName.get(), recoveryKey.get(), localization);
}
@Binds
@IntoMap
@FxControllerKey(AddVaultSuccessController.class)
abstract FxController bindAddVaultSuccessController(AddVaultSuccessController controller);
} }
@@ -52,8 +52,10 @@ public class CreateNewVaultLocationController implements FxController {
private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH; private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH;
public ToggleGroup predefinedLocationToggler; public ToggleGroup predefinedLocationToggler;
public RadioButton iclouddriveRadioButton;
public RadioButton dropboxRadioButton; public RadioButton dropboxRadioButton;
public RadioButton gdriveRadioButton; public RadioButton gdriveRadioButton;
public RadioButton onedriveRadioButton;
public RadioButton customRadioButton; public RadioButton customRadioButton;
@Inject @Inject
@@ -91,10 +93,14 @@ public class CreateNewVaultLocationController implements FxController {
} }
private void togglePredefinedLocation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) { private void togglePredefinedLocation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
if (dropboxRadioButton.equals(newValue)) { if (iclouddriveRadioButton.equals(newValue)) {
vaultPath.set(locationPresets.getIclouddriveLocation().resolve(vaultName.get()));
} else if (dropboxRadioButton.equals(newValue)) {
vaultPath.set(locationPresets.getDropboxLocation().resolve(vaultName.get())); vaultPath.set(locationPresets.getDropboxLocation().resolve(vaultName.get()));
} else if (gdriveRadioButton.equals(newValue)) { } else if (gdriveRadioButton.equals(newValue)) {
vaultPath.set(locationPresets.getGdriveLocation().resolve(vaultName.get())); vaultPath.set(locationPresets.getGdriveLocation().resolve(vaultName.get()));
} else if (onedriveRadioButton.equals(newValue)) {
vaultPath.set(locationPresets.getOnedriveLocation().resolve(vaultName.get()));
} else if (customRadioButton.equals(newValue)) { } else if (customRadioButton.equals(newValue)) {
vaultPath.set(customVaultPath.resolve(vaultName.get())); vaultPath.set(customVaultPath.resolve(vaultName.get()));
} }
@@ -5,16 +5,14 @@ import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding; import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding; import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.StringProperty; import javafx.beans.property.StringProperty;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay; import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label; import javafx.scene.control.Toggle;
import javafx.scene.layout.HBox; import javafx.scene.control.ToggleGroup;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.common.vaults.VaultListManager;
@@ -24,11 +22,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.Tasks; import org.cryptomator.ui.common.Tasks;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import org.cryptomator.ui.recoverykey.RecoveryKeyFactory; import org.cryptomator.ui.recoverykey.RecoveryKeyFactory;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -58,6 +52,7 @@ public class CreateNewVaultPasswordController implements FxController {
private final Stage window; private final Stage window;
private final Lazy<Scene> chooseLocationScene; private final Lazy<Scene> chooseLocationScene;
private final Lazy<Scene> recoveryKeyScene; private final Lazy<Scene> recoveryKeyScene;
private final Lazy<Scene> successScene;
private final ExecutorService executor; private final ExecutorService executor;
private final RecoveryKeyFactory recoveryKeyFactory; private final RecoveryKeyFactory recoveryKeyFactory;
private final StringProperty vaultNameProperty; private final StringProperty vaultNameProperty;
@@ -66,26 +61,22 @@ public class CreateNewVaultPasswordController implements FxController {
private final StringProperty recoveryKeyProperty; private final StringProperty recoveryKeyProperty;
private final VaultListManager vaultListManager; private final VaultListManager vaultListManager;
private final ResourceBundle resourceBundle; private final ResourceBundle resourceBundle;
private final PasswordStrengthUtil strengthRater; private final ObjectProperty<CharSequence> password;
private final ReadmeGenerator readmeGenerator; private final ReadmeGenerator readmeGenerator;
private final IntegerProperty passwordStrength;
private final BooleanProperty processing; private final BooleanProperty processing;
private final BooleanProperty readyToCreateVault; private final BooleanProperty readyToCreateVault;
private final ObjectBinding<ContentDisplay> createVaultButtonState; private final ObjectBinding<ContentDisplay> createVaultButtonState;
public NiceSecurePasswordField passwordField; public ToggleGroup recoveryKeyChoice;
public NiceSecurePasswordField reenterField; public Toggle showRecoveryKey;
public Label passwordStrengthLabel; public Toggle skipRecoveryKey;
public HBox passwordMatchBox;
public FontAwesome5IconView checkmark;
public FontAwesome5IconView cross;
public Label passwordMatchLabel;
@Inject @Inject
CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy<Scene> recoveryKeyScene, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, ReadmeGenerator readmeGenerator) { CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy<Scene> recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, @Named("newPassword") ObjectProperty<CharSequence> password, ReadmeGenerator readmeGenerator) {
this.window = window; this.window = window;
this.chooseLocationScene = chooseLocationScene; this.chooseLocationScene = chooseLocationScene;
this.recoveryKeyScene = recoveryKeyScene; this.recoveryKeyScene = recoveryKeyScene;
this.successScene = successScene;
this.executor = executor; this.executor = executor;
this.recoveryKeyFactory = recoveryKeyFactory; this.recoveryKeyFactory = recoveryKeyFactory;
this.vaultNameProperty = vaultName; this.vaultNameProperty = vaultName;
@@ -94,9 +85,8 @@ public class CreateNewVaultPasswordController implements FxController {
this.recoveryKeyProperty = recoveryKey; this.recoveryKeyProperty = recoveryKey;
this.vaultListManager = vaultListManager; this.vaultListManager = vaultListManager;
this.resourceBundle = resourceBundle; this.resourceBundle = resourceBundle;
this.strengthRater = strengthRater; this.password = password;
this.readmeGenerator = readmeGenerator; this.readmeGenerator = readmeGenerator;
this.passwordStrength = new SimpleIntegerProperty(-1);
this.processing = new SimpleBooleanProperty(); this.processing = new SimpleBooleanProperty();
this.readyToCreateVault = new SimpleBooleanProperty(); this.readyToCreateVault = new SimpleBooleanProperty();
this.createVaultButtonState = Bindings.createObjectBinding(this::getCreateVaultButtonState, processing); this.createVaultButtonState = Bindings.createObjectBinding(this::getCreateVaultButtonState, processing);
@@ -104,22 +94,8 @@ public class CreateNewVaultPasswordController implements FxController {
@FXML @FXML
public void initialize() { public void initialize() {
//binds the actual strength value to the rating of the password util BooleanBinding isValidNewPassword = Bindings.createBooleanBinding(() -> password.get() != null && password.get().length() > 0, password);
passwordStrength.bind(Bindings.createIntegerBinding(() -> strengthRater.computeRate(passwordField.getCharacters().toString()), passwordField.textProperty())); readyToCreateVault.bind(isValidNewPassword.and(recoveryKeyChoice.selectedToggleProperty().isNotNull()).and(processing.not()));
//binding indicating if the passwords not match
BooleanBinding passwordsMatch = Bindings.createBooleanBinding(() -> CharSequence.compare(passwordField.getCharacters(), reenterField.getCharacters()) == 0, passwordField.textProperty(), reenterField.textProperty());
BooleanBinding reenterFieldNotEmpty = reenterField.textProperty().isNotEmpty();
readyToCreateVault.bind(reenterFieldNotEmpty.and(passwordsMatch).and(processing.not()));
//make match indicator invisible when passwords do not match or one is empty
passwordMatchBox.visibleProperty().bind(reenterFieldNotEmpty);
checkmark.visibleProperty().bind(passwordsMatch.and(reenterFieldNotEmpty));
checkmark.managedProperty().bind(checkmark.visibleProperty());
cross.visibleProperty().bind(passwordsMatch.not().and(reenterFieldNotEmpty));
cross.managedProperty().bind(cross.visibleProperty());
passwordMatchLabel.textProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(resourceBundle.getString("addvaultwizard.new.passwordsMatch")).otherwise(resourceBundle.getString("addvaultwizard.new.passwordsDoNotMatch")));
//bindsings for the password strength indicator
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
} }
@FXML @FXML
@@ -130,7 +106,7 @@ public class CreateNewVaultPasswordController implements FxController {
@FXML @FXML
public void next() { public void next() {
Path pathToVault = vaultPathProperty.get(); Path pathToVault = vaultPathProperty.get();
try { try {
Files.createDirectory(pathToVault); Files.createDirectory(pathToVault);
} catch (FileAlreadyExistsException e) { } catch (FileAlreadyExistsException e) {
@@ -141,12 +117,41 @@ public class CreateNewVaultPasswordController implements FxController {
LOG.error("", e); LOG.error("", e);
} }
if (showRecoveryKey.equals(recoveryKeyChoice.getSelectedToggle())) {
showRecoveryKeyScene();
} else if (skipRecoveryKey.equals(recoveryKeyChoice.getSelectedToggle())) {
showSuccessScene();
} else {
throw new IllegalStateException("Unexpected toggle state");
}
}
private void showRecoveryKeyScene() {
Path pathToVault = vaultPathProperty.get();
processing.set(true); processing.set(true);
Tasks.create(() -> { Tasks.create(() -> {
initializeVault(pathToVault, passwordField.getCharacters()); initializeVault(pathToVault, password.get());
return recoveryKeyFactory.createRecoveryKey(pathToVault, passwordField.getCharacters()); return recoveryKeyFactory.createRecoveryKey(pathToVault, password.get());
}).onSuccess(recoveryKey -> { }).onSuccess(recoveryKey -> {
initializationSucceeded(pathToVault, recoveryKey); initializationSucceeded(pathToVault);
recoveryKeyProperty.set(recoveryKey);
window.setScene(recoveryKeyScene.get());
}).onError(IOException.class, e -> {
// TODO show generic error screen
LOG.error("", e);
}).andFinally(() -> {
processing.set(false);
}).runOnce(executor);
}
private void showSuccessScene() {
Path pathToVault = vaultPathProperty.get();
processing.set(true);
Tasks.create(() -> {
initializeVault(pathToVault, password.get());
}).onSuccess(() -> {
initializationSucceeded(pathToVault);
window.setScene(successScene.get());
}).onError(IOException.class, e -> { }).onError(IOException.class, e -> {
// TODO show generic error screen // TODO show generic error screen
LOG.error("", e); LOG.error("", e);
@@ -175,13 +180,11 @@ public class CreateNewVaultPasswordController implements FxController {
} }
LOG.info("Created vault at {}", path); LOG.info("Created vault at {}", path);
} }
private void initializationSucceeded(Path pathToVault, String recoveryKey) { private void initializationSucceeded(Path pathToVault) {
try { try {
Vault newVault = vaultListManager.add(pathToVault); Vault newVault = vaultListManager.add(pathToVault);
vaultProperty.set(newVault); vaultProperty.set(newVault);
recoveryKeyProperty.set(recoveryKey);
window.setScene(recoveryKeyScene.get());
} catch (NoSuchFileException e) { } catch (NoSuchFileException e) {
throw new UncheckedIOException(e); throw new UncheckedIOException(e);
} }
@@ -197,14 +200,6 @@ public class CreateNewVaultPasswordController implements FxController {
return vaultNameProperty; return vaultNameProperty;
} }
public IntegerProperty passwordStrengthProperty() {
return passwordStrength;
}
public int getPasswordStrength() {
return passwordStrength.get();
}
public BooleanProperty readyToCreateVaultProperty() { public BooleanProperty readyToCreateVaultProperty() {
return readyToCreateVault; return readyToCreateVault;
} }
@@ -1,7 +1,6 @@
package org.cryptomator.ui.addvaultwizard; package org.cryptomator.ui.addvaultwizard;
import dagger.Lazy; import dagger.Lazy;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.stage.Stage; import javafx.stage.Stage;
@@ -10,33 +9,20 @@ import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named;
public class CreateNewVaultRecoveryKeyController implements FxController { public class CreateNewVaultRecoveryKeyController implements FxController {
private final Stage window; private final Stage window;
private final Lazy<Scene> successScene; private final Lazy<Scene> successScene;
private final StringProperty recoveryKeyProperty;
@Inject @Inject
CreateNewVaultRecoveryKeyController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, @Named("recoveryKey")StringProperty recoveryKey) { CreateNewVaultRecoveryKeyController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene) {
this.window = window; this.window = window;
this.successScene = successScene; this.successScene = successScene;
this.recoveryKeyProperty = recoveryKey;
} }
@FXML @FXML
public void next() { public void next() {
window.setScene(successScene.get()); window.setScene(successScene.get());
} }
/* Getter/Setter */
public String getRecoveryKey() {
return recoveryKeyProperty.get();
}
public StringProperty recoveryKeyProperty() {
return recoveryKeyProperty;
}
} }
@@ -13,20 +13,30 @@ import java.nio.file.Paths;
public class LocationPresets { public class LocationPresets {
private static final String USER_HOME = System.getProperty("user.home"); private static final String USER_HOME = System.getProperty("user.home");
private static final String[] ICLOUDDRIVE_LOCATIONS = {"~/Library/Mobile Documents/iCloud~com~setolabs~Cryptomator/Documents"};
private static final String[] DROPBOX_LOCATIONS = {"~/Dropbox"}; private static final String[] DROPBOX_LOCATIONS = {"~/Dropbox"};
private static final String[] GDRIVE_LOCATIONS = {"~/Google Drive"}; private static final String[] GDRIVE_LOCATIONS = {"~/Google Drive"};
private static final String[] ONEDRIVE_LOCATIONS = {"~/OneDrive"};
private final ReadOnlyObjectProperty<Path> iclouddriveLocation;
private final ReadOnlyObjectProperty<Path> dropboxLocation; private final ReadOnlyObjectProperty<Path> dropboxLocation;
private final ReadOnlyObjectProperty<Path> gdriveLocation; private final ReadOnlyObjectProperty<Path> gdriveLocation;
private final ReadOnlyObjectProperty<Path> onedriveLocation;
private final BooleanBinding foundIclouddrive;
private final BooleanBinding foundDropbox; private final BooleanBinding foundDropbox;
private final BooleanBinding foundGdrive; private final BooleanBinding foundGdrive;
private final BooleanBinding foundOnedrive;
@Inject @Inject
public LocationPresets() { public LocationPresets() {
this.iclouddriveLocation = new SimpleObjectProperty<>(existingWritablePath(ICLOUDDRIVE_LOCATIONS));
this.dropboxLocation = new SimpleObjectProperty<>(existingWritablePath(DROPBOX_LOCATIONS)); this.dropboxLocation = new SimpleObjectProperty<>(existingWritablePath(DROPBOX_LOCATIONS));
this.gdriveLocation = new SimpleObjectProperty<>(existingWritablePath(GDRIVE_LOCATIONS)); this.gdriveLocation = new SimpleObjectProperty<>(existingWritablePath(GDRIVE_LOCATIONS));
this.onedriveLocation = new SimpleObjectProperty<>(existingWritablePath(ONEDRIVE_LOCATIONS));
this.foundIclouddrive = iclouddriveLocation.isNotNull();
this.foundDropbox = dropboxLocation.isNotNull(); this.foundDropbox = dropboxLocation.isNotNull();
this.foundGdrive = gdriveLocation.isNotNull(); this.foundGdrive = gdriveLocation.isNotNull();
this.foundOnedrive = onedriveLocation.isNotNull();
} }
private static Path existingWritablePath(String... candidates) { private static Path existingWritablePath(String... candidates) {
@@ -49,6 +59,22 @@ public class LocationPresets {
/* Observables */ /* Observables */
public ReadOnlyObjectProperty<Path> iclouddriveLocationProperty() {
return iclouddriveLocation;
}
public Path getIclouddriveLocation() {
return iclouddriveLocation.get();
}
public BooleanBinding foundIclouddriveProperty() {
return foundIclouddrive;
}
public boolean isFoundIclouddrive() {
return foundIclouddrive.get();
}
public ReadOnlyObjectProperty<Path> dropboxLocationProperty() { public ReadOnlyObjectProperty<Path> dropboxLocationProperty() {
return dropboxLocation; return dropboxLocation;
} }
@@ -73,7 +99,7 @@ public class LocationPresets {
return gdriveLocation.get(); return gdriveLocation.get();
} }
public BooleanBinding froundGdriveProperty() { public BooleanBinding foundGdriveProperty() {
return foundGdrive; return foundGdrive;
} }
@@ -81,4 +107,20 @@ public class LocationPresets {
return foundGdrive.get(); return foundGdrive.get();
} }
public ReadOnlyObjectProperty<Path> onedriveLocationProperty() {
return onedriveLocation;
}
public Path getOnedriveLocation() {
return onedriveLocation.get();
}
public BooleanBinding foundOnedriveProperty() {
return foundOnedrive;
}
public boolean isFoundOnedrive() {
return foundOnedrive.get();
}
} }
@@ -10,7 +10,7 @@ public class ReadmeGenerator {
// specs: https://web.archive.org/web/20190708132914/http://www.kleinlercher.at/tools/Windows_Protocols/Word2007RTFSpec9.pdf // specs: https://web.archive.org/web/20190708132914/http://www.kleinlercher.at/tools/Windows_Protocols/Word2007RTFSpec9.pdf
private static final String RTF_HEADER = "{\\rtf1\\fbidis\\ansi\\uc0\\fs32\n"; private static final String RTF_HEADER = "{\\rtf1\\fbidis\\ansi\\uc0\\fs32\n";
private static final String RTF_FOOTER = "}"; private static final String RTF_FOOTER = "}";
private static final String HELP_URL = "{\\field{\\*\\fldinst HYPERLINK \"http://www.google.com/\"}{\\fldrslt google.com}}"; private static final String HELP_URL = "{\\field{\\*\\fldinst HYPERLINK \"http://docs.cryptoamtor.org/\"}{\\fldrslt docs.cryptoamtor.org}}";
private final ResourceBundle resourceBundle; private final ResourceBundle resourceBundle;
@@ -3,6 +3,7 @@ package org.cryptomator.ui.changepassword;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding; import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty; import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleIntegerProperty;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Button; import javafx.scene.control.Button;
@@ -22,6 +23,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named;
import java.io.IOException; import java.io.IOException;
import java.util.ResourceBundle; import java.util.ResourceBundle;
@@ -33,47 +35,24 @@ public class ChangePasswordController implements FxController {
private final Stage window; private final Stage window;
private final Vault vault; private final Vault vault;
private final ResourceBundle resourceBundle; private final ObjectProperty<CharSequence> newPassword;
private final PasswordStrengthUtil strengthRater;
private final IntegerProperty passwordStrength;
public NiceSecurePasswordField oldPasswordField; public NiceSecurePasswordField oldPasswordField;
public NiceSecurePasswordField newPasswordField;
public NiceSecurePasswordField reenterPasswordField;
public Label passwordStrengthLabel;
public HBox passwordMatchBox;
public FontAwesome5IconView checkmark;
public FontAwesome5IconView cross;
public Label passwordMatchLabel;
public CheckBox finalConfirmationCheckbox; public CheckBox finalConfirmationCheckbox;
public Button finishButton; public Button finishButton;
@Inject @Inject
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) { public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty<CharSequence> newPassword) {
this.window = window; this.window = window;
this.vault = vault; this.vault = vault;
this.resourceBundle = resourceBundle; this.newPassword = newPassword;
this.strengthRater = strengthRater;
this.passwordStrength = new SimpleIntegerProperty(-1);
} }
@FXML @FXML
public void initialize() { public void initialize() {
//binds the actual strength value to the rating of the password util BooleanBinding hasNotConfirmedCheckbox = finalConfirmationCheckbox.selectedProperty().not();
passwordStrength.bind(Bindings.createIntegerBinding(() -> strengthRater.computeRate(newPasswordField.getCharacters().toString()), newPasswordField.textProperty())); BooleanBinding isInvalidNewPassword = Bindings.createBooleanBinding(() -> newPassword.get() == null || newPassword.get().length() == 0, newPassword);
//binding indicating if the passwords not match finishButton.disableProperty().bind(hasNotConfirmedCheckbox.or(isInvalidNewPassword));
BooleanBinding passwordsMatch = Bindings.createBooleanBinding(() -> CharSequence.compare(newPasswordField.getCharacters(), reenterPasswordField.getCharacters()) == 0, newPasswordField.textProperty(), reenterPasswordField.textProperty());
BooleanBinding reenterFieldNotEmpty = reenterPasswordField.textProperty().isNotEmpty();
//disable the finish button when passwords do not match or one is empty
finishButton.disableProperty().bind(reenterFieldNotEmpty.not().or(passwordsMatch.not()).or(finalConfirmationCheckbox.selectedProperty().not()));
//make match indicator invisible when passwords do not match or one is empty
passwordMatchBox.visibleProperty().bind(reenterFieldNotEmpty);
checkmark.visibleProperty().bind(passwordsMatch.and(reenterFieldNotEmpty));
checkmark.managedProperty().bind(checkmark.visibleProperty());
cross.visibleProperty().bind(passwordsMatch.not().and(reenterFieldNotEmpty));
cross.managedProperty().bind(cross.visibleProperty());
passwordMatchLabel.textProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(resourceBundle.getString("changepassword.passwordsMatch")).otherwise(resourceBundle.getString("changepassword.passwordsDoNotMatch")));
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
} }
@FXML @FXML
@@ -84,15 +63,15 @@ public class ChangePasswordController implements FxController {
@FXML @FXML
public void finish() { public void finish() {
try { try {
CryptoFileSystemProvider.changePassphrase(vault.getPath(), MASTERKEY_FILENAME, oldPasswordField.getCharacters(), newPasswordField.getCharacters()); CryptoFileSystemProvider.changePassphrase(vault.getPath(), MASTERKEY_FILENAME, oldPasswordField.getCharacters(), newPassword.get());
LOG.info("Successful changed password for {}", vault.getDisplayableName()); LOG.info("Successful changed password for {}", vault.getDisplayableName());
window.close(); window.close();
} catch (IOException e) { } catch (IOException e) {
//TODO // TODO show generic error screen
LOG.error("IO error occured during password change. Unable to perform operation.", e); LOG.error("IO error occured during password change. Unable to perform operation.", e);
e.printStackTrace(); e.printStackTrace();
} catch (InvalidPassphraseException e) { } catch (InvalidPassphraseException e) {
//TODO // TODO shake
LOG.info("Wrong old password."); LOG.info("Wrong old password.");
} }
} }
@@ -102,12 +81,5 @@ public class ChangePasswordController implements FxController {
public Vault getVault() { public Vault getVault() {
return vault; return vault;
} }
public IntegerProperty passwordStrengthProperty() {
return passwordStrength;
}
public int getPasswordStrength() {
return passwordStrength.get();
}
} }
@@ -4,6 +4,8 @@ import dagger.Binds;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import dagger.multibindings.IntoMap; import dagger.multibindings.IntoMap;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.stage.Modality; import javafx.stage.Modality;
@@ -14,15 +16,25 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey; import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Provider; import javax.inject.Provider;
import java.nio.CharBuffer;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle; import java.util.ResourceBundle;
@Module @Module
abstract class ChangePasswordModule { abstract class ChangePasswordModule {
@Provides
@ChangePasswordScoped
@Named("newPassword")
static ObjectProperty<CharSequence> provideNewPasswordProperty() {
return new SimpleObjectProperty<>("");
}
@Provides @Provides
@ChangePasswordWindow @ChangePasswordWindow
@@ -34,13 +46,13 @@ abstract class ChangePasswordModule {
@Provides @Provides
@ChangePasswordWindow @ChangePasswordWindow
@ChangePasswordScoped @ChangePasswordScoped
static Stage provideStage(@Named("changePasswordOwner") Stage owner, ResourceBundle resourceBundle, @Named("windowIcon") Optional<Image> windowIcon) { static Stage provideStage(@Named("changePasswordOwner") Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage(); Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("changepassword.title")); stage.setTitle(resourceBundle.getString("changepassword.title"));
stage.setResizable(false); stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL); stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner); stage.initOwner(owner);
windowIcon.ifPresent(stage.getIcons()::add); stage.getIcons().addAll(windowIcons);
return stage; return stage;
} }
@@ -58,5 +70,12 @@ abstract class ChangePasswordModule {
@IntoMap @IntoMap
@FxControllerKey(ChangePasswordController.class) @FxControllerKey(ChangePasswordController.class)
abstract FxController bindUnlockController(ChangePasswordController controller); abstract FxController bindUnlockController(ChangePasswordController controller);
@Provides
@IntoMap
@FxControllerKey(NewPasswordController.class)
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty<CharSequence> password) {
return new NewPasswordController(resourceBundle, strengthRater, password);
}
} }
@@ -0,0 +1,36 @@
package org.cryptomator.ui.common;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.value.WritableValue;
import javafx.stage.Window;
import javafx.util.Duration;
public class Animations {
public static Timeline createShakeWindowAnimation(Window window) {
WritableValue<Double> writableWindowX = new WritableValue<>() {
@Override
public Double getValue() {
return window.getX();
}
@Override
public void setValue(Double value) {
window.setX(value);
}
};
return new Timeline( //
new KeyFrame(Duration.ZERO, new KeyValue(writableWindowX, window.getX())), //
new KeyFrame(new Duration(100), new KeyValue(writableWindowX, window.getX() - 22.0)), //
new KeyFrame(new Duration(200), new KeyValue(writableWindowX, window.getX() + 18.0)), //
new KeyFrame(new Duration(300), new KeyValue(writableWindowX, window.getX() - 14.0)), //
new KeyFrame(new Duration(400), new KeyValue(writableWindowX, window.getX() + 10.0)), //
new KeyFrame(new Duration(500), new KeyValue(writableWindowX, window.getX() - 6.0)), //
new KeyFrame(new Duration(600), new KeyValue(writableWindowX, window.getX() + 2.0)), //
new KeyFrame(new Duration(700), new KeyValue(writableWindowX, window.getX())) //
);
}
}
@@ -12,15 +12,21 @@ public enum FxmlFile {
CHANGEPASSWORD("/fxml/changepassword.fxml"), // CHANGEPASSWORD("/fxml/changepassword.fxml"), //
FORGET_PASSWORD("/fxml/forget_password.fxml"), // FORGET_PASSWORD("/fxml/forget_password.fxml"), //
MAIN_WINDOW("/fxml/main_window.fxml"), // MAIN_WINDOW("/fxml/main_window.fxml"), //
MIGRATION_CAPABILITY_ERROR("/fxml/migration_capability_error.fxml"), //
MIGRATION_GENERIC_ERROR("/fxml/migration_generic_error.fxml"), //
MIGRATION_RUN("/fxml/migration_run.fxml"), // MIGRATION_RUN("/fxml/migration_run.fxml"), //
MIGRATION_START("/fxml/migration_start.fxml"), // MIGRATION_START("/fxml/migration_start.fxml"), //
MIGRATION_SUCCESS("/fxml/migration_success.fxml"), // MIGRATION_SUCCESS("/fxml/migration_success.fxml"), //
PREFERENCES("/fxml/preferences.fxml"), // PREFERENCES("/fxml/preferences.fxml"), //
QUIT("/fxml/quit.fxml"), // QUIT("/fxml/quit.fxml"), //
RECOVERYKEY_CREATE("/fxml/recoverykey_create.fxml"), // RECOVERYKEY_CREATE("/fxml/recoverykey_create.fxml"), //
RECOVERYKEY_DISPLAY("/fxml/recoverykey_display.fxml"), // RECOVERYKEY_RECOVER("/fxml/recoverykey_recover.fxml"), //
RECOVERYKEY_RESET_PASSWORD("/fxml/recoverykey_reset_password.fxml"), //
RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), //
REMOVE_VAULT("/fxml/remove_vault.fxml"), // REMOVE_VAULT("/fxml/remove_vault.fxml"), //
UNLOCK("/fxml/unlock.fxml"), UNLOCK("/fxml/unlock.fxml"),
UNLOCK_GENERIC_ERROR("/fxml/unlock_generic_error.fxml"), //
UNLOCK_INVALID_MOUNT_POINT("/fxml/unlock_invalid_mount_point.fxml"), //
UNLOCK_SUCCESS("/fxml/unlock_success.fxml"), // UNLOCK_SUCCESS("/fxml/unlock_success.fxml"), //
VAULT_OPTIONS("/fxml/vault_options.fxml"), // VAULT_OPTIONS("/fxml/vault_options.fxml"), //
WRONGFILEALERT("/fxml/wrongfilealert.fxml"); WRONGFILEALERT("/fxml/wrongfilealert.fxml");
@@ -0,0 +1,74 @@
package org.cryptomator.ui.common;
import javafx.beans.Observable;
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.Label;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.fxmisc.easybind.EasyBind;
import java.util.ResourceBundle;
public class NewPasswordController implements FxController {
private final ResourceBundle resourceBundle;
private final PasswordStrengthUtil strengthRater;
private final ObjectProperty<CharSequence> password;
private final IntegerProperty passwordStrength = new SimpleIntegerProperty(-1);
public NiceSecurePasswordField passwordField;
public NiceSecurePasswordField reenterField;
public Label passwordStrengthLabel;
public Label passwordMatchLabel;
public FontAwesome5IconView checkmark;
public FontAwesome5IconView cross;
public NewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, ObjectProperty<CharSequence> password) {
this.resourceBundle = resourceBundle;
this.strengthRater = strengthRater;
this.password = password;
}
@FXML
public void initialize() {
BooleanBinding passwordsMatch = Bindings.createBooleanBinding(this::hasSamePasswordInBothFields, passwordField.textProperty(), reenterField.textProperty());
BooleanBinding reenterFieldNotEmpty = reenterField.textProperty().isNotEmpty();
passwordStrength.bind(Bindings.createIntegerBinding(() -> strengthRater.computeRate(passwordField.getCharacters()), passwordField.textProperty()));
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
passwordMatchLabel.visibleProperty().bind(reenterFieldNotEmpty);
passwordMatchLabel.graphicProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(checkmark).otherwise(cross));
passwordMatchLabel.textProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(resourceBundle.getString("newPassword.passwordsMatch")).otherwise(resourceBundle.getString("newPassword.passwordsDoNotMatch")));
passwordField.textProperty().addListener(this::passwordsDidChange);
reenterField.textProperty().addListener(this::passwordsDidChange);
}
private void passwordsDidChange(@SuppressWarnings("unused") Observable observable) {
if (hasSamePasswordInBothFields() && strengthRater.fulfillsMinimumRequirements(passwordField.getCharacters())) {
password.set(passwordField.getCharacters());
} else {
password.set("");
}
}
private boolean hasSamePasswordInBothFields() {
return CharSequence.compare(passwordField.getCharacters(), reenterField.getCharacters()) == 0;
}
/* Getter/Setter */
public IntegerProperty passwordStrengthProperty() {
return passwordStrength;
}
public int getPasswordStrength() {
return passwordStrength.get();
}
}
@@ -8,12 +8,11 @@
*******************************************************************************/ *******************************************************************************/
package org.cryptomator.ui.common; package org.cryptomator.ui.common;
import com.google.common.base.Strings;
import com.nulabinc.zxcvbn.Zxcvbn; import com.nulabinc.zxcvbn.Zxcvbn;
import org.cryptomator.common.Environment;
import org.cryptomator.ui.fxapp.FxApplicationScoped; import org.cryptomator.ui.fxapp.FxApplicationScoped;
import javax.inject.Inject; import javax.inject.Inject;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.ResourceBundle; import java.util.ResourceBundle;
@@ -22,30 +21,37 @@ public class PasswordStrengthUtil {
private static final int PW_TRUNC_LEN = 100; // truncate very long passwords, since zxcvbn memory and runtime depends vastly on the length private static final int PW_TRUNC_LEN = 100; // truncate very long passwords, since zxcvbn memory and runtime depends vastly on the length
private static final String RESSOURCE_PREFIX = "passwordStrength.messageLabel."; private static final String RESSOURCE_PREFIX = "passwordStrength.messageLabel.";
private static final List<String> SANITIZED_INPUTS = List.of("cryptomator");
private final Zxcvbn zxcvbn;
private final List<String> sanitizedInputs;
private final ResourceBundle resourceBundle; private final ResourceBundle resourceBundle;
private final int minPwLength;
private final Zxcvbn zxcvbn;
@Inject @Inject
public PasswordStrengthUtil(ResourceBundle resourceBundle) { public PasswordStrengthUtil(ResourceBundle resourceBundle, Environment environment) {
this.resourceBundle = resourceBundle; this.resourceBundle = resourceBundle;
this.minPwLength = environment.getMinPwLength();
this.zxcvbn = new Zxcvbn(); this.zxcvbn = new Zxcvbn();
this.sanitizedInputs = List.of("cryptomator");
} }
public int computeRate(String password) { public boolean fulfillsMinimumRequirements(CharSequence password) {
if (Strings.isNullOrEmpty(password)) { return password.length() >= minPwLength;
}
public int computeRate(CharSequence password) {
if (password == null || password.length() < minPwLength) {
return -1; return -1;
} else { } else {
int numCharsToRate = Math.min(PW_TRUNC_LEN, password.length()); int numCharsToRate = Math.min(PW_TRUNC_LEN, password.length());
return zxcvbn.measure(password.substring(0, numCharsToRate), sanitizedInputs).getScore(); return zxcvbn.measure(password.subSequence(0, numCharsToRate), SANITIZED_INPUTS).getScore();
} }
} }
public String getStrengthDescription(Number score) { public String getStrengthDescription(Number score) {
if (resourceBundle.containsKey(RESSOURCE_PREFIX + score.intValue())) { if (score.intValue() == -1) {
return resourceBundle.getString("passwordStrength.messageLabel." + score.intValue()); return String.format(resourceBundle.getString(RESSOURCE_PREFIX + "tooShort"), minPwLength);
} else if (resourceBundle.containsKey(RESSOURCE_PREFIX + score.intValue())) {
return resourceBundle.getString(RESSOURCE_PREFIX + score.intValue());
} else { } else {
return ""; return "";
} }
@@ -0,0 +1,28 @@
package org.cryptomator.ui.common;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
public class StackTraceController implements FxController {
private final String stackTrace;
public StackTraceController(Throwable cause) {
this.stackTrace = provideStackTrace(cause);
}
private static String provideStackTrace(Throwable cause) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
cause.printStackTrace(new PrintStream(baos));
return baos.toString(StandardCharsets.UTF_8);
}
/* Getter/Setter */
public String getStackTrace() {
return stackTrace;
}
}
@@ -73,21 +73,21 @@ public class Tasks {
return new TaskImpl<>(callable, successHandler, errorHandlers, finallyHandler); return new TaskImpl<>(callable, successHandler, errorHandlers, finallyHandler);
} }
public Task<T> runOnce(ExecutorService executorService) { public Task<T> runOnce(ExecutorService executor) {
Task<T> task = build(); Task<T> task = build();
executorService.submit(task); executor.submit(task);
return task; return task;
} }
public Task<T> scheduleOnce(ScheduledExecutorService executorService, long delay, TimeUnit unit) { public Task<T> scheduleOnce(ScheduledExecutorService scheduler, long delay, TimeUnit unit) {
Task<T> task = build(); Task<T> task = build();
executorService.schedule(task, delay, unit); scheduler.schedule(task, delay, unit);
return task; return task;
} }
public ScheduledService<T> schedulePeriodically(ExecutorService executorService, Duration initialDelay, Duration period) { public ScheduledService<T> schedulePeriodically(ExecutorService executor, Duration initialDelay, Duration period) {
ScheduledService<T> service = new RestartingService<>(this::build); ScheduledService<T> service = new RestartingService<>(this::build);
service.setExecutor(executorService); service.setExecutor(executor);
service.setDelay(initialDelay); service.setDelay(initialDelay);
service.setPeriod(period); service.setPeriod(period);
Platform.runLater(service::start); Platform.runLater(service::start);
@@ -0,0 +1,350 @@
package org.cryptomator.ui.common;
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.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;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
@FxApplicationScoped
public class VaultService {
private static final Logger LOG = LoggerFactory.getLogger(VaultService.class);
private final ExecutorService executorService;
private final Optional<KeychainAccess> keychain;
@Inject
public VaultService(ExecutorService executorService, Optional<KeychainAccess> keychain) {
this.executorService = executorService;
this.keychain = keychain;
}
public void reveal(Vault vault) {
executorService.execute(createRevealTask(vault));
}
/**
* Creates but doesn't start a reveal task.
*
* @param vault The vault to reveal
*/
public Task<Vault> createRevealTask(Vault vault) {
Task<Vault> task = new RevealVaultTask(vault);
task.setOnSucceeded(evt -> LOG.info("Revealed {}", vault.getDisplayableName()));
task.setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayableName(), evt.getSource().getException()));
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 {
for (Vault vault : vaults) {
attemptAutoUnlock(vault, keychain.get());
}
}
}
/**
* Unlocks a vault in a background thread using a stored passphrase
*
* @param vault The vault to unlock
* @param keychainAccess The system keychain holding the passphrase for the vault
*/
public void attemptAutoUnlock(Vault vault, KeychainAccess keychainAccess) {
executorService.execute(createAutoUnlockTask(vault, keychainAccess));
}
/**
* 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.
*
* @param vault The vault to lock
* @param forced Whether to attempt a forced lock
*/
public void lock(Vault vault, boolean forced) {
executorService.execute(createLockTask(vault, forced));
}
/**
* Creates but doesn't start a lock task.
*
* @param vault The vault to lock
* @param forced Whether to attempt a forced lock
* @return The task
*/
public Task<Vault> createLockTask(Vault vault, boolean forced) {
Task<Vault> task = new LockVaultTask(vault, forced);
task.setOnSucceeded(evt -> LOG.info("Locked {}", vault.getDisplayableName()));
task.setOnFailed(evt -> LOG.error("Failed to lock " + vault.getDisplayableName(), evt.getSource().getException()));
return task;
}
/**
* Locks all given vaults in a background thread.
*
* @param vaults The vaults to lock
* @param forced Whether to attempt a forced lock
*/
public void lockAll(Collection<Vault> vaults, boolean forced) {
executorService.execute(createLockAllTask(vaults, forced));
}
/**
* Creates but doesn't start a lock-all task.
*
* @param vaults The list of vaults to be locked
* @param forced Whether to attempt a forced lock
* @return Meta-Task that waits until all vaults are locked or fails after the first failure of a subtask
*/
public Task<Collection<Vault>> createLockAllTask(Collection<Vault> vaults, boolean forced) {
List<Task<Vault>> lockTasks = vaults.stream().map(v -> new LockVaultTask(v, forced)).collect(Collectors.toUnmodifiableList());
lockTasks.forEach(executorService::execute);
Task<Collection<Vault>> task = new WaitForTasksTask(lockTasks);
String vaultNames = vaults.stream().map(Vault::getDisplayableName).collect(Collectors.joining(", "));
task.setOnSucceeded(evt -> LOG.info("Locked {}", vaultNames));
task.setOnFailed(evt -> LOG.error("Failed to lock vaults " + vaultNames, evt.getSource().getException()));
return task;
}
private static class RevealVaultTask extends Task<Vault> {
private final Vault vault;
/**
* @param vault The vault to lock
*/
public RevealVaultTask(Vault vault) {
this.vault = vault;
}
@Override
protected Vault call() throws Volume.VolumeException {
vault.reveal();
return vault;
}
}
/**
* A task that waits for completion of multiple other tasks
*/
private static class WaitForTasksTask extends Task<Collection<Vault>> {
private final Collection<Task<Vault>> startedTasks;
public WaitForTasksTask(Collection<Task<Vault>> tasks) {
this.startedTasks = List.copyOf(tasks);
}
@Override
protected Collection<Vault> call() throws Exception {
Iterator<Task<Vault>> remainingTasks = startedTasks.iterator();
Collection<Vault> completed = new ArrayList<>();
try {
// wait for all tasks:
while (remainingTasks.hasNext()) {
Vault lockedVault = remainingTasks.next().get();
completed.add(lockedVault);
}
} catch (ExecutionException e) {
// cancel all remaining:
while (remainingTasks.hasNext()) {
remainingTasks.next().cancel(true);
}
throw e;
}
return List.copyOf(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
*/
private static class LockVaultTask extends Task<Vault> {
private final Vault vault;
private final boolean forced;
/**
* @param vault The vault to lock
* @param forced Whether to attempt a forced lock
*/
public LockVaultTask(Vault vault, boolean forced) {
this.vault = vault;
this.forced = forced;
}
@Override
protected Vault call() throws Volume.VolumeException {
vault.lock(forced);
return vault;
}
@Override
protected void scheduled() {
vault.setState(VaultState.PROCESSING);
}
@Override
protected void succeeded() {
vault.setState(VaultState.LOCKED);
}
@Override
protected void failed() {
vault.setState(VaultState.UNLOCKED);
}
}
}
@@ -5,28 +5,35 @@ package org.cryptomator.ui.controls;
*/ */
public enum FontAwesome5Icon { public enum FontAwesome5Icon {
ANCHOR("\uF13D"), // ANCHOR("\uF13D"), //
ARROW_ALT_UP("\uF357"), // ARROW_UP("\uF062"), //
CHECK("\uF00C"), // CHECK("\uF00C"), //
COG("\uF013"), // COG("\uF013"), //
COGS("\uF085"), // COGS("\uF085"), //
EXCLAMATION("\uF12A"), COPY("\uF0C5"), //
CROWN("\uF521"), //
EXCLAMATION("\uF12A"), //
EXCLAMATION_CIRCLE("\uF06A"), //
EXCLAMATION_TRIANGLE("\uF071"), // EXCLAMATION_TRIANGLE("\uF071"), //
EYE("\uF06E"), // EYE("\uF06E"), //
EYE_SLASH("\uF070"), // EYE_SLASH("\uF070"), //
FILE_IMPORT("\uF56F"), // FILE_IMPORT("\uF56F"), //
FOLDER_OPEN("\uF07C"), // FOLDER_OPEN("\uF07C"), //
HAND_HOLDING_HEART("\uF4BE"), //
HEART("\uF004"), //
HDD("\uF0A0"), // HDD("\uF0A0"), //
KEY("\uF084"), // KEY("\uF084"), //
LOCK_ALT("\uF30D"), // LINK("\uF0C1"), //
LOCK_OPEN_ALT("\uF3C2"), // LOCK("\uF023"), //
MINUS("\uF068"), // LOCK_OPEN("\uF3C1"), //
MAGIC("\uF0D0"), //
PLUS("\uF067"), // PLUS("\uF067"), //
PRINT("\uF02F"), //
QUESTION("\uF128"), // QUESTION("\uF128"), //
SPARKLES("\uF890"), //
SPINNER("\uF110"), // SPINNER("\uF110"), //
SYNC("\uF021"), // SYNC("\uF021"), //
TIMES("\uF00D"), // TIMES("\uF00D"), //
WRENCH("\uF0AD"), // WRENCH("\uF0AD"), //
WINDOW_MINIMIZE("\uF2D1"), //
; ;
private final String unicode; private final String unicode;
@@ -18,7 +18,7 @@ public class FontAwesome5IconView extends Text {
private static final FontAwesome5Icon DEFAULT_GLYPH = FontAwesome5Icon.ANCHOR; private static final FontAwesome5Icon DEFAULT_GLYPH = FontAwesome5Icon.ANCHOR;
private static final double DEFAULT_GLYPH_SIZE = 12.0; private static final double DEFAULT_GLYPH_SIZE = 12.0;
private static final String FONT_PATH = "/css/fontawesome5-pro-solid.otf"; private static final String FONT_PATH = "/css/fontawesome5-free-solid.otf";
private static final Font FONT; private static final Font FONT;
private ObjectProperty<FontAwesome5Icon> glyph = new SimpleObjectProperty<>(this, "glyph", DEFAULT_GLYPH); private ObjectProperty<FontAwesome5Icon> glyph = new SimpleObjectProperty<>(this, "glyph", DEFAULT_GLYPH);
@@ -32,7 +32,7 @@ public class NiceSecurePasswordField extends StackPane {
iconContainer.getStyleClass().add(ICONS_STLYE_CLASS); iconContainer.getStyleClass().add(ICONS_STLYE_CLASS);
StackPane.setAlignment(iconContainer, Pos.CENTER_RIGHT); StackPane.setAlignment(iconContainer, Pos.CENTER_RIGHT);
capsLockedIcon.setGlyph(FontAwesome5Icon.ARROW_ALT_UP); capsLockedIcon.setGlyph(FontAwesome5Icon.ARROW_UP);
capsLockedIcon.setGlyphSize(ICON_SIZE); capsLockedIcon.setGlyphSize(ICON_SIZE);
capsLockedIcon.visibleProperty().bind(passwordField.capsLockedProperty()); capsLockedIcon.visibleProperty().bind(passwordField.capsLockedProperty());
capsLockedIcon.managedProperty().bind(passwordField.capsLockedProperty()); capsLockedIcon.managedProperty().bind(passwordField.capsLockedProperty());
@@ -89,7 +89,7 @@ public class NiceSecurePasswordField extends StackPane {
} }
public void swipe() { public void swipe() {
passwordField.swipe();; passwordField.swipe();
} }
public void selectAll() { public void selectAll() {
@@ -20,8 +20,8 @@ import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Provider; import javax.inject.Provider;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle; import java.util.ResourceBundle;
@Module @Module
@@ -37,13 +37,13 @@ abstract class ForgetPasswordModule {
@Provides @Provides
@ForgetPasswordWindow @ForgetPasswordWindow
@ForgetPasswordScoped @ForgetPasswordScoped
static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcon") Optional<Image> windowIcon, @Named("forgetPasswordOwner") Stage owner) { static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons, @Named("forgetPasswordOwner") Stage owner) {
Stage stage = new Stage(); Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("forgetPassword.title")); stage.setTitle(resourceBundle.getString("forgetPassword.title"));
stage.setResizable(false); stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL); stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner); stage.initOwner(owner);
windowIcon.ifPresent(stage.getIcons()::add); stage.getIcons().addAll(windowIcons);
return stage; return stage;
} }
@@ -9,6 +9,7 @@ import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableSet; import javafx.collections.ObservableSet;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.cryptomator.common.LicenseHolder;
import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.UiTheme; import org.cryptomator.common.settings.UiTheme;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
@@ -16,8 +17,10 @@ import org.cryptomator.jni.JniException;
import org.cryptomator.jni.MacApplicationUiAppearance; import org.cryptomator.jni.MacApplicationUiAppearance;
import org.cryptomator.jni.MacApplicationUiState; import org.cryptomator.jni.MacApplicationUiState;
import org.cryptomator.jni.MacFunctions; import org.cryptomator.jni.MacFunctions;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.mainwindow.MainWindowComponent; import org.cryptomator.ui.mainwindow.MainWindowComponent;
import org.cryptomator.ui.preferences.PreferencesComponent; import org.cryptomator.ui.preferences.PreferencesComponent;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import org.cryptomator.ui.quit.QuitComponent; import org.cryptomator.ui.quit.QuitComponent;
import org.cryptomator.ui.unlock.UnlockComponent; import org.cryptomator.ui.unlock.UnlockComponent;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -38,17 +41,21 @@ public class FxApplication extends Application {
private final UnlockComponent.Builder unlockWindowBuilder; private final UnlockComponent.Builder unlockWindowBuilder;
private final QuitComponent.Builder quitWindowBuilder; private final QuitComponent.Builder quitWindowBuilder;
private final Optional<MacFunctions> macFunctions; private final Optional<MacFunctions> macFunctions;
private final VaultService vaultService;
private final LicenseHolder licenseHolder;
private final ObservableSet<Stage> visibleStages = FXCollections.observableSet(); private final ObservableSet<Stage> visibleStages = FXCollections.observableSet();
private final BooleanBinding hasVisibleStages = Bindings.isNotEmpty(visibleStages); private final BooleanBinding hasVisibleStages = Bindings.isNotEmpty(visibleStages);
@Inject @Inject
FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, UnlockComponent.Builder unlockWindowBuilder, QuitComponent.Builder quitWindowBuilder, Optional<MacFunctions> macFunctions) { FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, UnlockComponent.Builder unlockWindowBuilder, QuitComponent.Builder quitWindowBuilder, Optional<MacFunctions> macFunctions, VaultService vaultService, LicenseHolder licenseHolder) {
this.settings = settings; this.settings = settings;
this.mainWindow = mainWindow; this.mainWindow = mainWindow;
this.preferencesWindow = preferencesWindow; this.preferencesWindow = preferencesWindow;
this.unlockWindowBuilder = unlockWindowBuilder; this.unlockWindowBuilder = unlockWindowBuilder;
this.quitWindowBuilder = quitWindowBuilder; this.quitWindowBuilder = quitWindowBuilder;
this.macFunctions = macFunctions; this.macFunctions = macFunctions;
this.vaultService = vaultService;
this.licenseHolder = licenseHolder;
} }
public void start() { public void start() {
@@ -79,9 +86,9 @@ public class FxApplication extends Application {
} }
} }
public void showPreferencesWindow() { public void showPreferencesWindow(SelectedPreferencesTab selectedTab) {
Platform.runLater(() -> { Platform.runLater(() -> {
Stage stage = preferencesWindow.get().showPreferencesWindow(); Stage stage = preferencesWindow.get().showPreferencesWindow(selectedTab);
addVisibleStage(stage); addVisibleStage(stage);
LOG.debug("Showing Preferences"); LOG.debug("Showing Preferences");
}); });
@@ -111,11 +118,16 @@ public class FxApplication extends Application {
}); });
} }
public VaultService getVaultService() {
return vaultService;
}
private void themeChanged(@SuppressWarnings("unused") ObservableValue<? extends UiTheme> observable, @SuppressWarnings("unused") UiTheme oldValue, UiTheme newValue) { private void themeChanged(@SuppressWarnings("unused") ObservableValue<? extends UiTheme> observable, @SuppressWarnings("unused") UiTheme oldValue, UiTheme newValue) {
loadSelectedStyleSheet(newValue); loadSelectedStyleSheet(newValue);
} }
private void loadSelectedStyleSheet(UiTheme theme) { private void loadSelectedStyleSheet(UiTheme desiredTheme) {
UiTheme theme = licenseHolder.isValidLicense() ? desiredTheme : UiTheme.LIGHT;
switch (theme) { switch (theme) {
// case CUSTOM: // case CUSTOM:
// // TODO // // TODO
@@ -5,8 +5,10 @@
*******************************************************************************/ *******************************************************************************/
package org.cryptomator.ui.fxapp; package org.cryptomator.ui.fxapp;
import dagger.Binds;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import javafx.application.Application;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.image.Image; import javafx.scene.image.Image;
@@ -20,7 +22,9 @@ import org.cryptomator.ui.unlock.UnlockComponent;
import javax.inject.Named; import javax.inject.Named;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Optional; import java.io.UncheckedIOException;
import java.util.Collections;
import java.util.List;
@Module(includes = {UpdateCheckerModule.class}, subcomponents = {MainWindowComponent.class, PreferencesComponent.class, UnlockComponent.class, QuitComponent.class}) @Module(includes = {UpdateCheckerModule.class}, subcomponents = {MainWindowComponent.class, PreferencesComponent.class, UnlockComponent.class, QuitComponent.class})
abstract class FxApplicationModule { abstract class FxApplicationModule {
@@ -32,19 +36,32 @@ abstract class FxApplicationModule {
} }
@Provides @Provides
@Named("windowIcon") @Named("windowIcons")
@FxApplicationScoped @FxApplicationScoped
static Optional<Image> provideWindowIcon() { static List<Image> provideWindowIcons() {
if (SystemUtils.IS_OS_MAC) { if (SystemUtils.IS_OS_MAC) {
return Optional.empty(); return Collections.emptyList();
} }
try (InputStream in = FxApplicationModule.class.getResourceAsStream("/window_icon_32.png")) { // TODO: use some higher res depending on display?
return Optional.of(new Image(in)); try {
return List.of( //
createImageFromResource("/window_icon_32.png"), //
createImageFromResource("/window_icon_512.png") //
);
} catch (IOException e) { } catch (IOException e) {
return Optional.empty(); throw new UncheckedIOException("Failed to load embedded resource.", e);
} }
} }
private static Image createImageFromResource(String resourceName) throws IOException {
try (InputStream in = FxApplicationModule.class.getResourceAsStream(resourceName)) {
return new Image(in);
}
}
@Binds
abstract Application bindApplication(FxApplication application);
@Provides @Provides
static MainWindowComponent provideMainWindowComponent(MainWindowComponent.Builder builder) { static MainWindowComponent provideMainWindowComponent(MainWindowComponent.Builder builder) {
return builder.build(); return builder.build();
@@ -1,16 +1,17 @@
package org.cryptomator.ui.launcher; package org.cryptomator.ui.launcher;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collection;
import java.util.stream.Stream; import java.util.stream.Stream;
public class AppLaunchEvent { public class AppLaunchEvent {
private final Stream<Path> pathsToOpen;
private final EventType type; private final EventType type;
private final Collection<Path> pathsToOpen;
public enum EventType {REVEAL_APP, OPEN_FILE} public enum EventType {REVEAL_APP, OPEN_FILE}
public AppLaunchEvent(EventType type, Stream<Path> pathsToOpen) { public AppLaunchEvent(EventType type, Collection<Path> pathsToOpen) {
this.type = type; this.type = type;
this.pathsToOpen = pathsToOpen; this.pathsToOpen = pathsToOpen;
} }
@@ -19,7 +20,7 @@ public class AppLaunchEvent {
return type; return type;
} }
public Stream<Path> getPathsToOpen() { public Collection<Path> getPathsToOpen() {
return pathsToOpen; return pathsToOpen;
} }
} }
@@ -0,0 +1,125 @@
package org.cryptomator.ui.launcher;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.collections.ObservableList;
import org.cryptomator.common.ShutdownHook;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.awt.Desktop;
import java.awt.EventQueue;
import java.awt.desktop.QuitResponse;
import java.util.EnumSet;
import java.util.EventObject;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
@Singleton
public class AppLifecycleListener {
private static final Logger LOG = LoggerFactory.getLogger(AppLifecycleListener.class);
public static final Set<VaultState> STATES_ALLOWING_TERMINATION = EnumSet.of(VaultState.LOCKED, VaultState.NEEDS_MIGRATION, VaultState.MISSING, VaultState.ERROR);
private final FxApplicationStarter fxApplicationStarter;
private final CountDownLatch shutdownLatch;
private final ObservableList<Vault> vaults;
private final AtomicBoolean allowQuitWithoutPrompt;
@Inject
AppLifecycleListener(FxApplicationStarter fxApplicationStarter, @Named("shutdownLatch") CountDownLatch shutdownLatch, ShutdownHook shutdownHook, ObservableList<Vault> vaults) {
this.fxApplicationStarter = fxApplicationStarter;
this.shutdownLatch = shutdownLatch;
this.vaults = vaults;
this.allowQuitWithoutPrompt = new AtomicBoolean(true);
vaults.addListener(this::vaultListChanged);
// register preferences shortcut
if (Desktop.getDesktop().isSupported(Desktop.Action.APP_PREFERENCES)) {
Desktop.getDesktop().setPreferencesHandler(this::showPreferencesWindow);
}
// register quit handler
if (Desktop.getDesktop().isSupported(Desktop.Action.APP_QUIT_HANDLER)) {
Desktop.getDesktop().setQuitHandler(this::handleQuitRequest);
}
shutdownHook.runOnShutdown(this::forceUnmountRemainingVaults);
}
/**
* Gracefully terminates the application.
*/
public void quit() {
handleQuitRequest(null, new QuitResponse() {
@Override
public void performQuit() {
System.exit(0);
}
@Override
public void cancelQuit() {
// no-op
}
});
}
private void handleQuitRequest(@SuppressWarnings("unused") EventObject e, QuitResponse response) {
QuitResponse decoratedQuitResponse = decorateQuitResponse(response);
if (allowQuitWithoutPrompt.get()) {
decoratedQuitResponse.performQuit();
} else {
fxApplicationStarter.get(true).thenAccept(app -> app.showQuitWindow(decoratedQuitResponse));
}
}
private QuitResponse decorateQuitResponse(QuitResponse originalQuitResponse) {
return new QuitResponse() {
@Override
public void performQuit() {
Platform.exit(); // will be no-op, if JavaFX never started.
shutdownLatch.countDown(); // main thread is waiting for this latch
EventQueue.invokeLater(originalQuitResponse::performQuit); // this will eventually call System.exit(0)
}
@Override
public void cancelQuit() {
originalQuitResponse.cancelQuit();
}
};
}
private void vaultListChanged(@SuppressWarnings("unused") Observable observable) {
assert Platform.isFxApplicationThread();
boolean allVaultsAllowTermination = vaults.stream().map(Vault::getState).allMatch(STATES_ALLOWING_TERMINATION::contains);
boolean suddenTerminationChanged = allowQuitWithoutPrompt.compareAndSet(!allVaultsAllowTermination, allVaultsAllowTermination);
if (suddenTerminationChanged) {
LOG.debug("Allow quitting without prompt: {}", allVaultsAllowTermination);
}
}
private void showPreferencesWindow(@SuppressWarnings("unused") EventObject actionEvent) {
fxApplicationStarter.get(true).thenAccept(app -> app.showPreferencesWindow(SelectedPreferencesTab.ANY));
}
private void forceUnmountRemainingVaults() {
for (Vault vault : vaults) {
if (vault.isUnlocked()) {
try {
vault.lock(true);
} catch (Volume.VolumeException e) {
LOG.error("Failed to unmount vault " + vault.getPath(), e);
}
}
}
}
}
@@ -1,6 +1,8 @@
package org.cryptomator.ui.launcher; package org.cryptomator.ui.launcher;
import javafx.collections.ObservableList;
import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.jni.JniException; import org.cryptomator.jni.JniException;
import org.cryptomator.jni.MacApplicationUiState; import org.cryptomator.jni.MacApplicationUiState;
import org.cryptomator.jni.MacFunctions; import org.cryptomator.jni.MacFunctions;
@@ -13,9 +15,8 @@ import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import java.awt.Desktop; import java.awt.Desktop;
import java.awt.SystemTray; import java.awt.SystemTray;
import java.awt.desktop.AppReopenedEvent;
import java.awt.desktop.AppReopenedListener; import java.awt.desktop.AppReopenedListener;
import java.awt.desktop.SystemEventListener; import java.util.Collection;
import java.util.Optional; import java.util.Optional;
@Singleton @Singleton
@@ -24,14 +25,16 @@ public class UiLauncher {
private static final Logger LOG = LoggerFactory.getLogger(UiLauncher.class); private static final Logger LOG = LoggerFactory.getLogger(UiLauncher.class);
private final Settings settings; private final Settings settings;
private final ObservableList<Vault> vaults;
private final TrayMenuComponent.Builder trayComponent; private final TrayMenuComponent.Builder trayComponent;
private final FxApplicationStarter fxApplicationStarter; private final FxApplicationStarter fxApplicationStarter;
private final AppLaunchEventHandler launchEventHandler; private final AppLaunchEventHandler launchEventHandler;
private final Optional<MacFunctions> macFunctions; private final Optional<MacFunctions> macFunctions;
@Inject @Inject
public UiLauncher(Settings settings, TrayMenuComponent.Builder trayComponent, FxApplicationStarter fxApplicationStarter, AppLaunchEventHandler launchEventHandler, Optional<MacFunctions> macFunctions) { public UiLauncher(Settings settings, ObservableList<Vault> vaults, TrayMenuComponent.Builder trayComponent, FxApplicationStarter fxApplicationStarter, AppLaunchEventHandler launchEventHandler, Optional<MacFunctions> macFunctions) {
this.settings = settings; this.settings = settings;
this.vaults = vaults;
this.trayComponent = trayComponent; this.trayComponent = trayComponent;
this.fxApplicationStarter = fxApplicationStarter; this.fxApplicationStarter = fxApplicationStarter;
this.launchEventHandler = launchEventHandler; this.launchEventHandler = launchEventHandler;
@@ -48,7 +51,7 @@ public class UiLauncher {
} }
// show window on start? // show window on start?
if (settings.startHidden().get()) { if (hasTrayIcon && settings.startHidden().get()) {
LOG.debug("Hiding application..."); LOG.debug("Hiding application...");
macFunctions.map(MacFunctions::uiState).ifPresent(JniException.ignore(MacApplicationUiState::transformToAgentApplication)); macFunctions.map(MacFunctions::uiState).ifPresent(JniException.ignore(MacApplicationUiState::transformToAgentApplication));
} else { } else {
@@ -58,6 +61,12 @@ public class UiLauncher {
// register app reopen listener // register app reopen listener
Desktop.getDesktop().addAppEventListener((AppReopenedListener) e -> showMainWindowAsync(hasTrayIcon)); 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));
}
launchEventHandler.startHandlingLaunchEvents(hasTrayIcon); launchEventHandler.startHandlingLaunchEvents(hasTrayIcon);
} }
@@ -1,30 +1,24 @@
package org.cryptomator.ui.mainwindow; package org.cryptomator.ui.mainwindow;
import javafx.beans.binding.BooleanBinding; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.input.DragEvent;
import javafx.scene.input.TransferMode; import javafx.scene.input.TransferMode;
import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.fxapp.UpdateChecker;
import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent; import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named;
import java.io.File; import java.io.File;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException; import java.nio.file.NoSuchFileException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collection; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
@MainWindowScoped @MainWindowScoped
public class MainWindowController implements FxController { public class MainWindowController implements FxController {
@@ -32,27 +26,14 @@ public class MainWindowController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(MainWindowController.class); private static final Logger LOG = LoggerFactory.getLogger(MainWindowController.class);
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes
private final Stage window;
private final FxApplication application;
private final boolean minimizeToSysTray;
private final UpdateChecker updateChecker;
private final BooleanBinding updateAvailable;
private final VaultListManager vaultListManager; private final VaultListManager vaultListManager;
private final WrongFileAlertComponent.Builder wrongFileAlert; private final WrongFileAlertComponent.Builder wrongFileAlert;
public HBox titleBar; private final BooleanProperty draggingOver = new SimpleBooleanProperty();
public VBox root; private final BooleanProperty draggingVaultOver = new SimpleBooleanProperty();
public Pane dragAndDropIndicator; public StackPane root;
public Region resizer;
private double xOffset;
private double yOffset;
@Inject @Inject
public MainWindowController(@MainWindow Stage window, FxApplication application, @Named("trayMenuSupported") boolean minimizeToSysTray, UpdateChecker updateChecker, VaultListManager vaultListManager, WrongFileAlertComponent.Builder wrongFileAlert) { public MainWindowController(VaultListManager vaultListManager, WrongFileAlertComponent.Builder wrongFileAlert) {
this.window = window;
this.application = application;
this.minimizeToSysTray = minimizeToSysTray;
this.updateChecker = updateChecker;
this.updateAvailable = updateChecker.latestVersionProperty().isNotNull();
this.vaultListManager = vaultListManager; this.vaultListManager = vaultListManager;
this.wrongFileAlert = wrongFileAlert; this.wrongFileAlert = wrongFileAlert;
} }
@@ -60,77 +41,70 @@ public class MainWindowController implements FxController {
@FXML @FXML
public void initialize() { public void initialize() {
LOG.debug("init MainWindowController"); LOG.debug("init MainWindowController");
titleBar.setOnMousePressed(event -> { root.setOnDragEntered(this::handleDragEvent);
xOffset = event.getSceneX(); root.setOnDragOver(this::handleDragEvent);
yOffset = event.getSceneY(); root.setOnDragDropped(this::handleDragEvent);
}); root.setOnDragExited(this::handleDragEvent);
titleBar.setOnMouseDragged(event -> {
window.setX(event.getScreenX() - xOffset);
window.setY(event.getScreenY() - yOffset);
});
resizer.setOnMouseDragged(event -> {
// we know for a fact that window is borderless. i.e. the scene starts at 0/0 of the window.
window.setWidth(event.getSceneX());
window.setHeight(event.getSceneY());
});
updateChecker.automaticallyCheckForUpdatesIfEnabled();
dragAndDropIndicator.setVisible(false);
root.setOnDragOver(event -> {
if (event.getGestureSource() != root && event.getDragboard().hasFiles()) {
/* allow for both copying and moving, whatever user chooses */
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
dragAndDropIndicator.setVisible(true);
}
event.consume();
});
root.setOnDragExited(event -> dragAndDropIndicator.setVisible(false));
root.setOnDragDropped(event -> {
if (event.getGestureSource() != root && event.getDragboard().hasFiles()) {
/* allow for both copying and moving, whatever user chooses */
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
Collection<Vault> vaultPaths = event.getDragboard().getFiles().stream().map(File::toPath).flatMap(this::addVault).collect(Collectors.toSet());
if (vaultPaths.isEmpty()) {
wrongFileAlert.build().showWrongFileAlertWindow();
}
}
event.consume();
});
} }
private Stream<Vault> addVault(Path pathToVault) { private void handleDragEvent(DragEvent event) {
if (DragEvent.DRAG_ENTERED.equals(event.getEventType()) && event.getGestureSource() == null) {
draggingOver.set(true);
} else if (DragEvent.DRAG_OVER.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) {
event.acceptTransferModes(TransferMode.ANY);
draggingVaultOver.set(event.getDragboard().getFiles().stream().map(File::toPath).anyMatch(this::containsVault));
} else if (DragEvent.DRAG_DROPPED.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) {
Set<Path> vaultPaths = event.getDragboard().getFiles().stream().map(File::toPath).filter(this::containsVault).collect(Collectors.toSet());
if (vaultPaths.isEmpty()) {
wrongFileAlert.build().showWrongFileAlertWindow();
} else {
vaultPaths.forEach(this::addVault);
}
event.setDropCompleted(!vaultPaths.isEmpty());
event.consume();
} else if (DragEvent.DRAG_EXITED.equals(event.getEventType())) {
draggingOver.set(false);
draggingVaultOver.set(false);
}
}
private boolean containsVault(Path path) {
if (path.getFileName().toString().equals(MASTERKEY_FILENAME)) {
return true;
} else if (Files.isDirectory(path) && Files.exists(path.resolve(MASTERKEY_FILENAME))) {
return true;
} else {
return false;
}
}
private void addVault(Path pathToVault) {
try { try {
if (pathToVault.getFileName().toString().equals(MASTERKEY_FILENAME)) { if (pathToVault.getFileName().toString().equals(MASTERKEY_FILENAME)) {
return Stream.of(vaultListManager.add(pathToVault.getParent())); vaultListManager.add(pathToVault.getParent());
} else { } else {
return Stream.of(vaultListManager.add(pathToVault)); vaultListManager.add(pathToVault);
} }
} catch (NoSuchFileException e) { } catch (NoSuchFileException e) {
LOG.debug("Not a vault: {}", pathToVault); LOG.debug("Not a vault: {}", pathToVault);
} }
return Stream.empty();
}
@FXML
public void close() {
if (minimizeToSysTray) {
window.close();
} else {
window.setIconified(true);
}
}
@FXML
public void showPreferences() {
application.showPreferencesWindow();
} }
/* Getter/Setter */ /* Getter/Setter */
public BooleanBinding updateAvailableProperty() { public BooleanProperty draggingOverProperty() {
return updateAvailable; return draggingOver;
} }
public boolean isUpdateAvailable() { public boolean isDraggingOver() {
return updateAvailable.get(); return draggingOver.get();
}
public BooleanProperty draggingVaultOverProperty() {
return draggingVaultOver;
}
public boolean isDraggingVaultOver() {
return draggingVaultOver.get();
} }
} }
@@ -25,8 +25,8 @@ import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Provider; import javax.inject.Provider;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle; import java.util.ResourceBundle;
@Module(subcomponents = {AddVaultWizardComponent.class, MigrationComponent.class, RemoveVaultComponent.class, VaultOptionsComponent.class, WrongFileAlertComponent.class}) @Module(subcomponents = {AddVaultWizardComponent.class, MigrationComponent.class, RemoveVaultComponent.class, VaultOptionsComponent.class, WrongFileAlertComponent.class})
@@ -42,7 +42,7 @@ abstract class MainWindowModule {
@Provides @Provides
@MainWindow @MainWindow
@MainWindowScoped @MainWindowScoped
static Stage provideStage(@Named("windowIcon") Optional<Image> windowIcon) { static Stage provideStage(@Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage(StageStyle.UNDECORATED); Stage stage = new Stage(StageStyle.UNDECORATED);
// TODO: min/max values chosen arbitrarily. We might wanna take a look at the user's resolution... // TODO: min/max values chosen arbitrarily. We might wanna take a look at the user's resolution...
stage.setMinWidth(650); stage.setMinWidth(650);
@@ -50,7 +50,7 @@ abstract class MainWindowModule {
stage.setMaxWidth(1000); stage.setMaxWidth(1000);
stage.setMaxHeight(700); stage.setMaxHeight(700);
stage.setTitle("Cryptomator"); stage.setTitle("Cryptomator");
windowIcon.ifPresent(stage.getIcons()::add); stage.getIcons().addAll(windowIcons);
return stage; return stage;
} }
@@ -68,6 +68,16 @@ abstract class MainWindowModule {
@FxControllerKey(MainWindowController.class) @FxControllerKey(MainWindowController.class)
abstract FxController bindMainWindowController(MainWindowController controller); abstract FxController bindMainWindowController(MainWindowController controller);
@Binds
@IntoMap
@FxControllerKey(MainWindowTitleController.class)
abstract FxController bindMainWindowTitleController(MainWindowTitleController controller);
@Binds
@IntoMap
@FxControllerKey(ResizeController.class)
abstract FxController bindResizeController(ResizeController controller);
@Binds @Binds
@IntoMap @IntoMap
@FxControllerKey(VaultListController.class) @FxControllerKey(VaultListController.class)
@@ -17,22 +17,22 @@ public class MainWindowSceneFactory extends DefaultSceneFactory {
protected static final KeyCodeCombination SHORTCUT_N = new KeyCodeCombination(KeyCode.N, KeyCombination.SHORTCUT_DOWN); protected static final KeyCodeCombination SHORTCUT_N = new KeyCodeCombination(KeyCode.N, KeyCombination.SHORTCUT_DOWN);
private final Lazy<MainWindowController> mainWindowController; private final Lazy<MainWindowTitleController> mainWindowTitleController;
private final Lazy<VaultListController> vaultListController; private final Lazy<VaultListController> vaultListController;
@Inject @Inject
public MainWindowSceneFactory(Settings settings, Lazy<MainWindowController> mainWindowController, Lazy<VaultListController> vaultListController) { public MainWindowSceneFactory(Settings settings, Lazy<MainWindowTitleController> mainWindowTitleController, Lazy<VaultListController> vaultListController) {
super(settings); super(settings);
this.mainWindowController = mainWindowController; this.mainWindowTitleController = mainWindowTitleController;
this.vaultListController = vaultListController; this.vaultListController = vaultListController;
} }
@Override @Override
protected void setupDefaultAccelerators(Scene scene, Stage stage) { protected void setupDefaultAccelerators(Scene scene, Stage stage) {
if (SystemUtils.IS_OS_WINDOWS) { if (SystemUtils.IS_OS_WINDOWS) {
scene.getAccelerators().put(ALT_F4, mainWindowController.get()::close); scene.getAccelerators().put(ALT_F4, mainWindowTitleController.get()::close);
} else { } else {
scene.getAccelerators().put(SHORTCUT_W, mainWindowController.get()::close); scene.getAccelerators().put(SHORTCUT_W, mainWindowTitleController.get()::close);
} }
scene.getAccelerators().put(SHORTCUT_N, vaultListController.get()::didClickAddVault); scene.getAccelerators().put(SHORTCUT_N, vaultListController.get()::didClickAddVault);
} }
@@ -0,0 +1,107 @@
package org.cryptomator.ui.mainwindow;
import javafx.beans.binding.BooleanBinding;
import javafx.fxml.FXML;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import org.cryptomator.common.LicenseHolder;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.fxapp.UpdateChecker;
import org.cryptomator.ui.launcher.AppLifecycleListener;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
@MainWindowScoped
public class MainWindowTitleController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(MainWindowTitleController.class);
public HBox titleBar;
private final AppLifecycleListener appLifecycle;
private final Stage window;
private final FxApplication application;
private final boolean minimizeToSysTray;
private final UpdateChecker updateChecker;
private final BooleanBinding updateAvailable;
private final LicenseHolder licenseHolder;
private double xOffset;
private double yOffset;
@Inject
MainWindowTitleController(AppLifecycleListener appLifecycle, @MainWindow Stage window, FxApplication application, @Named("trayMenuSupported") boolean minimizeToSysTray, UpdateChecker updateChecker, LicenseHolder licenseHolder) {
this.appLifecycle = appLifecycle;
this.window = window;
this.application = application;
this.minimizeToSysTray = minimizeToSysTray;
this.updateChecker = updateChecker;
this.updateAvailable = updateChecker.latestVersionProperty().isNotNull();
this.licenseHolder = licenseHolder;
}
@FXML
public void initialize() {
LOG.debug("init MainWindowTitleController");
updateChecker.automaticallyCheckForUpdatesIfEnabled();
titleBar.setOnMousePressed(event -> {
xOffset = event.getSceneX();
yOffset = event.getSceneY();
});
titleBar.setOnMouseDragged(event -> {
window.setX(event.getScreenX() - xOffset);
window.setY(event.getScreenY() - yOffset);
});
window.setOnCloseRequest(event -> {
close();
event.consume();
});
}
@FXML
public void close() {
if (minimizeToSysTray) {
window.close();
} else {
appLifecycle.quit();
}
}
@FXML
public void minimize() {
window.setIconified(true);
}
@FXML
public void showPreferences() {
application.showPreferencesWindow(SelectedPreferencesTab.ANY);
}
@FXML
public void showDonationKeyPreferences() {
application.showPreferencesWindow(SelectedPreferencesTab.DONATION_KEY);
}
/* Getter/Setter */
public LicenseHolder getLicenseHolder() {
return licenseHolder;
}
public BooleanBinding updateAvailableProperty() {
return updateAvailable;
}
public boolean isUpdateAvailable() {
return updateAvailable.get();
}
public boolean isMinimizeToSysTray() {
return minimizeToSysTray;
}
}
@@ -0,0 +1,102 @@
package org.cryptomator.ui.mainwindow;
import javafx.fxml.FXML;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import javafx.stage.Stage;
import org.cryptomator.ui.common.FxController;
import javax.inject.Inject;
@MainWindow
public class ResizeController implements FxController {
private final Stage window;
public Region tlResizer;
public Region trResizer;
public Region blResizer;
public Region brResizer;
private double origX, origY, origW, origH;
@Inject
ResizeController(@MainWindow Stage window) {
this.window = window;
// TODO inject settings and save current position and size
}
@FXML
public void initialize() {
tlResizer.setOnMousePressed(this::startResize);
trResizer.setOnMousePressed(this::startResize);
blResizer.setOnMousePressed(this::startResize);
brResizer.setOnMousePressed(this::startResize);
tlResizer.setOnMouseDragged(this::resizeTopLeft);
trResizer.setOnMouseDragged(this::resizeTopRight);
blResizer.setOnMouseDragged(this::resizeBottomLeft);
brResizer.setOnMouseDragged(this::resizeBottomRight);
}
private void startResize(MouseEvent evt) {
origX = window.getX();
origY = window.getY();
origW = window.getWidth();
origH = window.getHeight();
}
private void resizeTopLeft(MouseEvent evt) {
resizeTop(evt);
resizeLeft(evt);
}
private void resizeTopRight(MouseEvent evt) {
resizeTop(evt);
resizeRight(evt);
}
private void resizeBottomLeft(MouseEvent evt) {
resizeBottom(evt);
resizeLeft(evt);
}
private void resizeBottomRight(MouseEvent evt) {
resizeBottom(evt);
resizeRight(evt);
}
private void resizeTop(MouseEvent evt) {
double newY = evt.getScreenY();
double dy = newY - origY;
double newH = origH - dy;
if (newH < window.getMaxHeight() && newH > window.getMinHeight()) {
window.setY(newY);
window.setHeight(newH);
}
}
private void resizeLeft(MouseEvent evt) {
double newX = evt.getScreenX();
double dx = newX - origX;
double newW = origW - dx;
if (newW < window.getMaxWidth() && newW > window.getMinWidth()) {
window.setX(newX);
window.setWidth(newW);
}
}
private void resizeBottom(MouseEvent evt) {
double newH = evt.getSceneY();
if (newH < window.getMaxHeight() && newH > window.getMinHeight()) {
window.setHeight(newH);
}
}
private void resizeRight(MouseEvent evt) {
double newW = evt.getSceneX();
if (newW < window.getMaxWidth() && newW > window.getMinWidth()) {
window.setWidth(newW);
}
}
}
@@ -33,11 +33,11 @@ public class VaultDetailController implements FxController {
private FontAwesome5Icon getGlyphForVaultState(VaultState state) { private FontAwesome5Icon getGlyphForVaultState(VaultState state) {
switch (state) { switch (state) {
case LOCKED: case LOCKED:
return FontAwesome5Icon.LOCK_ALT; return FontAwesome5Icon.LOCK;
case PROCESSING: case PROCESSING:
return FontAwesome5Icon.SPINNER; return FontAwesome5Icon.SPINNER;
case UNLOCKED: case UNLOCKED:
return FontAwesome5Icon.LOCK_OPEN_ALT; return FontAwesome5Icon.LOCK_OPEN;
default: default:
return FontAwesome5Icon.EXCLAMATION_TRIANGLE; return FontAwesome5Icon.EXCLAMATION_TRIANGLE;
} }
@@ -4,53 +4,32 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.Tasks; import org.cryptomator.ui.common.VaultService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject; import javax.inject.Inject;
import java.util.concurrent.ExecutorService;
@MainWindowScoped @MainWindowScoped
public class VaultDetailUnlockedController implements FxController { public class VaultDetailUnlockedController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(VaultDetailUnlockedController.class);
private final ReadOnlyObjectProperty<Vault> vault; private final ReadOnlyObjectProperty<Vault> vault;
private final ExecutorService executor; private final VaultService vaultService;
@Inject @Inject
public VaultDetailUnlockedController(ObjectProperty<Vault> vault, ExecutorService executor) { public VaultDetailUnlockedController(ObjectProperty<Vault> vault, VaultService vaultService) {
this.vault = vault; this.vault = vault;
this.executor = executor; this.vaultService = vaultService;
} }
@FXML @FXML
public void revealAccessLocation() { public void revealAccessLocation() {
try { vaultService.reveal(vault.get());
vault.get().reveal();
} catch (Volume.VolumeException e) {
LOG.error("Failed to reveal vault.", e);
}
} }
@FXML @FXML
public void lock() { public void lock() {
Vault v = vault.get(); vaultService.lock(vault.get(), false);
v.setState(VaultState.PROCESSING); // TODO count lock attempts, and allow forced lock
Tasks.create(() -> {
v.lock(false);
}).onSuccess(() -> {
LOG.trace("Regular unmount succeeded.");
v.setState(VaultState.LOCKED);
}).onError(Exception.class, e -> {
v.setState(VaultState.UNLOCKED);
LOG.error("Regular unmount failed.", e);
// TODO
}).runOnce(executor);
} }
/* Getter/Setter */ /* Getter/Setter */
@@ -25,11 +25,11 @@ public class VaultListCellController implements FxController {
private FontAwesome5Icon getGlyphForVaultState(VaultState state) { private FontAwesome5Icon getGlyphForVaultState(VaultState state) {
switch (state) { switch (state) {
case LOCKED: case LOCKED:
return FontAwesome5Icon.LOCK_ALT; return FontAwesome5Icon.LOCK;
case PROCESSING: case PROCESSING:
return FontAwesome5Icon.SPINNER; return FontAwesome5Icon.SPINNER;
case UNLOCKED: case UNLOCKED:
return FontAwesome5Icon.LOCK_OPEN_ALT; return FontAwesome5Icon.LOCK_OPEN;
default: default:
return FontAwesome5Icon.EXCLAMATION_TRIANGLE; return FontAwesome5Icon.EXCLAMATION_TRIANGLE;
} }
@@ -1,14 +1,18 @@
package org.cryptomator.ui.mainwindow; package org.cryptomator.ui.mainwindow;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding; import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.ListView; import javafx.scene.control.ListView;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent; import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.removevault.RemoveVaultComponent; import org.cryptomator.ui.removevault.RemoveVaultComponent;
@@ -23,7 +27,6 @@ public class VaultListController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(VaultListController.class); private static final Logger LOG = LoggerFactory.getLogger(VaultListController.class);
private final Stage window;
private final ObservableList<Vault> vaults; private final ObservableList<Vault> vaults;
private final ObjectProperty<Vault> selectedVault; private final ObjectProperty<Vault> selectedVault;
private final VaultListCellFactory cellFactory; private final VaultListCellFactory cellFactory;
@@ -34,8 +37,7 @@ public class VaultListController implements FxController {
public ListView<Vault> vaultList; public ListView<Vault> vaultList;
@Inject @Inject
VaultListController(@MainWindow Stage window, ObservableList<Vault> vaults, ObjectProperty<Vault> selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVault) { VaultListController(ObservableList<Vault> vaults, ObjectProperty<Vault> selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVault) {
this.window = window;
this.vaults = vaults; this.vaults = vaults;
this.selectedVault = selectedVault; this.selectedVault = selectedVault;
this.cellFactory = cellFactory; this.cellFactory = cellFactory;
@@ -43,6 +45,7 @@ public class VaultListController implements FxController {
this.removeVault = removeVault; this.removeVault = removeVault;
this.noVaultSelected = selectedVault.isNull(); this.noVaultSelected = selectedVault.isNull();
this.emptyVaultList = Bindings.isEmpty(vaults); this.emptyVaultList = Bindings.isEmpty(vaults);
selectedVault.addListener(this::selectedVaultDidChange);
} }
public void initialize() { public void initialize() {
@@ -59,6 +62,23 @@ public class VaultListController implements FxController {
}); });
} }
private void selectedVaultDidChange(@SuppressWarnings("unused") ObservableValue<? extends Vault> observableValue, @SuppressWarnings("unused") Vault oldValue, Vault newValue) {
VaultState reportedState = newValue.getState();
switch (reportedState) {
case LOCKED:
case NEEDS_MIGRATION:
case MISSING:
VaultState determinedState = VaultListManager.determineVaultState(newValue.getPath());
newValue.setState(determinedState);
break;
case ERROR:
case UNLOCKED:
case PROCESSING:
default:
// no-op
}
}
@FXML @FXML
public void didClickAddVault() { public void didClickAddVault() {
addVaultWizard.build().showAddVaultWizard(); addVaultWizard.build().showAddVaultWizard();
@@ -0,0 +1,57 @@
package org.cryptomator.ui.migration;
import dagger.Lazy;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.ResourceBundle;
@MigrationScoped
public class MigrationCapabilityErrorController implements FxController {
private final Stage window;
private final ResourceBundle localization;
private final Lazy<Scene> startScene;
private final StringBinding missingCapabilityDescription;
private final ReadOnlyObjectProperty<FileSystemCapabilityChecker.Capability> missingCapability;
@Inject
MigrationCapabilityErrorController(@MigrationWindow Stage window, @Named("capabilityErrorCause") ObjectProperty<FileSystemCapabilityChecker.Capability> missingCapability, ResourceBundle localization, @FxmlScene(FxmlFile.MIGRATION_START) Lazy<Scene> startScene) {
this.window = window;
this.missingCapability = missingCapability;
this.localization = localization;
this.startScene = startScene;
this.missingCapabilityDescription = Bindings.createStringBinding(this::getMissingCapabilityDescription, missingCapability);
}
@FXML
public void back() {
window.setScene(startScene.get());
}
/* Getters */
public StringBinding missingCapabilityDescriptionProperty() {
return missingCapabilityDescription;
}
public String getMissingCapabilityDescription() {
FileSystemCapabilityChecker.Capability c = missingCapability.get();
if (c != null) {
return localization.getString("migration.error.missingFileSystemCapabilities.reason." + c.name());
} else {
return null;
}
}
}
@@ -0,0 +1,29 @@
package org.cryptomator.ui.migration;
import dagger.Lazy;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Inject;
@MigrationScoped
public class MigrationGenericErrorController implements FxController {
private final Stage window;
private final Lazy<Scene> startScene;
@Inject
MigrationGenericErrorController(@MigrationWindow Stage window, @FxmlScene(FxmlFile.MIGRATION_START) Lazy<Scene> startScene) {
this.window = window;
this.startScene = startScene;
}
@FXML
public void back() {
window.setScene(startScene.get());
}
}
@@ -4,22 +4,26 @@ import dagger.Binds;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import dagger.multibindings.IntoMap; import dagger.multibindings.IntoMap;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.stage.Modality; import javafx.stage.Modality;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
import org.cryptomator.ui.common.DefaultSceneFactory; import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FXMLLoaderFactory; import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey; import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StackTraceController;
import org.cryptomator.ui.mainwindow.MainWindow; import org.cryptomator.ui.mainwindow.MainWindow;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Provider; import javax.inject.Provider;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle; import java.util.ResourceBundle;
@Module @Module
@@ -35,16 +39,30 @@ abstract class MigrationModule {
@Provides @Provides
@MigrationWindow @MigrationWindow
@MigrationScoped @MigrationScoped
static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcon") Optional<Image> windowIcon) { static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage(); Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("migration.title")); stage.setTitle(resourceBundle.getString("migration.title"));
stage.setResizable(false); stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL); stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner); stage.initOwner(owner);
windowIcon.ifPresent(stage.getIcons()::add); stage.getIcons().addAll(windowIcons);
return stage; return stage;
} }
@Provides
@Named("genericErrorCause")
@MigrationScoped
static ObjectProperty<Throwable> provideGenericErrorCause() {
return new SimpleObjectProperty<>();
}
@Provides
@Named("capabilityErrorCause")
@MigrationScoped
static ObjectProperty<FileSystemCapabilityChecker.Capability> provideCapabilityErrorCause() {
return new SimpleObjectProperty<>();
}
@Provides @Provides
@FxmlScene(FxmlFile.MIGRATION_START) @FxmlScene(FxmlFile.MIGRATION_START)
@MigrationScoped @MigrationScoped
@@ -66,6 +84,21 @@ abstract class MigrationModule {
return fxmlLoaders.createScene("/fxml/migration_success.fxml"); return fxmlLoaders.createScene("/fxml/migration_success.fxml");
} }
@Provides
@FxmlScene(FxmlFile.MIGRATION_CAPABILITY_ERROR)
@MigrationScoped
static Scene provideMigrationCapabilityErrorScene(@MigrationWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/migration_capability_error.fxml");
}
@Provides
@FxmlScene(FxmlFile.MIGRATION_GENERIC_ERROR)
@MigrationScoped
static Scene provideMigrationGenericErrorScene(@MigrationWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/migration_generic_error.fxml");
}
// ------------------ // ------------------
@Binds @Binds
@@ -83,4 +116,21 @@ abstract class MigrationModule {
@FxControllerKey(MigrationSuccessController.class) @FxControllerKey(MigrationSuccessController.class)
abstract FxController bindMigrationSuccessController(MigrationSuccessController controller); abstract FxController bindMigrationSuccessController(MigrationSuccessController controller);
@Binds
@IntoMap
@FxControllerKey(MigrationCapabilityErrorController.class)
abstract FxController bindMigrationCapabilityErrorController(MigrationCapabilityErrorController controller);
@Binds
@IntoMap
@FxControllerKey(MigrationGenericErrorController.class)
abstract FxController bindMigrationGenericErrorController(MigrationGenericErrorController controller);
@Provides
@IntoMap
@FxControllerKey(StackTraceController.class)
static FxController provideStackTraceController(@Named("genericErrorCause") ObjectProperty<Throwable> errorCause) {
return new StackTraceController(errorCause.get());
}
} }
@@ -1,32 +1,28 @@
package org.cryptomator.ui.migration; package org.cryptomator.ui.migration;
import dagger.Lazy; import dagger.Lazy;
import javafx.animation.KeyFrame; import javafx.application.Platform;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding; import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty; import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.WritableValue;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay; import javafx.scene.control.ContentDisplay;
import javafx.stage.Stage; import javafx.stage.Stage;
import javafx.util.Duration;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState; import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
import org.cryptomator.cryptofs.migration.Migrators; import org.cryptomator.cryptofs.migration.Migrators;
import org.cryptomator.cryptofs.migration.api.MigrationProgressListener; import org.cryptomator.cryptofs.migration.api.MigrationProgressListener;
import org.cryptomator.cryptofs.migration.api.NoApplicableMigratorException;
import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.keychain.KeychainAccess; import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.keychain.KeychainAccessException; import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.FxmlScene;
@@ -36,44 +32,54 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named;
import java.util.Arrays; import java.util.Arrays;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@MigrationScoped @MigrationScoped
public class MigrationRunController implements FxController { public class MigrationRunController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(MigrationRunController.class); private static final Logger LOG = LoggerFactory.getLogger(MigrationRunController.class);
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes
private static final Duration MIGRATION_PROGRESS_UPDATE_INTERVAL = Duration.millis(25); private static final long MIGRATION_PROGRESS_UPDATE_MILLIS = 50;
private final Stage window; private final Stage window;
private final Vault vault; private final Vault vault;
private final ExecutorService executor; private final ExecutorService executor;
private final ScheduledExecutorService scheduler;
private final Optional<KeychainAccess> keychainAccess; private final Optional<KeychainAccess> keychainAccess;
private final ObjectProperty<FileSystemCapabilityChecker.Capability> missingCapability;
private final ObjectProperty<Throwable> errorCause;
private final Lazy<Scene> startScene; private final Lazy<Scene> startScene;
private final Lazy<Scene> successScene; private final Lazy<Scene> successScene;
private final ObjectBinding<ContentDisplay> migrateButtonContentDisplay; private final ObjectBinding<ContentDisplay> migrateButtonContentDisplay;
private final Lazy<Scene> capabilityErrorScene;
private final Lazy<Scene> genericErrorScene;
private final BooleanProperty migrationButtonDisabled; private final BooleanProperty migrationButtonDisabled;
private final DoubleProperty migrationProgress; private final DoubleProperty migrationProgress;
private final ScheduledService<Double> migrationProgressObservationService;
private volatile double volatileMigrationProgress = -1.0; private volatile double volatileMigrationProgress = -1.0;
public NiceSecurePasswordField passwordField; public NiceSecurePasswordField passwordField;
@Inject @Inject
public MigrationRunController(@MigrationWindow Stage window, @MigrationWindow Vault vault, ExecutorService executor, Optional<KeychainAccess> keychainAccess, @FxmlScene(FxmlFile.MIGRATION_START) Lazy<Scene> startScene, @FxmlScene(FxmlFile.MIGRATION_SUCCESS) Lazy<Scene> successScene) { public MigrationRunController(@MigrationWindow Stage window, @MigrationWindow Vault vault, ExecutorService executor, ScheduledExecutorService scheduler, Optional<KeychainAccess> keychainAccess, @Named("capabilityErrorCause") ObjectProperty<FileSystemCapabilityChecker.Capability> missingCapability, @Named("genericErrorCause") ObjectProperty<Throwable> errorCause, @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_GENERIC_ERROR) Lazy<Scene> genericErrorScene) {
this.window = window; this.window = window;
this.vault = vault; this.vault = vault;
this.executor = executor; this.executor = executor;
this.scheduler = scheduler;
this.keychainAccess = keychainAccess; this.keychainAccess = keychainAccess;
this.missingCapability = missingCapability;
this.errorCause = errorCause;
this.startScene = startScene; this.startScene = startScene;
this.successScene = successScene; this.successScene = successScene;
this.migrateButtonContentDisplay = Bindings.createObjectBinding(this::getMigrateButtonContentDisplay, vault.stateProperty()); this.migrateButtonContentDisplay = Bindings.createObjectBinding(this::getMigrateButtonContentDisplay, vault.stateProperty());
this.capabilityErrorScene = capabilityErrorScene;
this.genericErrorScene = genericErrorScene;
this.migrationButtonDisabled = new SimpleBooleanProperty(); this.migrationButtonDisabled = new SimpleBooleanProperty();
this.migrationProgress = new SimpleDoubleProperty(volatileMigrationProgress); this.migrationProgress = new SimpleDoubleProperty(volatileMigrationProgress);
this.migrationProgressObservationService = new MigrationProgressObservationService();
migrationProgressObservationService.setExecutor(executor);
migrationProgressObservationService.setPeriod(MIGRATION_PROGRESS_UPDATE_INTERVAL);
} }
public void initialize() { public void initialize() {
@@ -93,36 +99,42 @@ public class MigrationRunController implements FxController {
LOG.info("Migrating vault {}", vault.getPath()); LOG.info("Migrating vault {}", vault.getPath());
CharSequence password = passwordField.getCharacters(); CharSequence password = passwordField.getCharacters();
vault.setState(VaultState.PROCESSING); vault.setState(VaultState.PROCESSING);
migrationProgressObservationService.start(); ScheduledFuture<?> progressSyncTask = scheduler.scheduleAtFixedRate(() -> {
Platform.runLater(() -> {
migrationProgress.set(volatileMigrationProgress);
});
}, 0, MIGRATION_PROGRESS_UPDATE_MILLIS, TimeUnit.MILLISECONDS);
Tasks.create(() -> { Tasks.create(() -> {
Migrators migrators = Migrators.get(); Migrators migrators = Migrators.get();
migrators.migrate(vault.getPath(), MASTERKEY_FILENAME, password, this::migrationProgressChanged); migrators.migrate(vault.getPath(), MASTERKEY_FILENAME, password, this::migrationProgressChanged);
return migrators.needsMigration(vault.getPath(), MASTERKEY_FILENAME); return migrators.needsMigration(vault.getPath(), MASTERKEY_FILENAME);
}).onSuccess(needsAnotherMigration -> { }).onSuccess(needsAnotherMigration -> {
LOG.info("Migration of '{}' succeeded.", vault.getDisplayableName());
if (needsAnotherMigration) { if (needsAnotherMigration) {
LOG.info("Migration of '{}' succeeded, but another migration is required.", vault.getDisplayableName());
vault.setState(VaultState.NEEDS_MIGRATION); vault.setState(VaultState.NEEDS_MIGRATION);
} else { } else {
LOG.info("Migration of '{}' succeeded.", vault.getDisplayableName());
vault.setState(VaultState.LOCKED); vault.setState(VaultState.LOCKED);
passwordField.swipe(); passwordField.swipe();
window.setScene(successScene.get()); window.setScene(successScene.get());
} }
}).onError(InvalidPassphraseException.class, e -> { }).onError(InvalidPassphraseException.class, e -> {
shakeWindow(); Animations.createShakeWindowAnimation(window).play();
passwordField.selectAll(); passwordField.selectAll();
passwordField.requestFocus(); passwordField.requestFocus();
vault.setState(VaultState.NEEDS_MIGRATION); vault.setState(VaultState.NEEDS_MIGRATION);
}).onError(NoApplicableMigratorException.class, e -> { }).onError(FileSystemCapabilityChecker.MissingCapabilityException.class, e -> {
LOG.error("Can not migrate vault.", e); LOG.error("Underlying file system not supported.", e);
vault.setState(VaultState.ERROR); vault.setState(VaultState.ERROR);
// TODO show specific error screen missingCapability.set(e.getMissingCapability());
window.setScene(capabilityErrorScene.get());
}).onError(Exception.class, e -> { // including RuntimeExceptions }).onError(Exception.class, e -> { // including RuntimeExceptions
LOG.error("Migration failed for technical reasons.", e); LOG.error("Migration failed for technical reasons.", e);
vault.setState(VaultState.ERROR); vault.setState(VaultState.NEEDS_MIGRATION);
// TODO show generic error screen errorCause.set(e);
window.setScene(genericErrorScene.get());
}).andFinally(() -> { }).andFinally(() -> {
migrationProgressObservationService.cancel(); progressSyncTask.cancel(true);
migrationProgressObservationService.reset();
}).runOnce(executor); }).runOnce(executor);
} }
@@ -161,54 +173,6 @@ public class MigrationRunController implements FxController {
} }
} }
// Sets migrationProgress to volatileMigrationProgress at its configured interval
private class MigrationProgressObservationService extends ScheduledService<Double> {
@Override
protected Task<Double> createTask() {
return new Task<>() {
@Override
protected Double call() {
return volatileMigrationProgress;
}
};
}
@Override
protected void succeeded() {
assert getValue() != null;
migrationProgress.set(getValue());
super.succeeded();
}
}
/* Animations */
private void shakeWindow() {
WritableValue<Double> writableWindowX = new WritableValue<>() {
@Override
public Double getValue() {
return window.getX();
}
@Override
public void setValue(Double value) {
window.setX(value);
}
};
Timeline timeline = new Timeline( //
new KeyFrame(Duration.ZERO, new KeyValue(writableWindowX, window.getX())), //
new KeyFrame(new Duration(100), new KeyValue(writableWindowX, window.getX() - 22.0)), //
new KeyFrame(new Duration(200), new KeyValue(writableWindowX, window.getX() + 18.0)), //
new KeyFrame(new Duration(300), new KeyValue(writableWindowX, window.getX() - 14.0)), //
new KeyFrame(new Duration(400), new KeyValue(writableWindowX, window.getX() + 10.0)), //
new KeyFrame(new Duration(500), new KeyValue(writableWindowX, window.getX() - 6.0)), //
new KeyFrame(new Duration(600), new KeyValue(writableWindowX, window.getX() + 2.0)), //
new KeyFrame(new Duration(700), new KeyValue(writableWindowX, window.getX())) //
);
timeline.play();
}
/* Getter/Setter */ /* Getter/Setter */
public Vault getVault() { public Vault getVault() {
@@ -0,0 +1,43 @@
package org.cryptomator.ui.preferences;
import org.cryptomator.jni.MacFunctions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
class AutoStartMacStrategy implements AutoStartStrategy {
private static final Logger LOG = LoggerFactory.getLogger(AutoStartMacStrategy.class);
private final MacFunctions macFunctions;
public AutoStartMacStrategy(MacFunctions macFunctions) {
this.macFunctions = macFunctions;
}
@Override
public CompletionStage<Boolean> isAutoStartEnabled() {
boolean enabled = macFunctions.launchServices().isLoginItemEnabled();
return CompletableFuture.completedFuture(enabled);
}
@Override
public void enableAutoStart() throws TogglingAutoStartFailedException {
if (macFunctions.launchServices().enableLoginItem()) {
LOG.debug("Added login item.");
} else {
throw new TogglingAutoStartFailedException("Failed to add login item.");
}
}
@Override
public void disableAutoStart() throws TogglingAutoStartFailedException {
if (macFunctions.launchServices().disableLoginItem()) {
LOG.debug("Removed login item.");
} else {
throw new TogglingAutoStartFailedException("Failed to remove login item.");
}
}
}
@@ -0,0 +1,26 @@
package org.cryptomator.ui.preferences;
import dagger.Module;
import dagger.Provides;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.jni.MacFunctions;
import java.util.Optional;
@Module
abstract class AutoStartModule {
@Provides
@PreferencesScoped
public static Optional<AutoStartStrategy> provideAutoStartStrategy(Optional<MacFunctions> macFunctions) {
if (SystemUtils.IS_OS_MAC_OSX && macFunctions.isPresent()) {
return Optional.of(new AutoStartMacStrategy(macFunctions.get()));
} else if (SystemUtils.IS_OS_WINDOWS) {
Optional<String> exeName = ProcessHandle.current().info().command();
return exeName.map(AutoStartWinStrategy::new);
} else {
return Optional.empty();
}
}
}
@@ -0,0 +1,24 @@
package org.cryptomator.ui.preferences;
import java.util.concurrent.CompletionStage;
public interface AutoStartStrategy {
CompletionStage<Boolean> isAutoStartEnabled();
void enableAutoStart() throws TogglingAutoStartFailedException;
void disableAutoStart() throws TogglingAutoStartFailedException;
class TogglingAutoStartFailedException extends Exception {
public TogglingAutoStartFailedException(String message) {
super(message);
}
public TogglingAutoStartFailedException(String message, Throwable cause) {
super(message, cause);
}
}
}
@@ -0,0 +1,91 @@
package org.cryptomator.ui.preferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
class AutoStartWinStrategy implements AutoStartStrategy {
private static final Logger LOG = LoggerFactory.getLogger(AutoStartWinStrategy.class);
private static final String HKCU_AUTOSTART_KEY = "\"HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\"";
private static final String AUTOSTART_VALUE = "Cryptomator";
private final String exePath;
public AutoStartWinStrategy(String exePath) {
this.exePath = exePath;
}
@Override
public CompletionStage<Boolean> isAutoStartEnabled() {
ProcessBuilder regQuery = new ProcessBuilder("reg", "query", HKCU_AUTOSTART_KEY, //
"/v", AUTOSTART_VALUE);
try {
Process proc = regQuery.start();
return proc.onExit().thenApply(p -> p.exitValue() == 0);
} catch (IOException e) {
LOG.warn("Failed to query {} from registry key {}", AUTOSTART_VALUE, HKCU_AUTOSTART_KEY);
return CompletableFuture.completedFuture(false);
}
}
@Override
public void enableAutoStart() throws TogglingAutoStartFailedException {
ProcessBuilder regAdd = new ProcessBuilder("reg", "add", HKCU_AUTOSTART_KEY, //
"/v", AUTOSTART_VALUE, //
"/t", "REG_SZ", //
"/d", "\"" + exePath + "\"", //
"/f");
String command = regAdd.command().stream().collect(Collectors.joining(" "));
try {
Process proc = regAdd.start();
boolean finishedInTime = waitForProcess(proc, 5, TimeUnit.SECONDS);
if (finishedInTime) {
LOG.debug("Added {} to registry key {}.", AUTOSTART_VALUE, HKCU_AUTOSTART_KEY);
} else {
throw new TogglingAutoStartFailedException("Adding registry value failed.");
}
} catch (IOException e) {
throw new TogglingAutoStartFailedException("Adding registry value failed. " + command, e);
}
}
@Override
public void disableAutoStart() throws TogglingAutoStartFailedException {
ProcessBuilder regRemove = new ProcessBuilder("reg", "delete", HKCU_AUTOSTART_KEY, //
"/v", AUTOSTART_VALUE, //
"/f");
String command = regRemove.command().stream().collect(Collectors.joining(" "));
try {
Process proc = regRemove.start();
boolean finishedInTime = waitForProcess(proc, 5, TimeUnit.SECONDS);
if (finishedInTime) {
LOG.debug("Removed {} from registry key {}.", AUTOSTART_VALUE, HKCU_AUTOSTART_KEY);
} else {
throw new TogglingAutoStartFailedException("Removing registry value failed.");
}
} catch (IOException e) {
throw new TogglingAutoStartFailedException("Removing registry value failed. " + command, e);
}
}
private static boolean waitForProcess(Process proc, int timeout, TimeUnit timeUnit) {
boolean finishedInTime = false;
try {
finishedInTime = proc.waitFor(timeout, timeUnit);
} catch (InterruptedException e) {
LOG.error("Timeout while reading registry", e);
Thread.currentThread().interrupt();
} finally {
if (!finishedInTime) {
proc.destroyForcibly();
}
}
return finishedInTime;
}
}
@@ -0,0 +1,45 @@
package org.cryptomator.ui.preferences;
import javafx.application.Application;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.control.TextArea;
import org.cryptomator.common.LicenseHolder;
import org.cryptomator.ui.common.FxController;
import javax.inject.Inject;
@PreferencesScoped
public class DonationKeyPreferencesController implements FxController {
private static final String DONATION_URI = "https://store.cryptomator.org/desktop";
private final Application application;
private final LicenseHolder licenseHolder;
public TextArea donationKeyField;
@Inject
DonationKeyPreferencesController(Application application, LicenseHolder licenseHolder) {
this.application = application;
this.licenseHolder = licenseHolder;
}
@FXML
public void initialize() {
donationKeyField.setText(licenseHolder.getLicenseKey().orElse(null));
donationKeyField.textProperty().addListener(this::registrationKeyChanged);
}
private void registrationKeyChanged(@SuppressWarnings("unused") ObservableValue<? extends String> observable, @SuppressWarnings("unused") String oldValue, String newValue) {
licenseHolder.validateAndStoreLicense(newValue);
}
@FXML
public void getDonationKey() {
application.getHostServices().showDocument(DONATION_URI);
}
public LicenseHolder getLicenseHolder() {
return licenseHolder;
}
}
@@ -1,6 +1,9 @@
package org.cryptomator.ui.preferences; package org.cryptomator.ui.preferences;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.geometry.NodeOrientation; import javafx.geometry.NodeOrientation;
import javafx.scene.control.CheckBox; import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox; import javafx.scene.control.ChoiceBox;
@@ -8,6 +11,7 @@ import javafx.scene.control.RadioButton;
import javafx.scene.control.Toggle; import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup; import javafx.scene.control.ToggleGroup;
import javafx.util.StringConverter; import javafx.util.StringConverter;
import org.cryptomator.common.LicenseHolder;
import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.UiTheme; import org.cryptomator.common.settings.UiTheme;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
@@ -15,25 +19,38 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
@PreferencesScoped @PreferencesScoped
public class GeneralPreferencesController implements FxController { public class GeneralPreferencesController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(GeneralPreferencesController.class); private static final Logger LOG = LoggerFactory.getLogger(GeneralPreferencesController.class);
private final Settings settings; private final Settings settings;
private final boolean trayMenuSupported;
private final Optional<AutoStartStrategy> autoStartStrategy;
private final LicenseHolder licenseHolder;
private final ExecutorService executor;
public ChoiceBox<UiTheme> themeChoiceBox; public ChoiceBox<UiTheme> themeChoiceBox;
public CheckBox startHiddenCheckbox; public CheckBox startHiddenCheckbox;
public CheckBox debugModeCheckbox; public CheckBox debugModeCheckbox;
public CheckBox autoStartCheckbox;
public ToggleGroup nodeOrientation; public ToggleGroup nodeOrientation;
public RadioButton nodeOrientationLtr; public RadioButton nodeOrientationLtr;
public RadioButton nodeOrientationRtl; public RadioButton nodeOrientationRtl;
@Inject @Inject
GeneralPreferencesController(Settings settings) { GeneralPreferencesController(Settings settings, @Named("trayMenuSupported") boolean trayMenuSupported, Optional<AutoStartStrategy> autoStartStrategy, LicenseHolder licenseHolder, ExecutorService executor) {
this.settings = settings; this.settings = settings;
this.trayMenuSupported = trayMenuSupported;
this.autoStartStrategy = autoStartStrategy;
this.licenseHolder = licenseHolder;
this.executor = executor;
} }
@FXML
public void initialize() { public void initialize() {
themeChoiceBox.getItems().addAll(UiTheme.values()); themeChoiceBox.getItems().addAll(UiTheme.values());
themeChoiceBox.valueProperty().bindBidirectional(settings.theme()); themeChoiceBox.valueProperty().bindBidirectional(settings.theme());
@@ -43,9 +60,23 @@ public class GeneralPreferencesController implements FxController {
debugModeCheckbox.selectedProperty().bindBidirectional(settings.debugMode()); debugModeCheckbox.selectedProperty().bindBidirectional(settings.debugMode());
nodeOrientation.selectedToggleProperty().addListener(this::toggleNodeOrientation); autoStartStrategy.ifPresent(autoStart -> {
autoStart.isAutoStartEnabled().thenAccept(enabled -> {
Platform.runLater(() -> autoStartCheckbox.setSelected(enabled));
});
});
nodeOrientationLtr.setSelected(settings.userInterfaceOrientation().get() == NodeOrientation.LEFT_TO_RIGHT); nodeOrientationLtr.setSelected(settings.userInterfaceOrientation().get() == NodeOrientation.LEFT_TO_RIGHT);
nodeOrientationRtl.setSelected(settings.userInterfaceOrientation().get() == NodeOrientation.RIGHT_TO_LEFT); nodeOrientationRtl.setSelected(settings.userInterfaceOrientation().get() == NodeOrientation.RIGHT_TO_LEFT);
nodeOrientation.selectedToggleProperty().addListener(this::toggleNodeOrientation);
}
public boolean isTrayMenuSupported() {
return this.trayMenuSupported;
}
public boolean isAutoStartSupported() {
return autoStartStrategy.isPresent();
} }
private void toggleNodeOrientation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) { private void toggleNodeOrientation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
@@ -58,6 +89,20 @@ public class GeneralPreferencesController implements FxController {
} }
} }
@FXML
public void toggleAutoStart() {
autoStartStrategy.ifPresent(autoStart -> {
boolean enableAutoStart = autoStartCheckbox.isSelected();
Task<Void> toggleTask = new ToggleAutoStartTask(autoStart, enableAutoStart);
toggleTask.setOnFailed(evt -> autoStartCheckbox.setSelected(!enableAutoStart)); // restore previous state
executor.execute(toggleTask);
});
}
public LicenseHolder getLicenseHolder() {
return licenseHolder;
}
/* Helper classes */ /* Helper classes */
private static class UiThemeConverter extends StringConverter<UiTheme> { private static class UiThemeConverter extends StringConverter<UiTheme> {
@@ -73,4 +118,25 @@ public class GeneralPreferencesController implements FxController {
} }
} }
private static class ToggleAutoStartTask extends Task<Void> {
private final AutoStartStrategy autoStart;
private final boolean enable;
public ToggleAutoStartTask(AutoStartStrategy autoStart, boolean enable) {
this.autoStart = autoStart;
this.enable = enable;
}
@Override
protected Void call() throws Exception {
if (enable) {
autoStart.enableAutoStart();
} else {
autoStart.disableAutoStart();
}
return null;
}
}
} }
@@ -7,9 +7,9 @@ package org.cryptomator.ui.preferences;
import dagger.Lazy; import dagger.Lazy;
import dagger.Subcomponent; import dagger.Subcomponent;
import javafx.beans.property.ObjectProperty;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.FxmlScene;
@@ -23,7 +23,10 @@ public interface PreferencesComponent {
@FxmlScene(FxmlFile.PREFERENCES) @FxmlScene(FxmlFile.PREFERENCES)
Lazy<Scene> scene(); Lazy<Scene> scene();
default Stage showPreferencesWindow() { ObjectProperty<SelectedPreferencesTab> selectedTabProperty();
default Stage showPreferencesWindow(SelectedPreferencesTab selectedTab) {
selectedTabProperty().set(selectedTab);
Stage stage = window(); Stage stage = window();
stage.setScene(scene().get()); stage.setScene(scene().get());
stage.show(); stage.show();
@@ -1,6 +1,9 @@
package org.cryptomator.ui.preferences; package org.cryptomator.ui.preferences;
import javafx.beans.Observable;
import javafx.beans.binding.BooleanBinding; import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Tab; import javafx.scene.control.Tab;
import javafx.scene.control.TabPane; import javafx.scene.control.TabPane;
@@ -15,26 +18,50 @@ import javax.inject.Inject;
public class PreferencesController implements FxController { public class PreferencesController implements FxController {
private final Stage window; private final Stage window;
private final ObjectProperty<SelectedPreferencesTab> selectedTabProperty;
private final BooleanBinding updateAvailable; private final BooleanBinding updateAvailable;
public TabPane tabPane; public TabPane tabPane;
public Tab generalTab; public Tab generalTab;
public Tab volumeTab;
public Tab updatesTab; public Tab updatesTab;
public Tab donationKeyTab;
@Inject @Inject
public PreferencesController(@PreferencesWindow Stage window, UpdateChecker updateChecker) { public PreferencesController(@PreferencesWindow Stage window, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, UpdateChecker updateChecker) {
this.window = window; this.window = window;
this.selectedTabProperty = selectedTabProperty;
this.updateAvailable = updateChecker.latestVersionProperty().isNotNull(); this.updateAvailable = updateChecker.latestVersionProperty().isNotNull();
} }
@FXML @FXML
public void initialize() { public void initialize() {
window.setOnShowing(this::windowWillAppear); window.setOnShowing(this::windowWillAppear);
selectedTabProperty.addListener(observable -> this.selectChosenTab());
} }
private void windowWillAppear(@SuppressWarnings("unused") WindowEvent windowEvent) { private void selectChosenTab() {
if (updateAvailable.get()) { Tab toBeSelected = getTabToSelect(selectedTabProperty.get());
tabPane.getSelectionModel().select(updatesTab); tabPane.getSelectionModel().select(toBeSelected);
}
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 ANY:
default:
return updateAvailable.get() ? updatesTab : generalTab;
} }
} }
private void windowWillAppear(@SuppressWarnings("unused") WindowEvent windowEvent) {
selectChosenTab();
}
} }
@@ -4,6 +4,8 @@ import dagger.Binds;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import dagger.multibindings.IntoMap; import dagger.multibindings.IntoMap;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.stage.Stage; import javafx.stage.Stage;
@@ -16,13 +18,19 @@ import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Provider; import javax.inject.Provider;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle; import java.util.ResourceBundle;
@Module @Module(includes = {AutoStartModule.class})
abstract class PreferencesModule { abstract class PreferencesModule {
@Provides
@PreferencesScoped
static ObjectProperty<SelectedPreferencesTab> provideSelectedTabProperty() {
return new SimpleObjectProperty<>(SelectedPreferencesTab.ANY);
}
@Provides @Provides
@PreferencesWindow @PreferencesWindow
@PreferencesScoped @PreferencesScoped
@@ -33,11 +41,11 @@ abstract class PreferencesModule {
@Provides @Provides
@PreferencesWindow @PreferencesWindow
@PreferencesScoped @PreferencesScoped
static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcon") Optional<Image> windowIcon) { static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage(); Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("preferences.title")); stage.setTitle(resourceBundle.getString("preferences.title"));
stage.setResizable(false); stage.setResizable(false);
windowIcon.ifPresent(stage.getIcons()::add); stage.getIcons().addAll(windowIcons);
return stage; return stage;
} }
@@ -70,4 +78,9 @@ abstract class PreferencesModule {
@FxControllerKey(VolumePreferencesController.class) @FxControllerKey(VolumePreferencesController.class)
abstract FxController bindVolumePreferencesController(VolumePreferencesController controller); abstract FxController bindVolumePreferencesController(VolumePreferencesController controller);
@Binds
@IntoMap
@FxControllerKey(DonationKeyPreferencesController.class)
abstract FxController bindDonationKeyPreferencesController(DonationKeyPreferencesController controller);
} }
@@ -0,0 +1,28 @@
package org.cryptomator.ui.preferences;
public enum SelectedPreferencesTab {
/**
* Let the controller decide which tab to show.
*/
ANY,
/**
* Show general tab
*/
GENERAL,
/**
* Show volume tab
*/
VOLUME,
/**
* Show updates tab
*/
UPDATES,
/**
* Show donation key tab
*/
DONATION_KEY,
}
@@ -1,26 +1,22 @@
package org.cryptomator.ui.quit; package org.cryptomator.ui.quit;
import javafx.application.Platform;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay; import javafx.scene.control.ContentDisplay;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.VaultService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.inject.Inject; import javax.inject.Inject;
import java.awt.desktop.QuitResponse; import java.awt.desktop.QuitResponse;
import java.util.Iterator; import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
@QuitScoped @QuitScoped
public class QuitController implements FxController { public class QuitController implements FxController {
@@ -30,15 +26,17 @@ public class QuitController implements FxController {
private final Stage window; private final Stage window;
private final QuitResponse response; private final QuitResponse response;
private final ObservableList<Vault> unlockedVaults; private final ObservableList<Vault> unlockedVaults;
private final ExecutorService executor; private final ExecutorService executorService;
private final VaultService vaultService;
public Button lockAndQuitButton; public Button lockAndQuitButton;
@Inject @Inject
QuitController(@QuitWindow Stage window, QuitResponse response, ObservableList<Vault> vaults, ExecutorService executor) { QuitController(@QuitWindow Stage window, QuitResponse response, ObservableList<Vault> vaults, ExecutorService executorService, VaultService vaultService) {
this.window = window; this.window = window;
this.response = response; this.response = response;
this.unlockedVaults = vaults.filtered(Vault::isUnlocked); this.unlockedVaults = vaults.filtered(Vault::isUnlocked);
this.executor = executor; this.executorService = executorService;
this.vaultService = vaultService;
} }
@FXML @FXML
@@ -52,84 +50,24 @@ public class QuitController implements FxController {
public void lockAndQuit() { public void lockAndQuit() {
lockAndQuitButton.setDisable(true); lockAndQuitButton.setDisable(true);
lockAndQuitButton.setContentDisplay(ContentDisplay.LEFT); lockAndQuitButton.setContentDisplay(ContentDisplay.LEFT);
Iterator<Vault> toBeLocked = List.copyOf(unlockedVaults).iterator(); Task<Collection<Vault>> lockAllTask = vaultService.createLockAllTask(unlockedVaults, false);
ScheduledService<Void> lockAllService = new LockAllVaultsService(executor, toBeLocked); lockAllTask.setOnSucceeded(evt -> {
lockAllService.setOnSucceeded(evt -> { LOG.info("Locked {}", lockAllTask.getValue().stream().map(Vault::getDisplayableName).collect(Collectors.joining(", ")));
if (!toBeLocked.hasNext()) { if (unlockedVaults.isEmpty()) {
window.close(); window.close();
response.performQuit(); response.performQuit();
} }
}); });
lockAllService.setOnFailed(evt -> { lockAllTask.setOnFailed(evt -> {
LOG.warn("Locking failed", lockAllTask.getException());
lockAndQuitButton.setDisable(false); lockAndQuitButton.setDisable(false);
lockAndQuitButton.setContentDisplay(ContentDisplay.TEXT_ONLY); lockAndQuitButton.setContentDisplay(ContentDisplay.TEXT_ONLY);
// TODO: show force lock or force quit scene (and DO NOT cancelQuit() here!) // TODO: show force lock or force quit scene (and DO NOT cancelQuit() here!)
// see https://github.com/cryptomator/cryptomator/blob/1.4.16/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java#L151-L163 // see https://github.com/cryptomator/cryptomator/blob/1.4.16/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java#L151-L163
response.cancelQuit(); response.cancelQuit();
}); });
lockAllService.start(); executorService.execute(lockAllTask);
} }
/**
* @param vault The vault to lock
* @return Task that tries to lock the given vault gracefully.
*/
private Task<Void> createGracefulLockTask(Vault vault) {
Task task = new Task<Void>() {
@Override
protected Void call() throws Volume.VolumeException {
vault.lock(false);
LOG.info("Locked {}", vault.getDisplayableName());
return null;
}
};
task.setOnScheduled(evt -> {
vault.setState(VaultState.PROCESSING);
});
task.setOnSucceeded(evt -> {
vault.setState(VaultState.LOCKED);
});
task.setOnFailed(evt -> {
LOG.warn("Failed to lock vault", vault);
});
return task;
}
/**
* @return Task that succeeds immediately
*/
private Task<Void> createNoopTask() {
return new Task<>() {
@Override
protected Void call() {
return null;
}
};
}
private class LockAllVaultsService extends ScheduledService<Void> {
private final Iterator<Vault> vaultsToLock;
public LockAllVaultsService(Executor executor, Iterator<Vault> vaultsToLock) {
this.vaultsToLock = vaultsToLock;
setExecutor(executor);
setRestartOnFailure(false);
}
@Override
protected Task<Void> createTask() {
assert Platform.isFxApplicationThread();
if (vaultsToLock.hasNext()) {
return createGracefulLockTask(vaultsToLock.next());
} else {
// This should be unreachable code, since vaultsToLock is only accessed on the FX App Thread.
// But if quitting the application takes longer for any reason, this service should shut down properly
reset();
return createNoopTask();
}
}
}
} }
@@ -20,8 +20,8 @@ import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Provider; import javax.inject.Provider;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle; import java.util.ResourceBundle;
@Module @Module
@@ -37,12 +37,12 @@ abstract class QuitModule {
@Provides @Provides
@QuitWindow @QuitWindow
@QuitScoped @QuitScoped
static Stage provideStage(@Named("windowIcon") Optional<Image> windowIcon) { static Stage provideStage(@Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage(); Stage stage = new Stage();
stage.setMinWidth(300); stage.setMinWidth(300);
stage.setMinHeight(100); stage.setMinHeight(100);
stage.initModality(Modality.APPLICATION_MODAL); stage.initModality(Modality.APPLICATION_MODAL);
windowIcon.ifPresent(stage.getIcons()::add); stage.getIcons().addAll(windowIcons);
return stage; return stage;
} }
@@ -0,0 +1,69 @@
package org.cryptomator.ui.recoverykey;
import com.google.common.base.Strings;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class AutoCompleter {
private final List<String> dictionary;
public AutoCompleter(Collection<String> dictionary) {
this.dictionary = unmodifiableSortedRandomAccessList(dictionary);
}
private static <T extends Comparable<T>> List<T> unmodifiableSortedRandomAccessList(Collection<T> items) {
List<T> result = new ArrayList<>(items);
Collections.sort(result);
return Collections.unmodifiableList(result);
}
public Optional<String> autocomplete(String prefix) {
if (Strings.isNullOrEmpty(prefix)) {
return Optional.empty();
}
int potentialMatchIdx = findIndexOfLexicographicallyPreceeding(0, dictionary.size(), prefix);
if (potentialMatchIdx < dictionary.size()) {
String potentialMatch = dictionary.get(potentialMatchIdx);
return potentialMatch.startsWith(prefix) ? Optional.of(potentialMatch) : Optional.empty();
} else {
return Optional.empty();
}
}
/**
* Find the index of the first word in {@link #dictionary} that starts with a given prefix.
*
* This method performs an "unsuccessful" binary search (it doesn't return when encountering an exact match).
* Instead it continues searching in the left half (which includes the exact match) until only one element is left.
*
* If the dictionary doesn't contain a word "left" of the given prefix, this method returns an invalid index, though.
*
* @param begin Index of first element (inclusive)
* @param end Index of last element (exclusive)
* @param prefix
* @return index between [0, dictLen], i.e. index can exceed the upper bounds of {@link #dictionary}.
*/
private int findIndexOfLexicographicallyPreceeding(int begin, int end, String prefix) {
if (begin >= end) {
return begin; // this is usually where a binary search ends "unsuccessful"
}
int mid = (begin + end) / 2;
String word = dictionary.get(mid);
if (prefix.compareTo(word) <= 0) { // prefix preceeds or matches word
// proceed in left half
assert mid < end;
return findIndexOfLexicographicallyPreceeding(0, mid, prefix);
} else {
// proceed in right half
assert mid >= begin;
return findIndexOfLexicographicallyPreceeding(mid + 1, end, prefix);
}
}
}
@@ -21,11 +21,21 @@ public interface RecoveryKeyComponent {
Stage window(); Stage window();
@FxmlScene(FxmlFile.RECOVERYKEY_CREATE) @FxmlScene(FxmlFile.RECOVERYKEY_CREATE)
Lazy<Scene> scene(); Lazy<Scene> creationScene();
@FxmlScene(FxmlFile.RECOVERYKEY_RECOVER)
Lazy<Scene> recoverScene();
default void showRecoveryKeyCreationWindow() { default void showRecoveryKeyCreationWindow() {
Stage stage = window(); Stage stage = window();
stage.setScene(scene().get()); stage.setScene(creationScene().get());
stage.sizeToScene();
stage.show();
}
default void showRecoveryKeyRecoverWindow() {
Stage stage = window();
stage.setScene(recoverScene().get());
stage.sizeToScene(); stage.sizeToScene();
stage.show(); stage.show();
} }
@@ -1,27 +1,21 @@
package org.cryptomator.ui.recoverykey; package org.cryptomator.ui.recoverykey;
import dagger.Lazy; import dagger.Lazy;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.StringProperty; import javafx.beans.property.StringProperty;
import javafx.beans.value.WritableValue; import javafx.concurrent.Task;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.stage.Stage; import javafx.stage.Stage;
import javafx.util.Duration;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.Tasks;
import org.cryptomator.ui.controls.NiceSecurePasswordField; import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@@ -40,7 +34,7 @@ public class RecoveryKeyCreationController implements FxController {
public NiceSecurePasswordField passwordField; public NiceSecurePasswordField passwordField;
@Inject @Inject
public RecoveryKeyCreationController(@RecoveryKeyWindow Stage window, @FxmlScene(FxmlFile.RECOVERYKEY_DISPLAY) Lazy<Scene> successScene, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey) { public RecoveryKeyCreationController(@RecoveryKeyWindow Stage window, @FxmlScene(FxmlFile.RECOVERYKEY_SUCCESS) Lazy<Scene> successScene, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey) {
this.window = window; this.window = window;
this.successScene = successScene; this.successScene = successScene;
this.vault = vault; this.vault = vault;
@@ -48,19 +42,26 @@ public class RecoveryKeyCreationController implements FxController {
this.recoveryKeyFactory = recoveryKeyFactory; this.recoveryKeyFactory = recoveryKeyFactory;
this.recoveryKeyProperty = recoveryKey; this.recoveryKeyProperty = recoveryKey;
} }
@FXML @FXML
public void createRecoveryKey() { public void createRecoveryKey() {
Tasks.create(() -> { Task<String> task = new RecoveryKeyCreationTask();
return recoveryKeyFactory.createRecoveryKey(vault.getPath(), passwordField.getCharacters()); task.setOnScheduled(event -> {
}).onSuccess(result -> { LOG.debug("Creating recovery key for {}.", vault.getDisplayablePath());
recoveryKeyProperty.set(result); });
task.setOnSucceeded(event -> {
String recoveryKey = task.getValue();
recoveryKeyProperty.set(recoveryKey);
window.setScene(successScene.get()); window.setScene(successScene.get());
}).onError(IOException.class, e -> { });
LOG.error("Creation of recovery key failed.", e); task.setOnFailed(event -> {
}).onError(InvalidPassphraseException.class, e -> { if (task.getException() instanceof InvalidPassphraseException) {
shakeWindow(); Animations.createShakeWindowAnimation(window).play();
}).runOnce(executor); } else {
LOG.error("Creation of recovery key failed.", task.getException());
}
});
executor.submit(task);
} }
@FXML @FXML
@@ -68,31 +69,13 @@ public class RecoveryKeyCreationController implements FxController {
window.close(); window.close();
} }
/* Animations */ private class RecoveryKeyCreationTask extends Task<String> {
private void shakeWindow() { @Override
WritableValue<Double> writableWindowX = new WritableValue<>() { protected String call() throws IOException {
@Override return recoveryKeyFactory.createRecoveryKey(vault.getPath(), passwordField.getCharacters());
public Double getValue() { }
return window.getX();
}
@Override
public void setValue(Double value) {
window.setX(value);
}
};
Timeline timeline = new Timeline( //
new KeyFrame(Duration.ZERO, new KeyValue(writableWindowX, window.getX())), //
new KeyFrame(new Duration(100), new KeyValue(writableWindowX, window.getX() - 22.0)), //
new KeyFrame(new Duration(200), new KeyValue(writableWindowX, window.getX() + 18.0)), //
new KeyFrame(new Duration(300), new KeyValue(writableWindowX, window.getX() - 14.0)), //
new KeyFrame(new Duration(400), new KeyValue(writableWindowX, window.getX() + 10.0)), //
new KeyFrame(new Duration(500), new KeyValue(writableWindowX, window.getX() - 6.0)), //
new KeyFrame(new Duration(600), new KeyValue(writableWindowX, window.getX() + 2.0)), //
new KeyFrame(new Duration(700), new KeyValue(writableWindowX, window.getX())) //
);
timeline.play();
} }
/* Getter/Setter */ /* Getter/Setter */
@@ -1,26 +1,81 @@
package org.cryptomator.ui.recoverykey; package org.cryptomator.ui.recoverykey;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.print.PageLayout;
import javafx.print.Printer;
import javafx.print.PrinterJob;
import javafx.scene.control.Button;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.text.Font;
import javafx.scene.text.FontSmoothingType;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject; import java.util.ResourceBundle;
@RecoveryKeyScoped
public class RecoveryKeyDisplayController implements FxController { public class RecoveryKeyDisplayController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(RecoveryKeyDisplayController.class);
private final Stage window; private final Stage window;
private final Vault vault; private final String vaultName;
private final StringProperty recoveryKeyProperty; private final String recoveryKey;
private final ResourceBundle localization;
public Button copyButton;
@Inject public RecoveryKeyDisplayController(Stage window, String vaultName, String recoveryKey, ResourceBundle localization) {
public RecoveryKeyDisplayController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, @RecoveryKeyWindow StringProperty recoveryKey) {
this.window = window; this.window = window;
this.vault = vault; this.vaultName = vaultName;
this.recoveryKeyProperty = recoveryKey; this.recoveryKey = recoveryKey;
this.localization = localization;
}
@FXML
public void printRecoveryKey() {
// TODO localize
PrinterJob job = PrinterJob.createPrinterJob();
if (job != null && job.showPrintDialog(window)) {
PageLayout pageLayout = job.getJobSettings().getPageLayout();
Text heading = new Text("Cryptomator Recovery Key\n" + vaultName + "\n");
heading.setFont(Font.font("serif", FontWeight.BOLD, 20));
heading.setFontSmoothingType(FontSmoothingType.LCD);
Text key = new Text(recoveryKey);
key.setFont(Font.font("serif", FontWeight.NORMAL, 16));
key.setFontSmoothingType(FontSmoothingType.GRAY);
TextFlow textFlow = new TextFlow();
textFlow.setPrefSize(pageLayout.getPrintableWidth(), pageLayout.getPrintableHeight());
textFlow.getChildren().addAll(heading, key);
textFlow.setLineSpacing(6);
if (job.printPage(textFlow)) {
LOG.info("Recovery key printed.");
job.endJob();
} else {
LOG.warn("Printing recovery key failed.");
}
} else {
LOG.info("Printing recovery key canceled by user.");
}
}
@FXML
public void copyRecoveryKey() {
ClipboardContent clipboardContent = new ClipboardContent();
clipboardContent.putString(recoveryKey);
Clipboard.getSystemClipboard().setContent(clipboardContent);
LOG.info("Recovery key copied to clipboard.");
copyButton.setText(localization.getString("generic.button.copied"));
} }
@FXML @FXML
@@ -30,15 +85,15 @@ public class RecoveryKeyDisplayController implements FxController {
/* Getter/Setter */ /* Getter/Setter */
public Vault getVault() { public boolean isPrinterSupported() {
return vault; return Printer.getDefaultPrinter() != null;
}
public ReadOnlyStringProperty recoveryKeyProperty() {
return recoveryKeyProperty;
} }
public String getRecoveryKey() { public String getRecoveryKey() {
return recoveryKeyProperty.get(); return recoveryKey;
}
public String getVaultName() {
return vaultName;
} }
} }
@@ -10,11 +10,13 @@ import javax.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
@Singleton @Singleton
public class RecoveryKeyFactory { public class RecoveryKeyFactory {
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes
private static final byte[] PEPPER = new byte[0];
private final WordEncoder wordEncoder; private final WordEncoder wordEncoder;
@@ -22,6 +24,10 @@ public class RecoveryKeyFactory {
public RecoveryKeyFactory(WordEncoder wordEncoder) { public RecoveryKeyFactory(WordEncoder wordEncoder) {
this.wordEncoder = wordEncoder; this.wordEncoder = wordEncoder;
} }
public Collection<String> getDictionary() {
return wordEncoder.getWords();
}
/** /**
* @param vaultPath Path to the storage location of a vault * @param vaultPath Path to the storage location of a vault
@@ -32,7 +38,7 @@ public class RecoveryKeyFactory {
* @apiNote This is a long-running operation and should be invoked in a background thread * @apiNote This is a long-running operation and should be invoked in a background thread
*/ */
public String createRecoveryKey(Path vaultPath, CharSequence password) throws IOException, InvalidPassphraseException { public String createRecoveryKey(Path vaultPath, CharSequence password) throws IOException, InvalidPassphraseException {
byte[] rawKey = CryptoFileSystemProvider.exportRawKey(vaultPath, MASTERKEY_FILENAME, new byte[0], password); byte[] rawKey = CryptoFileSystemProvider.exportRawKey(vaultPath, MASTERKEY_FILENAME, PEPPER, password);
try { try {
return createRecoveryKey(rawKey); return createRecoveryKey(rawKey);
} finally { } finally {
@@ -53,26 +59,53 @@ public class RecoveryKeyFactory {
} }
} }
/**
* Creates a completely new masterkey using a recovery key.
* @param vaultPath Path to the storage location of a vault
* @param recoveryKey A recovery key for this vault
* @param newPassword The new password used to encrypt the keys
* @throws IOException If the masterkey file could not be written
* @throws IllegalArgumentException If the recoveryKey is invalid
* @apiNote This is a long-running operation and should be invoked in a background thread
*/
public void resetPasswordWithRecoveryKey(Path vaultPath, String recoveryKey, CharSequence newPassword) throws IOException, IllegalArgumentException {
final byte[] rawKey = decodeRecoveryKey(recoveryKey);
try {
CryptoFileSystemProvider.restoreRawKey(vaultPath, MASTERKEY_FILENAME, rawKey, PEPPER, newPassword);
} finally {
Arrays.fill(rawKey, (byte) 0x00);
}
}
/** /**
* Checks whether a String is a syntactically correct recovery key with a valid checksum * Checks whether a String is a syntactically correct recovery key with a valid checksum
* @param recoveryKey A word sequence which might be a recovery key * @param recoveryKey A word sequence which might be a recovery key
* @return <code>true</code> if this seems to be a legitimate recovery key * @return <code>true</code> if this seems to be a legitimate recovery key
*/ */
public boolean validateRecoveryKey(String recoveryKey) { public boolean validateRecoveryKey(String recoveryKey) {
final byte[] paddedKey;
try { try {
paddedKey = wordEncoder.decode(recoveryKey); byte[] key = decodeRecoveryKey(recoveryKey);
Arrays.fill(key, (byte) 0x00);
return true;
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return false; return false;
} }
if (paddedKey.length != 66) { }
return false;
private byte[] decodeRecoveryKey(String recoveryKey) throws IllegalArgumentException {
byte[] paddedKey = new byte[0];
try {
paddedKey = wordEncoder.decode(recoveryKey);
Preconditions.checkArgument(paddedKey.length == 66, "Recovery key doesn't consist of 66 bytes.");
byte[] rawKey = Arrays.copyOf(paddedKey, 64);
byte[] expectedCrc16 = Arrays.copyOfRange(paddedKey, 64, 66);
byte[] actualCrc32 = Hashing.crc32().hashBytes(rawKey).asBytes();
byte[] actualCrc16 = Arrays.copyOf(actualCrc32, 2);
Preconditions.checkArgument(Arrays.equals(expectedCrc16, actualCrc16), "Recovery key has invalid CRC.");
return rawKey;
} finally {
Arrays.fill(paddedKey, (byte) 0x00);
} }
byte[] rawKey = Arrays.copyOf(paddedKey, 64);
byte[] expectedCrc16 = Arrays.copyOfRange(paddedKey, 64, 66);
byte[] actualCrc32 = Hashing.crc32().hashBytes(rawKey).asBytes();
byte[] actualCrc16 = Arrays.copyOf(actualCrc32, 2);
return Arrays.equals(expectedCrc16, actualCrc16);
} }
} }
@@ -4,23 +4,28 @@ import dagger.Binds;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import dagger.multibindings.IntoMap; import dagger.multibindings.IntoMap;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty; import javafx.beans.property.StringProperty;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.stage.Modality; import javafx.stage.Modality;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.DefaultSceneFactory; import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FXMLLoaderFactory; import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey; import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Provider; import javax.inject.Provider;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle; import java.util.ResourceBundle;
@Module @Module
@@ -36,13 +41,13 @@ abstract class RecoveryKeyModule {
@Provides @Provides
@RecoveryKeyWindow @RecoveryKeyWindow
@RecoveryKeyScoped @RecoveryKeyScoped
static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcon") Optional<Image> windowIcon, @Named("keyRecoveryOwner") Stage owner) { static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons, @Named("keyRecoveryOwner") Stage owner) {
Stage stage = new Stage(); Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("recoveryKey.title")); stage.setTitle(resourceBundle.getString("recoveryKey.title"));
stage.setResizable(false); stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL); stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner); stage.initOwner(owner);
windowIcon.ifPresent(stage.getIcons()::add); stage.getIcons().addAll(windowIcons);
return stage; return stage;
} }
@@ -52,7 +57,15 @@ abstract class RecoveryKeyModule {
static StringProperty provideRecoveryKeyProperty() { static StringProperty provideRecoveryKeyProperty() {
return new SimpleStringProperty(); return new SimpleStringProperty();
} }
@Provides
@RecoveryKeyScoped
@Named("newPassword")
static ObjectProperty<CharSequence> provideNewPasswordProperty() {
return new SimpleObjectProperty<>("");
}
// ------------------ // ------------------
@Provides @Provides
@@ -63,10 +76,24 @@ abstract class RecoveryKeyModule {
} }
@Provides @Provides
@FxmlScene(FxmlFile.RECOVERYKEY_DISPLAY) @FxmlScene(FxmlFile.RECOVERYKEY_SUCCESS)
@RecoveryKeyScoped @RecoveryKeyScoped
static Scene provideRecoveryKeyDisplayScene(@RecoveryKeyWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideRecoveryKeySuccessScene(@RecoveryKeyWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/recoverykey_display.fxml"); return fxmlLoaders.createScene("/fxml/recoverykey_success.fxml");
}
@Provides
@FxmlScene(FxmlFile.RECOVERYKEY_RECOVER)
@RecoveryKeyScoped
static Scene provideRecoveryKeyRecoverScene(@RecoveryKeyWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/recoverykey_recover.fxml");
}
@Provides
@FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD)
@RecoveryKeyScoped
static Scene provideRecoveryKeyResetPasswordScene(@RecoveryKeyWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/recoverykey_reset_password.fxml");
} }
// ------------------ // ------------------
@@ -76,9 +103,33 @@ abstract class RecoveryKeyModule {
@FxControllerKey(RecoveryKeyCreationController.class) @FxControllerKey(RecoveryKeyCreationController.class)
abstract FxController bindRecoveryKeyCreationController(RecoveryKeyCreationController controller); abstract FxController bindRecoveryKeyCreationController(RecoveryKeyCreationController controller);
@Binds @Provides
@IntoMap @IntoMap
@FxControllerKey(RecoveryKeyDisplayController.class) @FxControllerKey(RecoveryKeyDisplayController.class)
abstract FxController bindRecoveryKeyDisplayController(RecoveryKeyDisplayController controller); static FxController provideRecoveryKeyDisplayController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, @RecoveryKeyWindow StringProperty recoveryKey, ResourceBundle localization) {
return new RecoveryKeyDisplayController(window, vault.getDisplayableName(), recoveryKey.get(), localization);
}
@Binds
@IntoMap
@FxControllerKey(RecoveryKeyRecoverController.class)
abstract FxController provideRecoveryKeyRecoverController(RecoveryKeyRecoverController controller);
@Binds
@IntoMap
@FxControllerKey(RecoveryKeySuccessController.class)
abstract FxController bindRecoveryKeySuccessController(RecoveryKeySuccessController controller);
@Binds
@IntoMap
@FxControllerKey(RecoveryKeyResetPasswordController.class)
abstract FxController bindRecoveryKeyResetPasswordController(RecoveryKeyResetPasswordController controller);
@Provides
@IntoMap
@FxControllerKey(NewPasswordController.class)
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty<CharSequence> password) {
return new NewPasswordController(resourceBundle, strengthRater, password);
}
} }
@@ -0,0 +1,116 @@
package org.cryptomator.ui.recoverykey;
import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
import dagger.Lazy;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextFormatter;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Inject;
import java.util.Optional;
@RecoveryKeyScoped
public class RecoveryKeyRecoverController implements FxController {
private final static CharMatcher ALLOWED_CHARS = CharMatcher.inRange('a', 'z').or(CharMatcher.is(' '));
private final Stage window;
private final Vault vault;
private final StringProperty recoveryKey;
private final RecoveryKeyFactory recoveryKeyFactory;
private final BooleanBinding validRecoveryKey;
private final Lazy<Scene> resetPasswordScene;
private final AutoCompleter autoCompleter;
public TextArea textarea;
@Inject
public RecoveryKeyRecoverController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, @RecoveryKeyWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory, @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) Lazy<Scene> resetPasswordScene) {
this.window = window;
this.vault = vault;
this.recoveryKey = recoveryKey;
this.recoveryKeyFactory = recoveryKeyFactory;
this.validRecoveryKey = Bindings.createBooleanBinding(this::isValidRecoveryKey, recoveryKey);
this.resetPasswordScene = resetPasswordScene;
this.autoCompleter = new AutoCompleter(recoveryKeyFactory.getDictionary());
}
@FXML
public void initialize() {
recoveryKey.bind(textarea.textProperty());
}
private TextFormatter.Change filterTextChange(TextFormatter.Change change) {
if (Strings.isNullOrEmpty(change.getText())) {
// pass-through caret/selection changes that don't affect the text
return change;
}
if (!ALLOWED_CHARS.matchesAllOf(change.getText())) {
return null; // reject change
}
String text = change.getControlNewText();
int caretPos = change.getCaretPosition();
if (caretPos == text.length() || text.charAt(caretPos) == ' ') { // are we at the end of a word?
int beginOfWord = Math.max(text.substring(0, caretPos).lastIndexOf(' ') + 1, 0);
String currentWord = text.substring(beginOfWord, caretPos);
Optional<String> suggestion = autoCompleter.autocomplete(currentWord);
if (suggestion.isPresent()) {
String completion = suggestion.get().substring(currentWord.length());
change.setText(change.getText() + completion);
change.setAnchor(caretPos + completion.length());
}
}
return change;
}
@FXML
public void onKeyPressed(KeyEvent keyEvent) {
if (keyEvent.getCode() == KeyCode.TAB && textarea.getAnchor() > textarea.getCaretPosition()) {
// apply autocompletion:
int pos = textarea.getAnchor();
textarea.insertText(pos, " ");
textarea.positionCaret(pos + 1);
}
}
@FXML
public void close() {
window.close();
}
@FXML
public void recover() {
window.setScene(resetPasswordScene.get());
}
/* Getter/Setter */
public Vault getVault() {
return vault;
}
public BooleanBinding validRecoveryKeyProperty() {
return validRecoveryKey;
}
public boolean isValidRecoveryKey() {
return recoveryKeyFactory.validateRecoveryKey(recoveryKey.get());
}
public TextFormatter getRecoveryKeyTextFormatter() {
return new TextFormatter<>(this::filterTextChange);
}
}
@@ -0,0 +1,94 @@
package org.cryptomator.ui.recoverykey;
import dagger.Lazy;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
@RecoveryKeyScoped
public class RecoveryKeyResetPasswordController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(RecoveryKeyResetPasswordController.class);
private final Stage window;
private final Vault vault;
private final RecoveryKeyFactory recoveryKeyFactory;
private final ExecutorService executor;
private final StringProperty recoveryKey;
private final ObjectProperty<CharSequence> newPassword;
private final Lazy<Scene> recoverScene;
private final BooleanBinding invalidNewPassword;
@Inject
public RecoveryKeyResetPasswordController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, @Named("newPassword")ObjectProperty<CharSequence> newPassword, @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy<Scene> recoverScene) {
this.window = window;
this.vault = vault;
this.recoveryKeyFactory = recoveryKeyFactory;
this.executor = executor;
this.recoveryKey = recoveryKey;
this.newPassword = newPassword;
this.recoverScene = recoverScene;
this.invalidNewPassword = Bindings.createBooleanBinding(this::isInvalidNewPassword, newPassword);
}
@FXML
public void back() {
window.setScene(recoverScene.get());
}
@FXML
public void done() {
Task<Void> task = new ResetPasswordTask();
task.setOnScheduled(event -> {
LOG.debug("Using recovery key to reset password for {}.", vault.getDisplayablePath());
});
task.setOnSucceeded(event -> {
LOG.info("Used recovery key to reset password for {}.", vault.getDisplayablePath());
// TODO show success screen
window.close();
});
task.setOnFailed(event -> {
// TODO show generic error screen
LOG.error("Creation of recovery key failed.", task.getException());
});
executor.submit(task);
}
private class ResetPasswordTask extends Task<Void> {
@Override
protected Void call() throws IOException, IllegalArgumentException {
recoveryKeyFactory.resetPasswordWithRecoveryKey(vault.getPath(), recoveryKey.get(), newPassword.get());
return null;
}
}
/* Getter/Setter */
public BooleanBinding invalidNewPasswordProperty() {
return invalidNewPassword;
}
public boolean isInvalidNewPassword() {
return newPassword.get() == null || newPassword.get().length() == 0;
}
}
@@ -0,0 +1,24 @@
package org.cryptomator.ui.recoverykey;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import org.cryptomator.ui.common.FxController;
import javax.inject.Inject;
@RecoveryKeyScoped
public class RecoveryKeySuccessController implements FxController {
private final Stage window;
@Inject
public RecoveryKeySuccessController(@RecoveryKeyWindow Stage window) {
this.window = window;
}
@FXML
public void close() {
window.close();
}
}
@@ -2,6 +2,7 @@ package org.cryptomator.ui.recoverykey;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
@@ -11,6 +12,7 @@ import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.Reader; import java.io.Reader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -31,6 +33,10 @@ class WordEncoder {
this(DEFAULT_WORD_FILE); this(DEFAULT_WORD_FILE);
} }
public List<String> getWords() {
return words;
}
public WordEncoder(String wordFile) { public WordEncoder(String wordFile) {
try (InputStream in = getClass().getResourceAsStream(wordFile); // try (InputStream in = getClass().getResourceAsStream(wordFile); //
Reader reader = new InputStreamReader(in, StandardCharsets.US_ASCII.newDecoder()); // Reader reader = new InputStreamReader(in, StandardCharsets.US_ASCII.newDecoder()); //
@@ -78,7 +84,7 @@ class WordEncoder {
* @throws IllegalArgumentException If the encoded string doesn't consist of a multiple of two words or one of the words is unknown to this encoder. * @throws IllegalArgumentException If the encoded string doesn't consist of a multiple of two words or one of the words is unknown to this encoder.
*/ */
public byte[] decode(String encoded) { public byte[] decode(String encoded) {
List<String> splitted = Splitter.on(DELIMITER).omitEmptyStrings().splitToList(encoded); List<String> splitted = Splitter.on(DELIMITER).omitEmptyStrings().splitToList(Strings.nullToEmpty(encoded));
Preconditions.checkArgument(splitted.size() % 2 == 0, "%s needs to be a multiple of two words", encoded); Preconditions.checkArgument(splitted.size() % 2 == 0, "%s needs to be a multiple of two words", encoded);
byte[] result = new byte[splitted.size() / 2 * 3]; byte[] result = new byte[splitted.size() / 2 * 3];
for (int i = 0; i < splitted.size(); i+=2) { for (int i = 0; i < splitted.size(); i+=2) {
@@ -21,8 +21,8 @@ import org.cryptomator.ui.mainwindow.MainWindow;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Provider; import javax.inject.Provider;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle; import java.util.ResourceBundle;
@Module @Module
@@ -38,13 +38,13 @@ abstract class RemoveVaultModule {
@Provides @Provides
@RemoveVaultWindow @RemoveVaultWindow
@RemoveVaultScoped @RemoveVaultScoped
static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcon") Optional<Image> windowIcon) { static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage(); Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("removeVault.title")); stage.setTitle(resourceBundle.getString("removeVault.title"));
stage.setResizable(false); stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL); stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner); stage.initOwner(owner);
windowIcon.ifPresent(stage.getIcons()::add); stage.getIcons().addAll(windowIcons);
return stage; return stage;
} }
@@ -3,54 +3,37 @@ package org.cryptomator.ui.traymenu;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.Observable; import javafx.beans.Observable;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState; import org.cryptomator.ui.launcher.AppLifecycleListener;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.launcher.FxApplicationStarter; import org.cryptomator.ui.launcher.FxApplicationStarter;
import org.slf4j.Logger; import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import org.slf4j.LoggerFactory;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named;
import java.awt.Desktop;
import java.awt.Menu; import java.awt.Menu;
import java.awt.MenuItem; import java.awt.MenuItem;
import java.awt.PopupMenu; import java.awt.PopupMenu;
import java.awt.desktop.QuitResponse;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
import java.util.EnumSet;
import java.util.EventObject; import java.util.EventObject;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer; import java.util.function.Consumer;
@TrayMenuScoped @TrayMenuScoped
class TrayMenuController { class TrayMenuController {
private static final Logger LOG = LoggerFactory.getLogger(TrayMenuController.class);
public static final Set<VaultState> STATES_ALLOWING_TERMINATION = EnumSet.of(VaultState.LOCKED, VaultState.NEEDS_MIGRATION, VaultState.MISSING, VaultState.ERROR);
private final ResourceBundle resourceBundle; private final ResourceBundle resourceBundle;
private final AppLifecycleListener appLifecycle;
private final FxApplicationStarter fxApplicationStarter; private final FxApplicationStarter fxApplicationStarter;
private final CountDownLatch shutdownLatch;
private final Settings settings;
private final ObservableList<Vault> vaults; private final ObservableList<Vault> vaults;
private final PopupMenu menu; private final PopupMenu menu;
private final AtomicBoolean allowSuddenTermination;
@Inject @Inject
TrayMenuController(ResourceBundle resourceBundle, FxApplicationStarter fxApplicationStarter, @Named("shutdownLatch") CountDownLatch shutdownLatch, Settings settings, ObservableList<Vault> vaults) { TrayMenuController(ResourceBundle resourceBundle, AppLifecycleListener appLifecycle, FxApplicationStarter fxApplicationStarter, ObservableList<Vault> vaults) {
this.resourceBundle = resourceBundle; this.resourceBundle = resourceBundle;
this.appLifecycle = appLifecycle;
this.fxApplicationStarter = fxApplicationStarter; this.fxApplicationStarter = fxApplicationStarter;
this.shutdownLatch = shutdownLatch;
this.settings = settings;
this.vaults = vaults; this.vaults = vaults;
this.menu = new PopupMenu(); this.menu = new PopupMenu();
this.allowSuddenTermination = new AtomicBoolean(true);
} }
public PopupMenu getMenu() { public PopupMenu getMenu() {
@@ -59,39 +42,12 @@ class TrayMenuController {
public void initTrayMenu() { public void initTrayMenu() {
vaults.addListener(this::vaultListChanged); vaults.addListener(this::vaultListChanged);
rebuildMenu(); rebuildMenu();
// register preferences shortcut
if (Desktop.getDesktop().isSupported(Desktop.Action.APP_PREFERENCES)) {
Desktop.getDesktop().setPreferencesHandler(this::showPreferencesWindow);
}
// register quit handler
if (Desktop.getDesktop().isSupported(Desktop.Action.APP_QUIT_HANDLER)) {
Desktop.getDesktop().setQuitHandler(this::handleQuitRequest);
}
// allow sudden termination
if (Desktop.getDesktop().isSupported(Desktop.Action.APP_SUDDEN_TERMINATION)) {
Desktop.getDesktop().enableSuddenTermination();
}
} }
private void vaultListChanged(@SuppressWarnings("unused") Observable observable) { private void vaultListChanged(@SuppressWarnings("unused") Observable observable) {
assert Platform.isFxApplicationThread(); assert Platform.isFxApplicationThread();
rebuildMenu(); rebuildMenu();
boolean allVaultsAllowTermination = vaults.stream().map(Vault::getState).allMatch(STATES_ALLOWING_TERMINATION::contains);
boolean suddenTerminationChanged = allowSuddenTermination.compareAndSet(!allVaultsAllowTermination, allVaultsAllowTermination);
if (suddenTerminationChanged && Desktop.getDesktop().isSupported(Desktop.Action.APP_SUDDEN_TERMINATION)) {
if (allVaultsAllowTermination) {
Desktop.getDesktop().enableSuddenTermination();
LOG.debug("sudden termination enabled");
} else {
Desktop.getDesktop().disableSuddenTermination();
LOG.debug("sudden termination disabled");
}
}
} }
private void rebuildMenu() { private void rebuildMenu() {
@@ -112,6 +68,11 @@ class TrayMenuController {
} }
menu.addSeparator(); menu.addSeparator();
MenuItem lockAllItem = new MenuItem(resourceBundle.getString("traymenu.lockAllVaults"));
lockAllItem.addActionListener(this::lockAllVaults);
lockAllItem.setEnabled(!vaults.filtered(Vault::isUnlocked).isEmpty());
menu.add(lockAllItem);
MenuItem quitApplicationItem = new MenuItem(resourceBundle.getString("traymenu.quitApplication")); MenuItem quitApplicationItem = new MenuItem(resourceBundle.getString("traymenu.quitApplication"));
quitApplicationItem.addActionListener(this::quitApplication); quitApplicationItem.addActionListener(this::quitApplication);
menu.add(quitApplicationItem); menu.add(quitApplicationItem);
@@ -126,11 +87,11 @@ class TrayMenuController {
submenu.add(unlockItem); submenu.add(unlockItem);
} else if (vault.isUnlocked()) { } else if (vault.isUnlocked()) {
MenuItem lockItem = new MenuItem(resourceBundle.getString("traymenu.vault.lock")); MenuItem lockItem = new MenuItem(resourceBundle.getString("traymenu.vault.lock"));
lockItem.setEnabled(false); // TODO add action listener lockItem.addActionListener(createActionListenerForVault(vault, this::lockVault));
submenu.add(lockItem); submenu.add(lockItem);
MenuItem revealItem = new MenuItem(resourceBundle.getString("traymenu.vault.reveal")); MenuItem revealItem = new MenuItem(resourceBundle.getString("traymenu.vault.reveal"));
revealItem.setEnabled(false); // TODO add action listener revealItem.addActionListener(createActionListenerForVault(vault, this::revealVault));
submenu.add(revealItem); submenu.add(revealItem);
} }
@@ -145,33 +106,28 @@ class TrayMenuController {
fxApplicationStarter.get(true).thenAccept(app -> app.showUnlockWindow(vault)); fxApplicationStarter.get(true).thenAccept(app -> app.showUnlockWindow(vault));
} }
private void lockVault(Vault vault) {
fxApplicationStarter.get(true).thenAccept(app -> app.getVaultService().lock(vault, false));
}
private void lockAllVaults(ActionEvent actionEvent) {
fxApplicationStarter.get(true).thenAccept(app -> app.getVaultService().lockAll(vaults.filtered(Vault::isUnlocked), false));
}
private void revealVault(Vault vault) {
fxApplicationStarter.get(true).thenAccept(app -> app.getVaultService().reveal(vault));
}
void showMainWindow(@SuppressWarnings("unused") ActionEvent actionEvent) { void showMainWindow(@SuppressWarnings("unused") ActionEvent actionEvent) {
fxApplicationStarter.get(true).thenAccept(app -> app.showMainWindow()); fxApplicationStarter.get(true).thenAccept(app -> app.showMainWindow());
} }
private void showPreferencesWindow(@SuppressWarnings("unused") EventObject actionEvent) { private void showPreferencesWindow(@SuppressWarnings("unused") EventObject actionEvent) {
fxApplicationStarter.get(true).thenAccept(FxApplication::showPreferencesWindow); fxApplicationStarter.get(true).thenAccept(app -> app.showPreferencesWindow(SelectedPreferencesTab.ANY));
}
private void handleQuitRequest(EventObject e, QuitResponse response) {
if (allowSuddenTermination.get()) {
response.performQuit(); // really?
} else {
fxApplicationStarter.get(true).thenAccept(app -> app.showQuitWindow(response));
}
} }
private void quitApplication(EventObject actionEvent) { private void quitApplication(EventObject actionEvent) {
handleQuitRequest(actionEvent, new QuitResponse() { appLifecycle.quit();
@Override
public void performQuit() {
shutdownLatch.countDown();
}
@Override
public void cancelQuit() {
// no-op
}
});
} }
} }
@@ -1,37 +1,35 @@
package org.cryptomator.ui.unlock; package org.cryptomator.ui.unlock;
import dagger.Lazy; import dagger.Lazy;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding; import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.WritableValue; import javafx.concurrent.Task;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.CheckBox; import javafx.scene.control.CheckBox;
import javafx.scene.control.ContentDisplay; import javafx.scene.control.ContentDisplay;
import javafx.stage.Stage; import javafx.stage.Stage;
import javafx.util.Duration;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState; import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
import org.cryptomator.keychain.KeychainAccess; import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.keychain.KeychainAccessException; import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.Tasks; import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.controls.NiceSecurePasswordField; import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent; import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named;
import java.nio.file.DirectoryNotEmptyException; import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.NotDirectoryException; import java.nio.file.NotDirectoryException;
import java.util.Arrays; import java.util.Arrays;
@@ -48,20 +46,28 @@ public class UnlockController implements FxController {
private final ExecutorService executor; private final ExecutorService executor;
private final ObjectBinding<ContentDisplay> unlockButtonState; private final ObjectBinding<ContentDisplay> unlockButtonState;
private final Optional<KeychainAccess> keychainAccess; private final Optional<KeychainAccess> keychainAccess;
private final VaultService vaultService;
private final Lazy<Scene> successScene; private final Lazy<Scene> successScene;
private final Lazy<Scene> invalidMountPointScene;
private final Lazy<Scene> genericErrorScene;
private final ObjectProperty<Throwable> genericErrorCause;
private final ForgetPasswordComponent.Builder forgetPassword; private final ForgetPasswordComponent.Builder forgetPassword;
private final BooleanProperty unlockButtonDisabled; private final BooleanProperty unlockButtonDisabled;
public NiceSecurePasswordField passwordField; public NiceSecurePasswordField passwordField;
public CheckBox savePassword; public CheckBox savePassword;
@Inject @Inject
public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor, Optional<KeychainAccess> keychainAccess, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, ForgetPasswordComponent.Builder forgetPassword) { 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, @FxmlScene(FxmlFile.UNLOCK_GENERIC_ERROR) Lazy<Scene> genericErrorScene, @Named("genericErrorCause") ObjectProperty<Throwable> genericErrorCause, ForgetPasswordComponent.Builder forgetPassword) {
this.window = window; this.window = window;
this.vault = vault; this.vault = vault;
this.executor = executor; this.executor = executor;
this.unlockButtonState = Bindings.createObjectBinding(this::getUnlockButtonState, vault.stateProperty()); this.unlockButtonState = Bindings.createObjectBinding(this::getUnlockButtonState, vault.stateProperty());
this.keychainAccess = keychainAccess; this.keychainAccess = keychainAccess;
this.vaultService = vaultService;
this.successScene = successScene; this.successScene = successScene;
this.invalidMountPointScene = invalidMountPointScene;
this.genericErrorScene = genericErrorScene;
this.genericErrorCause = genericErrorCause;
this.forgetPassword = forgetPassword; this.forgetPassword = forgetPassword;
this.unlockButtonDisabled = new SimpleBooleanProperty(); this.unlockButtonDisabled = new SimpleBooleanProperty();
} }
@@ -85,37 +91,36 @@ public class UnlockController implements FxController {
public void unlock() { public void unlock() {
LOG.trace("UnlockController.unlock()"); LOG.trace("UnlockController.unlock()");
CharSequence password = passwordField.getCharacters(); CharSequence password = passwordField.getCharacters();
vault.setState(VaultState.PROCESSING);
Tasks.create(() -> { Task<Vault> task = vaultService.createUnlockTask(vault, password);
vault.unlock(password); task.setOnSucceeded(event -> {
if (keychainAccess.isPresent() && savePassword.isSelected()) { if (keychainAccess.isPresent() && savePassword.isSelected()) {
keychainAccess.get().storePassphrase(vault.getId(), password); try {
keychainAccess.get().storePassphrase(vault.getId(), password);
} catch (KeychainAccessException e) {
LOG.error("Failed to store passphrase in system keychain.", e);
}
} }
}).onSuccess(() -> {
vault.setState(VaultState.UNLOCKED);
passwordField.swipe(); passwordField.swipe();
LOG.info("Unlock of '{}' succeeded.", vault.getDisplayableName()); LOG.info("Unlock of '{}' succeeded.", vault.getDisplayableName());
window.setScene(successScene.get()); window.setScene(successScene.get());
}).onError(InvalidPassphraseException.class, e -> { });
shakeWindow(); task.setOnFailed(event -> {
passwordField.selectAll(); if (task.getException() instanceof InvalidPassphraseException) {
passwordField.requestFocus(); Animations.createShakeWindowAnimation(window).play();
}).onError(UnsupportedVaultFormatException.class, e -> { passwordField.selectAll();
// TODO passwordField.requestFocus();
}).onError(NotDirectoryException.class, e -> { } else if (task.getException() instanceof NotDirectoryException
LOG.error("Unlock failed. Mount point not a directory: {}", e.getMessage()); || task.getException() instanceof DirectoryNotEmptyException) {
// TODO LOG.error("Unlock failed. Mount point not an empty directory: {}", task.getException().getMessage());
}).onError(DirectoryNotEmptyException.class, e -> { window.setScene(invalidMountPointScene.get());
LOG.error("Unlock failed. Mount point not empty: {}", e.getMessage()); } else {
// TODO LOG.error("Unlock failed for technical reasons.", task.getException());
}).onError(Exception.class, e -> { // including RuntimeExceptions genericErrorCause.set(task.getException());
LOG.error("Unlock failed for technical reasons.", e); window.setScene(genericErrorScene.get());
// TODO
}).andFinally(() -> {
if (!vault.isUnlocked()) {
vault.setState(VaultState.LOCKED);
} }
}).runOnce(executor); });
executor.execute(task);
} }
/* Save Password */ /* Save Password */
@@ -160,33 +165,6 @@ public class UnlockController implements FxController {
} }
} }
/* Animations */
private void shakeWindow() {
WritableValue<Double> writableWindowX = new WritableValue<>() {
@Override
public Double getValue() {
return window.getX();
}
@Override
public void setValue(Double value) {
window.setX(value);
}
};
Timeline timeline = new Timeline( //
new KeyFrame(Duration.ZERO, new KeyValue(writableWindowX, window.getX())), //
new KeyFrame(new Duration(100), new KeyValue(writableWindowX, window.getX() - 22.0)), //
new KeyFrame(new Duration(200), new KeyValue(writableWindowX, window.getX() + 18.0)), //
new KeyFrame(new Duration(300), new KeyValue(writableWindowX, window.getX() - 14.0)), //
new KeyFrame(new Duration(400), new KeyValue(writableWindowX, window.getX() + 10.0)), //
new KeyFrame(new Duration(500), new KeyValue(writableWindowX, window.getX() - 6.0)), //
new KeyFrame(new Duration(600), new KeyValue(writableWindowX, window.getX() + 2.0)), //
new KeyFrame(new Duration(700), new KeyValue(writableWindowX, window.getX())) //
);
timeline.play();
}
/* Getter/Setter */ /* Getter/Setter */
public Vault getVault() { public Vault getVault() {
@@ -0,0 +1,29 @@
package org.cryptomator.ui.unlock;
import dagger.Lazy;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Inject;
@UnlockScoped
public class UnlockGenericErrorController implements FxController {
private final Stage window;
private final Lazy<Scene> unlockScene;
@Inject
UnlockGenericErrorController(@UnlockWindow Stage window, @FxmlScene(FxmlFile.UNLOCK) Lazy<Scene> unlockScene) {
this.window = window;
this.unlockScene = unlockScene;
}
@FXML
public void back() {
window.setScene(unlockScene.get());
}
}
@@ -0,0 +1,39 @@
package org.cryptomator.ui.unlock;
import dagger.Lazy;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Inject;
@UnlockScoped
public class UnlockInvalidMountPointController implements FxController {
private final Stage window;
private final Lazy<Scene> unlockScene;
private final Vault vault;
@Inject
UnlockInvalidMountPointController(@UnlockWindow Stage window, @FxmlScene(FxmlFile.UNLOCK) Lazy<Scene> unlockScene, @UnlockWindow Vault vault) {
this.window = window;
this.unlockScene = unlockScene;
this.vault = vault;
}
@FXML
public void back() {
window.setScene(unlockScene.get());
}
/* Getter/Setter */
public String getMountPoint() {
return vault.getVaultSettings().getIndividualMountPath().orElse("AUTO");
}
}
@@ -4,6 +4,8 @@ import dagger.Binds;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import dagger.multibindings.IntoMap; import dagger.multibindings.IntoMap;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.stage.Modality; import javafx.stage.Modality;
@@ -14,12 +16,13 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey; import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StackTraceController;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent; import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Provider; import javax.inject.Provider;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle; import java.util.ResourceBundle;
@Module(subcomponents = {ForgetPasswordComponent.class}) @Module(subcomponents = {ForgetPasswordComponent.class})
@@ -35,15 +38,22 @@ abstract class UnlockModule {
@Provides @Provides
@UnlockWindow @UnlockWindow
@UnlockScoped @UnlockScoped
static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcon") Optional<Image> windowIcon) { static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
Stage stage = new Stage(); Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("unlock.title")); stage.setTitle(resourceBundle.getString("unlock.title"));
stage.setResizable(false); stage.setResizable(false);
stage.initModality(Modality.APPLICATION_MODAL); stage.initModality(Modality.APPLICATION_MODAL);
windowIcon.ifPresent(stage.getIcons()::add); stage.getIcons().addAll(windowIcons);
return stage; return stage;
} }
@Provides
@Named("genericErrorCause")
@UnlockScoped
static ObjectProperty<Throwable> provideGenericErrorCause() {
return new SimpleObjectProperty<>();
}
@Provides @Provides
@FxmlScene(FxmlFile.UNLOCK) @FxmlScene(FxmlFile.UNLOCK)
@UnlockScoped @UnlockScoped
@@ -58,6 +68,21 @@ abstract class UnlockModule {
return fxmlLoaders.createScene("/fxml/unlock_success.fxml"); return fxmlLoaders.createScene("/fxml/unlock_success.fxml");
} }
@Provides
@FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT)
@UnlockScoped
static Scene provideInvalidMountPointScene(@UnlockWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/unlock_invalid_mount_point.fxml");
}
@Provides
@FxmlScene(FxmlFile.UNLOCK_GENERIC_ERROR)
@UnlockScoped
static Scene provideGenericErrorScene(@UnlockWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/unlock_generic_error.fxml");
}
// ------------------ // ------------------
@@ -71,5 +96,22 @@ abstract class UnlockModule {
@FxControllerKey(UnlockSuccessController.class) @FxControllerKey(UnlockSuccessController.class)
abstract FxController bindUnlockSuccessController(UnlockSuccessController controller); abstract FxController bindUnlockSuccessController(UnlockSuccessController controller);
@Binds
@IntoMap
@FxControllerKey(UnlockInvalidMountPointController.class)
abstract FxController bindUnlockInvalidMountPointController(UnlockInvalidMountPointController controller);
@Binds
@IntoMap
@FxControllerKey(UnlockGenericErrorController.class)
abstract FxController bindUnlockGenericErrorController(UnlockGenericErrorController controller);
@Provides
@IntoMap
@FxControllerKey(StackTraceController.class)
static FxController provideStackTraceController(@Named("genericErrorCause") ObjectProperty<Throwable> errorCause) {
return new StackTraceController(errorCause.get());
}
} }
@@ -5,13 +5,13 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Task;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.ContentDisplay; import javafx.scene.control.ContentDisplay;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.Tasks; import org.cryptomator.ui.common.VaultService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -26,14 +26,16 @@ public class UnlockSuccessController implements FxController {
private final Stage window; private final Stage window;
private final Vault vault; private final Vault vault;
private final ExecutorService executor; private final ExecutorService executor;
private final VaultService vaultService;
private final ObjectProperty<ContentDisplay> revealButtonState; private final ObjectProperty<ContentDisplay> revealButtonState;
private final BooleanProperty revealButtonDisabled; private final BooleanProperty revealButtonDisabled;
@Inject @Inject
public UnlockSuccessController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor) { public UnlockSuccessController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor, VaultService vaultService) {
this.window = window; this.window = window;
this.vault = vault; this.vault = vault;
this.executor = executor; this.executor = executor;
this.vaultService = vaultService;
this.revealButtonState = new SimpleObjectProperty<>(ContentDisplay.TEXT_ONLY); this.revealButtonState = new SimpleObjectProperty<>(ContentDisplay.TEXT_ONLY);
this.revealButtonDisabled = new SimpleBooleanProperty(); this.revealButtonDisabled = new SimpleBooleanProperty();
} }
@@ -49,17 +51,19 @@ public class UnlockSuccessController implements FxController {
LOG.trace("UnlockSuccessController.revealAndClose()"); LOG.trace("UnlockSuccessController.revealAndClose()");
revealButtonState.set(ContentDisplay.LEFT); revealButtonState.set(ContentDisplay.LEFT);
revealButtonDisabled.set(true); revealButtonDisabled.set(true);
Tasks.create(() -> {
vault.reveal(); Task<Vault> revealTask = vaultService.createRevealTask(vault);
}).onSuccess(() -> { revealTask.setOnSucceeded(evt -> {
window.close();
}).onError(InvalidPassphraseException.class, e -> {
// TODO
LOG.warn("Reveal failed.", e);
}).andFinally(() -> {
revealButtonState.set(ContentDisplay.TEXT_ONLY); revealButtonState.set(ContentDisplay.TEXT_ONLY);
revealButtonDisabled.set(false); revealButtonDisabled.set(false);
}).runOnce(executor); window.close();
});
revealTask.setOnFailed(evt -> {
LOG.warn("Reveal failed.", revealTask.getException());
revealButtonState.set(ContentDisplay.TEXT_ONLY);
revealButtonDisabled.set(false);
});
executor.execute(revealTask);
} }
/* Getter/Setter */ /* Getter/Setter */
@@ -1,11 +1,9 @@
package org.cryptomator.ui.vaultoptions; package org.cryptomator.ui.vaultoptions;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.stage.Stage; import javafx.scene.control.CheckBox;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.changepassword.ChangePasswordComponent;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
import javax.inject.Inject; import javax.inject.Inject;
@@ -13,26 +11,15 @@ import javax.inject.Inject;
public class GeneralVaultOptionsController implements FxController { public class GeneralVaultOptionsController implements FxController {
private final Vault vault; private final Vault vault;
private final Stage window; public CheckBox unlockOnStartupCheckbox;
private final ChangePasswordComponent.Builder changePasswordWindow;
private final RecoveryKeyComponent.Builder recoveryKeyWindow;
@Inject @Inject
GeneralVaultOptionsController(@VaultOptionsWindow Vault vault, @VaultOptionsWindow Stage window, ChangePasswordComponent.Builder changePasswordWindow, RecoveryKeyComponent.Builder recoveryKeyWindow) { GeneralVaultOptionsController(@VaultOptionsWindow Vault vault) {
this.vault = vault; this.vault = vault;
this.window = window;
this.changePasswordWindow = changePasswordWindow;
this.recoveryKeyWindow = recoveryKeyWindow;
} }
@FXML @FXML
public void changePassword() { public void initialize() {
changePasswordWindow.vault(vault).owner(window).build().showChangePasswordWindow(); unlockOnStartupCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().unlockAfterStartup());
} }
@FXML
public void showRecoveryKey() {
recoveryKeyWindow.vault(vault).owner(window).build().showRecoveryKeyCreationWindow();
}
} }
@@ -0,0 +1,42 @@
package org.cryptomator.ui.vaultoptions;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.changepassword.ChangePasswordComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
import javax.inject.Inject;
@VaultOptionsScoped
public class MasterkeyOptionsController implements FxController {
private final Vault vault;
private final Stage window;
private final ChangePasswordComponent.Builder changePasswordWindow;
private final RecoveryKeyComponent.Builder recoveryKeyWindow;
@Inject
MasterkeyOptionsController(@VaultOptionsWindow Vault vault, @VaultOptionsWindow Stage window, ChangePasswordComponent.Builder changePasswordWindow, RecoveryKeyComponent.Builder recoveryKeyWindow) {
this.vault = vault;
this.window = window;
this.changePasswordWindow = changePasswordWindow;
this.recoveryKeyWindow = recoveryKeyWindow;
}
@FXML
public void changePassword() {
changePasswordWindow.vault(vault).owner(window).build().showChangePasswordWindow();
}
@FXML
public void showRecoveryKey() {
recoveryKeyWindow.vault(vault).owner(window).build().showRecoveryKeyCreationWindow();
}
@FXML
public void showRecoverVaultDialogue() {
recoveryKeyWindow.vault(vault).owner(window).build().showRecoveryKeyRecoverWindow();
}
}
@@ -84,12 +84,7 @@ public class MountOptionsController implements FxController {
driveLetterSelection.getItems().addAll(windowsDriveLetters.getAllDriveLetters()); driveLetterSelection.getItems().addAll(windowsDriveLetters.getAllDriveLetters());
driveLetterSelection.setConverter(new WinDriveLetterLabelConverter(windowsDriveLetters)); driveLetterSelection.setConverter(new WinDriveLetterLabelConverter(windowsDriveLetters));
driveLetterSelection.setValue(vault.getVaultSettings().winDriveLetter().get()); driveLetterSelection.setValue(vault.getVaultSettings().winDriveLetter().get());
vault.getVaultSettings().usesIndividualMountPath().bind(mountPoint.selectedToggleProperty().isEqualTo(mountPointCustomDir));
vault.getVaultSettings().winDriveLetter().bind( //
Bindings.when(mountPoint.selectedToggleProperty().isEqualTo(mountPointWinDriveLetter)) //
.then(driveLetterSelection.getSelectionModel().selectedItemProperty()) //
.otherwise((String) null) //
);
if (vault.getVaultSettings().usesIndividualMountPath().get()) { if (vault.getVaultSettings().usesIndividualMountPath().get()) {
mountPoint.selectToggle(mountPointCustomDir); mountPoint.selectToggle(mountPointCustomDir);
} else if (!Strings.isNullOrEmpty(vault.getVaultSettings().winDriveLetter().get())) { } else if (!Strings.isNullOrEmpty(vault.getVaultSettings().winDriveLetter().get())) {
@@ -97,6 +92,13 @@ public class MountOptionsController implements FxController {
} else { } else {
mountPoint.selectToggle(mountPointAuto); mountPoint.selectToggle(mountPointAuto);
} }
vault.getVaultSettings().usesIndividualMountPath().bind(mountPoint.selectedToggleProperty().isEqualTo(mountPointCustomDir));
vault.getVaultSettings().winDriveLetter().bind( //
Bindings.when(mountPoint.selectedToggleProperty().isEqualTo(mountPointWinDriveLetter)) //
.then(driveLetterSelection.getSelectionModel().selectedItemProperty()) //
.otherwise((String) null) //
);
} }
@FXML @FXML
@@ -116,7 +118,7 @@ public class MountOptionsController implements FxController {
@FXML @FXML
private void chooseCustomMountPoint() { private void chooseCustomMountPoint() {
DirectoryChooser directoryChooser = new DirectoryChooser(); DirectoryChooser directoryChooser = new DirectoryChooser();
directoryChooser.setTitle(resourceBundle.getString("vaultOptions.mount.winDirChooser")); directoryChooser.setTitle(resourceBundle.getString("vaultOptions.mount.mountPoint.directoryPickerTitle"));
try { try {
directoryChooser.setInitialDirectory(Path.of(System.getProperty("user.home")).toFile()); directoryChooser.setInitialDirectory(Path.of(System.getProperty("user.home")).toFile());
} catch (Exception e) { } catch (Exception e) {

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