Compare commits

..

54 Commits
1.1.4 ... 1.2.0

Author SHA1 Message Date
Sebastian Stenzel
3e374a927c Merge branch 'release/1.2.0'
# Conflicts:
#	main/ant-kit/pom.xml
#	main/commons-test/pom.xml
#	main/commons/pom.xml
#	main/filesystem-api/pom.xml
#	main/filesystem-charsets/pom.xml
#	main/filesystem-crypto-integration-tests/pom.xml
#	main/filesystem-crypto/pom.xml
#	main/filesystem-inmemory/pom.xml
#	main/filesystem-invariants-tests/pom.xml
#	main/filesystem-nameshortening/pom.xml
#	main/filesystem-nio/pom.xml
#	main/filesystem-stats/pom.xml
#	main/frontend-api/pom.xml
#	main/frontend-webdav/pom.xml
#	main/jacoco-report/pom.xml
#	main/pom.xml
#	main/uber-jar/pom.xml
#	main/ui/pom.xml
2016-09-19 15:10:43 +02:00
Sebastian Stenzel
84ac6d88f5 added new localization files to unit test [ci skip] 2016-09-15 23:55:37 +02:00
Sebastian Stenzel
72f6ee6477 updated localizations 2016-09-15 23:52:23 +02:00
Sebastian Stenzel
a3cfcb1131 Reject opening files when former filesize header is != -1 2016-09-15 23:26:13 +02:00
Sebastian Stenzel
d7d8d21ba4 Show warning when trying to migrate a masterkey with invalid version mac 2016-09-15 22:15:21 +02:00
Sebastian Stenzel
ef0425e2b1 fixes coverity issue 147409 2016-09-15 14:15:33 +02:00
Sebastian Stenzel
df1fd6d0b3 fixed coverity issue 72979 2016-09-15 14:15:25 +02:00
Sebastian Stenzel
a15acd64c8 set version to 1.2.0 2016-09-15 13:33:37 +02:00
Sebastian Stenzel
5b18eff01a increased cryptolib version to 1.0.2
[ci skip]
2016-09-15 13:28:20 +02:00
Sebastian Stenzel
47133c6f31 fixed change pw function leaving invalid JSON file if length gets shorter due to different encoding or pretty printing etc 2016-09-14 17:22:26 +02:00
Sebastian Stenzel
09ba4f5129 changed to jni lib version 1.0.0 2016-09-13 20:17:30 +02:00
Sebastian Stenzel
20d4047bed changed to cryptolib version 1.0.1 (which includes sources) [ci skip] 2016-09-12 23:16:17 +02:00
Sebastian Stenzel
56b71ef7d9 depends on relase version of cryptolib 1.0.0 2016-09-12 21:28:21 +02:00
Sebastian Stenzel
091e62057d Injecting CryptorProvider into UpgradeStrategies 2016-09-12 13:56:47 +02:00
Sebastian Stenzel
824bd9ea64 just added a comment [ci skip] 2016-09-08 18:41:36 +02:00
Sebastian Stenzel
697a791593 updated travis config 2016-09-08 18:34:28 +02:00
Sebastian Stenzel
7462a887b3 updated travis config 2016-09-08 18:30:11 +02:00
Sebastian Stenzel
3535e83d7d updated travis config 2016-09-08 18:20:57 +02:00
Sebastian Stenzel
cf0b4accb3 Merge branch 'feature/external-keychain' into develop 2016-09-04 16:21:52 +02:00
Sebastian Stenzel
a63bcfbaa2 relaxed "vault not empty" check 2016-09-04 16:04:16 +02:00
Sebastian Stenzel
5c4bf2a207 support home-relative paths for cryptomator.keychainPath 2016-09-04 12:27:23 +02:00
Sebastian Stenzel
c1611a12ed implemented Windows keychain 2016-09-03 23:04:53 +02:00
Markus Kreusch
0983120712 Removed Syso logging 2016-09-02 19:21:54 +02:00
Sebastian Stenzel
ce12af8495 Added save password functionality to UI 2016-09-02 15:49:09 +02:00
Sebastian Stenzel
dc117c8415 oracle-java8-unlimited-jce-policy apparently no longer needed (already installed) 2016-08-31 20:12:49 +02:00
Sebastian Stenzel
06e526a961 Merge branch 'develop' into feature/external-keychain
# Conflicts:
#	main/pom.xml
2016-08-31 19:45:15 +02:00
Sebastian Stenzel
2e343a951f Feature/travis container builds (#334)
improved build dependency caching + force updates of snapshots to bypass said cache
2016-08-31 19:39:55 +02:00
Sebastian Stenzel
141ffcf656 Merge branch 'feature/native-functions' into feature/external-keychain 2016-08-31 10:41:51 +02:00
Tobias Hagemann
d61e5c5a08 added "delete passphrase" method to keychain access 2016-08-31 01:08:58 +02:00
Tobias Hagemann
6a15fa132a app launches as foreground app on mac 2016-08-30 22:41:01 +02:00
Sebastian Stenzel
902b29ee0a Merge branch 'develop' into feature/external-keychain
# Conflicts:
#	main/pom.xml
#	main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java
2016-08-30 19:22:45 +02:00
Sebastian Stenzel
995bba616f cache maven dir 2016-08-30 19:15:26 +02:00
Sebastian Stenzel
f39b7b047f Merge branch 'feature/vaultVersion5' into develop 2016-08-30 19:14:28 +02:00
Sebastian Stenzel
72e52df4e0 implemented keychain access on OS X 2016-08-30 19:12:20 +02:00
Sebastian Stenzel
8018e9485e Merge branch 'feature/native-functions' into feature/external-keychain 2016-08-30 19:05:25 +02:00
Sebastian Stenzel
e0ae50378f externalized JNI bindings 2016-08-30 17:19:45 +02:00
Markus Kreusch
a9c2b0fc57 fixes #332 2016-08-29 21:08:58 +02:00
Sebastian Stenzel
dc58ba434a Make Cryptomator a foreground app when restoring from status bar icon 2016-08-29 20:14:48 +02:00
Sebastian Stenzel
34af306309 defined keychain access interfaces 2016-08-29 17:16:56 +02:00
Sebastian Stenzel
21d70b5ae4 moved from coveralls to codecov 2016-08-26 12:52:44 +02:00
Sebastian Stenzel
e90880ac9a speedboost 3000 2016-08-24 17:27:36 +02:00
Sebastian Stenzel
66faa13f40 unlock version 5 vaults 2016-08-23 21:35:13 +02:00
Sebastian Stenzel
8a4a29b4d1 added version 4 to 5 migrator 2016-08-23 21:15:52 +02:00
Sebastian Stenzel
8c8db84a4a refactored migration (using cryptolib) 2016-08-23 21:15:40 +02:00
Sebastian Stenzel
a499a3c80b Merge pull request #324 from oparoz/patch-1
Add that the solution works with Open Source clouds
2016-08-19 19:25:43 +02:00
Olivier Paroz
6a3ccf2b48 Add that the solution works with WebDAV clouds
I think it would be nice to promote other Open Source solutions which use standards such as WebDAV and work well with Cryptomator.
2016-08-19 19:07:39 +02:00
Tobias Hagemann
fcfcffe9cb updated tray icons for OS X [ci skip] 2016-08-19 16:56:04 +02:00
Sebastian Stenzel
363ed4ac4b Accept paths beginning with "~" in cryptomator.settingsPath JVM arg.
[ci skip]
2016-08-17 18:52:08 +02:00
Tobias Hagemann
1f73a08e09 added confirmation checkbox to upgrade screen [ci skip] 2016-08-17 18:11:27 +02:00
Sebastian Stenzel
fe0a34907f Simplified settings/log file path handling. Removed support for %appdata%. Use ~/AppData/Roaming instead! 2016-08-17 15:59:36 +02:00
Sebastian Stenzel
461b11700f added new upgrade log path setting to build script [ci skip] 2016-08-16 19:15:23 +02:00
Sebastian Stenzel
24bfbb59a4 fixes #310 2016-08-16 19:07:05 +02:00
Sebastian Stenzel
4476558e9c fixes #321 2016-08-16 12:33:15 +02:00
Sebastian Stenzel
79b819bca6 Merge branch 'release/1.1.4' into develop 2016-08-14 15:04:44 +02:00
120 changed files with 1890 additions and 643 deletions

View File

@@ -1,24 +1,30 @@
sudo: required
dist: trusty
language: java
sudo: required
dist: trusty
jdk:
- oraclejdk8
cache:
directories:
- $HOME/.m2
env:
global:
- secure: "Lgj042RD0X3rB8VZVZLWP1GetLhjd3PqI5JbJMlzgHJpDI6RkFIBLN9SWAGmkLPCehIp2zA5tu9+UVy0NNMxm9xz6SyjMCaxS28/fnYEXaNmwwDSF6O6gLUbdxyzoYIFPYOPmFxpzhebqnNIsxaM29oZpgRgUGqosCczQxiB+Ng=" #coveralls
- secure: "IfYURwZaDWuBDvyn47n0k1Zod/IQw1FF+CS5nnV08Q+NfC3vGGJMwV8m59XnbfwnWGxwvCaAbk4qP6s6+ijgZNKkvgfFMo3rfTok5zt43bIqgaFOANYV+OC/1c59gYD6ZUxhW5iNgMgU3qdsRtJuwSmfkVv/jKyLGfAbS4kN8BA=" #coverity
- secure: "lV9OwUbHMrMpLUH1CY+Z4puLDdFXytudyPlG1eGRsesdpuG6KM3uQVz6uAtf6lrU8DRbMM/T7ML+PmvQ4UoPPYLdLxESLLBat2qUPOIVBOhTSlCc7I0DmGy04CSvkeMy8dPaQC0ukgNiR7zwoNzfcpGRN/U9S8tziDruuHoZSrg=" #bintray
before_install: "curl -L --cookie 'oraclelicense=accept-securebackup-cookie;' http://download.oracle.com/otn-pub/java/jce/8/jce_policy-8.zip -o /tmp/policy.zip && sudo unzip -j -o /tmp/policy.zip *.jar -d `jdk_switcher home oraclejdk8`/jre/lib/security && rm /tmp/policy.zip"
script: mvn -fmain/pom.xml clean test
after_success: mvn -fmain/pom.xml -Ptest-coverage clean test jacoco:report-aggregate coveralls:report
addons:
coverity_scan:
project:
name: "cryptomator/cryptomator"
notification_email: sebastian.stenzel@cryptomator.org
build_command: "mvn -fmain/pom.xml clean test -DskipTests"
branch_pattern: release.*
install:
# "clean" needed until https://bugs.openjdk.java.net/browse/JDK-8067747 is resolved.
- mvn -fmain/pom.xml clean package -DskipTests dependency:go-offline -Ptest-coverage
- mvn -fmain/pom.xml clean package -DskipTests dependency:go-offline -Prelease
script:
- mvn --update-snapshots -fmain/pom.xml -Ptest-coverage clean test jacoco:report-aggregate
after_success:
- "bash <(curl -s https://codecov.io/bash)"
notifications:
webhooks:
urls:
@@ -31,17 +37,8 @@ notifications:
secure: "lngJ/HEAFBbD5AdiO9avMqptKpZHdmEwOzS9FabZjkdFh7yAYueTk5RniPUvShjsKtThYm7cJ8AtDMDwc07NvPrzbMBRtUJGwuDT+7c7YFALGFJ1NYi+emkC9x1oafvmPgEYSE+tMKzNcwrHi3ytGgKdIotsKwaF35QNXYA9aMs="
on_success: change
on_failure: always
before_deploy: mvn -fmain/pom.xml -Prelease clean package -DskipTests
addons:
coverity_scan:
project:
name: "cryptomator/cryptomator"
notification_email: sebastian.stenzel@cryptomator.org
build_command: "mvn -fmain/pom.xml clean test -DskipTests"
branch_pattern: release.*
before_deploy:
- mvn -fmain/pom.xml -Prelease clean package -DskipTests
deploy:
- provider: releases
prerelease: false

View File

@@ -14,7 +14,7 @@ Download native binaries of Cryptomator on [cryptomator.org](https://cryptomator
## Features
- Works with Dropbox, Google Drive, OneDrive, and any other cloud storage service that synchronizes with a local directory
- Works with Dropbox, Google Drive, OneDrive, Nextcloud and any other cloud storage service which synchronizes with a local directory
- Open Source means: No backdoors, control is better than trust
- Client-side: No accounts, no data shared with any online service
- Totally transparent: Just work on the virtual drive as if it were a USB flash drive

View File

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

View File

@@ -21,21 +21,6 @@
</fx:jar>
</target>
<!-- Create native image -->
<target name="create-linux-image-with-jvm" depends="create-jar">
<fx:deploy nativeBundles="image" outdir="antbuild" outfile="Cryptomator-${project.version}" verbose="true">
<fx:application refid="Cryptomator" />
<fx:platform j2se="8.0">
<fx:property name="cryptomator.logPath" value="~/.Cryptomator/cryptomator.log" />
<fx:jvmarg value="-Xmx512m"/>
</fx:platform>
<fx:resources>
<fx:fileset dir="antbuild" type="jar" includes="Cryptomator-${project.version}.jar" />
<fx:fileset dir="libs" type="jar" includes="*.jar" excludes="ui-${project.version}.jar"/>
</fx:resources>
</fx:deploy>
</target>
<!-- Create Debian package -->
<target name="deb" depends="create-jar">
<fx:deploy nativeBundles="deb" outdir="antbuild" outfile="Cryptomator-${project.version}" verbose="true">
@@ -45,7 +30,9 @@
</fx:info>
<fx:platform j2se="8.0">
<fx:property name="cryptomator.logPath" value="~/.Cryptomator/cryptomator.log" />
<fx:jvmarg value="-Xmx1048m"/>
<fx:property name="cryptomator.upgradeLogPath" value="~/.Cryptomator/upgrade.log" />
<fx:property name="cryptomator.settingsPath" value="~/.Cryptomator/settings.json" />
<fx:jvmarg value="-Xmx512m"/>
</fx:platform>
<fx:resources>
<fx:fileset dir="antbuild" type="jar" includes="Cryptomator-${project.version}.jar" />
@@ -66,7 +53,9 @@
</fx:info>
<fx:platform j2se="8.0">
<fx:property name="cryptomator.logPath" value="~/.Cryptomator/cryptomator.log" />
<fx:jvmarg value="-Xmx1048m"/>
<fx:property name="cryptomator.upgradeLogPath" value="~/.Cryptomator/upgrade.log" />
<fx:property name="cryptomator.settingsPath" value="~/.Cryptomator/settings.json" />
<fx:jvmarg value="-Xmx512m"/>
</fx:platform>
<fx:resources>
<fx:fileset dir="antbuild" type="jar" includes="Cryptomator-${project.version}.jar" />

View File

@@ -10,7 +10,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.1.4</version>
<version>1.2.0</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.1.4</version>
<version>1.2.0</version>
</parent>
<artifactId>commons</artifactId>
<name>Cryptomator common</name>

View File

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

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,6 +83,7 @@ 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.1.4</version>
<version>1.2.0</version>
</parent>
<artifactId>filesystem-api</artifactId>
<name>Cryptomator filesystem: API</name>

View File

@@ -17,6 +17,13 @@ public interface File extends Node, Comparable<File> {
static final int EOF = -1;
/**
* @return The current size of the file. This value is a snapshot and might have been changed by concurrent modifications.
* @throws UncheckedIOException
* if an {@link IOException} occurs
*/
long size() throws UncheckedIOException;
/**
* <p>
* Opens this file for reading.
@@ -39,7 +46,6 @@ public interface File extends Node, Comparable<File> {
* if an {@link IOException} occurs while opening the file, the
* file does not exist or is a directory
*/
ReadableFile openReadable() throws UncheckedIOException;
/**

View File

@@ -30,13 +30,6 @@ public interface ReadableFile extends ReadableByteChannel {
@Override
int read(ByteBuffer target) throws UncheckedIOException;
/**
* @return The current size of the file. This value is a snapshot and might have been changed by concurrent modifications.
* @throws UncheckedIOException
* if an {@link IOException} occurs
*/
long size() throws UncheckedIOException;
/**
* <p>
* Fast-forwards or rewinds the file to the specified position.

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,6 +29,11 @@ public abstract class DelegatingFile<D extends DelegatingFolder<D, ?>> extends D
return Optional.of(parent);
}
@Override
public long size() throws UncheckedIOException {
return delegate.size();
}
@Override
public ReadableFile openReadable() throws UncheckedIOException {
return delegate.openReadable();

View File

@@ -31,11 +31,6 @@ public class DelegatingReadableFile implements ReadableFile {
return delegate.read(target);
}
@Override
public long size() throws UncheckedIOException {
return delegate.size();
}
@Override
public void position(long position) throws UncheckedIOException {
delegate.position(position);

View File

@@ -30,6 +30,16 @@ public class DelegatingFileTest {
Assert.assertEquals(mockFile.name(), delegatingFile.name());
}
@Test
public void testSize() {
File mockFile = Mockito.mock(File.class);
DelegatingFile<?> delegatingFile = new TestDelegatingFile(null, mockFile);
Mockito.when(mockFile.size()).thenReturn(42l);
Assert.assertEquals(42l, delegatingFile.size());
Mockito.verify(mockFile).size();
}
@Test
public void testParent() {
Folder mockFolder = Mockito.mock(Folder.class);

View File

@@ -42,17 +42,6 @@ public class DelegatingReadableFileTest {
Mockito.verify(mockReadableFile).read(buf);
}
@Test
public void testSize() {
ReadableFile mockReadableFile = Mockito.mock(ReadableFile.class);
@SuppressWarnings("resource")
DelegatingReadableFile delegatingReadableFile = new DelegatingReadableFile(mockReadableFile);
Mockito.when(mockReadableFile.size()).thenReturn(42l);
Assert.assertEquals(42l, delegatingReadableFile.size());
Mockito.verify(mockReadableFile).size();
}
@Test
public void testPosition() {
ReadableFile mockReadableFile = Mockito.mock(ReadableFile.class);

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.1.4</version>
<version>1.2.0</version>
</parent>
<artifactId>filesystem-charsets</artifactId>
<name>Cryptomator filesystem: Charset compatibility layer</name>

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.1.4</version>
<version>1.2.0</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) readable.size());
ByteBuffer buf = ByteBuffer.allocate((int) physicalFile.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.1.4</version>
<version>1.2.0</version>
</parent>
<artifactId>filesystem-crypto</artifactId>
<name>Cryptomator filesystem: Encryption layer</name>
<properties>
<bouncycastle.version>1.51</bouncycastle.version>
<sivmode.version>1.0.7</sivmode.version>
<sivmode.version>1.0.8</sivmode.version>
</properties>
<dependencies>

View File

@@ -19,11 +19,6 @@ import javax.security.auth.Destroyable;
*/
public interface FileContentDecryptor extends Destroyable, Closeable {
/**
* @return Number of bytes of the decrypted file.
*/
long contentLength();
/**
* Appends further ciphertext to this decryptor. This method might block until space becomes available. If so, it is interruptable.
*

View File

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

View File

@@ -9,7 +9,6 @@
package org.cryptomator.crypto.engine.impl;
import static org.cryptomator.crypto.engine.impl.Constants.CURRENT_VAULT_VERSION;
import static org.cryptomator.crypto.engine.impl.Constants.SUPPORTED_VAULT_VERSIONS;
import java.io.IOException;
import java.nio.ByteBuffer;
@@ -110,7 +109,7 @@ class CryptorImpl implements Cryptor {
assert keyFile != null;
// check version
if (!SUPPORTED_VAULT_VERSIONS.contains(keyFile.getVersion())) {
if (!CURRENT_VAULT_VERSION.equals(keyFile.getVersion())) {
throw new UnsupportedVaultFormatException(keyFile.getVersion(), CURRENT_VAULT_VERSION);
}

View File

@@ -22,7 +22,6 @@ import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Supplier;
import javax.crypto.Cipher;
@@ -47,7 +46,6 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
private final Supplier<Mac> hmacSha256;
private final FileHeader header;
private final boolean authenticate;
private final LongAdder cleartextBytesDecrypted = new LongAdder();
private ByteBuffer ciphertextBuffer = ByteBuffer.allocate(CHUNK_SIZE);
private long chunkNumber = 0;
@@ -56,11 +54,11 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
this.header = FileHeader.decrypt(headerKey, hmacSha256, header);
this.authenticate = authenticate;
this.chunkNumber = firstCiphertextByte / CHUNK_SIZE; // floor() by int-truncation
}
@Override
public long contentLength() {
return header.getPayload().getFilesize();
// vault version 5 and onwards should have filesize: -1
if (this.header.getPayload().getFilesize() != -1l) {
throw new UncheckedIOException(new IOException("Attempted to decrypt file with invalid header (probably from previous vault version)"));
}
}
@Override
@@ -105,15 +103,7 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
@Override
public ByteBuffer cleartext() throws InterruptedException {
try {
final ByteBuffer cleartext = dataProcessor.processedData();
long bytesUntilLogicalEof = contentLength() - cleartextBytesDecrypted.sum();
if (bytesUntilLogicalEof <= 0) {
return FileContentCryptor.EOF;
} else if (bytesUntilLogicalEof < cleartext.remaining()) {
cleartext.limit((int) bytesUntilLogicalEof);
}
cleartextBytesDecrypted.add(cleartext.remaining());
return cleartext;
return dataProcessor.processedData();
} catch (ExecutionException e) {
if (e.getCause() instanceof AuthenticationFailedException) {
throw new AuthenticationFailedException(e);

View File

@@ -36,8 +36,6 @@ import org.cryptomator.io.ByteBuffers;
class FileContentEncryptorImpl implements FileContentEncryptor {
private static final String HMAC_SHA256 = "HmacSHA256";
private static final int PADDING_LOWER_BOUND = 4 * 1024; // 4k
private static final int PADDING_UPPER_BOUND = 16 * 1024 * 1024; // 16M
private static final int NUM_THREADS = Runtime.getRuntime().availableProcessors();
private static final int READ_AHEAD = 2;
private static final ExecutorService SHARED_DECRYPTION_EXECUTOR = Executors.newFixedThreadPool(NUM_THREADS);
@@ -63,7 +61,7 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
@Override
public ByteBuffer getHeader() {
header.getPayload().setFilesize(cleartextBytesScheduledForEncryption.sum());
header.getPayload().setFilesize(-1l);
return header.toByteBuffer(headerKey, hmacSha256);
}
@@ -76,7 +74,6 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
public void append(ByteBuffer cleartext) throws InterruptedException {
cleartextBytesScheduledForEncryption.add(cleartext.remaining());
if (cleartext == FileContentCryptor.EOF) {
appendSizeObfuscationPadding(cleartextBytesScheduledForEncryption.sum());
submitCleartextBuffer();
submitEof();
} else {
@@ -84,19 +81,6 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
}
}
private void appendSizeObfuscationPadding(long actualSize) throws InterruptedException {
final int maxPaddingLength = (int) Math.min(Math.max(actualSize / 10, PADDING_LOWER_BOUND), PADDING_UPPER_BOUND); // preferably 10%, but at least lower bound and no more than upper bound
final int randomPaddingLength = randomSource.nextInt(maxPaddingLength);
final ByteBuffer buf = ByteBuffer.allocate(PAYLOAD_SIZE);
int remainingPadding = randomPaddingLength;
while (remainingPadding > 0) {
int bytesInCurrentIteration = Math.min(remainingPadding, PAYLOAD_SIZE);
buf.clear().limit(bytesInCurrentIteration);
appendAllAndSubmitIfFull(buf);
remainingPadding -= bytesInCurrentIteration;
}
}
private void appendAllAndSubmitIfFull(ByteBuffer cleartext) throws InterruptedException {
while (cleartext.hasRemaining()) {
ByteBuffers.copy(cleartext, cleartextBuffer);

View File

@@ -15,6 +15,7 @@ import java.security.NoSuchAlgorithmException;
import java.util.regex.Pattern;
import javax.crypto.AEADBadTagException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.SecretKey;
import org.apache.commons.codec.binary.Base32;
@@ -70,8 +71,8 @@ class FilenameCryptorImpl implements FilenameCryptor {
try {
final byte[] cleartextBytes = AES_SIV.get().decrypt(encryptionKey, macKey, encryptedBytes, associatedData);
return new String(cleartextBytes, UTF_8);
} catch (AEADBadTagException e) {
throw new AuthenticationFailedException("Authentication failed.", e);
} catch (AEADBadTagException | IllegalBlockSizeException e) {
throw new AuthenticationFailedException("Invalid ciphertext.", e);
}
}

View File

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

View File

@@ -79,10 +79,10 @@ final class ConflictResolver {
}
private boolean isSameFileBasedOnSample(File file1, File file2, int sampleSize) {
try (ReadableFile r1 = file1.openReadable(); ReadableFile r2 = file2.openReadable()) {
if (r1.size() != r2.size()) {
return false;
} else {
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);

View File

@@ -8,6 +8,9 @@
*******************************************************************************/
package org.cryptomator.filesystem.crypto;
import static org.cryptomator.crypto.engine.impl.Constants.CHUNK_SIZE;
import static org.cryptomator.crypto.engine.impl.Constants.PAYLOAD_SIZE;
import java.io.UncheckedIOException;
import java.nio.file.FileAlreadyExistsException;
import java.util.Optional;
@@ -28,6 +31,25 @@ class CryptoFile extends CryptoNode implements File {
return parent().get().encryptChildName(name());
}
@Override
public long size() throws UncheckedIOException {
if (!physicalFile().isPresent()) {
return -1l;
} else {
File file = physicalFile().get();
long ciphertextSize = file.size() - cryptor.getFileContentCryptor().getHeaderSize();
long overheadPerChunk = CHUNK_SIZE - PAYLOAD_SIZE;
long numFullChunks = ciphertextSize / CHUNK_SIZE; // floor by int-truncation
long additionalCiphertextBytes = ciphertextSize % CHUNK_SIZE;
if (additionalCiphertextBytes > 0 && additionalCiphertextBytes <= overheadPerChunk) {
throw new IllegalArgumentException("Method not defined for input value " + ciphertextSize);
}
long additionalCleartextBytes = (additionalCiphertextBytes == 0) ? 0 : additionalCiphertextBytes - overheadPerChunk;
assert additionalCleartextBytes >= 0;
return PAYLOAD_SIZE * numFullChunks + additionalCleartextBytes;
}
}
@Override
public ReadableFile openReadable() {
boolean authenticate = !fileSystem().delegate().shouldSkipAuthentication(toString());

View File

@@ -70,12 +70,6 @@ class CryptoReadableFile implements ReadableFile {
}
}
@Override
public long size() throws UncheckedIOException {
assert decryptor != null : "decryptor is always being set during position(long)";
return decryptor.contentLength();
}
@Override
public void position(long position) throws UncheckedIOException {
if (readAheadTask != null) {

View File

@@ -103,6 +103,7 @@ class Masterkeys {
private static void writeMasterKey(File file, Cryptor cryptor, CharSequence passphrase) throws UncheckedIOException {
try (WritableFile writable = file.openWritable()) {
writable.truncate();
final byte[] fileContents = cryptor.writeKeysToMasterkeyFile(passphrase);
writable.write(ByteBuffer.wrap(fileContents));
}

View File

@@ -44,16 +44,9 @@ class NoFileContentCryptor implements FileContentCryptor {
private class Decryptor implements FileContentDecryptor {
private final BlockingQueue<Supplier<ByteBuffer>> cleartextQueue = new LinkedBlockingQueue<>();
private final long contentLength;
private Decryptor(ByteBuffer header) {
assert header.remaining() == Long.BYTES;
this.contentLength = header.getLong();
}
@Override
public long contentLength() {
return contentLength;
}
@Override

View File

@@ -21,7 +21,7 @@ public class CryptorImplTest {
@Test
public void testMasterkeyDecryptionWithCorrectPassphrase() throws IOException {
final String testMasterKey = "{\"version\":4,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
@@ -31,7 +31,7 @@ public class CryptorImplTest {
@Test(expected = InvalidPassphraseException.class)
public void testMasterkeyDecryptionWithWrongPassphrase() throws IOException {
final String testMasterKey = "{\"version\":4,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
@@ -52,7 +52,7 @@ public class CryptorImplTest {
@Ignore
@Test(expected = UnsupportedVaultFormatException.class)
public void testMasterkeyDecryptionWithMissingVersionMac() throws IOException {
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"}";
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
@@ -62,7 +62,7 @@ public class CryptorImplTest {
@Ignore
@Test(expected = UnsupportedVaultFormatException.class)
public void testMasterkeyDecryptionWithWrongVersionMac() throws IOException {
final String testMasterKey = "{\"version\":4,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"versionMac\":\"z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfoK=\"}";
@@ -72,14 +72,13 @@ public class CryptorImplTest {
@Test
public void testMasterkeyEncryption() throws IOException {
final String expectedMasterKey = "{\"version\":4,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," //
final String expectedMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
+ "\"hmacMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
+ "\"versionMac\":\"yuwoRE9GSdgQ2b//qRpTCj3W0qsVLxYVa7/KB3PkfA4=\"}";
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
cryptor.randomizeMasterkey();
final byte[] masterkeyFile = cryptor.writeKeysToMasterkeyFile("asd");
System.out.println(new String(masterkeyFile));
Assert.assertArrayEquals(expectedMasterKey.getBytes(), masterkeyFile);
}

View File

@@ -43,20 +43,6 @@ public class FileContentCryptorImplTest {
};
private static final SecureRandom RANDOM_MOCK_2 = new SecureRandom() {
@Override
public int nextInt(int bound) {
return 500;
}
@Override
public void nextBytes(byte[] bytes) {
Arrays.fill(bytes, (byte) 0x00);
}
};
@Test(expected = IllegalArgumentException.class)
public void testShortHeaderInDecryptor() throws InterruptedException {
final byte[] keyBytes = new byte[32];
@@ -137,45 +123,6 @@ public class FileContentCryptorImplTest {
Assert.assertArrayEquals("cleartext message".getBytes(), result);
}
@Test
public void testEncryptionAndDecryptionWithSizeObfuscationPadding() throws InterruptedException {
final byte[] keyBytes = new byte[32];
final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
FileContentCryptor cryptor = new FileContentCryptorImpl(encryptionKey, macKey, RANDOM_MOCK_2);
ByteBuffer header = ByteBuffer.allocate(cryptor.getHeaderSize());
ByteBuffer ciphertext = ByteBuffer.allocate(16 + 11 + 500 + 32 + 1); // 16 bytes iv + 11 bytes ciphertext + 500 bytes padding + 32 bytes mac + 1.
try (FileContentEncryptor encryptor = cryptor.createFileContentEncryptor(Optional.empty(), 0)) {
encryptor.append(ByteBuffer.wrap("hello world".getBytes()));
encryptor.append(FileContentCryptor.EOF);
ByteBuffer buf;
while ((buf = encryptor.ciphertext()) != FileContentCryptor.EOF) {
ByteBuffers.copy(buf, ciphertext);
}
ByteBuffers.copy(encryptor.getHeader(), header);
}
header.flip();
ciphertext.flip();
Assert.assertEquals(16 + 11 + 500 + 32, ciphertext.remaining());
ByteBuffer plaintext = ByteBuffer.allocate(12); // 11 bytes plaintext + 1
try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header, 0, true)) {
decryptor.append(ciphertext);
decryptor.append(FileContentCryptor.EOF);
ByteBuffer buf;
while ((buf = decryptor.cleartext()) != FileContentCryptor.EOF) {
ByteBuffers.copy(buf, plaintext);
}
}
plaintext.flip();
byte[] result = new byte[plaintext.remaining()];
plaintext.get(result);
Assert.assertArrayEquals("hello world".getBytes(), result);
}
@Test(timeout = 20000) // assuming a minimum speed of 10mb/s during encryption and decryption 20s should be enough
public void testEncryptionAndDecryptionSpeed() throws InterruptedException, IOException {
final byte[] keyBytes = new byte[32];

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("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
final byte[] content = Base64.decode("AAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3Og=");
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
@@ -68,7 +68,7 @@ public class FileContentDecryptorImplTest {
final byte[] keyBytes = new byte[32];
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJa==");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJG==");
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
@@ -80,7 +80,7 @@ public class FileContentDecryptorImplTest {
final byte[] keyBytes = new byte[32];
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
final byte[] content = Base64.decode("aAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3Og=");
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
@@ -101,7 +101,7 @@ public class FileContentDecryptorImplTest {
final byte[] keyBytes = new byte[32];
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
final byte[] content = Base64.decode("AAAAAAAAAAAAAAAAAAAAALTwrBTNYP7m3yTGKlhka9WPvX1Lpn5EYfVxlyX1ISgRXtdRnivM7r6F3OG=");
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, false)) {
@@ -124,7 +124,7 @@ public class FileContentDecryptorImplTest {
final byte[] keyBytes = new byte[32];
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
decryptor.cancelWithException(new IOException("can not do"));

View File

@@ -35,20 +35,6 @@ public class FileContentEncryptorImplTest {
};
private static final SecureRandom RANDOM_MOCK_2 = new SecureRandom() {
@Override
public int nextInt(int bound) {
return 42;
}
@Override
public void nextBytes(byte[] bytes) {
Arrays.fill(bytes, (byte) 0x00);
}
};
@Test
public void testEncryption() throws InterruptedException {
final byte[] keyBytes = new byte[32];
@@ -95,24 +81,4 @@ public class FileContentEncryptorImplTest {
}
}
@Test
public void testSizeObfuscation() throws InterruptedException {
final byte[] keyBytes = new byte[32];
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
try (FileContentEncryptor encryptor = new FileContentEncryptorImpl(headerKey, macKey, RANDOM_MOCK_2, 0)) {
encryptor.append(FileContentCryptor.EOF);
ByteBuffer result = ByteBuffer.allocate(91); // 16 bytes iv + 42 bytes size obfuscation + 32 bytes mac + 1
ByteBuffer buf;
while ((buf = encryptor.ciphertext()) != FileContentCryptor.EOF) {
ByteBuffers.copy(buf, result);
}
result.flip();
Assert.assertEquals(90, result.remaining());
}
}
}

View File

@@ -53,13 +53,26 @@ public class FileHeaderTest {
@Test
public void testDecryption() {
final byte[] keyBytes = new byte[32];
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
final ByteBuffer headerBuf = ByteBuffer.wrap(Base64.decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg=="));
final FileHeader header = FileHeader.decrypt(headerKey, new ThreadLocalMac(macKey, "HmacSHA256"), headerBuf);
Assert.assertEquals(-1l, header.getPayload().getFilesize());
Assert.assertArrayEquals(new byte[16], header.getIv());
Assert.assertArrayEquals(new byte[32], header.getPayload().getContentKey().getEncoded());
}
@Test
public void testDecryptionOfOldHeader() {
final byte[] keyBytes = new byte[32];
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
final ByteBuffer headerBuf = ByteBuffer.wrap(Base64.decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA=="));
final FileHeader header = FileHeader.decrypt(headerKey, new ThreadLocalMac(macKey, "HmacSHA256"), headerBuf);
Assert.assertEquals(42, header.getPayload().getFilesize());
Assert.assertEquals(42l, header.getPayload().getFilesize());
Assert.assertArrayEquals(new byte[16], header.getIv());
Assert.assertArrayEquals(new byte[32], header.getPayload().getContentKey().getEncoded());
}

View File

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

View File

@@ -43,6 +43,11 @@ class InMemoryFile extends InMemoryNode implements File {
return buf;
}
@Override
public long size() throws UncheckedIOException {
return content.get().limit();
}
@Override
public void moveTo(File destination) throws UncheckedIOException {
if (destination instanceof InMemoryFile) {
@@ -103,7 +108,7 @@ class InMemoryFile extends InMemoryNode implements File {
throw new UncheckedIOException(new FileAlreadyExistsException(k));
} else {
if (v == null) {
assert!content.get().hasRemaining();
assert !content.get().hasRemaining();
this.creationTime = Instant.now();
}
this.lastModified = Instant.now();
@@ -120,7 +125,7 @@ class InMemoryFile extends InMemoryNode implements File {
// returning null removes the entry.
return null;
});
assert!this.exists();
assert !this.exists();
}
@Override

View File

@@ -51,11 +51,6 @@ class InMemoryReadableFile implements ReadableFile {
}
}
@Override
public long size() throws UncheckedIOException {
return contentGetter.get().limit();
}
@Override
public void position(long position) throws UncheckedIOException {
assert position < Integer.MAX_VALUE : "Can not use that big in-memory files.";

View File

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

View File

@@ -9,7 +9,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.1.4</version>
<version>1.2.0</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.1.4</version>
<version>1.2.0</version>
</parent>
<artifactId>filesystem-nameshortening</artifactId>
<name>Cryptomator filesystem: Name shortening layer</name>

View File

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

View File

@@ -17,6 +17,11 @@ import java.util.stream.Stream;
class DefaultNioAccess implements NioAccess {
@Override
public long size(Path path) throws IOException {
return Files.size(path);
}
@Override
public AsynchronousFileChannel open(Path path, OpenOption... options) throws IOException {
return AsynchronousFileChannel.open(path, options);
@@ -59,7 +64,8 @@ class DefaultNioAccess implements NioAccess {
} catch (AccessDeniedException e) {
// workaround for https://github.com/cryptomator/cryptomator/issues/317
try {
if (path.toFile().delete()) return;
if (path.toFile().delete())
return;
} catch (UnsupportedOperationException e2) {
// ignore
}

View File

@@ -16,6 +16,8 @@ interface NioAccess {
public static final Holder<NioAccess> DEFAULT = new Holder<>(new DefaultNioAccess());
long size(Path path) throws IOException;
AsynchronousFileChannel open(Path path, OpenOption... options) throws IOException;
boolean isRegularFile(Path path, LinkOption... options);

View File

@@ -27,6 +27,15 @@ 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) {

View File

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

View File

@@ -16,6 +16,7 @@ import static org.mockito.Mockito.when;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
@@ -85,6 +86,27 @@ public class NioFileTest {
}
public class Size {
@Test
public void testSizeReturnsSizeOfRegularFile() throws IOException {
when(nioAccess.size(path)).thenReturn(42l);
assertThat(inTest.size(), is(42l));
}
@Test
public void testSizeThrowsExceptionIfRegularFileThrowsException() throws IOException {
Throwable t = new NoSuchFileException("foo");
when(nioAccess.size(path)).thenThrow(t);
thrown.expect(UncheckedIOException.class);
thrown.expectCause(org.hamcrest.Matchers.sameInstance(t));
inTest.size();
}
}
public class Open {
@Test

View File

@@ -83,16 +83,6 @@ public class ReadableNioFileTest {
inTest.position(-1);
}
@Test
public void testSizeReturnsSizeOfChannel() {
long expectedSize = 85472;
when(channel.size()).thenReturn(expectedSize);
long actualSize = inTest.size();
assertThat(actualSize, is(expectedSize));
}
@Test
public void testReadDelegatesToChannelReadFullyWithZeroPositionIfNotSet() {
ByteBuffer buffer = mock(ByteBuffer.class);

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.1.4</version>
<version>1.2.0</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.1.4</version>
<version>1.2.0</version>
</parent>
<artifactId>frontend-api</artifactId>
<name>Cryptomator frontend: API</name>

View File

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

View File

@@ -7,8 +7,10 @@ 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;
@@ -27,18 +29,15 @@ 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 = new HashSet<>();
private final Set<FrontendId> validFrontendIds = synchronizedSet(new HashSet<>());
@Inject
public Tarpit() {
}
public void register(FrontendId frontendId) {
validFrontendIds.add(frontendId);
}
public void unregister(FrontendId frontendId) {
validFrontendIds.remove(frontendId);
public void setValidFrontendIds(Collection<FrontendId> validFrontendIds) {
this.validFrontendIds.retainAll(validFrontendIds);
this.validFrontendIds.addAll(validFrontendIds);
}
public void handle(HttpServletRequest req) {

View File

@@ -24,15 +24,13 @@ class WebDavFrontend implements Frontend {
private final WebDavMounterProvider webdavMounterProvider;
private final ServletContextHandler handler;
private final URI uri;
private final Runnable afterClose;
private WebDavMount mount;
public WebDavFrontend(WebDavMounterProvider webdavMounterProvider, ServletContextHandler handler, URI uri, Runnable afterUnmount) throws FrontendCreationFailedException {
public WebDavFrontend(WebDavMounterProvider webdavMounterProvider, ServletContextHandler handler, URI uri) throws FrontendCreationFailedException {
this.webdavMounterProvider = webdavMounterProvider;
this.handler = handler;
this.uri = uri;
this.afterClose = afterUnmount;
try {
handler.start();
} catch (Exception e) {
@@ -42,12 +40,8 @@ class WebDavFrontend implements Frontend {
@Override
public void close() throws Exception {
try {
unmount();
handler.stop();
} finally {
afterClose.run();
}
unmount();
handler.stop();
}
@Override

View File

@@ -12,6 +12,7 @@ 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;
@@ -117,9 +118,12 @@ public class WebDavServer implements FrontendFactory {
throw new IllegalStateException(e);
}
final ServletContextHandler handler = addServlet(root, uri);
tarpit.register(id);
LOG.info("Servlet available under " + uri);
return new WebDavFrontend(webdavMounterProvider, handler, uri, () -> tarpit.unregister(id));
return new WebDavFrontend(webdavMounterProvider, handler, uri);
}
public void setValidFrontendIds(Collection<FrontendId> validFrontendIds) {
tarpit.setValidFrontendIds(validFrontendIds);
}
}

View File

@@ -32,14 +32,11 @@ import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.ReadableFile;
import org.cryptomator.filesystem.jackrabbit.FileLocator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.io.ByteStreams;
class DavFile extends DavNode<FileLocator> {
private static final Logger LOG = LoggerFactory.getLogger(DavFile.class);
protected static final String CONTENT_TYPE_VALUE = "application/octet-stream";
protected static final String CONTENT_DISPOSITION_HEADER = "Content-Disposition";
protected static final String CONTENT_DISPOSITION_VALUE = "attachment";
@@ -64,8 +61,8 @@ class DavFile extends DavNode<FileLocator> {
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);
}
}
@@ -157,12 +154,7 @@ class DavFile extends DavNode<FileLocator> {
private Optional<DavProperty<?>> sizeProperty() {
if (node.exists()) {
try (ReadableFile src = node.openReadable()) {
return Optional.of(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, src.size()));
} catch (RuntimeException e) {
LOG.warn("Could not determine file size of " + getResourcePath(), e);
return Optional.empty();
}
return Optional.of(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, node.size()));
} else {
return Optional.empty();
}

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

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

@@ -5,7 +5,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.1.4</version>
<version>1.2.0</version>
</parent>
<artifactId>jacoco-report</artifactId>
<name>Cryptomator Code Coverage Report</name>

52
main/keychain/pom.xml Normal file
View File

@@ -0,0 +1,52 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.2.0</version>
</parent>
<artifactId>keychain</artifactId>
<name>System Keychain Access</name>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.7</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.54</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>jni</artifactId>
</dependency>
<!-- DI -->
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger</artifactId>
</dependency>
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger-compiler</artifactId>
<scope>provided</scope>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>commons-test</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,26 @@
package org.cryptomator.keychain;
public interface KeychainAccess {
/**
* Associates a passphrase with a given key.
*
* @param key Key used to retrieve the passphrase via {@link #loadPassphrase(String)}.
* @param passphrase The secret to store in this keychain.
*/
void storePassphrase(String key, CharSequence passphrase);
/**
* @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
* @return The stored passphrase for the given key or <code>null</code> if no value for the given key could be found.
*/
char[] loadPassphrase(String key);
/**
* Deletes a passphrase with a given key.
*
* @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
*/
void deletePassphrase(String key);
}

View File

@@ -0,0 +1,10 @@
package org.cryptomator.keychain;
interface KeychainAccessStrategy extends KeychainAccess {
/**
* @return <code>true</code> if this KeychainAccessStrategy works on the current machine.
*/
boolean isSupported();
}

View File

@@ -0,0 +1,28 @@
package org.cryptomator.keychain;
import java.util.Optional;
import java.util.Set;
import org.cryptomator.jni.JniModule;
import com.google.common.collect.Sets;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.ElementsIntoSet;
@Module(includes = {JniModule.class})
public class KeychainModule {
@Provides
@ElementsIntoSet
Set<KeychainAccessStrategy> provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain) {
return Sets.newHashSet(macKeychain, winKeychain);
}
@Provides
public Optional<KeychainAccess> provideSupportedKeychain(Set<KeychainAccessStrategy> keychainAccessStrategies) {
return keychainAccessStrategies.stream().filter(KeychainAccessStrategy::isSupported).map(KeychainAccess.class::cast).findFirst();
}
}

View File

@@ -0,0 +1,44 @@
package org.cryptomator.keychain;
import java.util.Optional;
import javax.inject.Inject;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.jni.MacFunctions;
import org.cryptomator.jni.MacKeychainAccess;
class MacSystemKeychainAccess implements KeychainAccessStrategy {
private final MacKeychainAccess keychain;
@Inject
public MacSystemKeychainAccess(Optional<MacFunctions> macFunctions) {
if (macFunctions.isPresent()) {
this.keychain = macFunctions.get().keychainAccess();
} else {
this.keychain = null;
}
}
@Override
public void storePassphrase(String key, CharSequence passphrase) {
keychain.storePassword(key, passphrase);
}
@Override
public char[] loadPassphrase(String key) {
return keychain.loadPassword(key);
}
@Override
public boolean isSupported() {
return SystemUtils.IS_OS_MAC_OSX && keychain != null;
}
@Override
public void deletePassphrase(String key) {
keychain.deletePassword(key);
}
}

View File

@@ -0,0 +1,191 @@
package org.cryptomator.keychain;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.lang.reflect.Type;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import javax.inject.Inject;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.jni.WinDataProtection;
import org.cryptomator.jni.WinFunctions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
class WindowsProtectedKeychainAccess implements KeychainAccessStrategy {
private static final Logger LOG = LoggerFactory.getLogger(WindowsProtectedKeychainAccess.class);
private static final Gson GSON = new GsonBuilder().setPrettyPrinting() //
.registerTypeHierarchyAdapter(byte[].class, new ByteArrayJsonAdapter()) //
.disableHtmlEscaping().create();
private final WinDataProtection dataProtection;
private final Path keychainPath;
private Map<String, KeychainEntry> keychainEntries;
@Inject
public WindowsProtectedKeychainAccess(Optional<WinFunctions> winFunctions) {
if (winFunctions.isPresent()) {
this.dataProtection = winFunctions.get().dataProtection();
} else {
this.dataProtection = null;
}
String keychainPathProperty = System.getProperty("cryptomator.keychainPath");
if (dataProtection != null && keychainPathProperty == null) {
LOG.warn("Windows DataProtection module loaded, but no cryptomator.keychainPath property found.");
}
if (keychainPathProperty != null) {
if (keychainPathProperty.startsWith("~/")) {
keychainPathProperty = SystemUtils.USER_HOME + keychainPathProperty.substring(1);
}
this.keychainPath = FileSystems.getDefault().getPath(keychainPathProperty);
} else {
this.keychainPath = null;
}
}
@Override
public void storePassphrase(String key, CharSequence passphrase) {
loadKeychainEntriesIfNeeded();
ByteBuffer buf = UTF_8.encode(CharBuffer.wrap(passphrase));
byte[] cleartext = new byte[buf.remaining()];
buf.get(cleartext);
KeychainEntry entry = new KeychainEntry();
entry.salt = generateSalt();
entry.ciphertext = dataProtection.protect(cleartext, entry.salt);
Arrays.fill(buf.array(), (byte) 0x00);
Arrays.fill(cleartext, (byte) 0x00);
keychainEntries.put(key, entry);
saveKeychainEntries();
}
@Override
public char[] loadPassphrase(String key) {
loadKeychainEntriesIfNeeded();
KeychainEntry entry = keychainEntries.get(key);
if (entry == null) {
return null;
}
byte[] cleartext = dataProtection.unprotect(entry.ciphertext, entry.salt);
if (cleartext == null) {
return null;
}
CharBuffer buf = UTF_8.decode(ByteBuffer.wrap(cleartext));
char[] passphrase = new char[buf.remaining()];
buf.get(passphrase);
Arrays.fill(cleartext, (byte) 0x00);
Arrays.fill(buf.array(), (char) 0x00);
return passphrase;
}
@Override
public void deletePassphrase(String key) {
loadKeychainEntriesIfNeeded();
keychainEntries.remove(key);
saveKeychainEntries();
}
@Override
public boolean isSupported() {
return SystemUtils.IS_OS_WINDOWS && dataProtection != null && keychainPath != null;
}
private byte[] generateSalt() {
byte[] result = new byte[2 * Long.BYTES];
UUID uuid = UUID.randomUUID();
ByteBuffer buf = ByteBuffer.wrap(result);
buf.putLong(uuid.getMostSignificantBits());
buf.putLong(uuid.getLeastSignificantBits());
return result;
}
private void loadKeychainEntriesIfNeeded() {
if (keychainEntries == null) {
loadKeychainEntries();
}
assert keychainEntries != null;
}
private void loadKeychainEntries() {
Type type = new TypeToken<Map<String, KeychainEntry>>() {
}.getType();
try (InputStream in = Files.newInputStream(keychainPath, StandardOpenOption.READ); //
Reader reader = new InputStreamReader(in, UTF_8)) {
keychainEntries = GSON.fromJson(reader, type);
} catch (JsonParseException | NoSuchFileException e) {
LOG.info("Creating new keychain at path {}", keychainPath);
} catch (IOException e) {
throw new UncheckedIOException("Could not read keychain from path " + keychainPath, e);
}
if (keychainEntries == null) {
keychainEntries = new HashMap<>();
}
}
private void saveKeychainEntries() {
try (OutputStream out = Files.newOutputStream(keychainPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); //
Writer writer = new OutputStreamWriter(out, UTF_8)) {
GSON.toJson(keychainEntries, writer);
} catch (IOException e) {
throw new UncheckedIOException("Could not read keychain from path " + keychainPath, e);
}
}
private static class KeychainEntry {
@SerializedName("ciphertext")
byte[] ciphertext;
@SerializedName("salt")
byte[] salt;
}
private static class ByteArrayJsonAdapter implements JsonSerializer<byte[]>, JsonDeserializer<byte[]> {
private static final Base64 BASE64 = new Base64();
@Override
public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return BASE64.decode(json.getAsString().getBytes(StandardCharsets.UTF_8));
}
@Override
public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(new String(BASE64.encode(src), StandardCharsets.UTF_8));
}
}
}

View File

@@ -0,0 +1,17 @@
package org.cryptomator.keychain;
import java.util.Optional;
import org.junit.Assert;
import org.junit.Test;
public class KeychainModuleTest {
@Test
public void testGetKeychain() {
Optional<KeychainAccess> keychainAccess = DaggerTestKeychainComponent.builder().jniModule(new TestJniModule()).keychainModule(new TestKeychainModule()).build().keychainAccess();
Assert.assertTrue(keychainAccess.isPresent());
Assert.assertTrue(keychainAccess.get() instanceof MapKeychainAccess);
}
}

View File

@@ -0,0 +1,34 @@
package org.cryptomator.keychain;
import java.util.HashMap;
import java.util.Map;
class MapKeychainAccess implements KeychainAccessStrategy {
private final Map<String, char[]> map = new HashMap<>();
@Override
public void storePassphrase(String key, CharSequence passphrase) {
char[] pw = new char[passphrase.length()];
for (int i = 0; i < passphrase.length(); i++) {
pw[i] = passphrase.charAt(i);
}
map.put(key, pw);
}
@Override
public char[] loadPassphrase(String key) {
return map.get(key);
}
@Override
public void deletePassphrase(String key) {
map.remove(key);
}
@Override
public boolean isSupported() {
return true;
}
}

View File

@@ -0,0 +1,23 @@
package org.cryptomator.keychain;
import java.util.Optional;
import org.cryptomator.jni.JniModule;
import org.cryptomator.jni.MacFunctions;
import org.cryptomator.jni.WinFunctions;
import dagger.Lazy;
public class TestJniModule extends JniModule {
@Override
public Optional<WinFunctions> winFunctions(Lazy<WinFunctions> winFunction) {
return Optional.empty();
}
@Override
public Optional<MacFunctions> macFunctions(Lazy<MacFunctions> winFunction) {
return Optional.empty();
}
}

View File

@@ -0,0 +1,15 @@
package org.cryptomator.keychain;
import java.util.Optional;
import javax.inject.Singleton;
import dagger.Component;
@Singleton
@Component(modules = KeychainModule.class)
interface TestKeychainComponent {
Optional<KeychainAccess> keychainAccess();
}

View File

@@ -0,0 +1,14 @@
package org.cryptomator.keychain;
import java.util.Set;
import com.google.common.collect.Sets;
public class TestKeychainModule extends KeychainModule {
@Override
Set<KeychainAccessStrategy> provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain) {
return Sets.newHashSet(new MapKeychainAccess());
}
}

View File

@@ -0,0 +1,60 @@
package org.cryptomator.keychain;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import org.cryptomator.jni.WinDataProtection;
import org.cryptomator.jni.WinFunctions;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mockito;
import org.mockito.stubbing.Answer;
public class WindowsProtectedKeychainAccessTest {
@Rule
public final ExpectedException thrown = ExpectedException.none();
private Path tmpFile;
private WindowsProtectedKeychainAccess keychain;
@Before
public void setup() throws IOException, ReflectiveOperationException {
tmpFile = Files.createTempFile("unit-tests", ".tmp");
System.setProperty("cryptomator.keychainPath", tmpFile.toAbsolutePath().normalize().toString());
WinFunctions winFunctions = Mockito.mock(WinFunctions.class);
WinDataProtection winDataProtection = Mockito.mock(WinDataProtection.class);
Mockito.when(winFunctions.dataProtection()).thenReturn(winDataProtection);
Answer<byte[]> answerReturningFirstArg = invocation -> invocation.getArgumentAt(0, byte[].class).clone();
Mockito.when(winDataProtection.protect(Mockito.any(), Mockito.any())).thenAnswer(answerReturningFirstArg);
Mockito.when(winDataProtection.unprotect(Mockito.any(), Mockito.any())).thenAnswer(answerReturningFirstArg);
keychain = new WindowsProtectedKeychainAccess(Optional.of(winFunctions));
}
@After
public void teardown() throws IOException {
Files.deleteIfExists(tmpFile);
}
@Test
public void testStoreAndLoad() {
String storedPw1 = "topSecret";
String storedPw2 = "bottomSecret";
keychain.storePassphrase("myPassword", storedPw1);
keychain.storePassphrase("myOtherPassword", storedPw2);
String loadedPw1 = new String(keychain.loadPassphrase("myPassword"));
String loadedPw2 = new String(keychain.loadPassphrase("myOtherPassword"));
Assert.assertEquals(storedPw1, loadedPw1);
Assert.assertEquals(storedPw2, loadedPw2);
keychain.deletePassphrase("myPassword");
Assert.assertNull(keychain.loadPassphrase("myPassword"));
Assert.assertNull(keychain.loadPassphrase("nonExistingPassword"));
}
}

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright (c) 2014 Markus Kreusch
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - log4j config for WebDAV unit tests
-->
<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>
<!-- show our own debug messages: -->
<Logger name="org.cryptomator" level="DEBUG" />
<!-- mute dependencies: -->
<Root level="INFO">
<AppenderRef ref="Console" />
<AppenderRef ref="StdErr" />
</Root>
</Loggers>
</Configuration>

View File

@@ -1,13 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright (c) 2014 Sebastian Stenzel This file is licensed under the
terms of the MIT license. See the LICENSE.txt file for more info. Contributors:
<!-- Copyright (c) 2014 Sebastian Stenzel This file is licensed under the
terms of the MIT license. See the LICENSE.txt file for more info. Contributors:
Sebastian Stenzel - initial API and implementation -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.1.4</version>
<version>1.2.0</version>
<packaging>pom</packaging>
<name>Cryptomator</name>
@@ -28,6 +27,8 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- dependency versions -->
<cryptomator.cryptolib.version>1.0.2</cryptomator.cryptolib.version>
<cryptomator.jni.version>1.0.0</cryptomator.jni.version>
<log4j.version>2.1</log4j.version>
<slf4j.version>1.7.7</slf4j.version>
<junit.version>4.12</junit.version>
@@ -40,9 +41,22 @@
<commons-httpclient.version>3.1</commons-httpclient.version>
<jackson-databind.version>2.4.4</jackson-databind.version>
<mockito.version>1.10.19</mockito.version>
<dagger.version>2.4</dagger.version>
<dagger.version>2.6.1</dagger.version>
</properties>
<repositories>
<repository>
<id>ossrh-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<dependencyManagement>
<dependencies>
<!-- modules -->
@@ -57,7 +71,6 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-api</artifactId>
@@ -106,7 +119,6 @@
<artifactId>filesystem-stats</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>frontend-api</artifactId>
@@ -117,12 +129,28 @@
<artifactId>frontend-webdav</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>keychain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>ui</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Cryptomator Libs -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>cryptolib</artifactId>
<version>${cryptomator.cryptolib.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>jni</artifactId>
<version>${cryptomator.jni.version}</version>
</dependency>
<!-- Logging -->
<dependency>
@@ -168,19 +196,18 @@
<version>${commons-codec.version}</version>
</dependency>
<dependency>
<!-- org.apache.httpcomponents:httpclient is newer, but jackrabbit uses
this version. We don't have a reason to upgrade -->
<!-- org.apache.httpcomponents:httpclient is newer, but jackrabbit uses this version. We don't have a reason to upgrade -->
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>${commons-httpclient.version}</version>
</dependency>
<!-- EasyBind -->
<dependency>
<groupId>org.fxmisc.easybind</groupId>
<artifactId>easybind</artifactId>
<version>1.0.3</version>
</dependency>
</dependency>
<!-- Guava -->
<dependency>
@@ -267,14 +294,15 @@
<module>filesystem-inmemory</module>
<module>filesystem-nio</module>
<module>filesystem-nameshortening</module>
<module>filesystem-charsets</module>
<module>filesystem-crypto</module>
<module>filesystem-crypto-integration-tests</module>
<module>filesystem-stats</module>
<module>filesystem-invariants-tests</module>
<module>frontend-api</module>
<module>frontend-webdav</module>
<module>keychain</module>
<module>ui</module>
<module>filesystem-charsets</module>
</modules>
<profiles>
@@ -343,17 +371,6 @@
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.eluder.coveralls</groupId>
<artifactId>coveralls-maven-plugin</artifactId>
<version>4.0.0</version>
<configuration>
<jacocoReports>
<jacocoReport>jacoco-report/target/site/jacoco-aggregate/jacoco.xml</jacocoReport>
</jacocoReports>
<repoToken>${env.COVERALLS_REPO_TOKEN}</repoToken>
</configuration>
</plugin>
</plugins>
</build>

View File

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

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.1.4</version>
<version>1.2.0</version>
</parent>
<artifactId>ui</artifactId>
<name>Cryptomator GUI</name>
@@ -54,6 +54,20 @@
<groupId>org.cryptomator</groupId>
<artifactId>frontend-webdav</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>jni</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>keychain</artifactId>
</dependency>
<!-- CryptoLib -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>cryptolib</artifactId>
</dependency>
<!-- EasyBind -->
<dependency>

View File

@@ -8,10 +8,12 @@
*******************************************************************************/
package org.cryptomator.ui;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import javax.inject.Singleton;
import org.cryptomator.jni.MacFunctions;
import org.cryptomator.ui.controllers.MainController;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.util.AsyncTaskService;
@@ -34,4 +36,7 @@ interface CryptomatorComponent {
Localization localization();
ExitUtil exitUtil();
}
Optional<MacFunctions> nativeMacFunctions();
}

View File

@@ -8,6 +8,8 @@
*******************************************************************************/
package org.cryptomator.ui;
import static java.util.stream.Collectors.toList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -16,10 +18,16 @@ import javax.inject.Singleton;
import org.cryptomator.common.CommonsModule;
import org.cryptomator.crypto.engine.impl.CryptoEngineModule;
import org.cryptomator.cryptolib.CryptoLibModule;
import org.cryptomator.frontend.FrontendFactory;
import org.cryptomator.frontend.FrontendId;
import org.cryptomator.frontend.webdav.WebDavModule;
import org.cryptomator.frontend.webdav.WebDavServer;
import org.cryptomator.jni.JniModule;
import org.cryptomator.keychain.KeychainModule;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.model.VaultObjectMapperProvider;
import org.cryptomator.ui.model.Vaults;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.settings.SettingsProvider;
import org.cryptomator.ui.util.DeferredCloser;
@@ -31,9 +39,10 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import dagger.Module;
import dagger.Provides;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.stage.Stage;
@Module(includes = {CryptoEngineModule.class, CommonsModule.class, WebDavModule.class})
@Module(includes = {CryptoEngineModule.class, CommonsModule.class, WebDavModule.class, KeychainModule.class, JniModule.class, CryptoLibModule.class})
class CryptomatorModule {
private static final Logger LOG = LoggerFactory.getLogger(CryptomatorModule.class);
@@ -93,10 +102,17 @@ class CryptomatorModule {
@Provides
@Singleton
FrontendFactory provideFrontendFactory(DeferredCloser closer, WebDavServer webDavServer, Settings settings) {
FrontendFactory provideFrontendFactory(DeferredCloser closer, WebDavServer webDavServer, Vaults vaults, Settings settings) {
vaults.addListener((Observable o) -> setValidFrontendIds(webDavServer, vaults));
setValidFrontendIds(webDavServer, vaults);
webDavServer.setPort(settings.getPort());
webDavServer.start();
return closer.closeLater(webDavServer, WebDavServer::stop).get().orElseThrow(IllegalStateException::new);
}
private void setValidFrontendIds(WebDavServer webDavServer, Vaults vaults) {
webDavServer.setValidFrontendIds(vaults.stream() //
.map(Vault::getId).map(FrontendId::from).collect(toList()));
}
}

View File

@@ -21,6 +21,7 @@ import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
@@ -32,6 +33,9 @@ import javax.script.ScriptException;
import javax.swing.SwingUtilities;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.jni.JniException;
import org.cryptomator.jni.MacApplicationUiState;
import org.cryptomator.jni.MacFunctions;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.settings.Settings;
import org.slf4j.Logger;
@@ -48,12 +52,14 @@ class ExitUtil {
private final Stage mainWindow;
private final Localization localization;
private final Settings settings;
private final Optional<MacFunctions> macFunctions;
@Inject
public ExitUtil(@Named("mainWindow") Stage mainWindow, Localization localization, Settings settings) {
public ExitUtil(@Named("mainWindow") Stage mainWindow, Localization localization, Settings settings, Optional<MacFunctions> macFunctions) {
this.mainWindow = mainWindow;
this.localization = localization;
this.settings = settings;
this.macFunctions = macFunctions;
}
public void initExitHandler(Runnable exitCommand) {
@@ -88,6 +94,7 @@ class ExitUtil {
if (Platform.isImplicitExit()) {
exitCommand.run();
} else {
macFunctions.map(MacFunctions::uiState).ifPresent(JniException.ignore(MacApplicationUiState::transformToAgentApplication));
mainWindow.close();
this.showTrayNotification(trayIcon);
}
@@ -189,6 +196,7 @@ class ExitUtil {
private void restoreFromTray(ActionEvent event) {
Platform.runLater(() -> {
macFunctions.map(MacFunctions::uiState).ifPresent(JniException.ignore(MacApplicationUiState::transformToForegroundApplication));
mainWindow.show();
mainWindow.requestFocus();
});

View File

@@ -12,9 +12,12 @@ import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.concurrent.ExecutionException;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.cryptolib.common.SecureRandomModule;
import org.cryptomator.ui.controllers.MainController;
import org.cryptomator.ui.util.ActiveWindowStyleSupport;
import org.cryptomator.ui.util.DeferredCloser;
@@ -40,7 +43,15 @@ public class MainApplication extends Application {
@Override
public void start(Stage primaryStage) throws IOException {
LOG.info("JavaFX application started");
final CryptomatorComponent comp = DaggerCryptomatorComponent.builder().cryptomatorModule(new CryptomatorModule(this, primaryStage)).build();
final CryptomatorComponent comp;
try {
comp = DaggerCryptomatorComponent.builder() //
.cryptomatorModule(new CryptomatorModule(this, primaryStage)) //
.secureRandomModule(new SecureRandomModule(SecureRandom.getInstanceStrong())) //
.build();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Every implementation of the Java platform is required to support at least one strong SecureRandom implementation.", e);
}
final MainController mainCtrl = comp.mainController();
closer = comp.deferredCloser();

View File

@@ -12,6 +12,7 @@ package org.cryptomator.ui.controllers;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.util.Objects;
import java.util.Optional;
import javax.inject.Inject;
@@ -31,9 +32,7 @@ import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
@@ -49,9 +48,9 @@ public class ChangePasswordController extends LocalizedFXMLViewController {
private final Application app;
private final PasswordStrengthUtil strengthRater;
final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
private Optional<ChangePasswordListener> listener = Optional.empty();
private final IntegerProperty passwordStrength = new SimpleIntegerProperty(); // 0-4
private Optional<ChangePasswordListener> listener = Optional.empty();
private Vault vault;
@Inject
public ChangePasswordController(Application app, PasswordStrengthUtil strengthRater, Localization localization) {
@@ -101,7 +100,6 @@ public class ChangePasswordController extends LocalizedFXMLViewController {
BooleanBinding oldPasswordIsEmpty = oldPasswordField.textProperty().isEmpty();
BooleanBinding newPasswordIsEmpty = newPasswordField.textProperty().isEmpty();
BooleanBinding passwordsDiffer = newPasswordField.textProperty().isNotEqualTo(retypePasswordField.textProperty());
EasyBind.subscribe(vault, this::vaultDidChange);
changePasswordButton.disableProperty().bind(oldPasswordIsEmpty.or(newPasswordIsEmpty.or(passwordsDiffer)));
passwordStrength.bind(EasyBind.map(newPasswordField.textProperty(), strengthRater::computeRate));
@@ -118,13 +116,15 @@ public class ChangePasswordController extends LocalizedFXMLViewController {
return getClass().getResource("/fxml/change_password.fxml");
}
private void vaultDidChange(Vault newVault) {
oldPasswordField.clear();
newPasswordField.clear();
retypePasswordField.clear();
void setVault(Vault vault) {
this.vault = Objects.requireNonNull(vault);
oldPasswordField.swipe();
newPasswordField.swipe();
retypePasswordField.swipe();
// trigger "default" change to refresh key bindings:
changePasswordButton.setDefaultButton(false);
changePasswordButton.setDefaultButton(true);
messageText.setText(null);
}
// ****************************************
@@ -144,8 +144,8 @@ public class ChangePasswordController extends LocalizedFXMLViewController {
private void didClickChangePasswordButton(ActionEvent event) {
downloadsPageLink.setVisible(false);
try {
vault.get().changePassphrase(oldPasswordField.getCharacters(), newPasswordField.getCharacters());
messageText.setText(localization.getString("changePassword.infoMessage.success"));
vault.changePassphrase(oldPasswordField.getCharacters(), newPasswordField.getCharacters());
messageText.setText(null);
listener.ifPresent(this::invokeListenerLater);
} catch (InvalidPassphraseException e) {
messageText.setText(localization.getString("changePassword.errorMessage.wrongPassword"));

View File

@@ -10,9 +10,10 @@
package org.cryptomator.ui.controllers;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.FileAlreadyExistsException;
import java.util.Objects;
import java.util.Optional;
import javax.inject.Inject;
@@ -29,9 +30,7 @@ import org.slf4j.LoggerFactory;
import javafx.application.Platform;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
@@ -44,9 +43,9 @@ public class InitializeController extends LocalizedFXMLViewController {
private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class);
private final PasswordStrengthUtil strengthRater;
final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
private Optional<InitializationListener> listener = Optional.empty();
private final IntegerProperty passwordStrength = new SimpleIntegerProperty(); // 0-4
private Optional<InitializationListener> listener = Optional.empty();
private Vault vault;
@Inject
public InitializeController(Localization localization, PasswordStrengthUtil strengthRater) {
@@ -88,7 +87,6 @@ public class InitializeController extends LocalizedFXMLViewController {
public void initialize() {
BooleanBinding passwordIsEmpty = passwordField.textProperty().isEmpty();
BooleanBinding passwordsDiffer = passwordField.textProperty().isNotEqualTo(retypePasswordField.textProperty());
EasyBind.subscribe(vault, this::vaultDidChange);
okButton.disableProperty().bind(passwordIsEmpty.or(passwordsDiffer));
passwordStrength.bind(EasyBind.map(passwordField.textProperty(), strengthRater::computeRate));
@@ -105,9 +103,10 @@ public class InitializeController extends LocalizedFXMLViewController {
return getClass().getResource("/fxml/initialize.fxml");
}
private void vaultDidChange(Vault newVault) {
passwordField.clear();
retypePasswordField.clear();
void setVault(Vault vault) {
this.vault = Objects.requireNonNull(vault);
passwordField.swipe();
retypePasswordField.swipe();
// trigger "default" change to refresh key bindings:
okButton.setDefaultButton(false);
okButton.setDefaultButton(true);
@@ -121,11 +120,13 @@ public class InitializeController extends LocalizedFXMLViewController {
protected void initializeVault(ActionEvent event) {
final CharSequence passphrase = passwordField.getCharacters();
try {
vault.get().create(passphrase);
vault.create(passphrase);
listener.ifPresent(this::invokeListenerLater);
} catch (FileAlreadyExistsException ex) {
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
} catch (UncheckedIOException | IOException ex) {
} catch (DirectoryNotEmptyException ex) {
messageLabel.setText(localization.getString("initialize.messageLabel.notEmpty"));
} catch (IOException ex) {
LOG.error("I/O Exception", ex);
messageLabel.setText(localization.getString("initialize.messageLabel.initializationFailed"));
} finally {

View File

@@ -29,6 +29,7 @@ import org.cryptomator.ui.controls.DirectoryListCell;
import org.cryptomator.ui.model.UpgradeStrategies;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.model.VaultFactory;
import org.cryptomator.ui.model.Vaults;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.util.DialogBuilderUtil;
@@ -44,9 +45,6 @@ import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.geometry.Side;
@@ -81,7 +79,7 @@ public class MainController extends LocalizedFXMLViewController {
private final Lazy<SettingsController> settingsController;
private final Lazy<UpgradeStrategies> upgradeStrategies;
private final ObjectProperty<AbstractFXMLViewController> activeController = new SimpleObjectProperty<>();
private final ObservableList<Vault> vaults;
private final Vaults vaults;
private final ObjectProperty<Vault> selectedVault = new SimpleObjectProperty<>();
private final BooleanExpression isSelectedVaultUnlocked = BooleanBinding.booleanExpression(EasyBind.select(selectedVault).selectObject(Vault::unlockedProperty).orElse(false));
private final BooleanExpression isSelectedVaultValid = BooleanBinding.booleanExpression(EasyBind.monadic(selectedVault).map(Vault::isValidVaultDirectory).orElse(false));
@@ -92,7 +90,8 @@ public class MainController extends LocalizedFXMLViewController {
@Inject
public MainController(@Named("mainWindow") Stage mainWindow, Localization localization, Settings settings, VaultFactory vaultFactoy, Lazy<WelcomeController> welcomeController,
Lazy<InitializeController> initializeController, Lazy<NotFoundController> notFoundController, Lazy<UpgradeController> upgradeController, Lazy<UnlockController> unlockController,
Provider<UnlockedController> unlockedControllerProvider, Lazy<ChangePasswordController> changePasswordController, Lazy<SettingsController> settingsController, Lazy<UpgradeStrategies> upgradeStrategies) {
Provider<UnlockedController> unlockedControllerProvider, Lazy<ChangePasswordController> changePasswordController, Lazy<SettingsController> settingsController, Lazy<UpgradeStrategies> upgradeStrategies,
Vaults vaults) {
super(localization);
this.mainWindow = mainWindow;
this.vaultFactoy = vaultFactoy;
@@ -105,10 +104,7 @@ public class MainController extends LocalizedFXMLViewController {
this.changePasswordController = changePasswordController;
this.settingsController = settingsController;
this.upgradeStrategies = upgradeStrategies;
this.vaults = FXCollections.observableList(settings.getDirectories());
this.vaults.addListener((Change<? extends Vault> c) -> {
settings.save();
});
this.vaults = vaults;
// derived bindings:
this.isShowingSettings = activeController.isEqualTo(settingsController.get());
@@ -319,7 +315,8 @@ public class MainController extends LocalizedFXMLViewController {
private void showInitializeView() {
final InitializeController ctrl = initializeController.get();
ctrl.vault.bind(selectedVault);
ctrl.loadFxml();
ctrl.setVault(selectedVault.get());
ctrl.setListener(this::didInitialize);
activeController.set(ctrl);
}
@@ -330,7 +327,8 @@ public class MainController extends LocalizedFXMLViewController {
private void showUpgradeView() {
final UpgradeController ctrl = upgradeController.get();
ctrl.vault.bind(selectedVault);
ctrl.loadFxml();
ctrl.setVault(selectedVault.get());
ctrl.setListener(this::didUpgrade);
activeController.set(ctrl);
}
@@ -341,7 +339,8 @@ public class MainController extends LocalizedFXMLViewController {
private void showUnlockView() {
final UnlockController ctrl = unlockController.get();
ctrl.vault.bind(selectedVault);
ctrl.loadFxml();
ctrl.setVault(selectedVault.get());
ctrl.setListener(this::didUnlock);
activeController.set(ctrl);
}
@@ -357,6 +356,7 @@ public class MainController extends LocalizedFXMLViewController {
final UnlockedController ctrl = unlockedVaults.computeIfAbsent(vault, k -> {
return unlockedControllerProvider.get();
});
ctrl.loadFxml();
ctrl.setVault(vault);
ctrl.setListener(this::didLock);
activeController.set(ctrl);
@@ -372,7 +372,8 @@ public class MainController extends LocalizedFXMLViewController {
private void showChangePasswordView() {
final ChangePasswordController ctrl = changePasswordController.get();
ctrl.vault.bind(selectedVault);
ctrl.loadFxml();
ctrl.setVault(selectedVault.get());
ctrl.setListener(this::didChangePassword);
activeController.set(ctrl);
}

View File

@@ -9,7 +9,9 @@
package org.cryptomator.ui.controllers;
import java.net.URL;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Objects;
import java.util.Optional;
import javax.inject.Inject;
@@ -22,25 +24,24 @@ import org.cryptomator.frontend.CommandFailedException;
import org.cryptomator.frontend.FrontendCreationFailedException;
import org.cryptomator.frontend.FrontendFactory;
import org.cryptomator.frontend.webdav.mount.WindowsDriveLetters;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.util.AsyncTaskService;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import dagger.Lazy;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
@@ -61,22 +62,34 @@ public class UnlockController extends LocalizedFXMLViewController {
private final Settings settings;
private final WindowsDriveLetters driveLetters;
private final ChangeListener<Character> driveLetterChangeListener = this::winDriveLetterDidChange;
final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
private final Optional<KeychainAccess> keychainAccess;
private Vault vault;
private Optional<UnlockListener> listener = Optional.empty();
@Inject
public UnlockController(Application app, Localization localization, AsyncTaskService asyncTaskService, Lazy<FrontendFactory> frontendFactory, Settings settings, WindowsDriveLetters driveLetters) {
public UnlockController(Application app, Localization localization, AsyncTaskService asyncTaskService, Lazy<FrontendFactory> frontendFactory, Settings settings, WindowsDriveLetters driveLetters,
Optional<KeychainAccess> keychainAccess) {
super(localization);
this.app = app;
this.asyncTaskService = asyncTaskService;
this.frontendFactory = frontendFactory;
this.settings = settings;
this.driveLetters = driveLetters;
this.keychainAccess = keychainAccess;
}
@FXML
private SecPasswordField passwordField;
@FXML
private Button advancedOptionsButton;
@FXML
private Button unlockButton;
@FXML
private CheckBox savePassword;
@FXML
private TextField mountName;
@@ -86,12 +99,6 @@ public class UnlockController extends LocalizedFXMLViewController {
@FXML
private ChoiceBox<Character> winDriveLetter;
@FXML
private Button advancedOptionsButton;
@FXML
private Button unlockButton;
@FXML
private ProgressIndicator progressIndicator;
@@ -107,8 +114,10 @@ public class UnlockController extends LocalizedFXMLViewController {
@Override
public void initialize() {
advancedOptions.managedProperty().bind(advancedOptions.visibleProperty());
unlockButton.disableProperty().bind(passwordField.textProperty().isEmpty());
mountName.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents);
mountName.textProperty().addListener(this::mountNameDidChange);
savePassword.setDisable(!keychainAccess.isPresent());
if (SystemUtils.IS_OS_WINDOWS) {
winDriveLetter.setConverter(new WinDriveLetterLabelConverter());
} else {
@@ -117,9 +126,6 @@ public class UnlockController extends LocalizedFXMLViewController {
winDriveLetter.setVisible(false);
winDriveLetter.setManaged(false);
}
unlockButton.disableProperty().bind(passwordField.textProperty().isEmpty());
EasyBind.subscribe(vault, this::vaultDidChange);
}
@Override
@@ -127,11 +133,15 @@ public class UnlockController extends LocalizedFXMLViewController {
return getClass().getResource("/fxml/unlock.fxml");
}
private void vaultDidChange(Vault newVault) {
if (newVault == null) {
void setVault(Vault vault) {
// trigger "default" change to refresh key bindings:
unlockButton.setDefaultButton(false);
unlockButton.setDefaultButton(true);
if (vault.equals(this.vault)) {
return;
}
passwordField.clear();
this.vault = Objects.requireNonNull(vault);
passwordField.swipe();
advancedOptions.setVisible(false);
advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.show"));
progressIndicator.setVisible(false);
@@ -145,13 +155,21 @@ public class UnlockController extends LocalizedFXMLViewController {
}
downloadsPageLink.setVisible(false);
messageText.setText(null);
mountName.setText(newVault.getMountName());
mountName.setText(vault.getMountName());
if (SystemUtils.IS_OS_WINDOWS) {
chooseSelectedDriveLetter();
}
// trigger "default" change to refresh key bindings:
unlockButton.setDefaultButton(false);
unlockButton.setDefaultButton(true);
savePassword.setSelected(false);
// auto-fill pw from keychain:
if (keychainAccess.isPresent()) {
char[] storedPw = keychainAccess.get().loadPassphrase(vault.getId());
if (storedPw != null) {
savePassword.setSelected(true);
passwordField.setText(new String(storedPw));
passwordField.selectRange(storedPw.length, storedPw.length);
Arrays.fill(storedPw, ' ');
}
}
}
// ****************************************
@@ -160,7 +178,7 @@ public class UnlockController extends LocalizedFXMLViewController {
@FXML
public void didClickDownloadsLink(ActionEvent event) {
app.getHostServices().showDocument("https://cryptomator.org/downloads/");
app.getHostServices().showDocument("https://cryptomator.org/downloads/#allVersions");
}
// ****************************************
@@ -188,14 +206,11 @@ public class UnlockController extends LocalizedFXMLViewController {
}
private void mountNameDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
if (vault.get() == null) {
return;
}
// newValue is guaranteed to be a-z0-9_, see #filterAlphanumericKeyEvents
if (newValue.isEmpty()) {
mountName.setText(vault.get().getMountName());
mountName.setText(vault.getMountName());
} else {
vault.get().setMountName(newValue);
vault.setMountName(newValue);
}
}
@@ -242,20 +257,17 @@ public class UnlockController extends LocalizedFXMLViewController {
}
private void winDriveLetterDidChange(ObservableValue<? extends Character> property, Character oldValue, Character newValue) {
if (vault.get() == null) {
return;
}
vault.get().setWinDriveLetter(newValue);
vault.setWinDriveLetter(newValue);
settings.save();
}
private void chooseSelectedDriveLetter() {
assert SystemUtils.IS_OS_WINDOWS;
// if the vault prefers a drive letter, that is currently occupied, this is our last chance to reset this:
if (driveLetters.getOccupiedDriveLetters().contains(vault.get().getWinDriveLetter())) {
vault.get().setWinDriveLetter(null);
if (driveLetters.getOccupiedDriveLetters().contains(vault.getWinDriveLetter())) {
vault.setWinDriveLetter(null);
}
final Character letter = vault.get().getWinDriveLetter();
final Character letter = vault.getWinDriveLetter();
if (letter == null) {
// first option is known to be 'auto-assign' due to #WinDriveLetterComparator.
this.winDriveLetter.getSelectionModel().selectFirst();
@@ -275,10 +287,10 @@ public class UnlockController extends LocalizedFXMLViewController {
progressIndicator.setVisible(true);
downloadsPageLink.setVisible(false);
CharSequence password = passwordField.getCharacters();
asyncTaskService.asyncTaskOf(() -> this.unlock(vault.get(), password)).run();
asyncTaskService.asyncTaskOf(() -> this.unlock(password)).run();
}
private void unlock(Vault vault, CharSequence password) {
private void unlock(CharSequence password) {
try {
vault.activateFrontend(frontendFactory.get(), settings, password);
vault.reveal();
@@ -286,18 +298,28 @@ public class UnlockController extends LocalizedFXMLViewController {
messageText.setText(null);
listener.ifPresent(lstnr -> lstnr.didUnlock(vault));
});
if (keychainAccess.isPresent() && savePassword.isSelected()) {
keychainAccess.get().storePassphrase(vault.getId(), password);
} else {
passwordField.swipe();
}
} catch (InvalidPassphraseException e) {
Platform.runLater(() -> {
messageText.setText(localization.getString("unlock.errorMessage.wrongPassword"));
passwordField.selectAll();
passwordField.requestFocus();
});
} catch (UnsupportedVaultFormatException e) {
Platform.runLater(() -> {
downloadsPageLink.setVisible(true);
if (e.isVaultOlderThanSoftware()) {
// whitespace after localized text used as separator between text and hyperlink
messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
downloadsPageLink.setVisible(true);
} else if (e.isSoftwareOlderThanVault()) {
messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
downloadsPageLink.setVisible(true);
} else if (e.getDetectedVersion() == Integer.MAX_VALUE) {
messageText.setText(localization.getString("unlock.errorMessage.unauthenticVersionMac"));
}
});
} catch (FrontendCreationFailedException | CommandFailedException e) {
@@ -307,7 +329,6 @@ public class UnlockController extends LocalizedFXMLViewController {
});
} finally {
Platform.runLater(() -> {
passwordField.swipe();
mountName.setDisable(false);
advancedOptionsButton.setDisable(false);
progressIndicator.setVisible(false);

View File

@@ -15,21 +15,23 @@ import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.util.AsyncTaskService;
import org.fxmisc.easybind.EasyBind;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
public class UpgradeController extends LocalizedFXMLViewController {
final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
final ObjectProperty<Optional<UpgradeStrategy>> strategy = new SimpleObjectProperty<>();
private final ObjectProperty<Optional<UpgradeStrategy>> strategy = new SimpleObjectProperty<>();
private final UpgradeStrategies strategies;
private final AsyncTaskService asyncTaskService;
private Optional<UpgradeListener> listener = Optional.empty();
private Vault vault;
@Inject
public UpgradeController(Localization localization, UpgradeStrategies strategies, AsyncTaskService asyncTaskService) {
@@ -44,6 +46,9 @@ public class UpgradeController extends LocalizedFXMLViewController {
@FXML
private SecPasswordField passwordField;
@FXML
private CheckBox confirmationCheckbox;
@FXML
private Button upgradeButton;
@@ -59,9 +64,9 @@ public class UpgradeController extends LocalizedFXMLViewController {
return instruction.map(this::upgradeNotification).orElse("");
}).orElse(""));
upgradeButton.disableProperty().bind(passwordField.textProperty().isEmpty().or(passwordField.disabledProperty()));
EasyBind.subscribe(vault, this::vaultDidChange);
BooleanExpression passwordProvided = passwordField.textProperty().isNotEmpty().and(passwordField.disabledProperty().not());
BooleanExpression syncFinished = confirmationCheckbox.selectedProperty();
upgradeButton.disableProperty().bind(passwordProvided.not().or(syncFinished.not()));
}
@Override
@@ -69,9 +74,10 @@ public class UpgradeController extends LocalizedFXMLViewController {
return getClass().getResource("/fxml/upgrade.fxml");
}
private void vaultDidChange(Vault newVault) {
void setVault(Vault vault) {
this.vault = Objects.requireNonNull(vault);
errorLabel.setText(null);
strategy.set(strategies.getUpgradeStrategy(newVault));
strategy.set(strategies.getUpgradeStrategy(vault));
// trigger "default" change to refresh key bindings:
upgradeButton.setDefaultButton(false);
upgradeButton.setDefaultButton(true);
@@ -82,7 +88,7 @@ public class UpgradeController extends LocalizedFXMLViewController {
// ****************************************
private String upgradeNotification(UpgradeStrategy instruction) {
return instruction.getNotification(vault.get());
return instruction.getNotification(vault);
}
// ****************************************
@@ -95,15 +101,14 @@ public class UpgradeController extends LocalizedFXMLViewController {
}
private void upgrade(UpgradeStrategy instruction) {
Vault v = Objects.requireNonNull(vault.getValue());
passwordField.setDisable(true);
progressIndicator.setVisible(true);
asyncTaskService //
.asyncTaskOf(() -> {
if (!instruction.isApplicable(v)) {
throw new IllegalStateException("No ugprade needed for " + v.path().getValue());
if (!instruction.isApplicable(vault)) {
throw new IllegalStateException("No ugprade needed for " + vault.path().getValue());
}
instruction.upgrade(v, passwordField.getCharacters());
instruction.upgrade(vault, passwordField.getCharacters());
}) //
.onSuccess(this::showNextUpgrade) //
.onError(UpgradeFailedException.class, e -> {
@@ -118,7 +123,7 @@ public class UpgradeController extends LocalizedFXMLViewController {
private void showNextUpgrade() {
errorLabel.setText(null);
Optional<UpgradeStrategy> nextStrategy = strategies.getUpgradeStrategy(vault.getValue());
Optional<UpgradeStrategy> nextStrategy = strategies.getUpgradeStrategy(vault);
if (nextStrategy.isPresent()) {
strategy.set(nextStrategy);
} else {

View File

@@ -19,6 +19,7 @@ import java.util.regex.Pattern;
import org.apache.commons.lang3.SystemUtils;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender;
import org.apache.logging.log4j.core.appender.FileManager;
import org.apache.logging.log4j.core.config.plugins.Plugin;
@@ -26,10 +27,11 @@ import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.apache.logging.log4j.core.util.Booleans;
import org.apache.logging.log4j.util.Strings;
/**
* A preconfigured FileAppender only relying on a configurable system property, e.g. <code>-DlogPath=/var/log/cryptomator.log</code>.<br/>
* A preconfigured FileAppender only relying on a configurable system property, e.g. <code>-Dcryptomator.logPath=/var/log/cryptomator.log</code>.<br/>
* Other than the normal {@link org.apache.logging.log4j.core.appender.FileAppender} paths can be resolved relative to the users home directory.
*/
@Plugin(name = "ConfigurableFile", category = "Core", elementType = "appender", printObject = true)
@@ -37,7 +39,6 @@ public class ConfigurableFileAppender extends AbstractOutputStreamAppender<FileM
private static final long serialVersionUID = -6548221568069606389L;
private static final int DEFAULT_BUFFER_SIZE = 8192;
private static final String DEFAULT_FILE_NAME = "cryptomator.log";
private static final Pattern DRIVE_LETTER_WITH_PRECEEDING_SLASH = Pattern.compile("^/[A-Z]:", Pattern.CASE_INSENSITIVE);
protected ConfigurableFileAppender(String name, Layout<? extends Serializable> layout, Filter filter, FileManager manager) {
@@ -46,9 +47,8 @@ public class ConfigurableFileAppender extends AbstractOutputStreamAppender<FileM
}
@PluginFactory
public static ConfigurableFileAppender createAppender(@PluginAttribute("name") final String name, @PluginAttribute("pathPropertyName") final String pathPropertyName,
public static AbstractAppender createAppender(@PluginAttribute("name") final String name, @PluginAttribute("pathPropertyName") final String pathPropertyName, @PluginAttribute("append") final String append,
@PluginElement("Layout") Layout<? extends Serializable> layout) {
if (name == null) {
LOGGER.error("No name provided for HomeDirectoryAwareFileAppender");
return null;
@@ -59,41 +59,16 @@ public class ConfigurableFileAppender extends AbstractOutputStreamAppender<FileM
return null;
}
String fileName = System.getProperty(pathPropertyName);
final String fileName = System.getProperty(pathPropertyName);
if (Strings.isEmpty(fileName)) {
fileName = DEFAULT_FILE_NAME;
LOGGER.warn("No log file location provided in system property \"" + pathPropertyName + "\"");
return null;
}
final Path filePath;
if (fileName.startsWith("~/")) {
// home-dir-relative Path:
final Path userHome = FileSystems.getDefault().getPath(SystemUtils.USER_HOME);
filePath = userHome.resolve(fileName.substring(2));
} else if (fileName.startsWith("/")) {
// absolute Path:
filePath = FileSystems.getDefault().getPath(fileName);
} else if (SystemUtils.IS_OS_WINDOWS && fileName.startsWith("%appdata%/")) {
final String appdata = System.getenv("APPDATA");
final Path appdataPath = appdata != null ? FileSystems.getDefault().getPath(appdata) : FileSystems.getDefault().getPath(SystemUtils.USER_HOME);
filePath = appdataPath.resolve(fileName.substring(10));
} else {
// relative Path:
try {
String jarFileLocation = ConfigurableFileAppender.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
if (SystemUtils.IS_OS_WINDOWS && DRIVE_LETTER_WITH_PRECEEDING_SLASH.matcher(jarFileLocation).find()) {
// on windows we need to remove a preceeding slash from "/C:/foo/bar":
jarFileLocation = jarFileLocation.substring(1);
}
final Path workingDir = FileSystems.getDefault().getPath(jarFileLocation).getParent();
filePath = workingDir.resolve(fileName);
} catch (URISyntaxException e) {
LOGGER.error("Unable to resolve working directory ", e);
return null;
}
}
if (layout == null) {
layout = PatternLayout.createDefaultLayout();
final Path filePath = parsePath(fileName);
if (filePath == null) {
LOGGER.warn("Invalid path \"" + fileName + "\"");
return null;
}
if (!Files.exists(filePath.getParent())) {
@@ -105,8 +80,38 @@ public class ConfigurableFileAppender extends AbstractOutputStreamAppender<FileM
}
}
final FileManager manager = FileManager.getFileManager(filePath.toString(), false, false, true, null, layout, DEFAULT_BUFFER_SIZE);
final boolean shouldAppend = Booleans.parseBoolean(append, true);
if (layout == null) {
layout = PatternLayout.createDefaultLayout();
}
final FileManager manager = FileManager.getFileManager(filePath.toString(), shouldAppend, false, true, null, layout, DEFAULT_BUFFER_SIZE);
return new ConfigurableFileAppender(name, layout, null, manager);
}
private static Path parsePath(String path) {
if (path.startsWith("~/")) {
// home-dir-relative Path:
final Path userHome = FileSystems.getDefault().getPath(SystemUtils.USER_HOME);
return userHome.resolve(path.substring(2));
} else if (path.startsWith("/")) {
// absolute Path:
return FileSystems.getDefault().getPath(path);
} else {
// relative Path:
try {
String jarFileLocation = ConfigurableFileAppender.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
if (SystemUtils.IS_OS_WINDOWS && DRIVE_LETTER_WITH_PRECEEDING_SLASH.matcher(jarFileLocation).find()) {
// on windows we need to remove a preceeding slash from "/C:/foo/bar":
jarFileLocation = jarFileLocation.substring(1);
}
final Path workingDir = FileSystems.getDefault().getPath(jarFileLocation).getParent();
return workingDir.resolve(path);
} catch (URISyntaxException e) {
LOGGER.error("Unable to resolve working directory ", e);
return null;
}
}
}
}

View File

@@ -14,8 +14,8 @@ public class UpgradeStrategies {
private final Collection<UpgradeStrategy> strategies;
@Inject
public UpgradeStrategies(UpgradeVersion3DropBundleExtension upgrader1, UpgradeVersion3to4 upgrader2) {
strategies = Collections.unmodifiableList(Arrays.asList(upgrader1, upgrader2));
public UpgradeStrategies(UpgradeVersion3DropBundleExtension upgrader1, UpgradeVersion3to4 upgrader2, UpgradeVersion4to5 upgrader3) {
strategies = Collections.unmodifiableList(Arrays.asList(upgrader1, upgrader2, upgrader3));
}
public Optional<UpgradeStrategy> getUpgradeStrategy(Vault vault) {

View File

@@ -6,11 +6,11 @@ import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import javax.inject.Provider;
import org.cryptomator.crypto.engine.Cryptor;
import org.cryptomator.crypto.engine.InvalidPassphraseException;
import org.cryptomator.crypto.engine.UnsupportedVaultFormatException;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.KeyFile;
import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
import org.cryptomator.filesystem.crypto.Constants;
import org.cryptomator.ui.settings.Localization;
import org.slf4j.Logger;
@@ -20,12 +20,16 @@ public abstract class UpgradeStrategy {
private static final Logger LOG = LoggerFactory.getLogger(UpgradeStrategy.class);
protected final Provider<Cryptor> cryptorProvider;
protected final CryptorProvider cryptorProvider;
protected final Localization localization;
protected final int vaultVersionBeforeUpgrade;
protected final int vaultVersionAfterUpgrade;
UpgradeStrategy(Provider<Cryptor> cryptorProvider, Localization localization) {
UpgradeStrategy(CryptorProvider cryptorProvider, Localization localization, int vaultVersionBeforeUpgrade, int vaultVersionAfterUpgrade) {
this.cryptorProvider = cryptorProvider;
this.localization = localization;
this.vaultVersionBeforeUpgrade = vaultVersionBeforeUpgrade;
this.vaultVersionAfterUpgrade = vaultVersionAfterUpgrade;
}
/**
@@ -37,27 +41,37 @@ public abstract class UpgradeStrategy {
* Upgrades a vault. Might take a moment, should be run in a background thread.
*/
public void upgrade(Vault vault, CharSequence passphrase) throws UpgradeFailedException {
final Cryptor cryptor = cryptorProvider.get();
Cryptor cryptor = null;
try {
final Path masterkeyFile = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME);
final byte[] masterkeyFileContents = Files.readAllBytes(masterkeyFile);
cryptor.readKeysFromMasterkeyFile(masterkeyFileContents, passphrase);
cryptor = cryptorProvider.createFromKeyFile(KeyFile.parse(masterkeyFileContents), passphrase, vaultVersionBeforeUpgrade);
// create backup, as soon as we know the password was correct:
final Path masterkeyBackupFile = vault.path().getValue().resolve(Constants.MASTERKEY_BACKUP_FILENAME);
Files.copy(masterkeyFile, masterkeyBackupFile, StandardCopyOption.REPLACE_EXISTING);
// do stuff:
upgrade(vault, cryptor);
// write updated masterkey file:
final byte[] upgradedMasterkeyFileContents = cryptor.writeKeysToMasterkeyFile(passphrase);
final Path masterkeyFileAfterUpgrading = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME); // path may have changed
Files.write(masterkeyFileAfterUpgrading, upgradedMasterkeyFileContents, StandardOpenOption.TRUNCATE_EXISTING);
final byte[] upgradedMasterkeyFileContents = cryptor.writeKeysToMasterkeyFile(passphrase, vaultVersionAfterUpgrade).serialize();
final Path masterkeyFileAfterUpgrade = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME); // path may have changed
Files.write(masterkeyFileAfterUpgrade, upgradedMasterkeyFileContents, StandardOpenOption.TRUNCATE_EXISTING);
} catch (InvalidPassphraseException e) {
throw new UpgradeFailedException(localization.getString("unlock.errorMessage.wrongPassword"));
} catch (IOException | UnsupportedVaultFormatException e) {
} catch (UnsupportedVaultFormatException e) {
if (e.getDetectedVersion() == Integer.MAX_VALUE) {
LOG.warn("Version MAC authentication error in vault {}", vault.path().get());
throw new UpgradeFailedException(localization.getString("unlock.errorMessage.unauthenticVersionMac"));
} else {
LOG.warn("Upgrade failed.", e);
throw new UpgradeFailedException("Upgrade failed. Details in log message.");
}
} catch (IOException e) {
LOG.warn("Upgrade failed.", e);
throw new UpgradeFailedException("Upgrade failed. Details in log message.");
} finally {
cryptor.destroy();
if (cryptor != null) {
cryptor.destroy();
}
}
}
@@ -68,7 +82,21 @@ public abstract class UpgradeStrategy {
*
* @return <code>true</code> if and only if the vault can be migrated to a newer version without the risk of data losses.
*/
public abstract boolean isApplicable(Vault vault);
public boolean isApplicable(Vault vault) {
final Path masterkeyFile = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME);
try {
if (Files.isRegularFile(masterkeyFile)) {
byte[] masterkeyFileContents = Files.readAllBytes(masterkeyFile);
return KeyFile.parse(masterkeyFileContents).getVersion() == vaultVersionBeforeUpgrade;
} else {
LOG.warn("Not a file: {}", masterkeyFile);
return false;
}
} catch (IOException e) {
LOG.warn("Could not determine, whether upgrade is applicable.", e);
return false;
}
}
/**
* Thrown when data migration failed.

View File

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

View File

@@ -9,19 +9,20 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.BaseNCodec;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.engine.Cryptor;
import org.cryptomator.filesystem.crypto.Constants;
import org.cryptomator.cryptolib.api.CryptoLibVersion;
import org.cryptomator.cryptolib.api.CryptoLibVersion.Version;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.common.MessageDigestSupplier;
import org.cryptomator.ui.settings.Localization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -40,17 +41,12 @@ class UpgradeVersion3to4 extends UpgradeStrategy {
private static final String OLD_FOLDER_SUFFIX = "_";
private static final String NEW_FOLDER_PREFIX = "0";
private final MessageDigest sha1;
private final MessageDigest sha1 = MessageDigestSupplier.SHA1.get();
private final BaseNCodec base32 = new Base32();
@Inject
public UpgradeVersion3to4(Provider<Cryptor> cryptorProvider, Localization localization) {
super(cryptorProvider, localization);
try {
sha1 = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError("SHA-1 exists in every JVM");
}
public UpgradeVersion3to4(@CryptoLibVersion(Version.ONE) CryptorProvider version1CryptorProvider, Localization localization) {
super(version1CryptorProvider, localization, 3, 4);
}
@Override
@@ -144,21 +140,4 @@ class UpgradeVersion3to4 extends UpgradeStrategy {
}
}
@Override
public boolean isApplicable(Vault vault) {
final Path masterkeyFile = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME);
try {
if (Files.isRegularFile(masterkeyFile)) {
final String keyContents = new String(Files.readAllBytes(masterkeyFile), UTF_8);
return keyContents.contains("\"version\":3") || keyContents.contains("\"version\": 3");
} else {
LOG.warn("Not a file: {}", masterkeyFile);
return false;
}
} catch (IOException e) {
LOG.warn("Could not determine, whether upgrade is applicable.", e);
return false;
}
}
}

View File

@@ -0,0 +1,130 @@
package org.cryptomator.ui.model;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.cryptomator.cryptolib.Cryptors;
import org.cryptomator.cryptolib.api.CryptoLibVersion;
import org.cryptomator.cryptolib.api.CryptoLibVersion.Version;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.FileHeader;
import org.cryptomator.ui.settings.Localization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Contains the collective knowledge of all creatures who were alive during the development of vault format 3.
* This class uses no external classes from the crypto or shortening layer by purpose, so we don't need legacy code inside these.
*/
@Singleton
class UpgradeVersion4to5 extends UpgradeStrategy {
private static final Logger LOG = LoggerFactory.getLogger(UpgradeVersion4to5.class);
private static final Pattern BASE32_PATTERN = Pattern.compile("^([A-Z2-7]{8})*[A-Z2-7=]{8}");
@Inject
public UpgradeVersion4to5(@CryptoLibVersion(Version.ONE) CryptorProvider version1CryptorProvider, Localization localization) {
super(version1CryptorProvider, localization, 4, 5);
}
@Override
public String getNotification(Vault vault) {
return localization.getString("upgrade.version3to4.msg");
}
@Override
protected void upgrade(Vault vault, Cryptor cryptor) throws UpgradeFailedException {
Path dataDir = vault.path().get().resolve("d");
if (!Files.isDirectory(dataDir)) {
return; // empty vault. no migration needed.
}
try {
Files.walkFileTree(dataDir, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (BASE32_PATTERN.matcher(file.getFileName().toString()).find() && attrs.size() > cryptor.fileHeaderCryptor().headerSize()) {
migrate(file, attrs, cryptor);
}
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
LOG.error("Migration failed.", e);
throw new UpgradeFailedException(localization.getString("upgrade.version3to4.err.io"));
}
LOG.info("Migration finished.");
}
private void migrate(Path file, BasicFileAttributes attrs, Cryptor cryptor) throws IOException {
try (FileChannel ch = FileChannel.open(file, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
// read header:
ByteBuffer headerBuf = ByteBuffer.allocate(cryptor.fileHeaderCryptor().headerSize());
ch.read(headerBuf);
headerBuf.flip();
FileHeader header = cryptor.fileHeaderCryptor().decryptHeader(headerBuf);
long cleartextSize = header.getFilesize();
if (cleartextSize < 0) {
LOG.info("Skipping already migrated file {}.", file);
return;
} else if (cleartextSize > attrs.size()) {
LOG.warn("Skipping file {} with invalid file size {}/{}", file, cleartextSize, attrs.size());
return;
}
int headerSize = cryptor.fileHeaderCryptor().headerSize();
int ciphertextChunkSize = cryptor.fileContentCryptor().ciphertextChunkSize();
int cleartextChunkSize = cryptor.fileContentCryptor().cleartextChunkSize();
long newCiphertextSize = Cryptors.ciphertextSize(cleartextSize, cryptor);
long newEOF = headerSize + newCiphertextSize;
long newFullChunks = newCiphertextSize / ciphertextChunkSize; // int-truncation
long newAdditionalCiphertextBytes = newCiphertextSize % ciphertextChunkSize;
if (newAdditionalCiphertextBytes == 0) {
// (new) last block is already correct. just truncate:
LOG.info("Migrating {} of cleartext size {}: Truncating to new ciphertext size: {}", file, cleartextSize, newEOF);
ch.truncate(newEOF);
} else {
// last block may contain padding and needs to be re-encrypted:
long lastChunkIdx = newFullChunks;
LOG.info("Migrating {} of cleartext size {}: Re-encrypting chunk {}. New ciphertext size: {}", file, cleartextSize, lastChunkIdx, newEOF);
long beginOfLastChunk = headerSize + lastChunkIdx * ciphertextChunkSize;
assert beginOfLastChunk < newEOF;
int lastCleartextChunkLength = (int) (cleartextSize % cleartextChunkSize);
assert lastCleartextChunkLength < cleartextChunkSize;
assert lastCleartextChunkLength > 0;
ch.position(beginOfLastChunk);
ByteBuffer lastCiphertextChunk = ByteBuffer.allocate(ciphertextChunkSize);
int read = ch.read(lastCiphertextChunk);
if (read != -1) {
lastCiphertextChunk.flip();
ByteBuffer lastCleartextChunk = cryptor.fileContentCryptor().decryptChunk(lastCiphertextChunk, lastChunkIdx, header, true);
lastCleartextChunk.position(0).limit(lastCleartextChunkLength);
assert lastCleartextChunk.remaining() == lastCleartextChunkLength;
ByteBuffer newLastChunkCiphertext = cryptor.fileContentCryptor().encryptChunk(lastCleartextChunk, lastChunkIdx, header);
ch.truncate(beginOfLastChunk);
ch.write(newLastChunkCiphertext);
} else {
LOG.error("Reached EOF at position {}/{}", beginOfLastChunk, newEOF);
return; // must exit method before changing header!
}
}
header.setFilesize(-1l);
ByteBuffer newHeaderBuf = cryptor.fileHeaderCryptor().encryptHeader(header);
ch.position(0);
ch.write(newHeaderBuf);
}
}
}

View File

@@ -12,6 +12,7 @@ import static org.apache.commons.lang3.StringUtils.stripStart;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -30,6 +31,7 @@ import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.LazyInitializer;
import org.cryptomator.common.Optionals;
import org.cryptomator.crypto.engine.InvalidPassphraseException;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.charsets.NormalizedNameFileSystem;
import org.cryptomator.filesystem.crypto.CryptoFileSystemDelegate;
@@ -109,8 +111,10 @@ public class Vault implements CryptoFileSystemDelegate {
public void create(CharSequence passphrase) throws IOException {
try {
FileSystem fs = getNioFileSystem();
if (fs.children().count() > 0) {
throw new FileAlreadyExistsException(null, null, "Vault location not empty.");
if (fs.files().map(File::name).filter(s -> s.endsWith(VAULT_FILE_EXTENSION)).count() > 0) {
throw new FileAlreadyExistsException("masterkey.cryptomator", null, "Vault location not empty.");
} else if (fs.folders().count() > 0) {
throw new DirectoryNotEmptyException(fs.toString());
}
cryptoFileSystemFactory.initializeNew(fs, passphrase);
} catch (UncheckedIOException e) {
@@ -164,7 +168,7 @@ public class Vault implements CryptoFileSystemDelegate {
);
}
public void reveal() throws CommandFailedException {
public synchronized void reveal() throws CommandFailedException {
Optionals.ifPresent(filesystemFrontend.get(), Frontend::reveal);
}
@@ -186,7 +190,7 @@ public class Vault implements CryptoFileSystemDelegate {
// Getter/Setter
// *******************************************************************************/
public String getWebDavUrl() {
public synchronized String getWebDavUrl() {
return filesystemFrontend.get().map(Frontend::getWebDavUrl).orElseThrow(IllegalStateException::new);
}

View File

@@ -0,0 +1,177 @@
package org.cryptomator.ui.model;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.cryptomator.ui.settings.Settings;
import javafx.beans.InvalidationListener;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
@Singleton
public class Vaults implements ObservableList<Vault> {
private final ObservableList<Vault> delegate;
@Inject
public Vaults(Settings settings) {
this.delegate = FXCollections.observableList(settings.getDirectories());
addListener((Change<? extends Vault> change) -> settings.save());
}
public void addListener(ListChangeListener<? super Vault> listener) {
delegate.addListener(listener);
}
public void removeListener(ListChangeListener<? super Vault> listener) {
delegate.removeListener(listener);
}
public void addListener(InvalidationListener listener) {
delegate.addListener(listener);
}
public boolean addAll(Vault... elements) {
return delegate.addAll(elements);
}
public boolean setAll(Vault... elements) {
return delegate.setAll(elements);
}
public boolean setAll(Collection<? extends Vault> col) {
return delegate.setAll(col);
}
public boolean removeAll(Vault... elements) {
return delegate.removeAll(elements);
}
public void removeListener(InvalidationListener listener) {
delegate.removeListener(listener);
}
public boolean retainAll(Vault... elements) {
return delegate.retainAll(elements);
}
public void remove(int from, int to) {
delegate.remove(from, to);
}
public int size() {
return delegate.size();
}
public boolean isEmpty() {
return delegate.isEmpty();
}
public boolean contains(Object o) {
return delegate.contains(o);
}
public Iterator<Vault> iterator() {
return delegate.iterator();
}
public Object[] toArray() {
return delegate.toArray();
}
public <T> T[] toArray(T[] a) {
return delegate.toArray(a);
}
public boolean add(Vault e) {
return delegate.add(e);
}
public boolean remove(Object o) {
return delegate.remove(o);
}
public boolean containsAll(Collection<?> c) {
return delegate.containsAll(c);
}
public boolean addAll(Collection<? extends Vault> c) {
return delegate.addAll(c);
}
public boolean addAll(int index, Collection<? extends Vault> c) {
return delegate.addAll(index, c);
}
public boolean removeAll(Collection<?> c) {
return delegate.removeAll(c);
}
public boolean retainAll(Collection<?> c) {
return delegate.retainAll(c);
}
public void clear() {
delegate.clear();
}
public Vault get(int index) {
return delegate.get(index);
}
public Vault set(int index, Vault element) {
return delegate.set(index, element);
}
public void add(int index, Vault element) {
delegate.add(index, element);
}
public Vault remove(int index) {
return delegate.remove(index);
}
public int indexOf(Object o) {
return delegate.indexOf(o);
}
public int lastIndexOf(Object o) {
return delegate.lastIndexOf(o);
}
public ListIterator<Vault> listIterator() {
return delegate.listIterator();
}
public ListIterator<Vault> listIterator(int index) {
return delegate.listIterator(index);
}
public List<Vault> subList(int fromIndex, int toIndex) {
return delegate.subList(fromIndex, toIndex);
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || getClass() != obj.getClass()) return false;
return internalEquals((Vaults)obj);
}
private boolean internalEquals(Vaults other) {
return delegate.equals(other.delegate);
}
public int hashCode() {
return delegate.hashCode();
}
}

View File

@@ -39,7 +39,7 @@ public class Localization extends ResourceBundle {
Objects.requireNonNull(in);
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
this.fallback = new PropertyResourceBundle(reader);
LOG.info("Loaded localization from {}", LOCALIZATION_FILE);
LOG.info("Loaded localization from bundle:{}", LOCALIZATION_FILE);
} catch (IOException e) {
throw new UncheckedIOException(e);
}

View File

@@ -17,6 +17,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
@@ -28,6 +29,7 @@ import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -38,23 +40,17 @@ import com.fasterxml.jackson.databind.ObjectMapper;
public class SettingsProvider implements Provider<Settings> {
private static final Logger LOG = LoggerFactory.getLogger(SettingsProvider.class);
private static final Path SETTINGS_DIR;
private static final String SETTINGS_FILE = "settings.json";
private static final Path DEFAULT_SETTINGS_PATH;
private static final long SAVE_DELAY_MS = 1000;
static {
final String appdata = System.getenv("APPDATA");
final FileSystem fs = FileSystems.getDefault();
if (SystemUtils.IS_OS_WINDOWS && appdata != null) {
SETTINGS_DIR = fs.getPath(appdata, "Cryptomator");
} else if (SystemUtils.IS_OS_WINDOWS && appdata == null) {
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".Cryptomator");
if (SystemUtils.IS_OS_WINDOWS) {
DEFAULT_SETTINGS_PATH = fs.getPath(SystemUtils.USER_HOME, "AppData/Roaming/Cryptomator/settings.json");
} else if (SystemUtils.IS_OS_MAC_OSX) {
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, "Library/Application Support/Cryptomator");
DEFAULT_SETTINGS_PATH = fs.getPath(SystemUtils.USER_HOME, "Library/Application Support/Cryptomator/settings.json");
} else {
// (os.contains("solaris") || os.contains("sunos") || os.contains("linux") || os.contains("unix"))
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".Cryptomator");
DEFAULT_SETTINGS_PATH = fs.getPath(SystemUtils.USER_HOME, ".Cryptomator/settings.json");
}
}
@@ -68,11 +64,15 @@ public class SettingsProvider implements Provider<Settings> {
}
private Path getSettingsPath() throws IOException {
String settingsPathProperty = System.getProperty("cryptomator.settingsPath");
if (settingsPathProperty == null) {
return SETTINGS_DIR.resolve(SETTINGS_FILE);
final String settingsPathProperty = System.getProperty("cryptomator.settingsPath");
return Optional.ofNullable(settingsPathProperty).filter(StringUtils::isNotBlank).map(this::replaceHomeDir).map(FileSystems.getDefault()::getPath).orElse(DEFAULT_SETTINGS_PATH);
}
private String replaceHomeDir(String path) {
if (path.startsWith("~/")) {
return SystemUtils.USER_HOME + path.substring(1);
} else {
return FileSystems.getDefault().getPath(settingsPathProperty);
return path;
}
}
@@ -96,7 +96,7 @@ public class SettingsProvider implements Provider<Settings> {
}
ScheduledFuture<?> saveCmd = saveScheduler.schedule(() -> {
this.save(settings);
} , SAVE_DELAY_MS, TimeUnit.MILLISECONDS);
}, SAVE_DELAY_MS, TimeUnit.MILLISECONDS);
ScheduledFuture<?> previousSaveCmd = scheduledSaveCmd.getAndSet(saveCmd);
if (previousSaveCmd != null) {
previousSaveCmd.cancel(false);

View File

@@ -305,17 +305,17 @@
****************************************************************************/
.check-box {
-fx-label-padding: 0 0 0 3;
-fx-label-padding: 0 0 0 6px;
-fx-text-fill: COLOR_TEXT;
}
.check-box > .box {
-fx-padding: 3;
-fx-padding: 3px;
-fx-background-color: COLOR_BORDER_DARK, #FFF;
-fx-background-insets: 0, 1;
}
.check-box > .box > .mark {
-fx-background-color: transparent;
-fx-padding: 4;
-fx-padding: 4px;
-fx-shape: "M-1,4, L-1,5.5 L3.5,8.5 L9,0 L9,-1 L7,-1 L3,6 L1,4 Z";
}
.check-box:selected > .box {

View File

@@ -386,7 +386,7 @@
******************************************************************************/
.check-box {
-fx-label-padding: 0 0 0 3px;
-fx-label-padding: 0 0 0 6px;
-fx-text-fill: COLOR_TEXT;
}
.check-box > .box {

View File

@@ -354,7 +354,7 @@
******************************************************************************/
.check-box {
-fx-label-padding: 0 0 0 3px;
-fx-label-padding: 0 0 0 6px;
-fx-text-fill: COLOR_TEXT;
}
.check-box > .box {

View File

@@ -68,12 +68,16 @@
</HBox>
<!-- Row 3.1 -->
<Label text="%unlock.label.mountName" GridPane.rowIndex="1" GridPane.columnIndex="0" cacheShape="true" cache="true" />
<TextField fx:id="mountName" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%unlock.label.savePassword" cacheShape="true" cache="true" />
<CheckBox GridPane.rowIndex="1" GridPane.columnIndex="1" fx:id="savePassword" cacheShape="true" cache="true" />
<!-- Row 3.2 -->
<Label fx:id="winDriveLetterLabel" text="%unlock.label.winDriveLetter" GridPane.rowIndex="2" GridPane.columnIndex="0" cacheShape="true" cache="true" />
<ChoiceBox fx:id="winDriveLetter" GridPane.rowIndex="2" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
<Label GridPane.rowIndex="2" GridPane.columnIndex="0" text="%unlock.label.mountName" cacheShape="true" cache="true" />
<TextField GridPane.rowIndex="2" GridPane.columnIndex="1" fx:id="mountName" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
<!-- Row 3.3 -->
<Label GridPane.rowIndex="3" GridPane.columnIndex="0" fx:id="winDriveLetterLabel" text="%unlock.label.winDriveLetter" cacheShape="true" cache="true" />
<ChoiceBox GridPane.rowIndex="3" GridPane.columnIndex="1" fx:id="winDriveLetter" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
</GridPane>
<!-- Row 4 -->

View File

@@ -17,6 +17,8 @@
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.CheckBox?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" xmlns:fx="http://javafx.com/fxml" cacheShape="true" cache="true">
<padding>
@@ -34,15 +36,17 @@
<Pane prefHeight="12.0" GridPane.rowIndex="1" GridPane.columnIndex="0" GridPane.columnSpan="2" cacheShape="true" cache="true">
</Pane>
<Label text="%unlock.label.password" cacheShape="true" cache="true" GridPane.rowIndex="2" GridPane.columnIndex="0" />
<SecPasswordField fx:id="passwordField" cacheShape="true" cache="true" GridPane.rowIndex="2" GridPane.columnIndex="1" />
<Label text="%unlock.label.password" GridPane.rowIndex="2" GridPane.columnIndex="0" cacheShape="true" cache="true" />
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="2" GridPane.columnIndex="1" cacheShape="true" cache="true" />
<CheckBox fx:id="confirmationCheckbox" text="%upgrade.confirmation.label" wrapText="true" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" cacheShape="true" cache="true" />
<HBox alignment="CENTER_RIGHT" GridPane.hgrow="ALWAYS" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" cacheShape="true" cache="true">
<HBox alignment="CENTER_RIGHT" GridPane.hgrow="ALWAYS" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" cacheShape="true" cache="true">
<Button fx:id="upgradeButton" text="%upgrade.button" prefWidth="150.0" onAction="#didClickUpgradeButton" cacheShape="true" cache="true" />
</HBox>
<ProgressIndicator progress="-1" fx:id="progressIndicator" visible="false" cacheShape="true" cache="true" cacheHint="SPEED" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" />
<ProgressIndicator progress="-1" fx:id="progressIndicator" visible="false" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="2" cacheShape="true" cache="true" cacheHint="SPEED" />
<Label fx:id="errorLabel" wrapText="true" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="2" cacheShape="true" cache="true" />
<Label fx:id="errorLabel" wrapText="true" GridPane.rowIndex="6" GridPane.columnIndex="0" GridPane.columnSpan="2" cacheShape="true" cache="true" />
</children>
</GridPane>

View File

@@ -82,4 +82,10 @@ main.directoryList.remove.confirmation.title = Tresor entfernen
main.directoryList.remove.confirmation.header = Möchten Sie diesen Tresor wirklich entfernen?
main.directoryList.remove.confirmation.content = Dieser Tresor wird nur aus der Liste entfernt. Um den Tresor unwiderruflich zu löschen, entfernen Sie bitte die Dateien aus Ihrem Dateisystem.
upgrade.version3to4.msg = Dieser Tresor muss auf ein neueres Format aktualisiert werden.\nVerschlüsselte Ordnernamen werden dabei aktualisiert.\nStellen Sie bitte sicher, dass derzeit keine Synchronisation stattfindet.
upgrade.version3to4.err.io = Migration aufgrund eines I/O-Fehlers fehlgeschlagen.\nWeitere Informationen in der Log-Datei.
upgrade.version3to4.err.io = Migration aufgrund eines I/O-Fehlers fehlgeschlagen.\nWeitere Informationen in der Log-Datei.
settings.prefGvfsScheme.label = WebDAV Schema
# upgrade.fxml
upgrade.confirmation.label = Ja, die Synchronisation ist abgeschlossen
initialize.messageLabel.notEmpty = Tresor ist nicht leer
unlock.label.savePassword = Passwort speichern
unlock.errorMessage.unauthenticVersionMac = Versions-MAC konnte nicht authentifiziert werden.

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