Compare commits

...

411 Commits
0.9.0 ... 1.1.0

Author SHA1 Message Date
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
Sebastian Stenzel
ad2c9116b9 Release 1.0.4 2016-05-03 16:50:18 +02:00
Sebastian Stenzel
8e24745b3e Merge branch 'master' into patches-1.0.x 2016-05-03 16:46:18 +02:00
Sebastian Stenzel
08f664e3df Throttle calls to Settings.save() 2016-05-03 16:44:22 +02:00
Tobias Hagemann
b6d1d1dc22 updated linux app icon [ci skip] 2016-05-03 16:07:30 +02:00
Sebastian Stenzel
a0ef02b95c fixes #237 2016-05-03 13:17:45 +02:00
Sebastian Stenzel
a6cefe67c4 setting default port to 42427 [ci skip] 2016-05-03 10:48:24 +02:00
Sebastian Stenzel
be2b63ab2a support for UTF-8 localization files 2016-05-02 22:28:49 +02:00
Sebastian Stenzel
78f11b4a5e added korean localization [ci skip] 2016-05-02 16:01:28 +02:00
Sebastian Stenzel
0f20c7c3c9 fixes #209 2016-05-02 12:36:31 +02:00
Sebastian Stenzel
d4235174f7 imported localizations from POEditor fixes #231 and #234 (ci skip) 2016-05-02 11:37:40 +02:00
Sebastian Stenzel
f16be84aa3 restored bash-based webdav mounting for OS X before 10.10 (issue #211 - to be tested) 2016-05-02 11:11:42 +02:00
Sebastian Stenzel
833f2d8566 fixed travis test coverage configuration 2016-04-27 01:18:37 +02:00
Sebastian Stenzel
c02a63878e new method to calculate test coverage (across modules) 2016-04-27 01:14:41 +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
ac9fe28967 Merge branch 'master' into patches-1.0.x
Release 1.0.3d
2016-04-22 10:55:03 +02:00
Sebastian Stenzel
515755d84a updated antkit to support deb and rpm packages [ci skip] 2016-04-22 10:53:51 +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
Sebastian Stenzel
fef19fe6b3 Merge branch 'master' into patches-1.0.x 2016-04-14 22:39:56 +02:00
Sebastian Stenzel
5f56dacc4e adjusted travis configuration [ci skip] 2016-04-14 22:39:19 +02:00
Sebastian Stenzel
aa249dabb5 technical release 1.0.3c 2016-04-14 22:28:12 +02:00
Sebastian Stenzel
06a5bed6e3 Merge branch 'master' into patches-1.0.x 2016-04-14 22:27:15 +02:00
Sebastian Stenzel
02f1ffc6bf updated antkit creation (tarball no longer contains a base directory) 2016-04-14 22:26:36 +02:00
jncharon
bcfe040784 Merge remote-tracking branch 'cryptomator/master' 2016-04-13 18:22:16 +02:00
Sebastian Stenzel
de9af9e303 fixed funny detail label in vault list, if vault is not located inside home directory 2016-04-13 15:26:27 +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
Sebastian Stenzel
e2bc71a0bc added spanish translation template [ci skip] 2016-04-11 14:17:33 +02:00
Sebastian Stenzel
e528f6827c Added translation button [ci skip] 2016-04-11 14:02:11 +02:00
Sebastian Stenzel
2882ae8ef8 Update localization_de.properties (POEditor.com) 2016-04-11 13:46:51 +02:00
Sebastian Stenzel
e37f7cea1a Merge pull request #227 from jncharon/master
French translation
2016-04-11 10:55:26 +02:00
jncharon
9b4ee10155 Adjustments to the french translation 2016-04-10 15:53:53 +02:00
jncharon
c9d970955c French translaction 2016-04-10 15:10:47 +02:00
Sebastian Stenzel
9e0afd36c4 Merge branch 'master' into patches-1.0.x [ci skip] 2016-04-10 02:42:25 +02:00
Sebastian Stenzel
0e523599a3 add execution phase 2016-04-10 02:41:51 +02:00
Sebastian Stenzel
1df6589dd7 make sure, .tar.gz is built on travis 2016-04-10 02:36:10 +02:00
Sebastian Stenzel
fb60c97fd3 Merge branch 'master' into patches-1.0.x 2016-04-10 02:19:52 +02:00
Sebastian Stenzel
90cd149be8 Update .travis.yml 2016-04-10 02:19:01 +02:00
Sebastian Stenzel
89c04ad83b test release 1.0.3b 2016-04-10 02:07:52 +02:00
Sebastian Stenzel
f2d383a211 Merge branch 'master' into patches-1.0.x 2016-04-10 01:56:13 +02:00
Sebastian Stenzel
73fde5d020 null-safe status indicators 2016-04-10 01:54:44 +02:00
Sebastian Stenzel
5c0857e98e build ant-kit on tag/release [ci skip] 2016-04-10 01:53:44 +02:00
Sebastian Stenzel
3e87b9c0c6 oracle jdk8 + jce on trusty 2016-04-10 00:28:11 +02:00
Sebastian Stenzel
a1d0b6b1d3 trying to build with openjdk8 on trusty 2016-04-10 00:22:56 +02:00
Tobias Hagemann
b0d4b2e403 fixed support mail link in code of conduct [ci skip] 2016-04-06 00:04:36 +02:00
Tobias Hagemann
6996d36ea2 added issue template, contribution guide, code of conduct [ci skip] 2016-04-05 12:28:36 +02:00
Sebastian Stenzel
f77ba908da Patch 1.0.3 2016-03-25 16:43:58 +01:00
Sebastian Stenzel
9890789c51 Merge branch 'master' into patches-1.0.x
# Conflicts:
#	main/commons-test/pom.xml
#	main/commons/pom.xml
#	main/filesystem-api/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/pom.xml
#	main/uber-jar/pom.xml
#	main/ui/pom.xml
2016-03-25 16:42:44 +01:00
Sebastian Stenzel
a385f2eaef fixes #174 2016-03-25 16:41:30 +01:00
Sebastian Stenzel
553cb5ee3d Migration of vault bundles ending on ".cryptomator" to normal directories. 2016-03-24 22:51:40 +01:00
Markus Kreusch
d0dc8819f4 No longer using TrayIcon on linux systems
* Reason: TrayIcon not supported well on linux and caused problems
* Renamed TrayIconUtil to ExitUtil
* fixes #177
2016-03-24 14:20:10 +01:00
Sebastian Stenzel
221deeda25 removed .cryptomator directory extension when creating new vaults 2016-03-22 21:36:26 +01:00
Sebastian Stenzel
86000ac454 removed test dependencies from main project 2016-03-22 13:04:46 +01:00
Sebastian Stenzel
d026afec35 adjusted snapshot version [ci skip] 2016-03-21 16:49:58 +01:00
Sebastian Stenzel
0d57ebb24a Patch 1.0.2 2016-03-21 16:44:01 +01:00
Sebastian Stenzel
f12168ca94 allow deletion of corrupt directories, whose dir-file exists but physical directory doesn't. related to #181 2016-03-21 15:49:44 +01:00
Sebastian Stenzel
d397f59565 fixes #181 2016-03-21 14:46:15 +01:00
markus
77aaeabcde Release 1.0.1 2016-03-15 23:30:54 +01:00
Sebastian Stenzel
768f291ff7 avoid duplicate network drives on windows 2016-03-12 18:55:38 +01:00
Markus Kreusch
6a374cc237 Releases no longer marked as pre-release 2016-03-09 09:32:30 +01:00
Sebastian Stenzel
0133ec8fdf updated HEAD version to 1.1.0-SNAPSHOT [ci skip] 2016-03-09 09:21:08 +01:00
Sebastian Stenzel
d9ba4935b6 restored compatibility with vaults created on the iOS app 2016-03-09 09:01:26 +01:00
Sebastian Stenzel
b6ee29789e linking to /faq instead of /help.html (new website) [ci skip] 2016-03-09 00:38:20 +01:00
Markus Kreusch
5ee82271f5 Improved wording [ci skip] 2016-03-08 16:16:59 +01:00
Markus Kreusch
2eb4d87dd1 fixes #160 2016-03-08 16:14:47 +01:00
Sebastian Stenzel
d0afeab74b UI refinements, fixes #166 2016-03-08 15:31:10 +01:00
Sebastian Stenzel
cc74c2c05b fixes #165 again 2016-03-07 17:37:27 +01:00
Sebastian Stenzel
8865cf0e4b Revert "fixes #165"
This reverts commit 65550ce70f.
2016-03-07 17:30:15 +01:00
Markus Kreusch
65550ce70f fixes #165 2016-03-07 17:25:29 +01:00
Markus Kreusch
78300f8bf1 Localization of stats labels 2016-03-07 14:56:34 +01:00
Markus Kreusch
32c65a7dda Skipping test execution for coverity builds 2016-03-07 14:03:24 +01:00
Markus Kreusch
6d31ed7ea4 fixes #163
* coverity issue 72253
2016-03-07 13:55:21 +01:00
Markus Kreusch
c3e5d3f38e fixes #162 2016-03-07 13:13:45 +01:00
Markus Kreusch
e3900231aa Added all FileSystems to invariants tests 2016-03-07 11:04:36 +01:00
Sebastian Stenzel
06f13c57d4 Added webdav url as "string" to the clipboard - additionally to "url" type. This should make it possible to paste the copied address nearly everywhere. #73 [ci skip] 2016-03-05 22:06:12 +01:00
Sebastian Stenzel
fc1a5be85f fixed "encrypt anyway" button 2016-03-05 16:02:05 +01:00
Sebastian Stenzel
a30b310c04 close underlying file, if exception in constructor of CryptoReadableFile or CryptoWritableFile 2016-03-05 14:49:46 +01:00
Sebastian Stenzel
956dd855f9 WebDAV: return null, if file size could not be determined e.g. due to invalid file headers 2016-03-05 14:33:11 +01:00
Sebastian Stenzel
67ba7cac40 Vault doesn't need to be Serializable in order for ObjectMapper to work properly 2016-03-04 21:27:46 +01:00
Sebastian Stenzel
9117b6bc0e Coverity 72994 2016-03-04 17:56:02 +01:00
Sebastian Stenzel
bae826be28 Coverity 72980 2016-03-04 17:53:55 +01:00
Sebastian Stenzel
d845e8d97a Coverity 72979 2016-03-04 17:50:07 +01:00
Sebastian Stenzel
b37b2e4fb7 Coverity 72941 2016-03-04 17:48:57 +01:00
Sebastian Stenzel
69f6a9927d Coverity 72975, 72976, 72977 2016-03-04 17:48:07 +01:00
Sebastian Stenzel
addc9533eb Coverity 72988 2016-03-04 17:46:41 +01:00
Sebastian Stenzel
8b717993ed Coverity 72944 2016-03-04 17:45:33 +01:00
Sebastian Stenzel
f70d486462 Coverity 72964 2016-03-04 17:44:12 +01:00
Sebastian Stenzel
293ac0ea3c delete empty directories inside ./d/ 2016-03-04 16:51:10 +01:00
Sebastian Stenzel
e99a615b09 closing channel, if registration failed. Coverity issue 72309 2016-03-04 01:49:43 +01:00
Sebastian Stenzel
6da3fde864 work with number of bytes returned by ReadableFile.read(), Coverity issues 72259 and 72261 2016-03-04 01:39:50 +01:00
Sebastian Stenzel
3a725e4a16 fixed equals methods, Coverity issues 72280, 72281, 72283, 72284 2016-03-04 01:34:38 +01:00
Sebastian Stenzel
e3256a747f fixes Coverity issue 72287 2016-03-04 01:31:26 +01:00
Sebastian Stenzel
adc20ea2f2 code simplification, added further assertions for Coverity issue 72293 2016-03-04 01:29:34 +01:00
Sebastian Stenzel
997f841662 Only compare versions, if parsing was successful, see Coverity issue 72294 2016-03-04 01:23:48 +01:00
Sebastian Stenzel
e57b60f04e value is known to be null anyway 2016-03-04 01:23:16 +01:00
Sebastian Stenzel
d5b4fb4fe9 json parsing exception handling, see Coverity issues 72297, 72296, 72295 2016-03-04 01:20:38 +01:00
Sebastian Stenzel
edf92adfec thread safety, see Coverity issues 72313 and 72314 2016-03-04 01:05:24 +01:00
Sebastian Stenzel
718bacafa6 added coverity to travis configuration 2016-03-03 21:10:55 +01:00
Sebastian Stenzel
7122bdf199 changed version to 0.12.0-SNAPSHOT [ci skip] 2016-03-03 20:42:19 +01:00
Sebastian Stenzel
f9b988bf81 Beta 0.11.0 2016-03-03 17:56:17 +01:00
Sebastian Stenzel
9a3fab7545 fixes #83 [ci skip] 2016-03-03 17:40:46 +01:00
Tobias Hagemann
d9668182dd updated non-retina tray icons [ci skip] 2016-03-03 17:26:30 +01:00
Sebastian Stenzel
82e04553f6 ui polishing [ci skip] 2016-03-03 17:21:27 +01:00
Sebastian Stenzel
84ee2dfcaa fixes #50 2016-03-03 14:16:20 +01:00
Sebastian Stenzel
f05440fe7a UI code cleanup 2016-03-03 14:05:50 +01:00
Sebastian Stenzel
e3fd25aa41 quit application when closing the window and no vault is unlocked [ci skip] 2016-03-03 12:19:20 +01:00
Sebastian Stenzel
c130d0e4a0 fixes #149 2016-03-03 12:06:41 +01:00
Markus Kreusch
7fba38d78a BlockAlignedFileSystem no longer instantiable from other projects 2016-03-02 22:57:25 +01:00
Sebastian Stenzel
807fdae3b9 fixes #73 2016-03-02 22:41:05 +01:00
Sebastian Stenzel
890a0c4408 "UI refactoring", preparation for #73 2016-03-02 22:23:40 +01:00
Markus Kreusch
e57ee67208 Integrated BlacklistingFileSystem into ShorteningFileSystem 2016-03-02 21:57:43 +01:00
Sebastian Stenzel
c306151980 fixes #88 2016-03-02 13:10:26 +01:00
Sebastian Stenzel
164a0c6901 disable "remove" button when the selected vault is unlocked [ci skip] 2016-03-02 11:12:04 +01:00
Sebastian Stenzel
31c92bd4a2 added BSD 2-clause license for EasyBind [ci skip] 2016-03-01 20:53:50 +01:00
Sebastian Stenzel
94b8726379 - Added settings (references #88)
- Added dependency EasyBind to UI
- Using property bindings instead of listeners in lots of places of the UI now
2016-03-01 20:47:15 +01:00
Sebastian Stenzel
ca929241f2 fixes #131 2016-02-29 22:28:04 +01:00
Sebastian Stenzel
9abc0aab83 updated copyrights 2016-02-29 22:27:52 +01:00
Sebastian Stenzel
c9564bae62 - Added "remove vault" button to toolbar
- Using Ionicons in the toolbar now
2016-02-29 21:04:35 +01:00
Sebastian Stenzel
8fd0bbd9ed Using "cryptomator" prefix for all JVM args now [ci skip] 2016-02-29 18:44:50 +01:00
Sebastian Stenzel
be7b875be7 remove vaults that can no longer be read [ci skip] 2016-02-29 13:57:29 +01:00
Sebastian Stenzel
020597c42d removed unused imports [ci skip] 2016-02-29 12:39:43 +01:00
Sebastian Stenzel
75b67e5976 test simplification [ci skip] 2016-02-29 12:38:59 +01:00
Sebastian Stenzel
a6c99c273e some Windows WebDAV compatibility fixes 2016-02-29 12:25:24 +01:00
Sebastian Stenzel
be4dab2773 fixes #108 2016-02-28 18:51:17 +01:00
Sebastian Stenzel
7b68c427d6 fixes #141 2016-02-28 18:37:55 +01:00
Sebastian Stenzel
bc9b7c3a19 check vault version before unlocking 2016-02-28 18:14:07 +01:00
Sebastian Stenzel
cbb669aa40 reduced visibility of some classes 2016-02-28 17:49:21 +01:00
Sebastian Stenzel
be7e7e32b9 Same fix as previously made in commit 1a81b3a781: Deleting a DAV resource implicitly unlocks it. 2016-02-28 16:21:59 +01:00
Sebastian Stenzel
2ae5abfc0a - determine available space in NIO file system (fixes #97)
- mount volumes via Finder, not via shell script. this makes creating volumes manually unnecessary
2016-02-23 21:52:27 +01:00
Sebastian Stenzel
7cb435e517 support for RFC 4331: DAV:quota-available-bytes and DAV:quota-used-bytes
references #97; quota calculation in NIO filesystem still missing
2016-02-23 18:13:19 +01:00
Sebastian Stenzel
95b5f4c765 fixed switching to unlock view after initializing vault 2016-02-23 13:43:30 +01:00
Sebastian Stenzel
d926cbfd5c removed unused import [ci skip] 2016-02-22 23:49:27 +01:00
Sebastian Stenzel
61f1afba87 deleted legacy code 2016-02-22 23:48:01 +01:00
Sebastian Stenzel
c03bdd8425 - decrypt whole file, don't stop if enough data has been read from underlying fs
- write "length = 0" into file header until everything is encrypted

(tested on windows, everything is fine here)
2016-02-22 23:06:02 +01:00
Sebastian Stenzel
5b22806bbc fixed writing short ciphertext to file, that existed with a longer ciphertext before but wasn't truncated 2016-02-22 19:07:02 +01:00
Sebastian Stenzel
1467c8315c - moved method from WritableFile to File: moveTo, setLastModified, setCreationTime, delete
- moved method from File and Folder to Node: setLastModified, setCreationTime, delete
2016-02-22 16:42:31 +01:00
Sebastian Stenzel
e6a9786b7a minor bytebuffer allocation optimization 2016-02-22 14:58:27 +01:00
Sebastian Stenzel
819c56fe4c debugging WebDAV + Crypto + NIO 2016-02-22 14:58:08 +01:00
Sebastian Stenzel
ae7e865c24 fixed unit test 2016-02-22 13:12:36 +01:00
Sebastian Stenzel
c8df03a085 added test to check authentication errors in file headers 2016-02-22 13:08:58 +01:00
Sebastian Stenzel
5df9f35065 Locking unmapped URLs results in creation of that resource now, as specified in http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10.4 2016-02-22 13:06:56 +01:00
Sebastian Stenzel
ac4a68649e now really: fixed invalid URI during MOVE, if dest path contains special chars (like spaces) 2016-02-22 11:37:37 +01:00
Sebastian Stenzel
7bc678d4dc - don't write encrypted file header, if no actual write happens inside of an encrypted writable file (temporary workaround only!)
- reduced loglevel
2016-02-21 23:34:03 +01:00
Sebastian Stenzel
b48670e073 fixed invalid URI during MOVE, if dest path contains special chars (like spaces) 2016-02-21 22:54:01 +01:00
Markus Kreusch
391d8013b5 Fixed CryptoWritableFile errors when reading from moved file
* CryptoWritableFile no longer writes header with zero size if file is
opened for writing
* Refactored FileContentDecryptor: Using Supplier<Mac> instead of
ThreadLocal<Mac>
* Fixed InMemoryWritableFile: No longer open after moveTo
2016-02-21 21:33:35 +01:00
Markus Kreusch
023e7d70e5 corrected type name [ci skip] 2016-02-21 19:50:14 +01:00
Markus Kreusch
2c92435b91 Refactored WebDav test servers
* Moved common code into own type
2016-02-21 18:57:50 +01:00
Markus Kreusch
180b14b0c7 Added /foo to server path 2016-02-21 18:38:16 +01:00
Sebastian Stenzel
8280bfe10a code simplification [ci skip] 2016-02-21 11:55:44 +01:00
Markus Kreusch
1994610d57 Refactored Concurrency test to guarantee correct execution order of 2016-02-21 00:22:20 +01:00
Markus Kreusch
a54f925b70 Moved Concurrency tests into own class 2016-02-21 00:22:20 +01:00
Sebastian Stenzel
c7c4dd4581 added file size obfuscation padding 2016-02-21 00:20:57 +01:00
Markus Kreusch
c93e4e462b Added dagger to filesystem-invariants-tests
* Added dependencies to pom
* Moved dagger version in parent pom to variable
2016-02-20 19:58:30 +01:00
Markus Kreusch
6950ad496a Removed /foo from server path 2016-02-20 19:58:30 +01:00
Sebastian Stenzel
8a7fca7a95 amendment to previously committed unit test [ci skip] 2016-02-20 18:10:26 +01:00
Sebastian Stenzel
a5e6c9fb9b added concurrent read invariant test 2016-02-20 17:43:32 +01:00
Sebastian Stenzel
382c3a0258 correct answer to well-formed but still unsatisfiable range request 2016-02-20 14:34:11 +01:00
Sebastian Stenzel
7f313772e5 fixed random access positioning 2016-02-20 14:10:46 +01:00
Sebastian Stenzel
3a82dfb23f - fixed return value of readBlockAligend
- adjusted loglevel of BlockAlignedWritableFile
[ci skip]
2016-02-20 12:34:14 +01:00
Sebastian Stenzel
b85a110a24 - report authentication error on the UI
- reduced visibility of some classes
2016-02-18 16:39:34 +01:00
Sebastian Stenzel
5a84228678 removed duplicate test case [ci skip] 2016-02-17 17:37:38 +01:00
Sebastian Stenzel
57b40675ac WebDAV range request support is back! 2016-02-17 17:35:05 +01:00
Sebastian Stenzel
50e8a9e429 a few more webdav frontend tests 2016-02-17 13:33:31 +01:00
Sebastian Stenzel
f93d32c6fb faster directory moving (no recursive iteration on filesystem needed any longer) 2016-02-17 12:05:16 +01:00
Sebastian Stenzel
a909095a1c Fixed reuse of directory-IDs after moving encrypted directories but keeping references to the old locations.
This caused an illegal state where two different directories share the same directory id, making further file operations on these directories virtually impossible.
2016-02-15 23:31:09 +01:00
Sebastian Stenzel
03208ebc5e fixed CryptoFolder (damn partial commits...) 2016-02-15 22:39:52 +01:00
Sebastian Stenzel
c41225eab6 Code simplification 2016-02-15 22:34:11 +01:00
Sebastian Stenzel
2725b6b920 not spawning new threadpools for each individual encryption/decryption job anymore 2016-02-15 16:35:30 +01:00
Sebastian Stenzel
ed0540e78f Using Runnable instead of Consumer for InMemoryWritableFile's reference to InMemoryFile's delete function [ci skip] 2016-02-15 15:55:54 +01:00
Markus Kreusch
8cdb6d0eab Fixed CryptoFileSystem
* avoid creation of a file and folder with equal names
2016-02-14 23:07:55 +01:00
Markus Kreusch
932e26f6a6 Added further invariant tests 2016-02-14 23:06:16 +01:00
Sebastian Stenzel
53a9b08dd6 fixed unit test 2016-02-14 21:00:12 +01:00
Sebastian Stenzel
752601f4da further crypto layer optimizations for WebDAV compliance 2016-02-14 20:50:42 +01:00
Sebastian Stenzel
cf64a6c425 fixed webdav copy/move [ci skip] 2016-02-14 00:26:47 +01:00
Sebastian Stenzel
f0cb91b22f - separated crypto filesystem and nameshortening filesystem for easier invariant testing
- creation of crypto filesystem only via DI in all integration tests (package-private constructor)
- new Maven module "filesystem-crypto-integration-tests" for tests as well as to provide a ready-to-use Dagger Component for integration tests in other modules.
2016-02-13 14:33:16 +01:00
Sebastian Stenzel
0d82e7dcc7 code simplicifaction + lockmanager unit tests 2016-02-12 14:31:47 +01:00
Sebastian Stenzel
bf5988f5fe renamed packages 2016-02-12 13:01:03 +01:00
Sebastian Stenzel
3d24bc74b1 only one token must match, see http://www.webdav.org/specs/rfc2518.html#rfc.section.9.4.2 [ci skip] 2016-02-12 12:57:36 +01:00
Sebastian Stenzel
6f15ea0e1e passing litmus cond_put_corrupt_token test [ci skip] 2016-02-12 12:38:24 +01:00
Sebastian Stenzel
93ef366125 now passing 94.6% of litmus lock tests 2016-02-12 01:01:26 +01:00
Sebastian Stenzel
cbcefc4eb5 - check locks, even when no if-header is present
- updated Jackrabbit
2016-02-11 00:24:13 +01:00
Sebastian Stenzel
42b852b622 removed logging [ci skip] 2016-02-10 22:46:39 +01:00
Sebastian Stenzel
12fcf5aeaf several WebDAV compliance fixes 2016-02-10 19:23:43 +01:00
Sebastian Stenzel
a1a81cc0ba ROOT directory file no longer needed. root directory id is now "" by convention. 2016-02-08 21:53:24 +01:00
Sebastian Stenzel
853744002c prohibiting block swapping by adding file-IV and blocknumber to MAC 2016-02-08 18:47:02 +01:00
Sebastian Stenzel
4d2a786504 new vault-internal file extensions (no extension at all for files, "_" suffix for directories) 2016-02-08 15:17:50 +01:00
Sebastian Stenzel
bb185c3170 fixed bug causing directories to disappear immediately after renaming to a long name. 2016-02-08 15:16:43 +01:00
Sebastian Stenzel
e5d095606f chunk layout version 3 (random nonce per block) 2016-02-08 13:57:19 +01:00
Sebastian Stenzel
4a60e94183 version 3 header (no nonce, CTR mode) 2016-02-08 13:14:20 +01:00
Sebastian Stenzel
1d6f4284c8 determine file size of all files during directory listing [ci skip] 2016-02-08 12:40:54 +01:00
Sebastian Stenzel
eb1b4a9fe3 updated license header 2016-01-29 02:09:10 +01:00
Sebastian Stenzel
64775a7d19 using Guavas ByteStreams.copy where possible 2016-01-29 02:06:52 +01:00
Sebastian Stenzel
8784115c75 report client version in user agent header during update checks [ci skip] 2016-01-29 01:11:13 +01:00
Markus Kreusch
56fcb99248 Test filesystem api
* Invariant tests for the File interface
* Invariant tests for reading / writing files
** Due to missing features currently ignoring CryptoFileSystem
2016-01-28 23:26:49 +01:00
Markus Kreusch
bfe0a50205 Fixed ShorteningFileSystem/Folder
* ShorteningFileSystem now exists after instantiation
* Creation of ShorteningFolder does no longer fail
2016-01-28 23:25:51 +01:00
Sebastian Stenzel
26aa18de77 Restored I/O graph 2016-01-28 21:21:21 +01:00
Sebastian Stenzel
6af4ee08f7 I/O statistics filesystem layer 2016-01-28 20:49:32 +01:00
Sebastian Stenzel
4059f99fd5 restored DelegatingReadableFile and DelegatingWritableFile for I/O stats 2016-01-28 20:33:16 +01:00
Sebastian Stenzel
0dc30c27d9 added crypto file system tests 2016-01-28 19:49:20 +01:00
Sebastian Stenzel
3696fea3ee fixed invalid string formatter pattern 2016-01-28 19:07:10 +01:00
Sebastian Stenzel
bb34f5c17d nameshortening tests 2016-01-28 19:05:34 +01:00
Sebastian Stenzel
b2a37c4b95 - read XOR write during BlockAlignedWritableFile (not r+w at the same time)
- simplified DelegatingFileSystem
2016-01-27 20:14:51 +01:00
Sebastian Stenzel
a972480e72 supporting change password again - now via CryptoFileSystemFactory 2016-01-27 18:21:47 +01:00
Sebastian Stenzel
091a44e65d exception handling during mount/unmount/reveal operations 2016-01-27 14:46:48 +01:00
Sebastian Stenzel
c56d0b7d4a first compile-clean attempt to integrate the layered I/O subsystem with the existing UI 2016-01-26 20:17:33 +01:00
Sebastian Stenzel
e4d626eef5 added some basic integration tests for the WebDavServer 2016-01-25 16:48:11 +01:00
Sebastian Stenzel
9052e7995f added WebDavServer class to webdav frontend 2016-01-25 16:13:21 +01:00
Sebastian Stenzel
e23baa33f9 updated copyrights 2016-01-25 14:32:15 +01:00
Sebastian Stenzel
8d7c3a8f7b renamed module 2016-01-25 14:30:51 +01:00
Markus Kreusch
56b061206a Deleting temporary directories created in tests on shutdown
* Deleting using a shutdown hook (other approaches didn't work)
2016-01-25 00:03:22 +01:00
Markus Kreusch
f081e7d3ea Removed SharedFileChannel transferTo and corresponding methods
* Removed from SharedFileChannel and Test
* Refactored Copier#copy(File,File) to sequence of truncated, followed
by looping read and write till EOF
* Changed tests accordingly
* Implemented CryptoWritableFile#truncate to make things work
2016-01-24 22:14:06 +01:00
Markus Kreusch
e241c5ba05 Worked on TODOs in SharedFileChannelTest
* Now testing for correct exception thrown from write and read
2016-01-24 01:12:55 +01:00
Markus Kreusch
406a9970ba Fixed InMemoryFile#copyTo
* Does always copy the full contents
* Does always overwrite all contents in the target file
* Added file invariant test to test write after read returns written
bytes
2016-01-24 00:42:59 +01:00
Markus Kreusch
8ff5659680 Using AsynchronousFileChannel
* Replaced FileChannel with AsynchronousFileChannel
* Adapted tests
** transferTo tests and exception handling tests still pending (see
TODOs)
2016-01-23 23:52:31 +01:00
Sebastian Stenzel
2e5264bac2 support for forced decryption ignoring failed authentication 2016-01-23 16:49:04 +01:00
Sebastian Stenzel
a6bbc0ed44 - CryptoFS + InMemory Benchmark test
- faster growing in-memory files
2016-01-23 02:07:13 +01:00
Markus Kreusch
6479573346 Added FolderCopyToTests
* Tests for CopyTo Operation
* Changes to Matchers and Test-Utilities
* Changes to make things work
* TODO: One test still not working due to access to channel by multiple
threads
2016-01-22 22:58:16 +01:00
Sebastian Stenzel
cd72dae0d7 pass I/O exceptions on producer side to the consumer, so that decryption fails, if reading the decrypted file fails. 2016-01-17 21:44:47 +01:00
Markus Kreusch
d5c43f625f Added log4j config 2016-01-17 19:02:25 +01:00
Markus Kreusch
0a1eaa8600 Fixed exception when closing channel that was opened with exception 2016-01-17 19:02:24 +01:00
Sebastian Stenzel
9278426131 forgot one place in commit fa35b63 [ci skip] 2016-01-16 01:18:23 +01:00
Sebastian Stenzel
fd98f0a69f pass though IOExceptions from writer thread during close() [ci skip] 2016-01-16 01:09:10 +01:00
Sebastian Stenzel
fa35b63b6d same InterruptedIOException in all cases 2016-01-16 01:00:57 +01:00
Markus Kreusch
51f5b6661f Adjusted fix for issue in CryptoFileSystem when deleting a file
* CryptoWritableFile now only invokes writeTaks.get if not already
closed
* CryptoWritableFile now cancels writeTask before delete
2016-01-16 00:47:13 +01:00
Markus Kreusch
a7eb99f7d5 Renamed classes [ci skip] 2016-01-16 00:10:32 +01:00
Markus Kreusch
1a9ac16256 Additional filesystem-invariants-tests
* added tests
* fixed issue in CryptoFileSystem when deleting a file
** FileContentEncryptorImpl now does not submit an empty cleartext
buffer when receiving EOF
** CryptoWritableFile now only writesHeader on close if the file is
still open
2016-01-15 23:54:21 +01:00
Markus Kreusch
b330148b5f Avoid loosing exception stacktrace 2016-01-15 21:46:40 +01:00
Markus Kreusch
297ce34c1a Renamed classes [ci skip] 2016-01-15 19:28:35 +01:00
Markus Kreusch
20e7f4a548 Added FolderChildrenTests and FolderTests
* implemented some testcases
* fixed some issues
2016-01-15 19:13:46 +01:00
Markus Kreusch
18cf25738e Javadoc [ci skip] 2016-01-15 11:55:36 +01:00
Markus Kreusch
d14c81d066 Tests for AutoClosingStream 2016-01-15 11:52:32 +01:00
Markus Kreusch
459ce8b5d2 Completed implementation of AutoClosingStream 2016-01-14 21:47:27 +01:00
Sebastian Stenzel
bf0988bb20 - Changed OS X PUT request filter from timeout-controlled to headerfield-controlled
- added tests
2016-01-13 18:11:22 +01:00
Markus Kreusch
9c844e626a Fixed NioFileSystemIntegrationTests on windows
* Streams returned from NioFolder#children, files and folders are now
closed automatically after a terminal operation
* Not closing them lead to a bug on windows causing directories to be
not deleted after a successful Files.delete invocation
2016-01-12 23:52:28 +01:00
Sebastian Stenzel
8e1c63338f more modular design of UriNormalizationFilter [ci skip] 2016-01-12 21:03:18 +01:00
Markus Kreusch
b16ceb1ba8 Allow to set served path for NioWebDavServer by system property 2016-01-12 20:26:29 +01:00
Sebastian Stenzel
b2a90ddcf6 removed tests that no longer apply 2016-01-12 13:13:46 +01:00
Sebastian Stenzel
55bee3d0d5 Perform URI normalization based on the result of checking for actually existing files & folders instead of request parameters only. This should fixe MOVE requests on Linux 2016-01-12 12:32:39 +01:00
Sebastian Stenzel
58b4905c91 apply an idle timeout to chunked put requests (as finder doesn't terminate chunked transfers properly) [ci skip] 2016-01-11 18:43:57 +01:00
Sebastian Stenzel
9c9e9769ee fixed dependencies 2016-01-10 16:30:00 +01:00
Sebastian Stenzel
3b178030c7 - Fixes #128 and #119 by using unique directory id as associated data during filename encryption/decryption
- Using WeakValuedCache in all filesystem layers to prevent "twin" instances of the same folder
- Merge branch 'layered-io' of https://github.com/cryptomator/cryptomator into layered-io
2016-01-10 16:27:56 +01:00
Markus Kreusch
f735a64814 Improved request logging 2016-01-10 15:06:56 +01:00
Sebastian Stenzel
560c6251f6 moved WeakValuedCache to commons 2016-01-10 15:04:35 +01:00
Sebastian Stenzel
b2d425e11f added possibility to add associated data to filename encryption (references #128, #119) 2016-01-10 13:39:12 +01:00
Sebastian Stenzel
8f319b3f87 dir listing speed: dont't determine filesize during getProperties() but only when explicitly asked by getProperty(String) 2016-01-09 22:19:16 +01:00
Markus Kreusch
5ff4f4c9c7 Changed handling of creation time
* creationTime now creates an Optional
* Removed FileSystem#supports and FileSystemFeature
2016-01-09 19:15:07 +01:00
Sebastian Stenzel
31f1b6478a apparently webdav modification/creation times need to be UTC (even though the time zone is part of the RFC 1123 string...) [ci skip] 2016-01-09 18:47:00 +01:00
Sebastian Stenzel
a84a4b932d support for webdav properties:
- getcontentlength
- getlastmodified
- creationdate (why isn't this one called "get"?)
2016-01-09 18:34:13 +01:00
Markus Kreusch
248984ce20 Fixed test dependencies 2016-01-09 17:56:25 +01:00
Sebastian Stenzel
fcf4476ae3 Added ReadableFile#size() 2016-01-09 16:39:14 +01:00
Sebastian Stenzel
0c42392244 Make delegating folders create their parents during own creation if they don't exist yet. 2016-01-09 01:22:27 +01:00
Markus Kreusch
415423abd7 Added creation time
* Getter and setter for files and folders
* A way to determine if a file system supports creation dates
* WebDav compliant implementation in jackrabbit-adapter
* Tests
2016-01-09 00:51:25 +01:00
Markus Kreusch
a746a73667 Fixed problem with modification time property set as creation time
instead
2016-01-08 23:15:57 +01:00
Sebastian Stenzel
a8f53b7084 Increased performance of non-random-access read/write by switching to block-aligned mode only when necessary. 2016-01-06 10:50:31 +01:00
Markus Kreusch
94ed3a6b7c Reenabled NioFileSystem integrationtests 2016-01-06 01:55:08 +01:00
Markus Kreusch
3f44d9bb66 Added project filesystem-invariants-tests
* Implemented some tests to be run on multiple implementations to
demonstrate how such tests can be implemented
* Detected problems with CryptoFileSystem(NioFileSystem)
* Made CryptoFileSystem and CryptorImpl public / constructible from
other packages
2016-01-06 01:51:46 +01:00
Markus Kreusch
801253aa27 Tests for filesystem-nio
* Split tests into integration and unit tests
* Rewritten and completed unit tests
** By introducing a layer around Files.* to allow mocking of
NIO-Operations
** And introducing a factory to allow mocking of constructors
* Integration tests ignored temporarily
** TODO reduce amount of testcases and enable
2016-01-06 01:51:46 +01:00
Sebastian Stenzel
a05fa19de4 Merge branch 'separate-installers' into layered-io 2016-01-06 01:03:45 +01:00
Sebastian Stenzel
0b5e09ac60 moved OS-specific installers to separate git repos [ci skip] 2016-01-06 01:03:08 +01:00
Sebastian Stenzel
ddf6353729 Using DI to stack up filesystem layers 2016-01-05 16:55:52 +01:00
Sebastian Stenzel
b31f6a0aec fixed another test, but not that important. thus [ci skip] 2016-01-04 21:32:11 +01:00
Sebastian Stenzel
f75cf48d7b fixed that test... 2016-01-04 21:03:09 +01:00
Sebastian Stenzel
578f52f4ba cleanup, attempt to fix test 2016-01-04 20:46:10 +01:00
Sebastian Stenzel
ae55874709 Random Access Decryption 2016-01-04 20:31:49 +01:00
Sebastian Stenzel
f46a79fa63 Added test for AuthenticationFailedException during filename decryption 2016-01-02 14:40:17 +01:00
Markus Kreusch
ff4448bac0 Tests for filesystem-nio
* Renamed tests from ...IntegrationTest back to ...Test
** to allow better integration with moreunit
** because some methods of the classes can only be integration tested
some not which lead to a strange splitting of the tests
* Added more tests
2016-01-02 02:05:18 +01:00
Sebastian Stenzel
e9f5593e33 fixed InMemoryFileSystem: Only one volatile file/folder for a given name at a time. 2016-01-01 23:09:48 +01:00
Sebastian Stenzel
a9744167c1 new filesystem-aware WebDAV LocatorFactory 2016-01-01 22:48:16 +01:00
Sebastian Stenzel
4e7f3503d9 adjusted in-memory filesystem to comply with API (return files/folders when requested, even though the oposite kind exists for the given name) 2016-01-01 22:44:46 +01:00
Markus Kreusch
71face8091 Tests for filesystem-nio
* Renamed existing tests to ...IntegrationTest
* Created Unit-Tests for Readable- and WritableNioFile (incomplete)
2016-01-01 19:00:39 +01:00
Sebastian Stenzel
0c2caf4469 Even more FifoParallelDataProcessor simplification + test 2016-01-01 16:15:36 +01:00
Sebastian Stenzel
9665ca8dff Simplified FifoParallelDataProcessor 2016-01-01 16:06:19 +01:00
Sebastian Stenzel
c3652a22a0 reject execution when shut down 2016-01-01 14:10:15 +01:00
Sebastian Stenzel
3043aa29c2 added enc/dec benchmark test 2016-01-01 13:49:20 +01:00
Sebastian Stenzel
a388acfce4 added test 2016-01-01 12:50:25 +01:00
Markus Kreusch
64dec64e95 Created NioWebDavServer
* Extracted Server-logic from InMemoryWebDavServer to
FileSystemBasedWebdavServer
* Made InMemoryWebdavServer and NioWebdavvServer use
FileSystemBasedWebdavServer
2015-12-31 17:46:24 +01:00
Markus Kreusch
39535d08e7 Implemented NioFile
* Implementation of NioFile methods
* Extracted Readable/WritableNioFile into separate classes
** Created SharedFileChannel to allow Readable/WritableNioFile for the
same NioFile to use a single, shared FileChannel
* Added tests for NioFile
* Tests for Readable/WritableNioFile pending
2015-12-31 16:49:54 +01:00
Markus Kreusch
806e366a72 Added test for Copier
* Created Test covering all lines and mutations
* Added HierachicalContextRunner dependency to allow more structured
tests
2015-12-30 20:53:27 +01:00
Markus Kreusch
db4b8955f4 Implemented missing tests for NioFolder 2015-12-30 17:05:02 +01:00
Markus Kreusch
1bf0c76918 Added tests for NioFolder
* Completed testcases
* Implemented most of them (2 missing)
* Added OptionalMatcher
2015-12-30 00:43:02 +01:00
Sebastian Stenzel
282fd5ecee added copyright header [ci skip] 2015-12-29 22:09:12 +01:00
Sebastian Stenzel
634f176cf9 Added block-aligned read/write 2015-12-29 22:05:19 +01:00
Sebastian Stenzel
c86068d7bb updated InMemoryFile to support distinct read and write access without mixing up positions and stuff 2015-12-29 21:58:47 +01:00
Markus Kreusch
6acbba476b Fixed weak valued cache test
* Test should no longer fails when JVM memory is more than
Integer.MAX_VALUE
2015-12-29 21:48:00 +01:00
Sebastian Stenzel
9385c3bf6d separated filename shortening layer from metadata hiding layer 2015-12-29 16:24:42 +01:00
Sebastian Stenzel
aa89f60c2f package name org.cryptomator.webdav.jackrabbit was ambiguous 2015-12-29 14:29:25 +01:00
Sebastian Stenzel
44d1250986 Added new filesystem layer for read/write alignment with boundary of encrypted chunks. 2015-12-29 13:41:47 +01:00
Sebastian Stenzel
c21654eac1 DelegatingFileSystem: Use factories for Files, Folders, ReadableFiles and WritableFiles 2015-12-29 11:46:37 +01:00
Sebastian Stenzel
c466c7e3ba New base class for filesystem decorators, which basically just delegate work to the underlying file system 2015-12-29 10:50:23 +01:00
Sebastian Stenzel
3317babe6b renamed package org.cryptomator.crypto.fs to org.cryptomator.filesystem.crypto (same as all other org.cryptomator.filesystem.* filesystems) [ci skip] 2015-12-29 07:50:31 +01:00
Sebastian Stenzel
3998a7cd58 typo [ci skip] 2015-12-29 07:47:03 +01:00
Markus Kreusch
260303127e Replaced DirectoryWalker by FileSystemVisitor 2015-12-28 23:07:09 +01:00
Markus Kreusch
d073bdfad9 Removed FolderCreateMode from filesystem api
* Folder#create now always creates the parents
2015-12-28 21:29:34 +01:00
Markus Kreusch
62d8cdfe4f Added commons project 2015-12-28 20:58:38 +01:00
Markus Kreusch
356ea5c319 Refactored filesystem api
* Removed Readable- and WritableBytes
** Replaced with Readable-/WritableByteChannel
** Methods now integrated in Readable- and WritableFile
** Replaced positioned read/write by method to set the position
2015-12-28 20:54:18 +01:00
Markus Kreusch
1804a52740 Restructured test dependencies
* All modules now have commons-test as test dependency.
* commons-test has dependencies to junit, hamcrest and mockito
2015-12-28 19:30:40 +01:00
Markus Kreusch
3dd9c623f0 Fixed typo 2015-12-28 19:29:35 +01:00
Markus Kreusch
157839c32f Changes to filesystem API and nio implementation
* Partial implementation of nio filesystem
* Addded some tests
* Added project for common test dependencies
* Removed default implementation of Folder#delete
** reason: didn't work because empty folders were not deleted and this
cannot be done in the default implementation
2015-12-28 16:32:09 +01:00
Sebastian Stenzel
0254569826 webdav move/copy/delete 2015-12-28 14:06:40 +01:00
Sebastian Stenzel
389c49d846 PUT and MKCOL support. Simplified paths (utilizing a servlet filter to make sure, directory paths always end on "/" while file paths don't). 2015-12-27 21:53:50 +01:00
Sebastian Stenzel
d3000da2e9 added javax.servlet-api dependency needed by jackrabbit 2015-12-24 00:40:39 +01:00
Sebastian Stenzel
c4eee58f72 moved debug log4j config to correct directory [ci skip] 2015-12-24 00:33:38 +01:00
Sebastian Stenzel
e67c8f2816 first experiments with jackrabbit-filesystem-adapter 2015-12-24 00:32:07 +01:00
Sebastian Stenzel
dc5d3e21f3 Externalized FileHeader encryption/decryption to separate class 2015-12-20 14:51:53 +01:00
Sebastian Stenzel
80e1185325 Adding blocking behaviour when appending data to be en/decrypted.
Using composite instead of inheritance for FileContentDecryptorImpl and FileContentEncryptorImpl
2015-12-20 12:42:02 +01:00
Sebastian Stenzel
49c70c03fe now also testing MAC against openssl-computed result.
also we're testing encryption of "hello world" instead of "hello world " now :D
[ci skip]
2015-12-20 11:13:08 +01:00
Sebastian Stenzel
3045805751 File content encryption and decryption (still without padding, no partial support) 2015-12-20 00:38:14 +01:00
Sebastian Stenzel
4e0143eb05 started implementation of FileContentEncryptorImpl 2015-12-19 18:22:03 +01:00
Sebastian Stenzel
951a02a9a5 cleanup 2015-12-19 11:26:35 +01:00
Sebastian Stenzel
69b192fe82 shutting down file reader when closing the readablefile 2015-12-19 01:15:54 +01:00
Sebastian Stenzel
a055066f72 added timeouts to crypto fs tests 2015-12-19 00:54:51 +01:00
Sebastian Stenzel
a879ed2237 Preparations for file content encryption (no partial support yet) 2015-12-18 22:07:12 +01:00
Sebastian Stenzel
9711314080 removed unused code [ci skip] 2015-12-18 00:09:48 +01:00
Sebastian Stenzel
b22ac719f2 renamed filesystem layer maven modules 2015-12-18 00:06:31 +01:00
Markus Kreusch
25eed3dc4a Changes to filesystem API and nio implementation
* Partial implementation of nio filesystem
* Removed timeouts from openReadable and openWritable
* Added convenience methods for copying
* Added utility to support deadlock safe opening of multiple files
2015-12-17 23:46:58 +01:00
Sebastian Stenzel
58524e5099 added test for file/folder move operations 2015-12-16 19:56:46 +01:00
Sebastian Stenzel
eadf736e98 added new "shortening layer" responsible for shortening long file names
the crypto layer is no longer resposible for the postprocessing of long names, as this is an unrelated task without any security implications
2015-12-16 18:37:08 +01:00
Sebastian Stenzel
b41ccb6054 added slack notifaction to travis config 2015-12-15 23:26:35 +01:00
Sebastian Stenzel
72c0d2cb96 removed timeouts from tests, as timeouts were caused by slow JVM stuff like class loading or Cipher.getInstance 2015-12-15 21:43:24 +01:00
Sebastian Stenzel
92e9fc5871 Merge code coverage metrics from branch 'master' into layered-io 2015-12-15 21:35:01 +01:00
Sebastian Stenzel
044532ef15 reverted to original coveralls snippet 2015-12-15 21:25:55 +01:00
Sebastian Stenzel
e4955ea6c8 still trying to speed up build
building uber-jar only when deploying
2015-12-15 21:14:56 +01:00
Sebastian Stenzel
b2cb5d1dac trying to speed up build 2015-12-15 20:54:26 +01:00
Sebastian Stenzel
e11b0a3421 trying to speed up build 2015-12-15 20:48:12 +01:00
Sebastian Stenzel
de29c84a0c fixed code coverage job config 2015-12-15 20:39:10 +01:00
Sebastian Stenzel
b3c65267df added test coverage build plugin 2015-12-15 20:32:55 +01:00
Sebastian Stenzel
70eb0c99e4 implemented encryption/decryption of masterkey file in crypto layer 2015-12-15 19:50:42 +01:00
Sebastian Stenzel
35bb042430 moved CryptoFolder.contains(Node) to Folder.isAncestorOf(Node), clarified a few javadocs 2015-12-15 13:50:50 +01:00
Sebastian Stenzel
762f362784 adjusted to updated API, restored Folder.copy and Folder.move 2015-12-15 02:27:41 +01:00
Markus Kreusch
3c7651a78a Modified file system API
* Changed IOExceptions ot UncheckedIOExceptions
* Added javadoc
* Added directory move and copy operations
2015-12-14 23:49:28 +01:00
Sebastian Stenzel
99015680b1 began implementation of new filesystem api based encryption layer 2015-12-14 19:20:00 +01:00
Sebastian Stenzel
e1b74ce312 first tests with refactored io layers 2015-12-14 04:37:29 +01:00
Markus Kreusch
3971d3afd5 Added first draft of file system API 2015-12-13 21:45:03 +01:00
Sebastian Stenzel
3adfe6871b reduced visibility of test classes [ci skip] 2015-12-07 15:05:01 +01:00
Sebastian Stenzel
97a72ecbf7 make some better use of mocks during unit tests 2015-12-07 14:59:12 +01:00
Sebastian Stenzel
0697e19b01 less random numbers in tests, explicit test timeout 2015-12-06 14:52:46 +01:00
Sebastian Stenzel
973a2fb395 fail with exception, if crypto worker is waiting too long (1 second) to be able to write to the output stream 2015-12-06 14:41:27 +01:00
Sebastian Stenzel
fc06595977 cancel work if poisoning (i.e. graceful termination) fails after timeout 2015-12-06 12:02:56 +01:00
Sebastian Stenzel
e4220246ab (now really) fixed deadlock when crypto workers die due to exceptions 2015-12-06 11:10:19 +01:00
Sebastian Stenzel
128a93d44e fixed deadlock when crypto workers die due to exceptions 2015-12-06 02:05:20 +01:00
Sebastian Stenzel
a7c19624ce randomizing masterkeys is now a explicit task, so random numbers are not needed when loading a masterkey file from disk. trying to use an uninitialized cryptor results in runtime exceptions 2015-11-23 15:09:39 +01:00
Sebastian Stenzel
2fca4629b9 externalized AES-SIV to independent library 2015-11-18 08:25:50 +01:00
Sebastian Stenzel
4d5cc7a5a0 fixes #104 2015-11-05 20:39:05 +01:00
Sebastian Stenzel
ebb421bd4c fixes #100 (reveal script now running on background thread. to be confirmed on Windows Vista) 2015-11-03 00:29:39 +01:00
Sebastian Stenzel
0c116d0385 Merge branch '0.10.1'
Conflicts:
	main/core/pom.xml
	main/crypto-aes/pom.xml
	main/crypto-api/pom.xml
	main/installer-debian/pom.xml
	main/installer-osx/pom.xml
	main/installer-win-portable/pom.xml
	main/installer-win/pom.xml
	main/pom.xml
	main/uber-jar/pom.xml
	main/ui/pom.xml
2015-11-02 14:21:17 +01:00
Sebastian Stenzel
1f0aea9d8a Version 0.10.1 2015-11-02 14:16:49 +01:00
Sebastian Stenzel
6c4752cee1 Preparation for App Store builds:
possibility to disable update checks by setting java environment variable `-Dcryptomator.updatesManagedExternally=true`
2015-11-02 14:15:53 +01:00
Sebastian Stenzel
b254564657 fixes #77 2015-11-02 13:01:25 +01:00
Tobias Hagemann
014dbd912a starting development of 0.11.0 [ci skip] 2015-10-29 19:07:28 +01:00
Tobias Hagemann
1e18a11886 release 0.10.0
- fixes #92
- fixes #84
- improves #76
- improves #66 (fewer WebDAV requests needed for file creation)
- might solve a few problems of #68
2015-10-29 19:04:38 +01:00
Sebastian Stenzel
386059a238 enabled UI caching for most controls (references #76) 2015-10-29 12:49:45 +01:00
Sebastian Stenzel
b4ab09b3aa Using Java 8 LongAdder instead of AtomicLong for better performance during I/O analysis. 2015-10-29 11:46:48 +01:00
Sebastian Stenzel
289ac55ccd Deleted IORuntimeException in favour of UncheckedIOException 2015-10-29 11:26:28 +01:00
Sebastian Stenzel
b5160cddb9 improved recursive directory deletion 2015-10-29 10:24:11 +01:00
Sebastian Stenzel
1a81b3a781 several refactorings, especially concerning LOCK operations on windows 2015-10-28 22:44:59 +01:00
Tobias Hagemann
b6a5db5797 Merge pull request #95 from tuphamnguyen/patch-1
Updated README.md
2015-10-28 22:25:45 +01:00
Tu Pham Nguyen
aaf98c4fb9 Updated README.md
Minor spelling, capitalization, and grammar issues. Nothing serious :)
2015-10-28 16:03:51 -05:00
Sebastian Stenzel
55d1ffe703 improved smoothing, stricter IO impl during encryption 2015-10-28 17:55:28 +01:00
Sebastian Stenzel
5fefa3c6d4 exponential smoothing for I/O graph 2015-10-28 16:40:50 +01:00
Sebastian Stenzel
b404e52670 starting development of 0.10.0 [ci skip] 2015-10-16 13:54:56 +02:00
483 changed files with 30785 additions and 7688 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,8 +1,23 @@
sudo: required
dist: trusty
language: java
jdk:
- oraclejdk8
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 -Puber-jar clean package
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
notifications:
webhooks:
urls:
@@ -10,12 +25,30 @@ notifications:
on_success: change
on_failure: always
on_start: false
slack:
rooms:
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
deploy:
provider: releases
prerelease: true
prerelease: false
api_key:
secure: ZjE1j93v3qbPIe2YbmhS319aCbMdLQw0HuymmluTurxXsZtn9D4t2+eTr99vBVxGRuB5lzzGezPR5zjk5W7iHF7xhwrawXrFzr2rPJWzWFt0aM+Ry2njU1ROTGGXGTbv4anWeBlgMxLEInTAy/9ytOGNJlec83yc0THpOY2wxnk=
file: main/uber-jar/target/Cryptomator-$TRAVIS_TAG.jar
secure: "ZjE1j93v3qbPIe2YbmhS319aCbMdLQw0HuymmluTurxXsZtn9D4t2+eTr99vBVxGRuB5lzzGezPR5zjk5W7iHF7xhwrawXrFzr2rPJWzWFt0aM+Ry2njU1ROTGGXGTbv4anWeBlgMxLEInTAy/9ytOGNJlec83yc0THpOY2wxnk="
file:
- "main/uber-jar/target/Cryptomator-$TRAVIS_TAG.jar"
- "main/ant-kit/target/antkit.tar.gz"
skip_cleanup: true
on:
repo: cryptomator/cryptomator

74
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,74 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at support@cryptomator.org. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

33
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,33 @@
# Contributing to Cryptomator
## Did you find a bug?
- Ensure you're running the latest version of Cryptomator.
- Ensure the bug is related to the desktop version of Cryptomator. Bugs concerning the Cryptomator iOS app can be reported on the [Cryptomator for iOS issues list](https://github.com/cryptomator/cryptomator-ios/issues).
- Ensure the bug was not [already reported](https://github.com/cryptomator/cryptomator/issues). You can also check out our [FAQ](https://cryptomator.org/faq/) and our [Wiki](https://github.com/cryptomator/cryptomator/wiki).
- If you're unable to find an open issue addressing the problem, [submit a new one](https://github.com/cryptomator/cryptomator/issues/new).
## Do you have questions?
- Ask questions by [submitting a new issue](https://github.com/cryptomator/cryptomator/issues/new).
- [Contact us](https://cryptomator.org/contact/) directly by writing an email. Wir sprechen auch Deutsch!
- Have a chat with us on [Gitter](https://gitter.im/cryptomator/cryptomator).
## Did you write a patch that fixes a bug?
- Open a new pull request with the patch.
- Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable.
## Do you intend to add a new feature or change an existing one?
- Suggest your change by [submitting a new issue](https://github.com/cryptomator/cryptomator/issues/new) and start writing code.
## Code of Conduct
Help us keep Cryptomator open and inclusive. Please read and follow our [Code of Conduct](https://github.com/cryptomator/cryptomator/blob/master/CODE_OF_CONDUCT.md).
## Above all, thank you for your contributions
Thank you for taking the time to contribute to the project! :+1:
Cryptomator Team

19
ISSUE_TEMPLATE.md Normal file
View File

@@ -0,0 +1,19 @@
### Basic Info
- I'm running Cryptomator on: [Windows, OS X, and/or Debian (or other Linux Distribution), don't forget the version]
- I'm using Cryptomator in version: [you can check the version in the settings of Cryptomator]
### Description
[description of the bug, question or feature - what did you do? what problem occurred? etc.]
### Log File (optional)
```
[insert relevant parts of the log file here if applicable,
don't forget to redact sensitive information
on Windows: %appdata%/Cryptomator/cryptomator.log
on OS X: ~/Library/Logs/Cryptomator/cryptomator.log
on Debian: ~/.Cryptomator/cryptomator.log]
```

View File

@@ -1,12 +1,27 @@
Copyright (c) <YEAR>, <OWNER>
Copyright (c) [year], [fullname]
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
* Neither the name of [project] nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,23 @@
Copyright (c) [year], [fullname]
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -4,7 +4,7 @@ Copyright (c) 2014, Sebastian Stenzel
Cryptomator is licensed under the MIT license. The details can be found in the accompanying license file.
## Third party softwares
Cryptomator uses third party softwares that may be licensed under different licenses.
Cryptomator uses third party libraries and fonts that may be licensed under different licenses.
### AquaFX
The ProgressIndicator in ui/src/main/resource/css/mac_theme.css contains code from the AquaFX project.
@@ -50,6 +50,11 @@ Copyright 2012 Square, Inc.
Licensed under the Apache License, Version 2.0
### EasyBind
Copyright (c) 2014, TomasMikula
Licensed under the accompanying BSD simplified license.
### Apache Jakarta HttpClient
Copyright 1999-2007 The Apache Software Foundation
@@ -62,6 +67,11 @@ This product includes software developed at The Apache Software Foundation (http
ResolverUtil.java Copyright 2005-2006 Tim Fennell
### Ionicons
Copyright (c) 2016 Drifty (http://drifty.com/)
ionicons.ttf Licensed under the accompanying MIT license
### Jackrabbit WebDAV Library
Copyright 2004-2014 The Apache Software Foundation

View File

@@ -1,55 +1,67 @@
Cryptomator
====================
![cryptomator](cryptomator.png)
[![Build Status](https://travis-ci.org/cryptomator/cryptomator.svg?branch=master)](https://travis-ci.org/cryptomator/cryptomator)
[![Join the chat at https://gitter.im/totalvoidness/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)
[![Flattr Cryptomator](https://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=totalvoidness&url=https%3A%2F%2Fgithub.com%2Ftotalvoidness%2Fcryptomator&title=Cryptomator&language=en_GB&tags=github&category=software)
[![Coverity Scan Build Status](https://scan.coverity.com/projects/cryptomator-cryptomator/badge.svg?flat=1)](https://scan.coverity.com/projects/cryptomator-cryptomator)
[![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)
[![POEditor](https://img.shields.io/badge/POEditor-Help%20Translate-blue.svg?style=flat)](https://poeditor.com/join/project/bHwbvJmx0E)
Multiplatform transparent client-side encryption of your files in the cloud.
Multi-platform transparent client-side encryption of your files in the cloud.
If you want to take a look at the current beta version, go ahead and get your copy of cryptomator on [Cryptomator.org](https://cryptomator.org) or clone and build Cryptomator using Maven (instructions below).
Download native binaries of Cryptomator on [cryptomator.org](https://cryptomator.org/) or clone and build Cryptomator using Maven (instructions below).
## Features
- Totally transparent: Just work on the encrypted volume, as if it was an USB flash drive
- Works with Dropbox, OneDrive (Skydrive), Google Drive and any other cloud storage, that syncs with a local directory.
- In fact it works with any directory. You can use it to encrypt as many folders as you like
- AES encryption with 256 bit key length
- Client-side. No accounts, no data shared with any online service
- Filenames get encrypted too
- No need to provide credentials for any 3rd party service
- Open Source means: No backdoors. Control is better than trust
- Use as many encrypted folders in your dropbox as you want. Each having individual passwords
- No commerical interest, no government agency, no wasted taxpayers' money ;-)
- Works with Dropbox, Google Drive, OneDrive, and any other cloud storage service that 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
- AES encryption with 256-bit key length
- Filenames get encrypted, too
- Use as many vaults in your Dropbox as you want, each having individual passwords
### Privacy
- 256 bit keys (unlimited strength policy bundled with native binaries - 128 bit elsewhere)
- 256-bit keys (unlimited strength policy bundled with native binaries)
- Scrypt key derivation
- Cryptographically secure random numbers for salts, IVs and the masterkey of course
- Sensitive data is swiped from the heap asap
- Sensitive data is wiped from the heap asap
- Lightweight: [Complexity kills security](https://www.schneier.com/essays/archives/1999/11/a_plea_for_simplicit.html)
### Consistency
- HMAC over file contents to recognize changed ciphertext before decryption
- I/O operations are transactional and atomic, if the file systems supports it
- Each file contains all information needed for decryption (except for the key of course). No common metadata means no [SPOF](http://en.wikipedia.org/wiki/Single_point_of_failure)
- I/O operations are transactional and atomic, if the filesystems support it
- Each file contains all information needed for decryption (except for the key of course), no common metadata means no [SPOF](http://en.wikipedia.org/wiki/Single_point_of_failure)
### Security Architecture
For more information on the security details visit [cryptomator.org](https://cryptomator.org/architecture/).
## Building
#### Dependencies
* Java 8
* Maven 3
* Optional: OS-dependent build tools for native packaging
* Optional: JCE unlimited strength policy files (needed for 256 bit keys)
### Dependencies
* 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), [Linux](https://github.com/cryptomator/builder-containers))
### Run Maven
#### Building on Debian-based OS
```bash
apt-get install oracle-java8-installer oracle-java8-unlimited-jce-policy fakeroot maven git
git clone https://github.com/cryptomator/cryptomator.git
cd cryptomator/main
git checkout 0.7.1
mvn clean install -Pdebian
```
cd main
mvn clean install
```
## Contributing to Cryptomator
Please read our [contribution guide](https://github.com/cryptomator/cryptomator/blob/master/CONTRIBUTING.md), if you would like to report a bug, ask a question or help us with coding.
## Code of Conduct
Help us keep Cryptomator open and inclusive. Please read and follow our [Code of Conduct](https://github.com/cryptomator/cryptomator/blob/master/CODE_OF_CONDUCT.md).
## License
Distributed under the MIT X Consortium license. See the LICENSE file for more info.
Distributed under the MIT X Consortium license. See the `LICENSES/MIT-X-Consortium-License.txt` file for more info.

BIN
cryptomator.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

37
main/ant-kit/assembly.xml Normal file
View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
<id>tarball</id>
<includeBaseDirectory>false</includeBaseDirectory>
<formats>
<format>tar.gz</format>
</formats>
<fileSets>
<fileSet>
<directory>target/libs</directory>
<includes>
<include>*.jar</include>
</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>
<outputDirectory>package</outputDirectory>
</fileSet>
<fileSet>
<directory>target</directory>
<includes>
<include>build.xml</include>
</includes>
<filtered>false</filtered>
<outputDirectory>.</outputDirectory>
</fileSet>
</fileSets>
</assembly>

103
main/ant-kit/pom.xml Normal file
View File

@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2016 Sebastian Stenzel
This file is licensed under the terms of the MIT license.
-->
<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.1.0</version>
</parent>
<artifactId>ant-kit</artifactId>
<packaging>pom</packaging>
<name>Cryptomator Ant Build Kit</name>
<description>Builds a package that can be built with Ant locally</description>
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>ui</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<!-- copy libraries to target/libs/: -->
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-libs</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/libs</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<!-- copy resources to target/: -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>2.7</version>
<executions>
<execution>
<id>copy-resources</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}</outputDirectory>
<escapeString>\</escapeString>
<encoding>UTF-8</encoding>
<resources>
<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>
</execution>
</executions>
</plugin>
<!-- create antkit.tar.gz: -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<descriptors>
<descriptor>assembly.xml</descriptor>
</descriptors>
<appendAssemblyId>false</appendAssemblyId>
<finalName>antkit</finalName>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<project name="Cryptomator" default="create-jar" basedir="." xmlns:fx="javafx:com.sun.javafx.tools.ant">
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="\${java.class.path}:\${java.home}/../lib/ant-javafx.jar:." />
<!-- Define application to build -->
<fx:application id="Cryptomator" name="Cryptomator" version="${project.version}" mainClass="org.cryptomator.ui.Cryptomator" />
<!-- Create main application jar -->
<target name="create-jar">
<fx:jar destfile="antbuild/Cryptomator-${project.version}.jar">
<fx:application refid="Cryptomator" />
<fx:fileset dir="libs" includes="ui-${project.version}.jar" />
<fx:resources>
<fx:fileset dir="libs" type="jar" includes="*.jar" excludes="ui-${project.version}.jar" />
</fx:resources>
<fx:manifest>
<fx:attribute name="Implementation-Vendor" value="cryptomator.org" />
<fx:attribute name="Implementation-Title" value="Cryptomator"/>
<fx:attribute name="Implementation-Version" value="${project.version}" />
</fx:manifest>
</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">
<fx:application refid="Cryptomator" />
<fx:info title="Cryptomator" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility">
<fx:association mimetype="application/x-vnd.cryptomator-vault-metadata" extension="cryptomator" description="Cryptomator Vault Metadata" />
</fx:info>
<fx:platform j2se="8.0">
<fx:property name="cryptomator.logPath" value="~/.Cryptomator/cryptomator.log" />
<fx:jvmarg value="-Xmx1048m"/>
</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" />
</fx:deploy>
</target>
<!-- Create Red Hat package -->
<target name="rpm" depends="create-jar">
<fx:deploy nativeBundles="rpm" outdir="antbuild" outfile="Cryptomator-${project.version}" verbose="true">
<fx:application refid="Cryptomator" />
<fx:info title="Cryptomator" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility">
<fx:association mimetype="application/x-vnd.cryptomator-vault-metadata" extension="cryptomator" description="Cryptomator Vault Metadata" />
</fx:info>
<fx:platform j2se="8.0">
<fx:property name="cryptomator.logPath" value="~/.Cryptomator/cryptomator.log" />
<fx:jvmarg value="-Xmx1048m"/>
</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:permissions elevated="false" />
<fx:preferences install="true" />
</fx:deploy>
</target>
</project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

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

43
main/commons-test/pom.xml Normal file
View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2015 Markus Kreusch
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
-->
<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.1.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>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,73 @@
package org.cryptomator.common.test;
import static java.nio.file.Files.walkFileTree;
import static java.util.Collections.synchronizedSet;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashSet;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class TempFilesRemovedOnShutdown {
private static final Logger LOG = LoggerFactory.getLogger(TempFilesRemovedOnShutdown.class);
private static final Set<Path> PATHS_TO_REMOVE_ON_SHUTDOWN = synchronizedSet(new HashSet<>());
private static final Thread ON_SHUTDOWN_DELETER = new Thread(TempFilesRemovedOnShutdown::removeAll);
static {
Runtime.getRuntime().addShutdownHook(ON_SHUTDOWN_DELETER);
}
public static Path createTempDirectory(String prefix) throws IOException {
Path path = Files.createTempDirectory(prefix);
PATHS_TO_REMOVE_ON_SHUTDOWN.add(path);
return path;
}
private static void removeAll() {
PATHS_TO_REMOVE_ON_SHUTDOWN.forEach(TempFilesRemovedOnShutdown::remove);
}
private static void remove(Path path) {
try {
tryRemove(path);
} catch (Throwable e) {
LOG.debug("Failed to remove " + path, e);
}
}
private static void tryRemove(Path path) throws IOException {
walkFileTree(path, new FileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}
}

View File

@@ -0,0 +1,27 @@
package org.cryptomator.common.test.matcher;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
/**
* Wraps hamcrest contains and containsInAnyOrder matcher factory methods to
* avoid problems due to incorrect / inconsistent handling of generics by
* several java compilers.
*
* @author Markus Kreusch
*/
public class ContainsMatcher {
@SuppressWarnings({ "unchecked" })
@SafeVarargs
public static <T> Matcher<Iterable<? super T>> containsInAnyOrder(Matcher<? extends T>... matchers) {
return Matchers.containsInAnyOrder((Matcher[]) matchers);
}
@SuppressWarnings({ "unchecked" })
@SafeVarargs
public static <T> Matcher<Iterable<? super T>> contains(Matcher<? extends T>... matchers) {
return Matchers.contains((Matcher[]) matchers);
}
}

View File

@@ -0,0 +1,48 @@
package org.cryptomator.common.test.matcher;
import java.util.Optional;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;
public class ExceptionMatcher<T extends Throwable> extends TypeSafeDiagnosingMatcher<T> {
public static <T extends Throwable> ExceptionMatcher<T> ofType(Class<T> exceptionType) {
return new ExceptionMatcher<>(exceptionType);
}
private final Class<T> exceptionType;
private final Optional<Matcher<T>> subMatcher;
private ExceptionMatcher(Class<T> exceptionType) {
super(exceptionType);
this.exceptionType = exceptionType;
this.subMatcher = Optional.empty();
}
private ExceptionMatcher(Class<T> exceptionType, Matcher<T> subMatcher) {
super(exceptionType);
this.exceptionType = exceptionType;
this.subMatcher = Optional.of(subMatcher);
}
@Override
public void describeTo(Description description) {
subMatcher.ifPresent(description::appendDescriptionOf);
}
@Override
protected boolean matchesSafely(T item, Description mismatchDescription) {
if (subMatcher.map(matcher -> !matcher.matches(item)).orElse(false)) {
subMatcher.get().describeMismatch(item, mismatchDescription);
return false;
}
return true;
}
public Matcher<T> withCauseThat(Matcher<? super Throwable> matcher) {
return new ExceptionMatcher<T>(exceptionType, new PropertyMatcher<>(exceptionType, Throwable::getCause, "cause", matcher));
}
}

View File

@@ -0,0 +1,61 @@
package org.cryptomator.common.test.matcher;
import java.util.Optional;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;
public class OptionalMatcher {
public static <T> Matcher<Optional<T>> presentOptionalWithValueThat(Matcher<? super T> valueMatcher) {
return new TypeSafeDiagnosingMatcher<Optional<T>>(Optional.class) {
@Override
public void describeTo(Description description) {
description //
.appendText("a present Optional with a value that ") //
.appendDescriptionOf(valueMatcher);
}
@Override
protected boolean matchesSafely(Optional<T> item, Description mismatchDescription) {
if (item.isPresent()) {
if (valueMatcher.matches(item.get())) {
return true;
} else {
mismatchDescription.appendText("a present Optional with value that ");
valueMatcher.describeMismatch(item, mismatchDescription);
return false;
}
} else {
mismatchDescription.appendText("an empty Optional");
return false;
}
}
};
}
public static <T> Matcher<Optional<T>> emptyOptional() {
return new TypeSafeDiagnosingMatcher<Optional<T>>(Optional.class) {
@Override
public void describeTo(Description description) {
description.appendText("an empty Optional");
}
@Override
protected boolean matchesSafely(Optional<T> item, Description mismatchDescription) {
if (item.isPresent()) {
mismatchDescription.appendText("a present Optional of ").appendValue(item.get());
return false;
} else {
return true;
}
}
};
}
}

View File

@@ -0,0 +1,55 @@
/*******************************************************************************
* Copyright (c) 2015 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
******************************************************************************/
package org.cryptomator.common.test.matcher;
import java.util.function.Function;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;
public class PropertyMatcher<T, P> extends TypeSafeDiagnosingMatcher<T> {
private final Class<T> expectedType;
private final Function<? super T, P> getter;
private final String name;
private final Matcher<? super P> subMatcher;
public PropertyMatcher(Class<T> type, Function<? super T, P> getter, String name, Matcher<? super P> subMatcher) {
super(type);
this.expectedType = type;
this.getter = getter;
this.name = name;
this.subMatcher = subMatcher;
}
@Override
public void describeTo(Description description) {
description.appendText("a ") //
.appendText(expectedType.getSimpleName()) //
.appendText(" with a ") //
.appendText(name) //
.appendText(" that ") //
.appendDescriptionOf(subMatcher);
}
@Override
protected boolean matchesSafely(T item, Description mismatchDescription) {
P propertyValue = getter.apply(item);
if (subMatcher.matches(propertyValue)) {
return true;
} else {
mismatchDescription.appendText("a ") //
.appendText(expectedType.getSimpleName()) //
.appendText(" with a ") //
.appendText(name) //
.appendText(" that ");
subMatcher.describeMismatch(propertyValue, mismatchDescription);
return false;
}
}
}

View File

@@ -0,0 +1,61 @@
package org.cryptomator.common.test.mockito;
import static java.util.Arrays.asList;
import java.util.function.Consumer;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
public class Answers {
public static <T> Answer<T> collectParameters(Answer<T> answer, Consumer<?>... parameterConsumers) {
return new Answer<T>() {
@SuppressWarnings({"rawtypes", "unchecked"})
@Override
public T answer(InvocationOnMock invocation) throws Throwable {
for (int i = 0; i < invocation.getArguments().length; i++) {
if (parameterConsumers.length > i) {
((Consumer) parameterConsumers[i]).accept(invocation.getArguments()[i]);
}
}
return answer.answer(invocation);
}
};
}
@SafeVarargs
public static <T> Answer<T> consecutiveAnswers(Answer<T>... answers) {
if (answers == null || answers.length == 0) {
throw new IllegalArgumentException("Required at least one answer");
}
if (asList(answers).contains(null)) {
throw new IllegalArgumentException("No answers must be null");
}
return new Answer<T>() {
private int nextIndex = 0;
@Override
public T answer(InvocationOnMock invocation) throws Throwable {
try {
return answers[nextIndex].answer(invocation);
} finally {
nextIndex = (nextIndex + 1) % answers.length;
}
}
};
}
public static <T> Answer<T> value(T value) {
return new Answer<T>() {
@Override
public T answer(InvocationOnMock invocation) throws Throwable {
return value;
}
};
}
}

72
main/commons/pom.xml Normal file
View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2015 Markus Kreusch
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
-->
<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.1.0</version>
</parent>
<artifactId>commons</artifactId>
<name>Cryptomator common</name>
<description>Shared utilities</description>
<dependencies>
<!-- Libs -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- DI -->
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger</artifactId>
</dependency>
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger-compiler</artifactId>
<scope>provided</scope>
</dependency>
<!-- Test -->
<dependency>
<groupId>junit</groupId>
<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>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,26 @@
package org.cryptomator.common;
import java.util.function.Supplier;
public class CachingSupplier<T> implements Supplier<T> {
public static <T> Supplier<T> from(Supplier<T> delegate) {
return new CachingSupplier<>(delegate);
}
private Supplier<T> delegate;
private CachingSupplier(Supplier<T> delegate) {
this.delegate = () -> {
T result = delegate.get();
CachingSupplier.this.delegate = () -> result;
return result;
};
}
@Override
public T get() {
return delegate.get();
}
}

View File

@@ -0,0 +1,21 @@
package org.cryptomator.common;
import java.util.Comparator;
import javax.inject.Named;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module
public class CommonsModule {
@Provides
@Singleton
@Named("SemVer")
Comparator<String> providesSemVerComparator() {
return new SemVerComparator();
}
}

View File

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

View File

@@ -0,0 +1,34 @@
package org.cryptomator.common;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class Holder<V> implements Supplier<V>, Consumer<V> {
private final V initial;
private V value;
public <W extends V> Holder(W initial) {
this.initial = initial;
reset();
}
public V get() {
return value;
}
public void set(V value) {
this.value = value;
}
public void reset() {
set(initial);
}
@Override
public void accept(V value) {
set(value);
}
}

View File

@@ -0,0 +1,33 @@
package org.cryptomator.common;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
public final class LazyInitializer {
private LazyInitializer() {
}
/**
* Threadsafe lazy initialization pattern as proposed on http://stackoverflow.com/a/30247202/4014509
*
* @param <T> Type of the value
* @param reference A reference to a maybe not yet initialized value.
* @param factory A factory providing a value for the reference, if it doesn't exist yet. The factory may be invoked multiple times, but only one result will survive.
* @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;
} else {
final T newInstance = factory.get();
if (reference.compareAndSet(null, newInstance)) {
return newInstance;
} else {
return reference.get();
}
}
}
}

View File

@@ -0,0 +1,17 @@
package org.cryptomator.common;
import java.util.Optional;
public final class Optionals {
private Optionals() {
}
public static <T, E extends Exception> void ifPresent(Optional<T> optional, ConsumerThrowingException<T, E> consumer) throws E {
final T t = optional.orElse(null);
if (t != null) {
consumer.accept(t);
}
}
}

View File

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

View File

@@ -1,4 +1,12 @@
package org.cryptomator.ui.util;
/*******************************************************************************
* 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.common;
import java.util.Comparator;

View File

@@ -0,0 +1,48 @@
package org.cryptomator.common;
import java.util.concurrent.ExecutionException;
import java.util.function.BiConsumer;
import java.util.function.Function;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.ExecutionError;
import com.google.common.util.concurrent.UncheckedExecutionException;
public class WeakValuedCache<Key, Value> {
private final LoadingCache<Key, Value> delegate;
private WeakValuedCache(Function<Key, Value> loader) {
delegate = CacheBuilder.newBuilder() //
.weakValues() //
.build(new CacheLoader<Key, Value>() {
@Override
public Value load(Key key) {
return loader.apply(key);
}
});
}
public static <Key, Value> WeakValuedCache<Key, Value> usingLoader(Function<Key, Value> loader) {
return new WeakValuedCache<>(loader);
}
public Value get(Key key) {
try {
return delegate.get(key);
} catch (ExecutionException e) {
throw new IllegalStateException("No checked exception can be thrown by loader", e);
} catch (UncheckedExecutionException e) {
throw (RuntimeException) e.getCause();
} catch (ExecutionError e) {
throw (Error) e.getCause();
}
}
public void forEach(BiConsumer<Key, Value> function) {
delegate.asMap().forEach(function);
}
}

View File

@@ -0,0 +1,178 @@
package org.cryptomator.common.streams;
import static org.cryptomator.common.streams.AutoClosingStreamFactory.AUTO_CLOSING_STREAM_FACTORY;
import java.util.DoubleSummaryStatistics;
import java.util.OptionalDouble;
import java.util.function.BiConsumer;
import java.util.function.DoubleBinaryOperator;
import java.util.function.DoubleConsumer;
import java.util.function.DoublePredicate;
import java.util.function.ObjDoubleConsumer;
import java.util.function.Supplier;
import java.util.stream.DoubleStream;
public class AutoClosingDoubleStream extends DelegatingDoubleStream {
public static DoubleStream from(DoubleStream delegate) {
return new AutoClosingDoubleStream(delegate);
}
public AutoClosingDoubleStream(DoubleStream delegate) {
super(delegate, AUTO_CLOSING_STREAM_FACTORY);
}
@Override
public void forEach(DoubleConsumer action) {
try {
super.forEach(action);
} finally {
close();
}
}
@Override
public void forEachOrdered(DoubleConsumer action) {
try {
super.forEachOrdered(action);
} finally {
close();
}
}
@Override
public double[] toArray() {
try {
return super.toArray();
} finally {
close();
}
}
@Override
public double reduce(double identity, DoubleBinaryOperator op) {
try {
return super.reduce(identity, op);
} finally {
close();
}
}
@Override
public OptionalDouble reduce(DoubleBinaryOperator op) {
try {
return super.reduce(op);
} finally {
close();
}
}
@Override
public <R> R collect(Supplier<R> supplier, ObjDoubleConsumer<R> accumulator, BiConsumer<R, R> combiner) {
try {
return super.collect(supplier, accumulator, combiner);
} finally {
close();
}
}
@Override
public double sum() {
try {
return super.sum();
} finally {
close();
}
}
@Override
public OptionalDouble min() {
try {
return super.min();
} finally {
close();
}
}
@Override
public OptionalDouble max() {
try {
return super.max();
} finally {
close();
}
}
@Override
public long count() {
try {
return super.count();
} finally {
close();
}
}
@Override
public OptionalDouble average() {
try {
return super.average();
} finally {
close();
}
}
@Override
public DoubleSummaryStatistics summaryStatistics() {
try {
return super.summaryStatistics();
} finally {
close();
}
}
@Override
public boolean anyMatch(DoublePredicate predicate) {
try {
return super.anyMatch(predicate);
} finally {
close();
}
}
@Override
public boolean allMatch(DoublePredicate predicate) {
try {
return super.allMatch(predicate);
} finally {
close();
}
}
@Override
public boolean noneMatch(DoublePredicate predicate) {
try {
return super.noneMatch(predicate);
} finally {
close();
}
}
@Override
public OptionalDouble findFirst() {
try {
return super.findFirst();
} finally {
close();
}
}
@Override
public OptionalDouble findAny() {
try {
return super.findAny();
} finally {
close();
}
}
}

View File

@@ -0,0 +1,179 @@
package org.cryptomator.common.streams;
import static org.cryptomator.common.streams.AutoClosingStreamFactory.AUTO_CLOSING_STREAM_FACTORY;
import java.util.IntSummaryStatistics;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.function.BiConsumer;
import java.util.function.IntBinaryOperator;
import java.util.function.IntConsumer;
import java.util.function.IntPredicate;
import java.util.function.ObjIntConsumer;
import java.util.function.Supplier;
import java.util.stream.IntStream;
public class AutoClosingIntStream extends DelegatingIntStream {
public static IntStream from(IntStream delegate) {
return new AutoClosingIntStream(delegate);
}
public AutoClosingIntStream(IntStream delegate) {
super(delegate, AUTO_CLOSING_STREAM_FACTORY);
}
@Override
public void forEach(IntConsumer action) {
try {
super.forEach(action);
} finally {
close();
}
}
@Override
public void forEachOrdered(IntConsumer action) {
try {
super.forEachOrdered(action);
} finally {
close();
}
}
@Override
public int[] toArray() {
try {
return super.toArray();
} finally {
close();
}
}
@Override
public int reduce(int identity, IntBinaryOperator op) {
try {
return super.reduce(identity, op);
} finally {
close();
}
}
@Override
public OptionalInt reduce(IntBinaryOperator op) {
try {
return super.reduce(op);
} finally {
close();
}
}
@Override
public <R> R collect(Supplier<R> supplier, ObjIntConsumer<R> accumulator, BiConsumer<R, R> combiner) {
try {
return super.collect(supplier, accumulator, combiner);
} finally {
close();
}
}
@Override
public int sum() {
try {
return super.sum();
} finally {
close();
}
}
@Override
public OptionalInt min() {
try {
return super.min();
} finally {
close();
}
}
@Override
public OptionalInt max() {
try {
return super.max();
} finally {
close();
}
}
@Override
public long count() {
try {
return super.count();
} finally {
close();
}
}
@Override
public OptionalDouble average() {
try {
return super.average();
} finally {
close();
}
}
@Override
public IntSummaryStatistics summaryStatistics() {
try {
return super.summaryStatistics();
} finally {
close();
}
}
@Override
public boolean anyMatch(IntPredicate predicate) {
try {
return super.anyMatch(predicate);
} finally {
close();
}
}
@Override
public boolean allMatch(IntPredicate predicate) {
try {
return super.allMatch(predicate);
} finally {
close();
}
}
@Override
public boolean noneMatch(IntPredicate predicate) {
try {
return super.noneMatch(predicate);
} finally {
close();
}
}
@Override
public OptionalInt findFirst() {
try {
return super.findFirst();
} finally {
close();
}
}
@Override
public OptionalInt findAny() {
try {
return super.findAny();
} finally {
close();
}
}
}

View File

@@ -0,0 +1,179 @@
package org.cryptomator.common.streams;
import static org.cryptomator.common.streams.AutoClosingStreamFactory.AUTO_CLOSING_STREAM_FACTORY;
import java.util.LongSummaryStatistics;
import java.util.OptionalDouble;
import java.util.OptionalLong;
import java.util.function.BiConsumer;
import java.util.function.LongBinaryOperator;
import java.util.function.LongConsumer;
import java.util.function.LongPredicate;
import java.util.function.ObjLongConsumer;
import java.util.function.Supplier;
import java.util.stream.LongStream;
public class AutoClosingLongStream extends DelegatingLongStream {
public static LongStream from(LongStream delegate) {
return new AutoClosingLongStream(delegate);
}
public AutoClosingLongStream(LongStream delegate) {
super(delegate, AUTO_CLOSING_STREAM_FACTORY);
}
@Override
public void forEach(LongConsumer action) {
try {
super.forEach(action);
} finally {
close();
}
}
@Override
public void forEachOrdered(LongConsumer action) {
try {
super.forEachOrdered(action);
} finally {
close();
}
}
@Override
public long[] toArray() {
try {
return super.toArray();
} finally {
close();
}
}
@Override
public long reduce(long identity, LongBinaryOperator op) {
try {
return super.reduce(identity, op);
} finally {
close();
}
}
@Override
public OptionalLong reduce(LongBinaryOperator op) {
try {
return super.reduce(op);
} finally {
close();
}
}
@Override
public <R> R collect(Supplier<R> supplier, ObjLongConsumer<R> accumulator, BiConsumer<R, R> combiner) {
try {
return super.collect(supplier, accumulator, combiner);
} finally {
close();
}
}
@Override
public long sum() {
try {
return super.sum();
} finally {
close();
}
}
@Override
public OptionalLong min() {
try {
return super.min();
} finally {
close();
}
}
@Override
public OptionalLong max() {
try {
return super.max();
} finally {
close();
}
}
@Override
public long count() {
try {
return super.count();
} finally {
close();
}
}
@Override
public OptionalDouble average() {
try {
return super.average();
} finally {
close();
}
}
@Override
public LongSummaryStatistics summaryStatistics() {
try {
return super.summaryStatistics();
} finally {
close();
}
}
@Override
public boolean anyMatch(LongPredicate predicate) {
try {
return super.anyMatch(predicate);
} finally {
close();
}
}
@Override
public boolean allMatch(LongPredicate predicate) {
try {
return super.allMatch(predicate);
} finally {
close();
}
}
@Override
public boolean noneMatch(LongPredicate predicate) {
try {
return super.noneMatch(predicate);
} finally {
close();
}
}
@Override
public OptionalLong findFirst() {
try {
return super.findFirst();
} finally {
close();
}
}
@Override
public OptionalLong findAny() {
try {
return super.findAny();
} finally {
close();
}
}
}

View File

@@ -0,0 +1,173 @@
package org.cryptomator.common.streams;
import static org.cryptomator.common.streams.AutoClosingStreamFactory.AUTO_CLOSING_STREAM_FACTORY;
import java.util.Comparator;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
import java.util.function.Consumer;
import java.util.function.IntFunction;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Stream;
/**
* <p>
* A Stream which is automatically closed after execution of a terminal operation.
* <p>
* Streams returned by intermediate operations are also auto closing.
* <p>
* <b>Note:</b> When using {@link #iterator()} or {@link #spliterator()} auto closing does not occur.
*
* @author Markus Kreusch
*/
public class AutoClosingStream<T> extends DelegatingStream<T> {
public static <T> Stream<T> from(Stream<T> delegate) {
return new AutoClosingStream<>(delegate);
}
private AutoClosingStream(Stream<T> delegate) {
super(delegate, AUTO_CLOSING_STREAM_FACTORY);
}
public void forEach(Consumer<? super T> action) {
try {
super.forEach(action);
} finally {
close();
}
}
public void forEachOrdered(Consumer<? super T> action) {
try {
super.forEachOrdered(action);
} finally {
close();
}
}
public Object[] toArray() {
try {
return super.toArray();
} finally {
close();
}
}
public <A> A[] toArray(IntFunction<A[]> generator) {
try {
return super.toArray(generator);
} finally {
close();
}
}
public T reduce(T identity, BinaryOperator<T> accumulator) {
try {
return super.reduce(identity, accumulator);
} finally {
close();
}
}
public Optional<T> reduce(BinaryOperator<T> accumulator) {
try {
return super.reduce(accumulator);
} finally {
close();
}
}
public <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner) {
try {
return super.reduce(identity, accumulator, combiner);
} finally {
close();
}
}
public <R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner) {
try {
return super.collect(supplier, accumulator, combiner);
} finally {
close();
}
}
public <R, A> R collect(Collector<? super T, A, R> collector) {
try {
return super.collect(collector);
} finally {
close();
}
}
public Optional<T> min(Comparator<? super T> comparator) {
try {
return super.min(comparator);
} finally {
close();
}
}
public Optional<T> max(Comparator<? super T> comparator) {
try {
return super.max(comparator);
} finally {
close();
}
}
public long count() {
try {
return super.count();
} finally {
close();
}
}
public boolean anyMatch(Predicate<? super T> predicate) {
try {
return super.anyMatch(predicate);
} finally {
close();
}
}
public boolean allMatch(Predicate<? super T> predicate) {
try {
return super.allMatch(predicate);
} finally {
close();
}
}
public boolean noneMatch(Predicate<? super T> predicate) {
try {
return super.noneMatch(predicate);
} finally {
close();
}
}
public Optional<T> findFirst() {
try {
return super.findFirst();
} finally {
close();
}
}
public Optional<T> findAny() {
try {
return super.findAny();
} finally {
close();
}
}
}

View File

@@ -0,0 +1,51 @@
package org.cryptomator.common.streams;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;
class AutoClosingStreamFactory implements DelegatingStreamFactory {
public static final DelegatingStreamFactory AUTO_CLOSING_STREAM_FACTORY = new AutoClosingStreamFactory();
private AutoClosingStreamFactory() {
}
@Override
public <S> Stream<S> from(Stream<S> other) {
if (AutoClosingStream.class.isInstance(other)) {
return other;
} else {
return AutoClosingStream.from(other);
}
}
@Override
public IntStream from(IntStream other) {
if (AutoClosingIntStream.class.isInstance(other)) {
return other;
} else {
return AutoClosingIntStream.from(other);
}
}
@Override
public LongStream from(LongStream other) {
if (AutoClosingLongStream.class.isInstance(other)) {
return other;
} else {
return AutoClosingLongStream.from(other);
}
}
@Override
public DoubleStream from(DoubleStream other) {
if (AutoClosingDoubleStream.class.isInstance(other)) {
return other;
} else {
return AutoClosingDoubleStream.from(other);
}
}
}

View File

@@ -0,0 +1,179 @@
package org.cryptomator.common.streams;
import java.util.DoubleSummaryStatistics;
import java.util.OptionalDouble;
import java.util.PrimitiveIterator.OfDouble;
import java.util.function.BiConsumer;
import java.util.function.DoubleBinaryOperator;
import java.util.function.DoubleConsumer;
import java.util.function.DoubleFunction;
import java.util.function.DoublePredicate;
import java.util.function.DoubleToIntFunction;
import java.util.function.DoubleToLongFunction;
import java.util.function.DoubleUnaryOperator;
import java.util.function.ObjDoubleConsumer;
import java.util.function.Supplier;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;
abstract class DelegatingDoubleStream implements DoubleStream {
private final DoubleStream delegate;
private final DelegatingStreamFactory wrapper;
public DelegatingDoubleStream(DoubleStream delegate, DelegatingStreamFactory wrapper) {
this.delegate = delegate;
this.wrapper = wrapper;
}
public DoubleStream filter(DoublePredicate predicate) {
return wrapper.from(delegate.filter(predicate));
}
public boolean isParallel() {
return delegate.isParallel();
}
public DoubleStream map(DoubleUnaryOperator mapper) {
return wrapper.from(delegate.map(mapper));
}
public <U> Stream<U> mapToObj(DoubleFunction<? extends U> mapper) {
return wrapper.from(delegate.mapToObj(mapper));
}
public DoubleStream unordered() {
return wrapper.from(delegate.unordered());
}
public DoubleStream onClose(Runnable closeHandler) {
return wrapper.from(delegate.onClose(closeHandler));
}
public IntStream mapToInt(DoubleToIntFunction mapper) {
return wrapper.from(delegate.mapToInt(mapper));
}
public LongStream mapToLong(DoubleToLongFunction mapper) {
return wrapper.from(delegate.mapToLong(mapper));
}
public void close() {
delegate.close();
}
public DoubleStream flatMap(DoubleFunction<? extends DoubleStream> mapper) {
return wrapper.from(delegate.flatMap(mapper));
}
public DoubleStream distinct() {
return wrapper.from(delegate.distinct());
}
public DoubleStream sorted() {
return wrapper.from(delegate.sorted());
}
public DoubleStream peek(DoubleConsumer action) {
return wrapper.from(delegate.peek(action));
}
public DoubleStream limit(long maxSize) {
return wrapper.from(delegate.limit(maxSize));
}
public DoubleStream skip(long n) {
return wrapper.from(delegate.skip(n));
}
public void forEach(DoubleConsumer action) {
delegate.forEach(action);
}
public void forEachOrdered(DoubleConsumer action) {
delegate.forEachOrdered(action);
}
public double[] toArray() {
return delegate.toArray();
}
public double reduce(double identity, DoubleBinaryOperator op) {
return delegate.reduce(identity, op);
}
public OptionalDouble reduce(DoubleBinaryOperator op) {
return delegate.reduce(op);
}
public <R> R collect(Supplier<R> supplier, ObjDoubleConsumer<R> accumulator, BiConsumer<R, R> combiner) {
return delegate.collect(supplier, accumulator, combiner);
}
public double sum() {
return delegate.sum();
}
public OptionalDouble min() {
return delegate.min();
}
public OptionalDouble max() {
return delegate.max();
}
public long count() {
return delegate.count();
}
public OptionalDouble average() {
return delegate.average();
}
public DoubleSummaryStatistics summaryStatistics() {
return delegate.summaryStatistics();
}
public boolean anyMatch(DoublePredicate predicate) {
return delegate.anyMatch(predicate);
}
public boolean allMatch(DoublePredicate predicate) {
return delegate.allMatch(predicate);
}
public boolean noneMatch(DoublePredicate predicate) {
return delegate.noneMatch(predicate);
}
public OptionalDouble findFirst() {
return delegate.findFirst();
}
public OptionalDouble findAny() {
return delegate.findAny();
}
public Stream<Double> boxed() {
return wrapper.from(delegate.boxed());
}
public DoubleStream sequential() {
return wrapper.from(delegate.sequential());
}
public DoubleStream parallel() {
return wrapper.from(delegate.parallel());
}
public OfDouble iterator() {
return delegate.iterator();
}
public java.util.Spliterator.OfDouble spliterator() {
return delegate.spliterator();
}
}

View File

@@ -0,0 +1,188 @@
package org.cryptomator.common.streams;
import java.util.IntSummaryStatistics;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.PrimitiveIterator.OfInt;
import java.util.function.BiConsumer;
import java.util.function.IntBinaryOperator;
import java.util.function.IntConsumer;
import java.util.function.IntFunction;
import java.util.function.IntPredicate;
import java.util.function.IntToDoubleFunction;
import java.util.function.IntToLongFunction;
import java.util.function.IntUnaryOperator;
import java.util.function.ObjIntConsumer;
import java.util.function.Supplier;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;
abstract class DelegatingIntStream implements IntStream {
private final IntStream delegate;
private final DelegatingStreamFactory wrapper;
public DelegatingIntStream(IntStream delegate, DelegatingStreamFactory wrapper) {
this.delegate = delegate;
this.wrapper = wrapper;
}
public IntStream filter(IntPredicate predicate) {
return wrapper.from(delegate.filter(predicate));
}
public boolean isParallel() {
return delegate.isParallel();
}
public IntStream map(IntUnaryOperator mapper) {
return wrapper.from(delegate.map(mapper));
}
public <U> Stream<U> mapToObj(IntFunction<? extends U> mapper) {
return wrapper.from(delegate.mapToObj(mapper));
}
public IntStream unordered() {
return wrapper.from(delegate.unordered());
}
public LongStream mapToLong(IntToLongFunction mapper) {
return wrapper.from(delegate.mapToLong(mapper));
}
public IntStream onClose(Runnable closeHandler) {
return wrapper.from(delegate.onClose(closeHandler));
}
public DoubleStream mapToDouble(IntToDoubleFunction mapper) {
return wrapper.from(delegate.mapToDouble(mapper));
}
public void close() {
delegate.close();
}
public IntStream flatMap(IntFunction<? extends IntStream> mapper) {
return wrapper.from(delegate.flatMap(mapper));
}
public IntStream distinct() {
return wrapper.from(delegate.distinct());
}
public IntStream sorted() {
return wrapper.from(delegate.sorted());
}
public IntStream peek(IntConsumer action) {
return wrapper.from(delegate.peek(action));
}
public IntStream limit(long maxSize) {
return wrapper.from(delegate.limit(maxSize));
}
public IntStream skip(long n) {
return wrapper.from(delegate.skip(n));
}
public void forEach(IntConsumer action) {
delegate.forEach(action);
}
public void forEachOrdered(IntConsumer action) {
delegate.forEachOrdered(action);
}
public int[] toArray() {
return delegate.toArray();
}
public int reduce(int identity, IntBinaryOperator op) {
return delegate.reduce(identity, op);
}
public OptionalInt reduce(IntBinaryOperator op) {
return delegate.reduce(op);
}
public <R> R collect(Supplier<R> supplier, ObjIntConsumer<R> accumulator, BiConsumer<R, R> combiner) {
return delegate.collect(supplier, accumulator, combiner);
}
public int sum() {
return delegate.sum();
}
public OptionalInt min() {
return delegate.min();
}
public OptionalInt max() {
return delegate.max();
}
public long count() {
return delegate.count();
}
public OptionalDouble average() {
return delegate.average();
}
public IntSummaryStatistics summaryStatistics() {
return delegate.summaryStatistics();
}
public boolean anyMatch(IntPredicate predicate) {
return delegate.anyMatch(predicate);
}
public boolean allMatch(IntPredicate predicate) {
return delegate.allMatch(predicate);
}
public boolean noneMatch(IntPredicate predicate) {
return delegate.noneMatch(predicate);
}
public OptionalInt findFirst() {
return delegate.findFirst();
}
public OptionalInt findAny() {
return delegate.findAny();
}
public LongStream asLongStream() {
return wrapper.from(delegate.asLongStream());
}
public DoubleStream asDoubleStream() {
return wrapper.from(delegate.asDoubleStream());
}
public Stream<Integer> boxed() {
return wrapper.from(delegate.boxed());
}
public IntStream sequential() {
return wrapper.from(delegate.sequential());
}
public IntStream parallel() {
return wrapper.from(delegate.parallel());
}
public OfInt iterator() {
return delegate.iterator();
}
public java.util.Spliterator.OfInt spliterator() {
return delegate.spliterator();
}
}

View File

@@ -0,0 +1,184 @@
package org.cryptomator.common.streams;
import java.util.LongSummaryStatistics;
import java.util.OptionalDouble;
import java.util.OptionalLong;
import java.util.PrimitiveIterator.OfLong;
import java.util.function.BiConsumer;
import java.util.function.LongBinaryOperator;
import java.util.function.LongConsumer;
import java.util.function.LongFunction;
import java.util.function.LongPredicate;
import java.util.function.LongToDoubleFunction;
import java.util.function.LongToIntFunction;
import java.util.function.LongUnaryOperator;
import java.util.function.ObjLongConsumer;
import java.util.function.Supplier;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;
abstract class DelegatingLongStream implements LongStream {
private final LongStream delegate;
private final DelegatingStreamFactory wrapper;
public DelegatingLongStream(LongStream delegate, DelegatingStreamFactory wrapper) {
this.delegate = delegate;
this.wrapper = wrapper;
}
public LongStream filter(LongPredicate predicate) {
return wrapper.from(delegate.filter(predicate));
}
public boolean isParallel() {
return delegate.isParallel();
}
public LongStream map(LongUnaryOperator mapper) {
return wrapper.from(delegate.map(mapper));
}
public <U> Stream<U> mapToObj(LongFunction<? extends U> mapper) {
return wrapper.from(delegate.mapToObj(mapper));
}
public LongStream unordered() {
return wrapper.from(delegate.unordered());
}
public LongStream onClose(Runnable closeHandler) {
return wrapper.from(delegate.onClose(closeHandler));
}
public IntStream mapToInt(LongToIntFunction mapper) {
return wrapper.from(delegate.mapToInt(mapper));
}
public DoubleStream mapToDouble(LongToDoubleFunction mapper) {
return wrapper.from(delegate.mapToDouble(mapper));
}
public void close() {
delegate.close();
}
public LongStream flatMap(LongFunction<? extends LongStream> mapper) {
return wrapper.from(delegate.flatMap(mapper));
}
public LongStream distinct() {
return wrapper.from(delegate.distinct());
}
public LongStream sorted() {
return wrapper.from(delegate.sorted());
}
public LongStream peek(LongConsumer action) {
return wrapper.from(delegate.peek(action));
}
public LongStream limit(long maxSize) {
return wrapper.from(delegate.limit(maxSize));
}
public LongStream skip(long n) {
return wrapper.from(delegate.skip(n));
}
public void forEach(LongConsumer action) {
delegate.forEach(action);
}
public void forEachOrdered(LongConsumer action) {
delegate.forEachOrdered(action);
}
public long[] toArray() {
return delegate.toArray();
}
public long reduce(long identity, LongBinaryOperator op) {
return delegate.reduce(identity, op);
}
public OptionalLong reduce(LongBinaryOperator op) {
return delegate.reduce(op);
}
public <R> R collect(Supplier<R> supplier, ObjLongConsumer<R> accumulator, BiConsumer<R, R> combiner) {
return delegate.collect(supplier, accumulator, combiner);
}
public long sum() {
return delegate.sum();
}
public OptionalLong min() {
return delegate.min();
}
public OptionalLong max() {
return delegate.max();
}
public long count() {
return delegate.count();
}
public OptionalDouble average() {
return delegate.average();
}
public LongSummaryStatistics summaryStatistics() {
return delegate.summaryStatistics();
}
public boolean anyMatch(LongPredicate predicate) {
return delegate.anyMatch(predicate);
}
public boolean allMatch(LongPredicate predicate) {
return delegate.allMatch(predicate);
}
public boolean noneMatch(LongPredicate predicate) {
return delegate.noneMatch(predicate);
}
public OptionalLong findFirst() {
return delegate.findFirst();
}
public OptionalLong findAny() {
return delegate.findAny();
}
public DoubleStream asDoubleStream() {
return wrapper.from(delegate.asDoubleStream());
}
public Stream<Long> boxed() {
return wrapper.from(delegate.boxed());
}
public LongStream sequential() {
return wrapper.from(delegate.sequential());
}
public LongStream parallel() {
return wrapper.from(delegate.parallel());
}
public OfLong iterator() {
return delegate.iterator();
}
public java.util.Spliterator.OfLong spliterator() {
return delegate.spliterator();
}
}

View File

@@ -0,0 +1,194 @@
package org.cryptomator.common.streams;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Optional;
import java.util.Spliterator;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.function.ToDoubleFunction;
import java.util.function.ToIntFunction;
import java.util.function.ToLongFunction;
import java.util.stream.Collector;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;
abstract class DelegatingStream<T> implements Stream<T> {
private final Stream<T> delegate;
private final DelegatingStreamFactory wrapper;
protected DelegatingStream(Stream<T> delegate, DelegatingStreamFactory wrapper) {
this.delegate = delegate;
this.wrapper = wrapper;
}
public Iterator<T> iterator() {
return delegate.iterator();
}
public Spliterator<T> spliterator() {
return delegate.spliterator();
}
public boolean isParallel() {
return delegate.isParallel();
}
public Stream<T> sequential() {
return wrapper.from(delegate.sequential());
}
public Stream<T> parallel() {
return wrapper.from(delegate.parallel());
}
public Stream<T> unordered() {
return wrapper.from(delegate.unordered());
}
public Stream<T> onClose(Runnable closeHandler) {
return wrapper.from(delegate.onClose(closeHandler));
}
public void close() {
delegate.close();
}
public Stream<T> filter(Predicate<? super T> predicate) {
return wrapper.from(delegate.filter(predicate));
}
public <R> Stream<R> map(Function<? super T, ? extends R> mapper) {
return wrapper.from(delegate.map(mapper));
}
public IntStream mapToInt(ToIntFunction<? super T> mapper) {
return wrapper.from(delegate.mapToInt(mapper));
}
public LongStream mapToLong(ToLongFunction<? super T> mapper) {
return wrapper.from(delegate.mapToLong(mapper));
}
public DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper) {
return wrapper.from(delegate.mapToDouble(mapper));
}
public <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper) {
return wrapper.from(delegate.flatMap(mapper));
}
public IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper) {
return wrapper.from(delegate.flatMapToInt(mapper));
}
public LongStream flatMapToLong(Function<? super T, ? extends LongStream> mapper) {
return wrapper.from(delegate.flatMapToLong(mapper));
}
public DoubleStream flatMapToDouble(Function<? super T, ? extends DoubleStream> mapper) {
return wrapper.from(delegate.flatMapToDouble(mapper));
}
public Stream<T> distinct() {
return wrapper.from(delegate.distinct());
}
public Stream<T> sorted() {
return wrapper.from(delegate.sorted());
}
public Stream<T> sorted(Comparator<? super T> comparator) {
return wrapper.from(delegate.sorted(comparator));
}
public Stream<T> peek(Consumer<? super T> action) {
return wrapper.from(delegate.peek(action));
}
public Stream<T> limit(long maxSize) {
return wrapper.from(delegate.limit(maxSize));
}
public Stream<T> skip(long n) {
return wrapper.from(delegate.skip(n));
}
public void forEach(Consumer<? super T> action) {
delegate.forEach(action);
}
public void forEachOrdered(Consumer<? super T> action) {
delegate.forEachOrdered(action);
}
public Object[] toArray() {
return delegate.toArray();
}
public <A> A[] toArray(IntFunction<A[]> generator) {
return delegate.toArray(generator);
}
public T reduce(T identity, BinaryOperator<T> accumulator) {
return delegate.reduce(identity, accumulator);
}
public Optional<T> reduce(BinaryOperator<T> accumulator) {
return delegate.reduce(accumulator);
}
public <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner) {
return delegate.reduce(identity, accumulator, combiner);
}
public <R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner) {
return delegate.collect(supplier, accumulator, combiner);
}
public <R, A> R collect(Collector<? super T, A, R> collector) {
return delegate.collect(collector);
}
public Optional<T> min(Comparator<? super T> comparator) {
return delegate.min(comparator);
}
public Optional<T> max(Comparator<? super T> comparator) {
return delegate.max(comparator);
}
public long count() {
return delegate.count();
}
public boolean anyMatch(Predicate<? super T> predicate) {
return delegate.anyMatch(predicate);
}
public boolean allMatch(Predicate<? super T> predicate) {
return delegate.allMatch(predicate);
}
public boolean noneMatch(Predicate<? super T> predicate) {
return delegate.noneMatch(predicate);
}
public Optional<T> findFirst() {
return delegate.findFirst();
}
public Optional<T> findAny() {
return delegate.findAny();
}
}

View File

@@ -0,0 +1,22 @@
package org.cryptomator.common.streams;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;
public interface DelegatingStreamFactory {
<S> Stream<S> from(Stream<S> other);
IntStream from(IntStream other);
LongStream from(LongStream other);
DoubleStream from(DoubleStream other);
public interface ObjectStreamWrapper {
<S> Stream<S> from(Stream<S> other);
}
}

View File

@@ -0,0 +1,43 @@
package org.cryptomator.common;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.function.Supplier;
import org.junit.Test;
public class CachingSupplierTest {
@Test
public void testInvokingGetInvokesDelegate() {
@SuppressWarnings("unchecked")
Supplier<Object> delegate = mock(Supplier.class);
Object expectedResult = new Object();
when(delegate.get()).thenReturn(expectedResult);
Supplier<Object> inTest = CachingSupplier.from(delegate);
Object result = inTest.get();
assertThat(result, is(expectedResult));
}
@Test
public void testInvokingGetTwiceDoesNotInvokeDelegateTwice() {
@SuppressWarnings("unchecked")
Supplier<Object> delegate = mock(Supplier.class);
Object expectedResult = new Object();
when(delegate.get()).thenReturn(expectedResult);
Supplier<Object> inTest = CachingSupplier.from(delegate);
inTest.get();
Object result = inTest.get();
assertThat(result, is(expectedResult));
verify(delegate).get();
}
}

View File

@@ -0,0 +1,42 @@
package org.cryptomator.common;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import org.junit.Test;
public class HolderTest {
private static final Object INITIAL = new Object();
private static final Object VALUE = new Object();
private Holder<Object> inTest = new Holder<>(INITIAL);
@Test
public void testInitialValueIsInitial() {
assertThat(inTest.get(), is(INITIAL));
}
@Test
public void testSetChangesValue() {
inTest.set(VALUE);
assertThat(inTest.get(), is(VALUE));
}
@Test
public void testAcceptChangesValue() {
inTest.accept(VALUE);
assertThat(inTest.get(), is(VALUE));
}
@Test
public void testResetChangesValueToInitial() {
inTest.set(VALUE);
inTest.reset();
assertThat(inTest.get(), is(INITIAL));
}
}

View File

@@ -1,7 +1,16 @@
package org.cryptomator.ui.util;
/*******************************************************************************
* 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.common;
import java.util.Comparator;
import org.cryptomator.common.SemVerComparator;
import org.junit.Assert;
import org.junit.Test;

View File

@@ -0,0 +1,155 @@
package org.cryptomator.common;
import static java.lang.String.format;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.function.Function;
import org.cryptomator.common.WeakValuedCache;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
public class WeakValuedCacheTest {
private final String A_KEY = "aKey";
private final String ANOTHER_KEY = "anotherKey";
private WeakValuedCache<String, Value> inTest;
private Function<String, Value> loader;
@SuppressWarnings("unchecked")
@Before
public void setup() {
loader = Mockito.mock(Function.class);
inTest = WeakValuedCache.usingLoader(loader);
}
@Test
public void testResultOfGetIsResultOfLoaderForTheSameKey() {
Value theValue = new Value();
Value theOtherValue = new Value();
when(loader.apply(A_KEY)).thenReturn(theValue);
when(loader.apply(ANOTHER_KEY)).thenReturn(theOtherValue);
Value result = inTest.get(A_KEY);
Value anotherResult = inTest.get(ANOTHER_KEY);
assertThat(result, is(sameInstance(theValue)));
assertThat(anotherResult, is(sameInstance(theOtherValue)));
}
@Test
public void testCachedResultIsResultOfLoaderForTheSameKey() {
Value theValue = new Value();
Value theOtherValue = new Value();
when(loader.apply(A_KEY)).thenReturn(theValue);
when(loader.apply(ANOTHER_KEY)).thenReturn(theOtherValue);
inTest.get(A_KEY);
inTest.get(ANOTHER_KEY);
Value result = inTest.get(A_KEY);
Value anotherResult = inTest.get(ANOTHER_KEY);
assertThat(result, is(sameInstance(theValue)));
assertThat(anotherResult, is(sameInstance(theOtherValue)));
}
@Test
public void testTwiceInvocationOfGetDoesNotInvokeLoaderTwice() {
Value theValue = new Value();
when(loader.apply(A_KEY)).thenReturn(theValue);
inTest.get(A_KEY);
inTest.get(A_KEY);
verify(loader).apply(A_KEY);
}
@Test
public void testSecondInvocationOfGetReturnsTheSameResult() {
Value theValue = new Value();
when(loader.apply(A_KEY)).thenReturn(theValue);
inTest.get(A_KEY);
Value result = inTest.get(A_KEY);
assertThat(result, is(sameInstance(theValue)));
}
@Test
public void testCacheDoesNotPreventGarbageCollectionOfValues() {
when(loader.apply(A_KEY)).thenAnswer(this::createValueUsingMoreThanHalfTheJvmMemory);
inTest.get(A_KEY);
// force garbage collection of previously created value by creating an
// object so large it can not coexist with the value
createObjectUsingMoreThanHalfTheJvmMemory();
}
@Test(expected = RuntimeExceptionThrownInLoader.class)
public void testCacheRethrowsRuntimeExceptionsFromLoader() {
when(loader.apply(A_KEY)).thenThrow(new RuntimeExceptionThrownInLoader());
inTest.get(A_KEY);
}
@Test(expected = ErrorThrownInLoader.class)
public void testCacheRethrowsErrorsFromLoader() {
when(loader.apply(A_KEY)).thenThrow(new ErrorThrownInLoader());
inTest.get(A_KEY);
}
private Value createValueUsingMoreThanHalfTheJvmMemory(InvocationOnMock invocation) {
Object data = createObjectUsingMoreThanHalfTheJvmMemory();
Value value = new Value();
value.setPayload(data);
return value;
}
private Object createObjectUsingMoreThanHalfTheJvmMemory() {
long maxMemory = Runtime.getRuntime().maxMemory();
long moreThanHalfTheJvmMemory = maxMemory / 2 + 1;
return createObjectUsingAtLeast(moreThanHalfTheJvmMemory);
}
private Object createObjectUsingAtLeast(long minMemory) {
if (minMemory <= Integer.MAX_VALUE) {
return new byte[(int) minMemory];
} else if ((minMemory / Integer.MAX_VALUE) <= Integer.MAX_VALUE) {
int numberOfArraysWithMaxIntSize = (int) (minMemory / Integer.MAX_VALUE);
int numberOfRemainingBytes = (int) (minMemory - Integer.MAX_VALUE * numberOfArraysWithMaxIntSize);
return new byte[][][] { //
new byte[numberOfArraysWithMaxIntSize][Integer.MAX_VALUE], //
new byte[1][numberOfRemainingBytes] //
};
} else {
throw new IllegalArgumentException(format("Can not create object with more than 3.999999996 Exabyte"));
}
}
private static class RuntimeExceptionThrownInLoader extends RuntimeException {
}
private static class ErrorThrownInLoader extends Error {
}
private static class Value {
@SuppressWarnings("unused")
private Object payload;
public void setPayload(Object payload) {
this.payload = payload;
}
}
}

View File

@@ -0,0 +1,227 @@
package org.cryptomator.common.streams;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.DoubleSummaryStatistics;
import java.util.List;
import java.util.OptionalDouble;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.DoubleBinaryOperator;
import java.util.function.DoubleConsumer;
import java.util.function.DoubleFunction;
import java.util.function.DoublePredicate;
import java.util.function.DoubleToIntFunction;
import java.util.function.DoubleToLongFunction;
import java.util.function.DoubleUnaryOperator;
import java.util.function.Function;
import java.util.function.ObjDoubleConsumer;
import java.util.function.Supplier;
import java.util.stream.BaseStream;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;
import org.hamcrest.Matcher;
import org.junit.Before;
import org.junit.Rule;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.FromDataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
@SuppressWarnings({"unchecked", "rawtypes"})
@RunWith(Theories.class)
public class AutoClosingDoubleStreamTest {
private static final DoublePredicate A_DOUBLE_PREDICATE = any -> true;
private static final DoubleFunction A_DOUBLE_FUNCTION = i -> null;
private static final BiConsumer A_BICONSUMER = (a, b) -> {
};
private static final Supplier A_SUPPLIER = () -> null;
@DataPoints("intermediateOperations")
public static final List<DoubleermediateOperation<?>> INTERMEDIATE_OPERATIONS = new ArrayList<>();
@DataPoints("terminalOperations")
public static final List<TerminalOperation<?>> TERMINAL_OPERATIONS = new ArrayList<>();
private static final DoubleUnaryOperator A_DOUBLE_UNARY_OPERATOR = i -> 3;
private static final DoubleToLongFunction A_DOUBLE_TO_LONG_FUNCTION = i -> 3L;
private static final DoubleToIntFunction A_DOUBLE_TO_INT_FUNCTION = i -> 5;
private static final DoubleConsumer A_DOUBLE_CONSUMER = i -> {
};
private static final ObjDoubleConsumer AN_OBJ_DOUBLE_CONSUMER = (a, b) -> {
};
private static final DoubleBinaryOperator A_DOUBLE_BINARY_OPERATOR = (a, b) -> a;
static {
// define intermediate operations
test(DoubleStream.class, DoubleStream::distinct);
test(DoubleStream.class, stream -> stream.filter(A_DOUBLE_PREDICATE));
test(DoubleStream.class, stream -> stream.flatMap(A_DOUBLE_FUNCTION));
test(DoubleStream.class, stream -> stream.limit(5));
test(DoubleStream.class, stream -> stream.map(A_DOUBLE_UNARY_OPERATOR));
test(LongStream.class, stream -> stream.mapToLong(A_DOUBLE_TO_LONG_FUNCTION));
test(Stream.class, stream -> stream.mapToObj(A_DOUBLE_FUNCTION));
test(IntStream.class, stream -> stream.mapToInt(A_DOUBLE_TO_INT_FUNCTION));
test(DoubleStream.class, DoubleStream::parallel);
test(DoubleStream.class, stream -> stream.peek(A_DOUBLE_CONSUMER));
test(DoubleStream.class, DoubleStream::sequential);
test(DoubleStream.class, stream -> stream.skip(5));
test(DoubleStream.class, DoubleStream::sorted);
test(DoubleStream.class, DoubleStream::unordered);
test(Stream.class, DoubleStream::boxed);
// define terminal operations
test(stream -> stream.allMatch(A_DOUBLE_PREDICATE), true);
test(stream -> stream.anyMatch(A_DOUBLE_PREDICATE), true);
test(stream -> stream.collect(A_SUPPLIER, AN_OBJ_DOUBLE_CONSUMER, A_BICONSUMER), 7d);
test(DoubleStream::count, 3L);
test(DoubleStream::findAny, OptionalDouble.of(3));
test(DoubleStream::findFirst, OptionalDouble.of(3));
test(stream -> stream.forEach(A_DOUBLE_CONSUMER));
test(stream -> stream.forEachOrdered(A_DOUBLE_CONSUMER));
test(stream -> stream.max(), OptionalDouble.of(3));
test(stream -> stream.min(), OptionalDouble.of(3));
test(stream -> stream.noneMatch(A_DOUBLE_PREDICATE), true);
test(stream -> stream.reduce(A_DOUBLE_BINARY_OPERATOR), OptionalDouble.of(3));
test(stream -> stream.reduce(1, A_DOUBLE_BINARY_OPERATOR), 3d);
test(DoubleStream::toArray, new double[1]);
test(DoubleStream::sum, 1d);
test(DoubleStream::average, OptionalDouble.of(3d));
test(DoubleStream::summaryStatistics, new DoubleSummaryStatistics());
}
private static <T> void test(Consumer<DoubleStream> consumer) {
TERMINAL_OPERATIONS.add(new TerminalOperation<T>() {
@Override
public T result() {
return null;
}
@Override
public T apply(DoubleStream stream) {
consumer.accept(stream);
return null;
}
});
}
private static <T> void test(Function<DoubleStream, T> function, T result) {
TERMINAL_OPERATIONS.add(new TerminalOperation<T>() {
@Override
public T result() {
return result;
}
@Override
public T apply(DoubleStream stream) {
return function.apply(stream);
}
});
}
private static <T extends BaseStream> void test(Class<? extends T> type, Function<DoubleStream, T> function) {
INTERMEDIATE_OPERATIONS.add(new DoubleermediateOperation<T>() {
@Override
public Class<? extends T> type() {
return type;
}
@Override
public T apply(DoubleStream stream) {
return function.apply(stream);
}
});
}
@Rule
public ExpectedException thrown = ExpectedException.none();
private DoubleStream delegate;
private DoubleStream inTest;
@Before
public void setUp() {
delegate = mock(DoubleStream.class);
inTest = AutoClosingDoubleStream.from(delegate);
}
@Theory
public void testIntermediateOperationReturnsNewAutoClosingStream(@FromDataPoints("intermediateOperations") DoubleermediateOperation intermediateOperation) {
BaseStream newDelegate = (BaseStream) mock(intermediateOperation.type());
when(intermediateOperation.apply(delegate)).thenReturn(newDelegate);
BaseStream result = intermediateOperation.apply(inTest);
assertThat(result, isAutoClosing());
verifyDelegate(result, newDelegate);
}
@Theory
public void testTerminalOperationDelegatesToAndClosesDelegate(@FromDataPoints("terminalOperations") TerminalOperation terminalOperation) {
Object expectedResult = terminalOperation.result();
if (expectedResult != null) {
when(terminalOperation.apply(delegate)).thenReturn(expectedResult);
}
Object result = terminalOperation.apply(inTest);
InOrder inOrder = inOrder(delegate);
assertThat(result, is(expectedResult));
inOrder.verify(delegate).close();
}
@Theory
public void testTerminalOperationClosesDelegateEvenOnException(@FromDataPoints("terminalOperations") TerminalOperation terminalOperation) {
RuntimeException exception = new RuntimeException();
terminalOperation.apply(doThrow(exception).when(delegate));
thrown.expect(is(exception));
try {
terminalOperation.apply(inTest);
} finally {
verify(delegate).close();
}
}
private Matcher<BaseStream> isAutoClosing() {
return is(anyOf(instanceOf(AutoClosingStream.class), instanceOf(AutoClosingDoubleStream.class), instanceOf(AutoClosingIntStream.class), instanceOf(AutoClosingLongStream.class)));
}
private void verifyDelegate(BaseStream result, BaseStream newDelegate) {
result.close();
verify(newDelegate).close();
}
private interface TerminalOperation<T> {
T result();
T apply(DoubleStream stream);
}
private interface DoubleermediateOperation<T extends BaseStream> {
Class<? extends T> type();
T apply(DoubleStream stream);
}
}

View File

@@ -0,0 +1,228 @@
package org.cryptomator.common.streams;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.IntSummaryStatistics;
import java.util.List;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntBinaryOperator;
import java.util.function.IntConsumer;
import java.util.function.IntFunction;
import java.util.function.IntPredicate;
import java.util.function.IntToDoubleFunction;
import java.util.function.IntToLongFunction;
import java.util.function.IntUnaryOperator;
import java.util.function.ObjIntConsumer;
import java.util.function.Supplier;
import java.util.stream.BaseStream;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;
import org.hamcrest.Matcher;
import org.junit.Before;
import org.junit.Rule;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.FromDataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
@SuppressWarnings({"unchecked", "rawtypes"})
@RunWith(Theories.class)
public class AutoClosingIntStreamTest {
private static final IntPredicate AN_INT_PREDICATE = any -> true;
private static final IntFunction AN_INT_FUNCTION = i -> null;
private static final BiConsumer A_BICONSUMER = (a, b) -> {
};
private static final Supplier A_SUPPLIER = () -> null;
@DataPoints("intermediateOperations")
public static final List<IntermediateOperation<?>> INTERMEDIATE_OPERATIONS = new ArrayList<>();
@DataPoints("terminalOperations")
public static final List<TerminalOperation<?>> TERMINAL_OPERATIONS = new ArrayList<>();
private static final IntUnaryOperator AN_INT_UNARY_OPERATOR = i -> 3;
private static final IntToDoubleFunction AN_INT_TO_DOUBLE_FUNCTION = i -> 3d;
private static final IntToLongFunction AN_INT_TO_LONG_FUNCTION = i -> 5L;
private static final IntConsumer AN_INT_CONSUMER = i -> {
};
private static final ObjIntConsumer AN_OBJ_INT_CONSUMER = (a, b) -> {
};
private static final IntBinaryOperator AN_INT_BINARY_OPERATOR = (a, b) -> a;
static {
// define intermediate operations
test(IntStream.class, IntStream::distinct);
test(IntStream.class, stream -> stream.filter(AN_INT_PREDICATE));
test(IntStream.class, stream -> stream.flatMap(AN_INT_FUNCTION));
test(IntStream.class, stream -> stream.limit(5));
test(IntStream.class, stream -> stream.map(AN_INT_UNARY_OPERATOR));
test(DoubleStream.class, stream -> stream.mapToDouble(AN_INT_TO_DOUBLE_FUNCTION));
test(Stream.class, stream -> stream.mapToObj(AN_INT_FUNCTION));
test(LongStream.class, stream -> stream.mapToLong(AN_INT_TO_LONG_FUNCTION));
test(IntStream.class, IntStream::parallel);
test(IntStream.class, stream -> stream.peek(AN_INT_CONSUMER));
test(IntStream.class, IntStream::sequential);
test(IntStream.class, stream -> stream.skip(5));
test(IntStream.class, IntStream::sorted);
test(IntStream.class, IntStream::unordered);
test(Stream.class, IntStream::boxed);
// define terminal operations
test(stream -> stream.allMatch(AN_INT_PREDICATE), true);
test(stream -> stream.anyMatch(AN_INT_PREDICATE), true);
test(stream -> stream.collect(A_SUPPLIER, AN_OBJ_INT_CONSUMER, A_BICONSUMER), 7);
test(IntStream::count, 3L);
test(IntStream::findAny, OptionalInt.of(3));
test(IntStream::findFirst, OptionalInt.of(3));
test(stream -> stream.forEach(AN_INT_CONSUMER));
test(stream -> stream.forEachOrdered(AN_INT_CONSUMER));
test(stream -> stream.max(), OptionalInt.of(3));
test(stream -> stream.min(), OptionalInt.of(3));
test(stream -> stream.noneMatch(AN_INT_PREDICATE), true);
test(stream -> stream.reduce(AN_INT_BINARY_OPERATOR), OptionalInt.of(3));
test(stream -> stream.reduce(1, AN_INT_BINARY_OPERATOR), 3);
test(IntStream::toArray, new int[1]);
test(IntStream::sum, 1);
test(IntStream::average, OptionalDouble.of(3d));
test(IntStream::summaryStatistics, new IntSummaryStatistics());
}
private static <T> void test(Consumer<IntStream> consumer) {
TERMINAL_OPERATIONS.add(new TerminalOperation<T>() {
@Override
public T result() {
return null;
}
@Override
public T apply(IntStream stream) {
consumer.accept(stream);
return null;
}
});
}
private static <T> void test(Function<IntStream, T> function, T result) {
TERMINAL_OPERATIONS.add(new TerminalOperation<T>() {
@Override
public T result() {
return result;
}
@Override
public T apply(IntStream stream) {
return function.apply(stream);
}
});
}
private static <T extends BaseStream> void test(Class<? extends T> type, Function<IntStream, T> function) {
INTERMEDIATE_OPERATIONS.add(new IntermediateOperation<T>() {
@Override
public Class<? extends T> type() {
return type;
}
@Override
public T apply(IntStream stream) {
return function.apply(stream);
}
});
}
@Rule
public ExpectedException thrown = ExpectedException.none();
private IntStream delegate;
private IntStream inTest;
@Before
public void setUp() {
delegate = mock(IntStream.class);
inTest = AutoClosingIntStream.from(delegate);
}
@Theory
public void testIntermediateOperationReturnsNewAutoClosingStream(@FromDataPoints("intermediateOperations") IntermediateOperation intermediateOperation) {
BaseStream newDelegate = (BaseStream) mock(intermediateOperation.type());
when(intermediateOperation.apply(delegate)).thenReturn(newDelegate);
BaseStream result = intermediateOperation.apply(inTest);
assertThat(result, isAutoClosing());
verifyDelegate(result, newDelegate);
}
@Theory
public void testTerminalOperationDelegatesToAndClosesDelegate(@FromDataPoints("terminalOperations") TerminalOperation terminalOperation) {
Object expectedResult = terminalOperation.result();
if (expectedResult != null) {
when(terminalOperation.apply(delegate)).thenReturn(expectedResult);
}
Object result = terminalOperation.apply(inTest);
InOrder inOrder = inOrder(delegate);
assertThat(result, is(expectedResult));
inOrder.verify(delegate).close();
}
@Theory
public void testTerminalOperationClosesDelegateEvenOnException(@FromDataPoints("terminalOperations") TerminalOperation terminalOperation) {
RuntimeException exception = new RuntimeException();
terminalOperation.apply(doThrow(exception).when(delegate));
thrown.expect(is(exception));
try {
terminalOperation.apply(inTest);
} finally {
verify(delegate).close();
}
}
private Matcher<BaseStream> isAutoClosing() {
return is(anyOf(instanceOf(AutoClosingStream.class), instanceOf(AutoClosingDoubleStream.class), instanceOf(AutoClosingIntStream.class), instanceOf(AutoClosingLongStream.class)));
}
private void verifyDelegate(BaseStream result, BaseStream newDelegate) {
result.close();
verify(newDelegate).close();
}
private interface TerminalOperation<T> {
T result();
T apply(IntStream stream);
}
private interface IntermediateOperation<T extends BaseStream> {
Class<? extends T> type();
T apply(IntStream stream);
}
}

View File

@@ -0,0 +1,228 @@
package org.cryptomator.common.streams;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.List;
import java.util.LongSummaryStatistics;
import java.util.OptionalDouble;
import java.util.OptionalLong;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.LongBinaryOperator;
import java.util.function.LongConsumer;
import java.util.function.LongFunction;
import java.util.function.LongPredicate;
import java.util.function.LongToDoubleFunction;
import java.util.function.LongToIntFunction;
import java.util.function.LongUnaryOperator;
import java.util.function.ObjLongConsumer;
import java.util.function.Supplier;
import java.util.stream.BaseStream;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;
import org.hamcrest.Matcher;
import org.junit.Before;
import org.junit.Rule;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.FromDataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
@SuppressWarnings({"unchecked", "rawtypes"})
@RunWith(Theories.class)
public class AutoClosingLongStreamTest {
private static final LongPredicate AN_LONG_PREDICATE = any -> true;
private static final LongFunction AN_LONG_FUNCTION = i -> null;
private static final BiConsumer A_BICONSUMER = (a, b) -> {
};
private static final Supplier A_SUPPLIER = () -> null;
@DataPoints("intermediateOperations")
public static final List<LongermediateOperation<?>> INTERMEDIATE_OPERATIONS = new ArrayList<>();
@DataPoints("terminalOperations")
public static final List<TerminalOperation<?>> TERMINAL_OPERATIONS = new ArrayList<>();
private static final LongUnaryOperator AN_LONG_UNARY_OPERATOR = i -> 3;
private static final LongToDoubleFunction AN_LONG_TO_DOUBLE_FUNCTION = i -> 3d;
private static final LongToIntFunction AN_LONG_TO_INT_FUNCTION = i -> 5;
private static final LongConsumer AN_LONG_CONSUMER = i -> {
};
private static final ObjLongConsumer AN_OBJ_LONG_CONSUMER = (a, b) -> {
};
private static final LongBinaryOperator AN_LONG_BINARY_OPERATOR = (a, b) -> a;
static {
// define intermediate operations
test(LongStream.class, LongStream::distinct);
test(LongStream.class, stream -> stream.filter(AN_LONG_PREDICATE));
test(LongStream.class, stream -> stream.flatMap(AN_LONG_FUNCTION));
test(LongStream.class, stream -> stream.limit(5));
test(LongStream.class, stream -> stream.map(AN_LONG_UNARY_OPERATOR));
test(DoubleStream.class, stream -> stream.mapToDouble(AN_LONG_TO_DOUBLE_FUNCTION));
test(Stream.class, stream -> stream.mapToObj(AN_LONG_FUNCTION));
test(IntStream.class, stream -> stream.mapToInt(AN_LONG_TO_INT_FUNCTION));
test(LongStream.class, LongStream::parallel);
test(LongStream.class, stream -> stream.peek(AN_LONG_CONSUMER));
test(LongStream.class, LongStream::sequential);
test(LongStream.class, stream -> stream.skip(5));
test(LongStream.class, LongStream::sorted);
test(LongStream.class, LongStream::unordered);
test(Stream.class, LongStream::boxed);
// define terminal operations
test(stream -> stream.allMatch(AN_LONG_PREDICATE), true);
test(stream -> stream.anyMatch(AN_LONG_PREDICATE), true);
test(stream -> stream.collect(A_SUPPLIER, AN_OBJ_LONG_CONSUMER, A_BICONSUMER), 7L);
test(LongStream::count, 3L);
test(LongStream::findAny, OptionalLong.of(3));
test(LongStream::findFirst, OptionalLong.of(3));
test(stream -> stream.forEach(AN_LONG_CONSUMER));
test(stream -> stream.forEachOrdered(AN_LONG_CONSUMER));
test(stream -> stream.max(), OptionalLong.of(3));
test(stream -> stream.min(), OptionalLong.of(3));
test(stream -> stream.noneMatch(AN_LONG_PREDICATE), true);
test(stream -> stream.reduce(AN_LONG_BINARY_OPERATOR), OptionalLong.of(3));
test(stream -> stream.reduce(1, AN_LONG_BINARY_OPERATOR), 3L);
test(LongStream::toArray, new long[1]);
test(LongStream::sum, 1L);
test(LongStream::average, OptionalDouble.of(3d));
test(LongStream::summaryStatistics, new LongSummaryStatistics());
}
private static <T> void test(Consumer<LongStream> consumer) {
TERMINAL_OPERATIONS.add(new TerminalOperation<T>() {
@Override
public T result() {
return null;
}
@Override
public T apply(LongStream stream) {
consumer.accept(stream);
return null;
}
});
}
private static <T> void test(Function<LongStream, T> function, T result) {
TERMINAL_OPERATIONS.add(new TerminalOperation<T>() {
@Override
public T result() {
return result;
}
@Override
public T apply(LongStream stream) {
return function.apply(stream);
}
});
}
private static <T extends BaseStream> void test(Class<? extends T> type, Function<LongStream, T> function) {
INTERMEDIATE_OPERATIONS.add(new LongermediateOperation<T>() {
@Override
public Class<? extends T> type() {
return type;
}
@Override
public T apply(LongStream stream) {
return function.apply(stream);
}
});
}
@Rule
public ExpectedException thrown = ExpectedException.none();
private LongStream delegate;
private LongStream inTest;
@Before
public void setUp() {
delegate = mock(LongStream.class);
inTest = AutoClosingLongStream.from(delegate);
}
@Theory
public void testIntermediateOperationReturnsNewAutoClosingStream(@FromDataPoints("intermediateOperations") LongermediateOperation intermediateOperation) {
BaseStream newDelegate = (BaseStream) mock(intermediateOperation.type());
when(intermediateOperation.apply(delegate)).thenReturn(newDelegate);
BaseStream result = intermediateOperation.apply(inTest);
assertThat(result, isAutoClosing());
verifyDelegate(result, newDelegate);
}
@Theory
public void testTerminalOperationDelegatesToAndClosesDelegate(@FromDataPoints("terminalOperations") TerminalOperation terminalOperation) {
Object expectedResult = terminalOperation.result();
if (expectedResult != null) {
when(terminalOperation.apply(delegate)).thenReturn(expectedResult);
}
Object result = terminalOperation.apply(inTest);
InOrder inOrder = inOrder(delegate);
assertThat(result, is(expectedResult));
inOrder.verify(delegate).close();
}
@Theory
public void testTerminalOperationClosesDelegateEvenOnException(@FromDataPoints("terminalOperations") TerminalOperation terminalOperation) {
RuntimeException exception = new RuntimeException();
terminalOperation.apply(doThrow(exception).when(delegate));
thrown.expect(is(exception));
try {
terminalOperation.apply(inTest);
} finally {
verify(delegate).close();
}
}
private Matcher<BaseStream> isAutoClosing() {
return is(anyOf(instanceOf(AutoClosingStream.class), instanceOf(AutoClosingDoubleStream.class), instanceOf(AutoClosingIntStream.class), instanceOf(AutoClosingLongStream.class)));
}
private void verifyDelegate(BaseStream result, BaseStream newDelegate) {
result.close();
verify(newDelegate).close();
}
private interface TerminalOperation<T> {
T result();
T apply(LongStream stream);
}
private interface LongermediateOperation<T extends BaseStream> {
Class<? extends T> type();
T apply(LongStream stream);
}
}

View File

@@ -0,0 +1,231 @@
package org.cryptomator.common.streams;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.function.ToDoubleFunction;
import java.util.function.ToIntFunction;
import java.util.function.ToLongFunction;
import java.util.stream.BaseStream;
import java.util.stream.Collector;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;
import org.hamcrest.Matcher;
import org.junit.Before;
import org.junit.Rule;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.FromDataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
@SuppressWarnings({"unchecked", "rawtypes"})
@RunWith(Theories.class)
public class AutoClosingStreamTest {
private static final Predicate A_PREDICATE = any -> true;
private static final Function A_FUNCTION = any -> null;
private static final ToDoubleFunction A_TO_DOUBLE_FUNCTION = any -> 0d;
private static final ToIntFunction A_TO_INT_FUNCTION = any -> 1;
private static final ToLongFunction A_TO_LONG_FUNCTION = any -> 1L;
private static final Consumer A_CONSUMER = any -> {
};
private static final Comparator A_COMPARATOR = (left, right) -> 0;
private static final Collector A_COLLECTOR = mock(Collector.class);
private static final BinaryOperator A_BINARY_OPERATOR = (left, right) -> null;
private static final Object AN_OBJECT = new Object();
private static final IntFunction AN_INT_FUNCTION = i -> null;
private static final BiConsumer A_BICONSUMER = (a, b) -> {
};
private static final Supplier A_SUPPLIER = () -> null;
@DataPoints("intermediateOperations")
public static final List<IntermediateOperation<?>> INTERMEDIATE_OPERATIONS = new ArrayList<>();
@DataPoints("terminalOperations")
public static final List<TerminalOperation<?>> TERMINAL_OPERATIONS = new ArrayList<>();
static {
// define intermediate operations
test(Stream.class, Stream::distinct);
test(Stream.class, stream -> stream.filter(A_PREDICATE));
test(Stream.class, stream -> stream.flatMap(A_FUNCTION));
test(DoubleStream.class, stream -> stream.flatMapToDouble(A_FUNCTION));
test(IntStream.class, stream -> stream.flatMapToInt(A_FUNCTION));
test(LongStream.class, stream -> stream.flatMapToLong(A_FUNCTION));
test(Stream.class, stream -> stream.limit(5));
test(Stream.class, stream -> stream.map(A_FUNCTION));
test(DoubleStream.class, stream -> stream.mapToDouble(A_TO_DOUBLE_FUNCTION));
test(IntStream.class, stream -> stream.mapToInt(A_TO_INT_FUNCTION));
test(LongStream.class, stream -> stream.mapToLong(A_TO_LONG_FUNCTION));
test(Stream.class, Stream::parallel);
test(Stream.class, stream -> stream.peek(A_CONSUMER));
test(Stream.class, Stream::sequential);
test(Stream.class, stream -> stream.skip(5));
test(Stream.class, Stream::sorted);
test(Stream.class, stream -> stream.sorted(A_COMPARATOR));
test(Stream.class, Stream::unordered);
// define terminal operations
test(stream -> stream.allMatch(A_PREDICATE), true);
test(stream -> stream.anyMatch(A_PREDICATE), true);
test(stream -> stream.collect(A_COLLECTOR), new Object());
test(stream -> stream.collect(A_SUPPLIER, A_BICONSUMER, A_BICONSUMER), new Object());
test(Stream::count, 3L);
test(Stream::findAny, Optional.of(new Object()));
test(Stream::findFirst, Optional.of(new Object()));
test(stream -> stream.forEach(A_CONSUMER));
test(stream -> stream.forEachOrdered(A_CONSUMER));
test(stream -> stream.max(A_COMPARATOR), Optional.of(new Object()));
test(stream -> stream.min(A_COMPARATOR), Optional.of(new Object()));
test(stream -> stream.noneMatch(A_PREDICATE), true);
test(stream -> stream.reduce(A_BINARY_OPERATOR), Optional.of(new Object()));
test(stream -> stream.reduce(AN_OBJECT, A_BINARY_OPERATOR), Optional.of(new Object()));
test(stream -> stream.reduce(AN_OBJECT, A_BINARY_OPERATOR, A_BINARY_OPERATOR), Optional.of(new Object()));
test(Stream::toArray, new Object[1]);
test(stream -> stream.toArray(AN_INT_FUNCTION), new Object[1]);
}
private static <T> void test(Consumer<Stream> consumer) {
TERMINAL_OPERATIONS.add(new TerminalOperation<T>() {
@Override
public T result() {
return null;
}
@Override
public T apply(Stream stream) {
consumer.accept(stream);
return null;
}
});
}
private static <T> void test(Function<Stream, T> function, T result) {
TERMINAL_OPERATIONS.add(new TerminalOperation<T>() {
@Override
public T result() {
return result;
}
@Override
public T apply(Stream stream) {
return function.apply(stream);
}
});
}
private static <T extends BaseStream> void test(Class<? extends T> type, Function<Stream, T> function) {
INTERMEDIATE_OPERATIONS.add(new IntermediateOperation<T>() {
@Override
public Class<? extends T> type() {
return type;
}
@Override
public T apply(Stream stream) {
return function.apply(stream);
}
});
}
@Rule
public ExpectedException thrown = ExpectedException.none();
private Stream<Object> delegate;
private Stream<Object> inTest;
@Before
public void setUp() {
delegate = mock(Stream.class);
inTest = AutoClosingStream.from(delegate);
}
@Theory
public void testIntermediateOperationReturnsNewAutoClosingStream(@FromDataPoints("intermediateOperations") IntermediateOperation intermediateOperation) {
BaseStream newDelegate = (BaseStream) mock(intermediateOperation.type());
when(intermediateOperation.apply(delegate)).thenReturn(newDelegate);
BaseStream result = intermediateOperation.apply(inTest);
assertThat(result, isAutoClosing());
verifyDelegate(result, newDelegate);
}
@Theory
public void testTerminalOperationDelegatesToAndClosesDelegate(@FromDataPoints("terminalOperations") TerminalOperation terminalOperation) {
Object expectedResult = terminalOperation.result();
if (expectedResult != null) {
when(terminalOperation.apply(delegate)).thenReturn(expectedResult);
}
Object result = terminalOperation.apply(inTest);
InOrder inOrder = inOrder(delegate);
assertThat(result, is(expectedResult));
inOrder.verify(delegate).close();
}
@Theory
public void testTerminalOperationClosesDelegateEvenOnException(@FromDataPoints("terminalOperations") TerminalOperation terminalOperation) {
RuntimeException exception = new RuntimeException();
terminalOperation.apply(doThrow(exception).when(delegate));
thrown.expect(is(exception));
try {
terminalOperation.apply(inTest);
} finally {
verify(delegate).close();
}
}
private Matcher<BaseStream> isAutoClosing() {
return is(anyOf(instanceOf(AutoClosingStream.class), instanceOf(AutoClosingDoubleStream.class), instanceOf(AutoClosingIntStream.class), instanceOf(AutoClosingLongStream.class)));
}
private void verifyDelegate(BaseStream result, BaseStream newDelegate) {
result.close();
verify(newDelegate).close();
}
private interface TerminalOperation<T> {
T result();
T apply(Stream stream);
}
private interface IntermediateOperation<T extends BaseStream> {
Class<? extends T> type();
T apply(Stream stream);
}
}

View File

@@ -1,174 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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
******************************************************************************/
package org.cryptomator.webdav;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.Collection;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.webdav.jackrabbit.WebDavServlet;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class WebDavServer {
private static final Logger LOG = LoggerFactory.getLogger(WebDavServer.class);
private static final String LOCALHOST = SystemUtils.IS_OS_WINDOWS ? "::1" : "localhost";
private static final int MAX_PENDING_REQUESTS = 200;
private static final int MAX_THREADS = 200;
private static final int MIN_THREADS = 4;
private static final int THREAD_IDLE_SECONDS = 20;
private final Server server;
private final ServerConnector localConnector;
private final ContextHandlerCollection servletCollection;
public WebDavServer() {
final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(MAX_PENDING_REQUESTS);
final ThreadPool tp = new QueuedThreadPool(MAX_THREADS, MIN_THREADS, THREAD_IDLE_SECONDS, queue);
server = new Server(tp);
localConnector = new ServerConnector(server);
localConnector.setHost(LOCALHOST);
servletCollection = new ContextHandlerCollection();
if (SystemUtils.IS_OS_WINDOWS) {
final ServletContextHandler servletContext = new ServletContextHandler(servletCollection, "/", ServletContextHandler.NO_SESSIONS);
final ServletHolder servlet = new ServletHolder(WindowsSucksServlet.class);
servletContext.addServlet(servlet, "/");
}
server.setConnectors(new Connector[] {localConnector});
server.setHandler(servletCollection);
}
public synchronized void start() {
try {
server.start();
LOG.info("Cryptomator is running on port {}", getPort());
} catch (Exception ex) {
throw new RuntimeException("Server couldn't be started", ex);
}
}
public boolean isRunning() {
return server.isRunning();
}
public synchronized void stop() {
try {
server.stop();
} catch (Exception ex) {
LOG.error("Server couldn't be stopped", ex);
}
}
/**
* @param workDir Path of encrypted folder.
* @param cryptor A fully initialized cryptor instance ready to en- or decrypt streams.
* @param failingMacCollection A (observable, thread-safe) collection, to which the names of resources are written, whose MAC authentication fails.
* @param name The name of the folder. Must be non-empty and only contain any of _ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
* @return servlet
*/
public ServletLifeCycleAdapter createServlet(final Path workDir, final Cryptor cryptor, final Collection<String> failingMacCollection, final Collection<String> whitelistedResourceCollection, final String name) {
try {
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("name empty");
}
if (!StringUtils.containsOnly(name, "_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")) {
throw new IllegalArgumentException("name contains illegal characters: " + name);
}
final URI uri = new URI(null, null, localConnector.getHost(), localConnector.getLocalPort(), "/" + UUID.randomUUID().toString() + "/" + name, null, null);
final ServletContextHandler servletContext = new ServletContextHandler(servletCollection, uri.getRawPath(), ServletContextHandler.SESSIONS);
final ServletHolder servlet = getWebDavServletHolder(workDir.toString(), cryptor, failingMacCollection, whitelistedResourceCollection);
servletContext.addServlet(servlet, "/*");
servletCollection.mapContexts();
LOG.debug("{} available on http:{}", workDir, uri.getRawSchemeSpecificPart());
return new ServletLifeCycleAdapter(servletContext, uri);
} catch (URISyntaxException e) {
throw new IllegalStateException("Invalid hard-coded URI components.", e);
}
}
private ServletHolder getWebDavServletHolder(final String workDir, final Cryptor cryptor, final Collection<String> failingMacCollection, final Collection<String> whitelistedResourceCollection) {
final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor, failingMacCollection, whitelistedResourceCollection));
result.setInitParameter(WebDavServlet.CFG_FS_ROOT, workDir);
return result;
}
public int getPort() {
return localConnector.getLocalPort();
}
/**
* Exposes implementation-specific methods to other modules.
*/
public class ServletLifeCycleAdapter implements AutoCloseable {
private final LifeCycle lifecycle;
private final URI servletUri;
private ServletLifeCycleAdapter(LifeCycle lifecycle, URI servletUri) {
this.lifecycle = lifecycle;
this.servletUri = servletUri;
}
public boolean isRunning() {
return lifecycle.isRunning();
}
public boolean start() {
try {
lifecycle.start();
return true;
} catch (Exception e) {
LOG.error("Failed to start", e);
return false;
}
}
public boolean stop() {
try {
lifecycle.stop();
return true;
} catch (Exception e) {
LOG.error("Failed to stop", e);
return false;
}
}
public URI getServletUri() {
return servletUri;
}
@Override
public void close() throws Exception {
this.stop();
}
}
}

View File

@@ -1,31 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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
******************************************************************************/
package org.cryptomator.webdav;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Windows mount attempts will fail, if not all requests on parent paths of a WebDAV resource get served. This servlet will respond to any
* request with status code 200, if the requested resource doesn't match a different servlet.
*/
public class WindowsSucksServlet extends HttpServlet {
private static final long serialVersionUID = -515280795196074354L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setStatus(HttpServletResponse.SC_OK);
}
}

View File

@@ -1,31 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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
******************************************************************************/
package org.cryptomator.webdav.exceptions;
import org.apache.jackrabbit.webdav.DavException;
public class DavRuntimeException extends RuntimeException {
private static final long serialVersionUID = -4713080133052143303L;
public DavRuntimeException(DavException davException) {
super(davException);
}
@Override
public String getMessage() {
return getCause().getMessage();
}
@Override
public String getLocalizedMessage() {
return getCause().getLocalizedMessage();
}
}

View File

@@ -1,23 +0,0 @@
package org.cryptomator.webdav.exceptions;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
public class DecryptFailedRuntimeException extends RuntimeException {
private static final long serialVersionUID = -2726689824823439865L;
public DecryptFailedRuntimeException(DecryptFailedException cause) {
super(cause);
}
@Override
public String getMessage() {
return getCause().getMessage();
}
@Override
public String getLocalizedMessage() {
return getCause().getLocalizedMessage();
}
}

View File

@@ -1,31 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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
******************************************************************************/
package org.cryptomator.webdav.exceptions;
import java.io.IOException;
public class IORuntimeException extends RuntimeException {
private static final long serialVersionUID = -4713080133052143303L;
public IORuntimeException(IOException cause) {
super(cause);
}
@Override
public String getMessage() {
return getCause().getMessage();
}
@Override
public String getLocalizedMessage() {
return getCause().getLocalizedMessage();
}
}

View File

@@ -1,291 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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
******************************************************************************/
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.io.FilenameUtils;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavServletResponse;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.MultiStatusResponse;
import org.apache.jackrabbit.webdav.lock.ActiveLock;
import org.apache.jackrabbit.webdav.lock.LockInfo;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.apache.jackrabbit.webdav.lock.Scope;
import org.apache.jackrabbit.webdav.lock.Type;
import org.apache.jackrabbit.webdav.property.DavProperty;
import org.apache.jackrabbit.webdav.property.DavPropertyName;
import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
import org.apache.jackrabbit.webdav.property.DavPropertySet;
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.apache.jackrabbit.webdav.property.PropEntry;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
abstract class AbstractEncryptedNode implements DavResource {
private static final Logger LOG = LoggerFactory.getLogger(AbstractEncryptedNode.class);
private static final String DAV_COMPLIANCE_CLASSES = "1, 2";
private static final String[] DAV_CREATIONDATE_PROPNAMES = {DavPropertyName.CREATIONDATE.getName(), "Win32CreationTime"};
private static final String[] DAV_MODIFIEDDATE_PROPNAMES = {DavPropertyName.GETLASTMODIFIED.getName(), "Win32LastModifiedTime"};
protected final CryptoResourceFactory factory;
protected final DavResourceLocator locator;
protected final DavSession session;
protected final LockManager lockManager;
protected final Cryptor cryptor;
protected final Path filePath;
protected final DavPropertySet properties;
protected AbstractEncryptedNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, Path filePath) {
this.factory = factory;
this.locator = locator;
this.session = session;
this.lockManager = lockManager;
this.cryptor = cryptor;
this.filePath = filePath;
this.properties = new DavPropertySet();
if (filePath != null && Files.exists(filePath)) {
try {
final BasicFileAttributes attrs = Files.readAttributes(filePath, BasicFileAttributes.class);
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
} catch (IOException e) {
LOG.error("Error determining metadata " + filePath.toString(), e);
}
}
}
@Override
public String getComplianceClass() {
return DAV_COMPLIANCE_CLASSES;
}
@Override
public String getSupportedMethods() {
return METHODS;
}
@Override
public boolean exists() {
return Files.exists(filePath);
}
@Override
public String getDisplayName() {
final String resourcePath = getResourcePath();
final int lastSlash = resourcePath.lastIndexOf('/');
if (lastSlash == -1) {
return resourcePath;
} else {
return resourcePath.substring(lastSlash);
}
}
@Override
public DavResourceLocator getLocator() {
return locator;
}
@Override
public String getResourcePath() {
return locator.getResourcePath();
}
@Override
public String getHref() {
return locator.getHref(this.isCollection());
}
@Override
public long getModificationTime() {
try {
return Files.getLastModifiedTime(filePath).toMillis();
} catch (IOException e) {
return -1;
}
}
@Override
public DavPropertyName[] getPropertyNames() {
return getProperties().getPropertyNames();
}
@Override
public DavProperty<?> getProperty(DavPropertyName name) {
return getProperties().get(name);
}
@Override
public DavPropertySet getProperties() {
return properties;
}
@Override
public void setProperty(DavProperty<?> property) throws DavException {
getProperties().add(property);
LOG.trace("Set property {}", property.getName());
final String namespacelessPropertyName = property.getName().getName();
if (Files.exists(filePath)) {
try {
if (Arrays.asList(DAV_CREATIONDATE_PROPNAMES).contains(namespacelessPropertyName) && property.getValue() instanceof String) {
final String createDateStr = (String) property.getValue();
final FileTime createTime = FileTimeUtils.fromRfc1123String(createDateStr);
final BasicFileAttributeView attrView = Files.getFileAttributeView(filePath, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
attrView.setTimes(null, null, createTime);
LOG.debug("Updating Creation Date: {}", createTime.toString());
} else if (Arrays.asList(DAV_MODIFIEDDATE_PROPNAMES).contains(namespacelessPropertyName) && property.getValue() instanceof String) {
final String lastModifiedTimeStr = (String) property.getValue();
final FileTime lastModifiedTime = FileTimeUtils.fromRfc1123String(lastModifiedTimeStr);
final BasicFileAttributeView attrView = Files.getFileAttributeView(filePath, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
attrView.setTimes(lastModifiedTime, null, null);
LOG.debug("Updating Last Modified Date: {}", lastModifiedTime.toString());
}
} catch (IOException e) {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
}
@Override
public void removeProperty(DavPropertyName propertyName) throws DavException {
getProperties().remove(propertyName);
}
@Override
public MultiStatusResponse alterProperties(List<? extends PropEntry> changeList) throws DavException {
final DavPropertyNameSet names = new DavPropertyNameSet();
for (final PropEntry entry : changeList) {
if (entry instanceof DavProperty) {
final DavProperty<?> prop = (DavProperty<?>) entry;
this.setProperty(prop);
names.add(prop.getName());
} else if (entry instanceof DavPropertyName) {
final DavPropertyName name = (DavPropertyName) entry;
this.removeProperty(name);
names.add(name);
}
}
return new MultiStatusResponse(this, names);
}
@Override
public DavResource getCollection() {
if (locator.isRootLocation()) {
return null;
}
final String parentResource = FilenameUtils.getPathNoEndSeparator(locator.getResourcePath());
final DavResourceLocator parentLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), parentResource);
try {
return getFactory().createResource(parentLocator, session);
} catch (DavException e) {
throw new IllegalStateException("Unable to get parent resource with path " + parentLocator.getResourcePath(), e);
}
}
@Override
public final void move(DavResource dest) throws DavException {
if (dest instanceof AbstractEncryptedNode) {
try {
this.move((AbstractEncryptedNode) dest);
} catch (IOException e) {
LOG.error("Error moving file from " + this.getResourcePath() + " to " + dest.getResourcePath());
throw new IORuntimeException(e);
}
} else {
throw new IllegalArgumentException("Unsupported resource type: " + dest.getClass().getName());
}
}
public abstract void move(AbstractEncryptedNode dest) throws DavException, IOException;
@Override
public final void copy(DavResource dest, boolean shallow) throws DavException {
if (dest instanceof AbstractEncryptedNode) {
try {
this.copy((AbstractEncryptedNode) dest, shallow);
} catch (IOException e) {
LOG.error("Error copying file from " + this.getResourcePath() + " to " + dest.getResourcePath());
throw new IORuntimeException(e);
}
} else {
throw new IllegalArgumentException("Unsupported resource type: " + dest.getClass().getName());
}
}
public abstract void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException;
@Override
public boolean isLockable(Type type, Scope scope) {
return true;
}
@Override
public boolean hasLock(Type type, Scope scope) {
return lockManager.getLock(type, scope, this) != null;
}
@Override
public ActiveLock getLock(Type type, Scope scope) {
return lockManager.getLock(type, scope, this);
}
@Override
public ActiveLock[] getLocks() {
final ActiveLock exclusiveWriteLock = getLock(Type.WRITE, Scope.EXCLUSIVE);
return new ActiveLock[] {exclusiveWriteLock};
}
@Override
public ActiveLock lock(LockInfo reqLockInfo) throws DavException {
return lockManager.createLock(reqLockInfo, this);
}
@Override
public ActiveLock refreshLock(LockInfo reqLockInfo, String lockToken) throws DavException {
return lockManager.refreshLock(reqLockInfo, lockToken, this);
}
@Override
public void unlock(String lockToken) throws DavException {
lockManager.releaseLock(lockToken, this);
}
@Override
public void addLockManager(LockManager lockmgr) {
throw new UnsupportedOperationException("Locks are managed");
}
@Override
public CryptoResourceFactory getFactory() {
return factory;
}
@Override
public DavSession getSession() {
return session;
}
}

View File

@@ -1,126 +0,0 @@
package org.cryptomator.webdav.jackrabbit;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.webdav.DavLocatorFactory;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.util.EncodeUtil;
import org.apache.logging.log4j.util.Strings;
public class CleartextLocatorFactory implements DavLocatorFactory {
private final String pathPrefix;
public CleartextLocatorFactory(String pathPrefix) {
this.pathPrefix = pathPrefix;
}
// resourcePath == repositoryPath. No encryption here.
@Override
public DavResourceLocator createResourceLocator(String prefix, String href) {
final String fullPrefix = prefix.endsWith("/") ? prefix : prefix + "/";
final String relativeHref = StringUtils.removeStart(href, fullPrefix);
final String relativeCleartextPath = EncodeUtil.unescape(StringUtils.removeStart(relativeHref, "/"));
return new CleartextLocator(relativeCleartextPath);
}
@Override
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String resourcePath) {
return new CleartextLocator(resourcePath);
}
@Override
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) {
return new CleartextLocator(path);
}
private class CleartextLocator implements DavResourceLocator {
private final String relativeCleartextPath;
private CleartextLocator(String relativeCleartextPath) {
this.relativeCleartextPath = FilenameUtils.normalizeNoEndSeparator(relativeCleartextPath, true);
}
@Override
public String getPrefix() {
return pathPrefix;
}
@Override
public String getResourcePath() {
return relativeCleartextPath;
}
@Override
public String getWorkspacePath() {
return null;
}
@Override
public String getWorkspaceName() {
return null;
}
@Override
public boolean isSameWorkspace(DavResourceLocator locator) {
return false;
}
@Override
public boolean isSameWorkspace(String workspaceName) {
return false;
}
@Override
public String getHref(boolean isCollection) {
final String encodedResourcePath = EncodeUtil.escapePath(getResourcePath());
final String fullPrefix = pathPrefix.endsWith("/") ? pathPrefix : pathPrefix + "/";
final String href = fullPrefix.concat(encodedResourcePath);
assert href.equals(fullPrefix) || !href.endsWith("/");
if (isCollection) {
return href.concat("/");
} else {
return href;
}
}
@Override
public boolean isRootLocation() {
return Strings.isEmpty(relativeCleartextPath);
}
@Override
public DavLocatorFactory getFactory() {
return CleartextLocatorFactory.this;
}
@Override
public String getRepositoryPath() {
return relativeCleartextPath;
}
@Override
public String toString() {
return "Locator: " + relativeCleartextPath + " (Prefix: " + pathPrefix + ")";
}
@Override
public int hashCode() {
return relativeCleartextPath.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof CleartextLocator) {
final CleartextLocator other = (CleartextLocator) obj;
return relativeCleartextPath == null && other.relativeCleartextPath == null || relativeCleartextPath.equals(other.relativeCleartextPath);
} else {
return false;
}
}
}
}

View File

@@ -1,285 +0,0 @@
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.time.format.DateTimeParseException;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavMethods;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceFactory;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavServletRequest;
import org.apache.jackrabbit.webdav.DavServletResponse;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.apache.jackrabbit.webdav.lock.SimpleLockManager;
import org.apache.logging.log4j.util.Strings;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.eclipse.jetty.http.HttpHeader;
public class CryptoResourceFactory implements DavResourceFactory, FileConstants {
private static final String RANGE_BYTE_PREFIX = "bytes=";
private static final char RANGE_SET_SEP = ',';
private static final char RANGE_SEP = '-';
private final LockManager lockManager = new SimpleLockManager();
private final Cryptor cryptor;
private final CryptoWarningHandler cryptoWarningHandler;
private final Path dataRoot;
private final FilenameTranslator filenameTranslator;
CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, String vaultRoot) {
Path vaultRootPath = FileSystems.getDefault().getPath(vaultRoot);
this.cryptor = cryptor;
this.cryptoWarningHandler = cryptoWarningHandler;
this.dataRoot = vaultRootPath.resolve("d");
this.filenameTranslator = new FilenameTranslator(cryptor, vaultRootPath);
}
@Override
public final DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
if (locator.isRootLocation()) {
return createRootDirectory(locator, request.getDavSession());
}
try {
final Path filePath = getEncryptedFilePath(locator.getResourcePath(), false);
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath(), false);
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
final String ifRangeHeader = request.getHeader(HttpHeader.IF_RANGE.asString());
if (Files.exists(dirFilePath) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
// DIRECTORY
return createDirectory(locator, request.getDavSession(), dirFilePath);
} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null && isRangeSatisfiable(rangeHeader) && isIfRangePreconditionFulfilled(ifRangeHeader, filePath)) {
// FILE RANGE
final Pair<String, String> requestRange = getRequestRange(rangeHeader);
response.setStatus(DavServletResponse.SC_PARTIAL_CONTENT);
return createFilePart(locator, request.getDavSession(), requestRange, filePath);
} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null && isRangeSatisfiable(rangeHeader) && !isIfRangePreconditionFulfilled(ifRangeHeader, filePath)) {
// FULL FILE (if-range not fulfilled)
return createFile(locator, request.getDavSession(), filePath);
} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null && !isRangeSatisfiable(rangeHeader)) {
// FULL FILE (unsatisfiable range)
response.setStatus(DavServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
final EncryptedFile file = createFile(locator, request.getDavSession(), filePath);
response.addHeader(HttpHeader.CONTENT_RANGE.asString(), "bytes */" + file.getContentLength());
return file;
} else if (Files.exists(filePath) || DavMethods.METHOD_PUT.equals(request.getMethod())) {
// FULL FILE (as requested)
return createFile(locator, request.getDavSession(), filePath);
}
} catch (NonExistingParentException e) {
// return non-existing
}
return createNonExisting(locator, request.getDavSession());
}
@Override
public final DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
if (locator.isRootLocation()) {
return createRootDirectory(locator, session);
}
try {
final Path filePath = getEncryptedFilePath(locator.getResourcePath(), false);
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath(), false);
if (Files.exists(dirFilePath)) {
return createDirectory(locator, session, dirFilePath);
} else if (Files.exists(filePath)) {
return createFile(locator, session, filePath);
}
} catch (NonExistingParentException e) {
// return non-existing
}
return createNonExisting(locator, session);
}
DavResource createChildDirectoryResource(DavResourceLocator locator, DavSession session, Path existingDirectoryFile) throws DavException {
return createDirectory(locator, session, existingDirectoryFile);
}
DavResource createChildFileResource(DavResourceLocator locator, DavSession session, Path existingFile) throws DavException {
return createFile(locator, session, existingFile);
}
/**
* @return <code>true</code> if a partial response should be generated according to an If-Range precondition.
*/
private boolean isIfRangePreconditionFulfilled(String ifRangeHeader, Path filePath) throws DavException {
if (ifRangeHeader == null) {
// no header set -> fulfilled implicitly
return true;
} else {
try {
final FileTime expectedTime = FileTimeUtils.fromRfc1123String(ifRangeHeader);
final FileTime actualTime = Files.getLastModifiedTime(filePath);
return expectedTime.compareTo(actualTime) == 0;
} catch (DateTimeParseException e) {
throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Unsupported If-Range header: " + ifRangeHeader);
} catch (IOException e) {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
}
}
}
/**
* @return <code>true</code> if and only if exactly one byte range has been requested.
*/
private boolean isRangeSatisfiable(String rangeHeader) {
assert rangeHeader != null;
if (!rangeHeader.startsWith(RANGE_BYTE_PREFIX)) {
return false;
}
final String byteRangeSet = StringUtils.removeStartIgnoreCase(rangeHeader, RANGE_BYTE_PREFIX);
final String[] byteRanges = StringUtils.split(byteRangeSet, RANGE_SET_SEP);
if (byteRanges.length != 1) {
return false;
}
return true;
}
/**
* Processes the given range header field, if it is supported. Only headers containing a single byte range are supported.<br/>
* <code>
* bytes=100-200<br/>
* bytes=-500<br/>
* bytes=1000-
* </code>
*
* @return Tuple of left and right range.
* @throws DavException HTTP statuscode 400 for malformed requests.
* @throws IllegalArgumentException If the given rangeHeader is not satisfiable. Check with {@link #isRangeSatisfiable(String)} before.
*/
private Pair<String, String> getRequestRange(String rangeHeader) throws DavException {
assert rangeHeader != null;
if (!rangeHeader.startsWith(RANGE_BYTE_PREFIX)) {
throw new IllegalArgumentException("Unsatisfiable range. Should have generated 416 resonse.");
}
final String byteRangeSet = StringUtils.removeStartIgnoreCase(rangeHeader, RANGE_BYTE_PREFIX);
final String[] byteRanges = StringUtils.split(byteRangeSet, RANGE_SET_SEP);
if (byteRanges.length != 1) {
throw new IllegalArgumentException("Unsatisfiable range. Should have generated 416 resonse.");
}
final String byteRange = byteRanges[0];
final String[] bytePos = StringUtils.splitPreserveAllTokens(byteRange, RANGE_SEP);
if (bytePos.length != 2 || bytePos[0].isEmpty() && bytePos[1].isEmpty()) {
throw new DavException(DavServletResponse.SC_BAD_REQUEST, "malformed range header: " + rangeHeader);
}
return new ImmutablePair<>(bytePos[0], bytePos[1]);
}
/**
* @return Absolute file path for a given cleartext file resourcePath.
* @throws NonExistingParentException If one ancestor of the enrypted path is missing
*/
Path getEncryptedFilePath(String relativeCleartextPath, boolean createNonExisting) throws NonExistingParentException {
final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath);
final Path parent = getEncryptedDirectoryPath(parentCleartextPath, createNonExisting);
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
try {
final String encryptedFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
return parent.resolve(encryptedFilename);
} catch (IOException e) {
throw new IORuntimeException(e);
}
}
/**
* @return Absolute file path for a given cleartext file resourcePath.
* @throws NonExistingParentException If one ancestor of the enrypted path is missing
*/
Path getEncryptedDirectoryFilePath(String relativeCleartextPath, boolean createNonExisting) throws NonExistingParentException {
final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath);
final Path parent = getEncryptedDirectoryPath(parentCleartextPath, createNonExisting);
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
try {
final String encryptedFilename = filenameTranslator.getEncryptedDirFileName(cleartextFilename);
return parent.resolve(encryptedFilename);
} catch (IOException e) {
throw new IORuntimeException(e);
}
}
/**
* @param createNonExisting if <code>false</code>, a {@link NonExistingParentException} will be thrown for missing ancestors.
* @return Absolute directory path for a given cleartext directory resourcePath.
* @throws NonExistingParentException if one ancestor directory is missing.
*/
private Path getEncryptedDirectoryPath(String relativeCleartextPath, boolean createNonExisting) throws NonExistingParentException {
assert Strings.isEmpty(relativeCleartextPath) || !relativeCleartextPath.endsWith("/");
try {
final Path result;
if (Strings.isEmpty(relativeCleartextPath)) {
// root level
final String fixedRootDirectory = cryptor.encryptDirectoryPath("", FileSystems.getDefault().getSeparator());
result = dataRoot.resolve(fixedRootDirectory);
} else {
final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath);
final Path parent = getEncryptedDirectoryPath(parentCleartextPath, createNonExisting);
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
final String encryptedFilename = filenameTranslator.getEncryptedDirFileName(cleartextFilename);
final Path directoryFile = parent.resolve(encryptedFilename);
if (!createNonExisting && !Files.exists(directoryFile)) {
throw new NonExistingParentException();
}
final String directoryId = filenameTranslator.getDirectoryId(directoryFile, true);
final String directory = cryptor.encryptDirectoryPath(directoryId, FileSystems.getDefault().getSeparator());
result = dataRoot.resolve(directory);
}
Files.createDirectories(result);
return result;
} catch (IOException e) {
throw new IORuntimeException(e);
}
}
private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, Pair<String, String> requestRange, Path filePath) {
return new EncryptedFilePart(this, locator, session, requestRange, lockManager, cryptor, cryptoWarningHandler, filePath);
}
private EncryptedFile createFile(DavResourceLocator locator, DavSession session, Path filePath) {
return new EncryptedFile(this, locator, session, lockManager, cryptor, cryptoWarningHandler, filePath);
}
private EncryptedDir createRootDirectory(DavResourceLocator locator, DavSession session) throws DavException {
final Path rootFile = dataRoot.resolve(ROOT_FILE);
final Path rootDir = filenameTranslator.getEncryptedDirectoryPath("");
try {
// make sure, root dir always exists.
// create dir first (because it fails silently, if alreay existing)
Files.createDirectories(rootDir);
Files.createFile(rootFile);
} catch (FileAlreadyExistsException e) {
// no-op
} catch (IOException e) {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR);
}
return createDirectory(locator, session, dataRoot.resolve(ROOT_FILE));
}
private EncryptedDir createDirectory(DavResourceLocator locator, DavSession session, Path filePath) {
return new EncryptedDir(this, locator, session, lockManager, cryptor, filenameTranslator, filePath);
}
private NonExistingNode createNonExisting(DavResourceLocator locator, DavSession session) {
return new NonExistingNode(this, locator, session, lockManager, cryptor);
}
static class NonExistingParentException extends Exception {
private static final long serialVersionUID = 4421121746624627094L;
}
}

View File

@@ -1,26 +0,0 @@
package org.cryptomator.webdav.jackrabbit;
import java.util.Collection;
class CryptoWarningHandler {
private final Collection<String> resourcesWithInvalidMac;
private final Collection<String> whitelistedResources;
public CryptoWarningHandler(Collection<String> resourcesWithInvalidMac, Collection<String> whitelistedResources) {
this.resourcesWithInvalidMac = resourcesWithInvalidMac;
this.whitelistedResources = whitelistedResources;
}
public void macAuthFailed(String resourcePath) {
// collection might be a list, but we don't want duplicates:
if (!resourcesWithInvalidMac.contains(resourcePath)) {
resourcesWithInvalidMac.add(resourcePath);
}
}
public boolean ignoreMac(String resourcePath) {
return whitelistedResources.contains(resourcePath);
}
}

View File

@@ -1,344 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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
******************************************************************************/
package org.cryptomator.webdav.jackrabbit;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceIterator;
import org.apache.jackrabbit.webdav.DavResourceIteratorImpl;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavServletResponse;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.io.InputContext;
import org.apache.jackrabbit.webdav.io.OutputContext;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.apache.jackrabbit.webdav.property.DavPropertyName;
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.apache.jackrabbit.webdav.property.ResourceType;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.exceptions.CounterOverflowException;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
import org.cryptomator.webdav.exceptions.DavRuntimeException;
import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.eclipse.jetty.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
private static final Logger LOG = LoggerFactory.getLogger(EncryptedDir.class);
private final FilenameTranslator filenameTranslator;
private String directoryId;
private Path directoryPath;
public EncryptedDir(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, FilenameTranslator filenameTranslator, Path filePath) {
super(factory, locator, session, lockManager, cryptor, filePath);
this.filenameTranslator = filenameTranslator;
properties.add(new ResourceType(ResourceType.COLLECTION));
properties.add(new DefaultDavProperty<Integer>(DavPropertyName.ISCOLLECTION, 1));
}
/**
* @return Path or <code>null</code>, if directory does not yet exist.
*/
protected synchronized String getDirectoryId() {
if (directoryId == null) {
try {
directoryId = filenameTranslator.getDirectoryId(filePath, false);
} catch (IOException e) {
throw new IORuntimeException(e);
}
}
return directoryId;
}
/**
* @return Path or <code>null</code>, if directory does not yet exist.
*/
private synchronized Path getDirectoryPath() {
if (directoryPath == null) {
final String dirId = getDirectoryId();
if (dirId != null) {
directoryPath = filenameTranslator.getEncryptedDirectoryPath(directoryId);
}
}
return directoryPath;
}
@Override
public boolean isCollection() {
return true;
}
@Override
public long getModificationTime() {
try {
final Path dirPath = getDirectoryPath();
if (dirPath == null) {
return -1;
} else {
return Files.getLastModifiedTime(dirPath).toMillis();
}
} catch (IOException e) {
return -1;
}
}
@Override
public void addMember(DavResource resource, InputContext inputContext) throws DavException {
if (resource instanceof AbstractEncryptedNode) {
addMember((AbstractEncryptedNode) resource, inputContext);
} else {
throw new IllegalArgumentException("Unsupported resource type: " + resource.getClass().getName());
}
}
private void addMember(AbstractEncryptedNode childResource, InputContext inputContext) throws DavException {
if (childResource.isCollection()) {
this.addMemberDir(childResource.getLocator(), inputContext);
} else {
this.addMemberFile(childResource.getLocator(), inputContext);
}
}
private void addMemberDir(DavResourceLocator childLocator, InputContext inputContext) throws DavException {
final Path dirPath = getDirectoryPath();
if (dirPath == null) {
throw new DavException(DavServletResponse.SC_NOT_FOUND);
}
try {
final String cleartextDirName = FilenameUtils.getName(childLocator.getResourcePath());
final String ciphertextDirName = filenameTranslator.getEncryptedDirFileName(cleartextDirName);
final Path dirFilePath = dirPath.resolve(ciphertextDirName);
final String directoryId = filenameTranslator.getDirectoryId(dirFilePath, true);
final Path directoryPath = filenameTranslator.getEncryptedDirectoryPath(directoryId);
Files.createDirectories(directoryPath);
} catch (SecurityException e) {
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
} catch (IOException e) {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
}
}
private void addMemberFile(DavResourceLocator childLocator, InputContext inputContext) throws DavException {
final Path dirPath = getDirectoryPath();
if (dirPath == null) {
throw new DavException(DavServletResponse.SC_NOT_FOUND);
}
try {
final String cleartextFilename = FilenameUtils.getName(childLocator.getResourcePath());
final String ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
final Path filePath = dirPath.resolve(ciphertextFilename);
final Path tmpFilePath = Files.createTempFile(dirPath, null, null);
// encrypt to tmp file:
try (final FileChannel c = FileChannel.open(tmpFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
final SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, false)) {
cryptor.encryptFile(inputContext.getInputStream(), c);
} catch (SecurityException e) {
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
} catch (CounterOverflowException e) {
// lets indicate this to the client as a "file too big" error
throw new DavException(DavServletResponse.SC_INSUFFICIENT_SPACE_ON_RESOURCE, e);
} catch (EncryptFailedException e) {
LOG.error("Encryption failed for unknown reasons.", e);
throw new IllegalStateException("Encryption failed for unknown reasons.", e);
} finally {
IOUtils.closeQuietly(inputContext.getInputStream());
}
// mv tmp to target file:
try {
Files.move(tmpFilePath, filePath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(tmpFilePath, filePath, StandardCopyOption.REPLACE_EXISTING);
}
Files.setLastModifiedTime(filePath, FileTime.from(Instant.now()));
} catch (IOException e) {
LOG.error("Failed to create file.", e);
throw new IORuntimeException(e);
}
}
@Override
public DavResourceIterator getMembers() {
try {
final Path dirPath = getDirectoryPath();
if (dirPath == null) {
throw new DavException(DavServletResponse.SC_NOT_FOUND);
}
final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(dirPath, DIRECTORY_CONTENT_FILTER);
final List<DavResource> result = new ArrayList<>();
for (final Path childPath : directoryStream) {
try {
final String cleartextFilename = filenameTranslator.getCleartextFilename(childPath.getFileName().toString());
final String cleartextFilepath = FilenameUtils.concat(getResourcePath(), cleartextFilename);
final DavResourceLocator childLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), cleartextFilepath);
final DavResource resource;
if (StringUtil.endsWithIgnoreCase(childPath.getFileName().toString(), DIR_EXT)) {
resource = factory.createChildDirectoryResource(childLocator, session, childPath);
} else {
assert StringUtil.endsWithIgnoreCase(childPath.getFileName().toString(), FILE_EXT);
resource = factory.createChildFileResource(childLocator, session, childPath);
}
result.add(resource);
} catch (DecryptFailedException e) {
LOG.warn("Decryption of resource failed: " + childPath);
continue;
}
}
return new DavResourceIteratorImpl(result);
} catch (IOException e) {
LOG.error("Exception during getMembers.", e);
throw new IORuntimeException(e);
} catch (DavException e) {
LOG.error("Exception during getMembers.", e);
throw new DavRuntimeException(e);
}
}
@Override
public void removeMember(DavResource member) throws DavException {
if (member instanceof AbstractEncryptedNode) {
removeMember((AbstractEncryptedNode) member);
} else {
throw new IllegalArgumentException("Unsupported resource type: " + member.getClass().getName());
}
}
private void removeMember(AbstractEncryptedNode member) throws DavException {
final Path dirPath = getDirectoryPath();
if (dirPath == null) {
throw new DavException(DavServletResponse.SC_NOT_FOUND);
}
try {
final String cleartextFilename = FilenameUtils.getName(member.getResourcePath());
final String ciphertextFilename;
if (member instanceof EncryptedDir) {
final EncryptedDir subDir = (EncryptedDir) member;
// remove sub-members recursively before deleting own directory
for (Iterator<DavResource> iterator = member.getMembers(); iterator.hasNext();) {
DavResource m = iterator.next();
member.removeMember(m);
}
final Path subDirPath = subDir.getDirectoryPath();
if (subDirPath != null) {
Files.deleteIfExists(subDirPath);
}
ciphertextFilename = filenameTranslator.getEncryptedDirFileName(cleartextFilename);
} else {
ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
}
final Path memberPath = dirPath.resolve(ciphertextFilename);
Files.deleteIfExists(memberPath);
} catch (FileNotFoundException e) {
// no-op
} catch (IOException e) {
throw new IORuntimeException(e);
}
}
@Override
public void move(AbstractEncryptedNode dest) throws DavException, IOException {
// when moving a directory we only need to move the file (actual dir is ID-dependent and won't change)
final Path srcPath = filePath;
final Path dstPath;
if (dest instanceof NonExistingNode) {
dstPath = ((NonExistingNode) dest).materializeDirFilePath();
} else {
dstPath = dest.filePath;
}
// move:
Files.createDirectories(dstPath.getParent());
try {
Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING);
}
}
@Override
public void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException {
final Path dstDirFilePath;
if (dest instanceof NonExistingNode) {
dstDirFilePath = ((NonExistingNode) dest).materializeDirFilePath();
} else {
dstDirFilePath = dest.filePath;
}
// copy dirFile:
final String srcDirId = getDirectoryId();
if (srcDirId == null) {
throw new DavException(DavServletResponse.SC_NOT_FOUND);
}
final String dstDirId = UUID.randomUUID().toString();
try (final FileChannel c = FileChannel.open(dstDirFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, false)) {
c.write(ByteBuffer.wrap(dstDirId.getBytes(StandardCharsets.UTF_8)));
}
// copy actual dir:
if (!shallow) {
copyDirectoryContents(srcDirId, dstDirId);
} else {
final Path dstDirPath = filenameTranslator.getEncryptedDirectoryPath(dstDirId);
Files.createDirectories(dstDirPath);
}
}
private void copyDirectoryContents(String srcDirId, String dstDirId) throws IOException {
final Path srcDirPath = filenameTranslator.getEncryptedDirectoryPath(srcDirId);
final Path dstDirPath = filenameTranslator.getEncryptedDirectoryPath(dstDirId);
Files.createDirectories(dstDirPath);
final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(srcDirPath, DIRECTORY_CONTENT_FILTER);
for (final Path srcChildPath : directoryStream) {
final String childName = srcChildPath.getFileName().toString();
final Path dstChildPath = dstDirPath.resolve(childName);
if (StringUtils.endsWithIgnoreCase(childName, FILE_EXT)) {
try {
Files.copy(srcChildPath, dstChildPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.copy(srcChildPath, dstChildPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
}
} else if (StringUtils.endsWithIgnoreCase(childName, DIR_EXT)) {
final String srcSubdirId = filenameTranslator.getDirectoryId(srcChildPath, false);
final String dstSubdirId = filenameTranslator.getDirectoryId(dstChildPath, true);
copyDirectoryContents(srcSubdirId, dstSubdirId);
}
}
}
@Override
public void spool(OutputContext outputContext) throws IOException {
// do nothing
}
}

View File

@@ -1,154 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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
******************************************************************************/
package org.cryptomator.webdav.jackrabbit;
import java.io.EOFException;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.OverlappingFileLockException;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceIterator;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.io.InputContext;
import org.apache.jackrabbit.webdav.io.OutputContext;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.apache.jackrabbit.webdav.property.DavPropertyName;
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFile.class);
protected final CryptoWarningHandler cryptoWarningHandler;
protected final Long contentLength;
public EncryptedFile(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, Path filePath) {
super(factory, locator, session, lockManager, cryptor, filePath);
if (filePath == null) {
throw new IllegalArgumentException("filePath must not be null");
}
this.cryptoWarningHandler = cryptoWarningHandler;
Long contentLength = null;
if (Files.isRegularFile(filePath)) {
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ, StandardOpenOption.DSYNC); SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, true)) {
contentLength = cryptor.decryptedContentLength(c);
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, contentLength));
if (contentLength > RANGE_REQUEST_LOWER_LIMIT) {
properties.add(new HttpHeaderProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString()));
}
} catch (OverlappingFileLockException e) {
// file header currently locked, report -1 for unknown size.
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, -1l));
} catch (MacAuthenticationFailedException e) {
LOG.warn("Content length couldn't be determined due to MAC authentication violation.");
// don't add content length DAV property
} catch (IOException e) {
LOG.error("Error reading filesize " + filePath.toString(), e);
throw new IORuntimeException(e);
}
}
this.contentLength = contentLength;
}
public Long getContentLength() {
return contentLength;
}
@Override
public boolean isCollection() {
return false;
}
@Override
public void addMember(DavResource resource, InputContext inputContext) throws DavException {
throw new UnsupportedOperationException("Can not add member to file.");
}
@Override
public DavResourceIterator getMembers() {
throw new UnsupportedOperationException("Can not list members of file.");
}
@Override
public void removeMember(DavResource member) throws DavException {
throw new UnsupportedOperationException("Can not remove member to file.");
}
@Override
public void spool(OutputContext outputContext) throws IOException {
if (Files.isRegularFile(filePath)) {
outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis());
outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString());
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ); SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, true)) {
final Long contentLength = cryptor.decryptedContentLength(c);
if (contentLength != null) {
outputContext.setContentLength(contentLength);
}
if (outputContext.hasStream()) {
final boolean authenticate = !cryptoWarningHandler.ignoreMac(getLocator().getResourcePath());
cryptor.decryptFile(c, outputContext.getOutputStream(), authenticate);
outputContext.getOutputStream().flush();
}
} catch (EOFException e) {
LOG.warn("Unexpected end of stream (possibly client hung up).");
}
}
}
@Override
public void move(AbstractEncryptedNode dest) throws DavException, IOException {
final Path srcPath = filePath;
final Path dstPath;
if (dest instanceof NonExistingNode) {
dstPath = ((NonExistingNode) dest).materializeFilePath();
} else {
dstPath = dest.filePath;
}
try {
Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING);
}
}
@Override
public void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException {
final Path srcPath = filePath;
final Path dstPath;
if (dest instanceof NonExistingNode) {
dstPath = ((NonExistingNode) dest).materializeFilePath();
} else {
dstPath = dest.filePath;
}
try {
Files.copy(srcPath, dstPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.copy(srcPath, dstPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
}
}
}

View File

@@ -1,90 +0,0 @@
package org.cryptomator.webdav.jackrabbit;
import java.io.EOFException;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.io.OutputContext;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.cryptomator.crypto.Cryptor;
import org.eclipse.jetty.http.HttpHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Delivers only the requested range of bytes from a file.
*
* @see {@link https://tools.ietf.org/html/rfc7233#section-4}
*/
class EncryptedFilePart extends EncryptedFile {
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFilePart.class);
private final Pair<Long, Long> range;
public EncryptedFilePart(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, Pair<String, String> requestRange, LockManager lockManager, Cryptor cryptor,
CryptoWarningHandler cryptoWarningHandler, Path filePath) {
super(factory, locator, session, lockManager, cryptor, cryptoWarningHandler, filePath);
try {
final Long lower = requestRange.getLeft().isEmpty() ? null : Long.valueOf(requestRange.getLeft());
final Long upper = requestRange.getRight().isEmpty() ? null : Long.valueOf(requestRange.getRight());
if (lower == null) {
range = new ImmutablePair<Long, Long>(contentLength - upper, contentLength - 1);
} else if (upper == null) {
range = new ImmutablePair<Long, Long>(lower, contentLength - 1);
} else {
range = new ImmutablePair<Long, Long>(lower, Math.min(upper, contentLength - 1));
}
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid byte range: " + requestRange, e);
}
}
@Override
public void spool(OutputContext outputContext) throws IOException {
assert Files.isRegularFile(filePath);
assert contentLength != null;
final Long rangeLength = range.getRight() - range.getLeft() + 1;
outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis());
if (rangeLength <= 0 || range.getLeft() > contentLength - 1) {
// unsatisfiable content range:
outputContext.setContentLength(0);
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), "bytes */" + contentLength);
LOG.debug("Requested content range unsatisfiable: " + getContentRangeHeader(range.getLeft(), range.getRight(), contentLength));
return;
} else {
outputContext.setContentLength(rangeLength);
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getLeft(), range.getRight(), contentLength));
}
assert range.getLeft() > 0;
assert range.getLeft() < contentLength;
assert range.getRight() < contentLength;
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ)) {
if (outputContext.hasStream()) {
final boolean authenticate = !cryptoWarningHandler.ignoreMac(getLocator().getResourcePath());
cryptor.decryptRange(c, outputContext.getOutputStream(), range.getLeft(), rangeLength, authenticate);
outputContext.getOutputStream().flush();
}
} catch (EOFException e) {
if (LOG.isDebugEnabled()) {
LOG.trace("Unexpected end of stream during delivery of partial content (client hung up).");
}
}
}
private String getContentRangeHeader(long firstByte, long lastByte, long completeLength) {
return String.format("bytes %d-%d/%d", firstByte, lastByte, completeLength);
}
}

View File

@@ -1,108 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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
******************************************************************************/
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
interface FileConstants {
/**
* Number of bytes in the file header.
*/
long FILE_HEADER_LENGTH = 104;
/**
* Allow range requests for files > 32MiB.
*/
long RANGE_REQUEST_LOWER_LIMIT = 32 * 1024 * 1024;
/**
* Maximum path length on some file systems or cloud storage providers is restricted.<br/>
* Parent folder path uses up to 58 chars (sha256 -&gt; 32 bytes base32 encoded to 56 bytes + two slashes). That in mind we don't want the total path to be longer than 255 chars.<br/>
* 128 chars would be enought for up to 80 plaintext chars. Also we need up to 9 chars for our file extension. So lets use {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT}.
*/
int ENCRYPTED_FILENAME_LENGTH_LIMIT = 137;
/**
* Dummy file, on which file attributes can be stored for the root directory.
*/
String ROOT_FILE = "root";
/**
* For encrypted directory names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
*/
String DIR_EXT = ".dir";
/**
* For encrypted direcotry names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
*/
String LONG_DIR_EXT = ".lng.dir";
/**
* For encrypted file names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
*/
String FILE_EXT = ".file";
/**
* For encrypted file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
*/
String LONG_FILE_EXT = ".lng.file";
/**
* Length of prefix in file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars used to determine the corresponding metadata file.
*/
int LONG_NAME_PREFIX_LENGTH = 8;
/**
* Matches valid encrypted filenames (both normal and long filenames - see {@link #ENCRYPTED_FILENAME_LENGTH_LIMIT}).
*/
PathMatcher ENCRYPTED_FILE_MATCHER = new PathMatcher() {
private final Pattern BASIC_NAME_PATTERN = Pattern.compile("^[a-z2-7]+=*$", Pattern.CASE_INSENSITIVE);
private final Pattern LONG_NAME_PATTERN = Pattern.compile("^[a-z2-7]{8}[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", Pattern.CASE_INSENSITIVE);
@Override
public boolean matches(Path path) {
final String filename = path.getFileName().toString();
if (StringUtils.endsWithIgnoreCase(filename, LONG_FILE_EXT)) {
final String basename = StringUtils.removeEndIgnoreCase(filename, LONG_FILE_EXT);
return LONG_NAME_PATTERN.matcher(basename).matches();
} else if (StringUtils.endsWithIgnoreCase(filename, FILE_EXT)) {
final String basename = StringUtils.removeEndIgnoreCase(filename, FILE_EXT);
return BASIC_NAME_PATTERN.matcher(basename).matches();
} else if (StringUtils.endsWithIgnoreCase(filename, LONG_DIR_EXT)) {
final String basename = StringUtils.removeEndIgnoreCase(filename, LONG_DIR_EXT);
return LONG_NAME_PATTERN.matcher(basename).matches();
} else if (StringUtils.endsWithIgnoreCase(filename, DIR_EXT)) {
final String basename = StringUtils.removeEndIgnoreCase(filename, DIR_EXT);
return BASIC_NAME_PATTERN.matcher(basename).matches();
} else {
return false;
}
}
};
/**
* Filter to determine files of interest in encrypted directory. Based on {@link #ENCRYPTED_FILE_MATCHER}.
*/
Filter<Path> DIRECTORY_CONTENT_FILTER = new Filter<Path>() {
@Override
public boolean accept(Path entry) throws IOException {
return ENCRYPTED_FILE_MATCHER.matches(entry);
}
};
}

View File

@@ -1,34 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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
******************************************************************************/
package org.cryptomator.webdav.jackrabbit;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.temporal.Temporal;
final class FileTimeUtils {
private FileTimeUtils() {
throw new IllegalStateException("not instantiable");
}
static String toRfc1123String(FileTime time) {
final Temporal date = OffsetDateTime.ofInstant(time.toInstant(), ZoneOffset.UTC);
return DateTimeFormatter.RFC_1123_DATE_TIME.format(date);
}
static FileTime fromRfc1123String(String string) {
final Instant instant = Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(string));
return FileTime.from(instant);
}
}

View File

@@ -1,226 +0,0 @@
package org.cryptomator.webdav.jackrabbit;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileTime;
import java.util.Map;
import java.util.UUID;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import org.apache.commons.collections4.map.LRUMap;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
class FilenameTranslator implements FileConstants {
private static final int MAX_CACHED_DIRECTORY_IDS = 5000;
private static final int MAX_CACHED_METADATA_FILES = 1000;
private final Cryptor cryptor;
private final Path dataRoot;
private final Path metadataRoot;
private final ObjectMapper objectMapper = new ObjectMapper();
private final Map<Pair<Path, FileTime>, String> directoryIdCache = new LRUMap<>(MAX_CACHED_DIRECTORY_IDS); // <directoryFile, directoryId>
private final Map<Pair<Path, FileTime>, LongFilenameMetadata> metadataCache = new LRUMap<>(MAX_CACHED_METADATA_FILES); // <metadataFile, metadata>
public FilenameTranslator(Cryptor cryptor, Path vaultRoot) {
this.cryptor = cryptor;
this.dataRoot = vaultRoot.resolve("d");
this.metadataRoot = vaultRoot.resolve("m");
}
/* file and directory name en/decryption */
public String getDirectoryId(Path directoryFile, boolean createIfNonexisting) throws IOException {
try {
final Pair<Path, FileTime> key = ImmutablePair.of(directoryFile, Files.getLastModifiedTime(directoryFile));
String directoryId = directoryIdCache.get(key);
if (directoryId == null) {
directoryId = new String(readAllBytesAtomically(directoryFile), StandardCharsets.UTF_8);
directoryIdCache.put(key, directoryId);
}
return directoryId;
} catch (FileNotFoundException | NoSuchFileException e) {
if (createIfNonexisting) {
final String directoryId = UUID.randomUUID().toString();
writeAllBytesAtomically(directoryFile, directoryId.getBytes(StandardCharsets.UTF_8));
final Pair<Path, FileTime> key = ImmutablePair.of(directoryFile, Files.getLastModifiedTime(directoryFile));
directoryIdCache.put(key, directoryId);
return directoryId;
} else {
return null;
}
}
}
public Path getEncryptedDirectoryPath(String directoryId) {
final String encrypted = cryptor.encryptDirectoryPath(directoryId, FileSystems.getDefault().getSeparator());
return dataRoot.resolve(encrypted);
}
public String getEncryptedFilename(String cleartextFilename) throws IOException {
return getEncryptedFilename(cleartextFilename, FILE_EXT, LONG_FILE_EXT);
}
public String getEncryptedDirFileName(String cleartextDirName) throws IOException {
return getEncryptedFilename(cleartextDirName, DIR_EXT, LONG_DIR_EXT);
}
/**
* Encryption will blow up the filename length due to aes block sizes, IVs and base32 encoding. The result may be too long for some old file systems.<br/>
* This means that we need a workaround for filenames longer than the limit defined in {@link FileConstants#ENCRYPTED_FILENAME_LENGTH_LIMIT}.<br/>
* <br/>
* For filenames longer than this limit we use a metadata file containing the full encrypted paths. For the actual filename a unique alternative is created by concatenating the metadata filename
* and a unique id.
*/
private String getEncryptedFilename(String cleartextFilename, String basicExt, String longExt) throws IOException {
final String ivAndCiphertext = cryptor.encryptFilename(cleartextFilename);
if (ivAndCiphertext.length() + basicExt.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) {
final String metadataGroup = ivAndCiphertext.substring(0, LONG_NAME_PREFIX_LENGTH);
final LongFilenameMetadata metadata = readMetadata(metadataGroup);
final String longFilename = metadataGroup + metadata.getOrCreateUuidForEncryptedFilename(ivAndCiphertext).toString() + longExt;
this.writeMetadata(metadataGroup, metadata);
return longFilename;
} else {
return ivAndCiphertext + basicExt;
}
}
public String getCleartextFilename(String encryptedFilename) throws DecryptFailedException, IOException {
final String ciphertext;
if (StringUtils.endsWithIgnoreCase(encryptedFilename, LONG_FILE_EXT)) {
final String basename = StringUtils.removeEndIgnoreCase(encryptedFilename, LONG_FILE_EXT);
final String metadataGroup = basename.substring(0, LONG_NAME_PREFIX_LENGTH);
final String uuid = basename.substring(LONG_NAME_PREFIX_LENGTH);
final LongFilenameMetadata metadata = readMetadata(metadataGroup);
ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
} else if (StringUtils.endsWithIgnoreCase(encryptedFilename, FILE_EXT)) {
ciphertext = StringUtils.removeEndIgnoreCase(encryptedFilename, FILE_EXT);
} else if (StringUtils.endsWithIgnoreCase(encryptedFilename, LONG_DIR_EXT)) {
final String basename = StringUtils.removeEndIgnoreCase(encryptedFilename, LONG_DIR_EXT);
final String metadataGroup = basename.substring(0, LONG_NAME_PREFIX_LENGTH);
final String uuid = basename.substring(LONG_NAME_PREFIX_LENGTH);
final LongFilenameMetadata metadata = readMetadata(metadataGroup);
ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
} else if (StringUtils.endsWithIgnoreCase(encryptedFilename, DIR_EXT)) {
ciphertext = StringUtils.removeEndIgnoreCase(encryptedFilename, DIR_EXT);
} else {
throw new IllegalArgumentException("Unsupported path component: " + encryptedFilename);
}
return cryptor.decryptFilename(ciphertext);
}
/* Locked I/O */
private void writeAllBytesAtomically(Path path, byte[] bytes) throws IOException {
try (final FileChannel c = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
final SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, false)) {
c.write(ByteBuffer.wrap(bytes));
}
}
private byte[] readAllBytesAtomically(Path path) throws IOException {
try (final FileChannel c = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.DSYNC); final SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, true)) {
final ByteBuffer buffer = ByteBuffer.allocate((int) c.size());
c.read(buffer);
return buffer.array();
}
}
/* Long name metadata files */
private void writeMetadata(String metadataGroup, LongFilenameMetadata metadata) throws IOException {
final Path metadataDir = metadataRoot.resolve(metadataGroup.substring(0, 2));
Files.createDirectories(metadataDir);
final Path metadataFile = metadataDir.resolve(metadataGroup.substring(2));
// evict previously cached entries:
try {
final Pair<Path, FileTime> key = ImmutablePair.of(metadataFile, Files.getLastModifiedTime(metadataFile));
metadataCache.remove(key);
} catch (FileNotFoundException | NoSuchFileException e) {
// didn't exist yet? then we don't need to do anything anyway.
}
// write:
final byte[] metadataContent = objectMapper.writeValueAsBytes(metadata);
writeAllBytesAtomically(metadataFile, metadataContent);
// add to cache:
final Pair<Path, FileTime> key = ImmutablePair.of(metadataFile, Files.getLastModifiedTime(metadataFile));
metadataCache.put(key, metadata);
}
private LongFilenameMetadata readMetadata(String metadataGroup) throws IOException {
final Path metadataDir = metadataRoot.resolve(metadataGroup.substring(0, 2));
final Path metadataFile = metadataDir.resolve(metadataGroup.substring(2));
try {
// use cached metadata, if possible:
final Pair<Path, FileTime> key = ImmutablePair.of(metadataFile, Files.getLastModifiedTime(metadataFile));
LongFilenameMetadata metadata = metadataCache.get(key);
// else read from filesystem:
if (metadata == null) {
final byte[] metadataContent = readAllBytesAtomically(metadataFile);
metadata = objectMapper.readValue(metadataContent, LongFilenameMetadata.class);
metadataCache.put(key, metadata);
}
return metadata;
} catch (FileNotFoundException | NoSuchFileException e) {
// not yet existing:
return new LongFilenameMetadata();
}
}
private static class LongFilenameMetadata implements Serializable {
private static final long serialVersionUID = 6214509403824421320L;
@JsonDeserialize(as = DualHashBidiMap.class)
private BidiMap<UUID, String> encryptedFilenames = new DualHashBidiMap<>();
/* Getter/Setter */
public synchronized String getEncryptedFilenameForUUID(final UUID uuid) {
return encryptedFilenames.get(uuid);
}
public synchronized UUID getOrCreateUuidForEncryptedFilename(String encryptedFilename) {
UUID uuid = encryptedFilenames.getKey(encryptedFilename);
if (uuid == null) {
uuid = UUID.randomUUID();
encryptedFilenames.put(uuid, encryptedFilename);
}
return uuid;
}
// used by jackson
@SuppressWarnings("unused")
public BidiMap<UUID, String> getEncryptedFilenames() {
return encryptedFilenames;
}
// used by jackson
@SuppressWarnings("unused")
public void setEncryptedFilenames(BidiMap<UUID, String> encryptedFilenames) {
this.encryptedFilenames = encryptedFilenames;
}
}
}

View File

@@ -1,20 +0,0 @@
package org.cryptomator.webdav.jackrabbit;
import org.apache.jackrabbit.webdav.property.AbstractDavProperty;
import org.apache.jackrabbit.webdav.property.DavPropertyName;
class HttpHeaderProperty extends AbstractDavProperty<String> {
private final String value;
public HttpHeaderProperty(String key, String value) {
super(DavPropertyName.create(key), true);
this.value = value;
}
@Override
public String getValue() {
return value;
}
}

View File

@@ -1,104 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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
******************************************************************************/
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.nio.file.Path;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceIterator;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.io.InputContext;
import org.apache.jackrabbit.webdav.io.OutputContext;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.apache.jackrabbit.webdav.property.DavProperty;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.webdav.jackrabbit.CryptoResourceFactory.NonExistingParentException;
class NonExistingNode extends AbstractEncryptedNode {
public NonExistingNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
super(factory, locator, session, lockManager, cryptor, null);
}
@Override
public boolean exists() {
return false;
}
@Override
public boolean isCollection() {
return false;
}
@Override
public long getModificationTime() {
return -1;
}
@Override
public void spool(OutputContext outputContext) throws IOException {
throw new UnsupportedOperationException("Resource doesn't exist.");
}
@Override
public void addMember(DavResource resource, InputContext inputContext) throws DavException {
throw new UnsupportedOperationException("Resource doesn't exist.");
}
@Override
public DavResourceIterator getMembers() {
throw new UnsupportedOperationException("Resource doesn't exist.");
}
@Override
public void removeMember(DavResource member) throws DavException {
throw new UnsupportedOperationException("Resource doesn't exist.");
}
@Override
public void move(AbstractEncryptedNode destination) throws DavException {
throw new UnsupportedOperationException("Resource doesn't exist.");
}
@Override
public void copy(AbstractEncryptedNode destination, boolean shallow) throws DavException {
throw new UnsupportedOperationException("Resource doesn't exist.");
}
@Override
public void setProperty(DavProperty<?> property) throws DavException {
throw new UnsupportedOperationException("Resource doesn't exist.");
}
/**
* @return lazily resolved file path, e.g. needed during MOVE operations.
*/
public Path materializeFilePath() {
try {
return factory.getEncryptedFilePath(locator.getResourcePath(), true);
} catch (NonExistingParentException e) {
throw new IllegalStateException(e);
}
}
/**
* @return lazily resolved directory file path, e.g. needed during MOVE operations.
*/
public Path materializeDirFilePath() {
try {
return factory.getEncryptedDirectoryFilePath(locator.getResourcePath(), true);
} catch (NonExistingParentException e) {
throw new IllegalStateException(e);
}
}
}

View File

@@ -1,56 +0,0 @@
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.NonReadableChannelException;
import java.nio.channels.NonWritableChannelException;
import java.nio.channels.OverlappingFileLockException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Instances of this class wrap a file lock, that is created upon construction and destroyed by {@link #close()}.
*
* If the construction fails (e.g. if the file system does not support locks) no exception will be thrown and no lock is created.
*/
class SilentlyFailingFileLock implements AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(SilentlyFailingFileLock.class);
private final FileLock lock;
/**
* Invokes #SilentlyFailingFileLock(FileChannel, long, long, boolean) with a position of 0 and a size of {@link Long#MAX_VALUE}.
*/
SilentlyFailingFileLock(FileChannel channel, boolean shared) {
this(channel, 0L, Long.MAX_VALUE, shared);
}
/**
* @throws NonReadableChannelException If shared is true this channel was not opened for reading
* @throws NonWritableChannelException If shared is false but this channel was not opened for writing
* @see FileChannel#lock(long, long, boolean)
*/
SilentlyFailingFileLock(FileChannel channel, long position, long size, boolean shared) {
FileLock lock = null;
try {
lock = channel.tryLock(position, size, shared);
} catch (IOException | OverlappingFileLockException e) {
if (LOG.isDebugEnabled()) {
LOG.trace("Unable to lock file.");
}
} finally {
this.lock = lock;
}
}
@Override
public void close() throws IOException {
if (lock != null) {
lock.close();
}
}
}

View File

@@ -1,118 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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
******************************************************************************/
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.util.Collection;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavLocatorFactory;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceFactory;
import org.apache.jackrabbit.webdav.DavSessionProvider;
import org.apache.jackrabbit.webdav.WebdavRequest;
import org.apache.jackrabbit.webdav.WebdavResponse;
import org.apache.jackrabbit.webdav.server.AbstractWebdavServlet;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class WebDavServlet extends AbstractWebdavServlet {
private static final long serialVersionUID = 7965170007048673022L;
private static final Logger LOG = LoggerFactory.getLogger(WebDavServlet.class);
public static final String CFG_FS_ROOT = "cfg.fs.root";
private DavSessionProvider davSessionProvider;
private DavLocatorFactory davLocatorFactory;
private DavResourceFactory davResourceFactory;
private final Cryptor cryptor;
private final CryptoWarningHandler cryptoWarningHandler;
public WebDavServlet(final Cryptor cryptor, final Collection<String> failingMacCollection, final Collection<String> whitelistedResourceCollection) {
super();
this.cryptor = cryptor;
this.cryptoWarningHandler = new CryptoWarningHandler(failingMacCollection, whitelistedResourceCollection);
}
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
final String fsRoot = config.getInitParameter(CFG_FS_ROOT);
davSessionProvider = new DavSessionProviderImpl();
davLocatorFactory = new CleartextLocatorFactory(config.getServletContext().getContextPath());
davResourceFactory = new CryptoResourceFactory(cryptor, cryptoWarningHandler, fsRoot);
}
@Override
protected boolean isPreconditionValid(WebdavRequest request, DavResource resource) {
return !resource.exists() || request.matchesIfHeader(resource);
}
@Override
public DavSessionProvider getDavSessionProvider() {
return davSessionProvider;
}
@Override
public void setDavSessionProvider(DavSessionProvider davSessionProvider) {
this.davSessionProvider = davSessionProvider;
}
@Override
public DavLocatorFactory getLocatorFactory() {
return davLocatorFactory;
}
@Override
public void setLocatorFactory(DavLocatorFactory locatorFactory) {
this.davLocatorFactory = locatorFactory;
}
@Override
public DavResourceFactory getResourceFactory() {
return davResourceFactory;
}
@Override
public void setResourceFactory(DavResourceFactory resourceFactory) {
this.davResourceFactory = resourceFactory;
}
@Override
protected void doPut(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException {
long t0 = System.nanoTime();
super.doPut(request, response, resource);
if (LOG.isDebugEnabled()) {
long t1 = System.nanoTime();
LOG.trace("PUT TIME: " + (t1 - t0) / 1000 / 1000.0 + " ms");
}
}
@Override
protected void doGet(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException {
long t0 = System.nanoTime();
try {
super.doGet(request, response, resource);
} catch (MacAuthenticationFailedException e) {
LOG.warn("File integrity violation for " + resource.getLocator().getResourcePath());
cryptoWarningHandler.macAuthFailed(resource.getLocator().getResourcePath());
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
}
if (LOG.isDebugEnabled()) {
long t1 = System.nanoTime();
LOG.trace("GET TIME: " + (t1 - t0) / 1000 / 1000.0 + " ms");
}
}
}

View File

@@ -1,271 +0,0 @@
package org.cryptomator.webdav.jackrabbit;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.webdav.WebDavServer;
import org.cryptomator.webdav.WebDavServer.ServletLifeCycleAdapter;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.io.Files;
public class RangeRequestTest {
private static final Logger LOG = LoggerFactory.getLogger(RangeRequestTest.class);
private static final Aes256Cryptor CRYPTOR = new Aes256Cryptor();
private static final WebDavServer SERVER = new WebDavServer();
private static final File TMP_VAULT = Files.createTempDir();
private static ServletLifeCycleAdapter SERVLET;
private static URI VAULT_BASE_URI;
@BeforeClass
public static void startServer() throws URISyntaxException {
SERVER.start();
SERVLET = SERVER.createServlet(TMP_VAULT.toPath(), CRYPTOR, new ArrayList<String>(), new ArrayList<String>(), "JUnitTestVault");
SERVLET.start();
VAULT_BASE_URI = new URI("http", SERVLET.getServletUri().getSchemeSpecificPart() + "/", null);
Assert.assertTrue(SERVLET.isRunning());
Assert.assertNotNull(VAULT_BASE_URI);
}
@AfterClass
public static void stopServer() {
SERVLET.stop();
SERVER.stop();
FileUtils.deleteQuietly(TMP_VAULT);
}
@Test
public void testFullFileDecryption() throws IOException, URISyntaxException {
final URL testResourceUrl = new URL(VAULT_BASE_URI.toURL(), "fullFileDecryptionTestFile.txt");
final HttpClient client = new HttpClient();
// prepare 64MiB test data:
final byte[] plaintextData = new byte[16777216 * Integer.BYTES];
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);
for (int i = 0; i < 16777216; i++) {
bbIn.putInt(i);
}
final InputStream plaintextDataInputStream = new ByteArrayInputStream(plaintextData);
// put request:
final EntityEnclosingMethod putMethod = new PutMethod(testResourceUrl.toString());
putMethod.setRequestEntity(new ByteArrayRequestEntity(plaintextData));
final int putResponse = client.executeMethod(putMethod);
putMethod.releaseConnection();
Assert.assertEquals(201, putResponse);
// get request:
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
final int statusCode = client.executeMethod(getMethod);
Assert.assertEquals(200, statusCode);
// final byte[] received = new byte[plaintextData.length];
// IOUtils.read(getMethod.getResponseBodyAsStream(), received);
// Assert.assertArrayEquals(plaintextData, received);
Assert.assertTrue(IOUtils.contentEquals(plaintextDataInputStream, getMethod.getResponseBodyAsStream()));
getMethod.releaseConnection();
}
@Test
public void testAsyncRangeRequests() throws IOException, URISyntaxException, InterruptedException {
final URL testResourceUrl = new URL(VAULT_BASE_URI.toURL(), "asyncRangeRequestTestFile.txt");
final MultiThreadedHttpConnectionManager cm = new MultiThreadedHttpConnectionManager();
cm.getParams().setDefaultMaxConnectionsPerHost(50);
final HttpClient client = new HttpClient(cm);
// prepare 8MiB test data:
final byte[] plaintextData = new byte[2097152 * Integer.BYTES];
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);
for (int i = 0; i < 2097152; i++) {
bbIn.putInt(i);
}
// put request:
final EntityEnclosingMethod putMethod = new PutMethod(testResourceUrl.toString());
putMethod.setRequestEntity(new ByteArrayRequestEntity(plaintextData));
final int putResponse = client.executeMethod(putMethod);
putMethod.releaseConnection();
Assert.assertEquals(201, putResponse);
// multiple async range requests:
final List<ForkJoinTask<?>> tasks = new ArrayList<>();
final Random generator = new Random(System.currentTimeMillis());
final AtomicBoolean success = new AtomicBoolean(true);
// 10 full interrupted requests:
for (int i = 0; i < 10; i++) {
final ForkJoinTask<?> task = ForkJoinTask.adapt(() -> {
try {
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
final int statusCode = client.executeMethod(getMethod);
if (statusCode != 200) {
LOG.error("Invalid status code for interrupted full request");
success.set(false);
}
getMethod.getResponseBodyAsStream().read();
getMethod.getResponseBodyAsStream().close();
getMethod.releaseConnection();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
tasks.add(task);
}
// 50 crappy interrupted range requests:
for (int i = 0; i < 50; i++) {
final int lower = generator.nextInt(plaintextData.length);
final ForkJoinTask<?> task = ForkJoinTask.adapt(() -> {
try {
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
getMethod.addRequestHeader("Range", "bytes=" + lower + "-");
final int statusCode = client.executeMethod(getMethod);
if (statusCode != 206) {
LOG.error("Invalid status code for interrupted range request");
success.set(false);
}
getMethod.getResponseBodyAsStream().read();
getMethod.getResponseBodyAsStream().close();
getMethod.releaseConnection();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
tasks.add(task);
}
// 50 normal open range requests:
for (int i = 0; i < 50; i++) {
final int lower = generator.nextInt(plaintextData.length - 512);
final int upper = plaintextData.length - 1;
final ForkJoinTask<?> task = ForkJoinTask.adapt(() -> {
try {
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
getMethod.addRequestHeader("Range", "bytes=" + lower + "-");
final byte[] expected = Arrays.copyOfRange(plaintextData, lower, upper + 1);
final int statusCode = client.executeMethod(getMethod);
final byte[] responseBody = new byte[upper - lower + 10];
final int bytesRead = IOUtils.read(getMethod.getResponseBodyAsStream(), responseBody);
getMethod.releaseConnection();
if (statusCode != 206) {
LOG.error("Invalid status code for open range request");
success.set(false);
} else if (upper - lower + 1 != bytesRead) {
LOG.error("Invalid response length for open range request");
success.set(false);
} else if (!Arrays.equals(expected, Arrays.copyOfRange(responseBody, 0, bytesRead))) {
LOG.error("Invalid response body for open range request");
success.set(false);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
});
tasks.add(task);
}
// 200 normal closed range requests:
for (int i = 0; i < 200; i++) {
final int pos1 = generator.nextInt(plaintextData.length - 512);
final int pos2 = pos1 + 512;
final ForkJoinTask<?> task = ForkJoinTask.adapt(() -> {
try {
final int lower = Math.min(pos1, pos2);
final int upper = Math.max(pos1, pos2);
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
getMethod.addRequestHeader("Range", "bytes=" + lower + "-" + upper);
final byte[] expected = Arrays.copyOfRange(plaintextData, lower, upper + 1);
final int statusCode = client.executeMethod(getMethod);
final byte[] responseBody = new byte[upper - lower + 1];
final int bytesRead = IOUtils.read(getMethod.getResponseBodyAsStream(), responseBody);
getMethod.releaseConnection();
if (statusCode != 206) {
LOG.error("Invalid status code for closed range request");
success.set(false);
} else if (upper - lower + 1 != bytesRead) {
LOG.error("Invalid response length for closed range request");
success.set(false);
} else if (!Arrays.equals(expected, Arrays.copyOfRange(responseBody, 0, bytesRead))) {
LOG.error("Invalid response body for closed range request");
success.set(false);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
});
tasks.add(task);
}
Collections.shuffle(tasks, generator);
final ForkJoinPool pool = new ForkJoinPool(4);
for (ForkJoinTask<?> task : tasks) {
pool.execute(task);
}
for (ForkJoinTask<?> task : tasks) {
task.join();
}
pool.shutdown();
cm.shutdown();
Assert.assertTrue(success.get());
}
@Test
public void testUnsatisfiableRangeRequest() throws IOException, URISyntaxException {
final URL testResourceUrl = new URL(VAULT_BASE_URI.toURL(), "unsatisfiableRangeRequestTestFile.txt");
final HttpClient client = new HttpClient();
// prepare file content:
final byte[] fileContent = "This is some test file content.".getBytes();
// put request:
final EntityEnclosingMethod putMethod = new PutMethod(testResourceUrl.toString());
putMethod.setRequestEntity(new ByteArrayRequestEntity(fileContent));
final int putResponse = client.executeMethod(putMethod);
putMethod.releaseConnection();
Assert.assertEquals(201, putResponse);
// get request:
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
getMethod.addRequestHeader("Range", "chunks=1-2");
final int getResponse = client.executeMethod(getMethod);
final byte[] response = new byte[fileContent.length];
IOUtils.read(getMethod.getResponseBodyAsStream(), response);
getMethod.releaseConnection();
Assert.assertEquals(416, getResponse);
Assert.assertArrayEquals(fileContent, response);
}
}

View File

@@ -1,775 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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
******************************************************************************/
package org.cryptomator.crypto.aes256;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.security.auth.DestroyFailedException;
import javax.security.auth.Destroyable;
import org.bouncycastle.crypto.generators.SCrypt;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
private static final Logger LOG = LoggerFactory.getLogger(Aes256Cryptor.class);
/**
* Defined in static initializer. Defaults to 256, but falls back to maximum value possible, if JCE Unlimited Strength Jurisdiction Policy Files isn't installed. Those files can be downloaded
* here: http://www.oracle.com/technetwork/java/javase/downloads/.
*/
private static final int AES_KEY_LENGTH_IN_BITS;
/**
* PRNG for cryptographically secure random numbers. Defaults to SHA1-based number generator.
*
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#SecureRandom
*/
private final SecureRandom securePrng;
/**
* Jackson JSON-Mapper.
*/
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* The decrypted master key. Its lifecycle starts with the construction of an Aes256Cryptor instance or {@link #decryptMasterKey(InputStream, CharSequence)}. Its lifecycle ends with
* {@link #swipeSensitiveData()}.
*/
private SecretKey primaryMasterKey;
/**
* Decrypted secondary key used for hmac operations.
*/
private SecretKey hMacMasterKey;
static {
try {
final int maxKeyLength = Cipher.getMaxAllowedKeyLength(AES_KEY_ALGORITHM);
AES_KEY_LENGTH_IN_BITS = (maxKeyLength >= PREF_MASTER_KEY_LENGTH_IN_BITS) ? PREF_MASTER_KEY_LENGTH_IN_BITS : maxKeyLength;
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Algorithm should exist.", e);
}
}
/**
* Creates a new Cryptor with a newly initialized PRNG.
*/
public Aes256Cryptor() {
byte[] bytes = new byte[AES_KEY_LENGTH_IN_BITS / Byte.SIZE];
try {
securePrng = SecureRandom.getInstance(PRNG_ALGORITHM);
securePrng.setSeed(securePrng.generateSeed(PRNG_SEED_LENGTH));
securePrng.nextBytes(bytes);
this.primaryMasterKey = new SecretKeySpec(bytes, AES_KEY_ALGORITHM);
securePrng.nextBytes(bytes);
this.hMacMasterKey = new SecretKeySpec(bytes, HMAC_KEY_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("PRNG algorithm should exist.", e);
} finally {
Arrays.fill(bytes, (byte) 0);
}
}
/**
* Encrypts the current masterKey with the given password and writes the result to the given output stream.
*/
@Override
public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException {
try {
// derive key:
final byte[] kekSalt = randomData(SCRYPT_SALT_LENGTH);
final SecretKey kek = scrypt(password, kekSalt, SCRYPT_COST_PARAM, SCRYPT_BLOCK_SIZE, AES_KEY_LENGTH_IN_BITS);
// encrypt:
final Cipher encCipher = aesKeyWrapCipher(kek, Cipher.WRAP_MODE);
byte[] wrappedPrimaryKey = encCipher.wrap(primaryMasterKey);
byte[] wrappedSecondaryKey = encCipher.wrap(hMacMasterKey);
// save encrypted masterkey:
final KeyFile keyfile = new KeyFile();
keyfile.setVersion(KeyFile.CURRENT_VERSION);
keyfile.setScryptSalt(kekSalt);
keyfile.setScryptCostParam(SCRYPT_COST_PARAM);
keyfile.setScryptBlockSize(SCRYPT_BLOCK_SIZE);
keyfile.setKeyLength(AES_KEY_LENGTH_IN_BITS);
keyfile.setPrimaryMasterKey(wrappedPrimaryKey);
keyfile.setHMacMasterKey(wrappedSecondaryKey);
objectMapper.writeValue(out, keyfile);
} catch (InvalidKeyException | IllegalBlockSizeException ex) {
throw new IllegalStateException("Invalid hard coded configuration.", ex);
}
}
/**
* Reads the encrypted masterkey from the given input stream and decrypts it with the given password.
*
* @throws DecryptFailedException If the decryption failed for various reasons (including wrong password).
* @throws WrongPasswordException If the provided password was wrong. Note: Sometimes the algorithm itself fails due to a wrong password. In this case a DecryptFailedException will be thrown.
* @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In this case Java JCE needs to be installed.
* @throws UnsupportedVaultException If the masterkey file is too old or too modern.
*/
@Override
public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException, UnsupportedVaultException {
try {
// load encrypted masterkey:
final KeyFile keyfile = objectMapper.readValue(in, KeyFile.class);
// check version
if (keyfile.getVersion() != KeyFile.CURRENT_VERSION) {
throw new UnsupportedVaultException(keyfile.getVersion(), KeyFile.CURRENT_VERSION);
}
// check, whether the key length is supported:
final int maxKeyLen = Cipher.getMaxAllowedKeyLength(AES_KEY_ALGORITHM);
if (keyfile.getKeyLength() > maxKeyLen) {
throw new UnsupportedKeyLengthException(keyfile.getKeyLength(), maxKeyLen);
}
// derive key:
final SecretKey kek = scrypt(password, keyfile.getScryptSalt(), keyfile.getScryptCostParam(), keyfile.getScryptBlockSize(), keyfile.getKeyLength());
// decrypt and check password by catching AEAD exception
final Cipher decCipher = aesKeyWrapCipher(kek, Cipher.UNWRAP_MODE);
SecretKey primary = (SecretKey) decCipher.unwrap(keyfile.getPrimaryMasterKey(), AES_KEY_ALGORITHM, Cipher.SECRET_KEY);
SecretKey secondary = (SecretKey) decCipher.unwrap(keyfile.getHMacMasterKey(), HMAC_KEY_ALGORITHM, Cipher.SECRET_KEY);
// everything ok, assign decrypted keys:
this.primaryMasterKey = primary;
this.hMacMasterKey = secondary;
} catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException("Algorithm should exist.", ex);
} catch (InvalidKeyException e) {
throw new WrongPasswordException();
}
}
@Override
public boolean isDestroyed() {
return primaryMasterKey.isDestroyed() && hMacMasterKey.isDestroyed();
}
@Override
public void destroy() {
destroyQuietly(primaryMasterKey);
destroyQuietly(hMacMasterKey);
}
private void destroyQuietly(Destroyable d) {
try {
d.destroy();
} catch (DestroyFailedException e) {
// ignore
}
}
private Cipher aesKeyWrapCipher(SecretKey key, int cipherMode) {
try {
final Cipher cipher = Cipher.getInstance(AES_KEYWRAP_CIPHER);
cipher.init(cipherMode, key);
return cipher;
} catch (InvalidKeyException ex) {
throw new IllegalArgumentException("Invalid key.", ex);
} catch (NoSuchAlgorithmException | NoSuchPaddingException ex) {
throw new IllegalStateException("Algorithm/Padding should exist.", ex);
}
}
private Cipher aesCtrCipher(SecretKey key, byte[] iv, int cipherMode) {
try {
final Cipher cipher = Cipher.getInstance(AES_CTR_CIPHER);
cipher.init(cipherMode, key, new IvParameterSpec(iv));
return cipher;
} catch (InvalidKeyException ex) {
throw new IllegalArgumentException("Invalid key.", ex);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException ex) {
throw new IllegalStateException("Algorithm/Padding should exist and accept an IV.", ex);
}
}
private Cipher aesCbcCipher(SecretKey key, byte[] iv, int cipherMode) {
try {
final Cipher cipher = Cipher.getInstance(AES_CBC_CIPHER);
cipher.init(cipherMode, key, new IvParameterSpec(iv));
return cipher;
} catch (InvalidKeyException ex) {
throw new IllegalArgumentException("Invalid key.", ex);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException ex) {
throw new AssertionError("Every implementation of the Java platform is required to support AES/CBC/PKCS5Padding, which accepts an IV", ex);
}
}
private Mac hmacSha256(SecretKey key) {
try {
final Mac mac = Mac.getInstance(HMAC_KEY_ALGORITHM);
mac.init(key);
return mac;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError("Every implementation of the Java platform is required to support HmacSHA256.", e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException("Invalid key", e);
}
}
private MessageDigest sha256() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError("Every implementation of the Java platform is required to support Sha-256");
}
}
private byte[] randomData(int length) {
final byte[] result = new byte[length];
securePrng.nextBytes(result);
return result;
}
private SecretKey scrypt(CharSequence password, byte[] salt, int costParam, int blockSize, int keyLengthInBits) {
// use sb, as password.toString's implementation is unknown
final StringBuilder sb = new StringBuilder(password);
final byte[] pw = sb.toString().getBytes();
try {
final byte[] key = SCrypt.generate(pw, salt, costParam, blockSize, 1, keyLengthInBits / Byte.SIZE);
return new SecretKeySpec(key, AES_KEY_ALGORITHM);
} finally {
// destroy copied bytes of the plaintext password:
Arrays.fill(pw, (byte) 0);
for (int i = 0; i < password.length(); i++) {
sb.setCharAt(i, (char) 0);
}
}
}
@Override
public String encryptDirectoryPath(String cleartextDirectoryId, String nativePathSep) {
final byte[] cleartextBytes = cleartextDirectoryId.getBytes(StandardCharsets.UTF_8);
byte[] encryptedBytes = AesSivCipherUtil.sivEncrypt(primaryMasterKey, hMacMasterKey, cleartextBytes);
final byte[] hashed = sha256().digest(encryptedBytes);
final String encryptedThenHashedPath = ENCRYPTED_FILENAME_CODEC.encodeAsString(hashed);
return encryptedThenHashedPath.substring(0, 2) + nativePathSep + encryptedThenHashedPath.substring(2);
}
@Override
public String encryptFilename(String cleartextName) {
final byte[] cleartextBytes = cleartextName.getBytes(StandardCharsets.UTF_8);
final byte[] encryptedBytes = AesSivCipherUtil.sivEncrypt(primaryMasterKey, hMacMasterKey, cleartextBytes);
return ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes);
}
@Override
public String decryptFilename(String ciphertextName) throws DecryptFailedException {
final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(ciphertextName);
final byte[] cleartextBytes = AesSivCipherUtil.sivDecrypt(primaryMasterKey, hMacMasterKey, encryptedBytes);
return new String(cleartextBytes, StandardCharsets.UTF_8);
}
@Override
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException {
// read header:
encryptedFile.position(0);
final ByteBuffer headerBuf = ByteBuffer.allocate(104);
final int headerBytesRead = readFromChannel(encryptedFile, headerBuf);
if (headerBytesRead != headerBuf.capacity()) {
return null;
}
// read iv:
final byte[] iv = new byte[AES_BLOCK_LENGTH];
headerBuf.position(0);
headerBuf.get(iv);
// read sensitive header data:
final byte[] encryptedSensitiveHeaderContentBytes = new byte[48];
headerBuf.position(24);
headerBuf.get(encryptedSensitiveHeaderContentBytes);
// read stored header mac:
final byte[] storedHeaderMac = new byte[32];
headerBuf.position(72);
headerBuf.get(storedHeaderMac);
// calculate mac over first 72 bytes of header:
final Mac headerMac = this.hmacSha256(hMacMasterKey);
headerBuf.rewind();
headerBuf.limit(72);
headerMac.update(headerBuf);
final boolean macMatches = MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal());
if (!macMatches) {
throw new MacAuthenticationFailedException("MAC authentication failed.");
}
// decrypt sensitive header data:
final byte[] decryptedSensitiveHeaderContentBytes = decryptHeaderData(encryptedSensitiveHeaderContentBytes, iv);
final ByteBuffer sensitiveHeaderContentBuf = ByteBuffer.wrap(decryptedSensitiveHeaderContentBytes);
final Long fileSize = sensitiveHeaderContentBuf.getLong();
return fileSize;
}
private byte[] decryptHeaderData(byte[] ciphertextBytes, byte[] iv) {
try {
final Cipher sizeCipher = aesCbcCipher(primaryMasterKey, iv, Cipher.DECRYPT_MODE);
return sizeCipher.doFinal(ciphertextBytes);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new IllegalStateException(e);
}
}
private byte[] encryptHeaderData(byte[] plaintextBytes, byte[] iv) {
try {
final Cipher sizeCipher = aesCbcCipher(primaryMasterKey, iv, Cipher.ENCRYPT_MODE);
return sizeCipher.doFinal(plaintextBytes);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new IllegalStateException("Block size must be valid, as padding is requested. BadPaddingException not possible in encrypt mode.", e);
}
}
@Override
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException {
// read header:
encryptedFile.position(0l);
final ByteBuffer headerBuf = ByteBuffer.allocate(104);
final int headerBytesRead = readFromChannel(encryptedFile, headerBuf);
if (headerBytesRead != headerBuf.capacity()) {
throw new IOException("Failed to read file header.");
}
// read iv:
final byte[] iv = new byte[AES_BLOCK_LENGTH];
headerBuf.position(0);
headerBuf.get(iv);
// read nonce:
final byte[] nonce = new byte[8];
headerBuf.position(16);
headerBuf.get(nonce);
// read sensitive header data:
final byte[] encryptedSensitiveHeaderContentBytes = new byte[48];
headerBuf.position(24);
headerBuf.get(encryptedSensitiveHeaderContentBytes);
// read header mac:
final byte[] storedHeaderMac = new byte[32];
headerBuf.position(72);
headerBuf.get(storedHeaderMac);
// calculate mac over first 72 bytes of header:
if (authenticate) {
final Mac headerMac = this.hmacSha256(hMacMasterKey);
headerBuf.position(0);
headerBuf.limit(72);
headerMac.update(headerBuf);
if (!MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal())) {
throw new MacAuthenticationFailedException("Header MAC authentication failed.");
}
}
// decrypt sensitive header data:
final byte[] fileKeyBytes = new byte[32];
final byte[] decryptedSensitiveHeaderContentBytes = decryptHeaderData(encryptedSensitiveHeaderContentBytes, iv);
final ByteBuffer sensitiveHeaderContentBuf = ByteBuffer.wrap(decryptedSensitiveHeaderContentBytes);
final Long fileSize = sensitiveHeaderContentBuf.getLong();
sensitiveHeaderContentBuf.get(fileKeyBytes);
// prepare content decryption:
final SecretKey fileKey = new SecretKeySpec(fileKeyBytes, AES_KEY_ALGORITHM);
final LengthLimitingOutputStream paddingRemovingOutputStream = new LengthLimitingOutputStream(plaintextFile, fileSize);
final CryptoWorkerExecutor executor = new CryptoWorkerExecutor(Runtime.getRuntime().availableProcessors(), (lock, blockDone, currentBlock, inputQueue) -> {
return new DecryptWorker(lock, blockDone, currentBlock, inputQueue, authenticate, Channels.newChannel(paddingRemovingOutputStream)) {
@Override
protected Cipher initCipher(long startBlockNum) {
final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH);
nonceAndCounterBuf.put(nonce);
nonceAndCounterBuf.putLong(startBlockNum * CONTENT_MAC_BLOCK / AES_BLOCK_LENGTH);
final byte[] nonceAndCounter = nonceAndCounterBuf.array();
return aesCtrCipher(fileKey, nonceAndCounter, Cipher.DECRYPT_MODE);
}
@Override
protected Mac initMac() {
return hmacSha256(hMacMasterKey);
}
@Override
protected void checkMac(Mac mac, long blockNum, ByteBuffer ciphertextBuf, ByteBuffer macBuf) throws MacAuthenticationFailedException {
mac.update(iv);
mac.update(longToByteArray(blockNum));
mac.update(ciphertextBuf);
final byte[] calculatedMac = mac.doFinal();
final byte[] storedMac = new byte[mac.getMacLength()];
macBuf.get(storedMac);
if (!MessageDigest.isEqual(calculatedMac, storedMac)) {
throw new MacAuthenticationFailedException("Content MAC authentication failed.");
}
}
@Override
protected void decrypt(Cipher cipher, ByteBuffer ciphertextBuf, ByteBuffer plaintextBuf) throws DecryptFailedException {
assert plaintextBuf.remaining() >= cipher.getOutputSize(ciphertextBuf.remaining());
try {
cipher.update(ciphertextBuf, plaintextBuf);
} catch (ShortBufferException e) {
throw new DecryptFailedException(e);
}
}
};
});
// read as many blocks from file as possible, but wait if queue is full:
encryptedFile.position(104l);
final int maxNumBlocks = 64;
int numBlocks = 1;
int bytesRead = 0;
long blockNumber = 0;
do {
if (numBlocks < maxNumBlocks) {
numBlocks++;
}
final int inBufSize = numBlocks * (CONTENT_MAC_BLOCK + 32);
final ByteBuffer buf = ByteBuffer.allocate(inBufSize);
bytesRead = readFromChannel(encryptedFile, buf);
buf.flip();
final int blocksRead = (int) Math.ceil(bytesRead / (double) (CONTENT_MAC_BLOCK + 32));
final boolean consumedInTime = executor.offer(new BlocksData(buf.asReadOnlyBuffer(), blockNumber, blocksRead), 1, TimeUnit.SECONDS);
if (!consumedInTime) {
break;
}
blockNumber += numBlocks;
} while (bytesRead == numBlocks * (CONTENT_MAC_BLOCK + 32));
// wait for decryption workers to finish:
try {
executor.waitUntilDone();
} catch (ExecutionException e) {
final Throwable cause = e.getCause();
if (cause instanceof IOException) {
throw (IOException) cause;
} else if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else {
LOG.error("Unexpected exception", e);
}
} finally {
destroyQuietly(fileKey);
}
return paddingRemovingOutputStream.getBytesWritten();
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException {
// read header:
encryptedFile.position(0l);
final ByteBuffer headerBuf = ByteBuffer.allocate(104);
final int headerBytesRead = readFromChannel(encryptedFile, headerBuf);
if (headerBytesRead != headerBuf.capacity()) {
throw new IOException("Failed to read file header.");
}
// read iv:
final byte[] iv = new byte[AES_BLOCK_LENGTH];
headerBuf.position(0);
headerBuf.get(iv);
// read nonce:
final byte[] nonce = new byte[8];
headerBuf.position(16);
headerBuf.get(nonce);
// read sensitive header data:
final byte[] encryptedSensitiveHeaderContentBytes = new byte[48];
headerBuf.position(24);
headerBuf.get(encryptedSensitiveHeaderContentBytes);
// read header mac:
final byte[] storedHeaderMac = new byte[32];
headerBuf.position(72);
headerBuf.get(storedHeaderMac);
// calculate mac over first 72 bytes of header:
if (authenticate) {
final Mac headerMac = this.hmacSha256(hMacMasterKey);
headerBuf.position(0);
headerBuf.limit(72);
headerMac.update(headerBuf);
if (!MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal())) {
throw new MacAuthenticationFailedException("Header MAC authentication failed.");
}
}
// decrypt sensitive header data:
final byte[] fileKeyBytes = new byte[32];
final byte[] decryptedSensitiveHeaderContentBytes = decryptHeaderData(encryptedSensitiveHeaderContentBytes, iv);
final ByteBuffer sensitiveHeaderContentBuf = ByteBuffer.wrap(decryptedSensitiveHeaderContentBytes);
final Long fileSize = sensitiveHeaderContentBuf.getLong();
sensitiveHeaderContentBuf.get(fileKeyBytes);
assert pos + length - 1 < fileSize;
// find first relevant block:
final long startBlock = pos / CONTENT_MAC_BLOCK; // floor
final long startByte = startBlock * (CONTENT_MAC_BLOCK + 32) + 104;
final long offsetFromFirstBlock = pos - startBlock * CONTENT_MAC_BLOCK;
// append correct counter value to nonce:
final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH);
nonceAndCounterBuf.put(nonce);
nonceAndCounterBuf.putLong(startBlock * CONTENT_MAC_BLOCK / AES_BLOCK_LENGTH);
final byte[] nonceAndCounter = nonceAndCounterBuf.array();
// content decryption:
encryptedFile.position(startByte);
final SecretKey fileKey = new SecretKeySpec(fileKeyBytes, AES_KEY_ALGORITHM);
final Cipher cipher = this.aesCtrCipher(fileKey, nonceAndCounter, Cipher.DECRYPT_MODE);
final Mac contentMac = this.hmacSha256(hMacMasterKey);
try {
// reading ciphered input and MACs interleaved:
long bytesWritten = 0;
final ByteBuffer buf = ByteBuffer.allocate(CONTENT_MAC_BLOCK + 32);
int n = 0;
long blockNum = startBlock;
while ((n = readFromChannel(encryptedFile, buf)) > 0 && bytesWritten < length) {
if (n < 32) {
throw new DecryptFailedException("Invalid file content, missing MAC.");
}
buf.flip();
final ByteBuffer ciphertextBuf = buf.asReadOnlyBuffer();
ciphertextBuf.limit(n - 32);
// check MAC of current block:
if (authenticate) {
final byte[] storedMac = new byte[contentMac.getMacLength()];
final ByteBuffer storedMacBuf = buf.asReadOnlyBuffer();
storedMacBuf.position(n - 32);
storedMacBuf.get(storedMac);
contentMac.update(iv);
contentMac.update(longToByteArray(blockNum));
contentMac.update(ciphertextBuf);
ciphertextBuf.rewind();
final byte[] calculatedMac = contentMac.doFinal();
if (!MessageDigest.isEqual(calculatedMac, storedMac)) {
throw new MacAuthenticationFailedException("Content MAC authentication failed.");
}
}
// decrypt block:
final ByteBuffer plaintextBuf = ByteBuffer.allocate(cipher.getOutputSize(ciphertextBuf.remaining()));
cipher.update(ciphertextBuf, plaintextBuf);
plaintextBuf.flip();
final int offset = (bytesWritten == 0) ? (int) offsetFromFirstBlock : 0;
final long pending = length - bytesWritten;
final int available = plaintextBuf.remaining() - offset;
final int currentBatch = (int) Math.min(pending, available);
plaintextFile.write(plaintextBuf.array(), offset, currentBatch);
bytesWritten += currentBatch;
blockNum++;
buf.rewind();
}
return bytesWritten;
} catch (ShortBufferException e) {
throw new IllegalStateException("Output buffer size known to fit.", e);
} finally {
destroyQuietly(fileKey);
}
}
/**
* header = {16 byte iv, 8 byte nonce, 48 byte sensitive header data (file size + file key + padding), 32 byte headerMac}
*/
@Override
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
// truncate file
encryptedFile.truncate(0l);
// choose a random header IV:
final byte[] iv = randomData(AES_BLOCK_LENGTH);
// chosse 8 byte random nonce and 8 byte counter set to zero:
final byte[] nonce = randomData(8);
// choose a random content key:
final byte[] fileKeyBytes = randomData(32);
// 104 byte header buffer (16 header IV, 8 content nonce, 48 sensitive header data, 32 headerMac), filled after writing the content
final ByteBuffer headerBuf = ByteBuffer.allocate(104);
headerBuf.limit(104);
encryptedFile.write(headerBuf);
// prepare content encryption:
final SecretKey fileKey = new SecretKeySpec(fileKeyBytes, AES_KEY_ALGORITHM);
final CryptoWorkerExecutor executor = new CryptoWorkerExecutor(Runtime.getRuntime().availableProcessors(), (lock, blockDone, currentBlock, inputQueue) -> {
return new EncryptWorker(lock, blockDone, currentBlock, inputQueue, encryptedFile) {
@Override
protected Cipher initCipher(long startBlockNum) {
final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH);
nonceAndCounterBuf.put(nonce);
nonceAndCounterBuf.putLong(startBlockNum * CONTENT_MAC_BLOCK / AES_BLOCK_LENGTH);
final byte[] nonceAndCounter = nonceAndCounterBuf.array();
return aesCtrCipher(fileKey, nonceAndCounter, Cipher.ENCRYPT_MODE);
}
@Override
protected Mac initMac() {
return hmacSha256(hMacMasterKey);
}
@Override
protected byte[] calcMac(Mac mac, long blockNum, ByteBuffer ciphertextBuf) {
mac.update(iv);
mac.update(longToByteArray(blockNum));
mac.update(ciphertextBuf);
return mac.doFinal();
}
@Override
protected void encrypt(Cipher cipher, ByteBuffer plaintextBuf, ByteBuffer ciphertextBuf) throws EncryptFailedException {
try {
assert ciphertextBuf.remaining() >= cipher.getOutputSize(plaintextBuf.remaining());
cipher.update(plaintextBuf, ciphertextBuf);
} catch (ShortBufferException e) {
throw new EncryptFailedException(e);
}
}
};
});
// read as many blocks from file as possible, but wait if queue is full:
final byte[] randomPadding = this.randomData(AES_BLOCK_LENGTH);
final LengthObfuscatingInputStream in = new LengthObfuscatingInputStream(plaintextFile, randomPadding);
final ReadableByteChannel channel = Channels.newChannel(in);
int bytesRead = 0;
long blockNumber = 0;
final int maxNumBlocks = 64;
int numBlocks = 0;
do {
if (numBlocks < maxNumBlocks) {
numBlocks++;
}
final int inBufSize = numBlocks * CONTENT_MAC_BLOCK;
final ByteBuffer inBuf = ByteBuffer.allocate(inBufSize);
bytesRead = readFromChannel(channel, inBuf);
inBuf.flip();
final int blocksRead = (int) Math.ceil(bytesRead / (double) CONTENT_MAC_BLOCK);
final boolean consumedInTime = executor.offer(new BlocksData(inBuf.asReadOnlyBuffer(), blockNumber, blocksRead), 1, TimeUnit.SECONDS);
if (!consumedInTime) {
break;
}
blockNumber += numBlocks;
} while (bytesRead == numBlocks * CONTENT_MAC_BLOCK);
// wait for encryption workers to finish:
try {
executor.waitUntilDone();
} catch (ExecutionException e) {
final Throwable cause = e.getCause();
if (cause instanceof IOException) {
throw (IOException) cause;
} else if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else {
LOG.error("Unexpected exception", e);
}
} finally {
destroyQuietly(fileKey);
}
// create and write header:
final long plaintextSize = in.getRealInputLength();
final ByteBuffer sensitiveHeaderContentBuf = ByteBuffer.allocate(Long.BYTES + fileKeyBytes.length);
sensitiveHeaderContentBuf.putLong(plaintextSize);
sensitiveHeaderContentBuf.put(fileKeyBytes);
headerBuf.clear();
headerBuf.put(iv);
headerBuf.put(nonce);
headerBuf.put(encryptHeaderData(sensitiveHeaderContentBuf.array(), iv));
headerBuf.flip();
final Mac headerMac = this.hmacSha256(hMacMasterKey);
headerMac.update(headerBuf);
headerBuf.limit(104);
headerBuf.put(headerMac.doFinal());
headerBuf.flip();
encryptedFile.position(0);
encryptedFile.write(headerBuf);
return plaintextSize;
}
private byte[] longToByteArray(long lng) {
return ByteBuffer.allocate(Long.SIZE / Byte.SIZE).putLong(lng).array();
}
/**
* Reads bytes from a ReadableByteChannel.
* <p>
* This implementation guarantees that it will read as many bytes
* as possible before giving up; this may not always be the case for
* subclasses of {@link ReadableByteChannel}.
*
* @param input the byte channel to read
* @param buffer byte buffer destination
* @return the actual length read; may be less than requested if EOF was reached
* @throws IOException if a read error occurs
* @see
* <a href="http://commons.apache.org/proper/commons-io/apidocs/src-html/org/apache/commons/io/IOUtils.html">Apache Commons IOUtils 2.5</a>
*/
public static int readFromChannel(final ReadableByteChannel input, final ByteBuffer buffer) throws IOException {
final int length = buffer.remaining();
while (buffer.remaining() > 0) {
final int count = input.read(buffer);
if (count == -1) { // EOF
break;
}
}
return length - buffer.remaining();
}
}

View File

@@ -1,94 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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
******************************************************************************/
package org.cryptomator.crypto.aes256;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.BaseNCodec;
interface AesCryptographicConfiguration {
/**
* Number of bytes used as salt, where needed.
*/
int SCRYPT_SALT_LENGTH = 8;
/**
* Scrypt CPU/Memory cost parameter.
*/
int SCRYPT_COST_PARAM = 1 << 14;
/**
* Scrypt block size (affects memory consumption)
*/
int SCRYPT_BLOCK_SIZE = 8;
/**
* Preferred number of bytes of the master key.
*/
int PREF_MASTER_KEY_LENGTH_IN_BITS = 256;
/**
* Number of bytes used as seed for the PRNG.
*/
int PRNG_SEED_LENGTH = 16;
/**
* Algorithm used for random number generation.
*/
String PRNG_ALGORITHM = "SHA1PRNG";
/**
* Algorithm used for en/decryption.
*
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#AlgorithmParameters
*/
String AES_KEY_ALGORITHM = "AES";
/**
* Key algorithm for keyed MAC.
*/
String HMAC_KEY_ALGORITHM = "HmacSHA256";
/**
* Cipher specs for RFC 3394 masterkey encryption.
*
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher
*/
String AES_KEYWRAP_CIPHER = "AESWrap";
/**
* Cipher specs for file content encryption. Using CTR-mode for random access.<br/>
*
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher
*/
String AES_CTR_CIPHER = "AES/CTR/NoPadding";
/**
* Cipher specs for file header encryption (fixed-length block cipher).<br/>
*
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#impl
*/
String AES_CBC_CIPHER = "AES/CBC/PKCS5Padding";
/**
* AES block size is 128 bit or 16 bytes.
*/
int AES_BLOCK_LENGTH = 16;
/**
* Number of bytes, a content block over which a MAC is calculated consists of.
*/
int CONTENT_MAC_BLOCK = 32 * 1024;
/**
* How to encode the encrypted file names safely. Base32 uses only alphanumeric characters and is case-insensitive.
*/
BaseNCodec ENCRYPTED_FILENAME_CODEC = new Base32();
}

View File

@@ -1,230 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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
******************************************************************************/
package org.cryptomator.crypto.aes256;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.util.Arrays;
import javax.crypto.SecretKey;
import org.apache.commons.lang3.ArrayUtils;
import org.bouncycastle.crypto.BlockCipher;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.Mac;
import org.bouncycastle.crypto.engines.AESFastEngine;
import org.bouncycastle.crypto.macs.CMac;
import org.bouncycastle.crypto.paddings.ISO7816d4Padding;
import org.bouncycastle.crypto.params.KeyParameter;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
/**
* Implements the RFC 5297 SIV mode.
*/
final class AesSivCipherUtil {
private static final byte[] BYTES_ZERO = new byte[16];
private static final byte DOUBLING_CONST = (byte) 0x87;
static byte[] sivEncrypt(SecretKey aesKey, SecretKey macKey, byte[] plaintext, byte[]... additionalData) {
final byte[] aesKeyBytes = aesKey.getEncoded();
final byte[] macKeyBytes = macKey.getEncoded();
if (aesKeyBytes == null || macKeyBytes == null) {
throw new IllegalArgumentException("Can't get bytes of given key.");
}
try {
return sivEncrypt(aesKeyBytes, macKeyBytes, plaintext, additionalData);
} catch (InvalidKeyException ex) {
throw new IllegalArgumentException(ex);
} finally {
Arrays.fill(aesKeyBytes, (byte) 0);
Arrays.fill(macKeyBytes, (byte) 0);
}
}
static byte[] sivEncrypt(byte[] aesKey, byte[] macKey, byte[] plaintext, byte[]... additionalData) throws InvalidKeyException {
if (aesKey.length != 16 && aesKey.length != 24 && aesKey.length != 32) {
throw new InvalidKeyException("Invalid aesKey length " + aesKey.length);
}
final byte[] iv = s2v(macKey, plaintext, additionalData);
final int numBlocks = (plaintext.length + 15) / 16;
// clear out the 31st and 63rd (rightmost) bit:
final byte[] ctr = Arrays.copyOf(iv, 16);
ctr[8] = (byte) (ctr[8] & 0x7F);
ctr[12] = (byte) (ctr[12] & 0x7F);
final ByteBuffer ctrBuf = ByteBuffer.wrap(ctr);
final long initialCtrVal = ctrBuf.getLong(8);
final byte[] x = new byte[numBlocks * 16];
final BlockCipher aes = new AESFastEngine();
aes.init(true, new KeyParameter(aesKey));
for (int i = 0; i < numBlocks; i++) {
final long ctrVal = initialCtrVal + i;
ctrBuf.putLong(8, ctrVal);
aes.processBlock(ctrBuf.array(), 0, x, i * 16);
aes.reset();
}
final byte[] ciphertext = xor(plaintext, x);
return ArrayUtils.addAll(iv, ciphertext);
}
static byte[] sivDecrypt(SecretKey aesKey, SecretKey macKey, byte[] plaintext, byte[]... additionalData) throws DecryptFailedException {
final byte[] aesKeyBytes = aesKey.getEncoded();
final byte[] macKeyBytes = macKey.getEncoded();
if (aesKeyBytes == null || macKeyBytes == null) {
throw new IllegalArgumentException("Can't get bytes of given key.");
}
try {
return sivDecrypt(aesKeyBytes, macKeyBytes, plaintext, additionalData);
} catch (InvalidKeyException ex) {
throw new IllegalArgumentException(ex);
} finally {
Arrays.fill(aesKeyBytes, (byte) 0);
Arrays.fill(macKeyBytes, (byte) 0);
}
}
static byte[] sivDecrypt(byte[] aesKey, byte[] macKey, byte[] ciphertext, byte[]... additionalData) throws DecryptFailedException, InvalidKeyException {
if (aesKey.length != 16 && aesKey.length != 24 && aesKey.length != 32) {
throw new InvalidKeyException("Invalid aesKey length " + aesKey.length);
}
final byte[] iv = Arrays.copyOf(ciphertext, 16);
final byte[] actualCiphertext = Arrays.copyOfRange(ciphertext, 16, ciphertext.length);
final int numBlocks = (actualCiphertext.length + 15) / 16;
// clear out the 31st and 63rd (rightmost) bit:
final byte[] ctr = Arrays.copyOf(iv, 16);
ctr[8] = (byte) (ctr[8] & 0x7F);
ctr[12] = (byte) (ctr[12] & 0x7F);
final ByteBuffer ctrBuf = ByteBuffer.wrap(ctr);
final long initialCtrVal = ctrBuf.getLong(8);
final byte[] x = new byte[numBlocks * 16];
final BlockCipher aes = new AESFastEngine();
aes.init(true, new KeyParameter(aesKey));
for (int i = 0; i < numBlocks; i++) {
final long ctrVal = initialCtrVal + i;
ctrBuf.putLong(8, ctrVal);
aes.processBlock(ctrBuf.array(), 0, x, i * 16);
aes.reset();
}
final byte[] plaintext = xor(actualCiphertext, x);
final byte[] control = s2v(macKey, plaintext, additionalData);
if (MessageDigest.isEqual(control, iv)) {
return plaintext;
} else {
throw new DecryptFailedException("Authentication failed");
}
}
static byte[] s2v(byte[] macKey, byte[] plaintext, byte[]... additionalData) {
final CipherParameters params = new KeyParameter(macKey);
final BlockCipher aes = new AESFastEngine();
final CMac mac = new CMac(aes);
mac.init(params);
byte[] d = mac(mac, BYTES_ZERO);
for (byte[] s : additionalData) {
d = xor(dbl(d), mac(mac, s));
}
final byte[] t;
if (plaintext.length >= 16) {
t = xorend(plaintext, d);
} else {
t = xor(dbl(d), pad(plaintext));
}
return mac(mac, t);
}
private static byte[] mac(Mac mac, byte[] in) {
byte[] result = new byte[mac.getMacSize()];
mac.update(in, 0, in.length);
mac.doFinal(result, 0);
return result;
}
/**
* First bit 1, following bits 0.
*/
private static byte[] pad(byte[] in) {
final byte[] result = Arrays.copyOf(in, 16);
new ISO7816d4Padding().addPadding(result, in.length);
return result;
}
/**
* Code taken from {@link org.bouncycastle.crypto.macs.CMac}
*/
private static int shiftLeft(byte[] block, byte[] output) {
int i = block.length;
int bit = 0;
while (--i >= 0) {
int b = block[i] & 0xff;
output[i] = (byte) ((b << 1) | bit);
bit = (b >>> 7) & 1;
}
return bit;
}
/**
* Code taken from {@link org.bouncycastle.crypto.macs.CMac}
*/
private static byte[] dbl(byte[] in) {
byte[] ret = new byte[in.length];
int carry = shiftLeft(in, ret);
int xor = 0xff & DOUBLING_CONST;
/*
* NOTE: This construction is an attempt at a constant-time implementation.
*/
ret[in.length - 1] ^= (xor >>> ((1 - carry) << 3));
return ret;
}
private static byte[] xor(byte[] in1, byte[] in2) {
if (in1 == null || in2 == null || in1.length > in2.length) {
throw new IllegalArgumentException("Length of first input must be <= length of second input.");
}
final byte[] result = new byte[in1.length];
for (int i = 0; i < result.length; i++) {
result[i] = (byte) (in1[i] ^ in2[i]);
}
return result;
}
private static byte[] xorend(byte[] in1, byte[] in2) {
if (in1 == null || in2 == null || in1.length < in2.length) {
throw new IllegalArgumentException("Length of first input must be >= length of second input.");
}
final byte[] result = Arrays.copyOf(in1, in1.length);
final int diff = in1.length - in2.length;
for (int i = 0; i < in2.length; i++) {
result[i + diff] = (byte) (result[i + diff] ^ in2[i]);
}
return result;
}
}

View File

@@ -1,22 +0,0 @@
package org.cryptomator.crypto.aes256;
import java.nio.ByteBuffer;
class BlocksData {
public static final int MAX_NUM_BLOCKS = 128;
final ByteBuffer buffer;
final long startBlockNum;
final int numBlocks;
BlocksData(ByteBuffer buffer, long startBlockNum, int numBlocks) {
if (numBlocks > MAX_NUM_BLOCKS) {
throw new IllegalArgumentException("Too many blocks to process at once: " + numBlocks);
}
this.buffer = buffer;
this.startBlockNum = startBlockNum;
this.numBlocks = numBlocks;
}
}

View File

@@ -1,64 +0,0 @@
package org.cryptomator.crypto.aes256;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import org.cryptomator.crypto.exceptions.CryptingException;
abstract class CryptoWorker implements Callable<Void> {
static final BlocksData POISON = new BlocksData(ByteBuffer.allocate(0), -1L, 0);
final Lock lock;
final Condition blockDone;
final AtomicLong currentBlock;
final BlockingQueue<BlocksData> queue;
public CryptoWorker(Lock lock, Condition blockDone, AtomicLong currentBlock, BlockingQueue<BlocksData> queue) {
this.lock = lock;
this.blockDone = blockDone;
this.currentBlock = currentBlock;
this.queue = queue;
}
@Override
public final Void call() throws IOException {
try {
while (!Thread.currentThread().isInterrupted()) {
final BlocksData blocksData = queue.take();
if (blocksData == POISON) {
// put poison back in for other threads:
break;
}
final ByteBuffer processedBytes = this.process(blocksData);
lock.lock();
try {
while (currentBlock.get() != blocksData.startBlockNum) {
blockDone.await();
}
assert currentBlock.get() == blocksData.startBlockNum;
// yay, its my turn!
this.write(processedBytes);
// signal worker working on next block:
currentBlock.set(blocksData.startBlockNum + blocksData.numBlocks);
blockDone.signalAll();
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return null;
}
protected abstract ByteBuffer process(BlocksData block) throws CryptingException;
protected abstract void write(ByteBuffer processedBytes) throws IOException;
}

View File

@@ -1,112 +0,0 @@
package org.cryptomator.crypto.aes256;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class CryptoWorkerExecutor {
private static final Logger LOG = LoggerFactory.getLogger(CryptoWorkerExecutor.class);
private final int numWorkers;
private final Lock lock;
private final Condition blockDone;
private final AtomicLong currentBlock;
private final BlockingQueue<BlocksData> inputQueue;
private final ExecutorService executorService;
private final CompletionService<Void> completionService;
private boolean acceptWork;
/**
* Starts as many {@link CryptoWorker} as specified in the constructor, that start working immediately on the items submitted via {@link #offer(BlocksData, long, TimeUnit)}.
*/
public CryptoWorkerExecutor(int numWorkers, WorkerFactory workerFactory) {
this.numWorkers = numWorkers;
this.lock = new ReentrantLock();
this.blockDone = lock.newCondition();
this.currentBlock = new AtomicLong();
this.inputQueue = new LinkedBlockingQueue<>(numWorkers * 2); // one cycle read-ahead
this.executorService = Executors.newFixedThreadPool(numWorkers);
this.completionService = new ExecutorCompletionService<>(executorService);
this.acceptWork = true;
// start workers:
for (int i = 0; i < numWorkers; i++) {
final CryptoWorker worker = workerFactory.createWorker(lock, blockDone, currentBlock, inputQueue);
completionService.submit(worker);
}
}
/**
* Adds work to the work queue. On timeout all workers will be shut down.
*
* @see BlockingQueue#offer(Object, long, TimeUnit)
* @return <code>true</code> if the work has been added in time. <code>false</code> in any other case.
*/
public boolean offer(BlocksData data, long timeout, TimeUnit unit) {
if (!acceptWork) {
return false;
}
try {
final boolean success = inputQueue.offer(data, timeout, unit);
if (!success) {
this.acceptWork = false;
inputQueue.clear();
poisonWorkers();
}
return success;
} catch (InterruptedException e) {
LOG.error("Interrupted thread.", e);
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
return false;
}
/**
* Graceful shutdown of this executor, waiting for all jobs to finish (normally or by throwing exceptions).
*
* @throws ExecutionException If any of the workers failed.
*/
public void waitUntilDone() throws ExecutionException {
this.acceptWork = false;
try {
poisonWorkers();
// now workers will one after another finish their work, potentially throwing an ExecutionException:
for (int i = 0; i < numWorkers; i++) {
completionService.take().get();
}
} catch (InterruptedException e) {
LOG.error("Interrupted thread.", e);
Thread.currentThread().interrupt();
} finally {
// shutdown either after normal decryption or if ANY worker threw an exception:
executorService.shutdownNow();
}
}
private void poisonWorkers() throws InterruptedException {
// add enough poison for each worker:
for (int i = 0; i < numWorkers; i++) {
inputQueue.put(CryptoWorker.POISON);
}
}
@FunctionalInterface
interface WorkerFactory {
CryptoWorker createWorker(Lock lock, Condition blockDone, AtomicLong currentBlock, BlockingQueue<BlocksData> inputQueue);
}
}

View File

@@ -1,75 +0,0 @@
package org.cryptomator.crypto.aes256;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import org.cryptomator.crypto.exceptions.CryptingException;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
abstract class DecryptWorker extends CryptoWorker implements AesCryptographicConfiguration {
private final boolean shouldAuthenticate;
private final WritableByteChannel out;
public DecryptWorker(Lock lock, Condition blockDone, AtomicLong currentBlock, BlockingQueue<BlocksData> queue, boolean shouldAuthenticate, WritableByteChannel out) {
super(lock, blockDone, currentBlock, queue);
this.shouldAuthenticate = shouldAuthenticate;
this.out = out;
}
@Override
protected ByteBuffer process(BlocksData data) throws CryptingException {
final Cipher cipher = initCipher(data.startBlockNum);
final Mac mac = initMac();
final ByteBuffer plaintextBuf = ByteBuffer.allocate(cipher.getOutputSize(CONTENT_MAC_BLOCK) * data.numBlocks);
final ByteBuffer ciphertextBuf = data.buffer.asReadOnlyBuffer();
final ByteBuffer macBuf = data.buffer.asReadOnlyBuffer();
for (long blockNum = data.startBlockNum; blockNum < data.startBlockNum + data.numBlocks; blockNum++) {
assert (blockNum - data.startBlockNum) < BlocksData.MAX_NUM_BLOCKS;
assert (blockNum - data.startBlockNum) * CONTENT_MAC_BLOCK < Integer.MAX_VALUE;
final int pos = (int) (blockNum - data.startBlockNum) * (CONTENT_MAC_BLOCK + mac.getMacLength());
ciphertextBuf.limit(Math.min(data.buffer.limit() - mac.getMacLength(), pos + CONTENT_MAC_BLOCK));
ciphertextBuf.position(pos);
try {
macBuf.limit(ciphertextBuf.limit() + mac.getMacLength());
macBuf.position(ciphertextBuf.limit());
} catch (IllegalArgumentException e) {
throw new DecryptFailedException("Invalid file content, missing MAC.");
}
if (shouldAuthenticate) {
checkMac(mac, blockNum, ciphertextBuf, macBuf);
}
ciphertextBuf.position(pos);
decrypt(cipher, ciphertextBuf, plaintextBuf);
}
plaintextBuf.flip();
return plaintextBuf;
}
@Override
protected void write(ByteBuffer processedBytes) throws IOException {
out.write(processedBytes);
}
protected abstract Cipher initCipher(long startBlockNum);
protected abstract Mac initMac();
protected abstract void checkMac(Mac mac, long blockNum, ByteBuffer ciphertextBuf, ByteBuffer macBuf) throws MacAuthenticationFailedException;
protected abstract void decrypt(Cipher cipher, ByteBuffer ciphertextBuf, ByteBuffer plaintextBuf) throws DecryptFailedException;
}

View File

@@ -1,61 +0,0 @@
package org.cryptomator.crypto.aes256;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import org.cryptomator.crypto.exceptions.CryptingException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
abstract class EncryptWorker extends CryptoWorker implements AesCryptographicConfiguration {
private final WritableByteChannel out;
public EncryptWorker(Lock lock, Condition blockDone, AtomicLong currentBlock, BlockingQueue<BlocksData> queue, WritableByteChannel out) {
super(lock, blockDone, currentBlock, queue);
this.out = out;
}
@Override
protected ByteBuffer process(BlocksData data) throws CryptingException {
final Cipher cipher = initCipher(data.startBlockNum);
final Mac mac = initMac();
final ByteBuffer ciphertextBuf = ByteBuffer.allocate((cipher.getOutputSize(CONTENT_MAC_BLOCK) + mac.getMacLength()) * data.numBlocks);
final ByteBuffer plaintextBuf = data.buffer.asReadOnlyBuffer();
for (long blockNum = data.startBlockNum; blockNum < data.startBlockNum + data.numBlocks; blockNum++) {
final int pos = (int) (blockNum - data.startBlockNum) * CONTENT_MAC_BLOCK;
plaintextBuf.limit(Math.min(data.buffer.limit(), pos + CONTENT_MAC_BLOCK));
encrypt(cipher, plaintextBuf, ciphertextBuf);
final ByteBuffer toMac = ciphertextBuf.asReadOnlyBuffer();
toMac.limit(ciphertextBuf.position());
toMac.position((int) (blockNum - data.startBlockNum) * (CONTENT_MAC_BLOCK + mac.getMacLength()));
ciphertextBuf.put(calcMac(mac, blockNum, toMac));
}
ciphertextBuf.flip();
return ciphertextBuf;
}
@Override
protected void write(ByteBuffer processedBytes) throws IOException {
out.write(processedBytes);
}
protected abstract Cipher initCipher(long startBlockNum);
protected abstract Mac initMac();
protected abstract byte[] calcMac(Mac mac, long blockNum, ByteBuffer ciphertextBuf);
protected abstract void encrypt(Cipher cipher, ByteBuffer plaintextBuf, ByteBuffer ciphertextBuf) throws EncryptFailedException;
}

View File

@@ -1,77 +0,0 @@
package org.cryptomator.crypto.aes256;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
@JsonPropertyOrder(value = {"version", "scryptSalt", "scryptCostParam", "scryptBlockSize", "keyLength", "primaryMasterKey", "hMacMasterKey"})
public class KeyFile implements Serializable {
static final Integer CURRENT_VERSION = 2;
private static final long serialVersionUID = 8578363158959619885L;
private Integer version;
private byte[] scryptSalt;
private int scryptCostParam;
private int scryptBlockSize;
private int keyLength;
private byte[] primaryMasterKey;
private byte[] hMacMasterKey;
public Integer getVersion() {
return version;
}
public void setVersion(Integer version) {
this.version = version;
}
public byte[] getScryptSalt() {
return scryptSalt;
}
public void setScryptSalt(byte[] scryptSalt) {
this.scryptSalt = scryptSalt;
}
public int getScryptCostParam() {
return scryptCostParam;
}
public void setScryptCostParam(int scryptCostParam) {
this.scryptCostParam = scryptCostParam;
}
public int getScryptBlockSize() {
return scryptBlockSize;
}
public void setScryptBlockSize(int scryptBlockSize) {
this.scryptBlockSize = scryptBlockSize;
}
public int getKeyLength() {
return keyLength;
}
public void setKeyLength(int keyLength) {
this.keyLength = keyLength;
}
public byte[] getPrimaryMasterKey() {
return primaryMasterKey;
}
public void setPrimaryMasterKey(byte[] primaryMasterKey) {
this.primaryMasterKey = primaryMasterKey;
}
public byte[] getHMacMasterKey() {
return hMacMasterKey;
}
public void setHMacMasterKey(byte[] hMacMasterKey) {
this.hMacMasterKey = hMacMasterKey;
}
}

View File

@@ -1,47 +0,0 @@
package org.cryptomator.crypto.aes256;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class LengthLimitingOutputStream extends FilterOutputStream {
private final long limit;
private volatile long bytesWritten;
public LengthLimitingOutputStream(OutputStream out, long limit) {
super(out);
this.limit = limit;
this.bytesWritten = 0;
}
@Override
public void write(int b) throws IOException {
if (bytesWritten < limit) {
out.write(b);
increaseNumberOfWrittenBytes(1);
}
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
final long bytesAvailable = limit - bytesWritten;
final int adjustedLen = (int) Math.min(len, bytesAvailable);
if (adjustedLen > 0) {
out.write(b, off, adjustedLen);
increaseNumberOfWrittenBytes(adjustedLen);
}
}
public long getBytesWritten() {
return bytesWritten;
}
private void increaseNumberOfWrittenBytes(int amount) throws IOException {
bytesWritten += amount;
if (bytesWritten >= limit) {
out.flush();
}
}
}

View File

@@ -1,117 +0,0 @@
package org.cryptomator.crypto.aes256;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.apache.commons.io.IOUtils;
/**
* Not thread-safe!
*/
public class LengthObfuscatingInputStream extends FilterInputStream {
private final byte[] padding;
private int paddingLength = -1;
private long inputBytesRead = 0;
private int paddingBytesRead = 0;
LengthObfuscatingInputStream(InputStream in, byte[] padding) {
super(in);
this.padding = padding;
}
long getRealInputLength() {
return inputBytesRead;
}
private void choosePaddingLengthOnce() {
if (paddingLength == -1) {
long upperBound = Math.min(Math.max(inputBytesRead / 10, 4096), 16 * 1024 * 1024); // 10% of original bytes (at least 4KiB), but not more than 16MiBs
paddingLength = (int) (Math.random() * upperBound);
}
}
@Override
public int read() throws IOException {
final int b = in.read();
if (b != -1) {
// stream available:
inputBytesRead++;
return b;
} else {
choosePaddingLengthOnce();
return readFromPadding();
}
}
private int readFromPadding() {
if (paddingLength == -1) {
throw new IllegalStateException("No padding length chosen yet.");
}
if (paddingBytesRead < paddingLength) {
// padding available:
return padding[paddingBytesRead++ % padding.length];
} else {
// end of stream AND padding
return -1;
}
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
final int bytesRead = IOUtils.read(in, b, off, len); // 0 on EOF
inputBytesRead += bytesRead;
if (bytesRead == len) {
return bytesRead;
} else if (bytesRead < len) {
choosePaddingLengthOnce();
final int additionalBytesNeeded = len - bytesRead;
final int additionalBytesRead = readFromPadding(b, off + bytesRead, additionalBytesNeeded);
return (bytesRead == 0 && additionalBytesRead == 0) ? -1 : bytesRead + additionalBytesRead;
} else {
// bytesRead > len:
throw new IllegalStateException("Read more bytes than requested.");
}
}
/**
* @return bytes read from padding (0, if fully read)
*/
private int readFromPadding(byte[] b, int off, int len) {
if (len < 0) {
throw new IllegalArgumentException("Length must not be negative");
}
if (paddingLength == -1) {
throw new IllegalStateException("No padding length chosen yet.");
}
final int remainingPadding = paddingLength - paddingBytesRead;
if (remainingPadding > len) {
// padding available:
for (int i = 0; i < len; i++) {
b[off + i] = padding[paddingBytesRead++ % padding.length];
}
return len;
} else {
// partly available:
for (int i = 0; i < remainingPadding; i++) {
b[off + i] = padding[paddingBytesRead++ % padding.length];
}
return remainingPadding;
}
}
@Override
public long skip(long n) throws IOException {
throw new IOException("Skip not supported");
}
@Override
public boolean markSupported() {
return false;
}
}

View File

@@ -1,204 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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
******************************************************************************/
package org.cryptomator.crypto.aes256;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.util.Arrays;
import javax.security.auth.DestroyFailedException;
import org.apache.commons.io.IOUtils;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.junit.Assert;
import org.junit.Test;
public class Aes256CryptorTest {
@Test
public void testCorrectPassword() throws IOException, WrongPasswordException, DecryptFailedException, UnsupportedKeyLengthException, DestroyFailedException, UnsupportedVaultException {
final String pw = "asd";
final Aes256Cryptor cryptor = new Aes256Cryptor();
final ByteArrayOutputStream out = new ByteArrayOutputStream();
cryptor.encryptMasterKey(out, pw);
cryptor.destroy();
final Aes256Cryptor decryptor = new Aes256Cryptor();
final InputStream in = new ByteArrayInputStream(out.toByteArray());
decryptor.decryptMasterKey(in, pw);
IOUtils.closeQuietly(out);
IOUtils.closeQuietly(in);
}
@Test
public void testWrongPassword() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, DestroyFailedException, UnsupportedVaultException {
final String pw = "asd";
final Aes256Cryptor cryptor = new Aes256Cryptor();
final ByteArrayOutputStream out = new ByteArrayOutputStream();
cryptor.encryptMasterKey(out, pw);
cryptor.destroy();
IOUtils.closeQuietly(out);
// all these passwords are expected to fail.
final String[] wrongPws = {"a", "as", "asdf", "sdf", "das", "dsa", "foo", "bar", "baz"};
final Aes256Cryptor decryptor = new Aes256Cryptor();
for (final String wrongPw : wrongPws) {
final InputStream in = new ByteArrayInputStream(out.toByteArray());
try {
decryptor.decryptMasterKey(in, wrongPw);
Assert.fail("should not succeed.");
} catch (WrongPasswordException e) {
continue;
} finally {
IOUtils.closeQuietly(in);
}
}
}
@Test(expected = DecryptFailedException.class)
public void testIntegrityViolationDuringDecryption() throws IOException, DecryptFailedException, EncryptFailedException {
// our test plaintext data:
final byte[] plaintextData = "Hello World".getBytes();
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
// init cryptor:
final Aes256Cryptor cryptor = new Aes256Cryptor();
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(104 + plaintextData.length + 4096);
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
IOUtils.closeQuietly(encryptedOut);
encryptedData.position(0);
// toggle one bit inf first content byte:
encryptedData.position(64);
final byte fifthByte = encryptedData.get();
encryptedData.position(64);
encryptedData.put((byte) (fifthByte ^ 0x01));
encryptedData.position(0);
// decrypt modified content (should fail with DecryptFailedException):
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
cryptor.decryptFile(encryptedIn, plaintextOut, true);
}
@Test
public void testEncryptionAndDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, EncryptFailedException {
// our test plaintext data:
final byte[] plaintextData = "Hello World".getBytes();
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
// init cryptor:
final Aes256Cryptor cryptor = new Aes256Cryptor();
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(104 + plaintextData.length + 4096 + 32); // header + content + maximum possible size obfuscation padding + 32 bytes mac (per each 32k)
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
IOUtils.closeQuietly(encryptedOut);
encryptedData.position(0);
// decrypt file size:
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final Long filesize = cryptor.decryptedContentLength(encryptedIn);
Assert.assertEquals(plaintextData.length, filesize.longValue());
// decrypt:
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
final Long numDecryptedBytes = cryptor.decryptFile(encryptedIn, plaintextOut, true);
IOUtils.closeQuietly(encryptedIn);
IOUtils.closeQuietly(plaintextOut);
Assert.assertEquals(filesize.longValue(), numDecryptedBytes.longValue());
// check decrypted data:
final byte[] result = plaintextOut.toByteArray();
Assert.assertArrayEquals(plaintextData, result);
}
@Test
public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, EncryptFailedException {
// 8MiB test plaintext data:
final byte[] plaintextData = new byte[2097152 * Integer.BYTES];
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);
for (int i = 0; i < 2097152; i++) {
bbIn.putInt(i);
}
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
// init cryptor:
final Aes256Cryptor cryptor = new Aes256Cryptor();
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate((int) (104 + plaintextData.length * 1.2));
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
IOUtils.closeQuietly(encryptedOut);
encryptedData.position(0);
// decrypt:
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
final Long numDecryptedBytes = cryptor.decryptRange(encryptedIn, plaintextOut, 260000 * Integer.BYTES, 4000 * Integer.BYTES, true);
IOUtils.closeQuietly(encryptedIn);
IOUtils.closeQuietly(plaintextOut);
Assert.assertTrue(numDecryptedBytes > 0);
// check decrypted data:
final byte[] result = plaintextOut.toByteArray();
final byte[] expected = Arrays.copyOfRange(plaintextData, 260000 * Integer.BYTES, 264000 * Integer.BYTES);
Assert.assertArrayEquals(expected, result);
}
@Test
public void testEncryptionOfFilenames() throws IOException, DecryptFailedException {
final Aes256Cryptor cryptor = new Aes256Cryptor();
// directory paths
final String originalPath1 = "foo/bar/baz";
final String encryptedPath1a = cryptor.encryptDirectoryPath(originalPath1, "/");
final String encryptedPath1b = cryptor.encryptDirectoryPath(originalPath1, "/");
Assert.assertEquals(encryptedPath1a, encryptedPath1b);
// long file names
final String str50chars = "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee";
final String originalPath2 = str50chars + str50chars + str50chars + str50chars + str50chars + "_isLongerThan255Chars.txt";
final String encryptedPath2a = cryptor.encryptFilename(originalPath2);
final String encryptedPath2b = cryptor.encryptFilename(originalPath2);
Assert.assertEquals(encryptedPath2a, encryptedPath2b);
final String decryptedPath2 = cryptor.decryptFilename(encryptedPath2a);
Assert.assertEquals(originalPath2, decryptedPath2);
// block size length file names
final String originalPath3 = "aaaabbbbccccdddd";
final String encryptedPath3a = cryptor.encryptFilename(originalPath3);
final String encryptedPath3b = cryptor.encryptFilename(originalPath3);
Assert.assertEquals(encryptedPath3a, encryptedPath3b);
final String decryptedPath3 = cryptor.decryptFilename(encryptedPath3a);
Assert.assertEquals(originalPath3, decryptedPath3);
}
}

View File

@@ -1,224 +0,0 @@
package org.cryptomator.crypto.aes256;
import java.security.InvalidKeyException;
import org.apache.commons.codec.DecoderException;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.junit.Assert;
import org.junit.Test;
/**
* Official RFC 5297 test vector taken from https://tools.ietf.org/html/rfc5297#appendix-A.1
*/
public class AesSivCipherUtilTest {
@Test
public void testS2v() throws DecoderException {
final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
(byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, //
(byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, //
(byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0};
final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, //
(byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, //
(byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, //
(byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, //
(byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, //
(byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27};
final byte[] plaintext = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, //
(byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, //
(byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, //
(byte) 0xdd, (byte) 0xee};
final byte[] expected = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, //
(byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, //
(byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, //
(byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93};
final byte[] result = AesSivCipherUtil.s2v(macKey, plaintext, ad);
Assert.assertArrayEquals(expected, result);
}
@Test
public void testSivEncrypt() throws InvalidKeyException {
final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
(byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, //
(byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, //
(byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0};
final byte[] aesKey = {(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, //
(byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, //
(byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, //
(byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0xff};
final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, //
(byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, //
(byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, //
(byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, //
(byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, //
(byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27};
final byte[] plaintext = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, //
(byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, //
(byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, //
(byte) 0xdd, (byte) 0xee};
final byte[] expected = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, //
(byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, //
(byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, //
(byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93, //
(byte) 0x40, (byte) 0xc0, (byte) 0x2b, (byte) 0x96, //
(byte) 0x90, (byte) 0xc4, (byte) 0xdc, (byte) 0x04, //
(byte) 0xda, (byte) 0xef, (byte) 0x7f, (byte) 0x6a, //
(byte) 0xfe, (byte) 0x5c};
final byte[] result = AesSivCipherUtil.sivEncrypt(aesKey, macKey, plaintext, ad);
Assert.assertArrayEquals(expected, result);
}
@Test
public void testSivDecrypt() throws DecryptFailedException, InvalidKeyException {
final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
(byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, //
(byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, //
(byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0};
final byte[] aesKey = {(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, //
(byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, //
(byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, //
(byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0xff};
final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, //
(byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, //
(byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, //
(byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, //
(byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, //
(byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27};
final byte[] ciphertext = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, //
(byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, //
(byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, //
(byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93, //
(byte) 0x40, (byte) 0xc0, (byte) 0x2b, (byte) 0x96, //
(byte) 0x90, (byte) 0xc4, (byte) 0xdc, (byte) 0x04, //
(byte) 0xda, (byte) 0xef, (byte) 0x7f, (byte) 0x6a, //
(byte) 0xfe, (byte) 0x5c};
final byte[] expected = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, //
(byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, //
(byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, //
(byte) 0xdd, (byte) 0xee};
final byte[] result = AesSivCipherUtil.sivDecrypt(aesKey, macKey, ciphertext, ad);
Assert.assertArrayEquals(expected, result);
}
@Test(expected = DecryptFailedException.class)
public void testSivDecryptWithInvalidKey() throws DecryptFailedException, InvalidKeyException {
final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
(byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, //
(byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, //
(byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0};
final byte[] aesKey = {(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, //
(byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, //
(byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, //
(byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0x00};
final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, //
(byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, //
(byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, //
(byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, //
(byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, //
(byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27};
final byte[] ciphertext = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, //
(byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, //
(byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, //
(byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93, //
(byte) 0x40, (byte) 0xc0, (byte) 0x2b, (byte) 0x96, //
(byte) 0x90, (byte) 0xc4, (byte) 0xdc, (byte) 0x04, //
(byte) 0xda, (byte) 0xef, (byte) 0x7f, (byte) 0x6a, //
(byte) 0xfe, (byte) 0x5c};
final byte[] expected = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, //
(byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, //
(byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, //
(byte) 0xdd, (byte) 0xee};
final byte[] result = AesSivCipherUtil.sivDecrypt(aesKey, macKey, ciphertext, ad);
Assert.assertArrayEquals(expected, result);
}
/**
* https://tools.ietf.org/html/rfc5297#appendix-A.2
*/
@Test
public void testNonceBasedAuthenticatedEncryption() throws InvalidKeyException {
final byte[] macKey = {(byte) 0x7f, (byte) 0x7e, (byte) 0x7d, (byte) 0x7c, //
(byte) 0x7b, (byte) 0x7a, (byte) 0x79, (byte) 0x78, //
(byte) 0x77, (byte) 0x76, (byte) 0x75, (byte) 0x74, //
(byte) 0x73, (byte) 0x72, (byte) 0x71, (byte) 0x70};
final byte[] aesKey = {(byte) 0x40, (byte) 0x41, (byte) 0x42, (byte) 0x43, //
(byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, //
(byte) 0x48, (byte) 0x49, (byte) 0x4a, (byte) 0x4b, //
(byte) 0x4c, (byte) 0x4d, (byte) 0x4e, (byte) 0x4f};
final byte[] ad1 = {(byte) 0x00, (byte) 0x11, (byte) 0x22, (byte) 0x33, //
(byte) 0x44, (byte) 0x55, (byte) 0x66, (byte) 0x77, //
(byte) 0x88, (byte) 0x99, (byte) 0xaa, (byte) 0xbb, //
(byte) 0xcc, (byte) 0xdd, (byte) 0xee, (byte) 0xff, //
(byte) 0xde, (byte) 0xad, (byte) 0xda, (byte) 0xda, //
(byte) 0xde, (byte) 0xad, (byte) 0xda, (byte) 0xda, //
(byte) 0xff, (byte) 0xee, (byte) 0xdd, (byte) 0xcc, //
(byte) 0xbb, (byte) 0xaa, (byte) 0x99, (byte) 0x88, //
(byte) 0x77, (byte) 0x66, (byte) 0x55, (byte) 0x44, //
(byte) 0x33, (byte) 0x22, (byte) 0x11, (byte) 0x00};
final byte[] ad2 = {(byte) 0x10, (byte) 0x20, (byte) 0x30, (byte) 0x40, //
(byte) 0x50, (byte) 0x60, (byte) 0x70, (byte) 0x80, //
(byte) 0x90, (byte) 0xa0};
final byte[] nonce = {(byte) 0x09, (byte) 0xf9, (byte) 0x11, (byte) 0x02, //
(byte) 0x9d, (byte) 0x74, (byte) 0xe3, (byte) 0x5b, //
(byte) 0xd8, (byte) 0x41, (byte) 0x56, (byte) 0xc5, //
(byte) 0x63, (byte) 0x56, (byte) 0x88, (byte) 0xc0};
final byte[] plaintext = {(byte) 0x74, (byte) 0x68, (byte) 0x69, (byte) 0x73, //
(byte) 0x20, (byte) 0x69, (byte) 0x73, (byte) 0x20, //
(byte) 0x73, (byte) 0x6f, (byte) 0x6d, (byte) 0x65, //
(byte) 0x20, (byte) 0x70, (byte) 0x6c, (byte) 0x61, //
(byte) 0x69, (byte) 0x6e, (byte) 0x74, (byte) 0x65, //
(byte) 0x78, (byte) 0x74, (byte) 0x20, (byte) 0x74, //
(byte) 0x6f, (byte) 0x20, (byte) 0x65, (byte) 0x6e, //
(byte) 0x63, (byte) 0x72, (byte) 0x79, (byte) 0x70, //
(byte) 0x74, (byte) 0x20, (byte) 0x75, (byte) 0x73, //
(byte) 0x69, (byte) 0x6e, (byte) 0x67, (byte) 0x20, //
(byte) 0x53, (byte) 0x49, (byte) 0x56, (byte) 0x2d, //
(byte) 0x41, (byte) 0x45, (byte) 0x53};
final byte[] result = AesSivCipherUtil.sivEncrypt(aesKey, macKey, plaintext, ad1, ad2, nonce);
final byte[] expected = {(byte) 0x7b, (byte) 0xdb, (byte) 0x6e, (byte) 0x3b, //
(byte) 0x43, (byte) 0x26, (byte) 0x67, (byte) 0xeb, //
(byte) 0x06, (byte) 0xf4, (byte) 0xd1, (byte) 0x4b, //
(byte) 0xff, (byte) 0x2f, (byte) 0xbd, (byte) 0x0f, //
(byte) 0xcb, (byte) 0x90, (byte) 0x0f, (byte) 0x2f, //
(byte) 0xdd, (byte) 0xbe, (byte) 0x40, (byte) 0x43, //
(byte) 0x26, (byte) 0x60, (byte) 0x19, (byte) 0x65, //
(byte) 0xc8, (byte) 0x89, (byte) 0xbf, (byte) 0x17, //
(byte) 0xdb, (byte) 0xa7, (byte) 0x7c, (byte) 0xeb, //
(byte) 0x09, (byte) 0x4f, (byte) 0xa6, (byte) 0x63, //
(byte) 0xb7, (byte) 0xa3, (byte) 0xf7, (byte) 0x48, //
(byte) 0xba, (byte) 0x8a, (byte) 0xf8, (byte) 0x29, //
(byte) 0xea, (byte) 0x64, (byte) 0xad, (byte) 0x54, //
(byte) 0x4a, (byte) 0x27, (byte) 0x2e, (byte) 0x9c, //
(byte) 0x48, (byte) 0x5b, (byte) 0x62, (byte) 0xa3, //
(byte) 0xfd, (byte) 0x5c, (byte) 0x0d};
Assert.assertArrayEquals(expected, result);
}
}

View File

@@ -1,79 +0,0 @@
package org.cryptomator.crypto.aes256;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
class ByteBufferBackedSeekableChannel implements SeekableByteChannel {
private final ByteBuffer buffer;
private boolean open = true;
ByteBufferBackedSeekableChannel(ByteBuffer buffer) {
this.buffer = buffer;
}
@Override
public boolean isOpen() {
return open;
}
@Override
public void close() throws IOException {
open = false;
}
@Override
public int read(ByteBuffer dst) throws IOException {
if (buffer.remaining() == 0) {
return -1;
}
int num = Math.min(dst.remaining(), buffer.remaining());
byte[] bytes = new byte[num];
buffer.get(bytes);
dst.put(bytes);
return num;
}
@Override
public int write(ByteBuffer src) throws IOException {
int num = src.remaining();
if (buffer.remaining() < src.remaining()) {
buffer.limit(buffer.limit() + src.remaining());
}
buffer.put(src);
return num;
}
@Override
public long position() throws IOException {
return buffer.position();
}
@Override
public SeekableByteChannel position(long newPosition) throws IOException {
if (newPosition > Integer.MAX_VALUE) {
throw new UnsupportedOperationException();
}
if (newPosition > buffer.limit()) {
buffer.limit((int) newPosition);
}
buffer.position((int) newPosition);
return this;
}
@Override
public long size() throws IOException {
return buffer.limit();
}
@Override
public SeekableByteChannel truncate(long size) throws IOException {
if (size > Integer.MAX_VALUE) {
throw new UnsupportedOperationException();
}
buffer.limit((int) size);
return this;
}
}

View File

@@ -1,80 +0,0 @@
package org.cryptomator.crypto;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.SeekableByteChannel;
import javax.security.auth.DestroyFailedException;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
public class AbstractCryptorDecorator implements Cryptor {
protected final Cryptor cryptor;
public AbstractCryptorDecorator(Cryptor cryptor) {
this.cryptor = cryptor;
}
@Override
public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException {
cryptor.encryptMasterKey(out, password);
}
@Override
public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException, UnsupportedVaultException {
cryptor.decryptMasterKey(in, password);
}
@Override
public String encryptDirectoryPath(String cleartextDirectoryId, String nativePathSep) {
return cryptor.encryptDirectoryPath(cleartextDirectoryId, nativePathSep);
}
@Override
public String encryptFilename(String cleartextName) {
return cryptor.encryptFilename(cleartextName);
}
@Override
public String decryptFilename(String ciphertextName) throws DecryptFailedException {
return cryptor.decryptFilename(ciphertextName);
}
@Override
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException {
return cryptor.decryptedContentLength(encryptedFile);
}
@Override
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException {
return cryptor.decryptFile(encryptedFile, plaintextFile, authenticate);
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException {
return cryptor.decryptRange(encryptedFile, plaintextFile, pos, length, authenticate);
}
@Override
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
return cryptor.encryptFile(plaintextFile, encryptedFile);
}
@Override
public void destroy() throws DestroyFailedException {
cryptor.destroy();
}
@Override
public boolean isDestroyed() {
return cryptor.isDestroyed();
}
}

View File

@@ -1,97 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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
******************************************************************************/
package org.cryptomator.crypto;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.SeekableByteChannel;
import javax.security.auth.Destroyable;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
/**
* Provides access to cryptographic functions. All methods are threadsafe.
*/
public interface Cryptor extends Destroyable {
/**
* Encrypts the current masterKey with the given password and writes the result to the given output stream.
*/
void encryptMasterKey(OutputStream out, CharSequence password) throws IOException;
/**
* Reads the encrypted masterkey from the given input stream and decrypts it with the given password.
*
* @throws DecryptFailedException If the decryption failed for various reasons (including wrong password).
* @throws WrongPasswordException If the provided password was wrong. Note: Sometimes the algorithm itself fails due to a wrong password. In this case a DecryptFailedException will be thrown.
* @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In this case Java JCE needs to be installed.
* @throws UnsupportedVaultException If the masterkey file is too old or too modern.
*/
void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException, UnsupportedVaultException;
/**
* Encrypts a given plaintext path representing a directory structure. See {@link #encryptFilename(String, CryptorMetadataSupport)} for contents inside directories.
*
* @param cleartextDirectoryId A unique directory id
* @param nativePathSep Path separator like "/" used on local file system. Must not be null, even if cleartextPath is a sole file name without any path separators.
* @return Encrypted path.
*/
String encryptDirectoryPath(String cleartextDirectoryId, String nativePathSep);
/**
* Encrypts the name of a file. See {@link #encryptDirectoryPath(String, char)} for parent dir.
*
* @param cleartextName A plaintext filename without any preceeding directory paths.
* @return Encrypted filename.
*/
String encryptFilename(String cleartextName);
/**
* Decrypts the name of a file.
*
* @param ciphertextName A ciphertext filename without any preceeding directory paths.
* @return Decrypted filename.
* @throws DecryptFailedException If the decryption failed for various reasons (including wrong password).
*/
String decryptFilename(String ciphertextName) throws DecryptFailedException;
/**
* @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
* @return Content length of the decrypted file or <code>null</code> if unknown.
* @throws MacAuthenticationFailedException If the MAC auth failed.
*/
Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException;
/**
* @return Number of decrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.
* @throws DecryptFailedException If decryption failed
*/
Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException;
/**
* @param pos First byte (inclusive)
* @param length Number of requested bytes beginning at pos.
* @return Number of decrypted bytes. This might not be equal to the number of bytes requested due to potential overheads.
* @throws DecryptFailedException If decryption failed
*/
Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException;
/**
* @return Number of encrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.
*/
Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException;
}

View File

@@ -1,26 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 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
******************************************************************************/
package org.cryptomator.crypto;
/**
* Optional monitoring interface. If a cryptor implements this interface, it counts bytes de- and encrypted in a thread-safe manner.
*/
public interface CryptorIOSampling {
/**
* @return Number of encrypted bytes since the last reset.
*/
Long pollEncryptedBytes(boolean resetCounter);
/**
* @return Number of decrypted bytes since the last reset.
*/
Long pollDecryptedBytes(boolean resetCounter);
}

View File

@@ -1,65 +0,0 @@
package org.cryptomator.crypto;
import java.util.Map;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.AbstractDualBidiMap;
import org.apache.commons.collections4.map.LRUMap;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
public class PathCachingCryptorDecorator extends AbstractCryptorDecorator {
private static final int MAX_CACHED_PATHS = 5000;
private static final int MAX_CACHED_NAMES = 5000;
private final Map<String, String> pathCache = new LRUMap<>(MAX_CACHED_PATHS); // <cleartextDirectoryId, ciphertextPath>
private final BidiMap<String, String> nameCache = new BidiLRUMap<>(MAX_CACHED_NAMES); // <cleartextName, ciphertextName>
private PathCachingCryptorDecorator(Cryptor cryptor) {
super(cryptor);
}
public static Cryptor decorate(Cryptor cryptor) {
return new PathCachingCryptorDecorator(cryptor);
}
/* Cryptor */
@Override
public String encryptDirectoryPath(String cleartextDirectoryId, String nativePathSep) {
return pathCache.computeIfAbsent(cleartextDirectoryId, id -> cryptor.encryptDirectoryPath(id, nativePathSep));
}
@Override
public String encryptFilename(String cleartextName) {
return nameCache.computeIfAbsent(cleartextName, name -> cryptor.encryptFilename(name));
}
@Override
public String decryptFilename(String ciphertextName) throws DecryptFailedException {
String cleartextName = nameCache.getKey(ciphertextName);
if (cleartextName == null) {
cleartextName = cryptor.decryptFilename(ciphertextName);
nameCache.put(cleartextName, ciphertextName);
}
return cleartextName;
}
private static class BidiLRUMap<K, V> extends AbstractDualBidiMap<K, V> {
BidiLRUMap(int maxSize) {
super(new LRUMap<K, V>(maxSize), new LRUMap<V, K>(maxSize));
}
protected BidiLRUMap(final Map<K, V> normalMap, final Map<V, K> reverseMap, final BidiMap<V, K> inverseBidiMap) {
super(normalMap, reverseMap, inverseBidiMap);
}
@Override
protected BidiMap<V, K> createBidiMap(Map<V, K> normalMap, Map<K, V> reverseMap, BidiMap<K, V> inverseMap) {
return new BidiLRUMap<V, K>(normalMap, reverseMap, inverseMap);
}
}
}

View File

@@ -1,115 +0,0 @@
package org.cryptomator.crypto;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.SeekableByteChannel;
import java.util.concurrent.atomic.AtomicLong;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
public class SamplingCryptorDecorator extends AbstractCryptorDecorator implements CryptorIOSampling {
private final AtomicLong encryptedBytes;
private final AtomicLong decryptedBytes;
private SamplingCryptorDecorator(Cryptor cryptor) {
super(cryptor);
encryptedBytes = new AtomicLong();
decryptedBytes = new AtomicLong();
}
public static Cryptor decorate(Cryptor cryptor) {
return new SamplingCryptorDecorator(cryptor);
}
@Override
public Long pollEncryptedBytes(boolean resetCounter) {
if (resetCounter) {
return encryptedBytes.getAndSet(0);
} else {
return encryptedBytes.get();
}
}
@Override
public Long pollDecryptedBytes(boolean resetCounter) {
if (resetCounter) {
return decryptedBytes.getAndSet(0);
} else {
return decryptedBytes.get();
}
}
/* Cryptor */
@Override
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException {
final OutputStream countingOutputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
return cryptor.decryptFile(encryptedFile, countingOutputStream, authenticate);
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException {
final OutputStream countingOutputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
return cryptor.decryptRange(encryptedFile, countingOutputStream, pos, length, authenticate);
}
@Override
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
final InputStream countingInputStream = new CountingInputStream(encryptedBytes, plaintextFile);
return cryptor.encryptFile(countingInputStream, encryptedFile);
}
private class CountingInputStream extends InputStream {
private final InputStream in;
private final AtomicLong counter;
private CountingInputStream(AtomicLong counter, InputStream in) {
this.in = in;
this.counter = counter;
}
@Override
public int read() throws IOException {
int count = in.read();
counter.addAndGet(count);
return count;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int count = in.read(b, off, len);
counter.addAndGet(count);
return count;
}
}
private class CountingOutputStream extends OutputStream {
private final OutputStream out;
private final AtomicLong counter;
private CountingOutputStream(AtomicLong counter, OutputStream out) {
this.out = out;
this.counter = counter;
}
@Override
public void write(int b) throws IOException {
counter.incrementAndGet();
out.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
counter.addAndGet(len);
out.write(b, off, len);
}
}
}

View File

@@ -1,10 +0,0 @@
package org.cryptomator.crypto.exceptions;
public class CounterOverflowException extends EncryptFailedException {
private static final long serialVersionUID = 380066751064534731L;
public CounterOverflowException(String msg) {
super(msg);
}
}

View File

@@ -1,15 +0,0 @@
package org.cryptomator.crypto.exceptions;
import java.io.IOException;
public class CryptingException extends IOException {
private static final long serialVersionUID = -6622699014483319376L;
public CryptingException(String string) {
super(string);
}
public CryptingException(String string, Throwable t) {
super(string, t);
}
}

View File

@@ -1,13 +0,0 @@
package org.cryptomator.crypto.exceptions;
public class DecryptFailedException extends CryptingException {
private static final long serialVersionUID = -3855673600374897828L;
public DecryptFailedException(Throwable t) {
super("Decryption failed.", t);
}
public DecryptFailedException(String msg) {
super(msg);
}
}

View File

@@ -1,13 +0,0 @@
package org.cryptomator.crypto.exceptions;
public class EncryptFailedException extends CryptingException {
private static final long serialVersionUID = -3855673600374897828L;
public EncryptFailedException(Throwable t) {
super("Encryption failed.", t);
}
public EncryptFailedException(String msg) {
super(msg);
}
}

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