Compare commits

..

2 Commits
1.2.2 ... 1.0.5

Author SHA1 Message Date
Sebastian Stenzel
5f22bd5785 Increased version number to 1.0.5 2016-05-10 14:55:02 +02:00
Sebastian Stenzel
7312c0f9db repeated commit 86000ac 2016-05-10 14:53:18 +02:00
196 changed files with 1202 additions and 5093 deletions

6
.gitignore vendored
View File

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

View File

@@ -1,30 +1,23 @@
language: java
sudo: required
dist: trusty
language: java
jdk:
- oraclejdk8
cache:
directories:
- $HOME/.m2
env:
global:
- secure: "Lgj042RD0X3rB8VZVZLWP1GetLhjd3PqI5JbJMlzgHJpDI6RkFIBLN9SWAGmkLPCehIp2zA5tu9+UVy0NNMxm9xz6SyjMCaxS28/fnYEXaNmwwDSF6O6gLUbdxyzoYIFPYOPmFxpzhebqnNIsxaM29oZpgRgUGqosCczQxiB+Ng=" #coveralls
- secure: "IfYURwZaDWuBDvyn47n0k1Zod/IQw1FF+CS5nnV08Q+NfC3vGGJMwV8m59XnbfwnWGxwvCaAbk4qP6s6+ijgZNKkvgfFMo3rfTok5zt43bIqgaFOANYV+OC/1c59gYD6ZUxhW5iNgMgU3qdsRtJuwSmfkVv/jKyLGfAbS4kN8BA=" #coverity
- 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)"
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
notifications:
webhooks:
urls:
@@ -37,10 +30,19 @@ notifications:
secure: "lngJ/HEAFBbD5AdiO9avMqptKpZHdmEwOzS9FabZjkdFh7yAYueTk5RniPUvShjsKtThYm7cJ8AtDMDwc07NvPrzbMBRtUJGwuDT+7c7YFALGFJ1NYi+emkC9x1oafvmPgEYSE+tMKzNcwrHi3ytGgKdIotsKwaF35QNXYA9aMs="
on_success: change
on_failure: always
before_deploy:
- mvn -fmain/pom.xml -Prelease clean package -DskipTests
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
deploy:
- provider: releases
provider: releases
prerelease: false
api_key:
secure: "ZjE1j93v3qbPIe2YbmhS319aCbMdLQw0HuymmluTurxXsZtn9D4t2+eTr99vBVxGRuB5lzzGezPR5zjk5W7iHF7xhwrawXrFzr2rPJWzWFt0aM+Ry2njU1ROTGGXGTbv4anWeBlgMxLEInTAy/9ytOGNJlec83yc0THpOY2wxnk="
@@ -51,13 +53,3 @@ 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

View File

@@ -2,7 +2,6 @@
[![Build Status](https://travis-ci.org/cryptomator/cryptomator.svg?branch=master)](https://travis-ci.org/cryptomator/cryptomator)
[![Coverity Scan Build Status](https://scan.coverity.com/projects/cryptomator-cryptomator/badge.svg?flat=1)](https://scan.coverity.com/projects/cryptomator-cryptomator)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/2a0adf3cec6a4143b91035d3924178f1)](https://www.codacy.com/app/cryptomator/cryptomator?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=cryptomator/cryptomator&amp;utm_campaign=Badge_Grade)
[![Coverage Status](https://coveralls.io/repos/github/cryptomator/cryptomator/badge.svg?branch=master)](https://coveralls.io/github/cryptomator/cryptomator?branch=master)
[![Join the chat at https://gitter.im/cryptomator/cryptomator](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/cryptomator/cryptomator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Twitter](https://img.shields.io/badge/twitter-@Cryptomator-blue.svg?style=flat)](http://twitter.com/Cryptomator)
@@ -14,13 +13,12 @@ Download native binaries of Cryptomator on [cryptomator.org](https://cryptomator
## Features
- Works with Dropbox, Google Drive, OneDrive, Nextcloud and any other cloud storage service which synchronizes with a local directory
- Works with Dropbox, Google Drive, OneDrive, and any other cloud storage service that synchronizes with a local directory
- Open Source means: No backdoors, control is better than trust
- Client-side: No accounts, no data shared with any online service
- Totally transparent: Just work on the virtual drive as if it were a USB flash drive
- AES encryption with 256-bit key length
- File names get encrypted
- Folder structure gets obfuscated
- Filenames get encrypted, too
- Use as many vaults in your Dropbox as you want, each having individual passwords
### Privacy
@@ -47,7 +45,7 @@ For more information on the security details visit [cryptomator.org](https://cry
* Java 8 + JCE unlimited strength policy files (needed for 256-bit keys)
* Maven 3
* Optional: OS-dependent build tools for native packaging (see [Windows](https://github.com/cryptomator/cryptomator-win), [OS X](https://github.com/cryptomator/cryptomator-osx), [Linux](https://github.com/cryptomator/builder-containers))
* Optional: OS-dependent build tools for native packaging (see [Windows](https://github.com/cryptomator/cryptomator-win), [OS X](https://github.com/cryptomator/cryptomator-osx), [Debian](https://github.com/cryptomator/cryptomator-deb))
### Run Maven

View File

@@ -8,7 +8,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.2.2</version>
<version>1.0.5</version>
</parent>
<artifactId>ant-kit</artifactId>
<packaging>pom</packaging>

View File

@@ -21,6 +21,21 @@
</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">
@@ -30,9 +45,7 @@
</fx:info>
<fx:platform j2se="8.0">
<fx:property name="cryptomator.logPath" value="~/.Cryptomator/cryptomator.log" />
<fx:property name="cryptomator.upgradeLogPath" value="~/.Cryptomator/upgrade.log" />
<fx:property name="cryptomator.settingsPath" value="~/.Cryptomator/settings.json" />
<fx:jvmarg value="-Xmx512m"/>
<fx:jvmarg value="-Xmx1048m"/>
</fx:platform>
<fx:resources>
<fx:fileset dir="antbuild" type="jar" includes="Cryptomator-${project.version}.jar" />
@@ -53,9 +66,7 @@
</fx:info>
<fx:platform j2se="8.0">
<fx:property name="cryptomator.logPath" value="~/.Cryptomator/cryptomator.log" />
<fx:property name="cryptomator.upgradeLogPath" value="~/.Cryptomator/upgrade.log" />
<fx:property name="cryptomator.settingsPath" value="~/.Cryptomator/settings.json" />
<fx:jvmarg value="-Xmx512m"/>
<fx:jvmarg value="-Xmx1048m"/>
</fx:platform>
<fx:resources>
<fx:fileset dir="antbuild" type="jar" includes="Cryptomator-${project.version}.jar" />

View File

@@ -9,7 +9,7 @@ Priority: optional
Architecture: APPLICATION_ARCH
Provides: APPLICATION_PACKAGE
Installed-Size: APPLICATION_INSTALLED_SIZE
Depends: gvfs-bin, gvfs-backends, gvfs-fuse
Depends: gvfs-bin, gvfs-backends, gvfs-fuse, xdg-utils
Description: Multi-platform client-side encryption of your cloud files.
Cryptomator provides free client-side AES encryption for your cloud files.
Create encrypted vaults, which get mounted as virtual volumes. Whatever

View File

@@ -10,7 +10,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.2.2</version>
<version>1.0.5</version>
</parent>
<artifactId>commons-test</artifactId>
<name>Cryptomator common test dependencies</name>

View File

@@ -10,7 +10,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.2.2</version>
<version>1.0.5</version>
</parent>
<artifactId>commons</artifactId>
<name>Cryptomator common</name>

View File

@@ -1,7 +1,7 @@
package org.cryptomator.common;
@FunctionalInterface
public interface ConsumerThrowingException<T, E extends Throwable> {
public interface ConsumerThrowingException<T, E extends Exception> {
void accept(T t) throws E;

View File

@@ -17,17 +17,16 @@ public final class LazyInitializer {
* @return The initialized value
*/
public static <T> T initializeLazily(AtomicReference<T> reference, Supplier<T> factory) {
final T existing = reference.get();
if (existing != null) {
return existing;
final T existingInstance = reference.get();
if (existingInstance != null) {
return existingInstance;
} else {
return reference.updateAndGet(currentValue -> {
if (currentValue == null) {
return factory.get();
} else {
return currentValue;
}
});
final T newInstance = factory.get();
if (reference.compareAndSet(null, newInstance)) {
return newInstance;
} else {
return reference.get();
}
}
}

View File

@@ -1,7 +1,7 @@
package org.cryptomator.common;
@FunctionalInterface
public interface RunnableThrowingException<T extends Throwable> {
public interface RunnableThrowingException<T extends Exception> {
void run() throws T;

View File

@@ -1,56 +0,0 @@
/*******************************************************************************
* 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(')');
}
}

View File

@@ -1,8 +0,0 @@
package org.cryptomator.common;
@FunctionalInterface
public interface SupplierThrowingException<T, E extends Throwable> {
T get() throws E;
}

View File

@@ -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,7 +83,6 @@ public class WeakValuedCacheTest {
assertThat(result, is(sameInstance(theValue)));
}
@Ignore
@Test
public void testCacheDoesNotPreventGarbageCollectionOfValues() {
when(loader.apply(A_KEY)).thenAnswer(this::createValueUsingMoreThanHalfTheJvmMemory);

View File

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

View File

@@ -17,13 +17,6 @@ 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.
@@ -46,6 +39,7 @@ 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;
/**

View File

@@ -30,6 +30,13 @@ 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.

View File

@@ -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,11 +29,6 @@ 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();

View File

@@ -31,6 +31,11 @@ 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);

View File

@@ -4,7 +4,6 @@ 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;
@@ -29,9 +28,7 @@ public final class FileContents {
* @return The file's content interpreted in this FileContents' charset.
*/
public String readContents(File file) {
try ( //
ReadableByteChannel channel = file.openReadable(); //
Reader reader = Channels.newReader(channel, charset.newDecoder(), -1)) {
try (Reader reader = Channels.newReader(file.openReadable(), charset.newDecoder(), -1)) {
return IOUtils.toString(reader);
} catch (IOException e) {
throw new UncheckedIOException(e);

View File

@@ -30,16 +30,6 @@ 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);

View File

@@ -42,6 +42,17 @@ 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);

View File

@@ -12,10 +12,10 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.2.2</version>
<version>1.0.5</version>
</parent>
<artifactId>filesystem-charsets</artifactId>
<name>Cryptomator filesystem: Charset compatibility layer</name>
<name>Cryptomator filesystem: Filename charset compatibility layer</name>
<dependencies>
<dependency>

View File

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

View File

@@ -130,7 +130,7 @@ public class CryptoFileSystemIntegrationTest {
// toggle last bit
try (WritableFile writable = physicalFile.openWritable(); ReadableFile readable = physicalFile.openReadable()) {
ByteBuffer buf = ByteBuffer.allocate((int) physicalFile.size());
ByteBuffer buf = ByteBuffer.allocate((int) readable.size());
readable.read(buf);
buf.array()[buf.limit() - 1] ^= 0x01;
buf.flip();

View File

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

View File

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

View File

@@ -19,6 +19,11 @@ 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.
*

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ public final class Constants {
private Constants() {
}
static final Integer CURRENT_VAULT_VERSION = 5;
static final Integer CURRENT_VAULT_VERSION = 3;
public static final int PAYLOAD_SIZE = 32 * 1024;
public static final int NONCE_SIZE = 16;

View File

@@ -18,7 +18,6 @@ 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;
@@ -82,15 +81,14 @@ class CryptorImpl implements Cryptor {
@Override
public void randomizeMasterkey() {
final byte[] randomBytes = new byte[KEYLENGTH_IN_BYTES];
try {
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);
randomSource.nextBytes(randomBytes);
encryptionKey = new SecretKeySpec(randomBytes, ENCRYPTION_ALG);
randomSource.nextBytes(randomBytes);
macKey = new SecretKeySpec(randomBytes, ENCRYPTION_ALG);
} finally {
Arrays.fill(randomBytes, (byte) 0x00);
}
}
@@ -118,12 +116,12 @@ class CryptorImpl implements Cryptor {
final SecretKey kek = new SecretKeySpec(kekBytes, ENCRYPTION_ALG);
this.macKey = AesKeyWrap.unwrap(kek, keyFile.getMacMasterKey(), MAC_ALG);
// future use (as soon as we need to prevent downgrade attacks):
// final Mac mac = new ThreadLocalMac(macKey, MAC_ALG).get();
// final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.BYTES).putInt(CURRENT_VAULT_VERSION).array());
// if (!MessageDigest.isEqual(versionMac, keyFile.getVersionMac())) {
// destroyQuietly(macKey);
// throw new UnsupportedVaultFormatException(Integer.MAX_VALUE, CURRENT_VAULT_VERSION);
// }
// final Mac mac = new ThreadLocalMac(macKey, MAC_ALG).get();
// final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.BYTES).putInt(CURRENT_VAULT_VERSION).array());
// if (!MessageDigest.isEqual(versionMac, keyFile.getVersionMac())) {
// destroyQuietly(macKey);
// throw new UnsupportedVaultFormatException(Integer.MAX_VALUE, CURRENT_VAULT_VERSION);
// }
this.encryptionKey = AesKeyWrap.unwrap(kek, keyFile.getEncryptionMasterKey(), ENCRYPTION_ALG);
} catch (InvalidKeyException e) {
throw new InvalidPassphraseException();

View File

@@ -22,6 +22,7 @@ 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;
@@ -46,6 +47,7 @@ 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;
@@ -54,11 +56,11 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
this.header = FileHeader.decrypt(headerKey, hmacSha256, header);
this.authenticate = authenticate;
this.chunkNumber = firstCiphertextByte / CHUNK_SIZE; // floor() by int-truncation
// 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
public long contentLength() {
return header.getPayload().getFilesize();
}
@Override
@@ -103,7 +105,15 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
@Override
public ByteBuffer cleartext() throws InterruptedException {
try {
return dataProcessor.processedData();
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;
} catch (ExecutionException e) {
if (e.getCause() instanceof AuthenticationFailedException) {
throw new AuthenticationFailedException(e);

View File

@@ -36,6 +36,8 @@ 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);
@@ -61,7 +63,7 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
@Override
public ByteBuffer getHeader() {
header.getPayload().setFilesize(-1l);
header.getPayload().setFilesize(cleartextBytesScheduledForEncryption.sum());
return header.toByteBuffer(headerKey, hmacSha256);
}
@@ -74,6 +76,7 @@ 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 {
@@ -81,6 +84,19 @@ 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);

View File

@@ -11,14 +11,12 @@ 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;
@@ -38,13 +36,13 @@ class FileHeaderPayload implements Destroyable {
private final SecretKey contentKey;
public FileHeaderPayload(SecureRandom randomSource) {
this.filesize = 0;
filesize = 0;
final byte[] contentKey = new byte[CONTENT_KEY_LEN];
try {
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);
randomSource.nextBytes(contentKey);
this.contentKey = new SecretKeySpec(contentKey, AES);
} finally {
Arrays.fill(contentKey, (byte) 0x00);
}
}

View File

@@ -12,9 +12,8 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.regex.Pattern;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.AEADBadTagException;
import javax.crypto.SecretKey;
import org.apache.commons.codec.binary.Base32;
@@ -22,13 +21,10 @@ import org.apache.commons.codec.binary.BaseNCodec;
import org.cryptomator.crypto.engine.AuthenticationFailedException;
import org.cryptomator.crypto.engine.FilenameCryptor;
import org.cryptomator.siv.SivMode;
import org.cryptomator.siv.UnauthenticCiphertextException;
class FilenameCryptorImpl implements FilenameCryptor {
private static final BaseNCodec BASE32 = new Base32();
// https://tools.ietf.org/html/rfc4648#section-6
private static final Pattern BASE32_PATTERN = Pattern.compile("^([A-Z2-7]{8})*[A-Z2-7=]{8}");
private static final ThreadLocal<MessageDigest> SHA1 = new ThreadLocalSha1();
private static final ThreadLocal<SivMode> AES_SIV = new ThreadLocal<SivMode>() {
@Override
@@ -54,8 +50,8 @@ class FilenameCryptorImpl implements FilenameCryptor {
}
@Override
public Pattern encryptedNamePattern() {
return BASE32_PATTERN;
public boolean isEncryptedFilename(String ciphertextName) {
return BASE32.isInAlphabet(ciphertextName);
}
@Override
@@ -71,8 +67,8 @@ class FilenameCryptorImpl implements FilenameCryptor {
try {
final byte[] cleartextBytes = AES_SIV.get().decrypt(encryptionKey, macKey, encryptedBytes, associatedData);
return new String(cleartextBytes, UTF_8);
} catch (UnauthenticCiphertextException | IllegalBlockSizeException e) {
throw new AuthenticationFailedException("Invalid ciphertext.", e);
} catch (AEADBadTagException e) {
throw new AuthenticationFailedException("Authentication failed.", e);
}
}

View File

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

View File

@@ -100,6 +100,11 @@ class BlockAlignedReadableFile implements ReadableFile {
return delegate.isOpen();
}
@Override
public long size() throws UncheckedIOException {
return delegate.size();
}
@Override
public void close() throws UncheckedIOException {
delegate.close();

View File

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

View File

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

View File

@@ -8,8 +8,7 @@
*******************************************************************************/
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 static java.nio.charset.StandardCharsets.UTF_8;
import java.io.UncheckedIOException;
import java.nio.file.FileAlreadyExistsException;
@@ -28,26 +27,9 @@ class CryptoFile extends CryptoNode implements File {
@Override
protected Optional<String> encryptedName() {
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;
}
return parent().get().getDirectoryId().map(s -> s.getBytes(UTF_8)).map(parentDirId -> {
return cryptor.getFilenameCryptor().encryptFilename(name(), parentDirId);
});
}
@Override

View File

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

View File

@@ -70,6 +70,12 @@ 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) {

View File

@@ -16,7 +16,6 @@ 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;
@@ -53,14 +52,10 @@ 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);
success = true;
} finally {
if (!success) {
cryptor.destroy();
}
} catch (UncheckedIOException e) {
cryptor.destroy();
}
return cryptor;
}
@@ -91,9 +86,7 @@ class Masterkeys {
/* I/O */
private static void readMasterKey(File file, Cryptor cryptor, CharSequence passphrase) throws UncheckedIOException, InvalidPassphraseException {
try ( //
ReadableByteChannel channel = file.openReadable(); //
InputStream in = Channels.newInputStream(channel)) {
try (InputStream in = Channels.newInputStream(file.openReadable())) {
final byte[] fileContents = IOUtils.toByteArray(in);
cryptor.readKeysFromMasterkeyFile(fileContents, passphrase);
} catch (IOException e) {
@@ -103,7 +96,6 @@ 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));
}

View File

@@ -44,9 +44,16 @@ 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

View File

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

View File

@@ -21,20 +21,20 @@ public class CryptorImplTest {
@Test
public void testMasterkeyDecryptionWithCorrectPassphrase() throws IOException {
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
}
@Test(expected = InvalidPassphraseException.class)
public void testMasterkeyDecryptionWithWrongPassphrase() throws IOException {
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
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\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
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\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
final String testMasterKey = "{\"version\":3,\"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\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"versionMac\":\"z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfoK=\"}";
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLa=\"}";
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
}
@Test
public void testMasterkeyEncryption() throws IOException {
final String expectedMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," //
final String expectedMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
+ "\"hmacMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
+ "\"versionMac\":\"yuwoRE9GSdgQ2b//qRpTCj3W0qsVLxYVa7/KB3PkfA4=\"}";
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
cryptor.randomizeMasterkey();
final byte[] masterkeyFile = cryptor.writeKeysToMasterkeyFile("asd");

View File

@@ -43,6 +43,20 @@ 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];
@@ -123,6 +137,45 @@ 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];

View File

@@ -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("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
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("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJG==");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJa==");
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("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
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("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
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("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
decryptor.cancelWithException(new IOException("can not do"));

View File

@@ -35,6 +35,20 @@ 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];
@@ -81,4 +95,24 @@ 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());
}
}
}

View File

@@ -53,26 +53,13 @@ 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(42l, header.getPayload().getFilesize());
Assert.assertEquals(42, header.getPayload().getFilesize());
Assert.assertArrayEquals(new byte[16], header.getIv());
Assert.assertArrayEquals(new byte[32], header.getPayload().getContentKey().getEncoded());
}

View File

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

View File

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

View File

@@ -43,11 +43,6 @@ 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) {
@@ -108,7 +103,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();
@@ -125,7 +120,7 @@ class InMemoryFile extends InMemoryNode implements File {
// returning null removes the entry.
return null;
});
assert !this.exists();
assert!this.exists();
}
@Override

View File

@@ -51,6 +51,11 @@ 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.";
@@ -59,10 +64,8 @@ class InMemoryReadableFile implements ReadableFile {
@Override
public void close() throws UncheckedIOException {
if (open.get()) {
open.set(false);
readLock.unlock();
}
open.set(false);
readLock.unlock();
}
}

View File

@@ -104,7 +104,9 @@ public class InMemoryFileSystemTest {
Assert.assertTrue(fooFile.exists());
// check if size = 11 bytes
Assert.assertEquals(11, fooFile.size());
try (ReadableFile readable = fooFile.openReadable()) {
Assert.assertEquals(11, readable.size());
}
// copy foo to bar
File barFile = fs.file("bar.txt");

View File

@@ -9,7 +9,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.2.2</version>
<version>1.0.5</version>
</parent>
<artifactId>filesystem-invariants-tests</artifactId>
<name>Cryptomator filesystem: Invariants tests</name>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,6 @@ 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;
@@ -17,11 +16,6 @@ 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);
@@ -59,18 +53,7 @@ class DefaultNioAccess implements NioAccess {
@Override
public void delete(Path path) throws IOException {
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;
}
Files.delete(path);
}
@Override

View File

@@ -16,8 +16,6 @@ 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);

View File

@@ -27,33 +27,16 @@ 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 " + path);
throw new IllegalStateException("Current thread is currently writing this file");
}
if (lock.getReadHoldCount() > 0) {
throw new IllegalStateException("Current thread is already reading " + path);
throw new IllegalStateException("Current thread is already reading this file");
}
lock.readLock().lock();
ReadableFile result = null;
try {
result = instanceFactory.readableNioFile(path, sharedChannel, this::unlockReadLock);
} finally {
if (result == null) {
unlockReadLock();
}
}
return result;
return instanceFactory.readableNioFile(path, sharedChannel, this::unlockReadLock);
}
private void unlockReadLock() {
@@ -63,21 +46,13 @@ class NioFile extends NioNode implements File {
@Override
public WritableFile openWritable() throws UncheckedIOException {
if (lock.getWriteHoldCount() > 0) {
throw new IllegalStateException("Current thread is already writing " + path);
throw new IllegalStateException("Current thread is already writing this file");
}
if (lock.getReadHoldCount() > 0) {
throw new IllegalStateException("Current thread is currently reading " + path);
throw new IllegalStateException("Current thread is currently reading this file");
}
lockWriteLock();
WritableFile result = null;
try {
result = instanceFactory.writableNioFile(fileSystem(), path, sharedChannel, this::unlockWriteLock);
} finally {
if (result == null) {
unlockWriteLock();
}
}
return result;
return instanceFactory.writableNioFile(fileSystem(), path, sharedChannel, this::unlockWriteLock);
}
// visible for testing

View File

@@ -41,6 +41,11 @@ class ReadableNioFile implements ReadableFile {
return open;
}
@Override
public long size() throws UncheckedIOException {
return channel.size();
}
@Override
public void position(long position) throws UncheckedIOException {
assertOpen();

View File

@@ -16,7 +16,6 @@ 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;
@@ -86,27 +85,6 @@ 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
@@ -121,11 +99,10 @@ 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 " + path);
thrown.expectMessage("already reading this file");
inTest.openReadable();
}
@@ -134,7 +111,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(mock(ReadableNioFile.class), readableNioFile);
when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(null, readableNioFile);
inTest.openReadable();
captor.getValue().run();
@@ -145,11 +122,10 @@ 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 " + path);
thrown.expectMessage("currently writing this file");
inTest.openReadable();
}
@@ -157,7 +133,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(mock(WritableNioFile.class));
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(null);
inTest.openWritable();
captor.getValue().run();
@@ -178,7 +154,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(mock(WritableNioFile.class), writableNioFile);
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(null, writableNioFile);
inTest.openWritable();
captor.getValue().run();
@@ -189,31 +165,28 @@ 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 " + path);
thrown.expectMessage("already writing this file");
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 " + path);
thrown.expectMessage("currently reading this file");
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(readableNioFile);
when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(null);
inTest.openReadable();
captor.getValue().run();

View File

@@ -83,6 +83,16 @@ 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);

View File

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

View File

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

View File

@@ -14,20 +14,12 @@ import java.util.Optional;
public interface Frontend extends AutoCloseable {
public enum MountParam {
MOUNT_NAME, HOSTNAME, WIN_DRIVE_LETTER,
/**
* "dav" or "webdav"
*/
PREFERRED_GVFS_SCHEME
MOUNT_NAME, HOSTNAME, WIN_DRIVE_LETTER
}
void mount(Map<MountParam, Optional<String>> map) throws CommandFailedException;
/**
* Unmounts the file system and stops any file system handler threads.
*/
void close() throws Exception;
void unmount() throws CommandFailedException;
void reveal() throws CommandFailedException;

View File

@@ -16,11 +16,10 @@ public interface FrontendFactory {
* Provides a new frontend to access the given folder.
*
* @param root Root resource accessible through this frontend.
* @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
* @param uniqueName Name of the frontend, i.e. used to create subresources for the different frontends inside of a common virtual drive.
* @return A new frontend
* @throws FrontendCreationFailedException If creation was not possible.
*/
Frontend create(Folder root, FrontendId id, String name) throws FrontendCreationFailedException;
Frontend create(Folder root, String uniqueName) throws FrontendCreationFailedException;
}

View File

@@ -1,85 +0,0 @@
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;
}
}

View File

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

View File

@@ -1,30 +0,0 @@
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();
}
}
}

View File

@@ -1,71 +0,0 @@
/*******************************************************************************
* 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);
}
}

View File

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

View File

@@ -24,7 +24,6 @@ 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 {
@@ -46,13 +45,13 @@ class WebDavFrontend implements Frontend {
@Override
public void mount(Map<MountParam, Optional<String>> mountParams) throws CommandFailedException {
mount = webdavMounterProvider.chooseMounter(mountParams).mount(uri, mountParams);
mount = webdavMounterProvider.get().mount(uri, mountParams);
}
private void unmount() throws CommandFailedException {
@Override
public void unmount() throws CommandFailedException {
if (mount != null) {
mount.unmount();
mount = null;
}
}

View File

@@ -1,11 +0,0 @@
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 {
}

View File

@@ -8,11 +8,8 @@
*******************************************************************************/
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;
@@ -23,7 +20,6 @@ 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;
@@ -49,10 +45,9 @@ 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, DefaultServlet defaultServlet, Tarpit tarpit) {
WebDavServer(WebDavServletContextFactory servletContextFactory, WebDavMounterProvider webdavMounterProvider) {
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);
@@ -60,9 +55,8 @@ public class WebDavServer implements FrontendFactory {
this.servletCollection = new ContextHandlerCollection();
this.servletContextFactory = servletContextFactory;
this.webdavMounterProvider = webdavMounterProvider;
this.tarpit = tarpit;
servletCollection.addHandler(defaultServlet.createServletContextHandler());
servletCollection.addHandler(WindowsCompatibilityServlet.createServletContextHandler());
server.setConnectors(new Connector[] {localConnector});
server.setHandler(servletCollection);
}
@@ -109,8 +103,10 @@ public class WebDavServer implements FrontendFactory {
}
@Override
public Frontend create(Folder root, FrontendId id, String name) throws FrontendCreationFailedException {
String contextPath = format("/%s/%s", id, name);
public Frontend create(Folder root, String contextPath) throws FrontendCreationFailedException {
if (!contextPath.startsWith("/")) {
throw new IllegalArgumentException("contextPath must begin with '/'");
}
final URI uri;
try {
uri = new URI("http", null, "localhost", getPort(), contextPath, null, null);
@@ -121,9 +117,5 @@ 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);
}
}

View File

@@ -22,7 +22,6 @@ 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;
@@ -35,9 +34,10 @@ 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,7 +67,6 @@ 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));

View File

@@ -11,8 +11,6 @@ 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;
@@ -23,38 +21,26 @@ import org.cryptomator.frontend.webdav.filters.LoopbackFilter;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
@Singleton
class DefaultServlet extends HttpServlet {
/**
* The server needs to respond to requests to the root resource, because Windows is stupid.
*/
public class WindowsCompatibilityServlet 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");
// resp.addHeader("Allow", "OPTIONS, GET, HEAD, POST, TRACE, PROPFIND, PROPPATCH, MKCOL, COPY, PUT, DELETE, MOVE, LOCK, UNLOCK");
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
}
public ServletContextHandler createServletContextHandler() {
public static ServletContextHandler createServletContextHandler() {
final ServletContextHandler servletContext = new ServletContextHandler(null, ROOT_PATH, ServletContextHandler.NO_SESSIONS);
final ServletHolder servletHolder = new ServletHolder(ROOT_PATH, this);
final ServletHolder servletHolder = new ServletHolder(ROOT_PATH, WindowsCompatibilityServlet.class);
servletContext.addServlet(servletHolder, ROOT_PATH);
servletContext.addFilter(LoopbackFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST));
servletContext.addFilter(LoopbackFilter.class, ROOT_PATH, EnumSet.of(DispatcherType.REQUEST));
return servletContext;
}

View File

@@ -1,43 +0,0 @@
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);
}
}

View File

@@ -1,50 +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.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
}
}

View File

@@ -32,16 +32,14 @@ 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> {
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";
private static final Logger LOG = LoggerFactory.getLogger(DavFile.class);
public DavFile(FilesystemResourceFactory factory, LockManager lockManager, DavSession session, FileLocator node) {
super(factory, lockManager, session, node);
@@ -58,11 +56,8 @@ 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);
}
}
@@ -154,7 +149,12 @@ class DavFile extends DavNode<FileLocator> {
private Optional<DavProperty<?>> sizeProperty() {
if (node.exists()) {
return Optional.of(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, node.size()));
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();
}
} else {
return Optional.empty();
}

View File

@@ -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,9 +57,6 @@ 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);

View File

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

View File

@@ -69,8 +69,7 @@ class ExclusiveSharedLockManager implements LockManager {
}
String token = DavConstants.OPAQUE_LOCK_TOKEN_PREFIX + UUID.randomUUID();
Map<String, ActiveLock> lockMap = Objects.requireNonNull(lockedResources.computeIfAbsent(locator, loc -> new HashMap<>()));
return lockMap.computeIfAbsent(token, t -> new ExclusiveSharedLock(t, lockInfo));
return lockedResources.computeIfAbsent(locator, loc -> new HashMap<>()).computeIfAbsent(token, t -> new ExclusiveSharedLock(t, lockInfo));
}
private void removedExpiredLocksInLocatorHierarchy(FileSystemResourceLocator locator) {

View File

@@ -23,7 +23,7 @@ import org.cryptomator.frontend.Frontend.MountParam;
final class FallbackWebDavMounter implements WebDavMounterStrategy {
@Override
public boolean shouldWork(Map<MountParam, Optional<String>> mountParams) {
public boolean shouldWork() {
return true;
}

View File

@@ -1,94 +0,0 @@
/*******************************************************************************
* 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();
}
}
}

View File

@@ -30,14 +30,12 @@ final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy {
}
@Override
public boolean shouldWork(Map<MountParam, Optional<String>> mountParams) {
public boolean shouldWork() {
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 prefSchemeIsUnspecifiedOrWebDav;
return true;
} catch (CommandFailedException e) {
return false;
}
@@ -86,7 +84,15 @@ final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy {
@Override
public void reveal() throws CommandFailedException {
Script.fromLines("set -x", "gvfs-open \"webdav:$DAV_SSP\"").addEnv("DAV_SSP", webDavUri.getRawSchemeSpecificPart()).execute();
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);
}
}

View File

@@ -38,7 +38,7 @@ final class MacOsXAppleScriptWebDavMounter implements WebDavMounterStrategy {
}
@Override
public boolean shouldWork(Map<MountParam, Optional<String>> mountParams) {
public boolean shouldWork() {
return SystemUtils.IS_OS_MAC_OSX && semVerComparator.compare(SystemUtils.OS_VERSION, "10.10") >= 0;
}

View File

@@ -37,7 +37,7 @@ final class MacOsXShellScriptWebDavMounter implements WebDavMounterStrategy {
}
@Override
public boolean shouldWork(Map<MountParam, Optional<String>> mountParams) {
public boolean shouldWork() {
return SystemUtils.IS_OS_MAC_OSX && semVerComparator.compare(SystemUtils.OS_VERSION, "10.10") < 0;
}

View File

@@ -0,0 +1,105 @@
/*******************************************************************************
* 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();
}
}

View File

@@ -1,31 +0,0 @@
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();
}
}

View File

@@ -10,35 +10,34 @@
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.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.cryptomator.frontend.Frontend.MountParam;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class WebDavMounterProvider {
public class WebDavMounterProvider implements Provider<WebDavMounter> {
private static final Logger LOG = LoggerFactory.getLogger(WebDavMounterProvider.class);
private final Collection<WebDavMounterStrategy> availableStrategies;
private final WebDavMounterStrategy fallbackStrategy;
private final WebDavMounterStrategy choosenStrategy;
@Inject
public WebDavMounterProvider(Set<WebDavMounterStrategy> availableStrategies, @Named("fallback") WebDavMounterStrategy fallbackStrategy) {
this.availableStrategies = availableStrategies;
this.fallbackStrategy = fallbackStrategy;
public WebDavMounterProvider(MountStrategies availableStrategies) {
this.choosenStrategy = getStrategyWhichShouldWork(availableStrategies);
}
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;
@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;
}
}

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