Compare commits

...

111 Commits

Author SHA1 Message Date
Sebastian Stenzel
20e55eddf8 Merge branch 'develop' 2016-07-09 13:25:54 +02:00
Sebastian Stenzel
0fdcdc816a fixed unit test 2016-07-09 13:25:24 +02:00
Sebastian Stenzel
b7506d97a9 Merge branch 'hotfix/1.1.2' 2016-07-09 13:25:03 +02:00
Sebastian Stenzel
4ad7481dc7 fixed unit test 2016-07-09 11:44:53 +02:00
Sebastian Stenzel
bc815405d2 merged from hotfix/1.1.2 [ci skip] 2016-07-09 11:32:02 +02:00
Sebastian Stenzel
9c06e762c3 fixes #304 2016-07-09 11:28:36 +02:00
Sebastian Stenzel
1ac87dd32f fixed NPE [ci skip] 2016-07-08 15:52:00 +02:00
Sebastian Stenzel
e0ce7ce2ec Merge branch 'release/1.1.1'
# 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-07-08 11:59:39 +02:00
Sebastian Stenzel
3d951a9d7b set version to 1.1.1 2016-07-07 14:48:49 +02:00
Sebastian Stenzel
cec3d984b0 Merge branch 'develop' into release/1.1.1 2016-07-07 14:39:20 +02:00
Sebastian Stenzel
392e474cfa Update tr.txt (POEditor.com) 2016-07-07 14:28:52 +02:00
Sebastian Stenzel
41fb0d51a4 Update es.txt (POEditor.com) 2016-07-07 14:28:51 +02:00
Sebastian Stenzel
aa9fef2967 Update sk.txt (POEditor.com) 2016-07-07 14:28:49 +02:00
Sebastian Stenzel
adc9c02564 Update ru.txt (POEditor.com) 2016-07-07 14:28:48 +02:00
Sebastian Stenzel
ace64117a2 Update kr.txt (POEditor.com) 2016-07-07 14:28:46 +02:00
Sebastian Stenzel
fb4db2506b Update it.txt (POEditor.com) 2016-07-07 14:28:45 +02:00
Sebastian Stenzel
1076d971ae Update hu.txt (POEditor.com) 2016-07-07 14:28:43 +02:00
Sebastian Stenzel
eed1b1cff0 Update de.txt (POEditor.com) 2016-07-07 14:28:42 +02:00
Sebastian Stenzel
f5cb82e21e Update fr.txt (POEditor.com) 2016-07-07 14:28:40 +02:00
Sebastian Stenzel
67661f114b Update nl.txt (POEditor.com) 2016-07-07 14:28:39 +02:00
Sebastian Stenzel
8a3e09764a only remove .cryptomator extension for vault version 3 2016-07-07 14:25:55 +02:00
Sebastian Stenzel
eb3cfd6e6a updated placeholders [ci skip] 2016-07-06 16:25:31 +02:00
Sebastian Stenzel
4d1727d0e9 Merge branch 'develop' into release/1.1.1 2016-07-06 16:09:32 +02:00
Sebastian Stenzel
a51d853d1c adjusted number format regex [ci skip] 2016-07-06 16:09:14 +02:00
Sebastian Stenzel
d0039466f7 test technical correctness of localization files 2016-07-06 16:07:07 +02:00
Sebastian Stenzel
5c959989a2 Fixed Coverity defect 131711 2016-07-05 23:18:18 +02:00
Sebastian Stenzel
6283d2df3d Merge branch 'feature/vault-version-4' into develop 2016-07-03 17:41:46 +02:00
Sebastian Stenzel
a9e0dfdaf8 redesigned upgrade view 2016-07-03 17:38:46 +02:00
Sebastian Stenzel
45ca7e9e47 migration from vault version 3 to 4 2016-07-03 16:16:23 +02:00
Sebastian Stenzel
034b5c2718 updated localizations
[ci skip]
2016-07-01 11:14:49 +02:00
Sebastian Stenzel
e188649c79 adjusted test to vault version 4 2016-06-30 22:18:43 +02:00
Sebastian Stenzel
1468c6ec90 improved vault upgrading, preparation for migration to vault version 4 2016-06-30 22:09:45 +02:00
Sebastian Stenzel
07ba4eb537 Using 0 prefix instead of _ suffix to mark directories 2016-06-30 18:02:13 +02:00
Sebastian Stenzel
414bbef1a7 updated key generation 2016-06-10 14:04:55 +02:00
Sebastian Stenzel
e2b94ff6ef updated jacoco dependency 2016-06-08 19:11:56 +02:00
Sebastian Stenzel
41f8a9faca add "allow" response header field 2016-06-08 19:06:06 +02:00
Tobias Hagemann
1d9252e974 updated description of file chooser's extension filter [ci skip] 2016-06-07 01:07:54 +02:00
Sebastian Stenzel
80780eef3c Merge pull request #280 from aeris/fix-l10n-fr
Enhanced fr translation
[ci skip]
2016-05-30 14:27:00 +02:00
Aeris
87ff33956b Enhanced fr translation 2016-05-30 13:45:40 +02:00
Sebastian Stenzel
1804b98f05 trigger coverity scans for only release branches [ci skip] 2016-05-25 15:47:19 +02:00
Sebastian Stenzel
847c6813cc started development of 1.2.0 [ci skip] 2016-05-25 15:37:07 +02:00
Sebastian Stenzel
1dde5ff6e7 release 1.1.0 2016-05-25 15:17:40 +02:00
Sebastian Stenzel
76c9a19428 unset and set default buttons to make sure VK_ENTER triggers it 2016-05-25 12:29:49 +02:00
Sebastian Stenzel
25ee0519e1 some minor fixes
- reset password field contents when changing a vault
- hide "change password" option for uninitialized or missing vaults
2016-05-25 12:12:01 +02:00
Sebastian Stenzel
c184089c35 oopsy daisy, wie das duftet... [ci skip] 2016-05-24 11:36:46 +02:00
Sebastian Stenzel
d2bcc47857 Merge branch 'delete-confirmation'
fixes #228
2016-05-24 11:35:17 +02:00
Sebastian Stenzel
34629a69ea Using ControlsFX's BSD-licensed assets for dialogs, rather than OpenJDK's GPL licensed ones.
Adjusted dialog styles for Linux and Windows.
[ci skip]
2016-05-24 11:32:27 +02:00
Sebastian Stenzel
92c87f7b84 changed dialog L&F on OS X 2016-05-23 19:31:18 +02:00
Sebastian Stenzel
0dd96635ac code cleanup [ci skip] 2016-05-23 13:24:53 +02:00
Sebastian Stenzel
048c44a6e4 Update README.md 2016-05-23 12:36:58 +02:00
Sebastian Stenzel
06910ad1f4 fixes #229 2016-05-23 12:11:45 +02:00
Sebastian Stenzel
02a0f3acc6 fixed invariant FolderChildrenTests 2016-05-23 11:18:24 +02:00
Sebastian Stenzel
851f9240b7 updated link to MAC warning FAQ 2016-05-23 11:02:56 +02:00
Sebastian Stenzel
99fce8d0b7 automatically resolve conflicts for directory files, that contain the same directory ID 2016-05-23 11:02:44 +02:00
Sebastian Stenzel
bf05c59c3b Transparent conflict detection for long file names 2016-05-22 15:16:32 +02:00
Sebastian Stenzel
3dcebb1e1f fixed minor copy/paste error 2016-05-22 13:32:16 +02:00
Sebastian Stenzel
fe3efdf610 Merge pull request #269 from jncharon/master
fixes #56
2016-05-21 14:07:15 +02:00
jncharon
5f4ae46f82 Replaced the MouseListner by a MouseAdapter 2016-05-20 21:30:33 +02:00
jncharon
deef325319 Implementation of github issue #56 2016-05-20 19:29:53 +02:00
jncharon
fbe00a8fe3 Merge remote-tracking branch 'refs/remotes/cryptomator/master' 2016-05-16 14:20:55 +02:00
Sebastian Stenzel
dc87dade43 Merge pull request #259 from jncharon/issue-228-fixed
Fixes #228.
Still need to check license of icons, will discuss this with @MuscleRumble, who has a lot of icons that we bought the license for. If necessary we will change them.
2016-05-16 12:53:53 +02:00
Jean-Noël Charon
ba1625b5ad Merge pull request #2 from overheadhunter/issue-228-fixed
Reverted commit c0f4a2b, added .idea/ to .gitignore
2016-05-16 12:14:38 +02:00
Sebastian Stenzel
f6b126415e added IntelliJ files to .gitignore 2016-05-16 10:00:39 +02:00
Sebastian Stenzel
9147e1c08b Revert "Fucking .idea files I could not remove from the vcs"
This reverts commit c0f4a2b0d3.
2016-05-16 09:57:10 +02:00
Sebastian Stenzel
6c18103662 Remove files with non-decryptable names from dir listings 2016-05-13 18:59:06 +02:00
Sebastian Stenzel
6fc343ea12 more fault-tolerant behaviour when mapping of long filenames couldn't be found. 2016-05-13 14:10:37 +02:00
Sebastian Stenzel
d304d66cdd Updated localizations [ci skip] 2016-05-12 19:23:24 +02:00
Sebastian Stenzel
2ce9143b85 Merge branch 'conflict-detection' 2016-05-12 19:14:48 +02:00
Sebastian Stenzel
1c54e4f4ad in the unlikely event of an alternative name already being used, choose a new random conflict id. 2016-05-12 16:13:03 +02:00
Sebastian Stenzel
9fd6f2ecae transparently show sync conflicts (fixes #98) 2016-05-12 16:08:52 +02:00
Sebastian Stenzel
0d9f8eefc0 Using pattern based filename filtering
This is a preparation for finding valid encrypted names inside filenames that include additional characters
2016-05-12 11:51:14 +02:00
Sebastian Stenzel
40a1530f19 repeated commit 86000ac 2016-05-10 14:52:30 +02:00
Sebastian Stenzel
0477a0a2e3 Merge branch 'patches-1.0.x'
# Conflicts:
#	main/filesystem-charsets/pom.xml
2016-05-10 14:49:20 +02:00
Sebastian Stenzel
b77d4b5ae2 fixes #264 2016-05-10 14:33:21 +02:00
Sebastian Stenzel
7b6c5318c5 fixes #263 2016-05-10 14:31:55 +02:00
Sebastian Stenzel
6006d65ce0 new ant kit using a custom launcher binary due to #265 2016-05-10 14:26:06 +02:00
jncharon
2b01b76926 Merge remote-tracking branch 'refs/remotes/cryptomator/master' 2016-05-08 17:52:14 +02:00
Sebastian Stenzel
dcea9e21f0 added module to code coverage report 2016-05-07 15:00:20 +02:00
Sebastian Stenzel
78645ecdf6 fixes #264 2016-05-07 14:40:44 +02:00
Sebastian Stenzel
91646dd93d Merge branch 'password-strength'
Added password strength meter by Jean-Noël Charon, closing issue #198
2016-05-06 18:59:26 +02:00
jncharon
fca146e939 Merge remote-tracking branch 'remotes/origin/master' into issue-228-fixed 2016-05-05 21:22:28 +02:00
jncharon
62aa3ccc7f Merge remote-tracking branch 'refs/remotes/cryptomator/master' into issue-228-fixed 2016-05-05 21:12:53 +02:00
jncharon
c0f4a2b0d3 Fucking .idea files I could not remove from the vcs 2016-05-05 21:11:57 +02:00
Tobias Hagemann
68ee89af98 updated bot welcome asset [ci skip] 2016-05-03 19:12:21 +02:00
Sebastian Stenzel
ad2c9116b9 Release 1.0.4 2016-05-03 16:50:18 +02:00
Sebastian Stenzel
8e24745b3e Merge branch 'master' into patches-1.0.x 2016-05-03 16:46:18 +02:00
Sebastian Stenzel
08f664e3df Throttle calls to Settings.save() 2016-05-03 16:44:22 +02:00
Tobias Hagemann
b6d1d1dc22 updated linux app icon [ci skip] 2016-05-03 16:07:30 +02:00
Sebastian Stenzel
a0ef02b95c fixes #237 2016-05-03 13:17:45 +02:00
Sebastian Stenzel
a6cefe67c4 setting default port to 42427 [ci skip] 2016-05-03 10:48:24 +02:00
Sebastian Stenzel
be2b63ab2a support for UTF-8 localization files 2016-05-02 22:28:49 +02:00
Sebastian Stenzel
78f11b4a5e added korean localization [ci skip] 2016-05-02 16:01:28 +02:00
Sebastian Stenzel
0f20c7c3c9 fixes #209 2016-05-02 12:36:31 +02:00
Sebastian Stenzel
d4235174f7 imported localizations from POEditor fixes #231 and #234 (ci skip) 2016-05-02 11:37:40 +02:00
Sebastian Stenzel
f16be84aa3 restored bash-based webdav mounting for OS X before 10.10 (issue #211 - to be tested) 2016-05-02 11:11:42 +02:00
Sebastian Stenzel
833f2d8566 fixed travis test coverage configuration 2016-04-27 01:18:37 +02:00
Sebastian Stenzel
c02a63878e new method to calculate test coverage (across modules) 2016-04-27 01:14:41 +02:00
jncharon
6deb30307e Merge remote-tracking branch 'cryptomator/master' 2016-04-24 13:47:37 +02:00
jncharon
7357829741 Fix in the background color of the dialog boxes 2016-04-23 23:42:28 +02:00
jncharon
4bd04150c1 Implementation of github issue 228 2016-04-23 23:37:56 +02:00
Sebastian Stenzel
cf35772c18 Merge pull request #239 from jncharon/master
Fix in the change password screen
2016-04-21 09:11:23 +02:00
jncharon
b0fd226c4c Fix of the strength bar position (row) in the fxml 2016-04-20 22:32:30 +02:00
jncharon
0d188d1c0c Merge remote-tracking branch 'cryptomator/master' 2016-04-16 15:27:01 +02:00
Sebastian Stenzel
c6016ec7b2 using constructor-injection, organized imports, code autoformatting [ci skip] 2016-04-16 14:10:32 +02:00
Sebastian Stenzel
e8719a1f9b Merge pull request #232 from jncharon/master
Fixes #198, #157
2016-04-16 09:53:14 +02:00
jncharon
27baf78029 More refactoring following Sebastian comments 2016-04-16 00:12:59 +02:00
jncharon
bf5ce9a3a5 New password strength implementation based on zxcvbn4j 2016-04-15 22:52:57 +02:00
jncharon
bcfe040784 Merge remote-tracking branch 'cryptomator/master' 2016-04-13 18:22:16 +02:00
jncharon
d9b88ad1b7 Merge remote-tracking branch 'refs/remotes/cryptomator/master' 2016-04-12 21:53:44 +02:00
jncharon
e66e5b1d96 Added the password strength indicator in the change password window 2016-04-12 21:27:31 +02:00
jncharon
588166dce9 Added the password strength indicator in the initialize window 2016-04-12 21:00:41 +02:00
124 changed files with 3578 additions and 452 deletions

6
.gitignore vendored
View File

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

View File

@@ -16,7 +16,7 @@ before_install: "curl -L --cookie 'oraclelicense=accept-securebackup-cookie;' ht
script: mvn -fmain/pom.xml clean test
after_success: mvn -fmain/pom.xml clean test jacoco:report coveralls:report
after_success: mvn -fmain/pom.xml -Ptest-coverage clean test jacoco:report-aggregate coveralls:report
notifications:
webhooks:
@@ -39,7 +39,7 @@ addons:
name: "cryptomator/cryptomator"
notification_email: sebastian.stenzel@cryptomator.org
build_command: "mvn -fmain/pom.xml clean test -DskipTests"
branch_pattern: coverity_scan
branch_pattern: release.*
deploy:
provider: releases

View File

@@ -45,7 +45,7 @@ For more information on the security details visit [cryptomator.org](https://cry
* Java 8 + JCE unlimited strength policy files (needed for 256-bit keys)
* Maven 3
* Optional: OS-dependent build tools for native packaging (see [Windows](https://github.com/cryptomator/cryptomator-win), [OS X](https://github.com/cryptomator/cryptomator-osx), [Debian](https://github.com/cryptomator/cryptomator-deb))
* Optional: OS-dependent build tools for native packaging (see [Windows](https://github.com/cryptomator/cryptomator-win), [OS X](https://github.com/cryptomator/cryptomator-osx), [Linux](https://github.com/cryptomator/builder-containers))
### Run Maven

View File

@@ -14,6 +14,12 @@
</includes>
<outputDirectory>libs</outputDirectory>
</fileSet>
<fileSet>
<directory>target/fixed-binaries</directory>
<filtered>false</filtered>
<outputDirectory>fixed-binaries</outputDirectory>
<fileMode>755</fileMode>
</fileSet>
<fileSet>
<directory>target/package</directory>
<filtered>false</filtered>

View File

@@ -8,7 +8,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.3d</version>
<version>1.1.2</version>
</parent>
<artifactId>ant-kit</artifactId>
<packaging>pom</packaging>
@@ -60,6 +60,16 @@
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<excludes>
<exclude>fixed-binaries/**</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<includes>
<include>fixed-binaries/**</include>
</includes>
</resource>
</resources>
</configuration>

View File

@@ -50,6 +50,7 @@
<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:fileset dir="fixed-binaries" type="data" includes="linux-launcher-*" arch=""/>
</fx:resources>
<fx:permissions elevated="false" />
<fx:preferences install="true" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -0,0 +1,50 @@
#!/bin/sh
# postinst script for APPLICATION_NAME
#
# see: dh_installdeb(1)
set -e
# summary of how this script can be called:
# * <postinst> `configure' <most-recently-configured-version>
# * <old-postinst> `abort-upgrade' <new version>
# * <conflictor's-postinst> `abort-remove' `in-favour' <package>
# <new-version>
# * <postinst> `abort-remove'
# * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
# <failed-install-package> <version> `removing'
# <conflicting-package> <version>
# for details, see http://www.debian.org/doc/debian-policy/ or
# the debian-policy package
case "$1" in
configure)
echo Adding shortcut to the menu
SECONDARY_LAUNCHERS_INSTALL
APP_CDS_CACHE
xdg-desktop-menu install --novendor /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME.desktop
FILE_ASSOCIATION_INSTALL
rm /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME
if [ $(uname -m) = "x86_64" ]; then
mv /opt/APPLICATION_FS_NAME/app/linux-launcher-x64 /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME
else
mv /opt/APPLICATION_FS_NAME/app/linux-launcher-x86 /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME
fi
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
# dh_installdeb will replace this with shell code automatically
# generated by other debhelper scripts.
#DEBHELPER#
exit 0

View File

@@ -0,0 +1,54 @@
Summary: APPLICATION_SUMMARY
Name: APPLICATION_PACKAGE
Version: APPLICATION_VERSION
Release: 1
License: APPLICATION_LICENSE_TYPE
Vendor: APPLICATION_VENDOR
Prefix: /opt
Provides: APPLICATION_PACKAGE
Requires: ld-linux.so.2 libX11.so.6 libXext.so.6 libXi.so.6 libXrender.so.1 libXtst.so.6 libasound.so.2 libc.so.6 libdl.so.2 libgcc_s.so.1 libm.so.6 libpthread.so.0 libthread_db.so.1
Autoprov: 0
Autoreq: 0
#avoid ARCH subfolder
%define _rpmfilename %%{NAME}-%%{VERSION}-%%{RELEASE}.%%{ARCH}.rpm
#comment line below to enable effective jar compression
#it could easily get your package size from 40 to 15Mb but
#build time will substantially increase and it may require unpack200/system java to install
%define __jar_repack %{nil}
%description
APPLICATION_DESCRIPTION
%prep
%build
%install
rm -rf %{buildroot}
mkdir -p %{buildroot}/opt
cp -r %{_sourcedir}/APPLICATION_FS_NAME %{buildroot}/opt
%files
APPLICATION_LICENSE_FILE
/opt/APPLICATION_FS_NAME
%post
SECONDARY_LAUNCHERS_INSTALL
APP_CDS_CACHE
xdg-desktop-menu install --novendor /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME.desktop
FILE_ASSOCIATION_INSTALL
rm /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME
if [ $(uname -m) = "x86_64" ]; then
mv /opt/APPLICATION_FS_NAME/app/linux-launcher-x64 /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME
else
mv /opt/APPLICATION_FS_NAME/app/linux-launcher-x86 /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME
fi
%preun
SECONDARY_LAUNCHERS_REMOVE
xdg-desktop-menu uninstall --novendor /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME.desktop
FILE_ASSOCIATION_REMOVE
%clean

View File

@@ -10,17 +10,26 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.3d</version>
<version>1.1.2</version>
</parent>
<artifactId>commons-test</artifactId>
<name>Cryptomator common test dependencies</name>
<description>Shared utilities for tests</description>
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>commons</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</dependency>
<dependency>
<groupId>de.bechte.junit</groupId>
<artifactId>junit-hierarchicalcontextrunner</artifactId>
@@ -29,11 +38,6 @@
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>commons</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -10,23 +10,45 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.3d</version>
<version>1.1.2</version>
</parent>
<artifactId>commons</artifactId>
<name>Cryptomator common</name>
<description>Shared utilities</description>
<dependencies>
<!-- Libs -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</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>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>de.bechte.junit</groupId>
<artifactId>junit-hierarchicalcontextrunner</artifactId>
@@ -38,4 +60,13 @@
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,21 @@
package org.cryptomator.common;
import java.util.Comparator;
import javax.inject.Named;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module
public class CommonsModule {
@Provides
@Singleton
@Named("SemVer")
Comparator<String> providesSemVerComparator() {
return new SemVerComparator();
}
}

View File

@@ -6,7 +6,7 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.ui.util;
package org.cryptomator.common;
import java.util.Comparator;

View File

@@ -6,10 +6,11 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.ui.util;
package org.cryptomator.common;
import java.util.Comparator;
import org.cryptomator.common.SemVerComparator;
import org.junit.Assert;
import org.junit.Test;

View File

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

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2015 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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.1.2</version>
</parent>
<artifactId>filesystem-charsets</artifactId>
<name>Cryptomator filesystem: Charset compatibility layer</name>
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-api</artifactId>
</dependency>
<!-- Tests -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>commons-test</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-inmemory</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,32 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.filesystem.charsets;
import java.io.UncheckedIOException;
import java.text.Normalizer;
import java.text.Normalizer.Form;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.delegating.DelegatingFile;
class NormalizedNameFile extends DelegatingFile<NormalizedNameFolder> {
private final Form displayForm;
public NormalizedNameFile(NormalizedNameFolder parent, File delegate, Form displayForm) {
super(parent, delegate);
this.displayForm = displayForm;
}
@Override
public String name() throws UncheckedIOException {
return Normalizer.normalize(super.name(), displayForm);
}
}

View File

@@ -0,0 +1,27 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.filesystem.charsets;
import java.text.Normalizer.Form;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.delegating.DelegatingFileSystem;
public class NormalizedNameFileSystem extends NormalizedNameFolder implements DelegatingFileSystem {
public NormalizedNameFileSystem(Folder delegate, Form displayForm) {
super(null, delegate, displayForm);
}
@Override
public Folder getDelegate() {
return delegate;
}
}

View File

@@ -0,0 +1,76 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.filesystem.charsets;
import java.io.UncheckedIOException;
import java.text.Normalizer;
import java.text.Normalizer.Form;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.delegating.DelegatingFolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class NormalizedNameFolder extends DelegatingFolder<NormalizedNameFolder, NormalizedNameFile> {
private static final Logger LOG = LoggerFactory.getLogger(NormalizedNameFolder.class);
private final Form displayForm;
public NormalizedNameFolder(NormalizedNameFolder parent, Folder delegate, Form displayForm) {
super(parent, delegate);
this.displayForm = displayForm;
}
@Override
public String name() throws UncheckedIOException {
return Normalizer.normalize(super.name(), displayForm);
}
@Override
public NormalizedNameFile file(String name) throws UncheckedIOException {
String nfcName = Normalizer.normalize(name, Form.NFC);
String nfdName = Normalizer.normalize(name, Form.NFD);
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).");
} else if (!nfcName.equals(nfdName) && !nfcFile.exists() && nfdFile.exists()) {
LOG.info("Moving file from \"" + nfcName + "\" (NFD) to \"" + nfdName + "\" (NFC).");
nfdFile.moveTo(nfcFile);
}
return nfcFile;
}
@Override
protected NormalizedNameFile newFile(File delegate) {
return new NormalizedNameFile(this, delegate, displayForm);
}
@Override
public NormalizedNameFolder folder(String name) throws UncheckedIOException {
String nfcName = Normalizer.normalize(name, Form.NFC);
String nfdName = Normalizer.normalize(name, Form.NFD);
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).");
} else if (!nfcName.equals(nfdName) && !nfcFolder.exists() && nfdFolder.exists()) {
LOG.info("Moving folder from \"" + nfcName + "\" (NFD) to \"" + nfdName + "\" (NFC).");
nfdFolder.moveTo(nfcFolder);
}
return nfcFolder;
}
@Override
protected NormalizedNameFolder newFolder(Folder delegate) {
return new NormalizedNameFolder(this, delegate, displayForm);
}
}

View File

@@ -0,0 +1,16 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
/**
* Makes sure, the filesystems wrapped by this filesystem work only on UTF-8 encoded file paths using Normalization Form C.
* Filesystems wrapping this file system, on the other hand, will get filenames reported in a specified Normalization Form.
* It is recommended to use NFD for OS X and NFC for other operating systems.
* When looking for a file or folder with a name given in either form, both possibilities are considered
* and files/folders stored in NFD are automatically migrated to NFC.
*/
package org.cryptomator.filesystem.charsets;

View File

@@ -0,0 +1,90 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.filesystem.charsets;
import java.nio.ByteBuffer;
import java.text.Normalizer.Form;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.WritableFile;
import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
import org.junit.Assert;
import org.junit.Test;
public class NormalizedNameFileSystemTest {
@Test
public void testFileMigration() {
FileSystem inMemoryFs = new InMemoryFileSystem();
try (WritableFile writable = inMemoryFs.file("\u006F\u0302").openWritable()) {
writable.write(ByteBuffer.allocate(0));
}
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
Assert.assertTrue(normalizationFs.file("\u00F4").exists());
Assert.assertTrue(normalizationFs.file("\u006F\u0302").exists());
Assert.assertFalse(inMemoryFs.file("\u006F\u0302").exists());
Assert.assertTrue(inMemoryFs.file("\u00F4").exists());
}
@Test
public void testNoFileMigration() {
FileSystem inMemoryFs = new InMemoryFileSystem();
try (WritableFile writable = inMemoryFs.file("\u00F4").openWritable()) {
writable.write(ByteBuffer.allocate(0));
}
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
Assert.assertTrue(normalizationFs.file("\u00F4").exists());
Assert.assertTrue(normalizationFs.file("\u006F\u0302").exists());
Assert.assertFalse(inMemoryFs.file("\u006F\u0302").exists());
Assert.assertTrue(inMemoryFs.file("\u00F4").exists());
}
@Test
public void testFolderMigration() {
FileSystem inMemoryFs = new InMemoryFileSystem();
inMemoryFs.folder("\u006F\u0302").create();
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
Assert.assertTrue(normalizationFs.folder("\u00F4").exists());
Assert.assertTrue(normalizationFs.folder("\u006F\u0302").exists());
Assert.assertFalse(inMemoryFs.folder("\u006F\u0302").exists());
Assert.assertTrue(inMemoryFs.folder("\u00F4").exists());
}
@Test
public void testNoFolderMigration() {
FileSystem inMemoryFs = new InMemoryFileSystem();
inMemoryFs.folder("\u00F4").create();
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
Assert.assertTrue(normalizationFs.folder("\u00F4").exists());
Assert.assertTrue(normalizationFs.folder("\u006F\u0302").exists());
Assert.assertFalse(inMemoryFs.folder("\u006F\u0302").exists());
Assert.assertTrue(inMemoryFs.folder("\u00F4").exists());
}
@Test
public void testNfcDisplayNames() {
FileSystem inMemoryFs = new InMemoryFileSystem();
inMemoryFs.folder("a\u00F4").create();
inMemoryFs.folder("b\u006F\u0302").create();
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
Assert.assertEquals("a\u00F4", normalizationFs.folder("a\u00F4").name());
Assert.assertEquals("b\u00F4", normalizationFs.folder("b\u006F\u0302").name());
}
@Test
public void testNfdDisplayNames() {
FileSystem inMemoryFs = new InMemoryFileSystem();
inMemoryFs.folder("a\u00F4").create();
inMemoryFs.folder("b\u006F\u0302").create();
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFD);
Assert.assertEquals("a\u006F\u0302", normalizationFs.folder("a\u00F4").name());
Assert.assertEquals("b\u006F\u0302", normalizationFs.folder("b\u006F\u0302").name());
}
}

View File

@@ -0,0 +1,48 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.filesystem.charsets;
import java.text.Normalizer.Form;
import org.cryptomator.filesystem.File;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
public class NormalizedNameFileTest {
private File delegateNfc;
private File delegateNfd;
@Before
public void setup() {
delegateNfc = Mockito.mock(File.class);
delegateNfd = Mockito.mock(File.class);
Mockito.when(delegateNfc.name()).thenReturn("\u00C5");
Mockito.when(delegateNfd.name()).thenReturn("\u0041\u030A");
}
@Test
public void testDisplayNameInNfc() {
File file1 = new NormalizedNameFile(null, delegateNfc, Form.NFC);
File file2 = new NormalizedNameFile(null, delegateNfd, Form.NFC);
Assert.assertEquals("\u00C5", file1.name());
Assert.assertEquals("\u00C5", file2.name());
}
@Test
public void testDisplayNameInNfd() {
File file1 = new NormalizedNameFile(null, delegateNfc, Form.NFD);
File file2 = new NormalizedNameFile(null, delegateNfd, Form.NFD);
Assert.assertEquals("\u0041\u030A", file1.name());
Assert.assertEquals("\u0041\u030A", file2.name());
}
}

View File

@@ -0,0 +1,149 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.filesystem.charsets;
import java.text.Normalizer.Form;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.Folder;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
public class NormalizedNameFolderTest {
private Folder delegate;
private File delegateSubFileNfc;
private File delegateSubFileNfd;
private Folder delegateSubFolderNfc;
private Folder delegateSubFolderNfd;
@Before
public void setup() {
delegate = Mockito.mock(Folder.class);
delegateSubFileNfc = Mockito.mock(File.class);
delegateSubFileNfd = Mockito.mock(File.class);
Mockito.when(delegate.file("\u00C5")).thenReturn(delegateSubFileNfc);
Mockito.when(delegateSubFileNfc.name()).thenReturn("\u00C5");
Mockito.when(delegate.file("\u0041\u030A")).thenReturn(delegateSubFileNfd);
Mockito.when(delegateSubFileNfd.name()).thenReturn("\u0041\u030A");
delegateSubFolderNfc = Mockito.mock(Folder.class);
delegateSubFolderNfd = Mockito.mock(Folder.class);
Mockito.when(delegate.folder("\u00F4")).thenReturn(delegateSubFolderNfc);
Mockito.when(delegateSubFolderNfc.name()).thenReturn("\u00F4");
Mockito.when(delegate.folder("\u006F\u0302")).thenReturn(delegateSubFolderNfd);
Mockito.when(delegateSubFolderNfd.name()).thenReturn("\u006F\u0302");
}
@Test
public void testDisplayNameInNfc() {
Folder folder1 = new NormalizedNameFolder(null, delegateSubFolderNfc, Form.NFC);
Folder folder2 = new NormalizedNameFolder(null, delegateSubFolderNfd, Form.NFC);
Assert.assertEquals("\u00F4", folder1.name());
Assert.assertEquals("\u00F4", folder2.name());
}
@Test
public void testDisplayNameInNfd() {
Folder folder1 = new NormalizedNameFolder(null, delegateSubFolderNfc, Form.NFD);
Folder folder2 = new NormalizedNameFolder(null, delegateSubFolderNfd, Form.NFD);
Assert.assertEquals("\u006F\u0302", folder1.name());
Assert.assertEquals("\u006F\u0302", folder2.name());
}
@Test
public void testNoFolderMigration1() {
Mockito.when(delegateSubFolderNfc.exists()).thenReturn(true);
Mockito.when(delegateSubFolderNfd.exists()).thenReturn(false);
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
Folder sub1 = folder.folder("\u00F4");
Folder sub2 = folder.folder("\u006F\u0302");
Mockito.verify(delegateSubFolderNfd, Mockito.never()).moveTo(Mockito.any());
Assert.assertSame(sub1, sub2);
}
@Test
public void testNoFolderMigration2() {
Mockito.when(delegateSubFolderNfc.exists()).thenReturn(true);
Mockito.when(delegateSubFolderNfd.exists()).thenReturn(true);
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
Folder sub1 = folder.folder("\u00F4");
Folder sub2 = folder.folder("\u006F\u0302");
Mockito.verify(delegateSubFolderNfd, Mockito.never()).moveTo(Mockito.any());
Assert.assertSame(sub1, sub2);
}
@Test
public void testNoFolderMigration3() {
Mockito.when(delegateSubFolderNfc.exists()).thenReturn(false);
Mockito.when(delegateSubFolderNfd.exists()).thenReturn(false);
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
Folder sub1 = folder.folder("\u00F4");
Folder sub2 = folder.folder("\u006F\u0302");
Mockito.verify(delegateSubFolderNfd, Mockito.never()).moveTo(Mockito.any());
Assert.assertSame(sub1, sub2);
}
@Test
public void testFolderMigration() {
Mockito.when(delegateSubFolderNfc.exists()).thenReturn(false);
Mockito.when(delegateSubFolderNfd.exists()).thenReturn(true);
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
Folder sub1 = folder.folder("\u00F4");
Mockito.verify(delegateSubFolderNfd).moveTo(delegateSubFolderNfc);
Folder sub2 = folder.folder("\u006F\u0302");
Assert.assertSame(sub1, sub2);
}
@Test
public void testNoFileMigration1() {
Mockito.when(delegateSubFileNfc.exists()).thenReturn(true);
Mockito.when(delegateSubFileNfd.exists()).thenReturn(false);
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
File sub1 = folder.file("\u00C5");
File sub2 = folder.file("\u0041\u030A");
Mockito.verify(delegateSubFileNfd, Mockito.never()).moveTo(Mockito.any());
Assert.assertSame(sub1, sub2);
}
@Test
public void testNoFileMigration2() {
Mockito.when(delegateSubFileNfc.exists()).thenReturn(true);
Mockito.when(delegateSubFileNfd.exists()).thenReturn(true);
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
File sub1 = folder.file("\u00C5");
File sub2 = folder.file("\u0041\u030A");
Mockito.verify(delegateSubFileNfd, Mockito.never()).moveTo(Mockito.any());
Assert.assertSame(sub1, sub2);
}
@Test
public void testNoFileMigration3() {
Mockito.when(delegateSubFileNfc.exists()).thenReturn(false);
Mockito.when(delegateSubFileNfd.exists()).thenReturn(false);
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
File sub1 = folder.file("\u00C5");
File sub2 = folder.file("\u0041\u030A");
Mockito.verify(delegateSubFileNfd, Mockito.never()).moveTo(Mockito.any());
Assert.assertSame(sub1, sub2);
}
@Test
public void testFileMigration() {
Mockito.when(delegateSubFileNfc.exists()).thenReturn(false);
Mockito.when(delegateSubFileNfd.exists()).thenReturn(true);
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
File sub1 = folder.file("\u00C5");
Mockito.verify(delegateSubFileNfd).moveTo(delegateSubFileNfc);
File sub2 = folder.file("\u0041\u030A");
Assert.assertSame(sub1, sub2);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,16 @@
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 Integer CURRENT_VAULT_VERSION = 3;
static final Collection<Integer> SUPPORTED_VAULT_VERSIONS = Collections.unmodifiableCollection(Arrays.asList(3, 4));
static final Integer CURRENT_VAULT_VERSION = 4;
public static final int PAYLOAD_SIZE = 32 * 1024;
public static final int NONCE_SIZE = 16;

View File

@@ -9,6 +9,7 @@
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;
@@ -18,6 +19,7 @@ import java.security.SecureRandom;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicReference;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
@@ -81,14 +83,15 @@ class CryptorImpl implements Cryptor {
@Override
public void randomizeMasterkey() {
final byte[] randomBytes = new byte[KEYLENGTH_IN_BYTES];
try {
randomSource.nextBytes(randomBytes);
encryptionKey = new SecretKeySpec(randomBytes, ENCRYPTION_ALG);
randomSource.nextBytes(randomBytes);
macKey = new SecretKeySpec(randomBytes, ENCRYPTION_ALG);
} finally {
Arrays.fill(randomBytes, (byte) 0x00);
KeyGenerator encKeyGen = KeyGenerator.getInstance(ENCRYPTION_ALG);
encKeyGen.init(KEYLENGTH_IN_BYTES * Byte.SIZE, randomSource);
encryptionKey = encKeyGen.generateKey();
KeyGenerator macKeyGen = KeyGenerator.getInstance(MAC_ALG);
macKeyGen.init(KEYLENGTH_IN_BYTES * Byte.SIZE, randomSource);
macKey = macKeyGen.generateKey();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Hard-coded algorithm doesn't exist.", e);
}
}
@@ -107,7 +110,7 @@ class CryptorImpl implements Cryptor {
assert keyFile != null;
// check version
if (!CURRENT_VAULT_VERSION.equals(keyFile.getVersion())) {
if (!SUPPORTED_VAULT_VERSIONS.contains(keyFile.getVersion())) {
throw new UnsupportedVaultFormatException(keyFile.getVersion(), CURRENT_VAULT_VERSION);
}
@@ -116,12 +119,12 @@ class CryptorImpl implements Cryptor {
final SecretKey kek = new SecretKeySpec(kekBytes, ENCRYPTION_ALG);
this.macKey = AesKeyWrap.unwrap(kek, keyFile.getMacMasterKey(), MAC_ALG);
// future use (as soon as we need to prevent downgrade attacks):
// final Mac mac = new ThreadLocalMac(macKey, MAC_ALG).get();
// final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.BYTES).putInt(CURRENT_VAULT_VERSION).array());
// if (!MessageDigest.isEqual(versionMac, keyFile.getVersionMac())) {
// destroyQuietly(macKey);
// throw new UnsupportedVaultFormatException(Integer.MAX_VALUE, CURRENT_VAULT_VERSION);
// }
// final Mac mac = new ThreadLocalMac(macKey, MAC_ALG).get();
// final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.BYTES).putInt(CURRENT_VAULT_VERSION).array());
// if (!MessageDigest.isEqual(versionMac, keyFile.getVersionMac())) {
// destroyQuietly(macKey);
// throw new UnsupportedVaultFormatException(Integer.MAX_VALUE, CURRENT_VAULT_VERSION);
// }
this.encryptionKey = AesKeyWrap.unwrap(kek, keyFile.getEncryptionMasterKey(), ENCRYPTION_ALG);
} catch (InvalidKeyException e) {
throw new InvalidPassphraseException();

View File

@@ -11,12 +11,14 @@ package org.cryptomator.crypto.engine.impl;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;
@@ -36,13 +38,13 @@ class FileHeaderPayload implements Destroyable {
private final SecretKey contentKey;
public FileHeaderPayload(SecureRandom randomSource) {
filesize = 0;
final byte[] contentKey = new byte[CONTENT_KEY_LEN];
this.filesize = 0;
try {
randomSource.nextBytes(contentKey);
this.contentKey = new SecretKeySpec(contentKey, AES);
} finally {
Arrays.fill(contentKey, (byte) 0x00);
KeyGenerator keyGen = KeyGenerator.getInstance(AES);
keyGen.init(CONTENT_KEY_LEN * Byte.SIZE, randomSource);
this.contentKey = keyGen.generateKey();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Hard-coded algorithm doesn't exist.", e);
}
}

View File

@@ -12,6 +12,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.regex.Pattern;
import javax.crypto.AEADBadTagException;
import javax.crypto.SecretKey;
@@ -25,6 +26,8 @@ import org.cryptomator.siv.SivMode;
class FilenameCryptorImpl implements FilenameCryptor {
private static final BaseNCodec BASE32 = new Base32();
// https://tools.ietf.org/html/rfc4648#section-6
private static final Pattern BASE32_PATTERN = Pattern.compile("([A-Z2-7]{8})*[A-Z2-7=]{8}");
private static final ThreadLocal<MessageDigest> SHA1 = new ThreadLocalSha1();
private static final ThreadLocal<SivMode> AES_SIV = new ThreadLocal<SivMode>() {
@Override
@@ -50,8 +53,8 @@ class FilenameCryptorImpl implements FilenameCryptor {
}
@Override
public boolean isEncryptedFilename(String ciphertextName) {
return BASE32.isInAlphabet(ciphertextName);
public Pattern encryptedNamePattern() {
return BASE32_PATTERN;
}
@Override

View File

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

View File

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

View File

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

View File

@@ -8,8 +8,6 @@
*******************************************************************************/
package org.cryptomator.filesystem.crypto;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.UncheckedIOException;
import java.nio.file.FileAlreadyExistsException;
import java.util.Optional;
@@ -27,9 +25,7 @@ class CryptoFile extends CryptoNode implements File {
@Override
protected Optional<String> encryptedName() {
return parent().get().getDirectoryId().map(s -> s.getBytes(UTF_8)).map(parentDirId -> {
return cryptor.getFilenameCryptor().encryptFilename(name(), parentDirId);
});
return parent().get().encryptChildName(name());
}
@Override

View File

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

View File

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

View File

@@ -21,20 +21,20 @@ public class CryptorImplTest {
@Test
public void testMasterkeyDecryptionWithCorrectPassphrase() throws IOException {
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
final String testMasterKey = "{\"version\":4,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
}
@Test(expected = InvalidPassphraseException.class)
public void testMasterkeyDecryptionWithWrongPassphrase() throws IOException {
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
final String testMasterKey = "{\"version\":4,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "qwe");
}
@@ -44,7 +44,7 @@ public class CryptorImplTest {
final String testMasterKey = "{\"version\":-1,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
}
@@ -62,23 +62,24 @@ public class CryptorImplTest {
@Ignore
@Test(expected = UnsupportedVaultFormatException.class)
public void testMasterkeyDecryptionWithWrongVersionMac() throws IOException {
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
final String testMasterKey = "{\"version\":4,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLa=\"}";
+ "\"versionMac\":\"z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfoK=\"}";
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
}
@Test
public void testMasterkeyEncryption() throws IOException {
final String expectedMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," //
final String expectedMasterKey = "{\"version\":4,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
+ "\"hmacMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
+ "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
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

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

View File

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

View File

@@ -9,7 +9,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.3d</version>
<version>1.1.2</version>
</parent>
<artifactId>filesystem-invariants-tests</artifactId>
<name>Cryptomator filesystem: Invariants tests</name>
@@ -20,6 +20,10 @@
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-api</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-charsets</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-crypto</artifactId>

View File

@@ -4,11 +4,13 @@ import static org.cryptomator.common.test.TempFilesRemovedOnShutdown.createTempD
import java.io.IOException;
import java.io.UncheckedIOException;
import java.text.Normalizer.Form;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.charsets.NormalizedNameFileSystem;
import org.cryptomator.filesystem.crypto.CryptoEngineTestModule;
import org.cryptomator.filesystem.crypto.CryptoFileSystemDelegate;
import org.cryptomator.filesystem.crypto.CryptoFileSystemTestComponent;
@@ -35,8 +37,10 @@ class FileSystemFactories implements Iterable<FileSystemFactory> {
add("ShorteningFileSystem > InMemoryFileSystem", this::createShorteningFileSystemInMemory);
add("StatsFileSystem > NioFileSystem", this::createStatsFileSystemNio);
add("StatsFileSystem > InMemoryFileSystem", this::createStatsFileSystemInMemory);
add("StatsFileSystem > CryptoFileSystem > ShorteningFileSystem > InMemoryFileSystem", this::createCompoundFileSystemInMemory);
add("StatsFileSystem > CryptoFileSystem > ShorteningFileSystem > NioFileSystem", this::createCompoundFileSystemNio);
add("NormalizingFileSystem > NioFileSystem", this::createNormalizingFileSystemNio);
add("NormalizingFileSystem > InMemoryFileSystem", this::createNormalizingFileSystemInMemory);
add("StatsFileSystem > NormalizingFileSystem > CryptoFileSystem > ShorteningFileSystem > InMemoryFileSystem", this::createCompoundFileSystemInMemory);
add("StatsFileSystem > NormalizingFileSystem > CryptoFileSystem > ShorteningFileSystem > NioFileSystem", this::createCompoundFileSystemNio);
}
private FileSystem createCryptoFileSystemInMemory() {
@@ -63,6 +67,14 @@ class FileSystemFactories implements Iterable<FileSystemFactory> {
return createStatsFileSystem(createInMemoryFileSystem());
}
private FileSystem createNormalizingFileSystemNio() {
return createNormalizingFileSystem(createInMemoryFileSystem());
}
private FileSystem createNormalizingFileSystemInMemory() {
return createNormalizingFileSystem(createInMemoryFileSystem());
}
private FileSystem createCompoundFileSystemNio() {
return createCompoundFileSystem(createNioFileSystem());
}
@@ -84,13 +96,17 @@ class FileSystemFactories implements Iterable<FileSystemFactory> {
}
private FileSystem createCompoundFileSystem(FileSystem delegate) {
return createStatsFileSystem(createCryptoFileSystem(createShorteningFileSystem(delegate)));
return createStatsFileSystem(createNormalizingFileSystem(createCryptoFileSystem(createShorteningFileSystem(delegate))));
}
private FileSystem createStatsFileSystem(FileSystem delegate) {
return new StatsFileSystem(delegate);
}
private FileSystem createNormalizingFileSystem(FileSystem delegate) {
return new NormalizedNameFileSystem(delegate, Form.NFC);
}
private FileSystem createCryptoFileSystem(FileSystem delegate) {
CRYPTO_FS_COMP.cryptoFileSystemFactory().initializeNew(delegate, "aPassphrase");
return CRYPTO_FS_COMP.cryptoFileSystemFactory().unlockExisting(delegate, "aPassphrase", Mockito.mock(CryptoFileSystemDelegate.class));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.3d</version>
<version>1.1.2</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.0.3d</version>
<version>1.1.2</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.0.3d</version>
<version>1.1.2</version>
</parent>
<artifactId>frontend-webdav</artifactId>
<name>Cryptomator frontend: WebDAV frontend</name>

View File

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

View File

@@ -32,7 +32,7 @@ public class WindowsCompatibilityServlet extends HttpServlet {
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.addHeader("DAV", "1, 2");
resp.addHeader("MS-Author-Via", "DAV");
// resp.addHeader("Allow", "OPTIONS, GET, HEAD, POST, TRACE, PROPFIND, PROPPATCH, MKCOL, COPY, PUT, DELETE, MOVE, LOCK, UNLOCK");
resp.addHeader("Allow", "OPTIONS, GET, HEAD");
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
}

View File

@@ -12,11 +12,13 @@ package org.cryptomator.frontend.webdav.mount;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Comparator;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.commons.io.IOUtils;
@@ -26,15 +28,18 @@ import org.cryptomator.frontend.CommandFailedException;
import org.cryptomator.frontend.Frontend.MountParam;
@Singleton
final class MacOsXWebDavMounter implements WebDavMounterStrategy {
final class MacOsXAppleScriptWebDavMounter implements WebDavMounterStrategy {
private final Comparator<String> semVerComparator;
@Inject
MacOsXWebDavMounter() {
MacOsXAppleScriptWebDavMounter(@Named("SemVer") Comparator<String> semVerComparator) {
this.semVerComparator = semVerComparator;
}
@Override
public boolean shouldWork() {
return SystemUtils.IS_OS_MAC_OSX;
return SystemUtils.IS_OS_MAC_OSX && semVerComparator.compare(SystemUtils.OS_VERSION, "10.10") >= 0;
}
@Override

View File

@@ -0,0 +1,89 @@
/*******************************************************************************
* Copyright (c) 2014, 2016 Sebastian Stenzel, Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation, strategy fine tuning
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
******************************************************************************/
package org.cryptomator.frontend.webdav.mount;
import java.net.URI;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.util.Comparator;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.frontend.CommandFailedException;
import org.cryptomator.frontend.Frontend.MountParam;
import org.cryptomator.frontend.webdav.mount.command.Script;
@Singleton
final class MacOsXShellScriptWebDavMounter implements WebDavMounterStrategy {
private final Comparator<String> semVerComparator;
@Inject
MacOsXShellScriptWebDavMounter(@Named("SemVer") Comparator<String> semVerComparator) {
this.semVerComparator = semVerComparator;
}
@Override
public boolean shouldWork() {
return SystemUtils.IS_OS_MAC_OSX && semVerComparator.compare(SystemUtils.OS_VERSION, "10.10") < 0;
}
@Override
public void warmUp(int serverPort) {
// no-op
}
@Override
public WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) throws CommandFailedException {
final String mountName = mountParams.getOrDefault(MountParam.MOUNT_NAME, Optional.empty()).orElseThrow(() -> {
return new IllegalArgumentException("Missing mount parameter MOUNT_NAME.");
});
// we don't use the uri to derive a path, as it *could* be longer than 255 chars.
final String path = "/Volumes/Cryptomator_" + UUID.randomUUID().toString();
final Script mountScript = Script.fromLines("mkdir \"$MOUNT_PATH\"", "mount_webdav -S -v $MOUNT_NAME \"$DAV_AUTHORITY$DAV_PATH\" \"$MOUNT_PATH\"").addEnv("DAV_AUTHORITY", uri.getRawAuthority())
.addEnv("DAV_PATH", uri.getRawPath()).addEnv("MOUNT_PATH", path).addEnv("MOUNT_NAME", mountName);
mountScript.execute();
return new MacWebDavMount(path);
}
private static class MacWebDavMount extends AbstractWebDavMount {
private final String mountPath;
private final Script revealScript;
private final Script unmountScript;
private MacWebDavMount(String mountPath) {
this.mountPath = mountPath;
this.revealScript = Script.fromLines("open \"$MOUNT_PATH\"").addEnv("MOUNT_PATH", mountPath);
this.unmountScript = Script.fromLines("diskutil umount $MOUNT_PATH").addEnv("MOUNT_PATH", mountPath);
}
@Override
public void unmount() throws CommandFailedException {
// only attempt unmount if user didn't unmount manually:
if (Files.exists(FileSystems.getDefault().getPath(mountPath))) {
unmountScript.execute();
}
}
@Override
public void reveal() throws CommandFailedException {
revealScript.execute();
}
}
}

View File

@@ -19,74 +19,87 @@ import javax.inject.Singleton;
@Singleton
class MountStrategies implements Collection<WebDavMounterStrategy> {
private final Collection<WebDavMounterStrategy> delegate;
@Inject
MountStrategies(LinuxGvfsWebDavMounter linuxMounter, MacOsXWebDavMounter osxMounter, WindowsWebDavMounter winMounter) {
delegate = unmodifiableList(asList(linuxMounter, osxMounter, winMounter));
MountStrategies(LinuxGvfsWebDavMounter linuxMounter, MacOsXAppleScriptWebDavMounter osxAppleScriptMounter, MacOsXShellScriptWebDavMounter osxShellScriptMounter, WindowsWebDavMounter winMounter) {
delegate = unmodifiableList(asList(linuxMounter, osxAppleScriptMounter, osxShellScriptMounter, winMounter));
}
@Override
public int size() {
return delegate.size();
}
@Override
public boolean isEmpty() {
return delegate.isEmpty();
}
@Override
public boolean contains(Object o) {
return delegate.contains(o);
}
@Override
public Iterator<WebDavMounterStrategy> iterator() {
return delegate.iterator();
}
@Override
public Object[] toArray() {
return delegate.toArray();
}
@Override
public <T> T[] toArray(T[] a) {
return delegate.toArray(a);
}
@Override
public boolean add(WebDavMounterStrategy e) {
return delegate.add(e);
}
@Override
public boolean remove(Object o) {
return delegate.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return delegate.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends WebDavMounterStrategy> c) {
return delegate.addAll(c);
}
@Override
public boolean removeAll(Collection<?> c) {
return delegate.removeAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
return delegate.retainAll(c);
}
@Override
public void clear() {
delegate.clear();
}
@Override
public boolean equals(Object o) {
return delegate.equals(o);
}
@Override
public int hashCode() {
return delegate.hashCode();
}
}

View File

@@ -11,10 +11,16 @@ package org.cryptomator.frontend.webdav.mount;
import static org.cryptomator.frontend.webdav.mount.command.Script.fromLines;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -22,12 +28,16 @@ import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.frontend.CommandFailedException;
import org.cryptomator.frontend.Frontend.MountParam;
import org.cryptomator.frontend.webdav.mount.command.CommandResult;
import org.cryptomator.frontend.webdav.mount.command.Script;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A {@link WebDavMounterStrategy} utilizing the "net use" command.
@@ -37,7 +47,9 @@ import org.cryptomator.frontend.webdav.mount.command.Script;
@Singleton
final class WindowsWebDavMounter implements WebDavMounterStrategy {
private static final Logger LOG = LoggerFactory.getLogger(WindowsWebDavMounter.class);
private static final Pattern WIN_MOUNT_DRIVELETTER_PATTERN = Pattern.compile("\\s*([A-Z]):\\s*");
private static final Pattern REG_QUERY_PROXY_OVERRIDES_PATTERN = Pattern.compile("\\s*ProxyOverride\\s+REG_SZ\\s+(.*)\\s*");
private static final String AUTO_ASSIGN_DRIVE_LETTER = "*";
private static final String LOCALHOST = "localhost";
private static final int MOUNT_TIMEOUT_SECONDS = 60;
@@ -60,12 +72,12 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
@Override
public WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) throws CommandFailedException {
final String driveLetter = mountParams.getOrDefault(MountParam.WIN_DRIVE_LETTER, Optional.of(AUTO_ASSIGN_DRIVE_LETTER)).orElse(AUTO_ASSIGN_DRIVE_LETTER);
final String driveLetter = mountParams.getOrDefault(MountParam.WIN_DRIVE_LETTER, Optional.empty()).orElse(AUTO_ASSIGN_DRIVE_LETTER);
if (driveLetters.getOccupiedDriveLetters().contains(CharUtils.toChar(driveLetter))) {
throw new CommandFailedException("Drive letter occupied.");
}
final String hostname = mountParams.getOrDefault(MountParam.HOSTNAME, Optional.of(LOCALHOST)).orElse(LOCALHOST);
final String hostname = mountParams.getOrDefault(MountParam.HOSTNAME, Optional.empty()).orElse(LOCALHOST);
try {
final URI adjustedUri = new URI(uri.getScheme(), uri.getUserInfo(), hostname, uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment());
CommandResult mountResult = mount(adjustedUri, driveLetter);
@@ -74,14 +86,14 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
throw new IllegalArgumentException("Invalid host: " + hostname);
}
}
private CommandResult mount(URI uri, String driveLetter) throws CommandFailedException {
final Script proxyBypassScript = fromLines(
"reg add \"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v \"ProxyOverride\" /d \"<local>;%DAV_HOST%;%DAV_HOST%:%DAV_PORT%\" /f");
proxyBypassScript.addEnv("DAV_HOST", uri.getHost());
proxyBypassScript.addEnv("DAV_PORT", String.valueOf(uri.getPort()));
proxyBypassScript.execute();
try {
addProxyOverrides(uri);
} catch (IOException e) {
throw new CommandFailedException(e);
}
final String driveLetterStr = AUTO_ASSIGN_DRIVE_LETTER.equals(driveLetter) ? AUTO_ASSIGN_DRIVE_LETTER : driveLetter + ":";
final Script mountScript = fromLines("net use %DRIVE_LETTER% \\\\%DAV_HOST%@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no");
mountScript.addEnv("DRIVE_LETTER", driveLetterStr);
@@ -90,6 +102,44 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
mountScript.addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\'));
return mountScript.execute(MOUNT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
private void addProxyOverrides(URI uri) throws IOException, CommandFailedException {
try {
// get existing value for ProxyOverride key from reqistry:
ProcessBuilder query = new ProcessBuilder("reg", "query", "\"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\"", "/v", "ProxyOverride");
Process queryCmd = query.start();
String queryStdOut = IOUtils.toString(queryCmd.getInputStream(), StandardCharsets.UTF_8);
int queryResult = queryCmd.waitFor();
// determine new value for ProxyOverride key:
Set<String> overrides = new HashSet<>();
Matcher matcher = REG_QUERY_PROXY_OVERRIDES_PATTERN.matcher(queryStdOut);
if (queryResult == 0 && matcher.find()) {
String[] existingOverrides = StringUtils.split(matcher.group(1), ';');
overrides.addAll(Arrays.asList(existingOverrides));
}
overrides.removeIf(s -> s.startsWith(uri.getHost() + ":"));
overrides.add("<local>");
overrides.add(uri.getHost());
overrides.add(uri.getHost() + ":" + uri.getPort());
// set new value:
String overridesStr = StringUtils.join(overrides, ';');
ProcessBuilder add = new ProcessBuilder("reg", "add", "\"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\"", "/v", "ProxyOverride", "/d", "\"" + overridesStr + "\"", "/f");
LOG.debug("Invoking command: " + StringUtils.join(add.command(), ' '));
Process addCmd = add.start();
int addResult = addCmd.waitFor();
if (addResult != 0) {
String addStdErr = IOUtils.toString(addCmd.getErrorStream(), StandardCharsets.UTF_8);
throw new CommandFailedException(addStdErr);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
InterruptedIOException ioException = new InterruptedIOException();
ioException.initCause(e);
throw ioException;
}
}
private String getDriveLetter(String result) throws CommandFailedException {
final Matcher matcher = WIN_MOUNT_DRIVELETTER_PATTERN.matcher(result);

1
main/jacoco-report/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target/

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright (c) 2016 Sebastian Stenzel This file is licensed under the terms of the MIT license. See the LICENSE.txt file for more info. -->
<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.1.2</version>
</parent>
<artifactId>jacoco-report</artifactId>
<name>Cryptomator Code Coverage Report</name>
<dependencies>
<!-- Commons -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>commons</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>commons-test</artifactId>
</dependency>
<!-- Filesystem Layers -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-api</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-charsets</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-crypto</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-crypto-integration-tests</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-inmemory</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-nameshortening</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-nio</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-stats</artifactId>
</dependency>
<!-- Frontends -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>frontend-api</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>frontend-webdav</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<executions>
<execution>
<id>report-aggregate</id>
<phase>verify</phase>
<goals>
<goal>report-aggregate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -7,7 +7,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.3d</version>
<version>1.1.2</version>
<packaging>pom</packaging>
<name>Cryptomator</name>
@@ -35,21 +35,14 @@
<hamcrest.version>1.3</hamcrest.version> <!-- keep in sync with version required by JUnit -->
<commons-io.version>2.4</commons-io.version>
<commons-collections.version>4.0</commons-collections.version>
<commons-lang3.version>3.3.2</commons-lang3.version>
<commons-lang3.version>3.4</commons-lang3.version>
<commons-codec.version>1.10</commons-codec.version>
<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.0.2</dagger.version>
<dagger.version>2.4</dagger.version>
</properties>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencyManagement>
<dependencies>
<!-- modules -->
@@ -70,6 +63,11 @@
<artifactId>filesystem-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-charsets</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-nio</artifactId>
@@ -81,6 +79,12 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-invariants-tests</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-nameshortening</artifactId>
@@ -254,14 +258,6 @@
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-jul</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</dependency>
</dependencies>
<modules>
@@ -278,6 +274,7 @@
<module>frontend-api</module>
<module>frontend-webdav</module>
<module>ui</module>
<module>filesystem-charsets</module>
</modules>
<profiles>
@@ -288,6 +285,12 @@
<module>ant-kit</module>
</modules>
</profile>
<profile>
<id>test-coverage</id>
<modules>
<module>jacoco-report</module>
</modules>
</profile>
</profiles>
<build>
@@ -312,7 +315,7 @@
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.5.201505241946</version>
<version>0.7.7.201606060606</version>
<executions>
<execution>
<id>prepare-agent</id>
@@ -321,6 +324,12 @@
</goals>
</execution>
</executions>
<configuration>
<excludes>
<exclude>**/*_*</exclude>
<exclude>**/Dagger*</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</pluginManagement>
@@ -339,6 +348,9 @@
<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>

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.0.3d</version>
<version>1.1.2</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.0.3d</version>
<version>1.1.2</version>
</parent>
<artifactId>ui</artifactId>
<name>Cryptomator GUI</name>
@@ -38,6 +38,10 @@
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-crypto</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-charsets</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-stats</artifactId>
@@ -99,5 +103,12 @@
<groupId>org.cryptomator</groupId>
<artifactId>commons-test</artifactId>
</dependency>
<!-- Zxcvbn -->
<dependency>
<groupId>com.nulab-inc</groupId>
<artifactId>zxcvbn</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>
</project>

View File

@@ -36,6 +36,8 @@ public class Cryptomator {
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);
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

View File

@@ -8,13 +8,13 @@
*******************************************************************************/
package org.cryptomator.ui;
import java.util.Comparator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.inject.Named;
import javax.inject.Singleton;
import org.cryptomator.common.CommonsModule;
import org.cryptomator.crypto.engine.impl.CryptoEngineModule;
import org.cryptomator.frontend.FrontendFactory;
import org.cryptomator.frontend.webdav.WebDavServer;
@@ -24,7 +24,6 @@ import org.cryptomator.ui.model.VaultObjectMapperProvider;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.settings.SettingsProvider;
import org.cryptomator.ui.util.DeferredCloser;
import org.cryptomator.ui.util.SemVerComparator;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -33,7 +32,7 @@ import dagger.Provides;
import javafx.application.Application;
import javafx.stage.Stage;
@Module(includes = CryptoEngineModule.class)
@Module(includes = {CryptoEngineModule.class, CommonsModule.class})
class CryptomatorModule {
private final Application application;
@@ -65,13 +64,6 @@ class CryptomatorModule {
return closer;
}
@Provides
@Singleton
@Named("SemVer")
Comparator<String> provideSemVerComparator() {
return new SemVerComparator();
}
@Provides
@Singleton
@Named("VaultJsonMapper")

View File

@@ -5,6 +5,7 @@
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
* Jean-Noël Charon - implementation of github issue #56
*******************************************************************************/
package org.cryptomator.ui;
@@ -17,6 +18,8 @@ import java.awt.Toolkit;
import java.awt.TrayIcon;
import java.awt.TrayIcon.MessageType;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
@@ -75,6 +78,11 @@ class ExitUtil {
private void initTrayIconExitHandler(Runnable exitCommand) {
final TrayIcon trayIcon = createTrayIcon(exitCommand);
try {
// double clicking tray icon should open Cryptomator
if (SystemUtils.IS_OS_WINDOWS) {
trayIcon.addMouseListener(new TrayIconMouseListener());
}
SystemTray.getSystemTray().add(trayIcon);
mainWindow.setOnCloseRequest((e) -> {
if (Platform.isImplicitExit()) {
@@ -136,6 +144,7 @@ class ExitUtil {
return;
} else {
settings.setNumTrayNotifications(settings.getNumTrayNotifications() - 1);
settings.save();
}
final Runnable notificationCmd;
if (SystemUtils.IS_OS_MAC_OSX) {
@@ -167,6 +176,17 @@ class ExitUtil {
});
}
private class TrayIconMouseListener extends MouseAdapter {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) {
restoreFromTray(new ActionEvent(e.getSource(), e.getID(), e.paramString()));
}
}
}
private void restoreFromTray(ActionEvent event) {
Platform.runLater(() -> {
mainWindow.show();

View File

@@ -38,6 +38,7 @@ 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();
closer = comp.deferredCloser();

View File

@@ -5,6 +5,7 @@
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
* Jean-Noël Charon - password strength meter
*******************************************************************************/
package org.cryptomator.ui.controllers;
@@ -21,18 +22,24 @@ import org.cryptomator.crypto.engine.UnsupportedVaultFormatException;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.util.PasswordStrengthUtil;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.text.Text;
@Singleton
@@ -41,13 +48,16 @@ public class ChangePasswordController extends LocalizedFXMLViewController {
private static final Logger LOG = LoggerFactory.getLogger(ChangePasswordController.class);
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
@Inject
public ChangePasswordController(Application app, Localization localization) {
public ChangePasswordController(Application app, PasswordStrengthUtil strengthRater, Localization localization) {
super(localization);
this.app = app;
this.strengthRater = strengthRater;
}
@FXML
@@ -68,12 +78,39 @@ public class ChangePasswordController extends LocalizedFXMLViewController {
@FXML
private Hyperlink downloadsPageLink;
@FXML
private Label passwordStrengthLabel;
@FXML
private Region passwordStrengthLevel0;
@FXML
private Region passwordStrengthLevel1;
@FXML
private Region passwordStrengthLevel2;
@FXML
private Region passwordStrengthLevel3;
@FXML
private Region passwordStrengthLevel4;
@Override
public void initialize() {
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));
passwordStrengthLevel0.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(0), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel1.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(1), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel2.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(2), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel3.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(3), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel4.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(4), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
}
@Override
@@ -81,6 +118,15 @@ public class ChangePasswordController extends LocalizedFXMLViewController {
return getClass().getResource("/fxml/change_password.fxml");
}
private void vaultDidChange(Vault newVault) {
oldPasswordField.clear();
newPasswordField.clear();
retypePasswordField.clear();
// trigger "default" change to refresh key bindings:
changePasswordButton.setDefaultButton(false);
changePasswordButton.setDefaultButton(true);
}
// ****************************************
// Downloads link
// ****************************************

View File

@@ -2,9 +2,10 @@
* Copyright (c) 2014, 2016 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
* Jean-Noël Charon - password strength meter
******************************************************************************/
package org.cryptomator.ui.controllers;
@@ -20,29 +21,37 @@ import javax.inject.Singleton;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.util.PasswordStrengthUtil;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
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;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
@Singleton
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
@Inject
public InitializeController(Localization localization) {
public InitializeController(Localization localization, PasswordStrengthUtil strengthRater) {
super(localization);
this.strengthRater = strengthRater;
}
@FXML
@@ -57,11 +66,38 @@ public class InitializeController extends LocalizedFXMLViewController {
@FXML
private Label messageLabel;
@FXML
private Label passwordStrengthLabel;
@FXML
private Region passwordStrengthLevel0;
@FXML
private Region passwordStrengthLevel1;
@FXML
private Region passwordStrengthLevel2;
@FXML
private Region passwordStrengthLevel3;
@FXML
private Region passwordStrengthLevel4;
@Override
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));
passwordStrengthLevel0.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(0), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel1.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(1), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel2.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(2), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel3.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(3), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel4.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(4), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
}
@Override
@@ -69,6 +105,14 @@ public class InitializeController extends LocalizedFXMLViewController {
return getClass().getResource("/fxml/initialize.fxml");
}
private void vaultDidChange(Vault newVault) {
passwordField.clear();
retypePasswordField.clear();
// trigger "default" change to refresh key bindings:
okButton.setDefaultButton(false);
okButton.setDefaultButton(true);
}
// ****************************************
// OK button
// ****************************************

View File

@@ -102,7 +102,7 @@ public class MacWarningsController extends LocalizedFXMLViewController {
@FXML
private void didClickMoreInformationButton(ActionEvent event) {
application.getHostServices().showDocument("https://cryptomator.org/faq/#macWarning");
application.getHostServices().showDocument("https://cryptomator.freshdesk.com/support/solutions/articles/16000003666-what-does-mac-authentication-failed-mean-");
}
private void unauthenticatedResourcesDidChange(Change<? extends String> change) {

View File

@@ -5,6 +5,7 @@
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
* Jean-Noël Charon - confirmation dialog on vault removal
******************************************************************************/
package org.cryptomator.ui.controllers;
@@ -16,19 +17,22 @@ import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.apache.commons.lang3.SystemUtils;
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.settings.Localization;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.util.DialogBuilderUtil;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.monadic.MonadicBinding;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -37,18 +41,23 @@ import javafx.application.Platform;
import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings;
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;
import javafx.scene.Parent;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
@@ -70,18 +79,20 @@ public class MainController extends LocalizedFXMLViewController {
private final Provider<UnlockedController> unlockedControllerProvider;
private final Lazy<ChangePasswordController> changePasswordController;
private final Lazy<SettingsController> settingsController;
private final Lazy<UpgradeStrategies> upgradeStrategies;
private final ObjectProperty<AbstractFXMLViewController> activeController = new SimpleObjectProperty<>();
private final ObservableList<Vault> vaults;
private final ObjectProperty<Vault> selectedVault = new SimpleObjectProperty<>();
private final MonadicBinding<Boolean> isSelectedVaultUnlocked = EasyBind.select(selectedVault).selectObject(Vault::unlockedProperty);;
private final Binding<Boolean> canEditSelectedVault = EasyBind.combine(selectedVault.isNull(), isSelectedVaultUnlocked.orElse(false), Boolean::logicalOr);
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));
private final BooleanExpression canEditSelectedVault = selectedVault.isNotNull().and(isSelectedVaultUnlocked.not());
private final BooleanBinding isShowingSettings;
private final Map<Vault, UnlockedController> unlockedVaults = new HashMap<>();
@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) {
Provider<UnlockedController> unlockedControllerProvider, Lazy<ChangePasswordController> changePasswordController, Lazy<SettingsController> settingsController, Lazy<UpgradeStrategies> upgradeStrategies) {
super(localization);
this.mainWindow = mainWindow;
this.vaultFactoy = vaultFactoy;
@@ -93,7 +104,11 @@ public class MainController extends LocalizedFXMLViewController {
this.unlockedControllerProvider = unlockedControllerProvider;
this.changePasswordController = changePasswordController;
this.settingsController = settingsController;
this.upgradeStrategies = upgradeStrategies;
this.vaults = FXCollections.observableList(settings.getDirectories());
this.vaults.addListener((Change<? extends Vault> c) -> {
settings.save();
});
// derived bindings:
this.isShowingSettings = activeController.isEqualTo(settingsController.get());
@@ -102,6 +117,9 @@ public class MainController extends LocalizedFXMLViewController {
@FXML
private ContextMenu vaultListCellContextMenu;
@FXML
private MenuItem changePasswordMenuItem;
@FXML
private ContextMenu addVaultContextMenu;
@@ -132,8 +150,9 @@ public class MainController extends LocalizedFXMLViewController {
vaultList.setCellFactory(this::createDirecoryListCell);
activeController.set(welcomeController.get());
selectedVault.bind(vaultList.getSelectionModel().selectedItemProperty());
removeVaultButton.disableProperty().bind(canEditSelectedVault);
removeVaultButton.disableProperty().bind(canEditSelectedVault.not());
emptyListInstructions.visibleProperty().bind(Bindings.isEmpty(vaults));
changePasswordMenuItem.visibleProperty().bind(isSelectedVaultValid);
EasyBind.subscribe(selectedVault, this::selectedVaultDidChange);
EasyBind.subscribe(activeController, this::activeControllerDidChange);
@@ -186,7 +205,7 @@ public class MainController extends LocalizedFXMLViewController {
@FXML
private void didClickAddExistingVaults(ActionEvent event) {
final FileChooser fileChooser = new FileChooser();
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator vault", "*" + Vault.VAULT_FILE_EXTENSION));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*" + Vault.VAULT_FILE_EXTENSION));
final List<File> files = fileChooser.showOpenMultipleDialog(mainWindow);
if (files != null) {
for (final File file : files) {
@@ -224,9 +243,18 @@ public class MainController extends LocalizedFXMLViewController {
@FXML
private void didClickRemoveSelectedEntry(ActionEvent e) {
vaults.remove(selectedVault.get());
if (vaults.isEmpty()) {
activeController.set(welcomeController.get());
Alert confirmDialog = DialogBuilderUtil.buildConfirmationDialog( //
localization.getString("main.directoryList.remove.confirmation.title"), //
localization.getString("main.directoryList.remove.confirmation.header"), //
localization.getString("main.directoryList.remove.confirmation.content"), //
SystemUtils.IS_OS_MAC_OSX ? ButtonType.CANCEL : ButtonType.OK);
Optional<ButtonType> choice = confirmDialog.showAndWait();
if (ButtonType.OK.equals(choice.get())) {
vaults.remove(selectedVault.get());
if (vaults.isEmpty()) {
activeController.set(welcomeController.get());
}
}
}
@@ -263,7 +291,7 @@ public class MainController extends LocalizedFXMLViewController {
this.showUnlockedView(newValue);
} else if (!newValue.doesVaultDirectoryExist()) {
this.showNotFoundView();
} else if (newValue.isValidVaultDirectory() && newValue.needsUpgrade()) {
} else if (newValue.isValidVaultDirectory() && upgradeStrategies.get().getUpgradeStrategy(newValue).isPresent()) {
this.showUpgradeView();
} else if (newValue.isValidVaultDirectory()) {
this.showUnlockView();

View File

@@ -43,6 +43,9 @@ public class SettingsController extends LocalizedFXMLViewController {
@FXML
private TextField portField;
@FXML
private Label useIpv6Label;
@FXML
private CheckBox useIpv6Checkbox;
@@ -55,13 +58,14 @@ public class SettingsController extends LocalizedFXMLViewController {
checkForUpdatesCheckbox.setSelected(settings.isCheckForUpdatesEnabled() && !areUpdatesManagedExternally());
portField.setText(String.valueOf(settings.getPort()));
portField.addEventFilter(KeyEvent.KEY_TYPED, this::filterNumericKeyEvents);
useIpv6Checkbox.setDisable(!SystemUtils.IS_OS_WINDOWS);
useIpv6Label.setVisible(SystemUtils.IS_OS_WINDOWS);
useIpv6Checkbox.setVisible(SystemUtils.IS_OS_WINDOWS);
useIpv6Checkbox.setSelected(SystemUtils.IS_OS_WINDOWS && settings.shouldUseIpv6());
versionLabel.setText(String.format(localization.getString("settings.version.label"), applicationVersion().orElse("SNAPSHOT")));
EasyBind.subscribe(checkForUpdatesCheckbox.selectedProperty(), settings::setCheckForUpdatesEnabled);
EasyBind.subscribe(checkForUpdatesCheckbox.selectedProperty(), this::checkForUpdateDidChange);
EasyBind.subscribe(portField.textProperty(), this::portDidChange);
EasyBind.subscribe(useIpv6Checkbox.selectedProperty(), settings::setUseIpv6);
EasyBind.subscribe(useIpv6Checkbox.selectedProperty(), this::useIpv6DidChange);
}
@Override
@@ -73,21 +77,30 @@ public class SettingsController extends LocalizedFXMLViewController {
return Optional.ofNullable(getClass().getPackage().getImplementationVersion());
}
private void checkForUpdateDidChange(Boolean newValue) {
settings.setCheckForUpdatesEnabled(newValue);
settings.save();
}
private void portDidChange(String newValue) {
try {
int port = Integer.parseInt(newValue);
if (port < Settings.MIN_PORT) {
if (!settings.isPortValid(port)) {
settings.setPort(Settings.DEFAULT_PORT);
} else if (port < Settings.MAX_PORT) {
settings.setPort(port);
} else {
portField.setText(String.valueOf(Settings.MAX_PORT));
settings.setPort(port);
settings.save();
}
} catch (NumberFormatException e) {
portField.setText(String.valueOf(Settings.DEFAULT_PORT));
}
}
private void useIpv6DidChange(Boolean newValue) {
settings.setUseIpv6(newValue);
settings.save();
}
private void filterNumericKeyEvents(KeyEvent t) {
if (t.getCharacter() == null || t.getCharacter().length() == 0) {
return;

View File

@@ -119,7 +119,7 @@ public class UnlockController extends LocalizedFXMLViewController {
}
unlockButton.disableProperty().bind(passwordField.textProperty().isEmpty());
EasyBind.subscribe(vault, this::vaultChanged);
EasyBind.subscribe(vault, this::vaultDidChange);
}
@Override
@@ -127,7 +127,7 @@ public class UnlockController extends LocalizedFXMLViewController {
return getClass().getResource("/fxml/unlock.fxml");
}
private void vaultChanged(Vault newVault) {
private void vaultDidChange(Vault newVault) {
if (newVault == null) {
return;
}
@@ -149,6 +149,9 @@ public class UnlockController extends LocalizedFXMLViewController {
if (SystemUtils.IS_OS_WINDOWS) {
chooseSelectedDriveLetter();
}
// trigger "default" change to refresh key bindings:
unlockButton.setDefaultButton(false);
unlockButton.setDefaultButton(true);
}
// ****************************************

View File

@@ -7,8 +7,10 @@ import java.util.concurrent.ExecutorService;
import javax.inject.Inject;
import org.cryptomator.ui.model.UpgradeInstruction;
import org.cryptomator.ui.model.UpgradeInstruction.UpgradeFailedException;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.UpgradeStrategies;
import org.cryptomator.ui.model.UpgradeStrategy;
import org.cryptomator.ui.model.UpgradeStrategy.UpgradeFailedException;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.settings.Localization;
import org.fxmisc.easybind.EasyBind;
@@ -16,7 +18,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Platform;
import javafx.beans.binding.Binding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
@@ -30,19 +31,24 @@ public class UpgradeController extends LocalizedFXMLViewController {
private static final Logger LOG = LoggerFactory.getLogger(UpgradeController.class);
final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
final ObjectProperty<Optional<UpgradeStrategy>> strategy = new SimpleObjectProperty<>();
private final UpgradeStrategies strategies;
private final ExecutorService exec;
private final Binding<Optional<UpgradeInstruction>> upgradeInstruction = EasyBind.monadic(vault).map(Vault::availableUpgrade);
private Optional<UpgradeListener> listener = Optional.empty();
@Inject
public UpgradeController(Localization localization, ExecutorService exec) {
public UpgradeController(Localization localization, UpgradeStrategies strategies, ExecutorService exec) {
super(localization);
this.strategies = strategies;
this.exec = exec;
}
@FXML
private Label upgradeLabel;
@FXML
private SecPasswordField passwordField;
@FXML
private Button upgradeButton;
@@ -54,11 +60,13 @@ public class UpgradeController extends LocalizedFXMLViewController {
@Override
protected void initialize() {
upgradeLabel.textProperty().bind(EasyBind.monadic(upgradeInstruction).map(instruction -> {
upgradeLabel.textProperty().bind(EasyBind.monadic(strategy).map(instruction -> {
return instruction.map(this::upgradeNotification).orElse("");
}).orElse(""));
EasyBind.subscribe(vault, this::vaultChanged);
upgradeButton.disableProperty().bind(passwordField.textProperty().isEmpty().or(passwordField.disabledProperty()));
EasyBind.subscribe(vault, this::vaultDidChange);
}
@Override
@@ -66,16 +74,20 @@ public class UpgradeController extends LocalizedFXMLViewController {
return getClass().getResource("/fxml/upgrade.fxml");
}
private void vaultChanged(Vault newVault) {
private void vaultDidChange(Vault newVault) {
errorLabel.setText(null);
strategy.set(strategies.getUpgradeStrategy(newVault));
// trigger "default" change to refresh key bindings:
upgradeButton.setDefaultButton(false);
upgradeButton.setDefaultButton(true);
}
// ****************************************
// Upgrade label
// ****************************************
private String upgradeNotification(UpgradeInstruction instruction) {
return instruction.getNotification(vault.get(), localization);
private String upgradeNotification(UpgradeStrategy instruction) {
return instruction.getNotification(vault.get());
}
// ****************************************
@@ -84,36 +96,45 @@ public class UpgradeController extends LocalizedFXMLViewController {
@FXML
private void didClickUpgradeButton(ActionEvent event) {
upgradeInstruction.getValue().ifPresent(this::upgrade);
strategy.getValue().ifPresent(this::upgrade);
}
private void upgrade(UpgradeInstruction instruction) {
Vault v = vault.getValue();
Objects.requireNonNull(v);
private void upgrade(UpgradeStrategy instruction) {
Vault v = Objects.requireNonNull(vault.getValue());
passwordField.setDisable(true);
progressIndicator.setVisible(true);
upgradeButton.setDisable(true);
exec.submit(() -> {
if (!instruction.isApplicable(v)) {
LOG.error("No upgrade needed for " + v.path().getValue());
throw new IllegalStateException("No ugprade needed for " + v.path().getValue());
}
try {
instruction.upgrade(v, localization);
Platform.runLater(() -> {
progressIndicator.setVisible(false);
upgradeButton.setDisable(false);
listener.ifPresent(UpgradeListener::didUpgrade);
});
instruction.upgrade(v, passwordField.getCharacters());
Platform.runLater(this::showNextUpgrade);
} catch (UpgradeFailedException e) {
Platform.runLater(() -> {
errorLabel.setText(e.getLocalizedMessage());
});
} finally {
Platform.runLater(() -> {
progressIndicator.setVisible(false);
upgradeButton.setDisable(false);
passwordField.setDisable(false);
passwordField.swipe();
});
}
});
}
private void showNextUpgrade() {
errorLabel.setText(null);
Optional<UpgradeStrategy> nextStrategy = strategies.getUpgradeStrategy(vault.getValue());
if (nextStrategy.isPresent()) {
strategy.set(nextStrategy);
} else {
listener.ifPresent(UpgradeListener::didUpgrade);
}
}
/* callback */
public void setListener(UpgradeListener listener) {

View File

@@ -154,6 +154,7 @@ public class WelcomeController extends LocalizedFXMLViewController {
Platform.runLater(() -> {
this.updateLink.setText(msg);
this.updateLink.setVisible(true);
this.updateLink.setDisable(false);
});
}
}

View File

@@ -1,40 +0,0 @@
package org.cryptomator.ui.model;
import org.cryptomator.ui.settings.Localization;
public interface UpgradeInstruction {
static UpgradeInstruction[] AVAILABLE_INSTRUCTIONS = {new UpgradeVersion3DropBundleExtension()};
/**
* @return Localized string to display to the user when an upgrade is needed.
*/
String getNotification(Vault vault, Localization localization);
/**
* Upgrades a vault. Might take a moment, should be run in a background thread.
*/
void upgrade(Vault vault, Localization localization) throws UpgradeFailedException;
/**
* Determines in O(1), if an upgrade can be applied to a vault.
*
* @return <code>true</code> if and only if the vault can be migrated to a newer version without the risk of data losses.
*/
boolean isApplicable(Vault vault);
/**
* Thrown when data migration failed.
*/
public class UpgradeFailedException extends Exception {
UpgradeFailedException() {
}
UpgradeFailedException(String message) {
super(message);
}
}
}

View File

@@ -0,0 +1,30 @@
package org.cryptomator.ui.model;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class UpgradeStrategies {
private final Collection<UpgradeStrategy> strategies;
@Inject
public UpgradeStrategies(UpgradeVersion3DropBundleExtension upgrader1, UpgradeVersion3to4 upgrader2) {
strategies = Collections.unmodifiableList(Arrays.asList(upgrader1, upgrader2));
}
public Optional<UpgradeStrategy> getUpgradeStrategy(Vault vault) {
if (vault == null) {
return Optional.empty();
}
return strategies.stream().filter(strategy -> {
return strategy.isApplicable(vault);
}).findFirst();
}
}

View File

@@ -0,0 +1,87 @@
package org.cryptomator.ui.model;
import java.io.IOException;
import java.nio.file.Files;
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.filesystem.crypto.Constants;
import org.cryptomator.ui.settings.Localization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class UpgradeStrategy {
private static final Logger LOG = LoggerFactory.getLogger(UpgradeStrategy.class);
protected final Provider<Cryptor> cryptorProvider;
protected final Localization localization;
UpgradeStrategy(Provider<Cryptor> cryptorProvider, Localization localization) {
this.cryptorProvider = cryptorProvider;
this.localization = localization;
}
/**
* @return Localized string to display to the user when an upgrade is needed.
*/
public abstract String getNotification(Vault vault);
/**
* 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();
try {
final Path masterkeyFile = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME);
final byte[] masterkeyFileContents = Files.readAllBytes(masterkeyFile);
cryptor.readKeysFromMasterkeyFile(masterkeyFileContents, passphrase);
// create backup, as soon as we know the password was correct:
final Path masterkeyBackupFile = vault.path().getValue().resolve(Constants.MASTERKEY_BACKUP_FILENAME);
Files.copy(masterkeyFile, masterkeyBackupFile, StandardCopyOption.REPLACE_EXISTING);
// do stuff:
upgrade(vault, cryptor);
// write updated masterkey file:
final byte[] upgradedMasterkeyFileContents = cryptor.writeKeysToMasterkeyFile(passphrase);
final Path masterkeyFileAfterUpgrading = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME); // path may have changed
Files.write(masterkeyFileAfterUpgrading, upgradedMasterkeyFileContents, StandardOpenOption.TRUNCATE_EXISTING);
} 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();
}
}
protected abstract void upgrade(Vault vault, Cryptor cryptor) throws UpgradeFailedException;
/**
* Determines in O(1), if an upgrade can be applied to a vault.
*
* @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);
/**
* Thrown when data migration failed.
*/
public static class UpgradeFailedException extends Exception {
UpgradeFailedException() {
}
UpgradeFailedException(String message) {
super(message);
}
}
}

View File

@@ -1,22 +1,40 @@
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.ui.settings.Localization;
import org.cryptomator.ui.settings.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Platform;
class UpgradeVersion3DropBundleExtension implements UpgradeInstruction {
@Singleton
class UpgradeVersion3DropBundleExtension extends UpgradeStrategy {
private static final Logger LOG = LoggerFactory.getLogger(UpgradeVersion3DropBundleExtension.class);
private final Settings settings;
@Inject
public UpgradeVersion3DropBundleExtension(Provider<Cryptor> cryptorProvider, Localization localization, Settings settings) {
super(cryptorProvider, localization);
this.settings = settings;
}
@Override
public String getNotification(Vault vault, Localization localization) {
public String getNotification(Vault vault) {
String fmt = localization.getString("upgrade.version3dropBundleExtension.msg");
Path path = vault.path().getValue();
String oldVaultName = path.getFileName().toString();
@@ -25,7 +43,26 @@ class UpgradeVersion3DropBundleExtension implements UpgradeInstruction {
}
@Override
public void upgrade(Vault vault, Localization localization) throws UpgradeFailedException {
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();
String oldVaultName = path.getFileName().toString();
String newVaultName = StringUtils.removeEnd(oldVaultName, Vault.VAULT_FILE_EXTENSION);
@@ -39,6 +76,7 @@ class UpgradeVersion3DropBundleExtension implements UpgradeInstruction {
Files.move(path, path.resolveSibling(newVaultName));
Platform.runLater(() -> {
vault.setPath(newPath);
settings.save();
});
} catch (IOException e) {
LOG.error("Vault migration failed", e);
@@ -49,7 +87,24 @@ class UpgradeVersion3DropBundleExtension implements UpgradeInstruction {
@Override
public boolean isApplicable(Vault vault) {
return vault.path().getValue().getFileName().toString().endsWith(Vault.VAULT_FILE_EXTENSION);
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;
}
} else {
return false;
}
}
}

View File

@@ -0,0 +1,116 @@
package org.cryptomator.ui.model;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.cryptomator.crypto.engine.Cryptor;
import org.cryptomator.filesystem.crypto.Constants;
import org.cryptomator.ui.settings.Localization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
class UpgradeVersion3to4 extends UpgradeStrategy {
private static final Logger LOG = LoggerFactory.getLogger(UpgradeVersion3to4.class);
private static final Pattern BASE32_FOLLOWED_BY_UNDERSCORE_PATTERN = Pattern.compile("^(([A-Z2-7]{8})*[A-Z2-7=]{8})_");
private static final int FILE_MIN_SIZE = 88; // vault version 3 files have a header of 88 bytes (assuming no chunks at all)
@Inject
public UpgradeVersion3to4(Provider<Cryptor> cryptorProvider, Localization localization) {
super(cryptorProvider, localization);
}
@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 FileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
migrate(file, attrs);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
throw exc;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
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) throws IOException {
String name = file.getFileName().toString();
long size = attrs.size();
Matcher m = BASE32_FOLLOWED_BY_UNDERSCORE_PATTERN.matcher(name);
if (m.find(0) && size < FILE_MIN_SIZE) {
String base32 = m.group(1);
String suffix = name.substring(m.end());
String renamed = "0" + base32 + (suffix.isEmpty() ? "" : " " + suffix);
renameWithoutOverwriting(file, renamed);
}
}
private void renameWithoutOverwriting(Path path, String newName) throws IOException {
Path newPath = path.resolveSibling(newName);
for (int i = 2; Files.exists(newPath); i++) {
newPath = path.resolveSibling(newName + " " + i);
}
Files.move(path, newPath);
LOG.info("Renaming {} to {}", path, newPath.getFileName());
}
@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), 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;
}
}
}

View File

@@ -16,7 +16,6 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.Normalizer;
import java.text.Normalizer.Form;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
@@ -30,6 +29,7 @@ import org.cryptomator.common.LazyInitializer;
import org.cryptomator.common.Optionals;
import org.cryptomator.crypto.engine.InvalidPassphraseException;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.charsets.NormalizedNameFileSystem;
import org.cryptomator.filesystem.crypto.CryptoFileSystemDelegate;
import org.cryptomator.filesystem.crypto.CryptoFileSystemFactory;
import org.cryptomator.filesystem.nio.NioFileSystem;
@@ -63,9 +63,9 @@ public class Vault implements CryptoFileSystemDelegate {
public static final String VAULT_FILE_EXTENSION = ".cryptomator";
private final ObjectProperty<Path> path;
private final DeferredCloser closer;
private final ShorteningFileSystemFactory shorteningFileSystemFactory;
private final CryptoFileSystemFactory cryptoFileSystemFactory;
private final DeferredCloser closer;
private final BooleanProperty unlocked = new SimpleBooleanProperty();
private final ObservableList<String> namesOfResourcesWithInvalidMac = FXThreads.observableListOnMainThread(FXCollections.observableArrayList());
private final Set<String> whitelistedResourcesWithInvalidMac = new HashSet<>();
@@ -81,9 +81,9 @@ public class Vault implements CryptoFileSystemDelegate {
*/
Vault(Path vaultDirectoryPath, ShorteningFileSystemFactory shorteningFileSystemFactory, CryptoFileSystemFactory cryptoFileSystemFactory, DeferredCloser closer) {
this.path = new SimpleObjectProperty<Path>(vaultDirectoryPath);
this.closer = closer;
this.shorteningFileSystemFactory = shorteningFileSystemFactory;
this.cryptoFileSystemFactory = cryptoFileSystemFactory;
this.closer = closer;
try {
setMountName(name().getValue());
@@ -126,7 +126,8 @@ public class Vault implements CryptoFileSystemDelegate {
FileSystem fs = getNioFileSystem();
FileSystem shorteningFs = shorteningFileSystemFactory.get(fs);
FileSystem cryptoFs = cryptoFileSystemFactory.unlockExisting(shorteningFs, passphrase, this);
StatsFileSystem statsFs = new StatsFileSystem(cryptoFs);
FileSystem normalizingFs = new NormalizedNameFileSystem(cryptoFs, SystemUtils.IS_OS_MAC_OSX ? Form.NFD : Form.NFC);
StatsFileSystem statsFs = new StatsFileSystem(normalizingFs);
statsFileSystem = Optional.of(statsFs);
String contextPath = StringUtils.prependIfMissing(mountName, "/");
Frontend frontend = frontendFactory.create(statsFs, contextPath);
@@ -165,16 +166,6 @@ public class Vault implements CryptoFileSystemDelegate {
Optionals.ifPresent(filesystemFrontend.get(), Frontend::unmount);
}
public boolean needsUpgrade() {
return availableUpgrade().isPresent();
}
public Optional<UpgradeInstruction> availableUpgrade() {
return Arrays.stream(UpgradeInstruction.AVAILABLE_INSTRUCTIONS).filter(instruction -> {
return instruction.isApplicable(this);
}).findAny();
}
// ******************************************************************************
// Delegate methods
// ********************************************************************************/

View File

@@ -1,27 +1,70 @@
package org.cryptomator.ui.settings;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Objects;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.collections4.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class Localization extends ResourceBundle {
private static final Logger LOG = LoggerFactory.getLogger(Localization.class);
private static final String LOCALIZATION_DEFAULT_FILE = "/localization/en.txt";
private static final String LOCALIZATION_FILENAME_FMT = "/localization/%s.txt";
private static final String LOCALIZATION_FILE = String.format(LOCALIZATION_FILENAME_FMT, Locale.getDefault().getLanguage());
private final ResourceBundle fallback;
private final ResourceBundle localized;
@Inject
public Localization() {
this.parent = ResourceBundle.getBundle("localization");
try (InputStream in = getClass().getResourceAsStream(LOCALIZATION_DEFAULT_FILE)) {
Objects.requireNonNull(in);
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
this.fallback = new PropertyResourceBundle(reader);
LOG.info("Loaded localization from {}", LOCALIZATION_FILE);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
try (InputStream in = getClass().getResourceAsStream(LOCALIZATION_FILE)) {
if (in != null) {
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
this.localized = new PropertyResourceBundle(reader);
} else {
this.localized = this.fallback;
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
protected Object handleGetObject(String key) {
return parent.getObject(key);
return localized.containsKey(key) ? localized.getObject(key) : fallback.getObject(key);
}
@Override
public Enumeration<String> getKeys() {
return parent.getKeys();
Collection<String> keys = CollectionUtils.union(localized.keySet(), fallback.keySet());
return Collections.enumeration(keys);
}
}

View File

@@ -11,6 +11,7 @@ package org.cryptomator.ui.settings;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import org.cryptomator.ui.model.Vault;
@@ -23,10 +24,12 @@ public class Settings implements Serializable {
private static final long serialVersionUID = 7609959894417878744L;
public static final int MIN_PORT = 1024;
public static final int MAX_PORT = 65535;
public static final int DEFAULT_PORT = 0;
public static final int DEFAULT_PORT = 42427;
public static final boolean DEFAULT_USE_IPV6 = false;
public static final Integer DEFAULT_NUM_TRAY_NOTIFICATIONS = 3;
private final Consumer<Settings> saveCmd;
@JsonProperty("directories")
private List<Vault> directories;
@@ -35,7 +38,7 @@ public class Settings implements Serializable {
@JsonProperty("port")
private Integer port;
@JsonProperty("useIpv6")
private Boolean useIpv6;
@@ -45,8 +48,12 @@ public class Settings implements Serializable {
/**
* Package-private constructor; use {@link SettingsProvider}.
*/
Settings() {
Settings(Consumer<Settings> saveCmd) {
this.saveCmd = saveCmd;
}
public void save() {
saveCmd.accept(this);
}
/* Getter/Setter */
@@ -86,8 +93,8 @@ public class Settings implements Serializable {
}
}
private boolean isPortValid(int port) {
return port == DEFAULT_PORT || port >= MIN_PORT && port <= MAX_PORT;
public boolean isPortValid(int port) {
return port == DEFAULT_PORT || port >= MIN_PORT && port <= MAX_PORT || port == 0;
}
public boolean shouldUseIpv6() {

View File

@@ -16,6 +16,12 @@ import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.inject.Inject;
import javax.inject.Named;
@@ -23,7 +29,6 @@ import javax.inject.Provider;
import javax.inject.Singleton;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.DeferredCloser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -32,9 +37,10 @@ import com.fasterxml.jackson.databind.ObjectMapper;
@Singleton
public class SettingsProvider implements Provider<Settings> {
private static final Logger LOG = LoggerFactory.getLogger(Settings.class);
private static final Logger LOG = LoggerFactory.getLogger(SettingsProvider.class);
private static final Path SETTINGS_DIR;
private static final String SETTINGS_FILE = "settings.json";
private static final long SAVE_DELAY_MS = 1000;
static {
final String appdata = System.getenv("APPDATA");
@@ -52,12 +58,12 @@ public class SettingsProvider implements Provider<Settings> {
}
}
private final DeferredCloser deferredCloser;
private final ObjectMapper objectMapper;
private final ScheduledExecutorService saveScheduler = Executors.newSingleThreadScheduledExecutor();
private final AtomicReference<ScheduledFuture<?>> scheduledSaveCmd = new AtomicReference<>();
@Inject
public SettingsProvider(DeferredCloser deferredCloser, @Named("VaultJsonMapper") ObjectMapper objectMapper) {
this.deferredCloser = deferredCloser;
public SettingsProvider(@Named("VaultJsonMapper") ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@@ -72,28 +78,39 @@ public class SettingsProvider implements Provider<Settings> {
@Override
public Settings get() {
Settings settings = null;
final Settings settings = new Settings(this::scheduleSave);
try {
final Path settingsPath = getSettingsPath();
final InputStream in = Files.newInputStream(settingsPath, StandardOpenOption.READ);
settings = objectMapper.readValue(in, Settings.class);
objectMapper.readerForUpdating(settings).readValue(in);
LOG.info("Settings loaded from " + settingsPath);
} catch (IOException e) {
LOG.warn("Failed to load settings, creating new one.");
settings = new Settings();
LOG.info("Failed to load settings, creating new one.");
}
deferredCloser.closeLater(settings, this::save);
return settings;
}
private void save(Settings settings) {
private void scheduleSave(Settings settings) {
if (settings == null) {
return;
}
ScheduledFuture<?> saveCmd = saveScheduler.schedule(() -> {
this.save(settings);
} , SAVE_DELAY_MS, TimeUnit.MILLISECONDS);
ScheduledFuture<?> previousSaveCmd = scheduledSaveCmd.getAndSet(saveCmd);
if (previousSaveCmd != null) {
previousSaveCmd.cancel(false);
}
}
private void save(Settings settings) {
Objects.requireNonNull(settings);
try {
final Path settingsPath = getSettingsPath();
Files.createDirectories(settingsPath.getParent());
final OutputStream out = Files.newOutputStream(settingsPath, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
objectMapper.writeValue(out, settings);
LOG.info("Settings saved to " + settingsPath);
} catch (IOException e) {
LOG.error("Failed to save settings.", e);
}

View File

@@ -0,0 +1,53 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Jean-Noël Charon - initial API and implementation
*******************************************************************************/
package org.cryptomator.ui.util;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.text.Text;
public class DialogBuilderUtil {
public DialogBuilderUtil() {
}
public static Alert buildInformationDialog(String title, String header, String content, ButtonType defaultButton) {
return buildDialog(title, header, content, Alert.AlertType.INFORMATION, defaultButton);
}
public static Alert buildWarningDialog(String title, String header, String content, ButtonType defaultButton) {
return buildDialog(title, header, content, Alert.AlertType.WARNING, defaultButton);
}
public static Alert buildErrorDialog(String title, String header, String content, ButtonType defaultButton) {
return buildDialog(title, header, content, Alert.AlertType.ERROR, defaultButton);
}
public static Alert buildConfirmationDialog(String title, String header, String content, ButtonType defaultButton) {
return buildDialog(title, header, content, Alert.AlertType.CONFIRMATION, defaultButton);
}
private static Alert buildDialog(String title, String header, String content, Alert.AlertType type, ButtonType defaultButton) {
Text contentText = new Text(content);
contentText.setWrappingWidth(360.0);
Alert alert = new Alert(type);
alert.setTitle(title);
alert.setHeaderText(header);
alert.getDialogPane().setContent(contentText);
alert.getDialogPane().getButtonTypes().stream().forEach(buttonType -> {
Button btn = (Button) alert.getDialogPane().lookupButton(buttonType);
btn.setDefaultButton(buttonType.equals(defaultButton));
});
return alert;
}
}

View File

@@ -0,0 +1,87 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Jean-Noël Charon - initial API and implementation
*******************************************************************************/
package org.cryptomator.ui.util;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.ui.settings.Localization;
import com.nulabinc.zxcvbn.Zxcvbn;
import javafx.geometry.Insets;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.paint.Color;
@Singleton
public class PasswordStrengthUtil {
private final Zxcvbn zxcvbn;
private final List<String> sanitizedInputs;
private final Localization localization;
@Inject
public PasswordStrengthUtil(Localization localization) {
this.localization = localization;
this.zxcvbn = new Zxcvbn();
this.sanitizedInputs = new ArrayList<>();
this.sanitizedInputs.add("cryptomator");
}
public int computeRate(String password) {
if (StringUtils.isEmpty(password)) {
return -1;
} else {
return zxcvbn.measure(password, sanitizedInputs).getScore();
}
}
public Color getStrengthColor(Number score) {
switch (score.intValue()) {
case 0:
return Color.web("#e74c3c");
case 1:
return Color.web("#e67e22");
case 2:
return Color.web("#f1c40f");
case 3:
return Color.web("#40d47e");
case 4:
return Color.web("#27ae60");
default:
return Color.web("#ffffff", 0.5);
}
}
public Background getBackgroundWithStrengthColor(Number score) {
Color c = this.getStrengthColor(score);
BackgroundFill fill = new BackgroundFill(c, CornerRadii.EMPTY, Insets.EMPTY);
return new Background(fill);
}
public Background getBackgroundWithStrengthColor(Number score, Number threshold) {
return score.intValue() >= threshold.intValue() ? getBackgroundWithStrengthColor(score) : getBackgroundWithStrengthColor(-1);
}
public String getStrengthDescription(Number score) {
String key = "initialize.messageLabel.passwordStrength." + score.intValue();
if (localization.containsKey(key)) {
return localization.getString(key);
} else {
return "";
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -5,6 +5,7 @@
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
* Jean-Noël Charon - implementation of the dialog css
*
*/
@@ -50,6 +51,10 @@
-fx-font-family: Ionicons;
}
.caption-label {
-fx-font-size: 0.9em;
}
/****************************************************************************
* *
* Hyperlinks *
@@ -76,7 +81,7 @@
-fx-pref-height: 25px;
-fx-background-color: COLOR_BORDER, COLOR_VGRAD_LIGHT;
-fx-background-insets: 0, 1;
-fx-padding: 4px 8px 4px 8px;
-fx-padding: 4px 12px 6px 12px;
-fx-text-fill: COLOR_TEXT;
-fx-alignment: CENTER;
}
@@ -440,4 +445,65 @@
-fx-stroke-width: 2px;
}
.default-color0.chart-series-line { -fx-stroke: COLOR_CHART_GREEN; }
.default-color1.chart-series-line { -fx-stroke: COLOR_CHART_RED; }
.default-color1.chart-series-line { -fx-stroke: COLOR_CHART_RED; }
/*******************************************************************************
* *
* Dialog *
* *
******************************************************************************/
.dialog-pane {
-fx-background-color: COLOR_BACKGROUND;
-fx-padding: 20px 20px 20px 96px;
-fx-background-image: url("/img/dialog-appicon.png");
-fx-background-repeat: no-repeat;
-fx-background-position: 20px 20px;
}
/* HEADER */
.dialog-pane:header .header-panel {
-fx-padding: 0 0 12px 0;
}
/* TITLE */
.dialog-pane:header .header-panel .label {
-fx-font-weight: bold;
-fx-wrap-text: true;
-fx-font-size: 14px;
}
/* CONTENT LABEL */
.dialog-pane > .content {
-fx-alignment: top-left;
-fx-wrap-text: true;
-fx-font-size: 12px;
}
/* BUTTONS */
.dialog-pane > .button-bar > .container {
-fx-padding: 20px 0 0 0;
}
.alert.confirmation.dialog-pane,
.text-input-dialog.dialog-pane,
.choice-dialog.dialog-pane {
-fx-padding: 20px 20px 20px 80px;
-fx-background-image: url("/img/dialog-confirm.png");
}
.alert.information.dialog-pane {
-fx-padding: 20px 20px 20px 80px;
-fx-background-image: url("/img/dialog-information.png");
}
.alert.error.dialog-pane {
-fx-padding: 20px 20px 20px 80px;
-fx-background-image: url("/img/dialog-error.png");
}
.alert.warning.dialog-pane {
-fx-padding: 20px 20px 20px 80px;
-fx-background-image: url("/img/dialog-warning.png");
}

View File

@@ -5,11 +5,12 @@
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
* Jean-Noël Charon - implementation of the dialog css
*
*/
.root {
-fx-font-family: 'lucida-grande';
-fx-font-family: 'lucida-grande', sans-serif;
-fx-font-smoothing-type: lcd;
-fx-font-size: 13px;
@@ -49,6 +50,10 @@
-fx-font-family: Ionicons;
}
.caption-label {
-fx-font-size: 0.9em;
}
/****************************************************************************
* *
* Hyperlinks *
@@ -530,4 +535,53 @@
-fx-stroke-width: 2px;
}
.default-color0.chart-series-line { -fx-stroke: COLOR_CHART_GREEN; }
.default-color1.chart-series-line { -fx-stroke: COLOR_CHART_RED; }
.default-color1.chart-series-line { -fx-stroke: COLOR_CHART_RED; }
/*******************************************************************************
* *
* Dialog *
* *
******************************************************************************/
.dialog-pane {
-fx-background-color: COLOR_BACKGROUND;
-fx-padding: 20px 20px 20px 96px;
-fx-background-image: url("/img/dialog-appicon.png");
-fx-background-repeat: no-repeat;
-fx-background-position: 20px 20px;
}
/* HEADER */
.dialog-pane:header .header-panel {
-fx-padding: 0 0 12px 0;
}
/* TITLE */
.dialog-pane:header .header-panel .label {
-fx-font-weight: bold;
-fx-wrap-text: true;
}
/* CONTENT LABEL */
.dialog-pane > .content {
-fx-alignment: top-left;
-fx-wrap-text: true;
-fx-font-size: 11px;
-fx-line-spacing: 1.0;
}
/* BUTTONS */
.dialog-pane > .button-bar > .container {
-fx-padding: 12px 0 0 0;
}
.dialog-pane > .button-bar .button:default {
-fx-background-color: COLOR_HGRAD_BTN_DEF_BORDER, COLOR_HGRAD_BTN_DEF_BACKGROUND;
-fx-text-fill: #FFF;
}
.dialog-pane > .button-bar .button:default:armed {
-fx-background-color: COLOR_HGRAD_BTN_ARMED_BORDER, COLOR_HGRAD_BTN_ARMED_BACKGROUND;
-fx-text-fill: #FFF;
}

View File

@@ -5,6 +5,7 @@
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
* Jean-Noël Charon - implementation of the dialog css
*
*/
@@ -24,6 +25,10 @@
COLOR_CHART_GREEN: #A1CD5f;
COLOR_CHART_RED: #C75050;
COLOR_HGRAD_BTN_BACKGROUND: linear-gradient(to bottom, #F0F0F0 0%, #E5E5E5 100%);
COLOR_HGRAD_BTN_DISABLED_BORDER: linear-gradient(to bottom, #D2D2D2 0%, #C4C4C4 100%);
COLOR_HGRAD_BTN_ARMED_BACKGROUND: linear-gradient(to bottom, #DAECFC 0%, #C4E0FC 100%);
-fx-background-color: COLOR_BACKGROUND;
-fx-text-fill: COLOR_TEXT;
}
@@ -42,6 +47,10 @@
-fx-font-family: Ionicons;
}
.caption-label {
-fx-font-size: 0.9em;
}
/****************************************************************************
* *
* Hyperlinks *
@@ -66,7 +75,7 @@
.button,
.toggle-button {
-fx-pref-height: 27px;
-fx-background-color: COLOR_BORDER, linear-gradient(to bottom, #F0F0F0 0%, #E5E5E5 100%);
-fx-background-color: COLOR_BORDER, COLOR_HGRAD_BTN_BACKGROUND;
-fx-background-insets: 0, 1;
-fx-padding: 2px 12px 2px 12px;
-fx-text-fill: COLOR_TEXT;
@@ -76,12 +85,12 @@
}
.button:default {
-fx-background-color: COLOR_BORDER_FOCUS, linear-gradient(to bottom, #F0F0F0 0%, #E5E5E5 100%);
-fx-background-color: COLOR_BORDER_FOCUS, COLOR_HGRAD_BTN_BACKGROUND;
}
.button:disabled,
.button:default:disabled {
-fx-background-color: linear-gradient(to bottom, #D2D2D2 0%, #C4C4C4 100%), #F2F2F2;
-fx-background-color: COLOR_HGRAD_BTN_DISABLED_BORDER, #F2F2F2;
-fx-text-fill: #8B8B8B;
}
@@ -89,7 +98,7 @@
.button:default:armed,
.toggle-button:armed,
.toggle-button:selected {
-fx-background-color: COLOR_BORDER_FOCUS, linear-gradient(to bottom, #DAECFC 0%, #C4E0FC 100%);
-fx-background-color: COLOR_BORDER_FOCUS, COLOR_HGRAD_BTN_ARMED_BACKGROUND;
}
.button:focused,
@@ -512,4 +521,73 @@
-fx-stroke-width: 2px;
}
.default-color0.chart-series-line { -fx-stroke: COLOR_CHART_GREEN; }
.default-color1.chart-series-line { -fx-stroke: COLOR_CHART_RED; }
.default-color1.chart-series-line { -fx-stroke: COLOR_CHART_RED; }
/*******************************************************************************
* *
* Dialog *
* *
******************************************************************************/
.dialog-pane {
-fx-background-color: COLOR_BACKGROUND;
-fx-padding: 20px 20px 20px 96px;
-fx-background-image: url("/img/dialog-appicon.png");
-fx-background-repeat: no-repeat;
-fx-background-position: 20px 20px;
}
/* HEADER */
.dialog-pane:header .header-panel {
-fx-padding: 0 0 12px 0;
}
/* TITLE */
.dialog-pane:header .header-panel .label {
-fx-font-weight: bold;
-fx-wrap-text: true;
}
/* CONTENT LABEL */
.dialog-pane > .content {
-fx-alignment: top-left;
-fx-wrap-text: true;
-fx-font-size: 11px;
-fx-line-spacing: 1.0;
}
/* BUTTONS */
.dialog-pane > .button-bar > .container {
-fx-padding: 20px 0 0 0;
}
.dialog-pane > .button-bar .button:default {
-fx-background-color: COLOR_BORDER_FOCUS, COLOR_HGRAD_BTN_BACKGROUND;
}
.dialog-pane > .button-bar .button:default:armed {
-fx-background-color: COLOR_BORDER_FOCUS, COLOR_HGRAD_BTN_ARMED_BACKGROUND;
}
.alert.confirmation.dialog-pane,
.text-input-dialog.dialog-pane,
.choice-dialog.dialog-pane {
-fx-padding: 20px 20px 20px 80px;
-fx-background-image: url("/img/dialog-confirm.png");
}
.alert.information.dialog-pane {
-fx-padding: 20px 20px 20px 80px;
-fx-background-image: url("/img/dialog-information.png");
}
.alert.error.dialog-pane {
-fx-padding: 20px 20px 20px 80px;
-fx-background-image: url("/img/dialog-error.png");
}
.alert.warning.dialog-pane {
-fx-padding: 20px 20px 20px 80px;
-fx-background-image: url("/img/dialog-warning.png");
}

View File

@@ -6,6 +6,7 @@
Contributors:
Sebastian Stenzel - initial API and implementation
Jean-Noël Charon - password strength meter
-->
<?import java.net.URL?>
<?import java.lang.String?>
@@ -20,7 +21,9 @@
<?import javafx.scene.text.TextFlow?>
<?import javafx.scene.control.Hyperlink?>
<?import javafx.scene.text.Text?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.VBox?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" xmlns:fx="http://javafx.com/fxml" cacheShape="true" cache="true">
<padding>
@@ -46,10 +49,25 @@
<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="2" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
<!-- Row 3 -->
<Button fx:id="changePasswordButton" text="%changePassword.button.change" defaultButton="true" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickChangePasswordButton" disable="true" cacheShape="true" cache="true"/>
<VBox GridPane.columnIndex="1" GridPane.rowIndex="3" spacing="6.0">
<HBox spacing="6.0" prefHeight="6.0" cacheShape="true" cache="true">
<Region HBox.hgrow="ALWAYS" fx:id="passwordStrengthLevel0" cacheShape="true" cache="true" />
<Region HBox.hgrow="ALWAYS" fx:id="passwordStrengthLevel1" cacheShape="true" cache="true" />
<Region HBox.hgrow="ALWAYS" fx:id="passwordStrengthLevel2" cacheShape="true" cache="true" />
<Region HBox.hgrow="ALWAYS" fx:id="passwordStrengthLevel3" cacheShape="true" cache="true" />
<Region HBox.hgrow="ALWAYS" fx:id="passwordStrengthLevel4" cacheShape="true" cache="true" />
</HBox>
<Label fx:id="passwordStrengthLabel" styleClass="caption-label" cache="true" cacheShape="true" text="" />
</VBox>
<!-- Row 4 -->
<TextFlow GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" cacheShape="true" cache="true">
<Label text="%initialize.label.doNotForget" wrapText="true" cache="true" cacheShape="true" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="4" />
<!-- Row 5 -->
<Button fx:id="changePasswordButton" text="%changePassword.button.change" defaultButton="true" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickChangePasswordButton" disable="true" cacheShape="true" cache="true"/>
<!-- Row 6 -->
<TextFlow GridPane.rowIndex="6" GridPane.columnIndex="0" GridPane.columnSpan="2" cacheShape="true" cache="true">
<children>
<Text fx:id="messageText" cache="true" />
<Hyperlink fx:id="downloadsPageLink" text="%changePassword.label.downloadsPageLink" visible="false" onAction="#didClickDownloadsLink" cacheShape="true" cache="true" />

View File

@@ -6,15 +6,22 @@
Contributors:
Sebastian Stenzel - initial API and implementation
Jean-Noël Charon - password strength meter
-->
<?import java.lang.*?>
<?import java.net.URL?>
<?import javafx.scene.layout.HBox?>
<?import org.cryptomator.ui.controls.SecPasswordField?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.control.ProgressIndicator?>
<?import org.cryptomator.ui.controls.SecPasswordField?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.VBox?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" xmlns:fx="http://javafx.com/fxml" cacheShape="true" cache="true">
<padding>
@@ -30,17 +37,31 @@
<!-- Row 0 -->
<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%initialize.label.password" cacheShape="true" cache="true" />
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="0" GridPane.columnIndex="1" cacheShape="true" cache="true" />
<!-- Row 1 -->
<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%initialize.label.retypePassword" cacheShape="true" cache="true" />
<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="1" GridPane.columnIndex="1" cacheShape="true" cache="true" />
<!-- Row 2 -->
<Button fx:id="okButton" defaultButton="true" GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" text="%initialize.button.ok" prefWidth="150.0" onAction="#initializeVault" focusTraversable="false" disable="true" cacheShape="true" cache="true" />
<VBox GridPane.columnIndex="1" GridPane.rowIndex="2" spacing="6.0">
<HBox spacing="6.0" prefHeight="6.0" cacheShape="true" cache="true">
<Region HBox.hgrow="ALWAYS" fx:id="passwordStrengthLevel0" cacheShape="true" cache="true" />
<Region HBox.hgrow="ALWAYS" fx:id="passwordStrengthLevel1" cacheShape="true" cache="true" />
<Region HBox.hgrow="ALWAYS" fx:id="passwordStrengthLevel2" cacheShape="true" cache="true" />
<Region HBox.hgrow="ALWAYS" fx:id="passwordStrengthLevel3" cacheShape="true" cache="true" />
<Region HBox.hgrow="ALWAYS" fx:id="passwordStrengthLevel4" cacheShape="true" cache="true" />
</HBox>
<Label fx:id="passwordStrengthLabel" styleClass="caption-label" cache="true" cacheShape="true" text="" />
</VBox>
<!-- Row 3 -->
<Label fx:id="messageLabel" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" cacheShape="true" cache="true" />
<Label text="%initialize.label.doNotForget" wrapText="true" cache="true" cacheShape="true" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="3" />
<!-- Row 4 -->
<Button fx:id="okButton" cache="true" cacheShape="true" defaultButton="true" disable="true" focusTraversable="false" onAction="#initializeVault" prefWidth="150.0" text="%initialize.button.ok" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" GridPane.rowIndex="4" />
<!-- Row 5 -->
<Label fx:id="messageLabel" cache="true" cacheShape="true" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="5" />
</children>
</GridPane>

View File

@@ -34,7 +34,7 @@
<MenuItem text="%main.directoryList.contextMenu.remove" onAction="#didClickRemoveSelectedEntry">
<graphic><Label text="&#xf12a;" styleClass="ionicons"/></graphic>
</MenuItem>
<MenuItem text="%main.directoryList.contextMenu.changePassword" onAction="#didClickChangePassword">
<MenuItem text="%main.directoryList.contextMenu.changePassword" fx:id="changePasswordMenuItem" onAction="#didClickChangePassword">
<graphic><Label text="&#xf2bf;" styleClass="ionicons"/></graphic>
</MenuItem>
</items>

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