Compare commits

...
225 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
Armin Schrenk 970216dd59 Merge tag '1.5.12' into develop
1.5.12
2021-02-04 10:49:52 +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
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 4b670a59a3 adjusted to new cryptolib/cryptofs API 2021-01-25 21:31:16 +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
Sebastian Stenzel cb2bc17283 New translations strings.properties (Hungarian)
[ci skip]
2021-01-21 11:06:22 +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
Sebastian Stenzel c0a9a95e4f Adjusted to CryptoFS 2.0.0 2020-12-08 14:39:46 +01:00
144 changed files with 3381 additions and 1188 deletions
+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
+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>
+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.12</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.12</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];
} }
@@ -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>
@@ -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);
} }
@@ -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,6 +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.function.Consumer;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class FuseVolume extends AbstractVolume { public class FuseVolume extends AbstractVolume {
@@ -35,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);
} }
} }
@@ -89,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();
@@ -100,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,24 +102,31 @@ 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 (!flags.contains(FileSystemFlags.READONLY) && 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);
} }
@@ -133,29 +143,51 @@ public class Vault {
} }
} }
public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, VolumeException, InvalidMountPointException { public synchronized void unlock(MasterkeyLoader keyLoader) throws CryptoException, IOException, VolumeException, InvalidMountPointException {
if (cryptoFileSystem.get() == null) { if (cryptoFileSystem.get() != null) {
CryptoFileSystem fs = createCryptoFileSystem(passphrase);
cryptoFileSystem.set(fs);
try {
volume = volumeProvider.get();
volume.mount(fs, getEffectiveMountFlags());
} catch (Exception e) {
destroyCryptoFileSystem();
throw e;
}
} else {
throw new IllegalStateException("Already unlocked."); 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();
}
}
} }
public synchronized void lock(boolean forced) throws VolumeException { 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()) { if (forced && volume.supportsForcedUnmount()) {
volume.unmountForced(); volume.unmountForced();
} else { } else {
volume.unmount(); volume.unmount();
} }
destroyCryptoFileSystem();
//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 { public void reveal(Volume.Revealer vaultRevealer) throws VolumeException {
@@ -166,16 +198,12 @@ public class Vault {
// 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() {
@@ -195,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() {
@@ -203,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() {
@@ -211,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() {
@@ -219,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() {
@@ -227,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() {
@@ -235,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() {
@@ -251,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 {
@@ -299,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};
} }
@@ -355,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,7 +34,7 @@ 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;
/** /**
* Reveals the mounted volume. * Reveals the mounted volume.
@@ -17,6 +17,7 @@ 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; import java.util.function.Supplier;
public class WebDavVolume implements Volume { public class WebDavVolume implements Volume {
@@ -31,6 +32,7 @@ public class WebDavVolume implements Volume {
private WebDavServer server; private WebDavServer server;
private WebDavServletController servlet; private WebDavServletController servlet;
private Mounter.Mount mount; private Mounter.Mount mount;
private Consumer<Throwable> onExitAction;
@Inject @Inject
public WebDavVolume(Provider<WebDavServer> serverProvider, VaultSettings vaultSettings, Settings settings, WindowsDriveLetters windowsDriveLetters) { public WebDavVolume(Provider<WebDavServer> serverProvider, VaultSettings vaultSettings, Settings settings, WindowsDriveLetters windowsDriveLetters) {
@@ -41,12 +43,13 @@ public class WebDavVolume implements Volume {
} }
@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); startServlet(fs);
mountServlet(); mountServlet();
this.onExitAction = onExitAction;
} }
private void startServlet(CryptoFileSystem fs){ private void startServlet(CryptoFileSystem fs) {
if (server == null) { if (server == null) {
server = serverProvider.get(); server = serverProvider.get();
} }
@@ -66,7 +69,7 @@ public class WebDavVolume implements Volume {
//on windows, prevent an automatic drive letter selection in the upstream library. Either we choose already a specifc one or there is no free. //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; Supplier<String> driveLetterSupplier;
if(System.getProperty("os.name").toLowerCase().contains("windows") && vaultSettings.winDriveLetter().isEmpty().get()) { if (System.getProperty("os.name").toLowerCase().contains("windows") && vaultSettings.winDriveLetter().isEmpty().get()) {
driveLetterSupplier = () -> windowsDriveLetters.getAvailableDriveLetter().orElse(null); driveLetterSupplier = () -> windowsDriveLetters.getAvailableDriveLetter().orElse(null);
} else { } else {
driveLetterSupplier = () -> vaultSettings.winDriveLetter().get(); driveLetterSupplier = () -> vaultSettings.winDriveLetter().get();
@@ -101,6 +104,7 @@ public class WebDavVolume implements Volume {
throw new VolumeException(e); throw new VolumeException(e);
} }
cleanup(); cleanup();
onExitAction.accept(null);
} }
@Override @Override
@@ -111,6 +115,7 @@ public class WebDavVolume implements Volume {
throw new VolumeException(e); throw new VolumeException(e);
} }
cleanup(); cleanup();
onExitAction.accept(null);
} }
@Override @Override
@@ -32,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")));
@@ -43,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")));
@@ -54,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")));
} }
@@ -88,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));
} }
@@ -97,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")));
@@ -107,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")));
@@ -117,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;
}
} }
@@ -21,13 +21,18 @@ public class SettingsJsonAdapterTest {
@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);
+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.12</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 -29
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.12</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.1</cryptomator.integrations.linux.version> <cryptomator.integrations.linux.version>1.0.0-beta1</cryptomator.integrations.linux.version>
<cryptomator.fuse.version>1.2.8</cryptomator.fuse.version> <cryptomator.fuse.version>1.3.1</cryptomator.fuse.version>
<cryptomator.dokany.version>1.2.3</cryptomator.dokany.version> <cryptomator.dokany.version>1.3.1</cryptomator.dokany.version>
<cryptomator.webdav.version>1.1.1</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.12.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>
@@ -224,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>
@@ -437,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.12</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);
@@ -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;
@@ -175,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;
@@ -44,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;
@@ -55,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;
@@ -86,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 {
@@ -113,15 +117,23 @@ public class FxApplication extends Application {
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.")));
}
}); });
} }
@@ -137,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);
} }
@@ -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;
@@ -55,7 +57,7 @@ class AppLaunchEventHandler {
case REVEAL_APP -> fxApplicationStarter.get().thenAccept(FxApplication::showMainWindow); case REVEAL_APP -> fxApplicationStarter.get().thenAccept(FxApplication::showMainWindow);
case OPEN_FILE -> fxApplicationStarter.get().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;
@@ -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);
} }
} }
} }
@@ -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);
} }
} }
@@ -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)
@@ -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 */
@@ -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);
@@ -157,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
@@ -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;
@@ -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;
@@ -32,8 +32,8 @@ abstract class PreferencesModule {
@Provides @Provides
@PreferencesWindow @PreferencesWindow
@PreferencesScoped @PreferencesScoped
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
@@ -49,8 +49,8 @@ abstract class PreferencesModule {
@Provides @Provides
@FxmlScene(FxmlFile.PREFERENCES) @FxmlScene(FxmlFile.PREFERENCES)
@PreferencesScoped @PreferencesScoped
static Scene providePreferencesScene(@PreferencesWindow FXMLLoaderFactory fxmlLoaders) { static Scene providePreferencesScene(@PreferencesWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/preferences.fxml"); return fxmlLoaders.createScene(FxmlFile.PREFERENCES);
} }
// ------------------ // ------------------
@@ -77,8 +77,8 @@ abstract class PreferencesModule {
@Binds @Binds
@IntoMap @IntoMap
@FxControllerKey(DonationKeyPreferencesController.class) @FxControllerKey(SupporterCertificateController.class)
abstract FxController bindDonationKeyPreferencesController(DonationKeyPreferencesController controller); abstract FxController bindSupporterCertificatePreferencesController(SupporterCertificateController controller);
@Binds @Binds
@IntoMap @IntoMap
@@ -22,9 +22,9 @@ public enum SelectedPreferencesTab {
UPDATES, UPDATES,
/** /**
* Show donation key tab * Show contribute tab
*/ */
DONATION_KEY, CONTRIBUTE,
/** /**
* Show about tab * Show about tab
@@ -14,17 +14,17 @@ import javafx.scene.control.TextArea;
import javafx.scene.control.TextFormatter; import javafx.scene.control.TextFormatter;
@PreferencesScoped @PreferencesScoped
public class DonationKeyPreferencesController implements FxController { public class SupporterCertificateController implements FxController {
private static final String DONATION_URI = "https://store.cryptomator.org/desktop"; private static final String SUPPORTER_URI = "https://store.cryptomator.org/desktop";
private final Application application; private final Application application;
private final LicenseHolder licenseHolder; private final LicenseHolder licenseHolder;
private final Settings settings; private final Settings settings;
public TextArea donationKeyField; public TextArea supporterCertificateField;
@Inject @Inject
DonationKeyPreferencesController(Application application, LicenseHolder licenseHolder, Settings settings) { SupporterCertificateController(Application application, LicenseHolder licenseHolder, Settings settings) {
this.application = application; this.application = application;
this.licenseHolder = licenseHolder; this.licenseHolder = licenseHolder;
this.settings = settings; this.settings = settings;
@@ -32,9 +32,9 @@ public class DonationKeyPreferencesController implements FxController {
@FXML @FXML
public void initialize() { public void initialize() {
donationKeyField.setText(licenseHolder.getLicenseKey().orElse(null)); supporterCertificateField.setText(licenseHolder.getLicenseKey().orElse(null));
donationKeyField.textProperty().addListener(this::registrationKeyChanged); supporterCertificateField.textProperty().addListener(this::registrationKeyChanged);
donationKeyField.setTextFormatter(new TextFormatter<>(this::checkVaultNameLength)); supporterCertificateField.setTextFormatter(new TextFormatter<>(this::checkVaultNameLength));
} }
private TextFormatter.Change checkVaultNameLength(TextFormatter.Change change) { private TextFormatter.Change checkVaultNameLength(TextFormatter.Change change) {
@@ -53,8 +53,8 @@ public class DonationKeyPreferencesController implements FxController {
} }
@FXML @FXML
public void getDonationKey() { public void getSupporterCertificate() {
application.getHostServices().showDocument(DONATION_URI); application.getHostServices().showDocument(SUPPORTER_URI);
} }
public LicenseHolder getLicenseHolder() { public LicenseHolder getLicenseHolder() {
@@ -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;
@@ -25,8 +25,8 @@ abstract class QuitModule {
@Provides @Provides
@QuitWindow @QuitWindow
@QuitScoped @QuitScoped
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
@@ -43,8 +43,8 @@ abstract class QuitModule {
@Provides @Provides
@FxmlScene(FxmlFile.QUIT) @FxmlScene(FxmlFile.QUIT)
@QuitScoped @QuitScoped
static Scene provideQuitScene(@QuitWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideQuitScene(@QuitWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/quit.fxml"); return fxmlLoaders.createScene(FxmlFile.QUIT);
} }
// ------------------ // ------------------
@@ -2,6 +2,7 @@ package org.cryptomator.ui.recoverykey;
import dagger.Lazy; import dagger.Lazy;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.ui.common.Animations; import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.ErrorComponent;
@@ -80,7 +81,7 @@ public class RecoveryKeyCreationController implements FxController {
} }
@Override @Override
protected String call() throws IOException { protected String call() throws IOException, CryptoException {
return recoveryKeyFactory.createRecoveryKey(vault.getPath(), passwordField.getCharacters()); return recoveryKeyFactory.createRecoveryKey(vault.getPath(), passwordField.getCharacters());
} }
@@ -2,28 +2,34 @@ package org.cryptomator.ui.recoverykey;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.hash.Hashing; import com.google.common.hash.Hashing;
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.api.Masterkey;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
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;
@Singleton @Singleton
public class RecoveryKeyFactory { public class RecoveryKeyFactory {
private static final byte[] PEPPER = new byte[0];
private final WordEncoder wordEncoder; private final WordEncoder wordEncoder;
private final MasterkeyFileAccess masterkeyFileAccess;
@Inject @Inject
public RecoveryKeyFactory(WordEncoder wordEncoder) { public RecoveryKeyFactory(WordEncoder wordEncoder, MasterkeyFileAccess masterkeyFileAccess) {
this.wordEncoder = wordEncoder; this.wordEncoder = wordEncoder;
this.masterkeyFileAccess = masterkeyFileAccess;
} }
public Collection<String> getDictionary() { public Collection<String> getDictionary() {
@@ -36,11 +42,14 @@ public class RecoveryKeyFactory {
* @return The recovery key of the vault at the given path * @return The recovery key of the vault at the given path
* @throws IOException If the masterkey file could not be read * @throws IOException If the masterkey file could not be read
* @throws InvalidPassphraseException If the provided password is wrong * @throws InvalidPassphraseException If the provided password is wrong
* @throws CryptoException In case of other cryptographic errors
* @apiNote This is a long-running operation and should be invoked in a background thread * @apiNote This is a long-running operation and should be invoked in a background thread
*/ */
public String createRecoveryKey(Path vaultPath, CharSequence password) throws IOException, InvalidPassphraseException { public String createRecoveryKey(Path vaultPath, CharSequence password) throws IOException, InvalidPassphraseException, CryptoException {
byte[] rawKey = CryptoFileSystemProvider.exportRawKey(vaultPath, MASTERKEY_FILENAME, PEPPER, password); Path masterkeyPath = vaultPath.resolve(MASTERKEY_FILENAME);
try { byte[] rawKey = new byte[0];
try (var masterkey = masterkeyFileAccess.load(masterkeyPath, password)) {
rawKey = masterkey.getEncoded();
return createRecoveryKey(rawKey); return createRecoveryKey(rawKey);
} finally { } finally {
Arrays.fill(rawKey, (byte) 0x00); Arrays.fill(rawKey, (byte) 0x00);
@@ -72,8 +81,15 @@ public class RecoveryKeyFactory {
*/ */
public void resetPasswordWithRecoveryKey(Path vaultPath, String recoveryKey, CharSequence newPassword) throws IOException, IllegalArgumentException { public void resetPasswordWithRecoveryKey(Path vaultPath, String recoveryKey, CharSequence newPassword) throws IOException, IllegalArgumentException {
final byte[] rawKey = decodeRecoveryKey(recoveryKey); final byte[] rawKey = decodeRecoveryKey(recoveryKey);
try { try (var masterkey = new Masterkey(rawKey)) {
CryptoFileSystemProvider.restoreRawKey(vaultPath, MASTERKEY_FILENAME, rawKey, PEPPER, newPassword); Path masterkeyPath = vaultPath.resolve(MASTERKEY_FILENAME);
if (Files.exists(masterkeyPath)) {
byte[] oldMasterkeyBytes = Files.readAllBytes(masterkeyPath);
// TODO: deduplicate with ChangePasswordController:
Path backupKeyPath = vaultPath.resolve(MASTERKEY_FILENAME + MasterkeyBackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + MASTERKEY_BACKUP_SUFFIX);
Files.move(masterkeyPath, backupKeyPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
}
masterkeyFileAccess.persist(masterkey, masterkeyPath, newPassword);
} finally { } finally {
Arrays.fill(rawKey, (byte) 0x00); Arrays.fill(rawKey, (byte) 0x00);
} }
@@ -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;
@@ -33,8 +33,8 @@ abstract class RecoveryKeyModule {
@Provides @Provides
@RecoveryKeyWindow @RecoveryKeyWindow
@RecoveryKeyScoped @RecoveryKeyScoped
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
@@ -69,29 +69,29 @@ abstract class RecoveryKeyModule {
@Provides @Provides
@FxmlScene(FxmlFile.RECOVERYKEY_CREATE) @FxmlScene(FxmlFile.RECOVERYKEY_CREATE)
@RecoveryKeyScoped @RecoveryKeyScoped
static Scene provideRecoveryKeyCreationScene(@RecoveryKeyWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideRecoveryKeyCreationScene(@RecoveryKeyWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/recoverykey_create.fxml"); return fxmlLoaders.createScene(FxmlFile.RECOVERYKEY_CREATE);
} }
@Provides @Provides
@FxmlScene(FxmlFile.RECOVERYKEY_SUCCESS) @FxmlScene(FxmlFile.RECOVERYKEY_SUCCESS)
@RecoveryKeyScoped @RecoveryKeyScoped
static Scene provideRecoveryKeySuccessScene(@RecoveryKeyWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideRecoveryKeySuccessScene(@RecoveryKeyWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/recoverykey_success.fxml"); return fxmlLoaders.createScene(FxmlFile.RECOVERYKEY_SUCCESS);
} }
@Provides @Provides
@FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER)
@RecoveryKeyScoped @RecoveryKeyScoped
static Scene provideRecoveryKeyRecoverScene(@RecoveryKeyWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideRecoveryKeyRecoverScene(@RecoveryKeyWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/recoverykey_recover.fxml"); return fxmlLoaders.createScene(FxmlFile.RECOVERYKEY_RECOVER);
} }
@Provides @Provides
@FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD)
@RecoveryKeyScoped @RecoveryKeyScoped
static Scene provideRecoveryKeyResetPasswordScene(@RecoveryKeyWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideRecoveryKeyResetPasswordScene(@RecoveryKeyWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/recoverykey_reset_password.fxml"); return fxmlLoaders.createScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD);
} }
// ------------------ // ------------------
@@ -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;
@@ -26,8 +26,8 @@ abstract class RemoveVaultModule {
@Provides @Provides
@RemoveVaultWindow @RemoveVaultWindow
@RemoveVaultScoped @RemoveVaultScoped
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
@@ -45,8 +45,8 @@ abstract class RemoveVaultModule {
@Provides @Provides
@FxmlScene(FxmlFile.REMOVE_VAULT) @FxmlScene(FxmlFile.REMOVE_VAULT)
@RemoveVaultScoped @RemoveVaultScoped
static Scene provideRemoveVaultScene(@RemoveVaultWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideRemoveVaultScene(@RemoveVaultWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/remove_vault.fxml"); return fxmlLoaders.createScene(FxmlFile.REMOVE_VAULT);
} }
// ------------------ // ------------------
@@ -11,7 +11,7 @@ import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState; import org.cryptomator.common.vaults.VaultState;
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 VaultStatisticsModule {
@Provides @Provides
@VaultStatisticsWindow @VaultStatisticsWindow
@VaultStatisticsScoped @VaultStatisticsScoped
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
@@ -43,8 +43,8 @@ abstract class VaultStatisticsModule {
var weakStage = new WeakReference<>(stage); var weakStage = new WeakReference<>(stage);
vault.stateProperty().addListener(new ChangeListener<>() { vault.stateProperty().addListener(new ChangeListener<>() {
@Override @Override
public void changed(ObservableValue<? extends VaultState> observable, VaultState oldValue, VaultState newValue) { public void changed(ObservableValue<? extends VaultState.Value> observable, VaultState.Value oldValue, VaultState.Value newValue) {
if (newValue != VaultState.UNLOCKED) { if (newValue != VaultState.Value.UNLOCKED) {
Stage stage = weakStage.get(); Stage stage = weakStage.get();
if (stage != null) { if (stage != null) {
stage.hide(); stage.hide();
@@ -60,8 +60,8 @@ abstract class VaultStatisticsModule {
@Provides @Provides
@FxmlScene(FxmlFile.VAULT_STATISTICS) @FxmlScene(FxmlFile.VAULT_STATISTICS)
@VaultStatisticsScoped @VaultStatisticsScoped
static Scene provideVaultStatisticsScene(@VaultStatisticsWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideVaultStatisticsScene(@VaultStatisticsWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/stats.fxml"); return fxmlLoaders.createScene(FxmlFile.VAULT_STATISTICS);
} }
// ------------------ // ------------------
@@ -44,6 +44,9 @@ class TrayMenuController {
public void initTrayMenu() { public void initTrayMenu() {
vaults.addListener(this::vaultListChanged); vaults.addListener(this::vaultListChanged);
vaults.forEach(v -> {
v.displayNameProperty().addListener(this::vaultListChanged);
});
rebuildMenu(); rebuildMenu();
} }
@@ -0,0 +1,14 @@
package org.cryptomator.ui.unlock;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
public class UnlockCancelledException extends MasterkeyLoadingFailedException {
public UnlockCancelledException(String message) {
super(message);
}
public UnlockCancelledException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -1,15 +1,11 @@
package org.cryptomator.ui.unlock; package org.cryptomator.ui.unlock;
import dagger.Lazy;
import org.cryptomator.common.vaults.MountPointRequirement; import org.cryptomator.common.vaults.MountPointRequirement;
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.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Inject; import javax.inject.Inject;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage; import javafx.stage.Stage;
//At the current point in time only the CustomMountPointChooser may cause this window to be shown. //At the current point in time only the CustomMountPointChooser may cause this window to be shown.
@@ -17,19 +13,17 @@ import javafx.stage.Stage;
public class UnlockInvalidMountPointController implements FxController { public class UnlockInvalidMountPointController implements FxController {
private final Stage window; private final Stage window;
private final Lazy<Scene> unlockScene;
private final Vault vault; private final Vault vault;
@Inject @Inject
UnlockInvalidMountPointController(@UnlockWindow Stage window, @FxmlScene(FxmlFile.UNLOCK) Lazy<Scene> unlockScene, @UnlockWindow Vault vault) { UnlockInvalidMountPointController(@UnlockWindow Stage window, @UnlockWindow Vault vault) {
this.window = window; this.window = window;
this.unlockScene = unlockScene;
this.vault = vault; this.vault = vault;
} }
@FXML @FXML
public void back() { public void close() {
window.setScene(unlockScene.get()); window.close();
} }
/* Getter/Setter */ /* Getter/Setter */
@@ -4,20 +4,16 @@ import dagger.Binds;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import dagger.multibindings.IntoMap; import dagger.multibindings.IntoMap;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.common.DefaultSceneFactory; import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey; import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory; import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.common.UserInteractionLock; import org.cryptomator.ui.keyloading.KeyLoadingComponent;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent; import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Provider; import javax.inject.Provider;
@@ -27,59 +23,15 @@ import javafx.stage.Stage;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@Module(subcomponents = {ForgetPasswordComponent.class}) @Module(subcomponents = {KeyLoadingComponent.class})
abstract class UnlockModule { abstract class UnlockModule {
private static final Logger LOG = LoggerFactory.getLogger(UnlockModule.class);
public enum PasswordEntry {
PASSWORD_ENTERED,
CANCELED
}
@Provides
@UnlockScoped
static UserInteractionLock<PasswordEntry> providePasswordEntryLock() {
return new UserInteractionLock<>(null);
}
@Provides
@Named("savedPassword")
@UnlockScoped
static Optional<char[]> provideStoredPassword(KeychainManager keychain, @UnlockWindow 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
@UnlockScoped
static AtomicReference<char[]> providePassword(@Named("savedPassword") Optional<char[]> storedPassword) {
return new AtomicReference(storedPassword.orElse(null));
}
@Provides
@Named("savePassword")
@UnlockScoped
static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional<char[]> storedPassword) {
return new AtomicBoolean(storedPassword.isPresent());
}
@Provides @Provides
@UnlockWindow @UnlockWindow
@UnlockScoped @UnlockScoped
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
@@ -99,33 +51,28 @@ abstract class UnlockModule {
} }
@Provides @Provides
@FxmlScene(FxmlFile.UNLOCK) @UnlockWindow
@UnlockScoped @UnlockScoped
static Scene provideUnlockScene(@UnlockWindow FXMLLoaderFactory fxmlLoaders) { static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @UnlockWindow Vault vault, @UnlockWindow Stage window) {
return fxmlLoaders.createScene("/fxml/unlock.fxml"); return compBuilder.vault(vault).window(window).build().keyloadingStrategy();
} }
@Provides @Provides
@FxmlScene(FxmlFile.UNLOCK_SUCCESS) @FxmlScene(FxmlFile.UNLOCK_SUCCESS)
@UnlockScoped @UnlockScoped
static Scene provideUnlockSuccessScene(@UnlockWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideUnlockSuccessScene(@UnlockWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/unlock_success.fxml"); return fxmlLoaders.createScene(FxmlFile.UNLOCK_SUCCESS);
} }
@Provides @Provides
@FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT)
@UnlockScoped @UnlockScoped
static Scene provideInvalidMountPointScene(@UnlockWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideInvalidMountPointScene(@UnlockWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/unlock_invalid_mount_point.fxml"); return fxmlLoaders.createScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT);
} }
// ------------------ // ------------------
@Binds
@IntoMap
@FxControllerKey(UnlockController.class)
abstract FxController bindUnlockController(UnlockController controller);
@Binds @Binds
@IntoMap @IntoMap
@FxControllerKey(UnlockSuccessController.class) @FxControllerKey(UnlockSuccessController.class)
@@ -1,40 +1,31 @@
package org.cryptomator.ui.unlock; package org.cryptomator.ui.unlock;
import dagger.Lazy; import dagger.Lazy;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.mountpoint.InvalidMountPointException; import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.vaults.MountPointRequirement; import org.cryptomator.common.vaults.MountPointRequirement;
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.VolumeException; import org.cryptomator.common.vaults.Volume.VolumeException;
import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.integrations.keychain.KeychainAccessException; import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent; 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.VaultService; import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.unlock.UnlockModule.PasswordEntry; import org.cryptomator.ui.keyloading.KeyLoadingComponent;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
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.Platform; import javafx.application.Platform;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.stage.Stage; import javafx.stage.Stage;
import javafx.stage.Window;
import java.io.IOException; import java.io.IOException;
import java.nio.CharBuffer;
import java.nio.file.DirectoryNotEmptyException; import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileAlreadyExistsException;
import java.nio.file.NotDirectoryException; import java.nio.file.NotDirectoryException;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/** /**
* A multi-step task that consists of background activities as well as user interaction. * A multi-step task that consists of background activities as well as user interaction.
@@ -49,113 +40,47 @@ public class UnlockWorkflow extends Task<Boolean> {
private final Stage window; private final Stage window;
private final Vault vault; private final Vault vault;
private final VaultService vaultService; private final VaultService vaultService;
private final AtomicReference<char[]> password;
private final AtomicBoolean savePassword;
private final Optional<char[]> savedPassword;
private final UserInteractionLock<PasswordEntry> passwordEntryLock;
private final KeychainManager keychain;
private final Lazy<Scene> unlockScene;
private final Lazy<Scene> successScene; private final Lazy<Scene> successScene;
private final Lazy<Scene> invalidMountPointScene; private final Lazy<Scene> invalidMountPointScene;
private final ErrorComponent.Builder errorComponent; private final ErrorComponent.Builder errorComponent;
private final KeyLoadingStrategy keyLoadingStrategy;
@Inject @Inject
UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<PasswordEntry> passwordEntryLock, KeychainManager keychain, @FxmlScene(FxmlFile.UNLOCK) Lazy<Scene> unlockScene, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, ErrorComponent.Builder errorComponent) { UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, ErrorComponent.Builder errorComponent, @UnlockWindow KeyLoadingStrategy keyLoadingStrategy) {
this.window = window; this.window = window;
this.vault = vault; this.vault = vault;
this.vaultService = vaultService; this.vaultService = vaultService;
this.password = password;
this.savePassword = savePassword;
this.savedPassword = savedPassword;
this.passwordEntryLock = passwordEntryLock;
this.keychain = keychain;
this.unlockScene = unlockScene;
this.successScene = successScene; this.successScene = successScene;
this.invalidMountPointScene = invalidMountPointScene; this.invalidMountPointScene = invalidMountPointScene;
this.errorComponent = errorComponent; this.errorComponent = errorComponent;
this.keyLoadingStrategy = keyLoadingStrategy;
setOnFailed(event -> {
Throwable throwable = event.getSource().getException();
if (throwable instanceof InvalidMountPointException) {
handleInvalidMountPoint((InvalidMountPointException) throwable);
} else {
handleGenericError(throwable);
}
});
} }
@Override @Override
protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException { protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException, CryptoException {
try { try {
if (attemptUnlock()) { attemptUnlock();
handleSuccess(); return true;
return true; } catch (UnlockCancelledException e) {
cancel(false); // set Tasks state to cancelled
return false;
}
}
private void attemptUnlock() throws IOException, VolumeException, InvalidMountPointException, CryptoException {
boolean success = false;
try {
vault.unlock(keyLoadingStrategy);
success = true;
} catch (MasterkeyLoadingFailedException e) {
if (keyLoadingStrategy.recoverFromException(e)) {
LOG.info("Unlock attempt threw {}. Reattempting...", e.getClass().getSimpleName());
attemptUnlock();
} else { } else {
cancel(false); // set Tasks state to cancelled throw e;
return false;
} }
} finally { } finally {
wipePassword(password.get()); keyLoadingStrategy.cleanup(success);
wipePassword(savedPassword.orElse(null));
}
}
private boolean attemptUnlock() throws InterruptedException, IOException, VolumeException, InvalidMountPointException {
boolean proceed = password.get() != null || askForPassword(false) == PasswordEntry.PASSWORD_ENTERED;
while (proceed) {
try {
vault.unlock(CharBuffer.wrap(password.get()));
return true;
} catch (InvalidPassphraseException e) {
proceed = askForPassword(true) == PasswordEntry.PASSWORD_ENTERED;
}
}
return false;
}
private PasswordEntry askForPassword(boolean animateShake) throws InterruptedException {
Platform.runLater(() -> {
window.setScene(unlockScene.get());
window.show();
Window owner = window.getOwner();
if (owner != null) {
window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2);
window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2);
} else {
window.centerOnScreen();
}
if (animateShake) {
Animations.createShakeWindowAnimation(window).play();
}
});
return passwordEntryLock.awaitInteraction();
}
private void handleSuccess() {
LOG.info("Unlock of '{}' succeeded.", vault.getDisplayName());
if (savePassword.get()) {
savePasswordToSystemkeychain();
}
switch (vault.getVaultSettings().actionAfterUnlock().get()) {
case ASK -> Platform.runLater(() -> {
window.setScene(successScene.get());
window.show();
});
case REVEAL -> {
Platform.runLater(window::close);
vaultService.reveal(vault);
}
case IGNORE -> Platform.runLater(window::close);
}
}
private void savePasswordToSystemkeychain() {
if (keychain.isSupported()) {
try {
keychain.storePassphrase(vault.getId(), CharBuffer.wrap(password.get()));
} catch (KeychainAccessException e) {
LOG.error("Failed to store passphrase in system keychain.", e);
}
} }
} }
@@ -173,15 +98,12 @@ public class UnlockWorkflow extends Task<Boolean> {
LOG.error("Unlock failed. Mountpoint doesn't exist (needs to be a folder): {}", cause.getMessage()); LOG.error("Unlock failed. Mountpoint doesn't exist (needs to be a folder): {}", cause.getMessage());
} }
showInvalidMountPointScene(); showInvalidMountPointScene();
return;
} else if (cause instanceof FileAlreadyExistsException) { } else if (cause instanceof FileAlreadyExistsException) {
LOG.error("Unlock failed. Mountpoint already exists: {}", cause.getMessage()); LOG.error("Unlock failed. Mountpoint already exists: {}", cause.getMessage());
showInvalidMountPointScene(); showInvalidMountPointScene();
return;
} else if (cause instanceof DirectoryNotEmptyException) { } else if (cause instanceof DirectoryNotEmptyException) {
LOG.error("Unlock failed. Mountpoint not an empty directory: {}", cause.getMessage()); LOG.error("Unlock failed. Mountpoint not an empty directory: {}", cause.getMessage());
showInvalidMountPointScene(); showInvalidMountPointScene();
return;
} else { } else {
handleGenericError(impExc); handleGenericError(impExc);
} }
@@ -196,33 +118,44 @@ public class UnlockWorkflow extends Task<Boolean> {
private void handleGenericError(Throwable e) { private void handleGenericError(Throwable e) {
LOG.error("Unlock failed for technical reasons.", e); LOG.error("Unlock failed for technical reasons.", e);
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene(); errorComponent.cause(e).window(window).build().showErrorScene();
}
private void wipePassword(char[] pw) {
if (pw != null) {
Arrays.fill(pw, ' ');
}
}
@Override
protected void scheduled() {
vault.setState(VaultState.PROCESSING);
} }
@Override @Override
protected void succeeded() { protected void succeeded() {
vault.setState(VaultState.UNLOCKED); LOG.info("Unlock of '{}' succeeded.", vault.getDisplayName());
switch (vault.getVaultSettings().actionAfterUnlock().get()) {
case ASK -> Platform.runLater(() -> {
window.setScene(successScene.get());
window.show();
});
case REVEAL -> {
Platform.runLater(window::close);
vaultService.reveal(vault);
}
case IGNORE -> Platform.runLater(window::close);
}
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
} }
@Override @Override
protected void failed() { protected void failed() {
vault.setState(VaultState.LOCKED); LOG.info("Unlock of '{}' failed.", vault.getDisplayName());
Throwable throwable = super.getException();
if (throwable instanceof InvalidMountPointException e) {
handleInvalidMountPoint(e);
} else {
handleGenericError(throwable);
}
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
} }
@Override @Override
protected void cancelled() { protected void cancelled() {
vault.setState(VaultState.LOCKED); LOG.debug("Unlock of '{}' canceled.", vault.getDisplayName());
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
} }
} }
@@ -0,0 +1,23 @@
package org.cryptomator.ui.vaultoptions;
public enum SelectedVaultOptionsTab {
/**
* Let the controller decide which tab to show.
*/
ANY,
/**
* Show general tab
*/
GENERAL,
/**
* Show mounting tab
*/
MOUNT,
/**
* Show password tab
*/
KEY,
}
@@ -12,6 +12,7 @@ import org.cryptomator.common.vaults.Vault;
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 javafx.beans.property.ObjectProperty;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.stage.Stage; import javafx.stage.Stage;
@@ -25,7 +26,10 @@ public interface VaultOptionsComponent {
@FxmlScene(FxmlFile.VAULT_OPTIONS) @FxmlScene(FxmlFile.VAULT_OPTIONS)
Lazy<Scene> scene(); Lazy<Scene> scene();
default void showVaultOptionsWindow() { ObjectProperty<SelectedVaultOptionsTab> selectedTabProperty();
default void showVaultOptionsWindow(SelectedVaultOptionsTab selectedTab) {
selectedTabProperty().set(selectedTab);
Stage stage = window(); Stage stage = window();
stage.setScene(scene().get()); stage.setScene(scene().get());
stage.show(); stage.show();
@@ -1,13 +1,67 @@
package org.cryptomator.ui.vaultoptions; package org.cryptomator.ui.vaultoptions;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject; import javax.inject.Inject;
import javafx.beans.property.ObjectProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
@VaultOptionsScoped @VaultOptionsScoped
public class VaultOptionsController implements FxController { public class VaultOptionsController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(VaultOptionsController.class);
private final Stage window;
private final ObjectProperty<SelectedVaultOptionsTab> selectedTabProperty;
public TabPane tabPane;
public Tab generalTab;
public Tab mountTab;
public Tab keyTab;
@Inject @Inject
VaultOptionsController() {} VaultOptionsController(@VaultOptionsWindow Stage window, ObjectProperty<SelectedVaultOptionsTab> selectedTabProperty) {
this.window = window;
this.selectedTabProperty = selectedTabProperty;
}
@FXML
public void initialize() {
window.setOnShowing(this::windowWillAppear);
selectedTabProperty.addListener(observable -> this.selectChosenTab());
tabPane.getSelectionModel().selectedItemProperty().addListener(observable -> this.selectedTabChanged());
}
private void selectChosenTab() {
Tab toBeSelected = getTabToSelect(selectedTabProperty.get());
tabPane.getSelectionModel().select(toBeSelected);
}
private Tab getTabToSelect(SelectedVaultOptionsTab selectedTab) {
return switch (selectedTab) {
case ANY, GENERAL -> generalTab;
case MOUNT -> mountTab;
case KEY -> keyTab;
};
}
private void selectedTabChanged() {
Tab selectedTab = tabPane.getSelectionModel().getSelectedItem();
try {
SelectedVaultOptionsTab selectedVaultOptionsTab = SelectedVaultOptionsTab.valueOf(selectedTab.getId());
selectedTabProperty.set(selectedVaultOptionsTab);
} catch (IllegalArgumentException e) {
LOG.error("Unknown vault options tab id: {}", selectedTab.getId());
}
}
private void windowWillAppear(@SuppressWarnings("unused") WindowEvent windowEvent) {
selectChosenTab();
}
} }
@@ -7,16 +7,18 @@ import dagger.multibindings.IntoMap;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.changepassword.ChangePasswordComponent; import org.cryptomator.ui.changepassword.ChangePasswordComponent;
import org.cryptomator.ui.common.DefaultSceneFactory; import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey; import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory; import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindow; import org.cryptomator.ui.mainwindow.MainWindow;
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent; import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
import javax.inject.Provider; import javax.inject.Provider;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.stage.Modality; import javafx.stage.Modality;
import javafx.stage.Stage; import javafx.stage.Stage;
@@ -26,11 +28,17 @@ import java.util.ResourceBundle;
@Module(subcomponents = {ChangePasswordComponent.class, RecoveryKeyComponent.class}) @Module(subcomponents = {ChangePasswordComponent.class, RecoveryKeyComponent.class})
abstract class VaultOptionsModule { abstract class VaultOptionsModule {
@Provides
@VaultOptionsScoped
static ObjectProperty<SelectedVaultOptionsTab> provideSelectedTabProperty() {
return new SimpleObjectProperty<>(SelectedVaultOptionsTab.ANY);
}
@Provides @Provides
@VaultOptionsWindow @VaultOptionsWindow
@VaultOptionsScoped @VaultOptionsScoped
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
@@ -50,8 +58,8 @@ abstract class VaultOptionsModule {
@Provides @Provides
@FxmlScene(FxmlFile.VAULT_OPTIONS) @FxmlScene(FxmlFile.VAULT_OPTIONS)
@VaultOptionsScoped @VaultOptionsScoped
static Scene provideVaultOptionsScene(@VaultOptionsWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideVaultOptionsScene(@VaultOptionsWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/vault_options.fxml"); return fxmlLoaders.createScene(FxmlFile.VAULT_OPTIONS);
} }
// ------------------ // ------------------
@@ -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;
@@ -26,8 +26,8 @@ abstract class WrongFileAlertModule {
@Provides @Provides
@WrongFileAlertWindow @WrongFileAlertWindow
@WrongFileAlertScoped @WrongFileAlertScoped
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
@@ -45,8 +45,8 @@ abstract class WrongFileAlertModule {
@Provides @Provides
@FxmlScene(FxmlFile.WRONGFILEALERT) @FxmlScene(FxmlFile.WRONGFILEALERT)
@WrongFileAlertScoped @WrongFileAlertScoped
static Scene provideWrongFileAlertScene(@WrongFileAlertWindow FXMLLoaderFactory fxmlLoaders) { static Scene provideWrongFileAlertScene(@WrongFileAlertWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/wrongfilealert.fxml"); return fxmlLoaders.createScene(FxmlFile.WRONGFILEALERT);
} }
// ------------------ // ------------------
@@ -20,6 +20,8 @@
alignment="CENTER_LEFT"> alignment="CENTER_LEFT">
<fx:define> <fx:define>
<ToggleGroup fx:id="predefinedLocationToggler"/> <ToggleGroup fx:id="predefinedLocationToggler"/>
<FontAwesome5IconView fx:id="badLocation" styleClass="glyph-icon-red" glyph="TIMES" />
<FontAwesome5IconView fx:id="goodLocation" styleClass="glyph-icon-primary" glyph="CHECK" />
</fx:define> </fx:define>
<padding> <padding>
<Insets topRightBottomLeft="24"/> <Insets topRightBottomLeft="24"/>
@@ -33,6 +35,8 @@
<RadioButton fx:id="dropboxRadioButton" toggleGroup="${predefinedLocationToggler}" text="Dropbox" visible="${controller.locationPresets.foundDropbox}" managed="${controller.locationPresets.foundDropbox}"/> <RadioButton fx:id="dropboxRadioButton" toggleGroup="${predefinedLocationToggler}" text="Dropbox" visible="${controller.locationPresets.foundDropbox}" managed="${controller.locationPresets.foundDropbox}"/>
<RadioButton fx:id="gdriveRadioButton" toggleGroup="${predefinedLocationToggler}" text="Google Drive" visible="${controller.locationPresets.foundGdrive}" managed="${controller.locationPresets.foundGdrive}"/> <RadioButton fx:id="gdriveRadioButton" toggleGroup="${predefinedLocationToggler}" text="Google Drive" visible="${controller.locationPresets.foundGdrive}" managed="${controller.locationPresets.foundGdrive}"/>
<RadioButton fx:id="onedriveRadioButton" toggleGroup="${predefinedLocationToggler}" text="OneDrive" visible="${controller.locationPresets.foundOnedrive}" managed="${controller.locationPresets.foundOnedrive}"/> <RadioButton fx:id="onedriveRadioButton" toggleGroup="${predefinedLocationToggler}" text="OneDrive" visible="${controller.locationPresets.foundOnedrive}" managed="${controller.locationPresets.foundOnedrive}"/>
<RadioButton fx:id="megaRadioButton" toggleGroup="${predefinedLocationToggler}" text="MEGA" visible="${controller.locationPresets.foundMega}" managed="${controller.locationPresets.foundMega}"/>
<RadioButton fx:id="pcloudRadioButton" toggleGroup="${predefinedLocationToggler}" text="pCloud" visible="${controller.locationPresets.foundPcloud}" managed="${controller.locationPresets.foundPcloud}"/>
<HBox spacing="12" alignment="CENTER_LEFT"> <HBox spacing="12" alignment="CENTER_LEFT">
<RadioButton fx:id="customRadioButton" toggleGroup="${predefinedLocationToggler}" text="%addvaultwizard.new.directoryPickerLabel"/> <RadioButton fx:id="customRadioButton" toggleGroup="${predefinedLocationToggler}" text="%addvaultwizard.new.directoryPickerLabel"/>
<Button contentDisplay="LEFT" text="%addvaultwizard.new.directoryPickerButton" onAction="#chooseCustomVaultPath" disable="${controller.usePresetPath}"> <Button contentDisplay="LEFT" text="%addvaultwizard.new.directoryPickerButton" onAction="#chooseCustomVaultPath" disable="${controller.usePresetPath}">
@@ -47,12 +51,8 @@
<VBox spacing="6"> <VBox spacing="6">
<Label text="%addvaultwizard.new.locationLabel" labelFor="$locationTextField"/> <Label text="%addvaultwizard.new.locationLabel" labelFor="$locationTextField"/>
<TextField fx:id="locationTextField" promptText="%addvaultwizard.new.locationPrompt" text="${controller.vaultPath}" disable="true" HBox.hgrow="ALWAYS"/> <TextField promptText="%addvaultwizard.new.locationPrompt" text="${controller.vaultPath}" editable="false" disable="${!controller.anyRadioButtonSelected}" HBox.hgrow="ALWAYS"/>
<Label text="${controller.warningText}" wrapText="true" visible="${controller.showWarning}"> <Label fx:id="vaultPathStatus" styleClass="label-muted" alignment="CENTER_RIGHT" wrapText="true" visible="${controller.anyRadioButtonSelected}" maxWidth="Infinity" graphicTextGap="6" text="${controller.statusText}" graphic="${controller.statusGraphic}" />
<graphic>
<FontAwesome5IconView glyph="EXCLAMATION_TRIANGLE"/>
</graphic>
</Label>
</VBox> </VBox>
<Region VBox.vgrow="ALWAYS"/> <Region VBox.vgrow="ALWAYS"/>
@@ -38,12 +38,12 @@
<fx:include source="/fxml/preferences_updates.fxml"/> <fx:include source="/fxml/preferences_updates.fxml"/>
</content> </content>
</Tab> </Tab>
<Tab fx:id="donationKeyTab" id="DONATION_KEY" text="%preferences.donationKey"> <Tab fx:id="contributeTab" id="CONTRIBUTE" text="%preferences.contribute">
<graphic> <graphic>
<FontAwesome5IconView glyph="HEART"/> <FontAwesome5IconView glyph="HEART"/>
</graphic> </graphic>
<content> <content>
<fx:include source="/fxml/preferences_donationkey.fxml"/> <fx:include source="/fxml/preferences_contribute.fxml"/>
</content> </content>
</Tab> </Tab>
<Tab fx:id="aboutTab" id="ABOUT" text="%preferences.about"> <Tab fx:id="aboutTab" id="ABOUT" text="%preferences.about">
@@ -12,7 +12,7 @@
<?import javafx.scene.shape.Circle?> <?import javafx.scene.shape.Circle?>
<VBox xmlns:fx="http://javafx.com/fxml" <VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx" xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.preferences.DonationKeyPreferencesController" fx:controller="org.cryptomator.ui.preferences.SupporterCertificateController"
spacing="18"> spacing="18">
<padding> <padding>
<Insets topRightBottomLeft="12"/> <Insets topRightBottomLeft="12"/>
@@ -24,7 +24,7 @@
<Circle styleClass="glyph-icon-primary" radius="24"/> <Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="CROWN" glyphSize="24"/> <FontAwesome5IconView styleClass="glyph-icon-white" glyph="CROWN" glyphSize="24"/>
</StackPane> </StackPane>
<FormattedLabel format="%preferences.donationKey.registeredFor" arg1="${controller.licenseHolder.licenseSubject}" wrapText="true"/> <FormattedLabel format="%preferences.contribute.registeredFor" arg1="${controller.licenseHolder.licenseSubject}" wrapText="true"/>
</HBox> </HBox>
<HBox spacing="12" alignment="CENTER_LEFT" visible="${!controller.licenseHolder.validLicense}"> <HBox spacing="12" alignment="CENTER_LEFT" visible="${!controller.licenseHolder.validLicense}">
@@ -33,8 +33,8 @@
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="HAND_HOLDING_HEART" glyphSize="24"/> <FontAwesome5IconView styleClass="glyph-icon-white" glyph="HAND_HOLDING_HEART" glyphSize="24"/>
</StackPane> </StackPane>
<VBox HBox.hgrow="ALWAYS" spacing="6"> <VBox HBox.hgrow="ALWAYS" spacing="6">
<Label text="%preferences.donationKey.noDonationKey" wrapText="true" VBox.vgrow="ALWAYS"/> <Label text="%preferences.contribute.noCertificate" wrapText="true" VBox.vgrow="ALWAYS"/>
<Hyperlink text="%preferences.donationKey.getDonationKey" onAction="#getDonationKey" contentDisplay="LEFT"> <Hyperlink text="%preferences.contribute.getCertificate" onAction="#getSupporterCertificate" contentDisplay="LEFT">
<graphic> <graphic>
<FontAwesome5IconView glyph="LINK"/> <FontAwesome5IconView glyph="LINK"/>
</graphic> </graphic>
@@ -43,6 +43,6 @@
</HBox> </HBox>
</StackPane> </StackPane>
<TextArea fx:id="donationKeyField" wrapText="true" VBox.vgrow="ALWAYS" prefRowCount="6"/> <TextArea fx:id="supporterCertificateField" promptText="%preferences.contribute.promptText" wrapText="true" VBox.vgrow="ALWAYS" prefRowCount="6"/>
</children> </children>
</VBox> </VBox>
@@ -23,7 +23,7 @@
<HBox spacing="6" alignment="CENTER_LEFT"> <HBox spacing="6" alignment="CENTER_LEFT">
<Label text="%preferences.general.theme"/> <Label text="%preferences.general.theme"/>
<ChoiceBox fx:id="themeChoiceBox" disable="${!controller.licenseHolder.validLicense}"/> <ChoiceBox fx:id="themeChoiceBox" disable="${!controller.licenseHolder.validLicense}"/>
<Hyperlink styleClass="hyperlink-underline,hyperlink-muted" text="%preferences.general.unlockThemes" onAction="#showDonationTab" visible="${!controller.licenseHolder.validLicense}" managed="${!controller.licenseHolder.validLicense}"/> <Hyperlink styleClass="hyperlink-underline,hyperlink-muted" text="%preferences.general.unlockThemes" onAction="#showContributeTab" visible="${!controller.licenseHolder.validLicense}" managed="${!controller.licenseHolder.validLicense}"/>
</HBox> </HBox>
<HBox spacing="6" alignment="CENTER_LEFT"> <HBox spacing="6" alignment="CENTER_LEFT">

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