Compare commits

..
Author SHA1 Message Date
dependabot[bot]andGitHub 4cfaefe3f7 Bump the github-actions group across 1 directory with 7 updates
Bumps the github-actions group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [actions/checkout](https://github.com/actions/checkout) | `7.0.0` | `7.0.1` |
| [actions/setup-java](https://github.com/actions/setup-java) | `5.3.0` | `6.0.0` |
| [softprops/action-gh-release](https://github.com/softprops/action-gh-release) | `3.0.1` | `3.0.3` |
| [actions/cache](https://github.com/actions/cache) | `5.0.5` | `6.1.0` |
| [fjogeleit/http-request-action](https://github.com/fjogeleit/http-request-action) | `2.0.0` | `2.0.1` |
| [actions/stale](https://github.com/actions/stale) | `10.3.0` | `11.0.0` |
| [vedantmgoyal2009/winget-releaser](https://github.com/vedantmgoyal2009/winget-releaser) | `7bd472be23763def6e16bd06cc8b1cdfab0e2fd5` | `b3a5dae0047c6180023acba3f548c55fdf6b7193` |



Updates `actions/checkout` from 7.0.0 to 7.0.1
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1)

Updates `actions/setup-java` from 5.3.0 to 6.0.0
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/ad2b38190b15e4d6bdf0c97fb4fca8412226d287...dd06d9cba3e5552c54d9f8ea23572deb30010f7c)

Updates `softprops/action-gh-release` from 3.0.1 to 3.0.3
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/718ea10b132b3b2eba29c1007bb80653f286566b...efb35369e0ad2afab669f228072c1b0d510eae64)

Updates `actions/cache` from 5.0.5 to 6.1.0
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/27d5ce7f107fe9357f9df03efb73ab90386fccae...55cc8345863c7cc4c66a329aec7e433d2d1c52a9)

Updates `fjogeleit/http-request-action` from 2.0.0 to 2.0.1
- [Release notes](https://github.com/fjogeleit/http-request-action/releases)
- [Changelog](https://github.com/fjogeleit/http-request-action/blob/main/RELEASE.md)
- [Commits](https://github.com/fjogeleit/http-request-action/compare/551353b829c3646756b2ec2b3694f819d7957495...f98bb26551cd1af7d3eb2674578a80754ece88db)

Updates `actions/stale` from 10.3.0 to 11.0.0
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899...4391f3da665fdf50b6810c1a66712fb9ba21aa93)

Updates `vedantmgoyal2009/winget-releaser` from 7bd472be23763def6e16bd06cc8b1cdfab0e2fd5 to b3a5dae0047c6180023acba3f548c55fdf6b7193
- [Release notes](https://github.com/vedantmgoyal2009/winget-releaser/releases)
- [Commits](https://github.com/vedantmgoyal2009/winget-releaser/compare/7bd472be23763def6e16bd06cc8b1cdfab0e2fd5...b3a5dae0047c6180023acba3f548c55fdf6b7193)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: actions/setup-java
  dependency-version: 5.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: actions/stale
  dependency-version: 11.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: fjogeleit/http-request-action
  dependency-version: 2.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: softprops/action-gh-release
  dependency-version: 3.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: vedantmgoyal2009/winget-releaser
  dependency-version: b3a5dae0047c6180023acba3f548c55fdf6b7193
  dependency-type: direct:production
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-01 16:26:55 +00:00
Tobias Hagemann af99135172 Remove Uruky as gold sponsor [ci skip] 2026-09-01 11:21:33 +02:00
Armin SchrenkandGitHub a02224640b Merge pull request #4295 from cryptomator/feature/improve-tofu-dialog
Increase usablity of TOFU dialog
2026-08-27 15:00:55 +02:00
Armin Schrenk 04fe64b9e0 fix grammar 2026-08-25 17:31:55 +02:00
Armin Schrenk 44434aa404 Improve phrasing 2026-08-25 16:27:03 +02:00
Armin Schrenk 35ac351541 Increase usablity of TOFU dialog
* Add description
* display URLs in legible font
* add copy button for URLs
2026-08-25 14:12:00 +02:00
Armin Schrenk 1eb70aeea7 migrate code to common JDK 25 apis and constructs 2026-08-24 11:14:34 +02:00
Armin Schrenk abcbefdd2c Update org.fasterxml:jackson-* to version 2.21.6 2026-08-18 17:39:07 +02:00
Tobias Hagemann 7af6c79b80 Add Uruky as gold sponsor [ci skip] 2026-07-30 17:11:43 +02:00
Armin SchrenkandGitHub 193aa1887d Merge pull request #4274 from cryptomator/feature/4272-fix-locate-encrypted
Feature: Fix locate encrypted file on Linux
2026-07-17 17:51:53 +02:00
Armin Schrenk 0ce6008576 ensure path is always absolute
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-07-17 17:45:17 +02:00
Armin Schrenk 0bf3e0a071 [skip ci] use maven wrapper in build script
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-07-17 17:43:26 +02:00
Armin Schrenk 3d82e9a97f Fix JfxRevealPathService opens the file instead of revealing
also set priority to "Fallback"

Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-07-16 12:03:46 +02:00
Armin Schrenk bdab18b7c3 update integrations-linux
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-07-16 12:00:06 +02:00
83 changed files with 275 additions and 1210 deletions
+3 -3
View File
@@ -75,9 +75,9 @@ jobs:
openjfx-sha: 'e3fd682354346845d2944a2da2b1ff2b6cb9259d92027f2f9c121b9b93c5e42f'
appimagetool-sha: 'f0837e7448a0c1e4e650a93bb3e85802546e60654ef287576f46c71c126a9158'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Java
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
distribution: ${{ env.JAVA_DIST }}
java-version: ${{ env.JAVA_VERSION }}
@@ -220,7 +220,7 @@ jobs:
if-no-files-found: error
- name: Publish AppImage on GitHub Releases
if: inputs.upload-to-draft
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
with:
draft: true
fail_on_unmatched_files: true
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
pacman-key --populate archlinux
pacman -Syu --noconfirm --needed git base-devel sudo gnupg maven unzip github-cli curl pacman-contrib
- name: Checkout cryptomator/aur-bin
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: 'cryptomator/aur-bin'
token: ${{ secrets.CRYPTOBOT_PR_TOKEN }}
+3 -3
View File
@@ -22,14 +22,14 @@ jobs:
name: Compile and Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
distribution: ${{ env.JAVA_DIST }}
java-version: ${{ env.JAVA_VERSION }}
cache: 'maven'
- name: Cache SonarCloud packages
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
run: echo 'JDK_MAJOR_VERSION=${{ env.JDK_VERSION }}'.substring(0,2) >> "$env:GITHUB_ENV"
shell: pwsh
- name: Checkout latest JDK ${{ env.JDK_MAJOR_VERSION }}
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
java-version: ${{ env.JDK_MAJOR_VERSION}}
distribution: ${{ env.JDK_VENDOR }}
+2 -2
View File
@@ -45,7 +45,7 @@ jobs:
env:
INPUT_PPAVER: ${{ inputs.ppaver }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- id: deb-version
name: Determine deb-version
run: |
@@ -63,7 +63,7 @@ jobs:
env:
DEB_BUILD_DEPENDS: ${{ env.DEB_BUILD_DEPENDS }}
- name: Setup Java
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
distribution: ${{ env.JAVA_DIST }}
java-version: ${{ env.JAVA_VERSION }}
+1 -1
View File
@@ -53,7 +53,7 @@ jobs:
INTERVAL: 900
JSON_DATA: ${{ steps.get-stats.outputs.result }}
- name: Upload Results
uses: fjogeleit/http-request-action@551353b829c3646756b2ec2b3694f819d7957495 # v2.0.0
uses: fjogeleit/http-request-action@f98bb26551cd1af7d3eb2674578a80754ece88db # v2.0.1
with:
url: 'https://graphite-us-central1.grafana.net/metrics'
method: 'POST'
+5 -5
View File
@@ -25,14 +25,14 @@ jobs:
needs: get-version
if: needs.get-version.outputs.versionType != 'unknown'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Check the git tag is signed
run: git cat-file -p "${GITHUB_REF_NAME}" | grep "BEGIN SSH SIGNATURE"
- name: Check the git tag is on release or main branch
run: git branch -r --contains "${GITHUB_REF_NAME}" | grep -E '^\s*origin/(main|release/.*)\s*$'
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
- uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
distribution: ${{ env.JAVA_DIST }}
java-version: ${{ env.JAVA_VERSION }}
@@ -42,7 +42,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
- name: Draft a release
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
with:
draft: true
discussion_category_name: releases
@@ -60,7 +60,7 @@ jobs:
GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }}
- name: Publish asc on GitHub Releases
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
with:
draft: true
fail_on_unmatched_files: true
@@ -112,7 +112,7 @@ jobs:
SEMVER: ${{ needs.get-version.outputs.semVerStr }}
GH_TOKEN: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Compute source tarball SHA256
id: src-sha256
run: |
+2 -2
View File
@@ -39,11 +39,11 @@ jobs:
revNum: ${{ steps.versions.outputs.revNum }}
type: ${{ steps.versions.outputs.type}}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
distribution: ${{ env.JAVA_DIST }}
java-version: ${{ env.JAVA_VERSION }}
+5 -5
View File
@@ -49,16 +49,16 @@ jobs:
env:
SRC_GIT_SHA: ${{ inputs.src-tag || github.sha}}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: flathub/org.cryptomator.Cryptomator
submodules: true
- name: Checkout build script
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: build-scripts
- name: Checkout app source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: cryptomator
ref: ${{ env.SRC_GIT_SHA }}
@@ -176,7 +176,7 @@ jobs:
fi
env:
SRC_TAG: ${{ inputs.src-tag }}
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: flathub/org.cryptomator.Cryptomator
submodules: true #TODO: Update submodule!
@@ -184,7 +184,7 @@ jobs:
- name: Checkout release branch
run: |
git checkout -b release/${{ needs.get-version.outputs.semVerStr }}
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: cryptomator
- name: Download source tarball and compute checksum
+2 -2
View File
@@ -44,7 +44,7 @@ jobs:
pacman-key --init
pacman-key --populate archlinux
pacman -Syu --noconfirm --needed git base-devel sudo gnupg maven unzip
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: cryptomator
- name: Create build user
@@ -108,7 +108,7 @@ jobs:
env:
TAG: ${{ needs.get-version.outputs.semVerStr || github.event.release.tag_name }}
- name: Checkout cryptomator/aur repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: 'cryptomator/aur'
token: ${{ secrets.CRYPTOBOT_PR_TOKEN }}
+3 -3
View File
@@ -79,9 +79,9 @@ jobs:
openjfx-url: 'https://download2.gluonhq.com/openjfx/25.0.3/openjfx-25.0.3_osx-x64_bin-jmods.zip'
openjfx-sha: '3512fabe43aee467538d329cfbbaab3c53dff2a810f0d54e381f461d5e0fac43'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Java
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
distribution: ${{ env.JAVA_DIST }}
java-version: ${{ env.JAVA_VERSION }}
@@ -312,7 +312,7 @@ jobs:
if-no-files-found: error
- name: Publish dmg on GitHub Releases
if: inputs.upload-to-draft
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
with:
draft: true
fail_on_unmatched_files: true
+3 -3
View File
@@ -77,9 +77,9 @@ jobs:
openjfx-url: 'https://download2.gluonhq.com/openjfx/25.0.3/openjfx-25.0.3_osx-aarch64_bin-jmods.zip'
openjfx-sha: 'a52014d625b8b04e57fd71650f881c1397542b4018e1b04f1b4e66c8800a1f34'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Java
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
distribution: ${{ env.JAVA_DIST }}
java-version: ${{ env.JAVA_VERSION }}
@@ -311,7 +311,7 @@ jobs:
if-no-files-found: error
- name: Publish dmg on GitHub Releases
if: inputs.upload-to-draft
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
with:
draft: true
fail_on_unmatched_files: true
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
issues: write
pull-requests: write
steps:
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
with:
days-before-stale: 14
days-before-close: 0
+2 -2
View File
@@ -16,8 +16,8 @@ jobs:
name: Compile and Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
distribution: ${{ env.JAVA_DIST }}
java-version: ${{ env.JAVA_VERSION }}
+2 -2
View File
@@ -19,9 +19,9 @@ jobs:
name: Validate commits pushed to release/hotfix branch to fulfill release requirements
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Java
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
distribution: ${{ env.JAVA_DIST }}
java-version: ${{ env.JAVA_VERSION }}
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
issues: write
pull-requests: write
steps:
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
with:
days-before-stale: 365
days-before-close: 90
+5 -5
View File
@@ -86,9 +86,9 @@ jobs:
java-version: '26.0.1+8'
java-package: 'jdk'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Java
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
distribution: ${{ matrix.java-dist }}
java-version: ${{ matrix.java-version }}
@@ -352,7 +352,7 @@ jobs:
java-version: '26.0.1+8'
java-package: 'jdk'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install wix and extensions
run: |
dotnet tool install --global wix --version ${WIX_VERSION}
@@ -368,7 +368,7 @@ jobs:
- name: Strip version info from msi file name
run: mv dist/win/bundle/resources/Cryptomator*.msi dist/win/bundle/resources/Cryptomator.msi
- name: Setup Java
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
distribution: ${{ matrix.java-dist }}
java-version: ${{ matrix.java-version }}
@@ -481,7 +481,7 @@ jobs:
merge-multiple: true
- name: Publish installers on GitHub Releases
id: publish
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
with:
draft: true
fail_on_unmatched_files: true
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.CRYPTOBOT_PR_TOKEN }}
- name: Submit package
uses: vedantmgoyal2009/winget-releaser@7bd472be23763def6e16bd06cc8b1cdfab0e2fd5 # no_specific_version
uses: vedantmgoyal2009/winget-releaser@b3a5dae0047c6180023acba3f548c55fdf6b7193 # no_specific_version
with:
identifier: Cryptomator.Cryptomator
version: ${{ inputs.tag }}
+1 -1
View File
@@ -8,7 +8,7 @@
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_26" project-jdk-name="temurin-26" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" project-jdk-name="25" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
+2 -2
View File
@@ -9,8 +9,8 @@ Changes to prior versions can be found on the [Github release page](https://gith
## [Unreleased](https://github.com/cryptomator/cryptomator/compare/1.19.3...HEAD)
No changes yet.
### Changed
* Updated Jackson to 2.21.6
## [1.19.3](https://github.com/cryptomator/cryptomator/releases/1.19.3) - 2026-06-29
+2 -2
View File
@@ -1,11 +1,11 @@
[Desktop Entry]
Name=Cryptomator
Comment=Cloud Storage Encryption Utility
Exec=cryptomator %U
Exec=cryptomator %F
Icon=org.cryptomator.Cryptomator
Terminal=false
Type=Application
Categories=Utility;Security;FileTools;
StartupNotify=true
StartupWMClass=org.cryptomator.launcher.Cryptomator$MainApp
MimeType=application/vnd.cryptomator.encrypted;application/vnd.cryptomator.vault;x-scheme-handler/org.cryptomator;
MimeType=application/vnd.cryptomator.encrypted;application/vnd.cryptomator.vault;
+1 -1
View File
@@ -11,7 +11,7 @@ pkgdesc="Multiplatform transparent client-side encryption of your files in the c
arch=('any')
url="https://cryptomator.org/"
license=('GPL3')
depends=('fuse3' 'alsa-lib' 'hicolor-icon-theme' 'libxtst' 'libnet' 'libxrender' 'desktop-file-utils')
depends=('fuse3' 'alsa-lib' 'hicolor-icon-theme' 'libxtst' 'libnet' 'libxrender')
makedepends=('maven' 'unzip')
optdepends=('keepassxc-cryptomator: Use KeePassXC to store vault passwords' 'ttf-hanazono: Install this font when using Japanese system language')
_jdkver=26.0.1+8
-14
View File
@@ -46,20 +46,6 @@
<string>Any</string>
</dict>
</dict>
<!-- register org.cryptomator:// URL scheme -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>org.cryptomator.deeplink</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLSchemes</key>
<array>
<string>org.cryptomator</string>
</array>
</dict>
</array>
<!-- register .cryptomator extension -->
<key>CFBundleDocumentTypes</key>
<array>
-1
View File
@@ -9,4 +9,3 @@ installer
*.jmod
resources/jfxJmods.zip
license.rtf
**/FAvaultFile.properties
+4 -9
View File
@@ -40,11 +40,6 @@ if ((Get-Command "git" -ErrorAction SilentlyContinue) -eq $null)
Write-Error "Unable to find git.exe in your PATH (try: choco install git)"
exit 1
}
if ((Get-Command "mvn" -ErrorAction SilentlyContinue) -eq $null)
{
Write-Error "Unable to find mvn.cmd in your PATH (try: choco install maven)"
exit 1
}
if ((Get-Command 'wix' -ErrorAction SilentlyContinue) -eq $null)
{
Write-Error 'Unable to find wix in your PATH (try: dotnet tool install --global wix --version 6.0.2)'
@@ -65,7 +60,7 @@ if ($wixExtensions -notmatch 'WixToolset.BootstrapperApplications.wixext') {
}
$buildDir = Split-Path -Parent $PSCommandPath
$version = $(mvn -f $buildDir/../../pom.xml help:evaluate -Dexpression="project.version" -q -DforceStdout)
$version = $(../../mvnw.cmd -f $buildDir/../../pom.xml help:evaluate -Dexpression="project.version" -q -DforceStdout)
$semVerNo = $version -replace '(\d+\.\d+\.\d+).*','$1'
$revisionNo = $(git rev-list --count HEAD)
@@ -79,7 +74,7 @@ $copyright = "(C) $CopyrightStartYear - $((Get-Date).Year) $Vendor"
# compile
Invoke-CommandWithExitCheck -Command `
"mvn" -Arguments @("-B", "-f", "$buildDir/../../pom.xml", "clean", "package", "-DskipTests", "-Pwin")
"../../mvnw.cmd" -Arguments @("-B", "-f", "$buildDir/../../pom.xml", "clean", "package", "-DskipTests", "-Pwin")
Copy-Item "$buildDir\..\..\target\$MainJarGlob.jar" -Destination "$buildDir\..\..\target\mods"
# add runtime
@@ -212,7 +207,7 @@ if ($LASTEXITCODE -ne 0) {
#Create RTF license for msi
Invoke-CommandWithExitCheck -Command `
"mvn" -Arguments @("-B", "-f", "$buildDir/../../pom.xml", "license:add-third-party", `
"../../mvnw.cmd" -Arguments @("-B", "-f", "$buildDir/../../pom.xml", "license:add-third-party", `
"-Dlicense.thirdPartyFilename=license.rtf", `
"-Dlicense.fileTemplate=$buildDir\resources\licenseTemplate.ftl", `
"-Dlicense.outputDirectory=$buildDir\resources\", `
@@ -259,7 +254,7 @@ Invoke-CommandWithExitCheck -Command `
#Create RTF license for bundle
Invoke-CommandWithExitCheck -Command `
"mvn" -Arguments @("-B", "-f", "$buildDir/../../pom.xml", "license:add-third-party", `
"../../mvnw.cmd" -Arguments @("-B", "-f", "$buildDir/../../pom.xml", "license:add-third-party", `
"-Dlicense.thirdPartyFilename=license.rtf", `
"-Dlicense.fileTemplate=$buildDir\bundle\resources\licenseTemplate.ftl", `
"-Dlicense.outputDirectory=$buildDir\bundle\resources\", `
+4
View File
@@ -0,0 +1,4 @@
mime-type=application/vnd.cryptomator.vault
extension=cryptomator
description=Cryptomator Vault File
icon=C:\\Users\\Arbeit\\Skymatic\\cryptomator-jdk26-jpackage\\dist\\win\\resources\\Cryptomator-Vault.ico
-15
View File
@@ -27,7 +27,6 @@
<?define ProgIdContentType= "application/vnd.cryptomator.encrypted" ?>
<?define CloseApplicationTarget= "cryptomator.exe" ?>
<?define LoopbackAlias= "cryptomator-vault" ?>
<?define UrlProtocolScheme= "org.cryptomator" ?>
<?include $(var.JpConfigDir)/overrides.wxi ?>
@@ -98,19 +97,6 @@
<ns0:Extension Id="c9u" Advertise="no" ContentType="$(var.ProgIdContentType)"/>
</ns0:ProgId>
</ns0:Component>
<!-- Register "org.cryptomator://" URL protocol handler -->
<ns0:Component Bitness="always64" Id="UrlProtocolHandler" Guid="*">
<ns0:RegistryKey Root="HKMU" Key="Software\Classes\$(var.UrlProtocolScheme)">
<ns0:RegistryValue Type="string" Value="URL:$(var.JpAppName) Protocol" KeyPath="yes"/>
<ns0:RegistryValue Name="URL Protocol" Type="string" Value=""/>
<ns0:RegistryKey Key="DefaultIcon">
<ns0:RegistryValue Type="string" Value="[INSTALLDIR]$(var.JpAppName).exe,0"/>
</ns0:RegistryKey>
<ns0:RegistryKey Key="shell\open\command">
<ns0:RegistryValue Type="string" Value="&quot;[INSTALLDIR]$(var.JpAppName).exe&quot; &quot;%1&quot;"/>
</ns0:RegistryKey>
</ns0:RegistryKey>
</ns0:Component>
</ns0:DirectoryRef>
<ns0:StandardDirectory Id="CommonAppDataFolder">
@@ -140,7 +126,6 @@
<ns0:ComponentGroupRef Id="FileAssociations"/>
<!-- Ref to additional ProgIDs -->
<ns0:ComponentRef Id="nonStartingProgID"/>
<ns0:ComponentRef Id="UrlProtocolHandler"/>
<ns0:ComponentRef Id="AdminConfigDir"/>
<ns0:ComponentRef Id="AdminConfigFile"/>
</ns0:Feature>
+13 -3
View File
@@ -33,12 +33,12 @@
<nonModularGroupIds>org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents</nonModularGroupIds>
<!-- cryptomator dependencies -->
<cryptomator.cryptofs.version>2.11.0-SNAPSHOT</cryptomator.cryptofs.version>
<cryptomator.cryptofs.version>2.10.0</cryptomator.cryptofs.version>
<cryptomator.cryptolib.version>2.2.2</cryptomator.cryptolib.version>
<cryptomator.integrations.version>1.9.0</cryptomator.integrations.version>
<cryptomator.integrations.win.version>1.6.1</cryptomator.integrations.win.version>
<cryptomator.integrations.mac.version>1.5.0</cryptomator.integrations.mac.version>
<cryptomator.integrations.linux.version>1.7.0</cryptomator.integrations.linux.version>
<cryptomator.integrations.linux.version>1.8.0-beta1</cryptomator.integrations.linux.version>
<cryptomator.fuse.version>6.0.1</cryptomator.fuse.version>
<cryptomator.webdav.version>3.0.2</cryptomator.webdav.version>
<cryptomator.webdav-servlet.version>1.2.12</cryptomator.webdav-servlet.version>
@@ -48,7 +48,7 @@
<commons-lang3.version>3.20.0</commons-lang3.version>
<dagger.version>2.59.2</dagger.version>
<easybind.version>2.2</easybind.version>
<jackson.version>2.21.4</jackson.version>
<jackson.version>2.21.6</jackson.version>
<javafx.version>25.0.3</javafx.version>
<jwt.version>4.5.2</jwt.version>
<nimbus-jose.version>10.5</nimbus-jose.version>
@@ -299,6 +299,10 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${mvn-surefire.version}</version>
<configuration>
<!--suppress MavenModelInspection -->
<argLine>@{argLine} -javaagent:${org.mockito:mockito-core:jar}</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
@@ -399,6 +403,12 @@
<includeGroupIds>${nonModularGroupIds}</includeGroupIds>
</configuration>
</execution>
<execution>
<id>get-mockito-agent-path</id>
<goals>
<goal>properties</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
@@ -8,7 +8,6 @@ import org.slf4j.LoggerFactory;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Optional;
import java.util.Set;
@@ -166,7 +165,7 @@ public class Environment {
private Optional<Path> getPath(String propertyName) {
String value = System.getProperty(propertyName);
return Optional.ofNullable(value).map(Paths::get);
return Optional.ofNullable(value).map(Path::of);
}
@VisibleForTesting
@@ -77,7 +77,7 @@ public class ErrorCode {
public static ErrorCode of(Throwable throwable) {
var causalChain = Throwables.getCausalChain(throwable);
if (causalChain.size() > 1) {
var rootCause = causalChain.get(causalChain.size() - 1);
var rootCause = causalChain.getLast();
var parentOfRootCause = causalChain.get(causalChain.size() - 2);
var rootSpecificFrames = countTopmostFrames(rootCause.getStackTrace(), parentOfRootCause.getStackTrace());
return new ErrorCode(throwable, rootCause, rootSpecificFrames);
@@ -67,7 +67,7 @@ public class SubstitutingProperties extends PropertiesDecorator {
private enum Source {
ENV,
PROPS;
PROPS
}
}
@@ -8,9 +8,9 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -78,7 +78,7 @@ public final class OneDriveWindowsLocationPresetsProvider implements LocationPre
*/
@Blocking
private static void waitForSuccess(Process process, int timeoutSeconds, String cmdDescription) throws TimeoutException, InterruptedException, CommandFailedException {
boolean exited = process.waitFor(timeoutSeconds, TimeUnit.SECONDS);
boolean exited = process.waitFor(Duration.ofSeconds(timeoutSeconds));
if (!exited) {
throw new TimeoutException(cmdDescription + " timed out after " + timeoutSeconds + "s");
}
@@ -90,7 +90,7 @@ public final class MountWithinParentUtil {
EMPTY_DIR,
BROKEN_JUNCTION;
BROKEN_JUNCTION
}
@@ -22,7 +22,6 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.Random;
@@ -62,7 +61,7 @@ public class VaultSettings {
VaultSettings(VaultSettingsJson json) {
this.id = json.id;
this.path = new SimpleObjectProperty<>(this, "path", json.path == null ? null : Paths.get(json.path));
this.path = new SimpleObjectProperty<>(this, "path", json.path == null ? null : Path.of(json.path));
this.displayName = new SimpleStringProperty(this, "displayName", json.displayName);
this.unlockAfterStartup = new SimpleBooleanProperty(this, "unlockAfterStartup", json.unlockAfterStartup);
this.revealAfterMount = new SimpleBooleanProperty(this, "revealAfterMount", json.revealAfterMount);
@@ -47,7 +47,6 @@ import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.SimpleBooleanProperty;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.ReadOnlyFileSystemException;
import java.util.EnumSet;
import java.util.Objects;
@@ -58,7 +57,7 @@ import java.util.concurrent.atomic.AtomicReference;
public class Vault {
private static final Logger LOG = LoggerFactory.getLogger(Vault.class);
private static final Path HOME_DIR = Paths.get(SystemUtils.USER_HOME);
private static final Path HOME_DIR = Path.of(SystemUtils.USER_HOME);
private static final int UNLIMITED_FILENAME_LENGTH = Integer.MAX_VALUE;
private final VaultSettings vaultSettings;
@@ -58,7 +58,7 @@ public class VaultState extends ObservableValueBase<VaultState.Value> implements
/**
* Unknown state due to preceding unrecoverable exceptions.
*/
ERROR;
ERROR
}
private final AtomicReference<Value> value;
@@ -1,16 +1,13 @@
package org.cryptomator.launcher;
/**
* An event triggering an action in the running application instance.
* <p>
* Produced by the launch-argument handling (see {@link LaunchArgsParser} and the {@code *RequestHandler}s) and consumed
* by the UI's {@code AppLaunchEventHandler}. Each permitted subtype represents one supported action:
* <ul>
* <li>{@link RevealRunningEvent} - reveal the already-running app,</li>
* <li>{@link OpenFileEvent} - open one or more paths,</li>
* <li>{@link OpenHubVaultEvent} - open a Hub vault from a deeplink.</li>
* </ul>
*/
public sealed interface AppLaunchEvent permits RevealRunningEvent, OpenFileEvent, OpenHubVaultEvent {
import java.nio.file.Path;
import java.util.Collection;
public record AppLaunchEvent(AppLaunchEvent.EventType type, Collection<Path> pathsToOpen) {
public enum EventType {
REVEAL_APP,
OPEN_FILE
}
}
@@ -25,7 +25,7 @@ class CryptomatorModule {
@Provides
@Singleton
@Named("launchEventQueue")
static BlockingQueue<AppLaunchEvent> provideLaunchEventQueue() {
static BlockingQueue<AppLaunchEvent> provideFileOpenRequests() {
return new ArrayBlockingQueue<>(10);
}
@@ -41,7 +41,7 @@ class FileOpenRequestHandler {
private void openFiles(OpenFilesEvent evt) {
Collection<Path> pathsToOpen = evt.getFiles().stream().map(File::toPath).toList();
AppLaunchEvent launchEvent = new OpenFileEvent(pathsToOpen);
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
tryToEnqueueFileOpenRequest(launchEvent);
}
@@ -60,7 +60,7 @@ class FileOpenRequestHandler {
}
}).filter(Objects::nonNull).toList();
if (!pathsToOpen.isEmpty()) {
AppLaunchEvent launchEvent = new OpenFileEvent(pathsToOpen);
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
tryToEnqueueFileOpenRequest(launchEvent);
}
}
@@ -68,7 +68,7 @@ class FileOpenRequestHandler {
private void tryToEnqueueFileOpenRequest(AppLaunchEvent launchEvent) {
if (!launchEventQueue.offer(launchEvent)) {
LOG.warn("Could not enqueue application launch event {}.", launchEvent);
LOG.warn("Could not enqueue application launch event.", launchEvent);
}
}
@@ -7,6 +7,7 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.BlockingQueue;
@@ -15,28 +16,24 @@ class IpcMessageHandler implements IpcMessageListener {
private static final Logger LOG = LoggerFactory.getLogger(IpcMessageHandler.class);
private final LaunchArgsParser launchArgsParser;
private final FileOpenRequestHandler fileOpenRequestHandler;
private final BlockingQueue<AppLaunchEvent> launchEventQueue;
@Inject
public IpcMessageHandler(LaunchArgsParser launchArgsParser, @Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue) {
this.launchArgsParser = launchArgsParser;
public IpcMessageHandler(FileOpenRequestHandler fileOpenRequestHandler, @Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue) {
this.fileOpenRequestHandler = fileOpenRequestHandler;
this.launchEventQueue = launchEventQueue;
}
@Override
public void revealRunningApp() {
launchEventQueue.add(new RevealRunningEvent());
launchEventQueue.add(new AppLaunchEvent(AppLaunchEvent.EventType.REVEAL_APP, Collections.emptyList()));
}
@Override
public void handleLaunchArgs(List<String> args) {
LOG.debug("Received launch args: {}", args);
try {
launchArgsParser.process(args);
} catch (IllegalArgumentException e) {
LOG.warn("Ignoring malformed launch args: {}", e.getMessage());
}
LOG.debug("Received launch args: {}", args.stream().reduce((a, b) -> a + ", " + b).orElse(""));
fileOpenRequestHandler.handleLaunchArgs(args);
}
}
@@ -1,88 +0,0 @@
package org.cryptomator.launcher;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.net.URI;
import java.nio.file.Path;
import java.util.List;
import java.util.regex.Pattern;
/**
* Preprocesses the launch arguments and delegates them to the matching handler.
* <p>
* An argument is treated as a URI if it starts with a (non-{@code file}) scheme of at least two characters, e.g.
* {@code cryptomator://…}. Everything else - including plain paths and {@code file://…} URIs - is treated as a file path
* and forwarded to the {@link FileOpenRequestHandler}. The two-character minimum prevents Windows drive letters
* (e.g. {@code C:\…}) from being misinterpreted as URIs.
* <p>
* URIs and file paths must not be mixed and at most a single URI is accepted, which has to be the first argument.
*/
@Singleton
class LaunchArgsParser {
private static final Pattern SCHEME_PATTERN = Pattern.compile("^([a-zA-Z][a-zA-Z0-9+.-]+):.*");
private static final String FILE_SCHEME = "file";
private final FileOpenRequestHandler fileOpenRequestHandler;
private final URIOpenRequestHandler uriOpenRequestHandler;
private final NoopRequestHandler noopRequestHandler;
@Inject
public LaunchArgsParser(FileOpenRequestHandler fileOpenRequestHandler, URIOpenRequestHandler uriOpenRequestHandler, NoopRequestHandler noopRequestHandler) {
this.fileOpenRequestHandler = fileOpenRequestHandler;
this.uriOpenRequestHandler = uriOpenRequestHandler;
this.noopRequestHandler = noopRequestHandler;
}
/**
* Classifies the given launch arguments and delegates them to the responsible handler.
*
* @param args the raw launch arguments
* @throws IllegalArgumentException if URIs and file paths are mixed, if more than one URI is given, if a URI is not
* the first argument, or if a URI argument is malformed
*/
public void process(List<String> args) {
if(args.isEmpty()) {
noopRequestHandler.revealApp();
return;
}
var classified = args.stream().map(LaunchArgsParser::classify).toList();
var uris = classified.stream().filter(arg -> arg.kind() == Kind.URI).toList();
if (uris.isEmpty()) {
var paths = classified.stream().map(Arg::value).toList();
fileOpenRequestHandler.handleLaunchArgs(paths);
return;
}
if (uris.size() > 1) {
throw new IllegalArgumentException("Only a single URI argument is accepted, but got " + uris.size() + ".");
}
if (classified.getFirst().kind() != Kind.URI) {
throw new IllegalArgumentException("URI argument must be the first parameter.");
}
if (classified.size() > 1) {
throw new IllegalArgumentException("Mixing a URI with file paths is not supported.");
}
uriOpenRequestHandler.handleLaunchArgs(URI.create(classified.getFirst().value()));
}
private static Arg classify(String arg) {
var matcher = SCHEME_PATTERN.matcher(arg);
if (!matcher.matches()) {
return new Arg(Kind.PATH, arg);
}
var scheme = matcher.group(1);
if (FILE_SCHEME.equalsIgnoreCase(scheme)) {
// file:// URIs (e.g. passed by Linux file managers) are file paths in disguise
return new Arg(Kind.PATH, Path.of(URI.create(arg)).toString());
}
return new Arg(Kind.URI, arg);
}
private enum Kind {PATH, URI}
private record Arg(Kind kind, String value) {}
}
@@ -1,29 +0,0 @@
package org.cryptomator.launcher;
import javax.inject.Inject;
import javax.inject.Named;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Singleton;
import java.util.concurrent.BlockingQueue;
@Singleton
public class NoopRequestHandler {
private static final Logger LOG = LoggerFactory.getLogger(NoopRequestHandler.class);
private final BlockingQueue<AppLaunchEvent> launchEventQueue;
@Inject
public NoopRequestHandler(@Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue) {
this.launchEventQueue = launchEventQueue;
}
public void revealApp() {
AppLaunchEvent launchEvent = new RevealRunningEvent();
if (!launchEventQueue.offer(launchEvent)) {
LOG.warn("Could not enqueue application launch event {}.", launchEvent);
}
}
}
@@ -1,13 +0,0 @@
package org.cryptomator.launcher;
import java.nio.file.Path;
import java.util.Collection;
/**
* Requests that the given paths (e.g. {@code .cryptomator} vault files) are opened.
*
* @param pathsToOpen the paths to open
*/
public record OpenFileEvent(Collection<Path> pathsToOpen) implements AppLaunchEvent {
}
@@ -1,182 +0,0 @@
package org.cryptomator.launcher;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptofs.VaultConfigLoadException;
import org.cryptomator.ui.keyloading.hub.HubConfig;
import org.cryptomator.ui.keyloading.hub.HubKeyLoadingStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
/**
* Requests opening a Hub vault from an {@code org.cryptomator://vault/open#vaultConfig=…} deeplink.
* <p>
* The single parameter is the vault's {@code vault.cryptomator}, a compact JWS embedded verbatim, carried in the fragment part.
* <p>
* Notes:
* <ul>
* <li> The config is read <em>unverified</em>, since its signature is keyed on the masterkey, which is only obtainable from Hu later on. </li>
* <li> The deeplink parsing makes a strict validation due to untrusted input</li>
* </ul>
*
* @param vaultConfig the decoded, unverified vault config
* @param vaultId the vault's id within its Hub instance, taken from the config's {@code jti} claim
*/
public record OpenHubVaultEvent(VaultConfig.UnverifiedVaultConfig vaultConfig, UUID vaultId) implements AppLaunchEvent {
private static final Logger LOG = LoggerFactory.getLogger(OpenHubVaultEvent.class);
private static final String SCHEME = "org.cryptomator";
private static final String HOST = "vault";
private static final String PATH = "/open";
private static final String PARAM_VAULT_CONFIG = "vaultConfig";
private static final String HUB_HEADER = "hub";
private static final int MAX_CONFIG_LENGTH = 8192; //real Hub vault config is ~1KB leaving some room for extensions
/**
* Attempts to interpret the given URI as an {@code org.cryptomator://vault/open#vaultConfig=…} deeplink.
*
* @param uri the deeplink URI
* @return the parsed event, or an empty optional if the URI's scheme, host or path do not identify a vault-open
* deeplink
* @throws IllegalArgumentException if the URI identifies a vault-open deeplink, but the config is missing, too
* large, not decodable, or does not describe a Hub vault
*/
public static Optional<OpenHubVaultEvent> tryParse(URI uri) {
if (!SCHEME.equalsIgnoreCase(uri.getScheme()) || !HOST.equalsIgnoreCase(uri.getHost()) || !PATH.equals(uri.getPath())) {
return Optional.empty();
}
var params = parseParams(uri.getRawFragment());
var token = params.get(PARAM_VAULT_CONFIG);
if (token == null || token.isBlank()) {
throw new IllegalArgumentException("Missing required fragment parameter '" + PARAM_VAULT_CONFIG + "'.");
}
var vaultConfig = decode(token);
requireHubVault(vaultConfig);
var vaultId = extractVaultId(vaultConfig);
var leftoverParams = params.keySet().stream().filter(k -> !k.equals(PARAM_VAULT_CONFIG)).toList();
if (!leftoverParams.isEmpty()) {
LOG.debug("Ignoring unknown parameters {}", leftoverParams);
}
return Optional.of(new OpenHubVaultEvent(vaultConfig, vaultId));
}
private static VaultConfig.UnverifiedVaultConfig decode(String token) {
// a compact JWS is ASCII, so its character count is its byte count
if (token.length() > MAX_CONFIG_LENGTH) {
throw new IllegalArgumentException("Fragment parameter '%s' must not exceed %d bytes.".formatted(PARAM_VAULT_CONFIG, MAX_CONFIG_LENGTH));
}
try {
return VaultConfig.decode(token);
} catch (VaultConfigLoadException e) {
throw new IllegalArgumentException("Fragment parameter '" + PARAM_VAULT_CONFIG + "' is not a decodable vault config.", e);
}
}
/**
* Ensures the config describes a Hub vault and that the endpoints the app will talk to are usable.
*/
private static void requireHubVault(VaultConfig.UnverifiedVaultConfig vaultConfig) {
var keyIdScheme = vaultConfig.getKeyId().getScheme();
if (keyIdScheme == null || !keyIdScheme.startsWith(HubKeyLoadingStrategy.SCHEME_PREFIX)) {
throw new IllegalArgumentException("Vault config does not describe a Hub vault, but had key id scheme '" + keyIdScheme + "'.");
}
HubConfig hubConfig;
try {
hubConfig = vaultConfig.getHeader(HUB_HEADER, HubConfig.class);
} catch (RuntimeException e) {
throw new IllegalArgumentException("Vault config contains an unreadable '" + HUB_HEADER + "' header.", e);
}
if (hubConfig == null) {
throw new IllegalArgumentException("Vault config contains no '" + HUB_HEADER + "' header.");
}
URI apiBaseUrl;
try {
apiBaseUrl = hubConfig.getApiBaseUrl();
} catch (RuntimeException e) {
throw new IllegalArgumentException("Vault config declares no usable hub api base url.", e);
}
requireUsableEndpoint("apiBaseUrl", apiBaseUrl);
requireUsableEndpoint("authEndpoint", toUri("authEndpoint", hubConfig.authEndpoint));
}
private static URI toUri(String field, String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Vault config declares no hub " + field + ".");
}
try {
return URI.create(value);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Vault config's hub " + field + " is not a valid url, but was '" + value + "'.", e);
}
}
private static void requireUsableEndpoint(String field, URI uri) {
if (!uri.isAbsolute() || uri.getHost() == null) {
throw new IllegalArgumentException("Vault config's hub " + field + " is not an absolute url with a host, but was '" + uri + "'.");
}
// Whether an http host is acceptable (it is, for local development) is decided by CheckHostTrustController
// Here we only ensure the endpoint is shaped like something that decision can be made on.
var scheme = uri.getScheme();
if (!"https".equalsIgnoreCase(scheme) && !"http".equalsIgnoreCase(scheme)) {
throw new IllegalArgumentException("Vault config's hub " + field + " is neither http nor https, but was '" + uri + "'.");
}
}
/**
* Reads the vault id from the config's {@code jti} claim.
* <p>
* Requiring a UUID matters beyond well-formedness: the id is interpolated into the {@code api/vaults/{vaultId}/…}
* request path, so it must not be able to introduce a path segment. A {@code jti} is an arbitrary string, so parsing
* it as a {@link UUID} and passing that on - rather than the raw claim - is what keeps that guarantee.
* <p>
* Hub writes the same id into the key id's trailing path segment, and the two have always agreed, so a config where
* they differ is forged or broken and is rejected.
*/
private static UUID extractVaultId(VaultConfig.UnverifiedVaultConfig vaultConfig) {
var allegedVaultId = vaultConfig.allegedVaultId();
if (allegedVaultId == null || allegedVaultId.isBlank()) {
throw new IllegalArgumentException("Vault config declares no vault id.");
}
UUID vaultId;
try {
vaultId = UUID.fromString(allegedVaultId);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Vault config's vault id is not a uuid, but was '" + allegedVaultId + "'.", e);
}
var keyId = vaultConfig.getKeyId();
var path = keyId.getPath();
var lastSegment = path == null ? "" : path.substring(path.lastIndexOf('/') + 1);
if (!vaultId.toString().equalsIgnoreCase(lastSegment)) {
throw new IllegalArgumentException("Vault config's vault id '" + vaultId + "' does not match its key id '" + keyId + "'.");
}
return vaultId;
}
private static Map<String, String> parseParams(String rawParams) {
var params = new HashMap<String, String>();
if (rawParams == null || rawParams.isEmpty()) {
return params;
}
for (var pair : rawParams.split("&")) {
var idx = pair.indexOf('=');
if (idx < 0) {
continue;
}
var key = URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8);
var value = URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8);
params.put(key, value);
}
return params;
}
}
@@ -1,8 +0,0 @@
package org.cryptomator.launcher;
/**
* Requests that the already-running application instance reveals itself (brings its main window to the front).
*/
public record RevealRunningEvent() implements AppLaunchEvent {
}
@@ -34,7 +34,7 @@ public class SupportedLanguages {
var collator = Collator.getInstance(preferredLocale);
collator.setStrength(Collator.PRIMARY);
var sorted = new ArrayList<String>();
sorted.add(0, null);
sorted.addFirst(null);
sorted.add(1, ENGLISH);
LANGUAGE_TAGS.stream() //
.sorted((a, b) -> collator.compare(Locale.forLanguageTag(a).getDisplayName(), Locale.forLanguageTag(b).getDisplayName())) //
@@ -1,58 +0,0 @@
package org.cryptomator.launcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.net.URI;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.function.Function;
@Singleton
public class URIOpenRequestHandler {
private static final Logger LOG = LoggerFactory.getLogger(URIOpenRequestHandler.class);
/**
* The registered deeplink parsers, tried in order. Each returns a matching event, an empty optional if the URI is
* not its concern, or throws {@link IllegalArgumentException} if the URI is its concern but malformed.
*/
private static final List<Function<URI, Optional<? extends AppLaunchEvent>>> DEEPLINK_PARSERS = List.of( //
OpenHubVaultEvent::tryParse //
);
private final BlockingQueue<AppLaunchEvent> launchEventQueue;
@Inject
public URIOpenRequestHandler(@Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue) {
this.launchEventQueue = launchEventQueue;
}
public void handleLaunchArgs(URI uri) {
AppLaunchEvent launchEvent = toLaunchEvent(uri);
if (!launchEventQueue.offer(launchEvent)) {
LOG.warn("Could not enqueue application launch event {}.", launchEvent);
}
}
private AppLaunchEvent toLaunchEvent(URI uri) {
try {
for (var parser : DEEPLINK_PARSERS) {
var event = parser.apply(uri);
if (event.isPresent()) {
return event.get();
}
}
} catch (IllegalArgumentException e) {
LOG.warn("Received malformed deeplink {}: {}. Revealing running app instead.", uri, e.getMessage());
return new RevealRunningEvent();
}
LOG.warn("Received unsupported deeplink {}, revealing running app instead.", uri);
return new RevealRunningEvent();
}
}
@@ -41,7 +41,6 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
@@ -50,7 +49,7 @@ import java.util.concurrent.ExecutorService;
public class CreateNewVaultLocationController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultLocationController.class);
private static final Path DEFAULT_CUSTOM_VAULT_PATH = Paths.get(System.getProperty("user.home"));
private static final Path DEFAULT_CUSTOM_VAULT_PATH = Path.of(System.getProperty("user.home"));
private static final String TEMP_FILE_PREFIX = ".locationTest.cryptomator";
private final Stage window;
@@ -23,7 +23,7 @@ public class SystemBarUtil {
/**
* OS Bar placed at the bottom screen edge
*/
BOTTOM;
BOTTOM
}
/**
@@ -62,9 +62,9 @@ public class Dialogs {
.setOkButtonKey(BUTTON_KEY_CLOSE);
}
public SimpleDialog.Builder prepareHubVaultArchived(Stage window, String vaultDisplayName) {
public SimpleDialog.Builder prepareHubVaultArchived(Stage window, Vault vault) {
return createDialogBuilder().setOwner(window) //
.setTitleKey("unlock.title", vaultDisplayName) //
.setTitleKey("unlock.title", vault.getDisplayName()) //
.setMessageKey("hub.archived.message") //
.setDescriptionKey("hub.archived.description") //
.setIcon(FontAwesome5Icon.BAN)//
@@ -4,19 +4,14 @@ import org.cryptomator.common.vaults.NotAVaultDirectoryException;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.launcher.AppLaunchEvent;
import org.cryptomator.launcher.OpenFileEvent;
import org.cryptomator.launcher.OpenHubVaultEvent;
import org.cryptomator.launcher.RevealRunningEvent;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.dialogs.Dialogs;
import org.cryptomator.ui.keyloading.hub.HubVaults;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.stage.Stage;
import java.io.IOException;
import java.nio.file.Path;
@@ -36,18 +31,16 @@ class AppLaunchEventHandler {
private final ExecutorService executorService;
private final FxApplicationWindows appWindows;
private final VaultListManager vaultListManager;
private final ObservableList<Vault> vaults;
private final VaultService vaultService;
private final Stage primaryStage;
private final Dialogs dialogs;
@Inject
public AppLaunchEventHandler(@Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue, ExecutorService executorService, FxApplicationWindows appWindows, VaultListManager vaultListManager, ObservableList<Vault> vaults, VaultService vaultService, @PrimaryStage Stage primaryStage, Dialogs dialogs) {
public AppLaunchEventHandler(@Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue, ExecutorService executorService, FxApplicationWindows appWindows, VaultListManager vaultListManager, VaultService vaultService, @PrimaryStage Stage primaryStage, Dialogs dialogs) {
this.launchEventQueue = launchEventQueue;
this.executorService = executorService;
this.appWindows = appWindows;
this.vaultListManager = vaultListManager;
this.vaults = vaults;
this.vaultService = vaultService;
this.primaryStage = primaryStage;
this.dialogs = dialogs;
@@ -70,32 +63,10 @@ class AppLaunchEventHandler {
}
private void handleLaunchEvent(AppLaunchEvent event) {
switch (event) {
case RevealRunningEvent _ -> appWindows.showMainWindow();
case OpenFileEvent openFileEvent -> openFileEvent.pathsToOpen().forEach(this::openPotentialVault);
case OpenHubVaultEvent openHubVaultEvent -> openHubVault(openHubVaultEvent);
}
}
/**
* Whether a hub vault is set up on this machine is a purely local question - hub manages the vault's key, not where
* it lives. Only if it is not set up here do we need to ask hub about it.
*/
private void openHubVault(OpenHubVaultEvent event) {
var existing = HubVaults.findByVaultId(vaults, event.vaultId());
if (existing.isPresent()) {
var vault = existing.get();
Platform.runLater(() -> {
if (vault.isUnlocked()) {
vaultService.reveal(vault);
} else if (vault.isLocked()) {
appWindows.startUnlockWorkflow(vault, null);
}
});
} else {
//TODO: authenticate, ask hub for the vault's details and offer to add it, see docs/hub-vault-open-deeplink-plan.md
LOG.info("Hub vault {} is not set up on this machine.", event.vaultId());
appWindows.showMainWindow();
switch (event.type()) {
case REVEAL_APP -> appWindows.showMainWindow();
case OPEN_FILE -> event.pathsToOpen().forEach(this::openPotentialVault);
default -> LOG.warn("Unsupported event type: {}", event.type());
}
}
@@ -6,6 +6,7 @@ import org.cryptomator.integrations.common.Priority;
import org.cryptomator.integrations.revealpath.RevealFailedException;
import org.cryptomator.integrations.revealpath.RevealPathService;
import java.nio.file.Files;
import java.nio.file.Path;
/**
@@ -17,14 +18,17 @@ import java.nio.file.Path;
*/
@DisplayName("JavaFX HostServices (GTK)")
@OperatingSystem(OperatingSystem.Value.LINUX)
@Priority(10)
@Priority(Priority.FALLBACK)
public class JfxRevealPathService implements RevealPathService {
@Override
public void reveal(Path p) throws RevealFailedException {
var absPath = p.toAbsolutePath();
var fxApp = FxApplication.INSTANCE.get();
if (fxApp != null) {
fxApp.getHostServices().showDocument(p.toUri().toString());
// showDocument() launches the default app for a regular file, so reveal its parent directory instead
var target = Files.isRegularFile(absPath) ? absPath.getParent() : absPath;
fxApp.getHostServices().showDocument(target.toUri().toString());
} else {
throw new RevealFailedException("JavaFX Application not initialized");
}
@@ -90,7 +90,7 @@ public class Check {
SUCCEEDED,
SKIPPED,
ERROR,
CANCELLED;
CANCELLED
}
ObservableList<Result> getResults() {
@@ -84,7 +84,7 @@ public class CheckListController implements FxController {
checks.filtered(c -> !c.isChosenForExecution()).forEach(c -> c.setState(Check.CheckState.SKIPPED));
checkExecutor.executeBatch(chosenChecks);
checksListView.getSelectionModel().select(chosenChecks.get(0));
checksListView.getSelectionModel().select(chosenChecks.getFirst());
checksListView.refresh();
window.sizeToScene();
}
@@ -16,7 +16,6 @@ import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.keyloading.KeyLoadingComponent;
import org.cryptomator.ui.keyloading.KeyLoadingRef;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import javax.inject.Named;
@@ -65,11 +64,7 @@ abstract class HealthCheckModule {
@HealthCheckWindow
@HealthCheckScoped
static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Factory compFactory, @HealthCheckWindow Vault vault, @Named("unlockWindow") Stage window ) {
try {
return compFactory.create(KeyLoadingRef.forVault(vault), vault, window).keyloadingStrategy();
} catch (IOException e) {
return KeyLoadingStrategy.failed(e);
}
return compFactory.create(vault, window).keyloadingStrategy();
}
@Provides
@@ -2,7 +2,6 @@ package org.cryptomator.ui.keyloading;
import dagger.BindsInstance;
import dagger.Subcomponent;
import org.cryptomator.common.Nullable;
import org.cryptomator.common.vaults.Vault;
import javafx.stage.Stage;
@@ -17,14 +16,7 @@ public interface KeyLoadingComponent {
@Subcomponent.Factory
interface Factory {
/**
* @param vaultRef the {@link KeyLoadingRef} containing the info to load the key
* @param vault the local vault, or {@code null} if it is not set up on this machine.
* @param window the window to show the key loading scenes in
*/
KeyLoadingComponent create(@BindsInstance @KeyLoading KeyLoadingRef vaultRef, //
@BindsInstance @KeyLoading @Nullable Vault vault, //
@BindsInstance @KeyLoading Stage window);
KeyLoadingComponent create(@BindsInstance @KeyLoading Vault vault, @KeyLoading @BindsInstance Stage window);
}
}
@@ -2,6 +2,7 @@ package org.cryptomator.ui.keyloading;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlLoaderFactory;
@@ -9,6 +10,7 @@ import org.cryptomator.ui.keyloading.hub.HubKeyLoadingModule;
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule;
import javax.inject.Provider;
import java.io.IOException;
import java.util.Map;
import java.util.ResourceBundle;
@@ -25,10 +27,14 @@ abstract class KeyLoadingModule {
@Provides
@KeyLoading
@KeyLoadingScoped
static KeyLoadingStrategy provideKeyLoadingStrategy(@KeyLoading KeyLoadingRef vaultRef, Map<String, Provider<KeyLoadingStrategy>> strategies) {
String scheme = vaultRef.keyId().getScheme();
var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme));
return strategies.getOrDefault(scheme, () -> fallback).get();
static KeyLoadingStrategy provideKeyLoadingStrategy(@KeyLoading Vault vault, Map<String, Provider<KeyLoadingStrategy>> strategies) {
try {
String scheme = vault.getVaultConfigCache().get().getKeyId().getScheme();
var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme));
return strategies.getOrDefault(scheme, () -> fallback).get();
} catch (IOException e) {
return KeyLoadingStrategy.failed(e);
}
}
}
@@ -1,50 +0,0 @@
package org.cryptomator.ui.keyloading;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.VaultConfig;
import java.io.IOException;
import java.net.URI;
/**
* Identifies the vault a key is being loaded for, independently of whether that vault exists on this machine.
* <p>
* Key loading needs the (unverified) vault config selecting the strategy and addresses
* the vault within the strategy, and the display name titles the windows.
* <p>
* The config is <em>unverified</em>: its signature is keyed on the masterkey, which is exactly what key loading is
* about to obtain.
*
* @param vaultConfig the vault's unverified config
* @param displayName the vault's name, as shown to the user
*/
public record KeyLoadingRef(VaultConfig.UnverifiedVaultConfig vaultConfig, String displayName) {
/**
* Describes a vault that is already set up on this machine.
*
* @param vault the vault to load a key for
* @throws IOException if the vault's config cannot be read
*/
public static KeyLoadingRef forVault(Vault vault) throws IOException {
return new KeyLoadingRef(vault.getVaultConfigCache().get(), vault.getDisplayName());
}
/**
* The key id, whose scheme selects the key loading strategy.
*/
public URI keyId() {
return vaultConfig.getKeyId();
}
/**
* The vault's id, i.e. how the vault is addressed within its Hub instance.
* <p>
* Read from the config's {@code jti} claim, which is the authoritative source: the key id carries the same id in its
* trailing path segment, but only its <em>scheme</em> is a source of truth here.
*/
public String vaultId() {
return vaultConfig.allegedVaultId();
}
}
@@ -6,6 +6,8 @@ import org.cryptomator.common.settings.Settings;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.controls.FontAwesome5Icon;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.jetbrains.annotations.VisibleForTesting;
@@ -13,14 +15,24 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.animation.PauseTransition;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.net.URI;
import java.util.ResourceBundle;
import java.util.Set;
@@ -35,7 +47,11 @@ public class CheckHostTrustController implements FxController {
private static final String CHECK_KEY = "hub.checkHostTrust.message.check";
private static final String ASK_SINGULAR_KEY = "hub.checkHostTrust.message.ask";
private static final String ASK_PLURAL_KEY = "hub.checkHostTrust.message.ask.plural";
private static final String DESCRIPTION_SINGULAR_KEY = "hub.checkHostTrust.description.ask";
private static final String DESCRIPTION_PLURAL_KEY = "hub.checkHostTrust.description.ask.plural";
private static final String COPY_TOOLTIP_KEY = "hub.checkHostTrust.copyBtn.tooltip";
private static final String TRUSTED_CRYPTOMATOR_CLOUD_DOMAIN = ".cryptomator.cloud";
private static final Duration COPIED_INDICATION_DURATION = Duration.seconds(2);
private final Stage window;
private final HubConfig hubConfig;
@@ -49,9 +65,10 @@ public class CheckHostTrustController implements FxController {
private final ResourceBundle resourceBundle;
private final SortedSet<String> hostnames;
private final StringProperty messageLabel;
private final StringProperty descriptionLabel;
@FXML
private TextFlow hostnamesFlow;
private VBox hostnamesBox;
@Inject
public CheckHostTrustController(@KeyLoading Stage window, //
@@ -74,6 +91,7 @@ public class CheckHostTrustController implements FxController {
this.resourceBundle = resourceBundle;
this.hostnames = new TreeSet<>();
this.messageLabel = new SimpleStringProperty(resourceBundle.getString(CHECK_KEY));
this.descriptionLabel = new SimpleStringProperty("");
}
@FXML
@@ -117,12 +135,41 @@ public class CheckHostTrustController implements FxController {
}
private void renderHostnames() {
hostnamesFlow.getChildren().clear();
hostnamesBox.getChildren().clear();
for (var hostname : hostnames) {
hostnamesFlow.getChildren().add(new Text(hostname + System.lineSeparator()));
hostnamesBox.getChildren().add(createHostnameRow(hostname));
}
var messageKey = hostnames.size() > 1 ? ASK_PLURAL_KEY : ASK_SINGULAR_KEY;
messageLabel.set(resourceBundle.getString(messageKey));
var plural = hostnames.size() > 1;
messageLabel.set(resourceBundle.getString(plural ? ASK_PLURAL_KEY : ASK_SINGULAR_KEY));
descriptionLabel.set(resourceBundle.getString(plural ? DESCRIPTION_PLURAL_KEY : DESCRIPTION_SINGULAR_KEY));
}
private Node createHostnameRow(String hostname) {
var label = new Label(hostname);
label.setWrapText(true);
HBox.setHgrow(label, Priority.ALWAYS);
var icon = new FontAwesome5IconView();
icon.setGlyph(FontAwesome5Icon.COPY);
var copyLink = new Hyperlink(null, icon);
copyLink.setTooltip(new Tooltip(resourceBundle.getString(COPY_TOOLTIP_KEY)));
copyLink.setAccessibleText(resourceBundle.getString(COPY_TOOLTIP_KEY));
copyLink.setOnAction(_ -> copyToClipboard(hostname, icon));
var row = new HBox(6, label, copyLink);
row.setAlignment(Pos.CENTER_LEFT);
return row;
}
private void copyToClipboard(String hostname, FontAwesome5IconView icon) {
var clipboardContent = new ClipboardContent();
clipboardContent.putString(hostname);
Clipboard.getSystemClipboard().setContent(clipboardContent);
icon.setGlyph(FontAwesome5Icon.CHECK);
var resetIcon = new PauseTransition(COPIED_INDICATION_DURATION);
resetIcon.setOnFinished(_ -> icon.setGlyph(FontAwesome5Icon.COPY));
resetIcon.play();
}
private boolean isConsistentHubConfig() {
@@ -176,4 +223,12 @@ public class CheckHostTrustController implements FxController {
return messageLabel.get();
}
public StringProperty descriptionLabelProperty() {
return descriptionLabel;
}
public String getDescriptionLabel() {
return descriptionLabel.get();
}
}
@@ -7,6 +7,7 @@ import dagger.Provides;
import dagger.multibindings.IntoMap;
import dagger.multibindings.StringKey;
import org.cryptomator.common.settings.DeviceKey;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptolib.common.MessageDigestSupplier;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
@@ -14,12 +15,13 @@ import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingRef;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import javax.inject.Named;
import javafx.scene.Scene;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
@@ -30,15 +32,19 @@ public abstract class HubKeyLoadingModule {
@Provides
@KeyLoadingScoped
static HubConfig provideHubConfig(@KeyLoading KeyLoadingRef vaultRef) {
return vaultRef.vaultConfig().getHeader("hub", HubConfig.class);
static HubConfig provideHubConfig(@KeyLoading Vault vault) {
try {
return vault.getVaultConfigCache().get().getHeader("hub", HubConfig.class);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Provides
@KeyLoadingScoped
@Named("windowTitle")
static String provideWindowTitle(@KeyLoading KeyLoadingRef vaultRef, ResourceBundle resourceBundle) {
return String.format(resourceBundle.getString("unlock.title"), vaultRef.displayName());
static String provideWindowTitle(@KeyLoading Vault vault, ResourceBundle resourceBundle) {
return String.format(resourceBundle.getString("unlock.title"), vault.getDisplayName());
}
@@ -1,59 +0,0 @@
package org.cryptomator.ui.keyloading.hub;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.VaultConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Collection;
import java.util.Optional;
import java.util.UUID;
/**
* Locates Hub vaults among the vaults set up on this machine.
* <p>
* Hub manages a vault's key, not where it lives or how it is laid out, so the local vault list is the only place that
* can answer whether a given Hub vault is already set up here.
*/
public final class HubVaults {
private static final Logger LOG = LoggerFactory.getLogger(HubVaults.class);
private HubVaults() {
}
/**
* Finds the Hub vault with the given id.
* <p>
* A vault whose config cannot be read - e.g. because it sits on storage that is currently unavailable - is skipped
* rather than failing the lookup: one unreachable vault must not prevent finding a different one.
*
* @param vaults the vaults set up on this machine
* @param hubVaultId the vault's id within its Hub instance
* @return the local vault, or empty if none of them is that Hub vault
*/
public static Optional<Vault> findByVaultId(Collection<Vault> vaults, UUID hubVaultId) {
return vaults.stream() //
.filter(vault -> hasVaultId(vault, hubVaultId)) //
.findAny();
}
private static boolean hasVaultId(Vault vault, UUID hubVaultId) {
try {
return hasVaultId(vault.getVaultConfigCache().get(), hubVaultId);
} catch (IOException e) {
LOG.debug("Skipping vault {} while looking for hub vault {}, its config is not readable.", vault.getPath(), hubVaultId);
return false;
}
}
private static boolean hasVaultId(VaultConfig.UnverifiedVaultConfig config, UUID hubVaultId) {
var keyIdScheme = config.getKeyId().getScheme();
if (keyIdScheme == null || !keyIdScheme.startsWith(HubKeyLoadingStrategy.SCHEME_PREFIX)) {
return false; //not a hub vault, so it cannot be the one we are looking for
}
return hubVaultId.toString().equalsIgnoreCase(config.allegedVaultId());
}
}
@@ -7,12 +7,12 @@ import com.google.common.base.Preconditions;
import com.nimbusds.jose.JWEObject;
import dagger.Lazy;
import org.cryptomator.common.Constants;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.dialogs.Dialogs;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingRef;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -60,10 +60,10 @@ public class ReceiveKeyController implements FxController {
private final Lazy<Scene> invalidLicenseScene;
private final HttpClient httpClient;
private final Dialogs dialogs;
private final KeyLoadingRef vaultRef;
private final Vault vault;
@Inject
public ReceiveKeyController(@KeyLoading KeyLoadingRef vaultRef, //
public ReceiveKeyController(@KeyLoading Vault vault, //
ExecutorService executor, //
@KeyLoading Stage window, //
HubConfig hubConfig, //
@@ -79,7 +79,7 @@ public class ReceiveKeyController implements FxController {
Dialogs dialogs) {
this.window = window;
this.hubConfig = hubConfig;
this.vaultId = vaultRef.vaultId();
this.vaultId = extractVaultId(vault.getVaultConfigCache().getUnchecked().getKeyId()); // TODO: access vault config's JTI directly (requires changes in cryptofs)
this.deviceId = deviceId;
this.bearerToken = Objects.requireNonNull(tokenRef.get());
this.fsOwnerId = fsOwnerId;
@@ -92,7 +92,7 @@ public class ReceiveKeyController implements FxController {
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
this.httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).executor(executor).build();
this.dialogs = dialogs;
this.vaultRef = vaultRef;
this.vault = vault;
}
@FXML
@@ -313,7 +313,7 @@ public class ReceiveKeyController implements FxController {
private void accessGoneVaultArchived() {
window.close();
dialogs.prepareHubVaultArchived((Stage)window.getOwner(), vaultRef.displayName()).build().showAndWait();
dialogs.prepareHubVaultArchived((Stage)window.getOwner(), vault).build().showAndWait();
}
private void accountInitializationRequired() {
@@ -343,6 +343,12 @@ public class ReceiveKeyController implements FxController {
}
}
private static String extractVaultId(URI vaultKeyUri) {
assert vaultKeyUri.getScheme().startsWith(HubKeyLoadingStrategy.SCHEME_PREFIX);
var path = vaultKeyUri.getPath();
return path.substring(path.lastIndexOf('/') + 1);
}
@JsonIgnoreProperties(ignoreUnknown = true)
private record UserDto(@JsonProperty(value = "name", required = true) String name) {}
@@ -1,6 +1,5 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import org.cryptomator.common.Nullable;
import org.cryptomator.common.recovery.RecoveryActionType;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
@@ -19,7 +18,6 @@ import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import java.io.File;
import java.nio.file.Path;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
@@ -43,12 +41,12 @@ public class ChooseMasterkeyFileController implements FxController {
@Inject
public ChooseMasterkeyFileController(@KeyLoading Stage window, //
@KeyLoading @Nullable Vault vault, //
@KeyLoading Vault vault, //
CompletableFuture<Path> result, //
RecoveryKeyComponent.Factory recoveryKeyWindow, //
ResourceBundle resourceBundle) {
this.window = window;
this.vault = Objects.requireNonNull(vault, MasterkeyFileLoadingModule.NO_LOCAL_VAULT);
this.vault = vault;
this.result = result;
this.recoveryKeyWindow = recoveryKeyWindow;
this.resourceBundle = resourceBundle;
@@ -5,7 +5,6 @@ import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import dagger.multibindings.StringKey;
import org.cryptomator.common.Nullable;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.integrations.keychain.KeychainAccessException;
@@ -16,27 +15,20 @@ import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.slf4j.LoggerFactory;
import javax.inject.Named;
import java.util.Objects;
import java.util.Optional;
@Module(subcomponents = {ForgetPasswordComponent.class, PassphraseEntryComponent.class, ChooseMasterkeyFileComponent.class})
public interface MasterkeyFileLoadingModule {
/**
* Key loading may run for a yet-to-setup vault (i.e. deeplink with only a config) - Masterkey loading requires in the current implementation
* an already setup vault.
*/
String NO_LOCAL_VAULT = "masterkey file loading requires a local vault";
@Provides
@Named("savedPassword")
@KeyLoadingScoped
static Optional<char[]> provideStoredPassword(KeychainManager keychain, @KeyLoading @Nullable Vault vault) {
static Optional<char[]> provideStoredPassword(KeychainManager keychain, @KeyLoading Vault vault) {
if (!keychain.isSupported() || keychain.isLocked()) {
return Optional.empty();
} else {
try {
return Optional.ofNullable(keychain.loadPassphrase(Objects.requireNonNull(vault, NO_LOCAL_VAULT).getId()));
return Optional.ofNullable(keychain.loadPassphrase(vault.getId()));
} catch (KeychainAccessException e) {
LoggerFactory.getLogger(MasterkeyFileLoadingModule.class).error("Failed to load entry from system keychain.", e);
return Optional.empty();
@@ -2,7 +2,6 @@ package org.cryptomator.ui.keyloading.masterkeyfile;
import com.google.common.base.Preconditions;
import org.cryptomator.common.Constants;
import org.cryptomator.common.Nullable;
import org.cryptomator.common.Passphrase;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
@@ -26,7 +25,6 @@ import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.CancellationException;
@@ -50,8 +48,8 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
private boolean wrongPassphrase;
@Inject
public MasterkeyFileLoadingStrategy(@KeyLoading @Nullable Vault vault, MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Stage window, @Named("savedPassword") Optional<char[]> savedPassphrase, PassphraseEntryComponent.Builder passphraseEntry, ChooseMasterkeyFileComponent.Builder masterkeyFileChoice, KeychainManager keychain, ResourceBundle resourceBundle) {
this.vault = Objects.requireNonNull(vault, MasterkeyFileLoadingModule.NO_LOCAL_VAULT);
public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Stage window, @Named("savedPassword") Optional<char[]> savedPassphrase, PassphraseEntryComponent.Builder passphraseEntry, ChooseMasterkeyFileComponent.Builder masterkeyFileChoice, KeychainManager keychain, ResourceBundle resourceBundle) {
this.vault = vault;
this.masterkeyFileAccess = masterkeyFileAccess;
this.window = window;
this.passphraseEntry = passphraseEntry;
@@ -35,7 +35,6 @@ import javafx.scene.transform.Translate;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import javafx.util.Duration;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
@@ -67,9 +66,9 @@ public class PassphraseEntryController implements FxController {
public Animation unlockAnimation;
@Inject
public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading @Nullable Vault vault, CompletableFuture<PassphraseEntryResult> result, @Nullable @Named("savedPassword") Passphrase savedPassword, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain, ExecutorService backgroundExecutorService) {
public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, CompletableFuture<PassphraseEntryResult> result, @Nullable @Named("savedPassword") Passphrase savedPassword, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain, ExecutorService backgroundExecutorService) {
this.window = window;
this.vault = Objects.requireNonNull(vault, MasterkeyFileLoadingModule.NO_LOCAL_VAULT);
this.vault = vault;
this.result = result;
this.savedPassword = savedPassword;
this.forgetPassword = forgetPassword;
@@ -153,7 +153,7 @@ public class VaultListController implements FxController {
vaults.addListener((ListChangeListener.Change<? extends Vault> c) -> {
while (c.next()) {
if (c.wasAdded()) {
Vault anyAddedVault = c.getAddedSubList().get(0);
Vault anyAddedVault = c.getAddedSubList().getFirst();
vaultList.getSelectionModel().select(anyAddedVault);
}
}
@@ -217,7 +217,7 @@ public class RecoveryKeyValidateController implements FxController {
/**
* Recovery key is not a valid key.
*/
INVALID;
INVALID
}
}
@@ -140,9 +140,9 @@ public class VaultStatisticsController implements FxController {
long allTimeMaxAccessedFiles = Arrays.stream(maxAccessBuf).max().orElse(0L);
// remove oldest value:
decryptedBytesRead.getData().remove(0);
encryptedBytesWrite.getData().remove(0);
accessedFiles.getData().remove(0);
decryptedBytesRead.getData().removeFirst();
encryptedBytesWrite.getData().removeFirst();
accessedFiles.getData().removeFirst();
// add latest value:
decryptedBytesRead.getData().add(new Data<>(currentStep, decBytes));
@@ -110,7 +110,7 @@ public class UnlockInvalidMountPointController implements FxController {
//TODO Add option to show filesystem, e.g. for ExceptionType.HIDEAWAY_EXISTS
SHOW_PREFERENCES,
SHOW_VAULT_OPTIONS;
SHOW_VAULT_OPTIONS
}
@@ -14,7 +14,6 @@ import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.keyloading.KeyLoadingComponent;
import org.cryptomator.ui.keyloading.KeyLoadingRef;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
import org.jetbrains.annotations.Nullable;
@@ -26,7 +25,6 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.Map;
import java.util.ResourceBundle;
@@ -60,11 +58,7 @@ abstract class UnlockModule {
@UnlockWindow
@UnlockScoped
static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Factory compFactory, @UnlockWindow Vault vault, @UnlockWindow Stage window) {
try {
return compFactory.create(KeyLoadingRef.forVault(vault), vault, window).keyloadingStrategy();
} catch (IOException e) {
return KeyLoadingStrategy.failed(e);
}
return compFactory.create(vault, window).keyloadingStrategy();
}
@Provides
+15
View File
@@ -20,6 +20,10 @@
src: url('firacode_regular.ttf');
}
@font-face {
src: url('AtkinsonHyperlegibleMono-Regular.ttf');
}
/*******************************************************************************
* *
* Root Styling & Colors *
@@ -141,6 +145,17 @@
-fx-font-size: 1.1em;
}
.hostname-list {
-fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL;
-fx-background-insets: 0, 1px;
-fx-background-radius: 4px;
-fx-padding: 6px 8px 6px 8px;
}
.hostname-list .label {
-fx-font-family: 'Atkinson Hyperlegible Mono';
}
/*******************************************************************************
* *
* Glyph Icons *
+15
View File
@@ -20,6 +20,10 @@
src: url('firacode_regular.ttf');
}
@font-face {
src: url('AtkinsonHyperlegibleMono-Regular.ttf');
}
/*******************************************************************************
* *
* Root Styling & Colors *
@@ -141,6 +145,17 @@
-fx-font-size: 1.1em;
}
.hostname-list {
-fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL;
-fx-background-insets: 0, 1px;
-fx-background-radius: 4px;
-fx-padding: 6px 8px 6px 8px;
}
.hostname-list .label {
-fx-font-family: 'Atkinson Hyperlegible Mono';
}
/*******************************************************************************
* *
* Glyph Icons *
@@ -11,12 +11,11 @@
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<?import javafx.scene.text.TextFlow?>
<HBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.keyloading.hub.CheckHostTrustController"
minWidth="400"
maxWidth="400"
minWidth="450"
maxWidth="450"
minHeight="145"
spacing="12"
alignment="TOP_LEFT"
@@ -34,13 +33,15 @@
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="QUESTION" glyphSize="24"/>
</StackPane>
</Group>
<VBox HBox.hgrow="ALWAYS">
<VBox HBox.hgrow="ALWAYS" spacing="6">
<Label styleClass="label-large" text="${controller.messageLabel}" wrapText="true" textAlignment="LEFT">
<padding>
<Insets bottom="6" top="6"/>
</padding>
</Label>
<TextFlow fx:id="hostnamesFlow" styleClass="text-flow" minHeight="60"/>
<Label text="${controller.descriptionLabel}" wrapText="true" textAlignment="LEFT" visible="${!controller.descriptionLabel.empty}" managed="${!controller.descriptionLabel.empty}"/>
<!-- The box is filled in the controller, see createHostNamesRow() -->
<VBox fx:id="hostnamesBox" styleClass="hostname-list" spacing="4" visible="${!controller.descriptionLabel.empty}" managed="${!controller.descriptionLabel.empty}"/>
<Region VBox.vgrow="ALWAYS" minHeight="18"/>
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
<buttons>
@@ -173,6 +173,9 @@ hub.noKeychain.openBtn=Open Preferences
hub.checkHostTrust.message.check=Checking Configuration…
hub.checkHostTrust.message.ask=Trust this host?
hub.checkHostTrust.message.ask.plural=Trust these hosts?
hub.checkHostTrust.description.ask=Cryptomator does not recognize this host. To unlock the vault, it needs to be trusted. Only trust it if you recognize the address below.
hub.checkHostTrust.description.ask.plural=Cryptomator does not recognize these hosts. To unlock the vault, they need to be trusted. Only trust them if you recognize the addresses below.
hub.checkHostTrust.copyBtn.tooltip=Copy address to clipboard
hub.checkHostTrust.trustBtn=Trust
hub.checkHostTrust.denyBtn=Deny
### Waiting
@@ -11,7 +11,6 @@ import org.junit.jupiter.api.condition.EnabledIf;
import org.mockito.Mockito;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
@@ -56,7 +55,7 @@ public class EnvironmentTest {
List<Path> result = env.getPaths("test.path.property").toList();
MatcherAssert.assertThat(result, Matchers.hasSize(1));
MatcherAssert.assertThat(result, Matchers.hasItem(Paths.get("/foo/bar/test")));
MatcherAssert.assertThat(result, Matchers.hasItem(Path.of("/foo/bar/test")));
}
@Test
@@ -38,7 +38,7 @@ public class SettingsJsonTest {
Assertions.assertTrue(jsonObj.checkForUpdatesEnabled);
Assertions.assertEquals(2, jsonObj.directories.size());
Assertions.assertEquals("/vault1", jsonObj.directories.get(0).path);
Assertions.assertEquals("/vault1", jsonObj.directories.getFirst().path);
Assertions.assertEquals("/vault2", jsonObj.directories.get(1).path);
Assertions.assertEquals("--foo --bar", jsonObj.directories.get(1).mountFlags);
Assertions.assertEquals(8080, jsonObj.port);
@@ -67,7 +67,7 @@ public class SettingsJsonTest {
public void testSerialize() throws JsonProcessingException {
var jsonObj = new SettingsJson();
jsonObj.directories = List.of(new VaultSettingsJson(), new VaultSettingsJson());
jsonObj.directories.get(0).id = "test";
jsonObj.directories.getFirst().id = "test";
jsonObj.theme = UiTheme.DARK;
jsonObj.showTrayIcon = false;
@@ -17,7 +17,6 @@ import org.mockito.Mockito;
import java.nio.file.FileSystem;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -40,9 +39,10 @@ public class FileOpenRequestHandlerTest {
public void testOpenArgsWithCorrectPaths() {
inTest.handleLaunchArgs(List.of("foo", "bar"));
OpenFileEvent evt = Assertions.assertInstanceOf(OpenFileEvent.class, queue.poll());
AppLaunchEvent evt = queue.poll();
Assertions.assertNotNull(evt);
Collection<Path> paths = evt.pathsToOpen();
MatcherAssert.assertThat(paths, CoreMatchers.hasItems(Paths.get("foo"), Paths.get("bar")));
MatcherAssert.assertThat(paths, CoreMatchers.hasItems(Path.of("foo"), Path.of("bar")));
}
@Test
@@ -59,7 +59,7 @@ public class FileOpenRequestHandlerTest {
@Test
@DisplayName("./cryptomator.exe foo (with full event queue)")
public void testOpenArgsWithFullQueue() {
queue.add(new OpenFileEvent(Collections.emptyList()));
queue.add(new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, Collections.emptyList()));
Assumptions.assumeTrue(queue.remainingCapacity() == 0);
inTest.handleLaunchArgs(List.of("foo"));
@@ -1,98 +0,0 @@
package org.cryptomator.launcher;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import java.net.URI;
import java.util.List;
public class LaunchArgsParserTest {
private FileOpenRequestHandler fileOpenRequestHandler;
private URIOpenRequestHandler uriOpenRequestHandler;
private NoopRequestHandler noopRequestHandler;
private LaunchArgsParser inTest;
@BeforeEach
public void setup() {
fileOpenRequestHandler = Mockito.mock(FileOpenRequestHandler.class);
uriOpenRequestHandler = Mockito.mock(URIOpenRequestHandler.class);
noopRequestHandler = Mockito.mock(NoopRequestHandler.class);
inTest = new LaunchArgsParser(fileOpenRequestHandler, uriOpenRequestHandler, noopRequestHandler);
}
@Test
@DisplayName("only file paths are forwarded to the FileOpenRequestHandler")
public void testOnlyPaths() {
inTest.process(List.of("foo", "bar"));
Mockito.verify(fileOpenRequestHandler).handleLaunchArgs(List.of("foo", "bar"));
Mockito.verifyNoInteractions(uriOpenRequestHandler);
Mockito.verifyNoInteractions(noopRequestHandler);
}
@Test
@DisplayName("empty args are forwarded to the NoopRequestHandler")
public void testEmptyArgs() {
inTest.process(List.of());
Mockito.verify(noopRequestHandler).revealApp();
Mockito.verifyNoInteractions(uriOpenRequestHandler, fileOpenRequestHandler);
}
@Test
@DisplayName("a Windows path is not mistaken for a URI")
public void testWindowsPathIsNotAUri() {
inTest.process(List.of("C:\\Users\\foo\\vault.cryptomator"));
Mockito.verify(fileOpenRequestHandler).handleLaunchArgs(List.of("C:\\Users\\foo\\vault.cryptomator"));
Mockito.verifyNoInteractions(uriOpenRequestHandler, noopRequestHandler);
}
@Test
@DisplayName("a single cryptomator:// URI is forwarded to the URIOpenRequestHandler")
public void testSingleUri() {
inTest.process(List.of("cryptomator://vault/foo"));
Mockito.verify(uriOpenRequestHandler).handleLaunchArgs(URI.create("cryptomator://vault/foo"));
Mockito.verifyNoInteractions(fileOpenRequestHandler, noopRequestHandler);
}
@Test
@DisplayName("a file:// URI is converted to a path and forwarded to the FileOpenRequestHandler")
public void testFileUriIsTreatedAsPath() {
inTest.process(List.of("file:///tmp/vault.cryptomator"));
var captor = ArgumentCaptor.forClass(List.class);
Mockito.verify(fileOpenRequestHandler).handleLaunchArgs(captor.capture());
Mockito.verifyNoInteractions(uriOpenRequestHandler, noopRequestHandler);
Assertions.assertEquals(1, captor.getValue().size());
Assertions.assertFalse(captor.getValue().getFirst().toString().startsWith("file:"));
}
@Test
@DisplayName("mixing a URI with a file path fails")
public void testMixedUriAndPathFails() {
Assertions.assertThrows(IllegalArgumentException.class, () -> inTest.process(List.of("cryptomator://vault/foo", "bar")));
Mockito.verifyNoInteractions(fileOpenRequestHandler, uriOpenRequestHandler, noopRequestHandler);
}
@Test
@DisplayName("more than one URI fails")
public void testMultipleUrisFail() {
Assertions.assertThrows(IllegalArgumentException.class, () -> inTest.process(List.of("cryptomator://vault/foo", "cryptomator://vault/bar")));
Mockito.verifyNoInteractions(fileOpenRequestHandler, uriOpenRequestHandler, noopRequestHandler);
}
@Test
@DisplayName("a URI that is not the first parameter fails")
public void testUriNotFirstFails() {
Assertions.assertThrows(IllegalArgumentException.class, () -> inTest.process(List.of("foo", "cryptomator://vault/bar")));
Mockito.verifyNoInteractions(fileOpenRequestHandler, uriOpenRequestHandler, noopRequestHandler);
}
}
@@ -1,220 +0,0 @@
package org.cryptomator.launcher;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Optional;
import java.util.UUID;
public class OpenHubVaultEventTest {
private static final String VAULT_ID = "d3a1f0b2-7c4e-4a1d-9f3b-2e5c6a7b8c9d";
private static final String KEY_ID = "hub+https://hub.example.com/api/vaults/" + VAULT_ID;
private static final String TOKEN = hubVaultConfig(KEY_ID);
@Test
@DisplayName("a valid vault/open deeplink is parsed")
public void testValid() {
var inTest = OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + TOKEN)).orElseThrow();
Assertions.assertEquals(UUID.fromString(VAULT_ID), inTest.vaultId());
Assertions.assertEquals(URI.create(KEY_ID), inTest.vaultConfig().getKeyId());
}
@Test
@DisplayName("parameters in the query instead of the fragment are not accepted")
public void testQueryParamsRejected() {
var uri = URI.create("org.cryptomator://vault/open?vaultConfig=" + TOKEN);
Assertions.assertThrows(IllegalArgumentException.class, () -> OpenHubVaultEvent.tryParse(uri));
}
@Test
@DisplayName("an encoded separator inside a value cannot forge another parameter")
public void testNoParameterInjectionViaEncodedSeparator() {
// '%26' must stay part of the first value; decoding the fragment before splitting would turn it into a real
// separator and smuggle in a 'vaultConfig' the link never carried - hence getRawFragment(), decoding per value.
var uri = URI.create("org.cryptomator://vault/open#other=a%26vaultConfig=" + TOKEN);
Assertions.assertThrows(IllegalArgumentException.class, () -> OpenHubVaultEvent.tryParse(uri));
}
@Test
@DisplayName("a config exceeding the size limit is rejected")
public void testExceedsSizeLimit() {
var oversized = "a".repeat(8193);
Assertions.assertThrows(IllegalArgumentException.class, //
() -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + oversized)));
}
@Test
@DisplayName("a config that is not a decodable token is rejected")
public void testNotAToken() {
Assertions.assertThrows(IllegalArgumentException.class, //
() -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=not-a-jwt")));
}
@Test
@DisplayName("a config without a hub key id is rejected")
public void testNotAHubVault() {
var token = hubVaultConfig("masterkeyfile:masterkey.cryptomator");
Assertions.assertThrows(IllegalArgumentException.class, //
() -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + token)));
}
@Test
@DisplayName("a config without a hub header is rejected")
public void testNoHubHeader() {
var token = vaultConfig(KEY_ID, VAULT_ID, null);
Assertions.assertThrows(IllegalArgumentException.class, //
() -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + token)));
}
@ParameterizedTest
@DisplayName("a vault id that is not a uuid is rejected")
@ValueSource(strings = { //
"not-a-uuid", //
"", // absent
"../../evil" // must never reach the api/vaults/{vaultId}/... request path
})
public void testInvalidVaultId(String vaultId) {
var token = vaultConfig(KEY_ID, vaultId, hubHeader("https://hub.example.com/api", "https://login.example.com/auth"));
Assertions.assertThrows(IllegalArgumentException.class, //
() -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + token)));
}
@Test
@DisplayName("a vault id disagreeing with the key id is rejected")
public void testVaultIdMismatch() {
// Hub writes the same id into both, so a config where they differ is forged or broken
var token = vaultConfig(KEY_ID, "11111111-2222-3333-4444-555555555555", //
hubHeader("https://hub.example.com/api", "https://login.example.com/auth"));
Assertions.assertThrows(IllegalArgumentException.class, //
() -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + token)));
}
@ParameterizedTest
@DisplayName("an unusable hub endpoint is rejected")
@ValueSource(strings = { //
"ftp://hub.example.com/api", // neither http nor https
"/api", // not absolute
"https:///api" // no host
})
public void testUnusableApiBaseUrl(String apiBaseUrl) {
var token = vaultConfig(KEY_ID, VAULT_ID, hubHeader(apiBaseUrl, "https://login.example.com/auth"));
Assertions.assertThrows(IllegalArgumentException.class, //
() -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + token)));
}
@Test
@DisplayName("an unusable auth endpoint is rejected")
public void testUnusableAuthEndpoint() {
var token = vaultConfig(KEY_ID, VAULT_ID, hubHeader("https://hub.example.com/api", "not a url"));
Assertions.assertThrows(IllegalArgumentException.class, //
() -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + token)));
}
@Test
@DisplayName("an http endpoint is accepted here, host trust decides later")
public void testHttpEndpointAccepted() {
var token = vaultConfig(KEY_ID, VAULT_ID, hubHeader("http://localhost:8080/api", "http://localhost:8080/auth"));
Assertions.assertTrue(OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + token)).isPresent());
}
@Test
@DisplayName("a non-cryptomator scheme yields empty")
public void testWrongScheme() {
Assertions.assertEquals(Optional.empty(), OpenHubVaultEvent.tryParse(URI.create("foobar://vault/open#vaultConfig=" + TOKEN)));
}
@Test
@DisplayName("the bare cryptomator scheme is no longer recognized")
public void testLegacyScheme() {
Assertions.assertEquals(Optional.empty(), OpenHubVaultEvent.tryParse(URI.create("cryptomator://vault/open#vaultConfig=" + TOKEN)));
}
@Test
@DisplayName("an unknown host yields empty")
public void testWrongHost() {
Assertions.assertEquals(Optional.empty(), OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://foo/open#vaultConfig=" + TOKEN)));
}
@Test
@DisplayName("an unknown path yields empty")
public void testWrongPath() {
Assertions.assertEquals(Optional.empty(), OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/create#vaultConfig=" + TOKEN)));
}
@Test
@DisplayName("a matching host is recognized case-insensitively")
public void testHostCaseInsensitive() {
Assertions.assertTrue(OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://VAULT/open#vaultConfig=" + TOKEN)).isPresent());
}
@Test
@DisplayName("a missing config fails")
public void testMissingConfig() {
Assertions.assertThrows(IllegalArgumentException.class, () -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open")));
}
@Test
@DisplayName("a blank config fails")
public void testBlankConfig() {
Assertions.assertThrows(IllegalArgumentException.class, () -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=")));
}
private static String hubHeader(String apiBaseUrl, String authEndpoint) {
return """
,
"hub": {
"clientId":"cryptomator",\
"authEndpoint":"%s",\
"tokenEndpoint":"https://login.example.com/token",\
"authSuccessUrl":"https://hub.example.com/app/unlock-success",\
"authErrorUrl":"https://hub.example.com/app/unlock-error",\
"apiBaseUrl":"%s"
}""".formatted(authEndpoint, apiBaseUrl);
}
private static String hubVaultConfig(String keyId) {
return vaultConfig(keyId, VAULT_ID, hubHeader("https://hub.example.com/api", "https://login.example.com/auth"));
}
/**
* Builds a vault config token. Its signature is keyed on the masterkey, which the deeplink never carries, so a dummy
* signature is exactly what the parser operates on.
*/
private static String vaultConfig(String keyId, String vaultId, String extraHeaderFields) {
var header = """
{ "kid":"%s",\
"typ":"JWT",\
"alg":"HS256"\
%s
}""".formatted(keyId, extraHeaderFields == null ? "" : extraHeaderFields);
var payload = """
{ "jti":"%s",\
"format":8,\
"cipherCombo":"SIV_GCM",\
"shorteningThreshold":220\
}""".formatted(vaultId);
var encoder = Base64.getUrlEncoder().withoutPadding();
return encoder.encodeToString(header.getBytes(StandardCharsets.UTF_8)) //
+ "." + encoder.encodeToString(payload.getBytes(StandardCharsets.UTF_8)) //
+ "." + encoder.encodeToString("signature".getBytes(StandardCharsets.UTF_8));
}
}
@@ -1,56 +0,0 @@
package org.cryptomator.ui.fxapp;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.launcher.AppLaunchEvent;
import org.cryptomator.launcher.RevealRunningEvent;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.dialogs.Dialogs;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import javafx.collections.FXCollections;
import javafx.stage.Stage;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
public class AppLaunchEventHandlerTest {
private BlockingQueue<AppLaunchEvent> queue;
private ExecutorService executor;
private FxApplicationWindows appWindows;
private AppLaunchEventHandler handler;
@BeforeEach
public void setup() {
queue = new LinkedBlockingQueue<>();
executor = Executors.newSingleThreadExecutor();
appWindows = mock(FxApplicationWindows.class);
handler = new AppLaunchEventHandler(queue, executor, appWindows, mock(VaultListManager.class), FXCollections.observableArrayList(), mock(VaultService.class), mock(Stage.class), mock(Dialogs.class));
}
@AfterEach
public void teardown() {
executor.shutdownNow();
}
// TODO: Add test for OpenHubVaultEvent once the event opens the hub vault flow for real.
@Test
@DisplayName("a RevealRunningEvent reveals the main window")
public void testRevealRunningEventShowsMainWindow() {
queue.add(new RevealRunningEvent());
handler.startHandlingLaunchEvents();
verify(appWindows, timeout(2000)).showMainWindow();
}
}
@@ -1,92 +0,0 @@
package org.cryptomator.ui.keyloading.hub;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultConfigCache;
import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.net.URI;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class HubVaultsTest {
private static final UUID VAULT_ID = UUID.fromString("d3a1f0b2-7c4e-4a1d-9f3b-2e5c6a7b8c9d");
private static final UUID OTHER_VAULT_ID = UUID.fromString("11111111-2222-3333-4444-555555555555");
@Test
@DisplayName("the hub vault with the requested id is found")
public void testFindsMatchingVault() throws IOException {
var wanted = hubVault(VAULT_ID);
var vaults = List.of(hubVault(OTHER_VAULT_ID), wanted);
Assertions.assertEquals(Optional.of(wanted), HubVaults.findByVaultId(vaults, VAULT_ID));
}
@Test
@DisplayName("no vault with the requested id yields empty")
public void testNoMatch() throws IOException {
var vaults = List.of(hubVault(OTHER_VAULT_ID));
Assertions.assertEquals(Optional.empty(), HubVaults.findByVaultId(vaults, VAULT_ID));
}
@Test
@DisplayName("a password vault never matches, even carrying the same id")
public void testIgnoresPasswordVault() throws IOException {
var vaults = List.of(vault("masterkeyfile:masterkey.cryptomator", VAULT_ID));
Assertions.assertEquals(Optional.empty(), HubVaults.findByVaultId(vaults, VAULT_ID));
}
@Test
@DisplayName("a vault whose config cannot be read is skipped, not fatal")
public void testSkipsUnreadableVault() throws IOException {
// e.g. a vault on a network drive that is currently offline - it must not hide a vault further down the list
var wanted = hubVault(VAULT_ID);
var vaults = List.of(unreadableVault(), wanted);
Assertions.assertEquals(Optional.of(wanted), HubVaults.findByVaultId(vaults, VAULT_ID));
}
// setup/mock provider
private static Vault hubVault(UUID vaultId) throws IOException {
return vault("hub+https://hub.example.com/api/vaults/" + vaultId, vaultId);
}
private static Vault vault(String keyId, UUID vaultId) throws IOException {
var configCache = mock(VaultConfigCache.class);
var config = mockConfig(keyId, vaultId);
when(configCache.get()).thenReturn(config);
return vaultWith(configCache);
}
private static Vault unreadableVault() throws IOException {
var configCache = mock(VaultConfigCache.class);
when(configCache.get()).thenThrow(new IOException("vault directory unavailable"));
return vaultWith(configCache);
}
private static Vault vaultWith(VaultConfigCache configCache) {
var vault = mock(Vault.class);
when(vault.getVaultConfigCache()).thenReturn(configCache);
return vault;
}
private static UnverifiedVaultConfig mockConfig(String keyId, UUID vaultId) {
var mock = mock(UnverifiedVaultConfig.class);
when(mock.getKeyId()).thenReturn(URI.create(keyId));
when(mock.allegedVaultId()).thenReturn(vaultId.toString());
return mock;
}
}