mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-18 18:51:26 +00:00
Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3db757193e | ||
|
|
bac1d6fd83 | ||
|
|
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 |
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: release.*
|
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.2.2</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.2</version>
|
<version>1.2.2</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.2</version>
|
<version>1.2.2</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.2</version>
|
<version>1.2.2</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,7 +12,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.2.2</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>filesystem-charsets</artifactId>
|
<artifactId>filesystem-charsets</artifactId>
|
||||||
<name>Cryptomator filesystem: Charset compatibility layer</name>
|
<name>Cryptomator filesystem: Charset compatibility layer</name>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.2.2</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.2</version>
|
<version>1.2.2</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.4</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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
package org.cryptomator.crypto.engine.impl;
|
package org.cryptomator.crypto.engine.impl;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
|
|
||||||
public final class Constants {
|
public final class Constants {
|
||||||
|
|
||||||
private Constants() {
|
private Constants() {
|
||||||
}
|
}
|
||||||
|
|
||||||
static final Collection<Integer> SUPPORTED_VAULT_VERSIONS = Collections.unmodifiableCollection(Arrays.asList(3, 4));
|
static final Integer CURRENT_VAULT_VERSION = 5;
|
||||||
static final Integer CURRENT_VAULT_VERSION = 4;
|
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
package org.cryptomator.crypto.engine.impl;
|
package org.cryptomator.crypto.engine.impl;
|
||||||
|
|
||||||
import static org.cryptomator.crypto.engine.impl.Constants.CURRENT_VAULT_VERSION;
|
import static org.cryptomator.crypto.engine.impl.Constants.CURRENT_VAULT_VERSION;
|
||||||
import static org.cryptomator.crypto.engine.impl.Constants.SUPPORTED_VAULT_VERSIONS;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
@@ -110,7 +109,7 @@ class CryptorImpl implements Cryptor {
|
|||||||
assert keyFile != null;
|
assert keyFile != null;
|
||||||
|
|
||||||
// check version
|
// check version
|
||||||
if (!SUPPORTED_VAULT_VERSIONS.contains(keyFile.getVersion())) {
|
if (!CURRENT_VAULT_VERSION.equals(keyFile.getVersion())) {
|
||||||
throw new UnsupportedVaultFormatException(keyFile.getVersion(), CURRENT_VAULT_VERSION);
|
throw new UnsupportedVaultFormatException(keyFile.getVersion(), CURRENT_VAULT_VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,12 +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();
|
||||||
// https://tools.ietf.org/html/rfc4648#section-6
|
// 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 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
|
||||||
@@ -70,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();
|
||||||
|
|||||||
@@ -79,17 +79,21 @@ final class ConflictResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isSameFileBasedOnSample(File file1, File file2, int sampleSize) {
|
private boolean isSameFileBasedOnSample(File file1, File file2, int sampleSize) {
|
||||||
try (ReadableFile r1 = file1.openReadable(); ReadableFile r2 = file2.openReadable()) {
|
if (file1.size() != file2.size()) {
|
||||||
if (r1.size() != r2.size()) {
|
return false;
|
||||||
return false;
|
} else {
|
||||||
} else {
|
try (ReadableFile r1 = file1.openReadable(); ReadableFile r2 = file2.openReadable()) {
|
||||||
ByteBuffer beginOfFile1 = ByteBuffer.allocate(sampleSize);
|
ByteBuffer beginOfFile1 = ByteBuffer.allocate(sampleSize);
|
||||||
ByteBuffer beginOfFile2 = ByteBuffer.allocate(sampleSize);
|
ByteBuffer beginOfFile2 = ByteBuffer.allocate(sampleSize);
|
||||||
r1.read(beginOfFile1);
|
int bytesRead1 = r1.read(beginOfFile1);
|
||||||
r2.read(beginOfFile2);
|
int bytesRead2 = r2.read(beginOfFile2);
|
||||||
beginOfFile1.flip();
|
if (bytesRead1 == bytesRead2) {
|
||||||
beginOfFile2.flip();
|
beginOfFile1.flip();
|
||||||
return beginOfFile1.equals(beginOfFile2);
|
beginOfFile2.flip();
|
||||||
|
return beginOfFile1.equals(beginOfFile2);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,6 +10,7 @@ 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.apache.commons.lang3.StringUtils.removeStart;
|
||||||
import static org.cryptomator.filesystem.crypto.Constants.DIR_PREFIX;
|
import static org.cryptomator.filesystem.crypto.Constants.DIR_PREFIX;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,7 +21,7 @@ public class CryptorImplTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testMasterkeyDecryptionWithCorrectPassphrase() throws IOException {
|
public void testMasterkeyDecryptionWithCorrectPassphrase() throws IOException {
|
||||||
final String testMasterKey = "{\"version\":4,\"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\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
|
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
|
||||||
@@ -31,7 +31,7 @@ public class CryptorImplTest {
|
|||||||
|
|
||||||
@Test(expected = InvalidPassphraseException.class)
|
@Test(expected = InvalidPassphraseException.class)
|
||||||
public void testMasterkeyDecryptionWithWrongPassphrase() throws IOException {
|
public void testMasterkeyDecryptionWithWrongPassphrase() throws IOException {
|
||||||
final String testMasterKey = "{\"version\":4,\"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\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
|
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
|
||||||
@@ -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,7 +62,7 @@ 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\":4,\"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\":\"z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfoK=\"}";
|
+ "\"versionMac\":\"z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfoK=\"}";
|
||||||
@@ -72,14 +72,13 @@ public class CryptorImplTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testMasterkeyEncryption() throws IOException {
|
public void testMasterkeyEncryption() throws IOException {
|
||||||
final String expectedMasterKey = "{\"version\":4,\"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\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
|
+ "\"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");
|
||||||
System.out.println(new String(masterkeyFile));
|
|
||||||
Assert.assertArrayEquals(expectedMasterKey.getBytes(), masterkeyFile);
|
Assert.assertArrayEquals(expectedMasterKey.getBytes(), masterkeyFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.2.2</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.2</version>
|
<version>1.2.2</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.2</version>
|
<version>1.2.2</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.2</version>
|
<version>1.2.2</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.2</version>
|
<version>1.2.2</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.2</version>
|
<version>1.2.2</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.2</version>
|
<version>1.2.2</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,30 @@
|
|||||||
|
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 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,12 +23,24 @@ 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 {
|
||||||
@@ -36,11 +50,11 @@ public class WindowsCompatibilityServlet extends HttpServlet {
|
|||||||
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,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 {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -8,8 +8,11 @@
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
package org.cryptomator.frontend.webdav;
|
package org.cryptomator.frontend.webdav;
|
||||||
|
|
||||||
|
import static java.lang.String.format;
|
||||||
|
|
||||||
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 +23,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 +49,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 +60,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 +109,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 = format("/%s/%s", 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);
|
||||||
@@ -117,5 +121,9 @@ public class WebDavServer implements FrontendFactory {
|
|||||||
LOG.info("Servlet available under " + uri);
|
LOG.info("Servlet available under " + uri);
|
||||||
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;
|
||||||
@@ -34,10 +35,9 @@ import org.eclipse.jetty.servlet.ServletHolder;
|
|||||||
class WebDavServletContextFactory {
|
class WebDavServletContextFactory {
|
||||||
|
|
||||||
private static final String WILDCARD = "/*";
|
private static final String WILDCARD = "/*";
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public WebDavServletContextFactory() {
|
public WebDavServletContextFactory() {}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new Jetty ServletContextHandler, that can be be added to a servletCollection as follows:
|
* Creates a new Jetty ServletContextHandler, that can be be added to a servletCollection as follows:
|
||||||
@@ -67,6 +67,7 @@ class WebDavServletContextFactory {
|
|||||||
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.2</version>
|
<version>1.2.2</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.2</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();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package org.cryptomator.keychain;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.cryptomator.jni.JniModule;
|
||||||
|
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
|
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import dagger.multibindings.ElementsIntoSet;
|
||||||
|
|
||||||
|
@Module(includes = {JniModule.class})
|
||||||
|
public class KeychainModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@ElementsIntoSet
|
||||||
|
Set<KeychainAccessStrategy> provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain) {
|
||||||
|
return Sets.newHashSet(macKeychain, winKeychain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
public Optional<KeychainAccess> provideSupportedKeychain(Set<KeychainAccessStrategy> keychainAccessStrategies) {
|
||||||
|
return keychainAccessStrategies.stream().filter(KeychainAccessStrategy::isSupported).map(KeychainAccess.class::cast).findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package org.cryptomator.keychain;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
|
import org.cryptomator.jni.MacFunctions;
|
||||||
|
import org.cryptomator.jni.MacKeychainAccess;
|
||||||
|
|
||||||
|
class MacSystemKeychainAccess implements KeychainAccessStrategy {
|
||||||
|
|
||||||
|
private final MacKeychainAccess keychain;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public MacSystemKeychainAccess(Optional<MacFunctions> macFunctions) {
|
||||||
|
if (macFunctions.isPresent()) {
|
||||||
|
this.keychain = macFunctions.get().keychainAccess();
|
||||||
|
} else {
|
||||||
|
this.keychain = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void storePassphrase(String key, CharSequence passphrase) {
|
||||||
|
keychain.storePassword(key, passphrase);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char[] loadPassphrase(String key) {
|
||||||
|
return keychain.loadPassword(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSupported() {
|
||||||
|
return SystemUtils.IS_OS_MAC_OSX && keychain != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deletePassphrase(String key) {
|
||||||
|
keychain.deletePassword(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
package org.cryptomator.keychain;
|
||||||
|
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.CharBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.FileSystems;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.NoSuchFileException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import org.apache.commons.codec.binary.Base64;
|
||||||
|
import org.apache.commons.lang3.SystemUtils;
|
||||||
|
import org.cryptomator.jni.WinDataProtection;
|
||||||
|
import org.cryptomator.jni.WinFunctions;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonDeserializationContext;
|
||||||
|
import com.google.gson.JsonDeserializer;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonParseException;
|
||||||
|
import com.google.gson.JsonPrimitive;
|
||||||
|
import com.google.gson.JsonSerializationContext;
|
||||||
|
import com.google.gson.JsonSerializer;
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
class WindowsProtectedKeychainAccess implements KeychainAccessStrategy {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(WindowsProtectedKeychainAccess.class);
|
||||||
|
private static final Gson GSON = new GsonBuilder().setPrettyPrinting() //
|
||||||
|
.registerTypeHierarchyAdapter(byte[].class, new ByteArrayJsonAdapter()) //
|
||||||
|
.disableHtmlEscaping().create();
|
||||||
|
|
||||||
|
private final WinDataProtection dataProtection;
|
||||||
|
private final Path keychainPath;
|
||||||
|
private Map<String, KeychainEntry> keychainEntries;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public WindowsProtectedKeychainAccess(Optional<WinFunctions> winFunctions) {
|
||||||
|
if (winFunctions.isPresent()) {
|
||||||
|
this.dataProtection = winFunctions.get().dataProtection();
|
||||||
|
} else {
|
||||||
|
this.dataProtection = null;
|
||||||
|
}
|
||||||
|
String keychainPathProperty = System.getProperty("cryptomator.keychainPath");
|
||||||
|
if (dataProtection != null && keychainPathProperty == null) {
|
||||||
|
LOG.warn("Windows DataProtection module loaded, but no cryptomator.keychainPath property found.");
|
||||||
|
}
|
||||||
|
if (keychainPathProperty != null) {
|
||||||
|
if (keychainPathProperty.startsWith("~/")) {
|
||||||
|
keychainPathProperty = SystemUtils.USER_HOME + keychainPathProperty.substring(1);
|
||||||
|
}
|
||||||
|
this.keychainPath = FileSystems.getDefault().getPath(keychainPathProperty);
|
||||||
|
} else {
|
||||||
|
this.keychainPath = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void storePassphrase(String key, CharSequence passphrase) {
|
||||||
|
loadKeychainEntriesIfNeeded();
|
||||||
|
ByteBuffer buf = UTF_8.encode(CharBuffer.wrap(passphrase));
|
||||||
|
byte[] cleartext = new byte[buf.remaining()];
|
||||||
|
buf.get(cleartext);
|
||||||
|
KeychainEntry entry = new KeychainEntry();
|
||||||
|
entry.salt = generateSalt();
|
||||||
|
entry.ciphertext = dataProtection.protect(cleartext, entry.salt);
|
||||||
|
Arrays.fill(buf.array(), (byte) 0x00);
|
||||||
|
Arrays.fill(cleartext, (byte) 0x00);
|
||||||
|
keychainEntries.put(key, entry);
|
||||||
|
saveKeychainEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char[] loadPassphrase(String key) {
|
||||||
|
loadKeychainEntriesIfNeeded();
|
||||||
|
KeychainEntry entry = keychainEntries.get(key);
|
||||||
|
if (entry == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
byte[] cleartext = dataProtection.unprotect(entry.ciphertext, entry.salt);
|
||||||
|
if (cleartext == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
CharBuffer buf = UTF_8.decode(ByteBuffer.wrap(cleartext));
|
||||||
|
char[] passphrase = new char[buf.remaining()];
|
||||||
|
buf.get(passphrase);
|
||||||
|
Arrays.fill(cleartext, (byte) 0x00);
|
||||||
|
Arrays.fill(buf.array(), (char) 0x00);
|
||||||
|
return passphrase;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deletePassphrase(String key) {
|
||||||
|
loadKeychainEntriesIfNeeded();
|
||||||
|
keychainEntries.remove(key);
|
||||||
|
saveKeychainEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSupported() {
|
||||||
|
return SystemUtils.IS_OS_WINDOWS && dataProtection != null && keychainPath != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] generateSalt() {
|
||||||
|
byte[] result = new byte[2 * Long.BYTES];
|
||||||
|
UUID uuid = UUID.randomUUID();
|
||||||
|
ByteBuffer buf = ByteBuffer.wrap(result);
|
||||||
|
buf.putLong(uuid.getMostSignificantBits());
|
||||||
|
buf.putLong(uuid.getLeastSignificantBits());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadKeychainEntriesIfNeeded() {
|
||||||
|
if (keychainEntries == null) {
|
||||||
|
loadKeychainEntries();
|
||||||
|
}
|
||||||
|
assert keychainEntries != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadKeychainEntries() {
|
||||||
|
Type type = new TypeToken<Map<String, KeychainEntry>>() {
|
||||||
|
}.getType();
|
||||||
|
try (InputStream in = Files.newInputStream(keychainPath, StandardOpenOption.READ); //
|
||||||
|
Reader reader = new InputStreamReader(in, UTF_8)) {
|
||||||
|
keychainEntries = GSON.fromJson(reader, type);
|
||||||
|
} catch (JsonParseException | NoSuchFileException e) {
|
||||||
|
LOG.info("Creating new keychain at path {}", keychainPath);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException("Could not read keychain from path " + keychainPath, e);
|
||||||
|
}
|
||||||
|
if (keychainEntries == null) {
|
||||||
|
keychainEntries = new HashMap<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveKeychainEntries() {
|
||||||
|
try (OutputStream out = Files.newOutputStream(keychainPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); //
|
||||||
|
Writer writer = new OutputStreamWriter(out, UTF_8)) {
|
||||||
|
GSON.toJson(keychainEntries, writer);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException("Could not read keychain from path " + keychainPath, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class KeychainEntry {
|
||||||
|
@SerializedName("ciphertext")
|
||||||
|
byte[] ciphertext;
|
||||||
|
@SerializedName("salt")
|
||||||
|
byte[] salt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ByteArrayJsonAdapter implements JsonSerializer<byte[]>, JsonDeserializer<byte[]> {
|
||||||
|
|
||||||
|
private static final Base64 BASE64 = new Base64();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||||
|
return BASE64.decode(json.getAsString().getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) {
|
||||||
|
return new JsonPrimitive(new String(BASE64.encode(src), StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package org.cryptomator.keychain;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class KeychainModuleTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetKeychain() {
|
||||||
|
Optional<KeychainAccess> keychainAccess = DaggerTestKeychainComponent.builder().jniModule(new TestJniModule()).keychainModule(new TestKeychainModule()).build().keychainAccess();
|
||||||
|
Assert.assertTrue(keychainAccess.isPresent());
|
||||||
|
Assert.assertTrue(keychainAccess.get() instanceof MapKeychainAccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package org.cryptomator.keychain;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
class MapKeychainAccess implements KeychainAccessStrategy {
|
||||||
|
|
||||||
|
private final Map<String, char[]> map = new HashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void storePassphrase(String key, CharSequence passphrase) {
|
||||||
|
char[] pw = new char[passphrase.length()];
|
||||||
|
for (int i = 0; i < passphrase.length(); i++) {
|
||||||
|
pw[i] = passphrase.charAt(i);
|
||||||
|
}
|
||||||
|
map.put(key, pw);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char[] loadPassphrase(String key) {
|
||||||
|
return map.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deletePassphrase(String key) {
|
||||||
|
map.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSupported() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package org.cryptomator.keychain;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.cryptomator.jni.JniModule;
|
||||||
|
import org.cryptomator.jni.MacFunctions;
|
||||||
|
import org.cryptomator.jni.WinFunctions;
|
||||||
|
|
||||||
|
import dagger.Lazy;
|
||||||
|
|
||||||
|
public class TestJniModule extends JniModule {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<WinFunctions> winFunctions(Lazy<WinFunctions> winFunction) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<MacFunctions> macFunctions(Lazy<MacFunctions> winFunction) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user