diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
index 9ced33c15..a82e89ba7 100644
--- a/.github/ISSUE_TEMPLATE/bug.yml
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -26,6 +26,7 @@ body:
Examples:
- Operating System: Windows 10
- Cryptomator: 1.5.16
+ - OneDrive: 23.226
- LibreOffice: 7.1.4
value: |
- Operating System:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 0e12bbba9..3d2f42f8d 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -6,7 +6,7 @@ updates:
interval: "weekly"
day: "monday"
time: "06:00"
- timezone: "UTC"
+ timezone: "Etc/UTC"
groups:
java-test-dependencies:
patterns:
diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml
index 68127dbb6..9b08c82c8 100644
--- a/.github/workflows/appimage.yml
+++ b/.github/workflows/appimage.yml
@@ -11,7 +11,7 @@ on:
env:
JAVA_DIST: 'zulu'
- JAVA_VERSION: '21.0.1+12'
+ JAVA_VERSION: '21.0.2+13'
jobs:
get-version:
@@ -29,16 +29,16 @@ jobs:
include:
- os: ubuntu-latest
appimage-suffix: x86_64
- openjfx-url: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_linux-x64_bin-jmods.zip'
- openjfx-sha: 'f522ac2ae4bdd61f0219b7b8d2058ff72a22f36a44378453bcfdcd82f8f5e08c'
+ openjfx-url: 'https://download2.gluonhq.com/openjfx/21.0.1/openjfx-21.0.1_linux-x64_bin-jmods.zip'
+ openjfx-sha: '7baed11ca56d5fee85995fa6612d4299f1e8b7337287228f7f12fd50407c56f8'
- os: [self-hosted, Linux, ARM64]
appimage-suffix: aarch64
- openjfx-url: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_linux-aarch64_bin-jmods.zip'
- openjfx-sha: 'c0d80ebbe0aab404ef9ad8b46c05bf533a1e40b39b2720eebd9238d81f6326ca'
+ openjfx-url: 'https://download2.gluonhq.com/openjfx/21.0.1/openjfx-21.0.1_linux-aarch64_bin-jmods.zip'
+ openjfx-sha: '871e7b9d7af16aef2e55c1b7830d0e0b2503b13dd8641374ba7e55ecb81d2ef9'
steps:
- uses: actions/checkout@v4
- name: Setup Java
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4
with:
distribution: ${{ env.JAVA_DIST }}
java-version: ${{ env.JAVA_VERSION }}
@@ -74,6 +74,7 @@ jobs:
cp LICENSE.txt target
cp target/cryptomator-*.jar target/mods
- name: Run jlink
+ #Remark: no compression is applied for improved build compression later (here appimage)
run: >
${JAVA_HOME}/bin/jlink
--verbose
@@ -84,7 +85,7 @@ jobs:
--no-header-files
--no-man-pages
--strip-debug
- --compress=1
+ --compress zip-0
- name: Prepare additional launcher
run: envsubst '${SEMVER_STR} ${REVISION_NUM}' < dist/linux/launcher-gtk2.properties > launcher-gtk2.properties
env:
@@ -102,7 +103,7 @@ jobs:
--dest appdir
--name Cryptomator
--vendor "Skymatic GmbH"
- --copyright "(C) 2016 - 2023 Skymatic GmbH"
+ --copyright "(C) 2016 - 2024 Skymatic GmbH"
--app-version "${{ needs.get-version.outputs.semVerNum }}.${{ needs.get-version.outputs.revNum }}"
--java-options "--enable-preview"
--java-options "--enable-native-access=org.cryptomator.jfuse.linux.amd64,org.cryptomator.jfuse.linux.aarch64,org.purejava.appindicator"
@@ -163,7 +164,7 @@ jobs:
gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator-*.AppImage
gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator-*.AppImage.zsync
- name: Upload artifacts
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: appimage-${{ matrix.appimage-suffix }}
path: |
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 0d1eb5739..f29d22887 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -19,13 +19,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-java@v3
+ - uses: actions/setup-java@v4
with:
distribution: ${{ env.JAVA_DIST }}
java-version: ${{ env.JAVA_VERSION }}
cache: 'maven'
- name: Cache SonarCloud packages
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
diff --git a/.github/workflows/check-jdk-updates.yml b/.github/workflows/check-jdk-updates.yml
index 30954b9e4..b1cdca4bb 100644
--- a/.github/workflows/check-jdk-updates.yml
+++ b/.github/workflows/check-jdk-updates.yml
@@ -15,7 +15,7 @@ jobs:
outputs:
jdk-date: ${{ steps.get-data.outputs.jdk-date}}
steps:
- - uses: actions/setup-java@v3
+ - uses: actions/setup-java@v4
with:
java-version: ${{ env.JDK_VERSION }}
distribution: ${{ env.JDK_VENDOR }}
@@ -32,7 +32,7 @@ jobs:
jdk-date: ${{ steps.get-data.outputs.jdk-date}}
jdk-version: ${{ steps.get-data.outputs.jdk-version}}
steps:
- - uses: actions/setup-java@v3
+ - uses: actions/setup-java@v4
with:
java-version: 21
distribution: ${{ env.JDK_VENDOR }}
diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml
index f1cffb96b..39811359d 100644
--- a/.github/workflows/debian.yml
+++ b/.github/workflows/debian.yml
@@ -17,13 +17,13 @@ on:
env:
JAVA_DIST: 'zulu'
- JAVA_VERSION: '21.0.1+12'
+ JAVA_VERSION: '21.0.2+13'
COFFEELIBS_JDK: 21
- COFFEELIBS_JDK_VERSION: '21.0.1+12-0ppa1'
- OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_linux-x64_bin-jmods.zip'
- OPENJFX_JMODS_AMD64_HASH: 'f522ac2ae4bdd61f0219b7b8d2058ff72a22f36a44378453bcfdcd82f8f5e08c'
- OPENJFX_JMODS_AARCH64: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_linux-aarch64_bin-jmods.zip'
- OPENJFX_JMODS_AARCH64_HASH: 'c0d80ebbe0aab404ef9ad8b46c05bf533a1e40b39b2720eebd9238d81f6326ca'
+ COFFEELIBS_JDK_VERSION: '21.0.2+13-0ppa1'
+ OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/21.0.1/openjfx-21.0.1_linux-x64_bin-jmods.zip'
+ OPENJFX_JMODS_AMD64_HASH: '7baed11ca56d5fee85995fa6612d4299f1e8b7337287228f7f12fd50407c56f8'
+ OPENJFX_JMODS_AARCH64: 'https://download2.gluonhq.com/openjfx/21.0.1/openjfx-21.0.1_linux-aarch64_bin-jmods.zip'
+ OPENJFX_JMODS_AARCH64_HASH: '871e7b9d7af16aef2e55c1b7830d0e0b2503b13dd8641374ba7e55ecb81d2ef9'
jobs:
build:
@@ -46,7 +46,7 @@ jobs:
sudo apt-get update
sudo apt-get install debhelper devscripts dput coffeelibs-jdk-${{ env.COFFEELIBS_JDK }}=${{ env.COFFEELIBS_JDK_VERSION }} libgtk2.0-0
- name: Setup Java
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4
with:
distribution: ${{ env.JAVA_DIST }}
java-version: ${{ env.JAVA_VERSION }}
@@ -128,7 +128,7 @@ jobs:
run: |
gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator_*_amd64.deb
- name: Upload artifacts
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: linux-deb-package
path: |
diff --git a/.github/workflows/dependency-check.yml b/.github/workflows/dependency-check.yml
new file mode 100644
index 000000000..3d0ad7cfa
--- /dev/null
+++ b/.github/workflows/dependency-check.yml
@@ -0,0 +1,17 @@
+name: OWASP Maven Dependency Check
+on:
+ schedule:
+ - cron: '0 8 * * 0'
+ workflow_dispatch:
+
+
+jobs:
+ check-dependencies:
+ uses: skymatic/workflows/.github/workflows/run-dependency-check.yml@main
+ with:
+ runner-os: 'ubuntu-latest'
+ java-distribution: 'temurin'
+ java-version: 21
+ secrets:
+ nvd-api-key: ${{ secrets.NVD_API_KEY }}
+ slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
diff --git a/.github/workflows/dl-stats.yml b/.github/workflows/dl-stats.yml
index dc87a2bbd..b16899520 100644
--- a/.github/workflows/dl-stats.yml
+++ b/.github/workflows/dl-stats.yml
@@ -10,7 +10,7 @@ jobs:
steps:
- name: Get download count of latest releases
id: get-stats
- uses: actions/github-script@v6
+ uses: actions/github-script@v7
with:
script: |
const query = `query($owner:String!, $name:String!) {
diff --git a/.github/workflows/error-db.yml b/.github/workflows/error-db.yml
index e885af4a2..301713681 100644
--- a/.github/workflows/error-db.yml
+++ b/.github/workflows/error-db.yml
@@ -14,7 +14,7 @@ jobs:
- name: Query Discussion Data
if: github.event_name == 'discussion_comment' || github.event_name == 'discussion' && github.event.action != 'deleted'
id: query-data
- uses: actions/github-script@v6
+ uses: actions/github-script@v7
with:
script: |
const query = `query ($owner: String!, $name: String!, $discussionNumber: Int!) {
diff --git a/.github/workflows/get-version.yml b/.github/workflows/get-version.yml
index 1bed1cff8..ae2b60b4b 100644
--- a/.github/workflows/get-version.yml
+++ b/.github/workflows/get-version.yml
@@ -39,7 +39,7 @@ jobs:
with:
fetch-depth: 0
- name: Setup Java
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4
with:
distribution: ${{ env.JAVA_DIST }}
java-version: ${{ env.JAVA_VERSION }}
diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml
index 666e9f19c..6d2bc19ce 100644
--- a/.github/workflows/mac-dmg.yml
+++ b/.github/workflows/mac-dmg.yml
@@ -16,7 +16,7 @@ on:
env:
JAVA_DIST: 'zulu'
- JAVA_VERSION: '21.0.1+12'
+ JAVA_VERSION: '21.0.2+13'
jobs:
get-version:
@@ -37,19 +37,19 @@ jobs:
output-suffix: x64
xcode-path: '/Applications/Xcode_13.2.1.app'
fuse-lib: macFUSE
- openjfx-url: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_osx-x64_bin-jmods.zip'
- openjfx-sha: '55b8ff7453d59c89ae129f6c9c5ad7b09a5d359568811b376ac1766c14d6a17c'
+ openjfx-url: 'https://download2.gluonhq.com/openjfx/21.0.1/openjfx-21.0.1_osx-x64_bin-jmods.zip'
+ openjfx-sha: 'bd6abab20da73d5a968dcf2fd915d81b5fb919340e3bb84979ee9a888a829939'
- os: [self-hosted, macOS, ARM64]
architecture: aarch64
output-suffix: arm64
xcode-path: '/Applications/Xcode_13.2.1.app'
fuse-lib: FUSE-T
- openjfx-url: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_osx-aarch64_bin-jmods.zip'
- openjfx-sha: 'c60f5f19aa847e0e620e0b011e5de68f2c6755641c2141cec27a0b89f612beaf'
+ openjfx-url: 'https://download2.gluonhq.com/openjfx/21.0.1/openjfx-21.0.1_osx-aarch64_bin-jmods.zip'
+ openjfx-sha: '7afaa1c57a6cc3c384d636e597b9a5364693e2db4aaec0a6e63d2fa964400b58'
steps:
- uses: actions/checkout@v4
- name: Setup Java
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4
with:
distribution: ${{ env.JAVA_DIST }}
java-version: ${{ env.JAVA_VERSION }}
@@ -85,6 +85,7 @@ jobs:
cp LICENSE.txt target
cp target/cryptomator-*.jar target/mods
- name: Run jlink
+ #Remark: no compression is applied for improved build compression later (here dmg)
run: >
${JAVA_HOME}/bin/jlink
--verbose
@@ -95,7 +96,7 @@ jobs:
--no-header-files
--no-man-pages
--strip-debug
- --compress=1
+ --compress zip-0
- name: Run jpackage
run: >
${JAVA_HOME}/bin/jpackage
@@ -108,7 +109,7 @@ jobs:
--dest appdir
--name Cryptomator
--vendor "Skymatic GmbH"
- --copyright "(C) 2016 - 2023 Skymatic GmbH"
+ --copyright "(C) 2016 - 2024 Skymatic GmbH"
--app-version "${{ needs.get-version.outputs.semVerNum }}"
--java-options "--enable-preview"
--java-options "--enable-native-access=org.cryptomator.jfuse.mac"
@@ -249,7 +250,7 @@ jobs:
run: security delete-keychain $RUNNER_TEMP/codesign.keychain-db
continue-on-error: true
- name: Upload artifacts
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: dmg-${{ matrix.output-suffix }}
path: Cryptomator-*.dmg
diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml
index 1e5a848dd..43c634e20 100644
--- a/.github/workflows/no-response.yml
+++ b/.github/workflows/no-response.yml
@@ -12,7 +12,7 @@ jobs:
issues: write
pull-requests: write
steps:
- - uses: actions/stale@v8
+ - uses: actions/stale@v9
with:
days-before-stale: 14
days-before-close: 0
diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml
index a8f4c5617..2730f5c24 100644
--- a/.github/workflows/pullrequest.yml
+++ b/.github/workflows/pullrequest.yml
@@ -18,7 +18,7 @@ jobs:
if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')"
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-java@v3
+ - uses: actions/setup-java@v4
with:
distribution: ${{ env.JAVA_DIST }}
java-version: ${{ env.JAVA_VERSION }}
diff --git a/.github/workflows/release-check.yml b/.github/workflows/release-check.yml
index 1bbfb5d1a..90c778d50 100644
--- a/.github/workflows/release-check.yml
+++ b/.github/workflows/release-check.yml
@@ -10,12 +10,22 @@ defaults:
run:
shell: bash
+env:
+ JAVA_DIST: 'zulu'
+ JAVA_VERSION: 21
+
jobs:
- release-check-precondition:
+ check-preconditions:
name: Validate commits pushed to release/hotfix branch to fulfill release requirements
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
+ - name: Setup Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: ${{ env.JAVA_DIST }}
+ java-version: ${{ env.JAVA_VERSION }}
+ cache: 'maven'
- id: validate-pom-version
name: Validate POM version
run: |
@@ -37,4 +47,19 @@ jobs:
if ! grep -q "" dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml; then
echo "Release not set in dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml"
exit 1
- fi
\ No newline at end of file
+ fi
+ - name: Cache NVD DB
+ uses: actions/cache@v4
+ with:
+ path: ~/.m2/repository/org/owasp/dependency-check-data/
+ key: dependency-check-${{ github.run_id }}
+ restore-keys: |
+ dependency-check
+ env:
+ SEGMENT_DOWNLOAD_TIMEOUT_MINS: 5
+ - name: Run org.owasp:dependency-check plugin
+ id: dependency-check
+ continue-on-error: true
+ run: mvn -B verify -Pdependency-check -DskipTests
+ env:
+ NVD_API_KEY: ${{ secrets.NVD_API_KEY }}
\ No newline at end of file
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index f3a57687d..9a14cbe23 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -12,7 +12,7 @@ jobs:
issues: write
pull-requests: write
steps:
- - uses: actions/stale@v8
+ - uses: actions/stale@v9
with:
days-before-stale: 365
days-before-close: 90
diff --git a/.github/workflows/win-exe.yml b/.github/workflows/win-exe.yml
index 1002718d0..3eb11bf3b 100644
--- a/.github/workflows/win-exe.yml
+++ b/.github/workflows/win-exe.yml
@@ -15,11 +15,11 @@ on:
env:
JAVA_DIST: 'zulu'
- JAVA_VERSION: '21.0.1+12'
- OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_windows-x64_bin-jmods.zip'
- OPENJFX_JMODS_AMD64_HASH: '18625bbc13c57dbf802486564247a8d8cab72ec558c240a401bf6440384ebd77'
+ JAVA_VERSION: '21.0.2+13'
+ OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/21.0.1/openjfx-21.0.1_windows-x64_bin-jmods.zip'
+ OPENJFX_JMODS_AMD64_HASH: 'daf8acae631c016c24cfe23f88469400274d3441dd890615a42dfb501f3eb94a'
WINFSP_MSI: 'https://github.com/winfsp/winfsp/releases/download/v2.0/winfsp-2.0.23075.msi'
- WINFSP_UNINSTALLER: 'https://github.com/cryptomator/winfsp-uninstaller/releases/download/1.0.0-beta9/winfsp-uninstaller.exe'
+ WINFSP_UNINSTALLER: 'https://github.com/cryptomator/winfsp-uninstaller/releases/download/1.0.0/winfsp-uninstaller.exe'
defaults:
run:
@@ -41,7 +41,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup Java
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4
with:
distribution: ${{ env.JAVA_DIST }}
java-version: ${{ env.JAVA_VERSION }}
@@ -79,6 +79,7 @@ jobs:
cp LICENSE.txt target
cp target/cryptomator-*.jar target/mods
- name: Run jlink
+ #Remark: no compression is applied for improved build compression later (here msi)
run: >
${JAVA_HOME}/bin/jlink
--verbose
@@ -89,7 +90,7 @@ jobs:
--no-header-files
--no-man-pages
--strip-debug
- --compress=1
+ --compress zip-0
- name: Change win-console flag if debug is active
if: ${{ inputs.isDebug }}
run: echo "WIN_CONSOLE_FLAG=--win-console" >> $GITHUB_ENV
@@ -105,7 +106,7 @@ jobs:
--dest appdir
--name Cryptomator
--vendor "Skymatic GmbH"
- --copyright "(C) 2016 - 2023 Skymatic GmbH"
+ --copyright "(C) 2016 - 2024 Skymatic GmbH"
--app-version "${{ needs.get-version.outputs.semVerNum }}.${{ needs.get-version.outputs.revNum }}"
--java-options "--enable-preview"
--java-options "--enable-native-access=org.cryptomator.jfuse.win"
@@ -213,7 +214,7 @@ jobs:
--dest installer
--name Cryptomator
--vendor "Skymatic GmbH"
- --copyright "(C) 2016 - 2023 Skymatic GmbH"
+ --copyright "(C) 2016 - 2024 Skymatic GmbH"
--app-version "${{ needs.get-version.outputs.semVerNum }}.${{ needs.get-version.outputs.revNum}}"
--win-menu
--win-dir-chooser
@@ -245,7 +246,7 @@ jobs:
GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }}
- name: Upload artifacts
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: msi
path: |
@@ -269,13 +270,13 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Download .msi
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@v4
with:
name: msi
path: dist/win/bundle/resources
- name: Strip version info from msi file name
run: mv dist/win/bundle/resources/Cryptomator*.msi dist/win/bundle/resources/Cryptomator.msi
- - uses: actions/setup-java@v3
+ - uses: actions/setup-java@v4
with:
distribution: ${{ env.JAVA_DIST }}
java-version: ${{ env.JAVA_VERSION }}
@@ -308,7 +309,7 @@ jobs:
-out dist/win/bundle/
-dBundleVersion="${{ needs.get-version.outputs.semVerNum }}.${{ needs.get-version.outputs.revNum }}"
-dBundleVendor="Skymatic GmbH"
- -dBundleCopyright="(C) 2016 - 2023 Skymatic GmbH"
+ -dBundleCopyright="(C) 2016 - 2024 Skymatic GmbH"
-dAboutUrl="https://cryptomator.org"
-dHelpUrl="https://cryptomator.org/contact"
-dUpdateUrl="https://cryptomator.org/downloads/"
@@ -356,7 +357,7 @@ jobs:
GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }}
- name: Upload artifacts
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: exe
path: |
@@ -380,12 +381,12 @@ jobs:
needs: [build-msi, build-exe]
steps:
- name: Download .msi
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@v4
with:
name: msi
path: msi
- name: Download .exe
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@v4
with:
name: exe
path: exe
diff --git a/README.md b/README.md
index ec021ab5d..560f2e4f8 100644
--- a/README.md
+++ b/README.md
@@ -86,7 +86,7 @@ For more information on the security details visit [cryptomator.org](https://doc
### Dependencies
-* JDK 19 (e.g. temurin)
+* JDK 21 (e.g. temurin, zulu)
* Maven 3
### Run Maven
diff --git a/dist/linux/appimage/build.sh b/dist/linux/appimage/build.sh
index 0a4b7f65d..6af5b5663 100755
--- a/dist/linux/appimage/build.sh
+++ b/dist/linux/appimage/build.sh
@@ -8,11 +8,14 @@ REVISION_NO=`git rev-list --count HEAD`
if [ -z "${JAVA_HOME}" ]; then echo "JAVA_HOME not set. Run using JAVA_HOME=/path/to/jdk ./build.sh"; exit 1; fi
command -v mvn >/dev/null 2>&1 || { echo >&2 "mvn not found."; exit 1; }
command -v curl >/dev/null 2>&1 || { echo >&2 "curl not found."; exit 1; }
+command -v unzip >/dev/null 2>&1 || { echo >&2 "unzip not found."; exit 1; }
VERSION=$(mvn -f ../../../pom.xml help:evaluate -Dexpression=project.version -q -DforceStdout)
SEMVER_STR=${VERSION}
MACHINE_TYPE=$(uname -m)
+if [[ ! "${MACHINE_TYPE}" =~ x86_64|aarch64 ]]; then echo "Platform ${MACHINE_TYPE} not supported"; exit 1; fi
+
mvn -f ../../../pom.xml versions:set -DnewVersion=${SEMVER_STR}
# compile
@@ -20,17 +23,45 @@ mvn -B -f ../../../pom.xml clean package -Plinux -DskipTests
cp ../../../LICENSE.txt ../../../target
cp ../../../target/cryptomator-*.jar ../../../target/mods
+
+# download javaFX jmods
+OPENJFX_URL='https://download2.gluonhq.com/openjfx/21.0.1/openjfx-21.0.1_linux-x64_bin-jmods.zip'
+OPENJFX_SHA='7baed11ca56d5fee85995fa6612d4299f1e8b7337287228f7f12fd50407c56f8'
+OPENJFX_URL_aarch64='https://download2.gluonhq.com/openjfx/21.0.1/openjfx-21.0.1_linux-aarch64_bin-jmods.zip'
+OPENJFX_SHA_aarch64='871e7b9d7af16aef2e55c1b7830d0e0b2503b13dd8641374ba7e55ecb81d2ef9'
+
+if [[ "${MACHINE_TYPE}" = "aarch64" ]]; then
+ OPENJFX_URL="${OPENJFX_URL_aarch64}";
+ OPENJFX_SHA="${OPENJFX_SHA_aarch64}";
+fi
+
+curl -L ${OPENJFX_URL} -o openjfx-jmods.zip
+echo "${OPENJFX_SHA} openjfx-jmods.zip" | shasum -a256 --check
+mkdir -p openjfx-jmods
+unzip -j openjfx-jmods.zip \*/javafx.base.jmod \*/javafx.controls.jmod \*/javafx.fxml.jmod \*/javafx.graphics.jmod -d openjfx-jmods
+JMOD_VERSION=$(jmod describe openjfx-jmods/javafx.base.jmod | head -1)
+JMOD_VERSION=${JMOD_VERSION#*@}
+JMOD_VERSION=${JMOD_VERSION%%.*}
+POM_JFX_VERSION=$(mvn help:evaluate "-Dexpression=javafx.version" -q -DforceStdout)
+POM_JFX_VERSION=${POM_JFX_VERSION#*@}
+POM_JFX_VERSION=${POM_JFX_VERSION%%.*}
+if [ $POM_JFX_VERSION -ne $JMOD_VERSION_AMD64 ]; then
+ >&2 echo "Major JavaFX version in pom.xml (${POM_JFX_VERSION}) != amd64 jmod version (${JMOD_VERSION})"
+ exit 1
+fi
+
+
# add runtime
${JAVA_HOME}/bin/jlink \
--verbose \
--output runtime \
- --module-path "${JAVA_HOME}/jmods" \
+ --module-path "${JAVA_HOME}/jmods:openjfx-jmods" \
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.security.auth,jdk.accessibility,jdk.management.jfr,jdk.net \
--strip-native-commands \
--no-header-files \
--no-man-pages \
--strip-debug \
- --compress=1
+ --compress zip-0
# create app dir
envsubst '${SEMVER_STR} ${REVISION_NUM}' < ../launcher-gtk2.properties > launcher-gtk2.properties
@@ -46,7 +77,7 @@ ${JAVA_HOME}/bin/jpackage \
--vendor "Skymatic GmbH" \
--java-options "--enable-preview" \
--java-options "--enable-native-access=org.cryptomator.jfuse.linux.amd64,org.cryptomator.jfuse.linux.aarch64,org.purejava.appindicator" \
- --copyright "(C) 2016 - 2023 Skymatic GmbH" \
+ --copyright "(C) 2016 - 2024 Skymatic GmbH" \
--java-options "-Xss5m" \
--java-options "-Xmx256m" \
--app-version "${VERSION}.${REVISION_NO}" \
@@ -97,5 +128,5 @@ chmod +x /tmp/appimagetool.AppImage
echo ""
echo "Done. AppImage successfully created: cryptomator-${SEMVER_STR}-${MACHINE_TYPE}.AppImage"
echo ""
-echo >&2 "To clean up, run: rm -rf Cryptomator.AppDir appdir jni runtime squashfs-root; rm launcher-gtk2.properties /tmp/appimagetool.AppImage"
-echo ""
\ No newline at end of file
+echo >&2 "To clean up, run: rm -rf Cryptomator.AppDir appdir runtime squashfs-root openjfx-jmods; rm launcher-gtk2.properties /tmp/appimagetool.AppImage openjfx-jmods.zip"
+echo ""
diff --git a/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml b/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml
index b6f05d102..d92ced46f 100644
--- a/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml
+++ b/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml
@@ -66,6 +66,7 @@
+
diff --git a/dist/linux/debian/copyright b/dist/linux/debian/copyright
index 745218b67..322e549d0 100644
--- a/dist/linux/debian/copyright
+++ b/dist/linux/debian/copyright
@@ -4,11 +4,11 @@ Upstream-Contact: Cryptomator
Source: https://cryptomator.org
Files: *
-Copyright: 2016-2023 Skymatic GmbH
+Copyright: 2016-2024 Skymatic GmbH
License: GPL-3+
Files: debian/org.cryptomator.Cryptomator.appdata.xml
-Copyright: 2016-2023 Skymatic GmbH
+Copyright: 2016-2024 Skymatic GmbH
License: FSFAP
License: GPL-3+
diff --git a/dist/linux/debian/rules b/dist/linux/debian/rules
index d0a12e380..11650598d 100755
--- a/dist/linux/debian/rules
+++ b/dist/linux/debian/rules
@@ -24,6 +24,7 @@ override_dh_auto_clean:
override_dh_auto_build:
mkdir resources
ln -s ../common/org.cryptomator.Cryptomator512.png resources/cryptomator.png
+# Remark: no compression is applied for improved build compression later (here deb)
$(JAVA_HOME)/bin/jlink \
--output runtime \
--module-path "${JMODS_PATH}" \
@@ -32,7 +33,7 @@ override_dh_auto_build:
--no-header-files \
--no-man-pages \
--strip-debug \
- --compress=2
+ --compress zip-0
$(JAVA_HOME)/bin/jpackage \
--type app-image \
--runtime-image runtime \
@@ -44,7 +45,7 @@ override_dh_auto_build:
--vendor "Skymatic GmbH" \
--java-options "--enable-preview" \
--java-options "--enable-native-access=org.cryptomator.jfuse.linux.amd64,org.cryptomator.jfuse.linux.aarch64,org.purejava.appindicator" \
- --copyright "(C) 2016 - 2023 Skymatic GmbH" \
+ --copyright "(C) 2016 - 2024 Skymatic GmbH" \
--java-options "-Xss5m" \
--java-options "-Xmx256m" \
--java-options "-Dfile.encoding=\"utf-8\"" \
diff --git a/dist/mac/dmg/build.sh b/dist/mac/dmg/build.sh
index b2c8d55e3..df699aad1 100755
--- a/dist/mac/dmg/build.sh
+++ b/dist/mac/dmg/build.sh
@@ -21,7 +21,7 @@ rm -rf runtime dmg *.app *.dmg
# set variables
APP_NAME="Cryptomator"
VENDOR="Skymatic GmbH"
-COPYRIGHT_YEARS="2016 - 2023"
+COPYRIGHT_YEARS="2016 - 2024"
PACKAGE_IDENTIFIER="org.cryptomator"
MAIN_JAR_GLOB="cryptomator-*.jar"
MODULE_AND_MAIN_CLASS="org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator"
@@ -35,7 +35,7 @@ if [ "$(machine)" = "arm64e" ]; then
else
ARCH="x64"
fi
-OPENJFX_JMODS="https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_osx-${ARCH}_bin-jmods.zip"
+OPENJFX_JMODS="https://download2.gluonhq.com/openjfx/21.0.1/openjfx-21.0.1_osx-${ARCH}_bin-jmods.zip"
# check preconditions
if [ -z "${JAVA_HOME}" ]; then echo "JAVA_HOME not set. Run using JAVA_HOME=/path/to/jdk ./build.sh"; exit 1; fi
@@ -76,7 +76,7 @@ ${JAVA_HOME}/bin/jlink \
--no-header-files \
--no-man-pages \
--strip-debug \
- --compress=1
+ --compress zip-0
# create app dir
${JAVA_HOME}/bin/jpackage \
diff --git a/dist/mac/dmg/resources/licenseTemplate.ftl b/dist/mac/dmg/resources/licenseTemplate.ftl
index 98178151c..45a523f51 100644
--- a/dist/mac/dmg/resources/licenseTemplate.ftl
+++ b/dist/mac/dmg/resources/licenseTemplate.ftl
@@ -17,7 +17,7 @@
\f1\b0 \
\
-\f0\b \'a9 2016 \'96 2023 Skymatic GmbH
+\f0\b \'a9 2016 \'96 2024 Skymatic GmbH
\f1\b0 \
\
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\
diff --git a/dist/win/build.ps1 b/dist/win/build.ps1
index 2606a3778..7b1f7cc47 100644
--- a/dist/win/build.ps1
+++ b/dist/win/build.ps1
@@ -51,9 +51,9 @@ if ($clean -and (Test-Path -Path $runtimeImagePath)) {
}
## download jfx jmods
-$jmodsVersion='20.0.2'
+$jmodsVersion='21.0.1'
$jmodsUrl = "https://download2.gluonhq.com/openjfx/${jmodsVersion}/openjfx-${jmodsVersion}_windows-x64_bin-jmods.zip"
-$jfxJmodsChecksum = '18625bbc13c57dbf802486564247a8d8cab72ec558c240a401bf6440384ebd77'
+$jfxJmodsChecksum = 'daf8acae631c016c24cfe23f88469400274d3441dd890615a42dfb501f3eb94a'
$jfxJmodsZip = '.\resources\jfxJmods.zip'
if( !(Test-Path -Path $jfxJmodsZip) ) {
Write-Output "Downloading ${jmodsUrl}..."
@@ -69,7 +69,7 @@ Expand-Archive -Path $jfxJmodsZip -Force -DestinationPath ".\resources\"
Remove-Item -Recurse -Force -Path ".\resources\javafx-jmods"
Move-Item -Force -Path ".\resources\javafx-jmods-*" -Destination ".\resources\javafx-jmods" -ErrorAction Stop
-
+## create custom runtime
& "$Env:JAVA_HOME\bin\jlink" `
--verbose `
--output runtime `
@@ -79,7 +79,7 @@ Move-Item -Force -Path ".\resources\javafx-jmods-*" -Destination ".\resources\ja
--no-header-files `
--no-man-pages `
--strip-debug `
- --compress=1
+ --compress "zip-0" #do not compress to have improved msi compression
$appPath = ".\$AppName"
if ($clean -and (Test-Path -Path $appPath)) {
@@ -181,7 +181,7 @@ Write-Output "Downloading ${winfspMsiUrl}..."
Invoke-WebRequest $winfspMsiUrl -OutFile ".\bundle\resources\winfsp.msi" # redirects are followed by default
# download legacy-winfsp uninstaller
-$winfspUninstaller= 'https://github.com/cryptomator/winfsp-uninstaller/releases/download/1.0.0-beta9/winfsp-uninstaller.exe'
+$winfspUninstaller= 'https://github.com/cryptomator/winfsp-uninstaller/releases/download/1.0.0/winfsp-uninstaller.exe'
Write-Output "Downloading ${winfspUninstaller}..."
Invoke-WebRequest $winfspUninstaller -OutFile ".\bundle\resources\winfsp-uninstaller.exe" # redirects are followed by default
diff --git a/dist/win/bundle/resources/licenseTemplate.ftl b/dist/win/bundle/resources/licenseTemplate.ftl
index 40d55e292..6940f9fe6 100644
--- a/dist/win/bundle/resources/licenseTemplate.ftl
+++ b/dist/win/bundle/resources/licenseTemplate.ftl
@@ -10,7 +10,7 @@
\vieww12000\viewh15840\viewkind0
\pard\tx283\tx567\tx850\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\b\fs16\lang7 Cryptomator is distributed under the GPLv3 License, found below. Please see the bottom of this document for any other license applicable to code used within Cryptomator.\b0\par
\par
-\b\'a9 2016 \'96 2023 Skymatic GmbH \b0\par
+\b\'a9 2016 \'96 2024 Skymatic GmbH \b0\par
\par
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\par
\par
diff --git a/dist/win/resources/licenseTemplate.ftl b/dist/win/resources/licenseTemplate.ftl
index 88a80f8b6..011fda3cb 100644
--- a/dist/win/resources/licenseTemplate.ftl
+++ b/dist/win/resources/licenseTemplate.ftl
@@ -10,7 +10,7 @@
\vieww12000\viewh15840\viewkind0
\pard\tx283\tx567\tx850\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\b\fs16\lang7 Cryptomator is distributed under the GPLv3 License, found below. Please see the bottom of this document for any other license applicable to code used within Cryptomator.\b0\par
\par
-\b\'a9 2016 \'96 2023 Skymatic GmbH \b0\par
+\b\'a9 2016 \'96 2024 Skymatic GmbH \b0\par
\par
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\par
\par
diff --git a/pom.xml b/pom.xml
index 714735f87..8560276dd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
4.0.0
org.cryptomator
cryptomator
- 1.11.1
+ 1.12.0
Cryptomator Desktop App
@@ -35,42 +35,42 @@
2.6.8
1.3.0
- 1.2.4
- 1.2.2
- 1.4.0-beta2
- 4.0.0-beta5
+ 1.2.5
+ 1.2.3
+ 1.4.2
+ 4.0.0
2.0.0
- 2.0.5
+ 2.0.6
3.14.0
- 2.48.1
+ 2.50
2.2
- 32.1.3-jre
- 2.16.0
- 20.0.2
+ 33.0.0-jre
+ 2.16.1
+ 21.0.1
4.4.0
- 9.37.1
- 1.4.12
- 2.0.9
+ 9.37.3
+ 1.4.14
+ 2.0.11
0.8.0
1.8.2
- 5.10.1
- 5.7.0
+ 5.10.2
+ 5.10.0
2.2
24.1.0
- 9.0.1
+ 9.0.9
0.8.11
- 2.3.0
+ 2.4.0
1.2.1
- 3.11.0
+ 3.12.1
3.3.1
3.6.1
- 3.2.2
+ 3.2.5
3.3.0
@@ -460,17 +460,19 @@
org.owasp
dependency-check-maven
- 24
+ 24
0
true
true
suppression.xml
+ ${env.NVD_API_KEY}
check
+ validate
diff --git a/src/main/java/org/cryptomator/common/CommonsModule.java b/src/main/java/org/cryptomator/common/CommonsModule.java
index 4ac07495e..a1e3c0950 100644
--- a/src/main/java/org/cryptomator/common/CommonsModule.java
+++ b/src/main/java/org/cryptomator/common/CommonsModule.java
@@ -5,10 +5,8 @@
*******************************************************************************/
package org.cryptomator.common;
-import com.tobiasdiez.easybind.EasyBind;
import dagger.Module;
import dagger.Provides;
-import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.keychain.KeychainModule;
import org.cryptomator.common.mount.MountModule;
import org.cryptomator.common.settings.Settings;
@@ -22,8 +20,6 @@ import org.slf4j.LoggerFactory;
import javax.inject.Named;
import javax.inject.Singleton;
-import javafx.beans.value.ObservableValue;
-import java.net.InetSocketAddress;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Comparator;
@@ -136,13 +132,4 @@ public abstract class CommonsModule {
LOG.error("Uncaught exception in " + thread.getName(), throwable);
}
- @Provides
- @Singleton
- static ObservableValue provideServerSocketAddressBinding(Settings settings) {
- return settings.port.map(port -> {
- String host = SystemUtils.IS_OS_WINDOWS ? "127.0.0.1" : "localhost";
- return InetSocketAddress.createUnresolved(host, settings.port.intValue());
- });
- }
-
}
diff --git a/src/main/java/org/cryptomator/common/mount/ActualMountService.java b/src/main/java/org/cryptomator/common/mount/ActualMountService.java
deleted file mode 100644
index a96cc8e37..000000000
--- a/src/main/java/org/cryptomator/common/mount/ActualMountService.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package org.cryptomator.common.mount;
-
-import org.cryptomator.integrations.mount.MountService;
-
-public record ActualMountService(MountService service, boolean isDesired) {
-}
diff --git a/src/main/java/org/cryptomator/common/mount/ConflictingMountServiceException.java b/src/main/java/org/cryptomator/common/mount/ConflictingMountServiceException.java
new file mode 100644
index 000000000..b4d87169a
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/mount/ConflictingMountServiceException.java
@@ -0,0 +1,14 @@
+package org.cryptomator.common.mount;
+
+import org.cryptomator.integrations.mount.MountFailedException;
+
+/**
+ * Thrown by {@link Mounter} to indicate that the selected mount service can not be used
+ * due to incompatibilities with a different mount service that is already in use.
+ */
+public class ConflictingMountServiceException extends MountFailedException {
+
+ public ConflictingMountServiceException(String msg) {
+ super(msg);
+ }
+}
diff --git a/src/main/java/org/cryptomator/common/mount/MountModule.java b/src/main/java/org/cryptomator/common/mount/MountModule.java
index cbcb23e82..72872855c 100644
--- a/src/main/java/org/cryptomator/common/mount/MountModule.java
+++ b/src/main/java/org/cryptomator/common/mount/MountModule.java
@@ -4,21 +4,18 @@ import dagger.Module;
import dagger.Provides;
import org.cryptomator.common.ObservableUtil;
import org.cryptomator.common.settings.Settings;
-import org.cryptomator.integrations.mount.Mount;
import org.cryptomator.integrations.mount.MountService;
import javax.inject.Named;
import javax.inject.Singleton;
import javafx.beans.value.ObservableValue;
import java.util.List;
-import java.util.concurrent.atomic.AtomicReference;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
@Module
public class MountModule {
- private static final AtomicReference formerSelectedMountService = new AtomicReference<>(null);
- private static final List problematicFuseMountServices = List.of("org.cryptomator.frontend.fuse.mount.MacFuseMountProvider", "org.cryptomator.frontend.fuse.mount.FuseTMountProvider");
-
@Provides
@Singleton
static List provideSupportedMountServices() {
@@ -27,46 +24,18 @@ public class MountModule {
@Provides
@Singleton
- @Named("FUPFMS")
- static AtomicReference provideFirstUsedProblematicFuseMountService() {
- return new AtomicReference<>(null);
+ static ObservableValue provideDefaultMountService(List mountProviders, Settings settings) {
+ var fallbackProvider = mountProviders.stream().findFirst().get(); //there should always be a mount provider, at least webDAV
+ return ObservableUtil.mapWithDefault(settings.mountService, //
+ serviceName -> mountProviders.stream().filter(s -> s.getClass().getName().equals(serviceName)).findFirst().orElse(fallbackProvider), //
+ fallbackProvider);
}
@Provides
@Singleton
- static ObservableValue provideMountService(Settings settings, List serviceImpls, @Named("FUPFMS") AtomicReference fupfms) {
- var fallbackProvider = serviceImpls.stream().findFirst().orElse(null);
-
- var observableMountService = ObservableUtil.mapWithDefault(settings.mountService, //
- desiredServiceImpl -> { //
- var serviceFromSettings = serviceImpls.stream().filter(serviceImpl -> serviceImpl.getClass().getName().equals(desiredServiceImpl)).findAny(); //
- var targetedService = serviceFromSettings.orElse(fallbackProvider);
- return applyWorkaroundForProblematicFuse(targetedService, serviceFromSettings.isPresent(), fupfms);
- }, //
- () -> { //
- return applyWorkaroundForProblematicFuse(fallbackProvider, true, fupfms);
- });
- return observableMountService;
+ @Named("usedMountServices")
+ static Set provideSetOfUsedMountServices() {
+ return ConcurrentHashMap.newKeySet();
}
- //see https://github.com/cryptomator/cryptomator/issues/2786
- private synchronized static ActualMountService applyWorkaroundForProblematicFuse(MountService targetedService, boolean isDesired, AtomicReference firstUsedProblematicFuseMountService) {
- //set the first used problematic fuse service if applicable
- var targetIsProblematicFuse = isProblematicFuseService(targetedService);
- if (targetIsProblematicFuse && firstUsedProblematicFuseMountService.get() == null) {
- firstUsedProblematicFuseMountService.set(targetedService);
- }
-
- //do not use the targeted mount service and fallback to former one, if the service is problematic _and_ not the first problematic one used.
- if (targetIsProblematicFuse && !firstUsedProblematicFuseMountService.get().equals(targetedService)) {
- return new ActualMountService(formerSelectedMountService.get(), false);
- } else {
- formerSelectedMountService.set(targetedService);
- return new ActualMountService(targetedService, isDesired);
- }
- }
-
- public static boolean isProblematicFuseService(MountService service) {
- return problematicFuseMountServices.contains(service.getClass().getName());
- }
-}
+}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/common/mount/Mounter.java b/src/main/java/org/cryptomator/common/mount/Mounter.java
index 101524ea3..b63a12b1f 100644
--- a/src/main/java/org/cryptomator/common/mount/Mounter.java
+++ b/src/main/java/org/cryptomator/common/mount/Mounter.java
@@ -9,11 +9,15 @@ import org.cryptomator.integrations.mount.MountFailedException;
import org.cryptomator.integrations.mount.MountService;
import javax.inject.Inject;
+import javax.inject.Named;
import javax.inject.Singleton;
import javafx.beans.value.ObservableValue;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
import static org.cryptomator.integrations.mount.MountCapability.MOUNT_AS_DRIVE_LETTER;
import static org.cryptomator.integrations.mount.MountCapability.MOUNT_TO_EXISTING_DIR;
@@ -24,24 +28,39 @@ import static org.cryptomator.integrations.mount.MountCapability.UNMOUNT_FORCED;
@Singleton
public class Mounter {
- private final Settings settings;
+ // mount providers (key) can not be used if any of the conflicting mount providers (values) are already in use
+ private static final Map> CONFLICTING_MOUNT_SERVICES = Map.of(
+ "org.cryptomator.frontend.fuse.mount.MacFuseMountProvider", Set.of("org.cryptomator.frontend.fuse.mount.FuseTMountProvider"),
+ "org.cryptomator.frontend.fuse.mount.FuseTMountProvider", Set.of("org.cryptomator.frontend.fuse.mount.MacFuseMountProvider")
+ );
+
private final Environment env;
+ private final Settings settings;
private final WindowsDriveLetters driveLetters;
- private final ObservableValue mountServiceObservable;
+ private final List mountProviders;
+ private final Set usedMountServices;
+ private final ObservableValue defaultMountService;
@Inject
- public Mounter(Settings settings, Environment env, WindowsDriveLetters driveLetters, ObservableValue mountServiceObservable) {
- this.settings = settings;
+ public Mounter(Environment env, //
+ Settings settings, //
+ WindowsDriveLetters driveLetters, //
+ List mountProviders, //
+ @Named("usedMountServices") Set usedMountServices, //
+ ObservableValue defaultMountService) {
this.env = env;
+ this.settings = settings;
this.driveLetters = driveLetters;
- this.mountServiceObservable = mountServiceObservable;
+ this.mountProviders = mountProviders;
+ this.usedMountServices = usedMountServices;
+ this.defaultMountService = defaultMountService;
}
private class SettledMounter {
- private MountService service;
- private MountBuilder builder;
- private VaultSettings vaultSettings;
+ private final MountService service;
+ private final MountBuilder builder;
+ private final VaultSettings vaultSettings;
public SettledMounter(MountService service, MountBuilder builder, VaultSettings vaultSettings) {
this.service = service;
@@ -53,8 +72,13 @@ public class Mounter {
for (var capability : service.capabilities()) {
switch (capability) {
case FILE_SYSTEM_NAME -> builder.setFileSystemName("cryptoFs");
- case LOOPBACK_PORT ->
- builder.setLoopbackPort(settings.port.get()); //TODO: move port from settings to vaultsettings (see https://github.com/cryptomator/cryptomator/tree/feature/mount-setting-per-vault)
+ case LOOPBACK_PORT -> {
+ if (vaultSettings.mountService.getValue() == null) {
+ builder.setLoopbackPort(settings.port.get());
+ } else {
+ builder.setLoopbackPort(vaultSettings.port.get());
+ }
+ }
case LOOPBACK_HOST_NAME -> env.getLoopbackAlias().ifPresent(builder::setLoopbackHostName);
case READ_ONLY -> builder.setReadOnly(vaultSettings.usesReadOnlyMode.get());
case MOUNT_FLAGS -> {
@@ -131,13 +155,26 @@ public class Mounter {
}
public MountHandle mount(VaultSettings vaultSettings, Path cryptoFsRoot) throws IOException, MountFailedException {
- var mountService = this.mountServiceObservable.getValue().service();
+ var mountService = mountProviders.stream().filter(s -> s.getClass().getName().equals(vaultSettings.mountService.getValue())).findFirst().orElse(defaultMountService.getValue());
+
+ if (isConflictingMountService(mountService)) {
+ var msg = STR."\{mountService.getClass()} unavailable due to conflict with either of \{CONFLICTING_MOUNT_SERVICES.get(mountService.getClass().getName())}";
+ throw new ConflictingMountServiceException(msg);
+ }
+
+ usedMountServices.add(mountService);
+
var builder = mountService.forFileSystem(cryptoFsRoot);
- var internal = new SettledMounter(mountService, builder, vaultSettings);
+ var internal = new SettledMounter(mountService, builder, vaultSettings); // FIXME: no need for an inner class
var cleanup = internal.prepare();
return new MountHandle(builder.mount(), mountService.hasCapability(UNMOUNT_FORCED), cleanup);
}
+ public boolean isConflictingMountService(MountService service) {
+ var conflictingServices = CONFLICTING_MOUNT_SERVICES.getOrDefault(service.getClass().getName(), Set.of());
+ return usedMountServices.stream().map(MountService::getClass).map(Class::getName).anyMatch(conflictingServices::contains);
+ }
+
public record MountHandle(Mount mountObj, boolean supportsUnmountForced, Runnable specialCleanup) {
}
diff --git a/src/main/java/org/cryptomator/common/settings/VaultSettings.java b/src/main/java/org/cryptomator/common/settings/VaultSettings.java
index 6662f61ff..fd21fc197 100644
--- a/src/main/java/org/cryptomator/common/settings/VaultSettings.java
+++ b/src/main/java/org/cryptomator/common/settings/VaultSettings.java
@@ -8,7 +8,6 @@ package org.cryptomator.common.settings;
import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
import com.google.common.io.BaseEncoding;
-import org.apache.commons.lang3.SystemUtils;
import org.jetbrains.annotations.VisibleForTesting;
import javafx.beans.Observable;
@@ -40,6 +39,7 @@ public class VaultSettings {
static final WhenUnlocked DEFAULT_ACTION_AFTER_UNLOCK = WhenUnlocked.ASK;
static final boolean DEFAULT_AUTOLOCK_WHEN_IDLE = false;
static final int DEFAULT_AUTOLOCK_IDLE_SECONDS = 30 * 60;
+ static final int DEFAULT_PORT = 42427;
private static final Random RNG = new Random();
@@ -56,6 +56,8 @@ public class VaultSettings {
public final IntegerProperty autoLockIdleSeconds;
public final ObjectProperty mountPoint;
public final StringExpression mountName;
+ public final StringProperty mountService;
+ public final IntegerProperty port;
VaultSettings(VaultSettingsJson json) {
this.id = json.id;
@@ -70,6 +72,8 @@ public class VaultSettings {
this.autoLockWhenIdle = new SimpleBooleanProperty(this, "autoLockWhenIdle", json.autoLockWhenIdle);
this.autoLockIdleSeconds = new SimpleIntegerProperty(this, "autoLockIdleSeconds", json.autoLockIdleSeconds);
this.mountPoint = new SimpleObjectProperty<>(this, "mountPoint", json.mountPoint == null ? null : Path.of(json.mountPoint));
+ this.mountService = new SimpleStringProperty(this, "mountService", json.mountService);
+ this.port = new SimpleIntegerProperty(this, "port", json.port);
// mount name is no longer an explicit setting, see https://github.com/cryptomator/cryptomator/pull/1318
this.mountName = StringExpression.stringExpression(Bindings.createStringBinding(() -> {
final String name;
@@ -95,7 +99,7 @@ public class VaultSettings {
}
Observable[] observables() {
- return new Observable[]{actionAfterUnlock, autoLockIdleSeconds, autoLockWhenIdle, displayName, maxCleartextFilenameLength, mountFlags, mountPoint, path, revealAfterMount, unlockAfterStartup, usesReadOnlyMode};
+ return new Observable[]{actionAfterUnlock, autoLockIdleSeconds, autoLockWhenIdle, displayName, maxCleartextFilenameLength, mountFlags, mountPoint, path, revealAfterMount, unlockAfterStartup, usesReadOnlyMode, port, mountService};
}
public static VaultSettings withRandomId() {
@@ -124,6 +128,8 @@ public class VaultSettings {
json.autoLockWhenIdle = autoLockWhenIdle.get();
json.autoLockIdleSeconds = autoLockIdleSeconds.get();
json.mountPoint = mountPoint.map(Path::toString).getValue();
+ json.mountService = mountService.get();
+ json.port = port.get();
return json;
}
diff --git a/src/main/java/org/cryptomator/common/settings/VaultSettingsJson.java b/src/main/java/org/cryptomator/common/settings/VaultSettingsJson.java
index 2381203e5..43aa204e8 100644
--- a/src/main/java/org/cryptomator/common/settings/VaultSettingsJson.java
+++ b/src/main/java/org/cryptomator/common/settings/VaultSettingsJson.java
@@ -45,6 +45,12 @@ class VaultSettingsJson {
@JsonProperty("autoLockIdleSeconds")
int autoLockIdleSeconds = VaultSettings.DEFAULT_AUTOLOCK_IDLE_SECONDS;
+ @JsonProperty("mountService")
+ String mountService;
+
+ @JsonProperty("port")
+ int port = VaultSettings.DEFAULT_PORT;
+
@Deprecated(since = "1.7.0")
@JsonProperty(value = "winDriveLetter", access = JsonProperty.Access.WRITE_ONLY) // WRITE_ONLY means value is "written" into the java object during deserialization. Upvote this: https://github.com/FasterXML/jackson-annotations/issues/233
String winDriveLetter;
diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java
index 2e1e34a78..ac913d316 100644
--- a/src/main/java/org/cryptomator/common/vaults/Vault.java
+++ b/src/main/java/org/cryptomator/common/vaults/Vault.java
@@ -11,7 +11,6 @@ package org.cryptomator.common.vaults;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Constants;
import org.cryptomator.common.mount.Mounter;
-import org.cryptomator.common.mount.WindowsDriveLetters;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
@@ -73,7 +72,13 @@ public class Vault {
private final AtomicReference mountHandle = new AtomicReference<>(null);
@Inject
- Vault(VaultSettings vaultSettings, VaultConfigCache configCache, AtomicReference cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty lastKnownException, VaultStats stats, WindowsDriveLetters windowsDriveLetters, Mounter mounter) {
+ Vault(VaultSettings vaultSettings, //
+ VaultConfigCache configCache, //
+ AtomicReference cryptoFileSystem, //
+ VaultState state, //
+ @Named("lastKnownException") ObjectProperty lastKnownException, //
+ VaultStats stats, //
+ Mounter mounter) {
this.vaultSettings = vaultSettings;
this.configCache = configCache;
this.cryptoFileSystem = cryptoFileSystem;
diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java
index 46542ccb9..1a2374f85 100644
--- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java
+++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java
@@ -21,9 +21,11 @@ public enum FxmlFile {
HUB_INVALID_LICENSE("/fxml/hub_invalid_license.fxml"), //
HUB_RECEIVE_KEY("/fxml/hub_receive_key.fxml"), //
HUB_LEGACY_REGISTER_DEVICE("/fxml/hub_legacy_register_device.fxml"), //
+ HUB_LEGACY_REGISTER_SUCCESS("/fxml/hub_legacy_register_success.fxml"), //
HUB_REGISTER_SUCCESS("/fxml/hub_register_success.fxml"), //
+ HUB_REGISTER_DEVICE_ALREADY_EXISTS("/fxml/hub_register_device_already_exists.fxml"), //
HUB_REGISTER_FAILED("/fxml/hub_register_failed.fxml"), //
- HUB_SETUP_DEVICE("/fxml/hub_setup_device.fxml"), //
+ HUB_REGISTER_DEVICE("/fxml/hub_register_device.fxml"), //
HUB_UNAUTHORIZED_DEVICE("/fxml/hub_unauthorized_device.fxml"), //
HUB_REQUIRE_ACCOUNT_INIT("/fxml/hub_require_account_init.fxml"), //
LOCK_FORCED("/fxml/lock_forced.fxml"), //
@@ -43,8 +45,10 @@ public enum FxmlFile {
RECOVERYKEY_RESET_PASSWORD_SUCCESS("/fxml/recoverykey_reset_password_success.fxml"), //
RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), //
REMOVE_VAULT("/fxml/remove_vault.fxml"), //
+ SHARE_VAULT("/fxml/share_vault.fxml"), //
UPDATE_REMINDER("/fxml/update_reminder.fxml"), //
UNLOCK_ENTER_PASSWORD("/fxml/unlock_enter_password.fxml"),
+ UNLOCK_REQUIRES_RESTART("/fxml/unlock_requires_restart.fxml"), //
UNLOCK_INVALID_MOUNT_POINT("/fxml/unlock_invalid_mount_point.fxml"), //
UNLOCK_SELECT_MASTERKEYFILE("/fxml/unlock_select_masterkeyfile.fxml"), //
UNLOCK_SUCCESS("/fxml/unlock_success.fxml"), //
diff --git a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java
index c5ec19929..e454835cf 100644
--- a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java
+++ b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java
@@ -47,6 +47,7 @@ public enum FontAwesome5Icon {
QUESTION_CIRCLE("\uf059"), //
REDO("\uF01E"), //
SEARCH("\uF002"), //
+ SHARE("\uF064"), //
SPINNER("\uF110"), //
STETHOSCOPE("\uF0f1"), //
SYNC("\uF021"), //
diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java
index 86ca62c3b..af98e284c 100644
--- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java
+++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java
@@ -13,6 +13,7 @@ import org.cryptomator.ui.lock.LockComponent;
import org.cryptomator.ui.mainwindow.MainWindowComponent;
import org.cryptomator.ui.preferences.PreferencesComponent;
import org.cryptomator.ui.quit.QuitComponent;
+import org.cryptomator.ui.sharevault.ShareVaultComponent;
import org.cryptomator.ui.traymenu.TrayMenuComponent;
import org.cryptomator.ui.unlock.UnlockComponent;
import org.cryptomator.ui.updatereminder.UpdateReminderComponent;
@@ -22,7 +23,17 @@ import javafx.scene.image.Image;
import java.io.IOException;
import java.io.InputStream;
-@Module(includes = {UpdateCheckerModule.class}, subcomponents = {TrayMenuComponent.class, MainWindowComponent.class, PreferencesComponent.class, VaultOptionsComponent.class, UnlockComponent.class, LockComponent.class, QuitComponent.class, ErrorComponent.class, HealthCheckComponent.class, UpdateReminderComponent.class})
+@Module(includes = {UpdateCheckerModule.class}, subcomponents = {TrayMenuComponent.class, //
+ MainWindowComponent.class, //
+ PreferencesComponent.class, //
+ VaultOptionsComponent.class, //
+ UnlockComponent.class, //
+ LockComponent.class, //
+ QuitComponent.class, //
+ ErrorComponent.class, //
+ HealthCheckComponent.class, //
+ UpdateReminderComponent.class, //
+ ShareVaultComponent.class})
abstract class FxApplicationModule {
private static Image createImageFromResource(String resourceName) throws IOException {
diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java
index 866b1d9df..41a7ca785 100644
--- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java
+++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java
@@ -11,6 +11,7 @@ import org.cryptomator.ui.mainwindow.MainWindowComponent;
import org.cryptomator.ui.preferences.PreferencesComponent;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import org.cryptomator.ui.quit.QuitComponent;
+import org.cryptomator.ui.sharevault.ShareVaultComponent;
import org.cryptomator.ui.unlock.UnlockComponent;
import org.cryptomator.ui.unlock.UnlockWorkflow;
import org.cryptomator.ui.updatereminder.UpdateReminderComponent;
@@ -51,6 +52,7 @@ public class FxApplicationWindows {
private final ErrorComponent.Factory errorWindowFactory;
private final ExecutorService executor;
private final VaultOptionsComponent.Factory vaultOptionsWindow;
+ private final ShareVaultComponent.Factory shareVaultWindow;
private final FilteredList visibleWindows;
@Inject
@@ -64,6 +66,7 @@ public class FxApplicationWindows {
LockComponent.Factory lockWorkflowFactory, //
ErrorComponent.Factory errorWindowFactory, //
VaultOptionsComponent.Factory vaultOptionsWindow, //
+ ShareVaultComponent.Factory shareVaultWindow, //
ExecutorService executor) {
this.primaryStage = primaryStage;
this.trayIntegration = trayIntegration;
@@ -76,6 +79,7 @@ public class FxApplicationWindows {
this.errorWindowFactory = errorWindowFactory;
this.executor = executor;
this.vaultOptionsWindow = vaultOptionsWindow;
+ this.shareVaultWindow = shareVaultWindow;
this.visibleWindows = Window.getWindows().filtered(Window::isShowing);
}
@@ -122,6 +126,10 @@ public class FxApplicationWindows {
return CompletableFuture.supplyAsync(() -> preferencesWindow.get().showPreferencesWindow(selectedTab), Platform::runLater).whenComplete(this::reportErrors);
}
+ public void showShareVaultWindow(Vault vault) {
+ CompletableFuture.runAsync(() -> shareVaultWindow.create(vault).showShareVaultWindow(), Platform::runLater);
+ }
+
public CompletionStage showVaultOptionsWindow(Vault vault, SelectedVaultOptionsTab tab) {
return showMainWindow().thenApplyAsync((window) -> vaultOptionsWindow.create(vault).showVaultOptionsWindow(tab), Platform::runLater).whenComplete(this::reportErrors);
}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/DeviceAlreadyExistsException.java b/src/main/java/org/cryptomator/ui/keyloading/hub/DeviceAlreadyExistsException.java
new file mode 100644
index 000000000..0c942e67b
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/DeviceAlreadyExistsException.java
@@ -0,0 +1,12 @@
+package org.cryptomator.ui.keyloading.hub;
+
+import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
+
+/**
+ * Thrown, when Hub registerDevice-Request returns with 409
+ */
+class DeviceAlreadyExistsException extends MasterkeyLoadingFailedException {
+ public DeviceAlreadyExistsException() {
+ super("Device already registered on this Hub instance");
+ }
+}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubConfig.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubConfig.java
index f8ec7b854..eefad55a2 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubConfig.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubConfig.java
@@ -1,7 +1,7 @@
package org.cryptomator.ui.keyloading.hub;
+import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.net.URI;
@@ -19,6 +19,19 @@ public class HubConfig {
@Deprecated // use apiBaseUrl + "/devices/"
public String devicesResourceUrl;
+ /**
+ * A collection of String template processors to construct URIs related to this Hub instance.
+ */
+ @JsonIgnore
+ public final URIProcessors URIs = new URIProcessors();
+
+ /**
+ * Get the URI pointing to the /api/ base resource.
+ *
+ * @return /api/ URI
+ * @apiNote URI is guaranteed to end on /
+ * @see #URIs
+ */
public URI getApiBaseUrl() {
if (apiBaseUrl != null) {
// make sure to end on "/":
@@ -33,4 +46,17 @@ public class HubConfig {
public URI getWebappBaseUrl() {
return getApiBaseUrl().resolve("../app/");
}
+
+ public class URIProcessors {
+
+ /**
+ * Resolves paths relative to the /api/ endpoint of this Hub instance.
+ */
+ public final StringTemplate.Processor API = template -> {
+ var path = template.interpolate();
+ var relPath = path.startsWith("/") ? path.substring(1) : path;
+ return getApiBaseUrl().resolve(relPath);
+ };
+
+ }
}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java
index 235fbf639..f8710b8c0 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java
@@ -119,6 +119,12 @@ public abstract class HubKeyLoadingModule {
return fxmlLoaders.createScene(FxmlFile.HUB_LEGACY_REGISTER_DEVICE);
}
+ @Provides
+ @FxmlScene(FxmlFile.HUB_LEGACY_REGISTER_SUCCESS)
+ @KeyLoadingScoped
+ static Scene provideHubLegacyRegisterSuccessScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
+ return fxmlLoaders.createScene(FxmlFile.HUB_LEGACY_REGISTER_SUCCESS);
+ }
@Provides
@FxmlScene(FxmlFile.HUB_REGISTER_SUCCESS)
@@ -135,10 +141,17 @@ public abstract class HubKeyLoadingModule {
}
@Provides
- @FxmlScene(FxmlFile.HUB_SETUP_DEVICE)
+ @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE)
@KeyLoadingScoped
static Scene provideHubRegisterDeviceScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
- return fxmlLoaders.createScene(FxmlFile.HUB_SETUP_DEVICE);
+ return fxmlLoaders.createScene(FxmlFile.HUB_REGISTER_DEVICE);
+ }
+
+ @Provides
+ @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE_ALREADY_EXISTS)
+ @KeyLoadingScoped
+ static Scene provideHubRegisterDeviceAlreadyExistsScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
+ return fxmlLoaders.createScene(FxmlFile.HUB_REGISTER_DEVICE_ALREADY_EXISTS);
}
@Provides
@@ -185,6 +198,11 @@ public abstract class HubKeyLoadingModule {
@FxControllerKey(LegacyRegisterDeviceController.class)
abstract FxController bindLegacyRegisterDeviceController(LegacyRegisterDeviceController controller);
+ @Binds
+ @IntoMap
+ @FxControllerKey(LegacyRegisterSuccessController.class)
+ abstract FxController bindLegacyRegisterSuccessController(LegacyRegisterSuccessController controller);
+
@Binds
@IntoMap
@FxControllerKey(RegisterSuccessController.class)
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java
index 9ea5e7735..40f845a63 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java
@@ -1,7 +1,6 @@
package org.cryptomator.ui.keyloading.hub;
import com.google.common.base.Preconditions;
-import com.nimbusds.jose.JWEObject;
import dagger.Lazy;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.keychain.NoKeychainAccessProviderException;
@@ -44,6 +43,7 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
this.window = window;
this.keychainManager = keychainManager;
window.setTitle(windowTitle);
+ window.setOnCloseRequest(_ -> result.cancel(true));
this.authFlowScene = authFlowScene;
this.noKeychainScene = noKeychainScene;
this.result = result;
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/JWEHelper.java b/src/main/java/org/cryptomator/ui/keyloading/hub/JWEHelper.java
index 2333051be..41bb6902a 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/JWEHelper.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/JWEHelper.java
@@ -1,7 +1,6 @@
package org.cryptomator.ui.keyloading.hub;
import com.google.common.base.Preconditions;
-import com.google.common.io.BaseEncoding;
import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWEAlgorithm;
@@ -13,19 +12,20 @@ import com.nimbusds.jose.crypto.ECDHEncrypter;
import com.nimbusds.jose.crypto.PasswordBasedDecrypter;
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
-import com.nimbusds.jose.jwk.gen.JWKGenerator;
+import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.security.Key;
import java.security.KeyFactory;
-import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.Map;
@@ -37,26 +37,16 @@ class JWEHelper {
private static final String JWE_PAYLOAD_KEY_FIELD = "key";
private static final String EC_ALG = "EC";
- private JWEHelper(){}
+ private JWEHelper() {}
+
public static JWEObject encryptUserKey(ECPrivateKey userKey, ECPublicKey deviceKey) {
- try {
- var encodedUserKey = Base64.getEncoder().encodeToString(userKey.getEncoded());
- var keyGen = new ECKeyGenerator(Curve.P_384);
- var ephemeralKeyPair = keyGen.generate();
- var header = new JWEHeader.Builder(JWEAlgorithm.ECDH_ES, EncryptionMethod.A256GCM).ephemeralPublicKey(ephemeralKeyPair.toPublicJWK()).build();
- var payload = new Payload(Map.of(JWE_PAYLOAD_KEY_FIELD, encodedUserKey));
- var jwe = new JWEObject(header, payload);
- jwe.encrypt(new ECDHEncrypter(deviceKey));
- return jwe;
- } catch (JOSEException e) {
- throw new RuntimeException(e);
- }
+ return encryptKey(userKey, deviceKey);
}
public static ECPrivateKey decryptUserKey(JWEObject jwe, String setupCode) throws InvalidJweKeyException {
try {
jwe.decrypt(new PasswordBasedDecrypter(setupCode));
- return decodeUserKey(jwe);
+ return readKey(jwe, JWE_PAYLOAD_KEY_FIELD, JWEHelper::decodeECPrivateKey);
} catch (JOSEException e) {
throw new InvalidJweKeyException(e);
}
@@ -65,17 +55,23 @@ class JWEHelper {
public static ECPrivateKey decryptUserKey(JWEObject jwe, ECPrivateKey deviceKey) throws InvalidJweKeyException {
try {
jwe.decrypt(new ECDHDecrypter(deviceKey));
- return decodeUserKey(jwe);
+ return readKey(jwe, JWE_PAYLOAD_KEY_FIELD, JWEHelper::decodeECPrivateKey);
} catch (JOSEException e) {
throw new InvalidJweKeyException(e);
}
}
- private static ECPrivateKey decodeUserKey(JWEObject decryptedJwe) {
+ /**
+ * Attempts to decode a DER-encoded EC private key.
+ *
+ * @param encoded DER-encoded EC private key
+ * @return the decoded key
+ * @throws KeyDecodeFailedException On malformed input
+ */
+ public static ECPrivateKey decodeECPrivateKey(byte[] encoded) throws KeyDecodeFailedException {
try {
- var keySpec = readKey(decryptedJwe, JWE_PAYLOAD_KEY_FIELD, PKCS8EncodedKeySpec::new);
- var factory = KeyFactory.getInstance(EC_ALG);
- var privateKey = factory.generatePrivate(keySpec);
+ KeyFactory factory = KeyFactory.getInstance(EC_ALG);
+ var privateKey = factory.generatePrivate(new PKCS8EncodedKeySpec(encoded));
if (privateKey instanceof ECPrivateKey ecPrivateKey) {
return ecPrivateKey;
} else {
@@ -84,8 +80,49 @@ class JWEHelper {
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(EC_ALG + " not supported");
} catch (InvalidKeySpecException e) {
- LOG.warn("Unexpected JWE payload: {}", decryptedJwe.getPayload());
- throw new MasterkeyLoadingFailedException("Unexpected JWE payload", e);
+ throw new KeyDecodeFailedException(e);
+ }
+ }
+
+ /**
+ * Attempts to decode a DER-encoded EC public key.
+ *
+ * @param encoded DER-encoded EC public key
+ * @return the decoded key
+ * @throws KeyDecodeFailedException On malformed input
+ */
+ public static ECPublicKey decodeECPublicKey(byte[] encoded) throws KeyDecodeFailedException {
+ try {
+ KeyFactory factory = KeyFactory.getInstance(EC_ALG);
+ var publicKey = factory.generatePublic(new X509EncodedKeySpec(encoded));
+ if (publicKey instanceof ECPublicKey ecPublicKey) {
+ return ecPublicKey;
+ } else {
+ throw new IllegalStateException(EC_ALG + " key factory not generating ECPublicKeys");
+ }
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException(EC_ALG + " not supported");
+ } catch (InvalidKeySpecException e) {
+ throw new KeyDecodeFailedException(e);
+ }
+ }
+
+ public static JWEObject encryptVaultKey(Masterkey vaultKey, ECPublicKey userKey) {
+ return encryptKey(vaultKey, userKey);
+ }
+
+ private static JWEObject encryptKey(Key key, ECPublicKey userKey) {
+ try {
+ var encodedVaultKey = Base64.getEncoder().encodeToString(key.getEncoded());
+ var keyGen = new ECKeyGenerator(Curve.P_384);
+ var ephemeralKeyPair = keyGen.generate();
+ var header = new JWEHeader.Builder(JWEAlgorithm.ECDH_ES, EncryptionMethod.A256GCM).ephemeralPublicKey(ephemeralKeyPair.toPublicJWK()).build();
+ var payload = new Payload(Map.of(JWE_PAYLOAD_KEY_FIELD, encodedVaultKey));
+ var jwe = new JWEObject(header, payload);
+ jwe.encrypt(new ECDHEncrypter(userKey));
+ return jwe;
+ } catch (JOSEException e) {
+ throw new RuntimeException(e);
}
}
@@ -108,12 +145,12 @@ class JWEHelper {
var keyBytes = new byte[0];
try {
if (fields.get(keyField) instanceof String key) {
- keyBytes = BaseEncoding.base64().decode(key);
+ keyBytes = Base64.getDecoder().decode(key);
return rawKeyFactory.apply(keyBytes);
} else {
throw new IllegalArgumentException("JWE payload doesn't contain field " + keyField);
}
- } catch (IllegalArgumentException e) {
+ } catch (IllegalArgumentException | KeyDecodeFailedException e) {
LOG.error("Unexpected JWE payload: {}", jwe.getPayload());
throw new MasterkeyLoadingFailedException("Unexpected JWE payload", e);
} finally {
@@ -127,4 +164,11 @@ class JWEHelper {
super("Invalid key", cause);
}
}
+
+ public static class KeyDecodeFailedException extends CryptoException {
+
+ public KeyDecodeFailedException(Throwable cause) {
+ super("Malformed key", cause);
+ }
+ }
}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/LegacyRegisterDeviceController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/LegacyRegisterDeviceController.java
index 113ecd249..219616e8b 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/LegacyRegisterDeviceController.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/LegacyRegisterDeviceController.java
@@ -64,7 +64,7 @@ public class LegacyRegisterDeviceController implements FxController {
public Button registerBtn;
@Inject
- public LegacyRegisterDeviceController(@KeyLoading Stage window, ExecutorService executor, HubConfig hubConfig, @Named("deviceId") String deviceId, DeviceKey deviceKey, CompletableFuture result, @Named("bearerToken") AtomicReference bearerToken, @FxmlScene(FxmlFile.HUB_REGISTER_SUCCESS) Lazy registerSuccessScene, @FxmlScene(FxmlFile.HUB_REGISTER_FAILED) Lazy registerFailedScene) {
+ public LegacyRegisterDeviceController(@KeyLoading Stage window, ExecutorService executor, HubConfig hubConfig, @Named("deviceId") String deviceId, DeviceKey deviceKey, CompletableFuture result, @Named("bearerToken") AtomicReference bearerToken, @FxmlScene(FxmlFile.HUB_LEGACY_REGISTER_SUCCESS) Lazy registerSuccessScene, @FxmlScene(FxmlFile.HUB_REGISTER_FAILED) Lazy registerFailedScene) {
this.window = window;
this.hubConfig = hubConfig;
this.deviceId = deviceId;
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/LegacyRegisterSuccessController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/LegacyRegisterSuccessController.java
new file mode 100644
index 000000000..130bc3b4f
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/LegacyRegisterSuccessController.java
@@ -0,0 +1,24 @@
+package org.cryptomator.ui.keyloading.hub;
+
+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;
+
+@KeyLoadingScoped
+public class LegacyRegisterSuccessController implements FxController {
+ private final Stage window;
+
+ @Inject
+ public LegacyRegisterSuccessController(@KeyLoading Stage window) {
+ this.window = window;
+ }
+
+ @FXML
+ public void close() {
+ window.close();
+ }
+}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java
index c0681d4bb..3bfb4ec8e 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java
@@ -3,6 +3,7 @@ package org.cryptomator.ui.keyloading.hub;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Preconditions;
import com.nimbusds.jose.JWEObject;
import dagger.Lazy;
import org.cryptomator.common.vaults.Vault;
@@ -11,7 +12,6 @@ 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.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -32,7 +32,6 @@ import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.time.Duration;
-import java.time.Instant;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
@@ -48,29 +47,29 @@ public class ReceiveKeyController implements FxController {
private final Stage window;
private final HubConfig hubConfig;
+ private final String vaultId;
private final String deviceId;
private final String bearerToken;
private final CompletableFuture result;
- private final Lazy setupDeviceScene;
+ private final Lazy registerDeviceScene;
private final Lazy legacyRegisterDeviceScene;
private final Lazy unauthorizedScene;
private final Lazy accountInitializationScene;
- private final URI vaultBaseUri;
private final Lazy invalidLicenseScene;
private final HttpClient httpClient;
@Inject
- public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, HubConfig hubConfig, @Named("deviceId") String deviceId, @Named("bearerToken") AtomicReference tokenRef, CompletableFuture result, @FxmlScene(FxmlFile.HUB_SETUP_DEVICE) Lazy setupDeviceScene, @FxmlScene(FxmlFile.HUB_LEGACY_REGISTER_DEVICE) Lazy legacyRegisterDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy unauthorizedScene, @FxmlScene(FxmlFile.HUB_REQUIRE_ACCOUNT_INIT) Lazy accountInitializationScene, @FxmlScene(FxmlFile.HUB_INVALID_LICENSE) Lazy invalidLicenseScene) {
+ public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, HubConfig hubConfig, @Named("deviceId") String deviceId, @Named("bearerToken") AtomicReference tokenRef, CompletableFuture result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy registerDeviceScene, @FxmlScene(FxmlFile.HUB_LEGACY_REGISTER_DEVICE) Lazy legacyRegisterDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy unauthorizedScene, @FxmlScene(FxmlFile.HUB_REQUIRE_ACCOUNT_INIT) Lazy accountInitializationScene, @FxmlScene(FxmlFile.HUB_INVALID_LICENSE) Lazy invalidLicenseScene) {
this.window = window;
this.hubConfig = hubConfig;
+ this.vaultId = extractVaultId(vault.getVaultConfigCache().getUnchecked().getKeyId()); // TODO: access vault config's JTI directly (requires changes in cryptofs)
this.deviceId = deviceId;
this.bearerToken = Objects.requireNonNull(tokenRef.get());
this.result = result;
- this.setupDeviceScene = setupDeviceScene;
+ this.registerDeviceScene = registerDeviceScene;
this.legacyRegisterDeviceScene = legacyRegisterDeviceScene;
this.unauthorizedScene = unauthorizedScene;
this.accountInitializationScene = accountInitializationScene;
- this.vaultBaseUri = getVaultBaseUri(vault);
this.invalidLicenseScene = invalidLicenseScene;
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
this.httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).executor(executor).build();
@@ -78,70 +77,76 @@ public class ReceiveKeyController implements FxController {
@FXML
public void initialize() {
- requestVaultMasterkey();
+ receiveKey();
+ }
+
+ public void receiveKey() {
+ requestApiConfig();
}
/**
- * STEP 1 (Request): GET vault key for this user
+ * STEP 0 (Request): GET /api/config
*/
- private void requestVaultMasterkey() {
- var accessTokenUri = appendPath(vaultBaseUri, "/access-token");
- var request = HttpRequest.newBuilder(accessTokenUri) //
- .header("Authorization", "Bearer " + bearerToken) //
+ private void requestApiConfig() {
+ var configUri = hubConfig.URIs.API."config";
+ var request = HttpRequest.newBuilder(configUri) //
.GET() //
.timeout(REQ_TIMEOUT) //
.build();
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.US_ASCII)) //
- .thenAcceptAsync(this::receivedVaultMasterkey, Platform::runLater) //
+ .thenAcceptAsync(this::receivedApiConfig, Platform::runLater) //
.exceptionally(this::retrievalFailed);
}
/**
- * STEP 1 (Response): GET vault key for this user
+ * STEP 0 (Response): GET /api/config
*
* @param response Response
*/
- private void receivedVaultMasterkey(HttpResponse response) {
+ private void receivedApiConfig(HttpResponse response) {
LOG.debug("GET {} -> Status Code {}", response.request().uri(), response.statusCode());
- switch (response.statusCode()) {
- case 200 -> requestUserKey(response.body());
- case 402 -> licenseExceeded();
- case 403, 410 -> accessNotGranted(); // or vault has been archived, effectively disallowing access - TODO: add specific dialog?
- case 449 -> accountInitializationRequired();
- case 404 -> requestLegacyAccessToken();
- default -> throw new IllegalStateException("Unexpected response " + response.statusCode());
+ Preconditions.checkState(response.statusCode() == 200, "Unexpected response " + response.statusCode());
+ try {
+ var config = JSON.reader().readValue(response.body(), ConfigDto.class);
+ if (config.apiLevel >= 1) {
+ requestDeviceData();
+ } else {
+ requestLegacyAccessToken();
+ }
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
}
}
/**
- * STEP 2 (Request): GET user key for this device
+ * STEP 1 (Request): GET user key for this device
*/
- private void requestUserKey(String encryptedVaultKey) {
- var deviceTokenUri = URI.create(hubConfig.getApiBaseUrl() + "/devices/" + deviceId);
- var request = HttpRequest.newBuilder(deviceTokenUri) //
+ private void requestDeviceData() {
+ var deviceUri = hubConfig.URIs.API."devices/\{deviceId}";
+ var request = HttpRequest.newBuilder(deviceUri) //
.header("Authorization", "Bearer " + bearerToken) //
.GET() //
.timeout(REQ_TIMEOUT) //
.build();
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)) //
- .thenAcceptAsync(response -> receivedUserKey(encryptedVaultKey, response), Platform::runLater) //
+ .thenAcceptAsync(this::receivedDeviceData) //
.exceptionally(this::retrievalFailed);
}
/**
- * STEP 2 (Response): GET user key for this device
+ * STEP 1 (Response): GET user key for this device
*
* @param response Response
*/
- private void receivedUserKey(String encryptedVaultKey, HttpResponse response) {
+ private void receivedDeviceData(HttpResponse response) {
LOG.debug("GET {} -> Status Code {}", response.request().uri(), response.statusCode());
try {
switch (response.statusCode()) {
case 200 -> {
var device = JSON.reader().readValue(response.body(), DeviceDto.class);
- receivedBothEncryptedKeys(encryptedVaultKey, device.userPrivateKey);
+ requestVaultMasterkey(device.userPrivateKey);
}
- case 404 -> needsDeviceSetup(); // TODO: using the setup code, we can theoretically immediately unlock
+ case 404 -> Platform.runLater(this::needsDeviceRegistration);
default -> throw new IllegalStateException("Unexpected response " + response.statusCode());
}
} catch (IOException e) {
@@ -149,18 +154,49 @@ public class ReceiveKeyController implements FxController {
}
}
- private void needsDeviceSetup() {
- window.setScene(setupDeviceScene.get());
+ private void needsDeviceRegistration() {
+ window.setScene(registerDeviceScene.get());
}
- private void receivedBothEncryptedKeys(String encryptedVaultKey, String encryptedUserKey) throws IOException {
+ /**
+ * STEP 2 (Request): GET vault key for this user
+ */
+ private void requestVaultMasterkey(String encryptedUserKey) {
+ var vaultKeyUri = hubConfig.URIs.API."vaults/\{vaultId}/access-token";
+ var request = HttpRequest.newBuilder(vaultKeyUri) //
+ .header("Authorization", "Bearer " + bearerToken) //
+ .GET() //
+ .timeout(REQ_TIMEOUT) //
+ .build();
+ httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.US_ASCII)) //
+ .thenAcceptAsync(response -> receivedVaultMasterkey(encryptedUserKey, response), Platform::runLater) //
+ .exceptionally(this::retrievalFailed);
+ }
+
+ /**
+ * STEP 2 (Response): GET vault key for this user
+ *
+ * @param response Response
+ */
+ private void receivedVaultMasterkey(String encryptedUserKey, HttpResponse response) {
+ LOG.debug("GET {} -> Status Code {}", response.request().uri(), response.statusCode());
+ switch (response.statusCode()) {
+ case 200 -> receivedBothEncryptedKeys(response.body(), encryptedUserKey);
+ case 402 -> licenseExceeded();
+ case 403, 410 -> accessNotGranted(); // or vault has been archived, effectively disallowing access - TODO: add specific dialog?
+ case 449 -> accountInitializationRequired();
+ default -> throw new IllegalStateException("Unexpected response " + response.statusCode());
+ }
+ }
+
+ private void receivedBothEncryptedKeys(String encryptedVaultKey, String encryptedUserKey) {
try {
var vaultKeyJwe = JWEObject.parse(encryptedVaultKey);
var userKeyJwe = JWEObject.parse(encryptedUserKey);
result.complete(ReceivedKey.vaultKeyAndUserKey(vaultKeyJwe, userKeyJwe));
window.close();
} catch (ParseException e) {
- throw new IOException("Failed to parse JWE", e);
+ retrievalFailed(e);
}
}
@@ -169,7 +205,7 @@ public class ReceiveKeyController implements FxController {
*/
@Deprecated
private void requestLegacyAccessToken() {
- var legacyAccessTokenUri = appendPath(vaultBaseUri, "/keys/" + deviceId);
+ var legacyAccessTokenUri = hubConfig.URIs.API."vaults/\{vaultId}/keys/\{deviceId}";
var request = HttpRequest.newBuilder(legacyAccessTokenUri) //
.header("Authorization", "Bearer " + bearerToken) //
.GET() //
@@ -251,19 +287,15 @@ public class ReceiveKeyController implements FxController {
}
}
- private static URI getVaultBaseUri(Vault vault) {
- try {
- var url = vault.getVaultConfigCache().get().getKeyId();
- assert url.getScheme().startsWith(SCHEME_PREFIX);
- var correctedScheme = url.getScheme().substring(SCHEME_PREFIX.length());
- return new URI(correctedScheme, url.getSchemeSpecificPart(), url.getFragment());
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- } catch (URISyntaxException e) {
- throw new IllegalStateException("URI constructed from params known to be valid", e);
- }
+ private static String extractVaultId(URI vaultKeyUri) {
+ assert vaultKeyUri.getScheme().startsWith(SCHEME_PREFIX);
+ var path = vaultKeyUri.getPath();
+ return path.substring(path.lastIndexOf('/') + 1);
}
@JsonIgnoreProperties(ignoreUnknown = true)
private record DeviceDto(@JsonProperty(value = "userPrivateKey", required = true) String userPrivateKey) {}
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ private record ConfigDto(@JsonProperty(value = "apiLevel") int apiLevel) {}
}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java
index 837dc5032..b00d49874 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java
@@ -31,19 +31,25 @@ import javafx.scene.control.TextField;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.io.UncheckedIOException;
import java.net.InetAddress;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
+import java.security.interfaces.ECPublicKey;
import java.text.ParseException;
import java.time.Duration;
import java.time.Instant;
+import java.util.Base64;
+import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
@KeyLoadingScoped
public class RegisterDeviceController implements FxController {
@@ -57,12 +63,12 @@ public class RegisterDeviceController implements FxController {
private final String bearerToken;
private final Lazy registerSuccessScene;
private final Lazy registerFailedScene;
+ private final Lazy deviceAlreadyExistsScene;
private final String deviceId;
private final P384KeyPair deviceKeyPair;
private final CompletableFuture result;
private final HttpClient httpClient;
- private final BooleanProperty deviceNameAlreadyExists = new SimpleBooleanProperty(false);
private final BooleanProperty invalidSetupCode = new SimpleBooleanProperty(false);
private final BooleanProperty workInProgress = new SimpleBooleanProperty(false);
public TextField setupCodeField;
@@ -70,7 +76,7 @@ public class RegisterDeviceController implements FxController {
public Button registerBtn;
@Inject
- public RegisterDeviceController(@KeyLoading Stage window, ExecutorService executor, HubConfig hubConfig, @Named("deviceId") String deviceId, DeviceKey deviceKey, CompletableFuture result, @Named("bearerToken") AtomicReference bearerToken, @FxmlScene(FxmlFile.HUB_REGISTER_SUCCESS) Lazy registerSuccessScene, @FxmlScene(FxmlFile.HUB_REGISTER_FAILED) Lazy registerFailedScene) {
+ public RegisterDeviceController(@KeyLoading Stage window, ExecutorService executor, HubConfig hubConfig, @Named("deviceId") String deviceId, DeviceKey deviceKey, CompletableFuture result, @Named("bearerToken") AtomicReference bearerToken, @FxmlScene(FxmlFile.HUB_REGISTER_SUCCESS) Lazy registerSuccessScene, @FxmlScene(FxmlFile.HUB_REGISTER_FAILED) Lazy registerFailedScene, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE_ALREADY_EXISTS) Lazy deviceAlreadyExistsScene) {
this.window = window;
this.hubConfig = hubConfig;
this.deviceId = deviceId;
@@ -79,13 +85,13 @@ public class RegisterDeviceController implements FxController {
this.bearerToken = Objects.requireNonNull(bearerToken.get());
this.registerSuccessScene = registerSuccessScene;
this.registerFailedScene = registerFailedScene;
+ this.deviceAlreadyExistsScene = deviceAlreadyExistsScene;
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
this.httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).executor(executor).build();
}
public void initialize() {
deviceNameField.setText(determineHostname());
- deviceNameField.textProperty().addListener(observable -> deviceNameAlreadyExists.set(false));
deviceNameField.disableProperty().bind(workInProgress);
setupCodeField.textProperty().addListener(observable -> invalidSetupCode.set(false));
setupCodeField.disableProperty().bind(workInProgress);
@@ -108,9 +114,8 @@ public class RegisterDeviceController implements FxController {
public void register() {
workInProgress.set(true);
- var apiRootUrl = hubConfig.getApiBaseUrl();
- var userReq = HttpRequest.newBuilder(apiRootUrl.resolve("users/me")) //
+ var userReq = HttpRequest.newBuilder(hubConfig.URIs.API."users/me") //
.GET() //
.timeout(REQ_TIMEOUT) //
.header("Authorization", "Bearer " + bearerToken) //
@@ -126,17 +131,19 @@ public class RegisterDeviceController implements FxController {
}
}).thenApply(user -> {
try {
- assert user.privateKey != null; // api/vaults/{v}/user-tokens/me would have returned 403, if user wasn't fully set up yet
+ assert user.privateKey != null && user.publicKey != null; // api/vaults/{v}/user-tokens/me would have returned 403, if user wasn't fully set up yet
+ var userPublicKey = JWEHelper.decodeECPublicKey(Base64.getDecoder().decode(user.publicKey));
+ migrateLegacyDevices(userPublicKey); // TODO: remove eventually, when most users have migrated to Hub 1.3.x or newer
var userKey = JWEHelper.decryptUserKey(JWEObject.parse(user.privateKey), setupCodeField.getText());
return JWEHelper.encryptUserKey(userKey, deviceKeyPair.getPublic());
- } catch (ParseException e) {
+ } catch (ParseException | JWEHelper.KeyDecodeFailedException e) {
throw new RuntimeException("Server answered with unparsable user key", e);
}
}).thenCompose(jwe -> {
var now = Instant.now().toString();
var dto = new CreateDeviceDto(deviceId, deviceNameField.getText(), BaseEncoding.base64().encode(deviceKeyPair.getPublic().getEncoded()), "DESKTOP", jwe.serialize(), now);
var json = toJson(dto);
- var deviceUri = apiRootUrl.resolve("devices/" + deviceId);
+ var deviceUri = hubConfig.URIs.API."devices/\{deviceId}";
var putDeviceReq = HttpRequest.newBuilder(deviceUri) //
.PUT(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) //
.timeout(REQ_TIMEOUT) //
@@ -146,7 +153,7 @@ public class RegisterDeviceController implements FxController {
return httpClient.sendAsync(putDeviceReq, HttpResponse.BodyHandlers.discarding());
}).whenCompleteAsync((response, throwable) -> {
if (response != null) {
- this.handleResponse(response);
+ this.handleRegisterDeviceResponse(response);
} else {
this.setupFailed(throwable);
}
@@ -154,6 +161,46 @@ public class RegisterDeviceController implements FxController {
}, Platform::runLater);
}
+ private void migrateLegacyDevices(ECPublicKey userPublicKey) {
+ try {
+ // GET legacy access tokens
+ var getUri = hubConfig.URIs.API."devices/\{deviceId}/legacy-access-tokens";
+ var getReq = HttpRequest.newBuilder(getUri).GET().timeout(REQ_TIMEOUT).header("Authorization", "Bearer " + bearerToken).build();
+ var getRes = httpClient.send(getReq, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (getRes.statusCode() != 200) {
+ LOG.debug("GET {} resulted in status code {}. Skipping migration.", getUri, getRes.statusCode());
+ return;
+ }
+ Map legacyAccessTokens = JSON.readerForMapOf(String.class).readValue(getRes.body());
+ if (legacyAccessTokens.isEmpty()) {
+ return; // no migration required
+ }
+
+ // POST new access tokens
+ Map newAccessTokens = legacyAccessTokens.entrySet().stream().>mapMulti((entry, consumer) -> {
+ try (var vaultKey = JWEHelper.decryptVaultKey(JWEObject.parse(entry.getValue()), deviceKeyPair.getPrivate())) {
+ var newAccessToken = JWEHelper.encryptVaultKey(vaultKey, userPublicKey).serialize();
+ consumer.accept(Map.entry(entry.getKey(), newAccessToken));
+ } catch (ParseException | JWEHelper.InvalidJweKeyException e) {
+ LOG.warn("Failed to decrypt legacy access token for vault {}. Skipping migration.", entry.getKey());
+ }
+ }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ var postUri = hubConfig.URIs.API."users/me/access-tokens";
+ var postBody = JSON.writer().writeValueAsString(newAccessTokens);
+ var postReq = HttpRequest.newBuilder(postUri).POST(HttpRequest.BodyPublishers.ofString(postBody)).timeout(REQ_TIMEOUT).header("Authorization", "Bearer " + bearerToken).build();
+ var postRes = httpClient.send(postReq, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (postRes.statusCode() != 200) {
+ throw new IOException(STR."Unexpected response from POST \{postUri}: \{postRes.statusCode()}");
+ }
+ } catch (IOException e) {
+ // log and ignore: this is merely a best-effort attempt of migrating legacy devices. Failure is uncritical as this is merely a convenience feature.
+ LOG.error("Legacy Device Migration failed.", e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new UncheckedIOException(new InterruptedIOException("Legacy Device Migration interrupted"));
+ }
+ }
+
private UserDto fromJson(String json) {
try {
return JSON.reader().readValue(json, UserDto.class);
@@ -170,12 +217,12 @@ public class RegisterDeviceController implements FxController {
}
}
- private void handleResponse(HttpResponse response) {
+ private void handleRegisterDeviceResponse(HttpResponse response) {
if (response.statusCode() == 201) {
LOG.debug("Device registration for hub instance {} successful.", hubConfig.authSuccessUrl);
window.setScene(registerSuccessScene.get());
} else if (response.statusCode() == 409) {
- deviceNameAlreadyExists.set(true);
+ setupFailed(new DeviceAlreadyExistsException());
} else {
setupFailed(new IllegalStateException("Unexpected http status code " + response.statusCode()));
}
@@ -184,10 +231,13 @@ public class RegisterDeviceController implements FxController {
private void setupFailed(Throwable cause) {
switch (cause) {
case CompletionException e when e.getCause() instanceof JWEHelper.InvalidJweKeyException -> invalidSetupCode.set(true);
+ case DeviceAlreadyExistsException e -> {
+ LOG.debug("Device already registered in hub instance {} for different user", hubConfig.authSuccessUrl);
+ window.setScene(deviceAlreadyExistsScene.get());
+ }
default -> {
LOG.warn("Device setup failed.", cause);
window.setScene(registerFailedScene.get());
- result.completeExceptionally(cause);
}
}
}
@@ -202,15 +252,6 @@ public class RegisterDeviceController implements FxController {
}
//--- Getters & Setters
-
- public BooleanProperty deviceNameAlreadyExistsProperty() {
- return deviceNameAlreadyExists;
- }
-
- public boolean getDeviceNameAlreadyExists() {
- return deviceNameAlreadyExists.get();
- }
-
public BooleanProperty invalidSetupCodeProperty() {
return invalidSetupCode;
}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterFailedController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterFailedController.java
index 57150390c..7df27b06a 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterFailedController.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterFailedController.java
@@ -1,6 +1,5 @@
package org.cryptomator.ui.keyloading.hub;
-import com.nimbusds.jose.JWEObject;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.keyloading.KeyLoading;
@@ -22,8 +21,8 @@ public class RegisterFailedController implements FxController {
@FXML
public void close() {
+ result.cancel(true);
window.close();
}
-
}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterSuccessController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterSuccessController.java
index bba13516c..6988283a3 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterSuccessController.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterSuccessController.java
@@ -1,24 +1,43 @@
package org.cryptomator.ui.keyloading.hub;
+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 javax.inject.Inject;
import javafx.fxml.FXML;
+import javafx.scene.Scene;
import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
+import java.util.concurrent.CompletableFuture;
public class RegisterSuccessController implements FxController {
private final Stage window;
+ private final CompletableFuture result;
+ private final Lazy receiveKeyScene;
+ private final ReceiveKeyController receiveKeyController;
@Inject
- public RegisterSuccessController(@KeyLoading Stage window) {
+ public RegisterSuccessController(@KeyLoading Stage window, CompletableFuture result, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy receiveKeyScene, ReceiveKeyController receiveKeyController) {
this.window = window;
+ this.result = result;
+ this.receiveKeyScene = receiveKeyScene;
+ this.receiveKeyController = receiveKeyController;
+ this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
}
@FXML
- public void close() {
- window.close();
+ public void complete() {
+ window.setScene(receiveKeyScene.get());
+ receiveKeyController.receiveKey();
}
+ private void windowClosed(WindowEvent windowEvent) {
+ result.cancel(true);
+ }
+
+
}
diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java
index f90ad61c2..8212f598f 100644
--- a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java
+++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java
@@ -44,6 +44,11 @@ public class VaultDetailLockedController implements FxController {
appWindows.startUnlockWorkflow(vault.get(), mainWindow);
}
+ @FXML
+ public void share() {
+ appWindows.showShareVaultWindow(vault.get());
+ }
+
@FXML
public void showVaultOptions() {
vaultOptionsWindow.create(vault.get()).showVaultOptionsWindow(SelectedVaultOptionsTab.ANY);
diff --git a/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java
index 653c4c6e6..8cb49a679 100644
--- a/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java
+++ b/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java
@@ -2,17 +2,14 @@ package org.cryptomator.ui.preferences;
import dagger.Lazy;
import org.cryptomator.common.ObservableUtil;
-import org.cryptomator.common.mount.MountModule;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.integrations.mount.MountCapability;
import org.cryptomator.integrations.mount.MountService;
import org.cryptomator.ui.common.FxController;
import javax.inject.Inject;
-import javax.inject.Named;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
-import javafx.beans.binding.BooleanExpression;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.Button;
import javafx.scene.control.ChoiceBox;
@@ -21,24 +18,22 @@ import javafx.util.StringConverter;
import java.util.List;
import java.util.Optional;
import java.util.ResourceBundle;
-import java.util.concurrent.atomic.AtomicReference;
@PreferencesScoped
public class VolumePreferencesController implements FxController {
- private static final String DOCS_MOUNTING_URL = "https://docs.cryptomator.org/en/1.7/desktop/volume-type/";
- private static final int MIN_PORT = 1024;
- private static final int MAX_PORT = 65535;
+ public static final String DOCS_MOUNTING_URL = "https://docs.cryptomator.org/en/1.7/desktop/volume-type/";
+ public static final int MIN_PORT = 1024;
+ public static final int MAX_PORT = 65535;
private final Settings settings;
private final ObservableValue selectedMountService;
private final ResourceBundle resourceBundle;
- private final BooleanExpression loopbackPortSupported;
+ private final ObservableValue loopbackPortSupported;
private final ObservableValue mountToDirSupported;
private final ObservableValue mountToDriveLetterSupported;
private final ObservableValue mountFlagsSupported;
private final ObservableValue readonlySupported;
- private final ObservableValue fuseRestartRequired;
private final Lazy application;
private final List mountProviders;
public ChoiceBox volumeTypeChoiceBox;
@@ -46,7 +41,10 @@ public class VolumePreferencesController implements FxController {
public Button loopbackPortApplyButton;
@Inject
- VolumePreferencesController(Settings settings, Lazy application, List mountProviders, @Named("FUPFMS") AtomicReference firstUsedProblematicFuseMountService, ResourceBundle resourceBundle) {
+ VolumePreferencesController(Settings settings, //
+ Lazy application, //
+ List mountProviders, //
+ ResourceBundle resourceBundle) {
this.settings = settings;
this.application = application;
this.mountProviders = mountProviders;
@@ -54,17 +52,11 @@ public class VolumePreferencesController implements FxController {
var fallbackProvider = mountProviders.stream().findFirst().orElse(null);
this.selectedMountService = ObservableUtil.mapWithDefault(settings.mountService, serviceName -> mountProviders.stream().filter(s -> s.getClass().getName().equals(serviceName)).findFirst().orElse(fallbackProvider), fallbackProvider);
- this.loopbackPortSupported = BooleanExpression.booleanExpression(selectedMountService.map(s -> s.hasCapability(MountCapability.LOOPBACK_PORT)));
+ this.loopbackPortSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.LOOPBACK_PORT));
this.mountToDirSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_WITHIN_EXISTING_PARENT) || s.hasCapability(MountCapability.MOUNT_TO_EXISTING_DIR));
this.mountToDriveLetterSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_AS_DRIVE_LETTER));
this.mountFlagsSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_FLAGS));
this.readonlySupported = selectedMountService.map(s -> s.hasCapability(MountCapability.READ_ONLY));
- this.fuseRestartRequired = selectedMountService.map(s -> {//
- return firstUsedProblematicFuseMountService.get() != null //
- && MountModule.isProblematicFuseService(s) //
- && !firstUsedProblematicFuseMountService.get().equals(s);
- });
-
}
public void initialize() {
@@ -101,12 +93,12 @@ public class VolumePreferencesController implements FxController {
/* Property Getters */
- public BooleanExpression loopbackPortSupportedProperty() {
+ public ObservableValue loopbackPortSupportedProperty() {
return loopbackPortSupported;
}
public boolean isLoopbackPortSupported() {
- return loopbackPortSupported.get();
+ return loopbackPortSupported.getValue();
}
public ObservableValue readonlySupportedProperty() {
@@ -141,14 +133,6 @@ public class VolumePreferencesController implements FxController {
return mountFlagsSupported.getValue();
}
- public ObservableValue fuseRestartRequiredProperty() {
- return fuseRestartRequired;
- }
-
- public boolean getFuseRestartRequired() {
- return fuseRestartRequired.getValue();
- }
-
/* Helpers */
private class MountServiceConverter extends StringConverter {
diff --git a/src/main/java/org/cryptomator/ui/sharevault/ShareVaultComponent.java b/src/main/java/org/cryptomator/ui/sharevault/ShareVaultComponent.java
new file mode 100644
index 000000000..b7b41ec7e
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/sharevault/ShareVaultComponent.java
@@ -0,0 +1,34 @@
+package org.cryptomator.ui.sharevault;
+
+import dagger.BindsInstance;
+import dagger.Lazy;
+import dagger.Subcomponent;
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.ui.common.FxmlFile;
+import org.cryptomator.ui.common.FxmlScene;
+
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+
+@ShareVaultScoped
+@Subcomponent(modules = {ShareVaultModule.class})
+public interface ShareVaultComponent {
+
+ @ShareVaultWindow
+ Stage window();
+
+ @FxmlScene(FxmlFile.SHARE_VAULT)
+ Lazy scene();
+
+ default void showShareVaultWindow(){
+ Stage stage = window();
+ stage.setScene(scene().get());
+ stage.show();
+ }
+
+ @Subcomponent.Factory
+ interface Factory {
+ ShareVaultComponent create(@BindsInstance @ShareVaultWindow Vault vault);
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/ui/sharevault/ShareVaultController.java b/src/main/java/org/cryptomator/ui/sharevault/ShareVaultController.java
new file mode 100644
index 000000000..63230cbbf
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/sharevault/ShareVaultController.java
@@ -0,0 +1,76 @@
+package org.cryptomator.ui.sharevault;
+
+import dagger.Lazy;
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.keyloading.hub.HubKeyLoadingStrategy;
+
+import javax.inject.Inject;
+import javafx.application.Application;
+import javafx.fxml.FXML;
+import javafx.stage.Stage;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+@ShareVaultScoped
+public class ShareVaultController implements FxController {
+
+ private static final String SCHEME_PREFIX = "hub+";
+ private static final String VISIT_HUB_URL = "https://cryptomator.org/hub/";
+ private static final String BEST_PRACTICES_URL = "https://docs.cryptomator.org/en/latest/security/best-practices/#sharing-of-vaults";
+
+ private final Stage window;
+ private final Lazy application;
+ private final Vault vault;
+ private final Boolean hubVault;
+
+ @Inject
+ ShareVaultController(@ShareVaultWindow Stage window, //
+ Lazy application, //
+ @ShareVaultWindow Vault vault) {
+ this.window = window;
+ this.application = application;
+ this.vault = vault;
+ var vaultScheme = vault.getVaultConfigCache().getUnchecked().getKeyId().getScheme();
+ this.hubVault = (vaultScheme.equals(HubKeyLoadingStrategy.SCHEME_HUB_HTTP) || vaultScheme.equals(HubKeyLoadingStrategy.SCHEME_HUB_HTTPS));
+ }
+
+ @FXML
+ public void close() {
+ window.close();
+ }
+
+ @FXML
+ public void visitHub() {
+ application.get().getHostServices().showDocument(VISIT_HUB_URL);
+ }
+
+ @FXML
+ public void openHub() {
+ application.get().getHostServices().showDocument(getHubUri(vault).toString());
+ }
+
+ @FXML
+ public void visitBestPractices() {
+ application.get().getHostServices().showDocument(BEST_PRACTICES_URL);
+ }
+
+ private static URI getHubUri(Vault vault) {
+ try {
+ var keyID = new URI(vault.getVaultConfigCache().get().getKeyId().toString());
+ assert keyID.getScheme().startsWith(SCHEME_PREFIX);
+ return new URI(keyID.getScheme().substring(SCHEME_PREFIX.length()) + "://" + keyID.getHost() + "/app/vaults");
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ } catch (URISyntaxException e) {
+ throw new IllegalStateException("URI constructed from params known to be valid", e);
+ }
+ }
+
+ public boolean isHubVault() {
+ return hubVault;
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/ui/sharevault/ShareVaultModule.java b/src/main/java/org/cryptomator/ui/sharevault/ShareVaultModule.java
new file mode 100644
index 000000000..75742c7ce
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/sharevault/ShareVaultModule.java
@@ -0,0 +1,54 @@
+package org.cryptomator.ui.sharevault;
+
+import dagger.Binds;
+import dagger.Module;
+import dagger.Provides;
+import dagger.multibindings.IntoMap;
+import org.cryptomator.ui.common.DefaultSceneFactory;
+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.StageFactory;
+
+import javax.inject.Provider;
+import javafx.scene.Scene;
+import javafx.stage.Modality;
+import javafx.stage.Stage;
+import java.util.Map;
+import java.util.ResourceBundle;
+
+@Module
+abstract class ShareVaultModule {
+
+ @Provides
+ @ShareVaultWindow
+ @ShareVaultScoped
+ static FxmlLoaderFactory provideFxmlLoaderFactory(Map, Provider> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
+ return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
+ }
+
+ @Provides
+ @ShareVaultWindow
+ @ShareVaultScoped
+ static Stage provideStage(StageFactory factory, ResourceBundle resourceBundle) {
+ Stage stage = factory.create();
+ stage.setResizable(false);
+ stage.initModality(Modality.APPLICATION_MODAL);
+ stage.setTitle(resourceBundle.getString("shareVault.title"));
+ return stage;
+ }
+
+ @Provides
+ @FxmlScene(FxmlFile.SHARE_VAULT)
+ @ShareVaultScoped
+ static Scene provideShareVaultScene(@ShareVaultWindow FxmlLoaderFactory fxmlLoaders) {
+ return fxmlLoaders.createScene(FxmlFile.SHARE_VAULT);
+ }
+
+ @Binds
+ @IntoMap
+ @FxControllerKey(ShareVaultController.class)
+ abstract FxController bindShareVaultController(ShareVaultController controller);
+}
diff --git a/src/main/java/org/cryptomator/ui/sharevault/ShareVaultScoped.java b/src/main/java/org/cryptomator/ui/sharevault/ShareVaultScoped.java
new file mode 100644
index 000000000..2a9b97fb1
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/sharevault/ShareVaultScoped.java
@@ -0,0 +1,13 @@
+package org.cryptomator.ui.sharevault;
+
+import javax.inject.Scope;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Scope
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@interface ShareVaultScoped {
+
+}
diff --git a/src/main/java/org/cryptomator/ui/sharevault/ShareVaultWindow.java b/src/main/java/org/cryptomator/ui/sharevault/ShareVaultWindow.java
new file mode 100644
index 000000000..ace70180a
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/sharevault/ShareVaultWindow.java
@@ -0,0 +1,14 @@
+package org.cryptomator.ui.sharevault;
+
+import javax.inject.Qualifier;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+@Qualifier
+@Documented
+@Retention(RUNTIME)
+@interface ShareVaultWindow {
+
+}
diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockComponent.java b/src/main/java/org/cryptomator/ui/unlock/UnlockComponent.java
index 67e905200..825d0fc2d 100644
--- a/src/main/java/org/cryptomator/ui/unlock/UnlockComponent.java
+++ b/src/main/java/org/cryptomator/ui/unlock/UnlockComponent.java
@@ -19,16 +19,8 @@ import java.util.concurrent.Future;
@Subcomponent(modules = {UnlockModule.class})
public interface UnlockComponent {
- ExecutorService defaultExecutorService();
-
UnlockWorkflow unlockWorkflow();
- default Future startUnlockWorkflow() {
- UnlockWorkflow workflow = unlockWorkflow();
- defaultExecutorService().submit(workflow);
- return workflow;
- }
-
@Subcomponent.Factory
interface Factory {
UnlockComponent create(@BindsInstance @UnlockWindow Vault vault, @BindsInstance @Named("unlockWindowOwner") @Nullable Stage owner);
diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java b/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java
index f93999d21..95f13d383 100644
--- a/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java
+++ b/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java
@@ -81,6 +81,13 @@ abstract class UnlockModule {
return fxmlLoaders.createScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT);
}
+ @Provides
+ @FxmlScene(FxmlFile.UNLOCK_REQUIRES_RESTART)
+ @UnlockScoped
+ static Scene provideRestartRequiredScene(@UnlockWindow FxmlLoaderFactory fxmlLoaders) {
+ return fxmlLoaders.createScene(FxmlFile.UNLOCK_REQUIRES_RESTART);
+ }
+
// ------------------
@Binds
@@ -93,4 +100,9 @@ abstract class UnlockModule {
@FxControllerKey(UnlockInvalidMountPointController.class)
abstract FxController bindUnlockInvalidMountPointController(UnlockInvalidMountPointController controller);
+ @Binds
+ @IntoMap
+ @FxControllerKey(UnlockRequiresRestartController.class)
+ abstract FxController bindUnlockRequiresRestartController(UnlockRequiresRestartController controller);
+
}
diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockRequiresRestartController.java b/src/main/java/org/cryptomator/ui/unlock/UnlockRequiresRestartController.java
new file mode 100644
index 000000000..497194dff
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/unlock/UnlockRequiresRestartController.java
@@ -0,0 +1,47 @@
+package org.cryptomator.ui.unlock;
+
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.fxapp.FxApplicationWindows;
+import org.cryptomator.ui.vaultoptions.SelectedVaultOptionsTab;
+
+import javax.inject.Inject;
+import javafx.fxml.FXML;
+import javafx.stage.Stage;
+import java.util.ResourceBundle;
+
+@UnlockScoped
+public class UnlockRequiresRestartController implements FxController {
+
+ private final Stage window;
+ private final ResourceBundle resourceBundle;
+ private final FxApplicationWindows appWindows;
+ private final Vault vault;
+
+ @Inject
+ UnlockRequiresRestartController(@UnlockWindow Stage window, //
+ ResourceBundle resourceBundle, //
+ FxApplicationWindows appWindows, //
+ @UnlockWindow Vault vault) {
+ this.window = window;
+ this.resourceBundle = resourceBundle;
+ this.appWindows = appWindows;
+ this.vault = vault;
+ }
+
+ public void initialize() {
+ window.setTitle(String.format(resourceBundle.getString("unlock.error.title"), vault.getDisplayName()));
+ }
+
+ @FXML
+ public void close() {
+ window.close();
+ }
+
+ @FXML
+ public void closeAndOpenVaultOptions() {
+ appWindows.showVaultOptionsWindow(vault, SelectedVaultOptionsTab.MOUNT);
+ window.close();
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java b/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java
index 564d57ab6..98a49dec5 100644
--- a/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java
+++ b/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java
@@ -1,7 +1,7 @@
package org.cryptomator.ui.unlock;
-import com.google.common.base.Throwables;
import dagger.Lazy;
+import org.cryptomator.common.mount.ConflictingMountServiceException;
import org.cryptomator.common.mount.IllegalMountPointException;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
@@ -29,7 +29,7 @@ import java.io.IOException;
* This class runs the unlock process and controls when to display which UI.
*/
@UnlockScoped
-public class UnlockWorkflow extends Task {
+public class UnlockWorkflow extends Task {
private static final Logger LOG = LoggerFactory.getLogger(UnlockWorkflow.class);
@@ -38,42 +38,44 @@ public class UnlockWorkflow extends Task {
private final VaultService vaultService;
private final Lazy successScene;
private final Lazy invalidMountPointScene;
+ private final Lazy restartRequiredScene;
private final FxApplicationWindows appWindows;
private final KeyLoadingStrategy keyLoadingStrategy;
private final ObjectProperty illegalMountPointException;
@Inject
- UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, FxApplicationWindows appWindows, @UnlockWindow KeyLoadingStrategy keyLoadingStrategy, @UnlockWindow ObjectProperty illegalMountPointException) {
+ UnlockWorkflow(@UnlockWindow Stage window, //
+ @UnlockWindow Vault vault, //
+ VaultService vaultService, //
+ @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, //
+ @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, //
+ @FxmlScene(FxmlFile.UNLOCK_REQUIRES_RESTART) Lazy restartRequiredScene, //
+ FxApplicationWindows appWindows, //
+ @UnlockWindow KeyLoadingStrategy keyLoadingStrategy, //
+ @UnlockWindow ObjectProperty illegalMountPointException) {
this.window = window;
this.vault = vault;
this.vaultService = vaultService;
this.successScene = successScene;
this.invalidMountPointScene = invalidMountPointScene;
+ this.restartRequiredScene = restartRequiredScene;
this.appWindows = appWindows;
this.keyLoadingStrategy = keyLoadingStrategy;
this.illegalMountPointException = illegalMountPointException;
}
@Override
- protected Boolean call() throws InterruptedException, IOException, CryptoException, MountFailedException {
- try {
- attemptUnlock();
- return true;
- } catch (UnlockCancelledException e) {
- cancel(false); // set Tasks state to cancelled
- return false;
- }
- }
-
- private void attemptUnlock() throws IOException, CryptoException, MountFailedException {
+ protected Void call() throws InterruptedException, IOException, CryptoException, MountFailedException {
try {
keyLoadingStrategy.use(vault::unlock);
+ return null;
+ } catch (UnlockCancelledException e) {
+ cancel(false); // set Tasks state to cancelled
+ return null;
+ } catch (IOException | RuntimeException | MountFailedException e) {
+ throw e;
} catch (Exception e) {
- Throwables.propagateIfPossible(e, IOException.class);
- Throwables.propagateIfPossible(e, CryptoException.class);
- Throwables.propagateIfPossible(e, IllegalMountPointException.class);
- Throwables.propagateIfPossible(e, MountFailedException.class);
- throw new IllegalStateException("unexpected exception type", e);
+ throw new IllegalStateException("Unexpected exception type", e);
}
}
@@ -85,6 +87,13 @@ public class UnlockWorkflow extends Task {
});
}
+ private void handleConflictingMountServiceException() {
+ Platform.runLater(() -> {
+ window.setScene(restartRequiredScene.get());
+ window.show();
+ });
+ }
+
private void handleGenericError(Throwable e) {
LOG.error("Unlock failed for technical reasons.", e);
appWindows.showErrorWindow(e, window, null);
@@ -113,10 +122,10 @@ public class UnlockWorkflow extends Task {
protected void failed() {
LOG.info("Unlock of '{}' failed.", vault.getDisplayName());
Throwable throwable = super.getException();
- if(throwable instanceof IllegalMountPointException impe) {
- handleIllegalMountPointError(impe);
- } else {
- handleGenericError(throwable);
+ switch (throwable) {
+ case IllegalMountPointException e -> handleIllegalMountPointError(e);
+ case ConflictingMountServiceException _ -> handleConflictingMountServiceException();
+ default -> handleGenericError(throwable);
}
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
}
diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java
index 5eeab43e0..106623985 100644
--- a/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java
+++ b/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java
@@ -1,18 +1,25 @@
package org.cryptomator.ui.vaultoptions;
import com.google.common.base.Strings;
-import org.cryptomator.common.mount.ActualMountService;
+import dagger.Lazy;
+import org.cryptomator.common.ObservableUtil;
+import org.cryptomator.common.mount.Mounter;
import org.cryptomator.common.mount.WindowsDriveLetters;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.integrations.mount.MountCapability;
+import org.cryptomator.integrations.mount.MountService;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
+import org.cryptomator.ui.preferences.VolumePreferencesController;
import javax.inject.Inject;
+import javafx.application.Application;
+import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
+import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.RadioButton;
@@ -26,6 +33,8 @@ import java.io.File;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
+import java.util.List;
+import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Set;
@@ -36,14 +45,21 @@ public class MountOptionsController implements FxController {
private final VaultSettings vaultSettings;
private final WindowsDriveLetters windowsDriveLetters;
private final ResourceBundle resourceBundle;
+ private final Lazy application;
private final ObservableValue defaultMountFlags;
private final ObservableValue mountpointDirSupported;
private final ObservableValue mountpointDriveLetterSupported;
private final ObservableValue readOnlySupported;
private final ObservableValue mountFlagsSupported;
+ private final ObservableValue defaultMountServiceSelected;
private final ObservableValue directoryPath;
private final FxApplicationWindows applicationWindows;
+ private final List mountProviders;
+ private final ObservableValue defaultMountService;
+ private final ObservableValue selectedMountService;
+ private final ObservableValue selectedMountServiceRequiresRestart;
+ private final ObservableValue loopbackPortChangeable;
//-- FXML objects --
@@ -56,30 +72,58 @@ public class MountOptionsController implements FxController {
public RadioButton mountPointDirBtn;
public TextField directoryPathField;
public ChoiceBox driveLetterSelection;
+ public ChoiceBox vaultVolumeTypeChoiceBox;
+ public TextField vaultLoopbackPortField;
+ public Button vaultLoopbackPortApplyButton;
+
@Inject
- MountOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, ObservableValue mountService, WindowsDriveLetters windowsDriveLetters, ResourceBundle resourceBundle, FxApplicationWindows applicationWindows) {
+ MountOptionsController(@VaultOptionsWindow Stage window, //
+ @VaultOptionsWindow Vault vault, //
+ WindowsDriveLetters windowsDriveLetters, //
+ ResourceBundle resourceBundle, //
+ FxApplicationWindows applicationWindows, //
+ Lazy application, //
+ List mountProviders, //
+ Mounter mounter, //
+ ObservableValue defaultMountService) {
this.window = window;
this.vaultSettings = vault.getVaultSettings();
this.windowsDriveLetters = windowsDriveLetters;
this.resourceBundle = resourceBundle;
- this.defaultMountFlags = mountService.map(as -> {
- if (as.service().hasCapability(MountCapability.MOUNT_FLAGS)) {
- return as.service().getDefaultMountFlags();
+ this.applicationWindows = applicationWindows;
+ this.directoryPath = vault.getVaultSettings().mountPoint.map(p -> isDriveLetter(p) ? null : p.toString());
+ this.application = application;
+ this.mountProviders = mountProviders;
+ this.defaultMountService = defaultMountService;
+ this.selectedMountService = Bindings.createObjectBinding(this::reselectMountService, defaultMountService, vaultSettings.mountService);
+ this.selectedMountServiceRequiresRestart = selectedMountService.map(mounter::isConflictingMountService);
+
+ this.defaultMountFlags = selectedMountService.map(s -> {
+ if (s.hasCapability(MountCapability.MOUNT_FLAGS)) {
+ return s.getDefaultMountFlags();
} else {
return "";
}
});
- this.mountpointDirSupported = mountService.map(as -> as.service().hasCapability(MountCapability.MOUNT_TO_EXISTING_DIR) || as.service().hasCapability(MountCapability.MOUNT_WITHIN_EXISTING_PARENT));
- this.mountpointDriveLetterSupported = mountService.map(as -> as.service().hasCapability(MountCapability.MOUNT_AS_DRIVE_LETTER));
- this.mountFlagsSupported = mountService.map(as -> as.service().hasCapability(MountCapability.MOUNT_FLAGS));
- this.readOnlySupported = mountService.map(as -> as.service().hasCapability(MountCapability.READ_ONLY));
- this.directoryPath = vault.getVaultSettings().mountPoint.map(p -> isDriveLetter(p) ? null : p.toString());
- this.applicationWindows = applicationWindows;
+ this.mountFlagsSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_FLAGS));
+ this.defaultMountServiceSelected = ObservableUtil.mapWithDefault(vaultSettings.mountService, _ -> false, true);
+ this.readOnlySupported = selectedMountService.map(s -> s.hasCapability(MountCapability.READ_ONLY));
+ this.mountpointDirSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_TO_EXISTING_DIR) || s.hasCapability(MountCapability.MOUNT_WITHIN_EXISTING_PARENT));
+ this.mountpointDriveLetterSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_AS_DRIVE_LETTER));
+ this.loopbackPortChangeable = selectedMountService.map(s -> s.hasCapability(MountCapability.LOOPBACK_PORT) && vaultSettings.mountService.getValue() != null);
+ }
+
+ private MountService reselectMountService() {
+ var desired = vaultSettings.mountService.getValue();
+ var defaultMS = defaultMountService.getValue();
+ return mountProviders.stream().filter(s -> s.getClass().getName().equals(desired)).findFirst().orElse(defaultMS);
}
@FXML
public void initialize() {
+ defaultMountService.addListener((_, _, _) -> vaultVolumeTypeChoiceBox.setConverter(new MountServiceConverter()));
+
// readonly:
readOnlyCheckbox.selectedProperty().bindBidirectional(vaultSettings.usesReadOnlyMode);
@@ -106,6 +150,20 @@ public class MountOptionsController implements FxController {
mountPointToggleGroup.selectToggle(mountPointDirBtn);
}
mountPointToggleGroup.selectedToggleProperty().addListener(this::selectedToggleChanged);
+
+ vaultVolumeTypeChoiceBox.getItems().add(null);
+ vaultVolumeTypeChoiceBox.getItems().addAll(mountProviders);
+ vaultVolumeTypeChoiceBox.setConverter(new MountServiceConverter());
+ vaultVolumeTypeChoiceBox.getSelectionModel().select(isDefaultMountServiceSelected() ? null : selectedMountService.getValue());
+ vaultVolumeTypeChoiceBox.valueProperty().addListener((_, _, newProvider) -> {
+ var toSet = Optional.ofNullable(newProvider).map(nP -> nP.getClass().getName()).orElse(null);
+ vaultSettings.mountService.set(toSet);
+ });
+
+ vaultLoopbackPortField.setText(String.valueOf(vaultSettings.port.get()));
+ vaultLoopbackPortApplyButton.visibleProperty().bind(vaultSettings.port.asString().isNotEqualTo(vaultLoopbackPortField.textProperty()));
+ vaultLoopbackPortApplyButton.disableProperty().bind(Bindings.createBooleanBinding(this::validateLoopbackPort, vaultLoopbackPortField.textProperty()).not());
+
}
@FXML
@@ -229,6 +287,26 @@ public class MountOptionsController implements FxController {
}
+ public void openDocs() {
+ application.get().getHostServices().showDocument(VolumePreferencesController.DOCS_MOUNTING_URL);
+ }
+
+ private boolean validateLoopbackPort() {
+ try {
+ int port = Integer.parseInt(vaultLoopbackPortField.getText());
+ return port == 0 // choose port automatically
+ || port >= VolumePreferencesController.MIN_PORT && port <= VolumePreferencesController.MAX_PORT; // port within range
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
+ public void doChangeLoopbackPort() {
+ if (validateLoopbackPort()) {
+ vaultSettings.port.set(Integer.parseInt(vaultLoopbackPortField.getText()));
+ }
+ }
+
//@formatter:off
private static class NoDirSelectedException extends Exception {}
//@formatter:on
@@ -243,6 +321,14 @@ public class MountOptionsController implements FxController {
return mountFlagsSupported.getValue();
}
+ public ObservableValue defaultMountServiceSelectedProperty() {
+ return defaultMountServiceSelected;
+ }
+
+ public boolean isDefaultMountServiceSelected() {
+ return defaultMountServiceSelected.getValue();
+ }
+
public ObservableValue mountpointDirSupportedProperty() {
return mountpointDirSupported;
}
@@ -274,4 +360,37 @@ public class MountOptionsController implements FxController {
public String getDirectoryPath() {
return directoryPath.getValue();
}
+
+ public ObservableValue selectedMountServiceRequiresRestartProperty() {
+ return selectedMountServiceRequiresRestart;
+ }
+
+ public boolean getSelectedMountServiceRequiresRestart() {
+ return selectedMountServiceRequiresRestart.getValue();
+ }
+
+ public ObservableValue loopbackPortChangeableProperty() {
+ return loopbackPortChangeable;
+ }
+
+ public boolean isLoopbackPortChangeable() {
+ return loopbackPortChangeable.getValue();
+ }
+
+ private class MountServiceConverter extends StringConverter {
+
+ @Override
+ public String toString(MountService provider) {
+ if (provider == null) {
+ return String.format(resourceBundle.getString("vaultOptions.mount.volumeType.default"), defaultMountService.getValue().displayName());
+ } else {
+ return provider.displayName();
+ }
+ }
+
+ @Override
+ public MountService fromString(String string) {
+ throw new UnsupportedOperationException();
+ }
+ }
}
diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsController.java
index 3abc23e9e..78d228995 100644
--- a/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsController.java
+++ b/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsController.java
@@ -1,6 +1,7 @@
package org.cryptomator.ui.vaultoptions;
import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.keyloading.hub.HubKeyLoadingStrategy;
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingStrategy;
@@ -48,6 +49,10 @@ public class VaultOptionsController implements FxController {
if(!(vaultScheme.equals(HubKeyLoadingStrategy.SCHEME_HUB_HTTP) || vaultScheme.equals(HubKeyLoadingStrategy.SCHEME_HUB_HTTPS))){
tabPane.getTabs().remove(hubTab);
}
+
+ vault.stateProperty().addListener(observable -> {
+ tabPane.setDisable(vault.getState().equals(VaultState.Value.UNLOCKED));
+ });
}
private void selectChosenTab() {
diff --git a/src/main/resources/css/dark_theme.css b/src/main/resources/css/dark_theme.css
index beb50f6bc..86d9aaa71 100644
--- a/src/main/resources/css/dark_theme.css
+++ b/src/main/resources/css/dark_theme.css
@@ -936,3 +936,16 @@
-fx-padding: 0.5px;
-fx-background-color: CONTROL_BORDER_NORMAL;
}
+
+/*******************************************************************************
+ * *
+ * Ad box *
+ * *
+ ******************************************************************************/
+
+.ad-box {
+ -fx-padding: 12px;
+ -fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL;
+ -fx-background-insets: 0, 1px;
+ -fx-background-radius: 4px;
+}
\ No newline at end of file
diff --git a/src/main/resources/css/light_theme.css b/src/main/resources/css/light_theme.css
index a494269b7..88cf6d970 100644
--- a/src/main/resources/css/light_theme.css
+++ b/src/main/resources/css/light_theme.css
@@ -935,3 +935,16 @@
-fx-padding: 0.5px;
-fx-background-color: CONTROL_BORDER_NORMAL;
}
+
+/*******************************************************************************
+ * *
+ * Ad box *
+ * *
+ ******************************************************************************/
+
+.ad-box {
+ -fx-padding: 12px;
+ -fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL;
+ -fx-background-insets: 0, 1px;
+ -fx-background-radius: 4px;
+}
\ No newline at end of file
diff --git a/src/main/resources/fxml/hub_legacy_register_device.fxml b/src/main/resources/fxml/hub_legacy_register_device.fxml
index 51d4cf8b7..1bdd475a7 100644
--- a/src/main/resources/fxml/hub_legacy_register_device.fxml
+++ b/src/main/resources/fxml/hub_legacy_register_device.fxml
@@ -41,7 +41,7 @@
-
+
@@ -50,7 +50,7 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/src/main/resources/fxml/hub_register_device_already_exists.fxml b/src/main/resources/fxml/hub_register_device_already_exists.fxml
new file mode 100644
index 000000000..9a7c1f3c7
--- /dev/null
+++ b/src/main/resources/fxml/hub_register_device_already_exists.fxml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/fxml/hub_register_failed.fxml b/src/main/resources/fxml/hub_register_failed.fxml
index 7ebf59279..a088c0318 100644
--- a/src/main/resources/fxml/hub_register_failed.fxml
+++ b/src/main/resources/fxml/hub_register_failed.fxml
@@ -38,7 +38,7 @@
-
+
diff --git a/src/main/resources/fxml/hub_register_success.fxml b/src/main/resources/fxml/hub_register_success.fxml
index 822a4489e..0211eeba8 100644
--- a/src/main/resources/fxml/hub_register_success.fxml
+++ b/src/main/resources/fxml/hub_register_success.fxml
@@ -41,9 +41,9 @@
-
+
-
+
diff --git a/src/main/resources/fxml/preferences_about.fxml b/src/main/resources/fxml/preferences_about.fxml
index d403319fd..1555de504 100644
--- a/src/main/resources/fxml/preferences_about.fxml
+++ b/src/main/resources/fxml/preferences_about.fxml
@@ -22,7 +22,7 @@
-
+
diff --git a/src/main/resources/fxml/preferences_volume.fxml b/src/main/resources/fxml/preferences_volume.fxml
index f48b1c1c8..c173f03e2 100644
--- a/src/main/resources/fxml/preferences_volume.fxml
+++ b/src/main/resources/fxml/preferences_volume.fxml
@@ -32,8 +32,6 @@
-
-
diff --git a/src/main/resources/fxml/share_vault.fxml b/src/main/resources/fxml/share_vault.fxml
new file mode 100644
index 000000000..fc74079d2
--- /dev/null
+++ b/src/main/resources/fxml/share_vault.fxml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/fxml/unlock_requires_restart.fxml b/src/main/resources/fxml/unlock_requires_restart.fxml
new file mode 100644
index 000000000..571ea6a32
--- /dev/null
+++ b/src/main/resources/fxml/unlock_requires_restart.fxml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/fxml/vault_detail_locked.fxml b/src/main/resources/fxml/vault_detail_locked.fxml
index d4bbc7e4a..a268811ba 100644
--- a/src/main/resources/fxml/vault_detail_locked.fxml
+++ b/src/main/resources/fxml/vault_detail_locked.fxml
@@ -24,14 +24,17 @@
-
+
-
+
-