Compare commits

...

138 Commits
1.0.3 ... 1.1.2

Author SHA1 Message Date
Sebastian Stenzel
20e55eddf8 Merge branch 'develop' 2016-07-09 13:25:54 +02:00
Sebastian Stenzel
0fdcdc816a fixed unit test 2016-07-09 13:25:24 +02:00
Sebastian Stenzel
b7506d97a9 Merge branch 'hotfix/1.1.2' 2016-07-09 13:25:03 +02:00
Sebastian Stenzel
4ad7481dc7 fixed unit test 2016-07-09 11:44:53 +02:00
Sebastian Stenzel
bc815405d2 merged from hotfix/1.1.2 [ci skip] 2016-07-09 11:32:02 +02:00
Sebastian Stenzel
9c06e762c3 fixes #304 2016-07-09 11:28:36 +02:00
Sebastian Stenzel
1ac87dd32f fixed NPE [ci skip] 2016-07-08 15:52:00 +02:00
Sebastian Stenzel
e0ce7ce2ec Merge branch 'release/1.1.1'
# Conflicts:
#	main/ant-kit/pom.xml
#	main/commons-test/pom.xml
#	main/commons/pom.xml
#	main/filesystem-api/pom.xml
#	main/filesystem-charsets/pom.xml
#	main/filesystem-crypto-integration-tests/pom.xml
#	main/filesystem-crypto/pom.xml
#	main/filesystem-inmemory/pom.xml
#	main/filesystem-invariants-tests/pom.xml
#	main/filesystem-nameshortening/pom.xml
#	main/filesystem-nio/pom.xml
#	main/filesystem-stats/pom.xml
#	main/frontend-api/pom.xml
#	main/frontend-webdav/pom.xml
#	main/jacoco-report/pom.xml
#	main/pom.xml
#	main/uber-jar/pom.xml
#	main/ui/pom.xml
2016-07-08 11:59:39 +02:00
Sebastian Stenzel
3d951a9d7b set version to 1.1.1 2016-07-07 14:48:49 +02:00
Sebastian Stenzel
cec3d984b0 Merge branch 'develop' into release/1.1.1 2016-07-07 14:39:20 +02:00
Sebastian Stenzel
392e474cfa Update tr.txt (POEditor.com) 2016-07-07 14:28:52 +02:00
Sebastian Stenzel
41fb0d51a4 Update es.txt (POEditor.com) 2016-07-07 14:28:51 +02:00
Sebastian Stenzel
aa9fef2967 Update sk.txt (POEditor.com) 2016-07-07 14:28:49 +02:00
Sebastian Stenzel
adc9c02564 Update ru.txt (POEditor.com) 2016-07-07 14:28:48 +02:00
Sebastian Stenzel
ace64117a2 Update kr.txt (POEditor.com) 2016-07-07 14:28:46 +02:00
Sebastian Stenzel
fb4db2506b Update it.txt (POEditor.com) 2016-07-07 14:28:45 +02:00
Sebastian Stenzel
1076d971ae Update hu.txt (POEditor.com) 2016-07-07 14:28:43 +02:00
Sebastian Stenzel
eed1b1cff0 Update de.txt (POEditor.com) 2016-07-07 14:28:42 +02:00
Sebastian Stenzel
f5cb82e21e Update fr.txt (POEditor.com) 2016-07-07 14:28:40 +02:00
Sebastian Stenzel
67661f114b Update nl.txt (POEditor.com) 2016-07-07 14:28:39 +02:00
Sebastian Stenzel
8a3e09764a only remove .cryptomator extension for vault version 3 2016-07-07 14:25:55 +02:00
Sebastian Stenzel
eb3cfd6e6a updated placeholders [ci skip] 2016-07-06 16:25:31 +02:00
Sebastian Stenzel
4d1727d0e9 Merge branch 'develop' into release/1.1.1 2016-07-06 16:09:32 +02:00
Sebastian Stenzel
a51d853d1c adjusted number format regex [ci skip] 2016-07-06 16:09:14 +02:00
Sebastian Stenzel
d0039466f7 test technical correctness of localization files 2016-07-06 16:07:07 +02:00
Sebastian Stenzel
5c959989a2 Fixed Coverity defect 131711 2016-07-05 23:18:18 +02:00
Sebastian Stenzel
6283d2df3d Merge branch 'feature/vault-version-4' into develop 2016-07-03 17:41:46 +02:00
Sebastian Stenzel
a9e0dfdaf8 redesigned upgrade view 2016-07-03 17:38:46 +02:00
Sebastian Stenzel
45ca7e9e47 migration from vault version 3 to 4 2016-07-03 16:16:23 +02:00
Sebastian Stenzel
034b5c2718 updated localizations
[ci skip]
2016-07-01 11:14:49 +02:00
Sebastian Stenzel
e188649c79 adjusted test to vault version 4 2016-06-30 22:18:43 +02:00
Sebastian Stenzel
1468c6ec90 improved vault upgrading, preparation for migration to vault version 4 2016-06-30 22:09:45 +02:00
Sebastian Stenzel
07ba4eb537 Using 0 prefix instead of _ suffix to mark directories 2016-06-30 18:02:13 +02:00
Sebastian Stenzel
414bbef1a7 updated key generation 2016-06-10 14:04:55 +02:00
Sebastian Stenzel
e2b94ff6ef updated jacoco dependency 2016-06-08 19:11:56 +02:00
Sebastian Stenzel
41f8a9faca add "allow" response header field 2016-06-08 19:06:06 +02:00
Tobias Hagemann
1d9252e974 updated description of file chooser's extension filter [ci skip] 2016-06-07 01:07:54 +02:00
Sebastian Stenzel
80780eef3c Merge pull request #280 from aeris/fix-l10n-fr
Enhanced fr translation
[ci skip]
2016-05-30 14:27:00 +02:00
Aeris
87ff33956b Enhanced fr translation 2016-05-30 13:45:40 +02:00
Sebastian Stenzel
1804b98f05 trigger coverity scans for only release branches [ci skip] 2016-05-25 15:47:19 +02:00
Sebastian Stenzel
847c6813cc started development of 1.2.0 [ci skip] 2016-05-25 15:37:07 +02:00
Sebastian Stenzel
1dde5ff6e7 release 1.1.0 2016-05-25 15:17:40 +02:00
Sebastian Stenzel
76c9a19428 unset and set default buttons to make sure VK_ENTER triggers it 2016-05-25 12:29:49 +02:00
Sebastian Stenzel
25ee0519e1 some minor fixes
- reset password field contents when changing a vault
- hide "change password" option for uninitialized or missing vaults
2016-05-25 12:12:01 +02:00
Sebastian Stenzel
c184089c35 oopsy daisy, wie das duftet... [ci skip] 2016-05-24 11:36:46 +02:00
Sebastian Stenzel
d2bcc47857 Merge branch 'delete-confirmation'
fixes #228
2016-05-24 11:35:17 +02:00
Sebastian Stenzel
34629a69ea Using ControlsFX's BSD-licensed assets for dialogs, rather than OpenJDK's GPL licensed ones.
Adjusted dialog styles for Linux and Windows.
[ci skip]
2016-05-24 11:32:27 +02:00
Sebastian Stenzel
92c87f7b84 changed dialog L&F on OS X 2016-05-23 19:31:18 +02:00
Sebastian Stenzel
0dd96635ac code cleanup [ci skip] 2016-05-23 13:24:53 +02:00
Sebastian Stenzel
048c44a6e4 Update README.md 2016-05-23 12:36:58 +02:00
Sebastian Stenzel
06910ad1f4 fixes #229 2016-05-23 12:11:45 +02:00
Sebastian Stenzel
02a0f3acc6 fixed invariant FolderChildrenTests 2016-05-23 11:18:24 +02:00
Sebastian Stenzel
851f9240b7 updated link to MAC warning FAQ 2016-05-23 11:02:56 +02:00
Sebastian Stenzel
99fce8d0b7 automatically resolve conflicts for directory files, that contain the same directory ID 2016-05-23 11:02:44 +02:00
Sebastian Stenzel
bf05c59c3b Transparent conflict detection for long file names 2016-05-22 15:16:32 +02:00
Sebastian Stenzel
3dcebb1e1f fixed minor copy/paste error 2016-05-22 13:32:16 +02:00
Sebastian Stenzel
fe3efdf610 Merge pull request #269 from jncharon/master
fixes #56
2016-05-21 14:07:15 +02:00
jncharon
5f4ae46f82 Replaced the MouseListner by a MouseAdapter 2016-05-20 21:30:33 +02:00
jncharon
deef325319 Implementation of github issue #56 2016-05-20 19:29:53 +02:00
jncharon
fbe00a8fe3 Merge remote-tracking branch 'refs/remotes/cryptomator/master' 2016-05-16 14:20:55 +02:00
Sebastian Stenzel
dc87dade43 Merge pull request #259 from jncharon/issue-228-fixed
Fixes #228.
Still need to check license of icons, will discuss this with @MuscleRumble, who has a lot of icons that we bought the license for. If necessary we will change them.
2016-05-16 12:53:53 +02:00
Jean-Noël Charon
ba1625b5ad Merge pull request #2 from overheadhunter/issue-228-fixed
Reverted commit c0f4a2b, added .idea/ to .gitignore
2016-05-16 12:14:38 +02:00
Sebastian Stenzel
f6b126415e added IntelliJ files to .gitignore 2016-05-16 10:00:39 +02:00
Sebastian Stenzel
9147e1c08b Revert "Fucking .idea files I could not remove from the vcs"
This reverts commit c0f4a2b0d3.
2016-05-16 09:57:10 +02:00
Sebastian Stenzel
6c18103662 Remove files with non-decryptable names from dir listings 2016-05-13 18:59:06 +02:00
Sebastian Stenzel
6fc343ea12 more fault-tolerant behaviour when mapping of long filenames couldn't be found. 2016-05-13 14:10:37 +02:00
Sebastian Stenzel
d304d66cdd Updated localizations [ci skip] 2016-05-12 19:23:24 +02:00
Sebastian Stenzel
2ce9143b85 Merge branch 'conflict-detection' 2016-05-12 19:14:48 +02:00
Sebastian Stenzel
1c54e4f4ad in the unlikely event of an alternative name already being used, choose a new random conflict id. 2016-05-12 16:13:03 +02:00
Sebastian Stenzel
9fd6f2ecae transparently show sync conflicts (fixes #98) 2016-05-12 16:08:52 +02:00
Sebastian Stenzel
0d9f8eefc0 Using pattern based filename filtering
This is a preparation for finding valid encrypted names inside filenames that include additional characters
2016-05-12 11:51:14 +02:00
Sebastian Stenzel
40a1530f19 repeated commit 86000ac 2016-05-10 14:52:30 +02:00
Sebastian Stenzel
0477a0a2e3 Merge branch 'patches-1.0.x'
# Conflicts:
#	main/filesystem-charsets/pom.xml
2016-05-10 14:49:20 +02:00
Sebastian Stenzel
b77d4b5ae2 fixes #264 2016-05-10 14:33:21 +02:00
Sebastian Stenzel
7b6c5318c5 fixes #263 2016-05-10 14:31:55 +02:00
Sebastian Stenzel
6006d65ce0 new ant kit using a custom launcher binary due to #265 2016-05-10 14:26:06 +02:00
jncharon
2b01b76926 Merge remote-tracking branch 'refs/remotes/cryptomator/master' 2016-05-08 17:52:14 +02:00
Sebastian Stenzel
dcea9e21f0 added module to code coverage report 2016-05-07 15:00:20 +02:00
Sebastian Stenzel
78645ecdf6 fixes #264 2016-05-07 14:40:44 +02:00
Sebastian Stenzel
91646dd93d Merge branch 'password-strength'
Added password strength meter by Jean-Noël Charon, closing issue #198
2016-05-06 18:59:26 +02:00
jncharon
fca146e939 Merge remote-tracking branch 'remotes/origin/master' into issue-228-fixed 2016-05-05 21:22:28 +02:00
jncharon
62aa3ccc7f Merge remote-tracking branch 'refs/remotes/cryptomator/master' into issue-228-fixed 2016-05-05 21:12:53 +02:00
jncharon
c0f4a2b0d3 Fucking .idea files I could not remove from the vcs 2016-05-05 21:11:57 +02:00
Tobias Hagemann
68ee89af98 updated bot welcome asset [ci skip] 2016-05-03 19:12:21 +02:00
Sebastian Stenzel
ad2c9116b9 Release 1.0.4 2016-05-03 16:50:18 +02:00
Sebastian Stenzel
8e24745b3e Merge branch 'master' into patches-1.0.x 2016-05-03 16:46:18 +02:00
Sebastian Stenzel
08f664e3df Throttle calls to Settings.save() 2016-05-03 16:44:22 +02:00
Tobias Hagemann
b6d1d1dc22 updated linux app icon [ci skip] 2016-05-03 16:07:30 +02:00
Sebastian Stenzel
a0ef02b95c fixes #237 2016-05-03 13:17:45 +02:00
Sebastian Stenzel
a6cefe67c4 setting default port to 42427 [ci skip] 2016-05-03 10:48:24 +02:00
Sebastian Stenzel
be2b63ab2a support for UTF-8 localization files 2016-05-02 22:28:49 +02:00
Sebastian Stenzel
78f11b4a5e added korean localization [ci skip] 2016-05-02 16:01:28 +02:00
Sebastian Stenzel
0f20c7c3c9 fixes #209 2016-05-02 12:36:31 +02:00
Sebastian Stenzel
d4235174f7 imported localizations from POEditor fixes #231 and #234 (ci skip) 2016-05-02 11:37:40 +02:00
Sebastian Stenzel
f16be84aa3 restored bash-based webdav mounting for OS X before 10.10 (issue #211 - to be tested) 2016-05-02 11:11:42 +02:00
Sebastian Stenzel
833f2d8566 fixed travis test coverage configuration 2016-04-27 01:18:37 +02:00
Sebastian Stenzel
c02a63878e new method to calculate test coverage (across modules) 2016-04-27 01:14:41 +02:00
jncharon
6deb30307e Merge remote-tracking branch 'cryptomator/master' 2016-04-24 13:47:37 +02:00
jncharon
7357829741 Fix in the background color of the dialog boxes 2016-04-23 23:42:28 +02:00
jncharon
4bd04150c1 Implementation of github issue 228 2016-04-23 23:37:56 +02:00
Sebastian Stenzel
ac9fe28967 Merge branch 'master' into patches-1.0.x
Release 1.0.3d
2016-04-22 10:55:03 +02:00
Sebastian Stenzel
515755d84a updated antkit to support deb and rpm packages [ci skip] 2016-04-22 10:53:51 +02:00
Sebastian Stenzel
cf35772c18 Merge pull request #239 from jncharon/master
Fix in the change password screen
2016-04-21 09:11:23 +02:00
jncharon
b0fd226c4c Fix of the strength bar position (row) in the fxml 2016-04-20 22:32:30 +02:00
jncharon
0d188d1c0c Merge remote-tracking branch 'cryptomator/master' 2016-04-16 15:27:01 +02:00
Sebastian Stenzel
c6016ec7b2 using constructor-injection, organized imports, code autoformatting [ci skip] 2016-04-16 14:10:32 +02:00
Sebastian Stenzel
e8719a1f9b Merge pull request #232 from jncharon/master
Fixes #198, #157
2016-04-16 09:53:14 +02:00
jncharon
27baf78029 More refactoring following Sebastian comments 2016-04-16 00:12:59 +02:00
jncharon
bf5ce9a3a5 New password strength implementation based on zxcvbn4j 2016-04-15 22:52:57 +02:00
Sebastian Stenzel
fef19fe6b3 Merge branch 'master' into patches-1.0.x 2016-04-14 22:39:56 +02:00
Sebastian Stenzel
5f56dacc4e adjusted travis configuration [ci skip] 2016-04-14 22:39:19 +02:00
Sebastian Stenzel
aa249dabb5 technical release 1.0.3c 2016-04-14 22:28:12 +02:00
Sebastian Stenzel
06a5bed6e3 Merge branch 'master' into patches-1.0.x 2016-04-14 22:27:15 +02:00
Sebastian Stenzel
02f1ffc6bf updated antkit creation (tarball no longer contains a base directory) 2016-04-14 22:26:36 +02:00
jncharon
bcfe040784 Merge remote-tracking branch 'cryptomator/master' 2016-04-13 18:22:16 +02:00
Sebastian Stenzel
de9af9e303 fixed funny detail label in vault list, if vault is not located inside home directory 2016-04-13 15:26:27 +02:00
jncharon
d9b88ad1b7 Merge remote-tracking branch 'refs/remotes/cryptomator/master' 2016-04-12 21:53:44 +02:00
jncharon
e66e5b1d96 Added the password strength indicator in the change password window 2016-04-12 21:27:31 +02:00
jncharon
588166dce9 Added the password strength indicator in the initialize window 2016-04-12 21:00:41 +02:00
Sebastian Stenzel
e2bc71a0bc added spanish translation template [ci skip] 2016-04-11 14:17:33 +02:00
Sebastian Stenzel
e528f6827c Added translation button [ci skip] 2016-04-11 14:02:11 +02:00
Sebastian Stenzel
2882ae8ef8 Update localization_de.properties (POEditor.com) 2016-04-11 13:46:51 +02:00
Sebastian Stenzel
e37f7cea1a Merge pull request #227 from jncharon/master
French translation
2016-04-11 10:55:26 +02:00
jncharon
9b4ee10155 Adjustments to the french translation 2016-04-10 15:53:53 +02:00
jncharon
c9d970955c French translaction 2016-04-10 15:10:47 +02:00
Sebastian Stenzel
9e0afd36c4 Merge branch 'master' into patches-1.0.x [ci skip] 2016-04-10 02:42:25 +02:00
Sebastian Stenzel
0e523599a3 add execution phase 2016-04-10 02:41:51 +02:00
Sebastian Stenzel
1df6589dd7 make sure, .tar.gz is built on travis 2016-04-10 02:36:10 +02:00
Sebastian Stenzel
fb60c97fd3 Merge branch 'master' into patches-1.0.x 2016-04-10 02:19:52 +02:00
Sebastian Stenzel
90cd149be8 Update .travis.yml 2016-04-10 02:19:01 +02:00
Sebastian Stenzel
89c04ad83b test release 1.0.3b 2016-04-10 02:07:52 +02:00
Sebastian Stenzel
f2d383a211 Merge branch 'master' into patches-1.0.x 2016-04-10 01:56:13 +02:00
Sebastian Stenzel
73fde5d020 null-safe status indicators 2016-04-10 01:54:44 +02:00
Sebastian Stenzel
5c0857e98e build ant-kit on tag/release [ci skip] 2016-04-10 01:53:44 +02:00
Sebastian Stenzel
3e87b9c0c6 oracle jdk8 + jce on trusty 2016-04-10 00:28:11 +02:00
Sebastian Stenzel
a1d0b6b1d3 trying to build with openjdk8 on trusty 2016-04-10 00:22:56 +02:00
Tobias Hagemann
b0d4b2e403 fixed support mail link in code of conduct [ci skip] 2016-04-06 00:04:36 +02:00
Tobias Hagemann
6996d36ea2 added issue template, contribution guide, code of conduct [ci skip] 2016-04-05 12:28:36 +02:00
131 changed files with 4075 additions and 471 deletions

6
.gitignore vendored
View File

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

View File

@@ -1,3 +1,7 @@
sudo: required
dist: trusty
language: java
jdk:
@@ -8,11 +12,11 @@ env:
- 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"
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
after_success: mvn -fmain/pom.xml -Ptest-coverage clean test jacoco:report-aggregate coveralls:report
notifications:
webhooks:
@@ -27,7 +31,7 @@ notifications:
on_success: change
on_failure: always
before_deploy: mvn -fmain/pom.xml -Puber-jar clean package -DskipTests
before_deploy: mvn -fmain/pom.xml -Prelease clean package -DskipTests
addons:
coverity_scan:
@@ -35,14 +39,16 @@ addons:
name: "cryptomator/cryptomator"
notification_email: sebastian.stenzel@cryptomator.org
build_command: "mvn -fmain/pom.xml clean test -DskipTests"
branch_pattern: coverity_scan
branch_pattern: release.*
deploy:
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

74
CODE_OF_CONDUCT.md Normal file
View File

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

33
CONTRIBUTING.md Normal file
View File

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

19
ISSUE_TEMPLATE.md Normal file
View File

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

View File

@@ -1,44 +1,67 @@
Cryptomator
====================
![cryptomator](cryptomator.png)
[![Build Status](https://travis-ci.org/cryptomator/cryptomator.svg?branch=master)](https://travis-ci.org/cryptomator/cryptomator)
[![Coverity Scan Build Status](https://scan.coverity.com/projects/cryptomator-cryptomator/badge.svg?flat=1)](https://scan.coverity.com/projects/cryptomator-cryptomator)
[![Coverage Status](https://coveralls.io/repos/github/cryptomator/cryptomator/badge.svg?branch=master)](https://coveralls.io/github/cryptomator/cryptomator?branch=master)
[![Join the chat at https://gitter.im/cryptomator/cryptomator](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/cryptomator/cryptomator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Twitter](https://img.shields.io/badge/twitter-@Cryptomator-blue.svg?style=flat)](http://twitter.com/Cryptomator)
[![POEditor](https://img.shields.io/badge/POEditor-Help%20Translate-blue.svg?style=flat)](https://poeditor.com/join/project/bHwbvJmx0E)
Multiplatform transparent client-side encryption of your files in the cloud.
Multi-platform transparent client-side encryption of your files in the cloud.
If you want to take a look at the current beta version, go ahead and get your copy of cryptomator on [Cryptomator.org](https://cryptomator.org) or clone and build Cryptomator using Maven (instructions below).
Download native binaries of Cryptomator on [cryptomator.org](https://cryptomator.org/) or clone and build Cryptomator using Maven (instructions below).
## Features
- Totally transparent: Just work on the encrypted volume, as if it was an USB flash drive
- Works with Dropbox, OneDrive (Skydrive), Google Drive and any other cloud storage, that syncs with a local directory.
- In fact it works with any directory. You can use it to encrypt as many folders as you like
- Works with Dropbox, Google Drive, OneDrive, and any other cloud storage service that synchronizes with a local directory
- Open Source means: No backdoors, control is better than trust
- Client-side: No accounts, no data shared with any online service
- Totally transparent: Just work on the virtual drive as if it were a USB flash drive
- AES encryption with 256-bit key length
- 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 ;-)
- Filenames get encrypted, too
- Use as many vaults in your Dropbox as you want, each having individual passwords
### Privacy
- 256 bit keys (unlimited strength policy bundled with native binaries - 128-bit elsewhere)
- 256-bit keys (unlimited strength policy bundled with native binaries)
- Scrypt key derivation
- Cryptographically secure random numbers for salts, IVs and the 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
```
## Contributing to Cryptomator
Please read our [contribution guide](https://github.com/cryptomator/cryptomator/blob/master/CONTRIBUTING.md), if you would like to report a bug, ask a question or help us with coding.
## Code of Conduct
Help us keep Cryptomator open and inclusive. Please read and follow our [Code of Conduct](https://github.com/cryptomator/cryptomator/blob/master/CODE_OF_CONDUCT.md).
## License
Distributed under the MIT X Consortium license. See the LICENSE file for more info.
Distributed under the MIT X Consortium license. See the `LICENSES/MIT-X-Consortium-License.txt` file for more info.

BIN
cryptomator.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

1
main/ant-kit/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target/

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

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

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

@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2016 Sebastian Stenzel
This file is licensed under the terms of the MIT license.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.1.2</version>
</parent>
<artifactId>ant-kit</artifactId>
<packaging>pom</packaging>
<name>Cryptomator Ant Build Kit</name>
<description>Builds a package that can be built with Ant locally</description>
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>ui</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<!-- copy libraries to target/libs/: -->
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-libs</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/libs</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<!-- copy resources to target/: -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>2.7</version>
<executions>
<execution>
<id>copy-resources</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}</outputDirectory>
<escapeString>\</escapeString>
<encoding>UTF-8</encoding>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<excludes>
<exclude>fixed-binaries/**</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<includes>
<include>fixed-binaries/**</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<!-- create antkit.tar.gz: -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<descriptors>
<descriptor>assembly.xml</descriptor>
</descriptors>
<appendAssemblyId>false</appendAssemblyId>
<finalName>antkit</finalName>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -0,0 +1,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, xdg-utils
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.

View 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.

View File

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

View File

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

View File

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

View File

@@ -10,23 +10,45 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.3</version>
<version>1.1.2</version>
</parent>
<artifactId>commons</artifactId>
<name>Cryptomator common</name>
<description>Shared utilities</description>
<dependencies>
<!-- Libs -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- DI -->
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger</artifactId>
</dependency>
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger-compiler</artifactId>
<scope>provided</scope>
</dependency>
<!-- Test -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>de.bechte.junit</groupId>
<artifactId>junit-hierarchicalcontextrunner</artifactId>
@@ -38,4 +60,13 @@
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

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

View File

@@ -6,7 +6,7 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.ui.util;
package org.cryptomator.common;
import java.util.Comparator;

View File

@@ -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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,16 @@
package org.cryptomator.crypto.engine.impl;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
public final class Constants {
private Constants() {
}
static final Integer CURRENT_VAULT_VERSION = 3;
static final Collection<Integer> SUPPORTED_VAULT_VERSIONS = Collections.unmodifiableCollection(Arrays.asList(3, 4));
static final Integer CURRENT_VAULT_VERSION = 4;
public static final int PAYLOAD_SIZE = 32 * 1024;
public static final int NONCE_SIZE = 16;

View File

@@ -9,6 +9,7 @@
package org.cryptomator.crypto.engine.impl;
import static org.cryptomator.crypto.engine.impl.Constants.CURRENT_VAULT_VERSION;
import static org.cryptomator.crypto.engine.impl.Constants.SUPPORTED_VAULT_VERSIONS;
import java.io.IOException;
import java.nio.ByteBuffer;
@@ -18,6 +19,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 +83,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);
}
}
@@ -107,7 +110,7 @@ class CryptorImpl implements Cryptor {
assert keyFile != null;
// check version
if (!CURRENT_VAULT_VERSION.equals(keyFile.getVersion())) {
if (!SUPPORTED_VAULT_VERSIONS.contains(keyFile.getVersion())) {
throw new UnsupportedVaultFormatException(keyFile.getVersion(), CURRENT_VAULT_VERSION);
}
@@ -116,12 +119,12 @@ class CryptorImpl implements Cryptor {
final SecretKey kek = new SecretKeySpec(kekBytes, ENCRYPTION_ALG);
this.macKey = AesKeyWrap.unwrap(kek, keyFile.getMacMasterKey(), MAC_ALG);
// future use (as soon as we need to prevent downgrade attacks):
// final Mac mac = new ThreadLocalMac(macKey, MAC_ALG).get();
// final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.BYTES).putInt(CURRENT_VAULT_VERSION).array());
// if (!MessageDigest.isEqual(versionMac, keyFile.getVersionMac())) {
// destroyQuietly(macKey);
// throw new UnsupportedVaultFormatException(Integer.MAX_VALUE, CURRENT_VAULT_VERSION);
// }
// final Mac mac = new ThreadLocalMac(macKey, MAC_ALG).get();
// final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.BYTES).putInt(CURRENT_VAULT_VERSION).array());
// if (!MessageDigest.isEqual(versionMac, keyFile.getVersionMac())) {
// destroyQuietly(macKey);
// throw new UnsupportedVaultFormatException(Integer.MAX_VALUE, CURRENT_VAULT_VERSION);
// }
this.encryptionKey = AesKeyWrap.unwrap(kek, keyFile.getEncryptionMasterKey(), ENCRYPTION_ALG);
} catch (InvalidKeyException e) {
throw new InvalidPassphraseException();

View File

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

View File

@@ -12,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 javax.crypto.AEADBadTagException;
import javax.crypto.SecretKey;
@@ -25,6 +26,8 @@ import org.cryptomator.siv.SivMode;
class FilenameCryptorImpl implements FilenameCryptor {
private static final BaseNCodec BASE32 = new Base32();
// https://tools.ietf.org/html/rfc4648#section-6
private static final Pattern BASE32_PATTERN = Pattern.compile("([A-Z2-7]{8})*[A-Z2-7=]{8}");
private static final ThreadLocal<MessageDigest> SHA1 = new ThreadLocalSha1();
private static final ThreadLocal<SivMode> AES_SIV = new ThreadLocal<SivMode>() {
@Override
@@ -50,8 +53,8 @@ class FilenameCryptorImpl implements FilenameCryptor {
}
@Override
public boolean isEncryptedFilename(String ciphertextName) {
return BASE32.isInAlphabet(ciphertextName);
public Pattern encryptedNamePattern() {
return BASE32_PATTERN;
}
@Override

View File

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

View File

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

View File

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

View File

@@ -8,8 +8,6 @@
*******************************************************************************/
package org.cryptomator.filesystem.crypto;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.UncheckedIOException;
import java.nio.file.FileAlreadyExistsException;
import java.util.Optional;
@@ -27,9 +25,7 @@ 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

View File

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

View File

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

View File

@@ -21,20 +21,20 @@ public class CryptorImplTest {
@Test
public void testMasterkeyDecryptionWithCorrectPassphrase() throws IOException {
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
final String testMasterKey = "{\"version\":4,\"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\":4,\"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");
}
@@ -62,23 +62,24 @@ 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\":4,\"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\":4,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
+ "\"hmacMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
cryptor.randomizeMasterkey();
final byte[] masterkeyFile = cryptor.writeKeysToMasterkeyFile("asd");
System.out.println(new String(masterkeyFile));
Assert.assertArrayEquals(expectedMasterKey.getBytes(), masterkeyFile);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,10 +10,12 @@ package org.cryptomator.frontend.webdav;
import javax.inject.Singleton;
import org.cryptomator.common.CommonsModule;
import dagger.Component;
@Singleton
@Component
@Component(modules = {CommonsModule.class})
public interface WebDavComponent {
WebDavServer server();

View File

@@ -32,7 +32,7 @@ public class WindowsCompatibilityServlet extends HttpServlet {
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.addHeader("DAV", "1, 2");
resp.addHeader("MS-Author-Via", "DAV");
// resp.addHeader("Allow", "OPTIONS, GET, HEAD, POST, TRACE, PROPFIND, PROPPATCH, MKCOL, COPY, PUT, DELETE, MOVE, LOCK, UNLOCK");
resp.addHeader("Allow", "OPTIONS, GET, HEAD");
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
}

View File

@@ -12,11 +12,13 @@ package org.cryptomator.frontend.webdav.mount;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Comparator;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.commons.io.IOUtils;
@@ -26,15 +28,18 @@ import org.cryptomator.frontend.CommandFailedException;
import org.cryptomator.frontend.Frontend.MountParam;
@Singleton
final class MacOsXWebDavMounter implements WebDavMounterStrategy {
final class MacOsXAppleScriptWebDavMounter implements WebDavMounterStrategy {
private final Comparator<String> semVerComparator;
@Inject
MacOsXWebDavMounter() {
MacOsXAppleScriptWebDavMounter(@Named("SemVer") Comparator<String> semVerComparator) {
this.semVerComparator = semVerComparator;
}
@Override
public boolean shouldWork() {
return SystemUtils.IS_OS_MAC_OSX;
return SystemUtils.IS_OS_MAC_OSX && semVerComparator.compare(SystemUtils.OS_VERSION, "10.10") >= 0;
}
@Override

View File

@@ -0,0 +1,89 @@
/*******************************************************************************
* Copyright (c) 2014, 2016 Sebastian Stenzel, Markus Kreusch
* 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, strategy fine tuning
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
******************************************************************************/
package org.cryptomator.frontend.webdav.mount;
import java.net.URI;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.util.Comparator;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.frontend.CommandFailedException;
import org.cryptomator.frontend.Frontend.MountParam;
import org.cryptomator.frontend.webdav.mount.command.Script;
@Singleton
final class MacOsXShellScriptWebDavMounter implements WebDavMounterStrategy {
private final Comparator<String> semVerComparator;
@Inject
MacOsXShellScriptWebDavMounter(@Named("SemVer") Comparator<String> semVerComparator) {
this.semVerComparator = semVerComparator;
}
@Override
public boolean shouldWork() {
return SystemUtils.IS_OS_MAC_OSX && semVerComparator.compare(SystemUtils.OS_VERSION, "10.10") < 0;
}
@Override
public void warmUp(int serverPort) {
// no-op
}
@Override
public WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) throws CommandFailedException {
final String mountName = mountParams.getOrDefault(MountParam.MOUNT_NAME, Optional.empty()).orElseThrow(() -> {
return new IllegalArgumentException("Missing mount parameter MOUNT_NAME.");
});
// we don't use the uri to derive a path, as it *could* be longer than 255 chars.
final String path = "/Volumes/Cryptomator_" + UUID.randomUUID().toString();
final Script mountScript = Script.fromLines("mkdir \"$MOUNT_PATH\"", "mount_webdav -S -v $MOUNT_NAME \"$DAV_AUTHORITY$DAV_PATH\" \"$MOUNT_PATH\"").addEnv("DAV_AUTHORITY", uri.getRawAuthority())
.addEnv("DAV_PATH", uri.getRawPath()).addEnv("MOUNT_PATH", path).addEnv("MOUNT_NAME", mountName);
mountScript.execute();
return new MacWebDavMount(path);
}
private static class MacWebDavMount extends AbstractWebDavMount {
private final String mountPath;
private final Script revealScript;
private final Script unmountScript;
private MacWebDavMount(String mountPath) {
this.mountPath = mountPath;
this.revealScript = Script.fromLines("open \"$MOUNT_PATH\"").addEnv("MOUNT_PATH", mountPath);
this.unmountScript = Script.fromLines("diskutil umount $MOUNT_PATH").addEnv("MOUNT_PATH", mountPath);
}
@Override
public void unmount() throws CommandFailedException {
// only attempt unmount if user didn't unmount manually:
if (Files.exists(FileSystems.getDefault().getPath(mountPath))) {
unmountScript.execute();
}
}
@Override
public void reveal() throws CommandFailedException {
revealScript.execute();
}
}
}

View File

@@ -19,74 +19,87 @@ import javax.inject.Singleton;
@Singleton
class MountStrategies implements Collection<WebDavMounterStrategy> {
private final Collection<WebDavMounterStrategy> delegate;
@Inject
MountStrategies(LinuxGvfsWebDavMounter linuxMounter, MacOsXWebDavMounter osxMounter, WindowsWebDavMounter winMounter) {
delegate = unmodifiableList(asList(linuxMounter, osxMounter, winMounter));
MountStrategies(LinuxGvfsWebDavMounter linuxMounter, MacOsXAppleScriptWebDavMounter osxAppleScriptMounter, MacOsXShellScriptWebDavMounter osxShellScriptMounter, WindowsWebDavMounter winMounter) {
delegate = unmodifiableList(asList(linuxMounter, osxAppleScriptMounter, osxShellScriptMounter, winMounter));
}
@Override
public int size() {
return delegate.size();
}
@Override
public boolean isEmpty() {
return delegate.isEmpty();
}
@Override
public boolean contains(Object o) {
return delegate.contains(o);
}
@Override
public Iterator<WebDavMounterStrategy> iterator() {
return delegate.iterator();
}
@Override
public Object[] toArray() {
return delegate.toArray();
}
@Override
public <T> T[] toArray(T[] a) {
return delegate.toArray(a);
}
@Override
public boolean add(WebDavMounterStrategy e) {
return delegate.add(e);
}
@Override
public boolean remove(Object o) {
return delegate.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return delegate.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends WebDavMounterStrategy> c) {
return delegate.addAll(c);
}
@Override
public boolean removeAll(Collection<?> c) {
return delegate.removeAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
return delegate.retainAll(c);
}
@Override
public void clear() {
delegate.clear();
}
@Override
public boolean equals(Object o) {
return delegate.equals(o);
}
@Override
public int hashCode() {
return delegate.hashCode();
}
}

View File

@@ -11,10 +11,16 @@ package org.cryptomator.frontend.webdav.mount;
import static org.cryptomator.frontend.webdav.mount.command.Script.fromLines;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -22,12 +28,16 @@ import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.frontend.CommandFailedException;
import org.cryptomator.frontend.Frontend.MountParam;
import org.cryptomator.frontend.webdav.mount.command.CommandResult;
import org.cryptomator.frontend.webdav.mount.command.Script;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A {@link WebDavMounterStrategy} utilizing the "net use" command.
@@ -37,7 +47,9 @@ import org.cryptomator.frontend.webdav.mount.command.Script;
@Singleton
final class WindowsWebDavMounter implements WebDavMounterStrategy {
private static final Logger LOG = LoggerFactory.getLogger(WindowsWebDavMounter.class);
private static final Pattern WIN_MOUNT_DRIVELETTER_PATTERN = Pattern.compile("\\s*([A-Z]):\\s*");
private static final Pattern REG_QUERY_PROXY_OVERRIDES_PATTERN = Pattern.compile("\\s*ProxyOverride\\s+REG_SZ\\s+(.*)\\s*");
private static final String AUTO_ASSIGN_DRIVE_LETTER = "*";
private static final String LOCALHOST = "localhost";
private static final int MOUNT_TIMEOUT_SECONDS = 60;
@@ -60,12 +72,12 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
@Override
public WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) throws CommandFailedException {
final String driveLetter = mountParams.getOrDefault(MountParam.WIN_DRIVE_LETTER, Optional.of(AUTO_ASSIGN_DRIVE_LETTER)).orElse(AUTO_ASSIGN_DRIVE_LETTER);
final String driveLetter = mountParams.getOrDefault(MountParam.WIN_DRIVE_LETTER, Optional.empty()).orElse(AUTO_ASSIGN_DRIVE_LETTER);
if (driveLetters.getOccupiedDriveLetters().contains(CharUtils.toChar(driveLetter))) {
throw new CommandFailedException("Drive letter occupied.");
}
final String hostname = mountParams.getOrDefault(MountParam.HOSTNAME, Optional.of(LOCALHOST)).orElse(LOCALHOST);
final String hostname = mountParams.getOrDefault(MountParam.HOSTNAME, Optional.empty()).orElse(LOCALHOST);
try {
final URI adjustedUri = new URI(uri.getScheme(), uri.getUserInfo(), hostname, uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment());
CommandResult mountResult = mount(adjustedUri, driveLetter);
@@ -74,14 +86,14 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
throw new IllegalArgumentException("Invalid host: " + hostname);
}
}
private CommandResult mount(URI uri, String driveLetter) throws CommandFailedException {
final Script proxyBypassScript = fromLines(
"reg add \"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v \"ProxyOverride\" /d \"<local>;%DAV_HOST%;%DAV_HOST%:%DAV_PORT%\" /f");
proxyBypassScript.addEnv("DAV_HOST", uri.getHost());
proxyBypassScript.addEnv("DAV_PORT", String.valueOf(uri.getPort()));
proxyBypassScript.execute();
try {
addProxyOverrides(uri);
} catch (IOException e) {
throw new CommandFailedException(e);
}
final String driveLetterStr = AUTO_ASSIGN_DRIVE_LETTER.equals(driveLetter) ? AUTO_ASSIGN_DRIVE_LETTER : driveLetter + ":";
final Script mountScript = fromLines("net use %DRIVE_LETTER% \\\\%DAV_HOST%@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no");
mountScript.addEnv("DRIVE_LETTER", driveLetterStr);
@@ -90,6 +102,44 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
mountScript.addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\'));
return mountScript.execute(MOUNT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
private void addProxyOverrides(URI uri) throws IOException, CommandFailedException {
try {
// get existing value for ProxyOverride key from reqistry:
ProcessBuilder query = new ProcessBuilder("reg", "query", "\"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\"", "/v", "ProxyOverride");
Process queryCmd = query.start();
String queryStdOut = IOUtils.toString(queryCmd.getInputStream(), StandardCharsets.UTF_8);
int queryResult = queryCmd.waitFor();
// determine new value for ProxyOverride key:
Set<String> overrides = new HashSet<>();
Matcher matcher = REG_QUERY_PROXY_OVERRIDES_PATTERN.matcher(queryStdOut);
if (queryResult == 0 && matcher.find()) {
String[] existingOverrides = StringUtils.split(matcher.group(1), ';');
overrides.addAll(Arrays.asList(existingOverrides));
}
overrides.removeIf(s -> s.startsWith(uri.getHost() + ":"));
overrides.add("<local>");
overrides.add(uri.getHost());
overrides.add(uri.getHost() + ":" + uri.getPort());
// set new value:
String overridesStr = StringUtils.join(overrides, ';');
ProcessBuilder add = new ProcessBuilder("reg", "add", "\"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\"", "/v", "ProxyOverride", "/d", "\"" + overridesStr + "\"", "/f");
LOG.debug("Invoking command: " + StringUtils.join(add.command(), ' '));
Process addCmd = add.start();
int addResult = addCmd.waitFor();
if (addResult != 0) {
String addStdErr = IOUtils.toString(addCmd.getErrorStream(), StandardCharsets.UTF_8);
throw new CommandFailedException(addStdErr);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
InterruptedIOException ioException = new InterruptedIOException();
ioException.initCause(e);
throw ioException;
}
}
private String getDriveLetter(String result) throws CommandFailedException {
final Matcher matcher = WIN_MOUNT_DRIVELETTER_PATTERN.matcher(result);

1
main/jacoco-report/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target/

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright (c) 2016 Sebastian Stenzel This file is licensed under the terms of the MIT license. See the LICENSE.txt file for more info. -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.1.2</version>
</parent>
<artifactId>jacoco-report</artifactId>
<name>Cryptomator Code Coverage Report</name>
<dependencies>
<!-- Commons -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>commons</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>commons-test</artifactId>
</dependency>
<!-- Filesystem Layers -->
<dependency>
<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>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-crypto-integration-tests</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-inmemory</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-nameshortening</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-nio</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-stats</artifactId>
</dependency>
<!-- Frontends -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>frontend-api</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>frontend-webdav</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<executions>
<execution>
<id>report-aggregate</id>
<phase>verify</phase>
<goals>
<goal>report-aggregate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -7,7 +7,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.3</version>
<version>1.1.2</version>
<packaging>pom</packaging>
<name>Cryptomator</name>
@@ -35,21 +35,14 @@
<hamcrest.version>1.3</hamcrest.version> <!-- keep in sync with version required by JUnit -->
<commons-io.version>2.4</commons-io.version>
<commons-collections.version>4.0</commons-collections.version>
<commons-lang3.version>3.3.2</commons-lang3.version>
<commons-lang3.version>3.4</commons-lang3.version>
<commons-codec.version>1.10</commons-codec.version>
<commons-httpclient.version>3.1</commons-httpclient.version>
<jackson-databind.version>2.4.4</jackson-databind.version>
<mockito.version>1.10.19</mockito.version>
<dagger.version>2.0.2</dagger.version>
<dagger.version>2.4</dagger.version>
</properties>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencyManagement>
<dependencies>
<!-- modules -->
@@ -70,6 +63,11 @@
<artifactId>filesystem-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-charsets</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-nio</artifactId>
@@ -81,6 +79,12 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-invariants-tests</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-nameshortening</artifactId>
@@ -254,14 +258,6 @@
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-jul</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</dependency>
</dependencies>
<modules>
@@ -278,13 +274,21 @@
<module>frontend-api</module>
<module>frontend-webdav</module>
<module>ui</module>
<module>filesystem-charsets</module>
</modules>
<profiles>
<profile>
<id>uber-jar</id>
<id>release</id>
<modules>
<module>uber-jar</module>
<module>ant-kit</module>
</modules>
</profile>
<profile>
<id>test-coverage</id>
<modules>
<module>jacoco-report</module>
</modules>
</profile>
</profiles>
@@ -311,7 +315,7 @@
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.5.201505241946</version>
<version>0.7.7.201606060606</version>
<executions>
<execution>
<id>prepare-agent</id>
@@ -320,6 +324,12 @@
</goals>
</execution>
</executions>
<configuration>
<excludes>
<exclude>**/*_*</exclude>
<exclude>**/Dagger*</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</pluginManagement>
@@ -338,6 +348,9 @@
<artifactId>coveralls-maven-plugin</artifactId>
<version>4.0.0</version>
<configuration>
<jacocoReports>
<jacocoReport>jacoco-report/target/site/jacoco-aggregate/jacoco.xml</jacocoReport>
</jacocoReports>
<repoToken>${env.COVERALLS_REPO_TOKEN}</repoToken>
</configuration>
</plugin>

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.3</version>
<version>1.1.2</version>
</parent>
<artifactId>uber-jar</artifactId>
<packaging>pom</packaging>

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.3</version>
<version>1.1.2</version>
</parent>
<artifactId>ui</artifactId>
<name>Cryptomator GUI</name>
@@ -38,6 +38,10 @@
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-crypto</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-charsets</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-stats</artifactId>
@@ -99,5 +103,12 @@
<groupId>org.cryptomator</groupId>
<artifactId>commons-test</artifactId>
</dependency>
<!-- Zxcvbn -->
<dependency>
<groupId>com.nulab-inc</groupId>
<artifactId>zxcvbn</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>
</project>

View File

@@ -36,6 +36,8 @@ public class Cryptomator {
private static final CleanShutdownPerformer CLEAN_SHUTDOWN_PERFORMER = new CleanShutdownPerformer();
public static void main(String[] args) {
String cryptomatorVersion = Optional.ofNullable(Cryptomator.class.getPackage().getImplementationVersion()).orElse("SNAPSHOT");
LOG.info("Starting Cryptomator {} on {} {} ({})", cryptomatorVersion, SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
if (SystemUtils.IS_OS_MAC_OSX) {
/*
* On OSX we're in an awkward position. We need to register a handler in the main thread of this application. However, we can't

View File

@@ -8,13 +8,13 @@
*******************************************************************************/
package org.cryptomator.ui;
import java.util.Comparator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.inject.Named;
import javax.inject.Singleton;
import org.cryptomator.common.CommonsModule;
import org.cryptomator.crypto.engine.impl.CryptoEngineModule;
import org.cryptomator.frontend.FrontendFactory;
import org.cryptomator.frontend.webdav.WebDavServer;
@@ -24,7 +24,6 @@ import org.cryptomator.ui.model.VaultObjectMapperProvider;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.settings.SettingsProvider;
import org.cryptomator.ui.util.DeferredCloser;
import org.cryptomator.ui.util.SemVerComparator;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -33,7 +32,7 @@ import dagger.Provides;
import javafx.application.Application;
import javafx.stage.Stage;
@Module(includes = CryptoEngineModule.class)
@Module(includes = {CryptoEngineModule.class, CommonsModule.class})
class CryptomatorModule {
private final Application application;
@@ -65,13 +64,6 @@ class CryptomatorModule {
return closer;
}
@Provides
@Singleton
@Named("SemVer")
Comparator<String> provideSemVerComparator() {
return new SemVerComparator();
}
@Provides
@Singleton
@Named("VaultJsonMapper")

View File

@@ -5,6 +5,7 @@
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
* Jean-Noël Charon - implementation of github issue #56
*******************************************************************************/
package org.cryptomator.ui;
@@ -17,6 +18,8 @@ import java.awt.Toolkit;
import java.awt.TrayIcon;
import java.awt.TrayIcon.MessageType;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
@@ -75,6 +78,11 @@ class ExitUtil {
private void initTrayIconExitHandler(Runnable exitCommand) {
final TrayIcon trayIcon = createTrayIcon(exitCommand);
try {
// double clicking tray icon should open Cryptomator
if (SystemUtils.IS_OS_WINDOWS) {
trayIcon.addMouseListener(new TrayIconMouseListener());
}
SystemTray.getSystemTray().add(trayIcon);
mainWindow.setOnCloseRequest((e) -> {
if (Platform.isImplicitExit()) {
@@ -136,6 +144,7 @@ class ExitUtil {
return;
} else {
settings.setNumTrayNotifications(settings.getNumTrayNotifications() - 1);
settings.save();
}
final Runnable notificationCmd;
if (SystemUtils.IS_OS_MAC_OSX) {
@@ -167,6 +176,17 @@ class ExitUtil {
});
}
private class TrayIconMouseListener extends MouseAdapter {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) {
restoreFromTray(new ActionEvent(e.getSource(), e.getID(), e.paramString()));
}
}
}
private void restoreFromTray(ActionEvent event) {
Platform.runLater(() -> {
mainWindow.show();

View File

@@ -38,6 +38,7 @@ public class MainApplication extends Application {
@Override
public void start(Stage primaryStage) throws IOException {
LOG.info("JavaFX application started");
final CryptomatorComponent comp = DaggerCryptomatorComponent.builder().cryptomatorModule(new CryptomatorModule(this, primaryStage)).build();
final MainController mainCtrl = comp.mainController();
closer = comp.deferredCloser();

View File

@@ -5,6 +5,7 @@
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
* Jean-Noël Charon - password strength meter
*******************************************************************************/
package org.cryptomator.ui.controllers;
@@ -21,18 +22,24 @@ import org.cryptomator.crypto.engine.UnsupportedVaultFormatException;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.util.PasswordStrengthUtil;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.text.Text;
@Singleton
@@ -41,13 +48,16 @@ public class ChangePasswordController extends LocalizedFXMLViewController {
private static final Logger LOG = LoggerFactory.getLogger(ChangePasswordController.class);
private final Application app;
private final PasswordStrengthUtil strengthRater;
final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
private Optional<ChangePasswordListener> listener = Optional.empty();
private final IntegerProperty passwordStrength = new SimpleIntegerProperty(); // 0-4
@Inject
public ChangePasswordController(Application app, Localization localization) {
public ChangePasswordController(Application app, PasswordStrengthUtil strengthRater, Localization localization) {
super(localization);
this.app = app;
this.strengthRater = strengthRater;
}
@FXML
@@ -68,12 +78,39 @@ public class ChangePasswordController extends LocalizedFXMLViewController {
@FXML
private Hyperlink downloadsPageLink;
@FXML
private Label passwordStrengthLabel;
@FXML
private Region passwordStrengthLevel0;
@FXML
private Region passwordStrengthLevel1;
@FXML
private Region passwordStrengthLevel2;
@FXML
private Region passwordStrengthLevel3;
@FXML
private Region passwordStrengthLevel4;
@Override
public void initialize() {
BooleanBinding oldPasswordIsEmpty = oldPasswordField.textProperty().isEmpty();
BooleanBinding newPasswordIsEmpty = newPasswordField.textProperty().isEmpty();
BooleanBinding passwordsDiffer = newPasswordField.textProperty().isNotEqualTo(retypePasswordField.textProperty());
EasyBind.subscribe(vault, this::vaultDidChange);
changePasswordButton.disableProperty().bind(oldPasswordIsEmpty.or(newPasswordIsEmpty.or(passwordsDiffer)));
passwordStrength.bind(EasyBind.map(newPasswordField.textProperty(), strengthRater::computeRate));
passwordStrengthLevel0.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(0), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel1.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(1), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel2.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(2), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel3.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(3), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel4.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(4), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
}
@Override
@@ -81,6 +118,15 @@ public class ChangePasswordController extends LocalizedFXMLViewController {
return getClass().getResource("/fxml/change_password.fxml");
}
private void vaultDidChange(Vault newVault) {
oldPasswordField.clear();
newPasswordField.clear();
retypePasswordField.clear();
// trigger "default" change to refresh key bindings:
changePasswordButton.setDefaultButton(false);
changePasswordButton.setDefaultButton(true);
}
// ****************************************
// Downloads link
// ****************************************

View File

@@ -2,9 +2,10 @@
* Copyright (c) 2014, 2016 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
* Jean-Noël Charon - password strength meter
******************************************************************************/
package org.cryptomator.ui.controllers;
@@ -20,29 +21,37 @@ import javax.inject.Singleton;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.util.PasswordStrengthUtil;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Platform;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
@Singleton
public class InitializeController extends LocalizedFXMLViewController {
private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class);
private final PasswordStrengthUtil strengthRater;
final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
private Optional<InitializationListener> listener = Optional.empty();
private final IntegerProperty passwordStrength = new SimpleIntegerProperty(); // 0-4
@Inject
public InitializeController(Localization localization) {
public InitializeController(Localization localization, PasswordStrengthUtil strengthRater) {
super(localization);
this.strengthRater = strengthRater;
}
@FXML
@@ -57,11 +66,38 @@ public class InitializeController extends LocalizedFXMLViewController {
@FXML
private Label messageLabel;
@FXML
private Label passwordStrengthLabel;
@FXML
private Region passwordStrengthLevel0;
@FXML
private Region passwordStrengthLevel1;
@FXML
private Region passwordStrengthLevel2;
@FXML
private Region passwordStrengthLevel3;
@FXML
private Region passwordStrengthLevel4;
@Override
public void initialize() {
BooleanBinding passwordIsEmpty = passwordField.textProperty().isEmpty();
BooleanBinding passwordsDiffer = passwordField.textProperty().isNotEqualTo(retypePasswordField.textProperty());
EasyBind.subscribe(vault, this::vaultDidChange);
okButton.disableProperty().bind(passwordIsEmpty.or(passwordsDiffer));
passwordStrength.bind(EasyBind.map(passwordField.textProperty(), strengthRater::computeRate));
passwordStrengthLevel0.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(0), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel1.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(1), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel2.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(2), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel3.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(3), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel4.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(4), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
}
@Override
@@ -69,6 +105,14 @@ public class InitializeController extends LocalizedFXMLViewController {
return getClass().getResource("/fxml/initialize.fxml");
}
private void vaultDidChange(Vault newVault) {
passwordField.clear();
retypePasswordField.clear();
// trigger "default" change to refresh key bindings:
okButton.setDefaultButton(false);
okButton.setDefaultButton(true);
}
// ****************************************
// OK button
// ****************************************

View File

@@ -102,7 +102,7 @@ public class MacWarningsController extends LocalizedFXMLViewController {
@FXML
private void didClickMoreInformationButton(ActionEvent event) {
application.getHostServices().showDocument("https://cryptomator.org/faq/#macWarning");
application.getHostServices().showDocument("https://cryptomator.freshdesk.com/support/solutions/articles/16000003666-what-does-mac-authentication-failed-mean-");
}
private void unauthenticatedResourcesDidChange(Change<? extends String> change) {

View File

@@ -5,6 +5,7 @@
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
* Jean-Noël Charon - confirmation dialog on vault removal
******************************************************************************/
package org.cryptomator.ui.controllers;
@@ -16,19 +17,22 @@ import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.controls.DirectoryListCell;
import org.cryptomator.ui.model.UpgradeStrategies;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.model.VaultFactory;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.util.DialogBuilderUtil;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.monadic.MonadicBinding;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -37,18 +41,23 @@ import javafx.application.Platform;
import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.geometry.Side;
import javafx.scene.Parent;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
@@ -70,18 +79,20 @@ public class MainController extends LocalizedFXMLViewController {
private final Provider<UnlockedController> unlockedControllerProvider;
private final Lazy<ChangePasswordController> changePasswordController;
private final Lazy<SettingsController> settingsController;
private final Lazy<UpgradeStrategies> upgradeStrategies;
private final ObjectProperty<AbstractFXMLViewController> activeController = new SimpleObjectProperty<>();
private final ObservableList<Vault> vaults;
private final ObjectProperty<Vault> selectedVault = new SimpleObjectProperty<>();
private final MonadicBinding<Boolean> isSelectedVaultUnlocked = EasyBind.select(selectedVault).selectObject(Vault::unlockedProperty);;
private final Binding<Boolean> canEditSelectedVault = EasyBind.combine(selectedVault.isNull(), isSelectedVaultUnlocked.orElse(false), Boolean::logicalOr);
private final BooleanExpression isSelectedVaultUnlocked = BooleanBinding.booleanExpression(EasyBind.select(selectedVault).selectObject(Vault::unlockedProperty).orElse(false));
private final BooleanExpression isSelectedVaultValid = BooleanBinding.booleanExpression(EasyBind.monadic(selectedVault).map(Vault::isValidVaultDirectory).orElse(false));
private final BooleanExpression canEditSelectedVault = selectedVault.isNotNull().and(isSelectedVaultUnlocked.not());
private final BooleanBinding isShowingSettings;
private final Map<Vault, UnlockedController> unlockedVaults = new HashMap<>();
@Inject
public MainController(@Named("mainWindow") Stage mainWindow, Localization localization, Settings settings, VaultFactory vaultFactoy, Lazy<WelcomeController> welcomeController,
Lazy<InitializeController> initializeController, Lazy<NotFoundController> notFoundController, Lazy<UpgradeController> upgradeController, Lazy<UnlockController> unlockController,
Provider<UnlockedController> unlockedControllerProvider, Lazy<ChangePasswordController> changePasswordController, Lazy<SettingsController> settingsController) {
Provider<UnlockedController> unlockedControllerProvider, Lazy<ChangePasswordController> changePasswordController, Lazy<SettingsController> settingsController, Lazy<UpgradeStrategies> upgradeStrategies) {
super(localization);
this.mainWindow = mainWindow;
this.vaultFactoy = vaultFactoy;
@@ -93,7 +104,11 @@ public class MainController extends LocalizedFXMLViewController {
this.unlockedControllerProvider = unlockedControllerProvider;
this.changePasswordController = changePasswordController;
this.settingsController = settingsController;
this.upgradeStrategies = upgradeStrategies;
this.vaults = FXCollections.observableList(settings.getDirectories());
this.vaults.addListener((Change<? extends Vault> c) -> {
settings.save();
});
// derived bindings:
this.isShowingSettings = activeController.isEqualTo(settingsController.get());
@@ -102,6 +117,9 @@ public class MainController extends LocalizedFXMLViewController {
@FXML
private ContextMenu vaultListCellContextMenu;
@FXML
private MenuItem changePasswordMenuItem;
@FXML
private ContextMenu addVaultContextMenu;
@@ -132,8 +150,9 @@ public class MainController extends LocalizedFXMLViewController {
vaultList.setCellFactory(this::createDirecoryListCell);
activeController.set(welcomeController.get());
selectedVault.bind(vaultList.getSelectionModel().selectedItemProperty());
removeVaultButton.disableProperty().bind(canEditSelectedVault);
removeVaultButton.disableProperty().bind(canEditSelectedVault.not());
emptyListInstructions.visibleProperty().bind(Bindings.isEmpty(vaults));
changePasswordMenuItem.visibleProperty().bind(isSelectedVaultValid);
EasyBind.subscribe(selectedVault, this::selectedVaultDidChange);
EasyBind.subscribe(activeController, this::activeControllerDidChange);
@@ -186,7 +205,7 @@ public class MainController extends LocalizedFXMLViewController {
@FXML
private void didClickAddExistingVaults(ActionEvent event) {
final FileChooser fileChooser = new FileChooser();
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator vault", "*" + Vault.VAULT_FILE_EXTENSION));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*" + Vault.VAULT_FILE_EXTENSION));
final List<File> files = fileChooser.showOpenMultipleDialog(mainWindow);
if (files != null) {
for (final File file : files) {
@@ -224,9 +243,18 @@ public class MainController extends LocalizedFXMLViewController {
@FXML
private void didClickRemoveSelectedEntry(ActionEvent e) {
vaults.remove(selectedVault.get());
if (vaults.isEmpty()) {
activeController.set(welcomeController.get());
Alert confirmDialog = DialogBuilderUtil.buildConfirmationDialog( //
localization.getString("main.directoryList.remove.confirmation.title"), //
localization.getString("main.directoryList.remove.confirmation.header"), //
localization.getString("main.directoryList.remove.confirmation.content"), //
SystemUtils.IS_OS_MAC_OSX ? ButtonType.CANCEL : ButtonType.OK);
Optional<ButtonType> choice = confirmDialog.showAndWait();
if (ButtonType.OK.equals(choice.get())) {
vaults.remove(selectedVault.get());
if (vaults.isEmpty()) {
activeController.set(welcomeController.get());
}
}
}
@@ -263,7 +291,7 @@ public class MainController extends LocalizedFXMLViewController {
this.showUnlockedView(newValue);
} else if (!newValue.doesVaultDirectoryExist()) {
this.showNotFoundView();
} else if (newValue.isValidVaultDirectory() && newValue.needsUpgrade()) {
} else if (newValue.isValidVaultDirectory() && upgradeStrategies.get().getUpgradeStrategy(newValue).isPresent()) {
this.showUpgradeView();
} else if (newValue.isValidVaultDirectory()) {
this.showUnlockView();

View File

@@ -43,6 +43,9 @@ public class SettingsController extends LocalizedFXMLViewController {
@FXML
private TextField portField;
@FXML
private Label useIpv6Label;
@FXML
private CheckBox useIpv6Checkbox;
@@ -55,13 +58,14 @@ public class SettingsController extends LocalizedFXMLViewController {
checkForUpdatesCheckbox.setSelected(settings.isCheckForUpdatesEnabled() && !areUpdatesManagedExternally());
portField.setText(String.valueOf(settings.getPort()));
portField.addEventFilter(KeyEvent.KEY_TYPED, this::filterNumericKeyEvents);
useIpv6Checkbox.setDisable(!SystemUtils.IS_OS_WINDOWS);
useIpv6Label.setVisible(SystemUtils.IS_OS_WINDOWS);
useIpv6Checkbox.setVisible(SystemUtils.IS_OS_WINDOWS);
useIpv6Checkbox.setSelected(SystemUtils.IS_OS_WINDOWS && settings.shouldUseIpv6());
versionLabel.setText(String.format(localization.getString("settings.version.label"), applicationVersion().orElse("SNAPSHOT")));
EasyBind.subscribe(checkForUpdatesCheckbox.selectedProperty(), settings::setCheckForUpdatesEnabled);
EasyBind.subscribe(checkForUpdatesCheckbox.selectedProperty(), this::checkForUpdateDidChange);
EasyBind.subscribe(portField.textProperty(), this::portDidChange);
EasyBind.subscribe(useIpv6Checkbox.selectedProperty(), settings::setUseIpv6);
EasyBind.subscribe(useIpv6Checkbox.selectedProperty(), this::useIpv6DidChange);
}
@Override
@@ -73,21 +77,30 @@ public class SettingsController extends LocalizedFXMLViewController {
return Optional.ofNullable(getClass().getPackage().getImplementationVersion());
}
private void checkForUpdateDidChange(Boolean newValue) {
settings.setCheckForUpdatesEnabled(newValue);
settings.save();
}
private void portDidChange(String newValue) {
try {
int port = Integer.parseInt(newValue);
if (port < Settings.MIN_PORT) {
if (!settings.isPortValid(port)) {
settings.setPort(Settings.DEFAULT_PORT);
} else if (port < Settings.MAX_PORT) {
settings.setPort(port);
} else {
portField.setText(String.valueOf(Settings.MAX_PORT));
settings.setPort(port);
settings.save();
}
} catch (NumberFormatException e) {
portField.setText(String.valueOf(Settings.DEFAULT_PORT));
}
}
private void useIpv6DidChange(Boolean newValue) {
settings.setUseIpv6(newValue);
settings.save();
}
private void filterNumericKeyEvents(KeyEvent t) {
if (t.getCharacter() == null || t.getCharacter().length() == 0) {
return;

View File

@@ -119,7 +119,7 @@ public class UnlockController extends LocalizedFXMLViewController {
}
unlockButton.disableProperty().bind(passwordField.textProperty().isEmpty());
EasyBind.subscribe(vault, this::vaultChanged);
EasyBind.subscribe(vault, this::vaultDidChange);
}
@Override
@@ -127,7 +127,7 @@ public class UnlockController extends LocalizedFXMLViewController {
return getClass().getResource("/fxml/unlock.fxml");
}
private void vaultChanged(Vault newVault) {
private void vaultDidChange(Vault newVault) {
if (newVault == null) {
return;
}
@@ -149,6 +149,9 @@ public class UnlockController extends LocalizedFXMLViewController {
if (SystemUtils.IS_OS_WINDOWS) {
chooseSelectedDriveLetter();
}
// trigger "default" change to refresh key bindings:
unlockButton.setDefaultButton(false);
unlockButton.setDefaultButton(true);
}
// ****************************************

View File

@@ -7,8 +7,10 @@ import java.util.concurrent.ExecutorService;
import javax.inject.Inject;
import org.cryptomator.ui.model.UpgradeInstruction;
import org.cryptomator.ui.model.UpgradeInstruction.UpgradeFailedException;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.UpgradeStrategies;
import org.cryptomator.ui.model.UpgradeStrategy;
import org.cryptomator.ui.model.UpgradeStrategy.UpgradeFailedException;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.settings.Localization;
import org.fxmisc.easybind.EasyBind;
@@ -16,7 +18,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Platform;
import javafx.beans.binding.Binding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
@@ -30,19 +31,24 @@ public class UpgradeController extends LocalizedFXMLViewController {
private static final Logger LOG = LoggerFactory.getLogger(UpgradeController.class);
final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
final ObjectProperty<Optional<UpgradeStrategy>> strategy = new SimpleObjectProperty<>();
private final UpgradeStrategies strategies;
private final ExecutorService exec;
private final Binding<Optional<UpgradeInstruction>> upgradeInstruction = EasyBind.monadic(vault).map(Vault::availableUpgrade);
private Optional<UpgradeListener> listener = Optional.empty();
@Inject
public UpgradeController(Localization localization, ExecutorService exec) {
public UpgradeController(Localization localization, UpgradeStrategies strategies, ExecutorService exec) {
super(localization);
this.strategies = strategies;
this.exec = exec;
}
@FXML
private Label upgradeLabel;
@FXML
private SecPasswordField passwordField;
@FXML
private Button upgradeButton;
@@ -54,11 +60,13 @@ public class UpgradeController extends LocalizedFXMLViewController {
@Override
protected void initialize() {
upgradeLabel.textProperty().bind(EasyBind.monadic(upgradeInstruction).map(instruction -> {
upgradeLabel.textProperty().bind(EasyBind.monadic(strategy).map(instruction -> {
return instruction.map(this::upgradeNotification).orElse("");
}).orElse(""));
EasyBind.subscribe(vault, this::vaultChanged);
upgradeButton.disableProperty().bind(passwordField.textProperty().isEmpty().or(passwordField.disabledProperty()));
EasyBind.subscribe(vault, this::vaultDidChange);
}
@Override
@@ -66,16 +74,20 @@ public class UpgradeController extends LocalizedFXMLViewController {
return getClass().getResource("/fxml/upgrade.fxml");
}
private void vaultChanged(Vault newVault) {
private void vaultDidChange(Vault newVault) {
errorLabel.setText(null);
strategy.set(strategies.getUpgradeStrategy(newVault));
// trigger "default" change to refresh key bindings:
upgradeButton.setDefaultButton(false);
upgradeButton.setDefaultButton(true);
}
// ****************************************
// Upgrade label
// ****************************************
private String upgradeNotification(UpgradeInstruction instruction) {
return instruction.getNotification(vault.get(), localization);
private String upgradeNotification(UpgradeStrategy instruction) {
return instruction.getNotification(vault.get());
}
// ****************************************
@@ -84,36 +96,45 @@ public class UpgradeController extends LocalizedFXMLViewController {
@FXML
private void didClickUpgradeButton(ActionEvent event) {
upgradeInstruction.getValue().ifPresent(this::upgrade);
strategy.getValue().ifPresent(this::upgrade);
}
private void upgrade(UpgradeInstruction instruction) {
Vault v = vault.getValue();
Objects.requireNonNull(v);
private void upgrade(UpgradeStrategy instruction) {
Vault v = Objects.requireNonNull(vault.getValue());
passwordField.setDisable(true);
progressIndicator.setVisible(true);
upgradeButton.setDisable(true);
exec.submit(() -> {
if (!instruction.isApplicable(v)) {
LOG.error("No upgrade needed for " + v.path().getValue());
throw new IllegalStateException("No ugprade needed for " + v.path().getValue());
}
try {
instruction.upgrade(v, localization);
Platform.runLater(() -> {
progressIndicator.setVisible(false);
upgradeButton.setDisable(false);
listener.ifPresent(UpgradeListener::didUpgrade);
});
instruction.upgrade(v, passwordField.getCharacters());
Platform.runLater(this::showNextUpgrade);
} catch (UpgradeFailedException e) {
Platform.runLater(() -> {
errorLabel.setText(e.getLocalizedMessage());
});
} finally {
Platform.runLater(() -> {
progressIndicator.setVisible(false);
upgradeButton.setDisable(false);
passwordField.setDisable(false);
passwordField.swipe();
});
}
});
}
private void showNextUpgrade() {
errorLabel.setText(null);
Optional<UpgradeStrategy> nextStrategy = strategies.getUpgradeStrategy(vault.getValue());
if (nextStrategy.isPresent()) {
strategy.set(nextStrategy);
} else {
listener.ifPresent(UpgradeListener::didUpgrade);
}
}
/* callback */
public void setListener(UpgradeListener listener) {

View File

@@ -154,6 +154,7 @@ public class WelcomeController extends LocalizedFXMLViewController {
Platform.runLater(() -> {
this.updateLink.setText(msg);
this.updateLink.setVisible(true);
this.updateLink.setDisable(false);
});
}
}

View File

@@ -56,13 +56,8 @@ public class DirectoryListCell extends DraggableListCell<Vault> {
pathText.setTextOverrun(OverrunStyle.ELLIPSIS);
pathText.getStyleClass().add("detail-label");
statusIndicator.fillProperty().bind(EasyBind.monadic(itemProperty()).flatMap(Vault::unlockedProperty).map(unlocked -> {
return unlocked ? GREEN_FILL : RED_FILL;
}));
statusIndicator.strokeProperty().bind(EasyBind.monadic(itemProperty()).flatMap(Vault::unlockedProperty).map(unlocked -> {
return unlocked ? GREEN_STROKE : RED_STROKE;
}));
statusIndicator.fillProperty().bind(EasyBind.monadic(itemProperty()).flatMap(Vault::unlockedProperty).filter(Boolean.TRUE::equals).map(unlocked -> GREEN_FILL).orElse(RED_FILL));
statusIndicator.strokeProperty().bind(EasyBind.monadic(itemProperty()).flatMap(Vault::unlockedProperty).filter(Boolean.TRUE::equals).map(unlocked -> GREEN_STROKE).orElse(RED_STROKE));
tooltipProperty().bind(EasyBind.monadic(itemProperty()).flatMap(Vault::path).map(p -> new Tooltip(p.toString())));
contextMenuProperty().bind(EasyBind.monadic(itemProperty()).flatMap(Vault::unlockedProperty).map(unlocked -> {

View File

@@ -1,40 +0,0 @@
package org.cryptomator.ui.model;
import org.cryptomator.ui.settings.Localization;
public interface UpgradeInstruction {
static UpgradeInstruction[] AVAILABLE_INSTRUCTIONS = {new UpgradeVersion3DropBundleExtension()};
/**
* @return Localized string to display to the user when an upgrade is needed.
*/
String getNotification(Vault vault, Localization localization);
/**
* Upgrades a vault. Might take a moment, should be run in a background thread.
*/
void upgrade(Vault vault, Localization localization) throws UpgradeFailedException;
/**
* Determines in O(1), if an upgrade can be applied to a vault.
*
* @return <code>true</code> if and only if the vault can be migrated to a newer version without the risk of data losses.
*/
boolean isApplicable(Vault vault);
/**
* Thrown when data migration failed.
*/
public class UpgradeFailedException extends Exception {
UpgradeFailedException() {
}
UpgradeFailedException(String message) {
super(message);
}
}
}

View File

@@ -0,0 +1,30 @@
package org.cryptomator.ui.model;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class UpgradeStrategies {
private final Collection<UpgradeStrategy> strategies;
@Inject
public UpgradeStrategies(UpgradeVersion3DropBundleExtension upgrader1, UpgradeVersion3to4 upgrader2) {
strategies = Collections.unmodifiableList(Arrays.asList(upgrader1, upgrader2));
}
public Optional<UpgradeStrategy> getUpgradeStrategy(Vault vault) {
if (vault == null) {
return Optional.empty();
}
return strategies.stream().filter(strategy -> {
return strategy.isApplicable(vault);
}).findFirst();
}
}

View File

@@ -0,0 +1,87 @@
package org.cryptomator.ui.model;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import javax.inject.Provider;
import org.cryptomator.crypto.engine.Cryptor;
import org.cryptomator.crypto.engine.InvalidPassphraseException;
import org.cryptomator.crypto.engine.UnsupportedVaultFormatException;
import org.cryptomator.filesystem.crypto.Constants;
import org.cryptomator.ui.settings.Localization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class UpgradeStrategy {
private static final Logger LOG = LoggerFactory.getLogger(UpgradeStrategy.class);
protected final Provider<Cryptor> cryptorProvider;
protected final Localization localization;
UpgradeStrategy(Provider<Cryptor> cryptorProvider, Localization localization) {
this.cryptorProvider = cryptorProvider;
this.localization = localization;
}
/**
* @return Localized string to display to the user when an upgrade is needed.
*/
public abstract String getNotification(Vault vault);
/**
* Upgrades a vault. Might take a moment, should be run in a background thread.
*/
public void upgrade(Vault vault, CharSequence passphrase) throws UpgradeFailedException {
final Cryptor cryptor = cryptorProvider.get();
try {
final Path masterkeyFile = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME);
final byte[] masterkeyFileContents = Files.readAllBytes(masterkeyFile);
cryptor.readKeysFromMasterkeyFile(masterkeyFileContents, passphrase);
// create backup, as soon as we know the password was correct:
final Path masterkeyBackupFile = vault.path().getValue().resolve(Constants.MASTERKEY_BACKUP_FILENAME);
Files.copy(masterkeyFile, masterkeyBackupFile, StandardCopyOption.REPLACE_EXISTING);
// do stuff:
upgrade(vault, cryptor);
// write updated masterkey file:
final byte[] upgradedMasterkeyFileContents = cryptor.writeKeysToMasterkeyFile(passphrase);
final Path masterkeyFileAfterUpgrading = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME); // path may have changed
Files.write(masterkeyFileAfterUpgrading, upgradedMasterkeyFileContents, StandardOpenOption.TRUNCATE_EXISTING);
} catch (InvalidPassphraseException e) {
throw new UpgradeFailedException(localization.getString("unlock.errorMessage.wrongPassword"));
} catch (IOException | UnsupportedVaultFormatException e) {
LOG.warn("Upgrade failed.", e);
throw new UpgradeFailedException("Upgrade failed. Details in log message.");
} finally {
cryptor.destroy();
}
}
protected abstract void upgrade(Vault vault, Cryptor cryptor) throws UpgradeFailedException;
/**
* Determines in O(1), if an upgrade can be applied to a vault.
*
* @return <code>true</code> if and only if the vault can be migrated to a newer version without the risk of data losses.
*/
public abstract boolean isApplicable(Vault vault);
/**
* Thrown when data migration failed.
*/
public static class UpgradeFailedException extends Exception {
UpgradeFailedException() {
}
UpgradeFailedException(String message) {
super(message);
}
}
}

View File

@@ -1,22 +1,40 @@
package org.cryptomator.ui.model;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.engine.Cryptor;
import org.cryptomator.crypto.engine.InvalidPassphraseException;
import org.cryptomator.crypto.engine.UnsupportedVaultFormatException;
import org.cryptomator.filesystem.crypto.Constants;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.settings.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Platform;
class UpgradeVersion3DropBundleExtension implements UpgradeInstruction {
@Singleton
class UpgradeVersion3DropBundleExtension extends UpgradeStrategy {
private static final Logger LOG = LoggerFactory.getLogger(UpgradeVersion3DropBundleExtension.class);
private final Settings settings;
@Inject
public UpgradeVersion3DropBundleExtension(Provider<Cryptor> cryptorProvider, Localization localization, Settings settings) {
super(cryptorProvider, localization);
this.settings = settings;
}
@Override
public String getNotification(Vault vault, Localization localization) {
public String getNotification(Vault vault) {
String fmt = localization.getString("upgrade.version3dropBundleExtension.msg");
Path path = vault.path().getValue();
String oldVaultName = path.getFileName().toString();
@@ -25,7 +43,26 @@ class UpgradeVersion3DropBundleExtension implements UpgradeInstruction {
}
@Override
public void upgrade(Vault vault, Localization localization) throws UpgradeFailedException {
public void upgrade(Vault vault, CharSequence passphrase) throws UpgradeFailedException {
final Cryptor cryptor = cryptorProvider.get();
try {
final Path masterkeyFile = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME);
final byte[] masterkeyFileContents = Files.readAllBytes(masterkeyFile);
cryptor.readKeysFromMasterkeyFile(masterkeyFileContents, passphrase);
upgrade(vault, cryptor);
// don't write new masterkey. this is a special case, as we were stupid and didn't increase the vault version with this upgrade...
} catch (InvalidPassphraseException e) {
throw new UpgradeFailedException(localization.getString("unlock.errorMessage.wrongPassword"));
} catch (IOException | UnsupportedVaultFormatException e) {
LOG.warn("Upgrade failed.", e);
throw new UpgradeFailedException("Upgrade failed. Details in log message.");
} finally {
cryptor.destroy();
}
}
@Override
protected void upgrade(Vault vault, Cryptor cryptor) throws UpgradeFailedException {
Path path = vault.path().getValue();
String oldVaultName = path.getFileName().toString();
String newVaultName = StringUtils.removeEnd(oldVaultName, Vault.VAULT_FILE_EXTENSION);
@@ -39,6 +76,7 @@ class UpgradeVersion3DropBundleExtension implements UpgradeInstruction {
Files.move(path, path.resolveSibling(newVaultName));
Platform.runLater(() -> {
vault.setPath(newPath);
settings.save();
});
} catch (IOException e) {
LOG.error("Vault migration failed", e);
@@ -49,7 +87,24 @@ class UpgradeVersion3DropBundleExtension implements UpgradeInstruction {
@Override
public boolean isApplicable(Vault vault) {
return vault.path().getValue().getFileName().toString().endsWith(Vault.VAULT_FILE_EXTENSION);
Path vaultPath = vault.path().getValue();
if (vaultPath.toString().endsWith(Vault.VAULT_FILE_EXTENSION)) {
final Path masterkeyFile = vaultPath.resolve(Constants.MASTERKEY_FILENAME);
try {
if (Files.isRegularFile(masterkeyFile)) {
final String keyContents = new String(Files.readAllBytes(masterkeyFile), StandardCharsets.UTF_8);
return keyContents.contains("\"version\":3") || keyContents.contains("\"version\": 3");
} else {
LOG.warn("Not a file: {}", masterkeyFile);
return false;
}
} catch (IOException e) {
LOG.warn("Could not determine, whether upgrade is applicable.", e);
return false;
}
} else {
return false;
}
}
}

View File

@@ -0,0 +1,116 @@
package org.cryptomator.ui.model;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.cryptomator.crypto.engine.Cryptor;
import org.cryptomator.filesystem.crypto.Constants;
import org.cryptomator.ui.settings.Localization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
class UpgradeVersion3to4 extends UpgradeStrategy {
private static final Logger LOG = LoggerFactory.getLogger(UpgradeVersion3to4.class);
private static final Pattern BASE32_FOLLOWED_BY_UNDERSCORE_PATTERN = Pattern.compile("^(([A-Z2-7]{8})*[A-Z2-7=]{8})_");
private static final int FILE_MIN_SIZE = 88; // vault version 3 files have a header of 88 bytes (assuming no chunks at all)
@Inject
public UpgradeVersion3to4(Provider<Cryptor> cryptorProvider, Localization localization) {
super(cryptorProvider, localization);
}
@Override
public String getNotification(Vault vault) {
return localization.getString("upgrade.version3to4.msg");
}
@Override
protected void upgrade(Vault vault, Cryptor cryptor) throws UpgradeFailedException {
Path dataDir = vault.path().get().resolve("d");
if (!Files.isDirectory(dataDir)) {
return; // empty vault. no migration needed.
}
try {
Files.walkFileTree(dataDir, new FileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
migrate(file, attrs);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
throw exc;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
LOG.error("Migration failed.", e);
throw new UpgradeFailedException(localization.getString("upgrade.version3to4.err.io"));
}
LOG.info("Migration finished.");
}
private void migrate(Path file, BasicFileAttributes attrs) throws IOException {
String name = file.getFileName().toString();
long size = attrs.size();
Matcher m = BASE32_FOLLOWED_BY_UNDERSCORE_PATTERN.matcher(name);
if (m.find(0) && size < FILE_MIN_SIZE) {
String base32 = m.group(1);
String suffix = name.substring(m.end());
String renamed = "0" + base32 + (suffix.isEmpty() ? "" : " " + suffix);
renameWithoutOverwriting(file, renamed);
}
}
private void renameWithoutOverwriting(Path path, String newName) throws IOException {
Path newPath = path.resolveSibling(newName);
for (int i = 2; Files.exists(newPath); i++) {
newPath = path.resolveSibling(newName + " " + i);
}
Files.move(path, newPath);
LOG.info("Renaming {} to {}", path, newPath.getFileName());
}
@Override
public boolean isApplicable(Vault vault) {
final Path masterkeyFile = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME);
try {
if (Files.isRegularFile(masterkeyFile)) {
final String keyContents = new String(Files.readAllBytes(masterkeyFile), StandardCharsets.UTF_8);
return keyContents.contains("\"version\":3") || keyContents.contains("\"version\": 3");
} else {
LOG.warn("Not a file: {}", masterkeyFile);
return false;
}
} catch (IOException e) {
LOG.warn("Could not determine, whether upgrade is applicable.", e);
return false;
}
}
}

View File

@@ -16,7 +16,6 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.Normalizer;
import java.text.Normalizer.Form;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
@@ -30,6 +29,7 @@ import org.cryptomator.common.LazyInitializer;
import org.cryptomator.common.Optionals;
import org.cryptomator.crypto.engine.InvalidPassphraseException;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.charsets.NormalizedNameFileSystem;
import org.cryptomator.filesystem.crypto.CryptoFileSystemDelegate;
import org.cryptomator.filesystem.crypto.CryptoFileSystemFactory;
import org.cryptomator.filesystem.nio.NioFileSystem;
@@ -63,9 +63,9 @@ public class Vault implements CryptoFileSystemDelegate {
public static final String VAULT_FILE_EXTENSION = ".cryptomator";
private final ObjectProperty<Path> path;
private final DeferredCloser closer;
private final ShorteningFileSystemFactory shorteningFileSystemFactory;
private final CryptoFileSystemFactory cryptoFileSystemFactory;
private final DeferredCloser closer;
private final BooleanProperty unlocked = new SimpleBooleanProperty();
private final ObservableList<String> namesOfResourcesWithInvalidMac = FXThreads.observableListOnMainThread(FXCollections.observableArrayList());
private final Set<String> whitelistedResourcesWithInvalidMac = new HashSet<>();
@@ -81,9 +81,9 @@ public class Vault implements CryptoFileSystemDelegate {
*/
Vault(Path vaultDirectoryPath, ShorteningFileSystemFactory shorteningFileSystemFactory, CryptoFileSystemFactory cryptoFileSystemFactory, DeferredCloser closer) {
this.path = new SimpleObjectProperty<Path>(vaultDirectoryPath);
this.closer = closer;
this.shorteningFileSystemFactory = shorteningFileSystemFactory;
this.cryptoFileSystemFactory = cryptoFileSystemFactory;
this.closer = closer;
try {
setMountName(name().getValue());
@@ -126,7 +126,8 @@ public class Vault implements CryptoFileSystemDelegate {
FileSystem fs = getNioFileSystem();
FileSystem shorteningFs = shorteningFileSystemFactory.get(fs);
FileSystem cryptoFs = cryptoFileSystemFactory.unlockExisting(shorteningFs, passphrase, this);
StatsFileSystem statsFs = new StatsFileSystem(cryptoFs);
FileSystem normalizingFs = new NormalizedNameFileSystem(cryptoFs, SystemUtils.IS_OS_MAC_OSX ? Form.NFD : Form.NFC);
StatsFileSystem statsFs = new StatsFileSystem(normalizingFs);
statsFileSystem = Optional.of(statsFs);
String contextPath = StringUtils.prependIfMissing(mountName, "/");
Frontend frontend = frontendFactory.create(statsFs, contextPath);
@@ -165,16 +166,6 @@ public class Vault implements CryptoFileSystemDelegate {
Optionals.ifPresent(filesystemFrontend.get(), Frontend::unmount);
}
public boolean needsUpgrade() {
return availableUpgrade().isPresent();
}
public Optional<UpgradeInstruction> availableUpgrade() {
return Arrays.stream(UpgradeInstruction.AVAILABLE_INSTRUCTIONS).filter(instruction -> {
return instruction.isApplicable(this);
}).findAny();
}
// ******************************************************************************
// Delegate methods
// ********************************************************************************/
@@ -214,7 +205,7 @@ public class Vault implements CryptoFileSystemDelegate {
String homePrefix = SystemUtils.IS_OS_WINDOWS ? "~\\" : "~/";
return homePrefix + relativePath.toString();
} else {
return path.toString();
return path.getValue().toString();
}
});
}

View File

@@ -1,27 +1,70 @@
package org.cryptomator.ui.settings;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Objects;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.collections4.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class Localization extends ResourceBundle {
private static final Logger LOG = LoggerFactory.getLogger(Localization.class);
private static final String LOCALIZATION_DEFAULT_FILE = "/localization/en.txt";
private static final String LOCALIZATION_FILENAME_FMT = "/localization/%s.txt";
private static final String LOCALIZATION_FILE = String.format(LOCALIZATION_FILENAME_FMT, Locale.getDefault().getLanguage());
private final ResourceBundle fallback;
private final ResourceBundle localized;
@Inject
public Localization() {
this.parent = ResourceBundle.getBundle("localization");
try (InputStream in = getClass().getResourceAsStream(LOCALIZATION_DEFAULT_FILE)) {
Objects.requireNonNull(in);
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
this.fallback = new PropertyResourceBundle(reader);
LOG.info("Loaded localization from {}", LOCALIZATION_FILE);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
try (InputStream in = getClass().getResourceAsStream(LOCALIZATION_FILE)) {
if (in != null) {
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
this.localized = new PropertyResourceBundle(reader);
} else {
this.localized = this.fallback;
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
protected Object handleGetObject(String key) {
return parent.getObject(key);
return localized.containsKey(key) ? localized.getObject(key) : fallback.getObject(key);
}
@Override
public Enumeration<String> getKeys() {
return parent.getKeys();
Collection<String> keys = CollectionUtils.union(localized.keySet(), fallback.keySet());
return Collections.enumeration(keys);
}
}

View File

@@ -11,6 +11,7 @@ package org.cryptomator.ui.settings;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import org.cryptomator.ui.model.Vault;
@@ -23,10 +24,12 @@ public class Settings implements Serializable {
private static final long serialVersionUID = 7609959894417878744L;
public static final int MIN_PORT = 1024;
public static final int MAX_PORT = 65535;
public static final int DEFAULT_PORT = 0;
public static final int DEFAULT_PORT = 42427;
public static final boolean DEFAULT_USE_IPV6 = false;
public static final Integer DEFAULT_NUM_TRAY_NOTIFICATIONS = 3;
private final Consumer<Settings> saveCmd;
@JsonProperty("directories")
private List<Vault> directories;
@@ -35,7 +38,7 @@ public class Settings implements Serializable {
@JsonProperty("port")
private Integer port;
@JsonProperty("useIpv6")
private Boolean useIpv6;
@@ -45,8 +48,12 @@ public class Settings implements Serializable {
/**
* Package-private constructor; use {@link SettingsProvider}.
*/
Settings() {
Settings(Consumer<Settings> saveCmd) {
this.saveCmd = saveCmd;
}
public void save() {
saveCmd.accept(this);
}
/* Getter/Setter */
@@ -86,8 +93,8 @@ public class Settings implements Serializable {
}
}
private boolean isPortValid(int port) {
return port == DEFAULT_PORT || port >= MIN_PORT && port <= MAX_PORT;
public boolean isPortValid(int port) {
return port == DEFAULT_PORT || port >= MIN_PORT && port <= MAX_PORT || port == 0;
}
public boolean shouldUseIpv6() {

View File

@@ -16,6 +16,12 @@ import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.inject.Inject;
import javax.inject.Named;
@@ -23,7 +29,6 @@ import javax.inject.Provider;
import javax.inject.Singleton;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.DeferredCloser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -32,9 +37,10 @@ import com.fasterxml.jackson.databind.ObjectMapper;
@Singleton
public class SettingsProvider implements Provider<Settings> {
private static final Logger LOG = LoggerFactory.getLogger(Settings.class);
private static final Logger LOG = LoggerFactory.getLogger(SettingsProvider.class);
private static final Path SETTINGS_DIR;
private static final String SETTINGS_FILE = "settings.json";
private static final long SAVE_DELAY_MS = 1000;
static {
final String appdata = System.getenv("APPDATA");
@@ -52,12 +58,12 @@ public class SettingsProvider implements Provider<Settings> {
}
}
private final DeferredCloser deferredCloser;
private final ObjectMapper objectMapper;
private final ScheduledExecutorService saveScheduler = Executors.newSingleThreadScheduledExecutor();
private final AtomicReference<ScheduledFuture<?>> scheduledSaveCmd = new AtomicReference<>();
@Inject
public SettingsProvider(DeferredCloser deferredCloser, @Named("VaultJsonMapper") ObjectMapper objectMapper) {
this.deferredCloser = deferredCloser;
public SettingsProvider(@Named("VaultJsonMapper") ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@@ -72,28 +78,39 @@ public class SettingsProvider implements Provider<Settings> {
@Override
public Settings get() {
Settings settings = null;
final Settings settings = new Settings(this::scheduleSave);
try {
final Path settingsPath = getSettingsPath();
final InputStream in = Files.newInputStream(settingsPath, StandardOpenOption.READ);
settings = objectMapper.readValue(in, Settings.class);
objectMapper.readerForUpdating(settings).readValue(in);
LOG.info("Settings loaded from " + settingsPath);
} catch (IOException e) {
LOG.warn("Failed to load settings, creating new one.");
settings = new Settings();
LOG.info("Failed to load settings, creating new one.");
}
deferredCloser.closeLater(settings, this::save);
return settings;
}
private void save(Settings settings) {
private void scheduleSave(Settings settings) {
if (settings == null) {
return;
}
ScheduledFuture<?> saveCmd = saveScheduler.schedule(() -> {
this.save(settings);
} , SAVE_DELAY_MS, TimeUnit.MILLISECONDS);
ScheduledFuture<?> previousSaveCmd = scheduledSaveCmd.getAndSet(saveCmd);
if (previousSaveCmd != null) {
previousSaveCmd.cancel(false);
}
}
private void save(Settings settings) {
Objects.requireNonNull(settings);
try {
final Path settingsPath = getSettingsPath();
Files.createDirectories(settingsPath.getParent());
final OutputStream out = Files.newOutputStream(settingsPath, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
objectMapper.writeValue(out, settings);
LOG.info("Settings saved to " + settingsPath);
} catch (IOException e) {
LOG.error("Failed to save settings.", e);
}

View File

@@ -0,0 +1,53 @@
/*******************************************************************************
* 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:
* Jean-Noël Charon - initial API and implementation
*******************************************************************************/
package org.cryptomator.ui.util;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.text.Text;
public class DialogBuilderUtil {
public DialogBuilderUtil() {
}
public static Alert buildInformationDialog(String title, String header, String content, ButtonType defaultButton) {
return buildDialog(title, header, content, Alert.AlertType.INFORMATION, defaultButton);
}
public static Alert buildWarningDialog(String title, String header, String content, ButtonType defaultButton) {
return buildDialog(title, header, content, Alert.AlertType.WARNING, defaultButton);
}
public static Alert buildErrorDialog(String title, String header, String content, ButtonType defaultButton) {
return buildDialog(title, header, content, Alert.AlertType.ERROR, defaultButton);
}
public static Alert buildConfirmationDialog(String title, String header, String content, ButtonType defaultButton) {
return buildDialog(title, header, content, Alert.AlertType.CONFIRMATION, defaultButton);
}
private static Alert buildDialog(String title, String header, String content, Alert.AlertType type, ButtonType defaultButton) {
Text contentText = new Text(content);
contentText.setWrappingWidth(360.0);
Alert alert = new Alert(type);
alert.setTitle(title);
alert.setHeaderText(header);
alert.getDialogPane().setContent(contentText);
alert.getDialogPane().getButtonTypes().stream().forEach(buttonType -> {
Button btn = (Button) alert.getDialogPane().lookupButton(buttonType);
btn.setDefaultButton(buttonType.equals(defaultButton));
});
return alert;
}
}

View File

@@ -0,0 +1,87 @@
/*******************************************************************************
* 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:
* Jean-Noël Charon - initial API and implementation
*******************************************************************************/
package org.cryptomator.ui.util;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.ui.settings.Localization;
import com.nulabinc.zxcvbn.Zxcvbn;
import javafx.geometry.Insets;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.paint.Color;
@Singleton
public class PasswordStrengthUtil {
private final Zxcvbn zxcvbn;
private final List<String> sanitizedInputs;
private final Localization localization;
@Inject
public PasswordStrengthUtil(Localization localization) {
this.localization = localization;
this.zxcvbn = new Zxcvbn();
this.sanitizedInputs = new ArrayList<>();
this.sanitizedInputs.add("cryptomator");
}
public int computeRate(String password) {
if (StringUtils.isEmpty(password)) {
return -1;
} else {
return zxcvbn.measure(password, sanitizedInputs).getScore();
}
}
public Color getStrengthColor(Number score) {
switch (score.intValue()) {
case 0:
return Color.web("#e74c3c");
case 1:
return Color.web("#e67e22");
case 2:
return Color.web("#f1c40f");
case 3:
return Color.web("#40d47e");
case 4:
return Color.web("#27ae60");
default:
return Color.web("#ffffff", 0.5);
}
}
public Background getBackgroundWithStrengthColor(Number score) {
Color c = this.getStrengthColor(score);
BackgroundFill fill = new BackgroundFill(c, CornerRadii.EMPTY, Insets.EMPTY);
return new Background(fill);
}
public Background getBackgroundWithStrengthColor(Number score, Number threshold) {
return score.intValue() >= threshold.intValue() ? getBackgroundWithStrengthColor(score) : getBackgroundWithStrengthColor(-1);
}
public String getStrengthDescription(Number score) {
String key = "initialize.messageLabel.passwordStrength." + score.intValue();
if (localization.containsKey(key)) {
return localization.getString(key);
} else {
return "";
}
}
}

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