mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-23 13:11:28 +00:00
Compare commits
155 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cef3a5fc77 | ||
|
|
9956f43fd9 | ||
|
|
2b84593bde | ||
|
|
4e728fd387 | ||
|
|
438ade1106 | ||
|
|
fe54f4ec66 | ||
|
|
fe86b4c593 | ||
|
|
a583afeb60 | ||
|
|
a585d3cf16 | ||
|
|
3db757193e | ||
|
|
bac1d6fd83 | ||
|
|
39ee8a9cde | ||
|
|
1263b3af81 | ||
|
|
dafa29d8a3 | ||
|
|
2bc6fe89ad | ||
|
|
8439216233 | ||
|
|
aab616d184 | ||
|
|
70c3a38c49 | ||
|
|
c64294ac3e | ||
|
|
82330db871 | ||
|
|
c54a721f9a | ||
|
|
355bbb5459 | ||
|
|
63daa0f121 | ||
|
|
50885d5c7c | ||
|
|
4d68818ec5 | ||
|
|
6fb20dd509 | ||
|
|
2bb87dfa96 | ||
|
|
3e374a927c | ||
|
|
84ac6d88f5 | ||
|
|
72f6ee6477 | ||
|
|
a3cfcb1131 | ||
|
|
d7d8d21ba4 | ||
|
|
ef0425e2b1 | ||
|
|
df1fd6d0b3 | ||
|
|
2fa04d7b7c | ||
|
|
a15acd64c8 | ||
|
|
5b18eff01a | ||
|
|
47133c6f31 | ||
|
|
09ba4f5129 | ||
|
|
20d4047bed | ||
|
|
56b71ef7d9 | ||
|
|
091e62057d | ||
|
|
824bd9ea64 | ||
|
|
697a791593 | ||
|
|
7462a887b3 | ||
|
|
3535e83d7d | ||
|
|
cf0b4accb3 | ||
|
|
a63bcfbaa2 | ||
|
|
5c4bf2a207 | ||
|
|
c1611a12ed | ||
|
|
0983120712 | ||
|
|
ce12af8495 | ||
|
|
dc117c8415 | ||
|
|
06e526a961 | ||
|
|
2e343a951f | ||
|
|
141ffcf656 | ||
|
|
d61e5c5a08 | ||
|
|
6a15fa132a | ||
|
|
902b29ee0a | ||
|
|
995bba616f | ||
|
|
f39b7b047f | ||
|
|
72e52df4e0 | ||
|
|
8018e9485e | ||
|
|
e0ae50378f | ||
|
|
a9c2b0fc57 | ||
|
|
dc58ba434a | ||
|
|
34af306309 | ||
|
|
21d70b5ae4 | ||
|
|
e90880ac9a | ||
|
|
66faa13f40 | ||
|
|
8a4a29b4d1 | ||
|
|
8c8db84a4a | ||
|
|
a499a3c80b | ||
|
|
6a3ccf2b48 | ||
|
|
fcfcffe9cb | ||
|
|
363ed4ac4b | ||
|
|
1f73a08e09 | ||
|
|
fe0a34907f | ||
|
|
461b11700f | ||
|
|
24bfbb59a4 | ||
|
|
4476558e9c | ||
|
|
560171832c | ||
|
|
6e93d40e51 | ||
|
|
79b819bca6 | ||
|
|
a18c406cf0 | ||
|
|
6730a83cac | ||
|
|
3b3ebd2196 | ||
|
|
505b6542c7 | ||
|
|
31368f0cba | ||
|
|
5b5dd756b1 | ||
|
|
f6ebbb23d1 | ||
|
|
3f0373b08f | ||
|
|
4c3c60060d | ||
|
|
28f275c22d | ||
|
|
24df3c3809 | ||
|
|
034a667e07 | ||
|
|
008e3e3b05 | ||
|
|
94a5bf7596 | ||
|
|
e8db836eff | ||
|
|
429b26f3d8 | ||
|
|
3ae8327300 | ||
|
|
df7e9a0af1 | ||
|
|
93d3eca0ab | ||
|
|
7753d1f0e7 | ||
|
|
d7c6c24932 | ||
|
|
1a75f23081 | ||
|
|
f071efe1b9 | ||
|
|
a8ad335aed | ||
|
|
7022a80c95 | ||
|
|
9a2f602d6c | ||
|
|
c78a4aa241 | ||
|
|
975ce4d973 | ||
|
|
1e6ff0d969 | ||
|
|
69e133d561 | ||
|
|
20e55eddf8 | ||
|
|
0fdcdc816a | ||
|
|
b7506d97a9 | ||
|
|
4ad7481dc7 | ||
|
|
bc815405d2 | ||
|
|
9c06e762c3 | ||
|
|
1ac87dd32f | ||
|
|
e0ce7ce2ec | ||
|
|
3d951a9d7b | ||
|
|
cec3d984b0 | ||
|
|
392e474cfa | ||
|
|
41fb0d51a4 | ||
|
|
aa9fef2967 | ||
|
|
adc9c02564 | ||
|
|
ace64117a2 | ||
|
|
fb4db2506b | ||
|
|
1076d971ae | ||
|
|
eed1b1cff0 | ||
|
|
f5cb82e21e | ||
|
|
67661f114b | ||
|
|
8a3e09764a | ||
|
|
eb3cfd6e6a | ||
|
|
4d1727d0e9 | ||
|
|
a51d853d1c | ||
|
|
d0039466f7 | ||
|
|
5c959989a2 | ||
|
|
6283d2df3d | ||
|
|
a9e0dfdaf8 | ||
|
|
45ca7e9e47 | ||
|
|
034b5c2718 | ||
|
|
e188649c79 | ||
|
|
1468c6ec90 | ||
|
|
07ba4eb537 | ||
|
|
414bbef1a7 | ||
|
|
e2b94ff6ef | ||
|
|
41f8a9faca | ||
|
|
1d9252e974 | ||
|
|
80780eef3c | ||
|
|
87ff33956b | ||
|
|
1804b98f05 | ||
|
|
847c6813cc |
60
.travis.yml
60
.travis.yml
@@ -1,23 +1,30 @@
|
|||||||
sudo: required
|
|
||||||
|
|
||||||
dist: trusty
|
|
||||||
|
|
||||||
language: java
|
language: java
|
||||||
|
sudo: required
|
||||||
|
dist: trusty
|
||||||
jdk:
|
jdk:
|
||||||
- oraclejdk8
|
- oraclejdk8
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- $HOME/.m2
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
- secure: "Lgj042RD0X3rB8VZVZLWP1GetLhjd3PqI5JbJMlzgHJpDI6RkFIBLN9SWAGmkLPCehIp2zA5tu9+UVy0NNMxm9xz6SyjMCaxS28/fnYEXaNmwwDSF6O6gLUbdxyzoYIFPYOPmFxpzhebqnNIsxaM29oZpgRgUGqosCczQxiB+Ng=" #coveralls
|
|
||||||
- secure: "IfYURwZaDWuBDvyn47n0k1Zod/IQw1FF+CS5nnV08Q+NfC3vGGJMwV8m59XnbfwnWGxwvCaAbk4qP6s6+ijgZNKkvgfFMo3rfTok5zt43bIqgaFOANYV+OC/1c59gYD6ZUxhW5iNgMgU3qdsRtJuwSmfkVv/jKyLGfAbS4kN8BA=" #coverity
|
- secure: "IfYURwZaDWuBDvyn47n0k1Zod/IQw1FF+CS5nnV08Q+NfC3vGGJMwV8m59XnbfwnWGxwvCaAbk4qP6s6+ijgZNKkvgfFMo3rfTok5zt43bIqgaFOANYV+OC/1c59gYD6ZUxhW5iNgMgU3qdsRtJuwSmfkVv/jKyLGfAbS4kN8BA=" #coverity
|
||||||
|
- secure: "lV9OwUbHMrMpLUH1CY+Z4puLDdFXytudyPlG1eGRsesdpuG6KM3uQVz6uAtf6lrU8DRbMM/T7ML+PmvQ4UoPPYLdLxESLLBat2qUPOIVBOhTSlCc7I0DmGy04CSvkeMy8dPaQC0ukgNiR7zwoNzfcpGRN/U9S8tziDruuHoZSrg=" #bintray
|
||||||
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"
|
addons:
|
||||||
|
coverity_scan:
|
||||||
script: mvn -fmain/pom.xml clean test
|
project:
|
||||||
|
name: "cryptomator/cryptomator"
|
||||||
after_success: mvn -fmain/pom.xml -Ptest-coverage clean test jacoco:report-aggregate coveralls:report
|
notification_email: sebastian.stenzel@cryptomator.org
|
||||||
|
build_command: "mvn -fmain/pom.xml clean test -DskipTests"
|
||||||
|
branch_pattern: release.*
|
||||||
|
install:
|
||||||
|
# "clean" needed until https://bugs.openjdk.java.net/browse/JDK-8067747 is resolved.
|
||||||
|
- mvn -fmain/pom.xml clean package -DskipTests dependency:go-offline -Ptest-coverage
|
||||||
|
- mvn -fmain/pom.xml clean package -DskipTests dependency:go-offline -Prelease
|
||||||
|
script:
|
||||||
|
- mvn --update-snapshots -fmain/pom.xml -Ptest-coverage clean test jacoco:report-aggregate
|
||||||
|
after_success:
|
||||||
|
- "bash <(curl -s https://codecov.io/bash)"
|
||||||
notifications:
|
notifications:
|
||||||
webhooks:
|
webhooks:
|
||||||
urls:
|
urls:
|
||||||
@@ -30,19 +37,10 @@ notifications:
|
|||||||
secure: "lngJ/HEAFBbD5AdiO9avMqptKpZHdmEwOzS9FabZjkdFh7yAYueTk5RniPUvShjsKtThYm7cJ8AtDMDwc07NvPrzbMBRtUJGwuDT+7c7YFALGFJ1NYi+emkC9x1oafvmPgEYSE+tMKzNcwrHi3ytGgKdIotsKwaF35QNXYA9aMs="
|
secure: "lngJ/HEAFBbD5AdiO9avMqptKpZHdmEwOzS9FabZjkdFh7yAYueTk5RniPUvShjsKtThYm7cJ8AtDMDwc07NvPrzbMBRtUJGwuDT+7c7YFALGFJ1NYi+emkC9x1oafvmPgEYSE+tMKzNcwrHi3ytGgKdIotsKwaF35QNXYA9aMs="
|
||||||
on_success: change
|
on_success: change
|
||||||
on_failure: always
|
on_failure: always
|
||||||
|
before_deploy:
|
||||||
before_deploy: mvn -fmain/pom.xml -Prelease clean package -DskipTests
|
- mvn -fmain/pom.xml -Prelease clean package -DskipTests
|
||||||
|
|
||||||
addons:
|
|
||||||
coverity_scan:
|
|
||||||
project:
|
|
||||||
name: "cryptomator/cryptomator"
|
|
||||||
notification_email: sebastian.stenzel@cryptomator.org
|
|
||||||
build_command: "mvn -fmain/pom.xml clean test -DskipTests"
|
|
||||||
branch_pattern: coverity_scan
|
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
provider: releases
|
- provider: releases
|
||||||
prerelease: false
|
prerelease: false
|
||||||
api_key:
|
api_key:
|
||||||
secure: "ZjE1j93v3qbPIe2YbmhS319aCbMdLQw0HuymmluTurxXsZtn9D4t2+eTr99vBVxGRuB5lzzGezPR5zjk5W7iHF7xhwrawXrFzr2rPJWzWFt0aM+Ry2njU1ROTGGXGTbv4anWeBlgMxLEInTAy/9ytOGNJlec83yc0THpOY2wxnk="
|
secure: "ZjE1j93v3qbPIe2YbmhS319aCbMdLQw0HuymmluTurxXsZtn9D4t2+eTr99vBVxGRuB5lzzGezPR5zjk5W7iHF7xhwrawXrFzr2rPJWzWFt0aM+Ry2njU1ROTGGXGTbv4anWeBlgMxLEInTAy/9ytOGNJlec83yc0THpOY2wxnk="
|
||||||
@@ -53,3 +51,13 @@ deploy:
|
|||||||
on:
|
on:
|
||||||
repo: cryptomator/cryptomator
|
repo: cryptomator/cryptomator
|
||||||
tags: true
|
tags: true
|
||||||
|
- provider: script
|
||||||
|
script: "curl -X POST -u cryptobot:${BINTRAY_API_KEY} -H 'Content-Type: application/json' -d '{\"name\": \"${TRAVIS_TAG}\", \"vcs_tag\": \"${TRAVIS_TAG}\"}' https://api.bintray.com/packages/cryptomator/cryptomator/cryptomator-win/versions"
|
||||||
|
on:
|
||||||
|
repo: cryptomator/cryptomator
|
||||||
|
tags: true
|
||||||
|
- provider: script
|
||||||
|
script: "curl -X POST -u cryptobot:${BINTRAY_API_KEY} -H 'Content-Type: application/json' -d '{\"name\": \"${TRAVIS_TAG}\", \"vcs_tag\": \"${TRAVIS_TAG}\"}' https://api.bintray.com/packages/cryptomator/cryptomator/cryptomator-osx/versions"
|
||||||
|
on:
|
||||||
|
repo: cryptomator/cryptomator
|
||||||
|
tags: true
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
[](https://travis-ci.org/cryptomator/cryptomator)
|
[](https://travis-ci.org/cryptomator/cryptomator)
|
||||||
[](https://scan.coverity.com/projects/cryptomator-cryptomator)
|
[](https://scan.coverity.com/projects/cryptomator-cryptomator)
|
||||||
|
[](https://www.codacy.com/app/cryptomator/cryptomator?utm_source=github.com&utm_medium=referral&utm_content=cryptomator/cryptomator&utm_campaign=Badge_Grade)
|
||||||
[](https://coveralls.io/github/cryptomator/cryptomator?branch=master)
|
[](https://coveralls.io/github/cryptomator/cryptomator?branch=master)
|
||||||
[](https://gitter.im/cryptomator/cryptomator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
[](https://gitter.im/cryptomator/cryptomator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
[](http://twitter.com/Cryptomator)
|
[](http://twitter.com/Cryptomator)
|
||||||
@@ -13,12 +14,13 @@ Download native binaries of Cryptomator on [cryptomator.org](https://cryptomator
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Works with Dropbox, Google Drive, OneDrive, and any other cloud storage service that synchronizes with a local directory
|
- Works with Dropbox, Google Drive, OneDrive, Nextcloud and any other cloud storage service which synchronizes with a local directory
|
||||||
- Open Source means: No backdoors, control is better than trust
|
- Open Source means: No backdoors, control is better than trust
|
||||||
- Client-side: No accounts, no data shared with any online service
|
- 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
|
- Totally transparent: Just work on the virtual drive as if it were a USB flash drive
|
||||||
- AES encryption with 256-bit key length
|
- AES encryption with 256-bit key length
|
||||||
- Filenames get encrypted, too
|
- File names get encrypted
|
||||||
|
- Folder structure gets obfuscated
|
||||||
- Use as many vaults in your Dropbox as you want, each having individual passwords
|
- Use as many vaults in your Dropbox as you want, each having individual passwords
|
||||||
|
|
||||||
### Privacy
|
### Privacy
|
||||||
@@ -51,9 +53,11 @@ For more information on the security details visit [cryptomator.org](https://cry
|
|||||||
|
|
||||||
```
|
```
|
||||||
cd main
|
cd main
|
||||||
mvn clean install
|
mvn clean install -Prelease
|
||||||
```
|
```
|
||||||
|
|
||||||
|
An executable jar file will be created inside `main/uber-jar/target`.
|
||||||
|
|
||||||
## Contributing to Cryptomator
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.1.0</version>
|
<version>1.2.3</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>ant-kit</artifactId>
|
<artifactId>ant-kit</artifactId>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
|
|||||||
@@ -21,21 +21,6 @@
|
|||||||
</fx:jar>
|
</fx:jar>
|
||||||
</target>
|
</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 -->
|
<!-- Create Debian package -->
|
||||||
<target name="deb" depends="create-jar">
|
<target name="deb" depends="create-jar">
|
||||||
<fx:deploy nativeBundles="deb" outdir="antbuild" outfile="Cryptomator-${project.version}" verbose="true">
|
<fx:deploy nativeBundles="deb" outdir="antbuild" outfile="Cryptomator-${project.version}" verbose="true">
|
||||||
@@ -45,7 +30,9 @@
|
|||||||
</fx:info>
|
</fx:info>
|
||||||
<fx:platform j2se="8.0">
|
<fx:platform j2se="8.0">
|
||||||
<fx:property name="cryptomator.logPath" value="~/.Cryptomator/cryptomator.log" />
|
<fx:property name="cryptomator.logPath" value="~/.Cryptomator/cryptomator.log" />
|
||||||
<fx:jvmarg value="-Xmx1048m"/>
|
<fx:property name="cryptomator.upgradeLogPath" value="~/.Cryptomator/upgrade.log" />
|
||||||
|
<fx:property name="cryptomator.settingsPath" value="~/.Cryptomator/settings.json" />
|
||||||
|
<fx:jvmarg value="-Xmx512m"/>
|
||||||
</fx:platform>
|
</fx:platform>
|
||||||
<fx:resources>
|
<fx:resources>
|
||||||
<fx:fileset dir="antbuild" type="jar" includes="Cryptomator-${project.version}.jar" />
|
<fx:fileset dir="antbuild" type="jar" includes="Cryptomator-${project.version}.jar" />
|
||||||
@@ -66,7 +53,9 @@
|
|||||||
</fx:info>
|
</fx:info>
|
||||||
<fx:platform j2se="8.0">
|
<fx:platform j2se="8.0">
|
||||||
<fx:property name="cryptomator.logPath" value="~/.Cryptomator/cryptomator.log" />
|
<fx:property name="cryptomator.logPath" value="~/.Cryptomator/cryptomator.log" />
|
||||||
<fx:jvmarg value="-Xmx1048m"/>
|
<fx:property name="cryptomator.upgradeLogPath" value="~/.Cryptomator/upgrade.log" />
|
||||||
|
<fx:property name="cryptomator.settingsPath" value="~/.Cryptomator/settings.json" />
|
||||||
|
<fx:jvmarg value="-Xmx512m"/>
|
||||||
</fx:platform>
|
</fx:platform>
|
||||||
<fx:resources>
|
<fx:resources>
|
||||||
<fx:fileset dir="antbuild" type="jar" includes="Cryptomator-${project.version}.jar" />
|
<fx:fileset dir="antbuild" type="jar" includes="Cryptomator-${project.version}.jar" />
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Priority: optional
|
|||||||
Architecture: APPLICATION_ARCH
|
Architecture: APPLICATION_ARCH
|
||||||
Provides: APPLICATION_PACKAGE
|
Provides: APPLICATION_PACKAGE
|
||||||
Installed-Size: APPLICATION_INSTALLED_SIZE
|
Installed-Size: APPLICATION_INSTALLED_SIZE
|
||||||
Depends: gvfs-bin, gvfs-backends, gvfs-fuse, xdg-utils
|
Depends: gvfs-bin, gvfs-backends, gvfs-fuse
|
||||||
Description: Multi-platform client-side encryption of your cloud files.
|
Description: Multi-platform client-side encryption of your cloud files.
|
||||||
Cryptomator provides free client-side AES encryption for your cloud files.
|
Cryptomator provides free client-side AES encryption for your cloud files.
|
||||||
Create encrypted vaults, which get mounted as virtual volumes. Whatever
|
Create encrypted vaults, which get mounted as virtual volumes. Whatever
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.1.0</version>
|
<version>1.2.3</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>commons-test</artifactId>
|
<artifactId>commons-test</artifactId>
|
||||||
<name>Cryptomator common test dependencies</name>
|
<name>Cryptomator common test dependencies</name>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.1.0</version>
|
<version>1.2.3</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>commons</artifactId>
|
<artifactId>commons</artifactId>
|
||||||
<name>Cryptomator common</name>
|
<name>Cryptomator common</name>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package org.cryptomator.common;
|
package org.cryptomator.common;
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface ConsumerThrowingException<T, E extends Exception> {
|
public interface ConsumerThrowingException<T, E extends Throwable> {
|
||||||
|
|
||||||
void accept(T t) throws E;
|
void accept(T t) throws E;
|
||||||
|
|
||||||
|
|||||||
@@ -17,16 +17,17 @@ public final class LazyInitializer {
|
|||||||
* @return The initialized value
|
* @return The initialized value
|
||||||
*/
|
*/
|
||||||
public static <T> T initializeLazily(AtomicReference<T> reference, Supplier<T> factory) {
|
public static <T> T initializeLazily(AtomicReference<T> reference, Supplier<T> factory) {
|
||||||
final T existingInstance = reference.get();
|
final T existing = reference.get();
|
||||||
if (existingInstance != null) {
|
if (existing != null) {
|
||||||
return existingInstance;
|
return existing;
|
||||||
} else {
|
} else {
|
||||||
final T newInstance = factory.get();
|
return reference.updateAndGet(currentValue -> {
|
||||||
if (reference.compareAndSet(null, newInstance)) {
|
if (currentValue == null) {
|
||||||
return newInstance;
|
return factory.get();
|
||||||
} else {
|
} else {
|
||||||
return reference.get();
|
return currentValue;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package org.cryptomator.common;
|
package org.cryptomator.common;
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface RunnableThrowingException<T extends Exception> {
|
public interface RunnableThrowingException<T extends Throwable> {
|
||||||
|
|
||||||
void run() throws T;
|
void run() throws T;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (c) 2016 Markus Kreusch and others.
|
||||||
|
* This file is licensed under the terms of the MIT license.
|
||||||
|
* See the LICENSE.txt file for more info.
|
||||||
|
*
|
||||||
|
* Contributors:
|
||||||
|
* Markus Kreusch - initial implementation
|
||||||
|
*******************************************************************************/
|
||||||
|
package org.cryptomator.common;
|
||||||
|
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility to print stack traces while analyzing issues.
|
||||||
|
*
|
||||||
|
* @author Markus Kreusch
|
||||||
|
*/
|
||||||
|
public class StackTrace {
|
||||||
|
|
||||||
|
public static void print(String message) {
|
||||||
|
Thread thread = Thread.currentThread();
|
||||||
|
System.err.println(stackTraceFor(message, thread));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String stackTraceFor(String message, Thread thread) {
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
appendMessageAndThreadName(result, message, thread);
|
||||||
|
appendStackTrace(thread, result);
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void appendStackTrace(Thread thread, StringBuilder result) {
|
||||||
|
Stream.of(thread.getStackTrace()) //
|
||||||
|
.skip(4) //
|
||||||
|
.forEach(stackTraceElement -> append(stackTraceElement, result));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void appendMessageAndThreadName(StringBuilder result, String message, Thread thread) {
|
||||||
|
result //
|
||||||
|
.append('[') //
|
||||||
|
.append(thread.getName()) //
|
||||||
|
.append("] ") //
|
||||||
|
.append(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void append(StackTraceElement stackTraceElement, StringBuilder result) {
|
||||||
|
String className = stackTraceElement.getClassName();
|
||||||
|
String methodName = stackTraceElement.getMethodName();
|
||||||
|
String fileName = stackTraceElement.getFileName();
|
||||||
|
int lineNumber = stackTraceElement.getLineNumber();
|
||||||
|
result.append('\n') //
|
||||||
|
.append(className).append(':').append(methodName) //
|
||||||
|
.append(" (").append(fileName).append(':').append(lineNumber).append(')');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package org.cryptomator.common;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface SupplierThrowingException<T, E extends Throwable> {
|
||||||
|
|
||||||
|
T get() throws E;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -9,8 +9,8 @@ import static org.mockito.Mockito.when;
|
|||||||
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import org.cryptomator.common.WeakValuedCache;
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
|
import org.junit.Ignore;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
import org.mockito.invocation.InvocationOnMock;
|
import org.mockito.invocation.InvocationOnMock;
|
||||||
@@ -83,6 +83,7 @@ public class WeakValuedCacheTest {
|
|||||||
assertThat(result, is(sameInstance(theValue)));
|
assertThat(result, is(sameInstance(theValue)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
@Test
|
@Test
|
||||||
public void testCacheDoesNotPreventGarbageCollectionOfValues() {
|
public void testCacheDoesNotPreventGarbageCollectionOfValues() {
|
||||||
when(loader.apply(A_KEY)).thenAnswer(this::createValueUsingMoreThanHalfTheJvmMemory);
|
when(loader.apply(A_KEY)).thenAnswer(this::createValueUsingMoreThanHalfTheJvmMemory);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.1.0</version>
|
<version>1.2.3</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>filesystem-api</artifactId>
|
<artifactId>filesystem-api</artifactId>
|
||||||
<name>Cryptomator filesystem: API</name>
|
<name>Cryptomator filesystem: API</name>
|
||||||
|
|||||||
@@ -17,6 +17,13 @@ public interface File extends Node, Comparable<File> {
|
|||||||
|
|
||||||
static final int EOF = -1;
|
static final int EOF = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The current size of the file. This value is a snapshot and might have been changed by concurrent modifications.
|
||||||
|
* @throws UncheckedIOException
|
||||||
|
* if an {@link IOException} occurs
|
||||||
|
*/
|
||||||
|
long size() throws UncheckedIOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
* Opens this file for reading.
|
* Opens this file for reading.
|
||||||
@@ -39,7 +46,6 @@ public interface File extends Node, Comparable<File> {
|
|||||||
* if an {@link IOException} occurs while opening the file, the
|
* if an {@link IOException} occurs while opening the file, the
|
||||||
* file does not exist or is a directory
|
* file does not exist or is a directory
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ReadableFile openReadable() throws UncheckedIOException;
|
ReadableFile openReadable() throws UncheckedIOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -30,13 +30,6 @@ public interface ReadableFile extends ReadableByteChannel {
|
|||||||
@Override
|
@Override
|
||||||
int read(ByteBuffer target) throws UncheckedIOException;
|
int read(ByteBuffer target) throws UncheckedIOException;
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The current size of the file. This value is a snapshot and might have been changed by concurrent modifications.
|
|
||||||
* @throws UncheckedIOException
|
|
||||||
* if an {@link IOException} occurs
|
|
||||||
*/
|
|
||||||
long size() throws UncheckedIOException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
* Fast-forwards or rewinds the file to the specified position.
|
* Fast-forwards or rewinds the file to the specified position.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import org.cryptomator.filesystem.File;
|
|||||||
import org.cryptomator.filesystem.ReadableFile;
|
import org.cryptomator.filesystem.ReadableFile;
|
||||||
import org.cryptomator.filesystem.WritableFile;
|
import org.cryptomator.filesystem.WritableFile;
|
||||||
|
|
||||||
public abstract class DelegatingFile<D extends DelegatingFolder<D, ?>> extends DelegatingNode<File>implements File {
|
public abstract class DelegatingFile<D extends DelegatingFolder<D, ?>> extends DelegatingNode<File> implements File {
|
||||||
|
|
||||||
private final D parent;
|
private final D parent;
|
||||||
|
|
||||||
@@ -29,6 +29,11 @@ public abstract class DelegatingFile<D extends DelegatingFolder<D, ?>> extends D
|
|||||||
return Optional.of(parent);
|
return Optional.of(parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long size() throws UncheckedIOException {
|
||||||
|
return delegate.size();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ReadableFile openReadable() throws UncheckedIOException {
|
public ReadableFile openReadable() throws UncheckedIOException {
|
||||||
return delegate.openReadable();
|
return delegate.openReadable();
|
||||||
|
|||||||
@@ -31,11 +31,6 @@ public class DelegatingReadableFile implements ReadableFile {
|
|||||||
return delegate.read(target);
|
return delegate.read(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public long size() throws UncheckedIOException {
|
|
||||||
return delegate.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void position(long position) throws UncheckedIOException {
|
public void position(long position) throws UncheckedIOException {
|
||||||
delegate.position(position);
|
delegate.position(position);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import java.io.IOException;
|
|||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
import java.nio.channels.Channels;
|
import java.nio.channels.Channels;
|
||||||
|
import java.nio.channels.ReadableByteChannel;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
@@ -28,7 +29,9 @@ public final class FileContents {
|
|||||||
* @return The file's content interpreted in this FileContents' charset.
|
* @return The file's content interpreted in this FileContents' charset.
|
||||||
*/
|
*/
|
||||||
public String readContents(File file) {
|
public String readContents(File file) {
|
||||||
try (Reader reader = Channels.newReader(file.openReadable(), charset.newDecoder(), -1)) {
|
try ( //
|
||||||
|
ReadableByteChannel channel = file.openReadable(); //
|
||||||
|
Reader reader = Channels.newReader(channel, charset.newDecoder(), -1)) {
|
||||||
return IOUtils.toString(reader);
|
return IOUtils.toString(reader);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new UncheckedIOException(e);
|
throw new UncheckedIOException(e);
|
||||||
|
|||||||
@@ -30,6 +30,16 @@ public class DelegatingFileTest {
|
|||||||
Assert.assertEquals(mockFile.name(), delegatingFile.name());
|
Assert.assertEquals(mockFile.name(), delegatingFile.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSize() {
|
||||||
|
File mockFile = Mockito.mock(File.class);
|
||||||
|
DelegatingFile<?> delegatingFile = new TestDelegatingFile(null, mockFile);
|
||||||
|
|
||||||
|
Mockito.when(mockFile.size()).thenReturn(42l);
|
||||||
|
Assert.assertEquals(42l, delegatingFile.size());
|
||||||
|
Mockito.verify(mockFile).size();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testParent() {
|
public void testParent() {
|
||||||
Folder mockFolder = Mockito.mock(Folder.class);
|
Folder mockFolder = Mockito.mock(Folder.class);
|
||||||
|
|||||||
@@ -42,17 +42,6 @@ public class DelegatingReadableFileTest {
|
|||||||
Mockito.verify(mockReadableFile).read(buf);
|
Mockito.verify(mockReadableFile).read(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testSize() {
|
|
||||||
ReadableFile mockReadableFile = Mockito.mock(ReadableFile.class);
|
|
||||||
@SuppressWarnings("resource")
|
|
||||||
DelegatingReadableFile delegatingReadableFile = new DelegatingReadableFile(mockReadableFile);
|
|
||||||
|
|
||||||
Mockito.when(mockReadableFile.size()).thenReturn(42l);
|
|
||||||
Assert.assertEquals(42l, delegatingReadableFile.size());
|
|
||||||
Mockito.verify(mockReadableFile).size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPosition() {
|
public void testPosition() {
|
||||||
ReadableFile mockReadableFile = Mockito.mock(ReadableFile.class);
|
ReadableFile mockReadableFile = Mockito.mock(ReadableFile.class);
|
||||||
|
|||||||
@@ -12,10 +12,10 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.1.0</version>
|
<version>1.2.3</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>filesystem-charsets</artifactId>
|
<artifactId>filesystem-charsets</artifactId>
|
||||||
<name>Cryptomator filesystem: Filename charset compatibility layer</name>
|
<name>Cryptomator filesystem: Charset compatibility layer</name>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ class NormalizedNameFolder extends DelegatingFolder<NormalizedNameFolder, Normal
|
|||||||
NormalizedNameFile nfcFile = super.file(nfcName);
|
NormalizedNameFile nfcFile = super.file(nfcName);
|
||||||
NormalizedNameFile nfdFile = super.file(nfdName);
|
NormalizedNameFile nfdFile = super.file(nfdName);
|
||||||
if (!nfcName.equals(nfdName) && nfcFile.exists() && nfdFile.exists()) {
|
if (!nfcName.equals(nfdName) && nfcFile.exists() && nfdFile.exists()) {
|
||||||
LOG.warn("Ambiguous file names \"" + nfcName + "\" (NFC) vs. \"" + nfdName + "\" (NFD). Both files exist. Using \"" + nfcName + "\" (NFC).");
|
LOG.debug("Ambiguous file names \"" + nfcName + "\" (NFC) vs. \"" + nfdName + "\" (NFD). Both files exist. Using \"" + nfcName + "\" (NFC).");
|
||||||
} else if (!nfcName.equals(nfdName) && !nfcFile.exists() && nfdFile.exists()) {
|
} else if (!nfcName.equals(nfdName) && !nfcFile.exists() && nfdFile.exists()) {
|
||||||
LOG.info("Moving file from \"" + nfcName + "\" (NFD) to \"" + nfdName + "\" (NFC).");
|
LOG.debug("Moving file from \"" + nfcName + "\" (NFD) to \"" + nfdName + "\" (NFC).");
|
||||||
nfdFile.moveTo(nfcFile);
|
nfdFile.moveTo(nfcFile);
|
||||||
}
|
}
|
||||||
return nfcFile;
|
return nfcFile;
|
||||||
@@ -60,9 +60,9 @@ class NormalizedNameFolder extends DelegatingFolder<NormalizedNameFolder, Normal
|
|||||||
NormalizedNameFolder nfcFolder = super.folder(nfcName);
|
NormalizedNameFolder nfcFolder = super.folder(nfcName);
|
||||||
NormalizedNameFolder nfdFolder = super.folder(nfdName);
|
NormalizedNameFolder nfdFolder = super.folder(nfdName);
|
||||||
if (!nfcName.equals(nfdName) && nfcFolder.exists() && nfdFolder.exists()) {
|
if (!nfcName.equals(nfdName) && nfcFolder.exists() && nfdFolder.exists()) {
|
||||||
LOG.warn("Ambiguous folder names \"" + nfcName + "\" (NFC) vs. \"" + nfdName + "\" (NFD). Both files exist. Using \"" + nfcName + "\" (NFC).");
|
LOG.debug("Ambiguous folder names \"" + nfcName + "\" (NFC) vs. \"" + nfdName + "\" (NFD). Both files exist. Using \"" + nfcName + "\" (NFC).");
|
||||||
} else if (!nfcName.equals(nfdName) && !nfcFolder.exists() && nfdFolder.exists()) {
|
} else if (!nfcName.equals(nfdName) && !nfcFolder.exists() && nfdFolder.exists()) {
|
||||||
LOG.info("Moving folder from \"" + nfcName + "\" (NFD) to \"" + nfdName + "\" (NFC).");
|
LOG.debug("Moving folder from \"" + nfcName + "\" (NFD) to \"" + nfdName + "\" (NFC).");
|
||||||
nfdFolder.moveTo(nfcFolder);
|
nfdFolder.moveTo(nfcFolder);
|
||||||
}
|
}
|
||||||
return nfcFolder;
|
return nfcFolder;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.1.0</version>
|
<version>1.2.3</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>filesystem-crypto-integration-tests</artifactId>
|
<artifactId>filesystem-crypto-integration-tests</artifactId>
|
||||||
<name>Cryptomator filesystem: Encryption layer tests</name>
|
<name>Cryptomator filesystem: Encryption layer tests</name>
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ public class CryptoFileSystemIntegrationTest {
|
|||||||
|
|
||||||
// toggle last bit
|
// toggle last bit
|
||||||
try (WritableFile writable = physicalFile.openWritable(); ReadableFile readable = physicalFile.openReadable()) {
|
try (WritableFile writable = physicalFile.openWritable(); ReadableFile readable = physicalFile.openReadable()) {
|
||||||
ByteBuffer buf = ByteBuffer.allocate((int) readable.size());
|
ByteBuffer buf = ByteBuffer.allocate((int) physicalFile.size());
|
||||||
readable.read(buf);
|
readable.read(buf);
|
||||||
buf.array()[buf.limit() - 1] ^= 0x01;
|
buf.array()[buf.limit() - 1] ^= 0x01;
|
||||||
buf.flip();
|
buf.flip();
|
||||||
|
|||||||
@@ -12,14 +12,14 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.1.0</version>
|
<version>1.2.3</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>filesystem-crypto</artifactId>
|
<artifactId>filesystem-crypto</artifactId>
|
||||||
<name>Cryptomator filesystem: Encryption layer</name>
|
<name>Cryptomator filesystem: Encryption layer</name>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<bouncycastle.version>1.51</bouncycastle.version>
|
<bouncycastle.version>1.51</bouncycastle.version>
|
||||||
<sivmode.version>1.0.2</sivmode.version>
|
<sivmode.version>1.2.0</sivmode.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|||||||
@@ -19,11 +19,6 @@ import javax.security.auth.Destroyable;
|
|||||||
*/
|
*/
|
||||||
public interface FileContentDecryptor extends Destroyable, Closeable {
|
public interface FileContentDecryptor extends Destroyable, Closeable {
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Number of bytes of the decrypted file.
|
|
||||||
*/
|
|
||||||
long contentLength();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends further ciphertext to this decryptor. This method might block until space becomes available. If so, it is interruptable.
|
* Appends further ciphertext to this decryptor. This method might block until space becomes available. If so, it is interruptable.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -11,28 +11,28 @@ package org.cryptomator.crypto.engine;
|
|||||||
public class UnsupportedVaultFormatException extends CryptoException {
|
public class UnsupportedVaultFormatException extends CryptoException {
|
||||||
|
|
||||||
private final Integer detectedVersion;
|
private final Integer detectedVersion;
|
||||||
private final Integer supportedVersion;
|
private final Integer latestSupportedVersion;
|
||||||
|
|
||||||
public UnsupportedVaultFormatException(Integer detectedVersion, Integer supportedVersion) {
|
public UnsupportedVaultFormatException(Integer detectedVersion, Integer latestSupportedVersion) {
|
||||||
super("Tried to open vault of version " + detectedVersion + ", but can only handle version " + supportedVersion);
|
super("Tried to open vault of version " + detectedVersion + ", latest supported version is " + latestSupportedVersion);
|
||||||
this.detectedVersion = detectedVersion;
|
this.detectedVersion = detectedVersion;
|
||||||
this.supportedVersion = supportedVersion;
|
this.latestSupportedVersion = latestSupportedVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Integer getDetectedVersion() {
|
public Integer getDetectedVersion() {
|
||||||
return detectedVersion;
|
return detectedVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Integer getSupportedVersion() {
|
public Integer getLatestSupportedVersion() {
|
||||||
return supportedVersion;
|
return latestSupportedVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isVaultOlderThanSoftware() {
|
public boolean isVaultOlderThanSoftware() {
|
||||||
return detectedVersion == null || detectedVersion < supportedVersion;
|
return detectedVersion == null || detectedVersion < latestSupportedVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isSoftwareOlderThanVault() {
|
public boolean isSoftwareOlderThanVault() {
|
||||||
return detectedVersion > supportedVersion;
|
return detectedVersion > latestSupportedVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ public final class Constants {
|
|||||||
private Constants() {
|
private Constants() {
|
||||||
}
|
}
|
||||||
|
|
||||||
static final Integer CURRENT_VAULT_VERSION = 3;
|
static final Integer CURRENT_VAULT_VERSION = 5;
|
||||||
|
|
||||||
public static final int PAYLOAD_SIZE = 32 * 1024;
|
public static final int PAYLOAD_SIZE = 32 * 1024;
|
||||||
public static final int NONCE_SIZE = 16;
|
public static final int NONCE_SIZE = 16;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import java.security.SecureRandom;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import javax.crypto.KeyGenerator;
|
||||||
import javax.crypto.Mac;
|
import javax.crypto.Mac;
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
@@ -81,14 +82,15 @@ class CryptorImpl implements Cryptor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void randomizeMasterkey() {
|
public void randomizeMasterkey() {
|
||||||
final byte[] randomBytes = new byte[KEYLENGTH_IN_BYTES];
|
|
||||||
try {
|
try {
|
||||||
randomSource.nextBytes(randomBytes);
|
KeyGenerator encKeyGen = KeyGenerator.getInstance(ENCRYPTION_ALG);
|
||||||
encryptionKey = new SecretKeySpec(randomBytes, ENCRYPTION_ALG);
|
encKeyGen.init(KEYLENGTH_IN_BYTES * Byte.SIZE, randomSource);
|
||||||
randomSource.nextBytes(randomBytes);
|
encryptionKey = encKeyGen.generateKey();
|
||||||
macKey = new SecretKeySpec(randomBytes, MAC_ALG);
|
KeyGenerator macKeyGen = KeyGenerator.getInstance(MAC_ALG);
|
||||||
} finally {
|
macKeyGen.init(KEYLENGTH_IN_BYTES * Byte.SIZE, randomSource);
|
||||||
Arrays.fill(randomBytes, (byte) 0x00);
|
macKey = macKeyGen.generateKey();
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new IllegalStateException("Hard-coded algorithm doesn't exist.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,12 +118,12 @@ class CryptorImpl implements Cryptor {
|
|||||||
final SecretKey kek = new SecretKeySpec(kekBytes, ENCRYPTION_ALG);
|
final SecretKey kek = new SecretKeySpec(kekBytes, ENCRYPTION_ALG);
|
||||||
this.macKey = AesKeyWrap.unwrap(kek, keyFile.getMacMasterKey(), MAC_ALG);
|
this.macKey = AesKeyWrap.unwrap(kek, keyFile.getMacMasterKey(), MAC_ALG);
|
||||||
// future use (as soon as we need to prevent downgrade attacks):
|
// future use (as soon as we need to prevent downgrade attacks):
|
||||||
// final Mac mac = new ThreadLocalMac(macKey, MAC_ALG).get();
|
// final Mac mac = new ThreadLocalMac(macKey, MAC_ALG).get();
|
||||||
// final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.BYTES).putInt(CURRENT_VAULT_VERSION).array());
|
// final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.BYTES).putInt(CURRENT_VAULT_VERSION).array());
|
||||||
// if (!MessageDigest.isEqual(versionMac, keyFile.getVersionMac())) {
|
// if (!MessageDigest.isEqual(versionMac, keyFile.getVersionMac())) {
|
||||||
// destroyQuietly(macKey);
|
// destroyQuietly(macKey);
|
||||||
// throw new UnsupportedVaultFormatException(Integer.MAX_VALUE, CURRENT_VAULT_VERSION);
|
// throw new UnsupportedVaultFormatException(Integer.MAX_VALUE, CURRENT_VAULT_VERSION);
|
||||||
// }
|
// }
|
||||||
this.encryptionKey = AesKeyWrap.unwrap(kek, keyFile.getEncryptionMasterKey(), ENCRYPTION_ALG);
|
this.encryptionKey = AesKeyWrap.unwrap(kek, keyFile.getEncryptionMasterKey(), ENCRYPTION_ALG);
|
||||||
} catch (InvalidKeyException e) {
|
} catch (InvalidKeyException e) {
|
||||||
throw new InvalidPassphraseException();
|
throw new InvalidPassphraseException();
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import java.util.concurrent.Callable;
|
|||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.atomic.LongAdder;
|
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
@@ -47,7 +46,6 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
|
|||||||
private final Supplier<Mac> hmacSha256;
|
private final Supplier<Mac> hmacSha256;
|
||||||
private final FileHeader header;
|
private final FileHeader header;
|
||||||
private final boolean authenticate;
|
private final boolean authenticate;
|
||||||
private final LongAdder cleartextBytesDecrypted = new LongAdder();
|
|
||||||
private ByteBuffer ciphertextBuffer = ByteBuffer.allocate(CHUNK_SIZE);
|
private ByteBuffer ciphertextBuffer = ByteBuffer.allocate(CHUNK_SIZE);
|
||||||
private long chunkNumber = 0;
|
private long chunkNumber = 0;
|
||||||
|
|
||||||
@@ -56,11 +54,11 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
|
|||||||
this.header = FileHeader.decrypt(headerKey, hmacSha256, header);
|
this.header = FileHeader.decrypt(headerKey, hmacSha256, header);
|
||||||
this.authenticate = authenticate;
|
this.authenticate = authenticate;
|
||||||
this.chunkNumber = firstCiphertextByte / CHUNK_SIZE; // floor() by int-truncation
|
this.chunkNumber = firstCiphertextByte / CHUNK_SIZE; // floor() by int-truncation
|
||||||
}
|
|
||||||
|
// vault version 5 and onwards should have filesize: -1
|
||||||
@Override
|
if (this.header.getPayload().getFilesize() != -1l) {
|
||||||
public long contentLength() {
|
throw new UncheckedIOException(new IOException("Attempted to decrypt file with invalid header (probably from previous vault version)"));
|
||||||
return header.getPayload().getFilesize();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -105,15 +103,7 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
|
|||||||
@Override
|
@Override
|
||||||
public ByteBuffer cleartext() throws InterruptedException {
|
public ByteBuffer cleartext() throws InterruptedException {
|
||||||
try {
|
try {
|
||||||
final ByteBuffer cleartext = dataProcessor.processedData();
|
return dataProcessor.processedData();
|
||||||
long bytesUntilLogicalEof = contentLength() - cleartextBytesDecrypted.sum();
|
|
||||||
if (bytesUntilLogicalEof <= 0) {
|
|
||||||
return FileContentCryptor.EOF;
|
|
||||||
} else if (bytesUntilLogicalEof < cleartext.remaining()) {
|
|
||||||
cleartext.limit((int) bytesUntilLogicalEof);
|
|
||||||
}
|
|
||||||
cleartextBytesDecrypted.add(cleartext.remaining());
|
|
||||||
return cleartext;
|
|
||||||
} catch (ExecutionException e) {
|
} catch (ExecutionException e) {
|
||||||
if (e.getCause() instanceof AuthenticationFailedException) {
|
if (e.getCause() instanceof AuthenticationFailedException) {
|
||||||
throw new AuthenticationFailedException(e);
|
throw new AuthenticationFailedException(e);
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ import org.cryptomator.io.ByteBuffers;
|
|||||||
class FileContentEncryptorImpl implements FileContentEncryptor {
|
class FileContentEncryptorImpl implements FileContentEncryptor {
|
||||||
|
|
||||||
private static final String HMAC_SHA256 = "HmacSHA256";
|
private static final String HMAC_SHA256 = "HmacSHA256";
|
||||||
private static final int PADDING_LOWER_BOUND = 4 * 1024; // 4k
|
|
||||||
private static final int PADDING_UPPER_BOUND = 16 * 1024 * 1024; // 16M
|
|
||||||
private static final int NUM_THREADS = Runtime.getRuntime().availableProcessors();
|
private static final int NUM_THREADS = Runtime.getRuntime().availableProcessors();
|
||||||
private static final int READ_AHEAD = 2;
|
private static final int READ_AHEAD = 2;
|
||||||
private static final ExecutorService SHARED_DECRYPTION_EXECUTOR = Executors.newFixedThreadPool(NUM_THREADS);
|
private static final ExecutorService SHARED_DECRYPTION_EXECUTOR = Executors.newFixedThreadPool(NUM_THREADS);
|
||||||
@@ -63,7 +61,7 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ByteBuffer getHeader() {
|
public ByteBuffer getHeader() {
|
||||||
header.getPayload().setFilesize(cleartextBytesScheduledForEncryption.sum());
|
header.getPayload().setFilesize(-1l);
|
||||||
return header.toByteBuffer(headerKey, hmacSha256);
|
return header.toByteBuffer(headerKey, hmacSha256);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +74,6 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
|
|||||||
public void append(ByteBuffer cleartext) throws InterruptedException {
|
public void append(ByteBuffer cleartext) throws InterruptedException {
|
||||||
cleartextBytesScheduledForEncryption.add(cleartext.remaining());
|
cleartextBytesScheduledForEncryption.add(cleartext.remaining());
|
||||||
if (cleartext == FileContentCryptor.EOF) {
|
if (cleartext == FileContentCryptor.EOF) {
|
||||||
appendSizeObfuscationPadding(cleartextBytesScheduledForEncryption.sum());
|
|
||||||
submitCleartextBuffer();
|
submitCleartextBuffer();
|
||||||
submitEof();
|
submitEof();
|
||||||
} else {
|
} else {
|
||||||
@@ -84,19 +81,6 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void appendSizeObfuscationPadding(long actualSize) throws InterruptedException {
|
|
||||||
final int maxPaddingLength = (int) Math.min(Math.max(actualSize / 10, PADDING_LOWER_BOUND), PADDING_UPPER_BOUND); // preferably 10%, but at least lower bound and no more than upper bound
|
|
||||||
final int randomPaddingLength = randomSource.nextInt(maxPaddingLength);
|
|
||||||
final ByteBuffer buf = ByteBuffer.allocate(PAYLOAD_SIZE);
|
|
||||||
int remainingPadding = randomPaddingLength;
|
|
||||||
while (remainingPadding > 0) {
|
|
||||||
int bytesInCurrentIteration = Math.min(remainingPadding, PAYLOAD_SIZE);
|
|
||||||
buf.clear().limit(bytesInCurrentIteration);
|
|
||||||
appendAllAndSubmitIfFull(buf);
|
|
||||||
remainingPadding -= bytesInCurrentIteration;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void appendAllAndSubmitIfFull(ByteBuffer cleartext) throws InterruptedException {
|
private void appendAllAndSubmitIfFull(ByteBuffer cleartext) throws InterruptedException {
|
||||||
while (cleartext.hasRemaining()) {
|
while (cleartext.hasRemaining()) {
|
||||||
ByteBuffers.copy(cleartext, cleartextBuffer);
|
ByteBuffers.copy(cleartext, cleartextBuffer);
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ package org.cryptomator.crypto.engine.impl;
|
|||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.security.InvalidAlgorithmParameterException;
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
import javax.crypto.BadPaddingException;
|
import javax.crypto.BadPaddingException;
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
import javax.crypto.IllegalBlockSizeException;
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
|
import javax.crypto.KeyGenerator;
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
import javax.crypto.ShortBufferException;
|
import javax.crypto.ShortBufferException;
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
@@ -36,13 +38,13 @@ class FileHeaderPayload implements Destroyable {
|
|||||||
private final SecretKey contentKey;
|
private final SecretKey contentKey;
|
||||||
|
|
||||||
public FileHeaderPayload(SecureRandom randomSource) {
|
public FileHeaderPayload(SecureRandom randomSource) {
|
||||||
filesize = 0;
|
this.filesize = 0;
|
||||||
final byte[] contentKey = new byte[CONTENT_KEY_LEN];
|
|
||||||
try {
|
try {
|
||||||
randomSource.nextBytes(contentKey);
|
KeyGenerator keyGen = KeyGenerator.getInstance(AES);
|
||||||
this.contentKey = new SecretKeySpec(contentKey, AES);
|
keyGen.init(CONTENT_KEY_LEN * Byte.SIZE, randomSource);
|
||||||
} finally {
|
this.contentKey = keyGen.generateKey();
|
||||||
Arrays.fill(contentKey, (byte) 0x00);
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new IllegalStateException("Hard-coded algorithm doesn't exist.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import java.security.MessageDigest;
|
|||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import javax.crypto.AEADBadTagException;
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
import org.apache.commons.codec.binary.Base32;
|
import org.apache.commons.codec.binary.Base32;
|
||||||
@@ -22,11 +22,13 @@ import org.apache.commons.codec.binary.BaseNCodec;
|
|||||||
import org.cryptomator.crypto.engine.AuthenticationFailedException;
|
import org.cryptomator.crypto.engine.AuthenticationFailedException;
|
||||||
import org.cryptomator.crypto.engine.FilenameCryptor;
|
import org.cryptomator.crypto.engine.FilenameCryptor;
|
||||||
import org.cryptomator.siv.SivMode;
|
import org.cryptomator.siv.SivMode;
|
||||||
|
import org.cryptomator.siv.UnauthenticCiphertextException;
|
||||||
|
|
||||||
class FilenameCryptorImpl implements FilenameCryptor {
|
class FilenameCryptorImpl implements FilenameCryptor {
|
||||||
|
|
||||||
private static final BaseNCodec BASE32 = new Base32();
|
private static final BaseNCodec BASE32 = new Base32();
|
||||||
private static final Pattern BASE32_PATTERN = Pattern.compile("([A-Z0-9]{8})*[A-Z0-9=]{8}");
|
// 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<MessageDigest> SHA1 = new ThreadLocalSha1();
|
||||||
private static final ThreadLocal<SivMode> AES_SIV = new ThreadLocal<SivMode>() {
|
private static final ThreadLocal<SivMode> AES_SIV = new ThreadLocal<SivMode>() {
|
||||||
@Override
|
@Override
|
||||||
@@ -69,8 +71,8 @@ class FilenameCryptorImpl implements FilenameCryptor {
|
|||||||
try {
|
try {
|
||||||
final byte[] cleartextBytes = AES_SIV.get().decrypt(encryptionKey, macKey, encryptedBytes, associatedData);
|
final byte[] cleartextBytes = AES_SIV.get().decrypt(encryptionKey, macKey, encryptedBytes, associatedData);
|
||||||
return new String(cleartextBytes, UTF_8);
|
return new String(cleartextBytes, UTF_8);
|
||||||
} catch (AEADBadTagException e) {
|
} catch (UnauthenticCiphertextException | IllegalBlockSizeException e) {
|
||||||
throw new AuthenticationFailedException("Authentication failed.", e);
|
throw new AuthenticationFailedException("Invalid ciphertext.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,11 +100,6 @@ class BlockAlignedReadableFile implements ReadableFile {
|
|||||||
return delegate.isOpen();
|
return delegate.isOpen();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public long size() throws UncheckedIOException {
|
|
||||||
return delegate.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws UncheckedIOException {
|
public void close() throws UncheckedIOException {
|
||||||
delegate.close();
|
delegate.close();
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
package org.cryptomator.filesystem.crypto;
|
package org.cryptomator.filesystem.crypto;
|
||||||
|
|
||||||
import static org.cryptomator.filesystem.crypto.Constants.DIR_SUFFIX;
|
import static org.cryptomator.filesystem.crypto.Constants.DIR_PREFIX;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
@@ -12,7 +13,7 @@ import java.util.regex.Pattern;
|
|||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.cryptomator.filesystem.File;
|
import org.cryptomator.filesystem.File;
|
||||||
import org.cryptomator.filesystem.Folder;
|
import org.cryptomator.filesystem.Folder;
|
||||||
import org.cryptomator.io.FileContents;
|
import org.cryptomator.filesystem.ReadableFile;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ final class ConflictResolver {
|
|||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ConflictResolver.class);
|
private static final Logger LOG = LoggerFactory.getLogger(ConflictResolver.class);
|
||||||
private static final int UUID_FIRST_GROUP_STRLEN = 8;
|
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 Pattern encryptedNamePattern;
|
||||||
private final Function<String, Optional<String>> nameDecryptor;
|
private final Function<String, Optional<String>> nameDecryptor;
|
||||||
@@ -32,7 +34,7 @@ final class ConflictResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public File resolveIfNecessary(File file) {
|
public File resolveIfNecessary(File file) {
|
||||||
Matcher m = encryptedNamePattern.matcher(StringUtils.removeEnd(file.name(), DIR_SUFFIX));
|
Matcher m = encryptedNamePattern.matcher(StringUtils.removeStart(file.name(), DIR_PREFIX));
|
||||||
if (m.matches()) {
|
if (m.matches()) {
|
||||||
// full match, use file as is
|
// full match, use file as is
|
||||||
return file;
|
return file;
|
||||||
@@ -47,18 +49,16 @@ final class ConflictResolver {
|
|||||||
|
|
||||||
private File resolveConflict(File conflictingFile, MatchResult matchResult) {
|
private File resolveConflict(File conflictingFile, MatchResult matchResult) {
|
||||||
String ciphertext = matchResult.group();
|
String ciphertext = matchResult.group();
|
||||||
boolean isDirectory = conflictingFile.name().substring(matchResult.end()).startsWith(DIR_SUFFIX);
|
boolean isDirectory = conflictingFile.name().startsWith(DIR_PREFIX);
|
||||||
Optional<String> cleartext = nameDecryptor.apply(ciphertext);
|
Optional<String> cleartext = nameDecryptor.apply(ciphertext);
|
||||||
if (cleartext.isPresent()) {
|
if (cleartext.isPresent()) {
|
||||||
Folder folder = conflictingFile.parent().get();
|
Folder folder = conflictingFile.parent().get();
|
||||||
File canonicalFile = folder.file(isDirectory ? ciphertext + DIR_SUFFIX : ciphertext);
|
File canonicalFile = folder.file(isDirectory ? DIR_PREFIX + ciphertext : ciphertext);
|
||||||
if (canonicalFile.exists()) {
|
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:
|
// there must not be two directories pointing to the same directory id. In this case no human interaction is needed to resolve this conflict:
|
||||||
if (isDirectory && FileContents.UTF_8.readContents(canonicalFile).equals(FileContents.UTF_8.readContents(conflictingFile))) {
|
conflictingFile.delete();
|
||||||
conflictingFile.delete();
|
return canonicalFile;
|
||||||
return canonicalFile;
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
// conventional conflict detected! look for an alternative name:
|
// conventional conflict detected! look for an alternative name:
|
||||||
File alternativeFile;
|
File alternativeFile;
|
||||||
String conflictId;
|
String conflictId;
|
||||||
@@ -66,14 +66,11 @@ final class ConflictResolver {
|
|||||||
conflictId = createConflictId();
|
conflictId = createConflictId();
|
||||||
String alternativeCleartext = cleartext.get() + " (Conflict " + conflictId + ")";
|
String alternativeCleartext = cleartext.get() + " (Conflict " + conflictId + ")";
|
||||||
String alternativeCiphertext = nameEncryptor.apply(alternativeCleartext).get();
|
String alternativeCiphertext = nameEncryptor.apply(alternativeCleartext).get();
|
||||||
alternativeFile = folder.file(isDirectory ? alternativeCiphertext + DIR_SUFFIX : alternativeCiphertext);
|
alternativeFile = folder.file(isDirectory ? DIR_PREFIX + alternativeCiphertext : alternativeCiphertext);
|
||||||
} while (alternativeFile.exists());
|
} while (alternativeFile.exists());
|
||||||
LOG.info("Detected conflict {}:\n{}\n{}", conflictId, canonicalFile, conflictingFile);
|
LOG.debug("Detected conflict {}:\n{}\n{}", conflictId, canonicalFile, conflictingFile);
|
||||||
conflictingFile.moveTo(alternativeFile);
|
conflictingFile.moveTo(alternativeFile);
|
||||||
return alternativeFile;
|
return alternativeFile;
|
||||||
} else {
|
|
||||||
conflictingFile.moveTo(canonicalFile);
|
|
||||||
return canonicalFile;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// not decryptable; false positive
|
// not decryptable; false positive
|
||||||
@@ -81,6 +78,26 @@ final class ConflictResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isSameFileBasedOnSample(File file1, File file2, int sampleSize) {
|
||||||
|
if (file1.size() != file2.size()) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
try (ReadableFile r1 = file1.openReadable(); ReadableFile r2 = file2.openReadable()) {
|
||||||
|
ByteBuffer beginOfFile1 = ByteBuffer.allocate(sampleSize);
|
||||||
|
ByteBuffer beginOfFile2 = ByteBuffer.allocate(sampleSize);
|
||||||
|
int bytesRead1 = r1.read(beginOfFile1);
|
||||||
|
int bytesRead2 = r2.read(beginOfFile2);
|
||||||
|
if (bytesRead1 == bytesRead2) {
|
||||||
|
beginOfFile1.flip();
|
||||||
|
beginOfFile2.flip();
|
||||||
|
return beginOfFile1.equals(beginOfFile2);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String createConflictId() {
|
private String createConflictId() {
|
||||||
return UUID.randomUUID().toString().substring(0, UUID_FIRST_GROUP_STRLEN);
|
return UUID.randomUUID().toString().substring(0, UUID_FIRST_GROUP_STRLEN);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ public final class Constants {
|
|||||||
static final String DATA_ROOT_DIR = "d";
|
static final String DATA_ROOT_DIR = "d";
|
||||||
static final String ROOT_DIRECOTRY_ID = "";
|
static final String ROOT_DIRECOTRY_ID = "";
|
||||||
|
|
||||||
static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
|
public static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
|
||||||
static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup";
|
public static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup";
|
||||||
|
|
||||||
static final String DIR_SUFFIX = "_";
|
static final String DIR_PREFIX = "0";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
package org.cryptomator.filesystem.crypto;
|
package org.cryptomator.filesystem.crypto;
|
||||||
|
|
||||||
|
import static org.cryptomator.crypto.engine.impl.Constants.CHUNK_SIZE;
|
||||||
|
import static org.cryptomator.crypto.engine.impl.Constants.PAYLOAD_SIZE;
|
||||||
|
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
import java.nio.file.FileAlreadyExistsException;
|
import java.nio.file.FileAlreadyExistsException;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -28,6 +31,25 @@ class CryptoFile extends CryptoNode implements File {
|
|||||||
return parent().get().encryptChildName(name());
|
return parent().get().encryptChildName(name());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long size() throws UncheckedIOException {
|
||||||
|
if (!physicalFile().isPresent()) {
|
||||||
|
return -1l;
|
||||||
|
} else {
|
||||||
|
File file = physicalFile().get();
|
||||||
|
long ciphertextSize = file.size() - cryptor.getFileContentCryptor().getHeaderSize();
|
||||||
|
long overheadPerChunk = CHUNK_SIZE - PAYLOAD_SIZE;
|
||||||
|
long numFullChunks = ciphertextSize / CHUNK_SIZE; // floor by int-truncation
|
||||||
|
long additionalCiphertextBytes = ciphertextSize % CHUNK_SIZE;
|
||||||
|
if (additionalCiphertextBytes > 0 && additionalCiphertextBytes <= overheadPerChunk) {
|
||||||
|
throw new IllegalArgumentException("Method not defined for input value " + ciphertextSize);
|
||||||
|
}
|
||||||
|
long additionalCleartextBytes = (additionalCiphertextBytes == 0) ? 0 : additionalCiphertextBytes - overheadPerChunk;
|
||||||
|
assert additionalCleartextBytes >= 0;
|
||||||
|
return PAYLOAD_SIZE * numFullChunks + additionalCleartextBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ReadableFile openReadable() {
|
public ReadableFile openReadable() {
|
||||||
boolean authenticate = !fileSystem().delegate().shouldSkipAuthentication(toString());
|
boolean authenticate = !fileSystem().delegate().shouldSkipAuthentication(toString());
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ package org.cryptomator.filesystem.crypto;
|
|||||||
|
|
||||||
import static java.lang.String.format;
|
import static java.lang.String.format;
|
||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
import static org.cryptomator.filesystem.crypto.Constants.DIR_SUFFIX;
|
import static org.apache.commons.lang3.StringUtils.removeStart;
|
||||||
|
import static org.cryptomator.filesystem.crypto.Constants.DIR_PREFIX;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
@@ -54,9 +55,9 @@ class CryptoFolder extends CryptoNode implements Folder {
|
|||||||
@Override
|
@Override
|
||||||
protected Optional<String> encryptedName() {
|
protected Optional<String> encryptedName() {
|
||||||
if (parent().isPresent()) {
|
if (parent().isPresent()) {
|
||||||
return parent().get().encryptChildName(name()).map(s -> s + DIR_SUFFIX);
|
return parent().get().encryptChildName(name()).map(s -> DIR_PREFIX + s);
|
||||||
} else {
|
} else {
|
||||||
return Optional.of(cryptor.getFilenameCryptor().encryptFilename(name()) + DIR_SUFFIX);
|
return Optional.of(DIR_PREFIX + cryptor.getFilenameCryptor().encryptFilename(name()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,15 +92,15 @@ class CryptoFolder extends CryptoNode implements Folder {
|
|||||||
private Stream<File> nonConflictingFiles() {
|
private Stream<File> nonConflictingFiles() {
|
||||||
if (exists()) {
|
if (exists()) {
|
||||||
final Stream<? extends File> files = physicalFolder().filter(Folder::exists).map(Folder::files).orElse(Stream.empty());
|
final Stream<? extends File> files = physicalFolder().filter(Folder::exists).map(Folder::files).orElse(Stream.empty());
|
||||||
return files.filter(containsEncryptedName()).map(conflictResolver::resolveIfNecessary).distinct();
|
return files.filter(startsWithEncryptedName()).map(conflictResolver::resolveIfNecessary).distinct();
|
||||||
} else {
|
} else {
|
||||||
throw new UncheckedIOException(new FileNotFoundException(format("Folder %s does not exist", this)));
|
throw new UncheckedIOException(new FileNotFoundException(format("Folder %s does not exist", this)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Predicate<File> containsEncryptedName() {
|
private Predicate<File> startsWithEncryptedName() {
|
||||||
final Pattern encryptedNamePattern = cryptor.getFilenameCryptor().encryptedNamePattern();
|
final Pattern encryptedNamePattern = cryptor.getFilenameCryptor().encryptedNamePattern();
|
||||||
return (File file) -> encryptedNamePattern.matcher(file.name()).find();
|
return (File file) -> encryptedNamePattern.matcher(removeStart(file.name(),DIR_PREFIX)).find();
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<String> decryptChildName(String ciphertextFileName) {
|
Optional<String> decryptChildName(String ciphertextFileName) {
|
||||||
@@ -121,20 +122,20 @@ class CryptoFolder extends CryptoNode implements Folder {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Stream<CryptoFile> files() {
|
public Stream<CryptoFile> files() {
|
||||||
return nonConflictingFiles().map(File::name).filter(endsWithDirSuffix().negate()).map(this::decryptChildName).filter(Optional::isPresent).map(Optional::get).map(this::file);
|
return nonConflictingFiles().map(File::name).filter(startsWithDirPrefix().negate()).map(this::decryptChildName).filter(Optional::isPresent).map(Optional::get).map(this::file);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Stream<CryptoFolder> folders() {
|
public Stream<CryptoFolder> folders() {
|
||||||
return nonConflictingFiles().map(File::name).filter(endsWithDirSuffix()).map(this::removeDirSuffix).map(this::decryptChildName).filter(Optional::isPresent).map(Optional::get).map(this::folder);
|
return nonConflictingFiles().map(File::name).filter(startsWithDirPrefix()).map(this::removeDirPrefix).map(this::decryptChildName).filter(Optional::isPresent).map(Optional::get).map(this::folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Predicate<String> endsWithDirSuffix() {
|
private Predicate<String> startsWithDirPrefix() {
|
||||||
return (String encryptedFolderName) -> StringUtils.endsWith(encryptedFolderName, DIR_SUFFIX);
|
return (String encryptedFolderName) -> StringUtils.startsWith(encryptedFolderName, DIR_PREFIX);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String removeDirSuffix(String encryptedFolderName) {
|
private String removeDirPrefix(String encryptedFolderName) {
|
||||||
return StringUtils.removeEnd(encryptedFolderName, DIR_SUFFIX);
|
return StringUtils.removeStart(encryptedFolderName, DIR_PREFIX);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -70,12 +70,6 @@ class CryptoReadableFile implements ReadableFile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public long size() throws UncheckedIOException {
|
|
||||||
assert decryptor != null : "decryptor is always being set during position(long)";
|
|
||||||
return decryptor.contentLength();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void position(long position) throws UncheckedIOException {
|
public void position(long position) throws UncheckedIOException {
|
||||||
if (readAheadTask != null) {
|
if (readAheadTask != null) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import java.io.InputStream;
|
|||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.channels.Channels;
|
import java.nio.channels.Channels;
|
||||||
|
import java.nio.channels.ReadableByteChannel;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Provider;
|
import javax.inject.Provider;
|
||||||
@@ -52,10 +53,14 @@ class Masterkeys {
|
|||||||
public Cryptor decrypt(Folder vaultLocation, CharSequence passphrase) throws InvalidPassphraseException {
|
public Cryptor decrypt(Folder vaultLocation, CharSequence passphrase) throws InvalidPassphraseException {
|
||||||
File masterkeyFile = vaultLocation.file(MASTERKEY_FILENAME);
|
File masterkeyFile = vaultLocation.file(MASTERKEY_FILENAME);
|
||||||
Cryptor cryptor = cryptorProvider.get();
|
Cryptor cryptor = cryptorProvider.get();
|
||||||
|
boolean success = false;
|
||||||
try {
|
try {
|
||||||
readMasterKey(masterkeyFile, cryptor, passphrase);
|
readMasterKey(masterkeyFile, cryptor, passphrase);
|
||||||
} catch (UncheckedIOException e) {
|
success = true;
|
||||||
cryptor.destroy();
|
} finally {
|
||||||
|
if (!success) {
|
||||||
|
cryptor.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return cryptor;
|
return cryptor;
|
||||||
}
|
}
|
||||||
@@ -86,7 +91,9 @@ class Masterkeys {
|
|||||||
/* I/O */
|
/* I/O */
|
||||||
|
|
||||||
private static void readMasterKey(File file, Cryptor cryptor, CharSequence passphrase) throws UncheckedIOException, InvalidPassphraseException {
|
private static void readMasterKey(File file, Cryptor cryptor, CharSequence passphrase) throws UncheckedIOException, InvalidPassphraseException {
|
||||||
try (InputStream in = Channels.newInputStream(file.openReadable())) {
|
try ( //
|
||||||
|
ReadableByteChannel channel = file.openReadable(); //
|
||||||
|
InputStream in = Channels.newInputStream(channel)) {
|
||||||
final byte[] fileContents = IOUtils.toByteArray(in);
|
final byte[] fileContents = IOUtils.toByteArray(in);
|
||||||
cryptor.readKeysFromMasterkeyFile(fileContents, passphrase);
|
cryptor.readKeysFromMasterkeyFile(fileContents, passphrase);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -96,6 +103,7 @@ class Masterkeys {
|
|||||||
|
|
||||||
private static void writeMasterKey(File file, Cryptor cryptor, CharSequence passphrase) throws UncheckedIOException {
|
private static void writeMasterKey(File file, Cryptor cryptor, CharSequence passphrase) throws UncheckedIOException {
|
||||||
try (WritableFile writable = file.openWritable()) {
|
try (WritableFile writable = file.openWritable()) {
|
||||||
|
writable.truncate();
|
||||||
final byte[] fileContents = cryptor.writeKeysToMasterkeyFile(passphrase);
|
final byte[] fileContents = cryptor.writeKeysToMasterkeyFile(passphrase);
|
||||||
writable.write(ByteBuffer.wrap(fileContents));
|
writable.write(ByteBuffer.wrap(fileContents));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,16 +44,9 @@ class NoFileContentCryptor implements FileContentCryptor {
|
|||||||
private class Decryptor implements FileContentDecryptor {
|
private class Decryptor implements FileContentDecryptor {
|
||||||
|
|
||||||
private final BlockingQueue<Supplier<ByteBuffer>> cleartextQueue = new LinkedBlockingQueue<>();
|
private final BlockingQueue<Supplier<ByteBuffer>> cleartextQueue = new LinkedBlockingQueue<>();
|
||||||
private final long contentLength;
|
|
||||||
|
|
||||||
private Decryptor(ByteBuffer header) {
|
private Decryptor(ByteBuffer header) {
|
||||||
assert header.remaining() == Long.BYTES;
|
assert header.remaining() == Long.BYTES;
|
||||||
this.contentLength = header.getLong();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long contentLength() {
|
|
||||||
return contentLength;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -21,20 +21,20 @@ public class CryptorImplTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testMasterkeyDecryptionWithCorrectPassphrase() throws IOException {
|
public void testMasterkeyDecryptionWithCorrectPassphrase() throws IOException {
|
||||||
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||||
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||||
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||||
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
|
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
|
||||||
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
||||||
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
|
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = InvalidPassphraseException.class)
|
@Test(expected = InvalidPassphraseException.class)
|
||||||
public void testMasterkeyDecryptionWithWrongPassphrase() throws IOException {
|
public void testMasterkeyDecryptionWithWrongPassphrase() throws IOException {
|
||||||
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||||
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||||
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||||
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
|
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
|
||||||
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
||||||
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "qwe");
|
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "qwe");
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ public class CryptorImplTest {
|
|||||||
final String testMasterKey = "{\"version\":-1,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
final String testMasterKey = "{\"version\":-1,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||||
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||||
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||||
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
|
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
|
||||||
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
||||||
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
|
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ public class CryptorImplTest {
|
|||||||
@Ignore
|
@Ignore
|
||||||
@Test(expected = UnsupportedVaultFormatException.class)
|
@Test(expected = UnsupportedVaultFormatException.class)
|
||||||
public void testMasterkeyDecryptionWithMissingVersionMac() throws IOException {
|
public void testMasterkeyDecryptionWithMissingVersionMac() throws IOException {
|
||||||
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||||
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||||
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"}";
|
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"}";
|
||||||
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
||||||
@@ -62,20 +62,20 @@ public class CryptorImplTest {
|
|||||||
@Ignore
|
@Ignore
|
||||||
@Test(expected = UnsupportedVaultFormatException.class)
|
@Test(expected = UnsupportedVaultFormatException.class)
|
||||||
public void testMasterkeyDecryptionWithWrongVersionMac() throws IOException {
|
public void testMasterkeyDecryptionWithWrongVersionMac() throws IOException {
|
||||||
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||||
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||||
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||||
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLa=\"}";
|
+ "\"versionMac\":\"z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfoK=\"}";
|
||||||
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
||||||
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
|
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testMasterkeyEncryption() throws IOException {
|
public void testMasterkeyEncryption() throws IOException {
|
||||||
final String expectedMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," //
|
final String expectedMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," //
|
||||||
+ "\"primaryMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
|
+ "\"primaryMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
|
||||||
+ "\"hmacMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
|
+ "\"hmacMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
|
||||||
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
|
+ "\"versionMac\":\"yuwoRE9GSdgQ2b//qRpTCj3W0qsVLxYVa7/KB3PkfA4=\"}";
|
||||||
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
||||||
cryptor.randomizeMasterkey();
|
cryptor.randomizeMasterkey();
|
||||||
final byte[] masterkeyFile = cryptor.writeKeysToMasterkeyFile("asd");
|
final byte[] masterkeyFile = cryptor.writeKeysToMasterkeyFile("asd");
|
||||||
|
|||||||
@@ -43,20 +43,6 @@ public class FileContentCryptorImplTest {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private static final SecureRandom RANDOM_MOCK_2 = new SecureRandom() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int nextInt(int bound) {
|
|
||||||
return 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void nextBytes(byte[] bytes) {
|
|
||||||
Arrays.fill(bytes, (byte) 0x00);
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
@Test(expected = IllegalArgumentException.class)
|
@Test(expected = IllegalArgumentException.class)
|
||||||
public void testShortHeaderInDecryptor() throws InterruptedException {
|
public void testShortHeaderInDecryptor() throws InterruptedException {
|
||||||
final byte[] keyBytes = new byte[32];
|
final byte[] keyBytes = new byte[32];
|
||||||
@@ -137,45 +123,6 @@ public class FileContentCryptorImplTest {
|
|||||||
Assert.assertArrayEquals("cleartext message".getBytes(), result);
|
Assert.assertArrayEquals("cleartext message".getBytes(), result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testEncryptionAndDecryptionWithSizeObfuscationPadding() throws InterruptedException {
|
|
||||||
final byte[] keyBytes = new byte[32];
|
|
||||||
final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES");
|
|
||||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
|
||||||
FileContentCryptor cryptor = new FileContentCryptorImpl(encryptionKey, macKey, RANDOM_MOCK_2);
|
|
||||||
|
|
||||||
ByteBuffer header = ByteBuffer.allocate(cryptor.getHeaderSize());
|
|
||||||
ByteBuffer ciphertext = ByteBuffer.allocate(16 + 11 + 500 + 32 + 1); // 16 bytes iv + 11 bytes ciphertext + 500 bytes padding + 32 bytes mac + 1.
|
|
||||||
try (FileContentEncryptor encryptor = cryptor.createFileContentEncryptor(Optional.empty(), 0)) {
|
|
||||||
encryptor.append(ByteBuffer.wrap("hello world".getBytes()));
|
|
||||||
encryptor.append(FileContentCryptor.EOF);
|
|
||||||
ByteBuffer buf;
|
|
||||||
while ((buf = encryptor.ciphertext()) != FileContentCryptor.EOF) {
|
|
||||||
ByteBuffers.copy(buf, ciphertext);
|
|
||||||
}
|
|
||||||
ByteBuffers.copy(encryptor.getHeader(), header);
|
|
||||||
}
|
|
||||||
header.flip();
|
|
||||||
ciphertext.flip();
|
|
||||||
|
|
||||||
Assert.assertEquals(16 + 11 + 500 + 32, ciphertext.remaining());
|
|
||||||
|
|
||||||
ByteBuffer plaintext = ByteBuffer.allocate(12); // 11 bytes plaintext + 1
|
|
||||||
try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header, 0, true)) {
|
|
||||||
decryptor.append(ciphertext);
|
|
||||||
decryptor.append(FileContentCryptor.EOF);
|
|
||||||
ByteBuffer buf;
|
|
||||||
while ((buf = decryptor.cleartext()) != FileContentCryptor.EOF) {
|
|
||||||
ByteBuffers.copy(buf, plaintext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
plaintext.flip();
|
|
||||||
|
|
||||||
byte[] result = new byte[plaintext.remaining()];
|
|
||||||
plaintext.get(result);
|
|
||||||
Assert.assertArrayEquals("hello world".getBytes(), result);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout = 20000) // assuming a minimum speed of 10mb/s during encryption and decryption 20s should be enough
|
@Test(timeout = 20000) // assuming a minimum speed of 10mb/s during encryption and decryption 20s should be enough
|
||||||
public void testEncryptionAndDecryptionSpeed() throws InterruptedException, IOException {
|
public void testEncryptionAndDecryptionSpeed() throws InterruptedException, IOException {
|
||||||
final byte[] keyBytes = new byte[32];
|
final byte[] keyBytes = new byte[32];
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ public class FileContentDecryptorImplTest {
|
|||||||
final byte[] keyBytes = new byte[32];
|
final byte[] keyBytes = new byte[32];
|
||||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
|
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
|
||||||
final byte[] content = Base64.decode("AAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3Og=");
|
final byte[] content = Base64.decode("AAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3Og=");
|
||||||
|
|
||||||
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
|
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
|
||||||
@@ -68,7 +68,7 @@ public class FileContentDecryptorImplTest {
|
|||||||
final byte[] keyBytes = new byte[32];
|
final byte[] keyBytes = new byte[32];
|
||||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJa==");
|
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJG==");
|
||||||
|
|
||||||
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
|
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ public class FileContentDecryptorImplTest {
|
|||||||
final byte[] keyBytes = new byte[32];
|
final byte[] keyBytes = new byte[32];
|
||||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
|
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
|
||||||
final byte[] content = Base64.decode("aAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3Og=");
|
final byte[] content = Base64.decode("aAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3Og=");
|
||||||
|
|
||||||
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
|
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
|
||||||
@@ -101,7 +101,7 @@ public class FileContentDecryptorImplTest {
|
|||||||
final byte[] keyBytes = new byte[32];
|
final byte[] keyBytes = new byte[32];
|
||||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
|
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
|
||||||
final byte[] content = Base64.decode("AAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3OG=");
|
final byte[] content = Base64.decode("AAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3OG=");
|
||||||
|
|
||||||
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, false)) {
|
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, false)) {
|
||||||
@@ -124,7 +124,7 @@ public class FileContentDecryptorImplTest {
|
|||||||
final byte[] keyBytes = new byte[32];
|
final byte[] keyBytes = new byte[32];
|
||||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
|
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
|
||||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
|
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
|
||||||
|
|
||||||
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
|
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
|
||||||
decryptor.cancelWithException(new IOException("can not do"));
|
decryptor.cancelWithException(new IOException("can not do"));
|
||||||
|
|||||||
@@ -35,20 +35,6 @@ public class FileContentEncryptorImplTest {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private static final SecureRandom RANDOM_MOCK_2 = new SecureRandom() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int nextInt(int bound) {
|
|
||||||
return 42;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void nextBytes(byte[] bytes) {
|
|
||||||
Arrays.fill(bytes, (byte) 0x00);
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testEncryption() throws InterruptedException {
|
public void testEncryption() throws InterruptedException {
|
||||||
final byte[] keyBytes = new byte[32];
|
final byte[] keyBytes = new byte[32];
|
||||||
@@ -95,24 +81,4 @@ public class FileContentEncryptorImplTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testSizeObfuscation() throws InterruptedException {
|
|
||||||
final byte[] keyBytes = new byte[32];
|
|
||||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
|
||||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
|
|
||||||
|
|
||||||
try (FileContentEncryptor encryptor = new FileContentEncryptorImpl(headerKey, macKey, RANDOM_MOCK_2, 0)) {
|
|
||||||
encryptor.append(FileContentCryptor.EOF);
|
|
||||||
|
|
||||||
ByteBuffer result = ByteBuffer.allocate(91); // 16 bytes iv + 42 bytes size obfuscation + 32 bytes mac + 1
|
|
||||||
ByteBuffer buf;
|
|
||||||
while ((buf = encryptor.ciphertext()) != FileContentCryptor.EOF) {
|
|
||||||
ByteBuffers.copy(buf, result);
|
|
||||||
}
|
|
||||||
result.flip();
|
|
||||||
|
|
||||||
Assert.assertEquals(90, result.remaining());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,13 +53,26 @@ public class FileHeaderTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testDecryption() {
|
public void testDecryption() {
|
||||||
|
final byte[] keyBytes = new byte[32];
|
||||||
|
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||||
|
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||||
|
final ByteBuffer headerBuf = ByteBuffer.wrap(Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg=="));
|
||||||
|
final FileHeader header = FileHeader.decrypt(headerKey, new ThreadLocalMac(macKey, "HmacSHA256"), headerBuf);
|
||||||
|
|
||||||
|
Assert.assertEquals(-1l, header.getPayload().getFilesize());
|
||||||
|
Assert.assertArrayEquals(new byte[16], header.getIv());
|
||||||
|
Assert.assertArrayEquals(new byte[32], header.getPayload().getContentKey().getEncoded());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDecryptionOfOldHeader() {
|
||||||
final byte[] keyBytes = new byte[32];
|
final byte[] keyBytes = new byte[32];
|
||||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||||
final ByteBuffer headerBuf = ByteBuffer.wrap(Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA=="));
|
final ByteBuffer headerBuf = ByteBuffer.wrap(Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA=="));
|
||||||
final FileHeader header = FileHeader.decrypt(headerKey, new ThreadLocalMac(macKey, "HmacSHA256"), headerBuf);
|
final FileHeader header = FileHeader.decrypt(headerKey, new ThreadLocalMac(macKey, "HmacSHA256"), headerBuf);
|
||||||
|
|
||||||
Assert.assertEquals(42, header.getPayload().getFilesize());
|
Assert.assertEquals(42l, header.getPayload().getFilesize());
|
||||||
Assert.assertArrayEquals(new byte[16], header.getIv());
|
Assert.assertArrayEquals(new byte[16], header.getIv());
|
||||||
Assert.assertArrayEquals(new byte[32], header.getPayload().getContentKey().getEncoded());
|
Assert.assertArrayEquals(new byte[32], header.getPayload().getContentKey().getEncoded());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public class ConflictResolverTest {
|
|||||||
unrelatedFile = Mockito.mock(File.class);
|
unrelatedFile = Mockito.mock(File.class);
|
||||||
|
|
||||||
String canonicalFileName = encode.apply("test name").get();
|
String canonicalFileName = encode.apply("test name").get();
|
||||||
String canonicalFolderName = canonicalFileName + Constants.DIR_SUFFIX;
|
String canonicalFolderName = Constants.DIR_PREFIX + canonicalFileName;
|
||||||
String conflictingFileName = canonicalFileName + " (version 2)";
|
String conflictingFileName = canonicalFileName + " (version 2)";
|
||||||
String conflictingFolderName = canonicalFolderName + " (version 2)";
|
String conflictingFolderName = canonicalFolderName + " (version 2)";
|
||||||
String unrelatedName = "notBa$e32!";
|
String unrelatedName = "notBa$e32!";
|
||||||
@@ -70,6 +70,7 @@ public class ConflictResolverTest {
|
|||||||
Mockito.doReturn(Optional.of(folder)).when(unrelatedFile).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(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(canonicalFileName)).thenReturn(canonicalFile);
|
||||||
Mockito.when(folder.file(canonicalFolderName)).thenReturn(canonicalFolder);
|
Mockito.when(folder.file(canonicalFolderName)).thenReturn(canonicalFolder);
|
||||||
Mockito.when(folder.file(conflictingFileName)).thenReturn(conflictingFile);
|
Mockito.when(folder.file(conflictingFileName)).thenReturn(conflictingFile);
|
||||||
@@ -79,29 +80,29 @@ public class ConflictResolverTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCanonicalName() {
|
public void testCanonicalName() {
|
||||||
File resolved = conflictResolver.resolveIfNecessary(canonicalFile);
|
File result = conflictResolver.resolveIfNecessary(canonicalFile);
|
||||||
Assert.assertSame(canonicalFile, resolved);
|
Assert.assertSame(canonicalFile, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testUnrelatedName() {
|
public void testUnrelatedName() {
|
||||||
File resolved = conflictResolver.resolveIfNecessary(unrelatedFile);
|
File result = conflictResolver.resolveIfNecessary(unrelatedFile);
|
||||||
Assert.assertSame(unrelatedFile, resolved);
|
Assert.assertSame(unrelatedFile, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testConflictingFile() {
|
public void testConflictingFile() {
|
||||||
File resolved = conflictResolver.resolveIfNecessary(conflictingFile);
|
File result = conflictResolver.resolveIfNecessary(conflictingFile);
|
||||||
Mockito.verify(conflictingFile).moveTo(resolved);
|
Mockito.verify(conflictingFile).moveTo(resolved);
|
||||||
Assert.assertSame(resolved, resolved);
|
Assert.assertSame(resolved, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testConflictingFileIfCanonicalDoesntExist() {
|
public void testConflictingFileIfCanonicalDoesntExist() {
|
||||||
Mockito.when(canonicalFile.exists()).thenReturn(false);
|
Mockito.when(canonicalFile.exists()).thenReturn(false);
|
||||||
File resolved = conflictResolver.resolveIfNecessary(conflictingFile);
|
File result = conflictResolver.resolveIfNecessary(conflictingFile);
|
||||||
Mockito.verify(conflictingFile).moveTo(canonicalFile);
|
Mockito.verify(conflictingFile).moveTo(resolved);
|
||||||
Assert.assertSame(canonicalFile, resolved);
|
Assert.assertSame(resolved, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -121,9 +122,11 @@ public class ConflictResolverTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testConflictingFolderWithSameId() {
|
public void testConflictingFolderWithSameId() {
|
||||||
ReadableFile directoryId1 = Mockito.mock(ReadableFile.class);
|
ReadableFile directoryId1 = Mockito.mock(ReadableFile.class);
|
||||||
|
ReadableFile directoryId2 = Mockito.mock(ReadableFile.class);
|
||||||
Mockito.when(canonicalFolder.openReadable()).thenReturn(directoryId1);
|
Mockito.when(canonicalFolder.openReadable()).thenReturn(directoryId1);
|
||||||
Mockito.when(conflictingFolder.openReadable()).thenReturn(directoryId1);
|
Mockito.when(conflictingFolder.openReadable()).thenReturn(directoryId2);
|
||||||
Mockito.when(directoryId1.read(Mockito.any())).thenAnswer(new FillBufferAnswer("id1"));
|
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);
|
File result = conflictResolver.resolveIfNecessary(conflictingFolder);
|
||||||
Mockito.verify(conflictingFolder).delete();
|
Mockito.verify(conflictingFolder).delete();
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.1.0</version>
|
<version>1.2.3</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>filesystem-inmemory</artifactId>
|
<artifactId>filesystem-inmemory</artifactId>
|
||||||
<name>Cryptomator filesystem: In-memory mock</name>
|
<name>Cryptomator filesystem: In-memory mock</name>
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ class InMemoryFile extends InMemoryNode implements File {
|
|||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long size() throws UncheckedIOException {
|
||||||
|
return content.get().limit();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void moveTo(File destination) throws UncheckedIOException {
|
public void moveTo(File destination) throws UncheckedIOException {
|
||||||
if (destination instanceof InMemoryFile) {
|
if (destination instanceof InMemoryFile) {
|
||||||
@@ -103,7 +108,7 @@ class InMemoryFile extends InMemoryNode implements File {
|
|||||||
throw new UncheckedIOException(new FileAlreadyExistsException(k));
|
throw new UncheckedIOException(new FileAlreadyExistsException(k));
|
||||||
} else {
|
} else {
|
||||||
if (v == null) {
|
if (v == null) {
|
||||||
assert!content.get().hasRemaining();
|
assert !content.get().hasRemaining();
|
||||||
this.creationTime = Instant.now();
|
this.creationTime = Instant.now();
|
||||||
}
|
}
|
||||||
this.lastModified = Instant.now();
|
this.lastModified = Instant.now();
|
||||||
@@ -120,7 +125,7 @@ class InMemoryFile extends InMemoryNode implements File {
|
|||||||
// returning null removes the entry.
|
// returning null removes the entry.
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
assert!this.exists();
|
assert !this.exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -51,11 +51,6 @@ class InMemoryReadableFile implements ReadableFile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public long size() throws UncheckedIOException {
|
|
||||||
return contentGetter.get().limit();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void position(long position) throws UncheckedIOException {
|
public void position(long position) throws UncheckedIOException {
|
||||||
assert position < Integer.MAX_VALUE : "Can not use that big in-memory files.";
|
assert position < Integer.MAX_VALUE : "Can not use that big in-memory files.";
|
||||||
@@ -64,8 +59,10 @@ class InMemoryReadableFile implements ReadableFile {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws UncheckedIOException {
|
public void close() throws UncheckedIOException {
|
||||||
open.set(false);
|
if (open.get()) {
|
||||||
readLock.unlock();
|
open.set(false);
|
||||||
|
readLock.unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,9 +104,7 @@ public class InMemoryFileSystemTest {
|
|||||||
Assert.assertTrue(fooFile.exists());
|
Assert.assertTrue(fooFile.exists());
|
||||||
|
|
||||||
// check if size = 11 bytes
|
// check if size = 11 bytes
|
||||||
try (ReadableFile readable = fooFile.openReadable()) {
|
Assert.assertEquals(11, fooFile.size());
|
||||||
Assert.assertEquals(11, readable.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
// copy foo to bar
|
// copy foo to bar
|
||||||
File barFile = fs.file("bar.txt");
|
File barFile = fs.file("bar.txt");
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.1.0</version>
|
<version>1.2.3</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>filesystem-invariants-tests</artifactId>
|
<artifactId>filesystem-invariants-tests</artifactId>
|
||||||
<name>Cryptomator filesystem: Invariants tests</name>
|
<name>Cryptomator filesystem: Invariants tests</name>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.1.0</version>
|
<version>1.2.3</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>filesystem-nameshortening</artifactId>
|
<artifactId>filesystem-nameshortening</artifactId>
|
||||||
<name>Cryptomator filesystem: Name shortening layer</name>
|
<name>Cryptomator filesystem: Name shortening layer</name>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ final class ConflictResolver {
|
|||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ConflictResolver.class);
|
private static final Logger LOG = LoggerFactory.getLogger(ConflictResolver.class);
|
||||||
private static final String LONG_NAME_FILE_EXT = ".lng";
|
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 Pattern BASE32_PATTERN = Pattern.compile("^0?([A-Z2-7]{8})*[A-Z2-7=]{8}");
|
||||||
private static final int UUID_FIRST_GROUP_STRLEN = 8;
|
private static final int UUID_FIRST_GROUP_STRLEN = 8;
|
||||||
|
|
||||||
private ConflictResolver() {
|
private ConflictResolver() {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.1.0</version>
|
<version>1.2.3</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>filesystem-nio</artifactId>
|
<artifactId>filesystem-nio</artifactId>
|
||||||
<name>Cryptomator filesystem: NIO-based physical layer</name>
|
<name>Cryptomator filesystem: NIO-based physical layer</name>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.cryptomator.filesystem.nio;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.channels.AsynchronousFileChannel;
|
import java.nio.channels.AsynchronousFileChannel;
|
||||||
|
import java.nio.file.AccessDeniedException;
|
||||||
import java.nio.file.CopyOption;
|
import java.nio.file.CopyOption;
|
||||||
import java.nio.file.FileSystems;
|
import java.nio.file.FileSystems;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -16,6 +17,11 @@ import java.util.stream.Stream;
|
|||||||
|
|
||||||
class DefaultNioAccess implements NioAccess {
|
class DefaultNioAccess implements NioAccess {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long size(Path path) throws IOException {
|
||||||
|
return Files.size(path);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AsynchronousFileChannel open(Path path, OpenOption... options) throws IOException {
|
public AsynchronousFileChannel open(Path path, OpenOption... options) throws IOException {
|
||||||
return AsynchronousFileChannel.open(path, options);
|
return AsynchronousFileChannel.open(path, options);
|
||||||
@@ -53,7 +59,18 @@ class DefaultNioAccess implements NioAccess {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete(Path path) throws IOException {
|
public void delete(Path path) throws IOException {
|
||||||
Files.delete(path);
|
try {
|
||||||
|
Files.delete(path);
|
||||||
|
} catch (AccessDeniedException e) {
|
||||||
|
// workaround for https://github.com/cryptomator/cryptomator/issues/317
|
||||||
|
try {
|
||||||
|
if (path.toFile().delete())
|
||||||
|
return;
|
||||||
|
} catch (UnsupportedOperationException e2) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ interface NioAccess {
|
|||||||
|
|
||||||
public static final Holder<NioAccess> DEFAULT = new Holder<>(new DefaultNioAccess());
|
public static final Holder<NioAccess> DEFAULT = new Holder<>(new DefaultNioAccess());
|
||||||
|
|
||||||
|
long size(Path path) throws IOException;
|
||||||
|
|
||||||
AsynchronousFileChannel open(Path path, OpenOption... options) throws IOException;
|
AsynchronousFileChannel open(Path path, OpenOption... options) throws IOException;
|
||||||
|
|
||||||
boolean isRegularFile(Path path, LinkOption... options);
|
boolean isRegularFile(Path path, LinkOption... options);
|
||||||
|
|||||||
@@ -27,16 +27,33 @@ class NioFile extends NioNode implements File {
|
|||||||
sharedChannel = instanceFactory.sharedFileChannel(path, nioAccess);
|
sharedChannel = instanceFactory.sharedFileChannel(path, nioAccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long size() throws UncheckedIOException {
|
||||||
|
try {
|
||||||
|
return nioAccess.size(path);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ReadableFile openReadable() throws UncheckedIOException {
|
public ReadableFile openReadable() throws UncheckedIOException {
|
||||||
if (lock.getWriteHoldCount() > 0) {
|
if (lock.getWriteHoldCount() > 0) {
|
||||||
throw new IllegalStateException("Current thread is currently writing this file");
|
throw new IllegalStateException("Current thread is currently writing " + path);
|
||||||
}
|
}
|
||||||
if (lock.getReadHoldCount() > 0) {
|
if (lock.getReadHoldCount() > 0) {
|
||||||
throw new IllegalStateException("Current thread is already reading this file");
|
throw new IllegalStateException("Current thread is already reading " + path);
|
||||||
}
|
}
|
||||||
lock.readLock().lock();
|
lock.readLock().lock();
|
||||||
return instanceFactory.readableNioFile(path, sharedChannel, this::unlockReadLock);
|
ReadableFile result = null;
|
||||||
|
try {
|
||||||
|
result = instanceFactory.readableNioFile(path, sharedChannel, this::unlockReadLock);
|
||||||
|
} finally {
|
||||||
|
if (result == null) {
|
||||||
|
unlockReadLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void unlockReadLock() {
|
private void unlockReadLock() {
|
||||||
@@ -46,13 +63,21 @@ class NioFile extends NioNode implements File {
|
|||||||
@Override
|
@Override
|
||||||
public WritableFile openWritable() throws UncheckedIOException {
|
public WritableFile openWritable() throws UncheckedIOException {
|
||||||
if (lock.getWriteHoldCount() > 0) {
|
if (lock.getWriteHoldCount() > 0) {
|
||||||
throw new IllegalStateException("Current thread is already writing this file");
|
throw new IllegalStateException("Current thread is already writing " + path);
|
||||||
}
|
}
|
||||||
if (lock.getReadHoldCount() > 0) {
|
if (lock.getReadHoldCount() > 0) {
|
||||||
throw new IllegalStateException("Current thread is currently reading this file");
|
throw new IllegalStateException("Current thread is currently reading " + path);
|
||||||
}
|
}
|
||||||
lockWriteLock();
|
lockWriteLock();
|
||||||
return instanceFactory.writableNioFile(fileSystem(), path, sharedChannel, this::unlockWriteLock);
|
WritableFile result = null;
|
||||||
|
try {
|
||||||
|
result = instanceFactory.writableNioFile(fileSystem(), path, sharedChannel, this::unlockWriteLock);
|
||||||
|
} finally {
|
||||||
|
if (result == null) {
|
||||||
|
unlockWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// visible for testing
|
// visible for testing
|
||||||
|
|||||||
@@ -41,11 +41,6 @@ class ReadableNioFile implements ReadableFile {
|
|||||||
return open;
|
return open;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public long size() throws UncheckedIOException {
|
|
||||||
return channel.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void position(long position) throws UncheckedIOException {
|
public void position(long position) throws UncheckedIOException {
|
||||||
assertOpen();
|
assertOpen();
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import static org.mockito.Mockito.when;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
|
import java.nio.file.NoSuchFileException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.attribute.FileTime;
|
import java.nio.file.attribute.FileTime;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@@ -85,6 +86,27 @@ public class NioFileTest {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class Size {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSizeReturnsSizeOfRegularFile() throws IOException {
|
||||||
|
when(nioAccess.size(path)).thenReturn(42l);
|
||||||
|
|
||||||
|
assertThat(inTest.size(), is(42l));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSizeThrowsExceptionIfRegularFileThrowsException() throws IOException {
|
||||||
|
Throwable t = new NoSuchFileException("foo");
|
||||||
|
when(nioAccess.size(path)).thenThrow(t);
|
||||||
|
|
||||||
|
thrown.expect(UncheckedIOException.class);
|
||||||
|
thrown.expectCause(org.hamcrest.Matchers.sameInstance(t));
|
||||||
|
inTest.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public class Open {
|
public class Open {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -99,10 +121,11 @@ public class NioFileTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testOpenReadableInvokedBeforeInvokingAfterCloseOperationThrowsIllegalStateException() {
|
public void testOpenReadableInvokedBeforeInvokingAfterCloseOperationThrowsIllegalStateException() {
|
||||||
|
when(instanceFactory.readableNioFile(same(path), same(channel), any())).thenReturn(mock(ReadableNioFile.class));
|
||||||
inTest.openReadable();
|
inTest.openReadable();
|
||||||
|
|
||||||
thrown.expect(IllegalStateException.class);
|
thrown.expect(IllegalStateException.class);
|
||||||
thrown.expectMessage("already reading this file");
|
thrown.expectMessage("already reading " + path);
|
||||||
|
|
||||||
inTest.openReadable();
|
inTest.openReadable();
|
||||||
}
|
}
|
||||||
@@ -111,7 +134,7 @@ public class NioFileTest {
|
|||||||
public void testOpenReadableInvokedAfterAfterCloseOperationCreatesNewReadableFile() {
|
public void testOpenReadableInvokedAfterAfterCloseOperationCreatesNewReadableFile() {
|
||||||
ReadableNioFile readableNioFile = mock(ReadableNioFile.class);
|
ReadableNioFile readableNioFile = mock(ReadableNioFile.class);
|
||||||
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
||||||
when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(null, readableNioFile);
|
when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(mock(ReadableNioFile.class), readableNioFile);
|
||||||
inTest.openReadable();
|
inTest.openReadable();
|
||||||
captor.getValue().run();
|
captor.getValue().run();
|
||||||
|
|
||||||
@@ -122,10 +145,11 @@ public class NioFileTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testOpenReadableInvokedBeforeInvokingAfterCloseOperationOfOpenWritableThrowsIllegalStateException() {
|
public void testOpenReadableInvokedBeforeInvokingAfterCloseOperationOfOpenWritableThrowsIllegalStateException() {
|
||||||
|
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), any())).thenReturn(mock(WritableNioFile.class));
|
||||||
inTest.openWritable();
|
inTest.openWritable();
|
||||||
|
|
||||||
thrown.expect(IllegalStateException.class);
|
thrown.expect(IllegalStateException.class);
|
||||||
thrown.expectMessage("currently writing this file");
|
thrown.expectMessage("currently writing " + path);
|
||||||
|
|
||||||
inTest.openReadable();
|
inTest.openReadable();
|
||||||
}
|
}
|
||||||
@@ -133,7 +157,7 @@ public class NioFileTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testOpenReadableInvokedAfterInvokingAfterCloseOperationWorks() {
|
public void testOpenReadableInvokedAfterInvokingAfterCloseOperationWorks() {
|
||||||
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
||||||
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(null);
|
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(mock(WritableNioFile.class));
|
||||||
inTest.openWritable();
|
inTest.openWritable();
|
||||||
captor.getValue().run();
|
captor.getValue().run();
|
||||||
|
|
||||||
@@ -154,7 +178,7 @@ public class NioFileTest {
|
|||||||
public void testOpenWritableInvokedAfterAfterCloseOperationCreatesNewWritableFile() {
|
public void testOpenWritableInvokedAfterAfterCloseOperationCreatesNewWritableFile() {
|
||||||
WritableNioFile writableNioFile = mock(WritableNioFile.class);
|
WritableNioFile writableNioFile = mock(WritableNioFile.class);
|
||||||
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
||||||
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(null, writableNioFile);
|
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(mock(WritableNioFile.class), writableNioFile);
|
||||||
inTest.openWritable();
|
inTest.openWritable();
|
||||||
captor.getValue().run();
|
captor.getValue().run();
|
||||||
|
|
||||||
@@ -165,28 +189,31 @@ public class NioFileTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testOpenWritableInvokedBeforeInvokingAfterCloseOperationThrowsIllegalStateException() {
|
public void testOpenWritableInvokedBeforeInvokingAfterCloseOperationThrowsIllegalStateException() {
|
||||||
|
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), any())).thenReturn(mock(WritableNioFile.class));
|
||||||
inTest.openWritable();
|
inTest.openWritable();
|
||||||
|
|
||||||
thrown.expect(IllegalStateException.class);
|
thrown.expect(IllegalStateException.class);
|
||||||
thrown.expectMessage("already writing this file");
|
thrown.expectMessage("already writing " + path);
|
||||||
|
|
||||||
inTest.openWritable();
|
inTest.openWritable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testOpenWritableInvokedBeforeInvokingAfterCloseOperationFromOpenReadableThrowsIllegalStateException() {
|
public void testOpenWritableInvokedBeforeInvokingAfterCloseOperationFromOpenReadableThrowsIllegalStateException() {
|
||||||
|
when(instanceFactory.readableNioFile(same(path), same(channel), any())).thenReturn(mock(ReadableNioFile.class));
|
||||||
inTest.openReadable();
|
inTest.openReadable();
|
||||||
|
|
||||||
thrown.expect(IllegalStateException.class);
|
thrown.expect(IllegalStateException.class);
|
||||||
thrown.expectMessage("currently reading this file");
|
thrown.expectMessage("currently reading " + path);
|
||||||
|
|
||||||
inTest.openWritable();
|
inTest.openWritable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testOpenWritableInvokedAfterInvokingAfterCloseOperationWorks() {
|
public void testOpenWritableInvokedAfterInvokingAfterCloseOperationWorks() {
|
||||||
|
ReadableNioFile readableNioFile = mock(ReadableNioFile.class);
|
||||||
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
||||||
when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(null);
|
when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(readableNioFile);
|
||||||
inTest.openReadable();
|
inTest.openReadable();
|
||||||
captor.getValue().run();
|
captor.getValue().run();
|
||||||
|
|
||||||
|
|||||||
@@ -83,16 +83,6 @@ public class ReadableNioFileTest {
|
|||||||
inTest.position(-1);
|
inTest.position(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testSizeReturnsSizeOfChannel() {
|
|
||||||
long expectedSize = 85472;
|
|
||||||
when(channel.size()).thenReturn(expectedSize);
|
|
||||||
|
|
||||||
long actualSize = inTest.size();
|
|
||||||
|
|
||||||
assertThat(actualSize, is(expectedSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReadDelegatesToChannelReadFullyWithZeroPositionIfNotSet() {
|
public void testReadDelegatesToChannelReadFullyWithZeroPositionIfNotSet() {
|
||||||
ByteBuffer buffer = mock(ByteBuffer.class);
|
ByteBuffer buffer = mock(ByteBuffer.class);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.1.0</version>
|
<version>1.2.3</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>filesystem-stats</artifactId>
|
<artifactId>filesystem-stats</artifactId>
|
||||||
<name>Cryptomator filesystem: Throughput statistics</name>
|
<name>Cryptomator filesystem: Throughput statistics</name>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.1.0</version>
|
<version>1.2.3</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>frontend-api</artifactId>
|
<artifactId>frontend-api</artifactId>
|
||||||
<name>Cryptomator frontend: API</name>
|
<name>Cryptomator frontend: API</name>
|
||||||
|
|||||||
@@ -14,12 +14,20 @@ import java.util.Optional;
|
|||||||
public interface Frontend extends AutoCloseable {
|
public interface Frontend extends AutoCloseable {
|
||||||
|
|
||||||
public enum MountParam {
|
public enum MountParam {
|
||||||
MOUNT_NAME, HOSTNAME, WIN_DRIVE_LETTER
|
MOUNT_NAME, HOSTNAME, WIN_DRIVE_LETTER,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "dav" or "webdav"
|
||||||
|
*/
|
||||||
|
PREFERRED_GVFS_SCHEME
|
||||||
}
|
}
|
||||||
|
|
||||||
void mount(Map<MountParam, Optional<String>> map) throws CommandFailedException;
|
void mount(Map<MountParam, Optional<String>> map) throws CommandFailedException;
|
||||||
|
|
||||||
void unmount() throws CommandFailedException;
|
/**
|
||||||
|
* Unmounts the file system and stops any file system handler threads.
|
||||||
|
*/
|
||||||
|
void close() throws Exception;
|
||||||
|
|
||||||
void reveal() throws CommandFailedException;
|
void reveal() throws CommandFailedException;
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ public interface FrontendFactory {
|
|||||||
* Provides a new frontend to access the given folder.
|
* Provides a new frontend to access the given folder.
|
||||||
*
|
*
|
||||||
* @param root Root resource accessible through this frontend.
|
* @param root Root resource accessible through this frontend.
|
||||||
* @param uniqueName Name of the frontend, i.e. used to create subresources for the different frontends inside of a common virtual drive.
|
* @param id unique id of the frontend, i.e. used to generate a unique uri
|
||||||
|
* @param name Name of the frontend, i.e. used to generate a readable/recognizable name of a common virtual drive
|
||||||
* @return A new frontend
|
* @return A new frontend
|
||||||
* @throws FrontendCreationFailedException If creation was not possible.
|
* @throws FrontendCreationFailedException If creation was not possible.
|
||||||
*/
|
*/
|
||||||
Frontend create(Folder root, String uniqueName) throws FrontendCreationFailedException;
|
Frontend create(Folder root, FrontendId id, String name) throws FrontendCreationFailedException;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package org.cryptomator.frontend;
|
||||||
|
|
||||||
|
import static java.util.UUID.randomUUID;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class FrontendId implements Serializable {
|
||||||
|
|
||||||
|
public static final String FRONTEND_ID_PATTERN = "[a-zA-Z0-9_-]{12}";
|
||||||
|
|
||||||
|
public static FrontendId generate() {
|
||||||
|
return new FrontendId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FrontendId from(String value) {
|
||||||
|
return new FrontendId(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
private FrontendId() {
|
||||||
|
this(generateId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private FrontendId(String value) {
|
||||||
|
if (!value.matches(FRONTEND_ID_PATTERN)) {
|
||||||
|
throw new IllegalArgumentException("Invalid frontend id " + value);
|
||||||
|
}
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String generateId() {
|
||||||
|
return asBase64String(nineBytesFrom(randomUUID()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String asBase64String(ByteBuffer bytes) {
|
||||||
|
ByteBuffer base64Buffer = Base64.getUrlEncoder().encode(bytes);
|
||||||
|
return new String(asByteArray(base64Buffer), StandardCharsets.US_ASCII);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ByteBuffer nineBytesFrom(UUID uuid) {
|
||||||
|
ByteBuffer uuidBuffer = ByteBuffer.allocate(9);
|
||||||
|
uuidBuffer.putLong(uuid.getMostSignificantBits());
|
||||||
|
uuidBuffer.put((byte) (uuid.getLeastSignificantBits() & 0xFF));
|
||||||
|
uuidBuffer.flip();
|
||||||
|
return uuidBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] asByteArray(ByteBuffer buffer) {
|
||||||
|
if (buffer.hasArray()) {
|
||||||
|
return buffer.array();
|
||||||
|
} else {
|
||||||
|
byte[] bytes = new byte[buffer.remaining()];
|
||||||
|
buffer.get(bytes);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (obj == null || obj.getClass() != getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return obj == this || internalEquals((FrontendId) obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean internalEquals(FrontendId obj) {
|
||||||
|
return value.equals(obj.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return value.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.1.0</version>
|
<version>1.2.3</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>frontend-webdav</artifactId>
|
<artifactId>frontend-webdav</artifactId>
|
||||||
<name>Cryptomator frontend: WebDAV frontend</name>
|
<name>Cryptomator frontend: WebDAV frontend</name>
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package org.cryptomator.frontend.webdav;
|
||||||
|
|
||||||
|
import static java.lang.String.format;
|
||||||
|
import static org.cryptomator.frontend.FrontendId.FRONTEND_ID_PATTERN;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.cryptomator.frontend.FrontendId;
|
||||||
|
|
||||||
|
class ContextPaths {
|
||||||
|
|
||||||
|
private static final Pattern SERVLET_PATH_WITH_FRONTEND_ID_PATTERN = Pattern.compile("^/(" + FRONTEND_ID_PATTERN + ")(/.*)?$");
|
||||||
|
private static final int FRONTEND_ID_GROUP = 1;
|
||||||
|
|
||||||
|
public static String from(FrontendId id, String name) {
|
||||||
|
return format("/%s/%s", id, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String removeFrontendId(String path) {
|
||||||
|
return path.replaceAll("/" + FRONTEND_ID_PATTERN + "/", "/[...]/");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<FrontendId> extractFrontendId(String path) {
|
||||||
|
Matcher matcher = SERVLET_PATH_WITH_FRONTEND_ID_PATTERN.matcher(path);
|
||||||
|
if (matcher.matches()) {
|
||||||
|
return Optional.of(FrontendId.from(matcher.group(FRONTEND_ID_GROUP)));
|
||||||
|
} else {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ package org.cryptomator.frontend.webdav;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
import javax.servlet.DispatcherType;
|
import javax.servlet.DispatcherType;
|
||||||
import javax.servlet.ServletException;
|
import javax.servlet.ServletException;
|
||||||
import javax.servlet.http.HttpServlet;
|
import javax.servlet.http.HttpServlet;
|
||||||
@@ -21,26 +23,38 @@ import org.cryptomator.frontend.webdav.filters.LoopbackFilter;
|
|||||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||||
import org.eclipse.jetty.servlet.ServletHolder;
|
import org.eclipse.jetty.servlet.ServletHolder;
|
||||||
|
|
||||||
/**
|
@Singleton
|
||||||
* The server needs to respond to requests to the root resource, because Windows is stupid.
|
class DefaultServlet extends HttpServlet {
|
||||||
*/
|
|
||||||
public class WindowsCompatibilityServlet extends HttpServlet {
|
|
||||||
|
|
||||||
private static final String ROOT_PATH = "/";
|
private static final String ROOT_PATH = "/";
|
||||||
|
private static final String WILDCARD = "/*";
|
||||||
|
|
||||||
|
private final Tarpit tarpit;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public DefaultServlet(Tarpit tarpit) {
|
||||||
|
this.tarpit = tarpit;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||||
|
tarpit.handle(req);
|
||||||
|
super.service(req, resp);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||||
resp.addHeader("DAV", "1, 2");
|
resp.addHeader("DAV", "1, 2");
|
||||||
resp.addHeader("MS-Author-Via", "DAV");
|
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);
|
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ServletContextHandler createServletContextHandler() {
|
public ServletContextHandler createServletContextHandler() {
|
||||||
final ServletContextHandler servletContext = new ServletContextHandler(null, ROOT_PATH, ServletContextHandler.NO_SESSIONS);
|
final ServletContextHandler servletContext = new ServletContextHandler(null, ROOT_PATH, ServletContextHandler.NO_SESSIONS);
|
||||||
final ServletHolder servletHolder = new ServletHolder(ROOT_PATH, WindowsCompatibilityServlet.class);
|
final ServletHolder servletHolder = new ServletHolder(ROOT_PATH, this);
|
||||||
servletContext.addServlet(servletHolder, ROOT_PATH);
|
servletContext.addServlet(servletHolder, ROOT_PATH);
|
||||||
servletContext.addFilter(LoopbackFilter.class, ROOT_PATH, EnumSet.of(DispatcherType.REQUEST));
|
servletContext.addFilter(LoopbackFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST));
|
||||||
return servletContext;
|
return servletContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package org.cryptomator.frontend.webdav;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.server.HandlerContainer;
|
||||||
|
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||||
|
|
||||||
|
class FontendIdHidingServletContextHandler extends ServletContextHandler {
|
||||||
|
|
||||||
|
public FontendIdHidingServletContextHandler(HandlerContainer parent, String contextPath, int options) {
|
||||||
|
super(parent, contextPath, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return ContextPaths.removeFrontendId(super.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (c) 2016 Markus Kreusch
|
||||||
|
* This file is licensed under the terms of the MIT license.
|
||||||
|
* See the LICENSE.txt file for more info.
|
||||||
|
*******************************************************************************/
|
||||||
|
package org.cryptomator.frontend.webdav;
|
||||||
|
|
||||||
|
import static java.lang.Math.max;
|
||||||
|
import static java.lang.System.currentTimeMillis;
|
||||||
|
import static java.util.Collections.synchronizedSet;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
import org.cryptomator.frontend.FrontendId;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class Tarpit implements Serializable {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(Tarpit.class);
|
||||||
|
private static final long DELAY_MS = 10000;
|
||||||
|
|
||||||
|
private final Set<FrontendId> validFrontendIds = synchronizedSet(new HashSet<>());
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public Tarpit() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValidFrontendIds(Collection<FrontendId> validFrontendIds) {
|
||||||
|
this.validFrontendIds.retainAll(validFrontendIds);
|
||||||
|
this.validFrontendIds.addAll(validFrontendIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handle(HttpServletRequest req) {
|
||||||
|
if (isRequestWithInvalidVaultId(req)) {
|
||||||
|
delayExecutionUninterruptibly();
|
||||||
|
LOG.debug("Delayed request to " + req.getRequestURI() + " by " + DELAY_MS + "ms");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRequestWithInvalidVaultId(HttpServletRequest req) {
|
||||||
|
Optional<FrontendId> frontendId = ContextPaths.extractFrontendId(req.getServletPath());
|
||||||
|
return frontendId.isPresent() && !isValid(frontendId.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void delayExecutionUninterruptibly() {
|
||||||
|
long expected = currentTimeMillis() + DELAY_MS;
|
||||||
|
long sleepTime = DELAY_MS;
|
||||||
|
while (expected > currentTimeMillis()) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(sleepTime);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
sleepTime = max(0, currentTimeMillis() - expected + 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValid(FrontendId frontendId) {
|
||||||
|
return validFrontendIds.contains(frontendId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ class WebDavFrontend implements Frontend {
|
|||||||
private final WebDavMounterProvider webdavMounterProvider;
|
private final WebDavMounterProvider webdavMounterProvider;
|
||||||
private final ServletContextHandler handler;
|
private final ServletContextHandler handler;
|
||||||
private final URI uri;
|
private final URI uri;
|
||||||
|
|
||||||
private WebDavMount mount;
|
private WebDavMount mount;
|
||||||
|
|
||||||
public WebDavFrontend(WebDavMounterProvider webdavMounterProvider, ServletContextHandler handler, URI uri) throws FrontendCreationFailedException {
|
public WebDavFrontend(WebDavMounterProvider webdavMounterProvider, ServletContextHandler handler, URI uri) throws FrontendCreationFailedException {
|
||||||
@@ -45,13 +46,13 @@ class WebDavFrontend implements Frontend {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void mount(Map<MountParam, Optional<String>> mountParams) throws CommandFailedException {
|
public void mount(Map<MountParam, Optional<String>> mountParams) throws CommandFailedException {
|
||||||
mount = webdavMounterProvider.get().mount(uri, mountParams);
|
mount = webdavMounterProvider.chooseMounter(mountParams).mount(uri, mountParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void unmount() throws CommandFailedException {
|
||||||
public void unmount() throws CommandFailedException {
|
|
||||||
if (mount != null) {
|
if (mount != null) {
|
||||||
mount.unmount();
|
mount.unmount();
|
||||||
|
mount = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.cryptomator.frontend.webdav;
|
||||||
|
|
||||||
|
import org.cryptomator.common.CommonsModule;
|
||||||
|
import org.cryptomator.frontend.webdav.mount.WebDavMounterModule;
|
||||||
|
|
||||||
|
import dagger.Module;
|
||||||
|
|
||||||
|
@Module(includes = {CommonsModule.class, WebDavMounterModule.class})
|
||||||
|
public class WebDavModule {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ package org.cryptomator.frontend.webdav;
|
|||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.concurrent.BlockingQueue;
|
import java.util.concurrent.BlockingQueue;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ import org.cryptomator.filesystem.Folder;
|
|||||||
import org.cryptomator.frontend.Frontend;
|
import org.cryptomator.frontend.Frontend;
|
||||||
import org.cryptomator.frontend.FrontendCreationFailedException;
|
import org.cryptomator.frontend.FrontendCreationFailedException;
|
||||||
import org.cryptomator.frontend.FrontendFactory;
|
import org.cryptomator.frontend.FrontendFactory;
|
||||||
|
import org.cryptomator.frontend.FrontendId;
|
||||||
import org.cryptomator.frontend.webdav.mount.WebDavMounterProvider;
|
import org.cryptomator.frontend.webdav.mount.WebDavMounterProvider;
|
||||||
import org.eclipse.jetty.server.Connector;
|
import org.eclipse.jetty.server.Connector;
|
||||||
import org.eclipse.jetty.server.Server;
|
import org.eclipse.jetty.server.Server;
|
||||||
@@ -45,9 +47,10 @@ public class WebDavServer implements FrontendFactory {
|
|||||||
private final ContextHandlerCollection servletCollection;
|
private final ContextHandlerCollection servletCollection;
|
||||||
private final WebDavServletContextFactory servletContextFactory;
|
private final WebDavServletContextFactory servletContextFactory;
|
||||||
private final WebDavMounterProvider webdavMounterProvider;
|
private final WebDavMounterProvider webdavMounterProvider;
|
||||||
|
private final Tarpit tarpit;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
WebDavServer(WebDavServletContextFactory servletContextFactory, WebDavMounterProvider webdavMounterProvider) {
|
WebDavServer(WebDavServletContextFactory servletContextFactory, WebDavMounterProvider webdavMounterProvider, DefaultServlet defaultServlet, Tarpit tarpit) {
|
||||||
final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(MAX_PENDING_REQUESTS);
|
final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(MAX_PENDING_REQUESTS);
|
||||||
final ThreadPool tp = new QueuedThreadPool(MAX_THREADS, MIN_THREADS, THREAD_IDLE_SECONDS, queue);
|
final ThreadPool tp = new QueuedThreadPool(MAX_THREADS, MIN_THREADS, THREAD_IDLE_SECONDS, queue);
|
||||||
this.server = new Server(tp);
|
this.server = new Server(tp);
|
||||||
@@ -55,8 +58,9 @@ public class WebDavServer implements FrontendFactory {
|
|||||||
this.servletCollection = new ContextHandlerCollection();
|
this.servletCollection = new ContextHandlerCollection();
|
||||||
this.servletContextFactory = servletContextFactory;
|
this.servletContextFactory = servletContextFactory;
|
||||||
this.webdavMounterProvider = webdavMounterProvider;
|
this.webdavMounterProvider = webdavMounterProvider;
|
||||||
|
this.tarpit = tarpit;
|
||||||
servletCollection.addHandler(WindowsCompatibilityServlet.createServletContextHandler());
|
|
||||||
|
servletCollection.addHandler(defaultServlet.createServletContextHandler());
|
||||||
server.setConnectors(new Connector[] {localConnector});
|
server.setConnectors(new Connector[] {localConnector});
|
||||||
server.setHandler(servletCollection);
|
server.setHandler(servletCollection);
|
||||||
}
|
}
|
||||||
@@ -103,10 +107,8 @@ public class WebDavServer implements FrontendFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Frontend create(Folder root, String contextPath) throws FrontendCreationFailedException {
|
public Frontend create(Folder root, FrontendId id, String name) throws FrontendCreationFailedException {
|
||||||
if (!contextPath.startsWith("/")) {
|
String contextPath = ContextPaths.from(id, name);
|
||||||
throw new IllegalArgumentException("contextPath must begin with '/'");
|
|
||||||
}
|
|
||||||
final URI uri;
|
final URI uri;
|
||||||
try {
|
try {
|
||||||
uri = new URI("http", null, "localhost", getPort(), contextPath, null, null);
|
uri = new URI("http", null, "localhost", getPort(), contextPath, null, null);
|
||||||
@@ -114,8 +116,12 @@ public class WebDavServer implements FrontendFactory {
|
|||||||
throw new IllegalStateException(e);
|
throw new IllegalStateException(e);
|
||||||
}
|
}
|
||||||
final ServletContextHandler handler = addServlet(root, uri);
|
final ServletContextHandler handler = addServlet(root, uri);
|
||||||
LOG.info("Servlet available under " + uri);
|
LOG.info("Servlet available under " + ContextPaths.removeFrontendId(uri.toString()));
|
||||||
return new WebDavFrontend(webdavMounterProvider, handler, uri);
|
return new WebDavFrontend(webdavMounterProvider, handler, uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setValidFrontendIds(Collection<FrontendId> validFrontendIds) {
|
||||||
|
tarpit.setValidFrontendIds(validFrontendIds);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import org.cryptomator.frontend.webdav.filters.AcceptRangeFilter;
|
|||||||
import org.cryptomator.frontend.webdav.filters.LoopbackFilter;
|
import org.cryptomator.frontend.webdav.filters.LoopbackFilter;
|
||||||
import org.cryptomator.frontend.webdav.filters.MacChunkedPutCompatibilityFilter;
|
import org.cryptomator.frontend.webdav.filters.MacChunkedPutCompatibilityFilter;
|
||||||
import org.cryptomator.frontend.webdav.filters.MkcolComplianceFilter;
|
import org.cryptomator.frontend.webdav.filters.MkcolComplianceFilter;
|
||||||
|
import org.cryptomator.frontend.webdav.filters.PostRequestBlockingFilter;
|
||||||
import org.cryptomator.frontend.webdav.filters.UriNormalizationFilter;
|
import org.cryptomator.frontend.webdav.filters.UriNormalizationFilter;
|
||||||
import org.cryptomator.frontend.webdav.filters.UriNormalizationFilter.ResourceTypeChecker;
|
import org.cryptomator.frontend.webdav.filters.UriNormalizationFilter.ResourceTypeChecker;
|
||||||
import org.cryptomator.frontend.webdav.filters.UriNormalizationFilter.ResourceTypeChecker.ResourceType;
|
import org.cryptomator.frontend.webdav.filters.UriNormalizationFilter.ResourceTypeChecker.ResourceType;
|
||||||
@@ -63,10 +64,11 @@ class WebDavServletContextFactory {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
final String contextPath = StringUtils.removeEnd(contextRoot.getPath(), "/");
|
final String contextPath = StringUtils.removeEnd(contextRoot.getPath(), "/");
|
||||||
final ServletContextHandler servletContext = new ServletContextHandler(null, contextPath, ServletContextHandler.SESSIONS);
|
final ServletContextHandler servletContext = new FontendIdHidingServletContextHandler(null, contextPath, ServletContextHandler.SESSIONS);
|
||||||
final ServletHolder servletHolder = new ServletHolder(contextPath, new WebDavServlet(contextRoot, root));
|
final ServletHolder servletHolder = new ServletHolder(contextPath, new WebDavServlet(contextRoot, root));
|
||||||
servletContext.addServlet(servletHolder, WILDCARD);
|
servletContext.addServlet(servletHolder, WILDCARD);
|
||||||
servletContext.addFilter(LoopbackFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST));
|
servletContext.addFilter(LoopbackFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST));
|
||||||
|
servletContext.addFilter(PostRequestBlockingFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST));
|
||||||
servletContext.addFilter(MkcolComplianceFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST));
|
servletContext.addFilter(MkcolComplianceFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST));
|
||||||
servletContext.addFilter(AcceptRangeFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST));
|
servletContext.addFilter(AcceptRangeFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST));
|
||||||
servletContext.addFilter(new FilterHolder(new UriNormalizationFilter(resourceTypeChecker)), WILDCARD, EnumSet.of(DispatcherType.REQUEST));
|
servletContext.addFilter(new FilterHolder(new UriNormalizationFilter(resourceTypeChecker)), WILDCARD, EnumSet.of(DispatcherType.REQUEST));
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package org.cryptomator.frontend.webdav.filters;
|
||||||
|
|
||||||
|
import static java.util.Arrays.stream;
|
||||||
|
import static java.util.function.Predicate.isEqual;
|
||||||
|
import static java.util.stream.Collectors.joining;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import javax.servlet.http.HttpServletResponseWrapper;
|
||||||
|
|
||||||
|
class PostFromAllowHeaderRemovingHttpServletResponseWrapper extends HttpServletResponseWrapper {
|
||||||
|
|
||||||
|
public PostFromAllowHeaderRemovingHttpServletResponseWrapper(HttpServletResponse response) {
|
||||||
|
super(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addHeader(String name, String value) {
|
||||||
|
if (isAllowHeader(name)) {
|
||||||
|
super.setHeader(name, removePost(value));
|
||||||
|
} else {
|
||||||
|
super.addHeader(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHeader(String name, String value) {
|
||||||
|
if (isAllowHeader(name)) {
|
||||||
|
super.setHeader(name, removePost(value));
|
||||||
|
} else {
|
||||||
|
super.setHeader(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String removePost(String value) {
|
||||||
|
return stream(value.split("\\s*,\\s*"))
|
||||||
|
.filter(isEqual("POST").negate())
|
||||||
|
.collect(joining(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAllowHeader(String name) {
|
||||||
|
return "allow".equalsIgnoreCase(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* 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.frontend.webdav.filters;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.FilterConfig;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocks all post requests.
|
||||||
|
*/
|
||||||
|
public class PostRequestBlockingFilter implements HttpFilter {
|
||||||
|
|
||||||
|
private static final String POST_METHOD = "POST";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(FilterConfig filterConfig) throws ServletException {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
if (isPost(request)) {
|
||||||
|
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
|
||||||
|
} else {
|
||||||
|
chain.doFilter(request, new PostFromAllowHeaderRemovingHttpServletResponseWrapper(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPost(HttpServletRequest request) {
|
||||||
|
return POST_METHOD.equalsIgnoreCase(request.getMethod());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -32,14 +32,16 @@ import org.cryptomator.filesystem.File;
|
|||||||
import org.cryptomator.filesystem.Folder;
|
import org.cryptomator.filesystem.Folder;
|
||||||
import org.cryptomator.filesystem.ReadableFile;
|
import org.cryptomator.filesystem.ReadableFile;
|
||||||
import org.cryptomator.filesystem.jackrabbit.FileLocator;
|
import org.cryptomator.filesystem.jackrabbit.FileLocator;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import com.google.common.io.ByteStreams;
|
import com.google.common.io.ByteStreams;
|
||||||
|
|
||||||
class DavFile extends DavNode<FileLocator> {
|
class DavFile extends DavNode<FileLocator> {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(DavFile.class);
|
protected static final String CONTENT_TYPE_VALUE = "application/octet-stream";
|
||||||
|
protected static final String CONTENT_DISPOSITION_HEADER = "Content-Disposition";
|
||||||
|
protected static final String CONTENT_DISPOSITION_VALUE = "attachment";
|
||||||
|
protected static final String X_CONTENT_TYPE_OPTIONS_HEADER = "X-Content-Type-Options";
|
||||||
|
protected static final String X_CONTENT_TYPE_OPTIONS_VALUE = "nosniff";
|
||||||
|
|
||||||
public DavFile(FilesystemResourceFactory factory, LockManager lockManager, DavSession session, FileLocator node) {
|
public DavFile(FilesystemResourceFactory factory, LockManager lockManager, DavSession session, FileLocator node) {
|
||||||
super(factory, lockManager, session, node);
|
super(factory, lockManager, session, node);
|
||||||
@@ -56,8 +58,11 @@ class DavFile extends DavNode<FileLocator> {
|
|||||||
if (!outputContext.hasStream()) {
|
if (!outputContext.hasStream()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
outputContext.setContentType(CONTENT_TYPE_VALUE);
|
||||||
|
outputContext.setProperty(CONTENT_DISPOSITION_HEADER, CONTENT_DISPOSITION_VALUE);
|
||||||
|
outputContext.setProperty(X_CONTENT_TYPE_OPTIONS_HEADER, X_CONTENT_TYPE_OPTIONS_VALUE);
|
||||||
|
outputContext.setContentLength(node.size());
|
||||||
try (ReadableFile src = node.openReadable(); WritableByteChannel dst = Channels.newChannel(outputContext.getOutputStream())) {
|
try (ReadableFile src = node.openReadable(); WritableByteChannel dst = Channels.newChannel(outputContext.getOutputStream())) {
|
||||||
outputContext.setContentLength(src.size());
|
|
||||||
ByteStreams.copy(src, dst);
|
ByteStreams.copy(src, dst);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,12 +154,7 @@ class DavFile extends DavNode<FileLocator> {
|
|||||||
|
|
||||||
private Optional<DavProperty<?>> sizeProperty() {
|
private Optional<DavProperty<?>> sizeProperty() {
|
||||||
if (node.exists()) {
|
if (node.exists()) {
|
||||||
try (ReadableFile src = node.openReadable()) {
|
return Optional.of(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, node.size()));
|
||||||
return Optional.of(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, src.size()));
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
LOG.warn("Could not determine file size of " + getResourcePath(), e);
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ class DavFileWithRange extends DavFile {
|
|||||||
if (!outputContext.hasStream()) {
|
if (!outputContext.hasStream()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
final long contentLength = node.size();
|
||||||
try (ReadableFile src = node.openReadable(); OutputStream out = outputContext.getOutputStream()) {
|
try (ReadableFile src = node.openReadable(); OutputStream out = outputContext.getOutputStream()) {
|
||||||
final long contentLength = src.size();
|
|
||||||
final Pair<Long, Long> range = getEffectiveRange(contentLength);
|
final Pair<Long, Long> range = getEffectiveRange(contentLength);
|
||||||
if (range.getLeft() < 0 || range.getLeft() > range.getRight() || range.getRight() > contentLength) {
|
if (range.getLeft() < 0 || range.getLeft() > range.getRight() || range.getRight() > contentLength) {
|
||||||
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), "bytes */" + contentLength);
|
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), "bytes */" + contentLength);
|
||||||
@@ -57,6 +57,9 @@ class DavFileWithRange extends DavFile {
|
|||||||
final Long rangeLength = range.getRight() - range.getLeft() + 1;
|
final Long rangeLength = range.getRight() - range.getLeft() + 1;
|
||||||
outputContext.setContentLength(rangeLength);
|
outputContext.setContentLength(rangeLength);
|
||||||
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), contentRangeResponseHeader(range.getLeft(), range.getRight(), contentLength));
|
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), contentRangeResponseHeader(range.getLeft(), range.getRight(), contentLength));
|
||||||
|
outputContext.setContentType(CONTENT_TYPE_VALUE);
|
||||||
|
outputContext.setProperty(CONTENT_DISPOSITION_HEADER, CONTENT_DISPOSITION_VALUE);
|
||||||
|
outputContext.setProperty(X_CONTENT_TYPE_OPTIONS_HEADER, X_CONTENT_TYPE_OPTIONS_VALUE);
|
||||||
src.position(range.getLeft());
|
src.position(range.getLeft());
|
||||||
InputStream limitedIn = ByteStreams.limit(Channels.newInputStream(src), rangeLength);
|
InputStream limitedIn = ByteStreams.limit(Channels.newInputStream(src), rangeLength);
|
||||||
ByteStreams.copy(limitedIn, out);
|
ByteStreams.copy(limitedIn, out);
|
||||||
|
|||||||
@@ -39,10 +39,10 @@ class DavFileWithUnsatisfiableRange extends DavFile {
|
|||||||
if (!outputContext.hasStream()) {
|
if (!outputContext.hasStream()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
final long contentLength = node.size();
|
||||||
|
outputContext.setContentLength(contentLength);
|
||||||
|
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), "bytes */" + contentLength);
|
||||||
try (ReadableFile src = node.openReadable(); OutputStream out = outputContext.getOutputStream()) {
|
try (ReadableFile src = node.openReadable(); OutputStream out = outputContext.getOutputStream()) {
|
||||||
final long contentLength = src.size();
|
|
||||||
outputContext.setContentLength(contentLength);
|
|
||||||
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), "bytes */" + contentLength);
|
|
||||||
ByteStreams.copy(src, Channels.newChannel(out));
|
ByteStreams.copy(src, Channels.newChannel(out));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ class ExclusiveSharedLockManager implements LockManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String token = DavConstants.OPAQUE_LOCK_TOKEN_PREFIX + UUID.randomUUID();
|
String token = DavConstants.OPAQUE_LOCK_TOKEN_PREFIX + UUID.randomUUID();
|
||||||
return lockedResources.computeIfAbsent(locator, loc -> new HashMap<>()).computeIfAbsent(token, t -> new ExclusiveSharedLock(t, lockInfo));
|
Map<String, ActiveLock> lockMap = Objects.requireNonNull(lockedResources.computeIfAbsent(locator, loc -> new HashMap<>()));
|
||||||
|
return lockMap.computeIfAbsent(token, t -> new ExclusiveSharedLock(t, lockInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removedExpiredLocksInLocatorHierarchy(FileSystemResourceLocator locator) {
|
private void removedExpiredLocksInLocatorHierarchy(FileSystemResourceLocator locator) {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import org.cryptomator.frontend.Frontend.MountParam;
|
|||||||
final class FallbackWebDavMounter implements WebDavMounterStrategy {
|
final class FallbackWebDavMounter implements WebDavMounterStrategy {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldWork() {
|
public boolean shouldWork(Map<MountParam, Optional<String>> mountParams) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* 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
|
||||||
|
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
|
||||||
|
* Mohit Raju - Added fallback schema-name "webdav" when opening file managers
|
||||||
|
******************************************************************************/
|
||||||
|
package org.cryptomator.frontend.webdav.mount;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
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 LinuxGvfsDavMounter implements WebDavMounterStrategy {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
LinuxGvfsDavMounter() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldWork(Map<MountParam, Optional<String>> mountParams) {
|
||||||
|
if (SystemUtils.IS_OS_LINUX) {
|
||||||
|
Optional<String> prefScheme = mountParams.getOrDefault(MountParam.PREFERRED_GVFS_SCHEME, Optional.empty());
|
||||||
|
boolean prefSchemeIsUnspecifiedOrDav = !prefScheme.isPresent() || prefScheme.get().equalsIgnoreCase("dav");
|
||||||
|
final Script checkScripts = Script.fromLines("which gvfs-mount xdg-open");
|
||||||
|
try {
|
||||||
|
checkScripts.execute();
|
||||||
|
return prefSchemeIsUnspecifiedOrDav;
|
||||||
|
} catch (CommandFailedException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void warmUp(int serverPort) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) throws CommandFailedException {
|
||||||
|
final Script mountScript = Script.fromLines("set -x", "gvfs-mount \"dav:$DAV_SSP\"").addEnv("DAV_SSP", uri.getRawSchemeSpecificPart());
|
||||||
|
mountScript.execute();
|
||||||
|
return new LinuxGvfsDavMount(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class LinuxGvfsDavMount extends AbstractWebDavMount {
|
||||||
|
private final URI webDavUri;
|
||||||
|
private final Script testMountStillExistsScript;
|
||||||
|
private final Script unmountScript;
|
||||||
|
|
||||||
|
private LinuxGvfsDavMount(URI webDavUri) {
|
||||||
|
this.webDavUri = webDavUri;
|
||||||
|
this.testMountStillExistsScript = Script.fromLines("set -x", "test `gvfs-mount --list | grep \"$DAV_SSP\" | wc -l` -eq 1").addEnv("DAV_SSP", webDavUri.getRawSchemeSpecificPart());
|
||||||
|
this.unmountScript = Script.fromLines("set -x", "gvfs-mount -u \"dav:$DAV_SSP\"").addEnv("DAV_SSP", webDavUri.getRawSchemeSpecificPart());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unmount() throws CommandFailedException {
|
||||||
|
boolean mountStillExists;
|
||||||
|
try {
|
||||||
|
testMountStillExistsScript.execute();
|
||||||
|
mountStillExists = true;
|
||||||
|
} catch (CommandFailedException e) {
|
||||||
|
mountStillExists = false;
|
||||||
|
}
|
||||||
|
// only attempt unmount if user didn't unmount manually:
|
||||||
|
if (mountStillExists) {
|
||||||
|
unmountScript.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reveal() throws CommandFailedException {
|
||||||
|
Script.fromLines("set -x", "gvfs-open \"dav:$DAV_SSP\"").addEnv("DAV_SSP", webDavUri.getRawSchemeSpecificPart()).execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -30,12 +30,14 @@ final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldWork() {
|
public boolean shouldWork(Map<MountParam, Optional<String>> mountParams) {
|
||||||
if (SystemUtils.IS_OS_LINUX) {
|
if (SystemUtils.IS_OS_LINUX) {
|
||||||
|
Optional<String> prefScheme = mountParams.getOrDefault(MountParam.PREFERRED_GVFS_SCHEME, Optional.empty());
|
||||||
|
boolean prefSchemeIsUnspecifiedOrWebDav = !prefScheme.isPresent() || prefScheme.get().equalsIgnoreCase("webdav");
|
||||||
final Script checkScripts = Script.fromLines("which gvfs-mount xdg-open");
|
final Script checkScripts = Script.fromLines("which gvfs-mount xdg-open");
|
||||||
try {
|
try {
|
||||||
checkScripts.execute();
|
checkScripts.execute();
|
||||||
return true;
|
return prefSchemeIsUnspecifiedOrWebDav;
|
||||||
} catch (CommandFailedException e) {
|
} catch (CommandFailedException e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -84,15 +86,7 @@ final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reveal() throws CommandFailedException {
|
public void reveal() throws CommandFailedException {
|
||||||
try {
|
Script.fromLines("set -x", "gvfs-open \"webdav:$DAV_SSP\"").addEnv("DAV_SSP", webDavUri.getRawSchemeSpecificPart()).execute();
|
||||||
openMountWithWebdavUri("dav:" + webDavUri.getRawSchemeSpecificPart()).execute();
|
|
||||||
} catch (CommandFailedException exception) {
|
|
||||||
openMountWithWebdavUri("webdav:" + webDavUri.getRawSchemeSpecificPart()).execute();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Script openMountWithWebdavUri(String webdavUri) {
|
|
||||||
return Script.fromLines("set -x", "xdg-open \"$DAV_URI\"").addEnv("DAV_URI", webdavUri);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ final class MacOsXAppleScriptWebDavMounter implements WebDavMounterStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldWork() {
|
public boolean shouldWork(Map<MountParam, Optional<String>> mountParams) {
|
||||||
return SystemUtils.IS_OS_MAC_OSX && semVerComparator.compare(SystemUtils.OS_VERSION, "10.10") >= 0;
|
return SystemUtils.IS_OS_MAC_OSX && semVerComparator.compare(SystemUtils.OS_VERSION, "10.10") >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ final class MacOsXShellScriptWebDavMounter implements WebDavMounterStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldWork() {
|
public boolean shouldWork(Map<MountParam, Optional<String>> mountParams) {
|
||||||
return SystemUtils.IS_OS_MAC_OSX && semVerComparator.compare(SystemUtils.OS_VERSION, "10.10") < 0;
|
return SystemUtils.IS_OS_MAC_OSX && semVerComparator.compare(SystemUtils.OS_VERSION, "10.10") < 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
/*******************************************************************************
|
|
||||||
* 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.frontend.webdav.mount;
|
|
||||||
|
|
||||||
import static java.util.Arrays.asList;
|
|
||||||
import static java.util.Collections.unmodifiableList;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Iterator;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class MountStrategies implements Collection<WebDavMounterStrategy> {
|
|
||||||
|
|
||||||
private final Collection<WebDavMounterStrategy> delegate;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package org.cryptomator.frontend.webdav.mount;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
|
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import dagger.multibindings.ElementsIntoSet;
|
||||||
|
|
||||||
|
@Module
|
||||||
|
public class WebDavMounterModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@ElementsIntoSet
|
||||||
|
static Set<WebDavMounterStrategy> provideMounters(LinuxGvfsWebDavMounter linuxWebDavMounter, LinuxGvfsDavMounter linuxDavMounter, MacOsXAppleScriptWebDavMounter osxAppleScriptMounter,
|
||||||
|
MacOsXShellScriptWebDavMounter osxShellScriptMounter, WindowsWebDavMounter winMounter) {
|
||||||
|
return Sets.newHashSet(linuxWebDavMounter, linuxDavMounter, osxAppleScriptMounter, osxShellScriptMounter, winMounter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
@Named("fallback")
|
||||||
|
static WebDavMounterStrategy provideFallbackStrategy() {
|
||||||
|
return new FallbackWebDavMounter();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -10,34 +10,35 @@
|
|||||||
package org.cryptomator.frontend.webdav.mount;
|
package org.cryptomator.frontend.webdav.mount;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Provider;
|
import javax.inject.Named;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import org.cryptomator.frontend.Frontend.MountParam;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class WebDavMounterProvider implements Provider<WebDavMounter> {
|
public class WebDavMounterProvider {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(WebDavMounterProvider.class);
|
private static final Logger LOG = LoggerFactory.getLogger(WebDavMounterProvider.class);
|
||||||
private final WebDavMounterStrategy choosenStrategy;
|
private final Collection<WebDavMounterStrategy> availableStrategies;
|
||||||
|
private final WebDavMounterStrategy fallbackStrategy;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public WebDavMounterProvider(MountStrategies availableStrategies) {
|
public WebDavMounterProvider(Set<WebDavMounterStrategy> availableStrategies, @Named("fallback") WebDavMounterStrategy fallbackStrategy) {
|
||||||
this.choosenStrategy = getStrategyWhichShouldWork(availableStrategies);
|
this.availableStrategies = availableStrategies;
|
||||||
|
this.fallbackStrategy = fallbackStrategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public WebDavMounter chooseMounter(Map<MountParam, Optional<String>> mountParams) {
|
||||||
public WebDavMounter get() {
|
WebDavMounterStrategy result = availableStrategies.stream().filter(strategy -> strategy.shouldWork(mountParams)).findFirst().orElse(fallbackStrategy);
|
||||||
return this.choosenStrategy;
|
LOG.info("Using {}", result.getClass().getSimpleName());
|
||||||
}
|
return result;
|
||||||
|
|
||||||
private WebDavMounterStrategy getStrategyWhichShouldWork(Collection<WebDavMounterStrategy> availableStrategies) {
|
|
||||||
WebDavMounterStrategy strategy = availableStrategies.stream().filter(WebDavMounterStrategy::shouldWork).findFirst().orElse(new FallbackWebDavMounter());
|
|
||||||
LOG.info("Using {}", strategy.getClass().getSimpleName());
|
|
||||||
return strategy;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,11 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package org.cryptomator.frontend.webdav.mount;
|
package org.cryptomator.frontend.webdav.mount;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.cryptomator.frontend.Frontend.MountParam;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A strategy able to mount a webdav share and display it to the user.
|
* A strategy able to mount a webdav share and display it to the user.
|
||||||
*
|
*
|
||||||
@@ -19,7 +24,7 @@ interface WebDavMounterStrategy extends WebDavMounter {
|
|||||||
/**
|
/**
|
||||||
* @return {@code false} if this {@code WebDavMounterStrategy} can not work on the local machine, {@code true} if it could work
|
* @return {@code false} if this {@code WebDavMounterStrategy} can not work on the local machine, {@code true} if it could work
|
||||||
*/
|
*/
|
||||||
boolean shouldWork();
|
boolean shouldWork(Map<MountParam, Optional<String>> mountParams);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoked when mounting strategy gets chosen. On some operating systems (we don't want to tell names here) mounting might be faster,
|
* Invoked when mounting strategy gets chosen. On some operating systems (we don't want to tell names here) mounting might be faster,
|
||||||
|
|||||||
@@ -9,11 +9,11 @@
|
|||||||
package org.cryptomator.frontend.webdav.mount;
|
package org.cryptomator.frontend.webdav.mount;
|
||||||
|
|
||||||
import static java.util.stream.Collectors.toSet;
|
import static java.util.stream.Collectors.toSet;
|
||||||
import static java.util.stream.IntStream.rangeClosed;
|
|
||||||
|
|
||||||
import java.nio.file.FileSystems;
|
import java.nio.file.FileSystems;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
import java.util.stream.StreamSupport;
|
import java.util.stream.StreamSupport;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
@@ -24,16 +24,21 @@ import org.apache.commons.lang3.SystemUtils;
|
|||||||
|
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public final class WindowsDriveLetters {
|
public final class WindowsDriveLetters {
|
||||||
|
|
||||||
private static final Set<Character> A_TO_Z = rangeClosed('A', 'Z').mapToObj(i -> (char) i).collect(toSet());
|
private static final Set<Character> A_TO_Z;
|
||||||
|
|
||||||
|
static {
|
||||||
|
try (IntStream stream = IntStream.rangeClosed('A', 'Z')) {
|
||||||
|
A_TO_Z = stream.mapToObj(i -> (char) i).collect(toSet());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public WindowsDriveLetters() {
|
public WindowsDriveLetters() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<Character> getOccupiedDriveLetters() {
|
public Set<Character> getOccupiedDriveLetters() {
|
||||||
if (!SystemUtils.IS_OS_WINDOWS) {
|
if (!SystemUtils.IS_OS_WINDOWS) {
|
||||||
throw new UnsupportedOperationException("This method is only defined for Windows file systems");
|
throw new UnsupportedOperationException("This method is only defined for Windows file systems");
|
||||||
@@ -41,7 +46,7 @@ public final class WindowsDriveLetters {
|
|||||||
Iterable<Path> rootDirs = FileSystems.getDefault().getRootDirectories();
|
Iterable<Path> rootDirs = FileSystems.getDefault().getRootDirectories();
|
||||||
return StreamSupport.stream(rootDirs.spliterator(), false).map(Path::toString).map(CharUtils::toChar).map(Character::toUpperCase).collect(toSet());
|
return StreamSupport.stream(rootDirs.spliterator(), false).map(Path::toString).map(CharUtils::toChar).map(Character::toUpperCase).collect(toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<Character> getAvailableDriveLetters() {
|
public Set<Character> getAvailableDriveLetters() {
|
||||||
return Sets.difference(A_TO_Z, getOccupiedDriveLetters());
|
return Sets.difference(A_TO_Z, getOccupiedDriveLetters());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldWork() {
|
public boolean shouldWork(Map<MountParam, Optional<String>> mountParams) {
|
||||||
return SystemUtils.IS_OS_WINDOWS;
|
return SystemUtils.IS_OS_WINDOWS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
|
|||||||
mountScript.addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\'));
|
mountScript.addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\'));
|
||||||
return mountScript.execute(MOUNT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
return mountScript.execute(MOUNT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addProxyOverrides(URI uri) throws IOException, CommandFailedException {
|
private void addProxyOverrides(URI uri) throws IOException, CommandFailedException {
|
||||||
try {
|
try {
|
||||||
// get existing value for ProxyOverride key from reqistry:
|
// get existing value for ProxyOverride key from reqistry:
|
||||||
@@ -110,7 +110,7 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
|
|||||||
Process queryCmd = query.start();
|
Process queryCmd = query.start();
|
||||||
String queryStdOut = IOUtils.toString(queryCmd.getInputStream(), StandardCharsets.UTF_8);
|
String queryStdOut = IOUtils.toString(queryCmd.getInputStream(), StandardCharsets.UTF_8);
|
||||||
int queryResult = queryCmd.waitFor();
|
int queryResult = queryCmd.waitFor();
|
||||||
|
|
||||||
// determine new value for ProxyOverride key:
|
// determine new value for ProxyOverride key:
|
||||||
Set<String> overrides = new HashSet<>();
|
Set<String> overrides = new HashSet<>();
|
||||||
Matcher matcher = REG_QUERY_PROXY_OVERRIDES_PATTERN.matcher(queryStdOut);
|
Matcher matcher = REG_QUERY_PROXY_OVERRIDES_PATTERN.matcher(queryStdOut);
|
||||||
@@ -122,7 +122,7 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
|
|||||||
overrides.add("<local>");
|
overrides.add("<local>");
|
||||||
overrides.add(uri.getHost());
|
overrides.add(uri.getHost());
|
||||||
overrides.add(uri.getHost() + ":" + uri.getPort());
|
overrides.add(uri.getHost() + ":" + uri.getPort());
|
||||||
|
|
||||||
// set new value:
|
// set new value:
|
||||||
String overridesStr = StringUtils.join(overrides, ';');
|
String overridesStr = StringUtils.join(overrides, ';');
|
||||||
ProcessBuilder add = new ProcessBuilder("reg", "add", "\"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\"", "/v", "ProxyOverride", "/d", "\"" + overridesStr + "\"", "/f");
|
ProcessBuilder add = new ProcessBuilder("reg", "add", "\"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\"", "/v", "ProxyOverride", "/d", "\"" + overridesStr + "\"", "/f");
|
||||||
@@ -133,6 +133,8 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
|
|||||||
String addStdErr = IOUtils.toString(addCmd.getErrorStream(), StandardCharsets.UTF_8);
|
String addStdErr = IOUtils.toString(addCmd.getErrorStream(), StandardCharsets.UTF_8);
|
||||||
throw new CommandFailedException(addStdErr);
|
throw new CommandFailedException(addStdErr);
|
||||||
}
|
}
|
||||||
|
} catch (IOException | CommandFailedException e) {
|
||||||
|
LOG.info("Failed to add proxy overrides", e);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
InterruptedIOException ioException = new InterruptedIOException();
|
InterruptedIOException ioException = new InterruptedIOException();
|
||||||
@@ -158,7 +160,7 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
|
|||||||
private WindowsWebDavMount(String driveLetter) {
|
private WindowsWebDavMount(String driveLetter) {
|
||||||
this.driveLetter = CharUtils.toCharacterObject(driveLetter);
|
this.driveLetter = CharUtils.toCharacterObject(driveLetter);
|
||||||
this.openExplorerScript = fromLines("start explorer.exe " + driveLetter + ":");
|
this.openExplorerScript = fromLines("start explorer.exe " + driveLetter + ":");
|
||||||
this.unmountScript = fromLines("net use " + driveLetter + ": /delete");
|
this.unmountScript = fromLines("net use " + driveLetter + ": /delete /no");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -61,13 +61,18 @@ final class CommandRunner {
|
|||||||
static CommandResult execute(Script script, long timeout, TimeUnit unit) throws CommandFailedException {
|
static CommandResult execute(Script script, long timeout, TimeUnit unit) throws CommandFailedException {
|
||||||
try {
|
try {
|
||||||
final List<String> env = script.environment().entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.toList());
|
final List<String> env = script.environment().entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.toList());
|
||||||
|
final String[] lines = script.getLines();
|
||||||
|
if (lines.length == 0) {
|
||||||
|
throw new IllegalArgumentException("Invalid script");
|
||||||
|
}
|
||||||
CommandResult result = null;
|
CommandResult result = null;
|
||||||
for (final String line : script.getLines()) {
|
for (final String line : lines) {
|
||||||
final String[] cmds = ArrayUtils.add(determineCli(), line);
|
final String[] cmds = ArrayUtils.add(determineCli(), line);
|
||||||
final Process proc = Runtime.getRuntime().exec(cmds, env.toArray(new String[0]));
|
final Process proc = Runtime.getRuntime().exec(cmds, env.toArray(new String[0]));
|
||||||
result = run(proc, timeout, unit);
|
result = run(proc, timeout, unit);
|
||||||
result.assertOk();
|
result.assertOk();
|
||||||
}
|
}
|
||||||
|
assert result != null;
|
||||||
return result;
|
return result;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new CommandFailedException(e);
|
throw new CommandFailedException(e);
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
package org.cryptomator.frontend.webdav;
|
package org.cryptomator.frontend.webdav;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import javax.servlet.Servlet;
|
import javax.servlet.Servlet;
|
||||||
@@ -21,21 +23,26 @@ import org.junit.Test;
|
|||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
|
|
||||||
public class WindowsCompatibilityServletTest {
|
public class DefaultServletTest {
|
||||||
|
|
||||||
|
private Tarpit tarpit = mock(Tarpit.class);
|
||||||
|
|
||||||
|
private DefaultServlet inTest = new DefaultServlet(tarpit);
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testFactory() throws ServletException {
|
public void testFactory() throws ServletException {
|
||||||
ServletHolder[] holders = WindowsCompatibilityServlet.createServletContextHandler().getServletHandler().getServlets();
|
|
||||||
|
ServletHolder[] holders = inTest.createServletContextHandler().getServletHandler().getServlets();
|
||||||
Assert.assertEquals(1, holders.length);
|
Assert.assertEquals(1, holders.length);
|
||||||
ServletHolder holder = holders[0];
|
ServletHolder holder = holders[0];
|
||||||
|
|
||||||
Servlet servlet = holder.getServlet();
|
Servlet servlet = holder.getServlet();
|
||||||
Assert.assertTrue(servlet instanceof WindowsCompatibilityServlet);
|
Assert.assertTrue(servlet instanceof DefaultServlet);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testResponse() throws IOException, ServletException {
|
public void testResponse() throws IOException, ServletException {
|
||||||
final WindowsCompatibilityServlet servlet = new WindowsCompatibilityServlet();
|
final DefaultServlet servlet = inTest;
|
||||||
final HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
|
final HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
|
||||||
final HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
|
final HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
|
||||||
|
|
||||||
@@ -10,12 +10,10 @@ package org.cryptomator.frontend.webdav;
|
|||||||
|
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import org.cryptomator.common.CommonsModule;
|
|
||||||
|
|
||||||
import dagger.Component;
|
import dagger.Component;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Component(modules = {CommonsModule.class})
|
@Component(modules = {WebDavModule.class})
|
||||||
public interface WebDavComponent {
|
public interface WebDavComponent {
|
||||||
|
|
||||||
WebDavServer server();
|
WebDavServer server();
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.1.0</version>
|
<version>1.2.3</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>jacoco-report</artifactId>
|
<artifactId>jacoco-report</artifactId>
|
||||||
<name>Cryptomator Code Coverage Report</name>
|
<name>Cryptomator Code Coverage Report</name>
|
||||||
|
|||||||
52
main/keychain/pom.xml
Normal file
52
main/keychain/pom.xml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>org.cryptomator</groupId>
|
||||||
|
<artifactId>main</artifactId>
|
||||||
|
<version>1.2.3</version>
|
||||||
|
</parent>
|
||||||
|
<artifactId>keychain</artifactId>
|
||||||
|
<name>System Keychain Access</name>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-lang3</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.code.gson</groupId>
|
||||||
|
<artifactId>gson</artifactId>
|
||||||
|
<version>2.7</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>commons-codec</groupId>
|
||||||
|
<artifactId>commons-codec</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bouncycastle</groupId>
|
||||||
|
<artifactId>bcprov-jdk15on</artifactId>
|
||||||
|
<version>1.54</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.cryptomator</groupId>
|
||||||
|
<artifactId>jni</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- DI -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.dagger</groupId>
|
||||||
|
<artifactId>dagger</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.dagger</groupId>
|
||||||
|
<artifactId>dagger-compiler</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Test -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.cryptomator</groupId>
|
||||||
|
<artifactId>commons-test</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package org.cryptomator.keychain;
|
||||||
|
|
||||||
|
public interface KeychainAccess {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associates a passphrase with a given key.
|
||||||
|
*
|
||||||
|
* @param key Key used to retrieve the passphrase via {@link #loadPassphrase(String)}.
|
||||||
|
* @param passphrase The secret to store in this keychain.
|
||||||
|
*/
|
||||||
|
void storePassphrase(String key, CharSequence passphrase);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
|
||||||
|
* @return The stored passphrase for the given key or <code>null</code> if no value for the given key could be found.
|
||||||
|
*/
|
||||||
|
char[] loadPassphrase(String key);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a passphrase with a given key.
|
||||||
|
*
|
||||||
|
* @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
|
||||||
|
*/
|
||||||
|
void deletePassphrase(String key);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package org.cryptomator.keychain;
|
||||||
|
|
||||||
|
interface KeychainAccessStrategy extends KeychainAccess {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return <code>true</code> if this KeychainAccessStrategy works on the current machine.
|
||||||
|
*/
|
||||||
|
boolean isSupported();
|
||||||
|
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user