Compare commits

...

82 Commits
1.1.4 ... 1.2.3

Author SHA1 Message Date
Tobias Hagemann
cef3a5fc77 Merge branch 'release/1.2.3'
# 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/keychain/pom.xml
#	main/pom.xml
#	main/uber-jar/pom.xml
#	main/ui/pom.xml
2016-11-29 23:03:26 +01:00
Tobias Hagemann
9956f43fd9 updated to version 1.2.3 2016-11-29 22:24:26 +01:00
Tobias Hagemann
2b84593bde updated localization 2016-11-29 22:16:46 +01:00
Markus Kreusch
4e728fd387 Merge branch 'feature/issue-363' into develop 2016-11-29 15:21:48 +01:00
Tobias Hagemann
438ade1106 fixes #382 2016-11-27 14:28:44 +01:00
Sebastian Stenzel
fe54f4ec66 Update README.md
As suggested by @tallesh in #135
2016-11-27 10:14:34 +01:00
Markus Kreusch
fe86b4c593 Implemented #363 2016-11-14 22:26:55 +01:00
Markus Kreusch
a583afeb60 Merge branch 'feature/issue-393' into develop 2016-11-14 15:08:31 +01:00
Sebastian Stenzel
a585d3cf16 cherry picked from bac1d6f [ci skip] 2016-11-12 17:04:58 +01:00
Sebastian Stenzel
3db757193e Merge branch 'hotfix/1.2.2' 2016-11-12 17:02:04 +01:00
Sebastian Stenzel
bac1d6fd83 Updated siv-mode to 1.2.0 to be consistent with CryptoLib 2016-11-12 16:41:26 +01:00
Sebastian Stenzel
39ee8a9cde coverity issue 151831 2016-11-11 17:11:21 +01:00
Markus Kreusch
1263b3af81 fixed 'a' really bad thing in the last commit 2016-11-11 09:56:45 +01:00
Markus Kreusch
dafa29d8a3 Implemented #393 2016-11-10 22:58:45 +01:00
Sebastian Stenzel
2bc6fe89ad Merge branch 'release/1.2.1' 2016-11-10 15:23:11 +01:00
Sebastian Stenzel
8439216233 Updated version to 1.2.1 2016-11-10 15:13:28 +01:00
Sebastian Stenzel
aab616d184 Updated CryptoLib, hopefully fixes #373 2016-11-10 15:11:04 +01:00
Sebastian Stenzel
70c3a38c49 invoking UI methods on UI thread, might solve #351 2016-11-10 14:07:37 +01:00
Sebastian Stenzel
c64294ac3e Added chinese localizations, updated dutch localization 2016-11-10 13:41:09 +01:00
Markus Kreusch
82330db871 Additional logging for vault version upgrade 2016-11-09 15:54:10 +01:00
Sebastian Stenzel
c54a721f9a Merge pull request #385 from IAMtheIAM/patch-1
Update README.md
2016-11-06 21:59:16 +01:00
Sebastian Stenzel
355bbb5459 Merge branch 'develop' into patch-1 2016-11-06 21:53:28 +01:00
IAMtheIAM
63daa0f121 Update README.md
Update Readme with accurate info regarding v1.2.0
2016-11-06 03:56:56 -07:00
IAMtheIAM
50885d5c7c Update README.md 2016-11-05 16:51:16 -07:00
IAMtheIAM
4d68818ec5 Update README.md
Update features
2016-11-05 14:00:32 -07:00
IAMtheIAM
6fb20dd509 Update README.md
Added info about obfuscating file size and folder structure (two very important features that should be known!)
2016-11-05 13:54:39 -07:00
Sebastian Stenzel
2bb87dfa96 Merge branch 'release/1.2.0' into develop
# 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/keychain/pom.xml
#	main/pom.xml
#	main/uber-jar/pom.xml
#	main/ui/pom.xml
2016-09-19 15:12:24 +02:00
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
2fa04d7b7c increased version to 1.3.0-SNAPSHOT
[ci skip]
2016-09-15 13:35:22 +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
137 changed files with 2686 additions and 810 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,12 +14,13 @@ 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
- AES encryption with 256-bit key length
- Filenames get encrypted, too
- File names get encrypted
- Folder structure gets obfuscated
- Use as many vaults in your Dropbox as you want, each having individual passwords
### Privacy
@@ -52,9 +53,11 @@ For more information on the security details visit [cryptomator.org](https://cry
```
cd main
mvn clean install
mvn clean install -Prelease
```
An executable jar file will be created inside `main/uber-jar/target`.
## Contributing to Cryptomator
Please read our [contribution guide](https://github.com/cryptomator/cryptomator/blob/master/CONTRIBUTING.md), if you would like to report a bug, ask a question or help us with coding.

View File

@@ -8,7 +8,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.1.4</version>
<version>1.2.3</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.3</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.3</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.3</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.3</version>
</parent>
<artifactId>filesystem-charsets</artifactId>
<name>Cryptomator filesystem: Charset compatibility layer</name>

View File

@@ -40,9 +40,9 @@ class NormalizedNameFolder extends DelegatingFolder<NormalizedNameFolder, Normal
NormalizedNameFile nfcFile = super.file(nfcName);
NormalizedNameFile nfdFile = super.file(nfdName);
if (!nfcName.equals(nfdName) && nfcFile.exists() && nfdFile.exists()) {
LOG.warn("Ambiguous file names \"" + nfcName + "\" (NFC) vs. \"" + nfdName + "\" (NFD). Both files exist. Using \"" + nfcName + "\" (NFC).");
LOG.debug("Ambiguous file names \"" + nfcName + "\" (NFC) vs. \"" + nfdName + "\" (NFD). Both files exist. Using \"" + nfcName + "\" (NFC).");
} else if (!nfcName.equals(nfdName) && !nfcFile.exists() && nfdFile.exists()) {
LOG.info("Moving file from \"" + nfcName + "\" (NFD) to \"" + nfdName + "\" (NFC).");
LOG.debug("Moving file from \"" + nfcName + "\" (NFD) to \"" + nfdName + "\" (NFC).");
nfdFile.moveTo(nfcFile);
}
return nfcFile;
@@ -60,9 +60,9 @@ class NormalizedNameFolder extends DelegatingFolder<NormalizedNameFolder, Normal
NormalizedNameFolder nfcFolder = super.folder(nfcName);
NormalizedNameFolder nfdFolder = super.folder(nfdName);
if (!nfcName.equals(nfdName) && nfcFolder.exists() && nfdFolder.exists()) {
LOG.warn("Ambiguous folder names \"" + nfcName + "\" (NFC) vs. \"" + nfdName + "\" (NFD). Both files exist. Using \"" + nfcName + "\" (NFC).");
LOG.debug("Ambiguous folder names \"" + nfcName + "\" (NFC) vs. \"" + nfdName + "\" (NFD). Both files exist. Using \"" + nfcName + "\" (NFC).");
} else if (!nfcName.equals(nfdName) && !nfcFolder.exists() && nfdFolder.exists()) {
LOG.info("Moving folder from \"" + nfcName + "\" (NFD) to \"" + nfdName + "\" (NFC).");
LOG.debug("Moving folder from \"" + nfcName + "\" (NFD) to \"" + nfdName + "\" (NFC).");
nfdFolder.moveTo(nfcFolder);
}
return nfcFolder;

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.1.4</version>
<version>1.2.3</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.3</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.2.0</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

@@ -14,7 +14,7 @@ import java.security.MessageDigest;
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;
@@ -22,6 +22,7 @@ import org.apache.commons.codec.binary.BaseNCodec;
import org.cryptomator.crypto.engine.AuthenticationFailedException;
import org.cryptomator.crypto.engine.FilenameCryptor;
import org.cryptomator.siv.SivMode;
import org.cryptomator.siv.UnauthenticCiphertextException;
class FilenameCryptorImpl implements FilenameCryptor {
@@ -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 (UnauthenticCiphertextException | 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

@@ -68,7 +68,7 @@ final class ConflictResolver {
String alternativeCiphertext = nameEncryptor.apply(alternativeCleartext).get();
alternativeFile = folder.file(isDirectory ? DIR_PREFIX + alternativeCiphertext : alternativeCiphertext);
} while (alternativeFile.exists());
LOG.info("Detected conflict {}:\n{}\n{}", conflictId, canonicalFile, conflictingFile);
LOG.debug("Detected conflict {}:\n{}\n{}", conflictId, canonicalFile, conflictingFile);
conflictingFile.moveTo(alternativeFile);
return alternativeFile;
}
@@ -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.3</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.3</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.3</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.3</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.3</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.3</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.3</version>
</parent>
<artifactId>frontend-webdav</artifactId>
<name>Cryptomator frontend: WebDAV frontend</name>

View File

@@ -18,6 +18,10 @@ class ContextPaths {
return format("/%s/%s", id, name);
}
public static String removeFrontendId(String path) {
return path.replaceAll("/" + FRONTEND_ID_PATTERN + "/", "/[...]/");
}
public static Optional<FrontendId> extractFrontendId(String path) {
Matcher matcher = SERVLET_PATH_WITH_FRONTEND_ID_PATTERN.matcher(path);
if (matcher.matches()) {

View File

@@ -0,0 +1,17 @@
package org.cryptomator.frontend.webdav;
import org.eclipse.jetty.server.HandlerContainer;
import org.eclipse.jetty.servlet.ServletContextHandler;
class FontendIdHidingServletContextHandler extends ServletContextHandler {
public FontendIdHidingServletContextHandler(HandlerContainer parent, String contextPath, int options) {
super(parent, contextPath, options);
}
@Override
public String toString() {
return ContextPaths.removeFrontendId(super.toString());
}
}

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

@@ -8,10 +8,9 @@
*******************************************************************************/
package org.cryptomator.frontend.webdav;
import static java.lang.String.format;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
@@ -109,7 +108,7 @@ public class WebDavServer implements FrontendFactory {
@Override
public Frontend create(Folder root, FrontendId id, String name) throws FrontendCreationFailedException {
String contextPath = format("/%s/%s", id, name);
String contextPath = ContextPaths.from(id, name);
final URI uri;
try {
uri = new URI("http", null, "localhost", getPort(), contextPath, null, null);
@@ -117,9 +116,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));
LOG.info("Servlet available under " + ContextPaths.removeFrontendId(uri.toString()));
return new WebDavFrontend(webdavMounterProvider, handler, uri);
}
public void setValidFrontendIds(Collection<FrontendId> validFrontendIds) {
tarpit.setValidFrontendIds(validFrontendIds);
}
}

View File

@@ -35,9 +35,10 @@ import org.eclipse.jetty.servlet.ServletHolder;
class WebDavServletContextFactory {
private static final String WILDCARD = "/*";
@Inject
public WebDavServletContextFactory() {}
public WebDavServletContextFactory() {
}
/**
* Creates a new Jetty ServletContextHandler, that can be be added to a servletCollection as follows:
@@ -63,7 +64,7 @@ class WebDavServletContextFactory {
}
};
final String contextPath = StringUtils.removeEnd(contextRoot.getPath(), "/");
final ServletContextHandler servletContext = new ServletContextHandler(null, contextPath, ServletContextHandler.SESSIONS);
final ServletContextHandler servletContext = new FontendIdHidingServletContextHandler(null, contextPath, ServletContextHandler.SESSIONS);
final ServletHolder servletHolder = new ServletHolder(contextPath, new WebDavServlet(contextRoot, root));
servletContext.addServlet(servletHolder, WILDCARD);
servletContext.addFilter(LoopbackFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST));

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.3</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.3</version>
</parent>
<artifactId>keychain</artifactId>
<name>System Keychain Access</name>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.7</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.54</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>jni</artifactId>
</dependency>
<!-- DI -->
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger</artifactId>
</dependency>
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger-compiler</artifactId>
<scope>provided</scope>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>commons-test</artifactId>
</dependency>
</dependencies>
</project>

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.3</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.7</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.3</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.3</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

@@ -20,6 +20,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.ApplicationVersion;
import org.cryptomator.ui.util.SingleInstanceManager;
import org.cryptomator.ui.util.SingleInstanceManager.RemoteInstance;
import org.eclipse.jetty.util.ConcurrentHashSet;
@@ -33,63 +34,62 @@ public class Cryptomator {
public static final CompletableFuture<Consumer<File>> OPEN_FILE_HANDLER = new CompletableFuture<>();
private static final Logger LOG = LoggerFactory.getLogger(Cryptomator.class);
private static final Set<Runnable> SHUTDOWN_TASKS = new ConcurrentHashSet<>();
private static final CleanShutdownPerformer CLEAN_SHUTDOWN_PERFORMER = new CleanShutdownPerformer();
public static void main(String[] args) {
String cryptomatorVersion = Optional.ofNullable(Cryptomator.class.getPackage().getImplementationVersion()).orElse("SNAPSHOT");
LOG.info("Starting Cryptomator {} on {} {} ({})", cryptomatorVersion, SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
LOG.info("Starting Cryptomator {} on {} {} ({})", ApplicationVersion.orElse("SNAPSHOT"), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
if (SystemUtils.IS_OS_MAC_OSX) {
/*
* On OSX we're in an awkward position. We need to register a handler in the main thread of this application. However, we can't
* even pass objects to the application, so we're forced to use a static CompletableFuture for the handler, which actually opens
* the file in the application.
*
* Code taken from https://github.com/axet/desktop/blob/master/src/main/java/com/github/axet/desktop/os/mac/AppleHandlers.java
*/
try {
final Class<?> applicationClass = Class.forName("com.apple.eawt.Application");
final Class<?> openFilesHandlerClass = Class.forName("com.apple.eawt.OpenFilesHandler");
final Method getApplication = applicationClass.getMethod("getApplication");
final Object application = getApplication.invoke(null);
final Method setOpenFileHandler = applicationClass.getMethod("setOpenFileHandler", openFilesHandlerClass);
final ClassLoader openFilesHandlerClassLoader = openFilesHandlerClass.getClassLoader();
final OpenFilesHandlerClassHandler openFilesHandlerHandler = new OpenFilesHandlerClassHandler();
final Object openFilesHandlerObject = Proxy.newProxyInstance(openFilesHandlerClassLoader, new Class<?>[] {openFilesHandlerClass}, openFilesHandlerHandler);
setOpenFileHandler.invoke(application, openFilesHandlerObject);
} catch (ReflectiveOperationException | RuntimeException e) {
// Since we're trying to call OS-specific code, we'll just have
// to hope for the best.
LOG.error("exception adding OSX file open handler", e);
}
addOsxFileOpenHandler();
}
/*
* Perform certain things on VM termination.
*/
Runtime.getRuntime().addShutdownHook(CLEAN_SHUTDOWN_PERFORMER);
new CleanShutdownPerformer().registerShutdownHook();
/*
* Before starting the application, we check if there is already an instance running on this computer. If so, we send our command
* line arguments to that instance and quit.
*/
final Optional<RemoteInstance> remoteInstance = SingleInstanceManager.getRemoteInstance(MainApplication.APPLICATION_KEY);
if (remoteInstance.isPresent()) {
try (RemoteInstance instance = remoteInstance.get()) {
LOG.info("An instance of Cryptomator is already running at {}.", instance.getRemotePort());
for (int i = 0; i < args.length; i++) {
remoteInstance.get().sendMessage(args[i], 100);
}
} catch (Exception e) {
LOG.error("Error forwarding arguments to remote instance", e);
}
final Optional<RemoteInstance> runningInstance = SingleInstanceManager.getRemoteInstance(MainApplication.APPLICATION_KEY);
if (runningInstance.isPresent()) {
sendArgsToRunningInstance(args, runningInstance);
} else {
Application.launch(MainApplication.class, args);
}
}
private static void addOsxFileOpenHandler() {
/*
* On OSX we're in an awkward position. We need to register a handler in the main thread of this application. However, we can't
* even pass objects to the application, so we're forced to use a static CompletableFuture for the handler, which actually opens
* the file in the application.
*
* Code taken from https://github.com/axet/desktop/blob/master/src/main/java/com/github/axet/desktop/os/mac/AppleHandlers.java
*/
try {
final Class<?> applicationClass = Class.forName("com.apple.eawt.Application");
final Class<?> openFilesHandlerClass = Class.forName("com.apple.eawt.OpenFilesHandler");
final Method getApplication = applicationClass.getMethod("getApplication");
final Object application = getApplication.invoke(null);
final Method setOpenFileHandler = applicationClass.getMethod("setOpenFileHandler", openFilesHandlerClass);
final ClassLoader openFilesHandlerClassLoader = openFilesHandlerClass.getClassLoader();
final OpenFilesHandlerClassHandler openFilesHandlerHandler = new OpenFilesHandlerClassHandler();
final Object openFilesHandlerObject = Proxy.newProxyInstance(openFilesHandlerClassLoader, new Class<?>[] {openFilesHandlerClass}, openFilesHandlerHandler);
setOpenFileHandler.invoke(application, openFilesHandlerObject);
} catch (ReflectiveOperationException | RuntimeException e) {
// Since we're trying to call OS-specific code, we'll just have
// to hope for the best.
LOG.error("exception adding OSX file open handler", e);
}
}
private static void sendArgsToRunningInstance(String[] args, final Optional<RemoteInstance> remoteInstance) {
try (RemoteInstance instance = remoteInstance.get()) {
LOG.info("An instance of Cryptomator is already running at {}.", instance.getRemotePort());
for (int i = 0; i < args.length; i++) {
remoteInstance.get().sendMessage(args[i], 100);
}
} catch (Exception e) {
LOG.error("Error forwarding arguments to remote instance", e);
}
}
public static void addShutdownTask(Runnable r) {
SHUTDOWN_TASKS.add(r);
}
@@ -111,6 +111,10 @@ public class Cryptomator {
});
SHUTDOWN_TASKS.clear();
}
public void registerShutdownHook() {
Runtime.getRuntime().addShutdownHook(this);
}
}
private static void handleOpenFileRequest(File file) {

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,9 @@ interface CryptomatorComponent {
Localization localization();
ExitUtil exitUtil();
}
DebugMode debugMode();
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

@@ -0,0 +1,75 @@
package org.cryptomator.ui;
import static java.util.Arrays.asList;
import static org.apache.logging.log4j.LogManager.ROOT_LOGGER_NAME;
import java.util.Collection;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.cryptomator.ui.settings.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class DebugMode {
private static final Logger LOG = LoggerFactory.getLogger(DebugMode.class);
private static final Collection<LoggerUpgrade> LOGGER_UPGRADES = asList( //
loggerUpgrade(ROOT_LOGGER_NAME, Level.INFO), //
loggerUpgrade("org.cryptomator", Level.TRACE), //
loggerUpgrade("org.eclipse.jetty.server.Server", Level.DEBUG) //
);
private final Settings settings;
@Inject
public DebugMode(Settings settings) {
this.settings = settings;
}
public void initialize() {
if (settings.getDebugMode()) {
enable();
LOG.debug("Debug mode initialized");
}
}
private void enable() {
LoggerContext context = (LoggerContext) LogManager.getContext(false);
Configuration config = context.getConfiguration();
LOGGER_UPGRADES.forEach(loggerUpgrade -> loggerUpgrade.execute(config));
context.updateLoggers();
}
private static LoggerUpgrade loggerUpgrade(String loggerName, Level minLevel) {
return new LoggerUpgrade(loggerName, minLevel);
}
private static class LoggerUpgrade {
private final Level level;
private final String loggerName;
public LoggerUpgrade(String loggerName, Level minLevel) {
this.loggerName = loggerName;
this.level = minLevel;
}
public void execute(Configuration config) {
LoggerConfig loggerConfig = config.getLoggerConfig(loggerName);
if (loggerConfig.getLevel().isMoreSpecificThan(level)) {
loggerConfig.setLevel(level);
}
}
}
}

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,10 +43,46 @@ 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 MainController mainCtrl = comp.mainController();
CryptomatorComponent comp = createCryptomatorComponent(primaryStage);
MainController mainCtrl = comp.mainController();
closer = comp.deferredCloser();
comp.debugMode().initialize();
setupFXMLClassLoader();
setupStylesheets();
initializeStage(primaryStage, mainCtrl);
showWindow(primaryStage);
registerExitHandler(comp);
openFilesRequestedDuringStartup(primaryStage, mainCtrl);
registerApplicationToProcessOpenFileRequests(primaryStage, comp, mainCtrl);
}
@Override
public void stop() {
try {
closer.close();
} catch (ExecutionException e) {
LOG.error("Error closing ressources", e);
}
}
private CryptomatorComponent createCryptomatorComponent(Stage primaryStage) {
try {
return 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);
}
}
private void setupFXMLClassLoader() {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
FXMLLoader.setDefaultClassLoader(contextClassLoader);
Platform.runLater(() -> {
@@ -55,35 +94,55 @@ public class MainApplication extends Application {
Thread.currentThread().setContextClassLoader(contextClassLoader);
}
});
}
// Set stylesheets and initialize stage:
private void setupStylesheets() {
Font.loadFont(getClass().getResourceAsStream("/css/ionicons.ttf"), 12.0);
chooseNativeStylesheet();
}
private void initializeStage(Stage primaryStage, MainController mainCtrl) {
mainCtrl.initStage(primaryStage);
primaryStage.titleProperty().bind(mainCtrl.windowTitle());
primaryStage.setResizable(false);
if (SystemUtils.IS_OS_WINDOWS) {
primaryStage.getIcons().add(new Image(MainApplication.class.getResourceAsStream("/window_icon.png")));
}
}
// show window and start observing its focus:
private void showWindow(Stage primaryStage) {
primaryStage.show();
ActiveWindowStyleSupport.startObservingFocus(primaryStage);
comp.exitUtil().initExitHandler(this::quit);
}
// open files, if requested during startup:
private void registerExitHandler(CryptomatorComponent comp) {
comp.exitUtil().initExitHandler(this::quit);
}
private void openFilesRequestedDuringStartup(Stage primaryStage, final MainController mainCtrl) {
for (String arg : getParameters().getUnnamed()) {
handleCommandLineArg(arg, primaryStage, mainCtrl);
}
if (SystemUtils.IS_OS_MAC_OSX) {
Cryptomator.OPEN_FILE_HANDLER.complete(file -> handleCommandLineArg(file.getAbsolutePath(), primaryStage, mainCtrl));
}
}
// register this application instance as primary application, that other instances can send open file requests to:
private void registerApplicationToProcessOpenFileRequests(Stage primaryStage, final CryptomatorComponent comp, final MainController mainCtrl) throws IOException {
LocalInstance cryptomatorGuiInstance = closer.closeLater(SingleInstanceManager.startLocalInstance(APPLICATION_KEY, comp.executorService()), LocalInstance::close).get().get();
cryptomatorGuiInstance.registerListener(arg -> handleCommandLineArg(arg, primaryStage, mainCtrl));
}
private void chooseNativeStylesheet() {
if (SystemUtils.IS_OS_MAC_OSX) {
setUserAgentStylesheet(getClass().getResource("/css/mac_theme.css").toString());
} else if (SystemUtils.IS_OS_LINUX) {
setUserAgentStylesheet(getClass().getResource("/css/linux_theme.css").toString());
} else if (SystemUtils.IS_OS_WINDOWS) {
setUserAgentStylesheet(getClass().getResource("/css/win_theme.css").toString());
}
}
private void handleCommandLineArg(String arg, Stage primaryStage, MainController mainCtrl) {
// find correct location:
final Path path = FileSystems.getDefault().getPath(arg);
@@ -107,16 +166,6 @@ public class MainApplication extends Application {
});
}
private void chooseNativeStylesheet() {
if (SystemUtils.IS_OS_MAC_OSX) {
setUserAgentStylesheet(getClass().getResource("/css/mac_theme.css").toString());
} else if (SystemUtils.IS_OS_LINUX) {
setUserAgentStylesheet(getClass().getResource("/css/linux_theme.css").toString());
} else if (SystemUtils.IS_OS_WINDOWS) {
setUserAgentStylesheet(getClass().getResource("/css/win_theme.css").toString());
}
}
private void quit() {
Platform.runLater(() -> {
stop();
@@ -125,13 +174,4 @@ public class MainApplication extends Application {
});
}
@Override
public void stop() {
try {
closer.close();
} catch (ExecutionException e) {
LOG.error("Error closing ressources", e);
}
}
}

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

@@ -59,6 +59,9 @@ public class SettingsController extends LocalizedFXMLViewController {
@FXML
private ChoiceBox<String> prefGvfsScheme;
@FXML
private CheckBox debugModeCheckbox;
@Override
public void initialize() {
checkForUpdatesCheckbox.setDisable(areUpdatesManagedExternally());
@@ -74,11 +77,13 @@ public class SettingsController extends LocalizedFXMLViewController {
prefGvfsScheme.getItems().add("dav");
prefGvfsScheme.getItems().add("webdav");
prefGvfsScheme.setValue(settings.getPreferredGvfsScheme());
debugModeCheckbox.setSelected(settings.getDebugMode());
EasyBind.subscribe(checkForUpdatesCheckbox.selectedProperty(), this::checkForUpdateDidChange);
EasyBind.subscribe(portField.textProperty(), this::portDidChange);
EasyBind.subscribe(useIpv6Checkbox.selectedProperty(), this::useIpv6DidChange);
EasyBind.subscribe(prefGvfsScheme.valueProperty(), this::prefGvfsSchemeDidChange);
EasyBind.subscribe(debugModeCheckbox.selectedProperty(), this::debugModeDidChange);
}
@Override
@@ -114,6 +119,11 @@ public class SettingsController extends LocalizedFXMLViewController {
settings.save();
}
private void debugModeDidChange(Boolean newValue) {
settings.setDebugMode(newValue);
settings.save();
}
private void prefGvfsSchemeDidChange(String newValue) {
settings.setPreferredGvfsScheme(newValue);
settings.save();

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,27 @@ 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.cryptomator.ui.util.DialogBuilderUtil;
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.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
@@ -61,22 +65,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 +102,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 +117,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 +129,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 +136,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 +158,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 +181,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 +209,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 +260,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();
@@ -264,6 +279,37 @@ public class UnlockController extends LocalizedFXMLViewController {
}
}
// ****************************************
// Save password checkbox
// ****************************************
@FXML
private void didClickSavePasswordCheckbox(ActionEvent event) {
if (!savePassword.isSelected() && hasStoredPassword()) {
Alert confirmDialog = DialogBuilderUtil.buildConfirmationDialog( //
localization.getString("unlock.savePassword.delete.confirmation.title"), //
localization.getString("unlock.savePassword.delete.confirmation.header"), //
localization.getString("unlock.savePassword.delete.confirmation.content"), //
SystemUtils.IS_OS_MAC_OSX ? ButtonType.CANCEL : ButtonType.OK);
Optional<ButtonType> choice = confirmDialog.showAndWait();
if (ButtonType.OK.equals(choice.get())) {
keychainAccess.get().deletePassphrase(vault.getId());
} else if (ButtonType.CANCEL.equals(choice.get())) {
savePassword.setSelected(true);
}
}
}
private boolean hasStoredPassword() {
char[] storedPw = keychainAccess.get().loadPassphrase(vault.getId());
boolean hasPw = (storedPw != null);
if (storedPw != null) {
Arrays.fill(storedPw, ' ');
}
return hasPw;
}
// ****************************************
// Unlock button
// ****************************************
@@ -275,10 +321,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 +332,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 {
Platform.runLater(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 +363,6 @@ public class UnlockController extends LocalizedFXMLViewController {
});
} finally {
Platform.runLater(() -> {
passwordField.swipe();
mountName.setDisable(false);
advancedOptionsButton.setDisable(false);
progressIndicator.setVisible(false);

View File

@@ -38,6 +38,7 @@ import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ToggleButton;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
@@ -57,15 +58,6 @@ public class UnlockedController extends LocalizedFXMLViewController {
private Optional<LockListener> listener = Optional.empty();
private Timeline ioAnimation;
@Inject
public UnlockedController(Localization localization, Provider<MacWarningsController> macWarningsControllerProvider, AsyncTaskService asyncTaskService) {
super(localization);
this.macWarningsController = macWarningsControllerProvider.get();
this.asyncTaskService = asyncTaskService;
macWarningsController.vault.bind(this.vault);
}
@FXML
private Label messageLabel;
@@ -81,11 +73,25 @@ public class UnlockedController extends LocalizedFXMLViewController {
@FXML
private ContextMenu moreOptionsMenu;
@FXML
private MenuItem revealVaultMenuItem;
@Inject
public UnlockedController(Localization localization, Provider<MacWarningsController> macWarningsControllerProvider, AsyncTaskService asyncTaskService) {
super(localization);
this.macWarningsController = macWarningsControllerProvider.get();
this.asyncTaskService = asyncTaskService;
macWarningsController.vault.bind(this.vault);
}
@Override
public void initialize() {
macWarningsController.initStage(macWarningsWindow);
ActiveWindowStyleSupport.startObservingFocus(macWarningsWindow);
revealVaultMenuItem.disableProperty().bind(EasyBind.map(vault, vault -> vault != null && !vault.isMounted()));
EasyBind.subscribe(vault, this::vaultChanged);
EasyBind.subscribe(moreOptionsMenu.showingProperty(), moreOptionsButton::setSelected);
}
@@ -109,6 +115,11 @@ public class UnlockedController extends LocalizedFXMLViewController {
}
});
if (!vault.get().isMounted()) {
// TODO Markus Kreusch #393: hyperlink auf FAQ oder sowas?
messageLabel.setText(localization.getString("unlocked.label.mountFailed"));
}
// (re)start throughput statistics:
stopIoSampling();
startIoSampling();

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

@@ -13,7 +13,6 @@ import java.net.URL;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Named;
@@ -29,6 +28,7 @@ import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.util.ApplicationVersion;
import org.cryptomator.ui.util.AsyncTaskService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -104,7 +104,7 @@ public class WelcomeController extends LocalizedFXMLViewController {
asyncTaskService.asyncTaskOf(() -> {
final HttpClient client = new HttpClient();
final HttpMethod method = new GetMethod("https://cryptomator.org/downloads/latestVersion.json");
client.getParams().setParameter(HttpClientParams.USER_AGENT, "Cryptomator VersionChecker/" + applicationVersion().orElse("SNAPSHOT"));
client.getParams().setParameter(HttpClientParams.USER_AGENT, "Cryptomator VersionChecker/" + ApplicationVersion.orElse("SNAPSHOT"));
client.getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES);
client.getParams().setConnectionManagerTimeout(5000);
client.executeMethod(method);
@@ -124,10 +124,6 @@ public class WelcomeController extends LocalizedFXMLViewController {
}).run();
}
private Optional<String> applicationVersion() {
return Optional.ofNullable(getClass().getPackage().getImplementationVersion());
}
private void compareVersions(final Map<String, String> latestVersions) {
final String latestVersion;
if (SystemUtils.IS_OS_MAC_OSX) {
@@ -140,7 +136,7 @@ public class WelcomeController extends LocalizedFXMLViewController {
// no version check possible on unsupported OS
return;
}
final String currentVersion = applicationVersion().orElse(null);
final String currentVersion = ApplicationVersion.orElse(null);
LOG.debug("Current version: {}, lastest version: {}", currentVersion, latestVersion);
if (currentVersion != null && semVerComparator.compare(currentVersion, latestVersion) < 0) {
final String msg = String.format(localization.getString("welcome.newVersionMessage"), latestVersion, currentVersion);

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,40 @@ 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();
LOG.info("Upgrading {} from {} to {}.", vault.path().getValue(), vaultVersionBeforeUpgrade, vaultVersionAfterUpgrade);
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);
LOG.info("Backuped masterkey.");
// 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);
LOG.info("Updated masterkey.");
} 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 +85,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,138 @@
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);
} else {
LOG.info("Skipping irrelevant file {}.", file);
}
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 {
LOG.info("Starting migration of {}...", file);
try (FileChannel ch = FileChannel.open(file, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
// read header:
ByteBuffer headerBuf = ByteBuffer.allocate(cryptor.fileHeaderCryptor().headerSize());
ch.read(headerBuf);
headerBuf.flip();
LOG.info("\tHeader read");
FileHeader header = cryptor.fileHeaderCryptor().decryptHeader(headerBuf);
long cleartextSize = header.getFilesize();
if (cleartextSize < 0) {
LOG.info("\tSkipping already migrated file");
return;
} else if (cleartextSize > attrs.size()) {
LOG.warn("\tSkipping file with invalid file size {}/{}", 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("\tMigrating cleartext size {}: Truncating to new ciphertext size: {}", cleartextSize, newEOF);
ch.truncate(newEOF);
LOG.info("\tFile truncated");
} else {
// last block may contain padding and needs to be re-encrypted:
long lastChunkIdx = newFullChunks;
LOG.info("\tMigrating cleartext size {}: Re-encrypting chunk {}. New ciphertext size: {}", 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("\tReached EOF at position {}/{}", beginOfLastChunk, newEOF);
return; // must exit method before changing header!
}
LOG.info("\tReencrypted last block");
}
header.setFilesize(-1l);
ByteBuffer newHeaderBuf = cryptor.fileHeaderCryptor().encryptHeader(header);
ch.position(0);
ch.write(newHeaderBuf);
LOG.info("\tUpdated header");
}
LOG.info("Finished migration of {}.", file);
}
}

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;
@@ -48,6 +50,8 @@ import org.cryptomator.ui.util.DeferredClosable;
import org.cryptomator.ui.util.DeferredCloser;
import org.cryptomator.ui.util.FXThreads;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ImmutableMap;
@@ -63,6 +67,8 @@ import javafx.collections.ObservableList;
public class Vault implements CryptoFileSystemDelegate {
private static final Logger LOG = LoggerFactory.getLogger(CryptoFileSystemDelegate.class);
public static final String VAULT_FILE_EXTENSION = ".cryptomator";
private final ObjectProperty<Path> path;
@@ -70,6 +76,7 @@ public class Vault implements CryptoFileSystemDelegate {
private final CryptoFileSystemFactory cryptoFileSystemFactory;
private final DeferredCloser closer;
private final BooleanProperty unlocked = new SimpleBooleanProperty();
private final BooleanProperty mounted = new SimpleBooleanProperty();
private final ObservableList<String> namesOfResourcesWithInvalidMac = FXThreads.observableListOnMainThread(FXCollections.observableArrayList());
private final Set<String> whitelistedResourcesWithInvalidMac = new HashSet<>();
private final AtomicReference<FileSystem> nioFileSystem = new AtomicReference<>();
@@ -109,8 +116,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) {
@@ -127,7 +136,8 @@ public class Vault implements CryptoFileSystemDelegate {
}
public synchronized void activateFrontend(FrontendFactory frontendFactory, Settings settings, CharSequence passphrase) throws FrontendCreationFailedException {
boolean success = false;
boolean launchSuccess = false;
boolean mountSuccess = false;
try {
FileSystem fs = getNioFileSystem();
FileSystem shorteningFs = shorteningFileSystemFactory.get(fs);
@@ -136,22 +146,32 @@ public class Vault implements CryptoFileSystemDelegate {
StatsFileSystem statsFs = new StatsFileSystem(normalizingFs);
statsFileSystem = Optional.of(statsFs);
Frontend frontend = frontendFactory.create(statsFs, FrontendId.from(id), stripStart(mountName, "/"));
launchSuccess = true;
filesystemFrontend = closer.closeLater(frontend);
frontend.mount(getMountParams(settings));
success = true;
} catch (UncheckedIOException | CommandFailedException e) {
mountSuccess = true;
} catch (UncheckedIOException e) {
throw new FrontendCreationFailedException(e);
} catch (CommandFailedException e) {
LOG.error("Failed to mount vault " + mountName, e);
} finally {
// unlocked is a observable property and should only be changed by the FX application thread
final boolean finalSuccess = success;
Platform.runLater(() -> unlocked.set(finalSuccess));
boolean finalLaunchSuccess = launchSuccess;
boolean finalMountSuccess = mountSuccess;
Platform.runLater(() -> {
unlocked.set(finalLaunchSuccess);
mounted.set(finalMountSuccess);
});
}
}
public synchronized void deactivateFrontend() throws Exception {
filesystemFrontend.close();
statsFileSystem = Optional.empty();
Platform.runLater(() -> unlocked.set(false));
Platform.runLater(() -> {
mounted.set(false);
unlocked.set(false);
});
}
private Map<MountParam, Optional<String>> getMountParams(Settings settings) {
@@ -164,7 +184,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 +206,7 @@ public class Vault implements CryptoFileSystemDelegate {
// Getter/Setter
// *******************************************************************************/
public String getWebDavUrl() {
public synchronized String getWebDavUrl() {
return filesystemFrontend.get().map(Frontend::getWebDavUrl).orElseThrow(IllegalStateException::new);
}
@@ -235,10 +255,18 @@ public class Vault implements CryptoFileSystemDelegate {
return unlocked;
}
public BooleanProperty mountedProperty() {
return mounted;
}
public boolean isUnlocked() {
return unlocked.get();
}
public boolean isMounted() {
return mounted.get();
}
public ObservableList<String> getNamesOfResourcesWithInvalidMac() {
return namesOfResourcesWithInvalidMac;
}

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