mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-14 16:51:28 +00:00
Compare commits
762 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce17fff1ac | ||
|
|
2d55da309b | ||
|
|
761207155d | ||
|
|
456a879181 | ||
|
|
2e02e70916 | ||
|
|
dbc803a0d9 | ||
|
|
fdccafaa68 | ||
|
|
deebd82ecd | ||
|
|
0906abdea9 | ||
|
|
da94fe4f6f | ||
|
|
79672a28ee | ||
|
|
e052a4b261 | ||
|
|
425d4e8fed | ||
|
|
d2a2e2304d | ||
|
|
e7157a64ed | ||
|
|
245a995203 | ||
|
|
c29d7fb6a2 | ||
|
|
8cada6d0a2 | ||
|
|
d03446beef | ||
|
|
f62c0b4ca8 | ||
|
|
c046056736 | ||
|
|
e63cbf94d0 | ||
|
|
d48247b7c6 | ||
|
|
695dcd5de7 | ||
|
|
5e35985a3f | ||
|
|
d9325819fd | ||
|
|
f960238b47 | ||
|
|
d0aef5b683 | ||
|
|
c69fc95126 | ||
|
|
51cb41975b | ||
|
|
0debbb83b1 | ||
|
|
fbfe2b0a31 | ||
|
|
a719a83bce | ||
|
|
d353a9cb9d | ||
|
|
ee2e07a9a3 | ||
|
|
f6689043c7 | ||
|
|
45531dc54a | ||
|
|
0a26c192fe | ||
|
|
281cfb74d9 | ||
|
|
0d03eeb5bd | ||
|
|
36e669d729 | ||
|
|
b1f55d7cfe | ||
|
|
9aa8c46560 | ||
|
|
2ed00ed02d | ||
|
|
5d56f5beb6 | ||
|
|
1b937dfb06 | ||
|
|
42be5330fe | ||
|
|
be8949157f | ||
|
|
93b2a4e07a | ||
|
|
ada1195a26 | ||
|
|
93563f68e0 | ||
|
|
3faa0e83cc | ||
|
|
1985e2af72 | ||
|
|
3f03d36ad6 | ||
|
|
8359deb8eb | ||
|
|
e1f2330f95 | ||
|
|
87014c4db2 | ||
|
|
c9eb9b1938 | ||
|
|
28cb2e1301 | ||
|
|
277999112e | ||
|
|
a7fdf3d325 | ||
|
|
54ae332364 | ||
|
|
76075ffec4 | ||
|
|
ed448146f7 | ||
|
|
638e9c1a8b | ||
|
|
1624cffb2a | ||
|
|
f391a6521d | ||
|
|
02ae2e7ca0 | ||
|
|
40bd84a09c | ||
|
|
0c0fb1c4c5 | ||
|
|
ab39bd1667 | ||
|
|
b41636a208 | ||
|
|
0902de821a | ||
|
|
8a6265658e | ||
|
|
7750a49e65 | ||
|
|
b75b9781c1 | ||
|
|
2687c02e31 | ||
|
|
71b65e03d6 | ||
|
|
e09ee27219 | ||
|
|
5a3428d9b0 | ||
|
|
ed109977f8 | ||
|
|
d6c6f177e8 | ||
|
|
97f2cee1ae | ||
|
|
4fb8a27a78 | ||
|
|
79b825aaad | ||
|
|
28fedafb59 | ||
|
|
d9bff68555 | ||
|
|
cef3a5fc77 | ||
|
|
9956f43fd9 | ||
|
|
2b84593bde | ||
|
|
4e728fd387 | ||
|
|
438ade1106 | ||
|
|
fe54f4ec66 | ||
|
|
fe86b4c593 | ||
|
|
a583afeb60 | ||
|
|
a585d3cf16 | ||
|
|
3db757193e | ||
|
|
bac1d6fd83 | ||
|
|
39ee8a9cde | ||
|
|
1263b3af81 | ||
|
|
dafa29d8a3 | ||
|
|
2bc6fe89ad | ||
|
|
8439216233 | ||
|
|
aab616d184 | ||
|
|
70c3a38c49 | ||
|
|
c64294ac3e | ||
|
|
82330db871 | ||
|
|
c54a721f9a | ||
|
|
355bbb5459 | ||
|
|
63daa0f121 | ||
|
|
50885d5c7c | ||
|
|
4d68818ec5 | ||
|
|
6fb20dd509 | ||
|
|
2bb87dfa96 | ||
|
|
3e374a927c | ||
|
|
84ac6d88f5 | ||
|
|
72f6ee6477 | ||
|
|
a3cfcb1131 | ||
|
|
d7d8d21ba4 | ||
|
|
ef0425e2b1 | ||
|
|
df1fd6d0b3 | ||
|
|
2fa04d7b7c | ||
|
|
a15acd64c8 | ||
|
|
5b18eff01a | ||
|
|
47133c6f31 | ||
|
|
09ba4f5129 | ||
|
|
20d4047bed | ||
|
|
56b71ef7d9 | ||
|
|
091e62057d | ||
|
|
824bd9ea64 | ||
|
|
697a791593 | ||
|
|
7462a887b3 | ||
|
|
3535e83d7d | ||
|
|
cf0b4accb3 | ||
|
|
a63bcfbaa2 | ||
|
|
5c4bf2a207 | ||
|
|
c1611a12ed | ||
|
|
0983120712 | ||
|
|
ce12af8495 | ||
|
|
dc117c8415 | ||
|
|
06e526a961 | ||
|
|
2e343a951f | ||
|
|
141ffcf656 | ||
|
|
d61e5c5a08 | ||
|
|
6a15fa132a | ||
|
|
902b29ee0a | ||
|
|
995bba616f | ||
|
|
f39b7b047f | ||
|
|
72e52df4e0 | ||
|
|
8018e9485e | ||
|
|
e0ae50378f | ||
|
|
a9c2b0fc57 | ||
|
|
dc58ba434a | ||
|
|
34af306309 | ||
|
|
21d70b5ae4 | ||
|
|
e90880ac9a | ||
|
|
66faa13f40 | ||
|
|
8a4a29b4d1 | ||
|
|
8c8db84a4a | ||
|
|
a499a3c80b | ||
|
|
6a3ccf2b48 | ||
|
|
fcfcffe9cb | ||
|
|
363ed4ac4b | ||
|
|
1f73a08e09 | ||
|
|
fe0a34907f | ||
|
|
461b11700f | ||
|
|
24bfbb59a4 | ||
|
|
4476558e9c | ||
|
|
560171832c | ||
|
|
6e93d40e51 | ||
|
|
79b819bca6 | ||
|
|
a18c406cf0 | ||
|
|
6730a83cac | ||
|
|
3b3ebd2196 | ||
|
|
505b6542c7 | ||
|
|
31368f0cba | ||
|
|
5b5dd756b1 | ||
|
|
f6ebbb23d1 | ||
|
|
3f0373b08f | ||
|
|
4c3c60060d | ||
|
|
28f275c22d | ||
|
|
24df3c3809 | ||
|
|
034a667e07 | ||
|
|
008e3e3b05 | ||
|
|
94a5bf7596 | ||
|
|
e8db836eff | ||
|
|
429b26f3d8 | ||
|
|
3ae8327300 | ||
|
|
df7e9a0af1 | ||
|
|
93d3eca0ab | ||
|
|
7753d1f0e7 | ||
|
|
d7c6c24932 | ||
|
|
1a75f23081 | ||
|
|
f071efe1b9 | ||
|
|
a8ad335aed | ||
|
|
7022a80c95 | ||
|
|
9a2f602d6c | ||
|
|
c78a4aa241 | ||
|
|
975ce4d973 | ||
|
|
1e6ff0d969 | ||
|
|
69e133d561 | ||
|
|
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 | ||
|
|
9ea9cb6eb2 | ||
|
|
301ba9cdb7 | ||
|
|
740c4c2ba9 | ||
|
|
18e7dcd91f | ||
|
|
95133152f9 | ||
|
|
4cd243e32a | ||
|
|
f454f48248 | ||
|
|
ad3801b223 | ||
|
|
3f946d1c82 | ||
|
|
ecb178d5b2 | ||
|
|
ed7dc60f5e | ||
|
|
6bbfacd794 | ||
|
|
5a06d01ef5 | ||
|
|
aac9ead633 | ||
|
|
cdcc1626ce | ||
|
|
738d2dfc34 | ||
|
|
9771c6d1e7 | ||
|
|
bc0a26b0ad | ||
|
|
7349ef754e | ||
|
|
e8e80f306b | ||
|
|
e1ce400bcd | ||
|
|
8c4d5a9614 | ||
|
|
93a87c86a4 | ||
|
|
685e347524 | ||
|
|
9d2d847727 | ||
|
|
a00086ff2d | ||
|
|
d76154c8d1 | ||
|
|
bc76ab285d | ||
|
|
0d3a5b4e70 | ||
|
|
48f544ef91 | ||
|
|
45cf87d089 | ||
|
|
d7186bb2dd | ||
|
|
85f3487cf0 | ||
|
|
4a754d6a6c | ||
|
|
abf9920caf | ||
|
|
dd2863da5b | ||
|
|
d43396bcfb | ||
|
|
b6383f49b1 | ||
|
|
c5b241a68a | ||
|
|
00a39c80cb | ||
|
|
8d8fe74d3a | ||
|
|
e767436f5d | ||
|
|
03cdf1fdc9 | ||
|
|
49646aae41 | ||
|
|
f3aa636b8b | ||
|
|
c73f18e3b8 | ||
|
|
5f40ce50e7 | ||
|
|
744f9db958 | ||
|
|
111ee99ae1 | ||
|
|
7d81ff3b43 | ||
|
|
00a2c6c5ae | ||
|
|
587c45ee63 | ||
|
|
3d3cb7bb86 | ||
|
|
0e3513e86d | ||
|
|
8845efb983 | ||
|
|
88f81d2682 | ||
|
|
58d500baaf | ||
|
|
103ea9047f | ||
|
|
f4b07b9807 | ||
|
|
6a3b4d486d | ||
|
|
13bcde318b | ||
|
|
242486c0b1 | ||
|
|
ea9c8eee83 | ||
|
|
0d969432c2 | ||
|
|
be369b480b | ||
|
|
4cf872f916 | ||
|
|
3d3c36b66f | ||
|
|
54c2afe3d1 | ||
|
|
3c71878b6b | ||
|
|
f36a61df1c | ||
|
|
1642aa4688 | ||
|
|
6f9b16a7dc | ||
|
|
66ed9126de | ||
|
|
a07efc5209 | ||
|
|
bbeeb79812 | ||
|
|
4d08e9d72b | ||
|
|
040f260bf0 | ||
|
|
cdf9c28a38 | ||
|
|
a6972f62f2 | ||
|
|
1db32470b1 | ||
|
|
ed022412fe | ||
|
|
a2356b62c7 | ||
|
|
9aa6117fb0 | ||
|
|
b9b85a58ac |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -11,3 +11,9 @@
|
||||
.classpath
|
||||
target/
|
||||
test-output/
|
||||
|
||||
# IntelliJ Settings Files #
|
||||
.idea/
|
||||
out/
|
||||
.idea_modules/
|
||||
*.iws
|
||||
|
||||
57
.travis.yml
57
.travis.yml
@@ -1,11 +1,50 @@
|
||||
language: java
|
||||
sudo: required
|
||||
dist: trusty
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
script: mvn -fmain/pom.xml clean package
|
||||
notifications:
|
||||
webhooks:
|
||||
urls:
|
||||
- https://webhooks.gitter.im/e/7d429ab35361726e26f2
|
||||
on_success: change # options: [always|never|change] default: always
|
||||
on_failure: always # options: [always|never|change] default: always
|
||||
on_start: false # default: false
|
||||
- oraclejdk8
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.m2
|
||||
env:
|
||||
global:
|
||||
- secure: "IfYURwZaDWuBDvyn47n0k1Zod/IQw1FF+CS5nnV08Q+NfC3vGGJMwV8m59XnbfwnWGxwvCaAbk4qP6s6+ijgZNKkvgfFMo3rfTok5zt43bIqgaFOANYV+OC/1c59gYD6ZUxhW5iNgMgU3qdsRtJuwSmfkVv/jKyLGfAbS4kN8BA=" # COVERITY_SCAN_TOKEN
|
||||
- secure: "lV9OwUbHMrMpLUH1CY+Z4puLDdFXytudyPlG1eGRsesdpuG6KM3uQVz6uAtf6lrU8DRbMM/T7ML+PmvQ4UoPPYLdLxESLLBat2qUPOIVBOhTSlCc7I0DmGy04CSvkeMy8dPaQC0ukgNiR7zwoNzfcpGRN/U9S8tziDruuHoZSrg=" # BINTRAY_API_KEY
|
||||
- secure: "oWFgRTVP6lyTa7qVxlvkpm20MtVc3BtmsNXQJS6bfg2A0o/iCQMNx7OD59BaafCLGRKvCcJVESiC8FlSylVMS7CDSyYu0gg70NUiIuHp4NBM5inFWYCy/PdQsCTzr5uvNG+rMFQpMFRaCV0FrfM3tLondcVkhsHL68l93Xoexx4=" # CODACY_PROJECT_TOKEN
|
||||
addons:
|
||||
coverity_scan:
|
||||
project:
|
||||
name: "cryptomator/cryptomator"
|
||||
notification_email: sebastian.stenzel@cryptomator.org
|
||||
build_command: "mvn -fmain/pom.xml clean test -DskipTests"
|
||||
branch_pattern: release.*
|
||||
install:
|
||||
# "clean" needed until https://bugs.openjdk.java.net/browse/JDK-8067747 is resolved.
|
||||
- mvn -fmain/pom.xml clean package -DskipTests dependency:go-offline -Pcoverage
|
||||
- mvn -fmain/pom.xml clean package -DskipTests dependency:go-offline -Prelease
|
||||
script:
|
||||
- mvn --update-snapshots -fmain/pom.xml clean test jacoco:report verify -Pcoverage
|
||||
before_deploy:
|
||||
- mvn -fmain/pom.xml -Prelease clean package -DskipTests
|
||||
deploy:
|
||||
- provider: releases
|
||||
prerelease: false
|
||||
api_key:
|
||||
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
|
||||
tags: true
|
||||
- provider: script
|
||||
script: "curl -X POST -u cryptobot:${BINTRAY_API_KEY} -H 'Content-Type: application/json' -d '{\"name\": \"${TRAVIS_TAG}\", \"vcs_tag\": \"${TRAVIS_TAG}\"}' https://api.bintray.com/packages/cryptomator/cryptomator/cryptomator-win/versions"
|
||||
on:
|
||||
repo: cryptomator/cryptomator
|
||||
tags: true
|
||||
- provider: script
|
||||
script: "curl -X POST -u cryptobot:${BINTRAY_API_KEY} -H 'Content-Type: application/json' -d '{\"name\": \"${TRAVIS_TAG}\", \"vcs_tag\": \"${TRAVIS_TAG}\"}' https://api.bintray.com/packages/cryptomator/cryptomator/cryptomator-osx/versions"
|
||||
on:
|
||||
repo: cryptomator/cryptomator
|
||||
tags: true
|
||||
|
||||
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/
|
||||
38
CONTRIBUTING.md
Normal file
38
CONTRIBUTING.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# 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 and Android app can be reported on the [Cryptomator for iOS issues list](https://github.com/cryptomator/cryptomator-ios/issues) and [Cryptomator for Android issues list](https://github.com/cryptomator/cryptomator-android/issues) respectively.
|
||||
- Ensure the bug was not [already reported](https://github.com/cryptomator/cryptomator/issues). You can also check out our [knowledge base](https://cryptomator.freshdesk.com/support/solutions) 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).
|
||||
|
||||
## Do you miss a feature?
|
||||
|
||||
- Ensure the feature was not [already requested](https://github.com/cryptomator/cryptomator/issues).
|
||||
- You're welcome to suggest a feature by [submitting a new issue](https://github.com/cryptomator/cryptomator/issues/new).
|
||||
|
||||
## 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
|
||||
38
ISSUE_TEMPLATE.md
Normal file
38
ISSUE_TEMPLATE.md
Normal file
@@ -0,0 +1,38 @@
|
||||
To tick a checkbox replace [ ] with [x]. Make sure to replace placeholders (…) accordingly.
|
||||
|
||||
## Issue Checklist
|
||||
|
||||
Before creating a new issue make sure that you
|
||||
- [ ] searched [existing (and closed) issues](https://github.com/cryptomator/cryptomator/issues).
|
||||
- [ ] searched the [knowledge base](https://cryptomator.freshdesk.com/support/solutions).
|
||||
- [ ] have read the [contribution guide](https://github.com/cryptomator/cryptomator/blob/master/CONTRIBUTING.md).
|
||||
- [ ] have read the [Code of Conduct](https://github.com/cryptomator/cryptomator/blob/master/CODE_OF_CONDUCT.md).
|
||||
|
||||
## Basic Info
|
||||
|
||||
This is a
|
||||
- [ ] bug report.
|
||||
- [ ] feature request.
|
||||
- [ ] question or something else.
|
||||
|
||||
I'm using
|
||||
- [ ] Windows in version: …
|
||||
- [ ] macOS in version: …
|
||||
- [ ] Linux in version: …
|
||||
|
||||
I'm running Cryptomator in version: …
|
||||
(You can check the version in the Cryptomator settings.)
|
||||
|
||||
## Description
|
||||
|
||||
…
|
||||
(Please describe in detail what you did, what you expected, and what really happened.)
|
||||
|
||||
## Attachments (optional)
|
||||
|
||||
If you want to add the log file or screenshots, please add them as attachments. If your log file seems empty and doesn't show any errors, you may enable the [debug mode](https://cryptomator.freshdesk.com/support/solutions/articles/16000046480) first and reproduce the problem to ensure all important information is contained in there. You may use test data or redact sensitive information from the log file.
|
||||
|
||||
You can find the log file
|
||||
- on Windows: %appdata%/Cryptomator/cryptomator.log
|
||||
- on macOS: ~/Library/Logs/Cryptomator/cryptomator.log
|
||||
- on Linux: ~/.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.
|
||||
|
||||
83
README.md
83
README.md
@@ -1,55 +1,70 @@
|
||||
Cryptomator
|
||||
====================
|
||||

|
||||
|
||||
[](https://travis-ci.org/totalvoidness/cryptomator)
|
||||
[](https://gitter.im/totalvoidness/cryptomator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://travis-ci.org/cryptomator/cryptomator)
|
||||
[](https://scan.coverity.com/projects/cryptomator-cryptomator)
|
||||
[](https://www.codacy.com/app/cryptomator/cryptomator?utm_source=github.com&utm_medium=referral&utm_content=cryptomator/cryptomator&utm_campaign=Badge_Grade)
|
||||
[](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, Nextcloud and any other cloud storage service which synchronizes with a local directory
|
||||
- Open Source means: No backdoors, control is better than trust
|
||||
- Client-side: No accounts, no data shared with any online service
|
||||
- Totally transparent: Just work on the virtual drive as if it were a USB flash drive
|
||||
- AES encryption with 256-bit key length
|
||||
- File names get encrypted
|
||||
- Folder structure gets obfuscated
|
||||
- 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 (min. 8u51, we recommend to use the current version)
|
||||
* [JCE unlimited strength policy files](http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html) (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/totalvoidness/cryptomator.git
|
||||
cd cryptomator/main
|
||||
git checkout v0.5.1
|
||||
mvn clean install
|
||||
```
|
||||
cd main
|
||||
mvn clean install -Prelease
|
||||
```
|
||||
|
||||
An executable jar file will be created inside `main/uber-jar/target`.
|
||||
|
||||
## 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.3.0-rc4</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>launcher</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>3.0.2</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>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<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>
|
||||
72
main/ant-kit/src/main/resources/build.xml
Normal file
72
main/ant-kit/src/main/resources/build.xml
Normal file
@@ -0,0 +1,72 @@
|
||||
<?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.launcher.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="launcher-${project.version}.jar" />
|
||||
<fx:resources>
|
||||
<fx:fileset dir="libs" type="jar" includes="*.jar" excludes="launcher-${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 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:property name="cryptomator.upgradeLogPath" value="~/.Cryptomator/upgrade.log" />
|
||||
<fx:property name="cryptomator.settingsPath" value="~/.Cryptomator/settings.json" />
|
||||
<fx:property name="cryptomator.ipcPortPath" value="~/.Cryptomator/ipcPort.bin" />
|
||||
<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="launcher-${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:property name="cryptomator.upgradeLogPath" value="~/.Cryptomator/upgrade.log" />
|
||||
<fx:property name="cryptomator.settingsPath" value="~/.Cryptomator/settings.json" />
|
||||
<fx:property name="cryptomator.ipcPortPath" value="~/.Cryptomator/ipcPort.bin" />
|
||||
<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="launcher-${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>
|
||||
|
||||
</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 |
@@ -9,7 +9,7 @@ Priority: optional
|
||||
Architecture: APPLICATION_ARCH
|
||||
Provides: APPLICATION_PACKAGE
|
||||
Installed-Size: APPLICATION_INSTALLED_SIZE
|
||||
Depends: gvfs-bin, gvfs-backends, gvfs-fuse, xdg-utils
|
||||
Depends: gvfs-bin, gvfs-backends, gvfs-fuse
|
||||
Description: Multi-platform client-side encryption of your cloud files.
|
||||
Cryptomator provides free client-side AES encryption for your cloud files.
|
||||
Create encrypted vaults, which get mounted as virtual volumes. Whatever
|
||||
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
|
||||
56
main/commons/pom.xml
Normal file
56
main/commons/pom.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?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.3.0-rc4</version>
|
||||
</parent>
|
||||
<artifactId>commons</artifactId>
|
||||
<name>Cryptomator Commons</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>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.fxmisc.easybind</groupId>
|
||||
<artifactId>easybind</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>
|
||||
|
||||
<!-- Logging -->
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -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 Throwable> {
|
||||
|
||||
void accept(T t) throws E;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
|
||||
public final class LazyInitializer {
|
||||
|
||||
private LazyInitializer() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #initializeLazily(AtomicReference, SupplierThrowingException, Class)} except that no checked exception may be thrown by the factory function.
|
||||
*
|
||||
* @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) {
|
||||
SupplierThrowingException<T, RuntimeException> factoryThrowingRuntimeExceptions = () -> factory.get();
|
||||
return initializeLazily(reference, factoryThrowingRuntimeExceptions, RuntimeException.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Threadsafe lazy initialization pattern as proposed on http://stackoverflow.com/a/30247202/4014509
|
||||
*
|
||||
* @param <T> Type of the value
|
||||
* @param <E> Type of the any expected exception that may occur during initialization
|
||||
* @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.
|
||||
* @param exceptionType Expected exception type.
|
||||
* @return The initialized value
|
||||
* @throws E Exception thrown by the factory function.
|
||||
*/
|
||||
public static <T, E extends Exception> T initializeLazily(AtomicReference<T> reference, SupplierThrowingException<T, E> factory, Class<E> exceptionType) throws E {
|
||||
final T existing = reference.get();
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
} else {
|
||||
try {
|
||||
return reference.updateAndGet(invokeFactoryIfNull(factory));
|
||||
} catch (InitializationException e) {
|
||||
Throwables.throwIfUnchecked(e);
|
||||
Throwables.throwIfInstanceOf(e.getCause(), exceptionType);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static <T, E extends Exception> UnaryOperator<T> invokeFactoryIfNull(SupplierThrowingException<T, E> factory) throws InitializationException {
|
||||
return currentValue -> {
|
||||
if (currentValue == null) {
|
||||
try {
|
||||
return factory.get();
|
||||
} catch (Exception e) {
|
||||
Throwables.throwIfUnchecked(e); // don't catch unchecked exceptions
|
||||
throw new InitializationException(e);
|
||||
}
|
||||
} else {
|
||||
return currentValue;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static class InitializationException extends RuntimeException {
|
||||
|
||||
public InitializationException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
public final class Optionals {
|
||||
|
||||
private Optionals() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function that is equivalent to the input function but immediately gets the value of the returned optional when invoked.
|
||||
*
|
||||
* @param <T> the type of the input to the function
|
||||
* @param <R> the type of the result of the function
|
||||
* @param function An {@code Optional}-bearing input function {@code Function<Foo, Optional<Bar>>}
|
||||
* @return A {@code Function<Foo, Bar>}, that may throw a NoSuchElementException, if the original function returns an empty optional.
|
||||
*/
|
||||
public static <T, R> Function<T, R> unwrap(Function<T, Optional<R>> function) {
|
||||
return t -> function.apply(t).get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface RunnableThrowingException<T extends Throwable> {
|
||||
|
||||
void run() throws T;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* Compares version strings according to <a href="http://semver.org/spec/v2.0.0.html">SemVer 2.0.0</a>.
|
||||
*/
|
||||
public class SemVerComparator implements Comparator<String> {
|
||||
|
||||
private static final char VERSION_SEP = '.'; // http://semver.org/spec/v2.0.0.html#spec-item-2
|
||||
private static final String PRE_RELEASE_SEP = "-"; // http://semver.org/spec/v2.0.0.html#spec-item-9
|
||||
private static final String BUILD_SEP = "+"; // http://semver.org/spec/v2.0.0.html#spec-item-10
|
||||
|
||||
@Override
|
||||
public int compare(String version1, String version2) {
|
||||
// "Build metadata SHOULD be ignored when determining version precedence.
|
||||
// Thus two versions that differ only in the build metadata, have the same precedence."
|
||||
String v1WithoutBuildMetadata = StringUtils.substringBefore(version1, BUILD_SEP);
|
||||
String v2WithoutBuildMetadata = StringUtils.substringBefore(version2, BUILD_SEP);
|
||||
|
||||
if (v1WithoutBuildMetadata.equals(v2WithoutBuildMetadata)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
String v1MajorMinorPatch = StringUtils.substringBefore(v1WithoutBuildMetadata, PRE_RELEASE_SEP);
|
||||
String v2MajorMinorPatch = StringUtils.substringBefore(v2WithoutBuildMetadata, PRE_RELEASE_SEP);
|
||||
String v1PreReleaseVersion = StringUtils.substringAfter(v1WithoutBuildMetadata, PRE_RELEASE_SEP);
|
||||
String v2PreReleaseVersion = StringUtils.substringAfter(v2WithoutBuildMetadata, PRE_RELEASE_SEP);
|
||||
return compare(v1MajorMinorPatch, v1PreReleaseVersion, v2MajorMinorPatch, v2PreReleaseVersion);
|
||||
}
|
||||
|
||||
private int compare(String v1MajorMinorPatch, String v1PreReleaseVersion, String v2MajorMinorPatch, String v2PreReleaseVersion) {
|
||||
int comparisonResult = compareNumericallyThenLexicographically(v1MajorMinorPatch, v2MajorMinorPatch);
|
||||
if (comparisonResult == 0) {
|
||||
if (v1PreReleaseVersion.isEmpty()) {
|
||||
return 1; // 1.0.0 > 1.0.0-BETA
|
||||
} else if (v2PreReleaseVersion.isEmpty()) {
|
||||
return -1; // 1.0.0-BETA < 1.0.0
|
||||
} else {
|
||||
return compareNumericallyThenLexicographically(v1PreReleaseVersion, v2PreReleaseVersion);
|
||||
}
|
||||
} else {
|
||||
return comparisonResult;
|
||||
}
|
||||
}
|
||||
|
||||
private int compareNumericallyThenLexicographically(String version1, String version2) {
|
||||
final String[] vComps1 = StringUtils.split(version1, VERSION_SEP);
|
||||
final String[] vComps2 = StringUtils.split(version2, VERSION_SEP);
|
||||
final int commonCompCount = Math.min(vComps1.length, vComps2.length);
|
||||
|
||||
for (int i = 0; i < commonCompCount; i++) {
|
||||
int subversionComparisionResult = 0;
|
||||
try {
|
||||
final int v1 = Integer.parseInt(vComps1[i]);
|
||||
final int v2 = Integer.parseInt(vComps2[i]);
|
||||
subversionComparisionResult = v1 - v2;
|
||||
} catch (NumberFormatException ex) {
|
||||
// ok, lets compare this fragment lexicographically
|
||||
subversionComparisionResult = vComps1[i].compareTo(vComps2[i]);
|
||||
}
|
||||
if (subversionComparisionResult != 0) {
|
||||
return subversionComparisionResult;
|
||||
}
|
||||
}
|
||||
|
||||
// all in common so far? longest version string is considered the higher version:
|
||||
return vComps1.length - vComps2.length;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface SupplierThrowingException<T, E extends Throwable> {
|
||||
|
||||
T get() throws E;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014, 2016 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.common.settings;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
public class Settings {
|
||||
|
||||
public static final int MIN_PORT = 1024;
|
||||
public static final int MAX_PORT = 65535;
|
||||
public static final boolean DEFAULT_CHECK_FOR_UDPATES = true;
|
||||
public static final int DEFAULT_PORT = 42427;
|
||||
public static final boolean DEFAULT_USE_IPV6 = SystemUtils.IS_OS_WINDOWS;
|
||||
public static final int DEFAULT_NUM_TRAY_NOTIFICATIONS = 3;
|
||||
public static final String DEFAULT_GVFS_SCHEME = "dav";
|
||||
public static final boolean DEFAULT_DEBUG_MODE = false;
|
||||
|
||||
private final ObservableList<VaultSettings> directories = FXCollections.observableArrayList(VaultSettings::observables);
|
||||
private final BooleanProperty checkForUpdates = new SimpleBooleanProperty(DEFAULT_CHECK_FOR_UDPATES);
|
||||
private final IntegerProperty port = new SimpleIntegerProperty(DEFAULT_PORT);
|
||||
private final BooleanProperty useIpv6 = new SimpleBooleanProperty(DEFAULT_USE_IPV6);
|
||||
private final IntegerProperty numTrayNotifications = new SimpleIntegerProperty(DEFAULT_NUM_TRAY_NOTIFICATIONS);
|
||||
private final StringProperty preferredGvfsScheme = new SimpleStringProperty(DEFAULT_GVFS_SCHEME);
|
||||
private final BooleanProperty debugMode = new SimpleBooleanProperty(DEFAULT_DEBUG_MODE);
|
||||
private Consumer<Settings> saveCmd;
|
||||
|
||||
/**
|
||||
* Package-private constructor; use {@link SettingsProvider}.
|
||||
*/
|
||||
Settings() {
|
||||
directories.addListener((ListChangeListener.Change<? extends VaultSettings> change) -> this.save());
|
||||
checkForUpdates.addListener(this::somethingChanged);
|
||||
port.addListener(this::somethingChanged);
|
||||
useIpv6.addListener(this::somethingChanged);
|
||||
numTrayNotifications.addListener(this::somethingChanged);
|
||||
preferredGvfsScheme.addListener(this::somethingChanged);
|
||||
debugMode.addListener(this::somethingChanged);
|
||||
}
|
||||
|
||||
void setSaveCmd(Consumer<Settings> saveCmd) {
|
||||
this.saveCmd = saveCmd;
|
||||
}
|
||||
|
||||
private void somethingChanged(ObservableValue<?> observable, Object oldValue, Object newValue) {
|
||||
this.save();
|
||||
}
|
||||
|
||||
void save() {
|
||||
if (saveCmd != null) {
|
||||
saveCmd.accept(this);
|
||||
}
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public ObservableList<VaultSettings> getDirectories() {
|
||||
return directories;
|
||||
}
|
||||
|
||||
public BooleanProperty checkForUpdates() {
|
||||
return checkForUpdates;
|
||||
}
|
||||
|
||||
public IntegerProperty port() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public BooleanProperty useIpv6() {
|
||||
return useIpv6;
|
||||
}
|
||||
|
||||
public IntegerProperty numTrayNotifications() {
|
||||
return numTrayNotifications;
|
||||
}
|
||||
|
||||
public StringProperty preferredGvfsScheme() {
|
||||
return preferredGvfsScheme;
|
||||
}
|
||||
|
||||
public BooleanProperty debugMode() {
|
||||
return debugMode;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonToken;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
|
||||
public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SettingsJsonAdapter.class);
|
||||
|
||||
private final VaultSettingsJsonAdapter vaultSettingsJsonAdapter = new VaultSettingsJsonAdapter();
|
||||
|
||||
@Override
|
||||
public void write(JsonWriter out, Settings value) throws IOException {
|
||||
out.beginObject();
|
||||
out.name("directories");
|
||||
writeVaultSettingsArray(out, value.getDirectories());
|
||||
out.name("checkForUpdatesEnabled").value(value.checkForUpdates().get());
|
||||
out.name("port").value(value.port().get());
|
||||
out.name("useIpv6").value(value.useIpv6().get());
|
||||
out.name("numTrayNotifications").value(value.numTrayNotifications().get());
|
||||
out.name("preferredGvfsScheme").value(value.preferredGvfsScheme().get());
|
||||
out.name("debugMode").value(value.debugMode().get());
|
||||
out.endObject();
|
||||
}
|
||||
|
||||
private void writeVaultSettingsArray(JsonWriter out, Iterable<VaultSettings> vaultSettings) throws IOException {
|
||||
out.beginArray();
|
||||
for (VaultSettings value : vaultSettings) {
|
||||
vaultSettingsJsonAdapter.write(out, value);
|
||||
}
|
||||
out.endArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Settings read(JsonReader in) throws IOException {
|
||||
Settings settings = new Settings();
|
||||
|
||||
in.beginObject();
|
||||
while (in.hasNext()) {
|
||||
String name = in.nextName();
|
||||
switch (name) {
|
||||
case "directories":
|
||||
settings.getDirectories().addAll(readVaultSettingsArray(in));
|
||||
break;
|
||||
case "checkForUpdatesEnabled":
|
||||
settings.checkForUpdates().set(in.nextBoolean());
|
||||
break;
|
||||
case "port":
|
||||
settings.port().set(in.nextInt());
|
||||
break;
|
||||
case "useIpv6":
|
||||
// Temporarily we will disable loading this setting, as we want the default value on each app start.
|
||||
// This setting might be removed completely in the future
|
||||
// settings.useIpv6().set(in.nextBoolean());
|
||||
in.skipValue();
|
||||
break;
|
||||
case "numTrayNotifications":
|
||||
settings.numTrayNotifications().set(in.nextInt());
|
||||
break;
|
||||
case "preferredGvfsScheme":
|
||||
settings.preferredGvfsScheme().set(in.nextString());
|
||||
break;
|
||||
case "debugMode":
|
||||
settings.debugMode().set(in.nextBoolean());
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unsupported vault setting found in JSON: " + name);
|
||||
in.skipValue();
|
||||
}
|
||||
}
|
||||
in.endObject();
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
private List<VaultSettings> readVaultSettingsArray(JsonReader in) throws IOException {
|
||||
List<VaultSettings> result = new ArrayList<>();
|
||||
in.beginArray();
|
||||
while (!JsonToken.END_ARRAY.equals(in.peek())) {
|
||||
result.add(vaultSettingsJsonAdapter.read(in));
|
||||
}
|
||||
in.endArray();
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*******************************************************************************
|
||||
* 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.settings;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.LazyInitializer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
@Singleton
|
||||
public class SettingsProvider implements Provider<Settings> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SettingsProvider.class);
|
||||
private static final Path DEFAULT_SETTINGS_PATH;
|
||||
private static final long SAVE_DELAY_MS = 1000;
|
||||
|
||||
static {
|
||||
final FileSystem fs = FileSystems.getDefault();
|
||||
if (SystemUtils.IS_OS_WINDOWS) {
|
||||
DEFAULT_SETTINGS_PATH = fs.getPath(SystemUtils.USER_HOME, "AppData/Roaming/Cryptomator/settings.json");
|
||||
} else if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
DEFAULT_SETTINGS_PATH = fs.getPath(SystemUtils.USER_HOME, "Library/Application Support/Cryptomator/settings.json");
|
||||
} else {
|
||||
DEFAULT_SETTINGS_PATH = fs.getPath(SystemUtils.USER_HOME, ".Cryptomator/settings.json");
|
||||
}
|
||||
}
|
||||
|
||||
private final ScheduledExecutorService saveScheduler = Executors.newSingleThreadScheduledExecutor();
|
||||
private final AtomicReference<ScheduledFuture<?>> scheduledSaveCmd = new AtomicReference<>();
|
||||
private final AtomicReference<Settings> settings = new AtomicReference<>();
|
||||
private final SettingsJsonAdapter settingsJsonAdapter = new SettingsJsonAdapter();
|
||||
private final Gson gson;
|
||||
|
||||
@Inject
|
||||
public SettingsProvider() {
|
||||
this.gson = new GsonBuilder() //
|
||||
.setPrettyPrinting().setLenient().disableHtmlEscaping() //
|
||||
.registerTypeAdapter(Settings.class, settingsJsonAdapter) //
|
||||
.create();
|
||||
}
|
||||
|
||||
private Path getSettingsPath() {
|
||||
final String settingsPathProperty = System.getProperty("cryptomator.settingsPath");
|
||||
return Optional.ofNullable(settingsPathProperty).filter(StringUtils::isNotBlank).map(this::replaceHomeDir).map(FileSystems.getDefault()::getPath).orElse(DEFAULT_SETTINGS_PATH);
|
||||
}
|
||||
|
||||
private String replaceHomeDir(String path) {
|
||||
if (path.startsWith("~/")) {
|
||||
return SystemUtils.USER_HOME + path.substring(1);
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Settings get() {
|
||||
return LazyInitializer.initializeLazily(settings, this::load);
|
||||
}
|
||||
|
||||
private Settings load() {
|
||||
Settings settings;
|
||||
final Path settingsPath = getSettingsPath();
|
||||
try (InputStream in = Files.newInputStream(settingsPath, StandardOpenOption.READ); //
|
||||
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
|
||||
settings = gson.fromJson(reader, Settings.class);
|
||||
LOG.info("Settings loaded from " + settingsPath);
|
||||
} catch (IOException e) {
|
||||
LOG.info("Failed to load settings, creating new one.");
|
||||
settings = new Settings();
|
||||
}
|
||||
settings.setSaveCmd(this::scheduleSave);
|
||||
return settings;
|
||||
}
|
||||
|
||||
private void scheduleSave(Settings settings) {
|
||||
if (settings == null) {
|
||||
return;
|
||||
}
|
||||
ScheduledFuture<?> saveCmd = saveScheduler.schedule(() -> {
|
||||
this.save(settings);
|
||||
}, SAVE_DELAY_MS, TimeUnit.MILLISECONDS);
|
||||
ScheduledFuture<?> previousSaveCmd = scheduledSaveCmd.getAndSet(saveCmd);
|
||||
if (previousSaveCmd != null) {
|
||||
previousSaveCmd.cancel(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void save(Settings settings) {
|
||||
assert settings != null : "method should only be invoked by #scheduleSave, which checks for null";
|
||||
final Path settingsPath = getSettingsPath();
|
||||
try {
|
||||
Files.createDirectories(settingsPath.getParent());
|
||||
try (OutputStream out = Files.newOutputStream(settingsPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); //
|
||||
Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) {
|
||||
gson.toJson(settings, writer);
|
||||
LOG.info("Settings saved to " + settingsPath);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to save settings.", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Base64;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
public class VaultSettings {
|
||||
|
||||
public static final boolean DEFAULT_UNLOCK_AFTER_STARTUP = false;
|
||||
public static final boolean DEFAULT_MOUNT_AFTER_UNLOCK = true;
|
||||
public static final boolean DEFAULT_REAVEAL_AFTER_MOUNT = true;
|
||||
|
||||
private final String id;
|
||||
private final ObjectProperty<Path> path = new SimpleObjectProperty<>();
|
||||
private final StringProperty mountName = new SimpleStringProperty();
|
||||
private final StringProperty winDriveLetter = new SimpleStringProperty();
|
||||
private final BooleanProperty unlockAfterStartup = new SimpleBooleanProperty(DEFAULT_UNLOCK_AFTER_STARTUP);
|
||||
private final BooleanProperty mountAfterUnlock = new SimpleBooleanProperty(DEFAULT_MOUNT_AFTER_UNLOCK);
|
||||
private final BooleanProperty revealAfterMount = new SimpleBooleanProperty(DEFAULT_REAVEAL_AFTER_MOUNT);
|
||||
|
||||
public VaultSettings(String id) {
|
||||
this.id = Objects.requireNonNull(id);
|
||||
|
||||
EasyBind.subscribe(path, this::deriveMountNameFromPath);
|
||||
}
|
||||
|
||||
Observable[] observables() {
|
||||
return new Observable[] {path, mountName, winDriveLetter, unlockAfterStartup, mountAfterUnlock, revealAfterMount};
|
||||
}
|
||||
|
||||
private void deriveMountNameFromPath(Path path) {
|
||||
if (path != null && StringUtils.isBlank(mountName.get())) {
|
||||
mountName.set(normalizeMountName(path.getFileName().toString()));
|
||||
}
|
||||
}
|
||||
|
||||
public static VaultSettings withRandomId() {
|
||||
return new VaultSettings(generateId());
|
||||
}
|
||||
|
||||
private static String generateId() {
|
||||
return asBase64String(nineBytesFrom(UUID.randomUUID()));
|
||||
}
|
||||
|
||||
private static String asBase64String(byte[] bytes) {
|
||||
byte[] base64Bytes = Base64.getUrlEncoder().encode(bytes);
|
||||
return new String(base64Bytes, StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
private static byte[] nineBytesFrom(UUID uuid) {
|
||||
ByteBuffer uuidBuffer = ByteBuffer.allocate(9);
|
||||
uuidBuffer.putLong(uuid.getMostSignificantBits());
|
||||
uuidBuffer.put((byte) (uuid.getLeastSignificantBits() & 0xFF));
|
||||
uuidBuffer.flip();
|
||||
return uuidBuffer.array();
|
||||
}
|
||||
|
||||
public static String normalizeMountName(String mountName) {
|
||||
String normalizedMountName = StringUtils.stripAccents(mountName);
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (char c : normalizedMountName.toCharArray()) {
|
||||
if (Character.isWhitespace(c)) {
|
||||
if (builder.length() == 0 || builder.charAt(builder.length() - 1) != '_') {
|
||||
builder.append('_');
|
||||
}
|
||||
} else if (c < 127 && Character.isLetterOrDigit(c)) {
|
||||
builder.append(c);
|
||||
} else {
|
||||
if (builder.length() == 0 || builder.charAt(builder.length() - 1) != '_') {
|
||||
builder.append('_');
|
||||
}
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public ObjectProperty<Path> path() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public StringProperty mountName() {
|
||||
return mountName;
|
||||
}
|
||||
|
||||
public StringProperty winDriveLetter() {
|
||||
return winDriveLetter;
|
||||
}
|
||||
|
||||
public BooleanProperty unlockAfterStartup() {
|
||||
return unlockAfterStartup;
|
||||
}
|
||||
|
||||
public BooleanProperty mountAfterUnlock() {
|
||||
return mountAfterUnlock;
|
||||
}
|
||||
|
||||
public BooleanProperty revealAfterMount() {
|
||||
return revealAfterMount;
|
||||
}
|
||||
|
||||
/* Hashcode/Equals */
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof VaultSettings && obj.getClass().equals(this.getClass())) {
|
||||
VaultSettings other = (VaultSettings) obj;
|
||||
return Objects.equals(this.id, other.id);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
|
||||
class VaultSettingsJsonAdapter {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(VaultSettingsJsonAdapter.class);
|
||||
|
||||
public void write(JsonWriter out, VaultSettings value) throws IOException {
|
||||
out.beginObject();
|
||||
out.name("id").value(value.getId());
|
||||
out.name("path").value(value.path().get().toString());
|
||||
out.name("mountName").value(value.mountName().get());
|
||||
out.name("winDriveLetter").value(value.winDriveLetter().get());
|
||||
out.name("unlockAfterStartup").value(value.unlockAfterStartup().get());
|
||||
out.name("mountAfterUnlock").value(value.mountAfterUnlock().get());
|
||||
out.name("revealAfterMount").value(value.revealAfterMount().get());
|
||||
out.endObject();
|
||||
}
|
||||
|
||||
public VaultSettings read(JsonReader in) throws IOException {
|
||||
String id = null;
|
||||
String path = null;
|
||||
String mountName = null;
|
||||
String winDriveLetter = null;
|
||||
boolean unlockAfterStartup = VaultSettings.DEFAULT_UNLOCK_AFTER_STARTUP;
|
||||
boolean mountAfterUnlock = VaultSettings.DEFAULT_MOUNT_AFTER_UNLOCK;
|
||||
boolean revealAfterMount = VaultSettings.DEFAULT_REAVEAL_AFTER_MOUNT;
|
||||
|
||||
in.beginObject();
|
||||
while (in.hasNext()) {
|
||||
String name = in.nextName();
|
||||
switch (name) {
|
||||
case "id":
|
||||
id = in.nextString();
|
||||
break;
|
||||
case "path":
|
||||
path = in.nextString();
|
||||
break;
|
||||
case "mountName":
|
||||
mountName = in.nextString();
|
||||
break;
|
||||
case "winDriveLetter":
|
||||
winDriveLetter = in.nextString();
|
||||
break;
|
||||
case "unlockAfterStartup":
|
||||
unlockAfterStartup = in.nextBoolean();
|
||||
break;
|
||||
case "mountAfterUnlock":
|
||||
mountAfterUnlock = in.nextBoolean();
|
||||
break;
|
||||
case "revealAfterMount":
|
||||
revealAfterMount = in.nextBoolean();
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unsupported vault setting found in JSON: " + name);
|
||||
in.skipValue();
|
||||
}
|
||||
}
|
||||
in.endObject();
|
||||
|
||||
VaultSettings vaultSettings = (id == null) ? VaultSettings.withRandomId() : new VaultSettings(id);
|
||||
vaultSettings.mountName().set(mountName);
|
||||
vaultSettings.path().set(Paths.get(path));
|
||||
vaultSettings.winDriveLetter().set(winDriveLetter);
|
||||
vaultSettings.unlockAfterStartup().set(unlockAfterStartup);
|
||||
vaultSettings.mountAfterUnlock().set(mountAfterUnlock);
|
||||
vaultSettings.revealAfterMount().set(revealAfterMount);
|
||||
return vaultSettings;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class SemVerComparatorTest {
|
||||
|
||||
private final Comparator<String> semVerComparator = new SemVerComparator();
|
||||
|
||||
// equal versions
|
||||
|
||||
@Test
|
||||
public void compareEqualVersions() {
|
||||
Assert.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4")));
|
||||
Assert.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-alpha")));
|
||||
Assert.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4+20170101", "1.23.4+20171231")));
|
||||
Assert.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4-alpha+20170101", "1.23.4-alpha+20171231")));
|
||||
}
|
||||
|
||||
// newer versions in first argument
|
||||
|
||||
@Test
|
||||
public void compareHigherToLowerVersions() {
|
||||
Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.5", "1.23.4")));
|
||||
Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.24.4", "1.23.4")));
|
||||
Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23")));
|
||||
Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4-SNAPSHOT")));
|
||||
Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4-56.78")));
|
||||
Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-beta", "1.23.4-alpha")));
|
||||
Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-alpha.1", "1.23.4-alpha")));
|
||||
Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-56.79", "1.23.4-56.78")));
|
||||
}
|
||||
|
||||
// newer versions in second argument
|
||||
|
||||
@Test
|
||||
public void compareLowerToHigherVersions() {
|
||||
Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.5")));
|
||||
Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4", "1.24.4")));
|
||||
Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23", "1.23.4")));
|
||||
Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-SNAPSHOT", "1.23.4")));
|
||||
Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-56.78", "1.23.4")));
|
||||
Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-beta")));
|
||||
Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-alpha.1")));
|
||||
Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-56.78", "1.23.4-56.79")));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class SettingsJsonAdapterTest {
|
||||
|
||||
private final SettingsJsonAdapter adapter = new SettingsJsonAdapter();
|
||||
|
||||
@Test
|
||||
public void testDeserialize() throws IOException {
|
||||
String vault1Json = "{\"id\": \"1\", \"path\": \"/vault1\", \"mountName\": \"vault1\", \"winDriveLetter\": \"X\"}";
|
||||
String vault2Json = "{\"id\": \"2\", \"path\": \"/vault2\", \"mountName\": \"vault2\", \"winDriveLetter\": \"Y\"}";
|
||||
String json = "{\"directories\": [" + vault1Json + "," + vault2Json + "]," //
|
||||
+ "\"checkForUpdatesEnabled\": true,"//
|
||||
+ "\"port\": 8080,"//
|
||||
+ "\"useIpv6\": true,"//
|
||||
+ "\"numTrayNotifications\": 42}";
|
||||
|
||||
Settings settings = adapter.fromJson(json);
|
||||
|
||||
Assert.assertTrue(settings.checkForUpdates().get());
|
||||
Assert.assertEquals(2, settings.getDirectories().size());
|
||||
Assert.assertEquals(8080, settings.port().get());
|
||||
// Assert.assertTrue(settings.useIpv6().get()); temporarily ignored
|
||||
Assert.assertEquals(42, settings.numTrayNotifications().get());
|
||||
Assert.assertEquals("dav", settings.preferredGvfsScheme().get());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
public class SettingsTest {
|
||||
|
||||
@Test
|
||||
public void testAutoSave() throws IOException {
|
||||
@SuppressWarnings("unchecked")
|
||||
Consumer<Settings> changeListener = Mockito.mock(Consumer.class);
|
||||
Settings settings = new Settings();
|
||||
settings.setSaveCmd(changeListener);
|
||||
VaultSettings vaultSettings = VaultSettings.withRandomId();
|
||||
Mockito.verify(changeListener, Mockito.times(0)).accept(settings);
|
||||
|
||||
// first change (to property):
|
||||
settings.preferredGvfsScheme().set("asd");
|
||||
Mockito.verify(changeListener, Mockito.times(1)).accept(settings);
|
||||
|
||||
// second change (to list):
|
||||
settings.getDirectories().add(vaultSettings);
|
||||
Mockito.verify(changeListener, Mockito.times(2)).accept(settings);
|
||||
|
||||
// third change (to property of list item):
|
||||
vaultSettings.mountName().set("asd");
|
||||
Mockito.verify(changeListener, Mockito.times(3)).accept(settings);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.google.gson.stream.JsonReader;
|
||||
|
||||
public class VaultSettingsJsonAdapterTest {
|
||||
|
||||
private final VaultSettingsJsonAdapter adapter = new VaultSettingsJsonAdapter();
|
||||
|
||||
@Test
|
||||
public void testDeserialize() throws IOException {
|
||||
String json = "{\"id\": \"foo\", \"path\": \"/foo/bar\", \"mountName\": \"test\", \"winDriveLetter\": \"X\", \"shouldBeIgnored\": true}";
|
||||
JsonReader jsonReader = new JsonReader(new StringReader(json));
|
||||
|
||||
VaultSettings vaultSettings = adapter.read(jsonReader);
|
||||
Assert.assertEquals("foo", vaultSettings.getId());
|
||||
Assert.assertEquals(Paths.get("/foo/bar"), vaultSettings.path().get());
|
||||
Assert.assertEquals("test", vaultSettings.mountName().get());
|
||||
Assert.assertEquals("X", vaultSettings.winDriveLetter().get());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*******************************************************************************
|
||||
* 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.settings;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class VaultSettingsTest {
|
||||
|
||||
@Test
|
||||
public void testNormalize() throws Exception {
|
||||
assertEquals("_", VaultSettings.normalizeMountName(" "));
|
||||
assertEquals("a", VaultSettings.normalizeMountName("ä"));
|
||||
assertEquals("C", VaultSettings.normalizeMountName("Ĉ"));
|
||||
assertEquals("_", VaultSettings.normalizeMountName(":"));
|
||||
assertEquals("_", VaultSettings.normalizeMountName("汉语"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,72 +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.6.0</version>
|
||||
</parent>
|
||||
<artifactId>core</artifactId>
|
||||
<name>Cryptomator WebDAV and I/O module</name>
|
||||
|
||||
<properties>
|
||||
<jetty.version>9.2.10.v20150310</jetty.version>
|
||||
<jackrabbit.version>2.9.1</jackrabbit.version>
|
||||
<commons.transaction.version>1.2</commons.transaction.version>
|
||||
<jta.version>1.1</jta.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>crypto-api</artifactId>
|
||||
</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>
|
||||
|
||||
<!-- 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>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,174 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.webdav.jackrabbit.WebDavServlet;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||
import org.eclipse.jetty.util.thread.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public final class WebDavServer {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WebDavServer.class);
|
||||
private static final String LOCALHOST = SystemUtils.IS_OS_WINDOWS ? "::1" : "localhost";
|
||||
private static final int MAX_PENDING_REQUESTS = 200;
|
||||
private static final int MAX_THREADS = 200;
|
||||
private static final int MIN_THREADS = 4;
|
||||
private static final int THREAD_IDLE_SECONDS = 20;
|
||||
private final Server server;
|
||||
private final ServerConnector localConnector;
|
||||
private final ContextHandlerCollection servletCollection;
|
||||
|
||||
public WebDavServer() {
|
||||
final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(MAX_PENDING_REQUESTS);
|
||||
final ThreadPool tp = new QueuedThreadPool(MAX_THREADS, MIN_THREADS, THREAD_IDLE_SECONDS, queue);
|
||||
server = new Server(tp);
|
||||
localConnector = new ServerConnector(server);
|
||||
localConnector.setHost(LOCALHOST);
|
||||
servletCollection = new ContextHandlerCollection();
|
||||
|
||||
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 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);
|
||||
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 ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor, failingMacCollection));
|
||||
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 ioException) {
|
||||
super(ioException);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return getCause().getMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLocalizedMessage() {
|
||||
return getCause().getLocalizedMessage();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,293 +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.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.attribute.BasicFileAttributeView;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
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.DavResourceFactory;
|
||||
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.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";
|
||||
|
||||
protected final DavResourceFactory factory;
|
||||
protected final DavResourceLocator locator;
|
||||
protected final DavSession session;
|
||||
protected final LockManager lockManager;
|
||||
protected final Cryptor cryptor;
|
||||
protected final DavPropertySet properties;
|
||||
|
||||
protected AbstractEncryptedNode(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
|
||||
this.factory = factory;
|
||||
this.locator = locator;
|
||||
this.session = session;
|
||||
this.lockManager = lockManager;
|
||||
this.cryptor = cryptor;
|
||||
this.properties = new DavPropertySet();
|
||||
this.determineProperties();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getComplianceClass() {
|
||||
return DAV_COMPLIANCE_CLASSES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSupportedMethods() {
|
||||
return METHODS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
return Files.exists(path);
|
||||
}
|
||||
|
||||
@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() {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
try {
|
||||
return Files.getLastModifiedTime(path).toMillis();
|
||||
} catch (IOException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void determineProperties();
|
||||
|
||||
@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.info("Set property {}", property.getName());
|
||||
|
||||
try {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
if (DavPropertyName.CREATIONDATE.equals(property.getName()) && property.getValue() instanceof String) {
|
||||
final String createDateStr = (String) property.getValue();
|
||||
final FileTime createTime = FileTimeUtils.fromRfc1123String(createDateStr);
|
||||
final BasicFileAttributeView attrView = Files.getFileAttributeView(path, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||
attrView.setTimes(null, null, createTime);
|
||||
LOG.info("Updating Creation Date: {}", createTime.toString());
|
||||
} else if (DavPropertyName.GETLASTMODIFIED.equals(property.getName()) && property.getValue() instanceof String) {
|
||||
final String lastModifiedTimeStr = (String) property.getValue();
|
||||
final FileTime lastModifiedTime = FileTimeUtils.fromRfc1123String(lastModifiedTimeStr);
|
||||
final BasicFileAttributeView attrView = Files.getFileAttributeView(path, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||
attrView.setTimes(lastModifiedTime, null, null);
|
||||
LOG.info("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.getPath(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 void move(DavResource dest) throws DavException {
|
||||
final Path src = ResourcePathUtils.getPhysicalPath(this);
|
||||
final Path dst = ResourcePathUtils.getPhysicalPath(dest);
|
||||
try {
|
||||
// check for conflicts:
|
||||
if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) {
|
||||
throw new DavException(DavServletResponse.SC_CONFLICT, "File at destination already exists: " + dst.toString());
|
||||
}
|
||||
|
||||
// move:
|
||||
try {
|
||||
Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error moving file from " + src.toString() + " to " + dst.toString());
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copy(DavResource dest, boolean shallow) throws DavException {
|
||||
final Path src = ResourcePathUtils.getPhysicalPath(this);
|
||||
final Path dst = ResourcePathUtils.getPhysicalPath(dest);
|
||||
try {
|
||||
// check for conflicts:
|
||||
if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) {
|
||||
throw new DavException(DavServletResponse.SC_CONFLICT, "File at destination already exists: " + dst.toString());
|
||||
}
|
||||
|
||||
// copy:
|
||||
try {
|
||||
Files.copy(src, dst, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.copy(src, dst, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error copying file from " + src.toString() + " to " + dst.toString());
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@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 DavResourceFactory getFactory() {
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavSession getSession() {
|
||||
return session;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.collections4.BidiMap;
|
||||
import org.apache.commons.collections4.bidimap.AbstractDualBidiMap;
|
||||
import org.apache.commons.collections4.map.LRUMap;
|
||||
|
||||
final 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,19 +0,0 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
class CryptoWarningHandler {
|
||||
|
||||
private final Collection<String> resourcesWithInvalidMac;
|
||||
|
||||
public CryptoWarningHandler(Collection<String> resourcesWithInvalidMac) {
|
||||
this.resourcesWithInvalidMac = resourcesWithInvalidMac;
|
||||
}
|
||||
|
||||
public void macAuthFailed(String resourceName) {
|
||||
if (!resourcesWithInvalidMac.contains(resourceName)) {
|
||||
resourcesWithInvalidMac.add(resourceName);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,242 +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.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
|
||||
import org.apache.commons.collections4.BidiMap;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.builder.EqualsBuilder;
|
||||
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
||||
import org.apache.jackrabbit.webdav.DavLocatorFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.util.EncodeUtil;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.CryptorIOSupport;
|
||||
import org.cryptomator.crypto.SensitiveDataSwipeListener;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.webdav.exceptions.DecryptFailedRuntimeException;
|
||||
|
||||
class DavLocatorFactoryImpl implements DavLocatorFactory, SensitiveDataSwipeListener, CryptorIOSupport {
|
||||
|
||||
private static final int MAX_CACHED_PATHS = 10000;
|
||||
private final Path fsRoot;
|
||||
private final Cryptor cryptor;
|
||||
private final BidiMap<String, String> pathCache = new BidiLRUMap<>(MAX_CACHED_PATHS); // <decryptedPath, encryptedPath>
|
||||
|
||||
DavLocatorFactoryImpl(String fsRoot, Cryptor cryptor) {
|
||||
this.fsRoot = FileSystems.getDefault().getPath(fsRoot);
|
||||
this.cryptor = cryptor;
|
||||
cryptor.addSensitiveDataSwipeListener(this);
|
||||
}
|
||||
|
||||
/* DavLocatorFactory */
|
||||
|
||||
@Override
|
||||
public DavResourceLocator createResourceLocator(String prefix, String href) {
|
||||
final String fullPrefix = prefix.endsWith("/") ? prefix : prefix + "/";
|
||||
final String relativeHref = StringUtils.removeStart(href, fullPrefix);
|
||||
|
||||
final String resourcePath = EncodeUtil.unescape(StringUtils.removeStart(relativeHref, "/"));
|
||||
return new DavResourceLocatorImpl(fullPrefix, resourcePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws DecryptFailedRuntimeException, which should a checked exception, but Jackrabbit doesn't allow that.
|
||||
*/
|
||||
@Override
|
||||
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) {
|
||||
final String fullPrefix = prefix.endsWith("/") ? prefix : prefix + "/";
|
||||
|
||||
try {
|
||||
final String resourcePath = (isResourcePath) ? path : getResourcePath(path);
|
||||
return new DavResourceLocatorImpl(fullPrefix, resourcePath);
|
||||
} catch (DecryptFailedException e) {
|
||||
throw new DecryptFailedRuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String resourcePath) {
|
||||
try {
|
||||
return createResourceLocator(prefix, workspacePath, resourcePath, true);
|
||||
} catch (DecryptFailedRuntimeException e) {
|
||||
throw new IllegalStateException("Tried to decrypt resourcePath. Only repositoryPaths can be encrypted.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/* Encryption/Decryption */
|
||||
|
||||
/**
|
||||
* @return Encrypted absolute paths on the file system.
|
||||
*/
|
||||
private String getRepositoryPath(String resourcePath) {
|
||||
String encryptedPath = pathCache.get(resourcePath);
|
||||
if (encryptedPath == null) {
|
||||
encryptedPath = encryptRepositoryPath(resourcePath);
|
||||
pathCache.put(resourcePath, encryptedPath);
|
||||
}
|
||||
return encryptedPath;
|
||||
}
|
||||
|
||||
private String encryptRepositoryPath(String resourcePath) {
|
||||
if (resourcePath == null) {
|
||||
return fsRoot.toString();
|
||||
}
|
||||
final String encryptedRepoPath = cryptor.encryptPath(resourcePath, FileSystems.getDefault().getSeparator().charAt(0), '/', this);
|
||||
return fsRoot.resolve(encryptedRepoPath).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Decrypted path for use in URIs.
|
||||
*/
|
||||
private String getResourcePath(String repositoryPath) throws DecryptFailedException {
|
||||
String decryptedPath = pathCache.getKey(repositoryPath);
|
||||
if (decryptedPath == null) {
|
||||
decryptedPath = decryptResourcePath(repositoryPath);
|
||||
pathCache.put(decryptedPath, repositoryPath);
|
||||
}
|
||||
return decryptedPath;
|
||||
}
|
||||
|
||||
private String decryptResourcePath(String repositoryPath) throws DecryptFailedException {
|
||||
final Path absRepoPath = FileSystems.getDefault().getPath(repositoryPath);
|
||||
if (fsRoot.equals(absRepoPath)) {
|
||||
return null;
|
||||
} else {
|
||||
final Path relativeRepositoryPath = fsRoot.relativize(absRepoPath);
|
||||
final String resourcePath = cryptor.decryptPath(relativeRepositoryPath.toString(), FileSystems.getDefault().getSeparator().charAt(0), '/', this);
|
||||
return resourcePath;
|
||||
}
|
||||
}
|
||||
|
||||
/* CryptorIOSupport */
|
||||
|
||||
@Override
|
||||
public void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) throws IOException {
|
||||
final Path metaDataFile = fsRoot.resolve(encryptedPath);
|
||||
Files.write(metaDataFile, encryptedMetadata, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] readPathSpecificMetadata(String encryptedPath) throws IOException {
|
||||
final Path metaDataFile = fsRoot.resolve(encryptedPath);
|
||||
if (!Files.isReadable(metaDataFile)) {
|
||||
return null;
|
||||
} else {
|
||||
return Files.readAllBytes(metaDataFile);
|
||||
}
|
||||
}
|
||||
|
||||
/* SensitiveDataSwipeListener */
|
||||
|
||||
@Override
|
||||
public void swipeSensitiveData() {
|
||||
pathCache.clear();
|
||||
}
|
||||
|
||||
/* Locator */
|
||||
|
||||
private class DavResourceLocatorImpl implements DavResourceLocator {
|
||||
|
||||
private final String prefix;
|
||||
private final String resourcePath;
|
||||
|
||||
private DavResourceLocatorImpl(String prefix, String resourcePath) {
|
||||
this.prefix = prefix;
|
||||
this.resourcePath = FilenameUtils.normalizeNoEndSeparator(resourcePath, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPrefix() {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getResourcePath() {
|
||||
return resourcePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWorkspacePath() {
|
||||
return isRootLocation() ? null : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWorkspaceName() {
|
||||
return getPrefix();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSameWorkspace(DavResourceLocator locator) {
|
||||
return (locator == null) ? false : isSameWorkspace(locator.getWorkspaceName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSameWorkspace(String workspaceName) {
|
||||
return getWorkspaceName().equals(workspaceName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHref(boolean isCollection) {
|
||||
final String encodedResourcePath = EncodeUtil.escapePath(getResourcePath());
|
||||
final String href = getPrefix().concat(encodedResourcePath);
|
||||
if (isCollection && !href.endsWith("/")) {
|
||||
return href.concat("/");
|
||||
} else if (!isCollection && href.endsWith("/")) {
|
||||
return href.substring(0, href.length() - 1);
|
||||
} else {
|
||||
return href;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRootLocation() {
|
||||
return getResourcePath() == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavLocatorFactory getFactory() {
|
||||
return DavLocatorFactoryImpl.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRepositoryPath() {
|
||||
return DavLocatorFactoryImpl.this.getRepositoryPath(getResourcePath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final HashCodeBuilder builder = new HashCodeBuilder();
|
||||
builder.append(prefix);
|
||||
builder.append(resourcePath);
|
||||
return builder.toHashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof DavResourceLocatorImpl) {
|
||||
final DavResourceLocatorImpl other = (DavResourceLocatorImpl) obj;
|
||||
final EqualsBuilder builder = new EqualsBuilder();
|
||||
builder.append(this.prefix, other.prefix);
|
||||
builder.append(this.resourcePath, other.resourcePath);
|
||||
return builder.isEquals();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,88 +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.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
import org.apache.commons.httpclient.HttpStatus;
|
||||
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.cryptomator.crypto.Cryptor;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
|
||||
class DavResourceFactoryImpl implements DavResourceFactory {
|
||||
|
||||
private final LockManager lockManager = new SimpleLockManager();
|
||||
private final Cryptor cryptor;
|
||||
private final CryptoWarningHandler cryptoWarningHandler;
|
||||
private final ExecutorService backgroundTaskExecutor;
|
||||
|
||||
DavResourceFactoryImpl(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, ExecutorService backgroundTaskExecutor) {
|
||||
this.cryptor = cryptor;
|
||||
this.cryptoWarningHandler = cryptoWarningHandler;
|
||||
this.backgroundTaskExecutor = backgroundTaskExecutor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(locator);
|
||||
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
|
||||
|
||||
if (Files.isRegularFile(path) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) {
|
||||
response.setStatus(HttpStatus.SC_PARTIAL_CONTENT);
|
||||
return createFilePart(locator, request.getDavSession(), request);
|
||||
} else if (Files.isRegularFile(path) || DavMethods.METHOD_PUT.equals(request.getMethod())) {
|
||||
return createFile(locator, request.getDavSession());
|
||||
} else if (Files.isDirectory(path) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
|
||||
return createDirectory(locator, request.getDavSession());
|
||||
} else {
|
||||
return createNonExisting(locator, request.getDavSession());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(locator);
|
||||
|
||||
if (path != null && Files.isRegularFile(path)) {
|
||||
return createFile(locator, session);
|
||||
} else if (path != null && Files.isDirectory(path)) {
|
||||
return createDirectory(locator, session);
|
||||
} else {
|
||||
return createNonExisting(locator, session);
|
||||
}
|
||||
}
|
||||
|
||||
private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, DavServletRequest request) {
|
||||
return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, cryptoWarningHandler, backgroundTaskExecutor);
|
||||
}
|
||||
|
||||
private EncryptedFile createFile(DavResourceLocator locator, DavSession session) {
|
||||
return new EncryptedFile(this, locator, session, lockManager, cryptor, cryptoWarningHandler);
|
||||
}
|
||||
|
||||
private EncryptedDir createDirectory(DavResourceLocator locator, DavSession session) {
|
||||
return new EncryptedDir(this, locator, session, lockManager, cryptor);
|
||||
}
|
||||
|
||||
private NonExistingNode createNonExisting(DavResourceLocator locator, DavSession session) {
|
||||
return new NonExistingNode(this, locator, session, lockManager, cryptor);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,45 +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.util.HashSet;
|
||||
|
||||
import org.apache.jackrabbit.webdav.DavSession;
|
||||
|
||||
class DavSessionImpl implements DavSession {
|
||||
|
||||
private final HashSet<String> lockTokens = new HashSet<String>();
|
||||
private final HashSet<Object> references = new HashSet<Object>();
|
||||
|
||||
@Override
|
||||
public void addReference(Object reference) {
|
||||
references.add(reference);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeReference(Object reference) {
|
||||
references.remove(reference);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addLockToken(String token) {
|
||||
lockTokens.add(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getLockTokens() {
|
||||
return lockTokens.toArray(new String[lockTokens.size()]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeLockToken(String token) {
|
||||
lockTokens.remove(token);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,36 +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 org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavSession;
|
||||
import org.apache.jackrabbit.webdav.DavSessionProvider;
|
||||
import org.apache.jackrabbit.webdav.WebdavRequest;
|
||||
|
||||
class DavSessionProviderImpl implements DavSessionProvider {
|
||||
|
||||
@Override
|
||||
public boolean attachSession(WebdavRequest request) throws DavException {
|
||||
// every request gets a session
|
||||
final DavSession session = new DavSessionImpl();
|
||||
session.addReference(request);
|
||||
request.setDavSession(session);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releaseSession(WebdavRequest request) {
|
||||
final DavSession session = request.getDavSession();
|
||||
if (session != null) {
|
||||
session.removeReference(request);
|
||||
request.setDavSession(null);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,191 +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.channels.SeekableByteChannel;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||
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.EncryptFailedException;
|
||||
import org.cryptomator.webdav.exceptions.DavRuntimeException;
|
||||
import org.cryptomator.webdav.exceptions.DecryptFailedRuntimeException;
|
||||
import org.cryptomator.webdav.exceptions.IORuntimeException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
class EncryptedDir extends AbstractEncryptedNode {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EncryptedDir.class);
|
||||
|
||||
public EncryptedDir(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
|
||||
super(factory, locator, session, lockManager, cryptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCollection() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMember(DavResource resource, InputContext inputContext) throws DavException {
|
||||
if (resource.isCollection()) {
|
||||
this.addMemberDir(resource, inputContext);
|
||||
} else {
|
||||
this.addMemberFile(resource, inputContext);
|
||||
}
|
||||
}
|
||||
|
||||
private void addMemberDir(DavResource resource, InputContext inputContext) throws DavException {
|
||||
final Path childPath = ResourcePathUtils.getPhysicalPath(resource);
|
||||
try {
|
||||
Files.createDirectories(childPath);
|
||||
} catch (SecurityException e) {
|
||||
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to create subdirectory.", e);
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void addMemberFile(DavResource resource, InputContext inputContext) throws DavException {
|
||||
final Path childPath = ResourcePathUtils.getPhysicalPath(resource);
|
||||
try (final SeekableByteChannel channel = Files.newByteChannel(childPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
|
||||
cryptor.encryptFile(inputContext.getInputStream(), channel);
|
||||
} catch (SecurityException e) {
|
||||
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to create file.", e);
|
||||
throw new IORuntimeException(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());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceIterator getMembers() {
|
||||
final Path dir = ResourcePathUtils.getPhysicalPath(this);
|
||||
try {
|
||||
final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(dir, cryptor.getPayloadFilesFilter());
|
||||
final List<DavResource> result = new ArrayList<>();
|
||||
|
||||
for (final Path childPath : directoryStream) {
|
||||
try {
|
||||
final DavResourceLocator childLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), childPath.toString(), false);
|
||||
final DavResource resource = factory.createResource(childLocator, session);
|
||||
result.add(resource);
|
||||
} catch (DecryptFailedRuntimeException 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 {
|
||||
final Path memberPath = ResourcePathUtils.getPhysicalPath(member);
|
||||
try {
|
||||
if (Files.exists(memberPath)) {
|
||||
Files.walkFileTree(memberPath, new DeletingFileVisitor());
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
|
||||
} catch (IOException e) {
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spool(OutputContext outputContext) throws IOException {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void determineProperties() {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
properties.add(new ResourceType(ResourceType.COLLECTION));
|
||||
properties.add(new DefaultDavProperty<Integer>(DavPropertyName.ISCOLLECTION, 1));
|
||||
if (Files.exists(path)) {
|
||||
try {
|
||||
final BasicFileAttributes attrs = Files.readAttributes(path, 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 " + path.toString(), e);
|
||||
// don't add any further properties
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all files and folders, it visits.
|
||||
*/
|
||||
private static class DeletingFileVisitor extends SimpleFileVisitor<Path> {
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
|
||||
if (attributes.isRegularFile()) {
|
||||
Files.delete(file);
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
Files.delete(dir);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
|
||||
LOG.error("Failed to delete file " + file.toString(), exc);
|
||||
return FileVisitResult.TERMINATE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||
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.DecryptFailedException;
|
||||
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 {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFile.class);
|
||||
|
||||
protected final CryptoWarningHandler cryptoWarningHandler;
|
||||
|
||||
public EncryptedFile(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler) {
|
||||
super(factory, locator, session, lockManager, cryptor);
|
||||
this.cryptoWarningHandler = cryptoWarningHandler;
|
||||
}
|
||||
|
||||
@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 {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
if (Files.isRegularFile(path)) {
|
||||
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
|
||||
outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString());
|
||||
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
|
||||
final Long contentLength = cryptor.decryptedContentLength(channel);
|
||||
if (contentLength != null) {
|
||||
outputContext.setContentLength(contentLength);
|
||||
}
|
||||
if (outputContext.hasStream()) {
|
||||
cryptor.decryptFile(channel, outputContext.getOutputStream());
|
||||
}
|
||||
} catch (EOFException e) {
|
||||
LOG.warn("Unexpected end of stream (possibly client hung up).");
|
||||
} catch (MacAuthenticationFailedException e) {
|
||||
cryptoWarningHandler.macAuthFailed(getLocator().getResourcePath());
|
||||
} catch (DecryptFailedException e) {
|
||||
throw new IOException("Error decrypting file " + path.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void determineProperties() {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
if (Files.exists(path)) {
|
||||
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
|
||||
final Long contentLength = cryptor.decryptedContentLength(channel);
|
||||
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, contentLength));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error reading filesize " + path.toString(), e);
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
|
||||
properties.add(new HttpHeaderProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString()));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error determining metadata " + path.toString(), e);
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.MutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.DavServletRequest;
|
||||
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.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
|
||||
/**
|
||||
* 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 static final String BYTE_UNIT_PREFIX = "bytes=";
|
||||
private static final char RANGE_SET_SEP = ',';
|
||||
private static final char RANGE_SEP = '-';
|
||||
private static final Cache<DavResourceLocator, MacAuthenticationJob> cachedMacAuthenticationJobs = CacheBuilder.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build();
|
||||
|
||||
/**
|
||||
* e.g. range -500 (gets the last 500 bytes) -> (-1, 500)
|
||||
*/
|
||||
private static final Long SUFFIX_BYTE_RANGE_LOWER = -1L;
|
||||
|
||||
/**
|
||||
* e.g. range 500- (gets all bytes from 500) -> (500, MAX_LONG)
|
||||
*/
|
||||
private static final Long SUFFIX_BYTE_RANGE_UPPER = Long.MAX_VALUE;
|
||||
|
||||
private final Set<Pair<Long, Long>> requestedContentRanges = new HashSet<Pair<Long, Long>>();
|
||||
|
||||
public EncryptedFilePart(DavResourceFactory factory, DavResourceLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler,
|
||||
ExecutorService backgroundTaskExecutor) {
|
||||
super(factory, locator, session, lockManager, cryptor, cryptoWarningHandler);
|
||||
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
|
||||
if (rangeHeader == null) {
|
||||
throw new IllegalArgumentException("HTTP request doesn't contain a range header");
|
||||
}
|
||||
determineByteRanges(rangeHeader);
|
||||
|
||||
synchronized (cachedMacAuthenticationJobs) {
|
||||
if (cachedMacAuthenticationJobs.getIfPresent(locator) == null) {
|
||||
final MacAuthenticationJob macAuthJob = new MacAuthenticationJob(locator);
|
||||
cachedMacAuthenticationJobs.put(locator, macAuthJob);
|
||||
backgroundTaskExecutor.submit(macAuthJob);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void determineByteRanges(String rangeHeader) {
|
||||
final String byteRangeSet = StringUtils.removeStartIgnoreCase(rangeHeader, BYTE_UNIT_PREFIX);
|
||||
final String[] byteRanges = StringUtils.split(byteRangeSet, RANGE_SET_SEP);
|
||||
if (byteRanges.length == 0) {
|
||||
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
|
||||
}
|
||||
for (final String byteRange : byteRanges) {
|
||||
final String[] bytePos = StringUtils.splitPreserveAllTokens(byteRange, RANGE_SEP);
|
||||
if (bytePos.length != 2 || bytePos[0].isEmpty() && bytePos[1].isEmpty()) {
|
||||
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
|
||||
}
|
||||
final Long lower = bytePos[0].isEmpty() ? SUFFIX_BYTE_RANGE_LOWER : Long.valueOf(bytePos[0]);
|
||||
final Long upper = bytePos[1].isEmpty() ? SUFFIX_BYTE_RANGE_UPPER : Long.valueOf(bytePos[1]);
|
||||
if (lower > upper) {
|
||||
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
|
||||
}
|
||||
requestedContentRanges.add(new ImmutablePair<Long, Long>(lower, upper));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return One range, that spans all requested ranges.
|
||||
*/
|
||||
private Pair<Long, Long> getUnionRange(Long fileSize) {
|
||||
final long lastByte = fileSize - 1;
|
||||
final MutablePair<Long, Long> result = new MutablePair<Long, Long>();
|
||||
for (Pair<Long, Long> range : requestedContentRanges) {
|
||||
final long left;
|
||||
final long right;
|
||||
if (SUFFIX_BYTE_RANGE_LOWER.equals(range.getLeft())) {
|
||||
left = lastByte - range.getRight();
|
||||
right = lastByte;
|
||||
} else if (SUFFIX_BYTE_RANGE_UPPER.equals(range.getRight())) {
|
||||
left = range.getLeft();
|
||||
right = lastByte;
|
||||
} else {
|
||||
left = range.getLeft();
|
||||
right = range.getRight();
|
||||
}
|
||||
if (result.getLeft() == null || left < result.getLeft()) {
|
||||
result.setLeft(left);
|
||||
}
|
||||
if (result.getRight() == null || right > result.getRight()) {
|
||||
result.setRight(right);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spool(OutputContext outputContext) throws IOException {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
if (Files.isRegularFile(path)) {
|
||||
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
|
||||
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
|
||||
final Long fileSize = cryptor.decryptedContentLength(channel);
|
||||
final Pair<Long, Long> range = getUnionRange(fileSize);
|
||||
final Long rangeLength = range.getRight() - range.getLeft() + 1;
|
||||
outputContext.setContentLength(rangeLength);
|
||||
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getLeft(), range.getRight(), fileSize));
|
||||
if (outputContext.hasStream()) {
|
||||
cryptor.decryptRange(channel, outputContext.getOutputStream(), range.getLeft(), rangeLength);
|
||||
}
|
||||
} catch (EOFException e) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Unexpected end of stream during delivery of partial content (client hung up).");
|
||||
}
|
||||
} catch (DecryptFailedException e) {
|
||||
throw new IOException("Error decrypting file " + path.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getContentRangeHeader(long firstByte, long lastByte, long completeLength) {
|
||||
return String.format("%d-%d/%d", firstByte, lastByte, completeLength);
|
||||
}
|
||||
|
||||
private class MacAuthenticationJob implements Runnable {
|
||||
|
||||
private final DavResourceLocator locator;
|
||||
|
||||
public MacAuthenticationJob(final DavResourceLocator locator) {
|
||||
if (locator == null) {
|
||||
throw new IllegalArgumentException("locator must not be null.");
|
||||
}
|
||||
this.locator = locator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(locator);
|
||||
if (Files.isRegularFile(path) && Files.isReadable(path)) {
|
||||
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
|
||||
final boolean authentic = cryptor.isAuthentic(channel);
|
||||
if (!authentic) {
|
||||
cryptoWarningHandler.macAuthFailed(locator.getResourcePath());
|
||||
}
|
||||
} catch (ClosedByInterruptException ex) {
|
||||
LOG.debug("Couldn't finish MAC verification due to interruption of worker thread.");
|
||||
} catch (IOException e) {
|
||||
LOG.error("IOException during MAC verification of " + path.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return locator.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof MacAuthenticationJob) {
|
||||
final MacAuthenticationJob other = (MacAuthenticationJob) obj;
|
||||
return this.locator.equals(other.locator);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,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,65 +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 org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||
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.cryptomator.crypto.Cryptor;
|
||||
|
||||
class NonExistingNode extends AbstractEncryptedNode {
|
||||
|
||||
public NonExistingNode(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
|
||||
super(factory, locator, session, lockManager, cryptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCollection() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@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
|
||||
protected void determineProperties() {
|
||||
// do nothing.
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.jackrabbit;
|
||||
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
|
||||
final class ResourcePathUtils {
|
||||
|
||||
private ResourcePathUtils() {
|
||||
throw new IllegalStateException("not instantiable");
|
||||
}
|
||||
|
||||
public static Path getPhysicalPath(DavResource resource) {
|
||||
return getPhysicalPath(resource.getLocator());
|
||||
}
|
||||
|
||||
public static Path getPhysicalPath(DavResourceLocator locator) {
|
||||
return FileSystems.getDefault().getPath(locator.getRepositoryPath());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,105 +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.util.Collection;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.servlet.ServletConfig;
|
||||
import javax.servlet.ServletException;
|
||||
|
||||
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.server.AbstractWebdavServlet;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
|
||||
public class WebDavServlet extends AbstractWebdavServlet {
|
||||
|
||||
private static final long serialVersionUID = 7965170007048673022L;
|
||||
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;
|
||||
private ExecutorService backgroundTaskExecutor;
|
||||
|
||||
public WebDavServlet(final Cryptor cryptor, final Collection<String> failingMacCollection) {
|
||||
super();
|
||||
this.cryptor = cryptor;
|
||||
this.cryptoWarningHandler = new CryptoWarningHandler(failingMacCollection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(ServletConfig config) throws ServletException {
|
||||
super.init(config);
|
||||
final String fsRoot = config.getInitParameter(CFG_FS_ROOT);
|
||||
backgroundTaskExecutor = Executors.newCachedThreadPool();
|
||||
davSessionProvider = new DavSessionProviderImpl();
|
||||
davLocatorFactory = new DavLocatorFactoryImpl(fsRoot, cryptor);
|
||||
davResourceFactory = new DavResourceFactoryImpl(cryptor, cryptoWarningHandler, backgroundTaskExecutor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
backgroundTaskExecutor.shutdown();
|
||||
try {
|
||||
final boolean tasksFinished = backgroundTaskExecutor.awaitTermination(2, TimeUnit.SECONDS);
|
||||
if (!tasksFinished) {
|
||||
backgroundTaskExecutor.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
backgroundTaskExecutor.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,62 +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.6.0</version>
|
||||
</parent>
|
||||
<artifactId>crypto-aes</artifactId>
|
||||
<name>Cryptomator cryptographic module (AES)</name>
|
||||
<description>Provides stream ciphers and filename pseudonymization functions.</description>
|
||||
|
||||
<properties>
|
||||
<bouncycastle.version>1.51</bouncycastle.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>crypto-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Bouncycastle -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
<version>${bouncycastle.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Commons -->
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,605 +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.BufferedOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.DirectoryStream.Filter;
|
||||
import java.nio.file.Path;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.CipherInputStream;
|
||||
import javax.crypto.CipherOutputStream;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
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.apache.commons.io.output.NullOutputStream;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.bouncycastle.crypto.generators.SCrypt;
|
||||
import org.cryptomator.crypto.AbstractCryptor;
|
||||
import org.cryptomator.crypto.CryptorIOSupport;
|
||||
import org.cryptomator.crypto.aes256.CounterAwareInputStream.CounterAwareInputLimitReachedException;
|
||||
import org.cryptomator.crypto.exceptions.CounterOverflowException;
|
||||
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.WrongPasswordException;
|
||||
import org.cryptomator.crypto.io.SeekableByteChannelInputStream;
|
||||
import org.cryptomator.crypto.io.SeekableByteChannelOutputStream;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicConfiguration, FileNamingConventions {
|
||||
|
||||
/**
|
||||
* 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.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.
|
||||
*/
|
||||
@Override
|
||||
public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException {
|
||||
try {
|
||||
// load encrypted masterkey:
|
||||
final KeyFile keyfile = objectMapper.readValue(in, KeyFile.class);
|
||||
|
||||
// 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(), AES_KEY_LENGTH_IN_BITS);
|
||||
|
||||
// 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 void swipeSensitiveDataInternal() {
|
||||
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 and accept GCM specs.", 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 aesEcbCipher(SecretKey key, int cipherMode) {
|
||||
try {
|
||||
final Cipher cipher = Cipher.getInstance(AES_ECB_CIPHER);
|
||||
cipher.init(cipherMode, key);
|
||||
return cipher;
|
||||
} catch (InvalidKeyException ex) {
|
||||
throw new IllegalArgumentException("Invalid key.", ex);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException ex) {
|
||||
throw new AssertionError("Every implementation of the Java platform is required to support AES/ECB/PKCS5Padding.", 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 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 encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
|
||||
try {
|
||||
final String[] cleartextPathComps = StringUtils.split(cleartextPath, cleartextPathSep);
|
||||
final List<String> encryptedPathComps = new ArrayList<>(cleartextPathComps.length);
|
||||
for (final String cleartext : cleartextPathComps) {
|
||||
final String encrypted = encryptPathComponent(cleartext, primaryMasterKey, hMacMasterKey, ioSupport);
|
||||
encryptedPathComps.add(encrypted);
|
||||
}
|
||||
return StringUtils.join(encryptedPathComps, encryptedPathSep);
|
||||
} catch (InvalidKeyException | IOException e) {
|
||||
throw new IllegalStateException("Unable to encrypt path: " + cleartextPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Each path component, i.e. file or directory name separated by path separators, gets encrypted for its own.<br/>
|
||||
* Encryption will blow up the filename length due to aes block sizes 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 FileNamingConventions#ENCRYPTED_FILENAME_LENGTH_LIMIT}.<br/>
|
||||
* <br/>
|
||||
* In any case we will create the encrypted filename normally. For those, that are too long, we calculate a checksum. No
|
||||
* cryptographically secure hash is needed here. We just want an uniform distribution for better load balancing. All encrypted filenames
|
||||
* with the same checksum will then share a metadata file, in which a lookup map between encrypted filenames and short unique
|
||||
* alternative names are stored.<br/>
|
||||
* <br/>
|
||||
* These alternative names consist of the checksum, a unique id and a special file extension defined in
|
||||
* {@link FileNamingConventions#LONG_NAME_FILE_EXT}.
|
||||
*/
|
||||
private String encryptPathComponent(final String cleartext, final SecretKey aesKey, final SecretKey macKey, CryptorIOSupport ioSupport) throws IOException, InvalidKeyException {
|
||||
final byte[] cleartextBytes = cleartext.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
// encrypt:
|
||||
final byte[] encryptedBytes = AesSivCipherUtil.sivEncrypt(aesKey, macKey, cleartextBytes);
|
||||
final String ivAndCiphertext = ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes);
|
||||
|
||||
if (ivAndCiphertext.length() + BASIC_FILE_EXT.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) {
|
||||
final String groupPrefix = ivAndCiphertext.substring(0, LONG_NAME_PREFIX_LENGTH);
|
||||
final String metadataFilename = groupPrefix + METADATA_FILE_EXT;
|
||||
final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataFilename);
|
||||
final String alternativeFileName = groupPrefix + metadata.getOrCreateUuidForEncryptedFilename(ivAndCiphertext).toString() + LONG_NAME_FILE_EXT;
|
||||
this.storeMetadata(ioSupport, metadataFilename, metadata);
|
||||
return alternativeFileName;
|
||||
} else {
|
||||
return ivAndCiphertext + BASIC_FILE_EXT;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) throws DecryptFailedException {
|
||||
try {
|
||||
final String[] encryptedPathComps = StringUtils.split(encryptedPath, encryptedPathSep);
|
||||
final List<String> cleartextPathComps = new ArrayList<>(encryptedPathComps.length);
|
||||
for (final String encrypted : encryptedPathComps) {
|
||||
final String cleartext = decryptPathComponent(encrypted, primaryMasterKey, hMacMasterKey, ioSupport);
|
||||
cleartextPathComps.add(new String(cleartext));
|
||||
}
|
||||
return StringUtils.join(cleartextPathComps, cleartextPathSep);
|
||||
} catch (InvalidKeyException | IOException e) {
|
||||
throw new IllegalStateException("Unable to decrypt path: " + encryptedPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #encryptPathComponent(String, SecretKey, CryptorIOSupport)
|
||||
*/
|
||||
private String decryptPathComponent(final String encrypted, final SecretKey aesKey, final SecretKey macKey, CryptorIOSupport ioSupport) throws IOException, InvalidKeyException, DecryptFailedException {
|
||||
final String ciphertext;
|
||||
if (encrypted.endsWith(LONG_NAME_FILE_EXT)) {
|
||||
final String basename = StringUtils.removeEnd(encrypted, LONG_NAME_FILE_EXT);
|
||||
final String groupPrefix = basename.substring(0, LONG_NAME_PREFIX_LENGTH);
|
||||
final String uuid = basename.substring(LONG_NAME_PREFIX_LENGTH);
|
||||
final String metadataFilename = groupPrefix + METADATA_FILE_EXT;
|
||||
final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataFilename);
|
||||
ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
|
||||
} else if (encrypted.endsWith(BASIC_FILE_EXT)) {
|
||||
ciphertext = StringUtils.removeEndIgnoreCase(encrypted, BASIC_FILE_EXT);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported path component: " + encrypted);
|
||||
}
|
||||
|
||||
// decrypt:
|
||||
final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(ciphertext);
|
||||
final byte[] cleartextBytes = AesSivCipherUtil.sivDecrypt(aesKey, macKey, encryptedBytes);
|
||||
|
||||
return new String(cleartextBytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private LongFilenameMetadata getMetadata(CryptorIOSupport ioSupport, String metadataFile) throws IOException {
|
||||
final byte[] fileContent = ioSupport.readPathSpecificMetadata(metadataFile);
|
||||
if (fileContent == null) {
|
||||
return new LongFilenameMetadata();
|
||||
} else {
|
||||
return objectMapper.readValue(fileContent, LongFilenameMetadata.class);
|
||||
}
|
||||
}
|
||||
|
||||
private void storeMetadata(CryptorIOSupport ioSupport, String metadataFile, LongFilenameMetadata metadata) throws JsonProcessingException, IOException {
|
||||
ioSupport.writePathSpecificMetadata(metadataFile, objectMapper.writeValueAsBytes(metadata));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException {
|
||||
// skip 128bit IV + 256 bit MAC:
|
||||
encryptedFile.position(48);
|
||||
|
||||
// read encrypted value:
|
||||
final ByteBuffer encryptedFileSizeBuffer = ByteBuffer.allocate(AES_BLOCK_LENGTH);
|
||||
final int numFileSizeBytesRead = encryptedFile.read(encryptedFileSizeBuffer);
|
||||
|
||||
// return "unknown" value, if EOF
|
||||
if (numFileSizeBytesRead != encryptedFileSizeBuffer.capacity()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// decrypt size:
|
||||
try {
|
||||
final Cipher sizeCipher = aesEcbCipher(primaryMasterKey, Cipher.DECRYPT_MODE);
|
||||
final byte[] decryptedFileSize = sizeCipher.doFinal(encryptedFileSizeBuffer.array());
|
||||
final ByteBuffer fileSizeBuffer = ByteBuffer.wrap(decryptedFileSize);
|
||||
return fileSizeBuffer.getLong();
|
||||
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void encryptedContentLength(SeekableByteChannel encryptedFile, Long contentLength) throws IOException {
|
||||
final ByteBuffer encryptedFileSizeBuffer;
|
||||
|
||||
// encrypt content length in ECB mode (content length is less than one block):
|
||||
try {
|
||||
final ByteBuffer fileSizeBuffer = ByteBuffer.allocate(Long.BYTES);
|
||||
fileSizeBuffer.putLong(contentLength);
|
||||
final Cipher sizeCipher = aesEcbCipher(primaryMasterKey, Cipher.ENCRYPT_MODE);
|
||||
final byte[] encryptedFileSize = sizeCipher.doFinal(fileSizeBuffer.array());
|
||||
encryptedFileSizeBuffer = ByteBuffer.wrap(encryptedFileSize);
|
||||
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new IllegalStateException("Block size must be valid, as padding is requested. BadPaddingException not possible in encrypt mode.", e);
|
||||
}
|
||||
|
||||
// skip 128bit IV + 256 bit MAC:
|
||||
encryptedFile.position(48);
|
||||
|
||||
// write result:
|
||||
encryptedFile.write(encryptedFileSizeBuffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException {
|
||||
// init mac:
|
||||
final Mac calculatedMac = this.hmacSha256(hMacMasterKey);
|
||||
|
||||
// read stored mac:
|
||||
encryptedFile.position(16);
|
||||
final ByteBuffer storedMac = ByteBuffer.allocate(calculatedMac.getMacLength());
|
||||
final int numMacBytesRead = encryptedFile.read(storedMac);
|
||||
|
||||
// check validity of header:
|
||||
if (numMacBytesRead != calculatedMac.getMacLength()) {
|
||||
throw new IOException("Failed to read file header.");
|
||||
}
|
||||
|
||||
// go to begin of content:
|
||||
encryptedFile.position(64);
|
||||
|
||||
// calculated MAC
|
||||
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
|
||||
final InputStream macIn = new MacInputStream(in, calculatedMac);
|
||||
IOUtils.copyLarge(macIn, new NullOutputStream());
|
||||
|
||||
// compare (in constant time):
|
||||
return MessageDigest.isEqual(storedMac.array(), calculatedMac.doFinal());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
|
||||
// read iv:
|
||||
encryptedFile.position(0);
|
||||
final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
|
||||
final int numIvBytesRead = encryptedFile.read(countingIv);
|
||||
|
||||
// init mac:
|
||||
final Mac calculatedMac = this.hmacSha256(hMacMasterKey);
|
||||
|
||||
// read stored mac:
|
||||
final ByteBuffer storedMac = ByteBuffer.allocate(calculatedMac.getMacLength());
|
||||
final int numMacBytesRead = encryptedFile.read(storedMac);
|
||||
|
||||
// read file size:
|
||||
final Long fileSize = decryptedContentLength(encryptedFile);
|
||||
|
||||
// check validity of header:
|
||||
if (numIvBytesRead != AES_BLOCK_LENGTH || numMacBytesRead != calculatedMac.getMacLength() || fileSize == null) {
|
||||
throw new IOException("Failed to read file header.");
|
||||
}
|
||||
|
||||
// go to begin of content:
|
||||
encryptedFile.position(64);
|
||||
|
||||
// generate cipher:
|
||||
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.DECRYPT_MODE);
|
||||
|
||||
// read content
|
||||
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
|
||||
final InputStream macIn = new MacInputStream(in, calculatedMac);
|
||||
final InputStream cipheredIn = new CipherInputStream(macIn, cipher);
|
||||
final long bytesDecrypted = IOUtils.copyLarge(cipheredIn, plaintextFile, 0, fileSize);
|
||||
|
||||
// drain remaining bytes to /dev/null to complete MAC calculation:
|
||||
IOUtils.copyLarge(macIn, new NullOutputStream());
|
||||
|
||||
// compare (in constant time):
|
||||
final boolean macMatches = MessageDigest.isEqual(storedMac.array(), calculatedMac.doFinal());
|
||||
if (!macMatches) {
|
||||
// This exception will be thrown AFTER we sent the decrypted content to the user.
|
||||
// This has two advantages:
|
||||
// - we don't need to read files twice
|
||||
// - we can still restore files suffering from non-malicious bit rotting
|
||||
// Anyway me MUST make sure to warn the user. This will be done by the UI when catching this exception.
|
||||
throw new MacAuthenticationFailedException("MAC authentication failed.");
|
||||
}
|
||||
|
||||
return bytesDecrypted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
|
||||
// read iv:
|
||||
encryptedFile.position(0);
|
||||
final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
|
||||
final int numIvBytesRead = encryptedFile.read(countingIv);
|
||||
|
||||
// check validity of header:
|
||||
if (numIvBytesRead != AES_BLOCK_LENGTH) {
|
||||
throw new IOException("Failed to read file header.");
|
||||
}
|
||||
|
||||
// seek relevant position and update iv:
|
||||
long firstRelevantBlock = pos / AES_BLOCK_LENGTH; // cut of fraction!
|
||||
long beginOfFirstRelevantBlock = firstRelevantBlock * AES_BLOCK_LENGTH;
|
||||
long offsetInsideFirstRelevantBlock = pos - beginOfFirstRelevantBlock;
|
||||
countingIv.putInt(AES_BLOCK_LENGTH - Integer.BYTES, (int) firstRelevantBlock); // int-cast is possible, as max file size is 64GiB
|
||||
|
||||
// fast forward stream:
|
||||
encryptedFile.position(64l + beginOfFirstRelevantBlock);
|
||||
|
||||
// generate cipher:
|
||||
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.DECRYPT_MODE);
|
||||
|
||||
// read content
|
||||
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
|
||||
final InputStream cipheredIn = new CipherInputStream(in, cipher);
|
||||
return IOUtils.copyLarge(cipheredIn, plaintextFile, offsetInsideFirstRelevantBlock, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
|
||||
// truncate file
|
||||
encryptedFile.truncate(0);
|
||||
|
||||
// use an IV, whose last 8 bytes store a long used in counter mode and write initial value to file.
|
||||
final ByteBuffer countingIv = ByteBuffer.wrap(randomData(AES_BLOCK_LENGTH));
|
||||
countingIv.putInt(AES_BLOCK_LENGTH - Integer.BYTES, 0);
|
||||
encryptedFile.write(countingIv);
|
||||
|
||||
// init crypto stuff:
|
||||
final Mac mac = this.hmacSha256(hMacMasterKey);
|
||||
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.ENCRYPT_MODE);
|
||||
|
||||
// init mac buffer and skip 32 bytes
|
||||
final ByteBuffer macBuffer = ByteBuffer.allocate(mac.getMacLength());
|
||||
encryptedFile.write(macBuffer);
|
||||
|
||||
// encrypt and write "zero length" as a placeholder, which will be read by concurrent requests, as long as encryption didn't finish:
|
||||
encryptedContentLength(encryptedFile, 0l);
|
||||
|
||||
// write content:
|
||||
final OutputStream out = new SeekableByteChannelOutputStream(encryptedFile);
|
||||
final OutputStream macOut = new MacOutputStream(out, mac);
|
||||
final OutputStream cipheredOut = new CipherOutputStream(macOut, cipher);
|
||||
final OutputStream blockSizeBufferedOut = new BufferedOutputStream(cipheredOut, AES_BLOCK_LENGTH);
|
||||
final InputStream lengthLimitingIn = new CounterAwareInputStream(plaintextFile);
|
||||
final Long plaintextSize;
|
||||
try {
|
||||
plaintextSize = IOUtils.copyLarge(lengthLimitingIn, blockSizeBufferedOut);
|
||||
} catch (CounterAwareInputLimitReachedException ex) {
|
||||
encryptedFile.truncate(64l + CounterAwareInputStream.SIXTY_FOUR_GIGABYE);
|
||||
encryptedContentLength(encryptedFile, CounterAwareInputStream.SIXTY_FOUR_GIGABYE);
|
||||
// no additional padding needed here, as 64GiB is a multiple of 128bit
|
||||
throw new CounterOverflowException("File size exceeds limit (64Gib). Aborting to prevent counter overflow.");
|
||||
}
|
||||
|
||||
// ensure total byte count is a multiple of the block size, in CTR mode:
|
||||
final int remainderToFillLastBlock = AES_BLOCK_LENGTH - (int) (plaintextSize % AES_BLOCK_LENGTH);
|
||||
blockSizeBufferedOut.write(new byte[remainderToFillLastBlock]);
|
||||
|
||||
// for filesizes of up to 16GiB: append a few blocks of fake data:
|
||||
if (plaintextSize < (long) (Integer.MAX_VALUE / 4) * AES_BLOCK_LENGTH) {
|
||||
final int numberOfPlaintextBlocks = (int) Math.ceil(plaintextSize / AES_BLOCK_LENGTH);
|
||||
final int upToTenPercentFakeBlocks = (int) Math.ceil(Math.random() * 0.1 * numberOfPlaintextBlocks);
|
||||
final byte[] emptyBytes = this.randomData(AES_BLOCK_LENGTH);
|
||||
for (int i = 0; i < upToTenPercentFakeBlocks; i += AES_BLOCK_LENGTH) {
|
||||
blockSizeBufferedOut.write(emptyBytes);
|
||||
}
|
||||
}
|
||||
blockSizeBufferedOut.flush();
|
||||
|
||||
// write MAC of total ciphertext:
|
||||
macBuffer.clear();
|
||||
macBuffer.put(mac.doFinal());
|
||||
macBuffer.flip();
|
||||
encryptedFile.position(16); // right behind the IV
|
||||
encryptedFile.write(macBuffer); // 256 bit MAC
|
||||
|
||||
// encrypt and write plaintextSize:
|
||||
encryptedContentLength(encryptedFile, plaintextSize);
|
||||
|
||||
return plaintextSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter<Path> getPayloadFilesFilter() {
|
||||
return new Filter<Path>() {
|
||||
@Override
|
||||
public boolean accept(Path entry) throws IOException {
|
||||
return ENCRYPTED_FILE_GLOB_MATCHER.matches(entry);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,89 +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;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Number of bytes of the master key. Should be the maximum possible AES key length to provide best security.
|
||||
*/
|
||||
int PREF_MASTER_KEY_LENGTH_IN_BITS = 256;
|
||||
|
||||
/**
|
||||
* Number of bytes used as seed for the PRNG.
|
||||
*/
|
||||
int PRNG_SEED_LENGTH = 32;
|
||||
|
||||
/**
|
||||
* 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 name and file content encryption. Using CTR-mode for random access.<br/>
|
||||
* <strong>Important</strong>: As JCE doesn't support a padding, input must be a multiple of the block size.
|
||||
*
|
||||
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher
|
||||
*/
|
||||
String AES_CTR_CIPHER = "AES/CTR/NoPadding";
|
||||
|
||||
/**
|
||||
* Cipher specs for single block encryption (like file size).
|
||||
*
|
||||
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#impl
|
||||
*/
|
||||
String AES_ECB_CIPHER = "AES/ECB/PKCS5Padding";
|
||||
|
||||
/**
|
||||
* AES block size is 128 bit or 16 bytes.
|
||||
*/
|
||||
int AES_BLOCK_LENGTH = 16;
|
||||
|
||||
/**
|
||||
* Number of non-zero bytes in the IV used for file name encryption. Less means shorter encrypted filenames, more means higher entropy.
|
||||
* Maximum length is {@value #AES_BLOCK_LENGTH}. Even the shortest base32 (see {@link FileNamingConventions#ENCRYPTED_FILENAME_CODEC})
|
||||
* encoded byte array will need 8 chars. The maximum number of bytes that fit in 8 base32 chars is 5. Thus 5 is the ideal length.
|
||||
*/
|
||||
int FILE_NAME_IV_LENGTH = 5;
|
||||
|
||||
}
|
||||
@@ -1,226 +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) throws InvalidKeyException {
|
||||
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);
|
||||
} 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 InvalidKeyException, 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);
|
||||
} 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,59 +0,0 @@
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
|
||||
/**
|
||||
* Updates a {@link Mac} with the bytes read from this stream.
|
||||
*/
|
||||
class CounterAwareInputStream extends FilterInputStream {
|
||||
|
||||
static final long SIXTY_FOUR_GIGABYE = 1024l * 1024l * 1024l * 64l;
|
||||
|
||||
private final AtomicLong counter;
|
||||
|
||||
/**
|
||||
* @param in Stream from which to read contents, which will update the Mac.
|
||||
* @param mac Mac to be updated during writes.
|
||||
*/
|
||||
public CounterAwareInputStream(InputStream in) {
|
||||
super(in);
|
||||
this.counter = new AtomicLong(0l);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
int b = in.read();
|
||||
if (b != -1) {
|
||||
final long currentValue = counter.incrementAndGet();
|
||||
failWhen64GibReached(currentValue);
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
int read = in.read(b, off, len);
|
||||
if (read > 0) {
|
||||
final long currentValue = counter.addAndGet(read);
|
||||
failWhen64GibReached(currentValue);
|
||||
}
|
||||
return read;
|
||||
}
|
||||
|
||||
private void failWhen64GibReached(long currentValue) throws CounterAwareInputLimitReachedException {
|
||||
if (currentValue > SIXTY_FOUR_GIGABYE) {
|
||||
throw new CounterAwareInputLimitReachedException();
|
||||
}
|
||||
}
|
||||
|
||||
static class CounterAwareInputLimitReachedException extends IOException {
|
||||
private static final long serialVersionUID = -1905012809288019359L;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,61 +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.file.FileSystems;
|
||||
import java.nio.file.PathMatcher;
|
||||
|
||||
import org.apache.commons.codec.binary.Base32;
|
||||
import org.apache.commons.codec.binary.BaseNCodec;
|
||||
|
||||
interface FileNamingConventions {
|
||||
|
||||
/**
|
||||
* How to encode the encrypted file names safely. Base32 uses only alphanumeric characters and is case-insensitive.
|
||||
*/
|
||||
BaseNCodec ENCRYPTED_FILENAME_CODEC = new Base32();
|
||||
|
||||
/**
|
||||
* Maximum length possible on file systems with a filename limit of 255 chars.<br/>
|
||||
* Also we would need a few chars for our file extension, so lets use {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT}.
|
||||
*/
|
||||
int ENCRYPTED_FILENAME_LENGTH_LIMIT = 250;
|
||||
|
||||
/**
|
||||
* For plaintext file names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
|
||||
*/
|
||||
String BASIC_FILE_EXT = ".aes";
|
||||
|
||||
/**
|
||||
* Prefix in front of the actual encrypted file name used as IV.
|
||||
*/
|
||||
String IV_PREFIX_SEPARATOR = "_";
|
||||
|
||||
/**
|
||||
* For plaintext file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
|
||||
*/
|
||||
String LONG_NAME_FILE_EXT = ".lng.aes";
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* For metadata files for a certain group of files. The cryptor may decide what files to assign to the same group; hopefully using some
|
||||
* kind of uniform distribution for better load balancing.
|
||||
*/
|
||||
String METADATA_FILE_EXT = ".meta";
|
||||
|
||||
/**
|
||||
* Matches both, {@value #BASIC_FILE_EXT} and {@value #LONG_NAME_FILE_EXT} files.
|
||||
*/
|
||||
PathMatcher ENCRYPTED_FILE_GLOB_MATCHER = FileSystems.getDefault().getPathMatcher("glob:**/*{" + BASIC_FILE_EXT + "," + LONG_NAME_FILE_EXT + "}");
|
||||
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
|
||||
@JsonPropertyOrder(value = {"scryptSalt", "scryptCostParam", "scryptBlockSize", "keyLength", "primaryMasterKey", "hMacMasterKey"})
|
||||
public class KeyFile implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 8578363158959619885L;
|
||||
private byte[] scryptSalt;
|
||||
private int scryptCostParam;
|
||||
private int scryptBlockSize;
|
||||
private int keyLength;
|
||||
private byte[] primaryMasterKey;
|
||||
private byte[] hMacMasterKey;
|
||||
|
||||
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,49 +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.Serializable;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.collections4.BidiMap;
|
||||
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public BidiMap<UUID, String> getEncryptedFilenames() {
|
||||
return encryptedFilenames;
|
||||
}
|
||||
|
||||
public void setEncryptedFilenames(BidiMap<UUID, String> encryptedFilenames) {
|
||||
this.encryptedFilenames = encryptedFilenames;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
|
||||
/**
|
||||
* Updates a {@link Mac} with the bytes read from this stream.
|
||||
*/
|
||||
class MacInputStream extends FilterInputStream {
|
||||
|
||||
private final Mac mac;
|
||||
|
||||
/**
|
||||
* @param in Stream from which to read contents, which will update the Mac.
|
||||
* @param mac Mac to be updated during writes.
|
||||
*/
|
||||
public MacInputStream(InputStream in, Mac mac) {
|
||||
super(in);
|
||||
this.mac = mac;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
int b = in.read();
|
||||
if (b != -1) {
|
||||
mac.update((byte) b);
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
int read = in.read(b, off, len);
|
||||
if (read > 0) {
|
||||
mac.update(b, off, read);
|
||||
}
|
||||
return read;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
|
||||
/**
|
||||
* Updates a {@link Mac} with the bytes written to this stream.
|
||||
*/
|
||||
class MacOutputStream extends FilterOutputStream {
|
||||
|
||||
private final Mac mac;
|
||||
|
||||
/**
|
||||
* @param out Stream to redirect contents to after updating the mac.
|
||||
* @param mac Mac to be updated during writes.
|
||||
*/
|
||||
public MacOutputStream(OutputStream out, Mac mac) {
|
||||
super(out);
|
||||
this.mac = mac;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
mac.update((byte) b);
|
||||
out.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
mac.update(b, off, len);
|
||||
out.write(b, off, len);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,255 +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 java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.cryptomator.crypto.CryptorIOSupport;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.EncryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
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 {
|
||||
final String pw = "asd";
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
cryptor.encryptMasterKey(out, pw);
|
||||
cryptor.swipeSensitiveData();
|
||||
|
||||
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 {
|
||||
final String pw = "asd";
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
cryptor.encryptMasterKey(out, pw);
|
||||
cryptor.swipeSensitiveData();
|
||||
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
|
||||
public void testIntegrityAuthentication() 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(96);
|
||||
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);
|
||||
|
||||
// check mac (should return false)
|
||||
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||
final boolean authentic = cryptor.isAuthentic(encryptedIn);
|
||||
Assert.assertFalse(authentic);
|
||||
}
|
||||
|
||||
@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(96);
|
||||
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);
|
||||
}
|
||||
|
||||
@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(96);
|
||||
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);
|
||||
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 {
|
||||
// our test plaintext data:
|
||||
final byte[] plaintextData = new byte[65536 * Integer.BYTES];
|
||||
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);
|
||||
for (int i = 0; i < 65536; i++) {
|
||||
bbIn.putInt(i);
|
||||
}
|
||||
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
|
||||
|
||||
// init cryptor:
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
|
||||
// encrypt:
|
||||
final ByteBuffer encryptedData = ByteBuffer.allocate((int) (64 + 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, 25000 * Integer.BYTES, 30000 * Integer.BYTES);
|
||||
IOUtils.closeQuietly(encryptedIn);
|
||||
IOUtils.closeQuietly(plaintextOut);
|
||||
Assert.assertTrue(numDecryptedBytes > 0);
|
||||
|
||||
// check decrypted data:
|
||||
final byte[] result = plaintextOut.toByteArray();
|
||||
final byte[] expected = Arrays.copyOfRange(plaintextData, 25000 * Integer.BYTES, 55000 * Integer.BYTES);
|
||||
Assert.assertArrayEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncryptionOfFilenames() throws IOException, DecryptFailedException {
|
||||
final CryptorIOSupport ioSupportMock = new CryptoIOSupportMock();
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
|
||||
// short path components
|
||||
final String originalPath1 = "foo/bar/baz";
|
||||
final String encryptedPath1a = cryptor.encryptPath(originalPath1, '/', '/', ioSupportMock);
|
||||
final String encryptedPath1b = cryptor.encryptPath(originalPath1, '/', '/', ioSupportMock);
|
||||
Assert.assertEquals(encryptedPath1a, encryptedPath1b);
|
||||
final String decryptedPath1 = cryptor.decryptPath(encryptedPath1a, '/', '/', ioSupportMock);
|
||||
Assert.assertEquals(originalPath1, decryptedPath1);
|
||||
|
||||
// long path components
|
||||
final String str50chars = "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee";
|
||||
final String originalPath2 = "foo/" + str50chars + str50chars + str50chars + str50chars + str50chars + "/baz";
|
||||
final String encryptedPath2a = cryptor.encryptPath(originalPath2, '/', '/', ioSupportMock);
|
||||
final String encryptedPath2b = cryptor.encryptPath(originalPath2, '/', '/', ioSupportMock);
|
||||
Assert.assertEquals(encryptedPath2a, encryptedPath2b);
|
||||
final String decryptedPath2 = cryptor.decryptPath(encryptedPath2a, '/', '/', ioSupportMock);
|
||||
Assert.assertEquals(originalPath2, decryptedPath2);
|
||||
|
||||
// block size length path components
|
||||
final String originalPath3 = "aaaabbbbccccdddd";
|
||||
final String encryptedPath3a = cryptor.encryptPath(originalPath3, '/', '/', ioSupportMock);
|
||||
final String encryptedPath3b = cryptor.encryptPath(originalPath3, '/', '/', ioSupportMock);
|
||||
Assert.assertEquals(encryptedPath3a, encryptedPath3b);
|
||||
final String decryptedPath3 = cryptor.decryptPath(encryptedPath3a, '/', '/', ioSupportMock);
|
||||
Assert.assertEquals(originalPath3, decryptedPath3);
|
||||
}
|
||||
|
||||
private static class CryptoIOSupportMock implements CryptorIOSupport {
|
||||
|
||||
private final Map<String, byte[]> map = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) {
|
||||
map.put(encryptedPath, encryptedMetadata);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] readPathSpecificMetadata(String encryptedPath) {
|
||||
return map.get(encryptedPath);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,31 +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.6.0</version>
|
||||
</parent>
|
||||
<artifactId>crypto-api</artifactId>
|
||||
<name>Cryptomator cryptographic module API</name>
|
||||
|
||||
<dependencies>
|
||||
<!-- commons -->
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,38 +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.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public abstract class AbstractCryptor implements Cryptor {
|
||||
|
||||
private final Set<SensitiveDataSwipeListener> swipeListeners = new HashSet<>();
|
||||
|
||||
@Override
|
||||
public final void swipeSensitiveData() {
|
||||
this.swipeSensitiveDataInternal();
|
||||
for (final SensitiveDataSwipeListener sensitiveDataSwipeListener : swipeListeners) {
|
||||
sensitiveDataSwipeListener.swipeSensitiveData();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void swipeSensitiveDataInternal();
|
||||
|
||||
@Override
|
||||
public final void addSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) {
|
||||
this.swipeListeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void removeSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) {
|
||||
this.swipeListeners.remove(listener);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,113 +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 java.nio.file.DirectoryStream.Filter;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.EncryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
|
||||
/**
|
||||
* Provides access to cryptographic functions. All methods are threadsafe.
|
||||
*/
|
||||
public interface Cryptor extends SensitiveDataSwipeListener {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException;
|
||||
|
||||
/**
|
||||
* Encrypts each plaintext path component for its own.
|
||||
*
|
||||
* @param cleartextPath A relative path (UTF-8 encoded)
|
||||
* @param encryptedPathSep Path separator char like '/' used on local file system. Must not be null, even if cleartextPath is a sole
|
||||
* file name without any path separators.
|
||||
* @param cleartextPathSep Path separator char like '/' used in webdav URIs. Must not be null, even if cleartextPath is a sole file name
|
||||
* without any path separators.
|
||||
* @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
|
||||
* @return Encrypted path components concatenated by the given encryptedPathSep. Must not start with encryptedPathSep, unless the
|
||||
* encrypted path is explicitly absolute.
|
||||
*/
|
||||
String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport);
|
||||
|
||||
/**
|
||||
* Decrypts each encrypted path component for its own.
|
||||
*
|
||||
* @param encryptedPath A relative path (UTF-8 encoded)
|
||||
* @param encryptedPathSep Path separator char like '/' used on local file system. Must not be null, even if encryptedPath is a sole
|
||||
* file name without any path separators.
|
||||
* @param cleartextPathSep Path separator char like '/' used in webdav URIs. Must not be null, even if encryptedPath is a sole file name
|
||||
* without any path separators.
|
||||
* @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
|
||||
* @return Decrypted path components concatenated by the given cleartextPathSep. Must not start with cleartextPathSep, unless the
|
||||
* cleartext path is explicitly absolute.
|
||||
* @throws DecryptFailedException If the decryption failed for various reasons (including wrong password).
|
||||
*/
|
||||
String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) 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.
|
||||
*/
|
||||
Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException;
|
||||
|
||||
/**
|
||||
* @return true, if the stored MAC matches the calculated one.
|
||||
*/
|
||||
boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException;
|
||||
|
||||
/**
|
||||
* @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) 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) 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;
|
||||
|
||||
/**
|
||||
* @return A filter, that returns <code>true</code> for encrypted files, i.e. if the file is an actual user payload and not a supporting
|
||||
* metadata file of the {@link Cryptor}.
|
||||
*/
|
||||
Filter<Path> getPayloadFilesFilter();
|
||||
|
||||
void addSensitiveDataSwipeListener(SensitiveDataSwipeListener listener);
|
||||
|
||||
void removeSensitiveDataSwipeListener(SensitiveDataSwipeListener listener);
|
||||
|
||||
}
|
||||
@@ -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,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.crypto;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Methods that may be called by the Cryptor when accessing a path.
|
||||
*/
|
||||
public interface CryptorIOSupport {
|
||||
|
||||
/**
|
||||
* Persists encryptedMetadata to the given encryptedPath.
|
||||
*
|
||||
* @param encryptedPath A relative path
|
||||
* @throws IOException
|
||||
*/
|
||||
void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) throws IOException;
|
||||
|
||||
/**
|
||||
* @return Previously written encryptedMetadata stored at the given encryptedPath or <code>null</code> if no such file exists.
|
||||
*/
|
||||
byte[] readPathSpecificMetadata(String encryptedPath) throws IOException;
|
||||
|
||||
}
|
||||
@@ -1,173 +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.nio.file.DirectoryStream.Filter;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.EncryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
|
||||
public class SamplingDecorator implements Cryptor, CryptorIOSampling {
|
||||
|
||||
private final Cryptor cryptor;
|
||||
private final AtomicLong encryptedBytes;
|
||||
private final AtomicLong decryptedBytes;
|
||||
|
||||
private SamplingDecorator(Cryptor cryptor) {
|
||||
this.cryptor = cryptor;
|
||||
encryptedBytes = new AtomicLong();
|
||||
decryptedBytes = new AtomicLong();
|
||||
}
|
||||
|
||||
public static Cryptor decorate(Cryptor cryptor) {
|
||||
return new SamplingDecorator(cryptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void swipeSensitiveData() {
|
||||
cryptor.swipeSensitiveData();
|
||||
}
|
||||
|
||||
@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 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 {
|
||||
cryptor.decryptMasterKey(in, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
|
||||
encryptedBytes.addAndGet(StringUtils.length(cleartextPath));
|
||||
return cryptor.encryptPath(cleartextPath, encryptedPathSep, cleartextPathSep, ioSupport);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) throws DecryptFailedException {
|
||||
decryptedBytes.addAndGet(StringUtils.length(encryptedPath));
|
||||
return cryptor.decryptPath(encryptedPath, encryptedPathSep, cleartextPathSep, ioSupport);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException {
|
||||
return cryptor.decryptedContentLength(encryptedFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException {
|
||||
return cryptor.isAuthentic(encryptedFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
|
||||
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
|
||||
return cryptor.decryptFile(encryptedFile, countingInputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
|
||||
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
|
||||
return cryptor.decryptRange(encryptedFile, countingInputStream, pos, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
|
||||
final InputStream countingInputStream = new CountingInputStream(encryptedBytes, plaintextFile);
|
||||
return cryptor.encryptFile(countingInputStream, encryptedFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter<Path> getPayloadFilesFilter() {
|
||||
return cryptor.getPayloadFilesFilter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) {
|
||||
cryptor.addSensitiveDataSwipeListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) {
|
||||
cryptor.removeSensitiveDataSwipeListener(listener);
|
||||
}
|
||||
|
||||
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,19 +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;
|
||||
|
||||
public interface SensitiveDataSwipeListener {
|
||||
|
||||
/**
|
||||
* Removes sensitive data from memory. Depending on the data (e.g. for passwords) it might be necessary to overwrite the memory before
|
||||
* freeing the object.
|
||||
*/
|
||||
void swipeSensitiveData();
|
||||
|
||||
}
|
||||
@@ -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,13 +0,0 @@
|
||||
package org.cryptomator.crypto.exceptions;
|
||||
|
||||
public class DecryptFailedException extends StorageCryptingException {
|
||||
private static final long serialVersionUID = -3855673600374897828L;
|
||||
|
||||
public DecryptFailedException(Throwable t) {
|
||||
super("Decryption failed.", t);
|
||||
}
|
||||
|
||||
public DecryptFailedException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package org.cryptomator.crypto.exceptions;
|
||||
|
||||
public class EncryptFailedException extends StorageCryptingException {
|
||||
private static final long serialVersionUID = -3855673600374897828L;
|
||||
|
||||
public EncryptFailedException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.cryptomator.crypto.exceptions;
|
||||
|
||||
public class MacAuthenticationFailedException extends DecryptFailedException {
|
||||
|
||||
private static final long serialVersionUID = -5577052361643658772L;
|
||||
|
||||
public MacAuthenticationFailedException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package org.cryptomator.crypto.exceptions;
|
||||
|
||||
public class StorageCryptingException extends Exception {
|
||||
private static final long serialVersionUID = -6622699014483319376L;
|
||||
|
||||
public StorageCryptingException(String string) {
|
||||
super(string);
|
||||
}
|
||||
|
||||
public StorageCryptingException(String string, Throwable t) {
|
||||
super(string, t);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package org.cryptomator.crypto.exceptions;
|
||||
|
||||
public class UnsupportedKeyLengthException extends StorageCryptingException {
|
||||
private static final long serialVersionUID = 8114147446419390179L;
|
||||
|
||||
private final int requestedLength;
|
||||
private final int supportedLength;
|
||||
|
||||
public UnsupportedKeyLengthException(int length, int maxLength) {
|
||||
super(String.format("Key length (%d) exceeds policy maximum (%d).", length, maxLength));
|
||||
this.requestedLength = length;
|
||||
this.supportedLength = maxLength;
|
||||
}
|
||||
|
||||
public int getRequestedLength() {
|
||||
return requestedLength;
|
||||
}
|
||||
|
||||
public int getSupportedLength() {
|
||||
return supportedLength;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package org.cryptomator.crypto.exceptions;
|
||||
|
||||
public class WrongPasswordException extends StorageCryptingException {
|
||||
private static final long serialVersionUID = -602047799678568780L;
|
||||
|
||||
public WrongPasswordException() {
|
||||
super("Wrong password.");
|
||||
}
|
||||
}
|
||||
@@ -1,90 +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.io;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
|
||||
public class SeekableByteChannelInputStream extends InputStream {
|
||||
private final SeekableByteChannel channel;
|
||||
private volatile long markedPos = 0;
|
||||
|
||||
public SeekableByteChannelInputStream(SeekableByteChannel channel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
final ByteBuffer buffer = ByteBuffer.allocate(1);
|
||||
final int read = channel.read(buffer);
|
||||
if (read == 1) {
|
||||
return buffer.get(0);
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
final ByteBuffer buffer = ByteBuffer.wrap(b, off, len);
|
||||
return channel.read(buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
long available = channel.size() - channel.position();
|
||||
if (available > Integer.MAX_VALUE) {
|
||||
return Integer.MAX_VALUE;
|
||||
} else {
|
||||
return (int) available;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
final long pos = channel.position();
|
||||
final long max = channel.size();
|
||||
final long maxSkip = max - pos;
|
||||
final long actualSkip = Math.min(n, maxSkip);
|
||||
channel.position(channel.position() + actualSkip);
|
||||
return actualSkip;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
channel.close();
|
||||
super.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void mark(int readlimit) {
|
||||
try {
|
||||
markedPos = channel.position();
|
||||
} catch (IOException e) {
|
||||
markedPos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void reset() throws IOException {
|
||||
channel.position(markedPos);
|
||||
}
|
||||
|
||||
public synchronized void resetTo(long position) throws IOException {
|
||||
channel.position(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,64 +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.io;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
|
||||
public class SeekableByteChannelOutputStream extends OutputStream {
|
||||
|
||||
private final SeekableByteChannel channel;
|
||||
|
||||
public SeekableByteChannelOutputStream(SeekableByteChannel channel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
final byte actualByte = (byte) (b & 0x000000FF);
|
||||
final ByteBuffer buffer = ByteBuffer.allocate(1);
|
||||
buffer.put(actualByte);
|
||||
channel.write(buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
final ByteBuffer buffer = ByteBuffer.wrap(b, off, len);
|
||||
channel.write(buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
channel.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see SeekableByteChannel#truncate(long)
|
||||
*/
|
||||
public void truncate(long size) throws IOException {
|
||||
channel.truncate(size);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see SeekableByteChannel#position()
|
||||
*/
|
||||
public long position() throws IOException {
|
||||
return channel.position();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see SeekableByteChannel#position(long)
|
||||
*/
|
||||
public void position(long newPosition) throws IOException {
|
||||
channel.position(newPosition);
|
||||
}
|
||||
|
||||
}
|
||||
94
main/jacoco-report/pom.xml
Normal file
94
main/jacoco-report/pom.xml
Normal file
@@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (c) 2016 Sebastian Stenzel 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.3.0-rc4</version>
|
||||
</parent>
|
||||
<artifactId>jacoco-report</artifactId>
|
||||
<name>Cryptomator Code Coverage Report</name>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<dependencies>
|
||||
<!-- all modules containing unit tests: -->
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>commons</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>keychain</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>ui</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>launcher</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<!-- conflict with codacy-coverage-reporter -->
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- binary dependency used during build -->
|
||||
<dependency>
|
||||
<groupId>com.codacy</groupId>
|
||||
<artifactId>codacy-coverage-reporter</artifactId>
|
||||
<version>1.0.13</version>
|
||||
<classifier>assembly</classifier>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>report-aggregate</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>report-aggregate</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>1.5.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>java</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<mainClass>com.codacy.CodacyCoverageReporter</mainClass>
|
||||
<arguments>
|
||||
<argument>-l</argument>
|
||||
<argument>Java</argument>
|
||||
<argument>-r</argument>
|
||||
<argument>${project.build.directory}/site/jacoco-aggregate/jacoco.xml</argument>
|
||||
</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
47
main/keychain/pom.xml
Normal file
47
main/keychain/pom.xml
Normal file
@@ -0,0 +1,47 @@
|
||||
<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.3.0-rc4</version>
|
||||
</parent>
|
||||
<artifactId>keychain</artifactId>
|
||||
<name>System Keychain Access</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>jni</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>
|
||||
|
||||
<!-- Logging -->
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.cryptomator.keychain;
|
||||
|
||||
public interface KeychainAccess {
|
||||
|
||||
/**
|
||||
* Associates a passphrase with a given key.
|
||||
*
|
||||
* @param key Key used to retrieve the passphrase via {@link #loadPassphrase(String)}.
|
||||
* @param passphrase The secret to store in this keychain.
|
||||
*/
|
||||
void storePassphrase(String key, CharSequence passphrase);
|
||||
|
||||
/**
|
||||
* @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
|
||||
* @return The stored passphrase for the given key or <code>null</code> if no value for the given key could be found.
|
||||
*/
|
||||
char[] loadPassphrase(String key);
|
||||
|
||||
/**
|
||||
* Deletes a passphrase with a given key.
|
||||
*
|
||||
* @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
|
||||
*/
|
||||
void deletePassphrase(String key);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.cryptomator.keychain;
|
||||
|
||||
interface KeychainAccessStrategy extends KeychainAccess {
|
||||
|
||||
/**
|
||||
* @return <code>true</code> if this KeychainAccessStrategy works on the current machine.
|
||||
*/
|
||||
boolean isSupported();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.cryptomator.keychain;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import org.cryptomator.jni.JniModule;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.ElementsIntoSet;
|
||||
|
||||
@Module(includes = {JniModule.class})
|
||||
public class KeychainModule {
|
||||
|
||||
@Provides
|
||||
@ElementsIntoSet
|
||||
Set<KeychainAccessStrategy> provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain) {
|
||||
return Sets.newHashSet(macKeychain, winKeychain);
|
||||
}
|
||||
|
||||
@Provides
|
||||
public Optional<KeychainAccess> provideSupportedKeychain(Set<KeychainAccessStrategy> keychainAccessStrategies) {
|
||||
return keychainAccessStrategies.stream().filter(KeychainAccessStrategy::isSupported).map(KeychainAccess.class::cast).findFirst();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.cryptomator.keychain;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.jni.MacFunctions;
|
||||
import org.cryptomator.jni.MacKeychainAccess;
|
||||
|
||||
class MacSystemKeychainAccess implements KeychainAccessStrategy {
|
||||
|
||||
private final MacKeychainAccess keychain;
|
||||
|
||||
@Inject
|
||||
public MacSystemKeychainAccess(Optional<MacFunctions> macFunctions) {
|
||||
if (macFunctions.isPresent()) {
|
||||
this.keychain = macFunctions.get().keychainAccess();
|
||||
} else {
|
||||
this.keychain = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storePassphrase(String key, CharSequence passphrase) {
|
||||
keychain.storePassword(key, passphrase);
|
||||
}
|
||||
|
||||
@Override
|
||||
public char[] loadPassphrase(String key) {
|
||||
return keychain.loadPassword(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return SystemUtils.IS_OS_MAC_OSX && keychain != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deletePassphrase(String key) {
|
||||
keychain.deletePassword(key);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package org.cryptomator.keychain;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Reader;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.io.Writer;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
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.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.jni.WinDataProtection;
|
||||
import org.cryptomator.jni.WinFunctions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
class WindowsProtectedKeychainAccess implements KeychainAccessStrategy {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WindowsProtectedKeychainAccess.class);
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting() //
|
||||
.registerTypeHierarchyAdapter(byte[].class, new ByteArrayJsonAdapter()) //
|
||||
.disableHtmlEscaping().create();
|
||||
|
||||
private final WinDataProtection dataProtection;
|
||||
private final Path keychainPath;
|
||||
private Map<String, KeychainEntry> keychainEntries;
|
||||
|
||||
@Inject
|
||||
public WindowsProtectedKeychainAccess(Optional<WinFunctions> winFunctions) {
|
||||
if (winFunctions.isPresent()) {
|
||||
this.dataProtection = winFunctions.get().dataProtection();
|
||||
} else {
|
||||
this.dataProtection = null;
|
||||
}
|
||||
String keychainPathProperty = System.getProperty("cryptomator.keychainPath");
|
||||
if (dataProtection != null && keychainPathProperty == null) {
|
||||
LOG.warn("Windows DataProtection module loaded, but no cryptomator.keychainPath property found.");
|
||||
}
|
||||
if (keychainPathProperty != null) {
|
||||
if (keychainPathProperty.startsWith("~/")) {
|
||||
keychainPathProperty = SystemUtils.USER_HOME + keychainPathProperty.substring(1);
|
||||
}
|
||||
this.keychainPath = FileSystems.getDefault().getPath(keychainPathProperty);
|
||||
} else {
|
||||
this.keychainPath = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storePassphrase(String key, CharSequence passphrase) {
|
||||
loadKeychainEntriesIfNeeded();
|
||||
ByteBuffer buf = UTF_8.encode(CharBuffer.wrap(passphrase));
|
||||
byte[] cleartext = new byte[buf.remaining()];
|
||||
buf.get(cleartext);
|
||||
KeychainEntry entry = new KeychainEntry();
|
||||
entry.salt = generateSalt();
|
||||
entry.ciphertext = dataProtection.protect(cleartext, entry.salt);
|
||||
Arrays.fill(buf.array(), (byte) 0x00);
|
||||
Arrays.fill(cleartext, (byte) 0x00);
|
||||
keychainEntries.put(key, entry);
|
||||
saveKeychainEntries();
|
||||
}
|
||||
|
||||
@Override
|
||||
public char[] loadPassphrase(String key) {
|
||||
loadKeychainEntriesIfNeeded();
|
||||
KeychainEntry entry = keychainEntries.get(key);
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] cleartext = dataProtection.unprotect(entry.ciphertext, entry.salt);
|
||||
if (cleartext == null) {
|
||||
return null;
|
||||
}
|
||||
CharBuffer buf = UTF_8.decode(ByteBuffer.wrap(cleartext));
|
||||
char[] passphrase = new char[buf.remaining()];
|
||||
buf.get(passphrase);
|
||||
Arrays.fill(cleartext, (byte) 0x00);
|
||||
Arrays.fill(buf.array(), (char) 0x00);
|
||||
return passphrase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deletePassphrase(String key) {
|
||||
loadKeychainEntriesIfNeeded();
|
||||
keychainEntries.remove(key);
|
||||
saveKeychainEntries();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return SystemUtils.IS_OS_WINDOWS && dataProtection != null && keychainPath != null;
|
||||
}
|
||||
|
||||
private byte[] generateSalt() {
|
||||
byte[] result = new byte[2 * Long.BYTES];
|
||||
UUID uuid = UUID.randomUUID();
|
||||
ByteBuffer buf = ByteBuffer.wrap(result);
|
||||
buf.putLong(uuid.getMostSignificantBits());
|
||||
buf.putLong(uuid.getLeastSignificantBits());
|
||||
return result;
|
||||
}
|
||||
|
||||
private void loadKeychainEntriesIfNeeded() {
|
||||
if (keychainEntries == null) {
|
||||
loadKeychainEntries();
|
||||
}
|
||||
assert keychainEntries != null;
|
||||
}
|
||||
|
||||
private void loadKeychainEntries() {
|
||||
Type type = new TypeToken<Map<String, KeychainEntry>>() {
|
||||
}.getType();
|
||||
try (InputStream in = Files.newInputStream(keychainPath, StandardOpenOption.READ); //
|
||||
Reader reader = new InputStreamReader(in, UTF_8)) {
|
||||
keychainEntries = GSON.fromJson(reader, type);
|
||||
} catch (JsonParseException | NoSuchFileException e) {
|
||||
LOG.info("Creating new keychain at path {}", keychainPath);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Could not read keychain from path " + keychainPath, e);
|
||||
}
|
||||
if (keychainEntries == null) {
|
||||
keychainEntries = new HashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
private void saveKeychainEntries() {
|
||||
try (OutputStream out = Files.newOutputStream(keychainPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); //
|
||||
Writer writer = new OutputStreamWriter(out, UTF_8)) {
|
||||
GSON.toJson(keychainEntries, writer);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Could not read keychain from path " + keychainPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class KeychainEntry {
|
||||
@SerializedName("ciphertext")
|
||||
byte[] ciphertext;
|
||||
@SerializedName("salt")
|
||||
byte[] salt;
|
||||
}
|
||||
|
||||
private static class ByteArrayJsonAdapter implements JsonSerializer<byte[]>, JsonDeserializer<byte[]> {
|
||||
|
||||
private static final Base64 BASE64 = new Base64();
|
||||
|
||||
@Override
|
||||
public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
return BASE64.decode(json.getAsString().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(new String(BASE64.encode(src), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.cryptomator.keychain;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class KeychainModuleTest {
|
||||
|
||||
@Test
|
||||
public void testGetKeychain() {
|
||||
Optional<KeychainAccess> keychainAccess = DaggerTestKeychainComponent.builder().jniModule(new TestJniModule()).keychainModule(new TestKeychainModule()).build().keychainAccess();
|
||||
Assert.assertTrue(keychainAccess.isPresent());
|
||||
Assert.assertTrue(keychainAccess.get() instanceof MapKeychainAccess);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.cryptomator.keychain;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
class MapKeychainAccess implements KeychainAccessStrategy {
|
||||
|
||||
private final Map<String, char[]> map = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void storePassphrase(String key, CharSequence passphrase) {
|
||||
char[] pw = new char[passphrase.length()];
|
||||
for (int i = 0; i < passphrase.length(); i++) {
|
||||
pw[i] = passphrase.charAt(i);
|
||||
}
|
||||
map.put(key, pw);
|
||||
}
|
||||
|
||||
@Override
|
||||
public char[] loadPassphrase(String key) {
|
||||
return map.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deletePassphrase(String key) {
|
||||
map.remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.cryptomator.keychain;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.cryptomator.jni.JniModule;
|
||||
import org.cryptomator.jni.MacFunctions;
|
||||
import org.cryptomator.jni.WinFunctions;
|
||||
|
||||
import dagger.Lazy;
|
||||
|
||||
public class TestJniModule extends JniModule {
|
||||
|
||||
@Override
|
||||
public Optional<WinFunctions> winFunctions(Lazy<WinFunctions> winFunction) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<MacFunctions> macFunctions(Lazy<MacFunctions> winFunction) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user