diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 6c30aa606..9ced33c15 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -43,6 +43,7 @@ body: - WinFsp (Local Drive) - FUSE-T - macFUSE + - FUSE - WebDAV (Windows Explorer) - WebDAV (AppleScript) - WebDAV (gio) @@ -95,4 +96,4 @@ body: id: further-info attributes: label: Anything else? - description: Links? References? Screenshots? Configurations? Any data that might be necessary to reproduce the issue? \ No newline at end of file + description: Links? References? Screenshots? Configurations? Any data that might be necessary to reproduce the issue? diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index fbbc879b6..15c1ea3a9 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -10,8 +10,8 @@ on: required: false env: - JAVA_DIST: 'temurin' - JAVA_VERSION: 20 + JAVA_DIST: 'zulu' + JAVA_VERSION: 21 jobs: get-version: @@ -36,7 +36,7 @@ jobs: openjfx-url: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_linux-aarch64_bin-jmods.zip' openjfx-sha: 'c0d80ebbe0aab404ef9ad8b46c05bf533a1e40b39b2720eebd9238d81f6326ca' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Java uses: actions/setup-java@v3 with: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 13acee970..fbb57cbbf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,8 +6,8 @@ on: types: [labeled] env: - JAVA_DIST: 'temurin' - JAVA_VERSION: 20 + JAVA_DIST: 'zulu' + JAVA_VERSION: 21 defaults: run: @@ -18,7 +18,7 @@ jobs: name: Compile and Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-java@v3 with: distribution: ${{ env.JAVA_DIST }} diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml index 14cdca80a..054d6cadf 100644 --- a/.github/workflows/debian.yml +++ b/.github/workflows/debian.yml @@ -16,8 +16,9 @@ on: type: boolean env: - JAVA_DIST: 'temurin' - JAVA_VERSION: 20 + JAVA_DIST: 'zulu' + JAVA_VERSION: 21 + COFFEELIBS_JDK_VERSION: 21 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' @@ -28,7 +29,7 @@ jobs: name: Build Debian Package runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - id: versions name: Get version information run: | @@ -42,7 +43,7 @@ jobs: run: | sudo add-apt-repository ppa:coffeelibs/openjdk sudo apt-get update - sudo apt-get install debhelper devscripts dput coffeelibs-jdk-${{ env.JAVA_VERSION }} libgtk2.0-0 + sudo apt-get install debhelper devscripts dput coffeelibs-jdk-${{ env.COFFEELIBS_JDK_VERSION }} libgtk2.0-0 - name: Setup Java uses: actions/setup-java@v3 with: @@ -97,7 +98,8 @@ jobs: run: | cp -r dist/linux/debian/ pkgdir export RFC2822_TIMESTAMP=`date --rfc-2822` - envsubst '${SEMVER_STR} ${VERSION_NUM} ${REVISION_NUM}' < dist/linux/debian/rules > pkgdir/debian/rules + export DISABLE_UPDATE_CHECK=${{ inputs.dput }} + envsubst '${SEMVER_STR} ${VERSION_NUM} ${REVISION_NUM} ${DISABLE_UPDATE_CHECK}' < dist/linux/debian/rules > pkgdir/debian/rules envsubst '${PPA_VERSION} ${RFC2822_TIMESTAMP}' < dist/linux/debian/changelog > pkgdir/debian/changelog find . -name "*.jar" >> pkgdir/debian/source/include-binaries mv pkgdir cryptomator_${{ inputs.ppaver }} diff --git a/.github/workflows/error-db.yml b/.github/workflows/error-db.yml index 09a15fe1f..e885af4a2 100644 --- a/.github/workflows/error-db.yml +++ b/.github/workflows/error-db.yml @@ -2,7 +2,7 @@ name: Update Error Database on: discussion: - types: [created, edited, category_changed, answered, unanswered] + types: [created, edited, deleted, category_changed, answered, unanswered] discussion_comment: types: [created, edited, deleted] @@ -12,6 +12,7 @@ jobs: if: github.event.discussion.category.name == 'Errors' steps: - 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 with: @@ -47,8 +48,13 @@ jobs: - name: Merge Error Code Data run: | jq -c '.' ${{ steps.get-gist.outputs.file }} > original.json - echo $DISCUSSION | jq -c '.repository.discussion | .comments = .comments.totalCount | {(.id|tostring) : .}' > new.json - jq -s '.[0] * .[1]' original.json new.json > merged.json + if [ ! -z "$DISCUSSION" ] + then + echo $DISCUSSION | jq -c '.repository.discussion | .comments = .comments.totalCount | {(.id|tostring) : .}' > new.json + jq -s '.[0] * .[1]' original.json new.json > merged.json + else + cat original.json | jq 'del(.[] | select(.url=="https://github.com/cryptomator/cryptomator/discussions/${{ github.event.discussion.number }}"))' > merged.json + fi env: DISCUSSION: ${{ steps.query-data.outputs.result }} - name: Patch Gist diff --git a/.github/workflows/get-version.yml b/.github/workflows/get-version.yml index 44f5ccd85..1bed1cff8 100644 --- a/.github/workflows/get-version.yml +++ b/.github/workflows/get-version.yml @@ -22,8 +22,8 @@ on: value: ${{ jobs.determine-version.outputs.type }} env: - JAVA_DIST: 'temurin' - JAVA_VERSION: 20 + JAVA_DIST: 'zulu' + JAVA_VERSION: 21 jobs: determine-version: @@ -35,7 +35,7 @@ jobs: revNum: ${{ steps.versions.outputs.revNum }} type: ${{ steps.versions.outputs.type}} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Java diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml index a394101ff..8db4296d9 100644 --- a/.github/workflows/mac-dmg.yml +++ b/.github/workflows/mac-dmg.yml @@ -15,8 +15,8 @@ on: type: boolean env: - JAVA_DIST: 'temurin' - JAVA_VERSION: 20 + JAVA_DIST: 'zulu' + JAVA_VERSION: 21 jobs: get-version: @@ -47,7 +47,7 @@ jobs: openjfx-url: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_osx-aarch64_bin-jmods.zip' openjfx-sha: 'c60f5f19aa847e0e620e0b011e5de68f2c6755641c2141cec27a0b89f612beaf' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Java uses: actions/setup-java@v3 with: @@ -62,7 +62,7 @@ jobs: curl -L ${{ matrix.openjfx-url }} -o openjfx-jmods.zip echo "${{ matrix.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 + unzip -jo openjfx-jmods.zip \*/javafx.base.jmod \*/javafx.controls.jmod \*/javafx.fxml.jmod \*/javafx.graphics.jmod -d openjfx-jmods - name: Ensure major jfx version in pom and in jmods is the same run: | JMOD_VERSION=$(jmod describe openjfx-jmods/javafx.base.jmod | head -1) @@ -72,7 +72,7 @@ jobs: POM_JFX_VERSION=${POM_JFX_VERSION#*@} POM_JFX_VERSION=${POM_JFX_VERSION%%.*} - if [ $POM_JFX_VERSION -ne $JMOD_VERSION ]; then + if [ "${POM_JFX_VERSION}" -ne "${JMOD_VERSION}" ]; then >&2 echo "Major JavaFX version in pom.xml (${POM_JFX_VERSION}) != jmod version (${JMOD_VERSION})" exit 1 fi @@ -222,7 +222,6 @@ jobs: --app-drop-link 512 245 --eula "dist/mac/dmg/resources/license.rtf" --icon ".background" 128 758 - --icon ".fseventsd" 320 758 --icon ".VolumeIcon.icns" 512 758 Cryptomator-${VERSION_NO}-${{ matrix.output-suffix }}.dmg dmg env: diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index 14146d0cb..931817418 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -4,8 +4,8 @@ on: pull_request: env: - JAVA_DIST: 'temurin' - JAVA_VERSION: 20 + JAVA_DIST: 'zulu' + JAVA_VERSION: 21 defaults: run: @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-java@v3 with: distribution: ${{ env.JAVA_DIST }} diff --git a/.github/workflows/release-check.yml b/.github/workflows/release-check.yml index ec532081b..1bbfb5d1a 100644 --- a/.github/workflows/release-check.yml +++ b/.github/workflows/release-check.yml @@ -15,7 +15,7 @@ jobs: name: Validate commits pushed to release/hotfix branch to fulfill release requirements runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - id: validate-pom-version name: Validate POM version run: | diff --git a/.github/workflows/win-exe.yml b/.github/workflows/win-exe.yml index 066b7d49e..2b72a50a8 100644 --- a/.github/workflows/win-exe.yml +++ b/.github/workflows/win-exe.yml @@ -14,8 +14,8 @@ on: env: - JAVA_DIST: 'temurin' - JAVA_VERSION: 20 + JAVA_DIST: 'zulu' + JAVA_VERSION: 21 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' @@ -37,7 +37,7 @@ jobs: LOOPBACK_ALIAS: 'cryptomator-vault' WIN_CONSOLE_FLAG: '' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Java uses: actions/setup-java@v3 with: @@ -143,9 +143,29 @@ jobs: - name: Fix permissions run: attrib -r appdir/Cryptomator/Cryptomator.exe shell: pwsh - - name: Extract integrations DLL for code signing + - name: Extract jars with DLLs for Codesigning shell: pwsh - run: gci ./appdir/Cryptomator/app/mods/ -File integrations-win-*.jar | ForEach-Object {Set-Location -Path $_.Directory; jar --file=$($_.FullName) --extract integrations.dll } + run: | + Add-Type -AssemblyName "System.io.compression.filesystem" + $jarFolder = Resolve-Path ".\appdir\Cryptomator\app\mods" + $jarExtractDir = New-Item -Path ".\appdir\jar-extract" -ItemType Directory + + #for all jars inspect + Get-ChildItem -Path $jarFolder -Filter "*.jar" | ForEach-Object { + $jar = [Io.compression.zipfile]::OpenRead($_.FullName) + if (@($jar.Entries | Where-Object {$_.Name.ToString().EndsWith(".dll")} | Select-Object -First 1).Count -gt 0) { + #jars containing dlls extract + Set-Location $jarExtractDir + Expand-Archive -Path $_.FullName + } + $jar.Dispose() + } + - name: Extract wixhelper.dll for Codesigning #see https://github.com/cryptomator/cryptomator/issues/3130 + shell: pwsh + run: | + New-Item -Path appdir/jpackage-jmod -ItemType Directory + & $env:JAVA_HOME\bin\jmod.exe extract --dir jpackage-jmod "${env:JAVA_HOME}\jmods\jdk.jpackage.jmod" + Get-ChildItem -Recurse -Path "jpackage-jmod" -File wixhelper.dll | Select-Object -Last 1 | Copy-Item -Destination "appdir" - name: Codesign uses: skymatic/code-sign-action@v2 with: @@ -154,12 +174,22 @@ jobs: certificatesha1: 5FC94CE149E5B511E621F53A060AC67CBD446B3A description: Cryptomator timestampUrl: 'http://timestamp.digicert.com' - folder: appdir/Cryptomator + folder: appdir recursive: true - - name: Repack signed DLL into jar + - name: Replace DLLs inside jars with signed ones shell: pwsh run: | - gci ./appdir/Cryptomator/app/mods/ -File integrations-win-*.jar | ForEach-Object {Set-Location -Path $_.Directory; jar --file=$($_.FullName) --update integrations.dll; Remove-Item integrations.dll} + $jarExtractDir = Resolve-Path ".\appdir\jar-extract" + $jarFolder = Resolve-Path ".\appdir\Cryptomator\app\mods" + Get-ChildItem -Path $jarExtractDir | ForEach-Object { + $jarName = $_.Name + $jarFile = "${jarFolder}\${jarName}.jar" + Set-Location $_ + Get-ChildItem -Path $_ -Recurse -File "*.dll" | ForEach-Object { + # update jar with signed dll + jar --file="$jarFile" --update $(Resolve-Path -Relative -Path $_) + } + } - name: Generate license for MSI run: > mvn -B license:add-third-party @@ -193,6 +223,7 @@ jobs: --file-associations dist/win/resources/FAvaultFile.properties env: JP_WIXWIZARD_RESOURCES: ${{ github.workspace }}/dist/win/resources # requires abs path, used in resources/main.wxs + JP_WIXHELPER_DIR: ${{ github.workspace }}\appdir - name: Codesign MSI uses: skymatic/code-sign-action@v2 with: @@ -234,7 +265,7 @@ jobs: runs-on: windows-latest needs: [get-version, build-msi] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Download .msi uses: actions/download-artifact@v3 with: diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 1d25cbef3..a3e38d3aa 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -14,10 +14,10 @@ - + \ No newline at end of file diff --git a/dist/linux/appimage/build.sh b/dist/linux/appimage/build.sh index d3390c717..0a4b7f65d 100755 --- a/dist/linux/appimage/build.sh +++ b/dist/linux/appimage/build.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -e cd $(dirname $0) REVISION_NO=`git rev-list --count HEAD` @@ -10,6 +11,7 @@ command -v curl >/dev/null 2>&1 || { echo >&2 "curl not found."; exit 1; } VERSION=$(mvn -f ../../../pom.xml help:evaluate -Dexpression=project.version -q -DforceStdout) SEMVER_STR=${VERSION} +MACHINE_TYPE=$(uname -m) mvn -f ../../../pom.xml versions:set -DnewVersion=${SEMVER_STR} @@ -83,17 +85,17 @@ ln -s usr/share/applications/org.cryptomator.Cryptomator.desktop Cryptomator.App ln -s bin/cryptomator.sh Cryptomator.AppDir/AppRun # load AppImageTool -curl -L https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage -o /tmp/appimagetool.AppImage +curl -L https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-${MACHINE_TYPE}.AppImage -o /tmp/appimagetool.AppImage chmod +x /tmp/appimagetool.AppImage # create AppImage /tmp/appimagetool.AppImage \ Cryptomator.AppDir \ - cryptomator-${SEMVER_STR}-x86_64.AppImage \ - -u 'gh-releases-zsync|cryptomator|cryptomator|latest|cryptomator-*-x86_64.AppImage.zsync' + cryptomator-${SEMVER_STR}-${MACHINE_TYPE}.AppImage \ + -u 'gh-releases-zsync|cryptomator|cryptomator|latest|cryptomator-*-${MACHINE_TYPE}.AppImage.zsync' echo "" -echo "Done. AppImage successfully created: cryptomator-${SEMVER_STR}-x86_64.AppImage" +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 diff --git a/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml b/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml index 596ebea4a..6e4873712 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/control b/dist/linux/debian/control index b04812fbb..148fdb213 100644 --- a/dist/linux/debian/control +++ b/dist/linux/debian/control @@ -2,7 +2,7 @@ Source: cryptomator Maintainer: Cryptobot Section: utils Priority: optional -Build-Depends: debhelper (>=10), coffeelibs-jdk-20, libgtk2.0-0, libgtk-3-0, libxxf86vm1, libgl1 +Build-Depends: debhelper (>=10), coffeelibs-jdk-21, libgtk2.0-0, libgtk-3-0, libxxf86vm1, libgl1 Standards-Version: 4.5.0 Homepage: https://cryptomator.org Vcs-Git: https://github.com/cryptomator/cryptomator.git diff --git a/dist/linux/debian/rules b/dist/linux/debian/rules index 231f2a1d5..d0a12e380 100755 --- a/dist/linux/debian/rules +++ b/dist/linux/debian/rules @@ -4,7 +4,7 @@ # Uncomment this to turn on verbose mode. #export DH_VERBOSE=1 -JAVA_HOME = /usr/lib/jvm/java-20-coffeelibs +JAVA_HOME = /usr/lib/jvm/java-21-coffeelibs DEB_BUILD_ARCH ?= $(shell dpkg-architecture -qDEB_BUILD_ARCH) ifeq ($(DEB_BUILD_ARCH),amd64) JMODS_PATH = jmods/amd64:${JAVA_HOME}/jmods @@ -59,6 +59,7 @@ override_dh_auto_build: --java-options "-Dcryptomator.integrationsLinux.trayIconsDir=\"/usr/share/icons/hicolor/symbolic/apps\"" \ --java-options "-Dcryptomator.buildNumber=\"deb-${REVISION_NUM}\"" \ --java-options "-Dcryptomator.appVersion=\"${SEMVER_STR}\"" \ + --java-options "-Dcryptomator.disableUpdateCheck=\"${DISABLE_UPDATE_CHECK}\"" \ --app-version "${VERSION_NUM}.${REVISION_NUM}" \ --resource-dir resources \ --verbose diff --git a/dist/mac/dmg/build.sh b/dist/mac/dmg/build.sh index 6d586f02c..b2c8d55e3 100755 --- a/dist/mac/dmg/build.sh +++ b/dist/mac/dmg/build.sh @@ -49,21 +49,22 @@ fi # download and check jmods curl -L ${OPENJFX_JMODS} -o openjfx-jmods.zip mkdir -p openjfx-jmods/ -unzip -j openjfx-jmods.zip \*/javafx.base.jmod \*/javafx.controls.jmod \*/javafx.fxml.jmod \*/javafx.graphics.jmod -d openjfx-jmods/ +unzip -jo 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=$(mvn -f../../../pom.xml 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 ]; then ->&2 echo "Major JavaFX version in pom.xml (${POM_JFX_VERSION}) != jmod version (${JMOD_VERSION})" -exit 1 +if [ "${POM_JFX_VERSION}" -ne "${JMOD_VERSION}" ]; then + >&2 echo "Major JavaFX version in pom.xml (${POM_JFX_VERSION}) != jmod version (${JMOD_VERSION})" + exit 1 fi # compile mvn -B -f../../../pom.xml clean package -DskipTests -Pmac +cp ../../../LICENSE.txt ../../../target cp ../../../target/${MAIN_JAR_GLOB} ../../../target/mods # add runtime @@ -168,6 +169,5 @@ create-dmg \ --app-drop-link 512 245 \ --eula "resources/license.rtf" \ --icon ".background" 128 758 \ - --icon ".fseventsd" 320 758 \ --icon ".VolumeIcon.icns" 512 758 \ ${APP_NAME}-${VERSION_NO}.dmg dmg diff --git a/dist/win/build.ps1 b/dist/win/build.ps1 index bef7a9acb..9d2fb0def 100644 --- a/dist/win/build.ps1 +++ b/dist/win/build.ps1 @@ -63,9 +63,10 @@ if( !(Test-Path -Path $jfxJmodsZip) ) { $jmodsChecksumActual = $(Get-FileHash -Path $jfxJmodsZip -Algorithm SHA256).Hash if( $jmodsChecksumActual -ne $jfxJmodsChecksum ) { Write-Error "Checksum mismatch for jfxJmods.zip. Expected: $jfxJmodsChecksum, actual: $jmodsChecksumActual" - exit 1; + exit 1; } -Expand-Archive -Path $jfxJmodsZip -DestinationPath ".\resources\" +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 @@ -143,6 +144,7 @@ try { # create .msi $Env:JP_WIXWIZARD_RESOURCES = "$buildDir\resources" +$Env:JP_WIXHELPER_DIR = "." & "$Env:JAVA_HOME\bin\jpackage" ` --verbose ` --type msi ` diff --git a/dist/win/resources/main.wxs b/dist/win/resources/main.wxs index c940b9f9a..2fe2eb348 100644 --- a/dist/win/resources/main.wxs +++ b/dist/win/resources/main.wxs @@ -70,7 +70,7 @@ - + diff --git a/pom.xml b/pom.xml index 5e2e46028..1c67c597b 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ UTF-8 - 20 + 21 @@ -35,36 +35,36 @@ 2.6.7 1.3.0 - 1.2.2 - 1.2.1 - 1.3.0-beta6 - 3.0.0 + 1.2.4 + 1.2.2 + 1.4.0-beta2 + 4.0.0-beta4 2.0.0 - 2.0.3 + 2.0.5 3.13.0 - 2.48 + 2.48.1 2.2 - 32.1.2-jre - 2.15.2 + 32.1.3-jre + 2.15.3 20.0.2 4.4.0 - 9.31 + 9.37 1.4.11 2.0.9 - 0.6.0 + 0.7.0 1.8.2 5.10.0 - 5.5.0 + 5.6.0 2.2 24.0.1 8.4.0 - 0.8.10 + 0.8.11 2.2.0 1.2.1 3.11.0 diff --git a/src/main/java/org/cryptomator/common/Environment.java b/src/main/java/org/cryptomator/common/Environment.java index c47870dd8..981b3729b 100644 --- a/src/main/java/org/cryptomator/common/Environment.java +++ b/src/main/java/org/cryptomator/common/Environment.java @@ -2,6 +2,7 @@ package org.cryptomator.common; import com.google.common.base.Splitter; import com.google.common.base.Strings; +import org.jetbrains.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,6 +32,7 @@ public class Environment { private static final String BUILD_NUMBER_PROP_NAME = "cryptomator.buildNumber"; private static final String PLUGIN_DIR_PROP_NAME = "cryptomator.pluginDir"; private static final String TRAY_ICON_PROP_NAME = "cryptomator.showTrayIcon"; + private static final String DISABLE_UPDATE_CHECK_PROP_NAME = "cryptomator.disableUpdateCheck"; private Environment() {} @@ -43,15 +45,16 @@ public class Environment { logCryptomatorSystemProperty(SETTINGS_PATH_PROP_NAME); logCryptomatorSystemProperty(IPC_SOCKET_PATH_PROP_NAME); logCryptomatorSystemProperty(KEYCHAIN_PATHS_PROP_NAME); + logCryptomatorSystemProperty(P12_PATH_PROP_NAME); logCryptomatorSystemProperty(LOG_DIR_PROP_NAME); logCryptomatorSystemProperty(LOOPBACK_ALIAS_PROP_NAME); - logCryptomatorSystemProperty(PLUGIN_DIR_PROP_NAME); logCryptomatorSystemProperty(MOUNTPOINT_DIR_PROP_NAME); logCryptomatorSystemProperty(MIN_PW_LENGTH_PROP_NAME); logCryptomatorSystemProperty(APP_VERSION_PROP_NAME); logCryptomatorSystemProperty(BUILD_NUMBER_PROP_NAME); + logCryptomatorSystemProperty(PLUGIN_DIR_PROP_NAME); logCryptomatorSystemProperty(TRAY_ICON_PROP_NAME); - logCryptomatorSystemProperty(P12_PATH_PROP_NAME); + logCryptomatorSystemProperty(DISABLE_UPDATE_CHECK_PROP_NAME); } public static Environment getInstance() { @@ -74,10 +77,6 @@ public class Environment { return getPaths(SETTINGS_PATH_PROP_NAME); } - public Stream getP12Path() { - return getPaths(P12_PATH_PROP_NAME); - } - public Stream getIpcSocketPath() { return getPaths(IPC_SOCKET_PATH_PROP_NAME); } @@ -86,6 +85,10 @@ public class Environment { return getPaths(KEYCHAIN_PATHS_PROP_NAME); } + public Stream getP12Path() { + return getPaths(P12_PATH_PROP_NAME); + } + public Optional getLogDir() { return getPath(LOG_DIR_PROP_NAME); } @@ -94,14 +97,14 @@ public class Environment { return Optional.ofNullable(System.getProperty(LOOPBACK_ALIAS_PROP_NAME)); } - public Optional getPluginDir() { - return getPath(PLUGIN_DIR_PROP_NAME); - } - public Optional getMountPointsDir() { return getPath(MOUNTPOINT_DIR_PROP_NAME); } + public int getMinPwLength() { + return Integer.getInteger(MIN_PW_LENGTH_PROP_NAME, DEFAULT_MIN_PW_LENGTH); + } + /** * Returns the app version defined in the {@value APP_VERSION_PROP_NAME} property or returns "SNAPSHOT". * @@ -115,20 +118,24 @@ public class Environment { return Optional.ofNullable(System.getProperty(BUILD_NUMBER_PROP_NAME)); } - public int getMinPwLength() { - return Integer.getInteger(MIN_PW_LENGTH_PROP_NAME, DEFAULT_MIN_PW_LENGTH); + public Optional getPluginDir() { + return getPath(PLUGIN_DIR_PROP_NAME); } public boolean showTrayIcon() { return Boolean.getBoolean(TRAY_ICON_PROP_NAME); } + public boolean disableUpdateCheck() { + return Boolean.getBoolean(DISABLE_UPDATE_CHECK_PROP_NAME); + } + private Optional getPath(String propertyName) { String value = System.getProperty(propertyName); return Optional.ofNullable(value).map(Paths::get); } - // visible for testing + @VisibleForTesting Stream getPaths(String propertyName) { Stream rawSettingsPaths = getRawList(propertyName, System.getProperty("path.separator").charAt(0)); return rawSettingsPaths.filter(Predicate.not(Strings::isNullOrEmpty)).map(Path::of); diff --git a/src/main/java/org/cryptomator/common/ErrorCode.java b/src/main/java/org/cryptomator/common/ErrorCode.java index 7363e2278..d75ab97d0 100644 --- a/src/main/java/org/cryptomator/common/ErrorCode.java +++ b/src/main/java/org/cryptomator/common/ErrorCode.java @@ -3,6 +3,7 @@ package org.cryptomator.common; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.base.Throwables; +import org.jetbrains.annotations.VisibleForTesting; import java.util.Locale; import java.util.Objects; @@ -114,7 +115,7 @@ public class ErrorCode { * @param bottomFrames Other stack frames, potentially forming the bottom of the stack of allFrames * @return The number of additional frames in allFrames. In most cases this should be equal to the difference in size. */ - // visible for testing + @VisibleForTesting static int countTopmostFrames(StackTraceElement[] allFrames, StackTraceElement[] bottomFrames) { if (allFrames.length < bottomFrames.length) { // if frames had been stacked on top of bottomFrames, allFrames would be larger @@ -124,7 +125,7 @@ public class ErrorCode { } } - // visible for testing + @VisibleForTesting static int commonSuffixLength(T[] set, T[] subset) { Preconditions.checkArgument(set.length >= subset.length); // iterate items backwards as long as they are identical diff --git a/src/main/java/org/cryptomator/common/locationpresets/OneDriveWindowsLocationPresetsProvider.java b/src/main/java/org/cryptomator/common/locationpresets/OneDriveWindowsLocationPresetsProvider.java index 1d5bffd70..467d7785b 100644 --- a/src/main/java/org/cryptomator/common/locationpresets/OneDriveWindowsLocationPresetsProvider.java +++ b/src/main/java/org/cryptomator/common/locationpresets/OneDriveWindowsLocationPresetsProvider.java @@ -62,7 +62,7 @@ public final class OneDriveWindowsLocationPresetsProvider implements LocationPre ProcessBuilder command = new ProcessBuilder(args); Process p = command.start(); waitForSuccess(p, 3, "`reg query`"); - return p.inputReader(StandardCharsets.UTF_8).lines().filter(outputFilter); + return p.inputReader(StandardCharsets.ISO_8859_1).lines().filter(outputFilter); } diff --git a/src/main/java/org/cryptomator/common/mount/MountWithinParentUtil.java b/src/main/java/org/cryptomator/common/mount/MountWithinParentUtil.java index b632923a8..b436bc19a 100644 --- a/src/main/java/org/cryptomator/common/mount/MountWithinParentUtil.java +++ b/src/main/java/org/cryptomator/common/mount/MountWithinParentUtil.java @@ -1,6 +1,7 @@ package org.cryptomator.common.mount; import org.apache.commons.lang3.SystemUtils; +import org.jetbrains.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -66,7 +67,7 @@ public final class MountWithinParentUtil { } } - //visible for testing + @VisibleForTesting static MountPointState getMountPointState(Path path) throws IOException, IllegalMountPointException { if (Files.notExists(path, LinkOption.NOFOLLOW_LINKS)) { return MountPointState.NOT_EXISTING; @@ -82,7 +83,7 @@ public final class MountWithinParentUtil { return MountPointState.BROKEN_JUNCTION; } - //visible for testing + @VisibleForTesting enum MountPointState { NOT_EXISTING, @@ -93,7 +94,7 @@ public final class MountWithinParentUtil { } - //visible for testing + @VisibleForTesting static void removeResidualHideaway(Path mountPoint, Path hideaway) throws IOException { checkIsHideawayDirectory(mountPoint, hideaway); Files.delete(hideaway); //Fails if not empty @@ -155,7 +156,7 @@ public final class MountWithinParentUtil { } } - //visible for testing + @VisibleForTesting static Path getHideaway(Path mountPoint) { return mountPoint.resolveSibling(HIDEAWAY_PREFIX + mountPoint.getFileName().toString() + HIDEAWAY_SUFFIX); } diff --git a/src/main/java/org/cryptomator/common/settings/VaultSettings.java b/src/main/java/org/cryptomator/common/settings/VaultSettings.java index 4565aaedc..0a5fe45c1 100644 --- a/src/main/java/org/cryptomator/common/settings/VaultSettings.java +++ b/src/main/java/org/cryptomator/common/settings/VaultSettings.java @@ -8,7 +8,7 @@ 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.jetbrains.annotations.VisibleForTesting; import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.binding.StringExpression; @@ -131,7 +131,7 @@ public class VaultSettings { return json; } - //visible for testing + @VisibleForTesting static String normalizeDisplayName(String original) { if (original.isBlank() || ".".equals(original) || "..".equals(original)) { return "_"; diff --git a/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java b/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java index eb2418c69..dbdb37823 100644 --- a/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java +++ b/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java @@ -6,6 +6,7 @@ *******************************************************************************/ package org.cryptomator.launcher; +import org.jetbrains.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,7 +49,7 @@ class FileOpenRequestHandler { handleLaunchArgs(FileSystems.getDefault(), args); } - // visible for testing + @VisibleForTesting void handleLaunchArgs(FileSystem fs, List args) { Collection pathsToOpen = args.stream().map(str -> { try { diff --git a/src/main/java/org/cryptomator/logging/LogbackConfigurator.java b/src/main/java/org/cryptomator/logging/LogbackConfigurator.java index 511599132..3b77993cc 100644 --- a/src/main/java/org/cryptomator/logging/LogbackConfigurator.java +++ b/src/main/java/org/cryptomator/logging/LogbackConfigurator.java @@ -5,6 +5,7 @@ import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.encoder.PatternLayoutEncoder; import ch.qos.logback.classic.spi.Configurator; +import ch.qos.logback.classic.spi.ConfiguratorRank; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.Appender; import ch.qos.logback.core.ConsoleAppender; @@ -19,6 +20,7 @@ import org.cryptomator.common.Environment; import java.nio.file.Path; import java.util.Map; +@ConfiguratorRank(ConfiguratorRank.CUSTOM_NORMAL_PRIORITY) public class LogbackConfigurator extends ContextAwareBase implements Configurator { private static final String LOG_PATTERN = "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"; diff --git a/src/main/java/org/cryptomator/ui/addvaultwizard/ReadmeGenerator.java b/src/main/java/org/cryptomator/ui/addvaultwizard/ReadmeGenerator.java index 5cf14444a..2ffda4d73 100644 --- a/src/main/java/org/cryptomator/ui/addvaultwizard/ReadmeGenerator.java +++ b/src/main/java/org/cryptomator/ui/addvaultwizard/ReadmeGenerator.java @@ -1,5 +1,7 @@ package org.cryptomator.ui.addvaultwizard; +import org.jetbrains.annotations.VisibleForTesting; + import javax.inject.Inject; import java.util.List; import java.util.ResourceBundle; @@ -51,7 +53,7 @@ public class ReadmeGenerator { resourceBundle.getString("addvault.new.readme.accessLocation.4"))); } - // visible for testing + @VisibleForTesting String createDocument(Iterable paragraphs) { StringBuilder sb = new StringBuilder(RTF_HEADER); for (String p : paragraphs) { @@ -63,7 +65,7 @@ public class ReadmeGenerator { return sb.toString(); } - // visible for testing + @VisibleForTesting String escapeNonAsciiChars(CharSequence input) { StringBuilder sb = new StringBuilder(); appendEscaped(sb, input); diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java index 678795662..144b8bbb6 100644 --- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -20,9 +20,10 @@ public enum FxmlFile { HUB_AUTH_FLOW("/fxml/hub_auth_flow.fxml"), // HUB_INVALID_LICENSE("/fxml/hub_invalid_license.fxml"), // HUB_RECEIVE_KEY("/fxml/hub_receive_key.fxml"), // - HUB_REGISTER_DEVICE("/fxml/hub_register_device.fxml"), // + HUB_LEGACY_REGISTER_DEVICE("/fxml/hub_legacy_register_device.fxml"), // HUB_REGISTER_SUCCESS("/fxml/hub_register_success.fxml"), // - HUB_REGISTER_FAILED("/fxml/hub_register_failed.fxml"), + HUB_REGISTER_FAILED("/fxml/hub_register_failed.fxml"), // + HUB_SETUP_DEVICE("/fxml/hub_setup_device.fxml"), // HUB_UNAUTHORIZED_DEVICE("/fxml/hub_unauthorized_device.fxml"), // LOCK_FORCED("/fxml/lock_forced.fxml"), // LOCK_FAILED("/fxml/lock_failed.fxml"), // diff --git a/src/main/java/org/cryptomator/ui/convertvault/HubToPasswordConvertController.java b/src/main/java/org/cryptomator/ui/convertvault/HubToPasswordConvertController.java index 51ff65ec1..fd6d49b89 100644 --- a/src/main/java/org/cryptomator/ui/convertvault/HubToPasswordConvertController.java +++ b/src/main/java/org/cryptomator/ui/convertvault/HubToPasswordConvertController.java @@ -16,6 +16,7 @@ import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.fxapp.FxApplicationWindows; import org.cryptomator.ui.recoverykey.RecoveryKeyFactory; +import org.jetbrains.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -116,7 +117,7 @@ public class HubToPasswordConvertController implements FxController { }, Platform::runLater); // } - //visible for testing + @VisibleForTesting void convertInternal() throws CompletionException, IllegalArgumentException { var passphrase = newPasswordController.getNewPassword(); var vaultPath = vault.getPath(); @@ -141,7 +142,7 @@ public class HubToPasswordConvertController implements FxController { } } - //visible for testing + @VisibleForTesting void backupHubConfig(Path hubConfigPath) throws IOException { byte[] hubConfigBytes = Files.readAllBytes(hubConfigPath); Path backupPath = hubConfigPath.resolveSibling(VAULTCONFIG_FILENAME + BackupHelper.generateFileIdSuffix(hubConfigBytes) + MASTERKEY_BACKUP_SUFFIX); @@ -149,7 +150,7 @@ public class HubToPasswordConvertController implements FxController { LOG.debug("Successfully created hub config backup {}", backupPath.getFileName()); } - //visible for testing + @VisibleForTesting Path createPasswordConfig(Path passwordConfigPath, Path masterkeyFile, Passphrase passphrase) throws IOException, MasterkeyLoadingFailedException { var unverifiedVaultConfig = vault.getVaultConfigCache().get(); try (var masterkey = masterkeyFileAccess.load(masterkeyFile, passphrase)) { diff --git a/src/main/java/org/cryptomator/ui/error/ErrorController.java b/src/main/java/org/cryptomator/ui/error/ErrorController.java index 3feb3ff44..45c940bcb 100644 --- a/src/main/java/org/cryptomator/ui/error/ErrorController.java +++ b/src/main/java/org/cryptomator/ui/error/ErrorController.java @@ -31,6 +31,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.Comparator; import java.util.Map; import java.util.Optional; @@ -42,7 +43,8 @@ public class ErrorController implements FxController { private static final ObjectMapper JSON = new ObjectMapper(); private static final Logger LOG = LoggerFactory.getLogger(ErrorController.class); - private static final String ERROR_CODES_URL = "https://api.cryptomator.org/desktop/error-codes.json"; + private static final String USER_AGENT_FORMAT = "Cryptomator/%s (Build %s) (%s %s %s)"; + private static final String ERROR_CODES_URL_FORMAT = "https://api.cryptomator.org/desktop/error-codes.json?error-code=%s"; private static final String SEARCH_URL_FORMAT = "https://github.com/cryptomator/cryptomator/discussions/categories/errors?discussions_q=category:Errors+%s"; private static final String REPORT_URL_FORMAT = "https://github.com/cryptomator/cryptomator/discussions/new?category=Errors&title=Error+%s&body=%s"; private static final String SEARCH_ERRORCODE_DELIM = " OR "; @@ -142,11 +144,19 @@ public class ErrorController implements FxController { @FXML public void lookUpSolution() { + String userAgent = USER_AGENT_FORMAT.formatted( // + environment.getAppVersion(), // + environment.getBuildNumber().orElse("undefined"), // + System.getProperty("os.name"), // + System.getProperty("os.version"), // + System.getProperty("os.arch")); isLoadingHttpResponse.set(true); askedForLookupDatabasePermission.set(true); HttpClient httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); HttpRequest httpRequest = HttpRequest.newBuilder()// - .uri(URI.create(ERROR_CODES_URL))// + .header("User-Agent", userAgent) + .timeout(Duration.ofSeconds(10)) + .uri(URI.create(ERROR_CODES_URL_FORMAT.formatted(URLEncoder.encode(errorCode.toString(),StandardCharsets.UTF_8))))// .build(); httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofInputStream())// .thenAcceptAsync(this::loadHttpResponse, executorService)// diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java index d845655cf..711a0fa44 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java @@ -1,6 +1,7 @@ package org.cryptomator.ui.fxapp; import dagger.Lazy; +import org.cryptomator.common.Environment; import org.cryptomator.common.settings.Settings; import org.cryptomator.ui.traymenu.TrayMenuComponent; import org.slf4j.Logger; @@ -17,6 +18,7 @@ public class FxApplication { private static final Logger LOG = LoggerFactory.getLogger(FxApplication.class); private final long startupTime; + private final Environment environment; private final Settings settings; private final AppLaunchEventHandler launchEventHandler; private final Lazy trayMenu; @@ -26,8 +28,9 @@ public class FxApplication { private final AutoUnlocker autoUnlocker; @Inject - FxApplication(@Named("startupTime") long startupTime, Settings settings, AppLaunchEventHandler launchEventHandler, Lazy trayMenu, FxApplicationWindows appWindows, FxApplicationStyle applicationStyle, FxApplicationTerminator applicationTerminator, AutoUnlocker autoUnlocker) { + FxApplication(@Named("startupTime") long startupTime, Environment environment, Settings settings, AppLaunchEventHandler launchEventHandler, Lazy trayMenu, FxApplicationWindows appWindows, FxApplicationStyle applicationStyle, FxApplicationTerminator applicationTerminator, AutoUnlocker autoUnlocker) { this.startupTime = startupTime; + this.environment = environment; this.settings = settings; this.launchEventHandler = launchEventHandler; this.trayMenu = trayMenu; @@ -68,7 +71,9 @@ public class FxApplication { return null; }); - appWindows.checkAndShowUpdateReminderWindow(); + if (!environment.disableUpdateCheck()) { + appWindows.checkAndShowUpdateReminderWindow(); + } launchEventHandler.startHandlingLaunchEvents(); autoUnlocker.tryUnlockForTimespan(2, TimeUnit.MINUTES); diff --git a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java index 4418f79b5..709eb2fe7 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java +++ b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java @@ -22,23 +22,23 @@ public class UpdateChecker { private static final Logger LOG = LoggerFactory.getLogger(UpdateChecker.class); private static final Duration AUTOCHECK_DELAY = Duration.seconds(5); + private final Environment env; private final Settings settings; - private final String currentVersion; private final StringProperty latestVersionProperty; private final Comparator semVerComparator; private final ScheduledService updateCheckerService; @Inject UpdateChecker(Settings settings, Environment env, @Named("latestVersion") StringProperty latestVersionProperty, @Named("SemVer") Comparator semVerComparator, ScheduledService updateCheckerService) { + this.env = env; this.settings = settings; this.latestVersionProperty = latestVersionProperty; this.semVerComparator = semVerComparator; this.updateCheckerService = updateCheckerService; - this.currentVersion = env.getAppVersion(); } public void automaticallyCheckForUpdatesIfEnabled() { - if (settings.checkForUpdates.get()) { + if (!env.disableUpdateCheck() && settings.checkForUpdates.get()) { startCheckingForUpdates(AUTOCHECK_DELAY); } } @@ -63,9 +63,9 @@ public class UpdateChecker { private void checkSucceeded(WorkerStateEvent event) { String latestVersion = updateCheckerService.getValue(); - LOG.info("Current version: {}, lastest version: {}", currentVersion, latestVersion); + LOG.info("Current version: {}, lastest version: {}", getCurrentVersion(), latestVersion); - if (semVerComparator.compare(currentVersion, latestVersion) < 0) { + if (semVerComparator.compare(getCurrentVersion(), latestVersion) < 0) { // update is available latestVersionProperty.set(latestVersion); } else { @@ -88,7 +88,7 @@ public class UpdateChecker { } public String getCurrentVersion() { - return currentVersion; + return env.getAppVersion(); } } diff --git a/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java b/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java index d70301e78..b5f06d7e5 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java +++ b/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java @@ -63,6 +63,7 @@ public abstract class UpdateCheckerModule { return HttpRequest.newBuilder() // .uri(LATEST_VERSION_URI) // .header("User-Agent", userAgent) // + .timeout(java.time.Duration.ofSeconds(10)) .build(); } diff --git a/src/main/java/org/cryptomator/ui/health/StartController.java b/src/main/java/org/cryptomator/ui/health/StartController.java index 4e95b6b0f..9ff2502da 100644 --- a/src/main/java/org/cryptomator/ui/health/StartController.java +++ b/src/main/java/org/cryptomator/ui/health/StartController.java @@ -101,16 +101,16 @@ public class StartController implements FxController { } } - private void loadingKeyFailed(Throwable e) { - switch (e) { - case UnlockCancelledException uce -> {} //ok - case VaultKeyInvalidException vkie -> { - LOG.error("Invalid key"); //TODO: specific error screen + private void loadingKeyFailed(Throwable t) { + switch (t) { + case UnlockCancelledException e -> {} // ok // TODO: rename to _ with JEP 443 + case VaultKeyInvalidException e -> { // TODO: rename to _ with JEP 443 + LOG.error("Invalid key"); // TODO: specific error screen appWindows.showErrorWindow(e, window, null); } default -> { - LOG.error("Failed to load key.", e); - appWindows.showErrorWindow(e, window, null); + LOG.error("Failed to load key.", t); + appWindows.showErrorWindow(t, window, null); } } } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java index 5765f56e0..06e488581 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java @@ -35,13 +35,13 @@ public class AuthFlowController implements FxController { private final String deviceId; private final HubConfig hubConfig; private final AtomicReference tokenRef; - private final CompletableFuture result; + private final CompletableFuture result; private final Lazy receiveKeyScene; private final ObjectProperty authUri; private AuthFlowTask task; @Inject - public AuthFlowController(Application application, @KeyLoading Stage window, ExecutorService executor, @Named("deviceId") String deviceId, HubConfig hubConfig, @Named("bearerToken") AtomicReference tokenRef, CompletableFuture result, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy receiveKeyScene) { + public AuthFlowController(Application application, @KeyLoading Stage window, ExecutorService executor, @Named("deviceId") String deviceId, HubConfig hubConfig, @Named("bearerToken") AtomicReference tokenRef, CompletableFuture result, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy receiveKeyScene) { this.application = application; this.window = window; this.executor = executor; diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowTask.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowTask.java index 0379d4331..b85e98cd4 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowTask.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowTask.java @@ -8,6 +8,8 @@ import io.github.coffeelibs.tinyoauth2client.http.response.Response; import javafx.concurrent.Task; import java.io.IOException; import java.net.URI; +import java.net.http.HttpClient; +import java.time.Duration; import java.util.function.Consumer; class AuthFlowTask extends Task { @@ -34,6 +36,7 @@ class AuthFlowTask extends Task { protected String call() throws IOException, InterruptedException { var response = TinyOAuth2.client(hubConfig.clientId) // .withTokenEndpoint(URI.create(hubConfig.tokenEndpoint)) // + .withRequestTimeout(Duration.ofSeconds(10)) // .authFlow(URI.create(hubConfig.authEndpoint)) // .setSuccessResponse(Response.redirect(URI.create(hubConfig.authSuccessUrl + "&device=" + authFlowContext.deviceId()))) // .setErrorResponse(Response.redirect(URI.create(hubConfig.authErrorUrl + "&device=" + authFlowContext.deviceId()))) // diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/CreateDeviceDto.java b/src/main/java/org/cryptomator/ui/keyloading/hub/CreateDeviceDto.java deleted file mode 100644 index ed10a9257..000000000 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/CreateDeviceDto.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.cryptomator.ui.keyloading.hub; - -record CreateDeviceDto(String id, String name, String publicKey) { - -} diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HttpHelper.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HttpHelper.java deleted file mode 100644 index 0077467cc..000000000 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HttpHelper.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.cryptomator.ui.keyloading.hub; - -import com.google.common.io.CharStreams; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.http.HttpResponse; -import java.nio.charset.StandardCharsets; - -class HttpHelper { - - public static String readBody(HttpResponse response) throws IOException { - try (var in = response.body(); var reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { - return CharStreams.toString(reader); - } - } - -} 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 5f462b170..ef1fe30df 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubConfig.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubConfig.java @@ -1,6 +1,10 @@ package org.cryptomator.ui.keyloading.hub; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.net.URI; // needs to be accessible by JSON decoder @JsonIgnoreProperties(ignoreUnknown = true) @@ -9,8 +13,19 @@ public class HubConfig { public String clientId; public String authEndpoint; public String tokenEndpoint; - public String devicesResourceUrl; public String authSuccessUrl; public String authErrorUrl; + public @Nullable String apiBaseUrl; + @Deprecated // use apiBaseUrl + "/devices/" + public String devicesResourceUrl; + public URI getApiBaseUrl() { + if (apiBaseUrl != null) { + return URI.create(apiBaseUrl); + } else { + // legacy approach + assert devicesResourceUrl != null; + return URI.create(devicesResourceUrl + "/..").normalize(); + } + } } 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 7b8aae875..c8d308e8e 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java @@ -1,7 +1,6 @@ package org.cryptomator.ui.keyloading.hub; import com.google.common.io.BaseEncoding; -import com.nimbusds.jose.JWEObject; import dagger.Binds; import dagger.Module; import dagger.Provides; @@ -69,7 +68,7 @@ public abstract class HubKeyLoadingModule { @Provides @KeyLoadingScoped - static CompletableFuture provideResult() { + static CompletableFuture provideResult() { return new CompletableFuture<>(); } @@ -114,10 +113,10 @@ public abstract class HubKeyLoadingModule { } @Provides - @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) + @FxmlScene(FxmlFile.HUB_LEGACY_REGISTER_DEVICE) @KeyLoadingScoped - static Scene provideHubRegisterDeviceScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) { - return fxmlLoaders.createScene(FxmlFile.HUB_REGISTER_DEVICE); + static Scene provideHubLegacyRegisterDeviceScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene(FxmlFile.HUB_LEGACY_REGISTER_DEVICE); } @Provides @@ -134,6 +133,13 @@ public abstract class HubKeyLoadingModule { return fxmlLoaders.createScene(FxmlFile.HUB_REGISTER_FAILED); } + @Provides + @FxmlScene(FxmlFile.HUB_SETUP_DEVICE) + @KeyLoadingScoped + static Scene provideHubRegisterDeviceScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene(FxmlFile.HUB_SETUP_DEVICE); + } + @Provides @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) @KeyLoadingScoped @@ -166,6 +172,11 @@ public abstract class HubKeyLoadingModule { @FxControllerKey(RegisterDeviceController.class) abstract FxController bindRegisterDeviceController(RegisterDeviceController controller); + @Binds + @IntoMap + @FxControllerKey(LegacyRegisterDeviceController.class) + abstract FxController bindLegacyRegisterDeviceController(LegacyRegisterDeviceController 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 cc5edfcb4..9ea5e7735 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java @@ -36,11 +36,11 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy { private final KeychainManager keychainManager; private final Lazy authFlowScene; private final Lazy noKeychainScene; - private final CompletableFuture result; + private final CompletableFuture result; private final DeviceKey deviceKey; @Inject - public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy authFlowScene, @FxmlScene(FxmlFile.HUB_NO_KEYCHAIN) Lazy noKeychainScene, CompletableFuture result, DeviceKey deviceKey, KeychainManager keychainManager, @Named("windowTitle") String windowTitle) { + public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy authFlowScene, @FxmlScene(FxmlFile.HUB_NO_KEYCHAIN) Lazy noKeychainScene, CompletableFuture result, DeviceKey deviceKey, KeychainManager keychainManager, @Named("windowTitle") String windowTitle) { this.window = window; this.keychainManager = keychainManager; window.setTitle(windowTitle); @@ -60,7 +60,7 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy { var keypair = deviceKey.get(); showWindow(authFlowScene); var jwe = result.get(); - return JWEHelper.decrypt(jwe, keypair.getPrivate()); + return jwe.decryptMasterkey(keypair.getPrivate()); } catch (NoKeychainAccessProviderException e) { showWindow(noKeychainScene); throw new UnlockCancelledException("Unlock canceled due to missing prerequisites", e); 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 2c2b9baa4..2333051be 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/JWEHelper.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/JWEHelper.java @@ -2,35 +2,103 @@ 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; +import com.nimbusds.jose.JWEHeader; import com.nimbusds.jose.JWEObject; +import com.nimbusds.jose.Payload; import com.nimbusds.jose.crypto.ECDHDecrypter; +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.Masterkey; import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +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.util.Arrays; +import java.util.Base64; +import java.util.Map; +import java.util.function.Function; class JWEHelper { private static final Logger LOG = LoggerFactory.getLogger(JWEHelper.class); - private static final String JWE_PAYLOAD_MASTERKEY_FIELD = "key"; + private static final String JWE_PAYLOAD_KEY_FIELD = "key"; + private static final String EC_ALG = "EC"; private JWEHelper(){} - - public static Masterkey decrypt(JWEObject jwe, ECPrivateKey privateKey) throws MasterkeyLoadingFailedException { + public static JWEObject encryptUserKey(ECPrivateKey userKey, ECPublicKey deviceKey) { try { - jwe.decrypt(new ECDHDecrypter(privateKey)); - return readKey(jwe); + 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) { - LOG.warn("Failed to decrypt JWE: {}", jwe); - throw new MasterkeyLoadingFailedException("Failed to decrypt JWE", e); + throw new RuntimeException(e); } } - private static Masterkey readKey(JWEObject jwe) throws MasterkeyLoadingFailedException { + public static ECPrivateKey decryptUserKey(JWEObject jwe, String setupCode) throws InvalidJweKeyException { + try { + jwe.decrypt(new PasswordBasedDecrypter(setupCode)); + return decodeUserKey(jwe); + } catch (JOSEException e) { + throw new InvalidJweKeyException(e); + } + } + + public static ECPrivateKey decryptUserKey(JWEObject jwe, ECPrivateKey deviceKey) throws InvalidJweKeyException { + try { + jwe.decrypt(new ECDHDecrypter(deviceKey)); + return decodeUserKey(jwe); + } catch (JOSEException e) { + throw new InvalidJweKeyException(e); + } + } + + private static ECPrivateKey decodeUserKey(JWEObject decryptedJwe) { + try { + var keySpec = readKey(decryptedJwe, JWE_PAYLOAD_KEY_FIELD, PKCS8EncodedKeySpec::new); + var factory = KeyFactory.getInstance(EC_ALG); + var privateKey = factory.generatePrivate(keySpec); + if (privateKey instanceof ECPrivateKey ecPrivateKey) { + return ecPrivateKey; + } else { + throw new IllegalStateException(EC_ALG + " key factory not generating ECPrivateKeys"); + } + } 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); + } + } + + public static Masterkey decryptVaultKey(JWEObject jwe, ECPrivateKey privateKey) throws InvalidJweKeyException { + try { + jwe.decrypt(new ECDHDecrypter(privateKey)); + return readKey(jwe, JWE_PAYLOAD_KEY_FIELD, Masterkey::new); + } catch (JOSEException e) { + throw new InvalidJweKeyException(e); + } + } + + private static T readKey(JWEObject jwe, String keyField, Function rawKeyFactory) throws MasterkeyLoadingFailedException { Preconditions.checkArgument(jwe.getState() == JWEObject.State.DECRYPTED); var fields = jwe.getPayload().toJSONObject(); if (fields == null) { @@ -39,11 +107,11 @@ class JWEHelper { } var keyBytes = new byte[0]; try { - if (fields.get(JWE_PAYLOAD_MASTERKEY_FIELD) instanceof String key) { + if (fields.get(keyField) instanceof String key) { keyBytes = BaseEncoding.base64().decode(key); - return new Masterkey(keyBytes); + return rawKeyFactory.apply(keyBytes); } else { - throw new IllegalArgumentException("JWE payload doesn't contain field " + JWE_PAYLOAD_MASTERKEY_FIELD); + throw new IllegalArgumentException("JWE payload doesn't contain field " + keyField); } } catch (IllegalArgumentException e) { LOG.error("Unexpected JWE payload: {}", jwe.getPayload()); @@ -52,4 +120,11 @@ class JWEHelper { Arrays.fill(keyBytes, (byte) 0x00); } } + + public static class InvalidJweKeyException extends MasterkeyLoadingFailedException { + + public InvalidJweKeyException(Throwable cause) { + super("Invalid 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 new file mode 100644 index 000000000..113ecd249 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/LegacyRegisterDeviceController.java @@ -0,0 +1,191 @@ +package org.cryptomator.ui.keyloading.hub; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.databind.ObjectMapper; +import dagger.Lazy; +import org.cryptomator.common.settings.DeviceKey; +import org.cryptomator.cryptolib.common.P384KeyPair; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; +import org.cryptomator.ui.keyloading.KeyLoading; +import org.cryptomator.ui.keyloading.KeyLoadingScoped; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Named; +import javafx.application.Platform; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.fxml.FXML; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.TextField; +import javafx.stage.Stage; +import javafx.stage.WindowEvent; +import java.io.IOException; +import java.net.InetAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicReference; + +@KeyLoadingScoped +public class LegacyRegisterDeviceController implements FxController { + + private static final Logger LOG = LoggerFactory.getLogger(LegacyRegisterDeviceController.class); + private static final ObjectMapper JSON = new ObjectMapper().setDefaultLeniency(true); + private static final List EXPECTED_RESPONSE_CODES = List.of(201, 409); + + private final Stage window; + private final HubConfig hubConfig; + private final String bearerToken; + private final Lazy registerSuccessScene; + private final Lazy registerFailedScene; + private final String deviceId; + private final P384KeyPair keyPair; + private final CompletableFuture result; + private final DecodedJWT jwt; + private final HttpClient httpClient; + private final BooleanProperty deviceNameAlreadyExists = new SimpleBooleanProperty(false); + + public TextField deviceNameField; + 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) { + this.window = window; + this.hubConfig = hubConfig; + this.deviceId = deviceId; + this.keyPair = Objects.requireNonNull(deviceKey.get()); + this.result = result; + this.bearerToken = Objects.requireNonNull(bearerToken.get()); + this.registerSuccessScene = registerSuccessScene; + this.registerFailedScene = registerFailedScene; + this.jwt = JWT.decode(this.bearerToken); + this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed); + this.httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).executor(executor).build(); + } + + public void initialize() { + deviceNameField.setText(determineHostname()); + deviceNameField.textProperty().addListener(observable -> deviceNameAlreadyExists.set(false)); + } + + private String determineHostname() { + try { + var hostName = InetAddress.getLocalHost().getHostName(); + return Objects.requireNonNullElse(hostName, ""); + } catch (IOException e) { + return ""; + } + } + + @FXML + public void register() { + deviceNameAlreadyExists.set(false); + registerBtn.setContentDisplay(ContentDisplay.LEFT); + registerBtn.setDisable(true); + + var deviceUri = URI.create(hubConfig.devicesResourceUrl + deviceId); + var deviceKey = keyPair.getPublic().getEncoded(); + var dto = new CreateDeviceDto(); + dto.id = deviceId; + dto.name = deviceNameField.getText(); + dto.publicKey = Base64.getUrlEncoder().withoutPadding().encodeToString(deviceKey); + var json = toJson(dto); + var request = HttpRequest.newBuilder(deviceUri) // + .PUT(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) // + .header("Authorization", "Bearer " + bearerToken) // + .header("Content-Type", "application/json") // + .build(); + httpClient.sendAsync(request, HttpResponse.BodyHandlers.discarding()) // + .thenApply(response -> { + if (EXPECTED_RESPONSE_CODES.contains(response.statusCode())) { + return response; + } else { + throw new RuntimeException("Server answered with unexpected status code " + response.statusCode()); + } + }).handleAsync((response, throwable) -> { + if (response != null) { + this.handleResponse(response); + } else { + this.registrationFailed(throwable); + } + return null; + }, Platform::runLater); + } + + private String toJson(CreateDeviceDto dto) { + try { + return JSON.writer().writeValueAsString(dto); + } catch (JacksonException e) { + throw new IllegalStateException("Failed to serialize DTO", e); + } + } + + private void handleResponse(HttpResponse voidHttpResponse) { + assert EXPECTED_RESPONSE_CODES.contains(voidHttpResponse.statusCode()); + + if (voidHttpResponse.statusCode() == 409) { + deviceNameAlreadyExists.set(true); + registerBtn.setContentDisplay(ContentDisplay.TEXT_ONLY); + registerBtn.setDisable(false); + } else { + LOG.debug("Device registration for hub instance {} successful.", hubConfig.authSuccessUrl); + window.setScene(registerSuccessScene.get()); + } + } + + private void registrationFailed(Throwable cause) { + LOG.warn("Device registration failed.", cause); + window.setScene(registerFailedScene.get()); + result.completeExceptionally(cause); + } + + @FXML + public void close() { + window.close(); + } + + private void windowClosed(WindowEvent windowEvent) { + result.cancel(true); + } + + /* Getter */ + + public String getUserName() { + return jwt.getClaim("email").asString(); + } + + + //--- Getters & Setters + + public BooleanProperty deviceNameAlreadyExistsProperty() { + return deviceNameAlreadyExists; + } + + public boolean getDeviceNameAlreadyExists() { + return deviceNameAlreadyExists.get(); + } + + private static class CreateDeviceDto { + public String id; + public String name; + public final String type = "DESKTOP"; + public String publicKey; + + } + +} \ No newline at end of file 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 bd7497bec..66566a98a 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java @@ -1,5 +1,8 @@ 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.nimbusds.jose.JWEObject; import dagger.Lazy; import org.cryptomator.common.vaults.Vault; @@ -8,6 +11,9 @@ 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; import javax.inject.Inject; import javax.inject.Named; @@ -17,14 +23,16 @@ import javafx.scene.Scene; import javafx.stage.Stage; import javafx.stage.WindowEvent; import java.io.IOException; -import java.io.InputStream; import java.io.UncheckedIOException; import java.net.URI; import java.net.URISyntaxException; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.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; @@ -33,25 +41,32 @@ import java.util.concurrent.atomic.AtomicReference; @KeyLoadingScoped public class ReceiveKeyController implements FxController { + private static final Logger LOG = LoggerFactory.getLogger(ReceiveKeyController.class); private static final String SCHEME_PREFIX = "hub+"; + private static final ObjectMapper JSON = new ObjectMapper().setDefaultLeniency(true); + private static final Duration REQ_TIMEOUT = Duration.ofSeconds(10); private final Stage window; + private final HubConfig hubConfig; private final String deviceId; private final String bearerToken; - private final CompletableFuture result; - private final Lazy registerDeviceScene; + private final CompletableFuture result; + private final Lazy setupDeviceScene; + private final Lazy legacyRegisterDeviceScene; private final Lazy unauthorizedScene; private final URI vaultBaseUri; private final Lazy invalidLicenseScene; private final HttpClient httpClient; @Inject - public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, @Named("deviceId") String deviceId, @Named("bearerToken") AtomicReference tokenRef, CompletableFuture result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy registerDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy unauthorizedScene, @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_SETUP_DEVICE) Lazy setupDeviceScene, @FxmlScene(FxmlFile.HUB_LEGACY_REGISTER_DEVICE) Lazy legacyRegisterDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy unauthorizedScene, @FxmlScene(FxmlFile.HUB_INVALID_LICENSE) Lazy invalidLicenseScene) { this.window = window; + this.hubConfig = hubConfig; this.deviceId = deviceId; this.bearerToken = Objects.requireNonNull(tokenRef.get()); this.result = result; - this.registerDeviceScene = registerDeviceScene; + this.setupDeviceScene = setupDeviceScene; + this.legacyRegisterDeviceScene = legacyRegisterDeviceScene; this.unauthorizedScene = unauthorizedScene; this.vaultBaseUri = getVaultBaseUri(vault); this.invalidLicenseScene = invalidLicenseScene; @@ -61,23 +76,120 @@ public class ReceiveKeyController implements FxController { @FXML public void initialize() { - var keyUri = appendPath(vaultBaseUri, "/keys/" + deviceId); - var request = HttpRequest.newBuilder(keyUri) // + requestVaultMasterkey(); + } + + /** + * STEP 1 (Request): GET vault key for this user + */ + private void requestVaultMasterkey() { + var accessTokenUri = appendPath(vaultBaseUri, "/access-token"); + var request = HttpRequest.newBuilder(accessTokenUri) // .header("Authorization", "Bearer " + bearerToken) // .GET() // + .timeout(REQ_TIMEOUT) // .build(); - httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()) // - .thenAcceptAsync(this::loadedExistingKey, Platform::runLater) // + httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.US_ASCII)) // + .thenAcceptAsync(this::receivedVaultMasterkey, Platform::runLater) // .exceptionally(this::retrievalFailed); } - private void loadedExistingKey(HttpResponse response) { + /** + * STEP 1 (Response): GET vault key for this user + * + * @param response Response + */ + private void receivedVaultMasterkey(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 404 -> requestLegacyAccessToken(); + default -> throw new IllegalStateException("Unexpected response " + response.statusCode()); + } + } + + /** + * STEP 2 (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) // + .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) // + .exceptionally(this::retrievalFailed); + } + + /** + * STEP 2 (Response): GET user key for this device + * + * @param response Response + */ + private void receivedUserKey(String encryptedVaultKey, HttpResponse response) { + LOG.debug("GET {} -> Status Code {}", response.request().uri(), response.statusCode()); try { switch (response.statusCode()) { - case 200 -> retrievalSucceeded(response); + case 200 -> { + var device = JSON.reader().readValue(response.body(), DeviceDto.class); + receivedBothEncryptedKeys(encryptedVaultKey, device.userPrivateKey); + } + case 404 -> needsDeviceSetup(); // TODO: using the setup code, we can theoretically immediately unlock + default -> throw new IllegalStateException("Unexpected response " + response.statusCode()); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private void needsDeviceSetup() { + window.setScene(setupDeviceScene.get()); + } + + private void receivedBothEncryptedKeys(String encryptedVaultKey, String encryptedUserKey) throws IOException { + 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); + } + } + + /** + * LEGACY FALLBACK (Request): GET the legacy access token from Hub 1.x + */ + @Deprecated + private void requestLegacyAccessToken() { + var legacyAccessTokenUri = appendPath(vaultBaseUri, "/keys/" + deviceId); + var request = HttpRequest.newBuilder(legacyAccessTokenUri) // + .header("Authorization", "Bearer " + bearerToken) // + .GET() // + .timeout(REQ_TIMEOUT) // + .build(); + httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.US_ASCII)) // + .thenAcceptAsync(this::receivedLegacyAccessTokenResponse, Platform::runLater) // + .exceptionally(this::retrievalFailed); + } + + /** + * LEGACY FALLBACK (Response) + * + * @param response Response + */ + @Deprecated + private void receivedLegacyAccessTokenResponse(HttpResponse response) { + try { + switch (response.statusCode()) { + case 200 -> receivedLegacyAccessTokenSuccess(response.body()); case 402 -> licenseExceeded(); case 403, 410 -> accessNotGranted(); // or vault has been archived, effectively disallowing access - case 404 -> needsDeviceRegistration(); + case 404 -> needsLegacyDeviceRegistration(); default -> throw new IOException("Unexpected response " + response.statusCode()); } } catch (IOException e) { @@ -85,10 +197,11 @@ public class ReceiveKeyController implements FxController { } } - private void retrievalSucceeded(HttpResponse response) throws IOException { + @Deprecated + private void receivedLegacyAccessTokenSuccess(String rawToken) throws IOException { try { - var string = HttpHelper.readBody(response); - result.complete(JWEObject.parse(string)); + var token = JWEObject.parse(rawToken); + result.complete(ReceivedKey.legacyDeviceKey(token)); window.close(); } catch (ParseException e) { throw new IOException("Failed to parse JWE", e); @@ -99,8 +212,9 @@ public class ReceiveKeyController implements FxController { window.setScene(invalidLicenseScene.get()); } - private void needsDeviceRegistration() { - window.setScene(registerDeviceScene.get()); + @Deprecated + private void needsLegacyDeviceRegistration() { + window.setScene(legacyRegisterDeviceScene.get()); } private void accessNotGranted() { @@ -132,14 +246,17 @@ public class ReceiveKeyController implements FxController { private static URI getVaultBaseUri(Vault vault) { try { - var kid = vault.getVaultConfigCache().get().getKeyId(); - assert kid.getScheme().startsWith(SCHEME_PREFIX); - var hubUriScheme = kid.getScheme().substring(SCHEME_PREFIX.length()); - return new URI(hubUriScheme, kid.getSchemeSpecificPart(), kid.getFragment()); + 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); } } + + @JsonIgnoreProperties(ignoreUnknown = true) + private record DeviceDto(@JsonProperty(value = "userPrivateKey", required = true) String userPrivateKey) {} } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceivedKey.java b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceivedKey.java new file mode 100644 index 000000000..74da388d7 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceivedKey.java @@ -0,0 +1,45 @@ +package org.cryptomator.ui.keyloading.hub; + +import com.nimbusds.jose.JWEObject; +import org.cryptomator.cryptolib.api.Masterkey; + +import java.security.interfaces.ECPrivateKey; + +@FunctionalInterface +interface ReceivedKey { + + /** + * Decrypts the vault key. + * + * @param deviceKey This device's private key. + * @return The decrypted vault key + */ + Masterkey decryptMasterkey(ECPrivateKey deviceKey); + + /** + * Creates an unlock response object from the user key + vault key. + * + * @param vaultKeyJwe a JWE containing the symmetric vault key, encrypted for this device's user. + * @param userKeyJwe a JWE containing the user's private key, encrypted for this device. + * @return Ciphertext received by Hub, which can be decrypted using this device's private key. + */ + static ReceivedKey vaultKeyAndUserKey(JWEObject vaultKeyJwe, JWEObject userKeyJwe) { + return deviceKey -> { + var userKey = JWEHelper.decryptUserKey(userKeyJwe, deviceKey); + return JWEHelper.decryptVaultKey(vaultKeyJwe, userKey); + }; + } + + /** + * Creates an unlock response object from the received legacy "access token" JWE. + * + * @param vaultKeyJwe a JWE containing the symmetric vault key, encrypted for this device. + * @return Ciphertext received by Hub, which can be decrypted using this device's private key. + * @deprecated Only for compatibility with Hub 1.0 - 1.2 + */ + @Deprecated + static ReceivedKey legacyDeviceKey(JWEObject vaultKeyJwe) { + return deviceKey -> JWEHelper.decryptVaultKey(vaultKeyJwe, deviceKey); + } + +} 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 6fa6aa424..837dc5032 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java @@ -1,7 +1,7 @@ package org.cryptomator.ui.keyloading.hub; -import com.auth0.jwt.JWT; -import com.auth0.jwt.interfaces.DecodedJWT; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.io.BaseEncoding; @@ -20,6 +20,7 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Named; import javafx.application.Platform; +import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.fxml.FXML; @@ -31,14 +32,16 @@ import javafx.stage.Stage; import javafx.stage.WindowEvent; import java.io.IOException; import java.net.InetAddress; -import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; -import java.util.List; +import java.text.ParseException; +import java.time.Duration; +import java.time.Instant; 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; @@ -47,7 +50,7 @@ public class RegisterDeviceController implements FxController { private static final Logger LOG = LoggerFactory.getLogger(RegisterDeviceController.class); private static final ObjectMapper JSON = new ObjectMapper().setDefaultLeniency(true); - private static final List EXPECTED_RESPONSE_CODES = List.of(201, 409); + private static final Duration REQ_TIMEOUT = Duration.ofSeconds(10); private final Stage window; private final HubConfig hubConfig; @@ -55,26 +58,27 @@ public class RegisterDeviceController implements FxController { private final Lazy registerSuccessScene; private final Lazy registerFailedScene; private final String deviceId; - private final P384KeyPair keyPair; - private final CompletableFuture result; - private final DecodedJWT jwt; + private final P384KeyPair deviceKeyPair; + private final CompletableFuture result; private final HttpClient httpClient; - private final BooleanProperty deviceNameAlreadyExists = new SimpleBooleanProperty(false); + private final BooleanProperty deviceNameAlreadyExists = new SimpleBooleanProperty(false); + private final BooleanProperty invalidSetupCode = new SimpleBooleanProperty(false); + private final BooleanProperty workInProgress = new SimpleBooleanProperty(false); + public TextField setupCodeField; public TextField deviceNameField; 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) { this.window = window; this.hubConfig = hubConfig; this.deviceId = deviceId; - this.keyPair = Objects.requireNonNull(deviceKey.get()); + this.deviceKeyPair = Objects.requireNonNull(deviceKey.get()); this.result = result; this.bearerToken = Objects.requireNonNull(bearerToken.get()); this.registerSuccessScene = registerSuccessScene; this.registerFailedScene = registerFailedScene; - this.jwt = JWT.decode(this.bearerToken); this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed); this.httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).executor(executor).build(); } @@ -82,6 +86,13 @@ public class RegisterDeviceController implements FxController { 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); + var missingSetupCode = setupCodeField.textProperty().isEmpty(); + var missingDeviceName = deviceNameField.textProperty().isEmpty(); + registerBtn.disableProperty().bind(workInProgress.or(missingSetupCode).or(missingDeviceName)); + registerBtn.contentDisplayProperty().bind(Bindings.when(workInProgress).then(ContentDisplay.LEFT).otherwise(ContentDisplay.TEXT_ONLY)); } private String determineHostname() { @@ -95,35 +106,62 @@ public class RegisterDeviceController implements FxController { @FXML public void register() { - deviceNameAlreadyExists.set(false); - registerBtn.setContentDisplay(ContentDisplay.LEFT); - registerBtn.setDisable(true); + workInProgress.set(true); - var keyUri = URI.create(hubConfig.devicesResourceUrl + deviceId); - var deviceKey = keyPair.getPublic().getEncoded(); - var dto = new CreateDeviceDto(deviceId, deviceNameField.getText(), BaseEncoding.base64Url().omitPadding().encode(deviceKey)); - var json = toJson(dto); - var request = HttpRequest.newBuilder(keyUri) // + var apiRootUrl = hubConfig.getApiBaseUrl(); + + var userReq = HttpRequest.newBuilder(apiRootUrl.resolve("users/me")) // + .GET() // + .timeout(REQ_TIMEOUT) // .header("Authorization", "Bearer " + bearerToken) // - .header("Content-Type", "application/json").PUT(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) // + .header("Content-Type", "application/json") // .build(); - httpClient.sendAsync(request, HttpResponse.BodyHandlers.discarding()) // + httpClient.sendAsync(userReq, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)) // .thenApply(response -> { - if (EXPECTED_RESPONSE_CODES.contains(response.statusCode())) { - return response; + if (response.statusCode() == 200) { + var dto = fromJson(response.body()); + return Objects.requireNonNull(dto, "null or empty response body"); } else { throw new RuntimeException("Server answered with unexpected status code " + response.statusCode()); } - }).handleAsync((response, throwable) -> { + }).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 + var userKey = JWEHelper.decryptUserKey(JWEObject.parse(user.privateKey), setupCodeField.getText()); + return JWEHelper.encryptUserKey(userKey, deviceKeyPair.getPublic()); + } catch (ParseException 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 putDeviceReq = HttpRequest.newBuilder(deviceUri) // + .PUT(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) // + .timeout(REQ_TIMEOUT) // + .header("Authorization", "Bearer " + bearerToken) // + .header("Content-Type", "application/json") // + .build(); + return httpClient.sendAsync(putDeviceReq, HttpResponse.BodyHandlers.discarding()); + }).whenCompleteAsync((response, throwable) -> { if (response != null) { this.handleResponse(response); } else { - this.registrationFailed(throwable); + this.setupFailed(throwable); } - return null; + workInProgress.set(false); }, Platform::runLater); } + private UserDto fromJson(String json) { + try { + return JSON.reader().readValue(json, UserDto.class); + } catch (IOException e) { + throw new IllegalStateException("Failed to deserialize DTO", e); + } + } + private String toJson(CreateDeviceDto dto) { try { return JSON.writer().writeValueAsString(dto); @@ -132,23 +170,26 @@ public class RegisterDeviceController implements FxController { } } - private void handleResponse(HttpResponse voidHttpResponse) { - assert EXPECTED_RESPONSE_CODES.contains(voidHttpResponse.statusCode()); - - if (voidHttpResponse.statusCode() == 409) { - deviceNameAlreadyExists.set(true); - registerBtn.setContentDisplay(ContentDisplay.TEXT_ONLY); - registerBtn.setDisable(false); - } else { + private void handleResponse(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); + } else { + setupFailed(new IllegalStateException("Unexpected http status code " + response.statusCode())); } } - private void registrationFailed(Throwable cause) { - LOG.warn("Device registration failed.", cause); - window.setScene(registerFailedScene.get()); - result.completeExceptionally(cause); + private void setupFailed(Throwable cause) { + switch (cause) { + case CompletionException e when e.getCause() instanceof JWEHelper.InvalidJweKeyException -> invalidSetupCode.set(true); + default -> { + LOG.warn("Device setup failed.", cause); + window.setScene(registerFailedScene.get()); + result.completeExceptionally(cause); + } + } } @FXML @@ -160,13 +201,6 @@ public class RegisterDeviceController implements FxController { result.cancel(true); } - /* Getter */ - - public String getUserName() { - return jwt.getClaim("email").asString(); - } - - //--- Getters & Setters public BooleanProperty deviceNameAlreadyExistsProperty() { @@ -177,5 +211,21 @@ public class RegisterDeviceController implements FxController { return deviceNameAlreadyExists.get(); } + public BooleanProperty invalidSetupCodeProperty() { + return invalidSetupCode; + } + public boolean isInvalidSetupCode() { + return invalidSetupCode.get(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private record UserDto(String id, String name, String publicKey, String privateKey, String setupCode) {} + + private record CreateDeviceDto(@JsonProperty(required = true) String id, // + @JsonProperty(required = true) String name, // + @JsonProperty(required = true) String publicKey, // + @JsonProperty(required = true, defaultValue = "DESKTOP") String type, // + @JsonProperty(required = true) String userPrivateKey, // + @JsonProperty(required = true) String creationTime) {} } 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 8a4278d72..57150390c 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterFailedController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/RegisterFailedController.java @@ -12,10 +12,10 @@ import java.util.concurrent.CompletableFuture; public class RegisterFailedController implements FxController { private final Stage window; - private final CompletableFuture result; + private final CompletableFuture result; @Inject - public RegisterFailedController(@KeyLoading Stage window, CompletableFuture result) { + public RegisterFailedController(@KeyLoading Stage window, CompletableFuture result) { this.window = window; this.result = result; } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/UnauthorizedDeviceController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/UnauthorizedDeviceController.java index 1a7cbab02..c42ee1cd7 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/UnauthorizedDeviceController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/UnauthorizedDeviceController.java @@ -15,10 +15,10 @@ import java.util.concurrent.CompletableFuture; public class UnauthorizedDeviceController implements FxController { private final Stage window; - private final CompletableFuture result; + private final CompletableFuture result; @Inject - public UnauthorizedDeviceController(@KeyLoading Stage window, CompletableFuture result) { + public UnauthorizedDeviceController(@KeyLoading Stage window, CompletableFuture result) { this.window = window; this.result = result; this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed); diff --git a/src/main/java/org/cryptomator/ui/mainwindow/ResizeController.java b/src/main/java/org/cryptomator/ui/mainwindow/ResizeController.java index b136fa55c..2c3838ea0 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/ResizeController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/ResizeController.java @@ -7,7 +7,6 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import javafx.beans.binding.BooleanBinding; -import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.geometry.Rectangle2D; import javafx.scene.input.MouseEvent; @@ -67,37 +66,7 @@ public class ResizeController implements FxController { return (settings.windowHeight.get() == 0) && (settings.windowWidth.get() == 0) && (settings.windowXPosition.get() == 0) && (settings.windowYPosition.get() == 0); } - private boolean isWithinDisplayBounds() { - // (x1, y1) is the top left corner of the window, (x2, y2) is the bottom right corner - final double slack = 10; - final double width = window.getWidth() - 2 * slack; - final double height = window.getHeight() - 2 * slack; - final double x1 = window.getX() + slack; - final double y1 = window.getY() + slack; - final double x2 = x1 + width; - final double y2 = y1 + height; - - final ObservableList screens = Screen.getScreensForRectangle(x1, y1, width, height); - - // Find the total visible area of the window - double visibleArea = 0; - for (Screen screen : screens) { - Rectangle2D bounds = screen.getVisualBounds(); - - double xOverlap = Math.min(x2, bounds.getMaxX()) - Math.max(x1, bounds.getMinX()); - double yOverlap = Math.min(y2, bounds.getMaxY()) - Math.max(y1, bounds.getMinY()); - - visibleArea += xOverlap * yOverlap; - } - - final double windowArea = width * height; - - // Within bounds if the visible area matches the window area - return visibleArea == windowArea; - } - private void checkDisplayBounds(WindowEvent evt) { - // Minimizing a window in Windows and closing it could result in an out of bounds position at (x, y) = (-32000, -32000) // See https://devblogs.microsoft.com/oldnewthing/20041028-00/?p=37453 // If the position is (-32000, -32000), restore to the last saved position @@ -108,8 +77,9 @@ public class ResizeController implements FxController { window.setHeight(settings.windowHeight.get()); } - if (!isWithinDisplayBounds()) { + if (isOutOfDisplayBounds()) { // If the position is illegal, then the window appears on the main screen in the middle of the window. + LOG.debug("Resetting window position due to insufficient screen overlap"); Rectangle2D primaryScreenBounds = Screen.getPrimary().getBounds(); window.setX((primaryScreenBounds.getWidth() - window.getMinWidth()) / 2); window.setY((primaryScreenBounds.getHeight() - window.getMinHeight()) / 2); @@ -119,6 +89,22 @@ public class ResizeController implements FxController { } } + private boolean isOutOfDisplayBounds() { + // define a rect which is inset on all sides from the window's rect: + final double x = window.getX() + 20; // 20px left + final double y = window.getY() + 5; // 5px top + final double w = window.getWidth() - 40; // 20px left + 20px right + final double h = window.getHeight() - 25; // 5px top + 20px bottom + return isRectangleOutOfScreen(x, y, 0, h) // Left pixel column + || isRectangleOutOfScreen(x + w, y, 0, h) // Right pixel column + || isRectangleOutOfScreen(x, y, w, 0) // Top pixel row + || isRectangleOutOfScreen(x, y + h, w, 0); // Bottom pixel row + } + + private boolean isRectangleOutOfScreen(double x, double y, double width, double height) { + return Screen.getScreensForRectangle(x, y, width, height).isEmpty(); + } + private void startResize(MouseEvent evt) { origX = window.getX(); origY = window.getY(); diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java index 357222b33..8f0a91d58 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java @@ -20,9 +20,10 @@ import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; -import javafx.event.Event; import javafx.fxml.FXML; +import javafx.geometry.Side; import javafx.scene.control.Button; +import javafx.scene.control.ContextMenu; import javafx.scene.control.ListView; import javafx.scene.input.ContextMenuEvent; import javafx.scene.input.DragEvent; @@ -67,6 +68,8 @@ public class VaultListController implements FxController { public ListView vaultList; public StackPane root; public Button addVaultBtn; + @FXML + private ContextMenu addVaultContextMenu; @Inject VaultListController(@MainWindow Stage mainWindow, // @@ -140,15 +143,15 @@ public class VaultListController implements FxController { root.setOnDragOver(this::handleDragEvent); root.setOnDragDropped(this::handleDragEvent); root.setOnDragExited(this::handleDragEvent); - - addVaultBtn.addEventFilter(ContextMenuEvent.CONTEXT_MENU_REQUESTED, Event::consume); } @FXML - private void showMenu() { - double screenX = addVaultBtn.localToScreen(addVaultBtn.getBoundsInLocal()).getMinX(); - double screenY = addVaultBtn.localToScreen(addVaultBtn.getBoundsInLocal()).getMaxY(); - addVaultBtn.getContextMenu().show(addVaultBtn, screenX, screenY); + private void toggleMenu() { + if (addVaultContextMenu.isShowing()) { + addVaultContextMenu.hide(); + } else { + addVaultContextMenu.show(addVaultBtn, Side.BOTTOM, 0.0, 0.0); + } } private void deselect(MouseEvent released) { diff --git a/src/main/java/org/cryptomator/ui/preferences/PreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/PreferencesController.java index caaaf7d80..0937fccd9 100644 --- a/src/main/java/org/cryptomator/ui/preferences/PreferencesController.java +++ b/src/main/java/org/cryptomator/ui/preferences/PreferencesController.java @@ -1,5 +1,6 @@ package org.cryptomator.ui.preferences; +import org.cryptomator.common.Environment; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.fxapp.UpdateChecker; import org.slf4j.Logger; @@ -19,6 +20,7 @@ public class PreferencesController implements FxController { private static final Logger LOG = LoggerFactory.getLogger(PreferencesController.class); + private final Environment env; private final Stage window; private final ObjectProperty selectedTabProperty; private final BooleanBinding updateAvailable; @@ -31,7 +33,8 @@ public class PreferencesController implements FxController { public Tab aboutTab; @Inject - public PreferencesController(@PreferencesWindow Stage window, ObjectProperty selectedTabProperty, UpdateChecker updateChecker) { + public PreferencesController(Environment env, @PreferencesWindow Stage window, ObjectProperty selectedTabProperty, UpdateChecker updateChecker) { + this.env = env; this.window = window; this.selectedTabProperty = selectedTabProperty; this.updateAvailable = updateChecker.latestVersionProperty().isNotNull(); @@ -42,6 +45,9 @@ public class PreferencesController implements FxController { window.setOnShowing(this::windowWillAppear); selectedTabProperty.addListener(observable -> this.selectChosenTab()); tabPane.getSelectionModel().selectedItemProperty().addListener(observable -> this.selectedTabChanged()); + if (env.disableUpdateCheck()) { + tabPane.getTabs().remove(updatesTab); + } } private void selectChosenTab() { diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java index 73279396d..8f5bb0500 100644 --- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java @@ -8,6 +8,7 @@ import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.MasterkeyFileAccess; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; import javax.inject.Inject; import javax.inject.Singleton; @@ -58,7 +59,7 @@ public class RecoveryKeyFactory { } } - // visible for testing + @VisibleForTesting String createRecoveryKey(byte[] rawKey) { Preconditions.checkArgument(rawKey.length == 64, "key should be 64 bytes"); byte[] paddedKey = Arrays.copyOf(rawKey, 66); diff --git a/src/main/resources/fxml/hub_register_device.fxml b/src/main/resources/fxml/hub_legacy_register_device.fxml similarity index 96% rename from src/main/resources/fxml/hub_register_device.fxml rename to src/main/resources/fxml/hub_legacy_register_device.fxml index 8db67c272..51d4cf8b7 100644 --- a/src/main/resources/fxml/hub_register_device.fxml +++ b/src/main/resources/fxml/hub_legacy_register_device.fxml @@ -15,7 +15,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/vault_list.fxml b/src/main/resources/fxml/vault_list.fxml index 146fa877c..f9cb29258 100644 --- a/src/main/resources/fxml/vault_list.fxml +++ b/src/main/resources/fxml/vault_list.fxml @@ -28,27 +28,27 @@ - + + + + + + + + + + + + + + + + diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties index bba386cee..aee61fe19 100644 --- a/src/main/resources/i18n/strings.properties +++ b/src/main/resources/i18n/strings.properties @@ -154,9 +154,11 @@ hub.auth.loginLink=Not redirected? Click here to open it. hub.receive.message=Processing response… hub.receive.description=Cryptomator is receiving and processing the response from Hub. Please wait. ### Register Device -hub.register.message=Device name required -hub.register.description=This seems to be the first Hub access from this device. In order to identify it for access authorization, you need to name this device. +hub.register.message=New Device +hub.register.description=This is the first Hub access from this device. Please authorize it using your setup code. hub.register.nameLabel=Device Name +hub.register.setupCodeLabel=Setup Code +hub.register.invalidSetupCode=Invalid Setup Code hub.register.occupiedMsg=Name already in use hub.register.registerBtn=Confirm ### Registration Success diff --git a/src/main/resources/i18n/strings_bg.properties b/src/main/resources/i18n/strings_bg.properties index ace7569d6..172b27b5c 100644 --- a/src/main/resources/i18n/strings_bg.properties +++ b/src/main/resources/i18n/strings_bg.properties @@ -153,6 +153,7 @@ hub.invalidLicense.message=Лиценза за Hub е недействителе # Lock ## Force +lock.forced.retryBtn=Повторен опит ## Failure # Migration diff --git a/src/main/resources/i18n/strings_zh_TW.properties b/src/main/resources/i18n/strings_zh_TW.properties index 0aede0ed4..3f2ee62fd 100644 --- a/src/main/resources/i18n/strings_zh_TW.properties +++ b/src/main/resources/i18n/strings_zh_TW.properties @@ -22,6 +22,9 @@ error.hyperlink.report=回報錯誤 error.technicalDetails=詳情: error.existingSolutionDescription=Cryptomator 沒有預料到會發生這種情況。但我們找到了一個現有的解決方案來解決這個錯誤。請查看以下連結。 error.hyperlink.solution=查詢解決方案 +error.lookupPermissionMessage=Cryptomator 可以在線查找此問題的解決方案。 這將從您的 IP 地址向我們的問題數據庫發送請求。 +error.dismiss=忽略 +error.lookUpSolution=查詢解決方案 # Defaults defaults.vault.vaultName=加密檔案庫 @@ -134,6 +137,7 @@ unlock.error.customPath.message=無法將檔案庫掛載至自訂路徑 unlock.error.customPath.description.notSupported=如果要繼續使用自訂的掛載路徑,必須變更成支援的磁區空間類型,不然就必須使用不同的掛載路徑 unlock.error.customPath.description.notExists=自訂的掛載路徑並不存在‧ 請在本機創立該路徑,或者在加密庫選項中更改 unlock.error.customPath.description.inUse=磁碟代號或自訂掛載路徑「%s」已被使用。 +unlock.error.customPath.description.hideawayNotDir=無法移除用於解鎖的臨時隱藏檔案「%3$s」。請檢查該檔案,然後手動刪除。 unlock.error.customPath.description.couldNotBeCleaned=無法將您的保險庫掛載至路徑「%s」。請再試一次或選擇不同的路徑。 unlock.error.customPath.description.notEmptyDir=自訂掛載路徑「%s」不是一個空資料夾。請選擇一個空資料夾並重試。 unlock.error.customPath.description.generic=您為此保險庫選擇了自訂掛載路徑,但使用時出現了錯誤訊息:%2$s diff --git a/src/test/java/org/cryptomator/ui/keyloading/hub/JWEHelperTest.java b/src/test/java/org/cryptomator/ui/keyloading/hub/JWEHelperTest.java index 3d495e8c1..a2e46cc28 100644 --- a/src/test/java/org/cryptomator/ui/keyloading/hub/JWEHelperTest.java +++ b/src/test/java/org/cryptomator/ui/keyloading/hub/JWEHelperTest.java @@ -4,6 +4,7 @@ import com.nimbusds.jose.JWEObject; import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.cryptomator.cryptolib.common.P384KeyPair; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -15,18 +16,106 @@ import java.text.ParseException; import java.util.Arrays; import java.util.Base64; +@SuppressWarnings("resource") public class JWEHelperTest { - private static final String JWE = "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IllUcEY3bGtTc3JvZVVUVFdCb21LNzBTN0FhVTJyc0ptMURpZ1ZzbjRMY2F5eUxFNFBabldkYmFVcE9jQVV5a1ciLCJ5IjoiLU5pS3loUktjSk52Nm02Z0ZJUWc4cy1Xd1VXUW9uT3A5dkQ4cHpoa2tUU3U2RzFlU2FUTVlhZGltQ2Q4V0ExMSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..BECWGzd9UvhHcTJC.znt4TlS-qiNEjxiu2v-du_E1QOBnyBR6LCt865SHxD-kwRc1JwX_Lq9XVoFj2GnK9-9CgxhCLGurg5Jt9g38qv2brGAzWL7eSVeY1fIqdO_kUhLpGslRTN6h2U0NHJi2-iE.WDVI2kOk9Dy3PWHyIg8gKA"; + // key pairs from frontend tests (crypto.spec.ts): + private static final String USER_PRIV_KEY = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDCi4K1Ts3DgTz/ufkLX7EGMHjGpJv+WJmFgyzLwwaDFSfLpDw0Kgf3FKK+LAsV8r+hZANiAARLOtFebIjxVYUmDV09Q1sVxz2Nm+NkR8fu6UojVSRcCW13tEZatx8XGrIY9zC7oBCEdRqDc68PMSvS5RA0Pg9cdBNc/kgMZ1iEmEv5YsqOcaNADDSs0bLlXb35pX7Kx5Y="; + private static final String USER_PUB_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAESzrRXmyI8VWFJg1dPUNbFcc9jZvjZEfH7ulKI1UkXAltd7RGWrcfFxqyGPcwu6AQhHUag3OvDzEr0uUQND4PXHQTXP5IDGdYhJhL+WLKjnGjQAw0rNGy5V29+aV+yseW"; + private static final String DEVICE_PRIV_KEY = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDB2bmFCWy2p+EbAn8NWS5Om+GA7c5LHhRZb8g2pSMSf0fsd7k7dZDVrnyHFiLdd/YGhZANiAAR6bsjTEdXKWIuu1Bvj6Y8wySlIROy7YpmVZTY128ItovCD8pcR4PnFljvAIb2MshCdr1alX4g6cgDOqcTeREiObcSfucOU9Ry1pJ/GnX6KA0eSljrk6rxjSDos8aiZ6Mg="; + private static final String DEVICE_PUB_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEem7I0xHVyliLrtQb4+mPMMkpSETsu2KZlWU2NdvCLaLwg/KXEeD5xZY7wCG9jLIQna9WpV+IOnIAzqnE3kRIjm3En7nDlPUctaSfxp1+igNHkpY65Oq8Y0g6LPGomejI"; + + // used for JWE generation in frontend: (jwe.spec.ts): private static final String PRIV_KEY = "ME8CAQAwEAYHKoZIzj0CAQYFK4EEACIEODA2AgEBBDEA6QybmBitf94veD5aCLr7nlkF5EZpaXHCfq1AXm57AKQyGOjTDAF9EQB28fMywTDQ"; private static final String PUB_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERxQR+NRN6Wga01370uBBzr2NHDbKIC56tPUEq2HX64RhITGhii8Zzbkb1HnRmdF0aq6uqmUy4jUhuxnKxsv59A6JeK7Unn+mpmm3pQAygjoGc9wrvoH4HWJSQYUlsXDu"; @Test - public void testDecrypt() throws ParseException, InvalidKeySpecException { - var jwe = JWEObject.parse(JWE); - var keyPair = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(PRIV_KEY))); + @DisplayName("decryptUserKey with device key") + public void testDecryptUserKeyECDHES() throws ParseException, InvalidKeySpecException { + var jwe = JWEObject.parse(""" + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrZXlfb3BzIjpbXSwiZXh0Ijp\ + 0cnVlLCJrdHkiOiJFQyIsIngiOiJoeHpiSWh6SUJza3A5ZkZFUmJSQ2RfOU1fbWYxNElqaDZhcnNoVX\ + NkcEEyWno5ejZZNUs4NHpZR2I4b2FHemNUIiwieSI6ImJrMGRaNWhpelZ0TF9hN2hNejBjTUduNjhIR\ + jZFdWlyNHdlclNkTFV5QWd2NWUzVzNYSG5sdHJ2VlRyU3pzUWYiLCJjcnYiOiJQLTM4NCJ9LCJhcHUi\ + OiIiLCJhcHYiOiIifQ..pu3Q1nR_yvgRAapG.4zW0xm0JPxbcvZ66R-Mn3k841lHelDQfaUvsZZAtWs\ + L2w4FMi6H_uu6ArAWYLtNREa_zfcPuyuJsFferYPSNRUWt4OW6aWs-l_wfo7G1ceEVxztQXzQiwD30U\ + TA8OOdPcUuFfEq2-d9217jezrcyO6m6FjyssEZIrnRArUPWKzGdghXccGkkf0LTZcGJoHeKal-RtyP8\ + PfvEAWTjSOCpBlSdUJ-1JL3tyd97uVFNaVuH3i7vvcMoUP_bdr0XW3rvRgaeC6X4daPLUvR1hK5Msut\ + QMtM2vpFghS_zZxIQRqz3B2ECxa9Bjxhmn8kLX5heZ8fq3lH-bmJp1DxzZ4V1RkWk.yVwXG9yARa5Ih\ + q2koh2NbQ"""); + var deviceKeyPair = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(DEVICE_PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(DEVICE_PRIV_KEY))); - var masterkey = JWEHelper.decrypt(jwe, keyPair.getPrivate()); + var userKey = JWEHelper.decryptUserKey(jwe, deviceKeyPair.getPrivate()); + + Assertions.assertArrayEquals(Base64.getDecoder().decode(USER_PRIV_KEY), userKey.getEncoded()); + } + + @Test + @DisplayName("decryptUserKey with incorrect device key") + public void testDecryptUserKeyECDHESWrongKey() throws ParseException, InvalidKeySpecException { + var jwe = JWEObject.parse(""" + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrZXlfb3BzIjpbXSwiZXh0Ijp\ + 0cnVlLCJrdHkiOiJFQyIsIngiOiJoeHpiSWh6SUJza3A5ZkZFUmJSQ2RfOU1fbWYxNElqaDZhcnNoVX\ + NkcEEyWno5ejZZNUs4NHpZR2I4b2FHemNUIiwieSI6ImJrMGRaNWhpelZ0TF9hN2hNejBjTUduNjhIR\ + jZFdWlyNHdlclNkTFV5QWd2NWUzVzNYSG5sdHJ2VlRyU3pzUWYiLCJjcnYiOiJQLTM4NCJ9LCJhcHUi\ + OiIiLCJhcHYiOiIifQ..pu3Q1nR_yvgRAapG.4zW0xm0JPxbcvZ66R-Mn3k841lHelDQfaUvsZZAtWs\ + L2w4FMi6H_uu6ArAWYLtNREa_zfcPuyuJsFferYPSNRUWt4OW6aWs-l_wfo7G1ceEVxztQXzQiwD30U\ + TA8OOdPcUuFfEq2-d9217jezrcyO6m6FjyssEZIrnRArUPWKzGdghXccGkkf0LTZcGJoHeKal-RtyP8\ + PfvEAWTjSOCpBlSdUJ-1JL3tyd97uVFNaVuH3i7vvcMoUP_bdr0XW3rvRgaeC6X4daPLUvR1hK5Msut\ + QMtM2vpFghS_zZxIQRqz3B2ECxa9Bjxhmn8kLX5heZ8fq3lH-bmJp1DxzZ4V1RkWk.yVwXG9yARa5Ih\ + q2koh2NbQ"""); + var userKeyPair = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(USER_PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(USER_PRIV_KEY))); + var incorrectDevicePrivateKey = userKeyPair.getPrivate(); + + Assertions.assertThrows(JWEHelper.InvalidJweKeyException.class, () -> JWEHelper.decryptUserKey(jwe, incorrectDevicePrivateKey)); + } + + @Test + @DisplayName("decryptUserKey with setup code") + public void testDecryptUserKeyPBES2() throws ParseException { + var jwe = JWEObject.parse(""" + eyJhbGciOiJQQkVTMi1IUzUxMitBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIiwicDJzIjoiT3hMY0Q\ + xX1pCODc1c2hvUWY2Q1ZHQSIsInAyYyI6MTAwMCwiYXB1IjoiIiwiYXB2IjoiIn0.FD4fcrP4Pb\ + aKOQ9ZfXl0gpMM6Fa2rfqAvL0K5ZyYUiVeHCNV-A02Rg.urT1ShSv6qQxh8X7.gEqAiUWD98a2E\ + P7ITCPTw4DJo6-BpqrxA73D6gNIj9z4d1hN-EP99Q4mWBWLH97H8ugbG5rGsm8xsjsBqpWORQqF\ + mJZR2AhlPiwFaC7n_MDDBupSy_swDnCfj731Lal297IP5WbkFcmozKsyhmwdkctxjf_VHA.fJki\ + kDjUaxwUKqpvT7qaAQ + """); + + var userKey = JWEHelper.decryptUserKey(jwe, "123456"); + + Assertions.assertArrayEquals(Base64.getDecoder().decode(PRIV_KEY), userKey.getEncoded()); + } + + @Test + @DisplayName("decryptUserKey with incorrect setup code") + public void testDecryptUserKeyPBES2WrongKey() throws ParseException { + var jwe = JWEObject.parse(""" + eyJhbGciOiJQQkVTMi1IUzUxMitBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIiwicDJzIjoiT3hMY0Q\ + xX1pCODc1c2hvUWY2Q1ZHQSIsInAyYyI6MTAwMCwiYXB1IjoiIiwiYXB2IjoiIn0.FD4fcrP4Pb\ + aKOQ9ZfXl0gpMM6Fa2rfqAvL0K5ZyYUiVeHCNV-A02Rg.urT1ShSv6qQxh8X7.gEqAiUWD98a2E\ + P7ITCPTw4DJo6-BpqrxA73D6gNIj9z4d1hN-EP99Q4mWBWLH97H8ugbG5rGsm8xsjsBqpWORQqF\ + mJZR2AhlPiwFaC7n_MDDBupSy_swDnCfj731Lal297IP5WbkFcmozKsyhmwdkctxjf_VHA.fJki\ + kDjUaxwUKqpvT7qaAQ + """); + + Assertions.assertThrows(JWEHelper.InvalidJweKeyException.class, () -> JWEHelper.decryptUserKey(jwe, "654321")); + } + + @Test + @DisplayName("decryptVaultKey") + public void testDecryptVaultKey() throws ParseException, InvalidKeySpecException { + var jwe = JWEObject.parse(""" + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlA\ + tMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IllUcEY3bGtTc3JvZVVUVFdCb21LNzBTN0\ + FhVTJyc0ptMURpZ1ZzbjRMY2F5eUxFNFBabldkYmFVcE9jQVV5a1ciLCJ5IjoiLU5pS3loUktjSk52N\ + m02Z0ZJUWc4cy1Xd1VXUW9uT3A5dkQ4cHpoa2tUU3U2RzFlU2FUTVlhZGltQ2Q4V0ExMSJ9LCJhcHUi\ + OiIiLCJhcHYiOiIifQ..BECWGzd9UvhHcTJC.znt4TlS-qiNEjxiu2v-du_E1QOBnyBR6LCt865SHxD\ + -kwRc1JwX_Lq9XVoFj2GnK9-9CgxhCLGurg5Jt9g38qv2brGAzWL7eSVeY1fIqdO_kUhLpGslRTN6h2\ + U0NHJi2-iE.WDVI2kOk9Dy3PWHyIg8gKA"""); + var privateKey = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(PRIV_KEY))).getPrivate(); + + var masterkey = JWEHelper.decryptVaultKey(jwe, privateKey); var expectedEncKey = new byte[32]; var expectedMacKey = new byte[32]; @@ -44,13 +133,11 @@ public class JWEHelperTest { "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IkJyYm9UQkl5Y0NDUEdJQlBUekU2RjBnbTRzRjRCamZPN1I0a2x0aWlCaThKZkxxcVdXNVdUSVBLN01yMXV5QVUiLCJ5IjoiNUpGVUI0WVJiYjM2RUZpN2Y0TUxMcFFyZXd2UV9Tc3dKNHRVbFd1a2c1ZU04X1ZyM2pkeml2QXI2WThRczVYbSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..QEq4Z2m6iwBx2ioS.IBo8TbKJTS4pug.61Z-agIIXgP8bX10O_yEMA", // json payload field "key" not a string "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6ImNZdlVFZm9LYkJjenZySE5zQjUxOGpycUxPMGJDOW5lZjR4NzFFMUQ5dk95MXRqd1piZzV3cFI0OE5nU1RQdHgiLCJ5IjoiaWRJekhCWERzSzR2NTZEeU9yczJOcDZsSG1zb29fMXV0VTlzX3JNdVVkbkxuVXIzUXdLZkhYMWdaVXREM1RKayJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..0VZqu5ei9U3blGtq.eDvhU6drw7mIwvXu6Q.f05QnhI7JWG3IYHvexwdFQ" // json payload field "key" invalid base64 data }) - public void testDecryptInvalid(String malformed) throws ParseException, InvalidKeySpecException { + public void testDecryptInvalidVaultKey(String malformed) throws ParseException, InvalidKeySpecException { var jwe = JWEObject.parse(malformed); - var keyPair = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(PRIV_KEY))); + var privateKey = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(PRIV_KEY))).getPrivate(); - Assertions.assertThrows(MasterkeyLoadingFailedException.class, () -> { - JWEHelper.decrypt(jwe, keyPair.getPrivate()); - }); + Assertions.assertThrows(MasterkeyLoadingFailedException.class, () -> JWEHelper.decryptVaultKey(jwe, privateKey)); } } \ No newline at end of file