Compare commits

..
278 Commits
Author SHA1 Message Date
Armin Schrenk 8e5035f506 preparing 1.6.0 2021-05-06 14:27:34 +02:00
Sebastian StenzelandGitHub 0ce41e7ac3 New Crowdin updates (#1628)
New translations strings.properties

Japanese; Polish; Spanish; Dutch; Czech; Norwegian Bokmal; Korean; Greek; Catalan; Slovak; Russian; Punjabi; Italian; Arabic; Bosnian; Hungarian; German; Cyrillic); Latin); Romanian; French; Chinese Simplified; Croatian; Chinese Traditional; Portuguese; Latvian; Hindi; Indonesian; Turkish; Portuguese, Brazilian; Swedish; Norwegian Nynorsk; 

[ci skip]
2021-05-06 14:25:59 +02:00
Armin SchrenkandGitHub c1a249fd7f Merge pull request #1454 from cryptomator/feature/vault-format-8
Vault Format 8
2021-05-06 14:05:26 +02:00
Armin Schrenk c266c7583b bump dependencies (same as in cryptofs) 2021-05-06 13:59:37 +02:00
Armin Schrenk 00331d4857 check for vaultpath existence instead of catching (undocumented) exception 2021-05-06 13:51:30 +02:00
Armin Schrenk 8fd484e2bb Revert "Move window placements of "popup dialogs" to their corresponding controller"
This reverts commit bc83e23a34.
2021-05-06 13:44:05 +02:00
Armin Schrenk 165d740acd if vault directory is not present, declare it as missing 2021-05-06 12:51:39 +02:00
Armin Schrenk fd4010c6c9 cleanup 2021-05-06 12:43:15 +02:00
Armin Schrenk aa2900fa9e ensure to use cryptolib rc 2021-05-06 12:27:07 +02:00
Sebastian Stenzel 76d1875e01 adjusted to new CryptoFileSystemProvider.checkDirStructureForVault API 2021-05-06 09:56:50 +02:00
Armin Schrenk bc83e23a34 Move window placements of "popup dialogs" to their corresponding controller 2021-05-05 15:03:09 +02:00
Armin Schrenk 6a326bcbce remove password of migration window, when window is closed 2021-05-04 10:51:12 +02:00
Armin Schrenk 573ad03d68 bug fixin' 2021-05-03 17:33:28 +02:00
Armin Schrenk d710d406d0 Merge branch 'master' into develop 2021-04-23 10:30:12 +02:00
Armin Schrenk 554f245f5d Merge branch 'hotfix/1.5.16' 2021-04-23 10:27:48 +02:00
Sebastian Stenzel 34995088ba addressed some issues identified during code review 2021-04-23 10:24:31 +02:00
Sebastian Stenzel 69c63702f2 pass a single keyloader to cryptofs 2021-04-23 09:55:08 +02:00
Armin Schrenk db7e731554 bumping dokany-nio-adapter 2021-04-23 09:48:56 +02:00
Armin Schrenk e08b43bf17 preparing hotfix 1.5.16 2021-04-23 09:48:40 +02:00
Sebastian Stenzel 58b45bacd1 Merge branch 'develop' into feature/vault-format-8
[ci skip]
2021-04-21 13:27:54 +02:00
Sebastian Stenzel 4f942bc23f reset version
[ci skip]
2021-04-21 13:27:25 +02:00
Armin Schrenk 09a8618fa0 Merge tag '1.5.15' into develop
1.5.15
2021-04-21 12:26:46 +02:00
Armin Schrenk 52af530d1b Merge branch 'release/1.5.15' 2021-04-21 12:26:45 +02:00
Armin Schrenk 86a42234c6 preparing 1.5.15 2021-04-21 12:21:50 +02:00
Armin Schrenk 37c4e78b1d update third party licenses
[ci skip]
2021-04-21 12:05:37 +02:00
Sebastian StenzelandGitHub 9c2c234bee New Crowdin updates (#1604)
New translations strings.properties

Slovak; Czech; Italian; French; Catalan; Romanian; Polish; Chinese Simplified; Dutch; Chinese Traditional;
Norwegian Nynorsk; Croatian; Turkish; German; Spanish; Russian; Hungarian; Punjabi; Portuguese;
Norwegian Bokmal; Indonesian; Hindi; Bosnian; Cyrillic); Swedish; Greek; Korean; Portuguese, Brazilian; Latin); Arabic; Latvian; Japanese; 

[ci skip]
2021-04-21 12:01:09 +02:00
Sebastian Stenzel 59f91267ae fix save password 2021-04-21 11:20:44 +02:00
Sebastian Stenzel b3ff9423b4 Merge branch 'develop' into feature/vault-format-8
# Conflicts:
#	main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java
#	main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java
#	main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java
2021-04-21 11:20:31 +02:00
Armin Schrenk e3f3090341 bump javafx to version 16 2021-04-21 11:08:42 +02:00
Armin Schrenk 83be6c0864 enhanced release template for ci 2021-04-21 10:41:58 +02:00
Sebastian Stenzel c654951dc1 removed ambiguous filenameLengthLimit from settings, added maxCleartextFilenameLength, shortening is now a 100% independent setting stored in the vault rather than the device settings 2021-04-20 18:15:25 +02:00
Armin Schrenk 4e177c9ea7 Correcting misleading log message 2021-04-20 16:53:59 +02:00
Armin Schrenk dc3a951a1b add hotkeys to remove a vault 2021-04-20 15:07:14 +02:00
Armin Schrenk defa9c75eb add hotkeys for vault selection 2021-04-20 12:18:15 +02:00
Nour AghaandGitHub c1f498a114 Add MEGA & pCloud providers to vault location presets (#1622)
Fixes #1621
2021-04-20 08:09:17 +02:00
Armin Schrenk 8457c50ebc add muted style class to status text of newVaultLocationScreen 2021-04-19 16:11:28 +02:00
Armin SchrenkandGitHub 0144cbb99f Merge pull request #1618 from cryptomator/feature/#1508-observable-mounts
Closes #1508
2021-04-16 16:27:40 +02:00
Armin SchrenkandGitHub d6e4c7d177 Rework choose-location-screen in add vault wizard: (#1620)
* more checks for the chosen vault path
* every check has own error message
* perform checks when vault path changes
* if any radio button selected, enable vault path field (no-edit)
2021-04-16 16:18:01 +02:00
3376b16b7b Rename Donation Key to Supporter Certificate (#1613)
Renamed all occurences of donation key to supporter certificate and adjust ui.

Co-authored-by: Tobias Hagemann <tobias.hagemann@skymatic.de>
Co-authored-by: Sebastian Stenzel <overheadhunter@users.noreply.github.com>
2021-04-16 12:18:03 +02:00
Sebastian Stenzel 673fdcd095 stupid. 2021-04-15 13:45:20 +02:00
Armin Schrenk 0d00520ac1 Throw specifc exception on not completed lock of a vault 2021-04-15 12:54:19 +02:00
Armin Schrenk 22a0d3a9a5 bump fuse/dokany-nio versions 2021-04-15 11:11:05 +02:00
Sebastian Stenzel 24baa44e70 stronger encapsulation of vault state await/signal mechanism 2021-04-15 10:30:29 +02:00
Armin Schrenk 03886f88e8 Fix lock workflow for webdav:
* internally, wait for condition that onExit-Method is exceuted (with timeout)
* store and execute onExitAction also for webdav
2021-04-15 10:14:28 +02:00
Armin Schrenk 642816b631 rebuild tray menu when vaultname changes 2021-04-14 17:26:04 +02:00
Armin Schrenk cd5c55aad7 Refactor lock/unlock workflows:
* don't set vault state on successful lock workflow
* improved error handling
2021-04-13 11:22:42 +02:00
Armin Schrenk b066b4b045 opening a vault in read-only always assumes a filename length limit of 220
references #1605
2021-04-12 18:06:09 +02:00
JaniruTEC 41d2a2c77e Added .idea/uiDesigner.xml to .gitignore 2021-04-10 23:16:00 +02:00
Armin Schrenk 0840695e0a Refactor lock/unlock convinience methods in FxApplication:
* execute vault state transition here
* on failed transition show error window
* only start worfklow on successful transition
2021-04-08 17:28:49 +02:00
Armin Schrenk 8447f105b0 rename instance variables 2021-04-08 13:08:05 +02:00
Sebastian Stenzel c306d8df04 alternative thread-safe vault state without requiring explicit synchronization 2021-04-08 11:23:57 +02:00
Armin Schrenk beba6490c3 Add locking mechanism to change the vault state t 2021-04-06 13:26:58 +02:00
Armin Schrenk fb1078b35b bump to SNAPSHOT nio-adapter and refactor Volume.mount() method:
* returns void
* add onExitAction parameter
* adjust classes
2021-04-06 10:05:32 +02:00
Armin Schrenk be0912e6ca Merge branch 'develop' into feature/#1508-observable-mounts 2021-04-06 09:00:05 +02:00
Sebastian Stenzel 2810c044ea reduced visibility 2021-03-31 16:23:54 +02:00
Sebastian Stenzel e09bd160b7 Made masterkey loading strategies reusable 2021-03-31 16:12:31 +02:00
Sebastian Stenzel e6d1e4697c Merge develop into feature/vault-format-8 2021-03-31 11:39:16 +02:00
Armin Schrenk cf7cbae567 Set dev branch back to SNAPSHOT version
[ci skip]
2021-03-31 11:32:06 +02:00
Armin Schrenk 803f517c62 Merge tag '1.5.14' into develop
1.5.14
2021-03-31 11:11:34 +02:00
Armin Schrenk 0afa7b8e37 Merge branch 'release/1.5.14' 2021-03-31 11:11:27 +02:00
Armin Schrenk 1a0f70f8e8 updating third-party-licenses 2021-03-31 10:59:21 +02:00
Armin Schrenk 8737eb83f0 Bumping webdav-nio-adapter dependency 2021-03-31 10:58:23 +02:00
Armin Schrenk a96239a19f closes #1245 2021-03-31 10:52:30 +02:00
Armin Schrenk 2b7cfcd1dc preparing 1.5.14 2021-03-31 09:52:24 +02:00
Sebastian StenzelandGitHub ebccb61750 New Crowdin updates (#1560) 2021-03-31 09:46:45 +02:00
Armin Schrenk 285f2aec23 unify wording
[ci skip]
2021-03-30 16:10:24 +02:00
Armin SchrenkandGitHub 217e31fbd7 Merge pull request #1595 from cryptomator/feature/MCGA
Feature: Make Context menu Great Again
* adding additional entries
* fixes #1478
* only appears on a selected vault
2021-03-30 16:03:41 +02:00
Sebastian Stenzel f0ebf7a638 EasyBind rulez 2021-03-30 14:20:05 +02:00
Armin Schrenk c05c5e3f90 closes #961 2021-03-30 13:57:21 +02:00
Sebastian Stenzel 30f0c5e697 split VaultList and ContextMenu into two separate controllers 2021-03-30 13:43:37 +02:00
Armin Schrenk f989b8627c Update java-jwt 2021-03-30 13:10:54 +02:00
Armin Schrenk ff84230566 structure constructor 2021-03-30 11:53:30 +02:00
Armin Schrenk 3b4f6276b5 Improve selection model of list view:
* Clear selection if an empty space is clicked
* only open context menu if an item is selected
2021-03-30 11:25:22 +02:00
Armin Schrenk 2be7a050a4 refine wording
[ci skip]
2021-03-30 09:47:11 +02:00
Armin Schrenk 96a612127c add reveal entry to context menu. 2021-03-29 16:23:24 +02:00
Armin Schrenk 0bdfb7c9f9 Distinct between "unlock" and "unlock now" in context menu:
* inject keychainManager
* bind/unbind passphraseStored property to selected vault
* rename bindings to show possible action
* removed unused binding
* initialize properties properly
2021-03-29 12:59:48 +02:00
Armin Schrenk f556301249 fix suggestion 2021-03-29 12:47:46 +02:00
f77b237e59 simplify private function
Co-authored-by: Sebastian Stenzel <overheadhunter@users.noreply.github.com>
2021-03-29 12:44:57 +02:00
Armin Schrenk 61b7a39aad change level of a log message from warn to debug
[ci skip]
2021-03-29 10:53:48 +02:00
Tobias HagemannandGitHub 16bd84ee62 Update README.md
[ci skip]
2021-03-26 10:46:49 +01:00
Tobias HagemannandGitHub 3d0a97fcdf Update README.md
[ci skip]
2021-03-25 17:06:15 +01:00
Armin Schrenk 786d156b9f change wording in context menu entries 2021-03-23 15:08:23 +01:00
Armin Schrenk 629b6fb97d execute Service tasks on application thread 2021-03-23 12:53:17 +01:00
Armin Schrenk 17dc32bb79 lock vault on external unmount 2021-03-23 12:52:38 +01:00
Armin Schrenk c05e00d32a Change volume interface to observe mounts 2021-03-23 12:37:36 +01:00
Armin Schrenk 9dc8b2cb47 Use Bindings instead of properties 2021-03-23 10:50:04 +01:00
Armin Schrenk 0e32e96c7d made context menu great again:
* added entry to unlock / lock a vault
* added entry to show vault options
2021-03-22 17:10:45 +01:00
Armin Schrenk 97afadd7b9 fixes #1478 2021-03-22 12:58:45 +01:00
Armin Schrenk b199b65e38 closes #176 2021-03-22 12:51:07 +01:00
Tobias HagemannandGitHub 999abf3c13 Update README.md
[ci skip]
2021-03-22 12:24:54 +01:00
Sebastian Stenzel 2cdde54db6 updated mockito due to build errors caused by bytebuddy 2021-03-17 13:14:55 +01:00
Sebastian Stenzel 6941603cdd update to latest cryptolib/cryptofs beta 2021-03-17 13:03:55 +01:00
Sebastian Stenzel cdca4e047d Merge branch 'develop' into feature/vault-format-8 2021-03-17 12:55:47 +01:00
Sebastian StenzelandGitHub 31e938de6a Merge pull request #1587 from cryptomator/feature/jdk-16
Update to JDK 16
2021-03-17 12:29:29 +01:00
Sebastian Stenzel 4c10ab764a updated README 2021-03-17 09:33:43 +01:00
Sebastian Stenzel 4e075ab0ca using pattern-matching instanceof where applicable 2021-03-17 09:22:16 +01:00
Sebastian Stenzel f73ae9759f use new Stream.toList() where applicable 2021-03-17 09:16:44 +01:00
Sebastian Stenzel f6283b2f7e using text blocks where applicable 2021-03-17 09:12:54 +01:00
Sebastian Stenzel b23d2e4def allow use of reflection in codacy coverage reporter 2021-03-17 08:19:58 +01:00
Sebastian Stenzel 91a2943599 update to JDK 16 2021-03-17 08:16:26 +01:00
Sebastian Stenzel e32ce22d24 Merge branch 'develop' into feature/vault-format-8 2021-03-10 12:48:58 +01:00
Armin Schrenk e75c415b46 Load vault config on demand and don't cache it
* fixes issue with unlock after vault migration
2021-03-10 12:03:46 +01:00
Sebastian Stenzel 0ab28602d1 bumped cryptofs version 2021-03-05 17:33:58 +01:00
Sebastian Stenzel 62c8edff04 Choose key loading workflow depending on vaultconfig's key ID and allow KeyLoadingComponent to decide itself, what exceptions it can handle 2021-03-03 17:41:17 +01:00
Sebastian Stenzel d01c6268f8 remove unused field
[ci skip]
2021-03-02 16:16:24 +01:00
Sebastian Stenzel e083fd0bcc skip certain steps on forks 2021-03-02 11:55:20 +01:00
ff687f1bae fix: main/pom.xml to reduce vulnerabilities (#1569)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JAVA-ORGECLIPSEJETTY-1080611

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2021-03-02 08:43:10 +01:00
Sebastian Stenzel 557aaa2480 Removed unused imports
[ci skip]
2021-03-01 15:59:13 +01:00
Sebastian Stenzel 78e43d401d split package org.cryptoamtor.ui.unlock to allow for different kinds of unlock workflows 2021-03-01 15:58:00 +01:00
Sebastian Stenzel 7b08c5d287 Merge branch 'develop' into feature/vault-format-8
# Conflicts:
#	main/pom.xml
#	main/ui/src/main/resources/license/THIRD-PARTY.txt
[ci skip]
2021-02-26 10:10:50 +01:00
Sebastian Stenzel 84d2644f19 fix wrong versions that have been merged from main branch 2021-02-26 10:05:26 +01:00
Armin Schrenk f9a9d7b870 Merge tag '1.5.13' into develop
1.5.13
2021-02-24 16:21:07 +01:00
Armin Schrenk d75a8c4bba Merge branch 'release/1.5.13' 2021-02-24 16:21:06 +01:00
Armin Schrenk d326c7c990 preparing 1.5.13 2021-02-24 16:17:35 +01:00
Armin SchrenkandGitHub e8e63ca4fc Merge pull request #1517 from cryptomator/feature/translations
New Crowdin updates
2021-02-24 16:12:30 +01:00
Armin SchrenkandGitHub 6878f2e94b Merge pull request #1556 from cryptomator/feature/win-auto-theme
Feature/win auto theme
2021-02-24 14:31:17 +01:00
Armin Schrenk a17b416262 only remove system theme change listener, if one was previously registered. 2021-02-24 12:54:56 +01:00
Armin Schrenk fed572694f Merge branch 'develop' into feature/win-auto-theme
# Conflicts:
#	main/pom.xml
2021-02-24 12:25:04 +01:00
Armin Schrenk fe3d67d937 update build workflow 2021-02-24 12:21:13 +01:00
Armin Schrenk 3dcdeb1033 refactor pom to store targeted jdk build version in variable 2021-02-24 12:19:12 +01:00
Armin Schrenk 0898158c5a remove bintray as source repository in pom 2021-02-24 12:18:38 +01:00
Armin Schrenk 81091d4bdf bump integrations-* repos and implement new interface methods 2021-02-24 12:18:24 +01:00
Martin Beyer c9b1b1baa5 Fixes #1291 (Windows Automatic Theme) 2021-02-23 17:26:50 +01:00
Tobias Hagemann c81f3bd972 updated dependencies 2021-02-23 13:43:07 +01:00
Sebastian Stenzel 9f86b74320 New translations strings.properties (Greek)
[ci skip]
2021-02-23 09:32:09 +01:00
Sebastian Stenzel 2d8390a0f3 New translations strings.properties (Chinese Simplified)
[ci skip]
2021-02-22 11:47:50 +01:00
Sebastian Stenzel 8d3671175f New translations strings.properties (Swedish)
[ci skip]
2021-02-21 16:16:10 +01:00
Sebastian Stenzel 366011a222 New translations strings.properties (Swedish)
[ci skip]
2021-02-21 15:45:39 +01:00
Sebastian Stenzel c0c2e21055 New translations strings.properties (Korean)
[ci skip]
2021-02-20 02:29:34 +01:00
Sebastian Stenzel 63be96a151 New translations strings.properties (Korean)
[ci skip]
2021-02-20 01:58:05 +01:00
Sebastian Stenzel fc1df4bd99 New translations strings.properties (Catalan)
[ci skip]
2021-02-19 21:46:49 +01:00
Sebastian Stenzel 7657330931 New translations strings.properties (German)
[ci skip]
2021-02-19 13:17:33 +01:00
Sebastian Stenzel 4e44de3c3b New translations strings.properties (Portuguese, Brazilian)
[ci skip]
2021-02-19 01:07:24 +01:00
Sebastian Stenzel 8d902a72ca New translations strings.properties (Turkish)
[ci skip]
2021-02-18 19:39:40 +01:00
Sebastian Stenzel 7378031a72 New translations strings.properties (Spanish)
[ci skip]
2021-02-18 17:15:20 +01:00
Sebastian Stenzel f898c07c27 New translations strings.properties (German)
[ci skip]
2021-02-18 16:41:50 +01:00
Sebastian Stenzel c8f02d1045 New translations strings.properties (Czech)
[ci skip]
2021-02-18 14:56:07 +01:00
Sebastian Stenzel 4cc13b986b New translations strings.properties (Slovak)
[ci skip]
2021-02-18 14:22:12 +01:00
Sebastian Stenzel af7b6780f3 New translations strings.properties (Italian)
[ci skip]
2021-02-18 14:22:10 +01:00
Sebastian Stenzel 4a17d01004 New translations strings.properties (French)
[ci skip]
2021-02-18 13:42:57 +01:00
Sebastian Stenzel 1d68408e21 New translations strings.properties (Japanese)
[ci skip]
2021-02-18 13:42:49 +01:00
Sebastian Stenzel 821b1ee660 New translations strings.properties (Russian)
[ci skip]
2021-02-18 13:42:45 +01:00
Sebastian Stenzel c6616bd467 New translations strings.properties (Slovak)
[ci skip]
2021-02-18 13:42:44 +01:00
Sebastian Stenzel 4bb2589f16 New translations strings.properties (Polish)
[ci skip]
2021-02-18 13:42:37 +01:00
Sebastian Stenzel 896c3b3565 New translations strings.properties (Norwegian Bokmal)
[ci skip]
2021-02-18 13:09:32 +01:00
Sebastian Stenzel 6ee717a693 New translations strings.properties (French)
[ci skip]
2021-02-18 13:09:31 +01:00
Sebastian Stenzel 07ec858e1e New translations strings.properties (Spanish)
[ci skip]
2021-02-18 13:09:29 +01:00
Sebastian Stenzel a7c1e43ebc New translations strings.properties (Arabic)
[ci skip]
2021-02-18 13:09:28 +01:00
Sebastian Stenzel ac1cd3b073 New translations strings.properties (Catalan)
[ci skip]
2021-02-18 13:09:27 +01:00
Sebastian Stenzel 7474519faf New translations strings.properties (Czech)
[ci skip]
2021-02-18 13:09:26 +01:00
Sebastian Stenzel 340e474c33 New translations strings.properties (German)
[ci skip]
2021-02-18 13:09:24 +01:00
Sebastian Stenzel c90ab75427 New translations strings.properties (Greek)
[ci skip]
2021-02-18 13:09:23 +01:00
Sebastian Stenzel 848b3f77d3 New translations strings.properties (Hungarian)
[ci skip]
2021-02-18 13:09:21 +01:00
Sebastian Stenzel fb8cdb31ca New translations strings.properties (Italian)
[ci skip]
2021-02-18 13:09:20 +01:00
Sebastian Stenzel ba5f8053fb New translations strings.properties (Japanese)
[ci skip]
2021-02-18 13:09:19 +01:00
Sebastian Stenzel 38165becca New translations strings.properties (Korean)
[ci skip]
2021-02-18 13:09:17 +01:00
Sebastian Stenzel f731ac5c90 New translations strings.properties (Dutch)
[ci skip]
2021-02-18 13:09:16 +01:00
Sebastian Stenzel 0291702736 New translations strings.properties (Polish)
[ci skip]
2021-02-18 13:09:15 +01:00
Sebastian Stenzel ab63929861 New translations strings.properties (Portuguese)
[ci skip]
2021-02-18 13:09:12 +01:00
Sebastian Stenzel ca36fb8af3 New translations strings.properties (Slovak)
[ci skip]
2021-02-18 13:09:11 +01:00
Sebastian Stenzel 8cb29ff83b New translations strings.properties (Swedish)
[ci skip]
2021-02-18 13:09:10 +01:00
Sebastian Stenzel 304fa67dd7 New translations strings.properties (Turkish)
[ci skip]
2021-02-18 13:09:09 +01:00
Sebastian Stenzel a80ec32d2c New translations strings.properties (Chinese Simplified)
[ci skip]
2021-02-18 13:09:07 +01:00
Sebastian Stenzel dc4f21162e New translations strings.properties (Chinese Traditional)
[ci skip]
2021-02-18 13:09:06 +01:00
Sebastian Stenzel 3b8d7e0241 New translations strings.properties (Portuguese, Brazilian)
[ci skip]
2021-02-18 13:09:05 +01:00
Sebastian Stenzel 6891c68cf1 New translations strings.properties (Norwegian Nynorsk)
[ci skip]
2021-02-18 13:09:01 +01:00
Sebastian Stenzel 18b26fbc58 New translations strings.properties (Latvian)
[ci skip]
2021-02-18 13:09:00 +01:00
Sebastian Stenzel 1db8da6b6b New translations strings.properties (Bosnian)
[ci skip]
2021-02-18 13:08:57 +01:00
Sebastian Stenzel 14bce7e91d New translations strings.properties (Russian)
[ci skip]
2021-02-18 13:08:56 +01:00
Sebastian Stenzel 2674bc6f23 New translations strings.properties (Punjabi)
[ci skip]
2021-02-18 13:08:55 +01:00
Tobias HagemannandGitHub 74e1d6f991 Merge pull request #1545 from cryptomator/feature/save-password-clarification
"Save Password" Clarification
2021-02-18 13:07:04 +01:00
Tobias Hagemann 25dba028f8 added deep-linking to vault options tabs, changed "password saved" label to hyperlink 2021-02-18 11:32:14 +01:00
Tobias Hagemann bf51cc13d1 renamed "save password" to "remember password", switched placements of "vault options" hyperlink/button and "password saved" label 2021-02-18 09:15:19 +01:00
Sebastian Stenzel 24be36cccb New translations strings.properties (Spanish)
[ci skip]
2021-02-15 18:19:27 +01:00
Sebastian Stenzel 797e15c775 New translations strings.properties (Chinese Traditional)
[ci skip]
2021-02-15 17:08:04 +01:00
Sebastian Stenzel 2395dbcae9 New translations strings.properties (Italian)
[ci skip]
2021-02-15 16:09:11 +01:00
Sebastian Stenzel efacbbe2b6 New translations strings.properties (Bosnian)
[ci skip]
2021-02-15 15:40:45 +01:00
Sebastian Stenzel 2518086e46 New translations strings.properties (Polish)
[ci skip]
2021-02-15 15:40:44 +01:00
Sebastian Stenzel c733247611 New translations strings.properties (Japanese)
[ci skip]
2021-02-15 15:40:42 +01:00
Sebastian Stenzel 4f44e71e13 New translations strings.properties (Czech)
[ci skip]
2021-02-15 15:40:40 +01:00
Sebastian Stenzel 02aa46ece8 New translations strings.properties (Bosnian)
[ci skip]
2021-02-15 15:09:58 +01:00
Sebastian Stenzel 635f64c847 New translations strings.properties (German)
[ci skip]
2021-02-15 15:09:56 +01:00
Sebastian Stenzel 0a8516ae9a New translations strings.properties (French)
[ci skip]
2021-02-15 15:09:55 +01:00
Sebastian Stenzel c1edb30472 New translations strings.properties (Bosnian)
[ci skip]
2021-02-15 14:41:51 +01:00
Sebastian Stenzel 93c78b63ca New translations strings.properties (Russian)
[ci skip]
2021-02-15 14:41:50 +01:00
Sebastian Stenzel 3ccae93757 New translations strings.properties (Norwegian Bokmal)
[ci skip]
2021-02-15 14:09:30 +01:00
Sebastian Stenzel 450dec2baf New translations strings.properties (French)
[ci skip]
2021-02-15 14:09:29 +01:00
Sebastian Stenzel 36121527ca New translations strings.properties (Spanish)
[ci skip]
2021-02-15 14:09:27 +01:00
Sebastian Stenzel f30451f7e1 New translations strings.properties (Catalan)
[ci skip]
2021-02-15 14:09:25 +01:00
Sebastian Stenzel 74d7f63f64 New translations strings.properties (Czech)
[ci skip]
2021-02-15 14:09:24 +01:00
Sebastian Stenzel 350a4ab762 New translations strings.properties (German)
[ci skip]
2021-02-15 14:09:23 +01:00
Sebastian Stenzel 0e0e75b89c New translations strings.properties (Greek)
[ci skip]
2021-02-15 14:09:22 +01:00
Sebastian Stenzel e76440d928 New translations strings.properties (Hungarian)
[ci skip]
2021-02-15 14:09:20 +01:00
Sebastian Stenzel 0cbace67a5 New translations strings.properties (Italian)
[ci skip]
2021-02-15 14:09:19 +01:00
Sebastian Stenzel 84a3082589 New translations strings.properties (Japanese)
[ci skip]
2021-02-15 14:09:17 +01:00
Sebastian Stenzel cdfcca06d1 New translations strings.properties (Korean)
[ci skip]
2021-02-15 14:09:16 +01:00
Sebastian Stenzel 630bfa4cee New translations strings.properties (Dutch)
[ci skip]
2021-02-15 14:09:15 +01:00
Sebastian Stenzel 90cb7beb1f New translations strings.properties (Polish)
[ci skip]
2021-02-15 14:09:14 +01:00
Sebastian Stenzel d37fa33278 New translations strings.properties (Slovak)
[ci skip]
2021-02-15 14:09:11 +01:00
Sebastian Stenzel eed9c906cc New translations strings.properties (Swedish)
[ci skip]
2021-02-15 14:09:09 +01:00
Sebastian Stenzel 2b7ed5f4dd New translations strings.properties (Turkish)
[ci skip]
2021-02-15 14:09:08 +01:00
Sebastian Stenzel 6a7a309a2b New translations strings.properties (Chinese Simplified)
[ci skip]
2021-02-15 14:09:07 +01:00
Sebastian Stenzel 0382069995 New translations strings.properties (Chinese Traditional)
[ci skip]
2021-02-15 14:09:05 +01:00
Sebastian Stenzel 7c5f40d7d1 New translations strings.properties (Portuguese, Brazilian)
[ci skip]
2021-02-15 14:09:04 +01:00
Sebastian Stenzel 09e9530660 New translations strings.properties (Russian)
[ci skip]
2021-02-15 14:08:57 +01:00
Sebastian Stenzel 947d25b335 New translations strings.properties (Punjabi)
[ci skip]
2021-02-15 14:08:56 +01:00
Sebastian Stenzel 80cbc6699a fixed label
[ci skip]
2021-02-15 13:53:05 +01:00
Sebastian Stenzel da0933fa92 New translations strings.properties (Punjabi)
[ci skip]
2021-02-08 04:36:28 +01:00
Sebastian Stenzel 9ad217ca55 Merge branch 'develop' into feature/vault-format-8
[ci skip]
2021-02-04 12:38:01 +01:00
Sebastian Stenzel adc9f32fb3 Merge branch 'master' into develop 2021-02-04 12:31:18 +01:00
Sebastian Stenzel 65369bdbff Merge branch 'release/1.5.12' 2021-02-04 12:27:42 +01:00
Sebastian Stenzel d18e618ef1 preparing 1.5.12 2021-02-04 12:27:02 +01:00
Armin Schrenk 970216dd59 Merge tag '1.5.12' into develop
1.5.12
2021-02-04 10:49:52 +01:00
Armin Schrenk 6840a649c7 Merge branch 'release/1.5.12' 2021-02-04 10:49:08 +01:00
Sebastian Stenzel 1a0bc92bde Merge branch 'develop' into feature/vault-format-8 2021-02-01 12:08:23 +01:00
Sebastian Stenzel e4709ed6fe Refactored FxmlFile and FxmlLoaderFactory 2021-02-01 12:06:38 +01:00
Sebastian Stenzel 21038212c1 remove unused property
[ci skip]
2021-02-01 11:54:21 +01:00
Sebastian Stenzel b15471b4ff add new (optional) "choose masterkey file" step to unlock dialog 2021-01-29 17:44:45 +01:00
Armin Schrenk 19ebc7e562 preparing 1.5.12 2021-01-27 15:30:56 +01:00
Armin SchrenkandGitHub 0fa052d2d1 Merge pull request #1518 from cryptomator/fix/1515-suppressed-messages
Fixed suppressed "invalid mountpoint" messages
2021-01-27 15:22:47 +01:00
Armin Schrenk 402861b9c0 Update third party licenses 2021-01-27 12:37:50 +01:00
Armin Schrenk aca1666dea closes #1479 2021-01-27 12:36:22 +01:00
Sebastian Stenzel ff17b60f56 Refactored UnlockWorkflow using new vault format 8 APIs 2021-01-27 12:09:09 +01:00
Sebastian Stenzel 3284578445 Merge branch 'develop' into feature/vault-format-8
# Conflicts:
#	main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java
2021-01-25 21:42:13 +01:00
Sebastian Stenzel 70f6a4877c Make sure not to catch Errors 2021-01-25 21:38:46 +01:00
Sebastian Stenzel 85c5dc8dfb removed tmp workaround 2021-01-25 21:32:33 +01:00
Sebastian Stenzel 4b670a59a3 adjusted to new cryptolib/cryptofs API 2021-01-25 21:31:16 +01:00
JaniruTEC 23c113948f Replaced catch with wildcard catch and added additional logger call
See #1509 for further information and reasoning
2021-01-25 16:21:35 +01:00
JaniruTEC d86ea20c31 Fixed suppressed "invalid mountpoint" messages
Fixed #1515
2021-01-23 22:13:07 +01:00
Sebastian Stenzel 3f928cf958 Merge branch 'develop' into feature/vault-format-8
[ci skip]
2021-01-22 22:47:54 +01:00
Sebastian Stenzel 01929c2288 New translations strings.properties (Hungarian)
[ci skip]
2021-01-22 13:48:57 +01:00
Sebastian Stenzel 2634433b2c New translations strings.properties (Hungarian)
[ci skip]
2021-01-22 13:19:41 +01:00
Armin SchrenkandGitHub c1f44f76b9 Merge pull request #1512 from cryptomator/feature/refactor-reveal 2021-01-22 11:42:20 +01:00
Sebastian Stenzel b2f27c0a3d removed dead code
[ci skip]
2021-01-22 09:34:18 +01:00
Sebastian Stenzel b1d7cfc81b change scope of HostServiceRevealer to @FxApplicationScoped
because unlock always happens after application start anyway
2021-01-22 09:31:43 +01:00
Armin Schrenk 7cd5c66836 Refactor reveal() methods:
* new class HostServiceRevealer implemeting the Volume.Revealer-Interface
* this class is injected in vault service and always used for revealing
* removed Revealer parameter from several reveal methods
2021-01-21 17:25:23 +01:00
Armin Schrenk 95cef34234 remove unnecessary awtReavealer and trayModule 2021-01-21 16:09:16 +01:00
Armin Schrenk 8977440697 Rename RevealerFacade to Revealer 2021-01-21 15:29:04 +01:00
Armin Schrenk d5eb84a000 change reveal method in vault service:
* revealer is no member anymore
* reveal() takes as second argument revealer object
* several other classes hand over a revealer object
* added awt-revealer if application is not yet started
2021-01-21 15:26:35 +01:00
Sebastian Stenzel 81fb6b8794 get rid of RevealException 2021-01-21 15:06:37 +01:00
Armin Schrenk 92b390d5bb update third-party licenses 2021-01-21 12:44:32 +01:00
Armin Schrenk 8867532210 add javadoc 2021-01-21 12:44:11 +01:00
Armin Schrenk 77db435b4f Refactor reveal call stack to apply facade pattern. 2021-01-21 12:41:52 +01:00
Sebastian Stenzel cb2bc17283 New translations strings.properties (Hungarian)
[ci skip]
2021-01-21 11:06:22 +01:00
Tobias HagemannandGitHub aaa93239d0 Merge pull request #1516 from stacksnack/feature/translations
New translations strings.properties (Hungarian)
2021-01-21 11:03:27 +01:00
Sebastian Stenzel 93445e22d4 update library versions 2021-01-21 06:37:41 +01:00
stacksnackandGitHub 64897cfa21 New translations strings.properties (Hungarian)
Add hungarian translation
2021-01-20 21:37:41 +01:00
Armin Schrenk 7349a29831 Ensure that on windows systems always the gui chooses the mountpoint for webdav 2021-01-19 15:52:52 +01:00
Armin Schrenk 73554b4759 split WebDAV Volume mount method into servlet start and servlet mount 2021-01-19 15:52:20 +01:00
Armin Schrenk 0312f045aa add a logger to WebDAV-Volume 2021-01-19 15:28:50 +01:00
Armin Schrenk 1134c1b2ff closes #1471
hand the javafx hostservice showDocument() method from ui package to the underlying nio-adapter-libraries through
2021-01-19 15:27:46 +01:00
Sebastian Stenzel efebbc059a keep CTR+HMAC for now (until GCM is supported on all platforms) 2021-01-19 15:09:47 +01:00
Sebastian Stenzel 6e860d293a Merge branch 'develop' into feature/vault-format-8
[ci skip]
2021-01-19 15:05:15 +01:00
Armin Schrenk 8bb925fd93 fixes #1509 2021-01-18 18:41:21 +01:00
Sebastian Stenzel ca5d9eba36 New translations strings.properties (Norwegian Bokmal)
[ci skip]
2021-01-16 04:07:35 +01:00
Sebastian Stenzel 1a49e24d8a New translations strings.properties (Norwegian Bokmal)
[ci skip]
2021-01-16 03:37:26 +01:00
Sebastian Stenzel 24b0ed2502 New translations strings.properties (German)
[ci skip]
2021-01-14 18:18:01 +01:00
Sebastian Stenzel a226d5403a New translations strings.properties (Japanese)
[ci skip]
2021-01-14 14:02:17 +01:00
Sebastian Stenzel 0f3085cc73 fixes #1476 2021-01-14 09:56:57 +01:00
Sebastian Stenzel f0a040cf29 cleanup
[ci skip]
2021-01-14 09:56:36 +01:00
Sebastian StenzelandGitHub 0676748dde New Crowdin updates (#1466)
[ci skip]
2021-01-14 09:24:42 +01:00
Armin Schrenk 589b8384eb Only determine filenamelength limit if the the vault is mounted not read-only. 2021-01-11 14:15:37 +01:00
Julian RaufelderandGitHub 9e3947f337 Happy 2021 🎉 2021-01-11 10:08:35 +01:00
Tobias Hagemann 790cc4e772 simplified issue template
[ci skip]
2021-01-06 12:01:07 +01:00
Tobias Hagemann 72f4988632 fixes #1452 2021-01-05 14:14:14 +01:00
Sebastian StenzelandGitHub b3fa6bbf32 Merge pull request #1472 from cryptomator/feature/minimize
Add Minimize Button
2020-12-18 15:58:02 +01:00
Sebastian Stenzel c2dc487c79 make field volatile that might be modified concurrently 2020-12-18 13:37:23 +01:00
Sebastian Stenzel e74dd3be9e remove unused param 2020-12-18 13:36:16 +01:00
Sebastian Stenzel 1eeee61572 Add option to show minimize button despite tray icon being present
fixes #1179
2020-12-17 14:50:33 +01:00
Sebastian Stenzel 984b7a2d0e Removed @Named("trayMenuSupported") from FxApplication component 2020-12-17 14:46:52 +01:00
Sebastian Stenzel 45c714a123 Added checkbox in settings to start without a tray icon
references #1113, #1078, #1079, #1344
2020-12-16 16:53:49 +01:00
Sebastian Stenzel 96bb97d50a Added new setting showTrayIcon defaulting to new environment property cryptomator.showTrayIcon 2020-12-16 13:48:33 +01:00
Sebastian Stenzel 046acb9bbf mark fuse.experimental deprecated
[ci skip]
2020-12-16 13:33:50 +01:00
Armin SchrenkandGitHub ce4ec16c62 Update bug.md
Add info to also specify desktop environment on Linux systems in section system setup
2020-12-16 12:16:32 +01:00
Tobias HagemannandGitHub 96c8c9a9a9 Update bug.md 2020-12-16 09:07:39 +01:00
Tobias HagemannandGitHub 44db97327d Update feature.md 2020-12-16 09:06:11 +01:00
Sebastian Stenzel f441700c25 Merge branch 'master' into develop 2020-12-15 13:23:21 +01:00
Sebastian Stenzel c0a9a95e4f Adjusted to CryptoFS 2.0.0 2020-12-08 14:39:46 +01:00
171 changed files with 4251 additions and 1399 deletions
+4 -22
View File
@@ -5,28 +5,12 @@ labels: type:bug
--- ---
<!-- <!--
************************************** Please make sure to:
* * - Comply with our code of conduct: https://github.com/cryptomator/cryptomator/blob/develop/.github/CODE_OF_CONDUCT.md
* ⚠️⚠️⚠️ READ CAREFULLY ⚠️⚠️⚠️ * - Search for existing similar issues first: https://github.com/cryptomator/cryptomator/issues?q=
* *
**************************************
Do you want to ask a QUESTION? Are you looking for SUPPORT?
We're happy to help you via our support channels! Please read: https://github.com/cryptomator/cryptomator/blob/develop/SUPPORT.md
By filing an issue, you are expected to comply with our code of conduct: https://github.com/cryptomator/cryptomator/blob/develop/.github/CODE_OF_CONDUCT.md
Of course, we also expect you to search for existing similar issues first! ;) https://github.com/cryptomator/cryptomator/issues?q=
⚠️ IMPORTANT: If you don't stick to this template, the issue will get closed. ⚠️ IMPORTANT: If you don't stick to this template, the issue will get closed.
*****************************************************************************
* *
* To proof that you read this, please remove the X from the line below: *
* *
*****************************************************************************
--> -->
<!-- oooXooo -->
### Description ### Description
@@ -34,7 +18,7 @@ Of course, we also expect you to search for existing similar issues first! ;) ht
### System Setup ### System Setup
* Operating system and version: [Windows/macOS/Linux + Version] * Operating system and version: [Windows/macOS/Linux + Version ( + Desktop Environment, if Linux)]
* Cryptomator version: [Shown in the settings] * Cryptomator version: [Shown in the settings]
* Volume type: [Dokany/FUSE/WebDAV, shown in the settings] * Volume type: [Dokany/FUSE/WebDAV, shown in the settings]
@@ -61,7 +45,6 @@ Of course, we also expect you to search for existing similar issues first! ;) ht
[Any additional information, log files, screenshots, configuration, or data that might be necessary to reproduce the issue.] [Any additional information, log files, screenshots, configuration, or data that might be necessary to reproduce the issue.]
<!-- <!--
If you want to add the log file or screenshots, please add them as attachments. If your log file seems empty and doesn't show any errors, you may enable the debug mode first. Here is how to do that: https://community.cryptomator.org/t/how-do-i-enable-debug-mode/36 If you want to add the log file or screenshots, please add them as attachments. If your log file seems empty and doesn't show any errors, you may enable the debug mode first. Here is how to do that: https://community.cryptomator.org/t/how-do-i-enable-debug-mode/36
Then reproduce the problem to ensure all important information is contained in there. You may use test data or redact sensitive information from the log file. Then reproduce the problem to ensure all important information is contained in there. You may use test data or redact sensitive information from the log file.
@@ -70,5 +53,4 @@ Log file location:
- Windows: %appdata%/Cryptomator - Windows: %appdata%/Cryptomator
- macOS: ~/Library/Logs/Cryptomator - macOS: ~/Library/Logs/Cryptomator
- Linux: ~/.local/share/Cryptomator/logs - Linux: ~/.local/share/Cryptomator/logs
--> -->
+4 -4
View File
@@ -1,8 +1,8 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Cryptomator Community - name: Help & Support
url: https://community.cryptomator.org/ url: https://community.cryptomator.org/
about: Please ask and answer questions here about: You will find answers in our community forum
- name: Documentation - name: User Manual
url: https://docs.cryptomator.org/ url: https://docs.cryptomator.org/
about: Get instructions on how to use Cryptomator about: Read the Cryptomator documentation here
+3 -8
View File
@@ -5,14 +5,9 @@ labels: type:feature-request
--- ---
<!-- <!--
Please make sure to:
Do you want to ask a QUESTION? Are you looking for SUPPORT? - Comply with our code of conduct: https://github.com/cryptomator/cryptomator/blob/develop/.github/CODE_OF_CONDUCT.md
We're happy to help you via our support channels! Please read: https://github.com/cryptomator/cryptomator/blob/develop/SUPPORT.md - Search for existing similar issues first: https://github.com/cryptomator/cryptomator/issues?q=
By filing a feature request, you are expected to comply with our code of conduct: https://github.com/cryptomator/cryptomator/blob/develop/.github/CODE_OF_CONDUCT.md
Of course, we also expect you to search for existing similar feature requests first! ;)
--> -->
### Summary ### Summary
View File
View File
+22 -6
View File
@@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-java@v1 - uses: actions/setup-java@v1
with: with:
java-version: 14 java-version: 16
- uses: actions/cache@v1 - uses: actions/cache@v1
with: with:
path: ~/.m2/repository path: ~/.m2/repository
@@ -25,12 +25,13 @@ jobs:
- name: Build and Test - name: Build and Test
run: mvn -B install --file main/pom.xml -Pcoverage run: mvn -B install --file main/pom.xml -Pcoverage
- name: Run Codacy Coverage Reporter - name: Run Codacy Coverage Reporter
if: github.repository == 'cryptomator/cryptomator'
run: | run: |
curl -o ~/codacy-coverage-reporter.jar https://repo.maven.apache.org/maven2/com/codacy/codacy-coverage-reporter/7.1.0/codacy-coverage-reporter-7.1.0-assembly.jar curl -o ~/codacy-coverage-reporter.jar https://repo.maven.apache.org/maven2/com/codacy/codacy-coverage-reporter/7.1.0/codacy-coverage-reporter-7.1.0-assembly.jar
$JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r main/commons/target/site/jacoco/jacoco.xml --partial $JAVA_HOME/bin/java --illegal-access=permit -jar ~/codacy-coverage-reporter.jar report -l Java -r main/commons/target/site/jacoco/jacoco.xml --partial
$JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r main/ui/target/site/jacoco/jacoco.xml --partial $JAVA_HOME/bin/java --illegal-access=permit -jar ~/codacy-coverage-reporter.jar report -l Java -r main/ui/target/site/jacoco/jacoco.xml --partial
$JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r main/launcher/target/site/jacoco/jacoco.xml --partial $JAVA_HOME/bin/java --illegal-access=permit -jar ~/codacy-coverage-reporter.jar report -l Java -r main/launcher/target/site/jacoco/jacoco.xml --partial
$JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar final $JAVA_HOME/bin/java --illegal-access=permit -jar ~/codacy-coverage-reporter.jar final
env: env:
CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
- name: Assemble buildkit-linux.zip - name: Assemble buildkit-linux.zip
@@ -59,7 +60,7 @@ jobs:
name: Draft a Release on GitHub Releases name: Draft a Release on GitHub Releases
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: build needs: build
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/') && github.repository == 'cryptomator/cryptomator'
steps: steps:
- name: Download buildkit-linux.zip - name: Download buildkit-linux.zip
uses: actions/download-artifact@v1 uses: actions/download-artifact@v1
@@ -86,6 +87,21 @@ jobs:
release_name: ${{ github.ref }} release_name: ${{ github.ref }}
body: | body: |
:construction: Work in Progress :construction: Work in Progress
TODO:
* [ ] add Linux appimage, zsync file and signature file
* [ ] add Windows installer and signature file
* [ ] add MacOs disk image and signature file
## What's new
## Bugfixes
## Misc
---
:scroll: A complete list of closed issues is available [here](LINK)
draft: true draft: true
prerelease: false prerelease: false
- name: Upload buildkit-linux.zip to GitHub Releases - name: Upload buildkit-linux.zip to GitHub Releases
+20 -8
View File
@@ -6,16 +6,28 @@ on:
jobs: jobs:
closeTemplateViolation: closeTemplateViolation:
name: Close bug reports that violate the issue template name: Validate bug report against issue template
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: contains(github.event.issue.labels.*.name, 'type:bug')
steps: steps:
- if: | - name: Check "Description"
contains(github.event.issue.labels.*.name, 'type:bug') if: |
&& ( !contains(github.event.issue.body, env.MUST_CONTAIN)
!contains(github.event.issue.body, '<!-- oooooo -->') || contains(toJson(github.event.issue.body), env.MUST_NOT_CONTAIN)
|| !contains(github.event.issue.body, '### Description') run: exit 1
) env:
name: Close Issue MUST_CONTAIN: '### Description'
MUST_NOT_CONTAIN: '### Description\r\n\r\n[Summarize your problem.]\r\n\r\n### System Setup'
- name: Check "Steps to Reproduce"
if: |
!contains(github.event.issue.body, env.MUST_CONTAIN)
|| contains(toJson(github.event.issue.body), env.MUST_NOT_CONTAIN)
run: exit 1
env:
MUST_CONTAIN: '### Steps to Reproduce'
MUST_NOT_CONTAIN: '### Steps to Reproduce\r\n\r\n1. [First step]\r\n2. [Second step]\r\n3. [and so on…]\r\n\r\n#### Expected Behavior'
- name: Close issue if one of the checks failed
if: ${{ failure() }}
uses: peter-evans/close-issue@v1 uses: peter-evans/close-issue@v1
with: with:
comment: | comment: |
+1
View File
@@ -21,5 +21,6 @@ pom.xml.versionsBackup
.idea/compiler.xml .idea/compiler.xml
.idea/encodings.xml .idea/encodings.xml
.idea/jarRepositories.xml .idea/jarRepositories.xml
.idea/uiDesigner.xml
.idea/**/libraries/ .idea/**/libraries/
*.iml *.iml
+1 -1
View File
@@ -8,7 +8,7 @@
</list> </list>
</option> </option>
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_14" default="false" project-jdk-name="14" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_16" project-jdk-name="16" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </project>
+1 -1
View File
@@ -2,7 +2,7 @@
<configuration default="false" name="Cryptomator Linux" type="Application" factoryName="Application"> <configuration default="false" name="Cryptomator Linux" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" /> <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="launcher" /> <module name="launcher" />
<option name="VM_PARAMETERS" value="-Djdk.gtk.version=2 -Duser.language=en -Dcryptomator.settingsPath=&quot;~/.config/Cryptomator/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/.config/Cryptomator/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator/logs&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator/mnt&quot; -Xss20m -Xmx512m" /> <option name="VM_PARAMETERS" value="-Djdk.gtk.version=2 -Duser.language=en -Dcryptomator.settingsPath=&quot;~/.config/Cryptomator/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/.config/Cryptomator/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator/logs&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator/mnt&quot; -Dcryptomator.showTrayIcon=true -Xss20m -Xmx512m" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>
+1 -1
View File
@@ -2,7 +2,7 @@
<configuration default="false" name="Cryptomator Linux Dev" type="Application" factoryName="Application"> <configuration default="false" name="Cryptomator Linux Dev" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" /> <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="launcher" /> <module name="launcher" />
<option name="VM_PARAMETERS" value="-Djdk.gtk.version=2 -Duser.language=en -Dcryptomator.settingsPath=&quot;~/.config/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/.config/Cryptomator-Dev/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator-Dev/logs&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator-Dev/mnt&quot; -Dfuse.experimental=&quot;true&quot; -Xss20m -Xmx512m" /> <option name="VM_PARAMETERS" value="-Djdk.gtk.version=2 -Duser.language=en -Dcryptomator.settingsPath=&quot;~/.config/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/.config/Cryptomator-Dev/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator-Dev/logs&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator-Dev/mnt&quot; -Dcryptomator.showTrayIcon=true -Dfuse.experimental=&quot;true&quot; -Xss20m -Xmx512m" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>
+1 -1
View File
@@ -2,7 +2,7 @@
<configuration default="false" name="Cryptomator Windows" type="Application" factoryName="Application"> <configuration default="false" name="Cryptomator Windows" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" /> <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="launcher" /> <module name="launcher" />
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/AppData/Roaming/Cryptomator/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator&quot; -Dcryptomator.keychainPath=&quot;~/AppData/Roaming/Cryptomator/keychain.json&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator&quot; -Xss2m -Xmx512m" /> <option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/AppData/Roaming/Cryptomator/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator&quot; -Dcryptomator.keychainPath=&quot;~/AppData/Roaming/Cryptomator/keychain.json&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>
+1 -1
View File
@@ -2,7 +2,7 @@
<configuration default="false" name="Cryptomator Windows Dev" type="Application" factoryName="Application"> <configuration default="false" name="Cryptomator Windows Dev" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" /> <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="launcher" /> <module name="launcher" />
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/AppData/Roaming/Cryptomator-Dev/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator-Dev&quot; -Dcryptomator.keychainPath=&quot;~/AppData/Roaming/Cryptomator-Dev/keychain.json&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator-Dev&quot; -Dfuse.experimental=&quot;true&quot; -Xss2m -Xmx512m" /> <option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/AppData/Roaming/Cryptomator-Dev/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator-Dev&quot; -Dcryptomator.keychainPath=&quot;~/AppData/Roaming/Cryptomator-Dev/keychain.json&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator-Dev&quot; -Dfuse.experimental=&quot;true&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>
+1 -1
View File
@@ -5,7 +5,7 @@
</envs> </envs>
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" /> <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="launcher" /> <module name="launcher" />
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/Library/Application Support/Cryptomator/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator&quot; -Xss2m -Xmx512m -ea" /> <option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/Library/Application Support/Cryptomator/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>
+1 -1
View File
@@ -5,7 +5,7 @@
</envs> </envs>
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" /> <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="launcher" /> <module name="launcher" />
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/Library/Application Support/Cryptomator-Dev/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator-Dev&quot; -Xss2m -Xmx512m -ea" /> <option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/Library/Application Support/Cryptomator-Dev/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator-Dev&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>
+20 -5
View File
@@ -2,7 +2,7 @@
[![Build](https://github.com/cryptomator/cryptomator/workflows/Build/badge.svg)](https://github.com/cryptomator/cryptomator/actions?query=workflow%3ABuild) [![Build](https://github.com/cryptomator/cryptomator/workflows/Build/badge.svg)](https://github.com/cryptomator/cryptomator/actions?query=workflow%3ABuild)
[![Known Vulnerabilities](https://snyk.io/test/github/cryptomator/cryptomator/badge.svg?targetFile=main%2Fpom.xml)](https://snyk.io/test/github/cryptomator/cryptomator?targetFile=main%2Fpom.xml) [![Known Vulnerabilities](https://snyk.io/test/github/cryptomator/cryptomator/badge.svg?targetFile=main%2Fpom.xml)](https://snyk.io/test/github/cryptomator/cryptomator?targetFile=main%2Fpom.xml)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/2a0adf3cec6a4143b91035d3924178f1)](https://www.codacy.com/app/cryptomator/cryptomator?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=cryptomator/cryptomator&amp;utm_campaign=Badge_Grade) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/2a0adf3cec6a4143b91035d3924178f1)](https://www.codacy.com/gh/cryptomator/cryptomator/dashboard)
[![Twitter](https://img.shields.io/badge/twitter-@Cryptomator-blue.svg?style=flat)](http://twitter.com/Cryptomator) [![Twitter](https://img.shields.io/badge/twitter-@Cryptomator-blue.svg?style=flat)](http://twitter.com/Cryptomator)
[![Crowdin](https://badges.crowdin.net/cryptomator/localized.svg)](https://translate.cryptomator.org/) [![Crowdin](https://badges.crowdin.net/cryptomator/localized.svg)](https://translate.cryptomator.org/)
[![Latest Release](https://img.shields.io/github/release/cryptomator/cryptomator.svg)](https://github.com/cryptomator/cryptomator/releases/latest) [![Latest Release](https://img.shields.io/github/release/cryptomator/cryptomator.svg)](https://github.com/cryptomator/cryptomator/releases/latest)
@@ -17,11 +17,26 @@ Cryptomator is provided free of charge as an open-source project despite the hig
### Gold Sponsors ### Gold Sponsors
[<img src="https://cryptomator.org/img/sponsors/geewhiz.svg" alt="gee-whiz" height="96">](https://www.gee-whiz.de/) <table>
<tbody>
<tr>
<td><a href="https://www.gee-whiz.de/"><img src="https://cryptomator.org/img/sponsors/geewhiz.svg" alt="gee-whiz" height="80"></a></td>
<td><a href="https://proxy-hub.com/"><img src="https://cryptomator.org/img/sponsors/proxyhub.svg" alt="Proxy-Hub" height="80"></a></td>
</tr>
</tbody>
</table>
### Silver Sponsors ### Silver Sponsors
[![TheBestVPN](https://cryptomator.org/img/sponsors/thebestvpn.png)](https://thebestvpn.com/) <table>
<tbody>
<tr>
<td><a href="https://thebestvpn.com/"><img src="https://cryptomator.org/img/sponsors/thebestvpn@2x.png" alt="TheBestVPN" height="64"></a></td>
</tr>
</tbody>
</table>
- [Jameson Lopp](https://www.lopp.net/)
--- ---
@@ -33,7 +48,7 @@ Download native binaries of Cryptomator on [cryptomator.org](https://cryptomator
## Features ## Features
- Works with Dropbox, Google Drive, OneDrive, ownCloud, Nextcloud and any other cloud storage service which synchronizes with a local directory - Works with Dropbox, Google Drive, OneDrive, MEGA, pCloud, ownCloud, Nextcloud and any other cloud storage service which synchronizes with a local directory
- Open Source means: No backdoors, control is better than trust - Open Source means: No backdoors, control is better than trust
- Client-side: No accounts, no data shared with any online service - Client-side: No accounts, no data shared with any online service
- Totally transparent: Just work on the virtual drive as if it were a USB flash drive - Totally transparent: Just work on the virtual drive as if it were a USB flash drive
@@ -65,7 +80,7 @@ For more information on the security details visit [cryptomator.org](https://doc
### Dependencies ### Dependencies
* JDK 14 (e.g. adoptopenjdk) * JDK 16 (e.g. adoptopenjdk)
* Maven 3 * Maven 3
* Optional: OS-dependent build tools for native packaging (see [Windows](https://github.com/cryptomator/cryptomator-win), [OS X](https://github.com/cryptomator/cryptomator-osx), [Linux](https://github.com/cryptomator/builder-containers)) * Optional: OS-dependent build tools for native packaging (see [Windows](https://github.com/cryptomator/cryptomator-win), [OS X](https://github.com/cryptomator/cryptomator-osx), [Linux](https://github.com/cryptomator/builder-containers))
+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.11</version> <version>1.6.0</version>
</parent> </parent>
<artifactId>buildkit</artifactId> <artifactId>buildkit</artifactId>
<packaging>pom</packaging> <packaging>pom</packaging>
+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.11</version> <version>1.6.0</version>
</parent> </parent>
<artifactId>commons</artifactId> <artifactId>commons</artifactId>
<name>Cryptomator Commons</name> <name>Cryptomator Commons</name>
@@ -15,6 +15,7 @@ import org.cryptomator.common.settings.SettingsProvider;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultComponent; import org.cryptomator.common.vaults.VaultComponent;
import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.frontend.webdav.WebDavServer; import org.cryptomator.frontend.webdav.WebDavServer;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -25,6 +26,8 @@ import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Comparator; import java.util.Comparator;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@@ -47,10 +50,28 @@ public abstract class CommonsModule {
@Named("licensePublicKey") @Named("licensePublicKey")
static String provideLicensePublicKey() { static String provideLicensePublicKey() {
// in PEM format without the dash-escaped begin/end lines // in PEM format without the dash-escaped begin/end lines
return "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQB7NfnqiZbg2KTmoflmZ71PbXru7oW" // return """
+ "fmnV2yv3eDjlDfGruBrqz9TtXBZV/eYWt31xu1osIqaT12lKBvZ511aaAkIBeOEV" // MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQB7NfnqiZbg2KTmoflmZ71PbXru7oW\
+ "gwcBIlJr6kUw7NKzeJt7r2rrsOyQoOG2nWc/Of/NBqA3mIZRHk5Aq1YupFdD26QE" // fmnV2yv3eDjlDfGruBrqz9TtXBZV/eYWt31xu1osIqaT12lKBvZ511aaAkIBeOEV\
+ "r0DzRyj4ixPIt38CQB8="; gwcBIlJr6kUw7NKzeJt7r2rrsOyQoOG2nWc/Of/NBqA3mIZRHk5Aq1YupFdD26QE\
r0DzRyj4ixPIt38CQB8=\
""";
}
@Provides
@Singleton
static SecureRandom provideCSPRNG() {
try {
return SecureRandom.getInstanceStrong();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("A strong algorithm must exist in every Java platform.", e);
}
}
@Provides
@Singleton
static MasterkeyFileAccess provideMasterkeyFileAccess(SecureRandom csprng) {
return new MasterkeyFileAccess(Constants.PEPPER, csprng);
} }
@Provides @Provides
@@ -3,5 +3,8 @@ package org.cryptomator.common;
public interface Constants { public interface Constants {
String MASTERKEY_FILENAME = "masterkey.cryptomator"; String MASTERKEY_FILENAME = "masterkey.cryptomator";
String MASTERKEY_BACKUP_SUFFIX = ".bkup";
String VAULTCONFIG_FILENAME = "vault.cryptomator";
byte[] PEPPER = new byte[0];
} }
@@ -21,14 +21,13 @@ import java.util.stream.StreamSupport;
public class Environment { public class Environment {
private static final Logger LOG = LoggerFactory.getLogger(Environment.class); private static final Logger LOG = LoggerFactory.getLogger(Environment.class);
private static final String USER_HOME = System.getProperty("user.home");
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 char PATH_LIST_SEP = ':'; private static final char PATH_LIST_SEP = ':';
private static final int DEFAULT_MIN_PW_LENGTH = 8; private static final int DEFAULT_MIN_PW_LENGTH = 8;
@Inject @Inject
public Environment() { public Environment() {
LOG.debug("user.home: {}", System.getProperty("user.home"));
LOG.debug("java.library.path: {}", System.getProperty("java.library.path")); LOG.debug("java.library.path: {}", System.getProperty("java.library.path"));
LOG.debug("user.language: {}", System.getProperty("user.language")); LOG.debug("user.language: {}", System.getProperty("user.language"));
LOG.debug("user.region: {}", System.getProperty("user.region")); LOG.debug("user.region: {}", System.getProperty("user.region"));
@@ -40,6 +39,7 @@ public class Environment {
LOG.debug("cryptomator.mountPointsDir: {}", System.getProperty("cryptomator.mountPointsDir")); LOG.debug("cryptomator.mountPointsDir: {}", System.getProperty("cryptomator.mountPointsDir"));
LOG.debug("cryptomator.minPwLength: {}", System.getProperty("cryptomator.minPwLength")); LOG.debug("cryptomator.minPwLength: {}", System.getProperty("cryptomator.minPwLength"));
LOG.debug("cryptomator.buildNumber: {}", System.getProperty("cryptomator.buildNumber")); LOG.debug("cryptomator.buildNumber: {}", System.getProperty("cryptomator.buildNumber"));
LOG.debug("cryptomator.showTrayIcon: {}", System.getProperty("cryptomator.showTrayIcon"));
LOG.debug("fuse.experimental: {}", Boolean.getBoolean("fuse.experimental")); LOG.debug("fuse.experimental: {}", Boolean.getBoolean("fuse.experimental"));
} }
@@ -75,6 +75,11 @@ public class Environment {
return getInt("cryptomator.minPwLength", DEFAULT_MIN_PW_LENGTH); return getInt("cryptomator.minPwLength", DEFAULT_MIN_PW_LENGTH);
} }
public boolean showTrayIcon() {
return Boolean.getBoolean("cryptomator.showTrayIcon");
}
@Deprecated // TODO: remove as soon as custom mount path works properly on Win+Fuse
public boolean useExperimentalFuse() { public boolean useExperimentalFuse() {
return Boolean.getBoolean("fuse.experimental"); return Boolean.getBoolean("fuse.experimental");
} }
@@ -92,8 +97,13 @@ public class Environment {
String value = System.getProperty(propertyName); String value = System.getProperty(propertyName);
return Optional.ofNullable(value).map(Paths::get); return Optional.ofNullable(value).map(Paths::get);
} }
// visible for testing
// visible for testing
Path getHomeDir() {
return getPath("user.home").orElseThrow();
}
// visible for testing
Stream<Path> getPaths(String propertyName) { Stream<Path> getPaths(String propertyName) {
Stream<String> rawSettingsPaths = getRawList(propertyName, PATH_LIST_SEP); Stream<String> rawSettingsPaths = getRawList(propertyName, PATH_LIST_SEP);
return rawSettingsPaths.filter(Predicate.not(Strings::isNullOrEmpty)).map(Paths::get).map(this::replaceHomeDir); return rawSettingsPaths.filter(Predicate.not(Strings::isNullOrEmpty)).map(Paths::get).map(this::replaceHomeDir);
@@ -101,7 +111,7 @@ public class Environment {
private Path replaceHomeDir(Path path) { private Path replaceHomeDir(Path path) {
if (path.startsWith(RELATIVE_HOME_DIR)) { if (path.startsWith(RELATIVE_HOME_DIR)) {
return ABSOLUTE_HOME_DIR.resolve(RELATIVE_HOME_DIR.relativize(path)); return getHomeDir().resolve(RELATIVE_HOME_DIR.relativize(path));
} else { } else {
return path; return path;
} }
@@ -33,8 +33,8 @@ class LicenseChecker {
try { try {
byte[] keyBytes = BaseEncoding.base64().decode(pemEncodedPublicKey); byte[] keyBytes = BaseEncoding.base64().decode(pemEncodedPublicKey);
PublicKey key = KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(keyBytes)); PublicKey key = KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(keyBytes));
if (key instanceof ECPublicKey) { if (key instanceof ECPublicKey k) {
return (ECPublicKey) key; return k;
} else { } else {
throw new IllegalStateException("Key not an EC public key."); throw new IllegalStateException("Key not an EC public key.");
} }
@@ -68,6 +68,11 @@ public class KeychainManager implements KeychainAccessProvider {
return keychain.getValue() != null; return keychain.getValue() != null;
} }
@Override
public boolean isLocked() {
return keychain.getValue() == null || keychain.get().isLocked();
}
/** /**
* Checks if the keychain knows a passphrase for the given key. * Checks if the keychain knows a passphrase for the given key.
* <p> * <p>
@@ -9,6 +9,7 @@
package org.cryptomator.common.settings; package org.cryptomator.common.settings;
import org.apache.commons.lang3.SystemUtils; import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Environment;
import javafx.beans.Observable; import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
@@ -39,7 +40,8 @@ public class Settings {
public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT; public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT;
public static final KeychainBackend DEFAULT_KEYCHAIN_BACKEND = SystemUtils.IS_OS_WINDOWS ? KeychainBackend.WIN_SYSTEM_KEYCHAIN : SystemUtils.IS_OS_MAC ? KeychainBackend.MAC_SYSTEM_KEYCHAIN : KeychainBackend.GNOME; public static final KeychainBackend DEFAULT_KEYCHAIN_BACKEND = SystemUtils.IS_OS_WINDOWS ? KeychainBackend.WIN_SYSTEM_KEYCHAIN : SystemUtils.IS_OS_MAC ? KeychainBackend.MAC_SYSTEM_KEYCHAIN : KeychainBackend.GNOME;
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 = ""; public static final String DEFAULT_LICENSE_KEY = "";
public static final boolean DEFAULT_SHOW_MINIMIZE_BUTTON = false;
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);
@@ -54,13 +56,17 @@ public class Settings {
private final ObjectProperty<KeychainBackend> keychainBackend = new SimpleObjectProperty<>(DEFAULT_KEYCHAIN_BACKEND); private final ObjectProperty<KeychainBackend> keychainBackend = new SimpleObjectProperty<>(DEFAULT_KEYCHAIN_BACKEND);
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 final StringProperty licenseKey = new SimpleStringProperty(DEFAULT_LICENSE_KEY);
private final BooleanProperty showMinimizeButton = new SimpleBooleanProperty(DEFAULT_SHOW_MINIMIZE_BUTTON);
private final BooleanProperty showTrayIcon;
private Consumer<Settings> saveCmd; private Consumer<Settings> saveCmd;
/** /**
* Package-private constructor; use {@link SettingsProvider}. * Package-private constructor; use {@link SettingsProvider}.
*/ */
Settings() { Settings(Environment env) {
this.showTrayIcon = new SimpleBooleanProperty(env.showTrayIcon());
directories.addListener(this::somethingChanged); directories.addListener(this::somethingChanged);
askedForUpdateCheck.addListener(this::somethingChanged); askedForUpdateCheck.addListener(this::somethingChanged);
checkForUpdates.addListener(this::somethingChanged); checkForUpdates.addListener(this::somethingChanged);
@@ -74,6 +80,8 @@ public class Settings {
keychainBackend.addListener(this::somethingChanged); keychainBackend.addListener(this::somethingChanged);
userInterfaceOrientation.addListener(this::somethingChanged); userInterfaceOrientation.addListener(this::somethingChanged);
licenseKey.addListener(this::somethingChanged); licenseKey.addListener(this::somethingChanged);
showMinimizeButton.addListener(this::somethingChanged);
showTrayIcon.addListener(this::somethingChanged);
} }
void setSaveCmd(Consumer<Settings> saveCmd) { void setSaveCmd(Consumer<Settings> saveCmd) {
@@ -141,4 +149,12 @@ public class Settings {
public StringProperty licenseKey() { public StringProperty licenseKey() {
return licenseKey; return licenseKey;
} }
public BooleanProperty showMinimizeButton() {
return showMinimizeButton;
}
public BooleanProperty showTrayIcon() {
return showTrayIcon;
}
} }
@@ -9,19 +9,29 @@ import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter; import com.google.gson.stream.JsonWriter;
import org.cryptomator.common.Environment;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import javafx.geometry.NodeOrientation; import javafx.geometry.NodeOrientation;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@Singleton
public class SettingsJsonAdapter extends TypeAdapter<Settings> { public class SettingsJsonAdapter extends TypeAdapter<Settings> {
private static final Logger LOG = LoggerFactory.getLogger(SettingsJsonAdapter.class); private static final Logger LOG = LoggerFactory.getLogger(SettingsJsonAdapter.class);
private final VaultSettingsJsonAdapter vaultSettingsJsonAdapter = new VaultSettingsJsonAdapter(); private final VaultSettingsJsonAdapter vaultSettingsJsonAdapter = new VaultSettingsJsonAdapter();
private final Environment env;
@Inject
public SettingsJsonAdapter(Environment env) {
this.env = env;
}
@Override @Override
public void write(JsonWriter out, Settings value) throws IOException { public void write(JsonWriter out, Settings value) throws IOException {
@@ -40,6 +50,8 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
out.name("uiOrientation").value(value.userInterfaceOrientation().get().name()); out.name("uiOrientation").value(value.userInterfaceOrientation().get().name());
out.name("keychainBackend").value(value.keychainBackend().get().name()); out.name("keychainBackend").value(value.keychainBackend().get().name());
out.name("licenseKey").value(value.licenseKey().get()); out.name("licenseKey").value(value.licenseKey().get());
out.name("showMinimizeButton").value(value.showMinimizeButton().get());
out.name("showTrayIcon").value(value.showTrayIcon().get());
out.endObject(); out.endObject();
} }
@@ -53,7 +65,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
@Override @Override
public Settings read(JsonReader in) throws IOException { public Settings read(JsonReader in) throws IOException {
Settings settings = new Settings(); Settings settings = new Settings(env);
in.beginObject(); in.beginObject();
while (in.hasNext()) { while (in.hasNext()) {
@@ -72,6 +84,8 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
case "uiOrientation" -> settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString())); case "uiOrientation" -> settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
case "keychainBackend" -> settings.keychainBackend().set(parseKeychainBackend(in.nextString())); case "keychainBackend" -> settings.keychainBackend().set(parseKeychainBackend(in.nextString()));
case "licenseKey" -> settings.licenseKey().set(in.nextString()); case "licenseKey" -> settings.licenseKey().set(in.nextString());
case "showMinimizeButton" -> settings.showMinimizeButton().set(in.nextBoolean());
case "showTrayIcon" -> settings.showTrayIcon().set(in.nextBoolean());
default -> { default -> {
LOG.warn("Unsupported vault setting found in JSON: " + name); LOG.warn("Unsupported vault setting found in JSON: " + name);
in.skipValue(); in.skipValue();
@@ -137,5 +151,4 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
in.endArray(); in.endArray();
return result; return result;
} }
} }
@@ -49,13 +49,14 @@ public class SettingsProvider implements Supplier<Settings> {
private final AtomicReference<ScheduledFuture<?>> scheduledSaveCmd = new AtomicReference<>(); private final AtomicReference<ScheduledFuture<?>> scheduledSaveCmd = new AtomicReference<>();
private final Supplier<Settings> settings = Suppliers.memoize(this::load); private final Supplier<Settings> settings = Suppliers.memoize(this::load);
private final SettingsJsonAdapter settingsJsonAdapter = new SettingsJsonAdapter(); private final SettingsJsonAdapter settingsJsonAdapter;
private final Environment env; private final Environment env;
private final ScheduledExecutorService scheduler; private final ScheduledExecutorService scheduler;
private final Gson gson; private final Gson gson;
@Inject @Inject
public SettingsProvider(Environment env, ScheduledExecutorService scheduler) { public SettingsProvider(SettingsJsonAdapter settingsJsonAdapter, Environment env, ScheduledExecutorService scheduler) {
this.settingsJsonAdapter = settingsJsonAdapter;
this.env = env; this.env = env;
this.scheduler = scheduler; this.scheduler = scheduler;
this.gson = new GsonBuilder() // this.gson = new GsonBuilder() //
@@ -70,7 +71,7 @@ public class SettingsProvider implements Supplier<Settings> {
} }
private Settings load() { private Settings load() {
Settings settings = env.getSettingsPath().flatMap(this::tryLoad).findFirst().orElse(new Settings()); Settings settings = env.getSettingsPath().flatMap(this::tryLoad).findFirst().orElse(new Settings(env));
settings.setSaveCmd(this::scheduleSave); settings.setSaveCmd(this::scheduleSave);
return settings; return settings;
} }
@@ -90,7 +91,7 @@ public class SettingsProvider implements Supplier<Settings> {
} }
} catch (NoSuchFileException e) { } catch (NoSuchFileException e) {
return Stream.empty(); return Stream.empty();
} catch (IOException e) { } catch (IOException | JsonParseException e) {
LOG.warn("Exception while loading settings from " + path, e); LOG.warn("Exception while loading settings from " + path, e);
return Stream.empty(); return Stream.empty();
} }
@@ -8,7 +8,7 @@ public enum UiTheme {
AUTOMATIC("preferences.general.theme.automatic"); AUTOMATIC("preferences.general.theme.automatic");
public static UiTheme[] applicableValues() { public static UiTheme[] applicableValues() {
if (SystemUtils.IS_OS_MAC) { if (SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_WINDOWS) {
return values(); return values();
} else { } else {
return new UiTheme[]{LIGHT, DARK}; return new UiTheme[]{LIGHT, DARK};
@@ -24,8 +24,6 @@ import java.nio.file.Path;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Random; import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;
/** /**
* The settings specific to a single vault. * The settings specific to a single vault.
@@ -37,7 +35,7 @@ public class VaultSettings {
public static final boolean DEFAULT_USES_INDIVIDUAL_MOUNTPATH = false; public static final boolean DEFAULT_USES_INDIVIDUAL_MOUNTPATH = false;
public static final boolean DEFAULT_USES_READONLY_MODE = false; public static final boolean DEFAULT_USES_READONLY_MODE = false;
public static final String DEFAULT_MOUNT_FLAGS = ""; public static final String DEFAULT_MOUNT_FLAGS = "";
public static final int DEFAULT_FILENAME_LENGTH_LIMIT = -1; public static final int DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH = -1;
public static final WhenUnlocked DEFAULT_ACTION_AFTER_UNLOCK = WhenUnlocked.ASK; public static final WhenUnlocked DEFAULT_ACTION_AFTER_UNLOCK = WhenUnlocked.ASK;
private static final Random RNG = new Random(); private static final Random RNG = new Random();
@@ -52,7 +50,7 @@ public class VaultSettings {
private final StringProperty customMountPath = new SimpleStringProperty(); private final StringProperty customMountPath = new SimpleStringProperty();
private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE); private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE);
private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS); private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS);
private final IntegerProperty filenameLengthLimit = new SimpleIntegerProperty(DEFAULT_FILENAME_LENGTH_LIMIT); private final IntegerProperty maxCleartextFilenameLength = new SimpleIntegerProperty(DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH);
private final ObjectProperty<WhenUnlocked> actionAfterUnlock = new SimpleObjectProperty<>(DEFAULT_ACTION_AFTER_UNLOCK); private final ObjectProperty<WhenUnlocked> actionAfterUnlock = new SimpleObjectProperty<>(DEFAULT_ACTION_AFTER_UNLOCK);
private final StringBinding mountName; private final StringBinding mountName;
@@ -63,7 +61,7 @@ public class VaultSettings {
} }
Observable[] observables() { Observable[] observables() {
return new Observable[]{path, displayName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, filenameLengthLimit, actionAfterUnlock}; return new Observable[]{path, displayName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, maxCleartextFilenameLength, actionAfterUnlock};
} }
public static VaultSettings withRandomId() { public static VaultSettings withRandomId() {
@@ -152,8 +150,8 @@ public class VaultSettings {
return mountFlags; return mountFlags;
} }
public IntegerProperty filenameLengthLimit() { public IntegerProperty maxCleartextFilenameLength() {
return filenameLengthLimit; return maxCleartextFilenameLength;
} }
public ObjectProperty<WhenUnlocked> actionAfterUnlock() { public ObjectProperty<WhenUnlocked> actionAfterUnlock() {
@@ -173,8 +171,7 @@ public class VaultSettings {
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (obj instanceof VaultSettings && obj.getClass().equals(this.getClass())) { if (obj instanceof VaultSettings other && obj.getClass().equals(this.getClass())) {
VaultSettings other = (VaultSettings) obj;
return Objects.equals(this.id, other.id); return Objects.equals(this.id, other.id);
} else { } else {
return false; return false;
@@ -29,7 +29,7 @@ class VaultSettingsJsonAdapter {
out.name("customMountPath").value(value.customMountPath().get()); out.name("customMountPath").value(value.customMountPath().get());
out.name("usesReadOnlyMode").value(value.usesReadOnlyMode().get()); out.name("usesReadOnlyMode").value(value.usesReadOnlyMode().get());
out.name("mountFlags").value(value.mountFlags().get()); out.name("mountFlags").value(value.mountFlags().get());
out.name("filenameLengthLimit").value(value.filenameLengthLimit().get()); out.name("maxCleartextFilenameLength").value(value.maxCleartextFilenameLength().get());
out.name("actionAfterUnlock").value(value.actionAfterUnlock().get().name()); out.name("actionAfterUnlock").value(value.actionAfterUnlock().get().name());
out.endObject(); out.endObject();
} }
@@ -46,7 +46,7 @@ class VaultSettingsJsonAdapter {
boolean useCustomMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH; boolean useCustomMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH;
boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE; boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE;
String mountFlags = VaultSettings.DEFAULT_MOUNT_FLAGS; String mountFlags = VaultSettings.DEFAULT_MOUNT_FLAGS;
int filenameLengthLimit = VaultSettings.DEFAULT_FILENAME_LENGTH_LIMIT; int maxCleartextFilenameLength = VaultSettings.DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH;
WhenUnlocked actionAfterUnlock = VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK; WhenUnlocked actionAfterUnlock = VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;
in.beginObject(); in.beginObject();
@@ -64,7 +64,7 @@ class VaultSettingsJsonAdapter {
case "individualMountPath", "customMountPath" -> customMountPath = in.nextString(); case "individualMountPath", "customMountPath" -> customMountPath = in.nextString();
case "usesReadOnlyMode" -> usesReadOnlyMode = in.nextBoolean(); case "usesReadOnlyMode" -> usesReadOnlyMode = in.nextBoolean();
case "mountFlags" -> mountFlags = in.nextString(); case "mountFlags" -> mountFlags = in.nextString();
case "filenameLengthLimit" -> filenameLengthLimit = in.nextInt(); case "maxCleartextFilenameLength" -> maxCleartextFilenameLength = in.nextInt();
case "actionAfterUnlock" -> actionAfterUnlock = parseActionAfterUnlock(in.nextString()); case "actionAfterUnlock" -> actionAfterUnlock = parseActionAfterUnlock(in.nextString());
default -> { default -> {
LOG.warn("Unsupported vault setting found in JSON: " + name); LOG.warn("Unsupported vault setting found in JSON: " + name);
@@ -88,7 +88,7 @@ class VaultSettingsJsonAdapter {
vaultSettings.customMountPath().set(customMountPath); vaultSettings.customMountPath().set(customMountPath);
vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode); vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode);
vaultSettings.mountFlags().set(mountFlags); vaultSettings.mountFlags().set(mountFlags);
vaultSettings.filenameLengthLimit().set(filenameLengthLimit); vaultSettings.maxCleartextFilenameLength().set(maxCleartextFilenameLength);
vaultSettings.actionAfterUnlock().set(actionAfterUnlock); vaultSettings.actionAfterUnlock().set(actionAfterUnlock);
return vaultSettings; return vaultSettings;
} }
@@ -5,15 +5,15 @@ import org.cryptomator.common.mountpoint.MountPointChooser;
import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.settings.VolumeImpl; import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.cryptofs.CryptoFileSystem; import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.frontend.dokany.DokanyMountFailedException;
import org.cryptomator.frontend.dokany.Mount; import org.cryptomator.frontend.dokany.Mount;
import org.cryptomator.frontend.dokany.MountFactory; import org.cryptomator.frontend.dokany.MountFactory;
import org.cryptomator.frontend.dokany.MountFailedException;
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 javax.inject.Named;
import java.util.concurrent.ExecutorService; import java.util.function.Consumer;
public class DokanyVolume extends AbstractVolume { public class DokanyVolume extends AbstractVolume {
@@ -22,15 +22,13 @@ public class DokanyVolume extends AbstractVolume {
private static final String FS_TYPE_NAME = "CryptomatorFS"; private static final String FS_TYPE_NAME = "CryptomatorFS";
private final VaultSettings vaultSettings; private final VaultSettings vaultSettings;
private final MountFactory mountFactory;
private Mount mount; private Mount mount;
@Inject @Inject
public DokanyVolume(VaultSettings vaultSettings, ExecutorService executorService, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) { public DokanyVolume(VaultSettings vaultSettings, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
super(choosers); super(choosers);
this.vaultSettings = vaultSettings; this.vaultSettings = vaultSettings;
this.mountFactory = new MountFactory(executorService);
} }
@Override @Override
@@ -39,11 +37,11 @@ public class DokanyVolume extends AbstractVolume {
} }
@Override @Override
public void mount(CryptoFileSystem fs, String mountFlags) throws InvalidMountPointException, VolumeException { public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws InvalidMountPointException, VolumeException {
this.mountPoint = determineMountPoint(); this.mountPoint = determineMountPoint();
try { try {
this.mount = mountFactory.mount(fs.getPath("/"), mountPoint, vaultSettings.mountName().get(), FS_TYPE_NAME, mountFlags.strip()); this.mount = MountFactory.mount(fs.getPath("/"), mountPoint, vaultSettings.mountName().get(), FS_TYPE_NAME, mountFlags.strip(), onExitAction);
} catch (MountFailedException e) { } catch (DokanyMountFailedException e) {
if (vaultSettings.getCustomMountPath().isPresent()) { if (vaultSettings.getCustomMountPath().isPresent()) {
LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPoint); LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPoint);
} }
@@ -52,10 +50,11 @@ public class DokanyVolume extends AbstractVolume {
} }
@Override @Override
public void reveal() throws VolumeException { public void reveal(Revealer revealer) throws VolumeException {
boolean success = mount.reveal(); try {
if (!success) { mount.reveal(revealer::reveal);
throw new VolumeException("Reveal failed."); } catch (Exception e) {
throw new VolumeException(e);
} }
} }
@@ -79,6 +78,7 @@ public class DokanyVolume extends AbstractVolume {
public boolean supportsForcedUnmount() { public boolean supportsForcedUnmount() {
return true; return true;
} }
@Override @Override
public boolean isSupported() { public boolean isSupported() {
return DokanyVolume.isSupportedStatic(); return DokanyVolume.isSupportedStatic();
@@ -6,8 +6,8 @@ import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.mountpoint.MountPointChooser; import org.cryptomator.common.mountpoint.MountPointChooser;
import org.cryptomator.common.settings.VolumeImpl; import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.cryptofs.CryptoFileSystem; import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.frontend.fuse.mount.CommandFailedException;
import org.cryptomator.frontend.fuse.mount.EnvironmentVariables; import org.cryptomator.frontend.fuse.mount.EnvironmentVariables;
import org.cryptomator.frontend.fuse.mount.FuseMountException;
import org.cryptomator.frontend.fuse.mount.FuseMountFactory; import org.cryptomator.frontend.fuse.mount.FuseMountFactory;
import org.cryptomator.frontend.fuse.mount.FuseNotSupportedException; import org.cryptomator.frontend.fuse.mount.FuseNotSupportedException;
import org.cryptomator.frontend.fuse.mount.Mount; import org.cryptomator.frontend.fuse.mount.Mount;
@@ -20,7 +20,7 @@ import javax.inject.Named;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.SortedSet; import java.util.function.Consumer;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class FuseVolume extends AbstractVolume { public class FuseVolume extends AbstractVolume {
@@ -36,20 +36,21 @@ public class FuseVolume extends AbstractVolume {
} }
@Override @Override
public void mount(CryptoFileSystem fs, String mountFlags) throws InvalidMountPointException, VolumeException { public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws InvalidMountPointException, VolumeException {
this.mountPoint = determineMountPoint(); this.mountPoint = determineMountPoint();
mount(fs.getPath("/"), mountFlags, onExitAction);
mount(fs.getPath("/"), mountFlags);
} }
private void mount(Path root, String mountFlags) throws VolumeException { private void mount(Path root, String mountFlags, Consumer<Throwable> onExitAction) throws VolumeException {
try { try {
Mounter mounter = FuseMountFactory.getMounter(); Mounter mounter = FuseMountFactory.getMounter();
EnvironmentVariables envVars = EnvironmentVariables.create() // EnvironmentVariables envVars = EnvironmentVariables.create() //
.withFlags(splitFlags(mountFlags)).withMountPoint(mountPoint) // .withFlags(splitFlags(mountFlags)) //
.withMountPoint(mountPoint) //
.withFileNameTranscoder(mounter.defaultFileNameTranscoder()) //
.build(); .build();
this.mount = mounter.mount(root, envVars); this.mount = mounter.mount(root, envVars, onExitAction);
} catch (CommandFailedException | FuseNotSupportedException e) { } catch ( FuseMountException | FuseNotSupportedException e) {
throw new VolumeException("Unable to mount Filesystem", e); throw new VolumeException("Unable to mount Filesystem", e);
} }
} }
@@ -73,11 +74,10 @@ public class FuseVolume extends AbstractVolume {
} }
@Override @Override
public void reveal() throws VolumeException { public void reveal(Revealer revealer) throws VolumeException {
try { try {
mount.revealInFileManager(); mount.reveal(revealer::reveal);
} catch (CommandFailedException e) { } catch (Exception e) {
LOG.debug("Revealing the vault in file manger failed: " + e.getMessage());
throw new VolumeException(e); throw new VolumeException(e);
} }
} }
@@ -91,8 +91,7 @@ public class FuseVolume extends AbstractVolume {
public synchronized void unmountForced() throws VolumeException { public synchronized void unmountForced() throws VolumeException {
try { try {
mount.unmountForced(); mount.unmountForced();
mount.close(); } catch (FuseMountException e) {
} catch (CommandFailedException e) {
throw new VolumeException(e); throw new VolumeException(e);
} }
cleanupMountPoint(); cleanupMountPoint();
@@ -102,8 +101,7 @@ public class FuseVolume extends AbstractVolume {
public synchronized void unmount() throws VolumeException { public synchronized void unmount() throws VolumeException {
try { try {
mount.unmount(); mount.unmount();
mount.close(); } catch (FuseMountException e) {
} catch (CommandFailedException e) {
throw new VolumeException(e); throw new VolumeException(e);
} }
cleanupMountPoint(); cleanupMountPoint();
@@ -0,0 +1,12 @@
package org.cryptomator.common.vaults;
public class LockNotCompletedException extends Exception {
public LockNotCompletedException(String reason) {
super(reason);
}
public LockNotCompletedException(Throwable cause) {
super(cause);
}
}
@@ -17,10 +17,12 @@ import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.cryptofs.CryptoFileSystemProperties; import org.cryptomator.cryptofs.CryptoFileSystemProperties;
import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags; import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
import org.cryptomator.cryptofs.CryptoFileSystemProvider; import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig;
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker; import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.MasterkeyLoader;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -35,28 +37,29 @@ import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import java.io.IOException; import java.io.IOException;
import java.nio.file.NoSuchFileException; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
@PerVault @PerVault
public class Vault { public class Vault {
private static final Logger LOG = LoggerFactory.getLogger(Vault.class); private static final Logger LOG = LoggerFactory.getLogger(Vault.class);
private static final Path HOME_DIR = Paths.get(SystemUtils.USER_HOME); private static final Path HOME_DIR = Paths.get(SystemUtils.USER_HOME);
private static final int UNLIMITED_FILENAME_LENGTH = Integer.MAX_VALUE;
private final VaultSettings vaultSettings; private final VaultSettings vaultSettings;
private final Provider<Volume> volumeProvider; private final Provider<Volume> volumeProvider;
private final StringBinding defaultMountFlags; private final StringBinding defaultMountFlags;
private final AtomicReference<CryptoFileSystem> cryptoFileSystem; private final AtomicReference<CryptoFileSystem> cryptoFileSystem;
private final ObjectProperty<VaultState> state; private final VaultState state;
private final ObjectProperty<Exception> lastKnownException; private final ObjectProperty<Exception> lastKnownException;
private final VaultStats stats; private final VaultStats stats;
private final StringBinding displayName; private final StringBinding displayName;
@@ -74,7 +77,7 @@ public class Vault {
private volatile Volume volume; private volatile Volume volume;
@Inject @Inject
Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, ObjectProperty<VaultState> state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, VaultStats stats) { Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, VaultStats stats) {
this.vaultSettings = vaultSettings; this.vaultSettings = vaultSettings;
this.volumeProvider = volumeProvider; this.volumeProvider = volumeProvider;
this.defaultMountFlags = defaultMountFlags; this.defaultMountFlags = defaultMountFlags;
@@ -99,45 +102,37 @@ public class Vault {
// Commands // Commands
// ********************************************************************************/ // ********************************************************************************/
private CryptoFileSystem createCryptoFileSystem(CharSequence passphrase) throws NoSuchFileException, IOException, InvalidPassphraseException, CryptoException { private CryptoFileSystem createCryptoFileSystem(MasterkeyLoader keyLoader) throws IOException, MasterkeyLoadingFailedException {
Set<FileSystemFlags> flags = EnumSet.noneOf(FileSystemFlags.class); Set<FileSystemFlags> flags = EnumSet.noneOf(FileSystemFlags.class);
if (vaultSettings.usesReadOnlyMode().get()) { if (vaultSettings.usesReadOnlyMode().get()) {
flags.add(FileSystemFlags.READONLY); flags.add(FileSystemFlags.READONLY);
} else if(vaultSettings.maxCleartextFilenameLength().get() == -1) {
LOG.debug("Determining cleartext filename length limitations...");
var checker = new FileSystemCapabilityChecker();
int shorteningThreshold = getUnverifiedVaultConfig().orElseThrow().allegedShorteningThreshold();
int ciphertextLimit = checker.determineSupportedCiphertextFileNameLength(getPath());
if (ciphertextLimit < shorteningThreshold) {
int cleartextLimit = checker.determineSupportedCleartextFileNameLength(getPath());
vaultSettings.maxCleartextFilenameLength().set(cleartextLimit);
} else {
vaultSettings.maxCleartextFilenameLength().setValue(UNLIMITED_FILENAME_LENGTH);
}
} }
if (vaultSettings.filenameLengthLimit().get() == -1) {
LOG.debug("Determining file name length limitations..."); if (vaultSettings.maxCleartextFilenameLength().get() < UNLIMITED_FILENAME_LENGTH) {
int limit = new FileSystemCapabilityChecker().determineSupportedFileNameLength(getPath()); LOG.warn("Limiting cleartext filename length on this device to {}.", vaultSettings.maxCleartextFilenameLength().get());
vaultSettings.filenameLengthLimit().set(limit);
LOG.info("Storing file name length limit of {}", limit);
} }
assert vaultSettings.filenameLengthLimit().get() > 0;
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() // CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
.withPassphrase(passphrase) // .withKeyLoader(keyLoader) //
.withFlags(flags) // .withFlags(flags) //
.withMasterkeyFilename(MASTERKEY_FILENAME) // .withMaxCleartextNameLength(vaultSettings.maxCleartextFilenameLength().get()) //
.withMaxPathLength(vaultSettings.filenameLengthLimit().get() + Constants.MAX_ADDITIONAL_PATH_LENGTH) //
.withMaxNameLength(vaultSettings.filenameLengthLimit().get()) //
.build(); .build();
return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps); return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
} }
public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, VolumeException, InvalidMountPointException { private void destroyCryptoFileSystem() {
if (cryptoFileSystem.get() == null) { LOG.trace("Trying to close associated CryptoFS...");
CryptoFileSystem fs = createCryptoFileSystem(passphrase);
cryptoFileSystem.set(fs);
volume = volumeProvider.get();
volume.mount(fs, getEffectiveMountFlags());
} else {
throw new IllegalStateException("Already unlocked.");
}
}
public synchronized void lock(boolean forced) throws VolumeException {
if (forced && volume.supportsForcedUnmount()) {
volume.unmountForced();
} else {
volume.unmount();
}
CryptoFileSystem fs = cryptoFileSystem.getAndSet(null); CryptoFileSystem fs = cryptoFileSystem.getAndSet(null);
if (fs != null) { if (fs != null) {
try { try {
@@ -148,24 +143,67 @@ public class Vault {
} }
} }
public void reveal() throws VolumeException { public synchronized void unlock(MasterkeyLoader keyLoader) throws CryptoException, IOException, VolumeException, InvalidMountPointException {
volume.reveal(); if (cryptoFileSystem.get() != null) {
throw new IllegalStateException("Already unlocked.");
}
CryptoFileSystem fs = createCryptoFileSystem(keyLoader);
boolean success = false;
try {
cryptoFileSystem.set(fs);
volume = volumeProvider.get();
volume.mount(fs, getEffectiveMountFlags(), this::lockOnVolumeExit);
success = true;
} finally {
if (!success) {
destroyCryptoFileSystem();
}
}
}
private void lockOnVolumeExit(Throwable t) {
LOG.info("Unmounted vault '{}'", getDisplayName());
destroyCryptoFileSystem();
state.set(VaultState.Value.LOCKED);
if (t != null) {
LOG.warn("Unexpected unmount and lock of vault " + getDisplayName(), t);
}
}
public synchronized void lock(boolean forced) throws VolumeException, LockNotCompletedException {
//initiate unmount
if (forced && volume.supportsForcedUnmount()) {
volume.unmountForced();
} else {
volume.unmount();
}
//wait for lockOnVolumeExit to be executed
try {
boolean locked = state.awaitState(VaultState.Value.LOCKED, 3000, TimeUnit.MILLISECONDS);
if (!locked) {
throw new LockNotCompletedException("Locking of vault " + this.getDisplayName() + " still in progress.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new LockNotCompletedException(e);
}
}
public void reveal(Volume.Revealer vaultRevealer) throws VolumeException {
volume.reveal(vaultRevealer);
} }
// ****************************************************************************** // ******************************************************************************
// Observable Properties // Observable Properties
// ******************************************************************************* // *******************************************************************************
public ObjectProperty<VaultState> stateProperty() { public VaultState stateProperty() {
return state; return state;
} }
public VaultState getState() { public VaultState.Value getState() {
return state.get(); return state.getValue();
}
public void setState(VaultState value) {
state.setValue(value);
} }
public ObjectProperty<Exception> lastKnownExceptionProperty() { public ObjectProperty<Exception> lastKnownExceptionProperty() {
@@ -185,7 +223,7 @@ public class Vault {
} }
public boolean isLocked() { public boolean isLocked() {
return state.get() == VaultState.LOCKED; return state.get() == VaultState.Value.LOCKED;
} }
public BooleanBinding processingProperty() { public BooleanBinding processingProperty() {
@@ -193,7 +231,7 @@ public class Vault {
} }
public boolean isProcessing() { public boolean isProcessing() {
return state.get() == VaultState.PROCESSING; return state.get() == VaultState.Value.PROCESSING;
} }
public BooleanBinding unlockedProperty() { public BooleanBinding unlockedProperty() {
@@ -201,7 +239,7 @@ public class Vault {
} }
public boolean isUnlocked() { public boolean isUnlocked() {
return state.get() == VaultState.UNLOCKED; return state.get() == VaultState.Value.UNLOCKED;
} }
public BooleanBinding missingProperty() { public BooleanBinding missingProperty() {
@@ -209,7 +247,7 @@ public class Vault {
} }
public boolean isMissing() { public boolean isMissing() {
return state.get() == VaultState.MISSING; return state.get() == VaultState.Value.MISSING;
} }
public BooleanBinding needsMigrationProperty() { public BooleanBinding needsMigrationProperty() {
@@ -217,7 +255,7 @@ public class Vault {
} }
public boolean isNeedsMigration() { public boolean isNeedsMigration() {
return state.get() == VaultState.NEEDS_MIGRATION; return state.get() == VaultState.Value.NEEDS_MIGRATION;
} }
public BooleanBinding unknownErrorProperty() { public BooleanBinding unknownErrorProperty() {
@@ -225,7 +263,7 @@ public class Vault {
} }
public boolean isUnknownError() { public boolean isUnknownError() {
return state.get() == VaultState.ERROR; return state.get() == VaultState.Value.ERROR;
} }
public StringBinding displayNameProperty() { public StringBinding displayNameProperty() {
@@ -241,7 +279,7 @@ public class Vault {
} }
public String getAccessPoint() { public String getAccessPoint() {
if (state.get() == VaultState.UNLOCKED) { if (state.getValue() == VaultState.Value.UNLOCKED) {
assert volume != null; assert volume != null;
return volume.getMountPoint().orElse(Path.of("")).toString(); return volume.getMountPoint().orElse(Path.of("")).toString();
} else { } else {
@@ -289,6 +327,16 @@ public class Vault {
return stats; return stats;
} }
public Optional<UnverifiedVaultConfig> getUnverifiedVaultConfig() {
Path configPath = getPath().resolve(org.cryptomator.common.Constants.VAULTCONFIG_FILENAME);
try {
String token = Files.readString(configPath, StandardCharsets.US_ASCII);
return Optional.of(VaultConfig.decode(token));
} catch (IOException e) {
return Optional.empty();
}
}
public Observable[] observables() { public Observable[] observables() {
return new Observable[]{state}; return new Observable[]{state};
} }
@@ -345,8 +393,7 @@ public class Vault {
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (obj instanceof Vault && obj.getClass().equals(this.getClass())) { if (obj instanceof Vault other && obj.getClass().equals(this.getClass())) {
final Vault other = (Vault) obj;
return Objects.equals(this.vaultSettings, other.vaultSettings); return Objects.equals(this.vaultSettings, other.vaultSettings);
} else { } else {
return false; return false;
@@ -26,7 +26,7 @@ public interface VaultComponent {
Builder vaultSettings(VaultSettings vaultSettings); Builder vaultSettings(VaultSettings vaultSettings);
@BindsInstance @BindsInstance
Builder initialVaultState(VaultState vaultState); Builder initialVaultState(VaultState.Value vaultState);
@BindsInstance @BindsInstance
Builder initialErrorCause(@Nullable @Named("lastKnownException") Exception initialErrorCause); Builder initialErrorCause(@Nullable @Named("lastKnownException") Exception initialErrorCause);
@@ -22,10 +22,10 @@ class VaultListChangeListener implements ListChangeListener<Vault> {
public void onChanged(Change<? extends Vault> c) { public void onChanged(Change<? extends Vault> c) {
while (c.next()) { while (c.next()) {
if (c.wasAdded()) { if (c.wasAdded()) {
List<VaultSettings> addedSettings = c.getAddedSubList().stream().map(Vault::getVaultSettings).collect(Collectors.toList()); List<VaultSettings> addedSettings = c.getAddedSubList().stream().map(Vault::getVaultSettings).toList();
vaultSettingsList.addAll(c.getFrom(), addedSettings); vaultSettingsList.addAll(c.getFrom(), addedSettings);
} else if (c.wasRemoved()) { } else if (c.wasRemoved()) {
List<VaultSettings> removedSettings = c.getRemoved().stream().map(Vault::getVaultSettings).collect(Collectors.toList()); List<VaultSettings> removedSettings = c.getRemoved().stream().map(Vault::getVaultSettings).toList();
vaultSettingsList.removeAll(removedSettings); vaultSettingsList.removeAll(removedSettings);
} }
} }
@@ -11,6 +11,7 @@ package org.cryptomator.common.vaults;
import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.cryptofs.CryptoFileSystemProvider; import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptofs.DirStructure;
import org.cryptomator.cryptofs.migration.Migrators; import org.cryptomator.cryptofs.migration.Migrators;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -20,14 +21,16 @@ import javax.inject.Singleton;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
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;
import java.util.Optional; import java.util.Optional;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import java.util.stream.Collectors;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
import static org.cryptomator.common.vaults.VaultState.Value.ERROR;
@Singleton @Singleton
public class VaultListManager { public class VaultListManager {
@@ -52,19 +55,18 @@ public class VaultListManager {
return vaultList; return vaultList;
} }
public Vault add(Path pathToVault) throws NoSuchFileException { public Vault add(Path pathToVault) throws IOException {
Path normalizedPathToVault = pathToVault.normalize().toAbsolutePath(); Path normalizedPathToVault = pathToVault.normalize().toAbsolutePath();
if (!CryptoFileSystemProvider.containsVault(normalizedPathToVault, MASTERKEY_FILENAME)) { if (CryptoFileSystemProvider.checkDirStructureForVault(normalizedPathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) == DirStructure.UNRELATED) {
throw new NoSuchFileException(normalizedPathToVault.toString(), null, "Not a vault directory"); throw new NoSuchFileException(normalizedPathToVault.toString(), null, "Not a vault directory");
} }
Optional<Vault> alreadyExistingVault = get(normalizedPathToVault);
if (alreadyExistingVault.isPresent()) { return get(normalizedPathToVault) //
return alreadyExistingVault.get(); .orElseGet(() -> {
} else { Vault newVault = create(newVaultSettings(normalizedPathToVault));
Vault newVault = create(newVaultSettings(normalizedPathToVault)); vaultList.add(newVault);
vaultList.add(newVault); return newVault;
return newVault; });
}
} }
private VaultSettings newVaultSettings(Path path) { private VaultSettings newVaultSettings(Path path) {
@@ -79,7 +81,7 @@ public class VaultListManager {
} }
private void addAll(Collection<VaultSettings> vaultSettings) { private void addAll(Collection<VaultSettings> vaultSettings) {
Collection<Vault> vaults = vaultSettings.stream().map(this::create).collect(Collectors.toList()); Collection<Vault> vaults = vaultSettings.stream().map(this::create).toList();
vaultList.addAll(vaults); vaultList.addAll(vaults);
} }
@@ -94,43 +96,45 @@ public class VaultListManager {
private Vault create(VaultSettings vaultSettings) { private Vault create(VaultSettings vaultSettings) {
VaultComponent.Builder compBuilder = vaultComponentBuilder.vaultSettings(vaultSettings); VaultComponent.Builder compBuilder = vaultComponentBuilder.vaultSettings(vaultSettings);
try { try {
VaultState vaultState = determineVaultState(vaultSettings.path().get()); VaultState.Value vaultState = determineVaultState(vaultSettings.path().get());
compBuilder.initialVaultState(vaultState); compBuilder.initialVaultState(vaultState);
} catch (IOException e) { } catch (IOException e) {
LOG.warn("Failed to determine vault state for " + vaultSettings.path().get(), e); LOG.warn("Failed to determine vault state for " + vaultSettings.path().get(), e);
compBuilder.initialVaultState(VaultState.ERROR); compBuilder.initialVaultState(ERROR);
compBuilder.initialErrorCause(e); compBuilder.initialErrorCause(e);
} }
return compBuilder.build().vault(); return compBuilder.build().vault();
} }
public static VaultState redetermineVaultState(Vault vault) { public static VaultState.Value redetermineVaultState(Vault vault) {
VaultState previousState = vault.getState(); VaultState state = vault.stateProperty();
VaultState.Value previousState = state.getValue();
return switch (previousState) { return switch (previousState) {
case LOCKED, NEEDS_MIGRATION, MISSING -> { case LOCKED, NEEDS_MIGRATION, MISSING -> {
try { try {
VaultState determinedState = determineVaultState(vault.getPath()); var determinedState = determineVaultState(vault.getPath());
vault.setState(determinedState); state.set(determinedState);
yield determinedState; yield determinedState;
} catch (IOException e) { } catch (IOException e) {
LOG.warn("Failed to determine vault state for " + vault.getPath(), e); LOG.warn("Failed to determine vault state for " + vault.getPath(), e);
vault.setState(VaultState.ERROR); state.set(ERROR);
vault.setLastKnownException(e); vault.setLastKnownException(e);
yield VaultState.ERROR; yield ERROR;
} }
} }
case ERROR, UNLOCKED, PROCESSING -> previousState; case ERROR, UNLOCKED, PROCESSING -> previousState;
}; };
} }
private static VaultState determineVaultState(Path pathToVault) throws IOException { private static VaultState.Value determineVaultState(Path pathToVault) throws IOException {
if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) { if (!Files.exists(pathToVault)) {
return VaultState.MISSING; return VaultState.Value.MISSING;
} else if (Migrators.get().needsMigration(pathToVault, MASTERKEY_FILENAME)) {
return VaultState.NEEDS_MIGRATION;
} else {
return VaultState.LOCKED;
} }
return switch (CryptoFileSystemProvider.checkDirStructureForVault(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME)) {
case VAULT -> VaultState.Value.LOCKED;
case UNRELATED -> VaultState.Value.MISSING;
case MAYBE_LEGACY -> Migrators.get().needsMigration(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) ? VaultState.Value.NEEDS_MIGRATION : VaultState.Value.MISSING;
};
} }
} }
@@ -40,12 +40,6 @@ public class VaultModule {
return new AtomicReference<>(); return new AtomicReference<>();
} }
@Provides
@PerVault
public ObjectProperty<VaultState> provideVaultState(VaultState initialState) {
return new SimpleObjectProperty<>(initialState);
}
@Provides @Provides
@Named("lastKnownException") @Named("lastKnownException")
@PerVault @PerVault
@@ -53,7 +47,6 @@ public class VaultModule {
return new SimpleObjectProperty<>(initialErrorCause); return new SimpleObjectProperty<>(initialErrorCause);
} }
@Provides @Provides
public Volume provideVolume(Settings settings, WebDavVolume webDavVolume, FuseVolume fuseVolume, DokanyVolume dokanyVolume) { public Volume provideVolume(Settings settings, WebDavVolume webDavVolume, FuseVolume fuseVolume, DokanyVolume dokanyVolume) {
VolumeImpl preferredImpl = settings.preferredVolumeImpl().get(); VolumeImpl preferredImpl = settings.preferredVolumeImpl().get();
@@ -1,34 +1,141 @@
package org.cryptomator.common.vaults; package org.cryptomator.common.vaults;
public enum VaultState { import com.google.common.base.Preconditions;
/** import org.slf4j.Logger;
* No vault found at the provided path import org.slf4j.LoggerFactory;
*/
MISSING, import javax.inject.Inject;
import javafx.application.Platform;
import javafx.beans.value.ObservableObjectValue;
import javafx.beans.value.ObservableValueBase;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@PerVault
public class VaultState extends ObservableValueBase<VaultState.Value> implements ObservableObjectValue<VaultState.Value> {
private static final Logger LOG = LoggerFactory.getLogger(VaultState.class);
public enum Value {
/**
* No vault found at the provided path
*/
MISSING,
/**
* Vault requires migration to a newer vault format
*/
NEEDS_MIGRATION,
/**
* Vault ready to be unlocked
*/
LOCKED,
/**
* Vault in transition between two other states
*/
PROCESSING,
/**
* Vault is unlocked
*/
UNLOCKED,
/**
* Unknown state due to preceeding unrecoverable exceptions.
*/
ERROR;
}
private final AtomicReference<Value> value;
private final Lock lock = new ReentrantLock();
private final Condition valueChanged = lock.newCondition();
@Inject
public VaultState(VaultState.Value initialValue) {
this.value = new AtomicReference<>(initialValue);
}
@Override
public Value get() {
return getValue();
}
@Override
public Value getValue() {
return value.get();
}
/** /**
* Vault requires migration to a newer vault format * Transitions from <code>fromState</code> to <code>toState</code>.
*
* @param fromState Previous state
* @param toState New state
* @return <code>true</code> if successful
*/ */
NEEDS_MIGRATION, public boolean transition(Value fromState, Value toState) {
Preconditions.checkArgument(fromState != toState, "fromState must be different than toState");
boolean success = value.compareAndSet(fromState, toState);
if (success) {
fireValueChangedEvent();
} else {
LOG.debug("Failed transiting into state {}: Expected state was not{}.", fromState, toState);
}
return success;
}
public void set(Value newState) {
var oldState = value.getAndSet(newState);
if (oldState != newState) {
fireValueChangedEvent();
}
}
/** /**
* Vault ready to be unlocked * Waits for the specified time, until the desired state is reached.
*
* @param desiredState what state to wait for
* @param time the maximum time to wait
* @param unit the time unit of the {@code time} argument
* @return {@code false} if the waiting time detectably elapsed before reaching {@code desiredState}
* @throws InterruptedException if the current thread is interrupted
*/ */
LOCKED, public boolean awaitState(Value desiredState, long time, TimeUnit unit) throws InterruptedException {
lock.lock();
try {
long remaining = TimeUnit.NANOSECONDS.convert(time, unit);
while (value.get() != desiredState) {
if (remaining <= 0L) {
return false;
}
remaining = valueChanged.awaitNanos(remaining);
}
return true;
} finally {
lock.unlock();
}
}
/** private void signal() {
* Vault in transition between two other states lock.lock();
*/ try {
PROCESSING, valueChanged.signalAll();
} finally {
/** lock.unlock();
* Vault is unlocked }
*/ }
UNLOCKED,
/**
* Unknown state due to preceeding unrecoverable exceptions.
*/
ERROR;
@Override
protected void fireValueChangedEvent() {
signal();
if (Platform.isFxApplicationThread()) {
super.fireValueChangedEvent();
} else {
Platform.runLater(super::fireValueChangedEvent);
}
}
} }
@@ -26,7 +26,7 @@ public class VaultStats {
private static final Logger LOG = LoggerFactory.getLogger(VaultStats.class); private static final Logger LOG = LoggerFactory.getLogger(VaultStats.class);
private final AtomicReference<CryptoFileSystem> fs; private final AtomicReference<CryptoFileSystem> fs;
private final ObjectProperty<VaultState> state; private final VaultState state;
private final ScheduledService<Optional<CryptoFileSystemStats>> updateService; private final ScheduledService<Optional<CryptoFileSystemStats>> updateService;
private final LongProperty bytesPerSecondRead = new SimpleLongProperty(); private final LongProperty bytesPerSecondRead = new SimpleLongProperty();
private final LongProperty bytesPerSecondWritten = new SimpleLongProperty(); private final LongProperty bytesPerSecondWritten = new SimpleLongProperty();
@@ -41,7 +41,7 @@ public class VaultStats {
private final LongProperty filesWritten = new SimpleLongProperty(); private final LongProperty filesWritten = new SimpleLongProperty();
@Inject @Inject
VaultStats(AtomicReference<CryptoFileSystem> fs, ObjectProperty<VaultState> state, ExecutorService executor) { VaultStats(AtomicReference<CryptoFileSystem> fs, VaultState state, ExecutorService executor) {
this.fs = fs; this.fs = fs;
this.state = state; this.state = state;
this.updateService = new UpdateStatsService(); this.updateService = new UpdateStatsService();
@@ -52,13 +52,13 @@ public class VaultStats {
} }
private void vaultStateChanged(@SuppressWarnings("unused") Observable observable) { private void vaultStateChanged(@SuppressWarnings("unused") Observable observable) {
if (VaultState.UNLOCKED.equals(state.get())) { if (VaultState.Value.UNLOCKED == state.get()) {
assert fs.get() != null; assert fs.get() != null;
LOG.debug("start recording stats"); LOG.debug("start recording stats");
updateService.restart(); Platform.runLater(() -> updateService.restart());
} else { } else {
LOG.debug("stop recording stats"); LOG.debug("stop recording stats");
updateService.cancel(); Platform.runLater(() -> updateService.cancel());
} }
} }
@@ -7,6 +7,8 @@ import org.cryptomator.cryptofs.CryptoFileSystem;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletionStage;
import java.util.function.Consumer;
import java.util.stream.Stream; import java.util.stream.Stream;
/** /**
@@ -32,9 +34,17 @@ public interface Volume {
* @param fs * @param fs
* @throws IOException * @throws IOException
*/ */
void mount(CryptoFileSystem fs, String mountFlags) throws IOException, VolumeException, InvalidMountPointException; void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws IOException, VolumeException, InvalidMountPointException;
void reveal() throws VolumeException; /**
* Reveals the mounted volume.
* <p>
* The given {@code revealer} might be used to do it, but not necessarily.
*
* @param revealer An object capable of revealing the location of the mounted vault to view the content (e.g. in the default file browser).
* @throws VolumeException
*/
void reveal(Revealer revealer) throws VolumeException;
void unmount() throws VolumeException; void unmount() throws VolumeException;
@@ -79,4 +89,14 @@ public interface Volume {
} }
/**
* Hides and unifies the different Revealer implementations in the different nio-adapters.
*/
@FunctionalInterface
interface Revealer {
void reveal(Path p) throws VolumeException;
}
} }
@@ -17,6 +17,8 @@ import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class WebDavVolume implements Volume { public class WebDavVolume implements Volume {
@@ -25,21 +27,29 @@ public class WebDavVolume implements Volume {
private final Provider<WebDavServer> serverProvider; private final Provider<WebDavServer> serverProvider;
private final VaultSettings vaultSettings; private final VaultSettings vaultSettings;
private final Settings settings; private final Settings settings;
private final WindowsDriveLetters windowsDriveLetters;
private WebDavServer server; private WebDavServer server;
private WebDavServletController servlet; private WebDavServletController servlet;
private Mounter.Mount mount; private Mounter.Mount mount;
private Path mountPoint; private Consumer<Throwable> onExitAction;
@Inject @Inject
public WebDavVolume(Provider<WebDavServer> serverProvider, VaultSettings vaultSettings, Settings settings) { public WebDavVolume(Provider<WebDavServer> serverProvider, VaultSettings vaultSettings, Settings settings, WindowsDriveLetters windowsDriveLetters) {
this.serverProvider = serverProvider; this.serverProvider = serverProvider;
this.vaultSettings = vaultSettings; this.vaultSettings = vaultSettings;
this.settings = settings; this.settings = settings;
this.windowsDriveLetters = windowsDriveLetters;
} }
@Override @Override
public void mount(CryptoFileSystem fs, String mountFlags) throws VolumeException { public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws VolumeException {
startServlet(fs);
mountServlet();
this.onExitAction = onExitAction;
}
private void startServlet(CryptoFileSystem fs) {
if (server == null) { if (server == null) {
server = serverProvider.get(); server = serverProvider.get();
} }
@@ -50,32 +60,38 @@ public class WebDavVolume implements Volume {
String urlConformMountName = acceptable.negate().collapseFrom(vaultSettings.mountName().get(), '_'); String urlConformMountName = acceptable.negate().collapseFrom(vaultSettings.mountName().get(), '_');
servlet = server.createWebDavServlet(fs.getPath("/"), vaultSettings.getId() + "/" + urlConformMountName); servlet = server.createWebDavServlet(fs.getPath("/"), vaultSettings.getId() + "/" + urlConformMountName);
servlet.start(); servlet.start();
mount();
} }
private void mount() throws VolumeException { private void mountServlet() throws VolumeException {
if (servlet == null) { if (servlet == null) {
throw new IllegalStateException("Mounting requires unlocked WebDAV servlet."); throw new IllegalStateException("Mounting requires unlocked WebDAV servlet.");
} }
//on windows, prevent an automatic drive letter selection in the upstream library. Either we choose already a specifc one or there is no free.
Supplier<String> driveLetterSupplier;
if (System.getProperty("os.name").toLowerCase().contains("windows") && vaultSettings.winDriveLetter().isEmpty().get()) {
driveLetterSupplier = () -> windowsDriveLetters.getAvailableDriveLetter().orElse(null);
} else {
driveLetterSupplier = () -> vaultSettings.winDriveLetter().get();
}
MountParams mountParams = MountParams.create() // MountParams mountParams = MountParams.create() //
.withWindowsDriveLetter(vaultSettings.winDriveLetter().get()) // .withWindowsDriveLetter(driveLetterSupplier.get()) //
.withPreferredGvfsScheme(settings.preferredGvfsScheme().get().getPrefix())// .withPreferredGvfsScheme(settings.preferredGvfsScheme().get().getPrefix())//
.withWebdavHostname(getLocalhostAliasOrNull()) // .withWebdavHostname(getLocalhostAliasOrNull()) //
.build(); .build();
try { try {
this.mount = servlet.mount(mountParams); // might block this thread for a while this.mount = servlet.mount(mountParams); // might block this thread for a while
} catch (Mounter.CommandFailedException e) { } catch (Mounter.CommandFailedException e) {
e.printStackTrace();
throw new VolumeException(e); throw new VolumeException(e);
} }
} }
@Override @Override
public void reveal() throws VolumeException { public void reveal(Revealer revealer) throws VolumeException {
try { try {
mount.reveal(); mount.reveal(revealer::reveal);
} catch (Mounter.CommandFailedException e) { } catch (Exception e) {
e.printStackTrace();
throw new VolumeException(e); throw new VolumeException(e);
} }
} }
@@ -88,6 +104,7 @@ public class WebDavVolume implements Volume {
throw new VolumeException(e); throw new VolumeException(e);
} }
cleanup(); cleanup();
onExitAction.accept(null);
} }
@Override @Override
@@ -98,11 +115,12 @@ public class WebDavVolume implements Volume {
throw new VolumeException(e); throw new VolumeException(e);
} }
cleanup(); cleanup();
onExitAction.accept(null);
} }
@Override @Override
public Optional<Path> getMountPoint() { public Optional<Path> getMountPoint() {
return Optional.ofNullable(mountPoint); //TODO return mount.getMountPoint();
} }
@Override @Override
@@ -8,6 +8,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
@@ -20,14 +21,10 @@ class EnvironmentTest {
private Environment env; private Environment env;
@BeforeAll
static void init() {
System.setProperty("user.home", "/home/testuser");
}
@BeforeEach @BeforeEach
void initEach() { void init() {
env = new Environment(); env = Mockito.spy(new Environment());
Mockito.when(env.getHomeDir()).thenReturn(Path.of("/home/testuser"));
} }
@Test @Test
@@ -35,7 +32,7 @@ class EnvironmentTest {
public void testSettingsPath() { public void testSettingsPath() {
System.setProperty("cryptomator.settingsPath", "~/.config/Cryptomator/settings.json:~/.Cryptomator/settings.json"); System.setProperty("cryptomator.settingsPath", "~/.config/Cryptomator/settings.json:~/.Cryptomator/settings.json");
List<Path> result = env.getSettingsPath().collect(Collectors.toList()); List<Path> result = env.getSettingsPath().toList();
MatcherAssert.assertThat(result, Matchers.hasSize(2)); MatcherAssert.assertThat(result, Matchers.hasSize(2));
MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/.config/Cryptomator/settings.json"), // MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/.config/Cryptomator/settings.json"), //
Paths.get("/home/testuser/.Cryptomator/settings.json"))); Paths.get("/home/testuser/.Cryptomator/settings.json")));
@@ -46,7 +43,7 @@ class EnvironmentTest {
public void testIpcPortPath() { public void testIpcPortPath() {
System.setProperty("cryptomator.ipcPortPath", "~/.config/Cryptomator/ipcPort.bin:~/.Cryptomator/ipcPort.bin"); System.setProperty("cryptomator.ipcPortPath", "~/.config/Cryptomator/ipcPort.bin:~/.Cryptomator/ipcPort.bin");
List<Path> result = env.getIpcPortPath().collect(Collectors.toList()); List<Path> result = env.getIpcPortPath().toList();
MatcherAssert.assertThat(result, Matchers.hasSize(2)); MatcherAssert.assertThat(result, Matchers.hasSize(2));
MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/.config/Cryptomator/ipcPort.bin"), // MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/.config/Cryptomator/ipcPort.bin"), //
Paths.get("/home/testuser/.Cryptomator/ipcPort.bin"))); Paths.get("/home/testuser/.Cryptomator/ipcPort.bin")));
@@ -57,7 +54,7 @@ class EnvironmentTest {
public void testKeychainPath() { public void testKeychainPath() {
System.setProperty("cryptomator.keychainPath", "~/AppData/Roaming/Cryptomator/keychain.json"); System.setProperty("cryptomator.keychainPath", "~/AppData/Roaming/Cryptomator/keychain.json");
List<Path> result = env.getKeychainPath().collect(Collectors.toList()); List<Path> result = env.getKeychainPath().toList();
MatcherAssert.assertThat(result, Matchers.hasSize(1)); MatcherAssert.assertThat(result, Matchers.hasSize(1));
MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/AppData/Roaming/Cryptomator/keychain.json"))); MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/AppData/Roaming/Cryptomator/keychain.json")));
} }
@@ -91,7 +88,7 @@ class EnvironmentTest {
@DisplayName("test.path.property=") @DisplayName("test.path.property=")
public void testEmptyList() { public void testEmptyList() {
System.setProperty("test.path.property", ""); System.setProperty("test.path.property", "");
List<Path> result = env.getPaths("test.path.property").collect(Collectors.toList()); List<Path> result = env.getPaths("test.path.property").toList();
MatcherAssert.assertThat(result, Matchers.hasSize(0)); MatcherAssert.assertThat(result, Matchers.hasSize(0));
} }
@@ -100,7 +97,7 @@ class EnvironmentTest {
@DisplayName("test.path.property=/foo/bar/test") @DisplayName("test.path.property=/foo/bar/test")
public void testSingleAbsolutePath() { public void testSingleAbsolutePath() {
System.setProperty("test.path.property", "/foo/bar/test"); System.setProperty("test.path.property", "/foo/bar/test");
List<Path> result = env.getPaths("test.path.property").collect(Collectors.toList()); List<Path> result = env.getPaths("test.path.property").toList();
MatcherAssert.assertThat(result, Matchers.hasSize(1)); MatcherAssert.assertThat(result, Matchers.hasSize(1));
MatcherAssert.assertThat(result, Matchers.hasItem(Paths.get("/foo/bar/test"))); MatcherAssert.assertThat(result, Matchers.hasItem(Paths.get("/foo/bar/test")));
@@ -110,7 +107,7 @@ class EnvironmentTest {
@DisplayName("test.path.property=~/test") @DisplayName("test.path.property=~/test")
public void testSingleHomeRelativePath() { public void testSingleHomeRelativePath() {
System.setProperty("test.path.property", "~/test"); System.setProperty("test.path.property", "~/test");
List<Path> result = env.getPaths("test.path.property").collect(Collectors.toList()); List<Path> result = env.getPaths("test.path.property").toList();
MatcherAssert.assertThat(result, Matchers.hasSize(1)); MatcherAssert.assertThat(result, Matchers.hasSize(1));
MatcherAssert.assertThat(result, Matchers.hasItem(Paths.get("/home/testuser/test"))); MatcherAssert.assertThat(result, Matchers.hasItem(Paths.get("/home/testuser/test")));
@@ -120,7 +117,7 @@ class EnvironmentTest {
@DisplayName("test.path.property=~/test:~/test2:/foo/bar/test") @DisplayName("test.path.property=~/test:~/test2:/foo/bar/test")
public void testMultiplePaths() { public void testMultiplePaths() {
System.setProperty("test.path.property", "~/test:~/test2:/foo/bar/test"); System.setProperty("test.path.property", "~/test:~/test2:/foo/bar/test");
List<Path> result = env.getPaths("test.path.property").collect(Collectors.toList()); List<Path> result = env.getPaths("test.path.property").toList();
MatcherAssert.assertThat(result, Matchers.hasSize(3)); MatcherAssert.assertThat(result, Matchers.hasSize(3));
MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/test"), // MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/test"), //
@@ -9,10 +9,12 @@ import java.util.Optional;
class LicenseCheckerTest { class LicenseCheckerTest {
private static final String PUBLIC_KEY = "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBgc4HZz+/fBbC7lmEww0AO3NK9wVZ" // private static final String PUBLIC_KEY = """
+ "PDZ0VEnsaUFLEYpTzb90nITtJUcPUbvOsdZIZ1Q8fnbquAYgxXL5UgHMoywAib47" // MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBgc4HZz+/fBbC7lmEww0AO3NK9wVZ\
+ "6MkyyYgPk0BXZq3mq4zImTRNuaU9slj9TVJ3ScT3L1bXwVuPJDzpr5GOFpaj+WwM" // PDZ0VEnsaUFLEYpTzb90nITtJUcPUbvOsdZIZ1Q8fnbquAYgxXL5UgHMoywAib47\
+ "Al8G7CqwoJOsW7Kddns="; 6MkyyYgPk0BXZq3mq4zImTRNuaU9slj9TVJ3ScT3L1bXwVuPJDzpr5GOFpaj+WwM\
Al8G7CqwoJOsW7Kddns=\
""";
private LicenseChecker licenseChecker; private LicenseChecker licenseChecker;
@@ -44,4 +44,9 @@ class MapKeychainAccess implements KeychainAccessProvider {
return true; return true;
} }
@Override
public boolean isLocked() {
return false;
}
} }
@@ -5,26 +5,34 @@
*******************************************************************************/ *******************************************************************************/
package org.cryptomator.common.settings; package org.cryptomator.common.settings;
import org.cryptomator.common.Environment;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito;
import java.io.IOException; import java.io.IOException;
public class SettingsJsonAdapterTest { public class SettingsJsonAdapterTest {
private final SettingsJsonAdapter adapter = new SettingsJsonAdapter(); private final Environment env = Mockito.mock(Environment.class);
private final SettingsJsonAdapter adapter = new SettingsJsonAdapter(env);
@Test @Test
public void testDeserialize() throws IOException { public void testDeserialize() throws IOException {
String vault1Json = "{\"id\": \"1\", \"path\": \"/vault1\", \"mountName\": \"vault1\", \"winDriveLetter\": \"X\"}"; String json = """
String vault2Json = "{\"id\": \"2\", \"path\": \"/vault2\", \"mountName\": \"vault2\", \"winDriveLetter\": \"Y\"}"; {
String json = "{\"directories\": [" + vault1Json + "," + vault2Json + "]," // "directories": [
+ "\"checkForUpdatesEnabled\": true,"// {"id": "1", "path": "/vault1", "mountName": "vault1", "winDriveLetter": "X"},
+ "\"port\": 8080,"// {"id": "2", "path": "/vault2", "mountName": "vault2", "winDriveLetter": "Y"}
+ "\"numTrayNotifications\": 42,"// ],
+ "\"preferredVolumeImpl\": \"FUSE\"}"; "checkForUpdatesEnabled": true,
"port": 8080,
"numTrayNotifications": 42,
"preferredVolumeImpl": "FUSE"
}
""";
Settings settings = adapter.fromJson(json); Settings settings = adapter.fromJson(json);
@@ -5,6 +5,7 @@
*******************************************************************************/ *******************************************************************************/
package org.cryptomator.common.settings; package org.cryptomator.common.settings;
import org.cryptomator.common.Environment;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.Mockito; import org.mockito.Mockito;
@@ -14,8 +15,10 @@ public class SettingsTest {
@Test @Test
public void testAutoSave() { public void testAutoSave() {
Environment env = Mockito.mock(Environment.class);
@SuppressWarnings("unchecked") Consumer<Settings> changeListener = Mockito.mock(Consumer.class); @SuppressWarnings("unchecked") Consumer<Settings> changeListener = Mockito.mock(Consumer.class);
Settings settings = new Settings();
Settings settings = new Settings(env);
settings.setSaveCmd(changeListener); settings.setSaveCmd(changeListener);
VaultSettings vaultSettings = VaultSettings.withRandomId(); VaultSettings vaultSettings = VaultSettings.withRandomId();
Mockito.verify(changeListener, Mockito.times(0)).accept(settings); Mockito.verify(changeListener, Mockito.times(0)).accept(settings);
+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.11</version> <version>1.6.0</version>
</parent> </parent>
<artifactId>launcher</artifactId> <artifactId>launcher</artifactId>
<name>Cryptomator Launcher</name> <name>Cryptomator Launcher</name>
@@ -41,7 +41,7 @@ class FileOpenRequestHandler {
} }
private void openFiles(OpenFilesEvent evt) { private void openFiles(OpenFilesEvent evt) {
Collection<Path> pathsToOpen = evt.getFiles().stream().map(File::toPath).collect(Collectors.toList()); Collection<Path> pathsToOpen = evt.getFiles().stream().map(File::toPath).toList();
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen); AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
tryToEnqueueFileOpenRequest(launchEvent); tryToEnqueueFileOpenRequest(launchEvent);
} }
@@ -59,7 +59,7 @@ class FileOpenRequestHandler {
LOG.trace("Argument not a valid path: {}", str); LOG.trace("Argument not a valid path: {}", str);
return null; return null;
} }
}).filter(Objects::nonNull).collect(Collectors.toList()); }).filter(Objects::nonNull).toList();
if (!pathsToOpen.isEmpty()) { if (!pathsToOpen.isEmpty()) {
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen); AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
tryToEnqueueFileOpenRequest(launchEvent); tryToEnqueueFileOpenRequest(launchEvent);
@@ -47,8 +47,8 @@ public class LoggerModule {
@Singleton @Singleton
static LoggerContext provideLoggerContext() { static LoggerContext provideLoggerContext() {
ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory(); ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory();
if (loggerFactory instanceof LoggerContext) { if (loggerFactory instanceof LoggerContext context) {
return (LoggerContext) loggerFactory; return context;
} else { } else {
throw new IllegalStateException("SLF4J not bound to Logback."); throw new IllegalStateException("SLF4J not bound to Logback.");
} }
+23 -36
View File
@@ -3,7 +3,7 @@
<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.11</version> <version>1.6.0</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>Cryptomator</name> <name>Cryptomator</name>
@@ -22,41 +22,35 @@
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.jdk.version>16</project.jdk.version>
<!-- cryptomator dependencies --> <!-- cryptomator dependencies -->
<cryptomator.cryptofs.version>1.9.13</cryptomator.cryptofs.version> <cryptomator.cryptofs.version>2.0.0-rc2</cryptomator.cryptofs.version>
<cryptomator.integrations.version>0.1.6</cryptomator.integrations.version> <cryptomator.integrations.version>1.0.0-beta2</cryptomator.integrations.version>
<cryptomator.integrations.win.version>0.2.1</cryptomator.integrations.win.version> <cryptomator.integrations.win.version>1.0.0-beta2</cryptomator.integrations.win.version>
<cryptomator.integrations.mac.version>0.1.0-beta3</cryptomator.integrations.mac.version> <cryptomator.integrations.mac.version>1.0.0-beta2</cryptomator.integrations.mac.version>
<cryptomator.integrations.linux.version>0.1.0-beta2</cryptomator.integrations.linux.version> <cryptomator.integrations.linux.version>1.0.0-beta1</cryptomator.integrations.linux.version>
<cryptomator.fuse.version>1.2.6</cryptomator.fuse.version> <cryptomator.fuse.version>1.3.1</cryptomator.fuse.version>
<cryptomator.dokany.version>1.2.1</cryptomator.dokany.version> <cryptomator.dokany.version>1.3.1</cryptomator.dokany.version>
<cryptomator.webdav.version>1.0.14</cryptomator.webdav.version> <cryptomator.webdav.version>1.2.0</cryptomator.webdav.version>
<!-- 3rd party dependencies --> <!-- 3rd party dependencies -->
<javafx.version>15</javafx.version> <javafx.version>16</javafx.version>
<commons-lang3.version>3.11</commons-lang3.version> <commons-lang3.version>3.11</commons-lang3.version>
<jwt.version>3.11.0</jwt.version> <jwt.version>3.15.0</jwt.version>
<easybind.version>2.1.0</easybind.version> <easybind.version>2.1.0</easybind.version>
<guava.version>30.0-jre</guava.version> <guava.version>30.1.1-jre</guava.version>
<dagger.version>2.29.1</dagger.version> <dagger.version>2.35.1</dagger.version>
<gson.version>2.8.6</gson.version> <gson.version>2.8.6</gson.version>
<slf4j.version>1.7.30</slf4j.version> <slf4j.version>1.7.30</slf4j.version>
<logback.version>1.2.3</logback.version> <logback.version>1.2.3</logback.version>
<!-- test dependencies --> <!-- test dependencies -->
<junit.jupiter.version>5.7.0</junit.jupiter.version> <junit.jupiter.version>5.7.1</junit.jupiter.version>
<mockito.version>3.6.0</mockito.version> <mockito.version>3.9.0</mockito.version>
<hamcrest.version>2.2</hamcrest.version> <hamcrest.version>2.2</hamcrest.version>
</properties> </properties>
<repositories>
<repository>
<id>jcenter</id>
<url>https://jcenter.bintray.com</url>
</repository>
</repositories>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<!-- modules --> <!-- modules -->
@@ -82,6 +76,12 @@
<artifactId>cryptofs</artifactId> <artifactId>cryptofs</artifactId>
<version>${cryptomator.cryptofs.version}</version> <version>${cryptomator.cryptofs.version}</version>
</dependency> </dependency>
<!--TODO: only temporary workaround until 1.6.0-beta -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>cryptolib</artifactId>
<version>2.0.0-rc1</version>
</dependency>
<dependency> <dependency>
<groupId>org.cryptomator</groupId> <groupId>org.cryptomator</groupId>
<artifactId>fuse-nio-adapter</artifactId> <artifactId>fuse-nio-adapter</artifactId>
@@ -175,13 +175,6 @@
<artifactId>java-jwt</artifactId> <artifactId>java-jwt</artifactId>
<version>${jwt.version}</version> <version>${jwt.version}</version>
</dependency> </dependency>
<!-- fixes CVE-2020-25649, can be removed once https://github.com/auth0/java-jwt/pull/463 is closed and released -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.5.1</version>
</dependency>
<!-- EasyBind --> <!-- EasyBind -->
<dependency> <dependency>
@@ -231,12 +224,6 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- TODO: temporary fix for XXE attack, can be removed once java-jwt is updated -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.5.1</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>
@@ -444,7 +431,7 @@
<plugin> <plugin>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<configuration> <configuration>
<release>14</release> <release>${project.jdk.version}</release>
<annotationProcessorPaths> <annotationProcessorPaths>
<path> <path>
<groupId>com.google.dagger</groupId> <groupId>com.google.dagger</groupId>
-5
View File
@@ -1,11 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- This file lists false positives found by org.owasp:dependency-check-maven build plugin --> <!-- This file lists false positives found by org.owasp:dependency-check-maven build plugin -->
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.2.xsd"> <suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.2.xsd">
<suppress>
<notes><![CDATA[ Upstream fix backported from 2.11.0 to 2.10.5.1, see https://github.com/FasterXML/jackson-databind/issues/2589#issuecomment-714833837. ]]></notes>
<gav>com.fasterxml.jackson.core:jackson-databind:2.10.5.1</gav>
<cve>CVE-2020-25649</cve>
</suppress>
<suppress> <suppress>
<notes><![CDATA[ Suppress known vulnerabilities in FUSE libraries for fuse-nio-adapter. For more info, see suppression.xml of https://github.com/cryptomator/fuse-nio-adapter ]]></notes> <notes><![CDATA[ Suppress known vulnerabilities in FUSE libraries for fuse-nio-adapter. For more info, see suppression.xml of https://github.com/cryptomator/fuse-nio-adapter ]]></notes>
<gav regex="true">^org\.cryptomator:fuse-nio-adapter:.*$</gav> <gav regex="true">^org\.cryptomator:fuse-nio-adapter:.*$</gav>
+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.11</version> <version>1.6.0</version>
</parent> </parent>
<artifactId>ui</artifactId> <artifactId>ui</artifactId>
<name>Cryptomator GUI</name> <name>Cryptomator GUI</name>
@@ -6,7 +6,7 @@ import dagger.Provides;
import dagger.multibindings.IntoMap; import dagger.multibindings.IntoMap;
import org.cryptomator.common.vaults.Vault; 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;
@@ -43,8 +43,8 @@ public abstract class AddVaultModule {
@Provides @Provides
@AddVaultWizardWindow @AddVaultWizardWindow
@AddVaultWizardScoped @AddVaultWizardScoped
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle); return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
} }
@Provides @Provides
@@ -91,50 +91,50 @@ public abstract class AddVaultModule {
@Provides @Provides
@FxmlScene(FxmlFile.ADDVAULT_WELCOME) @FxmlScene(FxmlFile.ADDVAULT_WELCOME)
@AddVaultWizardScoped @AddVaultWizardScoped
static Scene provideWelcomeScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideWelcomeScene(@AddVaultWizardWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_WELCOME.getRessourcePathString()); return fxmlLoaders.createScene(FxmlFile.ADDVAULT_WELCOME);
} }
@Provides @Provides
@FxmlScene(FxmlFile.ADDVAULT_EXISTING) @FxmlScene(FxmlFile.ADDVAULT_EXISTING)
@AddVaultWizardScoped @AddVaultWizardScoped
static Scene provideChooseExistingVaultScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideChooseExistingVaultScene(@AddVaultWizardWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_EXISTING.getRessourcePathString()); return fxmlLoaders.createScene(FxmlFile.ADDVAULT_EXISTING);
} }
@Provides @Provides
@FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME)
@AddVaultWizardScoped @AddVaultWizardScoped
static Scene provideCreateNewVaultNameScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideCreateNewVaultNameScene(@AddVaultWizardWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_NAME.getRessourcePathString()); return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_NAME);
} }
@Provides @Provides
@FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION)
@AddVaultWizardScoped @AddVaultWizardScoped
static Scene provideCreateNewVaultLocationScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideCreateNewVaultLocationScene(@AddVaultWizardWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_LOCATION.getRessourcePathString()); return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_LOCATION);
} }
@Provides @Provides
@FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD)
@AddVaultWizardScoped @AddVaultWizardScoped
static Scene provideCreateNewVaultPasswordScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideCreateNewVaultPasswordScene(@AddVaultWizardWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_PASSWORD.getRessourcePathString()); return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_PASSWORD);
} }
@Provides @Provides
@FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY)
@AddVaultWizardScoped @AddVaultWizardScoped
static Scene provideCreateNewVaultRecoveryKeyScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideCreateNewVaultRecoveryKeyScene(@AddVaultWizardWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY.getRessourcePathString()); return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY);
} }
@Provides @Provides
@FxmlScene(FxmlFile.ADDVAULT_SUCCESS) @FxmlScene(FxmlFile.ADDVAULT_SUCCESS)
@AddVaultWizardScoped @AddVaultWizardScoped
static Scene provideCreateNewVaultSuccessScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideCreateNewVaultSuccessScene(@AddVaultWizardWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_SUCCESS.getRessourcePathString()); return fxmlLoaders.createScene(FxmlFile.ADDVAULT_SUCCESS);
} }
// ------------------ // ------------------
@@ -81,7 +81,7 @@ public class ChooseExistingVaultController implements FxController {
Vault newVault = vaultListManager.add(vaultPath.get()); Vault newVault = vaultListManager.add(vaultPath.get());
vault.set(newVault); vault.set(newVault);
window.setScene(successScene.get()); window.setScene(successScene.get());
} catch (NoSuchFileException e) { } catch (IOException e) {
LOG.error("Failed to open existing vault.", e); LOG.error("Failed to open existing vault.", e);
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene(); errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
} }
@@ -1,10 +1,10 @@
package org.cryptomator.ui.addvaultwizard; package org.cryptomator.ui.addvaultwizard;
import dagger.Lazy; import dagger.Lazy;
import org.cryptomator.ui.common.ErrorComponent;
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.controls.FontAwesome5IconView;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -15,21 +15,21 @@ import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
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.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton; 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.stage.DirectoryChooser; import javafx.stage.DirectoryChooser;
import javafx.stage.Stage; import javafx.stage.Stage;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ResourceBundle; import java.util.ResourceBundle;
@@ -43,55 +43,74 @@ public class CreateNewVaultLocationController implements FxController {
private final Stage window; private final Stage window;
private final Lazy<Scene> chooseNameScene; private final Lazy<Scene> chooseNameScene;
private final Lazy<Scene> choosePasswordScene; private final Lazy<Scene> choosePasswordScene;
private final ErrorComponent.Builder errorComponent;
private final LocationPresets locationPresets; private final LocationPresets locationPresets;
private final ObjectProperty<Path> vaultPath; private final ObjectProperty<Path> vaultPath;
private final StringProperty vaultName; private final StringProperty vaultName;
private final ResourceBundle resourceBundle; private final ResourceBundle resourceBundle;
private final BooleanBinding validVaultPath; private final BooleanBinding validVaultPath;
private final BooleanProperty usePresetPath; private final BooleanProperty usePresetPath;
private final StringProperty warningText; private final StringProperty statusText;
private final ObjectProperty<Node> statusGraphic;
private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH; private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH;
//FXML
public ToggleGroup predefinedLocationToggler; public ToggleGroup predefinedLocationToggler;
public RadioButton iclouddriveRadioButton; public RadioButton iclouddriveRadioButton;
public RadioButton dropboxRadioButton; public RadioButton dropboxRadioButton;
public RadioButton gdriveRadioButton; public RadioButton gdriveRadioButton;
public RadioButton onedriveRadioButton; public RadioButton onedriveRadioButton;
public RadioButton megaRadioButton;
public RadioButton pcloudRadioButton;
public RadioButton customRadioButton; public RadioButton customRadioButton;
public Label vaultPathStatus;
public FontAwesome5IconView goodLocation;
public FontAwesome5IconView badLocation;
@Inject @Inject
CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, ErrorComponent.Builder errorComponent, LocationPresets locationPresets, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) { CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, LocationPresets locationPresets, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) {
this.window = window; this.window = window;
this.chooseNameScene = chooseNameScene; this.chooseNameScene = chooseNameScene;
this.choosePasswordScene = choosePasswordScene; this.choosePasswordScene = choosePasswordScene;
this.errorComponent = errorComponent;
this.locationPresets = locationPresets; this.locationPresets = locationPresets;
this.vaultPath = vaultPath; this.vaultPath = vaultPath;
this.vaultName = vaultName; this.vaultName = vaultName;
this.resourceBundle = resourceBundle; this.resourceBundle = resourceBundle;
this.validVaultPath = Bindings.createBooleanBinding(this::isValidVaultPath, vaultPath); this.validVaultPath = Bindings.createBooleanBinding(this::validateVaultPathAndSetStatus, this.vaultPath);
this.usePresetPath = new SimpleBooleanProperty(); this.usePresetPath = new SimpleBooleanProperty();
this.warningText = new SimpleStringProperty(); this.statusText = new SimpleStringProperty();
this.statusGraphic = new SimpleObjectProperty<>();
} }
private boolean isValidVaultPath() { private boolean validateVaultPathAndSetStatus() {
return vaultPath.get() != null && Files.notExists(vaultPath.get()); final Path p = vaultPath.get();
if (p == null) {
statusText.set("Error: Path is NULL.");
statusGraphic.set(badLocation);
return false;
} else if (!Files.exists(p.getParent())) {
statusText.set(resourceBundle.getString("addvaultwizard.new.locationDoesNotExist"));
statusGraphic.set(badLocation);
return false;
} else if (!Files.isWritable(p.getParent())) {
statusText.set(resourceBundle.getString("addvaultwizard.new.locationIsNotWritable"));
statusGraphic.set(badLocation);
return false;
} else if (!Files.notExists(p)) {
statusText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
statusGraphic.set(badLocation);
return false;
} else {
statusText.set(resourceBundle.getString("addvaultwizard.new.locationIsOk"));
statusGraphic.set(goodLocation);
return true;
}
} }
@FXML @FXML
public void initialize() { public void initialize() {
predefinedLocationToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation); predefinedLocationToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation);
usePresetPath.bind(predefinedLocationToggler.selectedToggleProperty().isNotEqualTo(customRadioButton)); usePresetPath.bind(predefinedLocationToggler.selectedToggleProperty().isNotEqualTo(customRadioButton));
vaultPath.addListener(this::vaultPathDidChange);
}
private void vaultPathDidChange(@SuppressWarnings("unused") ObservableValue<? extends Path> observable, @SuppressWarnings("unused") Path oldValue, Path newValue) {
if (!Files.notExists(newValue)) {
warningText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
} else {
warningText.set(null);
}
} }
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) {
@@ -103,6 +122,10 @@ public class CreateNewVaultLocationController implements FxController {
vaultPath.set(locationPresets.getGdriveLocation().resolve(vaultName.get())); vaultPath.set(locationPresets.getGdriveLocation().resolve(vaultName.get()));
} else if (onedriveRadioButton.equals(newValue)) { } else if (onedriveRadioButton.equals(newValue)) {
vaultPath.set(locationPresets.getOnedriveLocation().resolve(vaultName.get())); vaultPath.set(locationPresets.getOnedriveLocation().resolve(vaultName.get()));
} else if (megaRadioButton.equals(newValue)) {
vaultPath.set(locationPresets.getMegaLocation().resolve(vaultName.get()));
} else if (pcloudRadioButton.equals(newValue)) {
vaultPath.set(locationPresets.getPcloudLocation().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()));
} }
@@ -115,21 +138,10 @@ public class CreateNewVaultLocationController implements FxController {
@FXML @FXML
public void next() { public void next() {
try { if (validateVaultPathAndSetStatus()) {
// check if we have write access AND the vaultPath doesn't already exist:
assert Files.isDirectory(vaultPath.get().getParent());
Path createdDir = Files.createDirectory(vaultPath.get());
Files.delete(createdDir); // assert: dir exists and is empty
window.setScene(choosePasswordScene.get()); window.setScene(choosePasswordScene.get());
} catch (FileAlreadyExistsException e) { } else {
LOG.warn("Can not use already existing vault path {}", vaultPath.get()); validVaultPath.invalidate();
warningText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
} catch (NoSuchFileException e) {
LOG.warn("At least one path component does not exist of path {}", vaultPath.get());
warningText.set(resourceBundle.getString("addvaultwizard.new.locationDoesNotExist"));
} catch (IOException e) {
LOG.error("Failed to create and delete directory at chosen vault path.", e);
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
} }
} }
@@ -179,19 +191,27 @@ public class CreateNewVaultLocationController implements FxController {
return usePresetPath.get(); return usePresetPath.get();
} }
public StringProperty warningTextProperty() { public BooleanBinding anyRadioButtonSelectedProperty() {
return warningText; return predefinedLocationToggler.selectedToggleProperty().isNotNull();
} }
public String getWarningText() { public boolean isAnyRadioButtonSelected() {
return warningText.get(); return anyRadioButtonSelectedProperty().get();
} }
public BooleanBinding showWarningProperty() { public StringProperty statusTextProperty() {
return warningText.isNotEmpty(); return statusText;
} }
public boolean isShowWarning() { public String getStatusText() {
return showWarningProperty().get(); return statusText.get();
}
public ObjectProperty<Node> statusGraphicProperty() {
return statusGraphic;
}
public Node getStatusGraphic() {
return statusGraphic.get();
} }
} }
@@ -5,11 +5,17 @@ import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.cryptofs.CryptoFileSystemProperties; import org.cryptomator.cryptofs.CryptoFileSystemProperties;
import org.cryptomator.cryptofs.CryptoFileSystemProvider; import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptofs.VaultCipherCombo;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.ErrorComponent;
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.Tasks;
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingStrategy;
import org.cryptomator.ui.recoverykey.RecoveryKeyFactory; import org.cryptomator.ui.recoverykey.RecoveryKeyFactory;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -31,13 +37,14 @@ import javafx.scene.control.ToggleGroup;
import javafx.stage.Stage; import javafx.stage.Stage;
import java.io.IOException; import java.io.IOException;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.channels.WritableByteChannel; import java.nio.channels.WritableByteChannel;
import java.nio.file.FileSystem; import java.nio.file.FileSystem;
import java.nio.file.Files; 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.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
import java.util.Collections; import java.security.SecureRandom;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@@ -48,6 +55,7 @@ import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
public class CreateNewVaultPasswordController implements FxController { public class CreateNewVaultPasswordController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultPasswordController.class); private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultPasswordController.class);
private static final URI DEFAULT_KEY_ID = URI.create(MasterkeyFileLoadingStrategy.SCHEME + ":" + MASTERKEY_FILENAME); // TODO better place?
private final Stage window; private final Stage window;
private final Lazy<Scene> chooseLocationScene; private final Lazy<Scene> chooseLocationScene;
@@ -64,6 +72,8 @@ public class CreateNewVaultPasswordController implements FxController {
private final ResourceBundle resourceBundle; private final ResourceBundle resourceBundle;
private final ObjectProperty<CharSequence> password; private final ObjectProperty<CharSequence> password;
private final ReadmeGenerator readmeGenerator; private final ReadmeGenerator readmeGenerator;
private final SecureRandom csprng;
private final MasterkeyFileAccess masterkeyFileAccess;
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;
@@ -73,7 +83,7 @@ public class CreateNewVaultPasswordController implements FxController {
public Toggle skipRecoveryKey; public Toggle skipRecoveryKey;
@Inject @Inject
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, ErrorComponent.Builder errorComponent, 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) { 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, ErrorComponent.Builder errorComponent, 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, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) {
this.window = window; this.window = window;
this.chooseLocationScene = chooseLocationScene; this.chooseLocationScene = chooseLocationScene;
this.recoveryKeyScene = recoveryKeyScene; this.recoveryKeyScene = recoveryKeyScene;
@@ -89,6 +99,8 @@ public class CreateNewVaultPasswordController implements FxController {
this.resourceBundle = resourceBundle; this.resourceBundle = resourceBundle;
this.password = password; this.password = password;
this.readmeGenerator = readmeGenerator; this.readmeGenerator = readmeGenerator;
this.csprng = csprng;
this.masterkeyFileAccess = masterkeyFileAccess;
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);
@@ -161,23 +173,34 @@ public class CreateNewVaultPasswordController implements FxController {
} }
private void initializeVault(Path path, CharSequence passphrase) throws IOException { private void initializeVault(Path path, CharSequence passphrase) throws IOException {
CryptoFileSystemProvider.initialize(path, MASTERKEY_FILENAME, passphrase); // 1. write masterkey:
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() // Path masterkeyFilePath = path.resolve(MASTERKEY_FILENAME);
.withPassphrase(passphrase) // try (Masterkey masterkey = Masterkey.generate(csprng)) {
.withFlags(Collections.emptySet()) // masterkeyFileAccess.persist(masterkey, masterkeyFilePath, passphrase);
.withMasterkeyFilename(MASTERKEY_FILENAME) //
.build();
String vaultReadmeFileName = resourceBundle.getString("addvault.new.readme.accessLocation.fileName"); // 2. initialize vault:
try (FileSystem fs = CryptoFileSystemProvider.newFileSystem(path, fsProps); // try {
WritableByteChannel ch = Files.newByteChannel(fs.getPath("/", vaultReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { MasterkeyLoader loader = ignored -> masterkey.clone();
ch.write(US_ASCII.encode(readmeGenerator.createVaultAccessLocationReadmeRtf())); CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(VaultCipherCombo.SIV_CTRMAC).withKeyLoader(loader).build();
CryptoFileSystemProvider.initialize(path, fsProps, DEFAULT_KEY_ID);
// 3. write vault-internal readme file:
String vaultReadmeFileName = resourceBundle.getString("addvault.new.readme.accessLocation.fileName");
try (FileSystem fs = CryptoFileSystemProvider.newFileSystem(path, fsProps); //
WritableByteChannel ch = Files.newByteChannel(fs.getPath("/", vaultReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
ch.write(US_ASCII.encode(readmeGenerator.createVaultAccessLocationReadmeRtf()));
}
} catch (CryptoException e) {
throw new IOException("Failed initialize vault.", e);
}
} }
// 4. write vault-external readme file:
String storagePathReadmeFileName = resourceBundle.getString("addvault.new.readme.storageLocation.fileName"); String storagePathReadmeFileName = resourceBundle.getString("addvault.new.readme.storageLocation.fileName");
try (WritableByteChannel ch = Files.newByteChannel(path.resolve(storagePathReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { try (WritableByteChannel ch = Files.newByteChannel(path.resolve(storagePathReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
ch.write(US_ASCII.encode(readmeGenerator.createVaultStorageLocationReadmeRtf())); ch.write(US_ASCII.encode(readmeGenerator.createVaultStorageLocationReadmeRtf()));
} }
LOG.info("Created vault at {}", path); LOG.info("Created vault at {}", path);
} }
@@ -185,7 +208,7 @@ public class CreateNewVaultPasswordController implements FxController {
try { try {
Vault newVault = vaultListManager.add(pathToVault); Vault newVault = vaultListManager.add(pathToVault);
vaultProperty.set(newVault); vaultProperty.set(newVault);
} catch (NoSuchFileException e) { } catch (IOException e) {
throw new UncheckedIOException(e); throw new UncheckedIOException(e);
} }
} }
@@ -16,15 +16,21 @@ public class LocationPresets {
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 static final String[] ONEDRIVE_LOCATIONS = {"~/OneDrive"};
private static final String[] MEGA_LOCATIONS = {"~/MEGA"};
private static final String[] PCLOUD_LOCATIONS = {"~/pCloudDrive"};
private final ReadOnlyObjectProperty<Path> iclouddriveLocation; 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 ReadOnlyObjectProperty<Path> onedriveLocation;
private final ReadOnlyObjectProperty<Path> megaLocation;
private final ReadOnlyObjectProperty<Path> pcloudLocation;
private final BooleanBinding foundIclouddrive; private final BooleanBinding foundIclouddrive;
private final BooleanBinding foundDropbox; private final BooleanBinding foundDropbox;
private final BooleanBinding foundGdrive; private final BooleanBinding foundGdrive;
private final BooleanBinding foundOnedrive; private final BooleanBinding foundOnedrive;
private final BooleanBinding foundMega;
private final BooleanBinding foundPcloud;
@Inject @Inject
public LocationPresets() { public LocationPresets() {
@@ -32,10 +38,14 @@ public class LocationPresets {
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.onedriveLocation = new SimpleObjectProperty<>(existingWritablePath(ONEDRIVE_LOCATIONS));
this.megaLocation = new SimpleObjectProperty<>(existingWritablePath(MEGA_LOCATIONS));
this.pcloudLocation = new SimpleObjectProperty<>(existingWritablePath(PCLOUD_LOCATIONS));
this.foundIclouddrive = iclouddriveLocation.isNotNull(); this.foundIclouddrive = iclouddriveLocation.isNotNull();
this.foundDropbox = dropboxLocation.isNotNull(); this.foundDropbox = dropboxLocation.isNotNull();
this.foundGdrive = gdriveLocation.isNotNull(); this.foundGdrive = gdriveLocation.isNotNull();
this.foundOnedrive = onedriveLocation.isNotNull(); this.foundOnedrive = onedriveLocation.isNotNull();
this.foundMega = megaLocation.isNotNull();
this.foundPcloud = pcloudLocation.isNotNull();
} }
private static Path existingWritablePath(String... candidates) { private static Path existingWritablePath(String... candidates) {
@@ -122,4 +132,36 @@ public class LocationPresets {
return foundOnedrive.get(); return foundOnedrive.get();
} }
public ReadOnlyObjectProperty<Path> megaLocationProperty() {
return megaLocation;
}
public Path getMegaLocation() {
return megaLocation.get();
}
public BooleanBinding foundMegaProperty() {
return foundMega;
}
public boolean isFoundMega() {
return foundMega.get();
}
public ReadOnlyObjectProperty<Path> pcloudLocationProperty() {
return pcloudLocation;
}
public Path getPcloudLocation() {
return pcloudLocation.get();
}
public BooleanBinding foundPcloudProperty() {
return foundPcloud;
}
public boolean isFoundPcloud() {
return foundPcloud.get();
}
} }
@@ -2,8 +2,10 @@ package org.cryptomator.ui.changepassword;
import org.cryptomator.common.keychain.KeychainManager; import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.CryptoFileSystemProvider; import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.integrations.keychain.KeychainAccessException; import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.common.Animations; import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.ErrorComponent;
@@ -23,8 +25,13 @@ import javafx.scene.control.CheckBox;
import javafx.stage.Stage; import javafx.stage.Stage;
import java.io.IOException; import java.io.IOException;
import java.nio.CharBuffer; import java.nio.CharBuffer;
import java.util.Optional; import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.security.SecureRandom;
import static org.cryptomator.common.Constants.MASTERKEY_BACKUP_SUFFIX;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
@ChangePasswordScoped @ChangePasswordScoped
@@ -37,18 +44,22 @@ public class ChangePasswordController implements FxController {
private final ObjectProperty<CharSequence> newPassword; private final ObjectProperty<CharSequence> newPassword;
private final ErrorComponent.Builder errorComponent; private final ErrorComponent.Builder errorComponent;
private final KeychainManager keychain; private final KeychainManager keychain;
private final SecureRandom csprng;
private final MasterkeyFileAccess masterkeyFileAccess;
public NiceSecurePasswordField oldPasswordField; public NiceSecurePasswordField oldPasswordField;
public CheckBox finalConfirmationCheckbox; public CheckBox finalConfirmationCheckbox;
public Button finishButton; public Button finishButton;
@Inject @Inject
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty<CharSequence> newPassword, ErrorComponent.Builder errorComponent, KeychainManager keychain) { public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty<CharSequence> newPassword, ErrorComponent.Builder errorComponent, KeychainManager keychain, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) {
this.window = window; this.window = window;
this.vault = vault; this.vault = vault;
this.newPassword = newPassword; this.newPassword = newPassword;
this.errorComponent = errorComponent; this.errorComponent = errorComponent;
this.keychain = keychain; this.keychain = keychain;
this.csprng = csprng;
this.masterkeyFileAccess = masterkeyFileAccess;
} }
@FXML @FXML
@@ -67,17 +78,26 @@ public class ChangePasswordController implements FxController {
@FXML @FXML
public void finish() { public void finish() {
try { try {
CryptoFileSystemProvider.changePassphrase(vault.getPath(), MASTERKEY_FILENAME, oldPasswordField.getCharacters(), newPassword.get()); //String normalizedOldPassphrase = Normalizer.normalize(oldPasswordField.getCharacters(), Normalizer.Form.NFC);
//String normalizedNewPassphrase = Normalizer.normalize(newPassword.get(), Normalizer.Form.NFC);
CharSequence oldPassphrase = oldPasswordField.getCharacters(); // TODO verify: is this already NFC-normalized?
CharSequence newPassphrase = newPassword.get(); // TODO verify: is this already NFC-normalized?
Path masterkeyPath = vault.getPath().resolve(MASTERKEY_FILENAME);
byte[] oldMasterkeyBytes = Files.readAllBytes(masterkeyPath);
byte[] newMasterkeyBytes = masterkeyFileAccess.changePassphrase(oldMasterkeyBytes, oldPassphrase, newPassphrase);
Path backupKeyPath = vault.getPath().resolve(MASTERKEY_FILENAME + MasterkeyBackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + MASTERKEY_BACKUP_SUFFIX);
Files.move(masterkeyPath, backupKeyPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
Files.write(masterkeyPath, newMasterkeyBytes, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
LOG.info("Successfully changed password for {}", vault.getDisplayName()); LOG.info("Successfully changed password for {}", vault.getDisplayName());
window.close(); window.close();
updatePasswordInSystemkeychain(); updatePasswordInSystemkeychain();
} catch (IOException e) {
LOG.error("IO error occured during password change. Unable to perform operation.", e);
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
} catch (InvalidPassphraseException e) { } catch (InvalidPassphraseException e) {
Animations.createShakeWindowAnimation(window).play(); Animations.createShakeWindowAnimation(window).play();
oldPasswordField.selectAll(); oldPasswordField.selectAll();
oldPasswordField.requestFocus(); oldPasswordField.requestFocus();
} catch (IOException | CryptoException e) {
LOG.error("Password change failed. Unable to perform operation.", e);
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
} }
} }
@@ -5,7 +5,7 @@ import dagger.Module;
import dagger.Provides; import dagger.Provides;
import dagger.multibindings.IntoMap; import dagger.multibindings.IntoMap;
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;
@@ -37,8 +37,8 @@ abstract class ChangePasswordModule {
@Provides @Provides
@ChangePasswordWindow @ChangePasswordWindow
@ChangePasswordScoped @ChangePasswordScoped
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle); return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
} }
@Provides @Provides
@@ -56,8 +56,8 @@ abstract class ChangePasswordModule {
@Provides @Provides
@FxmlScene(FxmlFile.CHANGEPASSWORD) @FxmlScene(FxmlFile.CHANGEPASSWORD)
@ChangePasswordScoped @ChangePasswordScoped
static Scene provideUnlockScene(@ChangePasswordWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideUnlockScene(@ChangePasswordWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/changepassword.fxml"); return fxmlLoaders.createScene(FxmlFile.CHANGEPASSWORD);
} }
@@ -42,8 +42,8 @@ public class DefaultSceneFactory implements Function<Parent, Scene> {
protected void configureScene(Scene scene) { protected void configureScene(Scene scene) {
scene.windowProperty().addListener(observable -> { scene.windowProperty().addListener(observable -> {
Window window = scene.getWindow(); Window window = scene.getWindow();
if (window instanceof Stage) { if (window instanceof Stage s) {
setupDefaultAccelerators(scene, (Stage) window); setupDefaultAccelerators(scene, s);
} }
}); });
} }
@@ -18,8 +18,8 @@ import java.util.ResourceBundle;
abstract class ErrorModule { abstract class ErrorModule {
@Provides @Provides
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle); return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
} }
@Provides @Provides
@@ -38,8 +38,8 @@ abstract class ErrorModule {
@Provides @Provides
@FxmlScene(FxmlFile.ERROR) @FxmlScene(FxmlFile.ERROR)
static Scene provideErrorScene(FXMLLoaderFactory fxmlLoaders) { static Scene provideErrorScene(FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.ERROR.getRessourcePathString()); return fxmlLoaders.createScene(FxmlFile.ERROR);
} }
@@ -26,8 +26,9 @@ public enum FxmlFile {
RECOVERYKEY_RESET_PASSWORD("/fxml/recoverykey_reset_password.fxml"), // RECOVERYKEY_RESET_PASSWORD("/fxml/recoverykey_reset_password.fxml"), //
RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), // RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), //
REMOVE_VAULT("/fxml/remove_vault.fxml"), // REMOVE_VAULT("/fxml/remove_vault.fxml"), //
UNLOCK("/fxml/unlock.fxml"), UNLOCK_ENTER_PASSWORD("/fxml/unlock_enter_password.fxml"),
UNLOCK_INVALID_MOUNT_POINT("/fxml/unlock_invalid_mount_point.fxml"), // UNLOCK_INVALID_MOUNT_POINT("/fxml/unlock_invalid_mount_point.fxml"), //
UNLOCK_SELECT_MASTERKEYFILE("/fxml/unlock_select_masterkeyfile.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"), //
VAULT_STATISTICS("/fxml/stats.fxml"), // VAULT_STATISTICS("/fxml/stats.fxml"), //
@@ -39,7 +40,7 @@ public enum FxmlFile {
this.ressourcePathString = ressourcePathString; this.ressourcePathString = ressourcePathString;
} }
public String getRessourcePathString() { String getRessourcePathString() {
return ressourcePathString; return ressourcePathString;
} }
} }
@@ -11,13 +11,13 @@ import java.util.Map;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import java.util.function.Function; import java.util.function.Function;
public class FXMLLoaderFactory { public class FxmlLoaderFactory {
private final Map<Class<? extends FxController>, Provider<FxController>> controllerFactories; private final Map<Class<? extends FxController>, Provider<FxController>> controllerFactories;
private final Function<Parent, Scene> sceneFactory; private final Function<Parent, Scene> sceneFactory;
private final ResourceBundle resourceBundle; private final ResourceBundle resourceBundle;
public FXMLLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> controllerFactories, Function<Parent, Scene> sceneFactory, ResourceBundle resourceBundle) { public FxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> controllerFactories, Function<Parent, Scene> sceneFactory, ResourceBundle resourceBundle) {
this.controllerFactories = controllerFactories; this.controllerFactories = controllerFactories;
this.sceneFactory = sceneFactory; this.sceneFactory = sceneFactory;
this.resourceBundle = resourceBundle; this.resourceBundle = resourceBundle;
@@ -48,13 +48,17 @@ public class FXMLLoaderFactory {
return loader; return loader;
} }
public Scene createScene(FxmlFile fxmlFile) {
return createScene(fxmlFile.getRessourcePathString());
}
/** /**
* {@link #load(String) Loads} the FXML file and creates a new Scene containing the loaded ui. * {@link #load(String) Loads} the FXML file and creates a new Scene containing the loaded ui.
* *
* @param fxmlResourceName Name of the resource (as in {@link Class#getResource(String)}). * @param fxmlResourceName Name of the resource (as in {@link Class#getResource(String)}).
* @throws UncheckedIOException wrapping any IOException thrown by {@link #load(String)). * @throws UncheckedIOException wrapping any IOException thrown by {@link #load(String)).
*/ */
public Scene createScene(String fxmlResourceName) { private Scene createScene(String fxmlResourceName) {
final FXMLLoader loader; final FXMLLoader loader;
try { try {
loader = load(fxmlResourceName); loader = load(fxmlResourceName);
@@ -0,0 +1,25 @@
package org.cryptomator.ui.common;
import dagger.Lazy;
import org.cryptomator.common.vaults.Volume;
import org.cryptomator.ui.fxapp.FxApplicationScoped;
import javax.inject.Inject;
import javafx.application.Application;
import java.nio.file.Path;
@FxApplicationScoped
public class HostServiceRevealer implements Volume.Revealer {
private final Lazy<Application> application;
@Inject
public HostServiceRevealer(Lazy<Application> application) {
this.application = application;
}
@Override
public void reveal(Path p) throws Volume.VolumeException {
application.get().getHostServices().showDocument(p.toUri().toString());
}
}
@@ -1,5 +1,6 @@
package org.cryptomator.ui.common; package org.cryptomator.ui.common;
import org.cryptomator.common.vaults.LockNotCompletedException;
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.common.vaults.Volume; import org.cryptomator.common.vaults.Volume;
@@ -23,10 +24,12 @@ public class VaultService {
private static final Logger LOG = LoggerFactory.getLogger(VaultService.class); private static final Logger LOG = LoggerFactory.getLogger(VaultService.class);
private final ExecutorService executorService; private final ExecutorService executorService;
private final HostServiceRevealer vaultRevealer;
@Inject @Inject
public VaultService(ExecutorService executorService) { public VaultService(ExecutorService executorService, HostServiceRevealer vaultRevealer) {
this.executorService = executorService; this.executorService = executorService;
this.vaultRevealer = vaultRevealer;
} }
public void reveal(Vault vault) { public void reveal(Vault vault) {
@@ -39,7 +42,7 @@ public class VaultService {
* @param vault The vault to reveal * @param vault The vault to reveal
*/ */
public Task<Vault> createRevealTask(Vault vault) { public Task<Vault> createRevealTask(Vault vault) {
Task<Vault> task = new RevealVaultTask(vault); Task<Vault> task = new RevealVaultTask(vault, vaultRevealer);
task.setOnSucceeded(evt -> LOG.info("Revealed {}", vault.getDisplayName())); task.setOnSucceeded(evt -> LOG.info("Revealed {}", vault.getDisplayName()));
task.setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), evt.getSource().getException())); task.setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), evt.getSource().getException()));
return task; return task;
@@ -99,19 +102,22 @@ public class VaultService {
private static class RevealVaultTask extends Task<Vault> { private static class RevealVaultTask extends Task<Vault> {
private final Vault vault; private final Vault vault;
private final Volume.Revealer revealer;
/** /**
* @param vault The vault to lock * @param vault The vault to lock
* @param revealer The object to use to show the vault content to the user.
*/ */
public RevealVaultTask(Vault vault) { public RevealVaultTask(Vault vault, Volume.Revealer revealer) {
this.vault = vault; this.vault = vault;
this.revealer = revealer;
setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), getException())); setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), getException()));
} }
@Override @Override
protected Vault call() throws Volume.VolumeException { protected Vault call() throws Volume.VolumeException {
vault.reveal(); vault.reveal(revealer);
return vault; return vault;
} }
} }
@@ -170,24 +176,29 @@ public class VaultService {
} }
@Override @Override
protected Vault call() throws Volume.VolumeException { protected Vault call() throws Volume.VolumeException, LockNotCompletedException {
vault.lock(forced); vault.lock(forced);
return vault; return vault;
} }
@Override @Override
protected void scheduled() { protected void scheduled() {
vault.setState(VaultState.PROCESSING); vault.stateProperty().transition(VaultState.Value.UNLOCKED, VaultState.Value.PROCESSING);
} }
@Override @Override
protected void succeeded() { protected void succeeded() {
vault.setState(VaultState.LOCKED); vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
} }
@Override @Override
protected void failed() { protected void failed() {
vault.setState(VaultState.UNLOCKED); vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
}
@Override
protected void cancelled() {
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
} }
} }
@@ -5,7 +5,7 @@ import dagger.Module;
import dagger.Provides; import dagger.Provides;
import dagger.multibindings.IntoMap; import dagger.multibindings.IntoMap;
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;
@@ -29,8 +29,8 @@ abstract class ForgetPasswordModule {
@Provides @Provides
@ForgetPasswordWindow @ForgetPasswordWindow
@ForgetPasswordScoped @ForgetPasswordScoped
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle); return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
} }
@Provides @Provides
@@ -62,8 +62,8 @@ abstract class ForgetPasswordModule {
@Provides @Provides
@FxmlScene(FxmlFile.FORGET_PASSWORD) @FxmlScene(FxmlFile.FORGET_PASSWORD)
@ForgetPasswordScoped @ForgetPasswordScoped
static Scene provideForgetPasswordScene(@ForgetPasswordWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideForgetPasswordScene(@ForgetPasswordWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/forget_password.fxml"); return fxmlLoaders.createScene(FxmlFile.FORGET_PASSWORD);
} }
// ------------------ // ------------------
@@ -5,11 +5,13 @@ 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;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.integrations.tray.TrayIntegrationProvider; import org.cryptomator.integrations.tray.TrayIntegrationProvider;
import org.cryptomator.integrations.uiappearance.Theme; import org.cryptomator.integrations.uiappearance.Theme;
import org.cryptomator.integrations.uiappearance.UiAppearanceException; import org.cryptomator.integrations.uiappearance.UiAppearanceException;
import org.cryptomator.integrations.uiappearance.UiAppearanceListener; import org.cryptomator.integrations.uiappearance.UiAppearanceListener;
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider; import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.VaultService; import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.lock.LockComponent; import org.cryptomator.ui.lock.LockComponent;
import org.cryptomator.ui.mainwindow.MainWindowComponent; import org.cryptomator.ui.mainwindow.MainWindowComponent;
@@ -32,6 +34,8 @@ import javafx.stage.Stage;
import javafx.stage.Window; import javafx.stage.Window;
import java.awt.desktop.QuitResponse; import java.awt.desktop.QuitResponse;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
@FxApplicationScoped @FxApplicationScoped
public class FxApplication extends Application { public class FxApplication extends Application {
@@ -42,8 +46,9 @@ public class FxApplication extends Application {
private final Lazy<MainWindowComponent> mainWindow; private final Lazy<MainWindowComponent> mainWindow;
private final Lazy<PreferencesComponent> preferencesWindow; private final Lazy<PreferencesComponent> preferencesWindow;
private final Lazy<QuitComponent> quitWindow; private final Lazy<QuitComponent> quitWindow;
private final Provider<UnlockComponent.Builder> unlockWindowBuilderProvider; private final Provider<UnlockComponent.Builder> unlockWorkflowBuilderProvider;
private final Provider<LockComponent.Builder> lockWindowBuilderProvider; private final Provider<LockComponent.Builder> lockWorkflowBuilderProvider;
private final ErrorComponent.Builder errorWindowBuilder;
private final Optional<TrayIntegrationProvider> trayIntegration; private final Optional<TrayIntegrationProvider> trayIntegration;
private final Optional<UiAppearanceProvider> appearanceProvider; private final Optional<UiAppearanceProvider> appearanceProvider;
private final VaultService vaultService; private final VaultService vaultService;
@@ -53,13 +58,14 @@ public class FxApplication extends Application {
private final UiAppearanceListener systemInterfaceThemeListener = this::systemInterfaceThemeChanged; private final UiAppearanceListener systemInterfaceThemeListener = this::systemInterfaceThemeChanged;
@Inject @Inject
FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Provider<UnlockComponent.Builder> unlockWindowBuilderProvider, Provider<LockComponent.Builder> lockWindowBuilderProvider, Lazy<QuitComponent> quitWindow, Optional<TrayIntegrationProvider> trayIntegration, Optional<UiAppearanceProvider> appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder) { FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Provider<UnlockComponent.Builder> unlockWorkflowBuilderProvider, Provider<LockComponent.Builder> lockWorkflowBuilderProvider, Lazy<QuitComponent> quitWindow, ErrorComponent.Builder errorWindowBuilder, Optional<TrayIntegrationProvider> trayIntegration, Optional<UiAppearanceProvider> appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder) {
this.settings = settings; this.settings = settings;
this.mainWindow = mainWindow; this.mainWindow = mainWindow;
this.preferencesWindow = preferencesWindow; this.preferencesWindow = preferencesWindow;
this.unlockWindowBuilderProvider = unlockWindowBuilderProvider; this.unlockWorkflowBuilderProvider = unlockWorkflowBuilderProvider;
this.lockWindowBuilderProvider = lockWindowBuilderProvider; this.lockWorkflowBuilderProvider = lockWorkflowBuilderProvider;
this.quitWindow = quitWindow; this.quitWindow = quitWindow;
this.errorWindowBuilder = errorWindowBuilder;
this.trayIntegration = trayIntegration; this.trayIntegration = trayIntegration;
this.appearanceProvider = appearanceProvider; this.appearanceProvider = appearanceProvider;
this.vaultService = vaultService; this.vaultService = vaultService;
@@ -84,7 +90,7 @@ public class FxApplication extends Application {
} }
private void hasVisibleStagesChanged(@SuppressWarnings("unused") ObservableValue<? extends Boolean> observableValue, @SuppressWarnings("unused") boolean oldValue, boolean newValue) { private void hasVisibleStagesChanged(@SuppressWarnings("unused") ObservableValue<? extends Boolean> observableValue, @SuppressWarnings("unused") boolean oldValue, boolean newValue) {
LOG.warn("has visible stages: {}", newValue); LOG.debug("has visible stages: {}", newValue);
if (newValue) { if (newValue) {
trayIntegration.ifPresent(TrayIntegrationProvider::restoredFromTray); trayIntegration.ifPresent(TrayIntegrationProvider::restoredFromTray);
} else { } else {
@@ -99,24 +105,35 @@ public class FxApplication extends Application {
}); });
} }
public void showMainWindow() { public CompletionStage<Stage> showMainWindow() {
CompletableFuture<Stage> future = new CompletableFuture<>();
Platform.runLater(() -> { Platform.runLater(() -> {
mainWindow.get().showMainWindow(); var win = mainWindow.get().showMainWindow();
LOG.debug("Showing MainWindow"); LOG.debug("Showing MainWindow");
future.complete(win);
}); });
return future;
} }
public void startUnlockWorkflow(Vault vault, Optional<Stage> owner) { public void startUnlockWorkflow(Vault vault, Optional<Stage> owner) {
Platform.runLater(() -> { Platform.runLater(() -> {
unlockWindowBuilderProvider.get().vault(vault).owner(owner).build().startUnlockWorkflow(); if (vault.stateProperty().transition(VaultState.Value.LOCKED, VaultState.Value.PROCESSING)) {
LOG.debug("Showing UnlockWindow for {}", vault.getDisplayName()); unlockWorkflowBuilderProvider.get().vault(vault).owner(owner).build().startUnlockWorkflow();
LOG.debug("Start unlock workflow for {}", vault.getDisplayName());
} else {
showMainWindow().thenAccept(mainWindow -> errorWindowBuilder.window(mainWindow).cause(new IllegalStateException("Unable to unlock vault in non-locked state.")));
}
}); });
} }
public void startLockWorkflow(Vault vault, Optional<Stage> owner) { public void startLockWorkflow(Vault vault, Optional<Stage> owner) {
Platform.runLater(() -> { Platform.runLater(() -> {
lockWindowBuilderProvider.get().vault(vault).owner(owner).build().startLockWorkflow(); if (vault.stateProperty().transition(VaultState.Value.UNLOCKED, VaultState.Value.PROCESSING)) {
LOG.debug("Start lock workflow for {}", vault.getDisplayName()); lockWorkflowBuilderProvider.get().vault(vault).owner(owner).build().startLockWorkflow();
LOG.debug("Start lock workflow for {}", vault.getDisplayName());
} else {
showMainWindow().thenAccept(mainWindow -> errorWindowBuilder.window(mainWindow).cause(new IllegalStateException("Unable to lock vault in non-unlocked state.")));
}
}); });
} }
@@ -132,13 +149,13 @@ public class FxApplication extends Application {
} }
private void appThemeChanged(@SuppressWarnings("unused") ObservableValue<? extends UiTheme> observable, @SuppressWarnings("unused") UiTheme oldValue, UiTheme newValue) { private void appThemeChanged(@SuppressWarnings("unused") ObservableValue<? extends UiTheme> observable, @SuppressWarnings("unused") UiTheme oldValue, UiTheme newValue) {
appearanceProvider.ifPresent(appearanceProvider -> { if (appearanceProvider.isPresent() && oldValue == UiTheme.AUTOMATIC && newValue != UiTheme.AUTOMATIC) {
try { try {
appearanceProvider.removeListener(systemInterfaceThemeListener); appearanceProvider.get().removeListener(systemInterfaceThemeListener);
} catch (UiAppearanceException e) { } catch (UiAppearanceException e) {
LOG.error("Failed to disable automatic theme switching."); LOG.error("Failed to disable automatic theme switching.");
} }
}); }
loadSelectedStyleSheet(newValue); loadSelectedStyleSheet(newValue);
} }
@@ -5,11 +5,8 @@
*******************************************************************************/ *******************************************************************************/
package org.cryptomator.ui.fxapp; package org.cryptomator.ui.fxapp;
import dagger.BindsInstance;
import dagger.Subcomponent; import dagger.Subcomponent;
import javax.inject.Named;
@FxApplicationScoped @FxApplicationScoped
@Subcomponent(modules = FxApplicationModule.class) @Subcomponent(modules = FxApplicationModule.class)
public interface FxApplicationComponent { public interface FxApplicationComponent {
@@ -19,9 +16,6 @@ public interface FxApplicationComponent {
@Subcomponent.Builder @Subcomponent.Builder
interface Builder { interface Builder {
@BindsInstance
Builder trayMenuSupported(@Named("trayMenuSupported") boolean trayMenuSupported);
FxApplicationComponent build(); FxApplicationComponent build();
} }
@@ -15,6 +15,7 @@ import javafx.beans.property.StringProperty;
import javafx.concurrent.ScheduledService; import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.util.Duration; import javafx.util.Duration;
import java.io.UncheckedIOException;
import java.net.URI; import java.net.URI;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
@@ -39,8 +40,13 @@ public abstract class UpdateCheckerModule {
@Provides @Provides
@FxApplicationScoped @FxApplicationScoped
static HttpClient provideHttpClient() { static Optional<HttpClient> provideHttpClient() {
return HttpClient.newHttpClient(); try {
return Optional.of(HttpClient.newHttpClient());
} catch (UncheckedIOException e) {
LOG.error("HttpClient for update check cannot be created.", e);
return Optional.empty();
}
} }
@Provides @Provides
@@ -66,11 +72,20 @@ public abstract class UpdateCheckerModule {
@Provides @Provides
@FxApplicationScoped @FxApplicationScoped
static ScheduledService<String> provideCheckForUpdatesService(ExecutorService executor, HttpClient httpClient, HttpRequest checkForUpdatesRequest, @Named("checkForUpdatesInterval") ObjectBinding<Duration> period) { static ScheduledService<String> provideCheckForUpdatesService(ExecutorService executor, Optional<HttpClient> httpClient, HttpRequest checkForUpdatesRequest, @Named("checkForUpdatesInterval") ObjectBinding<Duration> period) {
ScheduledService<String> service = new ScheduledService<>() { ScheduledService<String> service = new ScheduledService<>() {
@Override @Override
protected Task<String> createTask() { protected Task<String> createTask() {
return new UpdateCheckerTask(httpClient, checkForUpdatesRequest); if (httpClient.isPresent()) {
return new UpdateCheckerTask(httpClient.get(), checkForUpdatesRequest);
} else {
return new Task<>() {
@Override
protected String call() {
throw new NullPointerException("No HttpClient present.");
}
};
}
} }
}; };
service.setOnFailed(event -> LOG.error("Failed to execute update service", service.getException())); service.setOnFailed(event -> LOG.error("Failed to execute update service", service.getException()));
@@ -0,0 +1,14 @@
package org.cryptomator.ui.keyloading;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface KeyLoading {
}
@@ -0,0 +1,31 @@
package org.cryptomator.ui.keyloading;
import dagger.BindsInstance;
import dagger.Subcomponent;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import javafx.stage.Stage;
import java.util.Map;
import java.util.function.Supplier;
@KeyLoadingScoped
@Subcomponent(modules = {KeyLoadingModule.class})
public interface KeyLoadingComponent {
@KeyLoading
KeyLoadingStrategy keyloadingStrategy();
@Subcomponent.Builder
interface Builder {
@BindsInstance
Builder vault(@KeyLoading Vault vault);
@BindsInstance
Builder window(@KeyLoading Stage window);
KeyLoadingComponent build();
}
}
@@ -0,0 +1,48 @@
package org.cryptomator.ui.keyloading;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule;
import javax.inject.Provider;
import java.net.URI;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
@Module(includes = {MasterkeyFileLoadingModule.class})
abstract class KeyLoadingModule {
@Provides
@KeyLoading
@KeyLoadingScoped
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
}
@Provides
@KeyLoading
@KeyLoadingScoped
static Optional<URI> provideKeyId(@KeyLoading Vault vault) {
return vault.getUnverifiedVaultConfig().map(UnverifiedVaultConfig::getKeyId);
}
@Provides
@KeyLoading
@KeyLoadingScoped
static KeyLoadingStrategy provideKeyLoaderProvider(@KeyLoading Optional<URI> keyId, Map<String, Provider<KeyLoadingStrategy>> strategies) {
if (keyId.isEmpty()) {
return KeyLoadingStrategy.failed(new IllegalArgumentException("No key id provided"));
} else {
String scheme = keyId.get().getScheme();
var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme));
return strategies.getOrDefault(scheme, () -> fallback).get();
}
}
}
@@ -0,0 +1,13 @@
package org.cryptomator.ui.keyloading;
import javax.inject.Scope;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface KeyLoadingScoped {
}
@@ -0,0 +1,63 @@
package org.cryptomator.ui.keyloading;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import java.net.URI;
/**
* A reusable, stateful {@link MasterkeyLoader}, that can deal with certain exceptions.
*/
@FunctionalInterface
public interface KeyLoadingStrategy extends MasterkeyLoader {
/**
* Loads a master key. This might be a long-running operation, as it may require user input or expensive computations.
* <p>
* If loading fails exceptionally, this strategy might be able to {@link #recoverFromException(MasterkeyLoadingFailedException) recover from this exception}, so it can be used in a further attempt.
*
* @param keyId An URI uniquely identifying the source and identity of the key
* @return The raw key bytes. Must not be null
* @throws MasterkeyLoadingFailedException Thrown when it is impossible to fulfill the request
*/
@Override
Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException;
/**
* Allows the loader to try and recover from an exception thrown during the last attempt.
*
* @param exception An exception thrown by {@link #loadKey(URI)}.
* @return <code>true</code> if this component was able to handle the exception and another attempt can be made to load a masterkey
*/
default boolean recoverFromException(MasterkeyLoadingFailedException exception) {
return false;
}
/**
* Release any ressources or do follow-up tasks after loading a key.
*
* @param unlockedSuccessfully <code>true</code> if successfully unlocked a vault with the loaded key
* @implNote This method might be invoked multiple times, depending on whether multiple attempts to load a key are started.
*/
default void cleanup(boolean unlockedSuccessfully) {
// no-op
}
/**
* A key loading strategy that will always fail by throwing a {@link MasterkeyLoadingFailedException}.
*
* @param exception The cause of the failure. If not alreay an {@link MasterkeyLoadingFailedException}, it will get wrapped.
* @return A new KeyLoadingStrategy that will always fail with an {@link MasterkeyLoadingFailedException}.
*/
static KeyLoadingStrategy failed(Exception exception) {
return keyid -> {
if (exception instanceof MasterkeyLoadingFailedException e) {
throw e;
} else {
throw new MasterkeyLoadingFailedException("Can not load key", exception);
}
};
}
}
@@ -0,0 +1,62 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.nio.CharBuffer;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@KeyLoadingScoped
class MasterkeyFileLoadingFinisher {
private static final Logger LOG = LoggerFactory.getLogger(MasterkeyFileLoadingFinisher.class);
private final Vault vault;
private final Optional<char[]> storedPassword;
private final AtomicReference<char[]> enteredPassword;
private final AtomicBoolean shouldSavePassword;
private final KeychainManager keychain;
@Inject
MasterkeyFileLoadingFinisher(@KeyLoading Vault vault, @Named("savedPassword") Optional<char[]> storedPassword, AtomicReference<char[]> enteredPassword, @Named("savePassword") AtomicBoolean shouldSavePassword, KeychainManager keychain) {
this.vault = vault;
this.storedPassword = storedPassword;
this.enteredPassword = enteredPassword;
this.shouldSavePassword = shouldSavePassword;
this.keychain = keychain;
}
public void cleanup(boolean successfullyUnlocked) {
if (successfullyUnlocked && shouldSavePassword.get()) {
savePasswordToSystemkeychain();
}
wipePassword(storedPassword.orElse(null));
wipePassword(enteredPassword.getAndSet(null));
}
private void savePasswordToSystemkeychain() {
if (keychain.isSupported()) {
try {
keychain.storePassphrase(vault.getId(), CharBuffer.wrap(enteredPassword.get()));
} catch (KeychainAccessException e) {
LOG.error("Failed to store passphrase in system keychain.", e);
}
}
}
private void wipePassword(char[] pw) {
if (pw != null) {
Arrays.fill(pw, ' ');
}
}
}
@@ -0,0 +1,123 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import dagger.multibindings.StringKey;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Named;
import javafx.scene.Scene;
import java.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@Module(subcomponents = {ForgetPasswordComponent.class})
public abstract class MasterkeyFileLoadingModule {
private static final Logger LOG = LoggerFactory.getLogger(MasterkeyFileLoadingModule.class);
public enum PasswordEntry {
PASSWORD_ENTERED,
CANCELED
}
public enum MasterkeyFileProvision {
MASTERKEYFILE_PROVIDED,
CANCELED
}
@Provides
@KeyLoadingScoped
static UserInteractionLock<PasswordEntry> providePasswordEntryLock() {
return new UserInteractionLock<>(null);
}
@Provides
@KeyLoadingScoped
static UserInteractionLock<MasterkeyFileProvision> provideMasterkeyFileProvisionLock() {
return new UserInteractionLock<>(null);
}
@Provides
@Named("savedPassword")
@KeyLoadingScoped
static Optional<char[]> provideStoredPassword(KeychainManager keychain, @KeyLoading Vault vault) {
if (!keychain.isSupported()) {
return Optional.empty();
} else {
try {
return Optional.ofNullable(keychain.loadPassphrase(vault.getId()));
} catch (KeychainAccessException e) {
LOG.error("Failed to load entry from system keychain.", e);
return Optional.empty();
}
}
}
@Provides
@KeyLoadingScoped
static AtomicReference<Path> provideUserProvidedMasterkeyPath() {
return new AtomicReference<>();
}
@Provides
@KeyLoadingScoped
static AtomicReference<char[]> providePassword(@Named("savedPassword") Optional<char[]> storedPassword) {
return new AtomicReference<>(storedPassword.orElse(null));
}
@Provides
@Named("savePassword")
@KeyLoadingScoped
static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional<char[]> storedPassword) {
return new AtomicBoolean(storedPassword.isPresent());
}
@Provides
@FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD)
@KeyLoadingScoped
static Scene provideUnlockScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.UNLOCK_ENTER_PASSWORD);
}
@Provides
@FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE)
@KeyLoadingScoped
static Scene provideUnlockSelectMasterkeyFileScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE);
}
@Binds
@IntoMap
@FxControllerKey(PassphraseEntryController.class)
abstract FxController bindUnlockController(PassphraseEntryController controller);
@Binds
@IntoMap
@FxControllerKey(SelectMasterkeyFileController.class)
abstract FxController bindUnlockSelectMasterkeyFileController(SelectMasterkeyFileController controller);
@Binds
@IntoMap
@KeyLoadingScoped
@StringKey(MasterkeyFileLoadingStrategy.SCHEME)
abstract KeyLoadingStrategy bindMasterkeyFileLoadingStrategy(MasterkeyFileLoadingStrategy strategy);
}
@@ -0,0 +1,150 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import com.google.common.base.Preconditions;
import dagger.Lazy;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.cryptomator.ui.unlock.UnlockCancelledException;
import javax.inject.Inject;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.Window;
import java.net.URI;
import java.nio.CharBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicReference;
@KeyLoading
public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
public static final String SCHEME = "masterkeyfile";
private final Vault vault;
private final MasterkeyFileAccess masterkeyFileAcccess;
private final Stage window;
private final Lazy<Scene> passphraseEntryScene;
private final Lazy<Scene> selectMasterkeyFileScene;
private final UserInteractionLock<MasterkeyFileLoadingModule.PasswordEntry> passwordEntryLock;
private final UserInteractionLock<MasterkeyFileLoadingModule.MasterkeyFileProvision> masterkeyFileProvisionLock;
private final AtomicReference<char[]> password;
private final AtomicReference<Path> filePath;
private final MasterkeyFileLoadingFinisher finisher;
private boolean wrongPassword;
@Inject
public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAcccess, @KeyLoading Stage window, @FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD) Lazy<Scene> passphraseEntryScene, @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) Lazy<Scene> selectMasterkeyFileScene, UserInteractionLock<MasterkeyFileLoadingModule.PasswordEntry> passwordEntryLock, UserInteractionLock<MasterkeyFileLoadingModule.MasterkeyFileProvision> masterkeyFileProvisionLock, AtomicReference<char[]> password, AtomicReference<Path> filePath, MasterkeyFileLoadingFinisher finisher) {
this.vault = vault;
this.masterkeyFileAcccess = masterkeyFileAcccess;
this.window = window;
this.passphraseEntryScene = passphraseEntryScene;
this.selectMasterkeyFileScene = selectMasterkeyFileScene;
this.passwordEntryLock = passwordEntryLock;
this.masterkeyFileProvisionLock = masterkeyFileProvisionLock;
this.password = password;
this.filePath = filePath;
this.finisher = finisher;
}
@Override
public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException {
Preconditions.checkArgument(SCHEME.equalsIgnoreCase(keyId.getScheme()), "Only supports keys with scheme " + SCHEME);
try {
Path filePath = vault.getPath().resolve(keyId.getSchemeSpecificPart());
if (!Files.exists(filePath)) {
filePath = getAlternateMasterkeyFilePath();
}
CharSequence passphrase = getPassphrase();
return masterkeyFileAcccess.load(filePath, passphrase);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new UnlockCancelledException("Unlock interrupted", e);
}
}
@Override
public boolean recoverFromException(MasterkeyLoadingFailedException exception) {
if (exception instanceof InvalidPassphraseException) {
this.wrongPassword = true;
password.set(null);
return true; // reattempting key load
} else {
return false; // nothing we can do
}
}
@Override
public void cleanup(boolean unlockedSuccessfully) {
finisher.cleanup(unlockedSuccessfully);
}
private Path getAlternateMasterkeyFilePath() throws UnlockCancelledException, InterruptedException {
if (filePath == null) {
return switch (askUserForMasterkeyFilePath()) {
case MASTERKEYFILE_PROVIDED -> filePath.get();
case CANCELED -> throw new UnlockCancelledException("Choosing masterkey file cancelled.");
};
} else {
return filePath.get();
}
}
private MasterkeyFileLoadingModule.MasterkeyFileProvision askUserForMasterkeyFilePath() throws InterruptedException {
Platform.runLater(() -> {
window.setScene(selectMasterkeyFileScene.get());
window.show();
Window owner = window.getOwner();
if (owner != null) {
window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2);
window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2);
} else {
window.centerOnScreen();
}
});
return masterkeyFileProvisionLock.awaitInteraction();
}
private CharSequence getPassphrase() throws UnlockCancelledException, InterruptedException {
if (password.get() == null) {
return switch (askForPassphrase()) {
case PASSWORD_ENTERED -> CharBuffer.wrap(password.get());
case CANCELED -> throw new UnlockCancelledException("Password entry cancelled.");
};
} else {
// e.g. pre-filled from keychain or previous unlock attempt
return CharBuffer.wrap(password.get());
}
}
private MasterkeyFileLoadingModule.PasswordEntry askForPassphrase() throws InterruptedException {
Platform.runLater(() -> {
window.setScene(passphraseEntryScene.get());
window.show();
Window owner = window.getOwner();
if (owner != null) {
window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2);
window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2);
} else {
window.centerOnScreen();
}
if (wrongPassword) {
Animations.createShakeWindowAnimation(window).play();
}
});
return passwordEntryLock.awaitInteraction();
}
}
@@ -1,4 +1,4 @@
package org.cryptomator.ui.unlock; package org.cryptomator.ui.keyloading.masterkeyfile;
import org.cryptomator.common.keychain.KeychainManager; import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
@@ -7,6 +7,9 @@ import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.common.WeakBindings; import org.cryptomator.ui.common.WeakBindings;
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.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule.PasswordEntry;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -38,17 +41,17 @@ import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@UnlockScoped @KeyLoadingScoped
public class UnlockController implements FxController { public class PassphraseEntryController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class); private static final Logger LOG = LoggerFactory.getLogger(PassphraseEntryController.class);
private final Stage window; private final Stage window;
private final Vault vault; private final Vault vault;
private final AtomicReference<char[]> password; private final AtomicReference<char[]> password;
private final AtomicBoolean savePassword; private final AtomicBoolean savePassword;
private final Optional<char[]> savedPassword; private final Optional<char[]> savedPassword;
private final UserInteractionLock<UnlockModule.PasswordEntry> passwordEntryLock; private final UserInteractionLock<PasswordEntry> passwordEntryLock;
private final ForgetPasswordComponent.Builder forgetPassword; private final ForgetPasswordComponent.Builder forgetPassword;
private final KeychainManager keychain; private final KeychainManager keychain;
private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay; private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay;
@@ -66,7 +69,7 @@ public class UnlockController implements FxController {
public Animation unlockAnimation; public Animation unlockAnimation;
@Inject @Inject
public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<UnlockModule.PasswordEntry> passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) { public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<PasswordEntry> passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) {
this.window = window; this.window = window;
this.vault = vault; this.vault = vault;
this.password = password; this.password = password;
@@ -138,7 +141,7 @@ public class UnlockController implements FxController {
// if not already interacted, mark this workflow as cancelled: // if not already interacted, mark this workflow as cancelled:
if (passwordEntryLock.awaitingInteraction().get()) { if (passwordEntryLock.awaitingInteraction().get()) {
LOG.debug("Unlock canceled by user."); LOG.debug("Unlock canceled by user.");
passwordEntryLock.interacted(UnlockModule.PasswordEntry.CANCELED); passwordEntryLock.interacted(PasswordEntry.CANCELED);
} }
} }
@@ -154,7 +157,7 @@ public class UnlockController implements FxController {
if (oldPw != null) { if (oldPw != null) {
Arrays.fill(oldPw, ' '); Arrays.fill(oldPw, ' ');
} }
passwordEntryLock.interacted(UnlockModule.PasswordEntry.PASSWORD_ENTERED); passwordEntryLock.interacted(PasswordEntry.PASSWORD_ENTERED);
startUnlockAnimation(); startUnlockAnimation();
} }
@@ -0,0 +1,67 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule.MasterkeyFileProvision;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import java.io.File;
import java.nio.file.Path;
import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicReference;
@KeyLoadingScoped
public class SelectMasterkeyFileController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(SelectMasterkeyFileController.class);
private final Stage window;
private final AtomicReference<Path> masterkeyPath;
private final UserInteractionLock<MasterkeyFileProvision> masterkeyFileProvisionLock;
private final ResourceBundle resourceBundle;
@Inject
public SelectMasterkeyFileController(@KeyLoading Stage window, AtomicReference<Path> masterkeyPath, UserInteractionLock<MasterkeyFileProvision> masterkeyFileProvisionLock, ResourceBundle resourceBundle) {
this.window = window;
this.masterkeyPath = masterkeyPath;
this.masterkeyFileProvisionLock = masterkeyFileProvisionLock;
this.resourceBundle = resourceBundle;
this.window.setOnHiding(this::windowClosed);
}
@FXML
public void cancel() {
window.close();
}
private void windowClosed(WindowEvent windowEvent) {
// if not already interacted, mark this workflow as cancelled:
if (masterkeyFileProvisionLock.awaitingInteraction().get()) {
LOG.debug("Unlock canceled by user.");
masterkeyFileProvisionLock.interacted(MasterkeyFileProvision.CANCELED);
}
}
@FXML
public void proceed() {
LOG.trace("proceed()");
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(resourceBundle.getString("unlock.chooseMasterkey.filePickerTitle"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
File masterkeyFile = fileChooser.showOpenDialog(window);
if (masterkeyFile != null) {
LOG.debug("Chose masterkey file: {}", masterkeyFile);
masterkeyPath.set(masterkeyFile.toPath());
masterkeyFileProvisionLock.interacted(MasterkeyFileProvision.MASTERKEYFILE_PROVIDED);
}
}
}
@@ -1,5 +1,6 @@
package org.cryptomator.ui.launcher; package org.cryptomator.ui.launcher;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.ui.fxapp.FxApplication; import org.cryptomator.ui.fxapp.FxApplication;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -9,6 +10,7 @@ import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
import javafx.application.Platform; import javafx.application.Platform;
import java.io.IOException;
import java.nio.file.NoSuchFileException; import java.nio.file.NoSuchFileException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
@@ -34,15 +36,15 @@ class AppLaunchEventHandler {
this.vaultListManager = vaultListManager; this.vaultListManager = vaultListManager;
} }
public void startHandlingLaunchEvents(boolean hasTrayIcon) { public void startHandlingLaunchEvents() {
executorService.submit(() -> handleLaunchEvents(hasTrayIcon)); executorService.submit(this::handleLaunchEvents);
} }
private void handleLaunchEvents(boolean hasTrayIcon) { private void handleLaunchEvents() {
try { try {
while (!Thread.interrupted()) { while (!Thread.interrupted()) {
AppLaunchEvent event = launchEventQueue.take(); AppLaunchEvent event = launchEventQueue.take();
handleLaunchEvent(hasTrayIcon, event); handleLaunchEvent(event);
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
LOG.warn("Interrupted launch event handler."); LOG.warn("Interrupted launch event handler.");
@@ -50,12 +52,12 @@ class AppLaunchEventHandler {
} }
} }
private void handleLaunchEvent(boolean hasTrayIcon, AppLaunchEvent event) { private void handleLaunchEvent(AppLaunchEvent event) {
switch (event.getType()) { switch (event.getType()) {
case REVEAL_APP -> fxApplicationStarter.get(hasTrayIcon).thenAccept(FxApplication::showMainWindow); case REVEAL_APP -> fxApplicationStarter.get().thenAccept(FxApplication::showMainWindow);
case OPEN_FILE -> fxApplicationStarter.get(hasTrayIcon).thenRun(() -> { case OPEN_FILE -> fxApplicationStarter.get().thenRun(() -> {
Platform.runLater(() -> { Platform.runLater(() -> {
event.getPathsToOpen().forEach(this::addVault); event.getPathsToOpen().forEach(this::addOrRevealVault);
}); });
}); });
default -> LOG.warn("Unsupported event type: {}", event.getType()); default -> LOG.warn("Unsupported event type: {}", event.getType());
@@ -63,16 +65,21 @@ class AppLaunchEventHandler {
} }
// TODO dedup MainWindowController... // TODO dedup MainWindowController...
private void addVault(Path potentialVaultPath) { private void addOrRevealVault(Path potentialVaultPath) {
assert Platform.isFxApplicationThread(); assert Platform.isFxApplicationThread();
try { try {
final Vault v;
if (potentialVaultPath.getFileName().toString().equals(MASTERKEY_FILENAME)) { if (potentialVaultPath.getFileName().toString().equals(MASTERKEY_FILENAME)) {
vaultListManager.add(potentialVaultPath.getParent()); v = vaultListManager.add(potentialVaultPath.getParent());
} else { } else {
vaultListManager.add(potentialVaultPath); v = vaultListManager.add(potentialVaultPath);
}
if (v.isUnlocked()) {
fxApplicationStarter.get().thenAccept(app -> app.getVaultService().reveal(v));
} }
LOG.debug("Added vault {}", potentialVaultPath); LOG.debug("Added vault {}", potentialVaultPath);
} catch (NoSuchFileException e) { } catch (IOException e) {
LOG.error("Failed to add vault " + potentialVaultPath, e); LOG.error("Failed to add vault " + potentialVaultPath, e);
} }
} }
@@ -1,6 +1,7 @@
package org.cryptomator.ui.launcher; package org.cryptomator.ui.launcher;
import org.cryptomator.common.ShutdownHook; import org.cryptomator.common.ShutdownHook;
import org.cryptomator.common.vaults.LockNotCompletedException;
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.common.vaults.Volume; import org.cryptomator.common.vaults.Volume;
@@ -24,11 +25,13 @@ import java.util.Set;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import static org.cryptomator.common.vaults.VaultState.Value.*;
@Singleton @Singleton
public class AppLifecycleListener { public class AppLifecycleListener {
private static final Logger LOG = LoggerFactory.getLogger(AppLifecycleListener.class); 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); public static final Set<VaultState.Value> STATES_ALLOWING_TERMINATION = EnumSet.of(LOCKED, NEEDS_MIGRATION, MISSING, ERROR);
private final FxApplicationStarter fxApplicationStarter; private final FxApplicationStarter fxApplicationStarter;
private final CountDownLatch shutdownLatch; private final CountDownLatch shutdownLatch;
@@ -83,7 +86,7 @@ public class AppLifecycleListener {
if (allowQuitWithoutPrompt.get()) { if (allowQuitWithoutPrompt.get()) {
decoratedQuitResponse.performQuit(); decoratedQuitResponse.performQuit();
} else { } else {
fxApplicationStarter.get(true).thenAccept(app -> app.showQuitWindow(decoratedQuitResponse)); fxApplicationStarter.get().thenAccept(app -> app.showQuitWindow(decoratedQuitResponse));
} }
} }
@@ -113,11 +116,11 @@ public class AppLifecycleListener {
} }
private void showPreferencesWindow(@SuppressWarnings("unused") EventObject actionEvent) { private void showPreferencesWindow(@SuppressWarnings("unused") EventObject actionEvent) {
fxApplicationStarter.get(true).thenAccept(app -> app.showPreferencesWindow(SelectedPreferencesTab.ANY)); fxApplicationStarter.get().thenAccept(app -> app.showPreferencesWindow(SelectedPreferencesTab.ANY));
} }
private void showAboutWindow(@SuppressWarnings("unused") AboutEvent aboutEvent) { private void showAboutWindow(@SuppressWarnings("unused") AboutEvent aboutEvent) {
fxApplicationStarter.get(true).thenAccept(app -> app.showPreferencesWindow(SelectedPreferencesTab.ABOUT)); fxApplicationStarter.get().thenAccept(app -> app.showPreferencesWindow(SelectedPreferencesTab.ABOUT));
} }
private void forceUnmountRemainingVaults() { private void forceUnmountRemainingVaults() {
@@ -127,6 +130,8 @@ public class AppLifecycleListener {
vault.lock(true); vault.lock(true);
} catch (Volume.VolumeException e) { } catch (Volume.VolumeException e) {
LOG.error("Failed to unmount vault " + vault.getPath(), e); LOG.error("Failed to unmount vault " + vault.getPath(), e);
} catch (LockNotCompletedException e) {
LOG.error("Failed to lock vault " + vault.getPath(), e);
} }
} }
} }
@@ -1,5 +1,6 @@
package org.cryptomator.ui.launcher; package org.cryptomator.ui.launcher;
import dagger.Lazy;
import org.cryptomator.ui.fxapp.FxApplication; import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.fxapp.FxApplicationComponent; import org.cryptomator.ui.fxapp.FxApplicationComponent;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -18,37 +19,36 @@ public class FxApplicationStarter {
private static final Logger LOG = LoggerFactory.getLogger(FxApplicationStarter.class); private static final Logger LOG = LoggerFactory.getLogger(FxApplicationStarter.class);
private final FxApplicationComponent.Builder fxAppComponent; private final Lazy<FxApplicationComponent> fxAppComponent;
private final ExecutorService executor; private final ExecutorService executor;
private final AtomicBoolean started; private final AtomicBoolean started;
private final CompletableFuture<FxApplication> future; private final CompletableFuture<FxApplication> future;
@Inject @Inject
public FxApplicationStarter(FxApplicationComponent.Builder fxAppComponent, ExecutorService executor) { public FxApplicationStarter(Lazy<FxApplicationComponent> fxAppComponent, ExecutorService executor) {
this.fxAppComponent = fxAppComponent; this.fxAppComponent = fxAppComponent;
this.executor = executor; this.executor = executor;
this.started = new AtomicBoolean(); this.started = new AtomicBoolean();
this.future = new CompletableFuture<>(); this.future = new CompletableFuture<>();
} }
public CompletionStage<FxApplication> get(boolean hasTrayIcon) { public CompletionStage<FxApplication> get() {
if (!started.getAndSet(true)) { if (!started.getAndSet(true)) {
start(hasTrayIcon); start();
} }
return future; return future;
} }
private void start(boolean hasTrayIcon) { private void start() {
executor.submit(() -> { executor.submit(() -> {
LOG.debug("Starting JavaFX runtime..."); LOG.debug("Starting JavaFX runtime...");
Platform.startup(() -> { Platform.startup(() -> {
assert Platform.isFxApplicationThread(); assert Platform.isFxApplicationThread();
LOG.info("JavaFX Runtime started."); LOG.info("JavaFX Runtime started.");
FxApplication app = fxAppComponent.trayMenuSupported(hasTrayIcon).build().application(); FxApplication app = fxAppComponent.get().application();
app.start(); app.start();
future.complete(app); future.complete(app);
}); });
}); });
} }
} }
@@ -1,5 +1,6 @@
package org.cryptomator.ui.launcher; package org.cryptomator.ui.launcher;
import dagger.Lazy;
import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.integrations.tray.TrayIntegrationProvider; import org.cryptomator.integrations.tray.TrayIntegrationProvider;
@@ -24,60 +25,66 @@ public class UiLauncher {
private final Settings settings; private final Settings settings;
private final ObservableList<Vault> vaults; private final ObservableList<Vault> vaults;
private final TrayMenuComponent.Builder trayComponent; private final Lazy<TrayMenuComponent> trayMenu;
private final FxApplicationStarter fxApplicationStarter; private final FxApplicationStarter fxApplicationStarter;
private final AppLaunchEventHandler launchEventHandler; private final AppLaunchEventHandler launchEventHandler;
private final Optional<TrayIntegrationProvider> trayIntegration; private final Optional<TrayIntegrationProvider> trayIntegration;
@Inject @Inject
public UiLauncher(Settings settings, ObservableList<Vault> vaults, TrayMenuComponent.Builder trayComponent, FxApplicationStarter fxApplicationStarter, AppLaunchEventHandler launchEventHandler, Optional<TrayIntegrationProvider> trayIntegration) { public UiLauncher(Settings settings, ObservableList<Vault> vaults, Lazy<TrayMenuComponent> trayMenu, FxApplicationStarter fxApplicationStarter, AppLaunchEventHandler launchEventHandler, Optional<TrayIntegrationProvider> trayIntegration) {
this.settings = settings; this.settings = settings;
this.vaults = vaults; this.vaults = vaults;
this.trayComponent = trayComponent; this.trayMenu = trayMenu;
this.fxApplicationStarter = fxApplicationStarter; this.fxApplicationStarter = fxApplicationStarter;
this.launchEventHandler = launchEventHandler; this.launchEventHandler = launchEventHandler;
this.trayIntegration = trayIntegration; this.trayIntegration = trayIntegration;
} }
public void launch() { public void launch() {
final boolean hasTrayIcon; boolean hidden = settings.startHidden().get();
if (SystemTray.isSupported()) { if (SystemTray.isSupported() && settings.showTrayIcon().get()) {
trayComponent.build().addIconToSystemTray(); trayMenu.get().initializeTrayIcon();
hasTrayIcon = true; launch(true, hidden);
} else { } else {
hasTrayIcon = false; launch(false, hidden);
} }
}
// show window on start? private void launch(boolean withTrayIcon, boolean hidden) {
if (hasTrayIcon && settings.startHidden().get()) { // start hidden, minimized or normal?
if (withTrayIcon && hidden) {
LOG.debug("Hiding application..."); LOG.debug("Hiding application...");
trayIntegration.ifPresent(TrayIntegrationProvider::minimizedToTray); trayIntegration.ifPresent(TrayIntegrationProvider::minimizedToTray);
} else if (!withTrayIcon && hidden) {
LOG.debug("Minimizing application...");
showMainWindowAsync(true);
} else { } else {
showMainWindowAsync(hasTrayIcon); LOG.debug("Showing application...");
showMainWindowAsync(false);
} }
// register app reopen listener // register app reopen listener
Desktop.getDesktop().addAppEventListener((AppReopenedListener) e -> showMainWindowAsync(hasTrayIcon)); Desktop.getDesktop().addAppEventListener((AppReopenedListener) e -> showMainWindowAsync(false));
// auto unlock // auto unlock
Collection<Vault> vaultsToAutoUnlock = vaults.filtered(this::shouldAttemptAutoUnlock); Collection<Vault> vaultsToAutoUnlock = vaults.filtered(this::shouldAttemptAutoUnlock);
if (!vaultsToAutoUnlock.isEmpty()) { if (!vaultsToAutoUnlock.isEmpty()) {
fxApplicationStarter.get(hasTrayIcon).thenAccept(app -> { fxApplicationStarter.get().thenAccept(app -> {
for (Vault vault : vaultsToAutoUnlock) { for (Vault vault : vaultsToAutoUnlock) {
app.startUnlockWorkflow(vault, Optional.empty()); app.startUnlockWorkflow(vault, Optional.empty());
} }
}); });
} }
launchEventHandler.startHandlingLaunchEvents(hasTrayIcon); launchEventHandler.startHandlingLaunchEvents();
} }
private boolean shouldAttemptAutoUnlock(Vault vault) { private boolean shouldAttemptAutoUnlock(Vault vault) {
return vault.isLocked() && vault.getVaultSettings().unlockAfterStartup().get(); return vault.isLocked() && vault.getVaultSettings().unlockAfterStartup().get();
} }
private void showMainWindowAsync(boolean hasTrayIcon) { private void showMainWindowAsync(boolean minimize) {
fxApplicationStarter.get(hasTrayIcon).thenAccept(FxApplication::showMainWindow); fxApplicationStarter.get().thenCompose(FxApplication::showMainWindow).thenAccept(win -> win.setIconified(minimize));
} }
} }
@@ -19,6 +19,18 @@ import java.util.concurrent.BlockingQueue;
@Module(subcomponents = {TrayMenuComponent.class, FxApplicationComponent.class}) @Module(subcomponents = {TrayMenuComponent.class, FxApplicationComponent.class})
public abstract class UiLauncherModule { public abstract class UiLauncherModule {
@Provides
@Singleton
static TrayMenuComponent provideTrayMenuComponent(TrayMenuComponent.Builder builder) {
return builder.build();
}
@Provides
@Singleton
static FxApplicationComponent provideFxApplicationComponent(FxApplicationComponent.Builder builder) {
return builder.build();
}
@Provides @Provides
@Singleton @Singleton
static Optional<UiAppearanceProvider> provideAppearanceProvider() { static Optional<UiAppearanceProvider> provideAppearanceProvider() {
@@ -6,7 +6,7 @@ import dagger.Provides;
import dagger.multibindings.IntoMap; import dagger.multibindings.IntoMap;
import org.cryptomator.common.vaults.Vault; 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;
@@ -40,8 +40,8 @@ abstract class LockModule {
@Provides @Provides
@LockWindow @LockWindow
@LockScoped @LockScoped
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle); return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
} }
@Provides @Provides
@@ -63,15 +63,15 @@ abstract class LockModule {
@Provides @Provides
@FxmlScene(FxmlFile.LOCK_FORCED) @FxmlScene(FxmlFile.LOCK_FORCED)
@LockScoped @LockScoped
static Scene provideForceLockScene(@LockWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideForceLockScene(@LockWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/lock_forced.fxml"); return fxmlLoaders.createScene(FxmlFile.LOCK_FORCED);
} }
@Provides @Provides
@FxmlScene(FxmlFile.LOCK_FAILED) @FxmlScene(FxmlFile.LOCK_FAILED)
@LockScoped @LockScoped
static Scene provideLockFailedScene(@LockWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideLockFailedScene(@LockWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/lock_failed.fxml"); return fxmlLoaders.createScene(FxmlFile.LOCK_FAILED);
} }
// ------------------ // ------------------
@@ -1,9 +1,11 @@
package org.cryptomator.ui.lock; package org.cryptomator.ui.lock;
import dagger.Lazy; import dagger.Lazy;
import org.cryptomator.common.vaults.LockNotCompletedException;
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.common.vaults.Volume; import org.cryptomator.common.vaults.Volume;
import org.cryptomator.ui.common.ErrorComponent;
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.UserInteractionLock; import org.cryptomator.ui.common.UserInteractionLock;
@@ -35,21 +37,23 @@ public class LockWorkflow extends Task<Void> {
private final UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock; private final UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock;
private final Lazy<Scene> lockForcedScene; private final Lazy<Scene> lockForcedScene;
private final Lazy<Scene> lockFailedScene; private final Lazy<Scene> lockFailedScene;
private final ErrorComponent.Builder errorComponent;
@Inject @Inject
public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy<Scene> lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy<Scene> lockFailedScene) { public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy<Scene> lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy<Scene> lockFailedScene, ErrorComponent.Builder errorComponent) {
this.lockWindow = lockWindow; this.lockWindow = lockWindow;
this.vault = vault; this.vault = vault;
this.forceLockDecisionLock = forceLockDecisionLock; this.forceLockDecisionLock = forceLockDecisionLock;
this.lockForcedScene = lockForcedScene; this.lockForcedScene = lockForcedScene;
this.lockFailedScene = lockFailedScene; this.lockFailedScene = lockFailedScene;
this.errorComponent = errorComponent;
} }
@Override @Override
protected Void call() throws Volume.VolumeException, InterruptedException { protected Void call() throws Volume.VolumeException, InterruptedException, LockNotCompletedException {
try { try {
vault.lock(false); vault.lock(false);
} catch (Volume.VolumeException e) { } catch (Volume.VolumeException | LockNotCompletedException e) {
LOG.debug("Regular lock of {} failed.", vault.getDisplayName(), e); LOG.debug("Regular lock of {} failed.", vault.getDisplayName(), e);
var decision = askUserForAction(); var decision = askUserForAction();
switch (decision) { switch (decision) {
@@ -77,29 +81,29 @@ public class LockWorkflow extends Task<Void> {
return forceLockDecisionLock.awaitInteraction(); return forceLockDecisionLock.awaitInteraction();
} }
@Override
protected void scheduled() {
vault.setState(VaultState.PROCESSING);
}
@Override @Override
protected void succeeded() { protected void succeeded() {
LOG.info("Lock of {} succeeded.", vault.getDisplayName()); LOG.info("Lock of {} succeeded.", vault.getDisplayName());
vault.setState(VaultState.LOCKED); vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
} }
@Override @Override
protected void failed() { protected void failed() {
LOG.warn("Failed to lock {}.", vault.getDisplayName()); final var throwable = super.getException();
vault.setState(VaultState.UNLOCKED); LOG.warn("Lock of {} failed.", vault.getDisplayName(), throwable);
lockWindow.setScene(lockFailedScene.get()); vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
lockWindow.show(); if (throwable instanceof Volume.VolumeException) {
lockWindow.setScene(lockFailedScene.get());
lockWindow.show();
} else {
errorComponent.cause(throwable).window(lockWindow).build().showErrorScene();
}
} }
@Override @Override
protected void cancelled() { protected void cancelled() {
LOG.debug("Lock of {} canceled.", vault.getDisplayName()); LOG.debug("Lock of {} canceled.", vault.getDisplayName());
vault.setState(VaultState.UNLOCKED); vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
} }
} }
@@ -26,7 +26,6 @@ public interface MainWindowComponent {
default Stage showMainWindow() { default Stage showMainWindow() {
Stage stage = window(); Stage stage = window();
stage.setScene(scene().get()); stage.setScene(scene().get());
stage.setIconified(false);
stage.show(); stage.show();
stage.toFront(); stage.toFront();
stage.requestFocus(); stage.requestFocus();
@@ -3,6 +3,8 @@ package org.cryptomator.ui.mainwindow;
import org.apache.commons.lang3.SystemUtils; import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptofs.DirStructure;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent; import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -20,6 +22,7 @@ import javafx.scene.input.TransferMode;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.stage.Stage; import javafx.stage.Stage;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.nio.file.Files; 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;
@@ -27,6 +30,7 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
@MainWindowScoped @MainWindowScoped
public class MainWindowController implements FxController { public class MainWindowController implements FxController {
@@ -91,23 +95,21 @@ public class MainWindowController implements FxController {
} }
private boolean containsVault(Path path) { private boolean containsVault(Path path) {
if (path.getFileName().toString().equals(MASTERKEY_FILENAME)) { try {
return true; return CryptoFileSystemProvider.checkDirStructureForVault(path, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) != DirStructure.UNRELATED;
} else if (Files.isDirectory(path) && Files.exists(path.resolve(MASTERKEY_FILENAME))) { } catch (IOException e) {
return true;
} else {
return false; return false;
} }
} }
private void addVault(Path pathToVault) { private void addVault(Path pathToVault) {
try { try {
if (pathToVault.getFileName().toString().equals(MASTERKEY_FILENAME)) { if (pathToVault.getFileName().toString().equals(VAULTCONFIG_FILENAME)) {
vaultListManager.add(pathToVault.getParent()); vaultListManager.add(pathToVault.getParent());
} else { } else {
vaultListManager.add(pathToVault); vaultListManager.add(pathToVault);
} }
} catch (NoSuchFileException e) { } catch (IOException e) {
LOG.debug("Not a vault: {}", pathToVault); LOG.debug("Not a vault: {}", pathToVault);
} }
} }
@@ -6,7 +6,7 @@ import dagger.Provides;
import dagger.multibindings.IntoMap; import dagger.multibindings.IntoMap;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent; import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent;
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;
@@ -39,8 +39,8 @@ abstract class MainWindowModule {
@Provides @Provides
@MainWindow @MainWindow
@MainWindowScoped @MainWindowScoped
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, MainWindowSceneFactory sceneFactory, ResourceBundle resourceBundle) { static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, MainWindowSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle); return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
} }
@Provides @Provides
@@ -60,8 +60,8 @@ abstract class MainWindowModule {
@Provides @Provides
@FxmlScene(FxmlFile.MAIN_WINDOW) @FxmlScene(FxmlFile.MAIN_WINDOW)
@MainWindowScoped @MainWindowScoped
static Scene provideMainScene(@MainWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideMainScene(@MainWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/main_window.fxml"); return fxmlLoaders.createScene(FxmlFile.MAIN_WINDOW);
} }
// ------------------ // ------------------
@@ -86,6 +86,11 @@ abstract class MainWindowModule {
@FxControllerKey(VaultListController.class) @FxControllerKey(VaultListController.class)
abstract FxController bindVaultListController(VaultListController controller); abstract FxController bindVaultListController(VaultListController controller);
@Binds
@IntoMap
@FxControllerKey(VaultListContextMenuController.class)
abstract FxController bindVaultListContextMenuController(VaultListContextMenuController controller);
@Binds @Binds
@IntoMap @IntoMap
@FxControllerKey(VaultDetailController.class) @FxControllerKey(VaultDetailController.class)
@@ -7,13 +7,14 @@ import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.fxapp.UpdateChecker; import org.cryptomator.ui.fxapp.UpdateChecker;
import org.cryptomator.ui.launcher.AppLifecycleListener; import org.cryptomator.ui.launcher.AppLifecycleListener;
import org.cryptomator.ui.preferences.SelectedPreferencesTab; import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import org.cryptomator.ui.traymenu.TrayMenuComponent;
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 javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding; import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.stage.Stage; import javafx.stage.Stage;
@@ -23,32 +24,31 @@ public class MainWindowTitleController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(MainWindowTitleController.class); private static final Logger LOG = LoggerFactory.getLogger(MainWindowTitleController.class);
public HBox titleBar;
private final AppLifecycleListener appLifecycle; private final AppLifecycleListener appLifecycle;
private final Stage window; private final Stage window;
private final FxApplication application; private final FxApplication application;
private final boolean minimizeToSysTray; private final boolean trayMenuInitialized;
private final UpdateChecker updateChecker; private final UpdateChecker updateChecker;
private final BooleanBinding updateAvailable; private final BooleanBinding updateAvailable;
private final LicenseHolder licenseHolder; private final LicenseHolder licenseHolder;
private final Settings settings; private final Settings settings;
private final BooleanBinding debugModeEnabled; private final BooleanBinding showMinimizeButton;
public HBox titleBar;
private double xOffset; private double xOffset;
private double yOffset; private double yOffset;
@Inject @Inject
MainWindowTitleController(AppLifecycleListener appLifecycle, @MainWindow Stage window, FxApplication application, @Named("trayMenuSupported") boolean minimizeToSysTray, UpdateChecker updateChecker, LicenseHolder licenseHolder, Settings settings) { MainWindowTitleController(AppLifecycleListener appLifecycle, @MainWindow Stage window, FxApplication application, TrayMenuComponent trayMenu, UpdateChecker updateChecker, LicenseHolder licenseHolder, Settings settings) {
this.appLifecycle = appLifecycle; this.appLifecycle = appLifecycle;
this.window = window; this.window = window;
this.application = application; this.application = application;
this.minimizeToSysTray = minimizeToSysTray; this.trayMenuInitialized = trayMenu.isInitialized();
this.updateChecker = updateChecker; this.updateChecker = updateChecker;
this.updateAvailable = updateChecker.latestVersionProperty().isNotNull(); this.updateAvailable = updateChecker.latestVersionProperty().isNotNull();
this.licenseHolder = licenseHolder; this.licenseHolder = licenseHolder;
this.settings = settings; this.settings = settings;
this.debugModeEnabled = Bindings.createBooleanBinding(this::isDebugModeEnabled, settings.debugMode()); this.showMinimizeButton = Bindings.createBooleanBinding(this::isShowMinimizeButton, settings.showMinimizeButton(), settings.showTrayIcon());
} }
@FXML @FXML
@@ -71,7 +71,7 @@ public class MainWindowTitleController implements FxController {
@FXML @FXML
public void close() { public void close() {
if (minimizeToSysTray) { if (trayMenuInitialized) {
window.close(); window.close();
} else { } else {
appLifecycle.quit(); appLifecycle.quit();
@@ -95,7 +95,7 @@ public class MainWindowTitleController implements FxController {
@FXML @FXML
public void showDonationKeyPreferences() { public void showDonationKeyPreferences() {
application.showPreferencesWindow(SelectedPreferencesTab.DONATION_KEY); application.showPreferencesWindow(SelectedPreferencesTab.CONTRIBUTE);
} }
/* Getter/Setter */ /* Getter/Setter */
@@ -112,15 +112,24 @@ public class MainWindowTitleController implements FxController {
return updateAvailable.get(); return updateAvailable.get();
} }
public boolean isMinimizeToSysTray() { public boolean isTrayIconPresent() {
return minimizeToSysTray; return trayMenuInitialized;
} }
public BooleanBinding debugModeEnabledProperty() { public ReadOnlyBooleanProperty debugModeEnabledProperty() {
return debugModeEnabled; return settings.debugMode();
} }
public boolean isDebugModeEnabled() { public boolean isDebugModeEnabled() {
return settings.debugMode().get(); return debugModeEnabledProperty().get();
}
public BooleanBinding showMinimizeButtonProperty() {
return showMinimizeButton;
}
public boolean isShowMinimizeButton() {
// always show the minimize button if no tray icon is present OR it is explicitily enabled
return !trayMenuInitialized || settings.showMinimizeButton().get();
} }
} }
@@ -32,7 +32,8 @@ public class VaultDetailController implements FxController {
this.anyVaultSelected = vault.isNotNull(); this.anyVaultSelected = vault.isNotNull();
} }
private FontAwesome5Icon getGlyphForVaultState(VaultState state) { // TODO deduplicate w/ VaultListCellController
private FontAwesome5Icon getGlyphForVaultState(VaultState.Value state) {
if (state != null) { if (state != null) {
return switch (state) { return switch (state) {
case LOCKED -> FontAwesome5Icon.LOCK; case LOCKED -> FontAwesome5Icon.LOCK;
@@ -5,6 +5,7 @@ import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplication; import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.vaultoptions.SelectedVaultOptionsTab;
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent; import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
import javax.inject.Inject; import javax.inject.Inject;
@@ -47,7 +48,12 @@ public class VaultDetailLockedController implements FxController {
@FXML @FXML
public void showVaultOptions() { public void showVaultOptions() {
vaultOptionsWindow.vault(vault.get()).build().showVaultOptionsWindow(); vaultOptionsWindow.vault(vault.get()).build().showVaultOptionsWindow(SelectedVaultOptionsTab.ANY);
}
@FXML
public void showKeyVaultOptions() {
vaultOptionsWindow.vault(vault.get()).build().showVaultOptionsWindow(SelectedVaultOptionsTab.KEY);
} }
/* Getter/Setter */ /* Getter/Setter */
@@ -24,7 +24,8 @@ public class VaultListCellController implements FxController {
.map(this::getGlyphForVaultState); .map(this::getGlyphForVaultState);
} }
private FontAwesome5Icon getGlyphForVaultState(VaultState state) { // TODO deduplicate w/ VaultDetailController
private FontAwesome5Icon getGlyphForVaultState(VaultState.Value state) {
if (state != null) { if (state != null) {
return switch (state) { return switch (state) {
case LOCKED -> FontAwesome5Icon.LOCK; case LOCKED -> FontAwesome5Icon.LOCK;
@@ -1,7 +1,7 @@
package org.cryptomator.ui.mainwindow; package org.cryptomator.ui.mainwindow;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FXMLLoaderFactory; import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.controls.DraggableListCell; import org.cryptomator.ui.controls.DraggableListCell;
import javax.inject.Inject; import javax.inject.Inject;
@@ -17,10 +17,10 @@ import java.io.UncheckedIOException;
@MainWindowScoped @MainWindowScoped
public class VaultListCellFactory implements Callback<ListView<Vault>, ListCell<Vault>> { public class VaultListCellFactory implements Callback<ListView<Vault>, ListCell<Vault>> {
private final FXMLLoaderFactory fxmlLoaders; private final FxmlLoaderFactory fxmlLoaders;
@Inject @Inject
VaultListCellFactory(@MainWindow FXMLLoaderFactory fxmlLoaders) { VaultListCellFactory(@MainWindow FxmlLoaderFactory fxmlLoaders) {
this.fxmlLoaders = fxmlLoaders; this.fxmlLoaders = fxmlLoaders;
} }
@@ -0,0 +1,128 @@
package org.cryptomator.ui.mainwindow;
import com.tobiasdiez.easybind.EasyBind;
import com.tobiasdiez.easybind.optional.ObservableOptionalValue;
import com.tobiasdiez.easybind.optional.OptionalBinding;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.removevault.RemoveVaultComponent;
import org.cryptomator.ui.vaultoptions.SelectedVaultOptionsTab;
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
import javax.inject.Inject;
import javafx.beans.binding.Binding;
import javafx.beans.property.ObjectProperty;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import java.util.EnumSet;
import java.util.Optional;
import static org.cryptomator.common.vaults.VaultState.Value.*;
@MainWindowScoped
public class VaultListContextMenuController implements FxController {
private final ObservableOptionalValue<Vault> selectedVault;
private final Stage mainWindow;
private final FxApplication application;
private final KeychainManager keychain;
private final RemoveVaultComponent.Builder removeVault;
private final VaultOptionsComponent.Builder vaultOptionsWindow;
private final OptionalBinding<VaultState.Value> selectedVaultState;
private final Binding<Boolean> selectedVaultPassphraseStored;
private final Binding<Boolean> selectedVaultRemovable;
private final Binding<Boolean> selectedVaultUnlockable;
private final Binding<Boolean> selectedVaultLockable;
@Inject
VaultListContextMenuController(ObjectProperty<Vault> selectedVault, @MainWindow Stage mainWindow, FxApplication application, KeychainManager keychain, RemoveVaultComponent.Builder removeVault, VaultOptionsComponent.Builder vaultOptionsWindow) {
this.selectedVault = EasyBind.wrapNullable(selectedVault);
this.mainWindow = mainWindow;
this.application = application;
this.keychain = keychain;
this.removeVault = removeVault;
this.vaultOptionsWindow = vaultOptionsWindow;
this.selectedVaultState = this.selectedVault.mapObservable(Vault::stateProperty);
this.selectedVaultPassphraseStored = this.selectedVault.map(this::isPasswordStored).orElse(false);
this.selectedVaultRemovable = selectedVaultState.map(EnumSet.of(LOCKED, MISSING, ERROR, NEEDS_MIGRATION)::contains).orElse(false);
this.selectedVaultUnlockable = selectedVaultState.map(LOCKED::equals).orElse(false);
this.selectedVaultLockable = selectedVaultState.map(UNLOCKED::equals).orElse(false);
}
private boolean isPasswordStored(Vault vault) {
return keychain.getPassphraseStoredProperty(vault.getId()).get();
}
@FXML
public void didClickRemoveVault() {
selectedVault.ifValuePresent(v -> {
removeVault.vault(v).build().showRemoveVault();
});
}
@FXML
public void didClickShowVaultOptions() {
selectedVault.ifValuePresent(v -> {
vaultOptionsWindow.vault(v).build().showVaultOptionsWindow(SelectedVaultOptionsTab.ANY);
});
}
@FXML
public void didClickUnlockVault() {
selectedVault.ifValuePresent(v -> {
application.startUnlockWorkflow(v, Optional.of(mainWindow));
});
}
@FXML
public void didClickLockVault() {
selectedVault.ifValuePresent(v -> {
application.startLockWorkflow(v, Optional.of(mainWindow));
});
}
@FXML
public void didClickRevealVault() {
selectedVault.ifValuePresent(v -> {
application.getVaultService().reveal(v);
});
}
// Getter and Setter
public Binding<Boolean> selectedVaultUnlockableProperty() {
return selectedVaultUnlockable;
}
public boolean isSelectedVaultUnlockable() {
return selectedVaultUnlockable.getValue();
}
public Binding<Boolean> selectedVaultLockableProperty() {
return selectedVaultLockable;
}
public boolean isSelectedVaultLockable() {
return selectedVaultLockable.getValue();
}
public Binding<Boolean> selectedVaultRemovableProperty() {
return selectedVaultRemovable;
}
public boolean isSelectedVaultRemovable() {
return selectedVaultRemovable.getValue();
}
public Binding<Boolean> selectedVaultPassphraseStoredProperty() {
return selectedVaultPassphraseStored;
}
public boolean isSelectedVaultPassphraseStored() {
return selectedVaultPassphraseStored.getValue();
}
}
@@ -1,12 +1,11 @@
package org.cryptomator.ui.mainwindow; package org.cryptomator.ui.mainwindow;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.common.vaults.VaultListManager;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject; import javax.inject.Inject;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
@@ -17,30 +16,43 @@ 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.scene.input.ContextMenuEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;
import java.util.EnumSet;
import static org.cryptomator.common.vaults.VaultState.Value.ERROR;
import static org.cryptomator.common.vaults.VaultState.Value.LOCKED;
import static org.cryptomator.common.vaults.VaultState.Value.MISSING;
import static org.cryptomator.common.vaults.VaultState.Value.NEEDS_MIGRATION;
@MainWindowScoped @MainWindowScoped
public class VaultListController implements FxController { public class VaultListController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(VaultListController.class);
private final Stage mainWindow;
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;
private final AddVaultWizardComponent.Builder addVaultWizard; private final AddVaultWizardComponent.Builder addVaultWizard;
private final RemoveVaultComponent.Builder removeVault;
private final BooleanBinding noVaultSelected;
private final BooleanBinding emptyVaultList; private final BooleanBinding emptyVaultList;
private final RemoveVaultComponent.Builder removeVaultDialogue;
public ListView<Vault> vaultList; public ListView<Vault> vaultList;
@Inject @Inject
VaultListController(ObservableList<Vault> vaults, ObjectProperty<Vault> selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVault) { VaultListController(@MainWindow Stage mainWindow, ObservableList<Vault> vaults, ObjectProperty<Vault> selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVaultDialogue) {
this.mainWindow = mainWindow;
this.vaults = vaults; this.vaults = vaults;
this.selectedVault = selectedVault; this.selectedVault = selectedVault;
this.cellFactory = cellFactory; this.cellFactory = cellFactory;
this.addVaultWizard = addVaultWizard; this.addVaultWizard = addVaultWizard;
this.removeVault = removeVault; this.removeVaultDialogue = removeVaultDialogue;
this.noVaultSelected = selectedVault.isNull();
this.emptyVaultList = Bindings.isEmpty(vaults); this.emptyVaultList = Bindings.isEmpty(vaults);
selectedVault.addListener(this::selectedVaultDidChange); selectedVault.addListener(this::selectedVaultDidChange);
} }
@@ -56,6 +68,41 @@ public class VaultListController implements FxController {
} }
} }
}); });
vaultList.addEventFilter(MouseEvent.MOUSE_RELEASED, this::deselect);
vaultList.addEventFilter(ContextMenuEvent.CONTEXT_MENU_REQUESTED, request -> {
if (selectedVault.get() == null) {
request.consume();
}
});
vaultList.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> {
if (keyEvent.getCode() == KeyCode.DELETE) {
pressedShortcutToRemoveVault();
keyEvent.consume();
}
});
if (SystemUtils.IS_OS_MAC) {
vaultList.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> {
if (keyEvent.getCode() == KeyCode.BACK_SPACE) {
pressedShortcutToRemoveVault();
keyEvent.consume();
}
});
}
//register vault selection shortcut to the main window
mainWindow.addEventFilter(KeyEvent.KEY_RELEASED, keyEvent -> {
if (keyEvent.isShortcutDown() && keyEvent.getCode().isDigitKey()) {
vaultList.getSelectionModel().select(Integer.parseInt(keyEvent.getText()) - 1);
keyEvent.consume();
}
});
}
private void deselect(MouseEvent released) {
if (released.getY() > (vaultList.getItems().size() * vaultList.fixedCellSizeProperty().get())) {
vaultList.getSelectionModel().clearSelection();
released.consume();
}
} }
private void selectedVaultDidChange(@SuppressWarnings("unused") ObservableValue<? extends Vault> observableValue, @SuppressWarnings("unused") Vault oldValue, Vault newValue) { private void selectedVaultDidChange(@SuppressWarnings("unused") ObservableValue<? extends Vault> observableValue, @SuppressWarnings("unused") Vault oldValue, Vault newValue) {
@@ -70,13 +117,10 @@ public class VaultListController implements FxController {
addVaultWizard.build().showAddVaultWizard(); addVaultWizard.build().showAddVaultWizard();
} }
@FXML private void pressedShortcutToRemoveVault() {
public void didClickRemoveVault() { final var vault = selectedVault.get();
Vault v = selectedVault.get(); if (vault != null && EnumSet.of(LOCKED, MISSING, ERROR, NEEDS_MIGRATION).contains(vault.getState())) {
if (v != null) { removeVaultDialogue.vault(vault).build().showRemoveVault();
removeVault.vault(v).build().showRemoveVault();
} else {
LOG.debug("Cannot remove a vault if none is selected.");
} }
} }
@@ -90,11 +134,4 @@ public class VaultListController implements FxController {
return emptyVaultList.get(); return emptyVaultList.get();
} }
public BooleanBinding noVaultSelectedProperty() {
return noVaultSelected;
}
public boolean isNoVaultSelected() {
return noVaultSelected.get();
}
} }
@@ -6,7 +6,7 @@ import dagger.Provides;
import dagger.multibindings.IntoMap; import dagger.multibindings.IntoMap;
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker; 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;
@@ -30,8 +30,8 @@ abstract class MigrationModule {
@Provides @Provides
@MigrationWindow @MigrationWindow
@MigrationScoped @MigrationScoped
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle); return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
} }
@Provides @Provides
@@ -56,36 +56,36 @@ abstract class MigrationModule {
@Provides @Provides
@FxmlScene(FxmlFile.MIGRATION_START) @FxmlScene(FxmlFile.MIGRATION_START)
@MigrationScoped @MigrationScoped
static Scene provideMigrationStartScene(@MigrationWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideMigrationStartScene(@MigrationWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/migration_start.fxml"); return fxmlLoaders.createScene(FxmlFile.MIGRATION_START);
} }
@Provides @Provides
@FxmlScene(FxmlFile.MIGRATION_RUN) @FxmlScene(FxmlFile.MIGRATION_RUN)
@MigrationScoped @MigrationScoped
static Scene provideMigrationRunScene(@MigrationWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideMigrationRunScene(@MigrationWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/migration_run.fxml"); return fxmlLoaders.createScene(FxmlFile.MIGRATION_RUN);
} }
@Provides @Provides
@FxmlScene(FxmlFile.MIGRATION_SUCCESS) @FxmlScene(FxmlFile.MIGRATION_SUCCESS)
@MigrationScoped @MigrationScoped
static Scene provideMigrationSuccessScene(@MigrationWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideMigrationSuccessScene(@MigrationWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/migration_success.fxml"); return fxmlLoaders.createScene(FxmlFile.MIGRATION_SUCCESS);
} }
@Provides @Provides
@FxmlScene(FxmlFile.MIGRATION_CAPABILITY_ERROR) @FxmlScene(FxmlFile.MIGRATION_CAPABILITY_ERROR)
@MigrationScoped @MigrationScoped
static Scene provideMigrationCapabilityErrorScene(@MigrationWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideMigrationCapabilityErrorScene(@MigrationWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/migration_capability_error.fxml"); return fxmlLoaders.createScene(FxmlFile.MIGRATION_CAPABILITY_ERROR);
} }
@Provides @Provides
@FxmlScene(FxmlFile.MIGRATION_IMPOSSIBLE) @FxmlScene(FxmlFile.MIGRATION_IMPOSSIBLE)
@MigrationScoped @MigrationScoped
static Scene provideMigrationImpossibleScene(@MigrationWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideMigrationImpossibleScene(@MigrationWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/migration_impossible.fxml"); return fxmlLoaders.createScene(FxmlFile.MIGRATION_IMPOSSIBLE);
} }
// ------------------ // ------------------
@@ -26,6 +26,7 @@ import javax.inject.Named;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding; import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.ObjectExpression;
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.ObjectProperty;
@@ -43,6 +44,7 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
@MigrationScoped @MigrationScoped
public class MigrationRunController implements FxController { public class MigrationRunController implements FxController {
@@ -89,7 +91,12 @@ public class MigrationRunController implements FxController {
if (keychain.isSupported()) { if (keychain.isSupported()) {
loadStoredPassword(); loadStoredPassword();
} }
migrationButtonDisabled.bind(vault.stateProperty().isNotEqualTo(VaultState.NEEDS_MIGRATION).or(passwordField.textProperty().isEmpty()));
migrationButtonDisabled.bind(ObjectExpression.objectExpression(vault.stateProperty())
.isNotEqualTo(VaultState.Value.NEEDS_MIGRATION)
.or(passwordField.textProperty().isEmpty()));
window.setOnHiding(event -> passwordField.wipe());
} }
@FXML @FXML
@@ -101,7 +108,7 @@ public class MigrationRunController implements FxController {
public void migrate() { public void migrate() {
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.stateProperty().transition(VaultState.Value.NEEDS_MIGRATION, VaultState.Value.PROCESSING);
passwordField.setDisable(true); passwordField.setDisable(true);
ScheduledFuture<?> progressSyncTask = scheduler.scheduleAtFixedRate(() -> { ScheduledFuture<?> progressSyncTask = scheduler.scheduleAtFixedRate(() -> {
Platform.runLater(() -> { Platform.runLater(() -> {
@@ -110,15 +117,15 @@ public class MigrationRunController implements FxController {
}, 0, MIGRATION_PROGRESS_UPDATE_MILLIS, TimeUnit.MILLISECONDS); }, 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, this::migrationRequiresInput); migrators.migrate(vault.getPath(), VAULTCONFIG_FILENAME, MASTERKEY_FILENAME, password, this::migrationProgressChanged, this::migrationRequiresInput);
return migrators.needsMigration(vault.getPath(), MASTERKEY_FILENAME); return migrators.needsMigration(vault.getPath(), VAULTCONFIG_FILENAME, MASTERKEY_FILENAME);
}).onSuccess(needsAnotherMigration -> { }).onSuccess(needsAnotherMigration -> {
if (needsAnotherMigration) { if (needsAnotherMigration) {
LOG.info("Migration of '{}' succeeded, but another migration is required.", vault.getDisplayName()); LOG.info("Migration of '{}' succeeded, but another migration is required.", vault.getDisplayName());
vault.setState(VaultState.NEEDS_MIGRATION); vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
} else { } else {
LOG.info("Migration of '{}' succeeded.", vault.getDisplayName()); LOG.info("Migration of '{}' succeeded.", vault.getDisplayName());
vault.setState(VaultState.LOCKED); vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
passwordField.wipe(); passwordField.wipe();
window.setScene(successScene.get()); window.setScene(successScene.get());
} }
@@ -127,20 +134,20 @@ public class MigrationRunController implements FxController {
passwordField.setDisable(false); passwordField.setDisable(false);
passwordField.selectAll(); passwordField.selectAll();
passwordField.requestFocus(); passwordField.requestFocus();
vault.setState(VaultState.NEEDS_MIGRATION); vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
}).onError(FileSystemCapabilityChecker.MissingCapabilityException.class, e -> { }).onError(FileSystemCapabilityChecker.MissingCapabilityException.class, e -> {
LOG.error("Underlying file system not supported.", e); LOG.error("Underlying file system not supported.", e);
vault.setState(VaultState.NEEDS_MIGRATION); vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
missingCapability.set(e.getMissingCapability()); missingCapability.set(e.getMissingCapability());
window.setScene(capabilityErrorScene.get()); window.setScene(capabilityErrorScene.get());
}).onError(FileNameTooLongException.class, e -> { }).onError(FileNameTooLongException.class, e -> {
LOG.error("Migration failed because the underlying file system does not support long filenames.", e); LOG.error("Migration failed because the underlying file system does not support long filenames.", e);
vault.setState(VaultState.NEEDS_MIGRATION); vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
errorComponent.cause(e).window(window).returnToScene(startScene.get()).build().showErrorScene(); errorComponent.cause(e).window(window).returnToScene(startScene.get()).build().showErrorScene();
window.setScene(impossibleScene.get()); window.setScene(impossibleScene.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.NEEDS_MIGRATION); vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
errorComponent.cause(e).window(window).returnToScene(startScene.get()).build().showErrorScene(); errorComponent.cause(e).window(window).returnToScene(startScene.get()).build().showErrorScene();
}).andFinally(() -> { }).andFinally(() -> {
passwordField.setDisable(false); passwordField.setDisable(false);
@@ -10,11 +10,11 @@ import org.cryptomator.integrations.autostart.ToggleAutoStartFailedException;
import org.cryptomator.integrations.keychain.KeychainAccessProvider; import org.cryptomator.integrations.keychain.KeychainAccessProvider;
import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.traymenu.TrayMenuComponent;
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 javafx.application.Application; import javafx.application.Application;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
@@ -31,7 +31,6 @@ import java.util.Arrays;
import java.util.Optional; import java.util.Optional;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@PreferencesScoped @PreferencesScoped
@@ -41,11 +40,11 @@ public class GeneralPreferencesController implements FxController {
private final Stage window; private final Stage window;
private final Settings settings; private final Settings settings;
private final boolean trayMenuInitialized;
private final boolean trayMenuSupported; private final boolean trayMenuSupported;
private final Optional<AutoStartProvider> autoStartProvider; private final Optional<AutoStartProvider> autoStartProvider;
private final ObjectProperty<SelectedPreferencesTab> selectedTabProperty; private final ObjectProperty<SelectedPreferencesTab> selectedTabProperty;
private final LicenseHolder licenseHolder; private final LicenseHolder licenseHolder;
private final ExecutorService executor;
private final ResourceBundle resourceBundle; private final ResourceBundle resourceBundle;
private final Application application; private final Application application;
private final Environment environment; private final Environment environment;
@@ -53,6 +52,8 @@ public class GeneralPreferencesController implements FxController {
private final ErrorComponent.Builder errorComponent; private final ErrorComponent.Builder errorComponent;
public ChoiceBox<UiTheme> themeChoiceBox; public ChoiceBox<UiTheme> themeChoiceBox;
public ChoiceBox<KeychainBackend> keychainBackendChoiceBox; public ChoiceBox<KeychainBackend> keychainBackendChoiceBox;
public CheckBox showMinimizeButtonCheckbox;
public CheckBox showTrayIconCheckbox;
public CheckBox startHiddenCheckbox; public CheckBox startHiddenCheckbox;
public CheckBox debugModeCheckbox; public CheckBox debugModeCheckbox;
public CheckBox autoStartCheckbox; public CheckBox autoStartCheckbox;
@@ -60,16 +61,17 @@ public class GeneralPreferencesController implements FxController {
public RadioButton nodeOrientationLtr; public RadioButton nodeOrientationLtr;
public RadioButton nodeOrientationRtl; public RadioButton nodeOrientationRtl;
@Inject @Inject
GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, @Named("trayMenuSupported") boolean trayMenuSupported, Optional<AutoStartProvider> autoStartProvider, Set<KeychainAccessProvider> keychainAccessProviders, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ExecutorService executor, ResourceBundle resourceBundle, Application application, Environment environment, ErrorComponent.Builder errorComponent) { GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, TrayMenuComponent trayMenu, Optional<AutoStartProvider> autoStartProvider, Set<KeychainAccessProvider> keychainAccessProviders, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ResourceBundle resourceBundle, Application application, Environment environment, ErrorComponent.Builder errorComponent) {
this.window = window; this.window = window;
this.settings = settings; this.settings = settings;
this.trayMenuSupported = trayMenuSupported; this.trayMenuInitialized = trayMenu.isInitialized();
this.trayMenuSupported = trayMenu.isSupported();
this.autoStartProvider = autoStartProvider; this.autoStartProvider = autoStartProvider;
this.keychainAccessProviders = keychainAccessProviders; this.keychainAccessProviders = keychainAccessProviders;
this.selectedTabProperty = selectedTabProperty; this.selectedTabProperty = selectedTabProperty;
this.licenseHolder = licenseHolder; this.licenseHolder = licenseHolder;
this.executor = executor;
this.resourceBundle = resourceBundle; this.resourceBundle = resourceBundle;
this.application = application; this.application = application;
this.environment = environment; this.environment = environment;
@@ -85,6 +87,10 @@ public class GeneralPreferencesController implements FxController {
themeChoiceBox.valueProperty().bindBidirectional(settings.theme()); themeChoiceBox.valueProperty().bindBidirectional(settings.theme());
themeChoiceBox.setConverter(new UiThemeConverter(resourceBundle)); themeChoiceBox.setConverter(new UiThemeConverter(resourceBundle));
showMinimizeButtonCheckbox.selectedProperty().bindBidirectional(settings.showMinimizeButton());
showTrayIconCheckbox.selectedProperty().bindBidirectional(settings.showTrayIcon());
startHiddenCheckbox.selectedProperty().bindBidirectional(settings.startHidden()); startHiddenCheckbox.selectedProperty().bindBidirectional(settings.startHidden());
debugModeCheckbox.selectedProperty().bindBidirectional(settings.debugMode()); debugModeCheckbox.selectedProperty().bindBidirectional(settings.debugMode());
@@ -105,8 +111,12 @@ public class GeneralPreferencesController implements FxController {
return Arrays.stream(KeychainBackend.values()).filter(value -> namesOfAvailableProviders.contains(value.getProviderClass())).toArray(KeychainBackend[]::new); return Arrays.stream(KeychainBackend.values()).filter(value -> namesOfAvailableProviders.contains(value.getProviderClass())).toArray(KeychainBackend[]::new);
} }
public boolean isTrayMenuInitialized() {
return trayMenuInitialized;
}
public boolean isTrayMenuSupported() { public boolean isTrayMenuSupported() {
return this.trayMenuSupported; return trayMenuSupported;
} }
public boolean isAutoStartSupported() { public boolean isAutoStartSupported() {
@@ -147,8 +157,8 @@ public class GeneralPreferencesController implements FxController {
@FXML @FXML
public void showDonationTab() { public void showContributeTab() {
selectedTabProperty.set(SelectedPreferencesTab.DONATION_KEY); selectedTabProperty.set(SelectedPreferencesTab.CONTRIBUTE);
} }
@FXML @FXML
@@ -175,6 +185,7 @@ public class GeneralPreferencesController implements FxController {
public UiTheme fromString(String string) { public UiTheme fromString(String string) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
} }
private static class KeychainBackendConverter extends StringConverter<KeychainBackend> { private static class KeychainBackendConverter extends StringConverter<KeychainBackend> {
@@ -194,6 +205,6 @@ public class GeneralPreferencesController implements FxController {
public KeychainBackend fromString(String string) { public KeychainBackend fromString(String string) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
}
}
} }
@@ -26,7 +26,7 @@ public class PreferencesController implements FxController {
public Tab generalTab; public Tab generalTab;
public Tab volumeTab; public Tab volumeTab;
public Tab updatesTab; public Tab updatesTab;
public Tab donationKeyTab; public Tab contributeTab;
public Tab aboutTab; public Tab aboutTab;
@Inject @Inject
@@ -52,7 +52,7 @@ public class PreferencesController implements FxController {
return switch (selectedTab) { return switch (selectedTab) {
case UPDATES -> updatesTab; case UPDATES -> updatesTab;
case VOLUME -> volumeTab; case VOLUME -> volumeTab;
case DONATION_KEY -> donationKeyTab; case CONTRIBUTE -> contributeTab;
case GENERAL -> generalTab; case GENERAL -> generalTab;
case ABOUT -> aboutTab; case ABOUT -> aboutTab;
case ANY -> updateAvailable.get() ? updatesTab : generalTab; case ANY -> updateAvailable.get() ? updatesTab : generalTab;

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