mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-14 16:51:28 +00:00
Compare commits
477 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20e55eddf8 | ||
|
|
0fdcdc816a | ||
|
|
b7506d97a9 | ||
|
|
4ad7481dc7 | ||
|
|
bc815405d2 | ||
|
|
9c06e762c3 | ||
|
|
1ac87dd32f | ||
|
|
e0ce7ce2ec | ||
|
|
3d951a9d7b | ||
|
|
cec3d984b0 | ||
|
|
392e474cfa | ||
|
|
41fb0d51a4 | ||
|
|
aa9fef2967 | ||
|
|
adc9c02564 | ||
|
|
ace64117a2 | ||
|
|
fb4db2506b | ||
|
|
1076d971ae | ||
|
|
eed1b1cff0 | ||
|
|
f5cb82e21e | ||
|
|
67661f114b | ||
|
|
8a3e09764a | ||
|
|
eb3cfd6e6a | ||
|
|
4d1727d0e9 | ||
|
|
a51d853d1c | ||
|
|
d0039466f7 | ||
|
|
5c959989a2 | ||
|
|
6283d2df3d | ||
|
|
a9e0dfdaf8 | ||
|
|
45ca7e9e47 | ||
|
|
034b5c2718 | ||
|
|
e188649c79 | ||
|
|
1468c6ec90 | ||
|
|
07ba4eb537 | ||
|
|
414bbef1a7 | ||
|
|
e2b94ff6ef | ||
|
|
41f8a9faca | ||
|
|
1d9252e974 | ||
|
|
80780eef3c | ||
|
|
87ff33956b | ||
|
|
1804b98f05 | ||
|
|
847c6813cc | ||
|
|
1dde5ff6e7 | ||
|
|
76c9a19428 | ||
|
|
25ee0519e1 | ||
|
|
c184089c35 | ||
|
|
d2bcc47857 | ||
|
|
34629a69ea | ||
|
|
92c87f7b84 | ||
|
|
0dd96635ac | ||
|
|
048c44a6e4 | ||
|
|
06910ad1f4 | ||
|
|
02a0f3acc6 | ||
|
|
851f9240b7 | ||
|
|
99fce8d0b7 | ||
|
|
bf05c59c3b | ||
|
|
3dcebb1e1f | ||
|
|
fe3efdf610 | ||
|
|
5f4ae46f82 | ||
|
|
deef325319 | ||
|
|
fbe00a8fe3 | ||
|
|
dc87dade43 | ||
|
|
ba1625b5ad | ||
|
|
f6b126415e | ||
|
|
9147e1c08b | ||
|
|
6c18103662 | ||
|
|
6fc343ea12 | ||
|
|
d304d66cdd | ||
|
|
2ce9143b85 | ||
|
|
1c54e4f4ad | ||
|
|
9fd6f2ecae | ||
|
|
0d9f8eefc0 | ||
|
|
40a1530f19 | ||
|
|
0477a0a2e3 | ||
|
|
b77d4b5ae2 | ||
|
|
7b6c5318c5 | ||
|
|
6006d65ce0 | ||
|
|
2b01b76926 | ||
|
|
dcea9e21f0 | ||
|
|
78645ecdf6 | ||
|
|
91646dd93d | ||
|
|
fca146e939 | ||
|
|
62aa3ccc7f | ||
|
|
c0f4a2b0d3 | ||
|
|
68ee89af98 | ||
|
|
ad2c9116b9 | ||
|
|
8e24745b3e | ||
|
|
08f664e3df | ||
|
|
b6d1d1dc22 | ||
|
|
a0ef02b95c | ||
|
|
a6cefe67c4 | ||
|
|
be2b63ab2a | ||
|
|
78f11b4a5e | ||
|
|
0f20c7c3c9 | ||
|
|
d4235174f7 | ||
|
|
f16be84aa3 | ||
|
|
833f2d8566 | ||
|
|
c02a63878e | ||
|
|
6deb30307e | ||
|
|
7357829741 | ||
|
|
4bd04150c1 | ||
|
|
ac9fe28967 | ||
|
|
515755d84a | ||
|
|
cf35772c18 | ||
|
|
b0fd226c4c | ||
|
|
0d188d1c0c | ||
|
|
c6016ec7b2 | ||
|
|
e8719a1f9b | ||
|
|
27baf78029 | ||
|
|
bf5ce9a3a5 | ||
|
|
fef19fe6b3 | ||
|
|
5f56dacc4e | ||
|
|
aa249dabb5 | ||
|
|
06a5bed6e3 | ||
|
|
02f1ffc6bf | ||
|
|
bcfe040784 | ||
|
|
de9af9e303 | ||
|
|
d9b88ad1b7 | ||
|
|
e66e5b1d96 | ||
|
|
588166dce9 | ||
|
|
e2bc71a0bc | ||
|
|
e528f6827c | ||
|
|
2882ae8ef8 | ||
|
|
e37f7cea1a | ||
|
|
9b4ee10155 | ||
|
|
c9d970955c | ||
|
|
9e0afd36c4 | ||
|
|
0e523599a3 | ||
|
|
1df6589dd7 | ||
|
|
fb60c97fd3 | ||
|
|
90cd149be8 | ||
|
|
89c04ad83b | ||
|
|
f2d383a211 | ||
|
|
73fde5d020 | ||
|
|
5c0857e98e | ||
|
|
3e87b9c0c6 | ||
|
|
a1d0b6b1d3 | ||
|
|
b0d4b2e403 | ||
|
|
6996d36ea2 | ||
|
|
f77ba908da | ||
|
|
9890789c51 | ||
|
|
a385f2eaef | ||
|
|
553cb5ee3d | ||
|
|
d0dc8819f4 | ||
|
|
221deeda25 | ||
|
|
86000ac454 | ||
|
|
d026afec35 | ||
|
|
0d57ebb24a | ||
|
|
f12168ca94 | ||
|
|
d397f59565 | ||
|
|
77aaeabcde | ||
|
|
768f291ff7 | ||
|
|
6a374cc237 | ||
|
|
0133ec8fdf | ||
|
|
d9ba4935b6 | ||
|
|
b6ee29789e | ||
|
|
5ee82271f5 | ||
|
|
2eb4d87dd1 | ||
|
|
d0afeab74b | ||
|
|
cc74c2c05b | ||
|
|
8865cf0e4b | ||
|
|
65550ce70f | ||
|
|
78300f8bf1 | ||
|
|
32c65a7dda | ||
|
|
6d31ed7ea4 | ||
|
|
c3e5d3f38e | ||
|
|
e3900231aa | ||
|
|
06f13c57d4 | ||
|
|
fc1a5be85f | ||
|
|
a30b310c04 | ||
|
|
956dd855f9 | ||
|
|
67ba7cac40 | ||
|
|
9117b6bc0e | ||
|
|
bae826be28 | ||
|
|
d845e8d97a | ||
|
|
b37b2e4fb7 | ||
|
|
69f6a9927d | ||
|
|
addc9533eb | ||
|
|
8b717993ed | ||
|
|
f70d486462 | ||
|
|
293ac0ea3c | ||
|
|
e99a615b09 | ||
|
|
6da3fde864 | ||
|
|
3a725e4a16 | ||
|
|
e3256a747f | ||
|
|
adc20ea2f2 | ||
|
|
997f841662 | ||
|
|
e57b60f04e | ||
|
|
d5b4fb4fe9 | ||
|
|
edf92adfec | ||
|
|
718bacafa6 | ||
|
|
7122bdf199 | ||
|
|
f9b988bf81 | ||
|
|
9a3fab7545 | ||
|
|
d9668182dd | ||
|
|
82e04553f6 | ||
|
|
84ee2dfcaa | ||
|
|
f05440fe7a | ||
|
|
e3fd25aa41 | ||
|
|
c130d0e4a0 | ||
|
|
7fba38d78a | ||
|
|
807fdae3b9 | ||
|
|
890a0c4408 | ||
|
|
e57ee67208 | ||
|
|
c306151980 | ||
|
|
164a0c6901 | ||
|
|
31c92bd4a2 | ||
|
|
94b8726379 | ||
|
|
ca929241f2 | ||
|
|
9abc0aab83 | ||
|
|
c9564bae62 | ||
|
|
8fd0bbd9ed | ||
|
|
be7b875be7 | ||
|
|
020597c42d | ||
|
|
75b67e5976 | ||
|
|
a6c99c273e | ||
|
|
be4dab2773 | ||
|
|
7b68c427d6 | ||
|
|
bc9b7c3a19 | ||
|
|
cbb669aa40 | ||
|
|
be7e7e32b9 | ||
|
|
2ae5abfc0a | ||
|
|
7cb435e517 | ||
|
|
95b5f4c765 | ||
|
|
d926cbfd5c | ||
|
|
61f1afba87 | ||
|
|
c03bdd8425 | ||
|
|
5b22806bbc | ||
|
|
1467c8315c | ||
|
|
e6a9786b7a | ||
|
|
819c56fe4c | ||
|
|
ae7e865c24 | ||
|
|
c8df03a085 | ||
|
|
5df9f35065 | ||
|
|
ac4a68649e | ||
|
|
7bc678d4dc | ||
|
|
b48670e073 | ||
|
|
391d8013b5 | ||
|
|
023e7d70e5 | ||
|
|
2c92435b91 | ||
|
|
180b14b0c7 | ||
|
|
8280bfe10a | ||
|
|
1994610d57 | ||
|
|
a54f925b70 | ||
|
|
c7c4dd4581 | ||
|
|
c93e4e462b | ||
|
|
6950ad496a | ||
|
|
8a7fca7a95 | ||
|
|
a5e6c9fb9b | ||
|
|
382c3a0258 | ||
|
|
7f313772e5 | ||
|
|
3a82dfb23f | ||
|
|
b85a110a24 | ||
|
|
5a84228678 | ||
|
|
57b40675ac | ||
|
|
50e8a9e429 | ||
|
|
f93d32c6fb | ||
|
|
a909095a1c | ||
|
|
03208ebc5e | ||
|
|
c41225eab6 | ||
|
|
2725b6b920 | ||
|
|
ed0540e78f | ||
|
|
8cdb6d0eab | ||
|
|
932e26f6a6 | ||
|
|
53a9b08dd6 | ||
|
|
752601f4da | ||
|
|
cf64a6c425 | ||
|
|
f0cb91b22f | ||
|
|
0d82e7dcc7 | ||
|
|
bf5988f5fe | ||
|
|
3d24bc74b1 | ||
|
|
6f15ea0e1e | ||
|
|
93ef366125 | ||
|
|
cbcefc4eb5 | ||
|
|
42b852b622 | ||
|
|
12fcf5aeaf | ||
|
|
a1a81cc0ba | ||
|
|
853744002c | ||
|
|
4d2a786504 | ||
|
|
bb185c3170 | ||
|
|
e5d095606f | ||
|
|
4a60e94183 | ||
|
|
1d6f4284c8 | ||
|
|
eb1b4a9fe3 | ||
|
|
64775a7d19 | ||
|
|
8784115c75 | ||
|
|
56fcb99248 | ||
|
|
bfe0a50205 | ||
|
|
26aa18de77 | ||
|
|
6af4ee08f7 | ||
|
|
4059f99fd5 | ||
|
|
0dc30c27d9 | ||
|
|
3696fea3ee | ||
|
|
bb34f5c17d | ||
|
|
b2a37c4b95 | ||
|
|
a972480e72 | ||
|
|
091a44e65d | ||
|
|
c56d0b7d4a | ||
|
|
e4d626eef5 | ||
|
|
9052e7995f | ||
|
|
e23baa33f9 | ||
|
|
8d7c3a8f7b | ||
|
|
56b061206a | ||
|
|
f081e7d3ea | ||
|
|
e241c5ba05 | ||
|
|
406a9970ba | ||
|
|
8ff5659680 | ||
|
|
2e5264bac2 | ||
|
|
a6bbc0ed44 | ||
|
|
6479573346 | ||
|
|
cd72dae0d7 | ||
|
|
d5c43f625f | ||
|
|
0a1eaa8600 | ||
|
|
9278426131 | ||
|
|
fd98f0a69f | ||
|
|
fa35b63b6d | ||
|
|
51f5b6661f | ||
|
|
a7eb99f7d5 | ||
|
|
1a9ac16256 | ||
|
|
b330148b5f | ||
|
|
297ce34c1a | ||
|
|
20e7f4a548 | ||
|
|
18cf25738e | ||
|
|
d14c81d066 | ||
|
|
459ce8b5d2 | ||
|
|
bf0988bb20 | ||
|
|
9c844e626a | ||
|
|
8e1c63338f | ||
|
|
b16ceb1ba8 | ||
|
|
b2a90ddcf6 | ||
|
|
55bee3d0d5 | ||
|
|
58b4905c91 | ||
|
|
9c9e9769ee | ||
|
|
3b178030c7 | ||
|
|
f735a64814 | ||
|
|
560c6251f6 | ||
|
|
b2d425e11f | ||
|
|
8f319b3f87 | ||
|
|
5ff4f4c9c7 | ||
|
|
31f1b6478a | ||
|
|
a84a4b932d | ||
|
|
248984ce20 | ||
|
|
fcf4476ae3 | ||
|
|
0c42392244 | ||
|
|
415423abd7 | ||
|
|
a746a73667 | ||
|
|
a8f53b7084 | ||
|
|
94ed3a6b7c | ||
|
|
3f44d9bb66 | ||
|
|
801253aa27 | ||
|
|
a05fa19de4 | ||
|
|
0b5e09ac60 | ||
|
|
ddf6353729 | ||
|
|
b31f6a0aec | ||
|
|
f75cf48d7b | ||
|
|
578f52f4ba | ||
|
|
ae55874709 | ||
|
|
f46a79fa63 | ||
|
|
ff4448bac0 | ||
|
|
e9f5593e33 | ||
|
|
a9744167c1 | ||
|
|
4e7f3503d9 | ||
|
|
71face8091 | ||
|
|
0c2caf4469 | ||
|
|
9665ca8dff | ||
|
|
c3652a22a0 | ||
|
|
3043aa29c2 | ||
|
|
a388acfce4 | ||
|
|
64dec64e95 | ||
|
|
39535d08e7 | ||
|
|
806e366a72 | ||
|
|
db4b8955f4 | ||
|
|
1bf0c76918 | ||
|
|
282fd5ecee | ||
|
|
634f176cf9 | ||
|
|
c86068d7bb | ||
|
|
6acbba476b | ||
|
|
9385c3bf6d | ||
|
|
aa89f60c2f | ||
|
|
44d1250986 | ||
|
|
c21654eac1 | ||
|
|
c466c7e3ba | ||
|
|
3317babe6b | ||
|
|
3998a7cd58 | ||
|
|
260303127e | ||
|
|
d073bdfad9 | ||
|
|
62d8cdfe4f | ||
|
|
356ea5c319 | ||
|
|
1804a52740 | ||
|
|
3dd9c623f0 | ||
|
|
157839c32f | ||
|
|
0254569826 | ||
|
|
389c49d846 | ||
|
|
d3000da2e9 | ||
|
|
c4eee58f72 | ||
|
|
e67c8f2816 | ||
|
|
dc5d3e21f3 | ||
|
|
80e1185325 | ||
|
|
49c70c03fe | ||
|
|
3045805751 | ||
|
|
4e0143eb05 | ||
|
|
951a02a9a5 | ||
|
|
69b192fe82 | ||
|
|
a055066f72 | ||
|
|
a879ed2237 | ||
|
|
9711314080 | ||
|
|
b22ac719f2 | ||
|
|
25eed3dc4a | ||
|
|
58524e5099 | ||
|
|
eadf736e98 | ||
|
|
b41ccb6054 | ||
|
|
72c0d2cb96 | ||
|
|
92e9fc5871 | ||
|
|
044532ef15 | ||
|
|
e4955ea6c8 | ||
|
|
b2cb5d1dac | ||
|
|
e11b0a3421 | ||
|
|
de29c84a0c | ||
|
|
b3c65267df | ||
|
|
70eb0c99e4 | ||
|
|
35bb042430 | ||
|
|
762f362784 | ||
|
|
3c7651a78a | ||
|
|
99015680b1 | ||
|
|
e1b74ce312 | ||
|
|
3971d3afd5 | ||
|
|
3adfe6871b | ||
|
|
97a72ecbf7 | ||
|
|
0697e19b01 | ||
|
|
973a2fb395 | ||
|
|
fc06595977 | ||
|
|
e4220246ab | ||
|
|
128a93d44e | ||
|
|
a7c19624ce | ||
|
|
2fca4629b9 | ||
|
|
4d5cc7a5a0 | ||
|
|
ebb421bd4c | ||
|
|
0c116d0385 | ||
|
|
1f0aea9d8a | ||
|
|
6c4752cee1 | ||
|
|
b254564657 | ||
|
|
014dbd912a | ||
|
|
1e18a11886 | ||
|
|
386059a238 | ||
|
|
b4ab09b3aa | ||
|
|
289ac55ccd | ||
|
|
b5160cddb9 | ||
|
|
1a81b3a781 | ||
|
|
b6a5db5797 | ||
|
|
aaf98c4fb9 | ||
|
|
55d1ffe703 | ||
|
|
5fefa3c6d4 | ||
|
|
b404e52670 | ||
|
|
44475fa3f1 | ||
|
|
f430f3c579 | ||
|
|
3efa23987f | ||
|
|
1dce871354 | ||
|
|
d919c727cf | ||
|
|
b691e374eb | ||
|
|
ca88e05849 | ||
|
|
104c3b64f6 | ||
|
|
1bef4e786d | ||
|
|
c1f32105d8 | ||
|
|
09b4130c3e | ||
|
|
6d1e0fe609 | ||
|
|
e65c84ca1d | ||
|
|
095f60ec03 | ||
|
|
485df3aa71 | ||
|
|
6b073c1499 | ||
|
|
71983cc3a8 | ||
|
|
db2297d2f1 | ||
|
|
38ab167fa4 | ||
|
|
f87e8f55f1 | ||
|
|
a4e6365e0b | ||
|
|
06034fd95b | ||
|
|
571fee9524 | ||
|
|
af9deffa6d | ||
|
|
48b319ec99 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -11,3 +11,9 @@
|
||||
.classpath
|
||||
target/
|
||||
test-output/
|
||||
|
||||
# IntelliJ Settings Files #
|
||||
.idea/
|
||||
out/
|
||||
.idea_modules/
|
||||
*.iws
|
||||
|
||||
43
.travis.yml
43
.travis.yml
@@ -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: release.*
|
||||
|
||||
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
74
CODE_OF_CONDUCT.md
Normal 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
33
CONTRIBUTING.md
Normal 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
19
ISSUE_TEMPLATE.md
Normal 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]
|
||||
```
|
||||
@@ -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.
|
||||
|
||||
23
LICENSES/BSD-Simplified-License.txt
Normal file
23
LICENSES/BSD-Simplified-License.txt
Normal 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.
|
||||
109
NOTICE.md
109
NOTICE.md
@@ -4,53 +4,14 @@ 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 libraries and fonts that may be licensed under different licenses.
|
||||
|
||||
Cryptomator uses third party softwares 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.
|
||||
|
||||
Copyright 2013 Claudine Zillmann (http://aquafx-project.com/)
|
||||
|
||||
### Jackson
|
||||
Jackson is a high-performance, Free/Open Source JSON processing library.
|
||||
It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has
|
||||
been in development since 2007.
|
||||
It is currently developed by a community of developers, as well as supported
|
||||
commercially by FasterXML.com.
|
||||
|
||||
**Licensing:** Jackson core and extension components may licensed under different licenses.
|
||||
To find the details that apply to this artifact see the accompanying Apache 2.0 license file.
|
||||
For more information, including possible other licensing options, contact
|
||||
FasterXML.com (http://fasterxml.com).
|
||||
|
||||
**Credits:** A list of contributors may be found from CREDITS file, which is included
|
||||
in some artifacts (usually source distributions); but is always available
|
||||
from the source code management (SCM) system project uses.
|
||||
|
||||
|
||||
### Jetty
|
||||
Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
|
||||
|
||||
All rights reserved. This program and the accompanying materials
|
||||
are made available under the terms of the Eclipse Public License v1.0
|
||||
and Apache License v2.0 which accompanies this distribution.
|
||||
|
||||
The UnixCrypt.java code implements the one way cryptography used by
|
||||
Unix systems for simple password protection. Copyright 1996 Aki Yoshida,
|
||||
modified April 2001 by Iris Van den Broeke, Daniel Deville.
|
||||
Permission to use, copy, modify and distribute UnixCrypt
|
||||
for non-commercial or commercial purposes and without fee is
|
||||
granted provided that the copyright notice appears in all copies.
|
||||
|
||||
|
||||
### Jackrabbit WebDAV Library
|
||||
Copyright 2004-2014 The Apache Software Foundation
|
||||
|
||||
This product includes software developed at The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
Based on source code originally developed by Day Software (http://www.day.com/).
|
||||
|
||||
### Apache Jakarta HttpClient
|
||||
Copyright 1999-2007 The Apache Software Foundation
|
||||
|
||||
This product includes software developed by The Apache Software Foundation (http://www.apache.org/).
|
||||
Licensed under the accompanying BSD license file.
|
||||
|
||||
### Apache Commons Collections
|
||||
Copyright 2001-2013 The Apache Software Foundation
|
||||
@@ -83,6 +44,22 @@ Copyright (c) 2013, ControlsFX
|
||||
|
||||
Licensed under the accompanying BSD license file.
|
||||
|
||||
### Dagger 2
|
||||
Copyright 2014 Google, Inc.
|
||||
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
|
||||
|
||||
This product includes software developed by The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
### Apache Log4j
|
||||
Copyright 1999-2012 Apache Software Foundation
|
||||
|
||||
@@ -90,7 +67,49 @@ 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
|
||||
|
||||
This product includes software developed at The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
Based on source code originally developed by Day Software (http://www.day.com/).
|
||||
|
||||
### Jackson
|
||||
Jackson is a high-performance, Free/Open Source JSON processing library.
|
||||
It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has
|
||||
been in development since 2007.
|
||||
It is currently developed by a community of developers, as well as supported
|
||||
commercially by FasterXML.com.
|
||||
|
||||
**Licensing:** Jackson core and extension components may licensed under different licenses.
|
||||
To find the details that apply to this artifact see the accompanying Apache 2.0 license file.
|
||||
For more information, including possible other licensing options, contact
|
||||
FasterXML.com (http://fasterxml.com).
|
||||
|
||||
**Credits:** A list of contributors may be found from CREDITS file, which is included
|
||||
in some artifacts (usually source distributions); but is always available
|
||||
from the source code management (SCM) system project uses.
|
||||
|
||||
### Jetty
|
||||
Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
|
||||
|
||||
All rights reserved. This program and the accompanying materials
|
||||
are made available under the terms of the Eclipse Public License v1.0
|
||||
and Apache License v2.0 which accompanies this distribution.
|
||||
|
||||
The UnixCrypt.java code implements the one way cryptography used by
|
||||
Unix systems for simple password protection. Copyright 1996 Aki Yoshida,
|
||||
modified April 2001 by Iris Van den Broeke, Daniel Deville.
|
||||
Permission to use, copy, modify and distribute UnixCrypt
|
||||
for non-commercial or commercial purposes and without fee is
|
||||
granted provided that the copyright notice appears in all copies.
|
||||
|
||||
### JUnit
|
||||
Copyright (c) 2000-2006, www.hamcrest.org
|
||||
|
||||
Licensed under the accompanying BSD license file.
|
||||
Licensed under the accompanying BSD license file.
|
||||
|
||||
78
README.md
78
README.md
@@ -1,55 +1,67 @@
|
||||
Cryptomator
|
||||
====================
|
||||

|
||||
|
||||
[](https://travis-ci.org/cryptomator/cryptomator)
|
||||
[](https://gitter.im/cryptomator/cryptomator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](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)
|
||||
[](https://scan.coverity.com/projects/cryptomator-cryptomator)
|
||||
[](https://coveralls.io/github/cryptomator/cryptomator?branch=master)
|
||||
[](https://gitter.im/cryptomator/cryptomator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](http://twitter.com/Cryptomator)
|
||||
[](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
BIN
cryptomator.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
37
main/ant-kit/assembly.xml
Normal file
37
main/ant-kit/assembly.xml
Normal 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
103
main/ant-kit/pom.xml
Normal 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.2</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>
|
||||
80
main/ant-kit/src/main/resources/build.xml
Normal file
80
main/ant-kit/src/main/resources/build.xml
Normal 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.
Binary file not shown.
BIN
main/ant-kit/src/main/resources/package/linux/Cryptomator.png
Normal file
BIN
main/ant-kit/src/main/resources/package/linux/Cryptomator.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
50
main/ant-kit/src/main/resources/package/linux/postinst
Normal file
50
main/ant-kit/src/main/resources/package/linux/postinst
Normal 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
|
||||
54
main/ant-kit/src/main/resources/package/linux/spec
Normal file
54
main/ant-kit/src/main/resources/package/linux/spec
Normal 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
43
main/commons-test/pom.xml
Normal 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.2</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>
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
72
main/commons/pom.xml
Normal 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.2</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>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ConsumerThrowingException<T, E extends Exception> {
|
||||
|
||||
void accept(T t) throws E;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface RunnableThrowingException<T extends Exception> {
|
||||
|
||||
void run() throws T;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,24 +1,22 @@
|
||||
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 javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import org.cryptomator.ui.MainModule;
|
||||
import org.cryptomator.ui.test.GuiceJUnitRunner;
|
||||
import org.cryptomator.ui.test.GuiceJUnitRunner.GuiceModules;
|
||||
import org.cryptomator.common.SemVerComparator;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(GuiceJUnitRunner.class)
|
||||
@GuiceModules(MainModule.class)
|
||||
public class SemVerComparatorTest {
|
||||
|
||||
@Inject
|
||||
@Named("SemVer")
|
||||
private Comparator<String> semVerComparator;
|
||||
private final Comparator<String> semVerComparator = new SemVerComparator();
|
||||
|
||||
// equal versions
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<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>0.8.1</version>
|
||||
</parent>
|
||||
<artifactId>core</artifactId>
|
||||
<name>Cryptomator WebDAV and I/O module</name>
|
||||
|
||||
<properties>
|
||||
<jetty.version>9.3.1.v20150714</jetty.version>
|
||||
<jackrabbit.version>2.10.1</jackrabbit.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>crypto-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>crypto-aes</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Jetty (Servlet Container) -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-server</artifactId>
|
||||
<version>${jetty.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-webapp</artifactId>
|
||||
<version>${jetty.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-httpclient</groupId>
|
||||
<artifactId>commons-httpclient</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackrabbit -->
|
||||
<dependency>
|
||||
<groupId>org.apache.jackrabbit</groupId>
|
||||
<artifactId>jackrabbit-webdav</artifactId>
|
||||
<version>${jackrabbit.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Guava -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- I/O -->
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,172 +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();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,333 +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.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);
|
||||
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
final SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, 0L, FILE_HEADER_LENGTH, 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());
|
||||
}
|
||||
} 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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,152 +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);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,85 +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, upper);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Invalid byte range: " + requestRange, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spool(OutputContext outputContext) throws IOException {
|
||||
assert Files.isRegularFile(filePath);
|
||||
assert this.contentLength != null;
|
||||
|
||||
final Long rangeLength = range.getRight() - range.getLeft() + 1;
|
||||
outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis());
|
||||
if (rangeLength <= 0) {
|
||||
// unsatisfiable content range:
|
||||
outputContext.setContentLength(0);
|
||||
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getRight(), range.getRight(), contentLength));
|
||||
LOG.debug("Unsatisfiable content range: " + getContentRangeHeader(range.getLeft(), range.getRight(), contentLength));
|
||||
return;
|
||||
} else {
|
||||
outputContext.setContentLength(rangeLength);
|
||||
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getLeft(), 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);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 -> 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);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.warn("Unable to lock file.");
|
||||
}
|
||||
} finally {
|
||||
this.lock = lock;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (lock != null) {
|
||||
lock.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.debug("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.debug("GET TIME: " + (t1 - t0) / 1000 / 1000.0 + " ms");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,739 +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.apache.commons.io.IOUtils;
|
||||
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.cryptomator.crypto.io.SeekableByteChannelInputStream;
|
||||
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 = encryptedFile.read(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 = encryptedFile.read(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 = encryptedFile.read(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 = encryptedFile.read(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);
|
||||
sensitiveHeaderContentBuf.position(Long.BYTES); // skip file size
|
||||
sensitiveHeaderContentBuf.get(fileKeyBytes);
|
||||
|
||||
// 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 InputStream in = new SeekableByteChannelInputStream(encryptedFile);
|
||||
byte[] buffer = new byte[CONTENT_MAC_BLOCK + 32];
|
||||
int n = 0;
|
||||
long blockNum = startBlock;
|
||||
while ((n = IOUtils.read(in, buffer)) > 0 && bytesWritten < length) {
|
||||
if (n < 32) {
|
||||
throw new DecryptFailedException("Invalid file content, missing MAC.");
|
||||
}
|
||||
|
||||
// check MAC of current block:
|
||||
if (authenticate) {
|
||||
contentMac.update(iv);
|
||||
contentMac.update(longToByteArray(blockNum));
|
||||
contentMac.update(buffer, 0, n - 32);
|
||||
final byte[] calculatedMac = contentMac.doFinal();
|
||||
final byte[] storedMac = new byte[32];
|
||||
System.arraycopy(buffer, n - 32, storedMac, 0, 32);
|
||||
if (!MessageDigest.isEqual(calculatedMac, storedMac)) {
|
||||
throw new MacAuthenticationFailedException("Content MAC authentication failed.");
|
||||
}
|
||||
}
|
||||
|
||||
// decrypt block:
|
||||
final byte[] plaintext = cipher.update(buffer, 0, n - 32);
|
||||
final int offset = (bytesWritten == 0) ? (int) offsetFromFirstBlock : 0;
|
||||
final long pending = length - bytesWritten;
|
||||
final int available = plaintext.length - offset;
|
||||
final int currentBatch = (int) Math.min(pending, available);
|
||||
|
||||
plaintextFile.write(plaintext, offset, currentBatch);
|
||||
bytesWritten += currentBatch;
|
||||
blockNum++;
|
||||
}
|
||||
return bytesWritten;
|
||||
} 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 = channel.read(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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,40 +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);
|
||||
bytesWritten++;
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
bytesWritten += adjustedLen;
|
||||
}
|
||||
}
|
||||
|
||||
public long getBytesWritten() {
|
||||
return bytesWritten;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,129 +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 int available() throws IOException {
|
||||
final int inputAvailable = in.available();
|
||||
if (inputAvailable > 0) {
|
||||
return inputAvailable;
|
||||
} else {
|
||||
// remaining padding
|
||||
choosePaddingLengthOnce();
|
||||
return paddingLength - paddingBytesRead;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user