mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-14 16:51:28 +00:00
Compare commits
127 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e374a927c | ||
|
|
84ac6d88f5 | ||
|
|
72f6ee6477 | ||
|
|
a3cfcb1131 | ||
|
|
d7d8d21ba4 | ||
|
|
ef0425e2b1 | ||
|
|
df1fd6d0b3 | ||
|
|
a15acd64c8 | ||
|
|
5b18eff01a | ||
|
|
47133c6f31 | ||
|
|
09ba4f5129 | ||
|
|
20d4047bed | ||
|
|
56b71ef7d9 | ||
|
|
091e62057d | ||
|
|
824bd9ea64 | ||
|
|
697a791593 | ||
|
|
7462a887b3 | ||
|
|
3535e83d7d | ||
|
|
cf0b4accb3 | ||
|
|
a63bcfbaa2 | ||
|
|
5c4bf2a207 | ||
|
|
c1611a12ed | ||
|
|
0983120712 | ||
|
|
ce12af8495 | ||
|
|
dc117c8415 | ||
|
|
06e526a961 | ||
|
|
2e343a951f | ||
|
|
141ffcf656 | ||
|
|
d61e5c5a08 | ||
|
|
6a15fa132a | ||
|
|
902b29ee0a | ||
|
|
995bba616f | ||
|
|
f39b7b047f | ||
|
|
72e52df4e0 | ||
|
|
8018e9485e | ||
|
|
e0ae50378f | ||
|
|
a9c2b0fc57 | ||
|
|
dc58ba434a | ||
|
|
34af306309 | ||
|
|
21d70b5ae4 | ||
|
|
e90880ac9a | ||
|
|
66faa13f40 | ||
|
|
8a4a29b4d1 | ||
|
|
8c8db84a4a | ||
|
|
a499a3c80b | ||
|
|
6a3ccf2b48 | ||
|
|
fcfcffe9cb | ||
|
|
363ed4ac4b | ||
|
|
1f73a08e09 | ||
|
|
fe0a34907f | ||
|
|
461b11700f | ||
|
|
24bfbb59a4 | ||
|
|
4476558e9c | ||
|
|
560171832c | ||
|
|
6e93d40e51 | ||
|
|
79b819bca6 | ||
|
|
a18c406cf0 | ||
|
|
6730a83cac | ||
|
|
3b3ebd2196 | ||
|
|
505b6542c7 | ||
|
|
31368f0cba | ||
|
|
5b5dd756b1 | ||
|
|
f6ebbb23d1 | ||
|
|
3f0373b08f | ||
|
|
4c3c60060d | ||
|
|
28f275c22d | ||
|
|
24df3c3809 | ||
|
|
034a667e07 | ||
|
|
008e3e3b05 | ||
|
|
94a5bf7596 | ||
|
|
e8db836eff | ||
|
|
429b26f3d8 | ||
|
|
3ae8327300 | ||
|
|
df7e9a0af1 | ||
|
|
93d3eca0ab | ||
|
|
7753d1f0e7 | ||
|
|
d7c6c24932 | ||
|
|
1a75f23081 | ||
|
|
f071efe1b9 | ||
|
|
a8ad335aed | ||
|
|
7022a80c95 | ||
|
|
9a2f602d6c | ||
|
|
c78a4aa241 | ||
|
|
975ce4d973 | ||
|
|
1e6ff0d969 | ||
|
|
69e133d561 | ||
|
|
20e55eddf8 | ||
|
|
0fdcdc816a | ||
|
|
b7506d97a9 | ||
|
|
4ad7481dc7 | ||
|
|
bc815405d2 | ||
|
|
9c06e762c3 | ||
|
|
1ac87dd32f | ||
|
|
e0ce7ce2ec | ||
|
|
3d951a9d7b | ||
|
|
cec3d984b0 | ||
|
|
392e474cfa | ||
|
|
41fb0d51a4 | ||
|
|
aa9fef2967 | ||
|
|
adc9c02564 | ||
|
|
ace64117a2 | ||
|
|
fb4db2506b | ||
|
|
1076d971ae | ||
|
|
eed1b1cff0 | ||
|
|
f5cb82e21e | ||
|
|
67661f114b | ||
|
|
8a3e09764a | ||
|
|
eb3cfd6e6a | ||
|
|
4d1727d0e9 | ||
|
|
a51d853d1c | ||
|
|
d0039466f7 | ||
|
|
5c959989a2 | ||
|
|
6283d2df3d | ||
|
|
a9e0dfdaf8 | ||
|
|
45ca7e9e47 | ||
|
|
034b5c2718 | ||
|
|
e188649c79 | ||
|
|
1468c6ec90 | ||
|
|
07ba4eb537 | ||
|
|
414bbef1a7 | ||
|
|
e2b94ff6ef | ||
|
|
41f8a9faca | ||
|
|
1d9252e974 | ||
|
|
80780eef3c | ||
|
|
87ff33956b | ||
|
|
1804b98f05 | ||
|
|
847c6813cc |
60
.travis.yml
60
.travis.yml
@@ -1,23 +1,30 @@
|
||||
sudo: required
|
||||
|
||||
dist: trusty
|
||||
|
||||
language: java
|
||||
|
||||
sudo: required
|
||||
dist: trusty
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.m2
|
||||
env:
|
||||
global:
|
||||
- secure: "Lgj042RD0X3rB8VZVZLWP1GetLhjd3PqI5JbJMlzgHJpDI6RkFIBLN9SWAGmkLPCehIp2zA5tu9+UVy0NNMxm9xz6SyjMCaxS28/fnYEXaNmwwDSF6O6gLUbdxyzoYIFPYOPmFxpzhebqnNIsxaM29oZpgRgUGqosCczQxiB+Ng=" #coveralls
|
||||
- secure: "IfYURwZaDWuBDvyn47n0k1Zod/IQw1FF+CS5nnV08Q+NfC3vGGJMwV8m59XnbfwnWGxwvCaAbk4qP6s6+ijgZNKkvgfFMo3rfTok5zt43bIqgaFOANYV+OC/1c59gYD6ZUxhW5iNgMgU3qdsRtJuwSmfkVv/jKyLGfAbS4kN8BA=" #coverity
|
||||
|
||||
before_install: "curl -L --cookie 'oraclelicense=accept-securebackup-cookie;' http://download.oracle.com/otn-pub/java/jce/8/jce_policy-8.zip -o /tmp/policy.zip && sudo unzip -j -o /tmp/policy.zip *.jar -d `jdk_switcher home oraclejdk8`/jre/lib/security && rm /tmp/policy.zip"
|
||||
|
||||
script: mvn -fmain/pom.xml clean test
|
||||
|
||||
after_success: mvn -fmain/pom.xml -Ptest-coverage clean test jacoco:report-aggregate coveralls:report
|
||||
|
||||
- secure: "lV9OwUbHMrMpLUH1CY+Z4puLDdFXytudyPlG1eGRsesdpuG6KM3uQVz6uAtf6lrU8DRbMM/T7ML+PmvQ4UoPPYLdLxESLLBat2qUPOIVBOhTSlCc7I0DmGy04CSvkeMy8dPaQC0ukgNiR7zwoNzfcpGRN/U9S8tziDruuHoZSrg=" #bintray
|
||||
addons:
|
||||
coverity_scan:
|
||||
project:
|
||||
name: "cryptomator/cryptomator"
|
||||
notification_email: sebastian.stenzel@cryptomator.org
|
||||
build_command: "mvn -fmain/pom.xml clean test -DskipTests"
|
||||
branch_pattern: release.*
|
||||
install:
|
||||
# "clean" needed until https://bugs.openjdk.java.net/browse/JDK-8067747 is resolved.
|
||||
- mvn -fmain/pom.xml clean package -DskipTests dependency:go-offline -Ptest-coverage
|
||||
- mvn -fmain/pom.xml clean package -DskipTests dependency:go-offline -Prelease
|
||||
script:
|
||||
- mvn --update-snapshots -fmain/pom.xml -Ptest-coverage clean test jacoco:report-aggregate
|
||||
after_success:
|
||||
- "bash <(curl -s https://codecov.io/bash)"
|
||||
notifications:
|
||||
webhooks:
|
||||
urls:
|
||||
@@ -30,19 +37,10 @@ notifications:
|
||||
secure: "lngJ/HEAFBbD5AdiO9avMqptKpZHdmEwOzS9FabZjkdFh7yAYueTk5RniPUvShjsKtThYm7cJ8AtDMDwc07NvPrzbMBRtUJGwuDT+7c7YFALGFJ1NYi+emkC9x1oafvmPgEYSE+tMKzNcwrHi3ytGgKdIotsKwaF35QNXYA9aMs="
|
||||
on_success: change
|
||||
on_failure: always
|
||||
|
||||
before_deploy: mvn -fmain/pom.xml -Prelease clean package -DskipTests
|
||||
|
||||
addons:
|
||||
coverity_scan:
|
||||
project:
|
||||
name: "cryptomator/cryptomator"
|
||||
notification_email: sebastian.stenzel@cryptomator.org
|
||||
build_command: "mvn -fmain/pom.xml clean test -DskipTests"
|
||||
branch_pattern: coverity_scan
|
||||
|
||||
before_deploy:
|
||||
- mvn -fmain/pom.xml -Prelease clean package -DskipTests
|
||||
deploy:
|
||||
provider: releases
|
||||
- provider: releases
|
||||
prerelease: false
|
||||
api_key:
|
||||
secure: "ZjE1j93v3qbPIe2YbmhS319aCbMdLQw0HuymmluTurxXsZtn9D4t2+eTr99vBVxGRuB5lzzGezPR5zjk5W7iHF7xhwrawXrFzr2rPJWzWFt0aM+Ry2njU1ROTGGXGTbv4anWeBlgMxLEInTAy/9ytOGNJlec83yc0THpOY2wxnk="
|
||||
@@ -53,3 +51,13 @@ deploy:
|
||||
on:
|
||||
repo: cryptomator/cryptomator
|
||||
tags: true
|
||||
- provider: script
|
||||
script: "curl -X POST -u cryptobot:${BINTRAY_API_KEY} -H 'Content-Type: application/json' -d '{\"name\": \"${TRAVIS_TAG}\", \"vcs_tag\": \"${TRAVIS_TAG}\"}' https://api.bintray.com/packages/cryptomator/cryptomator/cryptomator-win/versions"
|
||||
on:
|
||||
repo: cryptomator/cryptomator
|
||||
tags: true
|
||||
- provider: script
|
||||
script: "curl -X POST -u cryptobot:${BINTRAY_API_KEY} -H 'Content-Type: application/json' -d '{\"name\": \"${TRAVIS_TAG}\", \"vcs_tag\": \"${TRAVIS_TAG}\"}' https://api.bintray.com/packages/cryptomator/cryptomator/cryptomator-osx/versions"
|
||||
on:
|
||||
repo: cryptomator/cryptomator
|
||||
tags: true
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
[](https://travis-ci.org/cryptomator/cryptomator)
|
||||
[](https://scan.coverity.com/projects/cryptomator-cryptomator)
|
||||
[](https://www.codacy.com/app/cryptomator/cryptomator?utm_source=github.com&utm_medium=referral&utm_content=cryptomator/cryptomator&utm_campaign=Badge_Grade)
|
||||
[](https://coveralls.io/github/cryptomator/cryptomator?branch=master)
|
||||
[](https://gitter.im/cryptomator/cryptomator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](http://twitter.com/Cryptomator)
|
||||
@@ -13,7 +14,7 @@ Download native binaries of Cryptomator on [cryptomator.org](https://cryptomator
|
||||
|
||||
## 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
|
||||
- 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
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.0</version>
|
||||
<version>1.2.0</version>
|
||||
</parent>
|
||||
<artifactId>ant-kit</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
@@ -21,21 +21,6 @@
|
||||
</fx:jar>
|
||||
</target>
|
||||
|
||||
<!-- Create native image -->
|
||||
<target name="create-linux-image-with-jvm" depends="create-jar">
|
||||
<fx:deploy nativeBundles="image" outdir="antbuild" outfile="Cryptomator-${project.version}" verbose="true">
|
||||
<fx:application refid="Cryptomator" />
|
||||
<fx:platform j2se="8.0">
|
||||
<fx:property name="cryptomator.logPath" value="~/.Cryptomator/cryptomator.log" />
|
||||
<fx:jvmarg value="-Xmx512m"/>
|
||||
</fx:platform>
|
||||
<fx:resources>
|
||||
<fx:fileset dir="antbuild" type="jar" includes="Cryptomator-${project.version}.jar" />
|
||||
<fx:fileset dir="libs" type="jar" includes="*.jar" excludes="ui-${project.version}.jar"/>
|
||||
</fx:resources>
|
||||
</fx:deploy>
|
||||
</target>
|
||||
|
||||
<!-- Create Debian package -->
|
||||
<target name="deb" depends="create-jar">
|
||||
<fx:deploy nativeBundles="deb" outdir="antbuild" outfile="Cryptomator-${project.version}" verbose="true">
|
||||
@@ -45,7 +30,9 @@
|
||||
</fx:info>
|
||||
<fx:platform j2se="8.0">
|
||||
<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:resources>
|
||||
<fx:fileset dir="antbuild" type="jar" includes="Cryptomator-${project.version}.jar" />
|
||||
@@ -66,7 +53,9 @@
|
||||
</fx:info>
|
||||
<fx:platform j2se="8.0">
|
||||
<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:resources>
|
||||
<fx:fileset dir="antbuild" type="jar" includes="Cryptomator-${project.version}.jar" />
|
||||
|
||||
@@ -9,7 +9,7 @@ Priority: optional
|
||||
Architecture: APPLICATION_ARCH
|
||||
Provides: APPLICATION_PACKAGE
|
||||
Installed-Size: APPLICATION_INSTALLED_SIZE
|
||||
Depends: gvfs-bin, gvfs-backends, gvfs-fuse, xdg-utils
|
||||
Depends: gvfs-bin, gvfs-backends, gvfs-fuse
|
||||
Description: Multi-platform client-side encryption of your cloud files.
|
||||
Cryptomator provides free client-side AES encryption for your cloud files.
|
||||
Create encrypted vaults, which get mounted as virtual volumes. Whatever
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.0</version>
|
||||
<version>1.2.0</version>
|
||||
</parent>
|
||||
<artifactId>commons-test</artifactId>
|
||||
<name>Cryptomator common test dependencies</name>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.0</version>
|
||||
<version>1.2.0</version>
|
||||
</parent>
|
||||
<artifactId>commons</artifactId>
|
||||
<name>Cryptomator common</name>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ConsumerThrowingException<T, E extends Exception> {
|
||||
public interface ConsumerThrowingException<T, E extends Throwable> {
|
||||
|
||||
void accept(T t) throws E;
|
||||
|
||||
|
||||
@@ -17,16 +17,17 @@ public final class LazyInitializer {
|
||||
* @return The initialized value
|
||||
*/
|
||||
public static <T> T initializeLazily(AtomicReference<T> reference, Supplier<T> factory) {
|
||||
final T existingInstance = reference.get();
|
||||
if (existingInstance != null) {
|
||||
return existingInstance;
|
||||
final T existing = reference.get();
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
} else {
|
||||
final T newInstance = factory.get();
|
||||
if (reference.compareAndSet(null, newInstance)) {
|
||||
return newInstance;
|
||||
} else {
|
||||
return reference.get();
|
||||
}
|
||||
return reference.updateAndGet(currentValue -> {
|
||||
if (currentValue == null) {
|
||||
return factory.get();
|
||||
} else {
|
||||
return currentValue;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface RunnableThrowingException<T extends Exception> {
|
||||
public interface RunnableThrowingException<T extends Throwable> {
|
||||
|
||||
void run() throws T;
|
||||
|
||||
|
||||
@@ -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 org.cryptomator.common.WeakValuedCache;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
@@ -83,6 +83,7 @@ public class WeakValuedCacheTest {
|
||||
assertThat(result, is(sameInstance(theValue)));
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
public void testCacheDoesNotPreventGarbageCollectionOfValues() {
|
||||
when(loader.apply(A_KEY)).thenAnswer(this::createValueUsingMoreThanHalfTheJvmMemory);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.0</version>
|
||||
<version>1.2.0</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-api</artifactId>
|
||||
<name>Cryptomator filesystem: API</name>
|
||||
|
||||
@@ -17,6 +17,13 @@ public interface File extends Node, Comparable<File> {
|
||||
|
||||
static final int EOF = -1;
|
||||
|
||||
/**
|
||||
* @return The current size of the file. This value is a snapshot and might have been changed by concurrent modifications.
|
||||
* @throws UncheckedIOException
|
||||
* if an {@link IOException} occurs
|
||||
*/
|
||||
long size() throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Opens this file for reading.
|
||||
@@ -39,7 +46,6 @@ public interface File extends Node, Comparable<File> {
|
||||
* if an {@link IOException} occurs while opening the file, the
|
||||
* file does not exist or is a directory
|
||||
*/
|
||||
|
||||
ReadableFile openReadable() throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,13 +30,6 @@ public interface ReadableFile extends ReadableByteChannel {
|
||||
@Override
|
||||
int read(ByteBuffer target) throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* @return The current size of the file. This value is a snapshot and might have been changed by concurrent modifications.
|
||||
* @throws UncheckedIOException
|
||||
* if an {@link IOException} occurs
|
||||
*/
|
||||
long size() throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Fast-forwards or rewinds the file to the specified position.
|
||||
|
||||
@@ -15,7 +15,7 @@ import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.ReadableFile;
|
||||
import org.cryptomator.filesystem.WritableFile;
|
||||
|
||||
public abstract class DelegatingFile<D extends DelegatingFolder<D, ?>> extends DelegatingNode<File>implements File {
|
||||
public abstract class DelegatingFile<D extends DelegatingFolder<D, ?>> extends DelegatingNode<File> implements File {
|
||||
|
||||
private final D parent;
|
||||
|
||||
@@ -29,6 +29,11 @@ public abstract class DelegatingFile<D extends DelegatingFolder<D, ?>> extends D
|
||||
return Optional.of(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws UncheckedIOException {
|
||||
return delegate.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadableFile openReadable() throws UncheckedIOException {
|
||||
return delegate.openReadable();
|
||||
|
||||
@@ -31,11 +31,6 @@ public class DelegatingReadableFile implements ReadableFile {
|
||||
return delegate.read(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws UncheckedIOException {
|
||||
return delegate.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void position(long position) throws UncheckedIOException {
|
||||
delegate.position(position);
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@@ -28,7 +29,9 @@ public final class FileContents {
|
||||
* @return The file's content interpreted in this FileContents' charset.
|
||||
*/
|
||||
public String readContents(File file) {
|
||||
try (Reader reader = Channels.newReader(file.openReadable(), charset.newDecoder(), -1)) {
|
||||
try ( //
|
||||
ReadableByteChannel channel = file.openReadable(); //
|
||||
Reader reader = Channels.newReader(channel, charset.newDecoder(), -1)) {
|
||||
return IOUtils.toString(reader);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
|
||||
@@ -30,6 +30,16 @@ public class DelegatingFileTest {
|
||||
Assert.assertEquals(mockFile.name(), delegatingFile.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSize() {
|
||||
File mockFile = Mockito.mock(File.class);
|
||||
DelegatingFile<?> delegatingFile = new TestDelegatingFile(null, mockFile);
|
||||
|
||||
Mockito.when(mockFile.size()).thenReturn(42l);
|
||||
Assert.assertEquals(42l, delegatingFile.size());
|
||||
Mockito.verify(mockFile).size();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParent() {
|
||||
Folder mockFolder = Mockito.mock(Folder.class);
|
||||
|
||||
@@ -42,17 +42,6 @@ public class DelegatingReadableFileTest {
|
||||
Mockito.verify(mockReadableFile).read(buf);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSize() {
|
||||
ReadableFile mockReadableFile = Mockito.mock(ReadableFile.class);
|
||||
@SuppressWarnings("resource")
|
||||
DelegatingReadableFile delegatingReadableFile = new DelegatingReadableFile(mockReadableFile);
|
||||
|
||||
Mockito.when(mockReadableFile.size()).thenReturn(42l);
|
||||
Assert.assertEquals(42l, delegatingReadableFile.size());
|
||||
Mockito.verify(mockReadableFile).size();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPosition() {
|
||||
ReadableFile mockReadableFile = Mockito.mock(ReadableFile.class);
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.0</version>
|
||||
<version>1.2.0</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-charsets</artifactId>
|
||||
<name>Cryptomator filesystem: Filename charset compatibility layer</name>
|
||||
<name>Cryptomator filesystem: Charset compatibility layer</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.0</version>
|
||||
<version>1.2.0</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-crypto-integration-tests</artifactId>
|
||||
<name>Cryptomator filesystem: Encryption layer tests</name>
|
||||
|
||||
@@ -130,7 +130,7 @@ public class CryptoFileSystemIntegrationTest {
|
||||
|
||||
// toggle last bit
|
||||
try (WritableFile writable = physicalFile.openWritable(); ReadableFile readable = physicalFile.openReadable()) {
|
||||
ByteBuffer buf = ByteBuffer.allocate((int) readable.size());
|
||||
ByteBuffer buf = ByteBuffer.allocate((int) physicalFile.size());
|
||||
readable.read(buf);
|
||||
buf.array()[buf.limit() - 1] ^= 0x01;
|
||||
buf.flip();
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.0</version>
|
||||
<version>1.2.0</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-crypto</artifactId>
|
||||
<name>Cryptomator filesystem: Encryption layer</name>
|
||||
|
||||
<properties>
|
||||
<bouncycastle.version>1.51</bouncycastle.version>
|
||||
<sivmode.version>1.0.2</sivmode.version>
|
||||
<sivmode.version>1.0.8</sivmode.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -19,11 +19,6 @@ import javax.security.auth.Destroyable;
|
||||
*/
|
||||
public interface FileContentDecryptor extends Destroyable, Closeable {
|
||||
|
||||
/**
|
||||
* @return Number of bytes of the decrypted file.
|
||||
*/
|
||||
long contentLength();
|
||||
|
||||
/**
|
||||
* Appends further ciphertext to this decryptor. This method might block until space becomes available. If so, it is interruptable.
|
||||
*
|
||||
|
||||
@@ -11,28 +11,28 @@ package org.cryptomator.crypto.engine;
|
||||
public class UnsupportedVaultFormatException extends CryptoException {
|
||||
|
||||
private final Integer detectedVersion;
|
||||
private final Integer supportedVersion;
|
||||
private final Integer latestSupportedVersion;
|
||||
|
||||
public UnsupportedVaultFormatException(Integer detectedVersion, Integer supportedVersion) {
|
||||
super("Tried to open vault of version " + detectedVersion + ", but can only handle version " + supportedVersion);
|
||||
public UnsupportedVaultFormatException(Integer detectedVersion, Integer latestSupportedVersion) {
|
||||
super("Tried to open vault of version " + detectedVersion + ", latest supported version is " + latestSupportedVersion);
|
||||
this.detectedVersion = detectedVersion;
|
||||
this.supportedVersion = supportedVersion;
|
||||
this.latestSupportedVersion = latestSupportedVersion;
|
||||
}
|
||||
|
||||
public Integer getDetectedVersion() {
|
||||
return detectedVersion;
|
||||
}
|
||||
|
||||
public Integer getSupportedVersion() {
|
||||
return supportedVersion;
|
||||
public Integer getLatestSupportedVersion() {
|
||||
return latestSupportedVersion;
|
||||
}
|
||||
|
||||
public boolean isVaultOlderThanSoftware() {
|
||||
return detectedVersion == null || detectedVersion < supportedVersion;
|
||||
return detectedVersion == null || detectedVersion < latestSupportedVersion;
|
||||
}
|
||||
|
||||
public boolean isSoftwareOlderThanVault() {
|
||||
return detectedVersion > supportedVersion;
|
||||
return detectedVersion > latestSupportedVersion;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ public final class Constants {
|
||||
private Constants() {
|
||||
}
|
||||
|
||||
static final Integer CURRENT_VAULT_VERSION = 3;
|
||||
static final Integer CURRENT_VAULT_VERSION = 5;
|
||||
|
||||
public static final int PAYLOAD_SIZE = 32 * 1024;
|
||||
public static final int NONCE_SIZE = 16;
|
||||
|
||||
@@ -18,6 +18,7 @@ import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
@@ -81,14 +82,15 @@ class CryptorImpl implements Cryptor {
|
||||
|
||||
@Override
|
||||
public void randomizeMasterkey() {
|
||||
final byte[] randomBytes = new byte[KEYLENGTH_IN_BYTES];
|
||||
try {
|
||||
randomSource.nextBytes(randomBytes);
|
||||
encryptionKey = new SecretKeySpec(randomBytes, ENCRYPTION_ALG);
|
||||
randomSource.nextBytes(randomBytes);
|
||||
macKey = new SecretKeySpec(randomBytes, MAC_ALG);
|
||||
} finally {
|
||||
Arrays.fill(randomBytes, (byte) 0x00);
|
||||
KeyGenerator encKeyGen = KeyGenerator.getInstance(ENCRYPTION_ALG);
|
||||
encKeyGen.init(KEYLENGTH_IN_BYTES * Byte.SIZE, randomSource);
|
||||
encryptionKey = encKeyGen.generateKey();
|
||||
KeyGenerator macKeyGen = KeyGenerator.getInstance(MAC_ALG);
|
||||
macKeyGen.init(KEYLENGTH_IN_BYTES * Byte.SIZE, randomSource);
|
||||
macKey = macKeyGen.generateKey();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("Hard-coded algorithm doesn't exist.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,12 +118,12 @@ class CryptorImpl implements Cryptor {
|
||||
final SecretKey kek = new SecretKeySpec(kekBytes, ENCRYPTION_ALG);
|
||||
this.macKey = AesKeyWrap.unwrap(kek, keyFile.getMacMasterKey(), MAC_ALG);
|
||||
// future use (as soon as we need to prevent downgrade attacks):
|
||||
// final Mac mac = new ThreadLocalMac(macKey, MAC_ALG).get();
|
||||
// final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.BYTES).putInt(CURRENT_VAULT_VERSION).array());
|
||||
// if (!MessageDigest.isEqual(versionMac, keyFile.getVersionMac())) {
|
||||
// destroyQuietly(macKey);
|
||||
// throw new UnsupportedVaultFormatException(Integer.MAX_VALUE, CURRENT_VAULT_VERSION);
|
||||
// }
|
||||
// final Mac mac = new ThreadLocalMac(macKey, MAC_ALG).get();
|
||||
// final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.BYTES).putInt(CURRENT_VAULT_VERSION).array());
|
||||
// if (!MessageDigest.isEqual(versionMac, keyFile.getVersionMac())) {
|
||||
// destroyQuietly(macKey);
|
||||
// throw new UnsupportedVaultFormatException(Integer.MAX_VALUE, CURRENT_VAULT_VERSION);
|
||||
// }
|
||||
this.encryptionKey = AesKeyWrap.unwrap(kek, keyFile.getEncryptionMasterKey(), ENCRYPTION_ALG);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidPassphraseException();
|
||||
|
||||
@@ -22,7 +22,6 @@ import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
@@ -47,7 +46,6 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
|
||||
private final Supplier<Mac> hmacSha256;
|
||||
private final FileHeader header;
|
||||
private final boolean authenticate;
|
||||
private final LongAdder cleartextBytesDecrypted = new LongAdder();
|
||||
private ByteBuffer ciphertextBuffer = ByteBuffer.allocate(CHUNK_SIZE);
|
||||
private long chunkNumber = 0;
|
||||
|
||||
@@ -56,11 +54,11 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
|
||||
this.header = FileHeader.decrypt(headerKey, hmacSha256, header);
|
||||
this.authenticate = authenticate;
|
||||
this.chunkNumber = firstCiphertextByte / CHUNK_SIZE; // floor() by int-truncation
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() {
|
||||
return header.getPayload().getFilesize();
|
||||
|
||||
// vault version 5 and onwards should have filesize: -1
|
||||
if (this.header.getPayload().getFilesize() != -1l) {
|
||||
throw new UncheckedIOException(new IOException("Attempted to decrypt file with invalid header (probably from previous vault version)"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -105,15 +103,7 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
|
||||
@Override
|
||||
public ByteBuffer cleartext() throws InterruptedException {
|
||||
try {
|
||||
final ByteBuffer cleartext = dataProcessor.processedData();
|
||||
long bytesUntilLogicalEof = contentLength() - cleartextBytesDecrypted.sum();
|
||||
if (bytesUntilLogicalEof <= 0) {
|
||||
return FileContentCryptor.EOF;
|
||||
} else if (bytesUntilLogicalEof < cleartext.remaining()) {
|
||||
cleartext.limit((int) bytesUntilLogicalEof);
|
||||
}
|
||||
cleartextBytesDecrypted.add(cleartext.remaining());
|
||||
return cleartext;
|
||||
return dataProcessor.processedData();
|
||||
} catch (ExecutionException e) {
|
||||
if (e.getCause() instanceof AuthenticationFailedException) {
|
||||
throw new AuthenticationFailedException(e);
|
||||
|
||||
@@ -36,8 +36,6 @@ import org.cryptomator.io.ByteBuffers;
|
||||
class FileContentEncryptorImpl implements FileContentEncryptor {
|
||||
|
||||
private static final String HMAC_SHA256 = "HmacSHA256";
|
||||
private static final int PADDING_LOWER_BOUND = 4 * 1024; // 4k
|
||||
private static final int PADDING_UPPER_BOUND = 16 * 1024 * 1024; // 16M
|
||||
private static final int NUM_THREADS = Runtime.getRuntime().availableProcessors();
|
||||
private static final int READ_AHEAD = 2;
|
||||
private static final ExecutorService SHARED_DECRYPTION_EXECUTOR = Executors.newFixedThreadPool(NUM_THREADS);
|
||||
@@ -63,7 +61,7 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
|
||||
|
||||
@Override
|
||||
public ByteBuffer getHeader() {
|
||||
header.getPayload().setFilesize(cleartextBytesScheduledForEncryption.sum());
|
||||
header.getPayload().setFilesize(-1l);
|
||||
return header.toByteBuffer(headerKey, hmacSha256);
|
||||
}
|
||||
|
||||
@@ -76,7 +74,6 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
|
||||
public void append(ByteBuffer cleartext) throws InterruptedException {
|
||||
cleartextBytesScheduledForEncryption.add(cleartext.remaining());
|
||||
if (cleartext == FileContentCryptor.EOF) {
|
||||
appendSizeObfuscationPadding(cleartextBytesScheduledForEncryption.sum());
|
||||
submitCleartextBuffer();
|
||||
submitEof();
|
||||
} else {
|
||||
@@ -84,19 +81,6 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
|
||||
}
|
||||
}
|
||||
|
||||
private void appendSizeObfuscationPadding(long actualSize) throws InterruptedException {
|
||||
final int maxPaddingLength = (int) Math.min(Math.max(actualSize / 10, PADDING_LOWER_BOUND), PADDING_UPPER_BOUND); // preferably 10%, but at least lower bound and no more than upper bound
|
||||
final int randomPaddingLength = randomSource.nextInt(maxPaddingLength);
|
||||
final ByteBuffer buf = ByteBuffer.allocate(PAYLOAD_SIZE);
|
||||
int remainingPadding = randomPaddingLength;
|
||||
while (remainingPadding > 0) {
|
||||
int bytesInCurrentIteration = Math.min(remainingPadding, PAYLOAD_SIZE);
|
||||
buf.clear().limit(bytesInCurrentIteration);
|
||||
appendAllAndSubmitIfFull(buf);
|
||||
remainingPadding -= bytesInCurrentIteration;
|
||||
}
|
||||
}
|
||||
|
||||
private void appendAllAndSubmitIfFull(ByteBuffer cleartext) throws InterruptedException {
|
||||
while (cleartext.hasRemaining()) {
|
||||
ByteBuffers.copy(cleartext, cleartextBuffer);
|
||||
|
||||
@@ -11,12 +11,14 @@ package org.cryptomator.crypto.engine.impl;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.ShortBufferException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
@@ -36,13 +38,13 @@ class FileHeaderPayload implements Destroyable {
|
||||
private final SecretKey contentKey;
|
||||
|
||||
public FileHeaderPayload(SecureRandom randomSource) {
|
||||
filesize = 0;
|
||||
final byte[] contentKey = new byte[CONTENT_KEY_LEN];
|
||||
this.filesize = 0;
|
||||
try {
|
||||
randomSource.nextBytes(contentKey);
|
||||
this.contentKey = new SecretKeySpec(contentKey, AES);
|
||||
} finally {
|
||||
Arrays.fill(contentKey, (byte) 0x00);
|
||||
KeyGenerator keyGen = KeyGenerator.getInstance(AES);
|
||||
keyGen.init(CONTENT_KEY_LEN * Byte.SIZE, randomSource);
|
||||
this.contentKey = keyGen.generateKey();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("Hard-coded algorithm doesn't exist.", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.crypto.AEADBadTagException;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.apache.commons.codec.binary.Base32;
|
||||
@@ -26,7 +27,8 @@ import org.cryptomator.siv.SivMode;
|
||||
class FilenameCryptorImpl implements FilenameCryptor {
|
||||
|
||||
private static final BaseNCodec BASE32 = new Base32();
|
||||
private static final Pattern BASE32_PATTERN = Pattern.compile("([A-Z0-9]{8})*[A-Z0-9=]{8}");
|
||||
// https://tools.ietf.org/html/rfc4648#section-6
|
||||
private static final Pattern BASE32_PATTERN = Pattern.compile("^([A-Z2-7]{8})*[A-Z2-7=]{8}");
|
||||
private static final ThreadLocal<MessageDigest> SHA1 = new ThreadLocalSha1();
|
||||
private static final ThreadLocal<SivMode> AES_SIV = new ThreadLocal<SivMode>() {
|
||||
@Override
|
||||
@@ -69,8 +71,8 @@ class FilenameCryptorImpl implements FilenameCryptor {
|
||||
try {
|
||||
final byte[] cleartextBytes = AES_SIV.get().decrypt(encryptionKey, macKey, encryptedBytes, associatedData);
|
||||
return new String(cleartextBytes, UTF_8);
|
||||
} catch (AEADBadTagException e) {
|
||||
throw new AuthenticationFailedException("Authentication failed.", e);
|
||||
} catch (AEADBadTagException | IllegalBlockSizeException e) {
|
||||
throw new AuthenticationFailedException("Invalid ciphertext.", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,11 +100,6 @@ class BlockAlignedReadableFile implements ReadableFile {
|
||||
return delegate.isOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws UncheckedIOException {
|
||||
return delegate.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws UncheckedIOException {
|
||||
delegate.close();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import static org.cryptomator.filesystem.crypto.Constants.DIR_SUFFIX;
|
||||
import static org.cryptomator.filesystem.crypto.Constants.DIR_PREFIX;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
@@ -12,7 +13,7 @@ import java.util.regex.Pattern;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.io.FileContents;
|
||||
import org.cryptomator.filesystem.ReadableFile;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -20,6 +21,7 @@ final class ConflictResolver {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ConflictResolver.class);
|
||||
private static final int UUID_FIRST_GROUP_STRLEN = 8;
|
||||
private static final int MAX_DIR_FILE_SIZE = 87; // "normal" file header has 88 bytes
|
||||
|
||||
private final Pattern encryptedNamePattern;
|
||||
private final Function<String, Optional<String>> nameDecryptor;
|
||||
@@ -32,7 +34,7 @@ final class ConflictResolver {
|
||||
}
|
||||
|
||||
public File resolveIfNecessary(File file) {
|
||||
Matcher m = encryptedNamePattern.matcher(StringUtils.removeEnd(file.name(), DIR_SUFFIX));
|
||||
Matcher m = encryptedNamePattern.matcher(StringUtils.removeStart(file.name(), DIR_PREFIX));
|
||||
if (m.matches()) {
|
||||
// full match, use file as is
|
||||
return file;
|
||||
@@ -47,18 +49,16 @@ final class ConflictResolver {
|
||||
|
||||
private File resolveConflict(File conflictingFile, MatchResult matchResult) {
|
||||
String ciphertext = matchResult.group();
|
||||
boolean isDirectory = conflictingFile.name().substring(matchResult.end()).startsWith(DIR_SUFFIX);
|
||||
boolean isDirectory = conflictingFile.name().startsWith(DIR_PREFIX);
|
||||
Optional<String> cleartext = nameDecryptor.apply(ciphertext);
|
||||
if (cleartext.isPresent()) {
|
||||
Folder folder = conflictingFile.parent().get();
|
||||
File canonicalFile = folder.file(isDirectory ? ciphertext + DIR_SUFFIX : ciphertext);
|
||||
if (canonicalFile.exists()) {
|
||||
File canonicalFile = folder.file(isDirectory ? DIR_PREFIX + ciphertext : ciphertext);
|
||||
if (isDirectory && canonicalFile.exists() && isSameFileBasedOnSample(canonicalFile, conflictingFile, MAX_DIR_FILE_SIZE)) {
|
||||
// there must not be two directories pointing to the same directory id. In this case no human interaction is needed to resolve this conflict:
|
||||
if (isDirectory && FileContents.UTF_8.readContents(canonicalFile).equals(FileContents.UTF_8.readContents(conflictingFile))) {
|
||||
conflictingFile.delete();
|
||||
return canonicalFile;
|
||||
}
|
||||
|
||||
conflictingFile.delete();
|
||||
return canonicalFile;
|
||||
} else {
|
||||
// conventional conflict detected! look for an alternative name:
|
||||
File alternativeFile;
|
||||
String conflictId;
|
||||
@@ -66,14 +66,11 @@ final class ConflictResolver {
|
||||
conflictId = createConflictId();
|
||||
String alternativeCleartext = cleartext.get() + " (Conflict " + conflictId + ")";
|
||||
String alternativeCiphertext = nameEncryptor.apply(alternativeCleartext).get();
|
||||
alternativeFile = folder.file(isDirectory ? alternativeCiphertext + DIR_SUFFIX : alternativeCiphertext);
|
||||
alternativeFile = folder.file(isDirectory ? DIR_PREFIX + alternativeCiphertext : alternativeCiphertext);
|
||||
} while (alternativeFile.exists());
|
||||
LOG.info("Detected conflict {}:\n{}\n{}", conflictId, canonicalFile, conflictingFile);
|
||||
conflictingFile.moveTo(alternativeFile);
|
||||
return alternativeFile;
|
||||
} else {
|
||||
conflictingFile.moveTo(canonicalFile);
|
||||
return canonicalFile;
|
||||
}
|
||||
} else {
|
||||
// not decryptable; false positive
|
||||
@@ -81,6 +78,26 @@ final class ConflictResolver {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSameFileBasedOnSample(File file1, File file2, int sampleSize) {
|
||||
if (file1.size() != file2.size()) {
|
||||
return false;
|
||||
} else {
|
||||
try (ReadableFile r1 = file1.openReadable(); ReadableFile r2 = file2.openReadable()) {
|
||||
ByteBuffer beginOfFile1 = ByteBuffer.allocate(sampleSize);
|
||||
ByteBuffer beginOfFile2 = ByteBuffer.allocate(sampleSize);
|
||||
int bytesRead1 = r1.read(beginOfFile1);
|
||||
int bytesRead2 = r2.read(beginOfFile2);
|
||||
if (bytesRead1 == bytesRead2) {
|
||||
beginOfFile1.flip();
|
||||
beginOfFile2.flip();
|
||||
return beginOfFile1.equals(beginOfFile2);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String createConflictId() {
|
||||
return UUID.randomUUID().toString().substring(0, UUID_FIRST_GROUP_STRLEN);
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ public final class Constants {
|
||||
static final String DATA_ROOT_DIR = "d";
|
||||
static final String ROOT_DIRECOTRY_ID = "";
|
||||
|
||||
static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
|
||||
static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup";
|
||||
public static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
|
||||
public static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup";
|
||||
|
||||
static final String DIR_SUFFIX = "_";
|
||||
static final String DIR_PREFIX = "0";
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
*******************************************************************************/
|
||||
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.nio.file.FileAlreadyExistsException;
|
||||
import java.util.Optional;
|
||||
@@ -28,6 +31,25 @@ class CryptoFile extends CryptoNode implements File {
|
||||
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
|
||||
public ReadableFile openReadable() {
|
||||
boolean authenticate = !fileSystem().delegate().shouldSkipAuthentication(toString());
|
||||
|
||||
@@ -10,7 +10,8 @@ package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.cryptomator.filesystem.crypto.Constants.DIR_SUFFIX;
|
||||
import static org.apache.commons.lang3.StringUtils.removeStart;
|
||||
import static org.cryptomator.filesystem.crypto.Constants.DIR_PREFIX;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.UncheckedIOException;
|
||||
@@ -54,9 +55,9 @@ class CryptoFolder extends CryptoNode implements Folder {
|
||||
@Override
|
||||
protected Optional<String> encryptedName() {
|
||||
if (parent().isPresent()) {
|
||||
return parent().get().encryptChildName(name()).map(s -> s + DIR_SUFFIX);
|
||||
return parent().get().encryptChildName(name()).map(s -> DIR_PREFIX + s);
|
||||
} else {
|
||||
return Optional.of(cryptor.getFilenameCryptor().encryptFilename(name()) + DIR_SUFFIX);
|
||||
return Optional.of(DIR_PREFIX + cryptor.getFilenameCryptor().encryptFilename(name()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,15 +92,15 @@ class CryptoFolder extends CryptoNode implements Folder {
|
||||
private Stream<File> nonConflictingFiles() {
|
||||
if (exists()) {
|
||||
final Stream<? extends File> files = physicalFolder().filter(Folder::exists).map(Folder::files).orElse(Stream.empty());
|
||||
return files.filter(containsEncryptedName()).map(conflictResolver::resolveIfNecessary).distinct();
|
||||
return files.filter(startsWithEncryptedName()).map(conflictResolver::resolveIfNecessary).distinct();
|
||||
} else {
|
||||
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();
|
||||
return (File file) -> encryptedNamePattern.matcher(file.name()).find();
|
||||
return (File file) -> encryptedNamePattern.matcher(removeStart(file.name(),DIR_PREFIX)).find();
|
||||
}
|
||||
|
||||
Optional<String> decryptChildName(String ciphertextFileName) {
|
||||
@@ -121,20 +122,20 @@ class CryptoFolder extends CryptoNode implements Folder {
|
||||
|
||||
@Override
|
||||
public Stream<CryptoFile> files() {
|
||||
return nonConflictingFiles().map(File::name).filter(endsWithDirSuffix().negate()).map(this::decryptChildName).filter(Optional::isPresent).map(Optional::get).map(this::file);
|
||||
return nonConflictingFiles().map(File::name).filter(startsWithDirPrefix().negate()).map(this::decryptChildName).filter(Optional::isPresent).map(Optional::get).map(this::file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<CryptoFolder> folders() {
|
||||
return nonConflictingFiles().map(File::name).filter(endsWithDirSuffix()).map(this::removeDirSuffix).map(this::decryptChildName).filter(Optional::isPresent).map(Optional::get).map(this::folder);
|
||||
return nonConflictingFiles().map(File::name).filter(startsWithDirPrefix()).map(this::removeDirPrefix).map(this::decryptChildName).filter(Optional::isPresent).map(Optional::get).map(this::folder);
|
||||
}
|
||||
|
||||
private Predicate<String> endsWithDirSuffix() {
|
||||
return (String encryptedFolderName) -> StringUtils.endsWith(encryptedFolderName, DIR_SUFFIX);
|
||||
private Predicate<String> startsWithDirPrefix() {
|
||||
return (String encryptedFolderName) -> StringUtils.startsWith(encryptedFolderName, DIR_PREFIX);
|
||||
}
|
||||
|
||||
private String removeDirSuffix(String encryptedFolderName) {
|
||||
return StringUtils.removeEnd(encryptedFolderName, DIR_SUFFIX);
|
||||
private String removeDirPrefix(String encryptedFolderName) {
|
||||
return StringUtils.removeStart(encryptedFolderName, DIR_PREFIX);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -70,12 +70,6 @@ class CryptoReadableFile implements ReadableFile {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws UncheckedIOException {
|
||||
assert decryptor != null : "decryptor is always being set during position(long)";
|
||||
return decryptor.contentLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void position(long position) throws UncheckedIOException {
|
||||
if (readAheadTask != null) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
@@ -52,10 +53,14 @@ class Masterkeys {
|
||||
public Cryptor decrypt(Folder vaultLocation, CharSequence passphrase) throws InvalidPassphraseException {
|
||||
File masterkeyFile = vaultLocation.file(MASTERKEY_FILENAME);
|
||||
Cryptor cryptor = cryptorProvider.get();
|
||||
boolean success = false;
|
||||
try {
|
||||
readMasterKey(masterkeyFile, cryptor, passphrase);
|
||||
} catch (UncheckedIOException e) {
|
||||
cryptor.destroy();
|
||||
success = true;
|
||||
} finally {
|
||||
if (!success) {
|
||||
cryptor.destroy();
|
||||
}
|
||||
}
|
||||
return cryptor;
|
||||
}
|
||||
@@ -86,7 +91,9 @@ class Masterkeys {
|
||||
/* I/O */
|
||||
|
||||
private static void readMasterKey(File file, Cryptor cryptor, CharSequence passphrase) throws UncheckedIOException, InvalidPassphraseException {
|
||||
try (InputStream in = Channels.newInputStream(file.openReadable())) {
|
||||
try ( //
|
||||
ReadableByteChannel channel = file.openReadable(); //
|
||||
InputStream in = Channels.newInputStream(channel)) {
|
||||
final byte[] fileContents = IOUtils.toByteArray(in);
|
||||
cryptor.readKeysFromMasterkeyFile(fileContents, passphrase);
|
||||
} catch (IOException e) {
|
||||
@@ -96,6 +103,7 @@ class Masterkeys {
|
||||
|
||||
private static void writeMasterKey(File file, Cryptor cryptor, CharSequence passphrase) throws UncheckedIOException {
|
||||
try (WritableFile writable = file.openWritable()) {
|
||||
writable.truncate();
|
||||
final byte[] fileContents = cryptor.writeKeysToMasterkeyFile(passphrase);
|
||||
writable.write(ByteBuffer.wrap(fileContents));
|
||||
}
|
||||
|
||||
@@ -44,16 +44,9 @@ class NoFileContentCryptor implements FileContentCryptor {
|
||||
private class Decryptor implements FileContentDecryptor {
|
||||
|
||||
private final BlockingQueue<Supplier<ByteBuffer>> cleartextQueue = new LinkedBlockingQueue<>();
|
||||
private final long contentLength;
|
||||
|
||||
private Decryptor(ByteBuffer header) {
|
||||
assert header.remaining() == Long.BYTES;
|
||||
this.contentLength = header.getLong();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() {
|
||||
return contentLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,20 +21,20 @@ public class CryptorImplTest {
|
||||
|
||||
@Test
|
||||
public void testMasterkeyDecryptionWithCorrectPassphrase() throws IOException {
|
||||
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
|
||||
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
|
||||
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
||||
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
|
||||
}
|
||||
|
||||
@Test(expected = InvalidPassphraseException.class)
|
||||
public void testMasterkeyDecryptionWithWrongPassphrase() throws IOException {
|
||||
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
|
||||
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
|
||||
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
||||
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "qwe");
|
||||
}
|
||||
@@ -44,7 +44,7 @@ public class CryptorImplTest {
|
||||
final String testMasterKey = "{\"version\":-1,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
|
||||
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
|
||||
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
||||
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
|
||||
}
|
||||
@@ -52,7 +52,7 @@ public class CryptorImplTest {
|
||||
@Ignore
|
||||
@Test(expected = UnsupportedVaultFormatException.class)
|
||||
public void testMasterkeyDecryptionWithMissingVersionMac() throws IOException {
|
||||
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"}";
|
||||
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
||||
@@ -62,20 +62,20 @@ public class CryptorImplTest {
|
||||
@Ignore
|
||||
@Test(expected = UnsupportedVaultFormatException.class)
|
||||
public void testMasterkeyDecryptionWithWrongVersionMac() throws IOException {
|
||||
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLa=\"}";
|
||||
+ "\"versionMac\":\"z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfoK=\"}";
|
||||
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
||||
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMasterkeyEncryption() throws IOException {
|
||||
final String expectedMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," //
|
||||
final String expectedMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," //
|
||||
+ "\"primaryMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
|
||||
+ "\"hmacMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
|
||||
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
|
||||
+ "\"versionMac\":\"yuwoRE9GSdgQ2b//qRpTCj3W0qsVLxYVa7/KB3PkfA4=\"}";
|
||||
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
||||
cryptor.randomizeMasterkey();
|
||||
final byte[] masterkeyFile = cryptor.writeKeysToMasterkeyFile("asd");
|
||||
|
||||
@@ -43,20 +43,6 @@ public class FileContentCryptorImplTest {
|
||||
|
||||
};
|
||||
|
||||
private static final SecureRandom RANDOM_MOCK_2 = new SecureRandom() {
|
||||
|
||||
@Override
|
||||
public int nextInt(int bound) {
|
||||
return 500;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nextBytes(byte[] bytes) {
|
||||
Arrays.fill(bytes, (byte) 0x00);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testShortHeaderInDecryptor() throws InterruptedException {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
@@ -137,45 +123,6 @@ public class FileContentCryptorImplTest {
|
||||
Assert.assertArrayEquals("cleartext message".getBytes(), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncryptionAndDecryptionWithSizeObfuscationPadding() throws InterruptedException {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
FileContentCryptor cryptor = new FileContentCryptorImpl(encryptionKey, macKey, RANDOM_MOCK_2);
|
||||
|
||||
ByteBuffer header = ByteBuffer.allocate(cryptor.getHeaderSize());
|
||||
ByteBuffer ciphertext = ByteBuffer.allocate(16 + 11 + 500 + 32 + 1); // 16 bytes iv + 11 bytes ciphertext + 500 bytes padding + 32 bytes mac + 1.
|
||||
try (FileContentEncryptor encryptor = cryptor.createFileContentEncryptor(Optional.empty(), 0)) {
|
||||
encryptor.append(ByteBuffer.wrap("hello world".getBytes()));
|
||||
encryptor.append(FileContentCryptor.EOF);
|
||||
ByteBuffer buf;
|
||||
while ((buf = encryptor.ciphertext()) != FileContentCryptor.EOF) {
|
||||
ByteBuffers.copy(buf, ciphertext);
|
||||
}
|
||||
ByteBuffers.copy(encryptor.getHeader(), header);
|
||||
}
|
||||
header.flip();
|
||||
ciphertext.flip();
|
||||
|
||||
Assert.assertEquals(16 + 11 + 500 + 32, ciphertext.remaining());
|
||||
|
||||
ByteBuffer plaintext = ByteBuffer.allocate(12); // 11 bytes plaintext + 1
|
||||
try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header, 0, true)) {
|
||||
decryptor.append(ciphertext);
|
||||
decryptor.append(FileContentCryptor.EOF);
|
||||
ByteBuffer buf;
|
||||
while ((buf = decryptor.cleartext()) != FileContentCryptor.EOF) {
|
||||
ByteBuffers.copy(buf, plaintext);
|
||||
}
|
||||
}
|
||||
plaintext.flip();
|
||||
|
||||
byte[] result = new byte[plaintext.remaining()];
|
||||
plaintext.get(result);
|
||||
Assert.assertArrayEquals("hello world".getBytes(), result);
|
||||
}
|
||||
|
||||
@Test(timeout = 20000) // assuming a minimum speed of 10mb/s during encryption and decryption 20s should be enough
|
||||
public void testEncryptionAndDecryptionSpeed() throws InterruptedException, IOException {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
|
||||
@@ -45,7 +45,7 @@ public class FileContentDecryptorImplTest {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
|
||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
|
||||
final byte[] content = Base64.decode("AAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3Og=");
|
||||
|
||||
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
|
||||
@@ -68,7 +68,7 @@ public class FileContentDecryptorImplTest {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJa==");
|
||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJG==");
|
||||
|
||||
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
|
||||
|
||||
@@ -80,7 +80,7 @@ public class FileContentDecryptorImplTest {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
|
||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
|
||||
final byte[] content = Base64.decode("aAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3Og=");
|
||||
|
||||
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
|
||||
@@ -101,7 +101,7 @@ public class FileContentDecryptorImplTest {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
|
||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
|
||||
final byte[] content = Base64.decode("AAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3OG=");
|
||||
|
||||
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, false)) {
|
||||
@@ -124,7 +124,7 @@ public class FileContentDecryptorImplTest {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
|
||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
|
||||
|
||||
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
|
||||
decryptor.cancelWithException(new IOException("can not do"));
|
||||
|
||||
@@ -35,20 +35,6 @@ public class FileContentEncryptorImplTest {
|
||||
|
||||
};
|
||||
|
||||
private static final SecureRandom RANDOM_MOCK_2 = new SecureRandom() {
|
||||
|
||||
@Override
|
||||
public int nextInt(int bound) {
|
||||
return 42;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nextBytes(byte[] bytes) {
|
||||
Arrays.fill(bytes, (byte) 0x00);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@Test
|
||||
public void testEncryption() throws InterruptedException {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
@@ -95,24 +81,4 @@ public class FileContentEncryptorImplTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSizeObfuscation() throws InterruptedException {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
|
||||
|
||||
try (FileContentEncryptor encryptor = new FileContentEncryptorImpl(headerKey, macKey, RANDOM_MOCK_2, 0)) {
|
||||
encryptor.append(FileContentCryptor.EOF);
|
||||
|
||||
ByteBuffer result = ByteBuffer.allocate(91); // 16 bytes iv + 42 bytes size obfuscation + 32 bytes mac + 1
|
||||
ByteBuffer buf;
|
||||
while ((buf = encryptor.ciphertext()) != FileContentCryptor.EOF) {
|
||||
ByteBuffers.copy(buf, result);
|
||||
}
|
||||
result.flip();
|
||||
|
||||
Assert.assertEquals(90, result.remaining());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -53,13 +53,26 @@ public class FileHeaderTest {
|
||||
|
||||
@Test
|
||||
public void testDecryption() {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
final ByteBuffer headerBuf = ByteBuffer.wrap(Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg=="));
|
||||
final FileHeader header = FileHeader.decrypt(headerKey, new ThreadLocalMac(macKey, "HmacSHA256"), headerBuf);
|
||||
|
||||
Assert.assertEquals(-1l, header.getPayload().getFilesize());
|
||||
Assert.assertArrayEquals(new byte[16], header.getIv());
|
||||
Assert.assertArrayEquals(new byte[32], header.getPayload().getContentKey().getEncoded());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecryptionOfOldHeader() {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
final ByteBuffer headerBuf = ByteBuffer.wrap(Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA=="));
|
||||
final FileHeader header = FileHeader.decrypt(headerKey, new ThreadLocalMac(macKey, "HmacSHA256"), headerBuf);
|
||||
|
||||
Assert.assertEquals(42, header.getPayload().getFilesize());
|
||||
Assert.assertEquals(42l, header.getPayload().getFilesize());
|
||||
Assert.assertArrayEquals(new byte[16], header.getIv());
|
||||
Assert.assertArrayEquals(new byte[32], header.getPayload().getContentKey().getEncoded());
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public class ConflictResolverTest {
|
||||
unrelatedFile = Mockito.mock(File.class);
|
||||
|
||||
String canonicalFileName = encode.apply("test name").get();
|
||||
String canonicalFolderName = canonicalFileName + Constants.DIR_SUFFIX;
|
||||
String canonicalFolderName = Constants.DIR_PREFIX + canonicalFileName;
|
||||
String conflictingFileName = canonicalFileName + " (version 2)";
|
||||
String conflictingFolderName = canonicalFolderName + " (version 2)";
|
||||
String unrelatedName = "notBa$e32!";
|
||||
@@ -70,6 +70,7 @@ public class ConflictResolverTest {
|
||||
Mockito.doReturn(Optional.of(folder)).when(unrelatedFile).parent();
|
||||
|
||||
Mockito.when(folder.file(Mockito.startsWith(canonicalFileName.substring(0, 8)))).thenReturn(resolved);
|
||||
Mockito.when(folder.file(Mockito.startsWith(canonicalFolderName.substring(0, 8)))).thenReturn(resolved);
|
||||
Mockito.when(folder.file(canonicalFileName)).thenReturn(canonicalFile);
|
||||
Mockito.when(folder.file(canonicalFolderName)).thenReturn(canonicalFolder);
|
||||
Mockito.when(folder.file(conflictingFileName)).thenReturn(conflictingFile);
|
||||
@@ -79,29 +80,29 @@ public class ConflictResolverTest {
|
||||
|
||||
@Test
|
||||
public void testCanonicalName() {
|
||||
File resolved = conflictResolver.resolveIfNecessary(canonicalFile);
|
||||
Assert.assertSame(canonicalFile, resolved);
|
||||
File result = conflictResolver.resolveIfNecessary(canonicalFile);
|
||||
Assert.assertSame(canonicalFile, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnrelatedName() {
|
||||
File resolved = conflictResolver.resolveIfNecessary(unrelatedFile);
|
||||
Assert.assertSame(unrelatedFile, resolved);
|
||||
File result = conflictResolver.resolveIfNecessary(unrelatedFile);
|
||||
Assert.assertSame(unrelatedFile, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConflictingFile() {
|
||||
File resolved = conflictResolver.resolveIfNecessary(conflictingFile);
|
||||
File result = conflictResolver.resolveIfNecessary(conflictingFile);
|
||||
Mockito.verify(conflictingFile).moveTo(resolved);
|
||||
Assert.assertSame(resolved, resolved);
|
||||
Assert.assertSame(resolved, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConflictingFileIfCanonicalDoesntExist() {
|
||||
Mockito.when(canonicalFile.exists()).thenReturn(false);
|
||||
File resolved = conflictResolver.resolveIfNecessary(conflictingFile);
|
||||
Mockito.verify(conflictingFile).moveTo(canonicalFile);
|
||||
Assert.assertSame(canonicalFile, resolved);
|
||||
File result = conflictResolver.resolveIfNecessary(conflictingFile);
|
||||
Mockito.verify(conflictingFile).moveTo(resolved);
|
||||
Assert.assertSame(resolved, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -121,9 +122,11 @@ public class ConflictResolverTest {
|
||||
@Test
|
||||
public void testConflictingFolderWithSameId() {
|
||||
ReadableFile directoryId1 = Mockito.mock(ReadableFile.class);
|
||||
ReadableFile directoryId2 = Mockito.mock(ReadableFile.class);
|
||||
Mockito.when(canonicalFolder.openReadable()).thenReturn(directoryId1);
|
||||
Mockito.when(conflictingFolder.openReadable()).thenReturn(directoryId1);
|
||||
Mockito.when(conflictingFolder.openReadable()).thenReturn(directoryId2);
|
||||
Mockito.when(directoryId1.read(Mockito.any())).thenAnswer(new FillBufferAnswer("id1"));
|
||||
Mockito.when(directoryId2.read(Mockito.any())).thenAnswer(new FillBufferAnswer("id1"));
|
||||
|
||||
File result = conflictResolver.resolveIfNecessary(conflictingFolder);
|
||||
Mockito.verify(conflictingFolder).delete();
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.0</version>
|
||||
<version>1.2.0</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-inmemory</artifactId>
|
||||
<name>Cryptomator filesystem: In-memory mock</name>
|
||||
|
||||
@@ -43,6 +43,11 @@ class InMemoryFile extends InMemoryNode implements File {
|
||||
return buf;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws UncheckedIOException {
|
||||
return content.get().limit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveTo(File destination) throws UncheckedIOException {
|
||||
if (destination instanceof InMemoryFile) {
|
||||
@@ -103,7 +108,7 @@ class InMemoryFile extends InMemoryNode implements File {
|
||||
throw new UncheckedIOException(new FileAlreadyExistsException(k));
|
||||
} else {
|
||||
if (v == null) {
|
||||
assert!content.get().hasRemaining();
|
||||
assert !content.get().hasRemaining();
|
||||
this.creationTime = Instant.now();
|
||||
}
|
||||
this.lastModified = Instant.now();
|
||||
@@ -120,7 +125,7 @@ class InMemoryFile extends InMemoryNode implements File {
|
||||
// returning null removes the entry.
|
||||
return null;
|
||||
});
|
||||
assert!this.exists();
|
||||
assert !this.exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -51,11 +51,6 @@ class InMemoryReadableFile implements ReadableFile {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws UncheckedIOException {
|
||||
return contentGetter.get().limit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void position(long position) throws UncheckedIOException {
|
||||
assert position < Integer.MAX_VALUE : "Can not use that big in-memory files.";
|
||||
@@ -64,8 +59,10 @@ class InMemoryReadableFile implements ReadableFile {
|
||||
|
||||
@Override
|
||||
public void close() throws UncheckedIOException {
|
||||
open.set(false);
|
||||
readLock.unlock();
|
||||
if (open.get()) {
|
||||
open.set(false);
|
||||
readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -104,9 +104,7 @@ public class InMemoryFileSystemTest {
|
||||
Assert.assertTrue(fooFile.exists());
|
||||
|
||||
// check if size = 11 bytes
|
||||
try (ReadableFile readable = fooFile.openReadable()) {
|
||||
Assert.assertEquals(11, readable.size());
|
||||
}
|
||||
Assert.assertEquals(11, fooFile.size());
|
||||
|
||||
// copy foo to bar
|
||||
File barFile = fs.file("bar.txt");
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.0</version>
|
||||
<version>1.2.0</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-invariants-tests</artifactId>
|
||||
<name>Cryptomator filesystem: Invariants tests</name>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.0</version>
|
||||
<version>1.2.0</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-nameshortening</artifactId>
|
||||
<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 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 ConflictResolver() {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.0</version>
|
||||
<version>1.2.0</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-nio</artifactId>
|
||||
<name>Cryptomator filesystem: NIO-based physical layer</name>
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.cryptomator.filesystem.nio;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.AsynchronousFileChannel;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.CopyOption;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
@@ -16,6 +17,11 @@ import java.util.stream.Stream;
|
||||
|
||||
class DefaultNioAccess implements NioAccess {
|
||||
|
||||
@Override
|
||||
public long size(Path path) throws IOException {
|
||||
return Files.size(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AsynchronousFileChannel open(Path path, OpenOption... options) throws IOException {
|
||||
return AsynchronousFileChannel.open(path, options);
|
||||
@@ -53,7 +59,18 @@ class DefaultNioAccess implements NioAccess {
|
||||
|
||||
@Override
|
||||
public void delete(Path path) throws IOException {
|
||||
Files.delete(path);
|
||||
try {
|
||||
Files.delete(path);
|
||||
} catch (AccessDeniedException e) {
|
||||
// workaround for https://github.com/cryptomator/cryptomator/issues/317
|
||||
try {
|
||||
if (path.toFile().delete())
|
||||
return;
|
||||
} catch (UnsupportedOperationException e2) {
|
||||
// ignore
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -16,6 +16,8 @@ interface NioAccess {
|
||||
|
||||
public static final Holder<NioAccess> DEFAULT = new Holder<>(new DefaultNioAccess());
|
||||
|
||||
long size(Path path) throws IOException;
|
||||
|
||||
AsynchronousFileChannel open(Path path, OpenOption... options) throws IOException;
|
||||
|
||||
boolean isRegularFile(Path path, LinkOption... options);
|
||||
|
||||
@@ -27,16 +27,33 @@ class NioFile extends NioNode implements File {
|
||||
sharedChannel = instanceFactory.sharedFileChannel(path, nioAccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws UncheckedIOException {
|
||||
try {
|
||||
return nioAccess.size(path);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadableFile openReadable() throws UncheckedIOException {
|
||||
if (lock.getWriteHoldCount() > 0) {
|
||||
throw new IllegalStateException("Current thread is currently writing this file");
|
||||
throw new IllegalStateException("Current thread is currently writing " + path);
|
||||
}
|
||||
if (lock.getReadHoldCount() > 0) {
|
||||
throw new IllegalStateException("Current thread is already reading this file");
|
||||
throw new IllegalStateException("Current thread is already reading " + path);
|
||||
}
|
||||
lock.readLock().lock();
|
||||
return instanceFactory.readableNioFile(path, sharedChannel, this::unlockReadLock);
|
||||
ReadableFile result = null;
|
||||
try {
|
||||
result = instanceFactory.readableNioFile(path, sharedChannel, this::unlockReadLock);
|
||||
} finally {
|
||||
if (result == null) {
|
||||
unlockReadLock();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void unlockReadLock() {
|
||||
@@ -46,13 +63,21 @@ class NioFile extends NioNode implements File {
|
||||
@Override
|
||||
public WritableFile openWritable() throws UncheckedIOException {
|
||||
if (lock.getWriteHoldCount() > 0) {
|
||||
throw new IllegalStateException("Current thread is already writing this file");
|
||||
throw new IllegalStateException("Current thread is already writing " + path);
|
||||
}
|
||||
if (lock.getReadHoldCount() > 0) {
|
||||
throw new IllegalStateException("Current thread is currently reading this file");
|
||||
throw new IllegalStateException("Current thread is currently reading " + path);
|
||||
}
|
||||
lockWriteLock();
|
||||
return instanceFactory.writableNioFile(fileSystem(), path, sharedChannel, this::unlockWriteLock);
|
||||
WritableFile result = null;
|
||||
try {
|
||||
result = instanceFactory.writableNioFile(fileSystem(), path, sharedChannel, this::unlockWriteLock);
|
||||
} finally {
|
||||
if (result == null) {
|
||||
unlockWriteLock();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// visible for testing
|
||||
|
||||
@@ -41,11 +41,6 @@ class ReadableNioFile implements ReadableFile {
|
||||
return open;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws UncheckedIOException {
|
||||
return channel.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void position(long position) throws UncheckedIOException {
|
||||
assertOpen();
|
||||
|
||||
@@ -16,6 +16,7 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Instant;
|
||||
@@ -85,6 +86,27 @@ public class NioFileTest {
|
||||
|
||||
}
|
||||
|
||||
public class Size {
|
||||
|
||||
@Test
|
||||
public void testSizeReturnsSizeOfRegularFile() throws IOException {
|
||||
when(nioAccess.size(path)).thenReturn(42l);
|
||||
|
||||
assertThat(inTest.size(), is(42l));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSizeThrowsExceptionIfRegularFileThrowsException() throws IOException {
|
||||
Throwable t = new NoSuchFileException("foo");
|
||||
when(nioAccess.size(path)).thenThrow(t);
|
||||
|
||||
thrown.expect(UncheckedIOException.class);
|
||||
thrown.expectCause(org.hamcrest.Matchers.sameInstance(t));
|
||||
inTest.size();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class Open {
|
||||
|
||||
@Test
|
||||
@@ -99,10 +121,11 @@ public class NioFileTest {
|
||||
|
||||
@Test
|
||||
public void testOpenReadableInvokedBeforeInvokingAfterCloseOperationThrowsIllegalStateException() {
|
||||
when(instanceFactory.readableNioFile(same(path), same(channel), any())).thenReturn(mock(ReadableNioFile.class));
|
||||
inTest.openReadable();
|
||||
|
||||
thrown.expect(IllegalStateException.class);
|
||||
thrown.expectMessage("already reading this file");
|
||||
thrown.expectMessage("already reading " + path);
|
||||
|
||||
inTest.openReadable();
|
||||
}
|
||||
@@ -111,7 +134,7 @@ public class NioFileTest {
|
||||
public void testOpenReadableInvokedAfterAfterCloseOperationCreatesNewReadableFile() {
|
||||
ReadableNioFile readableNioFile = mock(ReadableNioFile.class);
|
||||
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
||||
when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(null, readableNioFile);
|
||||
when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(mock(ReadableNioFile.class), readableNioFile);
|
||||
inTest.openReadable();
|
||||
captor.getValue().run();
|
||||
|
||||
@@ -122,10 +145,11 @@ public class NioFileTest {
|
||||
|
||||
@Test
|
||||
public void testOpenReadableInvokedBeforeInvokingAfterCloseOperationOfOpenWritableThrowsIllegalStateException() {
|
||||
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), any())).thenReturn(mock(WritableNioFile.class));
|
||||
inTest.openWritable();
|
||||
|
||||
thrown.expect(IllegalStateException.class);
|
||||
thrown.expectMessage("currently writing this file");
|
||||
thrown.expectMessage("currently writing " + path);
|
||||
|
||||
inTest.openReadable();
|
||||
}
|
||||
@@ -133,7 +157,7 @@ public class NioFileTest {
|
||||
@Test
|
||||
public void testOpenReadableInvokedAfterInvokingAfterCloseOperationWorks() {
|
||||
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
||||
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(null);
|
||||
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(mock(WritableNioFile.class));
|
||||
inTest.openWritable();
|
||||
captor.getValue().run();
|
||||
|
||||
@@ -154,7 +178,7 @@ public class NioFileTest {
|
||||
public void testOpenWritableInvokedAfterAfterCloseOperationCreatesNewWritableFile() {
|
||||
WritableNioFile writableNioFile = mock(WritableNioFile.class);
|
||||
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
||||
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(null, writableNioFile);
|
||||
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(mock(WritableNioFile.class), writableNioFile);
|
||||
inTest.openWritable();
|
||||
captor.getValue().run();
|
||||
|
||||
@@ -165,28 +189,31 @@ public class NioFileTest {
|
||||
|
||||
@Test
|
||||
public void testOpenWritableInvokedBeforeInvokingAfterCloseOperationThrowsIllegalStateException() {
|
||||
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), any())).thenReturn(mock(WritableNioFile.class));
|
||||
inTest.openWritable();
|
||||
|
||||
thrown.expect(IllegalStateException.class);
|
||||
thrown.expectMessage("already writing this file");
|
||||
thrown.expectMessage("already writing " + path);
|
||||
|
||||
inTest.openWritable();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOpenWritableInvokedBeforeInvokingAfterCloseOperationFromOpenReadableThrowsIllegalStateException() {
|
||||
when(instanceFactory.readableNioFile(same(path), same(channel), any())).thenReturn(mock(ReadableNioFile.class));
|
||||
inTest.openReadable();
|
||||
|
||||
thrown.expect(IllegalStateException.class);
|
||||
thrown.expectMessage("currently reading this file");
|
||||
thrown.expectMessage("currently reading " + path);
|
||||
|
||||
inTest.openWritable();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOpenWritableInvokedAfterInvokingAfterCloseOperationWorks() {
|
||||
ReadableNioFile readableNioFile = mock(ReadableNioFile.class);
|
||||
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
||||
when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(null);
|
||||
when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(readableNioFile);
|
||||
inTest.openReadable();
|
||||
captor.getValue().run();
|
||||
|
||||
|
||||
@@ -83,16 +83,6 @@ public class ReadableNioFileTest {
|
||||
inTest.position(-1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSizeReturnsSizeOfChannel() {
|
||||
long expectedSize = 85472;
|
||||
when(channel.size()).thenReturn(expectedSize);
|
||||
|
||||
long actualSize = inTest.size();
|
||||
|
||||
assertThat(actualSize, is(expectedSize));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadDelegatesToChannelReadFullyWithZeroPositionIfNotSet() {
|
||||
ByteBuffer buffer = mock(ByteBuffer.class);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.0</version>
|
||||
<version>1.2.0</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-stats</artifactId>
|
||||
<name>Cryptomator filesystem: Throughput statistics</name>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.0</version>
|
||||
<version>1.2.0</version>
|
||||
</parent>
|
||||
<artifactId>frontend-api</artifactId>
|
||||
<name>Cryptomator frontend: API</name>
|
||||
|
||||
@@ -14,12 +14,20 @@ import java.util.Optional;
|
||||
public interface Frontend extends AutoCloseable {
|
||||
|
||||
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 unmount() throws CommandFailedException;
|
||||
/**
|
||||
* Unmounts the file system and stops any file system handler threads.
|
||||
*/
|
||||
void close() throws Exception;
|
||||
|
||||
void reveal() throws CommandFailedException;
|
||||
|
||||
|
||||
@@ -16,10 +16,11 @@ public interface FrontendFactory {
|
||||
* Provides a new frontend to access the given folder.
|
||||
*
|
||||
* @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
|
||||
* @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>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.0</version>
|
||||
<version>1.2.0</version>
|
||||
</parent>
|
||||
<artifactId>frontend-webdav</artifactId>
|
||||
<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.util.EnumSet;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javax.servlet.DispatcherType;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
@@ -21,26 +23,38 @@ import org.cryptomator.frontend.webdav.filters.LoopbackFilter;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
|
||||
/**
|
||||
* The server needs to respond to requests to the root resource, because Windows is stupid.
|
||||
*/
|
||||
public class WindowsCompatibilityServlet extends HttpServlet {
|
||||
@Singleton
|
||||
class DefaultServlet extends HttpServlet {
|
||||
|
||||
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
|
||||
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||
resp.addHeader("DAV", "1, 2");
|
||||
resp.addHeader("MS-Author-Via", "DAV");
|
||||
// resp.addHeader("Allow", "OPTIONS, GET, HEAD, POST, TRACE, PROPFIND, PROPPATCH, MKCOL, COPY, PUT, DELETE, MOVE, LOCK, UNLOCK");
|
||||
resp.addHeader("Allow", "OPTIONS, GET, HEAD");
|
||||
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
|
||||
}
|
||||
|
||||
public static ServletContextHandler createServletContextHandler() {
|
||||
public ServletContextHandler createServletContextHandler() {
|
||||
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.addFilter(LoopbackFilter.class, ROOT_PATH, EnumSet.of(DispatcherType.REQUEST));
|
||||
servletContext.addFilter(LoopbackFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST));
|
||||
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 ServletContextHandler handler;
|
||||
private final URI uri;
|
||||
|
||||
private WebDavMount mount;
|
||||
|
||||
public WebDavFrontend(WebDavMounterProvider webdavMounterProvider, ServletContextHandler handler, URI uri) throws FrontendCreationFailedException {
|
||||
@@ -45,13 +46,13 @@ class WebDavFrontend implements Frontend {
|
||||
|
||||
@Override
|
||||
public void mount(Map<MountParam, Optional<String>> mountParams) throws CommandFailedException {
|
||||
mount = webdavMounterProvider.get().mount(uri, mountParams);
|
||||
mount = webdavMounterProvider.chooseMounter(mountParams).mount(uri, mountParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unmount() throws CommandFailedException {
|
||||
private void unmount() throws CommandFailedException {
|
||||
if (mount != null) {
|
||||
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;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
@@ -20,6 +23,7 @@ import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.frontend.Frontend;
|
||||
import org.cryptomator.frontend.FrontendCreationFailedException;
|
||||
import org.cryptomator.frontend.FrontendFactory;
|
||||
import org.cryptomator.frontend.FrontendId;
|
||||
import org.cryptomator.frontend.webdav.mount.WebDavMounterProvider;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
@@ -45,9 +49,10 @@ public class WebDavServer implements FrontendFactory {
|
||||
private final ContextHandlerCollection servletCollection;
|
||||
private final WebDavServletContextFactory servletContextFactory;
|
||||
private final WebDavMounterProvider webdavMounterProvider;
|
||||
private final Tarpit tarpit;
|
||||
|
||||
@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 ThreadPool tp = new QueuedThreadPool(MAX_THREADS, MIN_THREADS, THREAD_IDLE_SECONDS, queue);
|
||||
this.server = new Server(tp);
|
||||
@@ -55,8 +60,9 @@ public class WebDavServer implements FrontendFactory {
|
||||
this.servletCollection = new ContextHandlerCollection();
|
||||
this.servletContextFactory = servletContextFactory;
|
||||
this.webdavMounterProvider = webdavMounterProvider;
|
||||
|
||||
servletCollection.addHandler(WindowsCompatibilityServlet.createServletContextHandler());
|
||||
this.tarpit = tarpit;
|
||||
|
||||
servletCollection.addHandler(defaultServlet.createServletContextHandler());
|
||||
server.setConnectors(new Connector[] {localConnector});
|
||||
server.setHandler(servletCollection);
|
||||
}
|
||||
@@ -103,10 +109,8 @@ public class WebDavServer implements FrontendFactory {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Frontend create(Folder root, String contextPath) throws FrontendCreationFailedException {
|
||||
if (!contextPath.startsWith("/")) {
|
||||
throw new IllegalArgumentException("contextPath must begin with '/'");
|
||||
}
|
||||
public Frontend create(Folder root, FrontendId id, String name) throws FrontendCreationFailedException {
|
||||
String contextPath = format("/%s/%s", id, name);
|
||||
final URI uri;
|
||||
try {
|
||||
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);
|
||||
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.MacChunkedPutCompatibilityFilter;
|
||||
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.ResourceTypeChecker;
|
||||
import org.cryptomator.frontend.webdav.filters.UriNormalizationFilter.ResourceTypeChecker.ResourceType;
|
||||
@@ -34,10 +35,9 @@ import org.eclipse.jetty.servlet.ServletHolder;
|
||||
class WebDavServletContextFactory {
|
||||
|
||||
private static final String WILDCARD = "/*";
|
||||
|
||||
|
||||
@Inject
|
||||
public WebDavServletContextFactory() {
|
||||
}
|
||||
public WebDavServletContextFactory() {}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
servletContext.addServlet(servletHolder, WILDCARD);
|
||||
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(AcceptRangeFilter.class, 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.ReadableFile;
|
||||
import org.cryptomator.filesystem.jackrabbit.FileLocator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.common.io.ByteStreams;
|
||||
|
||||
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) {
|
||||
super(factory, lockManager, session, node);
|
||||
@@ -56,8 +58,11 @@ class DavFile extends DavNode<FileLocator> {
|
||||
if (!outputContext.hasStream()) {
|
||||
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())) {
|
||||
outputContext.setContentLength(src.size());
|
||||
ByteStreams.copy(src, dst);
|
||||
}
|
||||
}
|
||||
@@ -149,12 +154,7 @@ class DavFile extends DavNode<FileLocator> {
|
||||
|
||||
private Optional<DavProperty<?>> sizeProperty() {
|
||||
if (node.exists()) {
|
||||
try (ReadableFile src = node.openReadable()) {
|
||||
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();
|
||||
}
|
||||
return Optional.of(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, node.size()));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ class DavFileWithRange extends DavFile {
|
||||
if (!outputContext.hasStream()) {
|
||||
return;
|
||||
}
|
||||
final long contentLength = node.size();
|
||||
try (ReadableFile src = node.openReadable(); OutputStream out = outputContext.getOutputStream()) {
|
||||
final long contentLength = src.size();
|
||||
final Pair<Long, Long> range = getEffectiveRange(contentLength);
|
||||
if (range.getLeft() < 0 || range.getLeft() > range.getRight() || range.getRight() > 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;
|
||||
outputContext.setContentLength(rangeLength);
|
||||
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());
|
||||
InputStream limitedIn = ByteStreams.limit(Channels.newInputStream(src), rangeLength);
|
||||
ByteStreams.copy(limitedIn, out);
|
||||
|
||||
@@ -39,10 +39,10 @@ class DavFileWithUnsatisfiableRange extends DavFile {
|
||||
if (!outputContext.hasStream()) {
|
||||
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()) {
|
||||
final long contentLength = src.size();
|
||||
outputContext.setContentLength(contentLength);
|
||||
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), "bytes */" + contentLength);
|
||||
ByteStreams.copy(src, Channels.newChannel(out));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,8 @@ class ExclusiveSharedLockManager implements LockManager {
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -23,7 +23,7 @@ import org.cryptomator.frontend.Frontend.MountParam;
|
||||
final class FallbackWebDavMounter implements WebDavMounterStrategy {
|
||||
|
||||
@Override
|
||||
public boolean shouldWork() {
|
||||
public boolean shouldWork(Map<MountParam, Optional<String>> mountParams) {
|
||||
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
|
||||
public boolean shouldWork() {
|
||||
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 prefSchemeIsUnspecifiedOrWebDav = !prefScheme.isPresent() || prefScheme.get().equalsIgnoreCase("webdav");
|
||||
final Script checkScripts = Script.fromLines("which gvfs-mount xdg-open");
|
||||
try {
|
||||
checkScripts.execute();
|
||||
return true;
|
||||
return prefSchemeIsUnspecifiedOrWebDav;
|
||||
} catch (CommandFailedException e) {
|
||||
return false;
|
||||
}
|
||||
@@ -84,15 +86,7 @@ final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy {
|
||||
|
||||
@Override
|
||||
public void reveal() throws CommandFailedException {
|
||||
try {
|
||||
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);
|
||||
Script.fromLines("set -x", "gvfs-open \"webdav:$DAV_SSP\"").addEnv("DAV_SSP", webDavUri.getRawSchemeSpecificPart()).execute();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ final class MacOsXAppleScriptWebDavMounter implements WebDavMounterStrategy {
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ final class MacOsXShellScriptWebDavMounter implements WebDavMounterStrategy {
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.cryptomator.frontend.Frontend.MountParam;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@Singleton
|
||||
public class WebDavMounterProvider implements Provider<WebDavMounter> {
|
||||
public class WebDavMounterProvider {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WebDavMounterProvider.class);
|
||||
private final WebDavMounterStrategy choosenStrategy;
|
||||
private final Collection<WebDavMounterStrategy> availableStrategies;
|
||||
private final WebDavMounterStrategy fallbackStrategy;
|
||||
|
||||
@Inject
|
||||
public WebDavMounterProvider(MountStrategies availableStrategies) {
|
||||
this.choosenStrategy = getStrategyWhichShouldWork(availableStrategies);
|
||||
public WebDavMounterProvider(Set<WebDavMounterStrategy> availableStrategies, @Named("fallback") WebDavMounterStrategy fallbackStrategy) {
|
||||
this.availableStrategies = availableStrategies;
|
||||
this.fallbackStrategy = fallbackStrategy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebDavMounter get() {
|
||||
return this.choosenStrategy;
|
||||
}
|
||||
|
||||
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;
|
||||
public WebDavMounter chooseMounter(Map<MountParam, Optional<String>> mountParams) {
|
||||
WebDavMounterStrategy result = availableStrategies.stream().filter(strategy -> strategy.shouldWork(mountParams)).findFirst().orElse(fallbackStrategy);
|
||||
LOG.info("Using {}", result.getClass().getSimpleName());
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
******************************************************************************/
|
||||
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.
|
||||
*
|
||||
@@ -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
|
||||
*/
|
||||
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,
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
package org.cryptomator.frontend.webdav.mount;
|
||||
|
||||
import static java.util.stream.Collectors.toSet;
|
||||
import static java.util.stream.IntStream.rangeClosed;
|
||||
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -24,16 +24,21 @@ import org.apache.commons.lang3.SystemUtils;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
|
||||
@Singleton
|
||||
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
|
||||
public WindowsDriveLetters() {
|
||||
}
|
||||
|
||||
|
||||
public Set<Character> getOccupiedDriveLetters() {
|
||||
if (!SystemUtils.IS_OS_WINDOWS) {
|
||||
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();
|
||||
return StreamSupport.stream(rootDirs.spliterator(), false).map(Path::toString).map(CharUtils::toChar).map(Character::toUpperCase).collect(toSet());
|
||||
}
|
||||
|
||||
|
||||
public Set<Character> getAvailableDriveLetters() {
|
||||
return Sets.difference(A_TO_Z, getOccupiedDriveLetters());
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldWork() {
|
||||
public boolean shouldWork(Map<MountParam, Optional<String>> mountParams) {
|
||||
return SystemUtils.IS_OS_WINDOWS;
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
|
||||
mountScript.addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\'));
|
||||
return mountScript.execute(MOUNT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
|
||||
private void addProxyOverrides(URI uri) throws IOException, CommandFailedException {
|
||||
try {
|
||||
// get existing value for ProxyOverride key from reqistry:
|
||||
@@ -110,7 +110,7 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
|
||||
Process queryCmd = query.start();
|
||||
String queryStdOut = IOUtils.toString(queryCmd.getInputStream(), StandardCharsets.UTF_8);
|
||||
int queryResult = queryCmd.waitFor();
|
||||
|
||||
|
||||
// determine new value for ProxyOverride key:
|
||||
Set<String> overrides = new HashSet<>();
|
||||
Matcher matcher = REG_QUERY_PROXY_OVERRIDES_PATTERN.matcher(queryStdOut);
|
||||
@@ -122,7 +122,7 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
|
||||
overrides.add("<local>");
|
||||
overrides.add(uri.getHost());
|
||||
overrides.add(uri.getHost() + ":" + uri.getPort());
|
||||
|
||||
|
||||
// set new value:
|
||||
String overridesStr = StringUtils.join(overrides, ';');
|
||||
ProcessBuilder add = new ProcessBuilder("reg", "add", "\"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\"", "/v", "ProxyOverride", "/d", "\"" + overridesStr + "\"", "/f");
|
||||
@@ -133,6 +133,8 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
|
||||
String addStdErr = IOUtils.toString(addCmd.getErrorStream(), StandardCharsets.UTF_8);
|
||||
throw new CommandFailedException(addStdErr);
|
||||
}
|
||||
} catch (IOException | CommandFailedException e) {
|
||||
LOG.info("Failed to add proxy overrides", e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
InterruptedIOException ioException = new InterruptedIOException();
|
||||
@@ -158,7 +160,7 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
|
||||
private WindowsWebDavMount(String driveLetter) {
|
||||
this.driveLetter = CharUtils.toCharacterObject(driveLetter);
|
||||
this.openExplorerScript = fromLines("start explorer.exe " + driveLetter + ":");
|
||||
this.unmountScript = fromLines("net use " + driveLetter + ": /delete");
|
||||
this.unmountScript = fromLines("net use " + driveLetter + ": /delete /no");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -61,13 +61,18 @@ final class CommandRunner {
|
||||
static CommandResult execute(Script script, long timeout, TimeUnit unit) throws CommandFailedException {
|
||||
try {
|
||||
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;
|
||||
for (final String line : script.getLines()) {
|
||||
for (final String line : lines) {
|
||||
final String[] cmds = ArrayUtils.add(determineCli(), line);
|
||||
final Process proc = Runtime.getRuntime().exec(cmds, env.toArray(new String[0]));
|
||||
result = run(proc, timeout, unit);
|
||||
result.assertOk();
|
||||
}
|
||||
assert result != null;
|
||||
return result;
|
||||
} catch (IOException e) {
|
||||
throw new CommandFailedException(e);
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.frontend.webdav;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.Servlet;
|
||||
@@ -21,21 +23,26 @@ import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
|
||||
public class WindowsCompatibilityServletTest {
|
||||
public class DefaultServletTest {
|
||||
|
||||
private Tarpit tarpit = mock(Tarpit.class);
|
||||
|
||||
private DefaultServlet inTest = new DefaultServlet(tarpit);
|
||||
|
||||
@Test
|
||||
public void testFactory() throws ServletException {
|
||||
ServletHolder[] holders = WindowsCompatibilityServlet.createServletContextHandler().getServletHandler().getServlets();
|
||||
|
||||
ServletHolder[] holders = inTest.createServletContextHandler().getServletHandler().getServlets();
|
||||
Assert.assertEquals(1, holders.length);
|
||||
ServletHolder holder = holders[0];
|
||||
|
||||
Servlet servlet = holder.getServlet();
|
||||
Assert.assertTrue(servlet instanceof WindowsCompatibilityServlet);
|
||||
Assert.assertTrue(servlet instanceof DefaultServlet);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResponse() throws IOException, ServletException {
|
||||
final WindowsCompatibilityServlet servlet = new WindowsCompatibilityServlet();
|
||||
final DefaultServlet servlet = inTest;
|
||||
final HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
|
||||
final HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
|
||||
|
||||
@@ -10,12 +10,10 @@ package org.cryptomator.frontend.webdav;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.cryptomator.common.CommonsModule;
|
||||
|
||||
import dagger.Component;
|
||||
|
||||
@Singleton
|
||||
@Component(modules = {CommonsModule.class})
|
||||
@Component(modules = {WebDavModule.class})
|
||||
public interface WebDavComponent {
|
||||
|
||||
WebDavServer server();
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.1.0</version>
|
||||
<version>1.2.0</version>
|
||||
</parent>
|
||||
<artifactId>jacoco-report</artifactId>
|
||||
<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.0</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);
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user