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 @@ -