Merge branch 'feature/hub' into develop

# Conflicts:
#	.idea/runConfigurations/Cryptomator_Windows.xml
#	.idea/runConfigurations/Cryptomator_Windows_Dev.xml
#	dist/win/build.ps1
#	src/main/java/org/cryptomator/common/Environment.java
This commit is contained in:
Armin Schrenk
2022-07-26 12:29:33 +02:00
46 changed files with 1554 additions and 26 deletions

View File

@@ -60,7 +60,7 @@ jobs:
--verbose
--output runtime
--module-path "${JAVA_HOME}/jmods"
--add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr
--strip-native-commands
--no-header-files
--no-man-pages
@@ -92,6 +92,7 @@ jobs:
--java-options "-Dcryptomator.logDir=\"~/.local/share/Cryptomator/logs\""
--java-options "-Dcryptomator.pluginDir=\"~/.local/share/Cryptomator/plugins\""
--java-options "-Dcryptomator.settingsPath=\"~/.config/Cryptomator/settings.json:~/.Cryptomator/settings.json\""
--java-options "-Dcryptomator.p12Path=\"~/.config/Cryptomator/key.p12\""
--java-options "-Dcryptomator.ipcSocketPath=\"~/.config/Cryptomator/ipc.socket\""
--java-options "-Dcryptomator.mountPointsDir=\"~/.local/share/Cryptomator/mnt\""
--java-options "-Dcryptomator.showTrayIcon=false"

View File

@@ -60,7 +60,7 @@ jobs:
--verbose
--output runtime
--module-path "${JAVA_HOME}/jmods"
--add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr
--strip-native-commands
--no-header-files
--no-man-pages
@@ -89,6 +89,7 @@ jobs:
--java-options "-Dcryptomator.logDir=\"~/Library/Logs/Cryptomator\""
--java-options "-Dcryptomator.pluginDir=\"~/Library/Application Support/Cryptomator/Plugins\""
--java-options "-Dcryptomator.settingsPath=\"~/Library/Application Support/Cryptomator/settings.json\""
--java-options "-Dcryptomator.p12Path=\"~/Library/Application Support/Cryptomator/key.p12\""
--java-options "-Dcryptomator.ipcSocketPath=\"~/Library/Application Support/Cryptomator/ipc.socket\""
--java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"Cryptomator\""
--java-options "-Dcryptomator.showTrayIcon=true"

View File

@@ -69,7 +69,7 @@ jobs:
--verbose
--output runtime
--module-path "${JAVA_HOME}/jmods"
--add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr
--strip-native-commands
--no-header-files
--no-man-pages
@@ -96,6 +96,7 @@ jobs:
--java-options "-Dcryptomator.logDir=\"~/AppData/Roaming/Cryptomator\""
--java-options "-Dcryptomator.pluginDir=\"~/AppData/Roaming/Cryptomator/Plugins\""
--java-options "-Dcryptomator.settingsPath=\"~/AppData/Roaming/Cryptomator/settings.json\""
--java-options "-Dcryptomator.p12Path=\"~/AppData/Roaming/Cryptomator/key.p12\""
--java-options "-Dcryptomator.ipcSocketPath=\"~/AppData/Roaming/Cryptomator/ipc.socket\""
--java-options "-Dcryptomator.mountPointsDir=\"~/Cryptomator\""
--java-options "-Dcryptomator.showTrayIcon=true"

View File

@@ -2,7 +2,7 @@
<configuration default="false" name="Cryptomator Linux" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="cryptomator" />
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/.config/Cryptomator/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/.config/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator/logs&quot; -Dcryptomator.pluginDir=&quot;~/.local/share/Cryptomator/plugins&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator/mnt&quot; -Dcryptomator.showTrayIcon=true -Xss20m -Xmx512m" />
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/.config/Cryptomator/settings.json&quot; -Dcryptomator.p12Path=&quot;~/.config/Cryptomator/key.p12&quot; -Dcryptomator.ipcSocketPath=&quot;~/.config/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator/logs&quot; -Dcryptomator.pluginDir=&quot;~/.local/share/Cryptomator/plugins&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator/mnt&quot; -Dcryptomator.showTrayIcon=true -Xss20m -Xmx512m" />
<method v="2">
<option name="Make" enabled="true" />
</method>

View File

@@ -2,7 +2,7 @@
<configuration default="false" name="Cryptomator Linux Dev" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="cryptomator" />
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/.config/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/.config/Cryptomator-Dev/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator-Dev/logs&quot; -Dcryptomator.pluginDir=&quot;~/.local/share/Cryptomator-Dev/plugins&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator-Dev/mnt&quot; -Dcryptomator.showTrayIcon=true -Dfuse.experimental=&quot;true&quot; -Xss20m -Xmx512m" />
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/.config/Cryptomator-Dev/settings.json&quot; -Dcryptomator.p12Path=&quot;~/.config/Cryptomator-Dev/key.p12&quot; -Dcryptomator.ipcSocketPath=&quot;~/.config/Cryptomator-Dev/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator-Dev/logs&quot; -Dcryptomator.pluginDir=&quot;~/.local/share/Cryptomator-Dev/plugins&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator-Dev/mnt&quot; -Dcryptomator.showTrayIcon=true -Dfuse.experimental=&quot;true&quot; -Xss20m -Xmx512m" />
<method v="2">
<option name="Make" enabled="true" />
</method>

View File

@@ -2,7 +2,7 @@
<configuration default="false" name="Cryptomator Windows" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="cryptomator" />
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/AppData/Roaming/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator&quot; -Dcryptomator.pluginDir=&quot;~/AppData/Roaming/Cryptomator/Plugins&quot; -Dcryptomator.integrationsWin.keychainPaths=&quot;~/AppData/Roaming/Cryptomator-Dev/keychain.json&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" />
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/AppData/Roaming/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator&quot; -Dcryptomator.pluginDir=&quot;~/AppData/Roaming/Cryptomator/Plugins&quot; -Dcryptomator.integrationsWin.keychainPaths=&quot;~/AppData/Roaming/Cryptomator-Dev/keychain.json&quot; -Dcryptomator.p12Path=&quot;~/AppData/Roaming/Cryptomator/key.p12&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" />
<method v="2">
<option name="Make" enabled="true" />
</method>

View File

@@ -2,7 +2,7 @@
<configuration default="false" name="Cryptomator Windows Dev" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="cryptomator" />
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/AppData/Roaming/Cryptomator-Dev/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator-Dev&quot; -Dcryptomator.pluginDir=&quot;~/AppData/Roaming/Cryptomator-Dev/Plugins&quot; -Dcryptomator.integrationsWin.keychainPaths=&quot;~/AppData/Roaming/Cryptomator-Dev/keychain.json&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator-Dev&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" />
<option name="VM_PARAMETERS" value="-Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/AppData/Roaming/Cryptomator-Dev/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator-Dev&quot; -Dcryptomator.pluginDir=&quot;~/AppData/Roaming/Cryptomator-Dev/Plugins&quot; -Dcryptomator.integrationsWin.keychainPaths=&quot;~/AppData/Roaming/Cryptomator-Dev/keychain.json&quot; -Dcryptomator.p12Path=&quot;~/AppData/Roaming/Cryptomator-Dev/key.p12&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator-Dev&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" />
<method v="2">
<option name="Make" enabled="true" />
</method>

View File

@@ -5,7 +5,7 @@
</envs>
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="cryptomator" />
<option name="VM_PARAMETERS" value="-Dapple.awt.enableTemplateImages=true -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/Library/Application Support/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator&quot; -Dcryptomator.pluginDir=&quot;~/Library/Application Support/Cryptomator/Plugins&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
<option name="VM_PARAMETERS" value="-Dapple.awt.enableTemplateImages=true -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator/settings.json&quot; -Dcryptomator.p12Path=&quot;~/Library/Application Support/Cryptomator/key.p12&quot; -Dcryptomator.ipcSocketPath=&quot;~/Library/Application Support/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator&quot; -Dcryptomator.pluginDir=&quot;~/Library/Application Support/Cryptomator/Plugins&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
<method v="2">
<option name="Make" enabled="true" />
</method>

View File

@@ -5,7 +5,7 @@
</envs>
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="cryptomator" />
<option name="VM_PARAMETERS" value="-Dapple.awt.enableTemplateImages=true -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/Library/Application Support/Cryptomator-Dev/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator-Dev&quot; -Dcryptomator.pluginDir=&quot;~/Library/Application Support/Cryptomator-Dev/Plugins&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
<option name="VM_PARAMETERS" value="-Dapple.awt.enableTemplateImages=true -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator-Dev/settings.json&quot; -Dcryptomator.p12Path=&quot;~/Library/Application Support/Cryptomator-Dev/key.p12&quot; -Dcryptomator.ipcSocketPath=&quot;~/Library/Application Support/Cryptomator-Dev/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator-Dev&quot; -Dcryptomator.pluginDir=&quot;~/Library/Application Support/Cryptomator-Dev/Plugins&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
<method v="2">
<option name="Make" enabled="true" />
</method>

View File

@@ -19,7 +19,7 @@ cp ../../../target/cryptomator-*.jar ../../../target/mods
${JAVA_HOME}/bin/jlink \
--output runtime \
--module-path "${JAVA_HOME}/jmods" \
--add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr \
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr \
--strip-native-commands \
--no-header-files \
--no-man-pages \

View File

@@ -18,7 +18,7 @@ override_dh_auto_build:
ln -s ../common/org.cryptomator.Cryptomator512.png resources/cryptomator.png
jlink \
--output runtime \
--add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr \
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr \
--strip-native-commands \
--no-header-files \
--no-man-pages \
@@ -40,6 +40,7 @@ override_dh_auto_build:
--java-options "-Dcryptomator.logDir=\"~/.local/share/Cryptomator/logs\"" \
--java-options "-Dcryptomator.pluginDir=\"~/.local/share/Cryptomator/plugins\"" \
--java-options "-Dcryptomator.settingsPath=\"~/.config/Cryptomator/settings.json:~/.Cryptomator/settings.json\"" \
--java-options "-Dcryptomator.p12Path=\"~/.config/Cryptomator/key.p12\""
--java-options "-Dcryptomator.ipcSocketPath=\"~/.config/Cryptomator/ipc.socket\"" \
--java-options "-Dcryptomator.mountPointsDir=\"~/.local/share/Cryptomator/mnt\"" \
--java-options "-Dcryptomator.showTrayIcon=false" \

View File

@@ -45,7 +45,7 @@ cp ../../../target/${MAIN_JAR_GLOB} ../../../target/mods
${JAVA_HOME}/bin/jlink \
--output runtime \
--module-path "${JAVA_HOME}/jmods" \
--add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr \
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr \
--strip-native-commands \
--no-header-files \
--no-man-pages \
@@ -75,6 +75,7 @@ ${JAVA_HOME}/bin/jpackage \
--java-options "-Dcryptomator.pluginDir=\"~/Library/Application Support/${APP_NAME}/Plugins\"" \
--java-options "-Dcryptomator.settingsPath=\"~/Library/Application Support/${APP_NAME}/settings.json\"" \
--java-options "-Dcryptomator.ipcSocketPath=\"~/Library/Application Support/${APP_NAME}/ipc.socket\"" \
--java-options "-Dcryptomator.p12Path=\"~/Library/Application Support/${APP_NAME}/key.p12\"" \
--java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"${APP_NAME}\"" \
--java-options "-Dcryptomator.showTrayIcon=true" \
--java-options "-Dcryptomator.buildNumber=\"dmg-${REVISION_NO}\"" \

3
dist/win/build.ps1 vendored
View File

@@ -50,7 +50,7 @@ if ($clean -and (Test-Path -Path $runtimeImagePath)) {
--verbose `
--output runtime `
--module-path "$Env:JAVA_HOME/jmods" `
--add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr `
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr `
--strip-native-commands `
--no-header-files `
--no-man-pages `
@@ -83,6 +83,7 @@ if ($clean -and (Test-Path -Path $appPath)) {
--java-options "-Dcryptomator.pluginDir=`"~/AppData/Roaming/$AppName/Plugins`"" `
--java-options "-Dcryptomator.settingsPath=`"~/AppData/Roaming/$AppName/settings.json`"" `
--java-options "-Dcryptomator.ipcSocketPath=`"~/AppData/Roaming/$AppName/ipc.socket`"" `
--java-options "-Dcryptomator.p12Path=`"~/AppData/Roaming/$AppName/key.p12`"" `
--java-options "-Dcryptomator.mountPointsDir=`"~/$AppName`"" `
--java-options "-Dcryptomator.integrationsWin.autoStartShellLinkName=`"$AppName`"" `
--java-options "-Dcryptomator.integrationsWin.keychainPaths=`"~/AppData/Roaming/$AppName/keychain.json`"" `

30
pom.xml
View File

@@ -27,6 +27,7 @@
<nonModularGroupIds>com.github.serceman,com.github.jnr,org.ow2.asm,net.java.dev.jna,org.apache.jackrabbit,org.apache.httpcomponents,de.swiesend,org.purejava,com.github.hypfvieh</nonModularGroupIds>
<!-- cryptomator dependencies -->
<cryptomator.cryptolib.version>2.1.0-beta3</cryptomator.cryptolib.version>
<cryptomator.cryptofs.version>2.4.2</cryptomator.cryptofs.version>
<cryptomator.integrations.version>1.1.0</cryptomator.integrations.version>
<cryptomator.integrations.win.version>1.1.2</cryptomator.integrations.win.version>
@@ -37,16 +38,18 @@
<cryptomator.webdav.version>1.2.8</cryptomator.webdav.version>
<!-- 3rd party dependencies -->
<javafx.version>18.0.1</javafx.version>
<commons-lang3.version>3.12.0</commons-lang3.version>
<jwt.version>4.0.0</jwt.version>
<dagger.version>2.41</dagger.version>
<easybind.version>2.2</easybind.version>
<guava.version>31.1-jre</guava.version>
<dagger.version>2.41</dagger.version>
<gson.version>2.9.0</gson.version>
<zxcvbn.version>1.7.0</zxcvbn.version>
<slf4j.version>1.7.36</slf4j.version>
<javafx.version>18.0.1</javafx.version>
<jwt.version>4.0.0</jwt.version>
<nimbus-jose.version>9.23</nimbus-jose.version>
<logback.version>1.2.11</logback.version>
<slf4j.version>1.7.36</slf4j.version>
<tinyoauth2.version>0.5.1</tinyoauth2.version>
<zxcvbn.version>1.7.0</zxcvbn.version>
<!-- test dependencies -->
<junit.jupiter.version>5.8.1</junit.jupiter.version>
@@ -61,6 +64,11 @@
<dependencies>
<!-- Cryptomator Libs -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>cryptolib</artifactId>
<version>${cryptomator.cryptolib.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>cryptofs</artifactId>
@@ -133,12 +141,22 @@
<version>${commons-lang3.version}</version>
</dependency>
<!-- JWT -->
<!-- OAuth/JWT -->
<dependency>
<groupId>io.github.coffeelibs</groupId>
<artifactId>tiny-oauth2-client</artifactId>
<version>${tinyoauth2.version}</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>${nimbus-jose.version}</version>
</dependency>
<!-- EasyBind -->
<dependency>

View File

@@ -4,35 +4,45 @@ import org.cryptomator.ui.traymenu.AwtTrayMenuController;
module org.cryptomator.desktop {
requires static org.jetbrains.annotations;
requires org.cryptomator.cryptolib;
requires org.cryptomator.cryptofs;
requires org.cryptomator.frontend.dokany;
requires org.cryptomator.frontend.fuse;
requires org.cryptomator.frontend.webdav;
requires org.cryptomator.integrations.api;
// jdk:
requires java.desktop;
requires java.net.http;
requires javafx.base;
requires javafx.graphics;
requires javafx.controls;
requires javafx.fxml;
requires com.tobiasdiez.easybind;
requires jdk.crypto.ec;
// 3rd party:
requires com.auth0.jwt;
requires com.google.common;
requires com.google.gson;
requires com.nulabinc.zxcvbn;
requires com.tobiasdiez.easybind;
requires dagger;
requires io.github.coffeelibs.tinyoauth2client;
requires org.slf4j;
requires org.apache.commons.lang3;
requires dagger;
requires com.auth0.jwt;
/* TODO: filename-based modules: */
requires static javax.inject; /* ugly dagger/guava crap */
requires logback.classic;
requires logback.core;
requires com.nimbusds.jose.jwt;
exports org.cryptomator.ui.traymenu to org.cryptomator.integrations.api;
provides TrayMenuController with AwtTrayMenuController;
exports org.cryptomator.ui.keyloading.hub to com.fasterxml.jackson.databind;
opens org.cryptomator.common.settings to com.google.gson;
opens org.cryptomator.ui.keyloading.hub to com.google.gson, javafx.fxml;
opens org.cryptomator.launcher to javafx.graphics;

View File

@@ -27,6 +27,7 @@ public class Environment {
private static final String SETTINGS_PATH_PROP_NAME = "cryptomator.settingsPath";
private static final String IPC_SOCKET_PATH_PROP_NAME = "cryptomator.ipcSocketPath";
private static final String KEYCHAIN_PATHS_PROP_NAME = "cryptomator.integrationsWin.keychainPaths";
private static final String P12_PATH_PROP_NAME = "cryptomator.p12Path";
private static final String LOG_DIR_PROP_NAME = "cryptomator.logDir";
private static final String MOUNTPOINT_DIR_PROP_NAME = "cryptomator.mountPointsDir";
private static final String MIN_PW_LENGTH_PROP_NAME = "cryptomator.minPwLength";
@@ -52,6 +53,7 @@ public class Environment {
LOG.debug("{}: {}", APP_VERSION_PROP_NAME, System.getProperty(APP_VERSION_PROP_NAME));
LOG.debug("{}: {}", BUILD_NUMBER_PROP_NAME, System.getProperty(BUILD_NUMBER_PROP_NAME));
LOG.debug("{}: {}", TRAY_ICON_PROP_NAME, System.getProperty(TRAY_ICON_PROP_NAME));
LOG.debug("{}: {}", P12_PATH_PROP_NAME, System.getProperty(P12_PATH_PROP_NAME));
}
public boolean useCustomLogbackConfig() {
@@ -62,6 +64,10 @@ public class Environment {
return getPaths(SETTINGS_PATH_PROP_NAME);
}
public Stream<Path> getP12Path() {
return getPaths(P12_PATH_PROP_NAME);
}
public Stream<Path> ipcSocketPath() {
return getPaths(IPC_SOCKET_PATH_PROP_NAME);
}

View File

@@ -0,0 +1,104 @@
package org.cryptomator.common.settings;
import com.google.common.base.Preconditions;
import com.google.common.base.Suppliers;
import com.google.common.io.BaseEncoding;
import org.cryptomator.common.Environment;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.cryptolib.common.P384KeyPair;
import org.cryptomator.cryptolib.common.Pkcs12Exception;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.nio.CharBuffer;
import java.nio.file.Files;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.UUID;
import java.util.function.Supplier;
@Singleton
public class DeviceKey {
private static final Logger LOG = LoggerFactory.getLogger(DeviceKey.class);
private static final String KEYCHAIN_KEY = "cryptomator-device-p12";
private final KeychainManager keychainManager;
private final Environment env;
private final SecureRandom csprng;
private final Supplier<P384KeyPair> keyPairSupplier;
@Inject
public DeviceKey(KeychainManager keychainManager, Environment env, SecureRandom csprng) {
this.keychainManager = keychainManager;
this.env = env;
this.csprng = csprng;
this.keyPairSupplier = Suppliers.memoize(this::loadOrCreate);
}
public P384KeyPair get() throws DeviceKeyRetrievalException {
Preconditions.checkState(keychainManager.isSupported());
return keyPairSupplier.get();
}
private P384KeyPair loadOrCreate() throws DeviceKeyRetrievalException {
char[] passphrase = null;
try {
passphrase = keychainManager.loadPassphrase(KEYCHAIN_KEY);
if (passphrase != null) {
return loadExistingKeyPair(passphrase);
} else {
passphrase = randomPassword();
keychainManager.storePassphrase(KEYCHAIN_KEY, CharBuffer.wrap(passphrase));
return createAndStoreNewKeyPair(passphrase);
}
} catch (KeychainAccessException e) {
throw new DeviceKeyRetrievalException("Failed to access system keychain", e);
} catch (Pkcs12Exception | IOException e) {
throw new DeviceKeyRetrievalException("Failed to access .p12 file", e);
} finally {
if (passphrase != null) {
Arrays.fill(passphrase, '\0');
}
}
}
private P384KeyPair loadExistingKeyPair(char[] passphrase) throws IOException {
var p12File = env.getP12Path() //
.filter(Files::isRegularFile) //
.findFirst() //
.orElseThrow(() -> new DeviceKeyRetrievalException("Missing .p12 file"));
LOG.debug("Loading existing device key from {}", p12File);
return P384KeyPair.load(p12File, passphrase);
}
private P384KeyPair createAndStoreNewKeyPair(char[] passphrase) throws IOException {
var p12File = env.getP12Path() //
.findFirst() //
.orElseThrow(() -> new DeviceKeyRetrievalException("No path for .p12 file configured"));
var keyPair = P384KeyPair.generate();
LOG.debug("Store new device key to {}", p12File);
keyPair.store(p12File, passphrase);
return keyPair;
}
private char[] randomPassword() {
// this is a fast & easy attempt to create a random string:
var uuid = new UUID(csprng.nextLong(), csprng.nextLong());
return uuid.toString().toCharArray();
}
public static class DeviceKeyRetrievalException extends RuntimeException {
private DeviceKeyRetrievalException(String message) {
super(message);
}
private DeviceKeyRetrievalException(String message, Throwable cause) {
super(message, cause);
}
}
}

View File

@@ -13,6 +13,12 @@ public enum FxmlFile {
FORGET_PASSWORD("/fxml/forget_password.fxml"), //
HEALTH_START("/fxml/health_start.fxml"), //
HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), //
HUB_AUTH_FLOW("/fxml/hub_auth_flow.fxml"), //
HUB_RECEIVE_KEY("/fxml/hub_receive_key.fxml"), //
HUB_REGISTER_DEVICE("/fxml/hub_register_device.fxml"), //
HUB_REGISTER_SUCCESS("/fxml/hub_register_success.fxml"), //
HUB_REGISTER_FAILED("/fxml/hub_register_failed.fxml"),
HUB_UNAUTHORIZED_DEVICE("/fxml/hub_unauthorized_device.fxml"), //
LOCK_FORCED("/fxml/lock_forced.fxml"), //
LOCK_FAILED("/fxml/lock_failed.fxml"), //
MAIN_WINDOW("/fxml/main_window.fxml"), //

View File

@@ -6,6 +6,7 @@ import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.keyloading.hub.HubKeyLoadingModule;
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule;
import javax.inject.Provider;
@@ -13,7 +14,7 @@ import java.io.IOException;
import java.util.Map;
import java.util.ResourceBundle;
@Module(includes = {MasterkeyFileLoadingModule.class})
@Module(includes = {MasterkeyFileLoadingModule.class, HubKeyLoadingModule.class})
abstract class KeyLoadingModule {
@Provides

View File

@@ -0,0 +1,5 @@
package org.cryptomator.ui.keyloading.hub;
record AuthFlowContext(String deviceId) {
}

View File

@@ -0,0 +1,101 @@
package org.cryptomator.ui.keyloading.hub;
import com.nimbusds.jose.JWEObject;
import dagger.Lazy;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.WorkerStateEvent;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import java.net.URI;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
@KeyLoadingScoped
public class AuthFlowController implements FxController {
private final Application application;
private final Stage window;
private final ExecutorService executor;
private final String deviceId;
private final HubConfig hubConfig;
private final AtomicReference<String> tokenRef;
private final CompletableFuture<JWEObject> result;
private final Lazy<Scene> receiveKeyScene;
private final ObjectProperty<URI> authUri;
private AuthFlowTask task;
@Inject
public AuthFlowController(Application application, @KeyLoading Stage window, ExecutorService executor, @Named("deviceId") String deviceId, HubConfig hubConfig, @Named("bearerToken") AtomicReference<String> tokenRef, CompletableFuture<JWEObject> result, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy<Scene> receiveKeyScene) {
this.application = application;
this.window = window;
this.executor = executor;
this.deviceId = deviceId;
this.hubConfig = hubConfig;
this.tokenRef = tokenRef;
this.result = result;
this.receiveKeyScene = receiveKeyScene;
this.authUri = new SimpleObjectProperty<>();
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
}
@FXML
public void initialize() {
assert task == null;
task = new AuthFlowTask(hubConfig, new AuthFlowContext(deviceId), this::setAuthUri);
task.setOnFailed(this::authFailed);
task.setOnSucceeded(this::authSucceeded);
executor.submit(task);
}
@FXML
public void browse() {
application.getHostServices().showDocument(authUri.get().toString());
}
@FXML
public void cancel() {
window.close();
}
private void setAuthUri(URI uri) {
Platform.runLater(() -> {
authUri.set(uri);
browse();
});
}
private void windowClosed(WindowEvent windowEvent) {
// stop server, if it is still running
task.cancel();
result.cancel(true);
}
private void authSucceeded(WorkerStateEvent workerStateEvent) {
tokenRef.set(task.getValue());
window.requestFocus();
window.setScene(receiveKeyScene.get());
}
private void authFailed(WorkerStateEvent workerStateEvent) {
window.requestFocus();
var exception = workerStateEvent.getSource().getException();
result.completeExceptionally(exception);
}
}

View File

@@ -0,0 +1,53 @@
package org.cryptomator.ui.keyloading.hub;
import com.google.gson.JsonParser;
import io.github.coffeelibs.tinyoauth2client.AuthFlow;
import io.github.coffeelibs.tinyoauth2client.TinyOAuth2;
import io.github.coffeelibs.tinyoauth2client.http.response.Response;
import javafx.concurrent.Task;
import java.io.IOException;
import java.net.URI;
import java.util.function.Consumer;
class AuthFlowTask extends Task<String> {
private final HubConfig hubConfig;
private final AuthFlowContext authFlowContext;
private final Consumer<URI> redirectUriConsumer;
/**
* Spawns a server and waits for the redirectUri to be called.
*
* @param hubConfig Configuration object holding parameters required by {@link AuthFlow}
* @param redirectUriConsumer A callback invoked with the redirectUri, as soon as the server has started
*/
public AuthFlowTask(HubConfig hubConfig, AuthFlowContext authFlowContext, Consumer<URI> redirectUriConsumer) {
this.hubConfig = hubConfig;
this.authFlowContext = authFlowContext;
this.redirectUriConsumer = redirectUriConsumer;
}
@Override
protected String call() throws IOException, InterruptedException {
var response = TinyOAuth2.client(hubConfig.clientId) //
.withTokenEndpoint(URI.create(hubConfig.tokenEndpoint)) //
.authFlow(URI.create(hubConfig.authEndpoint)) //
.setSuccessResponse(Response.redirect(URI.create(hubConfig.authSuccessUrl + "&device=" + authFlowContext.deviceId()))) //
.setErrorResponse(Response.redirect(URI.create(hubConfig.authErrorUrl + "&device=" + authFlowContext.deviceId()))) //
.authorize(redirectUriConsumer);
if (response.statusCode() != 200) {
throw new NotOkResponseException("Authorization returned status code " + response.statusCode());
}
var json = JsonParser.parseString(response.body());
return json.getAsJsonObject().get("access_token").getAsString();
}
public static class NotOkResponseException extends RuntimeException {
NotOkResponseException(String msg) {
super(msg);
}
}
}

View File

@@ -0,0 +1,9 @@
package org.cryptomator.ui.keyloading.hub;
class CreateDeviceDto {
public String id;
public String name;
public String publicKey;
}

View File

@@ -0,0 +1,23 @@
package org.cryptomator.ui.keyloading.hub;
import com.google.common.io.CharStreams;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
class HttpHelper {
public static String readBody(HttpResponse<InputStream> response) throws IOException {
try (var in = response.body(); var reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
return CharStreams.toString(reader);
}
}
}

View File

@@ -0,0 +1,13 @@
package org.cryptomator.ui.keyloading.hub;
// needs to be accessible by JSON decoder
public class HubConfig {
public String clientId;
public String authEndpoint;
public String tokenEndpoint;
public String devicesResourceUrl;
public String authSuccessUrl;
public String authErrorUrl;
}

View File

@@ -0,0 +1,166 @@
package org.cryptomator.ui.keyloading.hub;
import com.google.common.io.BaseEncoding;
import com.nimbusds.jose.JWEObject;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import dagger.multibindings.StringKey;
import org.cryptomator.common.settings.DeviceKey;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptolib.common.MessageDigestSupplier;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import javax.inject.Named;
import javafx.scene.Scene;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
@Module
public abstract class HubKeyLoadingModule {
@Provides
@KeyLoadingScoped
static HubConfig provideHubConfig(@KeyLoading Vault vault) {
try {
return vault.getVaultConfigCache().get().getHeader("hub", HubConfig.class);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Provides
@KeyLoadingScoped
@Named("windowTitle")
static String provideWindowTitle(@KeyLoading Vault vault, ResourceBundle resourceBundle) {
return String.format(resourceBundle.getString("unlock.title"), vault.getDisplayName());
}
@Provides
@KeyLoadingScoped
@Named("deviceId")
static String provideDeviceId(DeviceKey deviceKey) {
var publicKey = Objects.requireNonNull(deviceKey.get()).getPublic().getEncoded();
var hashedKey = MessageDigestSupplier.SHA256.get().digest(publicKey);
return BaseEncoding.base16().encode(hashedKey);
}
@Provides
@Named("bearerToken")
@KeyLoadingScoped
static AtomicReference<String> provideBearerTokenRef() {
return new AtomicReference<>();
}
@Provides
@KeyLoadingScoped
static CompletableFuture<JWEObject> provideResult() {
return new CompletableFuture<>();
}
@Binds
@IntoMap
@KeyLoadingScoped
@StringKey(HubKeyLoadingStrategy.SCHEME_HUB_HTTP)
abstract KeyLoadingStrategy bindHubKeyLoadingStrategyToHubHttp(HubKeyLoadingStrategy strategy);
@Binds
@IntoMap
@KeyLoadingScoped
@StringKey(HubKeyLoadingStrategy.SCHEME_HUB_HTTPS)
abstract KeyLoadingStrategy bindHubKeyLoadingStrategyToHubHttps(HubKeyLoadingStrategy strategy);
@Provides
@FxmlScene(FxmlFile.HUB_AUTH_FLOW)
@KeyLoadingScoped
static Scene provideHubAuthFlowScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.HUB_AUTH_FLOW);
}
@Provides
@FxmlScene(FxmlFile.HUB_RECEIVE_KEY)
@KeyLoadingScoped
static Scene provideHubReceiveKeyScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.HUB_RECEIVE_KEY);
}
@Provides
@FxmlScene(FxmlFile.HUB_REGISTER_DEVICE)
@KeyLoadingScoped
static Scene provideHubRegisterDeviceScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.HUB_REGISTER_DEVICE);
}
@Provides
@FxmlScene(FxmlFile.HUB_REGISTER_SUCCESS)
@KeyLoadingScoped
static Scene provideHubRegisterSuccessScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.HUB_REGISTER_SUCCESS);
}
@Provides
@FxmlScene(FxmlFile.HUB_REGISTER_FAILED)
@KeyLoadingScoped
static Scene provideHubRegisterFailedScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.HUB_REGISTER_FAILED);
}
@Provides
@FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE)
@KeyLoadingScoped
static Scene provideHubUnauthorizedDeviceScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE);
}
@Binds
@IntoMap
@FxControllerKey(AuthFlowController.class)
abstract FxController bindAuthFlowController(AuthFlowController controller);
@Provides
@IntoMap
@FxControllerKey(NewPasswordController.class)
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) {
return new NewPasswordController(resourceBundle, strengthRater);
}
@Binds
@IntoMap
@FxControllerKey(ReceiveKeyController.class)
abstract FxController bindReceiveKeyController(ReceiveKeyController controller);
@Binds
@IntoMap
@FxControllerKey(RegisterDeviceController.class)
abstract FxController bindRegisterDeviceController(RegisterDeviceController controller);
@Binds
@IntoMap
@FxControllerKey(RegisterSuccessController.class)
abstract FxController bindRegisterSuccessController(RegisterSuccessController controller);
@Binds
@IntoMap
@FxControllerKey(RegisterFailedController.class)
abstract FxController bindRegisterFailedController(RegisterFailedController controller);
@Binds
@IntoMap
@FxControllerKey(UnauthorizedDeviceController.class)
abstract FxController bindUnauthorizedDeviceController(UnauthorizedDeviceController controller);
}

View File

@@ -0,0 +1,80 @@
package org.cryptomator.ui.keyloading.hub;
import com.google.common.base.Preconditions;
import com.nimbusds.jose.JWEObject;
import dagger.Lazy;
import org.cryptomator.common.settings.DeviceKey;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.cryptomator.ui.unlock.UnlockCancelledException;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.Window;
import java.net.URI;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@KeyLoading
public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
private static final String SCHEME_PREFIX = "hub+";
static final String SCHEME_HUB_HTTP = SCHEME_PREFIX + "http";
static final String SCHEME_HUB_HTTPS = SCHEME_PREFIX + "https";
private final Stage window;
private final Lazy<Scene> authFlowScene;
private final CompletableFuture<JWEObject> result;
private final DeviceKey deviceKey;
@Inject
public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy<Scene> authFlowScene, CompletableFuture<JWEObject> result, DeviceKey deviceKey, @Named("windowTitle") String windowTitle) {
this.window = window;
window.setTitle(windowTitle);
this.authFlowScene = authFlowScene;
this.result = result;
this.deviceKey = deviceKey;
}
@Override
public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException {
Preconditions.checkArgument(keyId.getScheme().startsWith(SCHEME_PREFIX));
try {
startAuthFlow();
var jwe = result.get();
return JWEHelper.decrypt(jwe, deviceKey.get().getPrivate());
} catch (DeviceKey.DeviceKeyRetrievalException e) {
throw new MasterkeyLoadingFailedException("Failed to load keypair", e);
} catch (CancellationException e) {
throw new UnlockCancelledException("User cancelled auth workflow", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new UnlockCancelledException("Loading interrupted", e);
} catch (ExecutionException e) {
throw new MasterkeyLoadingFailedException("Failed to retrieve key", e);
}
}
private void startAuthFlow() {
Platform.runLater(() -> {
window.setScene(authFlowScene.get());
window.show();
Window owner = window.getOwner();
if (owner != null) {
window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2);
window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2);
} else {
window.centerOnScreen();
}
});
}
}

View File

@@ -0,0 +1,55 @@
package org.cryptomator.ui.keyloading.hub;
import com.google.common.base.Preconditions;
import com.google.common.io.BaseEncoding;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWEObject;
import com.nimbusds.jose.crypto.ECDHDecrypter;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.interfaces.ECPrivateKey;
import java.util.Arrays;
class JWEHelper {
private static final Logger LOG = LoggerFactory.getLogger(JWEHelper.class);
private static final String JWE_PAYLOAD_MASTERKEY_FIELD = "key";
private JWEHelper(){}
public static Masterkey decrypt(JWEObject jwe, ECPrivateKey privateKey) throws MasterkeyLoadingFailedException {
try {
jwe.decrypt(new ECDHDecrypter(privateKey));
return readKey(jwe);
} catch (JOSEException e) {
LOG.warn("Failed to decrypt JWE: {}", jwe);
throw new MasterkeyLoadingFailedException("Failed to decrypt JWE", e);
}
}
private static Masterkey readKey(JWEObject jwe) throws MasterkeyLoadingFailedException {
Preconditions.checkArgument(jwe.getState() == JWEObject.State.DECRYPTED);
var fields = jwe.getPayload().toJSONObject();
if (fields == null) {
LOG.error("Expected JWE payload to be JSON: {}", jwe.getPayload());
throw new MasterkeyLoadingFailedException("Expected JWE payload to be JSON");
}
var keyBytes = new byte[0];
try {
if (fields.get(JWE_PAYLOAD_MASTERKEY_FIELD) instanceof String key) {
keyBytes = BaseEncoding.base64().decode(key);
return new Masterkey(keyBytes);
} else {
throw new IllegalArgumentException("JWE payload doesn't contain field " + JWE_PAYLOAD_MASTERKEY_FIELD);
}
} catch (IllegalArgumentException e) {
LOG.error("Unexpected JWE payload: {}", jwe.getPayload());
throw new MasterkeyLoadingFailedException("Unexpected JWE payload", e);
} finally {
Arrays.fill(keyBytes, (byte) 0x00);
}
}
}

View File

@@ -0,0 +1,140 @@
package org.cryptomator.ui.keyloading.hub;
import com.nimbusds.jose.JWEObject;
import dagger.Lazy;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.text.ParseException;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
@KeyLoadingScoped
public class ReceiveKeyController implements FxController {
private static final String SCHEME_PREFIX = "hub+";
private final Stage window;
private final String deviceId;
private final String bearerToken;
private final CompletableFuture<JWEObject> result;
private final Lazy<Scene> registerDeviceScene;
private final Lazy<Scene> unauthorizedScene;
private final URI vaultBaseUri;
private final HttpClient httpClient;
@Inject
public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, @Named("deviceId") String deviceId, @Named("bearerToken") AtomicReference<String> tokenRef, CompletableFuture<JWEObject> result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy<Scene> registerDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy<Scene> unauthorizedScene) {
this.window = window;
this.deviceId = deviceId;
this.bearerToken = Objects.requireNonNull(tokenRef.get());
this.result = result;
this.registerDeviceScene = registerDeviceScene;
this.unauthorizedScene = unauthorizedScene;
this.vaultBaseUri = getVaultBaseUri(vault);
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
this.httpClient = HttpClient.newBuilder().executor(executor).build();
}
@FXML
public void initialize() {
var keyUri = appendPath(vaultBaseUri, "/keys/" + deviceId);
var request = HttpRequest.newBuilder(keyUri) //
.header("Authorization", "Bearer " + bearerToken) //
.GET() //
.build();
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()) //
.thenAcceptAsync(this::loadedExistingKey, Platform::runLater) //
.exceptionally(this::retrievalFailed);
}
private void loadedExistingKey(HttpResponse<InputStream> response) {
try {
switch (response.statusCode()) {
case 200 -> retrievalSucceeded(response);
case 403 -> accessNotGranted();
case 404 -> needsDeviceRegistration();
default -> throw new IOException("Unexpected response " + response.statusCode());
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private void retrievalSucceeded(HttpResponse<InputStream> response) throws IOException {
try {
var string = HttpHelper.readBody(response);
result.complete(JWEObject.parse(string));
window.close();
} catch (ParseException e) {
throw new IOException("Failed to parse JWE", e);
}
}
private void needsDeviceRegistration() {
window.setScene(registerDeviceScene.get());
}
private void accessNotGranted() {
window.setScene(unauthorizedScene.get());
}
private Void retrievalFailed(Throwable cause) {
result.completeExceptionally(cause);
return null;
}
@FXML
public void cancel() {
window.close();
}
private void windowClosed(WindowEvent windowEvent) {
result.cancel(true);
}
private static URI appendPath(URI base, String path) {
try {
var newPath = base.getPath() + path;
return new URI(base.getScheme(), base.getAuthority(), newPath, base.getQuery(), base.getFragment());
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Can't append '" + path + "' to URI: " + base, e);
}
}
private static URI getVaultBaseUri(Vault vault) {
try {
var kid = vault.getVaultConfigCache().get().getKeyId();
assert kid.getScheme().startsWith(SCHEME_PREFIX);
var hubUriScheme = kid.getScheme().substring(SCHEME_PREFIX.length());
return new URI(hubUriScheme, kid.getSchemeSpecificPart(), kid.getFragment());
} catch (IOException e) {
throw new UncheckedIOException(e);
} catch (URISyntaxException e) {
throw new IllegalStateException("URI constructed from params known to be valid", e);
}
}
}

View File

@@ -0,0 +1,176 @@
package org.cryptomator.ui.keyloading.hub;
import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.google.common.io.BaseEncoding;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.nimbusds.jose.JWEObject;
import dagger.Lazy;
import org.cryptomator.common.settings.DeviceKey;
import org.cryptomator.cryptolib.common.P384KeyPair;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TextField;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
@KeyLoadingScoped
public class RegisterDeviceController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(RegisterDeviceController.class);
private static final Gson GSON = new GsonBuilder().setLenient().create();
private static final List<Integer> EXPECTED_RESPONSE_CODES = List.of(201, 409);
private final Stage window;
private final HubConfig hubConfig;
private final String bearerToken;
private final Lazy<Scene> registerSuccessScene;
private final Lazy<Scene> registerFailedScene;
private final String deviceId;
private final P384KeyPair keyPair;
private final CompletableFuture<JWEObject> result;
private final DecodedJWT jwt;
private final HttpClient httpClient;
private final BooleanProperty deviceNameAlreadyExists = new SimpleBooleanProperty(false);
public TextField deviceNameField;
public Button registerBtn;
@Inject
public RegisterDeviceController(@KeyLoading Stage window, ExecutorService executor, HubConfig hubConfig, @Named("deviceId") String deviceId, DeviceKey deviceKey, CompletableFuture<JWEObject> result, @Named("bearerToken") AtomicReference<String> bearerToken, @FxmlScene(FxmlFile.HUB_REGISTER_SUCCESS) Lazy<Scene> registerSuccessScene, @FxmlScene(FxmlFile.HUB_REGISTER_FAILED) Lazy<Scene> registerFailedScene) {
this.window = window;
this.hubConfig = hubConfig;
this.deviceId = deviceId;
this.keyPair = Objects.requireNonNull(deviceKey.get());
this.result = result;
this.bearerToken = Objects.requireNonNull(bearerToken.get());
this.registerSuccessScene = registerSuccessScene;
this.registerFailedScene = registerFailedScene;
this.jwt = JWT.decode(this.bearerToken);
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
this.httpClient = HttpClient.newBuilder().executor(executor).build();
}
public void initialize() {
deviceNameField.setText(determineHostname());
deviceNameField.textProperty().addListener(observable -> deviceNameAlreadyExists.set(false));
}
private String determineHostname() {
try {
var hostName = InetAddress.getLocalHost().getHostName();
return Objects.requireNonNullElse(hostName, "");
} catch (IOException e) {
return "";
}
}
@FXML
public void register() {
deviceNameAlreadyExists.set(false);
registerBtn.setContentDisplay(ContentDisplay.LEFT);
registerBtn.setDisable(true);
var keyUri = URI.create(hubConfig.devicesResourceUrl + deviceId);
var deviceKey = keyPair.getPublic().getEncoded();
var dto = new CreateDeviceDto();
dto.id = deviceId;
dto.name = deviceNameField.getText();
dto.publicKey = BaseEncoding.base64Url().omitPadding().encode(deviceKey);
var json = GSON.toJson(dto); // TODO: do we want to keep GSON? doesn't support records -.-
var request = HttpRequest.newBuilder(keyUri) //
.header("Authorization", "Bearer " + bearerToken) //
.header("Content-Type", "application/json").PUT(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) //
.build();
httpClient.sendAsync(request, HttpResponse.BodyHandlers.discarding()) //
.thenApply(response -> {
if (EXPECTED_RESPONSE_CODES.contains(response.statusCode())) {
return response;
} else {
throw new RuntimeException("Server answered with unexpected status code " + response.statusCode());
}
}).handleAsync((response, throwable) -> {
if (response != null) {
this.handleResponse(response);
} else {
this.registrationFailed(throwable);
}
return null;
}, Platform::runLater);
}
private void handleResponse(HttpResponse<Void> voidHttpResponse) {
assert EXPECTED_RESPONSE_CODES.contains(voidHttpResponse.statusCode());
if (voidHttpResponse.statusCode() == 409) {
deviceNameAlreadyExists.set(true);
registerBtn.setContentDisplay(ContentDisplay.TEXT_ONLY);
registerBtn.setDisable(false);
} else {
LOG.debug("Device registration for hub instance {} successful.", hubConfig.authSuccessUrl);
window.setScene(registerSuccessScene.get());
}
}
private void registrationFailed(Throwable cause) {
LOG.warn("Device registration failed.", cause);
window.setScene(registerFailedScene.get());
result.completeExceptionally(cause);
}
@FXML
public void close() {
window.close();
}
private void windowClosed(WindowEvent windowEvent) {
result.cancel(true);
}
/* Getter */
public String getUserName() {
return jwt.getClaim("email").asString();
}
//--- Getters & Setters
public BooleanProperty deviceNameAlreadyExistsProperty() {
return deviceNameAlreadyExists;
}
public boolean getDeviceNameAlreadyExists() {
return deviceNameAlreadyExists.get();
}
}

View File

@@ -0,0 +1,29 @@
package org.cryptomator.ui.keyloading.hub;
import com.nimbusds.jose.JWEObject;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.keyloading.KeyLoading;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import java.util.concurrent.CompletableFuture;
public class RegisterFailedController implements FxController {
private final Stage window;
private final CompletableFuture<JWEObject> result;
@Inject
public RegisterFailedController(@KeyLoading Stage window, CompletableFuture<JWEObject> result) {
this.window = window;
this.result = result;
}
@FXML
public void close() {
window.close();
}
}

View File

@@ -0,0 +1,24 @@
package org.cryptomator.ui.keyloading.hub;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.keyloading.KeyLoading;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.stage.Stage;
public class RegisterSuccessController implements FxController {
private final Stage window;
@Inject
public RegisterSuccessController(@KeyLoading Stage window) {
this.window = window;
}
@FXML
public void close() {
window.close();
}
}

View File

@@ -0,0 +1,35 @@
package org.cryptomator.ui.keyloading.hub;
import com.nimbusds.jose.JWEObject;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import java.util.concurrent.CompletableFuture;
@KeyLoadingScoped
public class UnauthorizedDeviceController implements FxController {
private final Stage window;
private final CompletableFuture<JWEObject> result;
@Inject
public UnauthorizedDeviceController(@KeyLoading Stage window, CompletableFuture<JWEObject> result) {
this.window = window;
this.result = result;
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
}
@FXML
public void close() {
window.close();
}
private void windowClosed(WindowEvent windowEvent) {
result.cancel(true);
}
}

View File

@@ -0,0 +1,24 @@
/**
* This {@link org.cryptomator.ui.keyloading.KeyLoadingStrategy strategy} retrieves the vault key from a web application, similar to
* <a href="https://datatracker.ietf.org/doc/html/rfc8252#section-7.3">RFC 8252</a> but with an encrypted masterkey instead of an authorization code.
* <p>
* If the <code>kid</code> of the vault config starts with either {@value org.cryptomator.ui.keyloading.hub.HubKeyLoadingStrategy#SCHEME_HUB_HTTP}
* or {@value org.cryptomator.ui.keyloading.hub.HubKeyLoadingStrategy#SCHEME_HUB_HTTPS}, the included http address is amended by three parameters and opened
* in a browser. These parameters are:
* <ul>
* <li>A device-specific public key (generated by this application and stored among its settings</li>
* <li>A unique device ID (stored in settings)</li>
* <li>A loopback callback address</li>
* </ul>
* <p>
* The callback address points to a embedded web server waiting to receive the masterkey encrypted specifically for this device, using the device-specific public key.
* <p>
* The vault key can be decrypted using this ECIES:
* <ol>
* <li>Generate shared secret using ECDH without cofactor</li>
* <li>Derive 44 bytes using ANSI X9.63 KDF with SHA256</li>
* <li>Decrypt payload via AES-GCM, using first 32 bytes as key, last 12 bytes as IV</li>
* <li>No MAC check required, as AES-GCM includes a tag already</li>
* </ol>
*/
package org.cryptomator.ui.keyloading.hub;

View File

@@ -1,10 +1,10 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import org.cryptomator.common.Nullable;
import org.cryptomator.common.Passphrase;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.common.Passphrase;
import org.cryptomator.ui.common.WeakBindings;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;

View File

@@ -1,5 +1,6 @@
package org.cryptomator.ui.vaultoptions;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -18,6 +19,7 @@ public class VaultOptionsController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(VaultOptionsController.class);
private final Stage window;
private final Vault vault;
private final ObjectProperty<SelectedVaultOptionsTab> selectedTabProperty;
public TabPane tabPane;
public Tab generalTab;
@@ -25,8 +27,9 @@ public class VaultOptionsController implements FxController {
public Tab keyTab;
@Inject
VaultOptionsController(@VaultOptionsWindow Stage window, ObjectProperty<SelectedVaultOptionsTab> selectedTabProperty) {
VaultOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, ObjectProperty<SelectedVaultOptionsTab> selectedTabProperty) {
this.window = window;
this.vault = vault;
this.selectedTabProperty = selectedTabProperty;
}
@@ -35,6 +38,9 @@ public class VaultOptionsController implements FxController {
window.setOnShowing(this::windowWillAppear);
selectedTabProperty.addListener(observable -> this.selectChosenTab());
tabPane.getSelectionModel().selectedItemProperty().addListener(observable -> this.selectedTabChanged());
if(!vault.getVaultConfigCache().getUnchecked().getKeyId().getScheme().equals("masterkeyfile")){
tabPane.getTabs().remove(keyTab);
}
}
private void selectChosenTab() {

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Hyperlink?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.Group?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<?import javafx.scene.text.TextFlow?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<HBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.keyloading.hub.AuthFlowController"
minWidth="400"
maxWidth="400"
minHeight="145"
spacing="12"
alignment="TOP_LEFT">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<Group>
<StackPane>
<padding>
<Insets topRightBottomLeft="6"/>
</padding>
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5Spinner styleClass="glyph-icon-white" glyphSize="24"/>
</StackPane>
</Group>
<VBox HBox.hgrow="ALWAYS">
<Label styleClass="label-large" text="%hub.auth.message" wrapText="true" textAlignment="LEFT">
<padding>
<Insets bottom="6" top="6"/>
</padding>
</Label>
<Label text="%hub.auth.description" wrapText="true"/>
<Hyperlink styleClass="hyperlink-underline" text="%hub.auth.loginLink" onAction="#browse">
<graphic>
<FontAwesome5IconView glyph="LINK" glyphSize="12"/>
</graphic>
<padding>
<Insets top="12"/>
</padding>
</Hyperlink>
<Region VBox.vgrow="ALWAYS" minHeight="18"/>
<ButtonBar buttonMinWidth="120" buttonOrder="+C">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel"/>
</buttons>
</ButtonBar>
</VBox>
</children>
</HBox>

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.Group?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<HBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.keyloading.hub.ReceiveKeyController"
minWidth="400"
maxWidth="400"
minHeight="145"
spacing="12"
alignment="TOP_LEFT">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<Group>
<StackPane>
<padding>
<Insets topRightBottomLeft="6"/>
</padding>
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5Spinner styleClass="glyph-icon-white" glyphSize="24"/>
</StackPane>
</Group>
<VBox HBox.hgrow="ALWAYS">
<Label styleClass="label-large" text="%hub.receive.message" wrapText="true" textAlignment="LEFT">
<padding>
<Insets bottom="6" top="6"/>
</padding>
</Label>
<Label text="%hub.receive.description" wrapText="true"/>
<Region VBox.vgrow="ALWAYS" minHeight="18"/>
<ButtonBar buttonMinWidth="120" buttonOrder="+C">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel"/>
</buttons>
</ButtonBar>
</VBox>
</children>
</HBox>

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.Group?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
<HBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.keyloading.hub.RegisterDeviceController"
minWidth="400"
maxWidth="400"
minHeight="145"
spacing="12"
alignment="TOP_LEFT">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<Group>
<StackPane>
<padding>
<Insets topRightBottomLeft="6"/>
</padding>
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="INFO" glyphSize="24"/>
</StackPane>
</Group>
<VBox HBox.hgrow="ALWAYS">
<Label styleClass="label-large" text="%hub.register.message" wrapText="true" textAlignment="LEFT">
<padding>
<Insets bottom="6" top="6"/>
</padding>
</Label>
<Label text="%hub.register.description" wrapText="true"/>
<HBox spacing="6" alignment="CENTER_LEFT">
<padding>
<Insets top="12"/>
</padding>
<Label text="%hub.register.nameLabel" labelFor="$deviceNameField"/>
<TextField fx:id="deviceNameField" HBox.hgrow="ALWAYS"/>
</HBox>
<HBox alignment="TOP_RIGHT">
<Label text="%hub.register.occupiedMsg" textAlignment="RIGHT" alignment="CENTER_RIGHT" visible="${controller.deviceNameAlreadyExists}" graphicTextGap="6">
<padding>
<Insets top="6"/>
</padding>
<graphic>
<FontAwesome5IconView glyph="TIMES" styleClass="glyph-icon-red"/>
</graphic>
</Label>
</HBox>
<Region VBox.vgrow="ALWAYS" minHeight="18"/>
<ButtonBar buttonMinWidth="120" buttonOrder="+CU">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
<Button fx:id="registerBtn" text="%hub.register.registerBtn" ButtonBar.buttonData="OTHER" defaultButton="true" onAction="#register" contentDisplay="TEXT_ONLY" >
<graphic>
<FontAwesome5Spinner glyphSize="12" />
</graphic>
</Button>
</buttons>
</ButtonBar>
</VBox>
</children>
</HBox>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.Group?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<HBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.keyloading.hub.RegisterFailedController"
minWidth="400"
maxWidth="400"
minHeight="145"
spacing="12"
alignment="TOP_LEFT">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<Group>
<StackPane>
<padding>
<Insets topRightBottomLeft="6"/>
</padding>
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="EXCLAMATION" glyphSize="24"/>
</StackPane>
</Group>
<VBox HBox.hgrow="ALWAYS">
<Label styleClass="label-large" text="%hub.registerFailed.message" wrapText="true" textAlignment="LEFT">
<padding>
<Insets bottom="6" top="6"/>
</padding>
</Label>
<Label text="%hub.registerFailed.description" wrapText="true"/>
<Region VBox.vgrow="ALWAYS" minHeight="18"/>
<ButtonBar buttonMinWidth="120" buttonOrder="+C">
<buttons>
<Button text="%generic.button.close" ButtonBar.buttonData="CANCEL_CLOSE" defaultButton="true" onAction="#close"/>
</buttons>
</ButtonBar>
</VBox>
</children>
</HBox>

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.Group?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<HBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.keyloading.hub.RegisterSuccessController"
minWidth="400"
maxWidth="400"
minHeight="145"
spacing="12"
alignment="TOP_LEFT">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<Group>
<StackPane>
<padding>
<Insets topRightBottomLeft="6"/>
</padding>
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="CHECK" glyphSize="24"/>
</StackPane>
</Group>
<VBox HBox.hgrow="ALWAYS">
<Label styleClass="label-large" text="%hub.registerSuccess.message" wrapText="true" textAlignment="LEFT">
<padding>
<Insets bottom="6" top="6"/>
</padding>
</Label>
<Label text="%hub.registerSuccess.description" wrapText="true"/>
<Region VBox.vgrow="ALWAYS" minHeight="18"/>
<ButtonBar buttonMinWidth="120" buttonOrder="+C">
<buttons>
<Button text="%generic.button.close" ButtonBar.buttonData="CANCEL_CLOSE" defaultButton="true" onAction="#close"/>
<!-- TODO: add request access button -->
</buttons>
</ButtonBar>
</VBox>
</children>
</HBox>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.Group?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<HBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.keyloading.hub.UnauthorizedDeviceController"
minWidth="400"
maxWidth="400"
minHeight="145"
spacing="12"
alignment="TOP_LEFT">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<Group>
<StackPane>
<padding>
<Insets topRightBottomLeft="6"/>
</padding>
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="BAN" glyphSize="24"/>
</StackPane>
</Group>
<VBox HBox.hgrow="ALWAYS">
<Label styleClass="label-large" text="%hub.unauthorized.message" wrapText="true" textAlignment="LEFT">
<padding>
<Insets bottom="6" top="6"/>
</padding>
</Label>
<Label text="%hub.unauthorized.description" wrapText="true"/>
<Region VBox.vgrow="ALWAYS" minHeight="18"/>
<ButtonBar buttonMinWidth="120" buttonOrder="+C">
<buttons>
<Button text="%generic.button.close" ButtonBar.buttonData="CANCEL_CLOSE" defaultButton="true" onAction="#close"/>
<!-- TODO: add request access button -->
<!--Button text="%generic.button.close" ButtonBar.buttonData="CANCEL_CLOSE" defaultButton="true" onAction="#close"/-->
</buttons>
</ButtonBar>
</VBox>
</children>
</HBox>

View File

@@ -28,7 +28,7 @@
<fx:include source="vault_options_mount.fxml"/>
</content>
</Tab>
<Tab fx:id="keyTab" id="KEY" text="%vaultOptions.masterkey">
<Tab fx:id="keyTab" id="KEY" text="%vaultOptions.masterkey"> <!-- is removed in controller, when config.keyid.scheme is not masterkeyfile -->
<graphic>
<FontAwesome5IconView glyph="KEY"/>
</graphic>

View File

@@ -129,6 +129,30 @@ unlock.error.message=Unable to unlock vault
unlock.error.invalidMountPoint.notExisting=Mount point "%s" is not a directory, not empty or does not exist.
unlock.error.invalidMountPoint.existing=Mount point "%s" already exists or parent folder is missing.
unlock.error.invalidMountPoint.driveLetterOccupied=Drive Letter "%s" is already in use.
## Hub
### Waiting
hub.auth.message=Waiting for authentication…
hub.auth.description=You should automatically be redirected to the login page.
hub.auth.loginLink=Not redirected? Click here to open it.
### Receive Key
hub.receive.message=Processing response…
hub.receive.description=Cryptomator is receiving and processing the response from Hub. Please wait.
### Register Device
hub.register.message=Device name required
hub.register.description=This seems to be the first Hub access from this device. In order to identify it for access authorization, you need to name this device.
hub.register.nameLabel=Device Name
hub.register.occupiedMsg=Name already in use
hub.register.registerBtn=Confirm
### Registration Success
hub.registerSuccess.message=Device named
hub.registerSuccess.description=To access the vault, your device needs to be authorized by the vault owner.
### Registration Failed
hub.registerFailed.message=Device naming failed
hub.registerFailed.description=An error was thrown in the naming process. For more details, look into the application log.
### Unauthorized
hub.unauthorized.message=Access denied
hub.unauthorized.description=Your device has not yet been authorized to access this vault. Ask the vault owner to authorize it.
# Lock
## Force

View File

@@ -0,0 +1,56 @@
package org.cryptomator.ui.keyloading.hub;
import com.nimbusds.jose.JWEObject;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.cryptomator.cryptolib.common.P384KeyPair;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Base64;
public class JWEHelperTest {
private static final String JWE = "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IllUcEY3bGtTc3JvZVVUVFdCb21LNzBTN0FhVTJyc0ptMURpZ1ZzbjRMY2F5eUxFNFBabldkYmFVcE9jQVV5a1ciLCJ5IjoiLU5pS3loUktjSk52Nm02Z0ZJUWc4cy1Xd1VXUW9uT3A5dkQ4cHpoa2tUU3U2RzFlU2FUTVlhZGltQ2Q4V0ExMSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..BECWGzd9UvhHcTJC.znt4TlS-qiNEjxiu2v-du_E1QOBnyBR6LCt865SHxD-kwRc1JwX_Lq9XVoFj2GnK9-9CgxhCLGurg5Jt9g38qv2brGAzWL7eSVeY1fIqdO_kUhLpGslRTN6h2U0NHJi2-iE.WDVI2kOk9Dy3PWHyIg8gKA";
private static final String PRIV_KEY = "ME8CAQAwEAYHKoZIzj0CAQYFK4EEACIEODA2AgEBBDEA6QybmBitf94veD5aCLr7nlkF5EZpaXHCfq1AXm57AKQyGOjTDAF9EQB28fMywTDQ";
private static final String PUB_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERxQR+NRN6Wga01370uBBzr2NHDbKIC56tPUEq2HX64RhITGhii8Zzbkb1HnRmdF0aq6uqmUy4jUhuxnKxsv59A6JeK7Unn+mpmm3pQAygjoGc9wrvoH4HWJSQYUlsXDu";
@Test
public void testDecrypt() throws ParseException, InvalidKeySpecException {
var jwe = JWEObject.parse(JWE);
var keyPair = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(PRIV_KEY)));
var masterkey = JWEHelper.decrypt(jwe, keyPair.getPrivate());
var expectedEncKey = new byte[32];
var expectedMacKey = new byte[32];
Arrays.fill(expectedEncKey, (byte) 0x55);
Arrays.fill(expectedMacKey, (byte) 0x77);
Assertions.assertArrayEquals(expectedEncKey, masterkey.getEncKey().getEncoded());
Assertions.assertArrayEquals(expectedMacKey, masterkey.getMacKey().getEncoded());
}
@ParameterizedTest
@ValueSource(strings = {
"eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6ImdodGR3VnNoUU8wRGFBdjVBOXBiZ1NCTW0yYzZKWVF4dkloR3p6RVdQTncxczZZcEFYeTRQTjBXRFJUWExtQ2wiLCJ5IjoiN3Rncm1Gd016NGl0ZmVQNzBndkpLcjRSaGdjdENCMEJHZjZjWE9WZ2M0bjVXMWQ4dFgxZ1RQakdrczNVSm1zUiJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..x6JWRGSojUJUJYpp.5BRuzcaV.lLIhGH7Wz0n_iTBAubDFZA", // wrong key
"eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IkM2bWhsNE5BTHhEdHMwUlFlNXlyZWxQVDQyOGhDVzJNeUNYS3EwdUI0TDFMdnpXRHhVaVk3YTdZcEhJakJXcVoiLCJ5IjoiakM2dWc1NE9tbmdpNE9jUk1hdkNrczJpcFpXQjdkUmotR3QzOFhPSDRwZ2tpQ0lybWNlUnFxTnU3Z0c3Qk1yOSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..HNJJghL-SvERFz2v.N0z8YwFg.rYw29iX4i8XujdM4P4KKWg", // payload is not json
"eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6InB3R05vcXRnY093MkJ6RDVmSnpBWDJvMzUwSWNsY3A5cFdVTHZ5VDRqRWVCRWdCc3hhTVJXQ1ZyNlJMVUVXVlMiLCJ5IjoiZ2lIVEE5MlF3VU5lbmg1OFV1bWFfb09BX3hnYmFDVWFXSlRnb3Z4WjU4R212TnN4eUlQRElLSm9WV1h5X0R6OSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..jDbzdI7d67_cUjGD.01BPnMq_tQ.aG_uFA6FYqoPS64QAJ4VBQ", // json payload doesn't contain "key"
"eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IkJyYm9UQkl5Y0NDUEdJQlBUekU2RjBnbTRzRjRCamZPN1I0a2x0aWlCaThKZkxxcVdXNVdUSVBLN01yMXV5QVUiLCJ5IjoiNUpGVUI0WVJiYjM2RUZpN2Y0TUxMcFFyZXd2UV9Tc3dKNHRVbFd1a2c1ZU04X1ZyM2pkeml2QXI2WThRczVYbSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..QEq4Z2m6iwBx2ioS.IBo8TbKJTS4pug.61Z-agIIXgP8bX10O_yEMA", // json payload field "key" not a string
"eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6ImNZdlVFZm9LYkJjenZySE5zQjUxOGpycUxPMGJDOW5lZjR4NzFFMUQ5dk95MXRqd1piZzV3cFI0OE5nU1RQdHgiLCJ5IjoiaWRJekhCWERzSzR2NTZEeU9yczJOcDZsSG1zb29fMXV0VTlzX3JNdVVkbkxuVXIzUXdLZkhYMWdaVXREM1RKayJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..0VZqu5ei9U3blGtq.eDvhU6drw7mIwvXu6Q.f05QnhI7JWG3IYHvexwdFQ" // json payload field "key" invalid base64 data
})
public void testDecryptInvalid(String malformed) throws ParseException, InvalidKeySpecException {
var jwe = JWEObject.parse(malformed);
var keyPair = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(PRIV_KEY)));
Assertions.assertThrows(MasterkeyLoadingFailedException.class, () -> {
JWEHelper.decrypt(jwe, keyPair.getPrivate());
});
}
}

View File

@@ -0,0 +1,11 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="${LOGLEVEL:-debug}">
<appender-ref ref="STDOUT" />
</root>
</configuration>