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 @@
-
-
+
+
-
+
@@ -26,20 +26,19 @@
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 891096945..ee700a1c2 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -8,7 +8,7 @@
-
+
\ 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 @@
-