Compare commits

...

184 Commits
1.0.4 ... 1.2.0

Author SHA1 Message Date
Sebastian Stenzel
3e374a927c Merge branch 'release/1.2.0'
# Conflicts:
#	main/ant-kit/pom.xml
#	main/commons-test/pom.xml
#	main/commons/pom.xml
#	main/filesystem-api/pom.xml
#	main/filesystem-charsets/pom.xml
#	main/filesystem-crypto-integration-tests/pom.xml
#	main/filesystem-crypto/pom.xml
#	main/filesystem-inmemory/pom.xml
#	main/filesystem-invariants-tests/pom.xml
#	main/filesystem-nameshortening/pom.xml
#	main/filesystem-nio/pom.xml
#	main/filesystem-stats/pom.xml
#	main/frontend-api/pom.xml
#	main/frontend-webdav/pom.xml
#	main/jacoco-report/pom.xml
#	main/pom.xml
#	main/uber-jar/pom.xml
#	main/ui/pom.xml
2016-09-19 15:10:43 +02:00
Sebastian Stenzel
84ac6d88f5 added new localization files to unit test [ci skip] 2016-09-15 23:55:37 +02:00
Sebastian Stenzel
72f6ee6477 updated localizations 2016-09-15 23:52:23 +02:00
Sebastian Stenzel
a3cfcb1131 Reject opening files when former filesize header is != -1 2016-09-15 23:26:13 +02:00
Sebastian Stenzel
d7d8d21ba4 Show warning when trying to migrate a masterkey with invalid version mac 2016-09-15 22:15:21 +02:00
Sebastian Stenzel
ef0425e2b1 fixes coverity issue 147409 2016-09-15 14:15:33 +02:00
Sebastian Stenzel
df1fd6d0b3 fixed coverity issue 72979 2016-09-15 14:15:25 +02:00
Sebastian Stenzel
a15acd64c8 set version to 1.2.0 2016-09-15 13:33:37 +02:00
Sebastian Stenzel
5b18eff01a increased cryptolib version to 1.0.2
[ci skip]
2016-09-15 13:28:20 +02:00
Sebastian Stenzel
47133c6f31 fixed change pw function leaving invalid JSON file if length gets shorter due to different encoding or pretty printing etc 2016-09-14 17:22:26 +02:00
Sebastian Stenzel
09ba4f5129 changed to jni lib version 1.0.0 2016-09-13 20:17:30 +02:00
Sebastian Stenzel
20d4047bed changed to cryptolib version 1.0.1 (which includes sources) [ci skip] 2016-09-12 23:16:17 +02:00
Sebastian Stenzel
56b71ef7d9 depends on relase version of cryptolib 1.0.0 2016-09-12 21:28:21 +02:00
Sebastian Stenzel
091e62057d Injecting CryptorProvider into UpgradeStrategies 2016-09-12 13:56:47 +02:00
Sebastian Stenzel
824bd9ea64 just added a comment [ci skip] 2016-09-08 18:41:36 +02:00
Sebastian Stenzel
697a791593 updated travis config 2016-09-08 18:34:28 +02:00
Sebastian Stenzel
7462a887b3 updated travis config 2016-09-08 18:30:11 +02:00
Sebastian Stenzel
3535e83d7d updated travis config 2016-09-08 18:20:57 +02:00
Sebastian Stenzel
cf0b4accb3 Merge branch 'feature/external-keychain' into develop 2016-09-04 16:21:52 +02:00
Sebastian Stenzel
a63bcfbaa2 relaxed "vault not empty" check 2016-09-04 16:04:16 +02:00
Sebastian Stenzel
5c4bf2a207 support home-relative paths for cryptomator.keychainPath 2016-09-04 12:27:23 +02:00
Sebastian Stenzel
c1611a12ed implemented Windows keychain 2016-09-03 23:04:53 +02:00
Markus Kreusch
0983120712 Removed Syso logging 2016-09-02 19:21:54 +02:00
Sebastian Stenzel
ce12af8495 Added save password functionality to UI 2016-09-02 15:49:09 +02:00
Sebastian Stenzel
dc117c8415 oracle-java8-unlimited-jce-policy apparently no longer needed (already installed) 2016-08-31 20:12:49 +02:00
Sebastian Stenzel
06e526a961 Merge branch 'develop' into feature/external-keychain
# Conflicts:
#	main/pom.xml
2016-08-31 19:45:15 +02:00
Sebastian Stenzel
2e343a951f Feature/travis container builds (#334)
improved build dependency caching + force updates of snapshots to bypass said cache
2016-08-31 19:39:55 +02:00
Sebastian Stenzel
141ffcf656 Merge branch 'feature/native-functions' into feature/external-keychain 2016-08-31 10:41:51 +02:00
Tobias Hagemann
d61e5c5a08 added "delete passphrase" method to keychain access 2016-08-31 01:08:58 +02:00
Tobias Hagemann
6a15fa132a app launches as foreground app on mac 2016-08-30 22:41:01 +02:00
Sebastian Stenzel
902b29ee0a Merge branch 'develop' into feature/external-keychain
# Conflicts:
#	main/pom.xml
#	main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java
2016-08-30 19:22:45 +02:00
Sebastian Stenzel
995bba616f cache maven dir 2016-08-30 19:15:26 +02:00
Sebastian Stenzel
f39b7b047f Merge branch 'feature/vaultVersion5' into develop 2016-08-30 19:14:28 +02:00
Sebastian Stenzel
72e52df4e0 implemented keychain access on OS X 2016-08-30 19:12:20 +02:00
Sebastian Stenzel
8018e9485e Merge branch 'feature/native-functions' into feature/external-keychain 2016-08-30 19:05:25 +02:00
Sebastian Stenzel
e0ae50378f externalized JNI bindings 2016-08-30 17:19:45 +02:00
Markus Kreusch
a9c2b0fc57 fixes #332 2016-08-29 21:08:58 +02:00
Sebastian Stenzel
dc58ba434a Make Cryptomator a foreground app when restoring from status bar icon 2016-08-29 20:14:48 +02:00
Sebastian Stenzel
34af306309 defined keychain access interfaces 2016-08-29 17:16:56 +02:00
Sebastian Stenzel
21d70b5ae4 moved from coveralls to codecov 2016-08-26 12:52:44 +02:00
Sebastian Stenzel
e90880ac9a speedboost 3000 2016-08-24 17:27:36 +02:00
Sebastian Stenzel
66faa13f40 unlock version 5 vaults 2016-08-23 21:35:13 +02:00
Sebastian Stenzel
8a4a29b4d1 added version 4 to 5 migrator 2016-08-23 21:15:52 +02:00
Sebastian Stenzel
8c8db84a4a refactored migration (using cryptolib) 2016-08-23 21:15:40 +02:00
Sebastian Stenzel
a499a3c80b Merge pull request #324 from oparoz/patch-1
Add that the solution works with Open Source clouds
2016-08-19 19:25:43 +02:00
Olivier Paroz
6a3ccf2b48 Add that the solution works with WebDAV clouds
I think it would be nice to promote other Open Source solutions which use standards such as WebDAV and work well with Cryptomator.
2016-08-19 19:07:39 +02:00
Tobias Hagemann
fcfcffe9cb updated tray icons for OS X [ci skip] 2016-08-19 16:56:04 +02:00
Sebastian Stenzel
363ed4ac4b Accept paths beginning with "~" in cryptomator.settingsPath JVM arg.
[ci skip]
2016-08-17 18:52:08 +02:00
Tobias Hagemann
1f73a08e09 added confirmation checkbox to upgrade screen [ci skip] 2016-08-17 18:11:27 +02:00
Sebastian Stenzel
fe0a34907f Simplified settings/log file path handling. Removed support for %appdata%. Use ~/AppData/Roaming instead! 2016-08-17 15:59:36 +02:00
Sebastian Stenzel
461b11700f added new upgrade log path setting to build script [ci skip] 2016-08-16 19:15:23 +02:00
Sebastian Stenzel
24bfbb59a4 fixes #310 2016-08-16 19:07:05 +02:00
Sebastian Stenzel
4476558e9c fixes #321 2016-08-16 12:33:15 +02:00
Sebastian Stenzel
560171832c Merge branch 'release/1.1.4'
Fixes #308, fixes #319, fixes #318, fixes #317, fixes #311, fixes #267

# Conflicts:
#	main/ant-kit/pom.xml
#	main/commons-test/pom.xml
#	main/commons/pom.xml
#	main/filesystem-api/pom.xml
#	main/filesystem-charsets/pom.xml
#	main/filesystem-crypto-integration-tests/pom.xml
#	main/filesystem-crypto/pom.xml
#	main/filesystem-inmemory/pom.xml
#	main/filesystem-invariants-tests/pom.xml
#	main/filesystem-nameshortening/pom.xml
#	main/filesystem-nio/pom.xml
#	main/filesystem-stats/pom.xml
#	main/frontend-api/pom.xml
#	main/frontend-webdav/pom.xml
#	main/jacoco-report/pom.xml
#	main/pom.xml
#	main/uber-jar/pom.xml
#	main/ui/pom.xml
2016-08-14 15:12:05 +02:00
Sebastian Stenzel
6e93d40e51 changed version to 1.1.4 2016-08-14 15:06:16 +02:00
Sebastian Stenzel
79b819bca6 Merge branch 'release/1.1.4' into develop 2016-08-14 15:04:44 +02:00
Sebastian Stenzel
a18c406cf0 fixed ConflictResolver 2016-08-14 14:56:44 +02:00
Sebastian Stenzel
6730a83cac fixes coverity issue 141842 2016-08-14 14:45:38 +02:00
Sebastian Stenzel
3b3ebd2196 fixes coverity issue 141838 2016-08-14 14:39:05 +02:00
Sebastian Stenzel
505b6542c7 fixes coverity issue 141844 2016-08-14 14:30:48 +02:00
Sebastian Stenzel
31368f0cba fixes coverity issue 141848 2016-08-14 14:28:52 +02:00
Sebastian Stenzel
5b5dd756b1 fixes coverity issue 141858 2016-08-14 14:27:48 +02:00
Sebastian Stenzel
f6ebbb23d1 fixes coverity issue 141860 2016-08-14 14:16:59 +02:00
Sebastian Stenzel
3f0373b08f removed xdg-utils dependencies, using gvfs-open instead of xdg-open. 2016-08-14 13:57:52 +02:00
Sebastian Stenzel
4c3c60060d Graceful unmounting on Windows and improved error handling of deferred closables. 2016-08-14 13:55:51 +02:00
Markus Kreusch
28f275c22d Requests on parent folders of valid vault urls no longer get delayed 2016-08-12 15:11:54 +02:00
Markus Kreusch
24df3c3809 GvfsMounters now use correct protocol. 2016-08-12 14:11:49 +02:00
Markus Kreusch
034a667e07 Fixed problem with sync conflict resolver. Issue #311 2016-08-11 11:19:12 +02:00
Markus Kreusch
008e3e3b05 Continue mounting also if command fails with error code. Issue #267 2016-08-11 09:39:38 +02:00
Markus Kreusch
94a5bf7596 Continue mounting if reg.exe command fails. Issue #267 2016-08-10 13:57:23 +02:00
Markus Kreusch
e8db836eff Workaround for Issue #317 2016-08-10 13:53:04 +02:00
Markus Kreusch
429b26f3d8 Added delay for requests on invalid vault ids. Issue #319 2016-08-10 13:44:26 +02:00
Markus Kreusch
3ae8327300 Added unique id to vaults / webdav urls.
Issue #319, Issue #308
2016-08-10 13:43:46 +02:00
Markus Kreusch
df7e9a0af1 Preventing post requests. Issue #319 2016-08-09 20:02:06 +02:00
Sebastian Stenzel
93d3eca0ab Yet another header to prevent browsers from guessing mime types. Kudos to @LukasReschke 2016-08-09 17:42:24 +02:00
Sebastian Stenzel
7753d1f0e7 If GET request is made by a browser, the file in question is downloaded instead of being executed. Issue #318 2016-08-09 17:35:17 +02:00
Sebastian Stenzel
d7c6c24932 updated tavis config 2016-08-01 10:21:10 +02:00
Sebastian Stenzel
1a75f23081 Merge branch 'feature/mount-method-settings' into develop 2016-08-01 10:17:44 +02:00
Sebastian Stenzel
f071efe1b9 allow user to specify whether to use dav:// or webdav:// scheme for Linux GVFS mounts. Fixes #307 2016-07-25 10:08:21 +02:00
Sebastian Stenzel
a8ad335aed Update README.md
[ci skip]
2016-07-18 12:18:47 +02:00
Markus Kreusch
7022a80c95 Improved error handling
* Created AsyncTaskService to build async UI operations which always log
uncaught exceptions
* Changed all executor service invocations in the UI to invocations of
AsyncTaskService
* Improved error handling in some other places, especially
try-with-resources
* Unlocking read/write locks in NioFile when opening of a channel fails
2016-07-14 13:58:17 +02:00
Sebastian Stenzel
9a2f602d6c fixes #270 2016-07-13 12:37:40 +02:00
Sebastian Stenzel
c78a4aa241 updated travis config [ci skip] 2016-07-11 22:14:35 +02:00
Tobias Hagemann
975ce4d973 Merge branch 'release/1.1.3'
# Conflicts:
#	main/ant-kit/pom.xml
#	main/commons-test/pom.xml
#	main/commons/pom.xml
#	main/filesystem-api/pom.xml
#	main/filesystem-charsets/pom.xml
#	main/filesystem-crypto-integration-tests/pom.xml
#	main/filesystem-crypto/pom.xml
#	main/filesystem-inmemory/pom.xml
#	main/filesystem-invariants-tests/pom.xml
#	main/filesystem-nameshortening/pom.xml
#	main/filesystem-nio/pom.xml
#	main/filesystem-stats/pom.xml
#	main/frontend-api/pom.xml
#	main/frontend-webdav/pom.xml
#	main/jacoco-report/pom.xml
#	main/pom.xml
#	main/uber-jar/pom.xml
#	main/ui/pom.xml
2016-07-11 21:16:23 +02:00
Tobias Hagemann
1e6ff0d969 set version to 1.1.3 2016-07-11 21:14:35 +02:00
Sebastian Stenzel
69e133d561 Improved migration from vault version 3 to 4. 2016-07-11 18:07:55 +02:00
Sebastian Stenzel
20e55eddf8 Merge branch 'develop' 2016-07-09 13:25:54 +02:00
Sebastian Stenzel
0fdcdc816a fixed unit test 2016-07-09 13:25:24 +02:00
Sebastian Stenzel
b7506d97a9 Merge branch 'hotfix/1.1.2' 2016-07-09 13:25:03 +02:00
Sebastian Stenzel
4ad7481dc7 fixed unit test 2016-07-09 11:44:53 +02:00
Sebastian Stenzel
bc815405d2 merged from hotfix/1.1.2 [ci skip] 2016-07-09 11:32:02 +02:00
Sebastian Stenzel
9c06e762c3 fixes #304 2016-07-09 11:28:36 +02:00
Sebastian Stenzel
1ac87dd32f fixed NPE [ci skip] 2016-07-08 15:52:00 +02:00
Sebastian Stenzel
e0ce7ce2ec Merge branch 'release/1.1.1'
# Conflicts:
#	main/ant-kit/pom.xml
#	main/commons-test/pom.xml
#	main/commons/pom.xml
#	main/filesystem-api/pom.xml
#	main/filesystem-charsets/pom.xml
#	main/filesystem-crypto-integration-tests/pom.xml
#	main/filesystem-crypto/pom.xml
#	main/filesystem-inmemory/pom.xml
#	main/filesystem-invariants-tests/pom.xml
#	main/filesystem-nameshortening/pom.xml
#	main/filesystem-nio/pom.xml
#	main/filesystem-stats/pom.xml
#	main/frontend-api/pom.xml
#	main/frontend-webdav/pom.xml
#	main/jacoco-report/pom.xml
#	main/pom.xml
#	main/uber-jar/pom.xml
#	main/ui/pom.xml
2016-07-08 11:59:39 +02:00
Sebastian Stenzel
3d951a9d7b set version to 1.1.1 2016-07-07 14:48:49 +02:00
Sebastian Stenzel
cec3d984b0 Merge branch 'develop' into release/1.1.1 2016-07-07 14:39:20 +02:00
Sebastian Stenzel
392e474cfa Update tr.txt (POEditor.com) 2016-07-07 14:28:52 +02:00
Sebastian Stenzel
41fb0d51a4 Update es.txt (POEditor.com) 2016-07-07 14:28:51 +02:00
Sebastian Stenzel
aa9fef2967 Update sk.txt (POEditor.com) 2016-07-07 14:28:49 +02:00
Sebastian Stenzel
adc9c02564 Update ru.txt (POEditor.com) 2016-07-07 14:28:48 +02:00
Sebastian Stenzel
ace64117a2 Update kr.txt (POEditor.com) 2016-07-07 14:28:46 +02:00
Sebastian Stenzel
fb4db2506b Update it.txt (POEditor.com) 2016-07-07 14:28:45 +02:00
Sebastian Stenzel
1076d971ae Update hu.txt (POEditor.com) 2016-07-07 14:28:43 +02:00
Sebastian Stenzel
eed1b1cff0 Update de.txt (POEditor.com) 2016-07-07 14:28:42 +02:00
Sebastian Stenzel
f5cb82e21e Update fr.txt (POEditor.com) 2016-07-07 14:28:40 +02:00
Sebastian Stenzel
67661f114b Update nl.txt (POEditor.com) 2016-07-07 14:28:39 +02:00
Sebastian Stenzel
8a3e09764a only remove .cryptomator extension for vault version 3 2016-07-07 14:25:55 +02:00
Sebastian Stenzel
eb3cfd6e6a updated placeholders [ci skip] 2016-07-06 16:25:31 +02:00
Sebastian Stenzel
4d1727d0e9 Merge branch 'develop' into release/1.1.1 2016-07-06 16:09:32 +02:00
Sebastian Stenzel
a51d853d1c adjusted number format regex [ci skip] 2016-07-06 16:09:14 +02:00
Sebastian Stenzel
d0039466f7 test technical correctness of localization files 2016-07-06 16:07:07 +02:00
Sebastian Stenzel
5c959989a2 Fixed Coverity defect 131711 2016-07-05 23:18:18 +02:00
Sebastian Stenzel
6283d2df3d Merge branch 'feature/vault-version-4' into develop 2016-07-03 17:41:46 +02:00
Sebastian Stenzel
a9e0dfdaf8 redesigned upgrade view 2016-07-03 17:38:46 +02:00
Sebastian Stenzel
45ca7e9e47 migration from vault version 3 to 4 2016-07-03 16:16:23 +02:00
Sebastian Stenzel
034b5c2718 updated localizations
[ci skip]
2016-07-01 11:14:49 +02:00
Sebastian Stenzel
e188649c79 adjusted test to vault version 4 2016-06-30 22:18:43 +02:00
Sebastian Stenzel
1468c6ec90 improved vault upgrading, preparation for migration to vault version 4 2016-06-30 22:09:45 +02:00
Sebastian Stenzel
07ba4eb537 Using 0 prefix instead of _ suffix to mark directories 2016-06-30 18:02:13 +02:00
Sebastian Stenzel
414bbef1a7 updated key generation 2016-06-10 14:04:55 +02:00
Sebastian Stenzel
e2b94ff6ef updated jacoco dependency 2016-06-08 19:11:56 +02:00
Sebastian Stenzel
41f8a9faca add "allow" response header field 2016-06-08 19:06:06 +02:00
Tobias Hagemann
1d9252e974 updated description of file chooser's extension filter [ci skip] 2016-06-07 01:07:54 +02:00
Sebastian Stenzel
80780eef3c Merge pull request #280 from aeris/fix-l10n-fr
Enhanced fr translation
[ci skip]
2016-05-30 14:27:00 +02:00
Aeris
87ff33956b Enhanced fr translation 2016-05-30 13:45:40 +02:00
Sebastian Stenzel
1804b98f05 trigger coverity scans for only release branches [ci skip] 2016-05-25 15:47:19 +02:00
Sebastian Stenzel
847c6813cc started development of 1.2.0 [ci skip] 2016-05-25 15:37:07 +02:00
Sebastian Stenzel
1dde5ff6e7 release 1.1.0 2016-05-25 15:17:40 +02:00
Sebastian Stenzel
76c9a19428 unset and set default buttons to make sure VK_ENTER triggers it 2016-05-25 12:29:49 +02:00
Sebastian Stenzel
25ee0519e1 some minor fixes
- reset password field contents when changing a vault
- hide "change password" option for uninitialized or missing vaults
2016-05-25 12:12:01 +02:00
Sebastian Stenzel
c184089c35 oopsy daisy, wie das duftet... [ci skip] 2016-05-24 11:36:46 +02:00
Sebastian Stenzel
d2bcc47857 Merge branch 'delete-confirmation'
fixes #228
2016-05-24 11:35:17 +02:00
Sebastian Stenzel
34629a69ea Using ControlsFX's BSD-licensed assets for dialogs, rather than OpenJDK's GPL licensed ones.
Adjusted dialog styles for Linux and Windows.
[ci skip]
2016-05-24 11:32:27 +02:00
Sebastian Stenzel
92c87f7b84 changed dialog L&F on OS X 2016-05-23 19:31:18 +02:00
Sebastian Stenzel
0dd96635ac code cleanup [ci skip] 2016-05-23 13:24:53 +02:00
Sebastian Stenzel
048c44a6e4 Update README.md 2016-05-23 12:36:58 +02:00
Sebastian Stenzel
06910ad1f4 fixes #229 2016-05-23 12:11:45 +02:00
Sebastian Stenzel
02a0f3acc6 fixed invariant FolderChildrenTests 2016-05-23 11:18:24 +02:00
Sebastian Stenzel
851f9240b7 updated link to MAC warning FAQ 2016-05-23 11:02:56 +02:00
Sebastian Stenzel
99fce8d0b7 automatically resolve conflicts for directory files, that contain the same directory ID 2016-05-23 11:02:44 +02:00
Sebastian Stenzel
bf05c59c3b Transparent conflict detection for long file names 2016-05-22 15:16:32 +02:00
Sebastian Stenzel
3dcebb1e1f fixed minor copy/paste error 2016-05-22 13:32:16 +02:00
Sebastian Stenzel
fe3efdf610 Merge pull request #269 from jncharon/master
fixes #56
2016-05-21 14:07:15 +02:00
jncharon
5f4ae46f82 Replaced the MouseListner by a MouseAdapter 2016-05-20 21:30:33 +02:00
jncharon
deef325319 Implementation of github issue #56 2016-05-20 19:29:53 +02:00
jncharon
fbe00a8fe3 Merge remote-tracking branch 'refs/remotes/cryptomator/master' 2016-05-16 14:20:55 +02:00
Sebastian Stenzel
dc87dade43 Merge pull request #259 from jncharon/issue-228-fixed
Fixes #228.
Still need to check license of icons, will discuss this with @MuscleRumble, who has a lot of icons that we bought the license for. If necessary we will change them.
2016-05-16 12:53:53 +02:00
Jean-Noël Charon
ba1625b5ad Merge pull request #2 from overheadhunter/issue-228-fixed
Reverted commit c0f4a2b, added .idea/ to .gitignore
2016-05-16 12:14:38 +02:00
Sebastian Stenzel
f6b126415e added IntelliJ files to .gitignore 2016-05-16 10:00:39 +02:00
Sebastian Stenzel
9147e1c08b Revert "Fucking .idea files I could not remove from the vcs"
This reverts commit c0f4a2b0d3.
2016-05-16 09:57:10 +02:00
Sebastian Stenzel
6c18103662 Remove files with non-decryptable names from dir listings 2016-05-13 18:59:06 +02:00
Sebastian Stenzel
6fc343ea12 more fault-tolerant behaviour when mapping of long filenames couldn't be found. 2016-05-13 14:10:37 +02:00
Sebastian Stenzel
d304d66cdd Updated localizations [ci skip] 2016-05-12 19:23:24 +02:00
Sebastian Stenzel
2ce9143b85 Merge branch 'conflict-detection' 2016-05-12 19:14:48 +02:00
Sebastian Stenzel
1c54e4f4ad in the unlikely event of an alternative name already being used, choose a new random conflict id. 2016-05-12 16:13:03 +02:00
Sebastian Stenzel
9fd6f2ecae transparently show sync conflicts (fixes #98) 2016-05-12 16:08:52 +02:00
Sebastian Stenzel
0d9f8eefc0 Using pattern based filename filtering
This is a preparation for finding valid encrypted names inside filenames that include additional characters
2016-05-12 11:51:14 +02:00
Sebastian Stenzel
40a1530f19 repeated commit 86000ac 2016-05-10 14:52:30 +02:00
Sebastian Stenzel
0477a0a2e3 Merge branch 'patches-1.0.x'
# Conflicts:
#	main/filesystem-charsets/pom.xml
2016-05-10 14:49:20 +02:00
Sebastian Stenzel
b77d4b5ae2 fixes #264 2016-05-10 14:33:21 +02:00
Sebastian Stenzel
7b6c5318c5 fixes #263 2016-05-10 14:31:55 +02:00
Sebastian Stenzel
6006d65ce0 new ant kit using a custom launcher binary due to #265 2016-05-10 14:26:06 +02:00
jncharon
2b01b76926 Merge remote-tracking branch 'refs/remotes/cryptomator/master' 2016-05-08 17:52:14 +02:00
Sebastian Stenzel
dcea9e21f0 added module to code coverage report 2016-05-07 15:00:20 +02:00
Sebastian Stenzel
78645ecdf6 fixes #264 2016-05-07 14:40:44 +02:00
Sebastian Stenzel
91646dd93d Merge branch 'password-strength'
Added password strength meter by Jean-Noël Charon, closing issue #198
2016-05-06 18:59:26 +02:00
jncharon
fca146e939 Merge remote-tracking branch 'remotes/origin/master' into issue-228-fixed 2016-05-05 21:22:28 +02:00
jncharon
62aa3ccc7f Merge remote-tracking branch 'refs/remotes/cryptomator/master' into issue-228-fixed 2016-05-05 21:12:53 +02:00
jncharon
c0f4a2b0d3 Fucking .idea files I could not remove from the vcs 2016-05-05 21:11:57 +02:00
Tobias Hagemann
68ee89af98 updated bot welcome asset [ci skip] 2016-05-03 19:12:21 +02:00
jncharon
6deb30307e Merge remote-tracking branch 'cryptomator/master' 2016-04-24 13:47:37 +02:00
jncharon
7357829741 Fix in the background color of the dialog boxes 2016-04-23 23:42:28 +02:00
jncharon
4bd04150c1 Implementation of github issue 228 2016-04-23 23:37:56 +02:00
Sebastian Stenzel
cf35772c18 Merge pull request #239 from jncharon/master
Fix in the change password screen
2016-04-21 09:11:23 +02:00
jncharon
b0fd226c4c Fix of the strength bar position (row) in the fxml 2016-04-20 22:32:30 +02:00
jncharon
0d188d1c0c Merge remote-tracking branch 'cryptomator/master' 2016-04-16 15:27:01 +02:00
Sebastian Stenzel
c6016ec7b2 using constructor-injection, organized imports, code autoformatting [ci skip] 2016-04-16 14:10:32 +02:00
Sebastian Stenzel
e8719a1f9b Merge pull request #232 from jncharon/master
Fixes #198, #157
2016-04-16 09:53:14 +02:00
jncharon
27baf78029 More refactoring following Sebastian comments 2016-04-16 00:12:59 +02:00
jncharon
bf5ce9a3a5 New password strength implementation based on zxcvbn4j 2016-04-15 22:52:57 +02:00
jncharon
bcfe040784 Merge remote-tracking branch 'cryptomator/master' 2016-04-13 18:22:16 +02:00
jncharon
d9b88ad1b7 Merge remote-tracking branch 'refs/remotes/cryptomator/master' 2016-04-12 21:53:44 +02:00
jncharon
e66e5b1d96 Added the password strength indicator in the change password window 2016-04-12 21:27:31 +02:00
jncharon
588166dce9 Added the password strength indicator in the initialize window 2016-04-12 21:00:41 +02:00
207 changed files with 5462 additions and 1209 deletions

6
.gitignore vendored
View File

@@ -11,3 +11,9 @@
.classpath
target/
test-output/
# IntelliJ Settings Files #
.idea/
out/
.idea_modules/
*.iws

View File

@@ -1,23 +1,30 @@
sudo: required
dist: trusty
language: java
sudo: required
dist: trusty
jdk:
- oraclejdk8
cache:
directories:
- $HOME/.m2
env:
global:
- secure: "Lgj042RD0X3rB8VZVZLWP1GetLhjd3PqI5JbJMlzgHJpDI6RkFIBLN9SWAGmkLPCehIp2zA5tu9+UVy0NNMxm9xz6SyjMCaxS28/fnYEXaNmwwDSF6O6gLUbdxyzoYIFPYOPmFxpzhebqnNIsxaM29oZpgRgUGqosCczQxiB+Ng=" #coveralls
- secure: "IfYURwZaDWuBDvyn47n0k1Zod/IQw1FF+CS5nnV08Q+NfC3vGGJMwV8m59XnbfwnWGxwvCaAbk4qP6s6+ijgZNKkvgfFMo3rfTok5zt43bIqgaFOANYV+OC/1c59gYD6ZUxhW5iNgMgU3qdsRtJuwSmfkVv/jKyLGfAbS4kN8BA=" #coverity
before_install: "curl -L --cookie 'oraclelicense=accept-securebackup-cookie;' http://download.oracle.com/otn-pub/java/jce/8/jce_policy-8.zip -o /tmp/policy.zip && sudo unzip -j -o /tmp/policy.zip *.jar -d `jdk_switcher home oraclejdk8`/jre/lib/security && rm /tmp/policy.zip"
script: mvn -fmain/pom.xml clean test
after_success: mvn -fmain/pom.xml -Ptest-coverage clean test jacoco:report-aggregate coveralls:report
- secure: "lV9OwUbHMrMpLUH1CY+Z4puLDdFXytudyPlG1eGRsesdpuG6KM3uQVz6uAtf6lrU8DRbMM/T7ML+PmvQ4UoPPYLdLxESLLBat2qUPOIVBOhTSlCc7I0DmGy04CSvkeMy8dPaQC0ukgNiR7zwoNzfcpGRN/U9S8tziDruuHoZSrg=" #bintray
addons:
coverity_scan:
project:
name: "cryptomator/cryptomator"
notification_email: sebastian.stenzel@cryptomator.org
build_command: "mvn -fmain/pom.xml clean test -DskipTests"
branch_pattern: release.*
install:
# "clean" needed until https://bugs.openjdk.java.net/browse/JDK-8067747 is resolved.
- mvn -fmain/pom.xml clean package -DskipTests dependency:go-offline -Ptest-coverage
- mvn -fmain/pom.xml clean package -DskipTests dependency:go-offline -Prelease
script:
- mvn --update-snapshots -fmain/pom.xml -Ptest-coverage clean test jacoco:report-aggregate
after_success:
- "bash <(curl -s https://codecov.io/bash)"
notifications:
webhooks:
urls:
@@ -30,19 +37,10 @@ notifications:
secure: "lngJ/HEAFBbD5AdiO9avMqptKpZHdmEwOzS9FabZjkdFh7yAYueTk5RniPUvShjsKtThYm7cJ8AtDMDwc07NvPrzbMBRtUJGwuDT+7c7YFALGFJ1NYi+emkC9x1oafvmPgEYSE+tMKzNcwrHi3ytGgKdIotsKwaF35QNXYA9aMs="
on_success: change
on_failure: always
before_deploy: mvn -fmain/pom.xml -Prelease clean package -DskipTests
addons:
coverity_scan:
project:
name: "cryptomator/cryptomator"
notification_email: sebastian.stenzel@cryptomator.org
build_command: "mvn -fmain/pom.xml clean test -DskipTests"
branch_pattern: coverity_scan
before_deploy:
- mvn -fmain/pom.xml -Prelease clean package -DskipTests
deploy:
provider: releases
- provider: releases
prerelease: false
api_key:
secure: "ZjE1j93v3qbPIe2YbmhS319aCbMdLQw0HuymmluTurxXsZtn9D4t2+eTr99vBVxGRuB5lzzGezPR5zjk5W7iHF7xhwrawXrFzr2rPJWzWFt0aM+Ry2njU1ROTGGXGTbv4anWeBlgMxLEInTAy/9ytOGNJlec83yc0THpOY2wxnk="
@@ -53,3 +51,13 @@ deploy:
on:
repo: cryptomator/cryptomator
tags: true
- provider: script
script: "curl -X POST -u cryptobot:${BINTRAY_API_KEY} -H 'Content-Type: application/json' -d '{\"name\": \"${TRAVIS_TAG}\", \"vcs_tag\": \"${TRAVIS_TAG}\"}' https://api.bintray.com/packages/cryptomator/cryptomator/cryptomator-win/versions"
on:
repo: cryptomator/cryptomator
tags: true
- provider: script
script: "curl -X POST -u cryptobot:${BINTRAY_API_KEY} -H 'Content-Type: application/json' -d '{\"name\": \"${TRAVIS_TAG}\", \"vcs_tag\": \"${TRAVIS_TAG}\"}' https://api.bintray.com/packages/cryptomator/cryptomator/cryptomator-osx/versions"
on:
repo: cryptomator/cryptomator
tags: true

View File

@@ -2,6 +2,7 @@
[![Build Status](https://travis-ci.org/cryptomator/cryptomator.svg?branch=master)](https://travis-ci.org/cryptomator/cryptomator)
[![Coverity Scan Build Status](https://scan.coverity.com/projects/cryptomator-cryptomator/badge.svg?flat=1)](https://scan.coverity.com/projects/cryptomator-cryptomator)
[![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)
[![Coverage Status](https://coveralls.io/repos/github/cryptomator/cryptomator/badge.svg?branch=master)](https://coveralls.io/github/cryptomator/cryptomator?branch=master)
[![Join the chat at https://gitter.im/cryptomator/cryptomator](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/cryptomator/cryptomator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Twitter](https://img.shields.io/badge/twitter-@Cryptomator-blue.svg?style=flat)](http://twitter.com/Cryptomator)
@@ -13,7 +14,7 @@ Download native binaries of Cryptomator on [cryptomator.org](https://cryptomator
## Features
- Works with Dropbox, Google Drive, OneDrive, and any other cloud storage service that synchronizes with a local directory
- Works with Dropbox, Google Drive, OneDrive, Nextcloud and any other cloud storage service which synchronizes with a local directory
- Open Source means: No backdoors, control is better than trust
- 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
@@ -45,7 +46,7 @@ For more information on the security details visit [cryptomator.org](https://cry
* Java 8 + JCE unlimited strength policy files (needed for 256-bit keys)
* 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), [Debian](https://github.com/cryptomator/cryptomator-deb))
* 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))
### Run Maven

View File

@@ -14,6 +14,12 @@
</includes>
<outputDirectory>libs</outputDirectory>
</fileSet>
<fileSet>
<directory>target/fixed-binaries</directory>
<filtered>false</filtered>
<outputDirectory>fixed-binaries</outputDirectory>
<fileMode>755</fileMode>
</fileSet>
<fileSet>
<directory>target/package</directory>
<filtered>false</filtered>

View File

@@ -8,7 +8,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.4</version>
<version>1.2.0</version>
</parent>
<artifactId>ant-kit</artifactId>
<packaging>pom</packaging>
@@ -60,6 +60,16 @@
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<excludes>
<exclude>fixed-binaries/**</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<includes>
<include>fixed-binaries/**</include>
</includes>
</resource>
</resources>
</configuration>

View File

@@ -21,21 +21,6 @@
</fx:jar>
</target>
<!-- Create native image -->
<target name="create-linux-image-with-jvm" depends="create-jar">
<fx:deploy nativeBundles="image" outdir="antbuild" outfile="Cryptomator-${project.version}" verbose="true">
<fx:application refid="Cryptomator" />
<fx:platform j2se="8.0">
<fx:property name="cryptomator.logPath" value="~/.Cryptomator/cryptomator.log" />
<fx:jvmarg value="-Xmx512m"/>
</fx:platform>
<fx:resources>
<fx:fileset dir="antbuild" type="jar" includes="Cryptomator-${project.version}.jar" />
<fx:fileset dir="libs" type="jar" includes="*.jar" excludes="ui-${project.version}.jar"/>
</fx:resources>
</fx:deploy>
</target>
<!-- Create Debian package -->
<target name="deb" depends="create-jar">
<fx:deploy nativeBundles="deb" outdir="antbuild" outfile="Cryptomator-${project.version}" verbose="true">
@@ -45,11 +30,14 @@
</fx:info>
<fx:platform j2se="8.0">
<fx:property name="cryptomator.logPath" value="~/.Cryptomator/cryptomator.log" />
<fx:jvmarg value="-Xmx1048m"/>
<fx:property name="cryptomator.upgradeLogPath" value="~/.Cryptomator/upgrade.log" />
<fx:property name="cryptomator.settingsPath" value="~/.Cryptomator/settings.json" />
<fx:jvmarg value="-Xmx512m"/>
</fx:platform>
<fx:resources>
<fx:fileset dir="antbuild" type="jar" includes="Cryptomator-${project.version}.jar" />
<fx:fileset dir="libs" type="jar" includes="*.jar" excludes="ui-${project.version}.jar"/>
<fx:fileset dir="fixed-binaries" type="data" includes="linux-launcher-*" arch=""/>
</fx:resources>
<fx:permissions elevated="false" />
<fx:preferences install="true" />
@@ -65,7 +53,9 @@
</fx:info>
<fx:platform j2se="8.0">
<fx:property name="cryptomator.logPath" value="~/.Cryptomator/cryptomator.log" />
<fx:jvmarg value="-Xmx1048m"/>
<fx:property name="cryptomator.upgradeLogPath" value="~/.Cryptomator/upgrade.log" />
<fx:property name="cryptomator.settingsPath" value="~/.Cryptomator/settings.json" />
<fx:jvmarg value="-Xmx512m"/>
</fx:platform>
<fx:resources>
<fx:fileset dir="antbuild" type="jar" includes="Cryptomator-${project.version}.jar" />

View File

@@ -9,7 +9,7 @@ Priority: optional
Architecture: APPLICATION_ARCH
Provides: APPLICATION_PACKAGE
Installed-Size: APPLICATION_INSTALLED_SIZE
Depends: gvfs-bin, gvfs-backends, gvfs-fuse, xdg-utils
Depends: gvfs-bin, gvfs-backends, gvfs-fuse
Description: Multi-platform client-side encryption of your cloud files.
Cryptomator provides free client-side AES encryption for your cloud files.
Create encrypted vaults, which get mounted as virtual volumes. Whatever

View File

@@ -0,0 +1,50 @@
#!/bin/sh
# postinst script for APPLICATION_NAME
#
# see: dh_installdeb(1)
set -e
# summary of how this script can be called:
# * <postinst> `configure' <most-recently-configured-version>
# * <old-postinst> `abort-upgrade' <new version>
# * <conflictor's-postinst> `abort-remove' `in-favour' <package>
# <new-version>
# * <postinst> `abort-remove'
# * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
# <failed-install-package> <version> `removing'
# <conflicting-package> <version>
# for details, see http://www.debian.org/doc/debian-policy/ or
# the debian-policy package
case "$1" in
configure)
echo Adding shortcut to the menu
SECONDARY_LAUNCHERS_INSTALL
APP_CDS_CACHE
xdg-desktop-menu install --novendor /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME.desktop
FILE_ASSOCIATION_INSTALL
rm /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME
if [ $(uname -m) = "x86_64" ]; then
mv /opt/APPLICATION_FS_NAME/app/linux-launcher-x64 /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME
else
mv /opt/APPLICATION_FS_NAME/app/linux-launcher-x86 /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME
fi
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
# dh_installdeb will replace this with shell code automatically
# generated by other debhelper scripts.
#DEBHELPER#
exit 0

View File

@@ -0,0 +1,54 @@
Summary: APPLICATION_SUMMARY
Name: APPLICATION_PACKAGE
Version: APPLICATION_VERSION
Release: 1
License: APPLICATION_LICENSE_TYPE
Vendor: APPLICATION_VENDOR
Prefix: /opt
Provides: APPLICATION_PACKAGE
Requires: ld-linux.so.2 libX11.so.6 libXext.so.6 libXi.so.6 libXrender.so.1 libXtst.so.6 libasound.so.2 libc.so.6 libdl.so.2 libgcc_s.so.1 libm.so.6 libpthread.so.0 libthread_db.so.1
Autoprov: 0
Autoreq: 0
#avoid ARCH subfolder
%define _rpmfilename %%{NAME}-%%{VERSION}-%%{RELEASE}.%%{ARCH}.rpm
#comment line below to enable effective jar compression
#it could easily get your package size from 40 to 15Mb but
#build time will substantially increase and it may require unpack200/system java to install
%define __jar_repack %{nil}
%description
APPLICATION_DESCRIPTION
%prep
%build
%install
rm -rf %{buildroot}
mkdir -p %{buildroot}/opt
cp -r %{_sourcedir}/APPLICATION_FS_NAME %{buildroot}/opt
%files
APPLICATION_LICENSE_FILE
/opt/APPLICATION_FS_NAME
%post
SECONDARY_LAUNCHERS_INSTALL
APP_CDS_CACHE
xdg-desktop-menu install --novendor /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME.desktop
FILE_ASSOCIATION_INSTALL
rm /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME
if [ $(uname -m) = "x86_64" ]; then
mv /opt/APPLICATION_FS_NAME/app/linux-launcher-x64 /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME
else
mv /opt/APPLICATION_FS_NAME/app/linux-launcher-x86 /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME
fi
%preun
SECONDARY_LAUNCHERS_REMOVE
xdg-desktop-menu uninstall --novendor /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME.desktop
FILE_ASSOCIATION_REMOVE
%clean

View File

@@ -10,17 +10,26 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.4</version>
<version>1.2.0</version>
</parent>
<artifactId>commons-test</artifactId>
<name>Cryptomator common test dependencies</name>
<description>Shared utilities for tests</description>
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>commons</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</dependency>
<dependency>
<groupId>de.bechte.junit</groupId>
<artifactId>junit-hierarchicalcontextrunner</artifactId>
@@ -29,11 +38,6 @@
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>commons</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -10,7 +10,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.4</version>
<version>1.2.0</version>
</parent>
<artifactId>commons</artifactId>
<name>Cryptomator common</name>
@@ -44,6 +44,11 @@
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>de.bechte.junit</groupId>
<artifactId>junit-hierarchicalcontextrunner</artifactId>

View File

@@ -1,7 +1,7 @@
package org.cryptomator.common;
@FunctionalInterface
public interface ConsumerThrowingException<T, E extends Exception> {
public interface ConsumerThrowingException<T, E extends Throwable> {
void accept(T t) throws E;

View File

@@ -17,16 +17,17 @@ public final class LazyInitializer {
* @return The initialized value
*/
public static <T> T initializeLazily(AtomicReference<T> reference, Supplier<T> factory) {
final T existingInstance = reference.get();
if (existingInstance != null) {
return existingInstance;
final T existing = reference.get();
if (existing != null) {
return existing;
} else {
final T newInstance = factory.get();
if (reference.compareAndSet(null, newInstance)) {
return newInstance;
} else {
return reference.get();
}
return reference.updateAndGet(currentValue -> {
if (currentValue == null) {
return factory.get();
} else {
return currentValue;
}
});
}
}

View File

@@ -1,7 +1,7 @@
package org.cryptomator.common;
@FunctionalInterface
public interface RunnableThrowingException<T extends Exception> {
public interface RunnableThrowingException<T extends Throwable> {
void run() throws T;

View File

@@ -0,0 +1,56 @@
/*******************************************************************************
* Copyright (c) 2016 Markus Kreusch and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch - initial implementation
*******************************************************************************/
package org.cryptomator.common;
import java.util.stream.Stream;
/**
* Utility to print stack traces while analyzing issues.
*
* @author Markus Kreusch
*/
public class StackTrace {
public static void print(String message) {
Thread thread = Thread.currentThread();
System.err.println(stackTraceFor(message, thread));
}
private static String stackTraceFor(String message, Thread thread) {
StringBuilder result = new StringBuilder();
appendMessageAndThreadName(result, message, thread);
appendStackTrace(thread, result);
return result.toString();
}
private static void appendStackTrace(Thread thread, StringBuilder result) {
Stream.of(thread.getStackTrace()) //
.skip(4) //
.forEach(stackTraceElement -> append(stackTraceElement, result));
}
private static void appendMessageAndThreadName(StringBuilder result, String message, Thread thread) {
result //
.append('[') //
.append(thread.getName()) //
.append("] ") //
.append(message);
}
private static void append(StackTraceElement stackTraceElement, StringBuilder result) {
String className = stackTraceElement.getClassName();
String methodName = stackTraceElement.getMethodName();
String fileName = stackTraceElement.getFileName();
int lineNumber = stackTraceElement.getLineNumber();
result.append('\n') //
.append(className).append(':').append(methodName) //
.append(" (").append(fileName).append(':').append(lineNumber).append(')');
}
}

View File

@@ -0,0 +1,8 @@
package org.cryptomator.common;
@FunctionalInterface
public interface SupplierThrowingException<T, E extends Throwable> {
T get() throws E;
}

View File

@@ -9,8 +9,8 @@ import static org.mockito.Mockito.when;
import java.util.function.Function;
import org.cryptomator.common.WeakValuedCache;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
@@ -83,6 +83,7 @@ public class WeakValuedCacheTest {
assertThat(result, is(sameInstance(theValue)));
}
@Ignore
@Test
public void testCacheDoesNotPreventGarbageCollectionOfValues() {
when(loader.apply(A_KEY)).thenAnswer(this::createValueUsingMoreThanHalfTheJvmMemory);

View File

@@ -9,7 +9,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.4</version>
<version>1.2.0</version>
</parent>
<artifactId>filesystem-api</artifactId>
<name>Cryptomator filesystem: API</name>

View File

@@ -17,6 +17,13 @@ public interface File extends Node, Comparable<File> {
static final int EOF = -1;
/**
* @return The current size of the file. This value is a snapshot and might have been changed by concurrent modifications.
* @throws UncheckedIOException
* if an {@link IOException} occurs
*/
long size() throws UncheckedIOException;
/**
* <p>
* Opens this file for reading.
@@ -39,7 +46,6 @@ public interface File extends Node, Comparable<File> {
* if an {@link IOException} occurs while opening the file, the
* file does not exist or is a directory
*/
ReadableFile openReadable() throws UncheckedIOException;
/**

View File

@@ -30,13 +30,6 @@ public interface ReadableFile extends ReadableByteChannel {
@Override
int read(ByteBuffer target) throws UncheckedIOException;
/**
* @return The current size of the file. This value is a snapshot and might have been changed by concurrent modifications.
* @throws UncheckedIOException
* if an {@link IOException} occurs
*/
long size() throws UncheckedIOException;
/**
* <p>
* Fast-forwards or rewinds the file to the specified position.

View File

@@ -15,7 +15,7 @@ import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.ReadableFile;
import org.cryptomator.filesystem.WritableFile;
public abstract class DelegatingFile<D extends DelegatingFolder<D, ?>> extends DelegatingNode<File>implements File {
public abstract class DelegatingFile<D extends DelegatingFolder<D, ?>> extends DelegatingNode<File> implements File {
private final D parent;
@@ -29,6 +29,11 @@ public abstract class DelegatingFile<D extends DelegatingFolder<D, ?>> extends D
return Optional.of(parent);
}
@Override
public long size() throws UncheckedIOException {
return delegate.size();
}
@Override
public ReadableFile openReadable() throws UncheckedIOException {
return delegate.openReadable();

View File

@@ -31,11 +31,6 @@ public class DelegatingReadableFile implements ReadableFile {
return delegate.read(target);
}
@Override
public long size() throws UncheckedIOException {
return delegate.size();
}
@Override
public void position(long position) throws UncheckedIOException {
delegate.position(position);

View File

@@ -4,6 +4,7 @@ import java.io.IOException;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
@@ -28,7 +29,9 @@ public final class FileContents {
* @return The file's content interpreted in this FileContents' charset.
*/
public String readContents(File file) {
try (Reader reader = Channels.newReader(file.openReadable(), charset.newDecoder(), -1)) {
try ( //
ReadableByteChannel channel = file.openReadable(); //
Reader reader = Channels.newReader(channel, charset.newDecoder(), -1)) {
return IOUtils.toString(reader);
} catch (IOException e) {
throw new UncheckedIOException(e);

View File

@@ -30,6 +30,16 @@ public class DelegatingFileTest {
Assert.assertEquals(mockFile.name(), delegatingFile.name());
}
@Test
public void testSize() {
File mockFile = Mockito.mock(File.class);
DelegatingFile<?> delegatingFile = new TestDelegatingFile(null, mockFile);
Mockito.when(mockFile.size()).thenReturn(42l);
Assert.assertEquals(42l, delegatingFile.size());
Mockito.verify(mockFile).size();
}
@Test
public void testParent() {
Folder mockFolder = Mockito.mock(Folder.class);

View File

@@ -42,17 +42,6 @@ public class DelegatingReadableFileTest {
Mockito.verify(mockReadableFile).read(buf);
}
@Test
public void testSize() {
ReadableFile mockReadableFile = Mockito.mock(ReadableFile.class);
@SuppressWarnings("resource")
DelegatingReadableFile delegatingReadableFile = new DelegatingReadableFile(mockReadableFile);
Mockito.when(mockReadableFile.size()).thenReturn(42l);
Assert.assertEquals(42l, delegatingReadableFile.size());
Mockito.verify(mockReadableFile).size();
}
@Test
public void testPosition() {
ReadableFile mockReadableFile = Mockito.mock(ReadableFile.class);

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2015 Sebastian Stenzel
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.2.0</version>
</parent>
<artifactId>filesystem-charsets</artifactId>
<name>Cryptomator filesystem: Charset compatibility layer</name>
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-api</artifactId>
</dependency>
<!-- Tests -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>commons-test</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-inmemory</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,32 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.filesystem.charsets;
import java.io.UncheckedIOException;
import java.text.Normalizer;
import java.text.Normalizer.Form;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.delegating.DelegatingFile;
class NormalizedNameFile extends DelegatingFile<NormalizedNameFolder> {
private final Form displayForm;
public NormalizedNameFile(NormalizedNameFolder parent, File delegate, Form displayForm) {
super(parent, delegate);
this.displayForm = displayForm;
}
@Override
public String name() throws UncheckedIOException {
return Normalizer.normalize(super.name(), displayForm);
}
}

View File

@@ -0,0 +1,27 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.filesystem.charsets;
import java.text.Normalizer.Form;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.delegating.DelegatingFileSystem;
public class NormalizedNameFileSystem extends NormalizedNameFolder implements DelegatingFileSystem {
public NormalizedNameFileSystem(Folder delegate, Form displayForm) {
super(null, delegate, displayForm);
}
@Override
public Folder getDelegate() {
return delegate;
}
}

View File

@@ -0,0 +1,76 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.filesystem.charsets;
import java.io.UncheckedIOException;
import java.text.Normalizer;
import java.text.Normalizer.Form;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.delegating.DelegatingFolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class NormalizedNameFolder extends DelegatingFolder<NormalizedNameFolder, NormalizedNameFile> {
private static final Logger LOG = LoggerFactory.getLogger(NormalizedNameFolder.class);
private final Form displayForm;
public NormalizedNameFolder(NormalizedNameFolder parent, Folder delegate, Form displayForm) {
super(parent, delegate);
this.displayForm = displayForm;
}
@Override
public String name() throws UncheckedIOException {
return Normalizer.normalize(super.name(), displayForm);
}
@Override
public NormalizedNameFile file(String name) throws UncheckedIOException {
String nfcName = Normalizer.normalize(name, Form.NFC);
String nfdName = Normalizer.normalize(name, Form.NFD);
NormalizedNameFile nfcFile = super.file(nfcName);
NormalizedNameFile nfdFile = super.file(nfdName);
if (!nfcName.equals(nfdName) && nfcFile.exists() && nfdFile.exists()) {
LOG.warn("Ambiguous file names \"" + nfcName + "\" (NFC) vs. \"" + nfdName + "\" (NFD). Both files exist. Using \"" + nfcName + "\" (NFC).");
} else if (!nfcName.equals(nfdName) && !nfcFile.exists() && nfdFile.exists()) {
LOG.info("Moving file from \"" + nfcName + "\" (NFD) to \"" + nfdName + "\" (NFC).");
nfdFile.moveTo(nfcFile);
}
return nfcFile;
}
@Override
protected NormalizedNameFile newFile(File delegate) {
return new NormalizedNameFile(this, delegate, displayForm);
}
@Override
public NormalizedNameFolder folder(String name) throws UncheckedIOException {
String nfcName = Normalizer.normalize(name, Form.NFC);
String nfdName = Normalizer.normalize(name, Form.NFD);
NormalizedNameFolder nfcFolder = super.folder(nfcName);
NormalizedNameFolder nfdFolder = super.folder(nfdName);
if (!nfcName.equals(nfdName) && nfcFolder.exists() && nfdFolder.exists()) {
LOG.warn("Ambiguous folder names \"" + nfcName + "\" (NFC) vs. \"" + nfdName + "\" (NFD). Both files exist. Using \"" + nfcName + "\" (NFC).");
} else if (!nfcName.equals(nfdName) && !nfcFolder.exists() && nfdFolder.exists()) {
LOG.info("Moving folder from \"" + nfcName + "\" (NFD) to \"" + nfdName + "\" (NFC).");
nfdFolder.moveTo(nfcFolder);
}
return nfcFolder;
}
@Override
protected NormalizedNameFolder newFolder(Folder delegate) {
return new NormalizedNameFolder(this, delegate, displayForm);
}
}

View File

@@ -0,0 +1,16 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
/**
* Makes sure, the filesystems wrapped by this filesystem work only on UTF-8 encoded file paths using Normalization Form C.
* Filesystems wrapping this file system, on the other hand, will get filenames reported in a specified Normalization Form.
* It is recommended to use NFD for OS X and NFC for other operating systems.
* When looking for a file or folder with a name given in either form, both possibilities are considered
* and files/folders stored in NFD are automatically migrated to NFC.
*/
package org.cryptomator.filesystem.charsets;

View File

@@ -0,0 +1,90 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.filesystem.charsets;
import java.nio.ByteBuffer;
import java.text.Normalizer.Form;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.WritableFile;
import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
import org.junit.Assert;
import org.junit.Test;
public class NormalizedNameFileSystemTest {
@Test
public void testFileMigration() {
FileSystem inMemoryFs = new InMemoryFileSystem();
try (WritableFile writable = inMemoryFs.file("\u006F\u0302").openWritable()) {
writable.write(ByteBuffer.allocate(0));
}
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
Assert.assertTrue(normalizationFs.file("\u00F4").exists());
Assert.assertTrue(normalizationFs.file("\u006F\u0302").exists());
Assert.assertFalse(inMemoryFs.file("\u006F\u0302").exists());
Assert.assertTrue(inMemoryFs.file("\u00F4").exists());
}
@Test
public void testNoFileMigration() {
FileSystem inMemoryFs = new InMemoryFileSystem();
try (WritableFile writable = inMemoryFs.file("\u00F4").openWritable()) {
writable.write(ByteBuffer.allocate(0));
}
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
Assert.assertTrue(normalizationFs.file("\u00F4").exists());
Assert.assertTrue(normalizationFs.file("\u006F\u0302").exists());
Assert.assertFalse(inMemoryFs.file("\u006F\u0302").exists());
Assert.assertTrue(inMemoryFs.file("\u00F4").exists());
}
@Test
public void testFolderMigration() {
FileSystem inMemoryFs = new InMemoryFileSystem();
inMemoryFs.folder("\u006F\u0302").create();
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
Assert.assertTrue(normalizationFs.folder("\u00F4").exists());
Assert.assertTrue(normalizationFs.folder("\u006F\u0302").exists());
Assert.assertFalse(inMemoryFs.folder("\u006F\u0302").exists());
Assert.assertTrue(inMemoryFs.folder("\u00F4").exists());
}
@Test
public void testNoFolderMigration() {
FileSystem inMemoryFs = new InMemoryFileSystem();
inMemoryFs.folder("\u00F4").create();
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
Assert.assertTrue(normalizationFs.folder("\u00F4").exists());
Assert.assertTrue(normalizationFs.folder("\u006F\u0302").exists());
Assert.assertFalse(inMemoryFs.folder("\u006F\u0302").exists());
Assert.assertTrue(inMemoryFs.folder("\u00F4").exists());
}
@Test
public void testNfcDisplayNames() {
FileSystem inMemoryFs = new InMemoryFileSystem();
inMemoryFs.folder("a\u00F4").create();
inMemoryFs.folder("b\u006F\u0302").create();
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
Assert.assertEquals("a\u00F4", normalizationFs.folder("a\u00F4").name());
Assert.assertEquals("b\u00F4", normalizationFs.folder("b\u006F\u0302").name());
}
@Test
public void testNfdDisplayNames() {
FileSystem inMemoryFs = new InMemoryFileSystem();
inMemoryFs.folder("a\u00F4").create();
inMemoryFs.folder("b\u006F\u0302").create();
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFD);
Assert.assertEquals("a\u006F\u0302", normalizationFs.folder("a\u00F4").name());
Assert.assertEquals("b\u006F\u0302", normalizationFs.folder("b\u006F\u0302").name());
}
}

View File

@@ -0,0 +1,48 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.filesystem.charsets;
import java.text.Normalizer.Form;
import org.cryptomator.filesystem.File;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
public class NormalizedNameFileTest {
private File delegateNfc;
private File delegateNfd;
@Before
public void setup() {
delegateNfc = Mockito.mock(File.class);
delegateNfd = Mockito.mock(File.class);
Mockito.when(delegateNfc.name()).thenReturn("\u00C5");
Mockito.when(delegateNfd.name()).thenReturn("\u0041\u030A");
}
@Test
public void testDisplayNameInNfc() {
File file1 = new NormalizedNameFile(null, delegateNfc, Form.NFC);
File file2 = new NormalizedNameFile(null, delegateNfd, Form.NFC);
Assert.assertEquals("\u00C5", file1.name());
Assert.assertEquals("\u00C5", file2.name());
}
@Test
public void testDisplayNameInNfd() {
File file1 = new NormalizedNameFile(null, delegateNfc, Form.NFD);
File file2 = new NormalizedNameFile(null, delegateNfd, Form.NFD);
Assert.assertEquals("\u0041\u030A", file1.name());
Assert.assertEquals("\u0041\u030A", file2.name());
}
}

View File

@@ -0,0 +1,149 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.filesystem.charsets;
import java.text.Normalizer.Form;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.Folder;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
public class NormalizedNameFolderTest {
private Folder delegate;
private File delegateSubFileNfc;
private File delegateSubFileNfd;
private Folder delegateSubFolderNfc;
private Folder delegateSubFolderNfd;
@Before
public void setup() {
delegate = Mockito.mock(Folder.class);
delegateSubFileNfc = Mockito.mock(File.class);
delegateSubFileNfd = Mockito.mock(File.class);
Mockito.when(delegate.file("\u00C5")).thenReturn(delegateSubFileNfc);
Mockito.when(delegateSubFileNfc.name()).thenReturn("\u00C5");
Mockito.when(delegate.file("\u0041\u030A")).thenReturn(delegateSubFileNfd);
Mockito.when(delegateSubFileNfd.name()).thenReturn("\u0041\u030A");
delegateSubFolderNfc = Mockito.mock(Folder.class);
delegateSubFolderNfd = Mockito.mock(Folder.class);
Mockito.when(delegate.folder("\u00F4")).thenReturn(delegateSubFolderNfc);
Mockito.when(delegateSubFolderNfc.name()).thenReturn("\u00F4");
Mockito.when(delegate.folder("\u006F\u0302")).thenReturn(delegateSubFolderNfd);
Mockito.when(delegateSubFolderNfd.name()).thenReturn("\u006F\u0302");
}
@Test
public void testDisplayNameInNfc() {
Folder folder1 = new NormalizedNameFolder(null, delegateSubFolderNfc, Form.NFC);
Folder folder2 = new NormalizedNameFolder(null, delegateSubFolderNfd, Form.NFC);
Assert.assertEquals("\u00F4", folder1.name());
Assert.assertEquals("\u00F4", folder2.name());
}
@Test
public void testDisplayNameInNfd() {
Folder folder1 = new NormalizedNameFolder(null, delegateSubFolderNfc, Form.NFD);
Folder folder2 = new NormalizedNameFolder(null, delegateSubFolderNfd, Form.NFD);
Assert.assertEquals("\u006F\u0302", folder1.name());
Assert.assertEquals("\u006F\u0302", folder2.name());
}
@Test
public void testNoFolderMigration1() {
Mockito.when(delegateSubFolderNfc.exists()).thenReturn(true);
Mockito.when(delegateSubFolderNfd.exists()).thenReturn(false);
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
Folder sub1 = folder.folder("\u00F4");
Folder sub2 = folder.folder("\u006F\u0302");
Mockito.verify(delegateSubFolderNfd, Mockito.never()).moveTo(Mockito.any());
Assert.assertSame(sub1, sub2);
}
@Test
public void testNoFolderMigration2() {
Mockito.when(delegateSubFolderNfc.exists()).thenReturn(true);
Mockito.when(delegateSubFolderNfd.exists()).thenReturn(true);
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
Folder sub1 = folder.folder("\u00F4");
Folder sub2 = folder.folder("\u006F\u0302");
Mockito.verify(delegateSubFolderNfd, Mockito.never()).moveTo(Mockito.any());
Assert.assertSame(sub1, sub2);
}
@Test
public void testNoFolderMigration3() {
Mockito.when(delegateSubFolderNfc.exists()).thenReturn(false);
Mockito.when(delegateSubFolderNfd.exists()).thenReturn(false);
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
Folder sub1 = folder.folder("\u00F4");
Folder sub2 = folder.folder("\u006F\u0302");
Mockito.verify(delegateSubFolderNfd, Mockito.never()).moveTo(Mockito.any());
Assert.assertSame(sub1, sub2);
}
@Test
public void testFolderMigration() {
Mockito.when(delegateSubFolderNfc.exists()).thenReturn(false);
Mockito.when(delegateSubFolderNfd.exists()).thenReturn(true);
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
Folder sub1 = folder.folder("\u00F4");
Mockito.verify(delegateSubFolderNfd).moveTo(delegateSubFolderNfc);
Folder sub2 = folder.folder("\u006F\u0302");
Assert.assertSame(sub1, sub2);
}
@Test
public void testNoFileMigration1() {
Mockito.when(delegateSubFileNfc.exists()).thenReturn(true);
Mockito.when(delegateSubFileNfd.exists()).thenReturn(false);
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
File sub1 = folder.file("\u00C5");
File sub2 = folder.file("\u0041\u030A");
Mockito.verify(delegateSubFileNfd, Mockito.never()).moveTo(Mockito.any());
Assert.assertSame(sub1, sub2);
}
@Test
public void testNoFileMigration2() {
Mockito.when(delegateSubFileNfc.exists()).thenReturn(true);
Mockito.when(delegateSubFileNfd.exists()).thenReturn(true);
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
File sub1 = folder.file("\u00C5");
File sub2 = folder.file("\u0041\u030A");
Mockito.verify(delegateSubFileNfd, Mockito.never()).moveTo(Mockito.any());
Assert.assertSame(sub1, sub2);
}
@Test
public void testNoFileMigration3() {
Mockito.when(delegateSubFileNfc.exists()).thenReturn(false);
Mockito.when(delegateSubFileNfd.exists()).thenReturn(false);
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
File sub1 = folder.file("\u00C5");
File sub2 = folder.file("\u0041\u030A");
Mockito.verify(delegateSubFileNfd, Mockito.never()).moveTo(Mockito.any());
Assert.assertSame(sub1, sub2);
}
@Test
public void testFileMigration() {
Mockito.when(delegateSubFileNfc.exists()).thenReturn(false);
Mockito.when(delegateSubFileNfd.exists()).thenReturn(true);
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
File sub1 = folder.file("\u00C5");
Mockito.verify(delegateSubFileNfd).moveTo(delegateSubFileNfc);
File sub2 = folder.file("\u0041\u030A");
Assert.assertSame(sub1, sub2);
}
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
</Console>
<Console name="StdErr" target="SYSTEM_ERR">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
</Console>
</Appenders>
<Loggers>
<Root level="DEBUG">
<AppenderRef ref="Console" />
<AppenderRef ref="StdErr" />
</Root>
</Loggers>
</Configuration>

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.4</version>
<version>1.2.0</version>
</parent>
<artifactId>filesystem-crypto-integration-tests</artifactId>
<name>Cryptomator filesystem: Encryption layer tests</name>

View File

@@ -130,7 +130,7 @@ public class CryptoFileSystemIntegrationTest {
// toggle last bit
try (WritableFile writable = physicalFile.openWritable(); ReadableFile readable = physicalFile.openReadable()) {
ByteBuffer buf = ByteBuffer.allocate((int) readable.size());
ByteBuffer buf = ByteBuffer.allocate((int) physicalFile.size());
readable.read(buf);
buf.array()[buf.limit() - 1] ^= 0x01;
buf.flip();

View File

@@ -12,14 +12,14 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.4</version>
<version>1.2.0</version>
</parent>
<artifactId>filesystem-crypto</artifactId>
<name>Cryptomator filesystem: Encryption layer</name>
<properties>
<bouncycastle.version>1.51</bouncycastle.version>
<sivmode.version>1.0.2</sivmode.version>
<sivmode.version>1.0.8</sivmode.version>
</properties>
<dependencies>

View File

@@ -8,7 +8,7 @@
*******************************************************************************/
package org.cryptomator.crypto.engine;
abstract class CryptoException extends RuntimeException {
public abstract class CryptoException extends RuntimeException {
public CryptoException() {
super();

View File

@@ -19,11 +19,6 @@ import javax.security.auth.Destroyable;
*/
public interface FileContentDecryptor extends Destroyable, Closeable {
/**
* @return Number of bytes of the decrypted file.
*/
long contentLength();
/**
* Appends further ciphertext to this decryptor. This method might block until space becomes available. If so, it is interruptable.
*

View File

@@ -8,6 +8,8 @@
*******************************************************************************/
package org.cryptomator.crypto.engine;
import java.util.regex.Pattern;
/**
* Provides deterministic encryption capabilities as filenames must not change on subsequent encryption attempts,
* otherwise each change results in major directory structure changes which would be a terrible idea for cloud storage encryption.
@@ -22,12 +24,9 @@ public interface FilenameCryptor {
String hashDirectoryId(String cleartextDirectoryId);
/**
* Tests without an actual decryption attempt, if a name is a well-formed ciphertext.
*
* @param ciphertextName Filename in question
* @return <code>true</code> if the given name is likely to be a valid ciphertext
* @return A Pattern that can be used to test, if a name is a well-formed ciphertext.
*/
boolean isEncryptedFilename(String ciphertextName);
Pattern encryptedNamePattern();
/**
* @param cleartextName original filename including cleartext file extension

View File

@@ -11,28 +11,28 @@ package org.cryptomator.crypto.engine;
public class UnsupportedVaultFormatException extends CryptoException {
private final Integer detectedVersion;
private final Integer supportedVersion;
private final Integer latestSupportedVersion;
public UnsupportedVaultFormatException(Integer detectedVersion, Integer supportedVersion) {
super("Tried to open vault of version " + detectedVersion + ", but can only handle version " + supportedVersion);
public UnsupportedVaultFormatException(Integer detectedVersion, Integer latestSupportedVersion) {
super("Tried to open vault of version " + detectedVersion + ", latest supported version is " + latestSupportedVersion);
this.detectedVersion = detectedVersion;
this.supportedVersion = supportedVersion;
this.latestSupportedVersion = latestSupportedVersion;
}
public Integer getDetectedVersion() {
return detectedVersion;
}
public Integer getSupportedVersion() {
return supportedVersion;
public Integer getLatestSupportedVersion() {
return latestSupportedVersion;
}
public boolean isVaultOlderThanSoftware() {
return detectedVersion == null || detectedVersion < supportedVersion;
return detectedVersion == null || detectedVersion < latestSupportedVersion;
}
public boolean isSoftwareOlderThanVault() {
return detectedVersion > supportedVersion;
return detectedVersion > latestSupportedVersion;
}
}

View File

@@ -5,7 +5,7 @@ public final class Constants {
private Constants() {
}
static final Integer CURRENT_VAULT_VERSION = 3;
static final Integer CURRENT_VAULT_VERSION = 5;
public static final int PAYLOAD_SIZE = 32 * 1024;
public static final int NONCE_SIZE = 16;

View File

@@ -18,6 +18,7 @@ import java.security.SecureRandom;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicReference;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
@@ -81,14 +82,15 @@ class CryptorImpl implements Cryptor {
@Override
public void randomizeMasterkey() {
final byte[] randomBytes = new byte[KEYLENGTH_IN_BYTES];
try {
randomSource.nextBytes(randomBytes);
encryptionKey = new SecretKeySpec(randomBytes, ENCRYPTION_ALG);
randomSource.nextBytes(randomBytes);
macKey = new SecretKeySpec(randomBytes, ENCRYPTION_ALG);
} finally {
Arrays.fill(randomBytes, (byte) 0x00);
KeyGenerator encKeyGen = KeyGenerator.getInstance(ENCRYPTION_ALG);
encKeyGen.init(KEYLENGTH_IN_BYTES * Byte.SIZE, randomSource);
encryptionKey = encKeyGen.generateKey();
KeyGenerator macKeyGen = KeyGenerator.getInstance(MAC_ALG);
macKeyGen.init(KEYLENGTH_IN_BYTES * Byte.SIZE, randomSource);
macKey = macKeyGen.generateKey();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Hard-coded algorithm doesn't exist.", e);
}
}
@@ -116,12 +118,12 @@ class CryptorImpl implements Cryptor {
final SecretKey kek = new SecretKeySpec(kekBytes, ENCRYPTION_ALG);
this.macKey = AesKeyWrap.unwrap(kek, keyFile.getMacMasterKey(), MAC_ALG);
// future use (as soon as we need to prevent downgrade attacks):
// final Mac mac = new ThreadLocalMac(macKey, MAC_ALG).get();
// final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.BYTES).putInt(CURRENT_VAULT_VERSION).array());
// if (!MessageDigest.isEqual(versionMac, keyFile.getVersionMac())) {
// destroyQuietly(macKey);
// throw new UnsupportedVaultFormatException(Integer.MAX_VALUE, CURRENT_VAULT_VERSION);
// }
// final Mac mac = new ThreadLocalMac(macKey, MAC_ALG).get();
// final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.BYTES).putInt(CURRENT_VAULT_VERSION).array());
// if (!MessageDigest.isEqual(versionMac, keyFile.getVersionMac())) {
// destroyQuietly(macKey);
// throw new UnsupportedVaultFormatException(Integer.MAX_VALUE, CURRENT_VAULT_VERSION);
// }
this.encryptionKey = AesKeyWrap.unwrap(kek, keyFile.getEncryptionMasterKey(), ENCRYPTION_ALG);
} catch (InvalidKeyException e) {
throw new InvalidPassphraseException();

View File

@@ -22,7 +22,6 @@ import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Supplier;
import javax.crypto.Cipher;
@@ -47,7 +46,6 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
private final Supplier<Mac> hmacSha256;
private final FileHeader header;
private final boolean authenticate;
private final LongAdder cleartextBytesDecrypted = new LongAdder();
private ByteBuffer ciphertextBuffer = ByteBuffer.allocate(CHUNK_SIZE);
private long chunkNumber = 0;
@@ -56,11 +54,11 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
this.header = FileHeader.decrypt(headerKey, hmacSha256, header);
this.authenticate = authenticate;
this.chunkNumber = firstCiphertextByte / CHUNK_SIZE; // floor() by int-truncation
}
@Override
public long contentLength() {
return header.getPayload().getFilesize();
// vault version 5 and onwards should have filesize: -1
if (this.header.getPayload().getFilesize() != -1l) {
throw new UncheckedIOException(new IOException("Attempted to decrypt file with invalid header (probably from previous vault version)"));
}
}
@Override
@@ -105,15 +103,7 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
@Override
public ByteBuffer cleartext() throws InterruptedException {
try {
final ByteBuffer cleartext = dataProcessor.processedData();
long bytesUntilLogicalEof = contentLength() - cleartextBytesDecrypted.sum();
if (bytesUntilLogicalEof <= 0) {
return FileContentCryptor.EOF;
} else if (bytesUntilLogicalEof < cleartext.remaining()) {
cleartext.limit((int) bytesUntilLogicalEof);
}
cleartextBytesDecrypted.add(cleartext.remaining());
return cleartext;
return dataProcessor.processedData();
} catch (ExecutionException e) {
if (e.getCause() instanceof AuthenticationFailedException) {
throw new AuthenticationFailedException(e);

View File

@@ -36,8 +36,6 @@ import org.cryptomator.io.ByteBuffers;
class FileContentEncryptorImpl implements FileContentEncryptor {
private static final String HMAC_SHA256 = "HmacSHA256";
private static final int PADDING_LOWER_BOUND = 4 * 1024; // 4k
private static final int PADDING_UPPER_BOUND = 16 * 1024 * 1024; // 16M
private static final int NUM_THREADS = Runtime.getRuntime().availableProcessors();
private static final int READ_AHEAD = 2;
private static final ExecutorService SHARED_DECRYPTION_EXECUTOR = Executors.newFixedThreadPool(NUM_THREADS);
@@ -63,7 +61,7 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
@Override
public ByteBuffer getHeader() {
header.getPayload().setFilesize(cleartextBytesScheduledForEncryption.sum());
header.getPayload().setFilesize(-1l);
return header.toByteBuffer(headerKey, hmacSha256);
}
@@ -76,7 +74,6 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
public void append(ByteBuffer cleartext) throws InterruptedException {
cleartextBytesScheduledForEncryption.add(cleartext.remaining());
if (cleartext == FileContentCryptor.EOF) {
appendSizeObfuscationPadding(cleartextBytesScheduledForEncryption.sum());
submitCleartextBuffer();
submitEof();
} else {
@@ -84,19 +81,6 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
}
}
private void appendSizeObfuscationPadding(long actualSize) throws InterruptedException {
final int maxPaddingLength = (int) Math.min(Math.max(actualSize / 10, PADDING_LOWER_BOUND), PADDING_UPPER_BOUND); // preferably 10%, but at least lower bound and no more than upper bound
final int randomPaddingLength = randomSource.nextInt(maxPaddingLength);
final ByteBuffer buf = ByteBuffer.allocate(PAYLOAD_SIZE);
int remainingPadding = randomPaddingLength;
while (remainingPadding > 0) {
int bytesInCurrentIteration = Math.min(remainingPadding, PAYLOAD_SIZE);
buf.clear().limit(bytesInCurrentIteration);
appendAllAndSubmitIfFull(buf);
remainingPadding -= bytesInCurrentIteration;
}
}
private void appendAllAndSubmitIfFull(ByteBuffer cleartext) throws InterruptedException {
while (cleartext.hasRemaining()) {
ByteBuffers.copy(cleartext, cleartextBuffer);

View File

@@ -11,12 +11,14 @@ package org.cryptomator.crypto.engine.impl;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;
@@ -36,13 +38,13 @@ class FileHeaderPayload implements Destroyable {
private final SecretKey contentKey;
public FileHeaderPayload(SecureRandom randomSource) {
filesize = 0;
final byte[] contentKey = new byte[CONTENT_KEY_LEN];
this.filesize = 0;
try {
randomSource.nextBytes(contentKey);
this.contentKey = new SecretKeySpec(contentKey, AES);
} finally {
Arrays.fill(contentKey, (byte) 0x00);
KeyGenerator keyGen = KeyGenerator.getInstance(AES);
keyGen.init(CONTENT_KEY_LEN * Byte.SIZE, randomSource);
this.contentKey = keyGen.generateKey();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Hard-coded algorithm doesn't exist.", e);
}
}

View File

@@ -12,8 +12,10 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.regex.Pattern;
import javax.crypto.AEADBadTagException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.SecretKey;
import org.apache.commons.codec.binary.Base32;
@@ -25,6 +27,8 @@ import org.cryptomator.siv.SivMode;
class FilenameCryptorImpl implements FilenameCryptor {
private static final BaseNCodec BASE32 = new Base32();
// https://tools.ietf.org/html/rfc4648#section-6
private static final Pattern BASE32_PATTERN = Pattern.compile("^([A-Z2-7]{8})*[A-Z2-7=]{8}");
private static final ThreadLocal<MessageDigest> SHA1 = new ThreadLocalSha1();
private static final ThreadLocal<SivMode> AES_SIV = new ThreadLocal<SivMode>() {
@Override
@@ -50,8 +54,8 @@ class FilenameCryptorImpl implements FilenameCryptor {
}
@Override
public boolean isEncryptedFilename(String ciphertextName) {
return BASE32.isInAlphabet(ciphertextName);
public Pattern encryptedNamePattern() {
return BASE32_PATTERN;
}
@Override
@@ -67,8 +71,8 @@ class FilenameCryptorImpl implements FilenameCryptor {
try {
final byte[] cleartextBytes = AES_SIV.get().decrypt(encryptionKey, macKey, encryptedBytes, associatedData);
return new String(cleartextBytes, UTF_8);
} catch (AEADBadTagException e) {
throw new AuthenticationFailedException("Authentication failed.", e);
} catch (AEADBadTagException | IllegalBlockSizeException e) {
throw new AuthenticationFailedException("Invalid ciphertext.", e);
}
}

View File

@@ -25,7 +25,7 @@ final class ThreadLocalAesCtrCipher {
try {
return Cipher.getInstance(AES_CTR);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException("Could not create MAC.", e);
throw new IllegalStateException("Could not create Cipher.", e);
}
}

View File

@@ -100,11 +100,6 @@ class BlockAlignedReadableFile implements ReadableFile {
return delegate.isOpen();
}
@Override
public long size() throws UncheckedIOException {
return delegate.size();
}
@Override
public void close() throws UncheckedIOException {
delegate.close();

View File

@@ -0,0 +1,105 @@
package org.cryptomator.filesystem.crypto;
import static org.cryptomator.filesystem.crypto.Constants.DIR_PREFIX;
import java.nio.ByteBuffer;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Function;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.ReadableFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
final class ConflictResolver {
private static final Logger LOG = LoggerFactory.getLogger(ConflictResolver.class);
private static final int UUID_FIRST_GROUP_STRLEN = 8;
private static final int MAX_DIR_FILE_SIZE = 87; // "normal" file header has 88 bytes
private final Pattern encryptedNamePattern;
private final Function<String, Optional<String>> nameDecryptor;
private final Function<String, Optional<String>> nameEncryptor;
public ConflictResolver(Pattern encryptedNamePattern, Function<String, Optional<String>> nameDecryptor, Function<String, Optional<String>> nameEncryptor) {
this.encryptedNamePattern = encryptedNamePattern;
this.nameDecryptor = nameDecryptor;
this.nameEncryptor = nameEncryptor;
}
public File resolveIfNecessary(File file) {
Matcher m = encryptedNamePattern.matcher(StringUtils.removeStart(file.name(), DIR_PREFIX));
if (m.matches()) {
// full match, use file as is
return file;
} else if (m.find(0)) {
// partial match, might be conflicting
return resolveConflict(file, m.toMatchResult());
} else {
// no match, file not relevant
return file;
}
}
private File resolveConflict(File conflictingFile, MatchResult matchResult) {
String ciphertext = matchResult.group();
boolean isDirectory = conflictingFile.name().startsWith(DIR_PREFIX);
Optional<String> cleartext = nameDecryptor.apply(ciphertext);
if (cleartext.isPresent()) {
Folder folder = conflictingFile.parent().get();
File canonicalFile = folder.file(isDirectory ? DIR_PREFIX + ciphertext : ciphertext);
if (isDirectory && canonicalFile.exists() && isSameFileBasedOnSample(canonicalFile, conflictingFile, MAX_DIR_FILE_SIZE)) {
// there must not be two directories pointing to the same directory id. In this case no human interaction is needed to resolve this conflict:
conflictingFile.delete();
return canonicalFile;
} else {
// conventional conflict detected! look for an alternative name:
File alternativeFile;
String conflictId;
do {
conflictId = createConflictId();
String alternativeCleartext = cleartext.get() + " (Conflict " + conflictId + ")";
String alternativeCiphertext = nameEncryptor.apply(alternativeCleartext).get();
alternativeFile = folder.file(isDirectory ? DIR_PREFIX + alternativeCiphertext : alternativeCiphertext);
} while (alternativeFile.exists());
LOG.info("Detected conflict {}:\n{}\n{}", conflictId, canonicalFile, conflictingFile);
conflictingFile.moveTo(alternativeFile);
return alternativeFile;
}
} else {
// not decryptable; false positive
return conflictingFile;
}
}
private boolean isSameFileBasedOnSample(File file1, File file2, int sampleSize) {
if (file1.size() != file2.size()) {
return false;
} else {
try (ReadableFile r1 = file1.openReadable(); ReadableFile r2 = file2.openReadable()) {
ByteBuffer beginOfFile1 = ByteBuffer.allocate(sampleSize);
ByteBuffer beginOfFile2 = ByteBuffer.allocate(sampleSize);
int bytesRead1 = r1.read(beginOfFile1);
int bytesRead2 = r2.read(beginOfFile2);
if (bytesRead1 == bytesRead2) {
beginOfFile1.flip();
beginOfFile2.flip();
return beginOfFile1.equals(beginOfFile2);
} else {
return false;
}
}
}
}
private String createConflictId() {
return UUID.randomUUID().toString().substring(0, UUID_FIRST_GROUP_STRLEN);
}
}

View File

@@ -8,9 +8,9 @@ public final class Constants {
static final String DATA_ROOT_DIR = "d";
static final String ROOT_DIRECOTRY_ID = "";
static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup";
public static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
public static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup";
static final String DIR_SUFFIX = "_";
static final String DIR_PREFIX = "0";
}

View File

@@ -8,7 +8,8 @@
*******************************************************************************/
package org.cryptomator.filesystem.crypto;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.cryptomator.crypto.engine.impl.Constants.CHUNK_SIZE;
import static org.cryptomator.crypto.engine.impl.Constants.PAYLOAD_SIZE;
import java.io.UncheckedIOException;
import java.nio.file.FileAlreadyExistsException;
@@ -27,9 +28,26 @@ class CryptoFile extends CryptoNode implements File {
@Override
protected Optional<String> encryptedName() {
return parent().get().getDirectoryId().map(s -> s.getBytes(UTF_8)).map(parentDirId -> {
return cryptor.getFilenameCryptor().encryptFilename(name(), parentDirId);
});
return parent().get().encryptChildName(name());
}
@Override
public long size() throws UncheckedIOException {
if (!physicalFile().isPresent()) {
return -1l;
} else {
File file = physicalFile().get();
long ciphertextSize = file.size() - cryptor.getFileContentCryptor().getHeaderSize();
long overheadPerChunk = CHUNK_SIZE - PAYLOAD_SIZE;
long numFullChunks = ciphertextSize / CHUNK_SIZE; // floor by int-truncation
long additionalCiphertextBytes = ciphertextSize % CHUNK_SIZE;
if (additionalCiphertextBytes > 0 && additionalCiphertextBytes <= overheadPerChunk) {
throw new IllegalArgumentException("Method not defined for input value " + ciphertextSize);
}
long additionalCleartextBytes = (additionalCiphertextBytes == 0) ? 0 : additionalCiphertextBytes - overheadPerChunk;
assert additionalCleartextBytes >= 0;
return PAYLOAD_SIZE * numFullChunks + additionalCleartextBytes;
}
}
@Override

View File

@@ -8,8 +8,10 @@
*******************************************************************************/
package org.cryptomator.filesystem.crypto;
import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.cryptomator.filesystem.crypto.Constants.DIR_SUFFIX;
import static org.apache.commons.lang3.StringUtils.removeStart;
import static org.cryptomator.filesystem.crypto.Constants.DIR_PREFIX;
import java.io.FileNotFoundException;
import java.io.UncheckedIOException;
@@ -18,37 +20,44 @@ import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.common.LazyInitializer;
import org.cryptomator.common.WeakValuedCache;
import org.cryptomator.common.streams.AutoClosingStream;
import org.cryptomator.crypto.engine.CryptoException;
import org.cryptomator.crypto.engine.Cryptor;
import org.cryptomator.filesystem.Deleter;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.Node;
import org.cryptomator.io.FileContents;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class CryptoFolder extends CryptoNode implements Folder {
private static final Logger LOG = LoggerFactory.getLogger(CryptoFolder.class);
private final WeakValuedCache<String, CryptoFolder> folders = WeakValuedCache.usingLoader(this::newFolder);
private final WeakValuedCache<String, CryptoFile> files = WeakValuedCache.usingLoader(this::newFile);
private final AtomicReference<String> directoryId = new AtomicReference<>();
private final ConflictResolver conflictResolver;
public CryptoFolder(CryptoFolder parent, String name, Cryptor cryptor) {
super(parent, name, cryptor);
this.conflictResolver = new ConflictResolver(cryptor.getFilenameCryptor().encryptedNamePattern(), this::decryptChildName, this::encryptChildName);
}
/* ======================= name + directory id ======================= */
@Override
protected Optional<String> encryptedName() {
if (parent().isPresent()) {
return parent().get().getDirectoryId().map(s -> s.getBytes(UTF_8)).map(parentDirId -> {
return cryptor.getFilenameCryptor().encryptFilename(name(), parentDirId) + DIR_SUFFIX;
});
return parent().get().encryptChildName(name()).map(s -> DIR_PREFIX + s);
} else {
return Optional.of(cryptor.getFilenameCryptor().encryptFilename(name()) + DIR_SUFFIX);
return Optional.of(DIR_PREFIX + cryptor.getFilenameCryptor().encryptFilename(name()));
}
}
@@ -73,24 +82,60 @@ class CryptoFolder extends CryptoNode implements Folder {
}));
}
/* ======================= children ======================= */
@Override
public Stream<? extends Node> children() {
return AutoClosingStream.from(Stream.concat(files(), folders()));
}
private Stream<File> nonConflictingFiles() {
if (exists()) {
final Stream<? extends File> files = physicalFolder().filter(Folder::exists).map(Folder::files).orElse(Stream.empty());
return files.filter(startsWithEncryptedName()).map(conflictResolver::resolveIfNecessary).distinct();
} else {
throw new UncheckedIOException(new FileNotFoundException(format("Folder %s does not exist", this)));
}
}
private Predicate<File> startsWithEncryptedName() {
final Pattern encryptedNamePattern = cryptor.getFilenameCryptor().encryptedNamePattern();
return (File file) -> encryptedNamePattern.matcher(removeStart(file.name(),DIR_PREFIX)).find();
}
Optional<String> decryptChildName(String ciphertextFileName) {
return getDirectoryId().map(s -> s.getBytes(UTF_8)).map(dirId -> {
try {
return cryptor.getFilenameCryptor().decryptFilename(ciphertextFileName, dirId);
} catch (CryptoException e) {
LOG.warn("Filename decryption of {} failed: {}", ciphertextFileName, e.getMessage());
return null;
}
});
}
Optional<String> encryptChildName(String cleartextFileName) {
return getDirectoryId().map(s -> s.getBytes(UTF_8)).map(dirId -> {
return cryptor.getFilenameCryptor().encryptFilename(cleartextFileName, dirId);
});
}
@Override
public Stream<CryptoFile> files() {
final Stream<? extends File> files = physicalFolder().filter(Folder::exists).map(Folder::files).orElse(Stream.empty());
return files.map(File::name).filter(isEncryptedFileName()).map(this::decryptChildFileName).map(this::file);
return nonConflictingFiles().map(File::name).filter(startsWithDirPrefix().negate()).map(this::decryptChildName).filter(Optional::isPresent).map(Optional::get).map(this::file);
}
private Predicate<String> isEncryptedFileName() {
return (String name) -> !name.endsWith(DIR_SUFFIX) && cryptor.getFilenameCryptor().isEncryptedFilename(name);
@Override
public Stream<CryptoFolder> folders() {
return nonConflictingFiles().map(File::name).filter(startsWithDirPrefix()).map(this::removeDirPrefix).map(this::decryptChildName).filter(Optional::isPresent).map(Optional::get).map(this::folder);
}
private String decryptChildFileName(String encryptedFileName) {
final byte[] dirId = getDirectoryId().get().getBytes(UTF_8);
return cryptor.getFilenameCryptor().decryptFilename(encryptedFileName, dirId);
private Predicate<String> startsWithDirPrefix() {
return (String encryptedFolderName) -> StringUtils.startsWith(encryptedFolderName, DIR_PREFIX);
}
private String removeDirPrefix(String encryptedFolderName) {
return StringUtils.removeStart(encryptedFolderName, DIR_PREFIX);
}
@Override
@@ -98,35 +143,21 @@ class CryptoFolder extends CryptoNode implements Folder {
return files.get(name);
}
public CryptoFile newFile(String name) {
return new CryptoFile(this, name, cryptor);
}
@Override
public Stream<CryptoFolder> folders() {
final Stream<? extends File> files = physicalFolder().filter(Folder::exists).map(Folder::files).orElse(Stream.empty());
return files.map(File::name).filter(isEncryptedDirectoryName()).map(this::decryptChildFolderName).map(this::folder);
}
private Predicate<String> isEncryptedDirectoryName() {
return (String name) -> name.endsWith(DIR_SUFFIX) && cryptor.getFilenameCryptor().isEncryptedFilename(StringUtils.removeEnd(name, DIR_SUFFIX));
}
private String decryptChildFolderName(String encryptedFolderName) {
final byte[] dirId = getDirectoryId().get().getBytes(UTF_8);
final String ciphertext = StringUtils.removeEnd(encryptedFolderName, DIR_SUFFIX);
return cryptor.getFilenameCryptor().decryptFilename(ciphertext, dirId);
}
@Override
public CryptoFolder folder(String name) {
return folders.get(name);
}
public CryptoFolder newFolder(String name) {
private CryptoFile newFile(String name) {
return new CryptoFile(this, name, cryptor);
}
private CryptoFolder newFolder(String name) {
return new CryptoFolder(this, name, cryptor);
}
/* ======================= create/move/delete ======================= */
@Override
public void create() {
parent.create();
@@ -176,7 +207,7 @@ class CryptoFolder extends CryptoNode implements Folder {
// cut all ties:
this.invalidateDirectoryIdsRecursively();
assert!exists();
assert !exists();
assert target.exists();
}

View File

@@ -70,12 +70,6 @@ class CryptoReadableFile implements ReadableFile {
}
}
@Override
public long size() throws UncheckedIOException {
assert decryptor != null : "decryptor is always being set during position(long)";
return decryptor.contentLength();
}
@Override
public void position(long position) throws UncheckedIOException {
if (readAheadTask != null) {

View File

@@ -16,6 +16,7 @@ import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import javax.inject.Inject;
import javax.inject.Provider;
@@ -52,10 +53,14 @@ class Masterkeys {
public Cryptor decrypt(Folder vaultLocation, CharSequence passphrase) throws InvalidPassphraseException {
File masterkeyFile = vaultLocation.file(MASTERKEY_FILENAME);
Cryptor cryptor = cryptorProvider.get();
boolean success = false;
try {
readMasterKey(masterkeyFile, cryptor, passphrase);
} catch (UncheckedIOException e) {
cryptor.destroy();
success = true;
} finally {
if (!success) {
cryptor.destroy();
}
}
return cryptor;
}
@@ -86,7 +91,9 @@ class Masterkeys {
/* I/O */
private static void readMasterKey(File file, Cryptor cryptor, CharSequence passphrase) throws UncheckedIOException, InvalidPassphraseException {
try (InputStream in = Channels.newInputStream(file.openReadable())) {
try ( //
ReadableByteChannel channel = file.openReadable(); //
InputStream in = Channels.newInputStream(channel)) {
final byte[] fileContents = IOUtils.toByteArray(in);
cryptor.readKeysFromMasterkeyFile(fileContents, passphrase);
} catch (IOException e) {
@@ -96,6 +103,7 @@ class Masterkeys {
private static void writeMasterKey(File file, Cryptor cryptor, CharSequence passphrase) throws UncheckedIOException {
try (WritableFile writable = file.openWritable()) {
writable.truncate();
final byte[] fileContents = cryptor.writeKeysToMasterkeyFile(passphrase);
writable.write(ByteBuffer.wrap(fileContents));
}

View File

@@ -44,16 +44,9 @@ class NoFileContentCryptor implements FileContentCryptor {
private class Decryptor implements FileContentDecryptor {
private final BlockingQueue<Supplier<ByteBuffer>> cleartextQueue = new LinkedBlockingQueue<>();
private final long contentLength;
private Decryptor(ByteBuffer header) {
assert header.remaining() == Long.BYTES;
this.contentLength = header.getLong();
}
@Override
public long contentLength() {
return contentLength;
}
@Override

View File

@@ -12,6 +12,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.regex.Pattern;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.BaseNCodec;
@@ -19,6 +20,7 @@ import org.apache.commons.codec.binary.BaseNCodec;
class NoFilenameCryptor implements FilenameCryptor {
private static final BaseNCodec BASE32 = new Base32();
private static final Pattern WILDCARD_PATTERN = Pattern.compile(".*");
private static final ThreadLocal<MessageDigest> SHA1 = new ThreadLocalSha1();
@Override
@@ -29,8 +31,8 @@ class NoFilenameCryptor implements FilenameCryptor {
}
@Override
public boolean isEncryptedFilename(String ciphertextName) {
return true;
public Pattern encryptedNamePattern() {
return WILDCARD_PATTERN;
}
@Override

View File

@@ -21,20 +21,20 @@ public class CryptorImplTest {
@Test
public void testMasterkeyDecryptionWithCorrectPassphrase() throws IOException {
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
}
@Test(expected = InvalidPassphraseException.class)
public void testMasterkeyDecryptionWithWrongPassphrase() throws IOException {
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "qwe");
}
@@ -44,7 +44,7 @@ public class CryptorImplTest {
final String testMasterKey = "{\"version\":-1,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
}
@@ -52,7 +52,7 @@ public class CryptorImplTest {
@Ignore
@Test(expected = UnsupportedVaultFormatException.class)
public void testMasterkeyDecryptionWithMissingVersionMac() throws IOException {
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"}";
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
@@ -62,20 +62,20 @@ public class CryptorImplTest {
@Ignore
@Test(expected = UnsupportedVaultFormatException.class)
public void testMasterkeyDecryptionWithWrongVersionMac() throws IOException {
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLa=\"}";
+ "\"versionMac\":\"z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfoK=\"}";
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
}
@Test
public void testMasterkeyEncryption() throws IOException {
final String expectedMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," //
final String expectedMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
+ "\"hmacMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
+ "\"versionMac\":\"yuwoRE9GSdgQ2b//qRpTCj3W0qsVLxYVa7/KB3PkfA4=\"}";
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
cryptor.randomizeMasterkey();
final byte[] masterkeyFile = cryptor.writeKeysToMasterkeyFile("asd");

View File

@@ -43,20 +43,6 @@ public class FileContentCryptorImplTest {
};
private static final SecureRandom RANDOM_MOCK_2 = new SecureRandom() {
@Override
public int nextInt(int bound) {
return 500;
}
@Override
public void nextBytes(byte[] bytes) {
Arrays.fill(bytes, (byte) 0x00);
}
};
@Test(expected = IllegalArgumentException.class)
public void testShortHeaderInDecryptor() throws InterruptedException {
final byte[] keyBytes = new byte[32];
@@ -137,45 +123,6 @@ public class FileContentCryptorImplTest {
Assert.assertArrayEquals("cleartext message".getBytes(), result);
}
@Test
public void testEncryptionAndDecryptionWithSizeObfuscationPadding() throws InterruptedException {
final byte[] keyBytes = new byte[32];
final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
FileContentCryptor cryptor = new FileContentCryptorImpl(encryptionKey, macKey, RANDOM_MOCK_2);
ByteBuffer header = ByteBuffer.allocate(cryptor.getHeaderSize());
ByteBuffer ciphertext = ByteBuffer.allocate(16 + 11 + 500 + 32 + 1); // 16 bytes iv + 11 bytes ciphertext + 500 bytes padding + 32 bytes mac + 1.
try (FileContentEncryptor encryptor = cryptor.createFileContentEncryptor(Optional.empty(), 0)) {
encryptor.append(ByteBuffer.wrap("hello world".getBytes()));
encryptor.append(FileContentCryptor.EOF);
ByteBuffer buf;
while ((buf = encryptor.ciphertext()) != FileContentCryptor.EOF) {
ByteBuffers.copy(buf, ciphertext);
}
ByteBuffers.copy(encryptor.getHeader(), header);
}
header.flip();
ciphertext.flip();
Assert.assertEquals(16 + 11 + 500 + 32, ciphertext.remaining());
ByteBuffer plaintext = ByteBuffer.allocate(12); // 11 bytes plaintext + 1
try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header, 0, true)) {
decryptor.append(ciphertext);
decryptor.append(FileContentCryptor.EOF);
ByteBuffer buf;
while ((buf = decryptor.cleartext()) != FileContentCryptor.EOF) {
ByteBuffers.copy(buf, plaintext);
}
}
plaintext.flip();
byte[] result = new byte[plaintext.remaining()];
plaintext.get(result);
Assert.assertArrayEquals("hello world".getBytes(), result);
}
@Test(timeout = 20000) // assuming a minimum speed of 10mb/s during encryption and decryption 20s should be enough
public void testEncryptionAndDecryptionSpeed() throws InterruptedException, IOException {
final byte[] keyBytes = new byte[32];

View File

@@ -45,7 +45,7 @@ public class FileContentDecryptorImplTest {
final byte[] keyBytes = new byte[32];
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
final byte[] content = Base64.decode("AAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3Og=");
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
@@ -68,7 +68,7 @@ public class FileContentDecryptorImplTest {
final byte[] keyBytes = new byte[32];
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJa==");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJG==");
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
@@ -80,7 +80,7 @@ public class FileContentDecryptorImplTest {
final byte[] keyBytes = new byte[32];
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
final byte[] content = Base64.decode("aAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3Og=");
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
@@ -101,7 +101,7 @@ public class FileContentDecryptorImplTest {
final byte[] keyBytes = new byte[32];
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
final byte[] content = Base64.decode("AAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3OG=");
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, false)) {
@@ -124,7 +124,7 @@ public class FileContentDecryptorImplTest {
final byte[] keyBytes = new byte[32];
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
decryptor.cancelWithException(new IOException("can not do"));

View File

@@ -35,20 +35,6 @@ public class FileContentEncryptorImplTest {
};
private static final SecureRandom RANDOM_MOCK_2 = new SecureRandom() {
@Override
public int nextInt(int bound) {
return 42;
}
@Override
public void nextBytes(byte[] bytes) {
Arrays.fill(bytes, (byte) 0x00);
}
};
@Test
public void testEncryption() throws InterruptedException {
final byte[] keyBytes = new byte[32];
@@ -95,24 +81,4 @@ public class FileContentEncryptorImplTest {
}
}
@Test
public void testSizeObfuscation() throws InterruptedException {
final byte[] keyBytes = new byte[32];
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
try (FileContentEncryptor encryptor = new FileContentEncryptorImpl(headerKey, macKey, RANDOM_MOCK_2, 0)) {
encryptor.append(FileContentCryptor.EOF);
ByteBuffer result = ByteBuffer.allocate(91); // 16 bytes iv + 42 bytes size obfuscation + 32 bytes mac + 1
ByteBuffer buf;
while ((buf = encryptor.ciphertext()) != FileContentCryptor.EOF) {
ByteBuffers.copy(buf, result);
}
result.flip();
Assert.assertEquals(90, result.remaining());
}
}
}

View File

@@ -53,13 +53,26 @@ public class FileHeaderTest {
@Test
public void testDecryption() {
final byte[] keyBytes = new byte[32];
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
final ByteBuffer headerBuf = ByteBuffer.wrap(Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg=="));
final FileHeader header = FileHeader.decrypt(headerKey, new ThreadLocalMac(macKey, "HmacSHA256"), headerBuf);
Assert.assertEquals(-1l, header.getPayload().getFilesize());
Assert.assertArrayEquals(new byte[16], header.getIv());
Assert.assertArrayEquals(new byte[32], header.getPayload().getContentKey().getEncoded());
}
@Test
public void testDecryptionOfOldHeader() {
final byte[] keyBytes = new byte[32];
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
final ByteBuffer headerBuf = ByteBuffer.wrap(Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA=="));
final FileHeader header = FileHeader.decrypt(headerKey, new ThreadLocalMac(macKey, "HmacSHA256"), headerBuf);
Assert.assertEquals(42, header.getPayload().getFilesize());
Assert.assertEquals(42l, header.getPayload().getFilesize());
Assert.assertArrayEquals(new byte[16], header.getIv());
Assert.assertArrayEquals(new byte[32], header.getPayload().getContentKey().getEncoded());
}

View File

@@ -0,0 +1,161 @@
package org.cryptomator.filesystem.crypto;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Pattern;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.BaseNCodec;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.ReadableFile;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
public class ConflictResolverTest {
private ConflictResolver conflictResolver;
private Folder folder;
private File canonicalFile;
private File canonicalFolder;
private File conflictingFile;
private File conflictingFolder;
private File resolved;
private File unrelatedFile;
@Before
public void setup() {
Pattern base32Pattern = Pattern.compile("([A-Z0-9]{8})*[A-Z0-9=]{8}");
BaseNCodec base32 = new Base32();
Function<String, Optional<String>> decode = (s) -> Optional.of(new String(base32.decode(s), StandardCharsets.UTF_8));
Function<String, Optional<String>> encode = (s) -> Optional.of(base32.encodeAsString(s.getBytes(StandardCharsets.UTF_8)));
conflictResolver = new ConflictResolver(base32Pattern, decode, encode);
folder = Mockito.mock(Folder.class);
canonicalFile = Mockito.mock(File.class);
canonicalFolder = Mockito.mock(File.class);
conflictingFile = Mockito.mock(File.class);
conflictingFolder = Mockito.mock(File.class);
resolved = Mockito.mock(File.class);
unrelatedFile = Mockito.mock(File.class);
String canonicalFileName = encode.apply("test name").get();
String canonicalFolderName = Constants.DIR_PREFIX + canonicalFileName;
String conflictingFileName = canonicalFileName + " (version 2)";
String conflictingFolderName = canonicalFolderName + " (version 2)";
String unrelatedName = "notBa$e32!";
Mockito.when(canonicalFile.name()).thenReturn(canonicalFileName);
Mockito.when(canonicalFolder.name()).thenReturn(canonicalFolderName);
Mockito.when(conflictingFile.name()).thenReturn(conflictingFileName);
Mockito.when(conflictingFolder.name()).thenReturn(conflictingFolderName);
Mockito.when(unrelatedFile.name()).thenReturn(unrelatedName);
Mockito.when(canonicalFile.exists()).thenReturn(true);
Mockito.when(canonicalFolder.exists()).thenReturn(true);
Mockito.when(conflictingFile.exists()).thenReturn(true);
Mockito.when(conflictingFolder.exists()).thenReturn(true);
Mockito.when(unrelatedFile.exists()).thenReturn(true);
Mockito.doReturn(Optional.of(folder)).when(canonicalFile).parent();
Mockito.doReturn(Optional.of(folder)).when(canonicalFolder).parent();
Mockito.doReturn(Optional.of(folder)).when(conflictingFile).parent();
Mockito.doReturn(Optional.of(folder)).when(conflictingFolder).parent();
Mockito.doReturn(Optional.of(folder)).when(unrelatedFile).parent();
Mockito.when(folder.file(Mockito.startsWith(canonicalFileName.substring(0, 8)))).thenReturn(resolved);
Mockito.when(folder.file(Mockito.startsWith(canonicalFolderName.substring(0, 8)))).thenReturn(resolved);
Mockito.when(folder.file(canonicalFileName)).thenReturn(canonicalFile);
Mockito.when(folder.file(canonicalFolderName)).thenReturn(canonicalFolder);
Mockito.when(folder.file(conflictingFileName)).thenReturn(conflictingFile);
Mockito.when(folder.file(conflictingFolderName)).thenReturn(conflictingFolder);
Mockito.when(folder.file(unrelatedName)).thenReturn(unrelatedFile);
}
@Test
public void testCanonicalName() {
File result = conflictResolver.resolveIfNecessary(canonicalFile);
Assert.assertSame(canonicalFile, result);
}
@Test
public void testUnrelatedName() {
File result = conflictResolver.resolveIfNecessary(unrelatedFile);
Assert.assertSame(unrelatedFile, result);
}
@Test
public void testConflictingFile() {
File result = conflictResolver.resolveIfNecessary(conflictingFile);
Mockito.verify(conflictingFile).moveTo(resolved);
Assert.assertSame(resolved, result);
}
@Test
public void testConflictingFileIfCanonicalDoesntExist() {
Mockito.when(canonicalFile.exists()).thenReturn(false);
File result = conflictResolver.resolveIfNecessary(conflictingFile);
Mockito.verify(conflictingFile).moveTo(resolved);
Assert.assertSame(resolved, result);
}
@Test
public void testConflictingFolderWithDifferentId() {
ReadableFile directoryId1 = Mockito.mock(ReadableFile.class);
ReadableFile directoryId2 = Mockito.mock(ReadableFile.class);
Mockito.when(canonicalFolder.openReadable()).thenReturn(directoryId1);
Mockito.when(conflictingFolder.openReadable()).thenReturn(directoryId2);
Mockito.when(directoryId1.read(Mockito.any())).thenAnswer(new FillBufferAnswer("id1"));
Mockito.when(directoryId2.read(Mockito.any())).thenAnswer(new FillBufferAnswer("id2"));
File result = conflictResolver.resolveIfNecessary(conflictingFolder);
Mockito.verify(conflictingFolder).moveTo(resolved);
Assert.assertSame(resolved, result);
}
@Test
public void testConflictingFolderWithSameId() {
ReadableFile directoryId1 = Mockito.mock(ReadableFile.class);
ReadableFile directoryId2 = Mockito.mock(ReadableFile.class);
Mockito.when(canonicalFolder.openReadable()).thenReturn(directoryId1);
Mockito.when(conflictingFolder.openReadable()).thenReturn(directoryId2);
Mockito.when(directoryId1.read(Mockito.any())).thenAnswer(new FillBufferAnswer("id1"));
Mockito.when(directoryId2.read(Mockito.any())).thenAnswer(new FillBufferAnswer("id1"));
File result = conflictResolver.resolveIfNecessary(conflictingFolder);
Mockito.verify(conflictingFolder).delete();
Assert.assertSame(canonicalFolder, result);
}
private static class FillBufferAnswer implements Answer<Integer> {
private final byte[] content;
private int bytesRead = 0;
public FillBufferAnswer(String content) {
this.content = content.getBytes(StandardCharsets.UTF_8);
}
@Override
public Integer answer(InvocationOnMock invocation) throws Throwable {
if (bytesRead >= content.length) {
bytesRead = 0;
return -1;
} else {
ByteBuffer buf = invocation.getArgumentAt(0, ByteBuffer.class);
int delta = Math.min(content.length - bytesRead, buf.remaining());
buf.put(content, bytesRead, delta);
bytesRead += delta;
return content.length;
}
}
}
}

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.4</version>
<version>1.2.0</version>
</parent>
<artifactId>filesystem-inmemory</artifactId>
<name>Cryptomator filesystem: In-memory mock</name>

View File

@@ -43,6 +43,11 @@ class InMemoryFile extends InMemoryNode implements File {
return buf;
}
@Override
public long size() throws UncheckedIOException {
return content.get().limit();
}
@Override
public void moveTo(File destination) throws UncheckedIOException {
if (destination instanceof InMemoryFile) {
@@ -103,7 +108,7 @@ class InMemoryFile extends InMemoryNode implements File {
throw new UncheckedIOException(new FileAlreadyExistsException(k));
} else {
if (v == null) {
assert!content.get().hasRemaining();
assert !content.get().hasRemaining();
this.creationTime = Instant.now();
}
this.lastModified = Instant.now();
@@ -120,7 +125,7 @@ class InMemoryFile extends InMemoryNode implements File {
// returning null removes the entry.
return null;
});
assert!this.exists();
assert !this.exists();
}
@Override

View File

@@ -51,11 +51,6 @@ class InMemoryReadableFile implements ReadableFile {
}
}
@Override
public long size() throws UncheckedIOException {
return contentGetter.get().limit();
}
@Override
public void position(long position) throws UncheckedIOException {
assert position < Integer.MAX_VALUE : "Can not use that big in-memory files.";
@@ -64,8 +59,10 @@ class InMemoryReadableFile implements ReadableFile {
@Override
public void close() throws UncheckedIOException {
open.set(false);
readLock.unlock();
if (open.get()) {
open.set(false);
readLock.unlock();
}
}
}

View File

@@ -104,9 +104,7 @@ public class InMemoryFileSystemTest {
Assert.assertTrue(fooFile.exists());
// check if size = 11 bytes
try (ReadableFile readable = fooFile.openReadable()) {
Assert.assertEquals(11, readable.size());
}
Assert.assertEquals(11, fooFile.size());
// copy foo to bar
File barFile = fs.file("bar.txt");

View File

@@ -9,7 +9,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.4</version>
<version>1.2.0</version>
</parent>
<artifactId>filesystem-invariants-tests</artifactId>
<name>Cryptomator filesystem: Invariants tests</name>
@@ -20,6 +20,10 @@
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-api</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-charsets</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-crypto</artifactId>

View File

@@ -4,11 +4,13 @@ import static org.cryptomator.common.test.TempFilesRemovedOnShutdown.createTempD
import java.io.IOException;
import java.io.UncheckedIOException;
import java.text.Normalizer.Form;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.charsets.NormalizedNameFileSystem;
import org.cryptomator.filesystem.crypto.CryptoEngineTestModule;
import org.cryptomator.filesystem.crypto.CryptoFileSystemDelegate;
import org.cryptomator.filesystem.crypto.CryptoFileSystemTestComponent;
@@ -35,8 +37,10 @@ class FileSystemFactories implements Iterable<FileSystemFactory> {
add("ShorteningFileSystem > InMemoryFileSystem", this::createShorteningFileSystemInMemory);
add("StatsFileSystem > NioFileSystem", this::createStatsFileSystemNio);
add("StatsFileSystem > InMemoryFileSystem", this::createStatsFileSystemInMemory);
add("StatsFileSystem > CryptoFileSystem > ShorteningFileSystem > InMemoryFileSystem", this::createCompoundFileSystemInMemory);
add("StatsFileSystem > CryptoFileSystem > ShorteningFileSystem > NioFileSystem", this::createCompoundFileSystemNio);
add("NormalizingFileSystem > NioFileSystem", this::createNormalizingFileSystemNio);
add("NormalizingFileSystem > InMemoryFileSystem", this::createNormalizingFileSystemInMemory);
add("StatsFileSystem > NormalizingFileSystem > CryptoFileSystem > ShorteningFileSystem > InMemoryFileSystem", this::createCompoundFileSystemInMemory);
add("StatsFileSystem > NormalizingFileSystem > CryptoFileSystem > ShorteningFileSystem > NioFileSystem", this::createCompoundFileSystemNio);
}
private FileSystem createCryptoFileSystemInMemory() {
@@ -63,6 +67,14 @@ class FileSystemFactories implements Iterable<FileSystemFactory> {
return createStatsFileSystem(createInMemoryFileSystem());
}
private FileSystem createNormalizingFileSystemNio() {
return createNormalizingFileSystem(createInMemoryFileSystem());
}
private FileSystem createNormalizingFileSystemInMemory() {
return createNormalizingFileSystem(createInMemoryFileSystem());
}
private FileSystem createCompoundFileSystemNio() {
return createCompoundFileSystem(createNioFileSystem());
}
@@ -84,13 +96,17 @@ class FileSystemFactories implements Iterable<FileSystemFactory> {
}
private FileSystem createCompoundFileSystem(FileSystem delegate) {
return createStatsFileSystem(createCryptoFileSystem(createShorteningFileSystem(delegate)));
return createStatsFileSystem(createNormalizingFileSystem(createCryptoFileSystem(createShorteningFileSystem(delegate))));
}
private FileSystem createStatsFileSystem(FileSystem delegate) {
return new StatsFileSystem(delegate);
}
private FileSystem createNormalizingFileSystem(FileSystem delegate) {
return new NormalizedNameFileSystem(delegate, Form.NFC);
}
private FileSystem createCryptoFileSystem(FileSystem delegate) {
CRYPTO_FS_COMP.cryptoFileSystemFactory().initializeNew(delegate, "aPassphrase");
return CRYPTO_FS_COMP.cryptoFileSystemFactory().unlockExisting(delegate, "aPassphrase", Mockito.mock(CryptoFileSystemDelegate.class));

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.4</version>
<version>1.2.0</version>
</parent>
<artifactId>filesystem-nameshortening</artifactId>
<name>Cryptomator filesystem: Name shortening layer</name>

View File

@@ -0,0 +1,70 @@
package org.cryptomator.filesystem.shortening;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.Folder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
final class ConflictResolver {
private static final Logger LOG = LoggerFactory.getLogger(ConflictResolver.class);
private static final String LONG_NAME_FILE_EXT = ".lng";
private static final Pattern BASE32_PATTERN = Pattern.compile("^0?([A-Z2-7]{8})*[A-Z2-7=]{8}");
private static final int UUID_FIRST_GROUP_STRLEN = 8;
private ConflictResolver() {
}
public static File resolveConflictIfNecessary(File potentiallyConflictingFile, FilenameShortener shortener) {
String shortName = potentiallyConflictingFile.name();
String basename = StringUtils.removeEnd(shortName, LONG_NAME_FILE_EXT);
Matcher matcher = BASE32_PATTERN.matcher(basename);
if (shortName.endsWith(LONG_NAME_FILE_EXT) && matcher.matches()) {
// no conflict.
return potentiallyConflictingFile;
} else if (shortName.endsWith(LONG_NAME_FILE_EXT) && matcher.find(0)) {
String canonicalShortName = matcher.group() + LONG_NAME_FILE_EXT;
return resolveConflict(potentiallyConflictingFile, canonicalShortName, shortener);
} else {
// not even shortened at all.
return potentiallyConflictingFile;
}
}
private static File resolveConflict(File conflictingFile, String canonicalShortName, FilenameShortener shortener) {
Folder parent = conflictingFile.parent().get();
File canonicalFile = parent.file(canonicalShortName);
if (canonicalFile.exists()) {
// foo (1).lng -> bar.lng
String canonicalLongName = shortener.inflate(canonicalShortName);
String alternativeLongName;
String alternativeShortName;
File alternativeFile;
String conflictId;
do {
conflictId = createConflictId();
alternativeLongName = canonicalLongName + " (Conflict " + conflictId + ")";
alternativeShortName = shortener.deflate(alternativeLongName);
alternativeFile = parent.file(alternativeShortName);
} while (alternativeFile.exists());
LOG.info("Detected conflict {}:\n{}\n{}", conflictId, canonicalFile, conflictingFile);
conflictingFile.moveTo(alternativeFile);
shortener.saveMapping(alternativeLongName, alternativeShortName);
return alternativeFile;
} else {
// foo (1).lng -> foo.lng
conflictingFile.moveTo(canonicalFile);
return canonicalFile;
}
}
private static String createConflictId() {
return UUID.randomUUID().toString().substring(0, UUID_FIRST_GROUP_STRLEN);
}
}

View File

@@ -8,8 +8,6 @@
*******************************************************************************/
package org.cryptomator.filesystem.shortening;
import java.io.FileNotFoundException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@@ -19,9 +17,12 @@ import org.apache.commons.codec.binary.BaseNCodec;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.io.FileContents;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class FilenameShortener {
private static final Logger LOG = LoggerFactory.getLogger(FilenameShortener.class);
private static final String LONG_NAME_FILE_EXT = ".lng";
private static final ThreadLocal<MessageDigest> SHA1 = new ThreadLocalSha1();
private static final BaseNCodec BASE32 = new Base32();
@@ -71,7 +72,8 @@ class FilenameShortener {
private String loadMapping(String shortName) {
final File mappingFile = mappingFile(shortName);
if (!mappingFile.exists()) {
throw new UncheckedIOException(new FileNotFoundException("Mapping file not found " + mappingFile));
LOG.warn("Mapping file not found: " + mappingFile);
return shortName;
} else {
return FileContents.UTF_8.readContents(mappingFile);
}

View File

@@ -22,7 +22,7 @@ class ShorteningFile extends DelegatingFile<ShorteningFolder> {
private final FilenameShortener shortener;
public ShorteningFile(ShorteningFolder parent, File delegate, String name, FilenameShortener shortener) {
super(parent, delegate);
super(parent, ConflictResolver.resolveConflictIfNecessary(delegate, shortener));
this.longName = new AtomicReference<>(name);
this.shortener = shortener;
}

View File

@@ -0,0 +1,65 @@
package org.cryptomator.filesystem.shortening;
import java.util.Optional;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
public class ConflictResolverTest {
private Folder metadataFolder;
private FilenameShortener shortener;
private Folder folder;
private File canonicalFile;
private File conflictingFile;
private File resolvedFile;
@Before
public void setup() {
metadataFolder = new InMemoryFileSystem();
shortener = new FilenameShortener(metadataFolder, 20);
folder = Mockito.mock(Folder.class);
canonicalFile = Mockito.mock(File.class);
conflictingFile = Mockito.mock(File.class);
resolvedFile = Mockito.mock(File.class);
String longName = "hello world, I am a very long file name. certainly longer than twenty characters.exe";
String shortName = shortener.deflate(longName);
shortener.saveMapping(longName, shortName);
String canonicalFileName = shortName;
String conflictingFileName = shortName.replace(".lng", " (1).lng");
Mockito.when(canonicalFile.name()).thenReturn(canonicalFileName);
Mockito.when(conflictingFile.name()).thenReturn(conflictingFileName);
Mockito.when(canonicalFile.exists()).thenReturn(true);
Mockito.when(conflictingFile.exists()).thenReturn(true);
Mockito.doReturn(Optional.of(folder)).when(canonicalFile).parent();
Mockito.doReturn(Optional.of(folder)).when(conflictingFile).parent();
Mockito.when(folder.file(Mockito.anyString())).thenReturn(resolvedFile);
Mockito.when(folder.file(canonicalFileName)).thenReturn(canonicalFile);
Mockito.when(folder.file(conflictingFileName)).thenReturn(conflictingFile);
}
@Test
public void testNoConflictToBeResolved() {
File resolved = ConflictResolver.resolveConflictIfNecessary(canonicalFile, new FilenameShortener(metadataFolder, 20));
Mockito.verify(conflictingFile, Mockito.never()).moveTo(Mockito.any());
Assert.assertSame(canonicalFile, resolved);
}
@Test
public void testConflictToBeResolved() {
File resolved = ConflictResolver.resolveConflictIfNecessary(conflictingFile, new FilenameShortener(metadataFolder, 20));
Mockito.verify(conflictingFile).moveTo(resolvedFile);
Assert.assertSame(resolvedFile, resolved);
}
}

View File

@@ -8,8 +8,6 @@
*******************************************************************************/
package org.cryptomator.filesystem.shortening;
import java.io.UncheckedIOException;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
import org.junit.Assert;
@@ -45,12 +43,12 @@ public class FilenameShortenerTest {
Assert.assertEquals("short", shortener.inflate("short"));
}
@Test(expected = UncheckedIOException.class)
@Test
public void testInflateWithoutMappingFile() {
FileSystem fs = new InMemoryFileSystem();
FilenameShortener shortener = new FilenameShortener(fs, 10);
shortener.inflate("iJustMadeThisNameUp.lng");
Assert.assertEquals("iJustMadeThisNameUp.lng", shortener.inflate("iJustMadeThisNameUp.lng"));
}
}

View File

@@ -16,6 +16,7 @@ import static org.junit.Assert.assertThat;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.concurrent.TimeoutException;
@@ -102,6 +103,31 @@ public class ShorteningFileSystemTest {
Assert.assertTrue(correspondingMetadataFile.exists());
}
@Test
public void testInflate() {
final FileSystem underlyingFs = new InMemoryFileSystem();
final Folder metadataRoot = underlyingFs.folder(METADATA_DIR_NAME);
final FileSystem fs = new ShorteningFileSystem(underlyingFs, METADATA_DIR_NAME, 10);
final File correspondingMetadataFile = metadataRoot.folder("QM").folder("JL").file("QMJL5GQUETRX2YRV6XDTJQ6NNM7IEUHP.lng");
final Folder shortenedFolder = underlyingFs.folder("QMJL5GQUETRX2YRV6XDTJQ6NNM7IEUHP.lng");
shortenedFolder.create();
correspondingMetadataFile.parent().get().create();
try (WritableFile w = correspondingMetadataFile.openWritable()) {
w.write(ByteBuffer.wrap("morethantenchars".getBytes(StandardCharsets.UTF_8)));
}
Assert.assertTrue(correspondingMetadataFile.exists());
Assert.assertTrue(fs.folders().map(Folder::name).anyMatch(n -> n.equals("morethantenchars")));
}
@Test
public void testInflateFailedDueToMissingMapping() {
final FileSystem underlyingFs = new InMemoryFileSystem();
final FileSystem fs = new ShorteningFileSystem(underlyingFs, METADATA_DIR_NAME, 10);
final Folder shortenedFolder = underlyingFs.folder("QMJL5GQUETRX2YRV6XDTJQ6NNM7IEUHP.lng");
shortenedFolder.create();
Assert.assertTrue(fs.folders().map(Folder::name).anyMatch(n -> n.equals("QMJL5GQUETRX2YRV6XDTJQ6NNM7IEUHP.lng")));
}
@Test
public void testMoveLongFolders() {
final FileSystem underlyingFs = new InMemoryFileSystem();

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
</Console>
<Console name="StdErr" target="SYSTEM_ERR">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
</Console>
</Appenders>
<Loggers>
<Root level="DEBUG">
<AppenderRef ref="Console" />
<AppenderRef ref="StdErr" />
</Root>
</Loggers>
</Configuration>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.4</version>
<version>1.2.0</version>
</parent>
<artifactId>filesystem-nio</artifactId>
<name>Cryptomator filesystem: NIO-based physical layer</name>

View File

@@ -2,6 +2,7 @@ package org.cryptomator.filesystem.nio;
import java.io.IOException;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.AccessDeniedException;
import java.nio.file.CopyOption;
import java.nio.file.FileSystems;
import java.nio.file.Files;
@@ -16,6 +17,11 @@ import java.util.stream.Stream;
class DefaultNioAccess implements NioAccess {
@Override
public long size(Path path) throws IOException {
return Files.size(path);
}
@Override
public AsynchronousFileChannel open(Path path, OpenOption... options) throws IOException {
return AsynchronousFileChannel.open(path, options);
@@ -53,7 +59,18 @@ class DefaultNioAccess implements NioAccess {
@Override
public void delete(Path path) throws IOException {
Files.delete(path);
try {
Files.delete(path);
} catch (AccessDeniedException e) {
// workaround for https://github.com/cryptomator/cryptomator/issues/317
try {
if (path.toFile().delete())
return;
} catch (UnsupportedOperationException e2) {
// ignore
}
throw e;
}
}
@Override

View File

@@ -16,6 +16,8 @@ interface NioAccess {
public static final Holder<NioAccess> DEFAULT = new Holder<>(new DefaultNioAccess());
long size(Path path) throws IOException;
AsynchronousFileChannel open(Path path, OpenOption... options) throws IOException;
boolean isRegularFile(Path path, LinkOption... options);

View File

@@ -27,16 +27,33 @@ class NioFile extends NioNode implements File {
sharedChannel = instanceFactory.sharedFileChannel(path, nioAccess);
}
@Override
public long size() throws UncheckedIOException {
try {
return nioAccess.size(path);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
public ReadableFile openReadable() throws UncheckedIOException {
if (lock.getWriteHoldCount() > 0) {
throw new IllegalStateException("Current thread is currently writing this file");
throw new IllegalStateException("Current thread is currently writing " + path);
}
if (lock.getReadHoldCount() > 0) {
throw new IllegalStateException("Current thread is already reading this file");
throw new IllegalStateException("Current thread is already reading " + path);
}
lock.readLock().lock();
return instanceFactory.readableNioFile(path, sharedChannel, this::unlockReadLock);
ReadableFile result = null;
try {
result = instanceFactory.readableNioFile(path, sharedChannel, this::unlockReadLock);
} finally {
if (result == null) {
unlockReadLock();
}
}
return result;
}
private void unlockReadLock() {
@@ -46,13 +63,21 @@ class NioFile extends NioNode implements File {
@Override
public WritableFile openWritable() throws UncheckedIOException {
if (lock.getWriteHoldCount() > 0) {
throw new IllegalStateException("Current thread is already writing this file");
throw new IllegalStateException("Current thread is already writing " + path);
}
if (lock.getReadHoldCount() > 0) {
throw new IllegalStateException("Current thread is currently reading this file");
throw new IllegalStateException("Current thread is currently reading " + path);
}
lockWriteLock();
return instanceFactory.writableNioFile(fileSystem(), path, sharedChannel, this::unlockWriteLock);
WritableFile result = null;
try {
result = instanceFactory.writableNioFile(fileSystem(), path, sharedChannel, this::unlockWriteLock);
} finally {
if (result == null) {
unlockWriteLock();
}
}
return result;
}
// visible for testing

View File

@@ -41,11 +41,6 @@ class ReadableNioFile implements ReadableFile {
return open;
}
@Override
public long size() throws UncheckedIOException {
return channel.size();
}
@Override
public void position(long position) throws UncheckedIOException {
assertOpen();

View File

@@ -16,6 +16,7 @@ import static org.mockito.Mockito.when;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
@@ -85,6 +86,27 @@ public class NioFileTest {
}
public class Size {
@Test
public void testSizeReturnsSizeOfRegularFile() throws IOException {
when(nioAccess.size(path)).thenReturn(42l);
assertThat(inTest.size(), is(42l));
}
@Test
public void testSizeThrowsExceptionIfRegularFileThrowsException() throws IOException {
Throwable t = new NoSuchFileException("foo");
when(nioAccess.size(path)).thenThrow(t);
thrown.expect(UncheckedIOException.class);
thrown.expectCause(org.hamcrest.Matchers.sameInstance(t));
inTest.size();
}
}
public class Open {
@Test
@@ -99,10 +121,11 @@ public class NioFileTest {
@Test
public void testOpenReadableInvokedBeforeInvokingAfterCloseOperationThrowsIllegalStateException() {
when(instanceFactory.readableNioFile(same(path), same(channel), any())).thenReturn(mock(ReadableNioFile.class));
inTest.openReadable();
thrown.expect(IllegalStateException.class);
thrown.expectMessage("already reading this file");
thrown.expectMessage("already reading " + path);
inTest.openReadable();
}
@@ -111,7 +134,7 @@ public class NioFileTest {
public void testOpenReadableInvokedAfterAfterCloseOperationCreatesNewReadableFile() {
ReadableNioFile readableNioFile = mock(ReadableNioFile.class);
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(null, readableNioFile);
when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(mock(ReadableNioFile.class), readableNioFile);
inTest.openReadable();
captor.getValue().run();
@@ -122,10 +145,11 @@ public class NioFileTest {
@Test
public void testOpenReadableInvokedBeforeInvokingAfterCloseOperationOfOpenWritableThrowsIllegalStateException() {
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), any())).thenReturn(mock(WritableNioFile.class));
inTest.openWritable();
thrown.expect(IllegalStateException.class);
thrown.expectMessage("currently writing this file");
thrown.expectMessage("currently writing " + path);
inTest.openReadable();
}
@@ -133,7 +157,7 @@ public class NioFileTest {
@Test
public void testOpenReadableInvokedAfterInvokingAfterCloseOperationWorks() {
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(null);
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(mock(WritableNioFile.class));
inTest.openWritable();
captor.getValue().run();
@@ -154,7 +178,7 @@ public class NioFileTest {
public void testOpenWritableInvokedAfterAfterCloseOperationCreatesNewWritableFile() {
WritableNioFile writableNioFile = mock(WritableNioFile.class);
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(null, writableNioFile);
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(mock(WritableNioFile.class), writableNioFile);
inTest.openWritable();
captor.getValue().run();
@@ -165,28 +189,31 @@ public class NioFileTest {
@Test
public void testOpenWritableInvokedBeforeInvokingAfterCloseOperationThrowsIllegalStateException() {
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), any())).thenReturn(mock(WritableNioFile.class));
inTest.openWritable();
thrown.expect(IllegalStateException.class);
thrown.expectMessage("already writing this file");
thrown.expectMessage("already writing " + path);
inTest.openWritable();
}
@Test
public void testOpenWritableInvokedBeforeInvokingAfterCloseOperationFromOpenReadableThrowsIllegalStateException() {
when(instanceFactory.readableNioFile(same(path), same(channel), any())).thenReturn(mock(ReadableNioFile.class));
inTest.openReadable();
thrown.expect(IllegalStateException.class);
thrown.expectMessage("currently reading this file");
thrown.expectMessage("currently reading " + path);
inTest.openWritable();
}
@Test
public void testOpenWritableInvokedAfterInvokingAfterCloseOperationWorks() {
ReadableNioFile readableNioFile = mock(ReadableNioFile.class);
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(null);
when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(readableNioFile);
inTest.openReadable();
captor.getValue().run();

View File

@@ -83,16 +83,6 @@ public class ReadableNioFileTest {
inTest.position(-1);
}
@Test
public void testSizeReturnsSizeOfChannel() {
long expectedSize = 85472;
when(channel.size()).thenReturn(expectedSize);
long actualSize = inTest.size();
assertThat(actualSize, is(expectedSize));
}
@Test
public void testReadDelegatesToChannelReadFullyWithZeroPositionIfNotSet() {
ByteBuffer buffer = mock(ByteBuffer.class);

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.4</version>
<version>1.2.0</version>
</parent>
<artifactId>filesystem-stats</artifactId>
<name>Cryptomator filesystem: Throughput statistics</name>

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.4</version>
<version>1.2.0</version>
</parent>
<artifactId>frontend-api</artifactId>
<name>Cryptomator frontend: API</name>

View File

@@ -14,12 +14,20 @@ import java.util.Optional;
public interface Frontend extends AutoCloseable {
public enum MountParam {
MOUNT_NAME, HOSTNAME, WIN_DRIVE_LETTER
MOUNT_NAME, HOSTNAME, WIN_DRIVE_LETTER,
/**
* "dav" or "webdav"
*/
PREFERRED_GVFS_SCHEME
}
void mount(Map<MountParam, Optional<String>> map) throws CommandFailedException;
void unmount() throws CommandFailedException;
/**
* Unmounts the file system and stops any file system handler threads.
*/
void close() throws Exception;
void reveal() throws CommandFailedException;

View File

@@ -16,10 +16,11 @@ public interface FrontendFactory {
* Provides a new frontend to access the given folder.
*
* @param root Root resource accessible through this frontend.
* @param uniqueName Name of the frontend, i.e. used to create subresources for the different frontends inside of a common virtual drive.
* @param id unique id of the frontend, i.e. used to generate a unique uri
* @param name Name of the frontend, i.e. used to generate a readable/recognizable name of a common virtual drive
* @return A new frontend
* @throws FrontendCreationFailedException If creation was not possible.
*/
Frontend create(Folder root, String uniqueName) throws FrontendCreationFailedException;
Frontend create(Folder root, FrontendId id, String name) throws FrontendCreationFailedException;
}

View File

@@ -0,0 +1,85 @@
package org.cryptomator.frontend;
import static java.util.UUID.randomUUID;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.UUID;
public class FrontendId implements Serializable {
public static final String FRONTEND_ID_PATTERN = "[a-zA-Z0-9_-]{12}";
public static FrontendId generate() {
return new FrontendId();
}
public static FrontendId from(String value) {
return new FrontendId(value);
}
private final String value;
private FrontendId() {
this(generateId());
}
private FrontendId(String value) {
if (!value.matches(FRONTEND_ID_PATTERN)) {
throw new IllegalArgumentException("Invalid frontend id " + value);
}
this.value = value;
}
private static String generateId() {
return asBase64String(nineBytesFrom(randomUUID()));
}
private static String asBase64String(ByteBuffer bytes) {
ByteBuffer base64Buffer = Base64.getUrlEncoder().encode(bytes);
return new String(asByteArray(base64Buffer), StandardCharsets.US_ASCII);
}
private static ByteBuffer nineBytesFrom(UUID uuid) {
ByteBuffer uuidBuffer = ByteBuffer.allocate(9);
uuidBuffer.putLong(uuid.getMostSignificantBits());
uuidBuffer.put((byte) (uuid.getLeastSignificantBits() & 0xFF));
uuidBuffer.flip();
return uuidBuffer;
}
private static byte[] asByteArray(ByteBuffer buffer) {
if (buffer.hasArray()) {
return buffer.array();
} else {
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
return bytes;
}
}
@Override
public boolean equals(Object obj) {
if (obj == null || obj.getClass() != getClass()) {
return false;
}
return obj == this || internalEquals((FrontendId) obj);
}
private boolean internalEquals(FrontendId obj) {
return value.equals(obj.value);
}
@Override
public int hashCode() {
return value.hashCode();
}
@Override
public String toString() {
return value;
}
}

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.4</version>
<version>1.2.0</version>
</parent>
<artifactId>frontend-webdav</artifactId>
<name>Cryptomator frontend: WebDAV frontend</name>

View File

@@ -0,0 +1,30 @@
package org.cryptomator.frontend.webdav;
import static java.lang.String.format;
import static org.cryptomator.frontend.FrontendId.FRONTEND_ID_PATTERN;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.cryptomator.frontend.FrontendId;
class ContextPaths {
private static final Pattern SERVLET_PATH_WITH_FRONTEND_ID_PATTERN = Pattern.compile("^/(" + FRONTEND_ID_PATTERN + ")(/.*)?$");
private static final int FRONTEND_ID_GROUP = 1;
public static String from(FrontendId id, String name) {
return format("/%s/%s", id, name);
}
public static Optional<FrontendId> extractFrontendId(String path) {
Matcher matcher = SERVLET_PATH_WITH_FRONTEND_ID_PATTERN.matcher(path);
if (matcher.matches()) {
return Optional.of(FrontendId.from(matcher.group(FRONTEND_ID_GROUP)));
} else {
return Optional.empty();
}
}
}

View File

@@ -11,6 +11,8 @@ package org.cryptomator.frontend.webdav;
import java.io.IOException;
import java.util.EnumSet;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.servlet.DispatcherType;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
@@ -21,26 +23,38 @@ import org.cryptomator.frontend.webdav.filters.LoopbackFilter;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
/**
* The server needs to respond to requests to the root resource, because Windows is stupid.
*/
public class WindowsCompatibilityServlet extends HttpServlet {
@Singleton
class DefaultServlet extends HttpServlet {
private static final String ROOT_PATH = "/";
private static final String WILDCARD = "/*";
private final Tarpit tarpit;
@Inject
public DefaultServlet(Tarpit tarpit) {
this.tarpit = tarpit;
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
tarpit.handle(req);
super.service(req, resp);
}
@Override
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.addHeader("DAV", "1, 2");
resp.addHeader("MS-Author-Via", "DAV");
// resp.addHeader("Allow", "OPTIONS, GET, HEAD, POST, TRACE, PROPFIND, PROPPATCH, MKCOL, COPY, PUT, DELETE, MOVE, LOCK, UNLOCK");
resp.addHeader("Allow", "OPTIONS, GET, HEAD");
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
}
public static ServletContextHandler createServletContextHandler() {
public ServletContextHandler createServletContextHandler() {
final ServletContextHandler servletContext = new ServletContextHandler(null, ROOT_PATH, ServletContextHandler.NO_SESSIONS);
final ServletHolder servletHolder = new ServletHolder(ROOT_PATH, WindowsCompatibilityServlet.class);
final ServletHolder servletHolder = new ServletHolder(ROOT_PATH, this);
servletContext.addServlet(servletHolder, ROOT_PATH);
servletContext.addFilter(LoopbackFilter.class, ROOT_PATH, EnumSet.of(DispatcherType.REQUEST));
servletContext.addFilter(LoopbackFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST));
return servletContext;
}

View File

@@ -0,0 +1,71 @@
/*******************************************************************************
* Copyright (c) 2016 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*******************************************************************************/
package org.cryptomator.frontend.webdav;
import static java.lang.Math.max;
import static java.lang.System.currentTimeMillis;
import static java.util.Collections.synchronizedSet;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.servlet.http.HttpServletRequest;
import org.cryptomator.frontend.FrontendId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
class Tarpit implements Serializable {
private static final Logger LOG = LoggerFactory.getLogger(Tarpit.class);
private static final long DELAY_MS = 10000;
private final Set<FrontendId> validFrontendIds = synchronizedSet(new HashSet<>());
@Inject
public Tarpit() {
}
public void setValidFrontendIds(Collection<FrontendId> validFrontendIds) {
this.validFrontendIds.retainAll(validFrontendIds);
this.validFrontendIds.addAll(validFrontendIds);
}
public void handle(HttpServletRequest req) {
if (isRequestWithInvalidVaultId(req)) {
delayExecutionUninterruptibly();
LOG.debug("Delayed request to " + req.getRequestURI() + " by " + DELAY_MS + "ms");
}
}
private boolean isRequestWithInvalidVaultId(HttpServletRequest req) {
Optional<FrontendId> frontendId = ContextPaths.extractFrontendId(req.getServletPath());
return frontendId.isPresent() && !isValid(frontendId.get());
}
private void delayExecutionUninterruptibly() {
long expected = currentTimeMillis() + DELAY_MS;
long sleepTime = DELAY_MS;
while (expected > currentTimeMillis()) {
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
sleepTime = max(0, currentTimeMillis() - expected + 10);
}
}
}
private boolean isValid(FrontendId frontendId) {
return validFrontendIds.contains(frontendId);
}
}

View File

@@ -24,6 +24,7 @@ class WebDavFrontend implements Frontend {
private final WebDavMounterProvider webdavMounterProvider;
private final ServletContextHandler handler;
private final URI uri;
private WebDavMount mount;
public WebDavFrontend(WebDavMounterProvider webdavMounterProvider, ServletContextHandler handler, URI uri) throws FrontendCreationFailedException {
@@ -45,13 +46,13 @@ class WebDavFrontend implements Frontend {
@Override
public void mount(Map<MountParam, Optional<String>> mountParams) throws CommandFailedException {
mount = webdavMounterProvider.get().mount(uri, mountParams);
mount = webdavMounterProvider.chooseMounter(mountParams).mount(uri, mountParams);
}
@Override
public void unmount() throws CommandFailedException {
private void unmount() throws CommandFailedException {
if (mount != null) {
mount.unmount();
mount = null;
}
}

View File

@@ -0,0 +1,11 @@
package org.cryptomator.frontend.webdav;
import org.cryptomator.common.CommonsModule;
import org.cryptomator.frontend.webdav.mount.WebDavMounterModule;
import dagger.Module;
@Module(includes = {CommonsModule.class, WebDavMounterModule.class})
public class WebDavModule {
}

View File

@@ -8,8 +8,11 @@
*******************************************************************************/
package org.cryptomator.frontend.webdav;
import static java.lang.String.format;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
@@ -20,6 +23,7 @@ import org.cryptomator.filesystem.Folder;
import org.cryptomator.frontend.Frontend;
import org.cryptomator.frontend.FrontendCreationFailedException;
import org.cryptomator.frontend.FrontendFactory;
import org.cryptomator.frontend.FrontendId;
import org.cryptomator.frontend.webdav.mount.WebDavMounterProvider;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
@@ -45,9 +49,10 @@ public class WebDavServer implements FrontendFactory {
private final ContextHandlerCollection servletCollection;
private final WebDavServletContextFactory servletContextFactory;
private final WebDavMounterProvider webdavMounterProvider;
private final Tarpit tarpit;
@Inject
WebDavServer(WebDavServletContextFactory servletContextFactory, WebDavMounterProvider webdavMounterProvider) {
WebDavServer(WebDavServletContextFactory servletContextFactory, WebDavMounterProvider webdavMounterProvider, DefaultServlet defaultServlet, Tarpit tarpit) {
final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(MAX_PENDING_REQUESTS);
final ThreadPool tp = new QueuedThreadPool(MAX_THREADS, MIN_THREADS, THREAD_IDLE_SECONDS, queue);
this.server = new Server(tp);
@@ -55,8 +60,9 @@ public class WebDavServer implements FrontendFactory {
this.servletCollection = new ContextHandlerCollection();
this.servletContextFactory = servletContextFactory;
this.webdavMounterProvider = webdavMounterProvider;
servletCollection.addHandler(WindowsCompatibilityServlet.createServletContextHandler());
this.tarpit = tarpit;
servletCollection.addHandler(defaultServlet.createServletContextHandler());
server.setConnectors(new Connector[] {localConnector});
server.setHandler(servletCollection);
}
@@ -103,10 +109,8 @@ public class WebDavServer implements FrontendFactory {
}
@Override
public Frontend create(Folder root, String contextPath) throws FrontendCreationFailedException {
if (!contextPath.startsWith("/")) {
throw new IllegalArgumentException("contextPath must begin with '/'");
}
public Frontend create(Folder root, FrontendId id, String name) throws FrontendCreationFailedException {
String contextPath = format("/%s/%s", id, name);
final URI uri;
try {
uri = new URI("http", null, "localhost", getPort(), contextPath, null, null);
@@ -117,5 +121,9 @@ public class WebDavServer implements FrontendFactory {
LOG.info("Servlet available under " + uri);
return new WebDavFrontend(webdavMounterProvider, handler, uri);
}
public void setValidFrontendIds(Collection<FrontendId> validFrontendIds) {
tarpit.setValidFrontendIds(validFrontendIds);
}
}

View File

@@ -22,6 +22,7 @@ import org.cryptomator.frontend.webdav.filters.AcceptRangeFilter;
import org.cryptomator.frontend.webdav.filters.LoopbackFilter;
import org.cryptomator.frontend.webdav.filters.MacChunkedPutCompatibilityFilter;
import org.cryptomator.frontend.webdav.filters.MkcolComplianceFilter;
import org.cryptomator.frontend.webdav.filters.PostRequestBlockingFilter;
import org.cryptomator.frontend.webdav.filters.UriNormalizationFilter;
import org.cryptomator.frontend.webdav.filters.UriNormalizationFilter.ResourceTypeChecker;
import org.cryptomator.frontend.webdav.filters.UriNormalizationFilter.ResourceTypeChecker.ResourceType;
@@ -34,10 +35,9 @@ import org.eclipse.jetty.servlet.ServletHolder;
class WebDavServletContextFactory {
private static final String WILDCARD = "/*";
@Inject
public WebDavServletContextFactory() {
}
public WebDavServletContextFactory() {}
/**
* Creates a new Jetty ServletContextHandler, that can be be added to a servletCollection as follows:
@@ -67,6 +67,7 @@ class WebDavServletContextFactory {
final ServletHolder servletHolder = new ServletHolder(contextPath, new WebDavServlet(contextRoot, root));
servletContext.addServlet(servletHolder, WILDCARD);
servletContext.addFilter(LoopbackFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST));
servletContext.addFilter(PostRequestBlockingFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST));
servletContext.addFilter(MkcolComplianceFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST));
servletContext.addFilter(AcceptRangeFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST));
servletContext.addFilter(new FilterHolder(new UriNormalizationFilter(resourceTypeChecker)), WILDCARD, EnumSet.of(DispatcherType.REQUEST));

View File

@@ -0,0 +1,43 @@
package org.cryptomator.frontend.webdav.filters;
import static java.util.Arrays.stream;
import static java.util.function.Predicate.isEqual;
import static java.util.stream.Collectors.joining;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
class PostFromAllowHeaderRemovingHttpServletResponseWrapper extends HttpServletResponseWrapper {
public PostFromAllowHeaderRemovingHttpServletResponseWrapper(HttpServletResponse response) {
super(response);
}
@Override
public void addHeader(String name, String value) {
if (isAllowHeader(name)) {
super.setHeader(name, removePost(value));
} else {
super.addHeader(name, value);
}
}
@Override
public void setHeader(String name, String value) {
if (isAllowHeader(name)) {
super.setHeader(name, removePost(value));
} else {
super.setHeader(name, value);
}
}
private String removePost(String value) {
return stream(value.split("\\s*,\\s*"))
.filter(isEqual("POST").negate())
.collect(joining(", "));
}
private boolean isAllowHeader(String name) {
return "allow".equalsIgnoreCase(name);
}
}

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