mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-14 16:51:28 +00:00
Compare commits
267 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -11,3 +11,9 @@
|
||||
.classpath
|
||||
target/
|
||||
test-output/
|
||||
|
||||
# IntelliJ Settings Files #
|
||||
.idea/
|
||||
out/
|
||||
.idea_modules/
|
||||
*.iws
|
||||
|
||||
62
.travis.yml
62
.travis.yml
@@ -1,19 +1,30 @@
|
||||
language: java
|
||||
|
||||
sudo: required
|
||||
dist: trusty
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.m2
|
||||
env:
|
||||
global:
|
||||
- secure: "Lgj042RD0X3rB8VZVZLWP1GetLhjd3PqI5JbJMlzgHJpDI6RkFIBLN9SWAGmkLPCehIp2zA5tu9+UVy0NNMxm9xz6SyjMCaxS28/fnYEXaNmwwDSF6O6gLUbdxyzoYIFPYOPmFxpzhebqnNIsxaM29oZpgRgUGqosCczQxiB+Ng=" #coveralls
|
||||
- secure: "IfYURwZaDWuBDvyn47n0k1Zod/IQw1FF+CS5nnV08Q+NfC3vGGJMwV8m59XnbfwnWGxwvCaAbk4qP6s6+ijgZNKkvgfFMo3rfTok5zt43bIqgaFOANYV+OC/1c59gYD6ZUxhW5iNgMgU3qdsRtJuwSmfkVv/jKyLGfAbS4kN8BA=" #coverity
|
||||
|
||||
before_install: "curl -L --cookie 'oraclelicense=accept-securebackup-cookie;' http://download.oracle.com/otn-pub/java/jce/8/jce_policy-8.zip -o /tmp/policy.zip && sudo unzip -j -o /tmp/policy.zip *.jar -d `jdk_switcher home oraclejdk8`/jre/lib/security && rm /tmp/policy.zip"
|
||||
|
||||
script: mvn -fmain/pom.xml clean test
|
||||
|
||||
after_success: mvn -fmain/pom.xml clean test jacoco:report coveralls:report
|
||||
|
||||
- secure: "lV9OwUbHMrMpLUH1CY+Z4puLDdFXytudyPlG1eGRsesdpuG6KM3uQVz6uAtf6lrU8DRbMM/T7ML+PmvQ4UoPPYLdLxESLLBat2qUPOIVBOhTSlCc7I0DmGy04CSvkeMy8dPaQC0ukgNiR7zwoNzfcpGRN/U9S8tziDruuHoZSrg=" #bintray
|
||||
addons:
|
||||
coverity_scan:
|
||||
project:
|
||||
name: "cryptomator/cryptomator"
|
||||
notification_email: sebastian.stenzel@cryptomator.org
|
||||
build_command: "mvn -fmain/pom.xml clean test -DskipTests"
|
||||
branch_pattern: release.*
|
||||
install:
|
||||
# "clean" needed until https://bugs.openjdk.java.net/browse/JDK-8067747 is resolved.
|
||||
- mvn -fmain/pom.xml clean package -DskipTests dependency:go-offline -Ptest-coverage
|
||||
- mvn -fmain/pom.xml clean package -DskipTests dependency:go-offline -Prelease
|
||||
script:
|
||||
- mvn --update-snapshots -fmain/pom.xml -Ptest-coverage clean test jacoco:report-aggregate
|
||||
after_success:
|
||||
- "bash <(curl -s https://codecov.io/bash)"
|
||||
notifications:
|
||||
webhooks:
|
||||
urls:
|
||||
@@ -26,24 +37,27 @@ notifications:
|
||||
secure: "lngJ/HEAFBbD5AdiO9avMqptKpZHdmEwOzS9FabZjkdFh7yAYueTk5RniPUvShjsKtThYm7cJ8AtDMDwc07NvPrzbMBRtUJGwuDT+7c7YFALGFJ1NYi+emkC9x1oafvmPgEYSE+tMKzNcwrHi3ytGgKdIotsKwaF35QNXYA9aMs="
|
||||
on_success: change
|
||||
on_failure: always
|
||||
|
||||
before_deploy: mvn -fmain/pom.xml -Puber-jar clean package -DskipTests
|
||||
|
||||
addons:
|
||||
coverity_scan:
|
||||
project:
|
||||
name: "cryptomator/cryptomator"
|
||||
notification_email: sebastian.stenzel@cryptomator.org
|
||||
build_command: "mvn -fmain/pom.xml clean test -DskipTests"
|
||||
branch_pattern: coverity_scan
|
||||
|
||||
before_deploy:
|
||||
- mvn -fmain/pom.xml -Prelease clean package -DskipTests
|
||||
deploy:
|
||||
provider: releases
|
||||
prerelease: true
|
||||
- provider: releases
|
||||
prerelease: false
|
||||
api_key:
|
||||
secure: "ZjE1j93v3qbPIe2YbmhS319aCbMdLQw0HuymmluTurxXsZtn9D4t2+eTr99vBVxGRuB5lzzGezPR5zjk5W7iHF7xhwrawXrFzr2rPJWzWFt0aM+Ry2njU1ROTGGXGTbv4anWeBlgMxLEInTAy/9ytOGNJlec83yc0THpOY2wxnk="
|
||||
file: main/uber-jar/target/Cryptomator-$TRAVIS_TAG.jar
|
||||
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/
|
||||
33
CONTRIBUTING.md
Normal file
33
CONTRIBUTING.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Contributing to Cryptomator
|
||||
|
||||
## Did you find a bug?
|
||||
|
||||
- Ensure you're running the latest version of Cryptomator.
|
||||
- Ensure the bug is related to the desktop version of Cryptomator. Bugs concerning the Cryptomator iOS app can be reported on the [Cryptomator for iOS issues list](https://github.com/cryptomator/cryptomator-ios/issues).
|
||||
- Ensure the bug was not [already reported](https://github.com/cryptomator/cryptomator/issues). You can also check out our [FAQ](https://cryptomator.org/faq/) and our [Wiki](https://github.com/cryptomator/cryptomator/wiki).
|
||||
- If you're unable to find an open issue addressing the problem, [submit a new one](https://github.com/cryptomator/cryptomator/issues/new).
|
||||
|
||||
## Do you have questions?
|
||||
|
||||
- Ask questions by [submitting a new issue](https://github.com/cryptomator/cryptomator/issues/new).
|
||||
- [Contact us](https://cryptomator.org/contact/) directly by writing an email. Wir sprechen auch Deutsch!
|
||||
- Have a chat with us on [Gitter](https://gitter.im/cryptomator/cryptomator).
|
||||
|
||||
## Did you write a patch that fixes a bug?
|
||||
|
||||
- Open a new pull request with the patch.
|
||||
- Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable.
|
||||
|
||||
## Do you intend to add a new feature or change an existing one?
|
||||
|
||||
- Suggest your change by [submitting a new issue](https://github.com/cryptomator/cryptomator/issues/new) and start writing code.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Help us keep Cryptomator open and inclusive. Please read and follow our [Code of Conduct](https://github.com/cryptomator/cryptomator/blob/master/CODE_OF_CONDUCT.md).
|
||||
|
||||
## Above all, thank you for your contributions
|
||||
|
||||
Thank you for taking the time to contribute to the project! :+1:
|
||||
|
||||
Cryptomator Team
|
||||
19
ISSUE_TEMPLATE.md
Normal file
19
ISSUE_TEMPLATE.md
Normal file
@@ -0,0 +1,19 @@
|
||||
### Basic Info
|
||||
|
||||
- I'm running Cryptomator on: [Windows, OS X, and/or Debian (or other Linux Distribution), don't forget the version]
|
||||
- I'm using Cryptomator in version: [you can check the version in the settings of Cryptomator]
|
||||
|
||||
### Description
|
||||
|
||||
[description of the bug, question or feature - what did you do? what problem occurred? etc.]
|
||||
|
||||
### Log File (optional)
|
||||
|
||||
```
|
||||
[insert relevant parts of the log file here if applicable,
|
||||
don't forget to redact sensitive information
|
||||
|
||||
on Windows: %appdata%/Cryptomator/cryptomator.log
|
||||
on OS X: ~/Library/Logs/Cryptomator/cryptomator.log
|
||||
on Debian: ~/.Cryptomator/cryptomator.log]
|
||||
```
|
||||
69
README.md
69
README.md
@@ -1,44 +1,71 @@
|
||||
Cryptomator
|
||||
====================
|
||||

|
||||
|
||||
[](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)
|
||||
[](https://coveralls.io/github/cryptomator/cryptomator?branch=master)
|
||||
[](https://gitter.im/cryptomator/cryptomator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](http://twitter.com/Cryptomator)
|
||||
[](https://poeditor.com/join/project/bHwbvJmx0E)
|
||||
|
||||
Multiplatform transparent client-side encryption of your files in the cloud.
|
||||
Multi-platform transparent client-side encryption of your files in the cloud.
|
||||
|
||||
If you want to take a look at the current beta version, go ahead and get your copy of cryptomator on [Cryptomator.org](https://cryptomator.org) or clone and build Cryptomator using Maven (instructions below).
|
||||
Download native binaries of Cryptomator on [cryptomator.org](https://cryptomator.org/) or clone and build Cryptomator using Maven (instructions below).
|
||||
|
||||
## Features
|
||||
- Totally transparent: Just work on the encrypted volume, as if it was an USB flash drive
|
||||
- Works with Dropbox, OneDrive (Skydrive), Google Drive and any other cloud storage, that syncs with a local directory.
|
||||
- In fact it works with any directory. You can use it to encrypt as many folders as you like
|
||||
|
||||
- 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
|
||||
- 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 commercial interest, no government agency, no wasted taxpayers' money ;-)
|
||||
- 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 master key of course
|
||||
- Sensitive data is swiped from the heap asap
|
||||
- Cryptographically secure random numbers for salts, IVs and the masterkey of course
|
||||
- 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 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)
|
||||
- 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
|
||||
### Dependencies
|
||||
|
||||
* Java 8 + JCE unlimited strength policy files (needed for 256-bit keys)
|
||||
* Maven 3
|
||||
* Optional: OS-dependent build tools for native packaging (See [Windows](https://github.com/cryptomator/cryptomator-win), [OS X](https://github.com/cryptomator/cryptomator-osx), [Debian](https://github.com/cryptomator/cryptomator-deb))
|
||||
* Optional: OS-dependent build tools for native packaging (see [Windows](https://github.com/cryptomator/cryptomator-win), [OS X](https://github.com/cryptomator/cryptomator-osx), [Linux](https://github.com/cryptomator/builder-containers))
|
||||
|
||||
### Run Maven
|
||||
|
||||
```
|
||||
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 |
1
main/ant-kit/.gitignore
vendored
Normal file
1
main/ant-kit/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target/
|
||||
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.2.3</version>
|
||||
</parent>
|
||||
<artifactId>ant-kit</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<name>Cryptomator Ant Build Kit</name>
|
||||
<description>Builds a package that can be built with Ant locally</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>ui</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- copy libraries to target/libs/: -->
|
||||
<plugin>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-libs</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>copy-dependencies</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}/libs</outputDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!-- copy resources to target/: -->
|
||||
<plugin>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<version>2.7</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-resources</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}</outputDirectory>
|
||||
<escapeString>\</escapeString>
|
||||
<encoding>UTF-8</encoding>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
<excludes>
|
||||
<exclude>fixed-binaries/**</exclude>
|
||||
</excludes>
|
||||
</resource>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>false</filtering>
|
||||
<includes>
|
||||
<include>fixed-binaries/**</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!-- create antkit.tar.gz: -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<descriptors>
|
||||
<descriptor>assembly.xml</descriptor>
|
||||
</descriptors>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
<finalName>antkit</finalName>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
69
main/ant-kit/src/main/resources/build.xml
Normal file
69
main/ant-kit/src/main/resources/build.xml
Normal file
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project name="Cryptomator" default="create-jar" basedir="." xmlns:fx="javafx:com.sun.javafx.tools.ant">
|
||||
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="\${java.class.path}:\${java.home}/../lib/ant-javafx.jar:." />
|
||||
|
||||
<!-- Define application to build -->
|
||||
<fx:application id="Cryptomator" name="Cryptomator" version="${project.version}" mainClass="org.cryptomator.ui.Cryptomator" />
|
||||
|
||||
<!-- Create main application jar -->
|
||||
<target name="create-jar">
|
||||
<fx:jar destfile="antbuild/Cryptomator-${project.version}.jar">
|
||||
<fx:application refid="Cryptomator" />
|
||||
<fx:fileset dir="libs" includes="ui-${project.version}.jar" />
|
||||
<fx:resources>
|
||||
<fx:fileset dir="libs" type="jar" includes="*.jar" excludes="ui-${project.version}.jar" />
|
||||
</fx:resources>
|
||||
<fx:manifest>
|
||||
<fx:attribute name="Implementation-Vendor" value="cryptomator.org" />
|
||||
<fx:attribute name="Implementation-Title" value="Cryptomator"/>
|
||||
<fx:attribute name="Implementation-Version" value="${project.version}" />
|
||||
</fx:manifest>
|
||||
</fx:jar>
|
||||
</target>
|
||||
|
||||
<!-- Create 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:jvmarg value="-Xmx512m"/>
|
||||
</fx:platform>
|
||||
<fx:resources>
|
||||
<fx:fileset dir="antbuild" type="jar" includes="Cryptomator-${project.version}.jar" />
|
||||
<fx:fileset dir="libs" type="jar" includes="*.jar" excludes="ui-${project.version}.jar"/>
|
||||
<fx:fileset dir="fixed-binaries" type="data" includes="linux-launcher-*" arch=""/>
|
||||
</fx:resources>
|
||||
<fx:permissions elevated="false" />
|
||||
<fx:preferences install="true" />
|
||||
</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:jvmarg value="-Xmx512m"/>
|
||||
</fx:platform>
|
||||
<fx:resources>
|
||||
<fx:fileset dir="antbuild" type="jar" includes="Cryptomator-${project.version}.jar" />
|
||||
<fx:fileset dir="libs" type="jar" includes="*.jar" excludes="ui-${project.version}.jar"/>
|
||||
</fx:resources>
|
||||
<fx: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 |
16
main/ant-kit/src/main/resources/package/linux/control
Normal file
16
main/ant-kit/src/main/resources/package/linux/control
Normal file
@@ -0,0 +1,16 @@
|
||||
Package: APPLICATION_PACKAGE
|
||||
Version: APPLICATION_VERSION
|
||||
Section: contrib/utils
|
||||
Maintainer: Sebastian Stenzel <sebastian.stenzel@gmail.com>
|
||||
Homepage: https://cryptomator.org
|
||||
Vcs-Git: https://github.com/totalvoidness/cryptomator.git
|
||||
Vcs-Browser: https://github.com/totalvoidness/cryptomator
|
||||
Priority: optional
|
||||
Architecture: APPLICATION_ARCH
|
||||
Provides: APPLICATION_PACKAGE
|
||||
Installed-Size: APPLICATION_INSTALLED_SIZE
|
||||
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
|
||||
you save on one of these volumes will end up encrypted inside your vault.
|
||||
23
main/ant-kit/src/main/resources/package/linux/copyright
Normal file
23
main/ant-kit/src/main/resources/package/linux/copyright
Normal file
@@ -0,0 +1,23 @@
|
||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: cryptomator
|
||||
Source: <https://github.com/totalvoidness/cryptomator>
|
||||
|
||||
Copyright: 2015 Sebastian Stenzel <sebastian.stenzel@gmail.com> and contributors.
|
||||
License: MIT
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
.
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
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
|
||||
@@ -1,23 +1,35 @@
|
||||
<?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. -->
|
||||
<!--
|
||||
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.0.0</version>
|
||||
<version>1.2.3</version>
|
||||
</parent>
|
||||
<artifactId>commons-test</artifactId>
|
||||
<name>Cryptomator common test dependencies</name>
|
||||
<description>Shared utilities for tests</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>commons</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.bechte.junit</groupId>
|
||||
<artifactId>junit-hierarchicalcontextrunner</artifactId>
|
||||
@@ -26,11 +38,6 @@
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest-all</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>commons</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -1,29 +1,54 @@
|
||||
<?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. -->
|
||||
<!--
|
||||
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.0.0</version>
|
||||
<version>1.2.3</version>
|
||||
</parent>
|
||||
<artifactId>commons</artifactId>
|
||||
<name>Cryptomator common</name>
|
||||
<description>Shared utilities</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- Libs -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- DI -->
|
||||
<dependency>
|
||||
<groupId>com.google.dagger</groupId>
|
||||
<artifactId>dagger</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.dagger</groupId>
|
||||
<artifactId>dagger-compiler</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Test -->
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.bechte.junit</groupId>
|
||||
<artifactId>junit-hierarchicalcontextrunner</artifactId>
|
||||
@@ -35,4 +60,13 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ConsumerThrowingException<T, E extends Exception> {
|
||||
public interface ConsumerThrowingException<T, E extends Throwable> {
|
||||
|
||||
void accept(T t) throws E;
|
||||
|
||||
|
||||
@@ -17,16 +17,17 @@ public final class LazyInitializer {
|
||||
* @return The initialized value
|
||||
*/
|
||||
public static <T> T initializeLazily(AtomicReference<T> reference, Supplier<T> factory) {
|
||||
final T existingInstance = reference.get();
|
||||
if (existingInstance != null) {
|
||||
return existingInstance;
|
||||
final T existing = reference.get();
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
} else {
|
||||
final T newInstance = factory.get();
|
||||
if (reference.compareAndSet(null, newInstance)) {
|
||||
return newInstance;
|
||||
} else {
|
||||
return reference.get();
|
||||
}
|
||||
return reference.updateAndGet(currentValue -> {
|
||||
if (currentValue == null) {
|
||||
return factory.get();
|
||||
} else {
|
||||
return currentValue;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface RunnableThrowingException<T extends Exception> {
|
||||
public interface RunnableThrowingException<T extends Throwable> {
|
||||
|
||||
void run() throws T;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.ui.util;
|
||||
package org.cryptomator.common;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Markus Kreusch and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Markus Kreusch - initial implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Utility to print stack traces while analyzing issues.
|
||||
*
|
||||
* @author Markus Kreusch
|
||||
*/
|
||||
public class StackTrace {
|
||||
|
||||
public static void print(String message) {
|
||||
Thread thread = Thread.currentThread();
|
||||
System.err.println(stackTraceFor(message, thread));
|
||||
}
|
||||
|
||||
private static String stackTraceFor(String message, Thread thread) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
appendMessageAndThreadName(result, message, thread);
|
||||
appendStackTrace(thread, result);
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private static void appendStackTrace(Thread thread, StringBuilder result) {
|
||||
Stream.of(thread.getStackTrace()) //
|
||||
.skip(4) //
|
||||
.forEach(stackTraceElement -> append(stackTraceElement, result));
|
||||
}
|
||||
|
||||
private static void appendMessageAndThreadName(StringBuilder result, String message, Thread thread) {
|
||||
result //
|
||||
.append('[') //
|
||||
.append(thread.getName()) //
|
||||
.append("] ") //
|
||||
.append(message);
|
||||
}
|
||||
|
||||
private static void append(StackTraceElement stackTraceElement, StringBuilder result) {
|
||||
String className = stackTraceElement.getClassName();
|
||||
String methodName = stackTraceElement.getMethodName();
|
||||
String fileName = stackTraceElement.getFileName();
|
||||
int lineNumber = stackTraceElement.getLineNumber();
|
||||
result.append('\n') //
|
||||
.append(className).append(':').append(methodName) //
|
||||
.append(" (").append(fileName).append(':').append(lineNumber).append(')');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface SupplierThrowingException<T, E extends Throwable> {
|
||||
|
||||
T get() throws E;
|
||||
|
||||
}
|
||||
@@ -6,10 +6,11 @@
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.ui.util;
|
||||
package org.cryptomator.common;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
import org.cryptomator.common.SemVerComparator;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -9,8 +9,8 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.cryptomator.common.WeakValuedCache;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
@@ -83,6 +83,7 @@ public class WeakValuedCacheTest {
|
||||
assertThat(result, is(sameInstance(theValue)));
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
public void testCacheDoesNotPreventGarbageCollectionOfValues() {
|
||||
when(loader.apply(A_KEY)).thenAnswer(this::createValueUsingMoreThanHalfTheJvmMemory);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<version>1.2.3</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-api</artifactId>
|
||||
<name>Cryptomator filesystem: API</name>
|
||||
|
||||
@@ -17,6 +17,13 @@ public interface File extends Node, Comparable<File> {
|
||||
|
||||
static final int EOF = -1;
|
||||
|
||||
/**
|
||||
* @return The current size of the file. This value is a snapshot and might have been changed by concurrent modifications.
|
||||
* @throws UncheckedIOException
|
||||
* if an {@link IOException} occurs
|
||||
*/
|
||||
long size() throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Opens this file for reading.
|
||||
@@ -39,7 +46,6 @@ public interface File extends Node, Comparable<File> {
|
||||
* if an {@link IOException} occurs while opening the file, the
|
||||
* file does not exist or is a directory
|
||||
*/
|
||||
|
||||
ReadableFile openReadable() throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,13 +30,6 @@ public interface ReadableFile extends ReadableByteChannel {
|
||||
@Override
|
||||
int read(ByteBuffer target) throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* @return The current size of the file. This value is a snapshot and might have been changed by concurrent modifications.
|
||||
* @throws UncheckedIOException
|
||||
* if an {@link IOException} occurs
|
||||
*/
|
||||
long size() throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Fast-forwards or rewinds the file to the specified position.
|
||||
|
||||
@@ -15,7 +15,7 @@ import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.ReadableFile;
|
||||
import org.cryptomator.filesystem.WritableFile;
|
||||
|
||||
public abstract class DelegatingFile<D extends DelegatingFolder<D, ?>> extends DelegatingNode<File>implements File {
|
||||
public abstract class DelegatingFile<D extends DelegatingFolder<D, ?>> extends DelegatingNode<File> implements File {
|
||||
|
||||
private final D parent;
|
||||
|
||||
@@ -29,6 +29,11 @@ public abstract class DelegatingFile<D extends DelegatingFolder<D, ?>> extends D
|
||||
return Optional.of(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws UncheckedIOException {
|
||||
return delegate.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadableFile openReadable() throws UncheckedIOException {
|
||||
return delegate.openReadable();
|
||||
|
||||
@@ -13,6 +13,7 @@ import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.cryptomator.common.WeakValuedCache;
|
||||
import org.cryptomator.common.streams.AutoClosingStream;
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.Node;
|
||||
@@ -35,7 +36,7 @@ public abstract class DelegatingFolder<D extends DelegatingFolder<D, F>, F exten
|
||||
|
||||
@Override
|
||||
public Stream<? extends Node> children() throws UncheckedIOException {
|
||||
return Stream.concat(folders(), files());
|
||||
return AutoClosingStream.from(Stream.concat(folders(), files()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -31,11 +31,6 @@ public class DelegatingReadableFile implements ReadableFile {
|
||||
return delegate.read(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws UncheckedIOException {
|
||||
return delegate.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void position(long position) throws UncheckedIOException {
|
||||
delegate.position(position);
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@@ -28,7 +29,9 @@ public final class FileContents {
|
||||
* @return The file's content interpreted in this FileContents' charset.
|
||||
*/
|
||||
public String readContents(File file) {
|
||||
try (Reader reader = Channels.newReader(file.openReadable(), charset.newDecoder(), -1)) {
|
||||
try ( //
|
||||
ReadableByteChannel channel = file.openReadable(); //
|
||||
Reader reader = Channels.newReader(channel, charset.newDecoder(), -1)) {
|
||||
return IOUtils.toString(reader);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
|
||||
@@ -30,6 +30,16 @@ public class DelegatingFileTest {
|
||||
Assert.assertEquals(mockFile.name(), delegatingFile.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSize() {
|
||||
File mockFile = Mockito.mock(File.class);
|
||||
DelegatingFile<?> delegatingFile = new TestDelegatingFile(null, mockFile);
|
||||
|
||||
Mockito.when(mockFile.size()).thenReturn(42l);
|
||||
Assert.assertEquals(42l, delegatingFile.size());
|
||||
Mockito.verify(mockFile).size();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParent() {
|
||||
Folder mockFolder = Mockito.mock(Folder.class);
|
||||
|
||||
@@ -42,17 +42,6 @@ public class DelegatingReadableFileTest {
|
||||
Mockito.verify(mockReadableFile).read(buf);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSize() {
|
||||
ReadableFile mockReadableFile = Mockito.mock(ReadableFile.class);
|
||||
@SuppressWarnings("resource")
|
||||
DelegatingReadableFile delegatingReadableFile = new DelegatingReadableFile(mockReadableFile);
|
||||
|
||||
Mockito.when(mockReadableFile.size()).thenReturn(42l);
|
||||
Assert.assertEquals(42l, delegatingReadableFile.size());
|
||||
Mockito.verify(mockReadableFile).size();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPosition() {
|
||||
ReadableFile mockReadableFile = Mockito.mock(ReadableFile.class);
|
||||
|
||||
45
main/filesystem-charsets/pom.xml
Normal file
45
main/filesystem-charsets/pom.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright (c) 2015 Sebastian Stenzel
|
||||
This file is licensed under the terms of the MIT license.
|
||||
See the LICENSE.txt file for more info.
|
||||
|
||||
Contributors:
|
||||
Sebastian Stenzel - initial API and implementation
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.2.3</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-charsets</artifactId>
|
||||
<name>Cryptomator filesystem: Charset compatibility layer</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>filesystem-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Tests -->
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>commons-test</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>filesystem-inmemory</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,32 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.charsets;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.text.Normalizer;
|
||||
import java.text.Normalizer.Form;
|
||||
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.delegating.DelegatingFile;
|
||||
|
||||
class NormalizedNameFile extends DelegatingFile<NormalizedNameFolder> {
|
||||
|
||||
private final Form displayForm;
|
||||
|
||||
public NormalizedNameFile(NormalizedNameFolder parent, File delegate, Form displayForm) {
|
||||
super(parent, delegate);
|
||||
this.displayForm = displayForm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() throws UncheckedIOException {
|
||||
return Normalizer.normalize(super.name(), displayForm);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.charsets;
|
||||
|
||||
import java.text.Normalizer.Form;
|
||||
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.delegating.DelegatingFileSystem;
|
||||
|
||||
public class NormalizedNameFileSystem extends NormalizedNameFolder implements DelegatingFileSystem {
|
||||
|
||||
public NormalizedNameFileSystem(Folder delegate, Form displayForm) {
|
||||
super(null, delegate, displayForm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Folder getDelegate() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.charsets;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.text.Normalizer;
|
||||
import java.text.Normalizer.Form;
|
||||
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.delegating.DelegatingFolder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
class NormalizedNameFolder extends DelegatingFolder<NormalizedNameFolder, NormalizedNameFile> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(NormalizedNameFolder.class);
|
||||
private final Form displayForm;
|
||||
|
||||
public NormalizedNameFolder(NormalizedNameFolder parent, Folder delegate, Form displayForm) {
|
||||
super(parent, delegate);
|
||||
this.displayForm = displayForm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() throws UncheckedIOException {
|
||||
return Normalizer.normalize(super.name(), displayForm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NormalizedNameFile file(String name) throws UncheckedIOException {
|
||||
String nfcName = Normalizer.normalize(name, Form.NFC);
|
||||
String nfdName = Normalizer.normalize(name, Form.NFD);
|
||||
NormalizedNameFile nfcFile = super.file(nfcName);
|
||||
NormalizedNameFile nfdFile = super.file(nfdName);
|
||||
if (!nfcName.equals(nfdName) && nfcFile.exists() && nfdFile.exists()) {
|
||||
LOG.debug("Ambiguous file names \"" + nfcName + "\" (NFC) vs. \"" + nfdName + "\" (NFD). Both files exist. Using \"" + nfcName + "\" (NFC).");
|
||||
} else if (!nfcName.equals(nfdName) && !nfcFile.exists() && nfdFile.exists()) {
|
||||
LOG.debug("Moving file from \"" + nfcName + "\" (NFD) to \"" + nfdName + "\" (NFC).");
|
||||
nfdFile.moveTo(nfcFile);
|
||||
}
|
||||
return nfcFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NormalizedNameFile newFile(File delegate) {
|
||||
return new NormalizedNameFile(this, delegate, displayForm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NormalizedNameFolder folder(String name) throws UncheckedIOException {
|
||||
String nfcName = Normalizer.normalize(name, Form.NFC);
|
||||
String nfdName = Normalizer.normalize(name, Form.NFD);
|
||||
NormalizedNameFolder nfcFolder = super.folder(nfcName);
|
||||
NormalizedNameFolder nfdFolder = super.folder(nfdName);
|
||||
if (!nfcName.equals(nfdName) && nfcFolder.exists() && nfdFolder.exists()) {
|
||||
LOG.debug("Ambiguous folder names \"" + nfcName + "\" (NFC) vs. \"" + nfdName + "\" (NFD). Both files exist. Using \"" + nfcName + "\" (NFC).");
|
||||
} else if (!nfcName.equals(nfdName) && !nfcFolder.exists() && nfdFolder.exists()) {
|
||||
LOG.debug("Moving folder from \"" + nfcName + "\" (NFD) to \"" + nfdName + "\" (NFC).");
|
||||
nfdFolder.moveTo(nfcFolder);
|
||||
}
|
||||
return nfcFolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NormalizedNameFolder newFolder(Folder delegate) {
|
||||
return new NormalizedNameFolder(this, delegate, displayForm);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
/**
|
||||
* Makes sure, the filesystems wrapped by this filesystem work only on UTF-8 encoded file paths using Normalization Form C.
|
||||
* Filesystems wrapping this file system, on the other hand, will get filenames reported in a specified Normalization Form.
|
||||
* It is recommended to use NFD for OS X and NFC for other operating systems.
|
||||
* When looking for a file or folder with a name given in either form, both possibilities are considered
|
||||
* and files/folders stored in NFD are automatically migrated to NFC.
|
||||
*/
|
||||
package org.cryptomator.filesystem.charsets;
|
||||
@@ -0,0 +1,90 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.charsets;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.text.Normalizer.Form;
|
||||
|
||||
import org.cryptomator.filesystem.FileSystem;
|
||||
import org.cryptomator.filesystem.WritableFile;
|
||||
import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class NormalizedNameFileSystemTest {
|
||||
|
||||
@Test
|
||||
public void testFileMigration() {
|
||||
FileSystem inMemoryFs = new InMemoryFileSystem();
|
||||
try (WritableFile writable = inMemoryFs.file("\u006F\u0302").openWritable()) {
|
||||
writable.write(ByteBuffer.allocate(0));
|
||||
}
|
||||
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
|
||||
Assert.assertTrue(normalizationFs.file("\u00F4").exists());
|
||||
Assert.assertTrue(normalizationFs.file("\u006F\u0302").exists());
|
||||
Assert.assertFalse(inMemoryFs.file("\u006F\u0302").exists());
|
||||
Assert.assertTrue(inMemoryFs.file("\u00F4").exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoFileMigration() {
|
||||
FileSystem inMemoryFs = new InMemoryFileSystem();
|
||||
try (WritableFile writable = inMemoryFs.file("\u00F4").openWritable()) {
|
||||
writable.write(ByteBuffer.allocate(0));
|
||||
}
|
||||
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
|
||||
Assert.assertTrue(normalizationFs.file("\u00F4").exists());
|
||||
Assert.assertTrue(normalizationFs.file("\u006F\u0302").exists());
|
||||
Assert.assertFalse(inMemoryFs.file("\u006F\u0302").exists());
|
||||
Assert.assertTrue(inMemoryFs.file("\u00F4").exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFolderMigration() {
|
||||
FileSystem inMemoryFs = new InMemoryFileSystem();
|
||||
inMemoryFs.folder("\u006F\u0302").create();
|
||||
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
|
||||
Assert.assertTrue(normalizationFs.folder("\u00F4").exists());
|
||||
Assert.assertTrue(normalizationFs.folder("\u006F\u0302").exists());
|
||||
Assert.assertFalse(inMemoryFs.folder("\u006F\u0302").exists());
|
||||
Assert.assertTrue(inMemoryFs.folder("\u00F4").exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoFolderMigration() {
|
||||
FileSystem inMemoryFs = new InMemoryFileSystem();
|
||||
inMemoryFs.folder("\u00F4").create();
|
||||
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
|
||||
Assert.assertTrue(normalizationFs.folder("\u00F4").exists());
|
||||
Assert.assertTrue(normalizationFs.folder("\u006F\u0302").exists());
|
||||
Assert.assertFalse(inMemoryFs.folder("\u006F\u0302").exists());
|
||||
Assert.assertTrue(inMemoryFs.folder("\u00F4").exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNfcDisplayNames() {
|
||||
FileSystem inMemoryFs = new InMemoryFileSystem();
|
||||
inMemoryFs.folder("a\u00F4").create();
|
||||
inMemoryFs.folder("b\u006F\u0302").create();
|
||||
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
|
||||
Assert.assertEquals("a\u00F4", normalizationFs.folder("a\u00F4").name());
|
||||
Assert.assertEquals("b\u00F4", normalizationFs.folder("b\u006F\u0302").name());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNfdDisplayNames() {
|
||||
FileSystem inMemoryFs = new InMemoryFileSystem();
|
||||
inMemoryFs.folder("a\u00F4").create();
|
||||
inMemoryFs.folder("b\u006F\u0302").create();
|
||||
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFD);
|
||||
Assert.assertEquals("a\u006F\u0302", normalizationFs.folder("a\u00F4").name());
|
||||
Assert.assertEquals("b\u006F\u0302", normalizationFs.folder("b\u006F\u0302").name());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.charsets;
|
||||
|
||||
import java.text.Normalizer.Form;
|
||||
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
public class NormalizedNameFileTest {
|
||||
|
||||
private File delegateNfc;
|
||||
private File delegateNfd;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
delegateNfc = Mockito.mock(File.class);
|
||||
delegateNfd = Mockito.mock(File.class);
|
||||
Mockito.when(delegateNfc.name()).thenReturn("\u00C5");
|
||||
Mockito.when(delegateNfd.name()).thenReturn("\u0041\u030A");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisplayNameInNfc() {
|
||||
File file1 = new NormalizedNameFile(null, delegateNfc, Form.NFC);
|
||||
File file2 = new NormalizedNameFile(null, delegateNfd, Form.NFC);
|
||||
Assert.assertEquals("\u00C5", file1.name());
|
||||
Assert.assertEquals("\u00C5", file2.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisplayNameInNfd() {
|
||||
File file1 = new NormalizedNameFile(null, delegateNfc, Form.NFD);
|
||||
File file2 = new NormalizedNameFile(null, delegateNfd, Form.NFD);
|
||||
Assert.assertEquals("\u0041\u030A", file1.name());
|
||||
Assert.assertEquals("\u0041\u030A", file2.name());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.charsets;
|
||||
|
||||
import java.text.Normalizer.Form;
|
||||
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
public class NormalizedNameFolderTest {
|
||||
|
||||
private Folder delegate;
|
||||
private File delegateSubFileNfc;
|
||||
private File delegateSubFileNfd;
|
||||
private Folder delegateSubFolderNfc;
|
||||
private Folder delegateSubFolderNfd;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
delegate = Mockito.mock(Folder.class);
|
||||
delegateSubFileNfc = Mockito.mock(File.class);
|
||||
delegateSubFileNfd = Mockito.mock(File.class);
|
||||
Mockito.when(delegate.file("\u00C5")).thenReturn(delegateSubFileNfc);
|
||||
Mockito.when(delegateSubFileNfc.name()).thenReturn("\u00C5");
|
||||
Mockito.when(delegate.file("\u0041\u030A")).thenReturn(delegateSubFileNfd);
|
||||
Mockito.when(delegateSubFileNfd.name()).thenReturn("\u0041\u030A");
|
||||
delegateSubFolderNfc = Mockito.mock(Folder.class);
|
||||
delegateSubFolderNfd = Mockito.mock(Folder.class);
|
||||
Mockito.when(delegate.folder("\u00F4")).thenReturn(delegateSubFolderNfc);
|
||||
Mockito.when(delegateSubFolderNfc.name()).thenReturn("\u00F4");
|
||||
Mockito.when(delegate.folder("\u006F\u0302")).thenReturn(delegateSubFolderNfd);
|
||||
Mockito.when(delegateSubFolderNfd.name()).thenReturn("\u006F\u0302");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisplayNameInNfc() {
|
||||
Folder folder1 = new NormalizedNameFolder(null, delegateSubFolderNfc, Form.NFC);
|
||||
Folder folder2 = new NormalizedNameFolder(null, delegateSubFolderNfd, Form.NFC);
|
||||
Assert.assertEquals("\u00F4", folder1.name());
|
||||
Assert.assertEquals("\u00F4", folder2.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisplayNameInNfd() {
|
||||
Folder folder1 = new NormalizedNameFolder(null, delegateSubFolderNfc, Form.NFD);
|
||||
Folder folder2 = new NormalizedNameFolder(null, delegateSubFolderNfd, Form.NFD);
|
||||
Assert.assertEquals("\u006F\u0302", folder1.name());
|
||||
Assert.assertEquals("\u006F\u0302", folder2.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoFolderMigration1() {
|
||||
Mockito.when(delegateSubFolderNfc.exists()).thenReturn(true);
|
||||
Mockito.when(delegateSubFolderNfd.exists()).thenReturn(false);
|
||||
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
|
||||
Folder sub1 = folder.folder("\u00F4");
|
||||
Folder sub2 = folder.folder("\u006F\u0302");
|
||||
Mockito.verify(delegateSubFolderNfd, Mockito.never()).moveTo(Mockito.any());
|
||||
Assert.assertSame(sub1, sub2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoFolderMigration2() {
|
||||
Mockito.when(delegateSubFolderNfc.exists()).thenReturn(true);
|
||||
Mockito.when(delegateSubFolderNfd.exists()).thenReturn(true);
|
||||
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
|
||||
Folder sub1 = folder.folder("\u00F4");
|
||||
Folder sub2 = folder.folder("\u006F\u0302");
|
||||
Mockito.verify(delegateSubFolderNfd, Mockito.never()).moveTo(Mockito.any());
|
||||
Assert.assertSame(sub1, sub2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoFolderMigration3() {
|
||||
Mockito.when(delegateSubFolderNfc.exists()).thenReturn(false);
|
||||
Mockito.when(delegateSubFolderNfd.exists()).thenReturn(false);
|
||||
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
|
||||
Folder sub1 = folder.folder("\u00F4");
|
||||
Folder sub2 = folder.folder("\u006F\u0302");
|
||||
Mockito.verify(delegateSubFolderNfd, Mockito.never()).moveTo(Mockito.any());
|
||||
Assert.assertSame(sub1, sub2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFolderMigration() {
|
||||
Mockito.when(delegateSubFolderNfc.exists()).thenReturn(false);
|
||||
Mockito.when(delegateSubFolderNfd.exists()).thenReturn(true);
|
||||
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
|
||||
Folder sub1 = folder.folder("\u00F4");
|
||||
Mockito.verify(delegateSubFolderNfd).moveTo(delegateSubFolderNfc);
|
||||
Folder sub2 = folder.folder("\u006F\u0302");
|
||||
Assert.assertSame(sub1, sub2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoFileMigration1() {
|
||||
Mockito.when(delegateSubFileNfc.exists()).thenReturn(true);
|
||||
Mockito.when(delegateSubFileNfd.exists()).thenReturn(false);
|
||||
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
|
||||
File sub1 = folder.file("\u00C5");
|
||||
File sub2 = folder.file("\u0041\u030A");
|
||||
Mockito.verify(delegateSubFileNfd, Mockito.never()).moveTo(Mockito.any());
|
||||
Assert.assertSame(sub1, sub2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoFileMigration2() {
|
||||
Mockito.when(delegateSubFileNfc.exists()).thenReturn(true);
|
||||
Mockito.when(delegateSubFileNfd.exists()).thenReturn(true);
|
||||
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
|
||||
File sub1 = folder.file("\u00C5");
|
||||
File sub2 = folder.file("\u0041\u030A");
|
||||
Mockito.verify(delegateSubFileNfd, Mockito.never()).moveTo(Mockito.any());
|
||||
Assert.assertSame(sub1, sub2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoFileMigration3() {
|
||||
Mockito.when(delegateSubFileNfc.exists()).thenReturn(false);
|
||||
Mockito.when(delegateSubFileNfd.exists()).thenReturn(false);
|
||||
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
|
||||
File sub1 = folder.file("\u00C5");
|
||||
File sub2 = folder.file("\u0041\u030A");
|
||||
Mockito.verify(delegateSubFileNfd, Mockito.never()).moveTo(Mockito.any());
|
||||
Assert.assertSame(sub1, sub2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFileMigration() {
|
||||
Mockito.when(delegateSubFileNfc.exists()).thenReturn(false);
|
||||
Mockito.when(delegateSubFileNfd.exists()).thenReturn(true);
|
||||
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
|
||||
File sub1 = folder.file("\u00C5");
|
||||
Mockito.verify(delegateSubFileNfd).moveTo(delegateSubFileNfc);
|
||||
File sub2 = folder.file("\u0041\u030A");
|
||||
Assert.assertSame(sub1, sub2);
|
||||
}
|
||||
|
||||
}
|
||||
22
main/filesystem-charsets/src/test/resources/log4j2.xml
Normal file
22
main/filesystem-charsets/src/test/resources/log4j2.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Configuration status="WARN">
|
||||
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
|
||||
</Console>
|
||||
<Console name="StdErr" target="SYSTEM_ERR">
|
||||
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
|
||||
</Console>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<Root level="DEBUG">
|
||||
<AppenderRef ref="Console" />
|
||||
<AppenderRef ref="StdErr" />
|
||||
</Root>
|
||||
</Loggers>
|
||||
|
||||
</Configuration>
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<version>1.2.3</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-crypto-integration-tests</artifactId>
|
||||
<name>Cryptomator filesystem: Encryption layer tests</name>
|
||||
|
||||
@@ -130,7 +130,7 @@ public class CryptoFileSystemIntegrationTest {
|
||||
|
||||
// toggle last bit
|
||||
try (WritableFile writable = physicalFile.openWritable(); ReadableFile readable = physicalFile.openReadable()) {
|
||||
ByteBuffer buf = ByteBuffer.allocate((int) readable.size());
|
||||
ByteBuffer buf = ByteBuffer.allocate((int) physicalFile.size());
|
||||
readable.read(buf);
|
||||
buf.array()[buf.limit() - 1] ^= 0x01;
|
||||
buf.flip();
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<version>1.2.3</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-crypto</artifactId>
|
||||
<name>Cryptomator filesystem: Encryption layer</name>
|
||||
|
||||
<properties>
|
||||
<bouncycastle.version>1.51</bouncycastle.version>
|
||||
<sivmode.version>1.0.2</sivmode.version>
|
||||
<sivmode.version>1.2.0</sivmode.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine;
|
||||
|
||||
abstract class CryptoException extends RuntimeException {
|
||||
public abstract class CryptoException extends RuntimeException {
|
||||
|
||||
public CryptoException() {
|
||||
super();
|
||||
|
||||
@@ -19,11 +19,6 @@ import javax.security.auth.Destroyable;
|
||||
*/
|
||||
public interface FileContentDecryptor extends Destroyable, Closeable {
|
||||
|
||||
/**
|
||||
* @return Number of bytes of the decrypted file.
|
||||
*/
|
||||
long contentLength();
|
||||
|
||||
/**
|
||||
* Appends further ciphertext to this decryptor. This method might block until space becomes available. If so, it is interruptable.
|
||||
*
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Provides deterministic encryption capabilities as filenames must not change on subsequent encryption attempts,
|
||||
* otherwise each change results in major directory structure changes which would be a terrible idea for cloud storage encryption.
|
||||
@@ -22,12 +24,9 @@ public interface FilenameCryptor {
|
||||
String hashDirectoryId(String cleartextDirectoryId);
|
||||
|
||||
/**
|
||||
* Tests without an actual decryption attempt, if a name is a well-formed ciphertext.
|
||||
*
|
||||
* @param ciphertextName Filename in question
|
||||
* @return <code>true</code> if the given name is likely to be a valid ciphertext
|
||||
* @return A Pattern that can be used to test, if a name is a well-formed ciphertext.
|
||||
*/
|
||||
boolean isEncryptedFilename(String ciphertextName);
|
||||
Pattern encryptedNamePattern();
|
||||
|
||||
/**
|
||||
* @param cleartextName original filename including cleartext file extension
|
||||
|
||||
@@ -11,28 +11,28 @@ package org.cryptomator.crypto.engine;
|
||||
public class UnsupportedVaultFormatException extends CryptoException {
|
||||
|
||||
private final Integer detectedVersion;
|
||||
private final Integer supportedVersion;
|
||||
private final Integer latestSupportedVersion;
|
||||
|
||||
public UnsupportedVaultFormatException(Integer detectedVersion, Integer supportedVersion) {
|
||||
super("Tried to open vault of version " + detectedVersion + ", but can only handle version " + supportedVersion);
|
||||
public UnsupportedVaultFormatException(Integer detectedVersion, Integer latestSupportedVersion) {
|
||||
super("Tried to open vault of version " + detectedVersion + ", latest supported version is " + latestSupportedVersion);
|
||||
this.detectedVersion = detectedVersion;
|
||||
this.supportedVersion = supportedVersion;
|
||||
this.latestSupportedVersion = latestSupportedVersion;
|
||||
}
|
||||
|
||||
public Integer getDetectedVersion() {
|
||||
return detectedVersion;
|
||||
}
|
||||
|
||||
public Integer getSupportedVersion() {
|
||||
return supportedVersion;
|
||||
public Integer getLatestSupportedVersion() {
|
||||
return latestSupportedVersion;
|
||||
}
|
||||
|
||||
public boolean isVaultOlderThanSoftware() {
|
||||
return detectedVersion == null || detectedVersion < supportedVersion;
|
||||
return detectedVersion == null || detectedVersion < latestSupportedVersion;
|
||||
}
|
||||
|
||||
public boolean isSoftwareOlderThanVault() {
|
||||
return detectedVersion > supportedVersion;
|
||||
return detectedVersion > latestSupportedVersion;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ public final class Constants {
|
||||
private Constants() {
|
||||
}
|
||||
|
||||
static final Integer CURRENT_VAULT_VERSION = 3;
|
||||
static final Integer CURRENT_VAULT_VERSION = 5;
|
||||
|
||||
public static final int PAYLOAD_SIZE = 32 * 1024;
|
||||
public static final int NONCE_SIZE = 16;
|
||||
|
||||
@@ -27,7 +27,12 @@ public class CryptoEngineModule {
|
||||
@Provides
|
||||
public SecureRandom provideSecureRandom() {
|
||||
try {
|
||||
return SecureRandom.getInstanceStrong();
|
||||
// https://tersesystems.com/2015/12/17/the-right-way-to-use-securerandom/
|
||||
final SecureRandom nativeRandom = SecureRandom.getInstanceStrong();
|
||||
byte[] seed = nativeRandom.generateSeed(55); // NIST SP800-90A suggests 440 bits for SHA1 seed
|
||||
SecureRandom sha1Random = SecureRandom.getInstance("SHA1PRNG");
|
||||
sha1Random.setSeed(seed);
|
||||
return sha1Random;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("No strong PRNGs available.", e);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
@@ -81,14 +82,15 @@ class CryptorImpl implements Cryptor {
|
||||
|
||||
@Override
|
||||
public void randomizeMasterkey() {
|
||||
final byte[] randomBytes = new byte[KEYLENGTH_IN_BYTES];
|
||||
try {
|
||||
randomSource.nextBytes(randomBytes);
|
||||
encryptionKey = new SecretKeySpec(randomBytes, ENCRYPTION_ALG);
|
||||
randomSource.nextBytes(randomBytes);
|
||||
macKey = new SecretKeySpec(randomBytes, ENCRYPTION_ALG);
|
||||
} finally {
|
||||
Arrays.fill(randomBytes, (byte) 0x00);
|
||||
KeyGenerator encKeyGen = KeyGenerator.getInstance(ENCRYPTION_ALG);
|
||||
encKeyGen.init(KEYLENGTH_IN_BYTES * Byte.SIZE, randomSource);
|
||||
encryptionKey = encKeyGen.generateKey();
|
||||
KeyGenerator macKeyGen = KeyGenerator.getInstance(MAC_ALG);
|
||||
macKeyGen.init(KEYLENGTH_IN_BYTES * Byte.SIZE, randomSource);
|
||||
macKey = macKeyGen.generateKey();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("Hard-coded algorithm doesn't exist.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,12 +118,12 @@ class CryptorImpl implements Cryptor {
|
||||
final SecretKey kek = new SecretKeySpec(kekBytes, ENCRYPTION_ALG);
|
||||
this.macKey = AesKeyWrap.unwrap(kek, keyFile.getMacMasterKey(), MAC_ALG);
|
||||
// future use (as soon as we need to prevent downgrade attacks):
|
||||
// final Mac mac = new ThreadLocalMac(macKey, MAC_ALG).get();
|
||||
// final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.BYTES).putInt(CURRENT_VAULT_VERSION).array());
|
||||
// if (!MessageDigest.isEqual(versionMac, keyFile.getVersionMac())) {
|
||||
// destroyQuietly(macKey);
|
||||
// throw new UnsupportedVaultFormatException(Integer.MAX_VALUE, CURRENT_VAULT_VERSION);
|
||||
// }
|
||||
// final Mac mac = new ThreadLocalMac(macKey, MAC_ALG).get();
|
||||
// final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.BYTES).putInt(CURRENT_VAULT_VERSION).array());
|
||||
// if (!MessageDigest.isEqual(versionMac, keyFile.getVersionMac())) {
|
||||
// destroyQuietly(macKey);
|
||||
// throw new UnsupportedVaultFormatException(Integer.MAX_VALUE, CURRENT_VAULT_VERSION);
|
||||
// }
|
||||
this.encryptionKey = AesKeyWrap.unwrap(kek, keyFile.getEncryptionMasterKey(), ENCRYPTION_ALG);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidPassphraseException();
|
||||
|
||||
@@ -22,7 +22,6 @@ import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
@@ -47,7 +46,6 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
|
||||
private final Supplier<Mac> hmacSha256;
|
||||
private final FileHeader header;
|
||||
private final boolean authenticate;
|
||||
private final LongAdder cleartextBytesDecrypted = new LongAdder();
|
||||
private ByteBuffer ciphertextBuffer = ByteBuffer.allocate(CHUNK_SIZE);
|
||||
private long chunkNumber = 0;
|
||||
|
||||
@@ -56,11 +54,11 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
|
||||
this.header = FileHeader.decrypt(headerKey, hmacSha256, header);
|
||||
this.authenticate = authenticate;
|
||||
this.chunkNumber = firstCiphertextByte / CHUNK_SIZE; // floor() by int-truncation
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() {
|
||||
return header.getPayload().getFilesize();
|
||||
|
||||
// vault version 5 and onwards should have filesize: -1
|
||||
if (this.header.getPayload().getFilesize() != -1l) {
|
||||
throw new UncheckedIOException(new IOException("Attempted to decrypt file with invalid header (probably from previous vault version)"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -105,15 +103,7 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
|
||||
@Override
|
||||
public ByteBuffer cleartext() throws InterruptedException {
|
||||
try {
|
||||
final ByteBuffer cleartext = dataProcessor.processedData();
|
||||
long bytesUntilLogicalEof = contentLength() - cleartextBytesDecrypted.sum();
|
||||
if (bytesUntilLogicalEof <= 0) {
|
||||
return FileContentCryptor.EOF;
|
||||
} else if (bytesUntilLogicalEof < cleartext.remaining()) {
|
||||
cleartext.limit((int) bytesUntilLogicalEof);
|
||||
}
|
||||
cleartextBytesDecrypted.add(cleartext.remaining());
|
||||
return cleartext;
|
||||
return dataProcessor.processedData();
|
||||
} catch (ExecutionException e) {
|
||||
if (e.getCause() instanceof AuthenticationFailedException) {
|
||||
throw new AuthenticationFailedException(e);
|
||||
|
||||
@@ -36,8 +36,6 @@ import org.cryptomator.io.ByteBuffers;
|
||||
class FileContentEncryptorImpl implements FileContentEncryptor {
|
||||
|
||||
private static final String HMAC_SHA256 = "HmacSHA256";
|
||||
private static final int PADDING_LOWER_BOUND = 4 * 1024; // 4k
|
||||
private static final int PADDING_UPPER_BOUND = 16 * 1024 * 1024; // 16M
|
||||
private static final int NUM_THREADS = Runtime.getRuntime().availableProcessors();
|
||||
private static final int READ_AHEAD = 2;
|
||||
private static final ExecutorService SHARED_DECRYPTION_EXECUTOR = Executors.newFixedThreadPool(NUM_THREADS);
|
||||
@@ -63,7 +61,7 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
|
||||
|
||||
@Override
|
||||
public ByteBuffer getHeader() {
|
||||
header.getPayload().setFilesize(cleartextBytesScheduledForEncryption.sum());
|
||||
header.getPayload().setFilesize(-1l);
|
||||
return header.toByteBuffer(headerKey, hmacSha256);
|
||||
}
|
||||
|
||||
@@ -76,7 +74,6 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
|
||||
public void append(ByteBuffer cleartext) throws InterruptedException {
|
||||
cleartextBytesScheduledForEncryption.add(cleartext.remaining());
|
||||
if (cleartext == FileContentCryptor.EOF) {
|
||||
appendSizeObfuscationPadding(cleartextBytesScheduledForEncryption.sum());
|
||||
submitCleartextBuffer();
|
||||
submitEof();
|
||||
} else {
|
||||
@@ -84,19 +81,6 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
|
||||
}
|
||||
}
|
||||
|
||||
private void appendSizeObfuscationPadding(long actualSize) throws InterruptedException {
|
||||
final int maxPaddingLength = (int) Math.min(Math.max(actualSize / 10, PADDING_LOWER_BOUND), PADDING_UPPER_BOUND); // preferably 10%, but at least lower bound and no more than upper bound
|
||||
final int randomPaddingLength = randomSource.nextInt(maxPaddingLength);
|
||||
final ByteBuffer buf = ByteBuffer.allocate(PAYLOAD_SIZE);
|
||||
int remainingPadding = randomPaddingLength;
|
||||
while (remainingPadding > 0) {
|
||||
int bytesInCurrentIteration = Math.min(remainingPadding, PAYLOAD_SIZE);
|
||||
buf.clear().limit(bytesInCurrentIteration);
|
||||
appendAllAndSubmitIfFull(buf);
|
||||
remainingPadding -= bytesInCurrentIteration;
|
||||
}
|
||||
}
|
||||
|
||||
private void appendAllAndSubmitIfFull(ByteBuffer cleartext) throws InterruptedException {
|
||||
while (cleartext.hasRemaining()) {
|
||||
ByteBuffers.copy(cleartext, cleartextBuffer);
|
||||
|
||||
@@ -11,12 +11,14 @@ package org.cryptomator.crypto.engine.impl;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.ShortBufferException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
@@ -36,13 +38,13 @@ class FileHeaderPayload implements Destroyable {
|
||||
private final SecretKey contentKey;
|
||||
|
||||
public FileHeaderPayload(SecureRandom randomSource) {
|
||||
filesize = 0;
|
||||
final byte[] contentKey = new byte[CONTENT_KEY_LEN];
|
||||
this.filesize = 0;
|
||||
try {
|
||||
randomSource.nextBytes(contentKey);
|
||||
this.contentKey = new SecretKeySpec(contentKey, AES);
|
||||
} finally {
|
||||
Arrays.fill(contentKey, (byte) 0x00);
|
||||
KeyGenerator keyGen = KeyGenerator.getInstance(AES);
|
||||
keyGen.init(CONTENT_KEY_LEN * Byte.SIZE, randomSource);
|
||||
this.contentKey = keyGen.generateKey();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("Hard-coded algorithm doesn't exist.", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,9 @@ import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.crypto.AEADBadTagException;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.apache.commons.codec.binary.Base32;
|
||||
@@ -21,10 +22,13 @@ import org.apache.commons.codec.binary.BaseNCodec;
|
||||
import org.cryptomator.crypto.engine.AuthenticationFailedException;
|
||||
import org.cryptomator.crypto.engine.FilenameCryptor;
|
||||
import org.cryptomator.siv.SivMode;
|
||||
import org.cryptomator.siv.UnauthenticCiphertextException;
|
||||
|
||||
class FilenameCryptorImpl implements FilenameCryptor {
|
||||
|
||||
private static final BaseNCodec BASE32 = new Base32();
|
||||
// https://tools.ietf.org/html/rfc4648#section-6
|
||||
private static final Pattern BASE32_PATTERN = Pattern.compile("^([A-Z2-7]{8})*[A-Z2-7=]{8}");
|
||||
private static final ThreadLocal<MessageDigest> SHA1 = new ThreadLocalSha1();
|
||||
private static final ThreadLocal<SivMode> AES_SIV = new ThreadLocal<SivMode>() {
|
||||
@Override
|
||||
@@ -50,8 +54,8 @@ class FilenameCryptorImpl implements FilenameCryptor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncryptedFilename(String ciphertextName) {
|
||||
return BASE32.isInAlphabet(ciphertextName);
|
||||
public Pattern encryptedNamePattern() {
|
||||
return BASE32_PATTERN;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -67,8 +71,8 @@ class FilenameCryptorImpl implements FilenameCryptor {
|
||||
try {
|
||||
final byte[] cleartextBytes = AES_SIV.get().decrypt(encryptionKey, macKey, encryptedBytes, associatedData);
|
||||
return new String(cleartextBytes, UTF_8);
|
||||
} catch (AEADBadTagException e) {
|
||||
throw new AuthenticationFailedException("Authentication failed.", e);
|
||||
} catch (UnauthenticCiphertextException | IllegalBlockSizeException e) {
|
||||
throw new AuthenticationFailedException("Invalid ciphertext.", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ final class ThreadLocalAesCtrCipher {
|
||||
try {
|
||||
return Cipher.getInstance(AES_CTR);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new IllegalStateException("Could not create MAC.", e);
|
||||
throw new IllegalStateException("Could not create Cipher.", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,11 +100,6 @@ class BlockAlignedReadableFile implements ReadableFile {
|
||||
return delegate.isOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws UncheckedIOException {
|
||||
return delegate.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws UncheckedIOException {
|
||||
delegate.close();
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import static org.cryptomator.filesystem.crypto.Constants.DIR_PREFIX;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.MatchResult;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.ReadableFile;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
final class ConflictResolver {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ConflictResolver.class);
|
||||
private static final int UUID_FIRST_GROUP_STRLEN = 8;
|
||||
private static final int MAX_DIR_FILE_SIZE = 87; // "normal" file header has 88 bytes
|
||||
|
||||
private final Pattern encryptedNamePattern;
|
||||
private final Function<String, Optional<String>> nameDecryptor;
|
||||
private final Function<String, Optional<String>> nameEncryptor;
|
||||
|
||||
public ConflictResolver(Pattern encryptedNamePattern, Function<String, Optional<String>> nameDecryptor, Function<String, Optional<String>> nameEncryptor) {
|
||||
this.encryptedNamePattern = encryptedNamePattern;
|
||||
this.nameDecryptor = nameDecryptor;
|
||||
this.nameEncryptor = nameEncryptor;
|
||||
}
|
||||
|
||||
public File resolveIfNecessary(File file) {
|
||||
Matcher m = encryptedNamePattern.matcher(StringUtils.removeStart(file.name(), DIR_PREFIX));
|
||||
if (m.matches()) {
|
||||
// full match, use file as is
|
||||
return file;
|
||||
} else if (m.find(0)) {
|
||||
// partial match, might be conflicting
|
||||
return resolveConflict(file, m.toMatchResult());
|
||||
} else {
|
||||
// no match, file not relevant
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
private File resolveConflict(File conflictingFile, MatchResult matchResult) {
|
||||
String ciphertext = matchResult.group();
|
||||
boolean isDirectory = conflictingFile.name().startsWith(DIR_PREFIX);
|
||||
Optional<String> cleartext = nameDecryptor.apply(ciphertext);
|
||||
if (cleartext.isPresent()) {
|
||||
Folder folder = conflictingFile.parent().get();
|
||||
File canonicalFile = folder.file(isDirectory ? DIR_PREFIX + ciphertext : ciphertext);
|
||||
if (isDirectory && canonicalFile.exists() && isSameFileBasedOnSample(canonicalFile, conflictingFile, MAX_DIR_FILE_SIZE)) {
|
||||
// there must not be two directories pointing to the same directory id. In this case no human interaction is needed to resolve this conflict:
|
||||
conflictingFile.delete();
|
||||
return canonicalFile;
|
||||
} else {
|
||||
// conventional conflict detected! look for an alternative name:
|
||||
File alternativeFile;
|
||||
String conflictId;
|
||||
do {
|
||||
conflictId = createConflictId();
|
||||
String alternativeCleartext = cleartext.get() + " (Conflict " + conflictId + ")";
|
||||
String alternativeCiphertext = nameEncryptor.apply(alternativeCleartext).get();
|
||||
alternativeFile = folder.file(isDirectory ? DIR_PREFIX + alternativeCiphertext : alternativeCiphertext);
|
||||
} while (alternativeFile.exists());
|
||||
LOG.debug("Detected conflict {}:\n{}\n{}", conflictId, canonicalFile, conflictingFile);
|
||||
conflictingFile.moveTo(alternativeFile);
|
||||
return alternativeFile;
|
||||
}
|
||||
} else {
|
||||
// not decryptable; false positive
|
||||
return conflictingFile;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSameFileBasedOnSample(File file1, File file2, int sampleSize) {
|
||||
if (file1.size() != file2.size()) {
|
||||
return false;
|
||||
} else {
|
||||
try (ReadableFile r1 = file1.openReadable(); ReadableFile r2 = file2.openReadable()) {
|
||||
ByteBuffer beginOfFile1 = ByteBuffer.allocate(sampleSize);
|
||||
ByteBuffer beginOfFile2 = ByteBuffer.allocate(sampleSize);
|
||||
int bytesRead1 = r1.read(beginOfFile1);
|
||||
int bytesRead2 = r2.read(beginOfFile2);
|
||||
if (bytesRead1 == bytesRead2) {
|
||||
beginOfFile1.flip();
|
||||
beginOfFile2.flip();
|
||||
return beginOfFile1.equals(beginOfFile2);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String createConflictId() {
|
||||
return UUID.randomUUID().toString().substring(0, UUID_FIRST_GROUP_STRLEN);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,9 +8,9 @@ public final class Constants {
|
||||
static final String DATA_ROOT_DIR = "d";
|
||||
static final String ROOT_DIRECOTRY_ID = "";
|
||||
|
||||
static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
|
||||
static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup";
|
||||
public static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
|
||||
public static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup";
|
||||
|
||||
static final String DIR_SUFFIX = "_";
|
||||
static final String DIR_PREFIX = "0";
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.cryptomator.crypto.engine.impl.Constants.CHUNK_SIZE;
|
||||
import static org.cryptomator.crypto.engine.impl.Constants.PAYLOAD_SIZE;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
@@ -27,9 +28,26 @@ class CryptoFile extends CryptoNode implements File {
|
||||
|
||||
@Override
|
||||
protected Optional<String> encryptedName() {
|
||||
return parent().get().getDirectoryId().map(s -> s.getBytes(UTF_8)).map(parentDirId -> {
|
||||
return cryptor.getFilenameCryptor().encryptFilename(name(), parentDirId);
|
||||
});
|
||||
return parent().get().encryptChildName(name());
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws UncheckedIOException {
|
||||
if (!physicalFile().isPresent()) {
|
||||
return -1l;
|
||||
} else {
|
||||
File file = physicalFile().get();
|
||||
long ciphertextSize = file.size() - cryptor.getFileContentCryptor().getHeaderSize();
|
||||
long overheadPerChunk = CHUNK_SIZE - PAYLOAD_SIZE;
|
||||
long numFullChunks = ciphertextSize / CHUNK_SIZE; // floor by int-truncation
|
||||
long additionalCiphertextBytes = ciphertextSize % CHUNK_SIZE;
|
||||
if (additionalCiphertextBytes > 0 && additionalCiphertextBytes <= overheadPerChunk) {
|
||||
throw new IllegalArgumentException("Method not defined for input value " + ciphertextSize);
|
||||
}
|
||||
long additionalCleartextBytes = (additionalCiphertextBytes == 0) ? 0 : additionalCiphertextBytes - overheadPerChunk;
|
||||
assert additionalCleartextBytes >= 0;
|
||||
return PAYLOAD_SIZE * numFullChunks + additionalCleartextBytes;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.cryptomator.filesystem.crypto.Constants.DIR_SUFFIX;
|
||||
import static org.apache.commons.lang3.StringUtils.removeStart;
|
||||
import static org.cryptomator.filesystem.crypto.Constants.DIR_PREFIX;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.UncheckedIOException;
|
||||
@@ -18,36 +20,44 @@ import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.cryptomator.common.LazyInitializer;
|
||||
import org.cryptomator.common.WeakValuedCache;
|
||||
import org.cryptomator.common.streams.AutoClosingStream;
|
||||
import org.cryptomator.crypto.engine.CryptoException;
|
||||
import org.cryptomator.crypto.engine.Cryptor;
|
||||
import org.cryptomator.filesystem.Deleter;
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.Node;
|
||||
import org.cryptomator.io.FileContents;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
class CryptoFolder extends CryptoNode implements Folder {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CryptoFolder.class);
|
||||
private final WeakValuedCache<String, CryptoFolder> folders = WeakValuedCache.usingLoader(this::newFolder);
|
||||
private final WeakValuedCache<String, CryptoFile> files = WeakValuedCache.usingLoader(this::newFile);
|
||||
private final AtomicReference<String> directoryId = new AtomicReference<>();
|
||||
private final ConflictResolver conflictResolver;
|
||||
|
||||
public CryptoFolder(CryptoFolder parent, String name, Cryptor cryptor) {
|
||||
super(parent, name, cryptor);
|
||||
this.conflictResolver = new ConflictResolver(cryptor.getFilenameCryptor().encryptedNamePattern(), this::decryptChildName, this::encryptChildName);
|
||||
}
|
||||
|
||||
/* ======================= name + directory id ======================= */
|
||||
|
||||
@Override
|
||||
protected Optional<String> encryptedName() {
|
||||
if (parent().isPresent()) {
|
||||
return parent().get().getDirectoryId().map(s -> s.getBytes(UTF_8)).map(parentDirId -> {
|
||||
return cryptor.getFilenameCryptor().encryptFilename(name(), parentDirId) + DIR_SUFFIX;
|
||||
});
|
||||
return parent().get().encryptChildName(name()).map(s -> DIR_PREFIX + s);
|
||||
} else {
|
||||
return Optional.of(cryptor.getFilenameCryptor().encryptFilename(name()) + DIR_SUFFIX);
|
||||
return Optional.of(DIR_PREFIX + cryptor.getFilenameCryptor().encryptFilename(name()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,24 +82,60 @@ class CryptoFolder extends CryptoNode implements Folder {
|
||||
}));
|
||||
}
|
||||
|
||||
/* ======================= children ======================= */
|
||||
|
||||
@Override
|
||||
public Stream<? extends Node> children() {
|
||||
return Stream.concat(files(), folders());
|
||||
return AutoClosingStream.from(Stream.concat(files(), folders()));
|
||||
}
|
||||
|
||||
private Stream<File> nonConflictingFiles() {
|
||||
if (exists()) {
|
||||
final Stream<? extends File> files = physicalFolder().filter(Folder::exists).map(Folder::files).orElse(Stream.empty());
|
||||
return files.filter(startsWithEncryptedName()).map(conflictResolver::resolveIfNecessary).distinct();
|
||||
} else {
|
||||
throw new UncheckedIOException(new FileNotFoundException(format("Folder %s does not exist", this)));
|
||||
}
|
||||
}
|
||||
|
||||
private Predicate<File> startsWithEncryptedName() {
|
||||
final Pattern encryptedNamePattern = cryptor.getFilenameCryptor().encryptedNamePattern();
|
||||
return (File file) -> encryptedNamePattern.matcher(removeStart(file.name(),DIR_PREFIX)).find();
|
||||
}
|
||||
|
||||
Optional<String> decryptChildName(String ciphertextFileName) {
|
||||
return getDirectoryId().map(s -> s.getBytes(UTF_8)).map(dirId -> {
|
||||
try {
|
||||
return cryptor.getFilenameCryptor().decryptFilename(ciphertextFileName, dirId);
|
||||
} catch (CryptoException e) {
|
||||
LOG.warn("Filename decryption of {} failed: {}", ciphertextFileName, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Optional<String> encryptChildName(String cleartextFileName) {
|
||||
return getDirectoryId().map(s -> s.getBytes(UTF_8)).map(dirId -> {
|
||||
return cryptor.getFilenameCryptor().encryptFilename(cleartextFileName, dirId);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<CryptoFile> files() {
|
||||
assert forceGetPhysicalFolder().exists();
|
||||
return forceGetPhysicalFolder().files().map(File::name).filter(isEncryptedFileName()).map(this::decryptChildFileName).map(this::file);
|
||||
return nonConflictingFiles().map(File::name).filter(startsWithDirPrefix().negate()).map(this::decryptChildName).filter(Optional::isPresent).map(Optional::get).map(this::file);
|
||||
}
|
||||
|
||||
private Predicate<String> isEncryptedFileName() {
|
||||
return (String name) -> !name.endsWith(DIR_SUFFIX) && cryptor.getFilenameCryptor().isEncryptedFilename(name);
|
||||
@Override
|
||||
public Stream<CryptoFolder> folders() {
|
||||
return nonConflictingFiles().map(File::name).filter(startsWithDirPrefix()).map(this::removeDirPrefix).map(this::decryptChildName).filter(Optional::isPresent).map(Optional::get).map(this::folder);
|
||||
}
|
||||
|
||||
private String decryptChildFileName(String encryptedFileName) {
|
||||
final byte[] dirId = getDirectoryId().get().getBytes(UTF_8);
|
||||
return cryptor.getFilenameCryptor().decryptFilename(encryptedFileName, dirId);
|
||||
private Predicate<String> startsWithDirPrefix() {
|
||||
return (String encryptedFolderName) -> StringUtils.startsWith(encryptedFolderName, DIR_PREFIX);
|
||||
}
|
||||
|
||||
private String removeDirPrefix(String encryptedFolderName) {
|
||||
return StringUtils.removeStart(encryptedFolderName, DIR_PREFIX);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -97,35 +143,21 @@ class CryptoFolder extends CryptoNode implements Folder {
|
||||
return files.get(name);
|
||||
}
|
||||
|
||||
public CryptoFile newFile(String name) {
|
||||
return new CryptoFile(this, name, cryptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<CryptoFolder> folders() {
|
||||
assert forceGetPhysicalFolder().exists();
|
||||
return forceGetPhysicalFolder().files().map(File::name).filter(isEncryptedDirectoryName()).map(this::decryptChildFolderName).map(this::folder);
|
||||
}
|
||||
|
||||
private Predicate<String> isEncryptedDirectoryName() {
|
||||
return (String name) -> name.endsWith(DIR_SUFFIX) && cryptor.getFilenameCryptor().isEncryptedFilename(StringUtils.removeEnd(name, DIR_SUFFIX));
|
||||
}
|
||||
|
||||
private String decryptChildFolderName(String encryptedFolderName) {
|
||||
final byte[] dirId = getDirectoryId().get().getBytes(UTF_8);
|
||||
final String ciphertext = StringUtils.removeEnd(encryptedFolderName, DIR_SUFFIX);
|
||||
return cryptor.getFilenameCryptor().decryptFilename(ciphertext, dirId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CryptoFolder folder(String name) {
|
||||
return folders.get(name);
|
||||
}
|
||||
|
||||
public CryptoFolder newFolder(String name) {
|
||||
private CryptoFile newFile(String name) {
|
||||
return new CryptoFile(this, name, cryptor);
|
||||
}
|
||||
|
||||
private CryptoFolder newFolder(String name) {
|
||||
return new CryptoFolder(this, name, cryptor);
|
||||
}
|
||||
|
||||
/* ======================= create/move/delete ======================= */
|
||||
|
||||
@Override
|
||||
public void create() {
|
||||
parent.create();
|
||||
@@ -175,7 +207,7 @@ class CryptoFolder extends CryptoNode implements Folder {
|
||||
// cut all ties:
|
||||
this.invalidateDirectoryIdsRecursively();
|
||||
|
||||
assert!exists();
|
||||
assert !exists();
|
||||
assert target.exists();
|
||||
}
|
||||
|
||||
@@ -189,7 +221,7 @@ class CryptoFolder extends CryptoNode implements Folder {
|
||||
Folder physicalFolder = forceGetPhysicalFolder();
|
||||
physicalFolder.delete();
|
||||
Folder physicalFolderParent = physicalFolder.parent().get();
|
||||
if (physicalFolderParent.folders().count() == 0) {
|
||||
if (physicalFolderParent.exists() && physicalFolderParent.folders().count() == 0) {
|
||||
physicalFolderParent.delete();
|
||||
}
|
||||
forceGetPhysicalFile().delete();
|
||||
|
||||
@@ -70,12 +70,6 @@ class CryptoReadableFile implements ReadableFile {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws UncheckedIOException {
|
||||
assert decryptor != null : "decryptor is always being set during position(long)";
|
||||
return decryptor.contentLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void position(long position) throws UncheckedIOException {
|
||||
if (readAheadTask != null) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
@@ -52,10 +53,14 @@ class Masterkeys {
|
||||
public Cryptor decrypt(Folder vaultLocation, CharSequence passphrase) throws InvalidPassphraseException {
|
||||
File masterkeyFile = vaultLocation.file(MASTERKEY_FILENAME);
|
||||
Cryptor cryptor = cryptorProvider.get();
|
||||
boolean success = false;
|
||||
try {
|
||||
readMasterKey(masterkeyFile, cryptor, passphrase);
|
||||
} catch (UncheckedIOException e) {
|
||||
cryptor.destroy();
|
||||
success = true;
|
||||
} finally {
|
||||
if (!success) {
|
||||
cryptor.destroy();
|
||||
}
|
||||
}
|
||||
return cryptor;
|
||||
}
|
||||
@@ -86,7 +91,9 @@ class Masterkeys {
|
||||
/* I/O */
|
||||
|
||||
private static void readMasterKey(File file, Cryptor cryptor, CharSequence passphrase) throws UncheckedIOException, InvalidPassphraseException {
|
||||
try (InputStream in = Channels.newInputStream(file.openReadable())) {
|
||||
try ( //
|
||||
ReadableByteChannel channel = file.openReadable(); //
|
||||
InputStream in = Channels.newInputStream(channel)) {
|
||||
final byte[] fileContents = IOUtils.toByteArray(in);
|
||||
cryptor.readKeysFromMasterkeyFile(fileContents, passphrase);
|
||||
} catch (IOException e) {
|
||||
@@ -96,6 +103,7 @@ class Masterkeys {
|
||||
|
||||
private static void writeMasterKey(File file, Cryptor cryptor, CharSequence passphrase) throws UncheckedIOException {
|
||||
try (WritableFile writable = file.openWritable()) {
|
||||
writable.truncate();
|
||||
final byte[] fileContents = cryptor.writeKeysToMasterkeyFile(passphrase);
|
||||
writable.write(ByteBuffer.wrap(fileContents));
|
||||
}
|
||||
|
||||
@@ -44,16 +44,9 @@ class NoFileContentCryptor implements FileContentCryptor {
|
||||
private class Decryptor implements FileContentDecryptor {
|
||||
|
||||
private final BlockingQueue<Supplier<ByteBuffer>> cleartextQueue = new LinkedBlockingQueue<>();
|
||||
private final long contentLength;
|
||||
|
||||
private Decryptor(ByteBuffer header) {
|
||||
assert header.remaining() == Long.BYTES;
|
||||
this.contentLength = header.getLong();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() {
|
||||
return contentLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -12,6 +12,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.codec.binary.Base32;
|
||||
import org.apache.commons.codec.binary.BaseNCodec;
|
||||
@@ -19,6 +20,7 @@ import org.apache.commons.codec.binary.BaseNCodec;
|
||||
class NoFilenameCryptor implements FilenameCryptor {
|
||||
|
||||
private static final BaseNCodec BASE32 = new Base32();
|
||||
private static final Pattern WILDCARD_PATTERN = Pattern.compile(".*");
|
||||
private static final ThreadLocal<MessageDigest> SHA1 = new ThreadLocalSha1();
|
||||
|
||||
@Override
|
||||
@@ -29,8 +31,8 @@ class NoFilenameCryptor implements FilenameCryptor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncryptedFilename(String ciphertextName) {
|
||||
return true;
|
||||
public Pattern encryptedNamePattern() {
|
||||
return WILDCARD_PATTERN;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,20 +21,20 @@ public class CryptorImplTest {
|
||||
|
||||
@Test
|
||||
public void testMasterkeyDecryptionWithCorrectPassphrase() throws IOException {
|
||||
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
|
||||
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
|
||||
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
||||
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
|
||||
}
|
||||
|
||||
@Test(expected = InvalidPassphraseException.class)
|
||||
public void testMasterkeyDecryptionWithWrongPassphrase() throws IOException {
|
||||
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
|
||||
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
|
||||
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
||||
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "qwe");
|
||||
}
|
||||
@@ -44,7 +44,7 @@ public class CryptorImplTest {
|
||||
final String testMasterKey = "{\"version\":-1,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
|
||||
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
|
||||
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
||||
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
|
||||
}
|
||||
@@ -52,7 +52,7 @@ public class CryptorImplTest {
|
||||
@Ignore
|
||||
@Test(expected = UnsupportedVaultFormatException.class)
|
||||
public void testMasterkeyDecryptionWithMissingVersionMac() throws IOException {
|
||||
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"}";
|
||||
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
||||
@@ -62,20 +62,20 @@ public class CryptorImplTest {
|
||||
@Ignore
|
||||
@Test(expected = UnsupportedVaultFormatException.class)
|
||||
public void testMasterkeyDecryptionWithWrongVersionMac() throws IOException {
|
||||
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLa=\"}";
|
||||
+ "\"versionMac\":\"z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfoK=\"}";
|
||||
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
||||
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMasterkeyEncryption() throws IOException {
|
||||
final String expectedMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," //
|
||||
final String expectedMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," //
|
||||
+ "\"primaryMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
|
||||
+ "\"hmacMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
|
||||
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
|
||||
+ "\"versionMac\":\"yuwoRE9GSdgQ2b//qRpTCj3W0qsVLxYVa7/KB3PkfA4=\"}";
|
||||
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
||||
cryptor.randomizeMasterkey();
|
||||
final byte[] masterkeyFile = cryptor.writeKeysToMasterkeyFile("asd");
|
||||
|
||||
@@ -43,20 +43,6 @@ public class FileContentCryptorImplTest {
|
||||
|
||||
};
|
||||
|
||||
private static final SecureRandom RANDOM_MOCK_2 = new SecureRandom() {
|
||||
|
||||
@Override
|
||||
public int nextInt(int bound) {
|
||||
return 500;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nextBytes(byte[] bytes) {
|
||||
Arrays.fill(bytes, (byte) 0x00);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testShortHeaderInDecryptor() throws InterruptedException {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
@@ -137,45 +123,6 @@ public class FileContentCryptorImplTest {
|
||||
Assert.assertArrayEquals("cleartext message".getBytes(), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncryptionAndDecryptionWithSizeObfuscationPadding() throws InterruptedException {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
FileContentCryptor cryptor = new FileContentCryptorImpl(encryptionKey, macKey, RANDOM_MOCK_2);
|
||||
|
||||
ByteBuffer header = ByteBuffer.allocate(cryptor.getHeaderSize());
|
||||
ByteBuffer ciphertext = ByteBuffer.allocate(16 + 11 + 500 + 32 + 1); // 16 bytes iv + 11 bytes ciphertext + 500 bytes padding + 32 bytes mac + 1.
|
||||
try (FileContentEncryptor encryptor = cryptor.createFileContentEncryptor(Optional.empty(), 0)) {
|
||||
encryptor.append(ByteBuffer.wrap("hello world".getBytes()));
|
||||
encryptor.append(FileContentCryptor.EOF);
|
||||
ByteBuffer buf;
|
||||
while ((buf = encryptor.ciphertext()) != FileContentCryptor.EOF) {
|
||||
ByteBuffers.copy(buf, ciphertext);
|
||||
}
|
||||
ByteBuffers.copy(encryptor.getHeader(), header);
|
||||
}
|
||||
header.flip();
|
||||
ciphertext.flip();
|
||||
|
||||
Assert.assertEquals(16 + 11 + 500 + 32, ciphertext.remaining());
|
||||
|
||||
ByteBuffer plaintext = ByteBuffer.allocate(12); // 11 bytes plaintext + 1
|
||||
try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header, 0, true)) {
|
||||
decryptor.append(ciphertext);
|
||||
decryptor.append(FileContentCryptor.EOF);
|
||||
ByteBuffer buf;
|
||||
while ((buf = decryptor.cleartext()) != FileContentCryptor.EOF) {
|
||||
ByteBuffers.copy(buf, plaintext);
|
||||
}
|
||||
}
|
||||
plaintext.flip();
|
||||
|
||||
byte[] result = new byte[plaintext.remaining()];
|
||||
plaintext.get(result);
|
||||
Assert.assertArrayEquals("hello world".getBytes(), result);
|
||||
}
|
||||
|
||||
@Test(timeout = 20000) // assuming a minimum speed of 10mb/s during encryption and decryption 20s should be enough
|
||||
public void testEncryptionAndDecryptionSpeed() throws InterruptedException, IOException {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
|
||||
@@ -45,7 +45,7 @@ public class FileContentDecryptorImplTest {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
|
||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
|
||||
final byte[] content = Base64.decode("AAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3Og=");
|
||||
|
||||
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
|
||||
@@ -68,7 +68,7 @@ public class FileContentDecryptorImplTest {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJa==");
|
||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJG==");
|
||||
|
||||
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
|
||||
|
||||
@@ -80,7 +80,7 @@ public class FileContentDecryptorImplTest {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
|
||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
|
||||
final byte[] content = Base64.decode("aAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3Og=");
|
||||
|
||||
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
|
||||
@@ -101,7 +101,7 @@ public class FileContentDecryptorImplTest {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
|
||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
|
||||
final byte[] content = Base64.decode("AAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3OG=");
|
||||
|
||||
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, false)) {
|
||||
@@ -124,7 +124,7 @@ public class FileContentDecryptorImplTest {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
|
||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
|
||||
|
||||
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
|
||||
decryptor.cancelWithException(new IOException("can not do"));
|
||||
|
||||
@@ -35,20 +35,6 @@ public class FileContentEncryptorImplTest {
|
||||
|
||||
};
|
||||
|
||||
private static final SecureRandom RANDOM_MOCK_2 = new SecureRandom() {
|
||||
|
||||
@Override
|
||||
public int nextInt(int bound) {
|
||||
return 42;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nextBytes(byte[] bytes) {
|
||||
Arrays.fill(bytes, (byte) 0x00);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@Test
|
||||
public void testEncryption() throws InterruptedException {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
@@ -95,24 +81,4 @@ public class FileContentEncryptorImplTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSizeObfuscation() throws InterruptedException {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
|
||||
|
||||
try (FileContentEncryptor encryptor = new FileContentEncryptorImpl(headerKey, macKey, RANDOM_MOCK_2, 0)) {
|
||||
encryptor.append(FileContentCryptor.EOF);
|
||||
|
||||
ByteBuffer result = ByteBuffer.allocate(91); // 16 bytes iv + 42 bytes size obfuscation + 32 bytes mac + 1
|
||||
ByteBuffer buf;
|
||||
while ((buf = encryptor.ciphertext()) != FileContentCryptor.EOF) {
|
||||
ByteBuffers.copy(buf, result);
|
||||
}
|
||||
result.flip();
|
||||
|
||||
Assert.assertEquals(90, result.remaining());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -53,13 +53,26 @@ public class FileHeaderTest {
|
||||
|
||||
@Test
|
||||
public void testDecryption() {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
final ByteBuffer headerBuf = ByteBuffer.wrap(Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg=="));
|
||||
final FileHeader header = FileHeader.decrypt(headerKey, new ThreadLocalMac(macKey, "HmacSHA256"), headerBuf);
|
||||
|
||||
Assert.assertEquals(-1l, header.getPayload().getFilesize());
|
||||
Assert.assertArrayEquals(new byte[16], header.getIv());
|
||||
Assert.assertArrayEquals(new byte[32], header.getPayload().getContentKey().getEncoded());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecryptionOfOldHeader() {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
final ByteBuffer headerBuf = ByteBuffer.wrap(Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA=="));
|
||||
final FileHeader header = FileHeader.decrypt(headerKey, new ThreadLocalMac(macKey, "HmacSHA256"), headerBuf);
|
||||
|
||||
Assert.assertEquals(42, header.getPayload().getFilesize());
|
||||
Assert.assertEquals(42l, header.getPayload().getFilesize());
|
||||
Assert.assertArrayEquals(new byte[16], header.getIv());
|
||||
Assert.assertArrayEquals(new byte[32], header.getPayload().getContentKey().getEncoded());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.codec.binary.Base32;
|
||||
import org.apache.commons.codec.binary.BaseNCodec;
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.ReadableFile;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
public class ConflictResolverTest {
|
||||
|
||||
private ConflictResolver conflictResolver;
|
||||
private Folder folder;
|
||||
private File canonicalFile;
|
||||
private File canonicalFolder;
|
||||
private File conflictingFile;
|
||||
private File conflictingFolder;
|
||||
private File resolved;
|
||||
private File unrelatedFile;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
Pattern base32Pattern = Pattern.compile("([A-Z0-9]{8})*[A-Z0-9=]{8}");
|
||||
BaseNCodec base32 = new Base32();
|
||||
Function<String, Optional<String>> decode = (s) -> Optional.of(new String(base32.decode(s), StandardCharsets.UTF_8));
|
||||
Function<String, Optional<String>> encode = (s) -> Optional.of(base32.encodeAsString(s.getBytes(StandardCharsets.UTF_8)));
|
||||
conflictResolver = new ConflictResolver(base32Pattern, decode, encode);
|
||||
|
||||
folder = Mockito.mock(Folder.class);
|
||||
canonicalFile = Mockito.mock(File.class);
|
||||
canonicalFolder = Mockito.mock(File.class);
|
||||
conflictingFile = Mockito.mock(File.class);
|
||||
conflictingFolder = Mockito.mock(File.class);
|
||||
resolved = Mockito.mock(File.class);
|
||||
unrelatedFile = Mockito.mock(File.class);
|
||||
|
||||
String canonicalFileName = encode.apply("test name").get();
|
||||
String canonicalFolderName = Constants.DIR_PREFIX + canonicalFileName;
|
||||
String conflictingFileName = canonicalFileName + " (version 2)";
|
||||
String conflictingFolderName = canonicalFolderName + " (version 2)";
|
||||
String unrelatedName = "notBa$e32!";
|
||||
|
||||
Mockito.when(canonicalFile.name()).thenReturn(canonicalFileName);
|
||||
Mockito.when(canonicalFolder.name()).thenReturn(canonicalFolderName);
|
||||
Mockito.when(conflictingFile.name()).thenReturn(conflictingFileName);
|
||||
Mockito.when(conflictingFolder.name()).thenReturn(conflictingFolderName);
|
||||
Mockito.when(unrelatedFile.name()).thenReturn(unrelatedName);
|
||||
|
||||
Mockito.when(canonicalFile.exists()).thenReturn(true);
|
||||
Mockito.when(canonicalFolder.exists()).thenReturn(true);
|
||||
Mockito.when(conflictingFile.exists()).thenReturn(true);
|
||||
Mockito.when(conflictingFolder.exists()).thenReturn(true);
|
||||
Mockito.when(unrelatedFile.exists()).thenReturn(true);
|
||||
|
||||
Mockito.doReturn(Optional.of(folder)).when(canonicalFile).parent();
|
||||
Mockito.doReturn(Optional.of(folder)).when(canonicalFolder).parent();
|
||||
Mockito.doReturn(Optional.of(folder)).when(conflictingFile).parent();
|
||||
Mockito.doReturn(Optional.of(folder)).when(conflictingFolder).parent();
|
||||
Mockito.doReturn(Optional.of(folder)).when(unrelatedFile).parent();
|
||||
|
||||
Mockito.when(folder.file(Mockito.startsWith(canonicalFileName.substring(0, 8)))).thenReturn(resolved);
|
||||
Mockito.when(folder.file(Mockito.startsWith(canonicalFolderName.substring(0, 8)))).thenReturn(resolved);
|
||||
Mockito.when(folder.file(canonicalFileName)).thenReturn(canonicalFile);
|
||||
Mockito.when(folder.file(canonicalFolderName)).thenReturn(canonicalFolder);
|
||||
Mockito.when(folder.file(conflictingFileName)).thenReturn(conflictingFile);
|
||||
Mockito.when(folder.file(conflictingFolderName)).thenReturn(conflictingFolder);
|
||||
Mockito.when(folder.file(unrelatedName)).thenReturn(unrelatedFile);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCanonicalName() {
|
||||
File result = conflictResolver.resolveIfNecessary(canonicalFile);
|
||||
Assert.assertSame(canonicalFile, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnrelatedName() {
|
||||
File result = conflictResolver.resolveIfNecessary(unrelatedFile);
|
||||
Assert.assertSame(unrelatedFile, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConflictingFile() {
|
||||
File result = conflictResolver.resolveIfNecessary(conflictingFile);
|
||||
Mockito.verify(conflictingFile).moveTo(resolved);
|
||||
Assert.assertSame(resolved, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConflictingFileIfCanonicalDoesntExist() {
|
||||
Mockito.when(canonicalFile.exists()).thenReturn(false);
|
||||
File result = conflictResolver.resolveIfNecessary(conflictingFile);
|
||||
Mockito.verify(conflictingFile).moveTo(resolved);
|
||||
Assert.assertSame(resolved, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConflictingFolderWithDifferentId() {
|
||||
ReadableFile directoryId1 = Mockito.mock(ReadableFile.class);
|
||||
ReadableFile directoryId2 = Mockito.mock(ReadableFile.class);
|
||||
Mockito.when(canonicalFolder.openReadable()).thenReturn(directoryId1);
|
||||
Mockito.when(conflictingFolder.openReadable()).thenReturn(directoryId2);
|
||||
Mockito.when(directoryId1.read(Mockito.any())).thenAnswer(new FillBufferAnswer("id1"));
|
||||
Mockito.when(directoryId2.read(Mockito.any())).thenAnswer(new FillBufferAnswer("id2"));
|
||||
|
||||
File result = conflictResolver.resolveIfNecessary(conflictingFolder);
|
||||
Mockito.verify(conflictingFolder).moveTo(resolved);
|
||||
Assert.assertSame(resolved, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConflictingFolderWithSameId() {
|
||||
ReadableFile directoryId1 = Mockito.mock(ReadableFile.class);
|
||||
ReadableFile directoryId2 = Mockito.mock(ReadableFile.class);
|
||||
Mockito.when(canonicalFolder.openReadable()).thenReturn(directoryId1);
|
||||
Mockito.when(conflictingFolder.openReadable()).thenReturn(directoryId2);
|
||||
Mockito.when(directoryId1.read(Mockito.any())).thenAnswer(new FillBufferAnswer("id1"));
|
||||
Mockito.when(directoryId2.read(Mockito.any())).thenAnswer(new FillBufferAnswer("id1"));
|
||||
|
||||
File result = conflictResolver.resolveIfNecessary(conflictingFolder);
|
||||
Mockito.verify(conflictingFolder).delete();
|
||||
Assert.assertSame(canonicalFolder, result);
|
||||
}
|
||||
|
||||
private static class FillBufferAnswer implements Answer<Integer> {
|
||||
|
||||
private final byte[] content;
|
||||
private int bytesRead = 0;
|
||||
|
||||
public FillBufferAnswer(String content) {
|
||||
this.content = content.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer answer(InvocationOnMock invocation) throws Throwable {
|
||||
if (bytesRead >= content.length) {
|
||||
bytesRead = 0;
|
||||
return -1;
|
||||
} else {
|
||||
ByteBuffer buf = invocation.getArgumentAt(0, ByteBuffer.class);
|
||||
int delta = Math.min(content.length - bytesRead, buf.remaining());
|
||||
buf.put(content, bytesRead, delta);
|
||||
bytesRead += delta;
|
||||
return content.length;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<version>1.2.3</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-inmemory</artifactId>
|
||||
<name>Cryptomator filesystem: In-memory mock</name>
|
||||
|
||||
@@ -43,6 +43,11 @@ class InMemoryFile extends InMemoryNode implements File {
|
||||
return buf;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws UncheckedIOException {
|
||||
return content.get().limit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveTo(File destination) throws UncheckedIOException {
|
||||
if (destination instanceof InMemoryFile) {
|
||||
@@ -103,7 +108,7 @@ class InMemoryFile extends InMemoryNode implements File {
|
||||
throw new UncheckedIOException(new FileAlreadyExistsException(k));
|
||||
} else {
|
||||
if (v == null) {
|
||||
assert!content.get().hasRemaining();
|
||||
assert !content.get().hasRemaining();
|
||||
this.creationTime = Instant.now();
|
||||
}
|
||||
this.lastModified = Instant.now();
|
||||
@@ -120,7 +125,7 @@ class InMemoryFile extends InMemoryNode implements File {
|
||||
// returning null removes the entry.
|
||||
return null;
|
||||
});
|
||||
assert!this.exists();
|
||||
assert !this.exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -51,11 +51,6 @@ class InMemoryReadableFile implements ReadableFile {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws UncheckedIOException {
|
||||
return contentGetter.get().limit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void position(long position) throws UncheckedIOException {
|
||||
assert position < Integer.MAX_VALUE : "Can not use that big in-memory files.";
|
||||
@@ -64,8 +59,10 @@ class InMemoryReadableFile implements ReadableFile {
|
||||
|
||||
@Override
|
||||
public void close() throws UncheckedIOException {
|
||||
open.set(false);
|
||||
readLock.unlock();
|
||||
if (open.get()) {
|
||||
open.set(false);
|
||||
readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -104,9 +104,7 @@ public class InMemoryFileSystemTest {
|
||||
Assert.assertTrue(fooFile.exists());
|
||||
|
||||
// check if size = 11 bytes
|
||||
try (ReadableFile readable = fooFile.openReadable()) {
|
||||
Assert.assertEquals(11, readable.size());
|
||||
}
|
||||
Assert.assertEquals(11, fooFile.size());
|
||||
|
||||
// copy foo to bar
|
||||
File barFile = fs.file("bar.txt");
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<version>1.2.3</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-invariants-tests</artifactId>
|
||||
<name>Cryptomator filesystem: Invariants tests</name>
|
||||
@@ -20,6 +20,10 @@
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>filesystem-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>filesystem-charsets</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>filesystem-crypto</artifactId>
|
||||
|
||||
@@ -4,11 +4,13 @@ import static org.cryptomator.common.test.TempFilesRemovedOnShutdown.createTempD
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.text.Normalizer.Form;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.cryptomator.filesystem.FileSystem;
|
||||
import org.cryptomator.filesystem.charsets.NormalizedNameFileSystem;
|
||||
import org.cryptomator.filesystem.crypto.CryptoEngineTestModule;
|
||||
import org.cryptomator.filesystem.crypto.CryptoFileSystemDelegate;
|
||||
import org.cryptomator.filesystem.crypto.CryptoFileSystemTestComponent;
|
||||
@@ -35,8 +37,10 @@ class FileSystemFactories implements Iterable<FileSystemFactory> {
|
||||
add("ShorteningFileSystem > InMemoryFileSystem", this::createShorteningFileSystemInMemory);
|
||||
add("StatsFileSystem > NioFileSystem", this::createStatsFileSystemNio);
|
||||
add("StatsFileSystem > InMemoryFileSystem", this::createStatsFileSystemInMemory);
|
||||
add("StatsFileSystem > CryptoFileSystem > ShorteningFileSystem > InMemoryFileSystem", this::createCompoundFileSystemInMemory);
|
||||
add("StatsFileSystem > CryptoFileSystem > ShorteningFileSystem > NioFileSystem", this::createCompoundFileSystemNio);
|
||||
add("NormalizingFileSystem > NioFileSystem", this::createNormalizingFileSystemNio);
|
||||
add("NormalizingFileSystem > InMemoryFileSystem", this::createNormalizingFileSystemInMemory);
|
||||
add("StatsFileSystem > NormalizingFileSystem > CryptoFileSystem > ShorteningFileSystem > InMemoryFileSystem", this::createCompoundFileSystemInMemory);
|
||||
add("StatsFileSystem > NormalizingFileSystem > CryptoFileSystem > ShorteningFileSystem > NioFileSystem", this::createCompoundFileSystemNio);
|
||||
}
|
||||
|
||||
private FileSystem createCryptoFileSystemInMemory() {
|
||||
@@ -63,6 +67,14 @@ class FileSystemFactories implements Iterable<FileSystemFactory> {
|
||||
return createStatsFileSystem(createInMemoryFileSystem());
|
||||
}
|
||||
|
||||
private FileSystem createNormalizingFileSystemNio() {
|
||||
return createNormalizingFileSystem(createInMemoryFileSystem());
|
||||
}
|
||||
|
||||
private FileSystem createNormalizingFileSystemInMemory() {
|
||||
return createNormalizingFileSystem(createInMemoryFileSystem());
|
||||
}
|
||||
|
||||
private FileSystem createCompoundFileSystemNio() {
|
||||
return createCompoundFileSystem(createNioFileSystem());
|
||||
}
|
||||
@@ -84,13 +96,17 @@ class FileSystemFactories implements Iterable<FileSystemFactory> {
|
||||
}
|
||||
|
||||
private FileSystem createCompoundFileSystem(FileSystem delegate) {
|
||||
return createStatsFileSystem(createCryptoFileSystem(createShorteningFileSystem(delegate)));
|
||||
return createStatsFileSystem(createNormalizingFileSystem(createCryptoFileSystem(createShorteningFileSystem(delegate))));
|
||||
}
|
||||
|
||||
private FileSystem createStatsFileSystem(FileSystem delegate) {
|
||||
return new StatsFileSystem(delegate);
|
||||
}
|
||||
|
||||
private FileSystem createNormalizingFileSystem(FileSystem delegate) {
|
||||
return new NormalizedNameFileSystem(delegate, Form.NFC);
|
||||
}
|
||||
|
||||
private FileSystem createCryptoFileSystem(FileSystem delegate) {
|
||||
CRYPTO_FS_COMP.cryptoFileSystemFactory().initializeNew(delegate, "aPassphrase");
|
||||
return CRYPTO_FS_COMP.cryptoFileSystemFactory().unlockExisting(delegate, "aPassphrase", Mockito.mock(CryptoFileSystemDelegate.class));
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<version>1.2.3</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-nameshortening</artifactId>
|
||||
<name>Cryptomator filesystem: Name shortening layer</name>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.cryptomator.filesystem.shortening;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
final class ConflictResolver {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ConflictResolver.class);
|
||||
private static final String LONG_NAME_FILE_EXT = ".lng";
|
||||
private static final Pattern BASE32_PATTERN = Pattern.compile("^0?([A-Z2-7]{8})*[A-Z2-7=]{8}");
|
||||
private static final int UUID_FIRST_GROUP_STRLEN = 8;
|
||||
|
||||
private ConflictResolver() {
|
||||
}
|
||||
|
||||
public static File resolveConflictIfNecessary(File potentiallyConflictingFile, FilenameShortener shortener) {
|
||||
String shortName = potentiallyConflictingFile.name();
|
||||
String basename = StringUtils.removeEnd(shortName, LONG_NAME_FILE_EXT);
|
||||
Matcher matcher = BASE32_PATTERN.matcher(basename);
|
||||
if (shortName.endsWith(LONG_NAME_FILE_EXT) && matcher.matches()) {
|
||||
// no conflict.
|
||||
return potentiallyConflictingFile;
|
||||
} else if (shortName.endsWith(LONG_NAME_FILE_EXT) && matcher.find(0)) {
|
||||
String canonicalShortName = matcher.group() + LONG_NAME_FILE_EXT;
|
||||
return resolveConflict(potentiallyConflictingFile, canonicalShortName, shortener);
|
||||
} else {
|
||||
// not even shortened at all.
|
||||
return potentiallyConflictingFile;
|
||||
}
|
||||
}
|
||||
|
||||
private static File resolveConflict(File conflictingFile, String canonicalShortName, FilenameShortener shortener) {
|
||||
Folder parent = conflictingFile.parent().get();
|
||||
File canonicalFile = parent.file(canonicalShortName);
|
||||
if (canonicalFile.exists()) {
|
||||
// foo (1).lng -> bar.lng
|
||||
String canonicalLongName = shortener.inflate(canonicalShortName);
|
||||
String alternativeLongName;
|
||||
String alternativeShortName;
|
||||
File alternativeFile;
|
||||
String conflictId;
|
||||
do {
|
||||
conflictId = createConflictId();
|
||||
alternativeLongName = canonicalLongName + " (Conflict " + conflictId + ")";
|
||||
alternativeShortName = shortener.deflate(alternativeLongName);
|
||||
alternativeFile = parent.file(alternativeShortName);
|
||||
} while (alternativeFile.exists());
|
||||
LOG.info("Detected conflict {}:\n{}\n{}", conflictId, canonicalFile, conflictingFile);
|
||||
conflictingFile.moveTo(alternativeFile);
|
||||
shortener.saveMapping(alternativeLongName, alternativeShortName);
|
||||
return alternativeFile;
|
||||
} else {
|
||||
// foo (1).lng -> foo.lng
|
||||
conflictingFile.moveTo(canonicalFile);
|
||||
return canonicalFile;
|
||||
}
|
||||
}
|
||||
|
||||
private static String createConflictId() {
|
||||
return UUID.randomUUID().toString().substring(0, UUID_FIRST_GROUP_STRLEN);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,8 +8,6 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.shortening;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
@@ -19,9 +17,12 @@ import org.apache.commons.codec.binary.BaseNCodec;
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.io.FileContents;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
class FilenameShortener {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FilenameShortener.class);
|
||||
private static final String LONG_NAME_FILE_EXT = ".lng";
|
||||
private static final ThreadLocal<MessageDigest> SHA1 = new ThreadLocalSha1();
|
||||
private static final BaseNCodec BASE32 = new Base32();
|
||||
@@ -71,7 +72,8 @@ class FilenameShortener {
|
||||
private String loadMapping(String shortName) {
|
||||
final File mappingFile = mappingFile(shortName);
|
||||
if (!mappingFile.exists()) {
|
||||
throw new UncheckedIOException(new FileNotFoundException("Mapping file not found " + mappingFile));
|
||||
LOG.warn("Mapping file not found: " + mappingFile);
|
||||
return shortName;
|
||||
} else {
|
||||
return FileContents.UTF_8.readContents(mappingFile);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class ShorteningFile extends DelegatingFile<ShorteningFolder> {
|
||||
private final FilenameShortener shortener;
|
||||
|
||||
public ShorteningFile(ShorteningFolder parent, File delegate, String name, FilenameShortener shortener) {
|
||||
super(parent, delegate);
|
||||
super(parent, ConflictResolver.resolveConflictIfNecessary(delegate, shortener));
|
||||
this.longName = new AtomicReference<>(name);
|
||||
this.shortener = shortener;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.cryptomator.filesystem.shortening;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
public class ConflictResolverTest {
|
||||
|
||||
private Folder metadataFolder;
|
||||
private FilenameShortener shortener;
|
||||
private Folder folder;
|
||||
private File canonicalFile;
|
||||
private File conflictingFile;
|
||||
private File resolvedFile;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
metadataFolder = new InMemoryFileSystem();
|
||||
shortener = new FilenameShortener(metadataFolder, 20);
|
||||
folder = Mockito.mock(Folder.class);
|
||||
canonicalFile = Mockito.mock(File.class);
|
||||
conflictingFile = Mockito.mock(File.class);
|
||||
resolvedFile = Mockito.mock(File.class);
|
||||
|
||||
String longName = "hello world, I am a very long file name. certainly longer than twenty characters.exe";
|
||||
String shortName = shortener.deflate(longName);
|
||||
shortener.saveMapping(longName, shortName);
|
||||
String canonicalFileName = shortName;
|
||||
String conflictingFileName = shortName.replace(".lng", " (1).lng");
|
||||
|
||||
Mockito.when(canonicalFile.name()).thenReturn(canonicalFileName);
|
||||
Mockito.when(conflictingFile.name()).thenReturn(conflictingFileName);
|
||||
|
||||
Mockito.when(canonicalFile.exists()).thenReturn(true);
|
||||
Mockito.when(conflictingFile.exists()).thenReturn(true);
|
||||
|
||||
Mockito.doReturn(Optional.of(folder)).when(canonicalFile).parent();
|
||||
Mockito.doReturn(Optional.of(folder)).when(conflictingFile).parent();
|
||||
|
||||
Mockito.when(folder.file(Mockito.anyString())).thenReturn(resolvedFile);
|
||||
Mockito.when(folder.file(canonicalFileName)).thenReturn(canonicalFile);
|
||||
Mockito.when(folder.file(conflictingFileName)).thenReturn(conflictingFile);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoConflictToBeResolved() {
|
||||
File resolved = ConflictResolver.resolveConflictIfNecessary(canonicalFile, new FilenameShortener(metadataFolder, 20));
|
||||
Mockito.verify(conflictingFile, Mockito.never()).moveTo(Mockito.any());
|
||||
Assert.assertSame(canonicalFile, resolved);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConflictToBeResolved() {
|
||||
File resolved = ConflictResolver.resolveConflictIfNecessary(conflictingFile, new FilenameShortener(metadataFolder, 20));
|
||||
Mockito.verify(conflictingFile).moveTo(resolvedFile);
|
||||
Assert.assertSame(resolvedFile, resolved);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,8 +8,6 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.shortening;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
|
||||
import org.cryptomator.filesystem.FileSystem;
|
||||
import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
|
||||
import org.junit.Assert;
|
||||
@@ -45,12 +43,12 @@ public class FilenameShortenerTest {
|
||||
Assert.assertEquals("short", shortener.inflate("short"));
|
||||
}
|
||||
|
||||
@Test(expected = UncheckedIOException.class)
|
||||
@Test
|
||||
public void testInflateWithoutMappingFile() {
|
||||
FileSystem fs = new InMemoryFileSystem();
|
||||
FilenameShortener shortener = new FilenameShortener(fs, 10);
|
||||
|
||||
shortener.inflate("iJustMadeThisNameUp.lng");
|
||||
Assert.assertEquals("iJustMadeThisNameUp.lng", shortener.inflate("iJustMadeThisNameUp.lng"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
@@ -102,6 +103,31 @@ public class ShorteningFileSystemTest {
|
||||
Assert.assertTrue(correspondingMetadataFile.exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInflate() {
|
||||
final FileSystem underlyingFs = new InMemoryFileSystem();
|
||||
final Folder metadataRoot = underlyingFs.folder(METADATA_DIR_NAME);
|
||||
final FileSystem fs = new ShorteningFileSystem(underlyingFs, METADATA_DIR_NAME, 10);
|
||||
final File correspondingMetadataFile = metadataRoot.folder("QM").folder("JL").file("QMJL5GQUETRX2YRV6XDTJQ6NNM7IEUHP.lng");
|
||||
final Folder shortenedFolder = underlyingFs.folder("QMJL5GQUETRX2YRV6XDTJQ6NNM7IEUHP.lng");
|
||||
shortenedFolder.create();
|
||||
correspondingMetadataFile.parent().get().create();
|
||||
try (WritableFile w = correspondingMetadataFile.openWritable()) {
|
||||
w.write(ByteBuffer.wrap("morethantenchars".getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
Assert.assertTrue(correspondingMetadataFile.exists());
|
||||
Assert.assertTrue(fs.folders().map(Folder::name).anyMatch(n -> n.equals("morethantenchars")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInflateFailedDueToMissingMapping() {
|
||||
final FileSystem underlyingFs = new InMemoryFileSystem();
|
||||
final FileSystem fs = new ShorteningFileSystem(underlyingFs, METADATA_DIR_NAME, 10);
|
||||
final Folder shortenedFolder = underlyingFs.folder("QMJL5GQUETRX2YRV6XDTJQ6NNM7IEUHP.lng");
|
||||
shortenedFolder.create();
|
||||
Assert.assertTrue(fs.folders().map(Folder::name).anyMatch(n -> n.equals("QMJL5GQUETRX2YRV6XDTJQ6NNM7IEUHP.lng")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMoveLongFolders() {
|
||||
final FileSystem underlyingFs = new InMemoryFileSystem();
|
||||
|
||||
22
main/filesystem-nameshortening/src/test/resources/log4j2.xml
Normal file
22
main/filesystem-nameshortening/src/test/resources/log4j2.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Configuration status="WARN">
|
||||
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
|
||||
</Console>
|
||||
<Console name="StdErr" target="SYSTEM_ERR">
|
||||
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
|
||||
</Console>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<Root level="DEBUG">
|
||||
<AppenderRef ref="Console" />
|
||||
<AppenderRef ref="StdErr" />
|
||||
</Root>
|
||||
</Loggers>
|
||||
|
||||
</Configuration>
|
||||
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<version>1.2.3</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-nio</artifactId>
|
||||
<name>Cryptomator filesystem: NIO-based physical layer</name>
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.cryptomator.filesystem.nio;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.AsynchronousFileChannel;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.CopyOption;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
@@ -16,6 +17,11 @@ import java.util.stream.Stream;
|
||||
|
||||
class DefaultNioAccess implements NioAccess {
|
||||
|
||||
@Override
|
||||
public long size(Path path) throws IOException {
|
||||
return Files.size(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AsynchronousFileChannel open(Path path, OpenOption... options) throws IOException {
|
||||
return AsynchronousFileChannel.open(path, options);
|
||||
@@ -53,7 +59,18 @@ class DefaultNioAccess implements NioAccess {
|
||||
|
||||
@Override
|
||||
public void delete(Path path) throws IOException {
|
||||
Files.delete(path);
|
||||
try {
|
||||
Files.delete(path);
|
||||
} catch (AccessDeniedException e) {
|
||||
// workaround for https://github.com/cryptomator/cryptomator/issues/317
|
||||
try {
|
||||
if (path.toFile().delete())
|
||||
return;
|
||||
} catch (UnsupportedOperationException e2) {
|
||||
// ignore
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -16,6 +16,8 @@ interface NioAccess {
|
||||
|
||||
public static final Holder<NioAccess> DEFAULT = new Holder<>(new DefaultNioAccess());
|
||||
|
||||
long size(Path path) throws IOException;
|
||||
|
||||
AsynchronousFileChannel open(Path path, OpenOption... options) throws IOException;
|
||||
|
||||
boolean isRegularFile(Path path, LinkOption... options);
|
||||
|
||||
@@ -27,16 +27,33 @@ class NioFile extends NioNode implements File {
|
||||
sharedChannel = instanceFactory.sharedFileChannel(path, nioAccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws UncheckedIOException {
|
||||
try {
|
||||
return nioAccess.size(path);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadableFile openReadable() throws UncheckedIOException {
|
||||
if (lock.getWriteHoldCount() > 0) {
|
||||
throw new IllegalStateException("Current thread is currently writing this file");
|
||||
throw new IllegalStateException("Current thread is currently writing " + path);
|
||||
}
|
||||
if (lock.getReadHoldCount() > 0) {
|
||||
throw new IllegalStateException("Current thread is already reading this file");
|
||||
throw new IllegalStateException("Current thread is already reading " + path);
|
||||
}
|
||||
lock.readLock().lock();
|
||||
return instanceFactory.readableNioFile(path, sharedChannel, this::unlockReadLock);
|
||||
ReadableFile result = null;
|
||||
try {
|
||||
result = instanceFactory.readableNioFile(path, sharedChannel, this::unlockReadLock);
|
||||
} finally {
|
||||
if (result == null) {
|
||||
unlockReadLock();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void unlockReadLock() {
|
||||
@@ -46,13 +63,21 @@ class NioFile extends NioNode implements File {
|
||||
@Override
|
||||
public WritableFile openWritable() throws UncheckedIOException {
|
||||
if (lock.getWriteHoldCount() > 0) {
|
||||
throw new IllegalStateException("Current thread is already writing this file");
|
||||
throw new IllegalStateException("Current thread is already writing " + path);
|
||||
}
|
||||
if (lock.getReadHoldCount() > 0) {
|
||||
throw new IllegalStateException("Current thread is currently reading this file");
|
||||
throw new IllegalStateException("Current thread is currently reading " + path);
|
||||
}
|
||||
lockWriteLock();
|
||||
return instanceFactory.writableNioFile(fileSystem(), path, sharedChannel, this::unlockWriteLock);
|
||||
WritableFile result = null;
|
||||
try {
|
||||
result = instanceFactory.writableNioFile(fileSystem(), path, sharedChannel, this::unlockWriteLock);
|
||||
} finally {
|
||||
if (result == null) {
|
||||
unlockWriteLock();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// visible for testing
|
||||
|
||||
@@ -41,11 +41,6 @@ class ReadableNioFile implements ReadableFile {
|
||||
return open;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws UncheckedIOException {
|
||||
return channel.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void position(long position) throws UncheckedIOException {
|
||||
assertOpen();
|
||||
|
||||
@@ -16,6 +16,7 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Instant;
|
||||
@@ -85,6 +86,27 @@ public class NioFileTest {
|
||||
|
||||
}
|
||||
|
||||
public class Size {
|
||||
|
||||
@Test
|
||||
public void testSizeReturnsSizeOfRegularFile() throws IOException {
|
||||
when(nioAccess.size(path)).thenReturn(42l);
|
||||
|
||||
assertThat(inTest.size(), is(42l));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSizeThrowsExceptionIfRegularFileThrowsException() throws IOException {
|
||||
Throwable t = new NoSuchFileException("foo");
|
||||
when(nioAccess.size(path)).thenThrow(t);
|
||||
|
||||
thrown.expect(UncheckedIOException.class);
|
||||
thrown.expectCause(org.hamcrest.Matchers.sameInstance(t));
|
||||
inTest.size();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class Open {
|
||||
|
||||
@Test
|
||||
@@ -99,10 +121,11 @@ public class NioFileTest {
|
||||
|
||||
@Test
|
||||
public void testOpenReadableInvokedBeforeInvokingAfterCloseOperationThrowsIllegalStateException() {
|
||||
when(instanceFactory.readableNioFile(same(path), same(channel), any())).thenReturn(mock(ReadableNioFile.class));
|
||||
inTest.openReadable();
|
||||
|
||||
thrown.expect(IllegalStateException.class);
|
||||
thrown.expectMessage("already reading this file");
|
||||
thrown.expectMessage("already reading " + path);
|
||||
|
||||
inTest.openReadable();
|
||||
}
|
||||
@@ -111,7 +134,7 @@ public class NioFileTest {
|
||||
public void testOpenReadableInvokedAfterAfterCloseOperationCreatesNewReadableFile() {
|
||||
ReadableNioFile readableNioFile = mock(ReadableNioFile.class);
|
||||
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
||||
when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(null, readableNioFile);
|
||||
when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(mock(ReadableNioFile.class), readableNioFile);
|
||||
inTest.openReadable();
|
||||
captor.getValue().run();
|
||||
|
||||
@@ -122,10 +145,11 @@ public class NioFileTest {
|
||||
|
||||
@Test
|
||||
public void testOpenReadableInvokedBeforeInvokingAfterCloseOperationOfOpenWritableThrowsIllegalStateException() {
|
||||
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), any())).thenReturn(mock(WritableNioFile.class));
|
||||
inTest.openWritable();
|
||||
|
||||
thrown.expect(IllegalStateException.class);
|
||||
thrown.expectMessage("currently writing this file");
|
||||
thrown.expectMessage("currently writing " + path);
|
||||
|
||||
inTest.openReadable();
|
||||
}
|
||||
@@ -133,7 +157,7 @@ public class NioFileTest {
|
||||
@Test
|
||||
public void testOpenReadableInvokedAfterInvokingAfterCloseOperationWorks() {
|
||||
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
||||
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(null);
|
||||
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(mock(WritableNioFile.class));
|
||||
inTest.openWritable();
|
||||
captor.getValue().run();
|
||||
|
||||
@@ -154,7 +178,7 @@ public class NioFileTest {
|
||||
public void testOpenWritableInvokedAfterAfterCloseOperationCreatesNewWritableFile() {
|
||||
WritableNioFile writableNioFile = mock(WritableNioFile.class);
|
||||
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
||||
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(null, writableNioFile);
|
||||
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(mock(WritableNioFile.class), writableNioFile);
|
||||
inTest.openWritable();
|
||||
captor.getValue().run();
|
||||
|
||||
@@ -165,28 +189,31 @@ public class NioFileTest {
|
||||
|
||||
@Test
|
||||
public void testOpenWritableInvokedBeforeInvokingAfterCloseOperationThrowsIllegalStateException() {
|
||||
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), any())).thenReturn(mock(WritableNioFile.class));
|
||||
inTest.openWritable();
|
||||
|
||||
thrown.expect(IllegalStateException.class);
|
||||
thrown.expectMessage("already writing this file");
|
||||
thrown.expectMessage("already writing " + path);
|
||||
|
||||
inTest.openWritable();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOpenWritableInvokedBeforeInvokingAfterCloseOperationFromOpenReadableThrowsIllegalStateException() {
|
||||
when(instanceFactory.readableNioFile(same(path), same(channel), any())).thenReturn(mock(ReadableNioFile.class));
|
||||
inTest.openReadable();
|
||||
|
||||
thrown.expect(IllegalStateException.class);
|
||||
thrown.expectMessage("currently reading this file");
|
||||
thrown.expectMessage("currently reading " + path);
|
||||
|
||||
inTest.openWritable();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOpenWritableInvokedAfterInvokingAfterCloseOperationWorks() {
|
||||
ReadableNioFile readableNioFile = mock(ReadableNioFile.class);
|
||||
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
||||
when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(null);
|
||||
when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(readableNioFile);
|
||||
inTest.openReadable();
|
||||
captor.getValue().run();
|
||||
|
||||
|
||||
@@ -83,16 +83,6 @@ public class ReadableNioFileTest {
|
||||
inTest.position(-1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSizeReturnsSizeOfChannel() {
|
||||
long expectedSize = 85472;
|
||||
when(channel.size()).thenReturn(expectedSize);
|
||||
|
||||
long actualSize = inTest.size();
|
||||
|
||||
assertThat(actualSize, is(expectedSize));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadDelegatesToChannelReadFullyWithZeroPositionIfNotSet() {
|
||||
ByteBuffer buffer = mock(ByteBuffer.class);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<version>1.2.3</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-stats</artifactId>
|
||||
<name>Cryptomator filesystem: Throughput statistics</name>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<version>1.2.3</version>
|
||||
</parent>
|
||||
<artifactId>frontend-api</artifactId>
|
||||
<name>Cryptomator frontend: API</name>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user