mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-09-23 00:14:40 +00:00
Compare commits
125
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6225d8d1f0 | ||
|
|
ed975b459e | ||
|
|
800b2440b3 | ||
|
|
f50a49e7a3 | ||
|
|
92ece4dfb6 | ||
|
|
174225c60e | ||
|
|
3071410a22 | ||
|
|
5c5777ffc5 | ||
|
|
8dd8b93656 | ||
|
|
3b2d455b4a | ||
|
|
b0dfa22903 | ||
|
|
88b1c28d88 | ||
|
|
5b85d7859a | ||
|
|
468eed58d5 | ||
|
|
34c17be474 | ||
|
|
5dedd6b1c1 | ||
|
|
25e8e81686 | ||
|
|
1f7ab03bbb | ||
|
|
86f3cb7288 | ||
|
|
a1b8bf23b4 | ||
|
|
1f21d5ea4c | ||
|
|
f8ff7201d7 | ||
|
|
3567c036c3 | ||
|
|
0cf1f087ad | ||
|
|
209f60727e | ||
|
|
560a979e0e | ||
|
|
1debe4c7c8 | ||
|
|
2e2aa77727 | ||
|
|
162ebf6545 | ||
|
|
2d503a0edb | ||
|
|
7f7f0a099a | ||
|
|
109f5d1faa | ||
|
|
9b55f6fc56 | ||
|
|
83879f5cfe | ||
|
|
083ac5a7e0 | ||
|
|
a7eba377ba | ||
|
|
4ee1e6d9f1 | ||
|
|
24a63c10d0 | ||
|
|
0d805b2d43 | ||
|
|
7bc47fe6d7 | ||
|
|
ae50846257 | ||
|
|
ce466e7715 | ||
|
|
15885545b5 | ||
|
|
29fedcd390 | ||
|
|
fadd6b761f | ||
|
|
d857335901 | ||
|
|
93b4cbfb2c | ||
|
|
625334c6c8 | ||
|
|
943374856e | ||
|
|
e1d7fd92ad | ||
|
|
c10dc74818 | ||
|
|
92d9f2c18d | ||
|
|
d7488b6984 | ||
|
|
88ad3cd724 | ||
|
|
ba9e0b0c12 | ||
|
|
bbe7255901 | ||
|
|
51e55d3e3b | ||
|
|
f704dc0de9 | ||
|
|
e858280f30 | ||
|
|
f9b170204a | ||
|
|
0ed7415a0d | ||
|
|
7a0d50ecca | ||
|
|
19e035aa33 | ||
|
|
830970cb75 | ||
|
|
9b2987d0a2 | ||
|
|
76a4062f8b | ||
|
|
113717f955 | ||
|
|
c40ad58028 | ||
|
|
d6a7efcb7f | ||
|
|
910b89d6ce | ||
|
|
5554dfdd89 | ||
|
|
1657cd50fc | ||
|
|
57bf5e11b5 | ||
|
|
594ca47c85 | ||
|
|
6c5ee14c73 | ||
|
|
8e9d54b44b | ||
|
|
70805665bf | ||
|
|
ac243a706e | ||
|
|
aa382ce80d | ||
|
|
b7da508ccc | ||
|
|
47bd0ca647 | ||
|
|
c5d6c0ce98 | ||
|
|
6017d6b7a9 | ||
|
|
e31e06b288 | ||
|
|
84ac803a7d | ||
|
|
bcb970afb1 | ||
|
|
9d640b57ce | ||
|
|
4c83617847 | ||
|
|
5d7906972b | ||
|
|
666cd4a4f0 | ||
|
|
eb4d39e8b4 | ||
|
|
a1ad89a2e2 | ||
|
|
38108299fa | ||
|
|
189e2679f1 | ||
|
|
22ceb57052 | ||
|
|
0130c7cce6 | ||
|
|
18b6484731 | ||
|
|
f19ff59005 | ||
|
|
8786b5ee2a | ||
|
|
c7e23f9646 | ||
|
|
4043e3f71f | ||
|
|
08887f1147 | ||
|
|
13a0578ab7 | ||
|
|
888bf04ec2 | ||
|
|
d0822041ec | ||
|
|
e65a57bba9 | ||
|
|
2bb1b09789 | ||
|
|
b83dad4e24 | ||
|
|
cfd433b328 | ||
|
|
f48963a6d0 | ||
|
|
6ce34efb52 | ||
|
|
cd10d38990 | ||
|
|
8cb21c97e3 | ||
|
|
0ad8ce77ef | ||
|
|
e358ffd666 | ||
|
|
496f9a9cd5 | ||
|
|
61025de0d6 | ||
|
|
f08049b960 | ||
|
|
746c8f54eb | ||
|
|
9fc1efa005 | ||
|
|
448eac8ff5 | ||
|
|
5a18d086e0 | ||
|
|
918ace2eb6 | ||
|
|
fe733967dc | ||
|
|
59f5c0cb12 |
@@ -43,6 +43,7 @@ body:
|
||||
- WinFsp (Local Drive)
|
||||
- FUSE-T
|
||||
- macFUSE
|
||||
- FUSE
|
||||
- WebDAV (Windows Explorer)
|
||||
- WebDAV (AppleScript)
|
||||
- WebDAV (gio)
|
||||
@@ -95,4 +96,4 @@ body:
|
||||
id: further-info
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: Links? References? Screenshots? Configurations? Any data that might be necessary to reproduce the issue?
|
||||
description: Links? References? Screenshots? Configurations? Any data that might be necessary to reproduce the issue?
|
||||
|
||||
@@ -10,7 +10,8 @@ on:
|
||||
required: false
|
||||
|
||||
env:
|
||||
JAVA_VERSION: 20
|
||||
JAVA_DIST: 'zulu'
|
||||
JAVA_VERSION: 21
|
||||
|
||||
jobs:
|
||||
get-version:
|
||||
@@ -20,27 +21,50 @@ jobs:
|
||||
|
||||
build:
|
||||
name: Build AppImage
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: [get-version]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
appimage-suffix: x86_64
|
||||
openjfx-url: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_linux-x64_bin-jmods.zip'
|
||||
openjfx-sha: 'f522ac2ae4bdd61f0219b7b8d2058ff72a22f36a44378453bcfdcd82f8f5e08c'
|
||||
- os: [self-hosted, Linux, ARM64]
|
||||
appimage-suffix: aarch64
|
||||
openjfx-url: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_linux-aarch64_bin-jmods.zip'
|
||||
openjfx-sha: 'c0d80ebbe0aab404ef9ad8b46c05bf533a1e40b39b2720eebd9238d81f6326ca'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
distribution: ${{ env.JAVA_DIST }}
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
java-package: 'jdk+fx'
|
||||
check-latest: true
|
||||
cache: 'maven'
|
||||
- name: Ensure major jfx version in pom equals in jdk
|
||||
shell: pwsh
|
||||
|
||||
- name: Download OpenJFX jmods
|
||||
id: download-jmods
|
||||
run: |
|
||||
$jfxPomVersion = (&mvn help:evaluate "-Dexpression=javafx.version" -q -DforceStdout) -split "\."
|
||||
$jfxJdkVersion = ((Get-Content -path "${env:JAVA_HOME}/lib/javafx.properties" | Where-Object {$_ -like 'javafx.version=*' }) -replace '.*=','') -split "\."
|
||||
if ($jfxPomVersion[0] -ne $jfxJdkVersion[0]) {
|
||||
Write-Error "Major part of JavaFX version in pom($($jfxPomVersion[0])) does not match the version in JDK($($jfxJdkVersion[0])) "
|
||||
curl -L ${{ matrix.openjfx-url }} -o openjfx-jmods.zip
|
||||
echo "${{ matrix.openjfx-sha }} openjfx-jmods.zip" | shasum -a256 --check
|
||||
mkdir -p openjfx-jmods
|
||||
unzip -j openjfx-jmods.zip \*/javafx.base.jmod \*/javafx.controls.jmod \*/javafx.fxml.jmod \*/javafx.graphics.jmod -d openjfx-jmods
|
||||
- name: Ensure major jfx version in pom and in jmods is the same
|
||||
run: |
|
||||
JMOD_VERSION=$(jmod describe openjfx-jmods/javafx.base.jmod | head -1)
|
||||
JMOD_VERSION=${JMOD_VERSION#*@}
|
||||
JMOD_VERSION=${JMOD_VERSION%%.*}
|
||||
POM_JFX_VERSION=$(mvn help:evaluate "-Dexpression=javafx.version" -q -DforceStdout)
|
||||
POM_JFX_VERSION=${POM_JFX_VERSION#*@}
|
||||
POM_JFX_VERSION=${POM_JFX_VERSION%%.*}
|
||||
|
||||
if [ $POM_JFX_VERSION -ne $JMOD_VERSION_AMD64 ]; then
|
||||
>&2 echo "Major JavaFX version in pom.xml (${POM_JFX_VERSION}) != amd64 jmod version (${JMOD_VERSION})"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
- name: Set version
|
||||
run : mvn versions:set -DnewVersion=${{ needs.get-version.outputs.semVerStr }}
|
||||
- name: Run maven
|
||||
@@ -54,7 +78,7 @@ jobs:
|
||||
${JAVA_HOME}/bin/jlink
|
||||
--verbose
|
||||
--output runtime
|
||||
--module-path "${JAVA_HOME}/jmods"
|
||||
--module-path "${JAVA_HOME}/jmods:openjfx-jmods"
|
||||
--add-modules java.base,java.desktop,java.instrument,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,javafx.base,javafx.graphics,javafx.controls,javafx.fxml,jdk.unsupported,jdk.crypto.ec,jdk.security.auth,jdk.accessibility,jdk.management.jfr,jdk.net
|
||||
--strip-native-commands
|
||||
--no-header-files
|
||||
@@ -119,7 +143,7 @@ jobs:
|
||||
ln -s bin/cryptomator.sh Cryptomator.AppDir/AppRun
|
||||
- name: Download AppImageKit
|
||||
run: |
|
||||
curl -L https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage -o appimagetool.AppImage
|
||||
curl -L https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-${{ matrix.appimage-suffix }}.AppImage -o appimagetool.AppImage
|
||||
chmod +x appimagetool.AppImage
|
||||
./appimagetool.AppImage --appimage-extract
|
||||
- name: Prepare GPG-Agent for signing with key 615D449FE6E6A235
|
||||
@@ -131,8 +155,8 @@ jobs:
|
||||
GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }}
|
||||
- name: Build AppImage
|
||||
run: >
|
||||
./squashfs-root/AppRun Cryptomator.AppDir cryptomator-${{ needs.get-version.outputs.semVerStr }}-x86_64.AppImage
|
||||
-u 'gh-releases-zsync|cryptomator|cryptomator|latest|cryptomator-*-x86_64.AppImage.zsync'
|
||||
./squashfs-root/AppRun Cryptomator.AppDir cryptomator-${{ needs.get-version.outputs.semVerStr }}-${{ matrix.appimage-suffix }}.AppImage
|
||||
-u 'gh-releases-zsync|cryptomator|cryptomator|latest|cryptomator-*-${{ matrix.appimage-suffix }}.AppImage.zsync'
|
||||
--sign --sign-key=615D449FE6E6A235 --sign-args="--batch --pinentry-mode loopback"
|
||||
- name: Create detached GPG signatures
|
||||
run: |
|
||||
@@ -141,7 +165,7 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: appimage
|
||||
name: appimage-${{ matrix.appimage-suffix }}
|
||||
path: |
|
||||
cryptomator-*.AppImage
|
||||
cryptomator-*.AppImage.zsync
|
||||
|
||||
@@ -6,7 +6,8 @@ on:
|
||||
types: [labeled]
|
||||
|
||||
env:
|
||||
JAVA_VERSION: 20
|
||||
JAVA_DIST: 'zulu'
|
||||
JAVA_VERSION: 21
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -17,10 +18,10 @@ jobs:
|
||||
name: Compile and Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
distribution: ${{ env.JAVA_DIST }}
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
cache: 'maven'
|
||||
- name: Cache SonarCloud packages
|
||||
|
||||
@@ -16,16 +16,20 @@ on:
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
JAVA_VERSION: 20
|
||||
JAVA_DIST: 'zulu'
|
||||
JAVA_VERSION: 21
|
||||
COFFEELIBS_JDK_VERSION: 21
|
||||
OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_linux-x64_bin-jmods.zip'
|
||||
OPENJFX_JMODS_AMD64_HASH: 'f522ac2ae4bdd61f0219b7b8d2058ff72a22f36a44378453bcfdcd82f8f5e08c'
|
||||
OPENJFX_JMODS_AARCH64: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_linux-aarch64_bin-jmods.zip'
|
||||
OPENJFX_JMODS_AARCH64_HASH: 'c0d80ebbe0aab404ef9ad8b46c05bf533a1e40b39b2720eebd9238d81f6326ca'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Debian Package
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- id: versions
|
||||
name: Get version information
|
||||
run: |
|
||||
@@ -39,11 +43,11 @@ jobs:
|
||||
run: |
|
||||
sudo add-apt-repository ppa:coffeelibs/openjdk
|
||||
sudo apt-get update
|
||||
sudo apt-get install debhelper devscripts dput coffeelibs-jdk-${{ env.JAVA_VERSION }} libgtk2.0-0
|
||||
sudo apt-get install debhelper devscripts dput coffeelibs-jdk-${{ env.COFFEELIBS_JDK_VERSION }} libgtk2.0-0
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
distribution: ${{ env.JAVA_DIST }}
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
check-latest: true
|
||||
cache: 'maven'
|
||||
@@ -53,9 +57,11 @@ jobs:
|
||||
id: download-jmods
|
||||
run: |
|
||||
curl -L ${{ env.OPENJFX_JMODS_AMD64 }} -o openjfx-amd64.zip
|
||||
echo "${{ env.OPENJFX_JMODS_AMD64_HASH }} openjfx-amd64.zip" | shasum -a256 --check
|
||||
mkdir -p jmods/amd64
|
||||
unzip -j openjfx-amd64.zip \*/javafx.base.jmod \*/javafx.controls.jmod \*/javafx.fxml.jmod \*/javafx.graphics.jmod -d jmods/amd64
|
||||
curl -L ${{ env.OPENJFX_JMODS_AARCH64 }} -o openjfx-aarch64.zip
|
||||
echo "${{ env.OPENJFX_JMODS_AARCH64_HASH }} openjfx-aarch64.zip" | shasum -a256 --check
|
||||
mkdir -p jmods/aarch64
|
||||
unzip -j openjfx-aarch64.zip \*/javafx.base.jmod \*/javafx.controls.jmod \*/javafx.fxml.jmod \*/javafx.graphics.jmod -d jmods/aarch64
|
||||
- name: Ensure major jfx version in pom and in jmods is the same
|
||||
@@ -92,7 +98,8 @@ jobs:
|
||||
run: |
|
||||
cp -r dist/linux/debian/ pkgdir
|
||||
export RFC2822_TIMESTAMP=`date --rfc-2822`
|
||||
envsubst '${SEMVER_STR} ${VERSION_NUM} ${REVISION_NUM}' < dist/linux/debian/rules > pkgdir/debian/rules
|
||||
export DISABLE_UPDATE_CHECK=${{ inputs.dput }}
|
||||
envsubst '${SEMVER_STR} ${VERSION_NUM} ${REVISION_NUM} ${DISABLE_UPDATE_CHECK}' < dist/linux/debian/rules > pkgdir/debian/rules
|
||||
envsubst '${PPA_VERSION} ${RFC2822_TIMESTAMP}' < dist/linux/debian/changelog > pkgdir/debian/changelog
|
||||
find . -name "*.jar" >> pkgdir/debian/source/include-binaries
|
||||
mv pkgdir cryptomator_${{ inputs.ppaver }}
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Update Error Database
|
||||
|
||||
on:
|
||||
discussion:
|
||||
types: [created, edited, category_changed, answered, unanswered]
|
||||
types: [created, edited, deleted, category_changed, answered, unanswered]
|
||||
discussion_comment:
|
||||
types: [created, edited, deleted]
|
||||
|
||||
@@ -12,6 +12,7 @@ jobs:
|
||||
if: github.event.discussion.category.name == 'Errors'
|
||||
steps:
|
||||
- name: Query Discussion Data
|
||||
if: github.event_name == 'discussion_comment' || github.event_name == 'discussion' && github.event.action != 'deleted'
|
||||
id: query-data
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
@@ -47,8 +48,13 @@ jobs:
|
||||
- name: Merge Error Code Data
|
||||
run: |
|
||||
jq -c '.' ${{ steps.get-gist.outputs.file }} > original.json
|
||||
echo $DISCUSSION | jq -c '.repository.discussion | .comments = .comments.totalCount | {(.id|tostring) : .}' > new.json
|
||||
jq -s '.[0] * .[1]' original.json new.json > merged.json
|
||||
if [ ! -z "$DISCUSSION" ]
|
||||
then
|
||||
echo $DISCUSSION | jq -c '.repository.discussion | .comments = .comments.totalCount | {(.id|tostring) : .}' > new.json
|
||||
jq -s '.[0] * .[1]' original.json new.json > merged.json
|
||||
else
|
||||
cat original.json | jq 'del(.[] | select(.url=="https://github.com/cryptomator/cryptomator/discussions/${{ github.event.discussion.number }}"))' > merged.json
|
||||
fi
|
||||
env:
|
||||
DISCUSSION: ${{ steps.query-data.outputs.result }}
|
||||
- name: Patch Gist
|
||||
|
||||
@@ -22,9 +22,8 @@ on:
|
||||
value: ${{ jobs.determine-version.outputs.type }}
|
||||
|
||||
env:
|
||||
JAVA_VERSION: 20
|
||||
JAVA_DIST: 'temurin'
|
||||
JAVA_CACHE: 'maven'
|
||||
JAVA_DIST: 'zulu'
|
||||
JAVA_VERSION: 21
|
||||
|
||||
jobs:
|
||||
determine-version:
|
||||
@@ -36,7 +35,7 @@ jobs:
|
||||
revNum: ${{ steps.versions.outputs.revNum }}
|
||||
type: ${{ steps.versions.outputs.type}}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Java
|
||||
@@ -44,7 +43,7 @@ jobs:
|
||||
with:
|
||||
distribution: ${{ env.JAVA_DIST }}
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
cache: ${{ env.JAVA_CACHE }}
|
||||
cache: 'maven'
|
||||
- id: versions
|
||||
name: Get version information
|
||||
run: |
|
||||
|
||||
@@ -15,7 +15,8 @@ on:
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
JAVA_VERSION: 20
|
||||
JAVA_DIST: 'zulu'
|
||||
JAVA_VERSION: 21
|
||||
|
||||
jobs:
|
||||
get-version:
|
||||
@@ -36,23 +37,22 @@ jobs:
|
||||
output-suffix: x64
|
||||
xcode-path: '/Applications/Xcode_13.2.1.app'
|
||||
fuse-lib: macFUSE
|
||||
openjfx-url: https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_osx-x64_bin-jmods.zip
|
||||
openjfx-sha: 55b8ff7453d59c89ae129f6c9c5ad7b09a5d359568811b376ac1766c14d6a17c
|
||||
openjfx-url: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_osx-x64_bin-jmods.zip'
|
||||
openjfx-sha: '55b8ff7453d59c89ae129f6c9c5ad7b09a5d359568811b376ac1766c14d6a17c'
|
||||
- os: [self-hosted, macOS, ARM64]
|
||||
architecture: aarch64
|
||||
output-suffix: arm64
|
||||
xcode-path: '/Applications/Xcode_13.2.1.app'
|
||||
fuse-lib: FUSE-T
|
||||
openjfx-url: https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_osx-aarch64_bin-jmods.zip
|
||||
openjfx-sha: c60f5f19aa847e0e620e0b011e5de68f2c6755641c2141cec27a0b89f612beaf
|
||||
openjfx-url: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_osx-aarch64_bin-jmods.zip'
|
||||
openjfx-sha: 'c60f5f19aa847e0e620e0b011e5de68f2c6755641c2141cec27a0b89f612beaf'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
distribution: ${{ env.JAVA_DIST }}
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
java-package: 'jdk'
|
||||
architecture: ${{ matrix.architecture }}
|
||||
check-latest: true
|
||||
cache: 'maven'
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
curl -L ${{ matrix.openjfx-url }} -o openjfx-jmods.zip
|
||||
echo "${{ matrix.openjfx-sha }} *openjfx-jmods.zip" | shasum -a256 --check
|
||||
mkdir -p openjfx-jmods/
|
||||
unzip -j openjfx-jmods.zip \*/javafx.base.jmod \*/javafx.controls.jmod \*/javafx.fxml.jmod \*/javafx.graphics.jmod -d openjfx-jmods
|
||||
unzip -jo openjfx-jmods.zip \*/javafx.base.jmod \*/javafx.controls.jmod \*/javafx.fxml.jmod \*/javafx.graphics.jmod -d openjfx-jmods
|
||||
- name: Ensure major jfx version in pom and in jmods is the same
|
||||
run: |
|
||||
JMOD_VERSION=$(jmod describe openjfx-jmods/javafx.base.jmod | head -1)
|
||||
@@ -72,8 +72,8 @@ jobs:
|
||||
POM_JFX_VERSION=${POM_JFX_VERSION#*@}
|
||||
POM_JFX_VERSION=${POM_JFX_VERSION%%.*}
|
||||
|
||||
if [ $POM_JFX_VERSION -ne $JMOD_VERSION ]; then
|
||||
>&2 echo "Major JavaFX version in pom.xml (${POM_JFX_VERSION}) != jmod version (${JMOD_VERSION_AMD64})"
|
||||
if [ "${POM_JFX_VERSION}" -ne "${JMOD_VERSION}" ]; then
|
||||
>&2 echo "Major JavaFX version in pom.xml (${POM_JFX_VERSION}) != jmod version (${JMOD_VERSION})"
|
||||
exit 1
|
||||
fi
|
||||
- name: Set version
|
||||
@@ -222,7 +222,6 @@ jobs:
|
||||
--app-drop-link 512 245
|
||||
--eula "dist/mac/dmg/resources/license.rtf"
|
||||
--icon ".background" 128 758
|
||||
--icon ".fseventsd" 320 758
|
||||
--icon ".VolumeIcon.icns" 512 758
|
||||
Cryptomator-${VERSION_NO}-${{ matrix.output-suffix }}.dmg dmg
|
||||
env:
|
||||
|
||||
@@ -4,7 +4,8 @@ on:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
JAVA_VERSION: 20
|
||||
JAVA_DIST: 'zulu'
|
||||
JAVA_VERSION: 21
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -16,10 +17,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
distribution: ${{ env.JAVA_DIST }}
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
cache: 'maven'
|
||||
- name: Build and Test
|
||||
|
||||
@@ -6,9 +6,6 @@ on:
|
||||
- 'release/**'
|
||||
- 'hotfix/**'
|
||||
|
||||
env:
|
||||
JAVA_VERSION: 20
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
@@ -18,7 +15,7 @@ jobs:
|
||||
name: Validate commits pushed to release/hotfix branch to fulfill release requirements
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- id: validate-pom-version
|
||||
name: Validate POM version
|
||||
run: |
|
||||
|
||||
@@ -14,11 +14,10 @@ on:
|
||||
|
||||
|
||||
env:
|
||||
JAVA_VERSION: 20
|
||||
JAVA_DIST: 'temurin'
|
||||
JAVA_CACHE: 'maven'
|
||||
JFX_JMODS_URL: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_windows-x64_bin-jmods.zip'
|
||||
JFX_JMODS_HASH: '18625bbc13c57dbf802486564247a8d8cab72ec558c240a401bf6440384ebd77'
|
||||
JAVA_DIST: 'zulu'
|
||||
JAVA_VERSION: 21
|
||||
OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/20.0.2/openjfx-20.0.2_windows-x64_bin-jmods.zip'
|
||||
OPENJFX_JMODS_AMD64_HASH: '18625bbc13c57dbf802486564247a8d8cab72ec558c240a401bf6440384ebd77'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -38,21 +37,20 @@ jobs:
|
||||
LOOPBACK_ALIAS: 'cryptomator-vault'
|
||||
WIN_CONSOLE_FLAG: ''
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: ${{ env.JAVA_DIST }}
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
java-package: 'jdk'
|
||||
check-latest: true
|
||||
cache: ${{ env.JAVA_CACHE }}
|
||||
cache: 'maven'
|
||||
- name: Download and extract JavaFX jmods from Gluon
|
||||
#In the last step we move all jmods files a dir level up because jmods are placed inside a directory in the zip
|
||||
run: |
|
||||
curl --output jfxjmods.zip -L "${{ env.JFX_JMODS_URL }}"
|
||||
if(!(Get-FileHash -Path jfxjmods.zip -Algorithm SHA256).Hash.ToLower().equals("${{ env.JFX_JMODS_HASH }}")) {
|
||||
throw "Wrong checksum of JMOD archive downloaded from ${{ env.JFX_JMODS_URL }}.";
|
||||
curl --output jfxjmods.zip -L "${{ env.OPENJFX_JMODS_AMD64 }}"
|
||||
if(!(Get-FileHash -Path jfxjmods.zip -Algorithm SHA256).Hash.ToLower().equals("${{ env.OPENJFX_JMODS_AMD64_HASH }}")) {
|
||||
throw "Wrong checksum of JMOD archive downloaded from ${{ env.OPENJFX_JMODS_AMD64 }}.";
|
||||
}
|
||||
Expand-Archive -Path jfxjmods.zip -DestinationPath jfxjmods
|
||||
Get-ChildItem -Path jfxjmods -Recurse -Filter "*.jmod" | ForEach-Object { Move-Item -Path $_ -Destination $_.Directory.Parent}
|
||||
@@ -145,9 +143,29 @@ jobs:
|
||||
- name: Fix permissions
|
||||
run: attrib -r appdir/Cryptomator/Cryptomator.exe
|
||||
shell: pwsh
|
||||
- name: Extract integrations DLL for code signing
|
||||
- name: Extract jars with DLLs for Codesigning
|
||||
shell: pwsh
|
||||
run: gci ./appdir/Cryptomator/app/mods/ -File integrations-win-*.jar | ForEach-Object {Set-Location -Path $_.Directory; jar --file=$($_.FullName) --extract integrations.dll }
|
||||
run: |
|
||||
Add-Type -AssemblyName "System.io.compression.filesystem"
|
||||
$jarFolder = Resolve-Path ".\appdir\Cryptomator\app\mods"
|
||||
$jarExtractDir = New-Item -Path ".\appdir\jar-extract" -ItemType Directory
|
||||
|
||||
#for all jars inspect
|
||||
Get-ChildItem -Path $jarFolder -Filter "*.jar" | ForEach-Object {
|
||||
$jar = [Io.compression.zipfile]::OpenRead($_.FullName)
|
||||
if (@($jar.Entries | Where-Object {$_.Name.ToString().EndsWith(".dll")} | Select-Object -First 1).Count -gt 0) {
|
||||
#jars containing dlls extract
|
||||
Set-Location $jarExtractDir
|
||||
Expand-Archive -Path $_.FullName
|
||||
}
|
||||
$jar.Dispose()
|
||||
}
|
||||
- name: Extract wixhelper.dll for Codesigning #see https://github.com/cryptomator/cryptomator/issues/3130
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -Path appdir/jpackage-jmod -ItemType Directory
|
||||
& $env:JAVA_HOME\bin\jmod.exe extract --dir jpackage-jmod "${env:JAVA_HOME}\jmods\jdk.jpackage.jmod"
|
||||
Get-ChildItem -Recurse -Path "jpackage-jmod" -File wixhelper.dll | Select-Object -Last 1 | Copy-Item -Destination "appdir"
|
||||
- name: Codesign
|
||||
uses: skymatic/code-sign-action@v2
|
||||
with:
|
||||
@@ -156,12 +174,22 @@ jobs:
|
||||
certificatesha1: 5FC94CE149E5B511E621F53A060AC67CBD446B3A
|
||||
description: Cryptomator
|
||||
timestampUrl: 'http://timestamp.digicert.com'
|
||||
folder: appdir/Cryptomator
|
||||
folder: appdir
|
||||
recursive: true
|
||||
- name: Repack signed DLL into jar
|
||||
- name: Replace DLLs inside jars with signed ones
|
||||
shell: pwsh
|
||||
run: |
|
||||
gci ./appdir/Cryptomator/app/mods/ -File integrations-win-*.jar | ForEach-Object {Set-Location -Path $_.Directory; jar --file=$($_.FullName) --update integrations.dll; Remove-Item integrations.dll}
|
||||
$jarExtractDir = Resolve-Path ".\appdir\jar-extract"
|
||||
$jarFolder = Resolve-Path ".\appdir\Cryptomator\app\mods"
|
||||
Get-ChildItem -Path $jarExtractDir | ForEach-Object {
|
||||
$jarName = $_.Name
|
||||
$jarFile = "${jarFolder}\${jarName}.jar"
|
||||
Set-Location $_
|
||||
Get-ChildItem -Path $_ -Recurse -File "*.dll" | ForEach-Object {
|
||||
# update jar with signed dll
|
||||
jar --file="$jarFile" --update $(Resolve-Path -Relative -Path $_)
|
||||
}
|
||||
}
|
||||
- name: Generate license for MSI
|
||||
run: >
|
||||
mvn -B license:add-third-party
|
||||
@@ -195,6 +223,7 @@ jobs:
|
||||
--file-associations dist/win/resources/FAvaultFile.properties
|
||||
env:
|
||||
JP_WIXWIZARD_RESOURCES: ${{ github.workspace }}/dist/win/resources # requires abs path, used in resources/main.wxs
|
||||
JP_WIXHELPER_DIR: ${{ github.workspace }}\appdir
|
||||
- name: Codesign MSI
|
||||
uses: skymatic/code-sign-action@v2
|
||||
with:
|
||||
@@ -236,7 +265,7 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
needs: [get-version, build-msi]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Download .msi
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
@@ -249,7 +278,7 @@ jobs:
|
||||
distribution: ${{ env.JAVA_DIST }}
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
check-latest: true
|
||||
cache: ${{ env.JAVA_CACHE }}
|
||||
cache: 'maven'
|
||||
- name: Generate license for exe
|
||||
run: >
|
||||
mvn -B license:add-third-party
|
||||
|
||||
Generated
+11
-12
@@ -14,10 +14,10 @@
|
||||
<option name="dagger.fastInit" value="enabled" />
|
||||
<option name="dagger.formatGeneratedSource" value="enabled" />
|
||||
<processorPath useClasspath="false">
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-compiler/2.45/dagger-compiler-2.45.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger/2.45/dagger-2.45.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-compiler/2.48.1/dagger-compiler-2.48.1.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger/2.48.1/dagger-2.48.1.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/javax/inject/javax.inject/1/javax.inject-1.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-producers/2.45/dagger-producers-2.45.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-producers/2.48.1/dagger-producers-2.48.1.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/guava/guava/31.0.1-jre/guava-31.0.1-jre.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar" />
|
||||
@@ -26,20 +26,19 @@
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotations/2.7.1/error_prone_annotations-2.7.1.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/j2objc/j2objc-annotations/1.3/j2objc-annotations-1.3.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-compat-qual/2.5.5/checker-compat-qual-2.5.5.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-spi/2.45/dagger-spi-2.45.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/devtools/ksp/symbol-processing-api/1.7.0-1.0.6/symbol-processing-api-1.7.0-1.0.6.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.7.0/kotlin-stdlib-1.7.0.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.7.0/kotlin-stdlib-common-1.7.0.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-spi/2.48.1/dagger-spi-2.48.1.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/devtools/ksp/symbol-processing-api/1.9.0-1.0.12/symbol-processing-api-1.9.0-1.0.12.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.9.0/kotlin-stdlib-1.9.0.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.9.0/kotlin-stdlib-common-1.9.0.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/squareup/javapoet/1.13.0/javapoet-1.13.0.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/squareup/kotlinpoet/1.11.0/kotlinpoet-1.11.0.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.7.0/kotlin-stdlib-jdk8-1.7.0.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.7.0/kotlin-stdlib-jdk7-1.7.0.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-reflect/1.6.10/kotlin-reflect-1.6.10.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/googlejavaformat/google-java-format/1.5/google-java-format-1.5.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/javac-shaded/9-dev-r4023-3/javac-shaded-9-dev-r4023-3.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/com/squareup/kotlinpoet/1.11.0/kotlinpoet-1.11.0.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.6.10/kotlin-stdlib-jdk8-1.6.10.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.6.10/kotlin-stdlib-jdk7-1.6.10.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-reflect/1.6.10/kotlin-reflect-1.6.10.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/net/ltgt/gradle/incap/incap/0.2/incap-0.2.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-metadata-jvm/0.5.0/kotlinx-metadata-jvm-0.5.0.jar" />
|
||||
</processorPath>
|
||||
<module name="cryptomator" />
|
||||
</profile>
|
||||
|
||||
Generated
+1
-1
@@ -8,7 +8,7 @@
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_20_PREVIEW" project-jdk-name="20" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21_PREVIEW" project-jdk-name="21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
Vendored
+6
-4
@@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cd $(dirname $0)
|
||||
REVISION_NO=`git rev-list --count HEAD`
|
||||
@@ -10,6 +11,7 @@ command -v curl >/dev/null 2>&1 || { echo >&2 "curl not found."; exit 1; }
|
||||
|
||||
VERSION=$(mvn -f ../../../pom.xml help:evaluate -Dexpression=project.version -q -DforceStdout)
|
||||
SEMVER_STR=${VERSION}
|
||||
MACHINE_TYPE=$(uname -m)
|
||||
|
||||
mvn -f ../../../pom.xml versions:set -DnewVersion=${SEMVER_STR}
|
||||
|
||||
@@ -83,17 +85,17 @@ ln -s usr/share/applications/org.cryptomator.Cryptomator.desktop Cryptomator.App
|
||||
ln -s bin/cryptomator.sh Cryptomator.AppDir/AppRun
|
||||
|
||||
# load AppImageTool
|
||||
curl -L https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage -o /tmp/appimagetool.AppImage
|
||||
curl -L https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-${MACHINE_TYPE}.AppImage -o /tmp/appimagetool.AppImage
|
||||
chmod +x /tmp/appimagetool.AppImage
|
||||
|
||||
# create AppImage
|
||||
/tmp/appimagetool.AppImage \
|
||||
Cryptomator.AppDir \
|
||||
cryptomator-${SEMVER_STR}-x86_64.AppImage \
|
||||
-u 'gh-releases-zsync|cryptomator|cryptomator|latest|cryptomator-*-x86_64.AppImage.zsync'
|
||||
cryptomator-${SEMVER_STR}-${MACHINE_TYPE}.AppImage \
|
||||
-u 'gh-releases-zsync|cryptomator|cryptomator|latest|cryptomator-*-${MACHINE_TYPE}.AppImage.zsync'
|
||||
|
||||
echo ""
|
||||
echo "Done. AppImage successfully created: cryptomator-${SEMVER_STR}-x86_64.AppImage"
|
||||
echo "Done. AppImage successfully created: cryptomator-${SEMVER_STR}-${MACHINE_TYPE}.AppImage"
|
||||
echo ""
|
||||
echo >&2 "To clean up, run: rm -rf Cryptomator.AppDir appdir jni runtime squashfs-root; rm launcher-gtk2.properties /tmp/appimagetool.AppImage"
|
||||
echo ""
|
||||
@@ -66,6 +66,8 @@
|
||||
</content_rating>
|
||||
|
||||
<releases>
|
||||
<release date="2023-09-20" version="1.10.1"/>
|
||||
<release date="2023-09-11" version="1.10.0"/>
|
||||
<release date="2023-08-11" version="1.9.4"/>
|
||||
<release date="2023-08-07" version="1.9.3"/>
|
||||
<release date="2023-07-24" version="1.9.2"/>
|
||||
|
||||
Vendored
+1
-1
@@ -2,7 +2,7 @@ Source: cryptomator
|
||||
Maintainer: Cryptobot <releases@cryptomator.org>
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Build-Depends: debhelper (>=10), coffeelibs-jdk-20, libgtk2.0-0, libgtk-3-0, libxxf86vm1, libgl1
|
||||
Build-Depends: debhelper (>=10), coffeelibs-jdk-21, libgtk2.0-0, libgtk-3-0, libxxf86vm1, libgl1
|
||||
Standards-Version: 4.5.0
|
||||
Homepage: https://cryptomator.org
|
||||
Vcs-Git: https://github.com/cryptomator/cryptomator.git
|
||||
|
||||
Vendored
+2
-1
@@ -4,7 +4,7 @@
|
||||
# Uncomment this to turn on verbose mode.
|
||||
#export DH_VERBOSE=1
|
||||
|
||||
JAVA_HOME = /usr/lib/jvm/java-20-coffeelibs
|
||||
JAVA_HOME = /usr/lib/jvm/java-21-coffeelibs
|
||||
DEB_BUILD_ARCH ?= $(shell dpkg-architecture -qDEB_BUILD_ARCH)
|
||||
ifeq ($(DEB_BUILD_ARCH),amd64)
|
||||
JMODS_PATH = jmods/amd64:${JAVA_HOME}/jmods
|
||||
@@ -59,6 +59,7 @@ override_dh_auto_build:
|
||||
--java-options "-Dcryptomator.integrationsLinux.trayIconsDir=\"/usr/share/icons/hicolor/symbolic/apps\"" \
|
||||
--java-options "-Dcryptomator.buildNumber=\"deb-${REVISION_NUM}\"" \
|
||||
--java-options "-Dcryptomator.appVersion=\"${SEMVER_STR}\"" \
|
||||
--java-options "-Dcryptomator.disableUpdateCheck=\"${DISABLE_UPDATE_CHECK}\"" \
|
||||
--app-version "${VERSION_NUM}.${REVISION_NUM}" \
|
||||
--resource-dir resources \
|
||||
--verbose
|
||||
|
||||
Vendored
+6
-6
@@ -49,21 +49,22 @@ fi
|
||||
# download and check jmods
|
||||
curl -L ${OPENJFX_JMODS} -o openjfx-jmods.zip
|
||||
mkdir -p openjfx-jmods/
|
||||
unzip -j openjfx-jmods.zip \*/javafx.base.jmod \*/javafx.controls.jmod \*/javafx.fxml.jmod \*/javafx.graphics.jmod -d openjfx-jmods/
|
||||
unzip -jo openjfx-jmods.zip \*/javafx.base.jmod \*/javafx.controls.jmod \*/javafx.fxml.jmod \*/javafx.graphics.jmod -d openjfx-jmods
|
||||
JMOD_VERSION=$(jmod describe openjfx-jmods/javafx.base.jmod | head -1)
|
||||
JMOD_VERSION=${JMOD_VERSION#*@}
|
||||
JMOD_VERSION=${JMOD_VERSION%%.*}
|
||||
POM_JFX_VERSION=$(mvn help:evaluate "-Dexpression=javafx.version" -q -DforceStdout)
|
||||
POM_JFX_VERSION=$(mvn -f../../../pom.xml help:evaluate "-Dexpression=javafx.version" -q -DforceStdout)
|
||||
POM_JFX_VERSION=${POM_JFX_VERSION#*@}
|
||||
POM_JFX_VERSION=${POM_JFX_VERSION%%.*}
|
||||
|
||||
if [ $POM_JFX_VERSION -ne $JMOD_VERSION ]; then
|
||||
>&2 echo "Major JavaFX version in pom.xml (${POM_JFX_VERSION}) != jmod version (${JMOD_VERSION})"
|
||||
exit 1
|
||||
if [ "${POM_JFX_VERSION}" -ne "${JMOD_VERSION}" ]; then
|
||||
>&2 echo "Major JavaFX version in pom.xml (${POM_JFX_VERSION}) != jmod version (${JMOD_VERSION})"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# compile
|
||||
mvn -B -f../../../pom.xml clean package -DskipTests -Pmac
|
||||
cp ../../../LICENSE.txt ../../../target
|
||||
cp ../../../target/${MAIN_JAR_GLOB} ../../../target/mods
|
||||
|
||||
# add runtime
|
||||
@@ -168,6 +169,5 @@ create-dmg \
|
||||
--app-drop-link 512 245 \
|
||||
--eula "resources/license.rtf" \
|
||||
--icon ".background" 128 758 \
|
||||
--icon ".fseventsd" 320 758 \
|
||||
--icon ".VolumeIcon.icns" 512 758 \
|
||||
${APP_NAME}-${VERSION_NO}.dmg dmg
|
||||
|
||||
Vendored
+5
-3
@@ -63,10 +63,11 @@ if( !(Test-Path -Path $jfxJmodsZip) ) {
|
||||
$jmodsChecksumActual = $(Get-FileHash -Path $jfxJmodsZip -Algorithm SHA256).Hash
|
||||
if( $jmodsChecksumActual -ne $jfxJmodsChecksum ) {
|
||||
Write-Error "Checksum mismatch for jfxJmods.zip. Expected: $jfxJmodsChecksum, actual: $jmodsChecksumActual"
|
||||
exit 1;
|
||||
exit 1;
|
||||
}
|
||||
Expand-Archive -Force -Path $jfxJmodsZip -DestinationPath ".\resources\"
|
||||
Move-Item -Path ".\resources\javafx-jmods-*" -Destination ".\resources\javafx-jmods" -ErrorAction Stop
|
||||
Expand-Archive -Path $jfxJmodsZip -Force -DestinationPath ".\resources\"
|
||||
Remove-Item -Recurse -Force -Path ".\resources\javafx-jmods"
|
||||
Move-Item -Force -Path ".\resources\javafx-jmods-*" -Destination ".\resources\javafx-jmods" -ErrorAction Stop
|
||||
|
||||
|
||||
& "$Env:JAVA_HOME\bin\jlink" `
|
||||
@@ -143,6 +144,7 @@ try {
|
||||
|
||||
# create .msi
|
||||
$Env:JP_WIXWIZARD_RESOURCES = "$buildDir\resources"
|
||||
$Env:JP_WIXHELPER_DIR = "."
|
||||
& "$Env:JAVA_HOME\bin\jpackage" `
|
||||
--verbose `
|
||||
--type msi `
|
||||
|
||||
Vendored
+1
-1
@@ -70,7 +70,7 @@
|
||||
<CustomAction Id="JpDisallowDowngrade" Error="!(loc.DowngradeErrorMessage)" />
|
||||
<?endif?>
|
||||
|
||||
<Binary Id="JpCaDll" SourceFile="wixhelper.dll"/>
|
||||
<Binary Id="JpCaDll" SourceFile="$(env.JP_WIXHELPER_DIR)\wixhelper.dll"/>
|
||||
<CustomAction Id="JpFindRelatedProducts" BinaryKey="JpCaDll" DllEntry="FindRelatedProductsEx" />
|
||||
|
||||
<?ifndef SkipCryptomatorLegacyCheck ?>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>cryptomator</artifactId>
|
||||
<version>1.10.0</version>
|
||||
<version>1.11.0</version>
|
||||
<name>Cryptomator Desktop App</name>
|
||||
|
||||
<organization>
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.jdk.version>20</project.jdk.version>
|
||||
<project.jdk.version>21</project.jdk.version>
|
||||
|
||||
<!-- Group IDs of jars that need to stay on the class path for now -->
|
||||
<!-- remove them, as soon they got modularized or support is dropped (i.e., WebDAV) -->
|
||||
@@ -35,36 +35,36 @@
|
||||
<!-- cryptomator dependencies -->
|
||||
<cryptomator.cryptofs.version>2.6.7</cryptomator.cryptofs.version>
|
||||
<cryptomator.integrations.version>1.3.0</cryptomator.integrations.version>
|
||||
<cryptomator.integrations.win.version>1.2.2</cryptomator.integrations.win.version>
|
||||
<cryptomator.integrations.mac.version>1.2.1</cryptomator.integrations.mac.version>
|
||||
<cryptomator.integrations.linux.version>1.3.0-beta6</cryptomator.integrations.linux.version>
|
||||
<cryptomator.fuse.version>3.0.0</cryptomator.fuse.version>
|
||||
<cryptomator.integrations.win.version>1.2.4</cryptomator.integrations.win.version>
|
||||
<cryptomator.integrations.mac.version>1.2.2</cryptomator.integrations.mac.version>
|
||||
<cryptomator.integrations.linux.version>1.4.0-beta2</cryptomator.integrations.linux.version>
|
||||
<cryptomator.fuse.version>4.0.0-beta4</cryptomator.fuse.version>
|
||||
<cryptomator.dokany.version>2.0.0</cryptomator.dokany.version>
|
||||
<cryptomator.webdav.version>2.0.3</cryptomator.webdav.version>
|
||||
<cryptomator.webdav.version>2.0.5</cryptomator.webdav.version>
|
||||
|
||||
<!-- 3rd party dependencies -->
|
||||
<commons-lang3.version>3.13.0</commons-lang3.version>
|
||||
<dagger.version>2.47</dagger.version>
|
||||
<dagger.version>2.48.1</dagger.version>
|
||||
<easybind.version>2.2</easybind.version>
|
||||
<guava.version>32.1.2-jre</guava.version>
|
||||
<jackson.version>2.15.2</jackson.version>
|
||||
<guava.version>32.1.3-jre</guava.version>
|
||||
<jackson.version>2.15.3</jackson.version>
|
||||
<javafx.version>20.0.2</javafx.version>
|
||||
<jwt.version>4.4.0</jwt.version>
|
||||
<nimbus-jose.version>9.31</nimbus-jose.version>
|
||||
<nimbus-jose.version>9.37</nimbus-jose.version>
|
||||
<logback.version>1.4.11</logback.version>
|
||||
<slf4j.version>2.0.7</slf4j.version>
|
||||
<tinyoauth2.version>0.6.0</tinyoauth2.version>
|
||||
<slf4j.version>2.0.9</slf4j.version>
|
||||
<tinyoauth2.version>0.7.0</tinyoauth2.version>
|
||||
<zxcvbn.version>1.8.2</zxcvbn.version>
|
||||
|
||||
<!-- test dependencies -->
|
||||
<junit.jupiter.version>5.10.0</junit.jupiter.version>
|
||||
<mockito.version>5.5.0</mockito.version>
|
||||
<mockito.version>5.6.0</mockito.version>
|
||||
<hamcrest.version>2.2</hamcrest.version>
|
||||
|
||||
<!-- build-time dependencies -->
|
||||
<jetbrains.annotations.version>24.0.1</jetbrains.annotations.version>
|
||||
<dependency-check.version>8.4.0</dependency-check.version>
|
||||
<jacoco.version>0.8.10</jacoco.version>
|
||||
<jacoco.version>0.8.11</jacoco.version>
|
||||
<license-generator.version>2.2.0</license-generator.version>
|
||||
<junit-tree-reporter.version>1.2.1</junit-tree-reporter.version>
|
||||
<mvn-compiler.version>3.11.0</mvn-compiler.version>
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.cryptomator.common;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.base.Strings;
|
||||
import org.jetbrains.annotations.VisibleForTesting;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -31,6 +32,7 @@ public class Environment {
|
||||
private static final String BUILD_NUMBER_PROP_NAME = "cryptomator.buildNumber";
|
||||
private static final String PLUGIN_DIR_PROP_NAME = "cryptomator.pluginDir";
|
||||
private static final String TRAY_ICON_PROP_NAME = "cryptomator.showTrayIcon";
|
||||
private static final String DISABLE_UPDATE_CHECK_PROP_NAME = "cryptomator.disableUpdateCheck";
|
||||
|
||||
private Environment() {}
|
||||
|
||||
@@ -43,15 +45,16 @@ public class Environment {
|
||||
logCryptomatorSystemProperty(SETTINGS_PATH_PROP_NAME);
|
||||
logCryptomatorSystemProperty(IPC_SOCKET_PATH_PROP_NAME);
|
||||
logCryptomatorSystemProperty(KEYCHAIN_PATHS_PROP_NAME);
|
||||
logCryptomatorSystemProperty(P12_PATH_PROP_NAME);
|
||||
logCryptomatorSystemProperty(LOG_DIR_PROP_NAME);
|
||||
logCryptomatorSystemProperty(LOOPBACK_ALIAS_PROP_NAME);
|
||||
logCryptomatorSystemProperty(PLUGIN_DIR_PROP_NAME);
|
||||
logCryptomatorSystemProperty(MOUNTPOINT_DIR_PROP_NAME);
|
||||
logCryptomatorSystemProperty(MIN_PW_LENGTH_PROP_NAME);
|
||||
logCryptomatorSystemProperty(APP_VERSION_PROP_NAME);
|
||||
logCryptomatorSystemProperty(BUILD_NUMBER_PROP_NAME);
|
||||
logCryptomatorSystemProperty(PLUGIN_DIR_PROP_NAME);
|
||||
logCryptomatorSystemProperty(TRAY_ICON_PROP_NAME);
|
||||
logCryptomatorSystemProperty(P12_PATH_PROP_NAME);
|
||||
logCryptomatorSystemProperty(DISABLE_UPDATE_CHECK_PROP_NAME);
|
||||
}
|
||||
|
||||
public static Environment getInstance() {
|
||||
@@ -74,10 +77,6 @@ public class Environment {
|
||||
return getPaths(SETTINGS_PATH_PROP_NAME);
|
||||
}
|
||||
|
||||
public Stream<Path> getP12Path() {
|
||||
return getPaths(P12_PATH_PROP_NAME);
|
||||
}
|
||||
|
||||
public Stream<Path> getIpcSocketPath() {
|
||||
return getPaths(IPC_SOCKET_PATH_PROP_NAME);
|
||||
}
|
||||
@@ -86,6 +85,10 @@ public class Environment {
|
||||
return getPaths(KEYCHAIN_PATHS_PROP_NAME);
|
||||
}
|
||||
|
||||
public Stream<Path> getP12Path() {
|
||||
return getPaths(P12_PATH_PROP_NAME);
|
||||
}
|
||||
|
||||
public Optional<Path> getLogDir() {
|
||||
return getPath(LOG_DIR_PROP_NAME);
|
||||
}
|
||||
@@ -94,14 +97,14 @@ public class Environment {
|
||||
return Optional.ofNullable(System.getProperty(LOOPBACK_ALIAS_PROP_NAME));
|
||||
}
|
||||
|
||||
public Optional<Path> getPluginDir() {
|
||||
return getPath(PLUGIN_DIR_PROP_NAME);
|
||||
}
|
||||
|
||||
public Optional<Path> getMountPointsDir() {
|
||||
return getPath(MOUNTPOINT_DIR_PROP_NAME);
|
||||
}
|
||||
|
||||
public int getMinPwLength() {
|
||||
return Integer.getInteger(MIN_PW_LENGTH_PROP_NAME, DEFAULT_MIN_PW_LENGTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the app version defined in the {@value APP_VERSION_PROP_NAME} property or returns "SNAPSHOT".
|
||||
*
|
||||
@@ -115,20 +118,24 @@ public class Environment {
|
||||
return Optional.ofNullable(System.getProperty(BUILD_NUMBER_PROP_NAME));
|
||||
}
|
||||
|
||||
public int getMinPwLength() {
|
||||
return Integer.getInteger(MIN_PW_LENGTH_PROP_NAME, DEFAULT_MIN_PW_LENGTH);
|
||||
public Optional<Path> getPluginDir() {
|
||||
return getPath(PLUGIN_DIR_PROP_NAME);
|
||||
}
|
||||
|
||||
public boolean showTrayIcon() {
|
||||
return Boolean.getBoolean(TRAY_ICON_PROP_NAME);
|
||||
}
|
||||
|
||||
public boolean disableUpdateCheck() {
|
||||
return Boolean.getBoolean(DISABLE_UPDATE_CHECK_PROP_NAME);
|
||||
}
|
||||
|
||||
private Optional<Path> getPath(String propertyName) {
|
||||
String value = System.getProperty(propertyName);
|
||||
return Optional.ofNullable(value).map(Paths::get);
|
||||
}
|
||||
|
||||
// visible for testing
|
||||
@VisibleForTesting
|
||||
Stream<Path> getPaths(String propertyName) {
|
||||
Stream<String> rawSettingsPaths = getRawList(propertyName, System.getProperty("path.separator").charAt(0));
|
||||
return rawSettingsPaths.filter(Predicate.not(Strings::isNullOrEmpty)).map(Path::of);
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.cryptomator.common;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.base.Throwables;
|
||||
import org.jetbrains.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
@@ -114,7 +115,7 @@ public class ErrorCode {
|
||||
* @param bottomFrames Other stack frames, potentially forming the bottom of the stack of <code>allFrames</code>
|
||||
* @return The number of additional frames in <code>allFrames</code>. In most cases this should be equal to the difference in size.
|
||||
*/
|
||||
// visible for testing
|
||||
@VisibleForTesting
|
||||
static int countTopmostFrames(StackTraceElement[] allFrames, StackTraceElement[] bottomFrames) {
|
||||
if (allFrames.length < bottomFrames.length) {
|
||||
// if frames had been stacked on top of bottomFrames, allFrames would be larger
|
||||
@@ -124,7 +125,7 @@ public class ErrorCode {
|
||||
}
|
||||
}
|
||||
|
||||
// visible for testing
|
||||
@VisibleForTesting
|
||||
static <T> int commonSuffixLength(T[] set, T[] subset) {
|
||||
Preconditions.checkArgument(set.length >= subset.length);
|
||||
// iterate items backwards as long as they are identical
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ public final class OneDriveWindowsLocationPresetsProvider implements LocationPre
|
||||
ProcessBuilder command = new ProcessBuilder(args);
|
||||
Process p = command.start();
|
||||
waitForSuccess(p, 3, "`reg query`");
|
||||
return p.inputReader(StandardCharsets.UTF_8).lines().filter(outputFilter);
|
||||
return p.inputReader(StandardCharsets.ISO_8859_1).lines().filter(outputFilter);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.cryptomator.common.mount;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.jetbrains.annotations.VisibleForTesting;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -66,7 +67,7 @@ public final class MountWithinParentUtil {
|
||||
}
|
||||
}
|
||||
|
||||
//visible for testing
|
||||
@VisibleForTesting
|
||||
static MountPointState getMountPointState(Path path) throws IOException, IllegalMountPointException {
|
||||
if (Files.notExists(path, LinkOption.NOFOLLOW_LINKS)) {
|
||||
return MountPointState.NOT_EXISTING;
|
||||
@@ -82,7 +83,7 @@ public final class MountWithinParentUtil {
|
||||
return MountPointState.BROKEN_JUNCTION;
|
||||
}
|
||||
|
||||
//visible for testing
|
||||
@VisibleForTesting
|
||||
enum MountPointState {
|
||||
|
||||
NOT_EXISTING,
|
||||
@@ -93,7 +94,7 @@ public final class MountWithinParentUtil {
|
||||
|
||||
}
|
||||
|
||||
//visible for testing
|
||||
@VisibleForTesting
|
||||
static void removeResidualHideaway(Path mountPoint, Path hideaway) throws IOException {
|
||||
checkIsHideawayDirectory(mountPoint, hideaway);
|
||||
Files.delete(hideaway); //Fails if not empty
|
||||
@@ -155,7 +156,7 @@ public final class MountWithinParentUtil {
|
||||
}
|
||||
}
|
||||
|
||||
//visible for testing
|
||||
@VisibleForTesting
|
||||
static Path getHideaway(Path mountPoint) {
|
||||
return mountPoint.resolveSibling(HIDEAWAY_PREFIX + mountPoint.getFileName().toString() + HIDEAWAY_SUFFIX);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.google.common.base.CharMatcher;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.jetbrains.annotations.VisibleForTesting;
|
||||
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
@@ -126,7 +127,7 @@ public class VaultSettings {
|
||||
return json;
|
||||
}
|
||||
|
||||
//visible for testing
|
||||
@VisibleForTesting
|
||||
static String normalizeDisplayName(String original) {
|
||||
if (original.isBlank() || ".".equals(original) || "..".equals(original)) {
|
||||
return "_";
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.launcher;
|
||||
|
||||
import org.jetbrains.annotations.VisibleForTesting;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -48,7 +49,7 @@ class FileOpenRequestHandler {
|
||||
handleLaunchArgs(FileSystems.getDefault(), args);
|
||||
}
|
||||
|
||||
// visible for testing
|
||||
@VisibleForTesting
|
||||
void handleLaunchArgs(FileSystem fs, List<String> args) {
|
||||
Collection<Path> pathsToOpen = args.stream().map(str -> {
|
||||
try {
|
||||
|
||||
@@ -5,6 +5,7 @@ import ch.qos.logback.classic.Logger;
|
||||
import ch.qos.logback.classic.LoggerContext;
|
||||
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
|
||||
import ch.qos.logback.classic.spi.Configurator;
|
||||
import ch.qos.logback.classic.spi.ConfiguratorRank;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.Appender;
|
||||
import ch.qos.logback.core.ConsoleAppender;
|
||||
@@ -19,6 +20,7 @@ import org.cryptomator.common.Environment;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
|
||||
@ConfiguratorRank(ConfiguratorRank.CUSTOM_NORMAL_PRIORITY)
|
||||
public class LogbackConfigurator extends ContextAwareBase implements Configurator {
|
||||
|
||||
private static final String LOG_PATTERN = "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n";
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.cryptomator.ui.addvaultwizard;
|
||||
|
||||
import org.jetbrains.annotations.VisibleForTesting;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.List;
|
||||
import java.util.ResourceBundle;
|
||||
@@ -51,7 +53,7 @@ public class ReadmeGenerator {
|
||||
resourceBundle.getString("addvault.new.readme.accessLocation.4")));
|
||||
}
|
||||
|
||||
// visible for testing
|
||||
@VisibleForTesting
|
||||
String createDocument(Iterable<String> paragraphs) {
|
||||
StringBuilder sb = new StringBuilder(RTF_HEADER);
|
||||
for (String p : paragraphs) {
|
||||
@@ -63,7 +65,7 @@ public class ReadmeGenerator {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// visible for testing
|
||||
@VisibleForTesting
|
||||
String escapeNonAsciiChars(CharSequence input) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
appendEscaped(sb, input);
|
||||
|
||||
@@ -20,9 +20,10 @@ public enum FxmlFile {
|
||||
HUB_AUTH_FLOW("/fxml/hub_auth_flow.fxml"), //
|
||||
HUB_INVALID_LICENSE("/fxml/hub_invalid_license.fxml"), //
|
||||
HUB_RECEIVE_KEY("/fxml/hub_receive_key.fxml"), //
|
||||
HUB_REGISTER_DEVICE("/fxml/hub_register_device.fxml"), //
|
||||
HUB_LEGACY_REGISTER_DEVICE("/fxml/hub_legacy_register_device.fxml"), //
|
||||
HUB_REGISTER_SUCCESS("/fxml/hub_register_success.fxml"), //
|
||||
HUB_REGISTER_FAILED("/fxml/hub_register_failed.fxml"),
|
||||
HUB_REGISTER_FAILED("/fxml/hub_register_failed.fxml"), //
|
||||
HUB_SETUP_DEVICE("/fxml/hub_setup_device.fxml"), //
|
||||
HUB_UNAUTHORIZED_DEVICE("/fxml/hub_unauthorized_device.fxml"), //
|
||||
LOCK_FORCED("/fxml/lock_forced.fxml"), //
|
||||
LOCK_FAILED("/fxml/lock_failed.fxml"), //
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationWindows;
|
||||
import org.cryptomator.ui.recoverykey.RecoveryKeyFactory;
|
||||
import org.jetbrains.annotations.VisibleForTesting;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -116,7 +117,7 @@ public class HubToPasswordConvertController implements FxController {
|
||||
}, Platform::runLater); //
|
||||
}
|
||||
|
||||
//visible for testing
|
||||
@VisibleForTesting
|
||||
void convertInternal() throws CompletionException, IllegalArgumentException {
|
||||
var passphrase = newPasswordController.getNewPassword();
|
||||
var vaultPath = vault.getPath();
|
||||
@@ -141,7 +142,7 @@ public class HubToPasswordConvertController implements FxController {
|
||||
}
|
||||
}
|
||||
|
||||
//visible for testing
|
||||
@VisibleForTesting
|
||||
void backupHubConfig(Path hubConfigPath) throws IOException {
|
||||
byte[] hubConfigBytes = Files.readAllBytes(hubConfigPath);
|
||||
Path backupPath = hubConfigPath.resolveSibling(VAULTCONFIG_FILENAME + BackupHelper.generateFileIdSuffix(hubConfigBytes) + MASTERKEY_BACKUP_SUFFIX);
|
||||
@@ -149,7 +150,7 @@ public class HubToPasswordConvertController implements FxController {
|
||||
LOG.debug("Successfully created hub config backup {}", backupPath.getFileName());
|
||||
}
|
||||
|
||||
//visible for testing
|
||||
@VisibleForTesting
|
||||
Path createPasswordConfig(Path passwordConfigPath, Path masterkeyFile, Passphrase passphrase) throws IOException, MasterkeyLoadingFailedException {
|
||||
var unverifiedVaultConfig = vault.getVaultConfigCache().get();
|
||||
try (var masterkey = masterkeyFileAccess.load(masterkeyFile, passphrase)) {
|
||||
|
||||
@@ -31,6 +31,7 @@ import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -42,7 +43,8 @@ public class ErrorController implements FxController {
|
||||
|
||||
private static final ObjectMapper JSON = new ObjectMapper();
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ErrorController.class);
|
||||
private static final String ERROR_CODES_URL = "https://api.cryptomator.org/desktop/error-codes.json";
|
||||
private static final String USER_AGENT_FORMAT = "Cryptomator/%s (Build %s) (%s %s %s)";
|
||||
private static final String ERROR_CODES_URL_FORMAT = "https://api.cryptomator.org/desktop/error-codes.json?error-code=%s";
|
||||
private static final String SEARCH_URL_FORMAT = "https://github.com/cryptomator/cryptomator/discussions/categories/errors?discussions_q=category:Errors+%s";
|
||||
private static final String REPORT_URL_FORMAT = "https://github.com/cryptomator/cryptomator/discussions/new?category=Errors&title=Error+%s&body=%s";
|
||||
private static final String SEARCH_ERRORCODE_DELIM = " OR ";
|
||||
@@ -142,11 +144,19 @@ public class ErrorController implements FxController {
|
||||
|
||||
@FXML
|
||||
public void lookUpSolution() {
|
||||
String userAgent = USER_AGENT_FORMAT.formatted( //
|
||||
environment.getAppVersion(), //
|
||||
environment.getBuildNumber().orElse("undefined"), //
|
||||
System.getProperty("os.name"), //
|
||||
System.getProperty("os.version"), //
|
||||
System.getProperty("os.arch"));
|
||||
isLoadingHttpResponse.set(true);
|
||||
askedForLookupDatabasePermission.set(true);
|
||||
HttpClient httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
|
||||
HttpRequest httpRequest = HttpRequest.newBuilder()//
|
||||
.uri(URI.create(ERROR_CODES_URL))//
|
||||
.header("User-Agent", userAgent)
|
||||
.timeout(Duration.ofSeconds(10))
|
||||
.uri(URI.create(ERROR_CODES_URL_FORMAT.formatted(URLEncoder.encode(errorCode.toString(),StandardCharsets.UTF_8))))//
|
||||
.build();
|
||||
httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofInputStream())//
|
||||
.thenAcceptAsync(this::loadHttpResponse, executorService)//
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.cryptomator.ui.fxapp;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.ui.traymenu.TrayMenuComponent;
|
||||
import org.slf4j.Logger;
|
||||
@@ -17,6 +18,7 @@ public class FxApplication {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FxApplication.class);
|
||||
|
||||
private final long startupTime;
|
||||
private final Environment environment;
|
||||
private final Settings settings;
|
||||
private final AppLaunchEventHandler launchEventHandler;
|
||||
private final Lazy<TrayMenuComponent> trayMenu;
|
||||
@@ -26,8 +28,9 @@ public class FxApplication {
|
||||
private final AutoUnlocker autoUnlocker;
|
||||
|
||||
@Inject
|
||||
FxApplication(@Named("startupTime") long startupTime, Settings settings, AppLaunchEventHandler launchEventHandler, Lazy<TrayMenuComponent> trayMenu, FxApplicationWindows appWindows, FxApplicationStyle applicationStyle, FxApplicationTerminator applicationTerminator, AutoUnlocker autoUnlocker) {
|
||||
FxApplication(@Named("startupTime") long startupTime, Environment environment, Settings settings, AppLaunchEventHandler launchEventHandler, Lazy<TrayMenuComponent> trayMenu, FxApplicationWindows appWindows, FxApplicationStyle applicationStyle, FxApplicationTerminator applicationTerminator, AutoUnlocker autoUnlocker) {
|
||||
this.startupTime = startupTime;
|
||||
this.environment = environment;
|
||||
this.settings = settings;
|
||||
this.launchEventHandler = launchEventHandler;
|
||||
this.trayMenu = trayMenu;
|
||||
@@ -68,7 +71,9 @@ public class FxApplication {
|
||||
return null;
|
||||
});
|
||||
|
||||
appWindows.checkAndShowUpdateReminderWindow();
|
||||
if (!environment.disableUpdateCheck()) {
|
||||
appWindows.checkAndShowUpdateReminderWindow();
|
||||
}
|
||||
|
||||
launchEventHandler.startHandlingLaunchEvents();
|
||||
autoUnlocker.tryUnlockForTimespan(2, TimeUnit.MINUTES);
|
||||
|
||||
@@ -22,23 +22,23 @@ public class UpdateChecker {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UpdateChecker.class);
|
||||
private static final Duration AUTOCHECK_DELAY = Duration.seconds(5);
|
||||
|
||||
private final Environment env;
|
||||
private final Settings settings;
|
||||
private final String currentVersion;
|
||||
private final StringProperty latestVersionProperty;
|
||||
private final Comparator<String> semVerComparator;
|
||||
private final ScheduledService<String> updateCheckerService;
|
||||
|
||||
@Inject
|
||||
UpdateChecker(Settings settings, Environment env, @Named("latestVersion") StringProperty latestVersionProperty, @Named("SemVer") Comparator<String> semVerComparator, ScheduledService<String> updateCheckerService) {
|
||||
this.env = env;
|
||||
this.settings = settings;
|
||||
this.latestVersionProperty = latestVersionProperty;
|
||||
this.semVerComparator = semVerComparator;
|
||||
this.updateCheckerService = updateCheckerService;
|
||||
this.currentVersion = env.getAppVersion();
|
||||
}
|
||||
|
||||
public void automaticallyCheckForUpdatesIfEnabled() {
|
||||
if (settings.checkForUpdates.get()) {
|
||||
if (!env.disableUpdateCheck() && settings.checkForUpdates.get()) {
|
||||
startCheckingForUpdates(AUTOCHECK_DELAY);
|
||||
}
|
||||
}
|
||||
@@ -63,9 +63,9 @@ public class UpdateChecker {
|
||||
|
||||
private void checkSucceeded(WorkerStateEvent event) {
|
||||
String latestVersion = updateCheckerService.getValue();
|
||||
LOG.info("Current version: {}, lastest version: {}", currentVersion, latestVersion);
|
||||
LOG.info("Current version: {}, lastest version: {}", getCurrentVersion(), latestVersion);
|
||||
|
||||
if (semVerComparator.compare(currentVersion, latestVersion) < 0) {
|
||||
if (semVerComparator.compare(getCurrentVersion(), latestVersion) < 0) {
|
||||
// update is available
|
||||
latestVersionProperty.set(latestVersion);
|
||||
} else {
|
||||
@@ -88,7 +88,7 @@ public class UpdateChecker {
|
||||
}
|
||||
|
||||
public String getCurrentVersion() {
|
||||
return currentVersion;
|
||||
return env.getAppVersion();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ public abstract class UpdateCheckerModule {
|
||||
return HttpRequest.newBuilder() //
|
||||
.uri(LATEST_VERSION_URI) //
|
||||
.header("User-Agent", userAgent) //
|
||||
.timeout(java.time.Duration.ofSeconds(10))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -101,16 +101,16 @@ public class StartController implements FxController {
|
||||
}
|
||||
}
|
||||
|
||||
private void loadingKeyFailed(Throwable e) {
|
||||
switch (e) {
|
||||
case UnlockCancelledException uce -> {} //ok
|
||||
case VaultKeyInvalidException vkie -> {
|
||||
LOG.error("Invalid key"); //TODO: specific error screen
|
||||
private void loadingKeyFailed(Throwable t) {
|
||||
switch (t) {
|
||||
case UnlockCancelledException e -> {} // ok // TODO: rename to _ with JEP 443
|
||||
case VaultKeyInvalidException e -> { // TODO: rename to _ with JEP 443
|
||||
LOG.error("Invalid key"); // TODO: specific error screen
|
||||
appWindows.showErrorWindow(e, window, null);
|
||||
}
|
||||
default -> {
|
||||
LOG.error("Failed to load key.", e);
|
||||
appWindows.showErrorWindow(e, window, null);
|
||||
LOG.error("Failed to load key.", t);
|
||||
appWindows.showErrorWindow(t, window, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,13 +35,13 @@ public class AuthFlowController implements FxController {
|
||||
private final String deviceId;
|
||||
private final HubConfig hubConfig;
|
||||
private final AtomicReference<String> tokenRef;
|
||||
private final CompletableFuture<JWEObject> result;
|
||||
private final CompletableFuture<ReceivedKey> result;
|
||||
private final Lazy<Scene> receiveKeyScene;
|
||||
private final ObjectProperty<URI> authUri;
|
||||
private AuthFlowTask task;
|
||||
|
||||
@Inject
|
||||
public AuthFlowController(Application application, @KeyLoading Stage window, ExecutorService executor, @Named("deviceId") String deviceId, HubConfig hubConfig, @Named("bearerToken") AtomicReference<String> tokenRef, CompletableFuture<JWEObject> result, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy<Scene> receiveKeyScene) {
|
||||
public AuthFlowController(Application application, @KeyLoading Stage window, ExecutorService executor, @Named("deviceId") String deviceId, HubConfig hubConfig, @Named("bearerToken") AtomicReference<String> tokenRef, CompletableFuture<ReceivedKey> result, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy<Scene> receiveKeyScene) {
|
||||
this.application = application;
|
||||
this.window = window;
|
||||
this.executor = executor;
|
||||
|
||||
@@ -8,6 +8,8 @@ import io.github.coffeelibs.tinyoauth2client.http.response.Response;
|
||||
import javafx.concurrent.Task;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
class AuthFlowTask extends Task<String> {
|
||||
@@ -34,6 +36,7 @@ class AuthFlowTask extends Task<String> {
|
||||
protected String call() throws IOException, InterruptedException {
|
||||
var response = TinyOAuth2.client(hubConfig.clientId) //
|
||||
.withTokenEndpoint(URI.create(hubConfig.tokenEndpoint)) //
|
||||
.withRequestTimeout(Duration.ofSeconds(10)) //
|
||||
.authFlow(URI.create(hubConfig.authEndpoint)) //
|
||||
.setSuccessResponse(Response.redirect(URI.create(hubConfig.authSuccessUrl + "&device=" + authFlowContext.deviceId()))) //
|
||||
.setErrorResponse(Response.redirect(URI.create(hubConfig.authErrorUrl + "&device=" + authFlowContext.deviceId()))) //
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
record CreateDeviceDto(String id, String name, String publicKey) {
|
||||
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.google.common.io.CharStreams;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
class HttpHelper {
|
||||
|
||||
public static String readBody(HttpResponse<InputStream> response) throws IOException {
|
||||
try (var in = response.body(); var reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
|
||||
return CharStreams.toString(reader);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
// needs to be accessible by JSON decoder
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@@ -9,8 +13,19 @@ public class HubConfig {
|
||||
public String clientId;
|
||||
public String authEndpoint;
|
||||
public String tokenEndpoint;
|
||||
public String devicesResourceUrl;
|
||||
public String authSuccessUrl;
|
||||
public String authErrorUrl;
|
||||
public @Nullable String apiBaseUrl;
|
||||
@Deprecated // use apiBaseUrl + "/devices/"
|
||||
public String devicesResourceUrl;
|
||||
|
||||
public URI getApiBaseUrl() {
|
||||
if (apiBaseUrl != null) {
|
||||
return URI.create(apiBaseUrl);
|
||||
} else {
|
||||
// legacy approach
|
||||
assert devicesResourceUrl != null;
|
||||
return URI.create(devicesResourceUrl + "/..").normalize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import com.nimbusds.jose.JWEObject;
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
@@ -69,7 +68,7 @@ public abstract class HubKeyLoadingModule {
|
||||
|
||||
@Provides
|
||||
@KeyLoadingScoped
|
||||
static CompletableFuture<JWEObject> provideResult() {
|
||||
static CompletableFuture<ReceivedKey> provideResult() {
|
||||
return new CompletableFuture<>();
|
||||
}
|
||||
|
||||
@@ -114,10 +113,10 @@ public abstract class HubKeyLoadingModule {
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.HUB_REGISTER_DEVICE)
|
||||
@FxmlScene(FxmlFile.HUB_LEGACY_REGISTER_DEVICE)
|
||||
@KeyLoadingScoped
|
||||
static Scene provideHubRegisterDeviceScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.HUB_REGISTER_DEVICE);
|
||||
static Scene provideHubLegacyRegisterDeviceScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.HUB_LEGACY_REGISTER_DEVICE);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@@ -134,6 +133,13 @@ public abstract class HubKeyLoadingModule {
|
||||
return fxmlLoaders.createScene(FxmlFile.HUB_REGISTER_FAILED);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.HUB_SETUP_DEVICE)
|
||||
@KeyLoadingScoped
|
||||
static Scene provideHubRegisterDeviceScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.HUB_SETUP_DEVICE);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE)
|
||||
@KeyLoadingScoped
|
||||
@@ -166,6 +172,11 @@ public abstract class HubKeyLoadingModule {
|
||||
@FxControllerKey(RegisterDeviceController.class)
|
||||
abstract FxController bindRegisterDeviceController(RegisterDeviceController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(LegacyRegisterDeviceController.class)
|
||||
abstract FxController bindLegacyRegisterDeviceController(LegacyRegisterDeviceController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(RegisterSuccessController.class)
|
||||
|
||||
@@ -36,11 +36,11 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
|
||||
private final KeychainManager keychainManager;
|
||||
private final Lazy<Scene> authFlowScene;
|
||||
private final Lazy<Scene> noKeychainScene;
|
||||
private final CompletableFuture<JWEObject> result;
|
||||
private final CompletableFuture<ReceivedKey> result;
|
||||
private final DeviceKey deviceKey;
|
||||
|
||||
@Inject
|
||||
public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy<Scene> authFlowScene, @FxmlScene(FxmlFile.HUB_NO_KEYCHAIN) Lazy<Scene> noKeychainScene, CompletableFuture<JWEObject> result, DeviceKey deviceKey, KeychainManager keychainManager, @Named("windowTitle") String windowTitle) {
|
||||
public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy<Scene> authFlowScene, @FxmlScene(FxmlFile.HUB_NO_KEYCHAIN) Lazy<Scene> noKeychainScene, CompletableFuture<ReceivedKey> result, DeviceKey deviceKey, KeychainManager keychainManager, @Named("windowTitle") String windowTitle) {
|
||||
this.window = window;
|
||||
this.keychainManager = keychainManager;
|
||||
window.setTitle(windowTitle);
|
||||
@@ -60,7 +60,7 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
|
||||
var keypair = deviceKey.get();
|
||||
showWindow(authFlowScene);
|
||||
var jwe = result.get();
|
||||
return JWEHelper.decrypt(jwe, keypair.getPrivate());
|
||||
return jwe.decryptMasterkey(keypair.getPrivate());
|
||||
} catch (NoKeychainAccessProviderException e) {
|
||||
showWindow(noKeychainScene);
|
||||
throw new UnlockCancelledException("Unlock canceled due to missing prerequisites", e);
|
||||
|
||||
@@ -2,35 +2,103 @@ package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import com.nimbusds.jose.EncryptionMethod;
|
||||
import com.nimbusds.jose.JOSEException;
|
||||
import com.nimbusds.jose.JWEAlgorithm;
|
||||
import com.nimbusds.jose.JWEHeader;
|
||||
import com.nimbusds.jose.JWEObject;
|
||||
import com.nimbusds.jose.Payload;
|
||||
import com.nimbusds.jose.crypto.ECDHDecrypter;
|
||||
import com.nimbusds.jose.crypto.ECDHEncrypter;
|
||||
import com.nimbusds.jose.crypto.PasswordBasedDecrypter;
|
||||
import com.nimbusds.jose.jwk.Curve;
|
||||
import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
|
||||
import com.nimbusds.jose.jwk.gen.JWKGenerator;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.interfaces.ECPrivateKey;
|
||||
import java.security.interfaces.ECPublicKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
class JWEHelper {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JWEHelper.class);
|
||||
private static final String JWE_PAYLOAD_MASTERKEY_FIELD = "key";
|
||||
private static final String JWE_PAYLOAD_KEY_FIELD = "key";
|
||||
private static final String EC_ALG = "EC";
|
||||
|
||||
private JWEHelper(){}
|
||||
|
||||
public static Masterkey decrypt(JWEObject jwe, ECPrivateKey privateKey) throws MasterkeyLoadingFailedException {
|
||||
public static JWEObject encryptUserKey(ECPrivateKey userKey, ECPublicKey deviceKey) {
|
||||
try {
|
||||
jwe.decrypt(new ECDHDecrypter(privateKey));
|
||||
return readKey(jwe);
|
||||
var encodedUserKey = Base64.getEncoder().encodeToString(userKey.getEncoded());
|
||||
var keyGen = new ECKeyGenerator(Curve.P_384);
|
||||
var ephemeralKeyPair = keyGen.generate();
|
||||
var header = new JWEHeader.Builder(JWEAlgorithm.ECDH_ES, EncryptionMethod.A256GCM).ephemeralPublicKey(ephemeralKeyPair.toPublicJWK()).build();
|
||||
var payload = new Payload(Map.of(JWE_PAYLOAD_KEY_FIELD, encodedUserKey));
|
||||
var jwe = new JWEObject(header, payload);
|
||||
jwe.encrypt(new ECDHEncrypter(deviceKey));
|
||||
return jwe;
|
||||
} catch (JOSEException e) {
|
||||
LOG.warn("Failed to decrypt JWE: {}", jwe);
|
||||
throw new MasterkeyLoadingFailedException("Failed to decrypt JWE", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Masterkey readKey(JWEObject jwe) throws MasterkeyLoadingFailedException {
|
||||
public static ECPrivateKey decryptUserKey(JWEObject jwe, String setupCode) throws InvalidJweKeyException {
|
||||
try {
|
||||
jwe.decrypt(new PasswordBasedDecrypter(setupCode));
|
||||
return decodeUserKey(jwe);
|
||||
} catch (JOSEException e) {
|
||||
throw new InvalidJweKeyException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static ECPrivateKey decryptUserKey(JWEObject jwe, ECPrivateKey deviceKey) throws InvalidJweKeyException {
|
||||
try {
|
||||
jwe.decrypt(new ECDHDecrypter(deviceKey));
|
||||
return decodeUserKey(jwe);
|
||||
} catch (JOSEException e) {
|
||||
throw new InvalidJweKeyException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static ECPrivateKey decodeUserKey(JWEObject decryptedJwe) {
|
||||
try {
|
||||
var keySpec = readKey(decryptedJwe, JWE_PAYLOAD_KEY_FIELD, PKCS8EncodedKeySpec::new);
|
||||
var factory = KeyFactory.getInstance(EC_ALG);
|
||||
var privateKey = factory.generatePrivate(keySpec);
|
||||
if (privateKey instanceof ECPrivateKey ecPrivateKey) {
|
||||
return ecPrivateKey;
|
||||
} else {
|
||||
throw new IllegalStateException(EC_ALG + " key factory not generating ECPrivateKeys");
|
||||
}
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException(EC_ALG + " not supported");
|
||||
} catch (InvalidKeySpecException e) {
|
||||
LOG.warn("Unexpected JWE payload: {}", decryptedJwe.getPayload());
|
||||
throw new MasterkeyLoadingFailedException("Unexpected JWE payload", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Masterkey decryptVaultKey(JWEObject jwe, ECPrivateKey privateKey) throws InvalidJweKeyException {
|
||||
try {
|
||||
jwe.decrypt(new ECDHDecrypter(privateKey));
|
||||
return readKey(jwe, JWE_PAYLOAD_KEY_FIELD, Masterkey::new);
|
||||
} catch (JOSEException e) {
|
||||
throw new InvalidJweKeyException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T readKey(JWEObject jwe, String keyField, Function<byte[], T> rawKeyFactory) throws MasterkeyLoadingFailedException {
|
||||
Preconditions.checkArgument(jwe.getState() == JWEObject.State.DECRYPTED);
|
||||
var fields = jwe.getPayload().toJSONObject();
|
||||
if (fields == null) {
|
||||
@@ -39,11 +107,11 @@ class JWEHelper {
|
||||
}
|
||||
var keyBytes = new byte[0];
|
||||
try {
|
||||
if (fields.get(JWE_PAYLOAD_MASTERKEY_FIELD) instanceof String key) {
|
||||
if (fields.get(keyField) instanceof String key) {
|
||||
keyBytes = BaseEncoding.base64().decode(key);
|
||||
return new Masterkey(keyBytes);
|
||||
return rawKeyFactory.apply(keyBytes);
|
||||
} else {
|
||||
throw new IllegalArgumentException("JWE payload doesn't contain field " + JWE_PAYLOAD_MASTERKEY_FIELD);
|
||||
throw new IllegalArgumentException("JWE payload doesn't contain field " + keyField);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.error("Unexpected JWE payload: {}", jwe.getPayload());
|
||||
@@ -52,4 +120,11 @@ class JWEHelper {
|
||||
Arrays.fill(keyBytes, (byte) 0x00);
|
||||
}
|
||||
}
|
||||
|
||||
public static class InvalidJweKeyException extends MasterkeyLoadingFailedException {
|
||||
|
||||
public InvalidJweKeyException(Throwable cause) {
|
||||
super("Invalid key", cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import com.fasterxml.jackson.core.JacksonException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.settings.DeviceKey;
|
||||
import org.cryptomator.cryptolib.common.P384KeyPair;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.WindowEvent;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@KeyLoadingScoped
|
||||
public class LegacyRegisterDeviceController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(LegacyRegisterDeviceController.class);
|
||||
private static final ObjectMapper JSON = new ObjectMapper().setDefaultLeniency(true);
|
||||
private static final List<Integer> EXPECTED_RESPONSE_CODES = List.of(201, 409);
|
||||
|
||||
private final Stage window;
|
||||
private final HubConfig hubConfig;
|
||||
private final String bearerToken;
|
||||
private final Lazy<Scene> registerSuccessScene;
|
||||
private final Lazy<Scene> registerFailedScene;
|
||||
private final String deviceId;
|
||||
private final P384KeyPair keyPair;
|
||||
private final CompletableFuture<ReceivedKey> result;
|
||||
private final DecodedJWT jwt;
|
||||
private final HttpClient httpClient;
|
||||
private final BooleanProperty deviceNameAlreadyExists = new SimpleBooleanProperty(false);
|
||||
|
||||
public TextField deviceNameField;
|
||||
public Button registerBtn;
|
||||
|
||||
@Inject
|
||||
public LegacyRegisterDeviceController(@KeyLoading Stage window, ExecutorService executor, HubConfig hubConfig, @Named("deviceId") String deviceId, DeviceKey deviceKey, CompletableFuture<ReceivedKey> result, @Named("bearerToken") AtomicReference<String> bearerToken, @FxmlScene(FxmlFile.HUB_REGISTER_SUCCESS) Lazy<Scene> registerSuccessScene, @FxmlScene(FxmlFile.HUB_REGISTER_FAILED) Lazy<Scene> registerFailedScene) {
|
||||
this.window = window;
|
||||
this.hubConfig = hubConfig;
|
||||
this.deviceId = deviceId;
|
||||
this.keyPair = Objects.requireNonNull(deviceKey.get());
|
||||
this.result = result;
|
||||
this.bearerToken = Objects.requireNonNull(bearerToken.get());
|
||||
this.registerSuccessScene = registerSuccessScene;
|
||||
this.registerFailedScene = registerFailedScene;
|
||||
this.jwt = JWT.decode(this.bearerToken);
|
||||
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
|
||||
this.httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).executor(executor).build();
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
deviceNameField.setText(determineHostname());
|
||||
deviceNameField.textProperty().addListener(observable -> deviceNameAlreadyExists.set(false));
|
||||
}
|
||||
|
||||
private String determineHostname() {
|
||||
try {
|
||||
var hostName = InetAddress.getLocalHost().getHostName();
|
||||
return Objects.requireNonNullElse(hostName, "");
|
||||
} catch (IOException e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void register() {
|
||||
deviceNameAlreadyExists.set(false);
|
||||
registerBtn.setContentDisplay(ContentDisplay.LEFT);
|
||||
registerBtn.setDisable(true);
|
||||
|
||||
var deviceUri = URI.create(hubConfig.devicesResourceUrl + deviceId);
|
||||
var deviceKey = keyPair.getPublic().getEncoded();
|
||||
var dto = new CreateDeviceDto();
|
||||
dto.id = deviceId;
|
||||
dto.name = deviceNameField.getText();
|
||||
dto.publicKey = Base64.getUrlEncoder().withoutPadding().encodeToString(deviceKey);
|
||||
var json = toJson(dto);
|
||||
var request = HttpRequest.newBuilder(deviceUri) //
|
||||
.PUT(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) //
|
||||
.header("Authorization", "Bearer " + bearerToken) //
|
||||
.header("Content-Type", "application/json") //
|
||||
.build();
|
||||
httpClient.sendAsync(request, HttpResponse.BodyHandlers.discarding()) //
|
||||
.thenApply(response -> {
|
||||
if (EXPECTED_RESPONSE_CODES.contains(response.statusCode())) {
|
||||
return response;
|
||||
} else {
|
||||
throw new RuntimeException("Server answered with unexpected status code " + response.statusCode());
|
||||
}
|
||||
}).handleAsync((response, throwable) -> {
|
||||
if (response != null) {
|
||||
this.handleResponse(response);
|
||||
} else {
|
||||
this.registrationFailed(throwable);
|
||||
}
|
||||
return null;
|
||||
}, Platform::runLater);
|
||||
}
|
||||
|
||||
private String toJson(CreateDeviceDto dto) {
|
||||
try {
|
||||
return JSON.writer().writeValueAsString(dto);
|
||||
} catch (JacksonException e) {
|
||||
throw new IllegalStateException("Failed to serialize DTO", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleResponse(HttpResponse<Void> voidHttpResponse) {
|
||||
assert EXPECTED_RESPONSE_CODES.contains(voidHttpResponse.statusCode());
|
||||
|
||||
if (voidHttpResponse.statusCode() == 409) {
|
||||
deviceNameAlreadyExists.set(true);
|
||||
registerBtn.setContentDisplay(ContentDisplay.TEXT_ONLY);
|
||||
registerBtn.setDisable(false);
|
||||
} else {
|
||||
LOG.debug("Device registration for hub instance {} successful.", hubConfig.authSuccessUrl);
|
||||
window.setScene(registerSuccessScene.get());
|
||||
}
|
||||
}
|
||||
|
||||
private void registrationFailed(Throwable cause) {
|
||||
LOG.warn("Device registration failed.", cause);
|
||||
window.setScene(registerFailedScene.get());
|
||||
result.completeExceptionally(cause);
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void close() {
|
||||
window.close();
|
||||
}
|
||||
|
||||
private void windowClosed(WindowEvent windowEvent) {
|
||||
result.cancel(true);
|
||||
}
|
||||
|
||||
/* Getter */
|
||||
|
||||
public String getUserName() {
|
||||
return jwt.getClaim("email").asString();
|
||||
}
|
||||
|
||||
|
||||
//--- Getters & Setters
|
||||
|
||||
public BooleanProperty deviceNameAlreadyExistsProperty() {
|
||||
return deviceNameAlreadyExists;
|
||||
}
|
||||
|
||||
public boolean getDeviceNameAlreadyExists() {
|
||||
return deviceNameAlreadyExists.get();
|
||||
}
|
||||
|
||||
private static class CreateDeviceDto {
|
||||
public String id;
|
||||
public String name;
|
||||
public final String type = "DESKTOP";
|
||||
public String publicKey;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.nimbusds.jose.JWEObject;
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
@@ -8,6 +11,9 @@ import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
@@ -17,14 +23,16 @@ import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.WindowEvent;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.ParseException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -33,25 +41,32 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
@KeyLoadingScoped
|
||||
public class ReceiveKeyController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ReceiveKeyController.class);
|
||||
private static final String SCHEME_PREFIX = "hub+";
|
||||
private static final ObjectMapper JSON = new ObjectMapper().setDefaultLeniency(true);
|
||||
private static final Duration REQ_TIMEOUT = Duration.ofSeconds(10);
|
||||
|
||||
private final Stage window;
|
||||
private final HubConfig hubConfig;
|
||||
private final String deviceId;
|
||||
private final String bearerToken;
|
||||
private final CompletableFuture<JWEObject> result;
|
||||
private final Lazy<Scene> registerDeviceScene;
|
||||
private final CompletableFuture<ReceivedKey> result;
|
||||
private final Lazy<Scene> setupDeviceScene;
|
||||
private final Lazy<Scene> legacyRegisterDeviceScene;
|
||||
private final Lazy<Scene> unauthorizedScene;
|
||||
private final URI vaultBaseUri;
|
||||
private final Lazy<Scene> invalidLicenseScene;
|
||||
private final HttpClient httpClient;
|
||||
|
||||
@Inject
|
||||
public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, @Named("deviceId") String deviceId, @Named("bearerToken") AtomicReference<String> tokenRef, CompletableFuture<JWEObject> result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy<Scene> registerDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy<Scene> unauthorizedScene, @FxmlScene(FxmlFile.HUB_INVALID_LICENSE) Lazy<Scene> invalidLicenseScene) {
|
||||
public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, HubConfig hubConfig, @Named("deviceId") String deviceId, @Named("bearerToken") AtomicReference<String> tokenRef, CompletableFuture<ReceivedKey> result, @FxmlScene(FxmlFile.HUB_SETUP_DEVICE) Lazy<Scene> setupDeviceScene, @FxmlScene(FxmlFile.HUB_LEGACY_REGISTER_DEVICE) Lazy<Scene> legacyRegisterDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy<Scene> unauthorizedScene, @FxmlScene(FxmlFile.HUB_INVALID_LICENSE) Lazy<Scene> invalidLicenseScene) {
|
||||
this.window = window;
|
||||
this.hubConfig = hubConfig;
|
||||
this.deviceId = deviceId;
|
||||
this.bearerToken = Objects.requireNonNull(tokenRef.get());
|
||||
this.result = result;
|
||||
this.registerDeviceScene = registerDeviceScene;
|
||||
this.setupDeviceScene = setupDeviceScene;
|
||||
this.legacyRegisterDeviceScene = legacyRegisterDeviceScene;
|
||||
this.unauthorizedScene = unauthorizedScene;
|
||||
this.vaultBaseUri = getVaultBaseUri(vault);
|
||||
this.invalidLicenseScene = invalidLicenseScene;
|
||||
@@ -61,23 +76,120 @@ public class ReceiveKeyController implements FxController {
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
var keyUri = appendPath(vaultBaseUri, "/keys/" + deviceId);
|
||||
var request = HttpRequest.newBuilder(keyUri) //
|
||||
requestVaultMasterkey();
|
||||
}
|
||||
|
||||
/**
|
||||
* STEP 1 (Request): GET vault key for this user
|
||||
*/
|
||||
private void requestVaultMasterkey() {
|
||||
var accessTokenUri = appendPath(vaultBaseUri, "/access-token");
|
||||
var request = HttpRequest.newBuilder(accessTokenUri) //
|
||||
.header("Authorization", "Bearer " + bearerToken) //
|
||||
.GET() //
|
||||
.timeout(REQ_TIMEOUT) //
|
||||
.build();
|
||||
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()) //
|
||||
.thenAcceptAsync(this::loadedExistingKey, Platform::runLater) //
|
||||
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.US_ASCII)) //
|
||||
.thenAcceptAsync(this::receivedVaultMasterkey, Platform::runLater) //
|
||||
.exceptionally(this::retrievalFailed);
|
||||
}
|
||||
|
||||
private void loadedExistingKey(HttpResponse<InputStream> response) {
|
||||
/**
|
||||
* STEP 1 (Response): GET vault key for this user
|
||||
*
|
||||
* @param response Response
|
||||
*/
|
||||
private void receivedVaultMasterkey(HttpResponse<String> response) {
|
||||
LOG.debug("GET {} -> Status Code {}", response.request().uri(), response.statusCode());
|
||||
switch (response.statusCode()) {
|
||||
case 200 -> requestUserKey(response.body());
|
||||
case 402 -> licenseExceeded();
|
||||
case 403, 410 -> accessNotGranted(); // or vault has been archived, effectively disallowing access - TODO: add specific dialog?
|
||||
case 404 -> requestLegacyAccessToken();
|
||||
default -> throw new IllegalStateException("Unexpected response " + response.statusCode());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* STEP 2 (Request): GET user key for this device
|
||||
*/
|
||||
private void requestUserKey(String encryptedVaultKey) {
|
||||
var deviceTokenUri = URI.create(hubConfig.getApiBaseUrl() + "/devices/" + deviceId);
|
||||
var request = HttpRequest.newBuilder(deviceTokenUri) //
|
||||
.header("Authorization", "Bearer " + bearerToken) //
|
||||
.GET() //
|
||||
.timeout(REQ_TIMEOUT) //
|
||||
.build();
|
||||
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)) //
|
||||
.thenAcceptAsync(response -> receivedUserKey(encryptedVaultKey, response), Platform::runLater) //
|
||||
.exceptionally(this::retrievalFailed);
|
||||
}
|
||||
|
||||
/**
|
||||
* STEP 2 (Response): GET user key for this device
|
||||
*
|
||||
* @param response Response
|
||||
*/
|
||||
private void receivedUserKey(String encryptedVaultKey, HttpResponse<String> response) {
|
||||
LOG.debug("GET {} -> Status Code {}", response.request().uri(), response.statusCode());
|
||||
try {
|
||||
switch (response.statusCode()) {
|
||||
case 200 -> retrievalSucceeded(response);
|
||||
case 200 -> {
|
||||
var device = JSON.reader().readValue(response.body(), DeviceDto.class);
|
||||
receivedBothEncryptedKeys(encryptedVaultKey, device.userPrivateKey);
|
||||
}
|
||||
case 404 -> needsDeviceSetup(); // TODO: using the setup code, we can theoretically immediately unlock
|
||||
default -> throw new IllegalStateException("Unexpected response " + response.statusCode());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void needsDeviceSetup() {
|
||||
window.setScene(setupDeviceScene.get());
|
||||
}
|
||||
|
||||
private void receivedBothEncryptedKeys(String encryptedVaultKey, String encryptedUserKey) throws IOException {
|
||||
try {
|
||||
var vaultKeyJwe = JWEObject.parse(encryptedVaultKey);
|
||||
var userKeyJwe = JWEObject.parse(encryptedUserKey);
|
||||
result.complete(ReceivedKey.vaultKeyAndUserKey(vaultKeyJwe, userKeyJwe));
|
||||
window.close();
|
||||
} catch (ParseException e) {
|
||||
throw new IOException("Failed to parse JWE", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LEGACY FALLBACK (Request): GET the legacy access token from Hub 1.x
|
||||
*/
|
||||
@Deprecated
|
||||
private void requestLegacyAccessToken() {
|
||||
var legacyAccessTokenUri = appendPath(vaultBaseUri, "/keys/" + deviceId);
|
||||
var request = HttpRequest.newBuilder(legacyAccessTokenUri) //
|
||||
.header("Authorization", "Bearer " + bearerToken) //
|
||||
.GET() //
|
||||
.timeout(REQ_TIMEOUT) //
|
||||
.build();
|
||||
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.US_ASCII)) //
|
||||
.thenAcceptAsync(this::receivedLegacyAccessTokenResponse, Platform::runLater) //
|
||||
.exceptionally(this::retrievalFailed);
|
||||
}
|
||||
|
||||
/**
|
||||
* LEGACY FALLBACK (Response)
|
||||
*
|
||||
* @param response Response
|
||||
*/
|
||||
@Deprecated
|
||||
private void receivedLegacyAccessTokenResponse(HttpResponse<String> response) {
|
||||
try {
|
||||
switch (response.statusCode()) {
|
||||
case 200 -> receivedLegacyAccessTokenSuccess(response.body());
|
||||
case 402 -> licenseExceeded();
|
||||
case 403, 410 -> accessNotGranted(); // or vault has been archived, effectively disallowing access
|
||||
case 404 -> needsDeviceRegistration();
|
||||
case 404 -> needsLegacyDeviceRegistration();
|
||||
default -> throw new IOException("Unexpected response " + response.statusCode());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
@@ -85,10 +197,11 @@ public class ReceiveKeyController implements FxController {
|
||||
}
|
||||
}
|
||||
|
||||
private void retrievalSucceeded(HttpResponse<InputStream> response) throws IOException {
|
||||
@Deprecated
|
||||
private void receivedLegacyAccessTokenSuccess(String rawToken) throws IOException {
|
||||
try {
|
||||
var string = HttpHelper.readBody(response);
|
||||
result.complete(JWEObject.parse(string));
|
||||
var token = JWEObject.parse(rawToken);
|
||||
result.complete(ReceivedKey.legacyDeviceKey(token));
|
||||
window.close();
|
||||
} catch (ParseException e) {
|
||||
throw new IOException("Failed to parse JWE", e);
|
||||
@@ -99,8 +212,9 @@ public class ReceiveKeyController implements FxController {
|
||||
window.setScene(invalidLicenseScene.get());
|
||||
}
|
||||
|
||||
private void needsDeviceRegistration() {
|
||||
window.setScene(registerDeviceScene.get());
|
||||
@Deprecated
|
||||
private void needsLegacyDeviceRegistration() {
|
||||
window.setScene(legacyRegisterDeviceScene.get());
|
||||
}
|
||||
|
||||
private void accessNotGranted() {
|
||||
@@ -132,14 +246,17 @@ public class ReceiveKeyController implements FxController {
|
||||
|
||||
private static URI getVaultBaseUri(Vault vault) {
|
||||
try {
|
||||
var kid = vault.getVaultConfigCache().get().getKeyId();
|
||||
assert kid.getScheme().startsWith(SCHEME_PREFIX);
|
||||
var hubUriScheme = kid.getScheme().substring(SCHEME_PREFIX.length());
|
||||
return new URI(hubUriScheme, kid.getSchemeSpecificPart(), kid.getFragment());
|
||||
var url = vault.getVaultConfigCache().get().getKeyId();
|
||||
assert url.getScheme().startsWith(SCHEME_PREFIX);
|
||||
var correctedScheme = url.getScheme().substring(SCHEME_PREFIX.length());
|
||||
return new URI(correctedScheme, url.getSchemeSpecificPart(), url.getFragment());
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IllegalStateException("URI constructed from params known to be valid", e);
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private record DeviceDto(@JsonProperty(value = "userPrivateKey", required = true) String userPrivateKey) {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.nimbusds.jose.JWEObject;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
|
||||
import java.security.interfaces.ECPrivateKey;
|
||||
|
||||
@FunctionalInterface
|
||||
interface ReceivedKey {
|
||||
|
||||
/**
|
||||
* Decrypts the vault key.
|
||||
*
|
||||
* @param deviceKey This device's private key.
|
||||
* @return The decrypted vault key
|
||||
*/
|
||||
Masterkey decryptMasterkey(ECPrivateKey deviceKey);
|
||||
|
||||
/**
|
||||
* Creates an unlock response object from the user key + vault key.
|
||||
*
|
||||
* @param vaultKeyJwe a JWE containing the symmetric vault key, encrypted for this device's user.
|
||||
* @param userKeyJwe a JWE containing the user's private key, encrypted for this device.
|
||||
* @return Ciphertext received by Hub, which can be decrypted using this device's private key.
|
||||
*/
|
||||
static ReceivedKey vaultKeyAndUserKey(JWEObject vaultKeyJwe, JWEObject userKeyJwe) {
|
||||
return deviceKey -> {
|
||||
var userKey = JWEHelper.decryptUserKey(userKeyJwe, deviceKey);
|
||||
return JWEHelper.decryptVaultKey(vaultKeyJwe, userKey);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an unlock response object from the received legacy "access token" JWE.
|
||||
*
|
||||
* @param vaultKeyJwe a JWE containing the symmetric vault key, encrypted for this device.
|
||||
* @return Ciphertext received by Hub, which can be decrypted using this device's private key.
|
||||
* @deprecated Only for compatibility with Hub 1.0 - 1.2
|
||||
*/
|
||||
@Deprecated
|
||||
static ReceivedKey legacyDeviceKey(JWEObject vaultKeyJwe) {
|
||||
return deviceKey -> JWEHelper.decryptVaultKey(vaultKeyJwe, deviceKey);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JacksonException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
@@ -20,6 +20,7 @@ import org.slf4j.LoggerFactory;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.fxml.FXML;
|
||||
@@ -31,14 +32,16 @@ import javafx.stage.Stage;
|
||||
import javafx.stage.WindowEvent;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.text.ParseException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@@ -47,7 +50,7 @@ public class RegisterDeviceController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(RegisterDeviceController.class);
|
||||
private static final ObjectMapper JSON = new ObjectMapper().setDefaultLeniency(true);
|
||||
private static final List<Integer> EXPECTED_RESPONSE_CODES = List.of(201, 409);
|
||||
private static final Duration REQ_TIMEOUT = Duration.ofSeconds(10);
|
||||
|
||||
private final Stage window;
|
||||
private final HubConfig hubConfig;
|
||||
@@ -55,26 +58,27 @@ public class RegisterDeviceController implements FxController {
|
||||
private final Lazy<Scene> registerSuccessScene;
|
||||
private final Lazy<Scene> registerFailedScene;
|
||||
private final String deviceId;
|
||||
private final P384KeyPair keyPair;
|
||||
private final CompletableFuture<JWEObject> result;
|
||||
private final DecodedJWT jwt;
|
||||
private final P384KeyPair deviceKeyPair;
|
||||
private final CompletableFuture<ReceivedKey> result;
|
||||
private final HttpClient httpClient;
|
||||
private final BooleanProperty deviceNameAlreadyExists = new SimpleBooleanProperty(false);
|
||||
|
||||
private final BooleanProperty deviceNameAlreadyExists = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty invalidSetupCode = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty workInProgress = new SimpleBooleanProperty(false);
|
||||
public TextField setupCodeField;
|
||||
public TextField deviceNameField;
|
||||
public Button registerBtn;
|
||||
|
||||
@Inject
|
||||
public RegisterDeviceController(@KeyLoading Stage window, ExecutorService executor, HubConfig hubConfig, @Named("deviceId") String deviceId, DeviceKey deviceKey, CompletableFuture<JWEObject> result, @Named("bearerToken") AtomicReference<String> bearerToken, @FxmlScene(FxmlFile.HUB_REGISTER_SUCCESS) Lazy<Scene> registerSuccessScene, @FxmlScene(FxmlFile.HUB_REGISTER_FAILED) Lazy<Scene> registerFailedScene) {
|
||||
public RegisterDeviceController(@KeyLoading Stage window, ExecutorService executor, HubConfig hubConfig, @Named("deviceId") String deviceId, DeviceKey deviceKey, CompletableFuture<ReceivedKey> result, @Named("bearerToken") AtomicReference<String> bearerToken, @FxmlScene(FxmlFile.HUB_REGISTER_SUCCESS) Lazy<Scene> registerSuccessScene, @FxmlScene(FxmlFile.HUB_REGISTER_FAILED) Lazy<Scene> registerFailedScene) {
|
||||
this.window = window;
|
||||
this.hubConfig = hubConfig;
|
||||
this.deviceId = deviceId;
|
||||
this.keyPair = Objects.requireNonNull(deviceKey.get());
|
||||
this.deviceKeyPair = Objects.requireNonNull(deviceKey.get());
|
||||
this.result = result;
|
||||
this.bearerToken = Objects.requireNonNull(bearerToken.get());
|
||||
this.registerSuccessScene = registerSuccessScene;
|
||||
this.registerFailedScene = registerFailedScene;
|
||||
this.jwt = JWT.decode(this.bearerToken);
|
||||
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
|
||||
this.httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).executor(executor).build();
|
||||
}
|
||||
@@ -82,6 +86,13 @@ public class RegisterDeviceController implements FxController {
|
||||
public void initialize() {
|
||||
deviceNameField.setText(determineHostname());
|
||||
deviceNameField.textProperty().addListener(observable -> deviceNameAlreadyExists.set(false));
|
||||
deviceNameField.disableProperty().bind(workInProgress);
|
||||
setupCodeField.textProperty().addListener(observable -> invalidSetupCode.set(false));
|
||||
setupCodeField.disableProperty().bind(workInProgress);
|
||||
var missingSetupCode = setupCodeField.textProperty().isEmpty();
|
||||
var missingDeviceName = deviceNameField.textProperty().isEmpty();
|
||||
registerBtn.disableProperty().bind(workInProgress.or(missingSetupCode).or(missingDeviceName));
|
||||
registerBtn.contentDisplayProperty().bind(Bindings.when(workInProgress).then(ContentDisplay.LEFT).otherwise(ContentDisplay.TEXT_ONLY));
|
||||
}
|
||||
|
||||
private String determineHostname() {
|
||||
@@ -95,35 +106,62 @@ public class RegisterDeviceController implements FxController {
|
||||
|
||||
@FXML
|
||||
public void register() {
|
||||
deviceNameAlreadyExists.set(false);
|
||||
registerBtn.setContentDisplay(ContentDisplay.LEFT);
|
||||
registerBtn.setDisable(true);
|
||||
workInProgress.set(true);
|
||||
|
||||
var keyUri = URI.create(hubConfig.devicesResourceUrl + deviceId);
|
||||
var deviceKey = keyPair.getPublic().getEncoded();
|
||||
var dto = new CreateDeviceDto(deviceId, deviceNameField.getText(), BaseEncoding.base64Url().omitPadding().encode(deviceKey));
|
||||
var json = toJson(dto);
|
||||
var request = HttpRequest.newBuilder(keyUri) //
|
||||
var apiRootUrl = hubConfig.getApiBaseUrl();
|
||||
|
||||
var userReq = HttpRequest.newBuilder(apiRootUrl.resolve("users/me")) //
|
||||
.GET() //
|
||||
.timeout(REQ_TIMEOUT) //
|
||||
.header("Authorization", "Bearer " + bearerToken) //
|
||||
.header("Content-Type", "application/json").PUT(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) //
|
||||
.header("Content-Type", "application/json") //
|
||||
.build();
|
||||
httpClient.sendAsync(request, HttpResponse.BodyHandlers.discarding()) //
|
||||
httpClient.sendAsync(userReq, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)) //
|
||||
.thenApply(response -> {
|
||||
if (EXPECTED_RESPONSE_CODES.contains(response.statusCode())) {
|
||||
return response;
|
||||
if (response.statusCode() == 200) {
|
||||
var dto = fromJson(response.body());
|
||||
return Objects.requireNonNull(dto, "null or empty response body");
|
||||
} else {
|
||||
throw new RuntimeException("Server answered with unexpected status code " + response.statusCode());
|
||||
}
|
||||
}).handleAsync((response, throwable) -> {
|
||||
}).thenApply(user -> {
|
||||
try {
|
||||
assert user.privateKey != null; // api/vaults/{v}/user-tokens/me would have returned 403, if user wasn't fully set up yet
|
||||
var userKey = JWEHelper.decryptUserKey(JWEObject.parse(user.privateKey), setupCodeField.getText());
|
||||
return JWEHelper.encryptUserKey(userKey, deviceKeyPair.getPublic());
|
||||
} catch (ParseException e) {
|
||||
throw new RuntimeException("Server answered with unparsable user key", e);
|
||||
}
|
||||
}).thenCompose(jwe -> {
|
||||
var now = Instant.now().toString();
|
||||
var dto = new CreateDeviceDto(deviceId, deviceNameField.getText(), BaseEncoding.base64().encode(deviceKeyPair.getPublic().getEncoded()), "DESKTOP", jwe.serialize(), now);
|
||||
var json = toJson(dto);
|
||||
var deviceUri = apiRootUrl.resolve("devices/" + deviceId);
|
||||
var putDeviceReq = HttpRequest.newBuilder(deviceUri) //
|
||||
.PUT(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) //
|
||||
.timeout(REQ_TIMEOUT) //
|
||||
.header("Authorization", "Bearer " + bearerToken) //
|
||||
.header("Content-Type", "application/json") //
|
||||
.build();
|
||||
return httpClient.sendAsync(putDeviceReq, HttpResponse.BodyHandlers.discarding());
|
||||
}).whenCompleteAsync((response, throwable) -> {
|
||||
if (response != null) {
|
||||
this.handleResponse(response);
|
||||
} else {
|
||||
this.registrationFailed(throwable);
|
||||
this.setupFailed(throwable);
|
||||
}
|
||||
return null;
|
||||
workInProgress.set(false);
|
||||
}, Platform::runLater);
|
||||
}
|
||||
|
||||
private UserDto fromJson(String json) {
|
||||
try {
|
||||
return JSON.reader().readValue(json, UserDto.class);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Failed to deserialize DTO", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String toJson(CreateDeviceDto dto) {
|
||||
try {
|
||||
return JSON.writer().writeValueAsString(dto);
|
||||
@@ -132,23 +170,26 @@ public class RegisterDeviceController implements FxController {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleResponse(HttpResponse<Void> voidHttpResponse) {
|
||||
assert EXPECTED_RESPONSE_CODES.contains(voidHttpResponse.statusCode());
|
||||
|
||||
if (voidHttpResponse.statusCode() == 409) {
|
||||
deviceNameAlreadyExists.set(true);
|
||||
registerBtn.setContentDisplay(ContentDisplay.TEXT_ONLY);
|
||||
registerBtn.setDisable(false);
|
||||
} else {
|
||||
private void handleResponse(HttpResponse<Void> response) {
|
||||
if (response.statusCode() == 201) {
|
||||
LOG.debug("Device registration for hub instance {} successful.", hubConfig.authSuccessUrl);
|
||||
window.setScene(registerSuccessScene.get());
|
||||
} else if (response.statusCode() == 409) {
|
||||
deviceNameAlreadyExists.set(true);
|
||||
} else {
|
||||
setupFailed(new IllegalStateException("Unexpected http status code " + response.statusCode()));
|
||||
}
|
||||
}
|
||||
|
||||
private void registrationFailed(Throwable cause) {
|
||||
LOG.warn("Device registration failed.", cause);
|
||||
window.setScene(registerFailedScene.get());
|
||||
result.completeExceptionally(cause);
|
||||
private void setupFailed(Throwable cause) {
|
||||
switch (cause) {
|
||||
case CompletionException e when e.getCause() instanceof JWEHelper.InvalidJweKeyException -> invalidSetupCode.set(true);
|
||||
default -> {
|
||||
LOG.warn("Device setup failed.", cause);
|
||||
window.setScene(registerFailedScene.get());
|
||||
result.completeExceptionally(cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -160,13 +201,6 @@ public class RegisterDeviceController implements FxController {
|
||||
result.cancel(true);
|
||||
}
|
||||
|
||||
/* Getter */
|
||||
|
||||
public String getUserName() {
|
||||
return jwt.getClaim("email").asString();
|
||||
}
|
||||
|
||||
|
||||
//--- Getters & Setters
|
||||
|
||||
public BooleanProperty deviceNameAlreadyExistsProperty() {
|
||||
@@ -177,5 +211,21 @@ public class RegisterDeviceController implements FxController {
|
||||
return deviceNameAlreadyExists.get();
|
||||
}
|
||||
|
||||
public BooleanProperty invalidSetupCodeProperty() {
|
||||
return invalidSetupCode;
|
||||
}
|
||||
|
||||
public boolean isInvalidSetupCode() {
|
||||
return invalidSetupCode.get();
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private record UserDto(String id, String name, String publicKey, String privateKey, String setupCode) {}
|
||||
|
||||
private record CreateDeviceDto(@JsonProperty(required = true) String id, //
|
||||
@JsonProperty(required = true) String name, //
|
||||
@JsonProperty(required = true) String publicKey, //
|
||||
@JsonProperty(required = true, defaultValue = "DESKTOP") String type, //
|
||||
@JsonProperty(required = true) String userPrivateKey, //
|
||||
@JsonProperty(required = true) String creationTime) {}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,10 @@ import java.util.concurrent.CompletableFuture;
|
||||
public class RegisterFailedController implements FxController {
|
||||
|
||||
private final Stage window;
|
||||
private final CompletableFuture<JWEObject> result;
|
||||
private final CompletableFuture<ReceivedKey> result;
|
||||
|
||||
@Inject
|
||||
public RegisterFailedController(@KeyLoading Stage window, CompletableFuture<JWEObject> result) {
|
||||
public RegisterFailedController(@KeyLoading Stage window, CompletableFuture<ReceivedKey> result) {
|
||||
this.window = window;
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ import java.util.concurrent.CompletableFuture;
|
||||
public class UnauthorizedDeviceController implements FxController {
|
||||
|
||||
private final Stage window;
|
||||
private final CompletableFuture<JWEObject> result;
|
||||
private final CompletableFuture<ReceivedKey> result;
|
||||
|
||||
@Inject
|
||||
public UnauthorizedDeviceController(@KeyLoading Stage window, CompletableFuture<JWEObject> result) {
|
||||
public UnauthorizedDeviceController(@KeyLoading Stage window, CompletableFuture<ReceivedKey> result) {
|
||||
this.window = window;
|
||||
this.result = result;
|
||||
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
|
||||
|
||||
@@ -7,7 +7,6 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
@@ -67,37 +66,7 @@ public class ResizeController implements FxController {
|
||||
return (settings.windowHeight.get() == 0) && (settings.windowWidth.get() == 0) && (settings.windowXPosition.get() == 0) && (settings.windowYPosition.get() == 0);
|
||||
}
|
||||
|
||||
private boolean isWithinDisplayBounds() {
|
||||
// (x1, y1) is the top left corner of the window, (x2, y2) is the bottom right corner
|
||||
final double slack = 10;
|
||||
final double width = window.getWidth() - 2 * slack;
|
||||
final double height = window.getHeight() - 2 * slack;
|
||||
final double x1 = window.getX() + slack;
|
||||
final double y1 = window.getY() + slack;
|
||||
final double x2 = x1 + width;
|
||||
final double y2 = y1 + height;
|
||||
|
||||
final ObservableList<Screen> screens = Screen.getScreensForRectangle(x1, y1, width, height);
|
||||
|
||||
// Find the total visible area of the window
|
||||
double visibleArea = 0;
|
||||
for (Screen screen : screens) {
|
||||
Rectangle2D bounds = screen.getVisualBounds();
|
||||
|
||||
double xOverlap = Math.min(x2, bounds.getMaxX()) - Math.max(x1, bounds.getMinX());
|
||||
double yOverlap = Math.min(y2, bounds.getMaxY()) - Math.max(y1, bounds.getMinY());
|
||||
|
||||
visibleArea += xOverlap * yOverlap;
|
||||
}
|
||||
|
||||
final double windowArea = width * height;
|
||||
|
||||
// Within bounds if the visible area matches the window area
|
||||
return visibleArea == windowArea;
|
||||
}
|
||||
|
||||
private void checkDisplayBounds(WindowEvent evt) {
|
||||
|
||||
// Minimizing a window in Windows and closing it could result in an out of bounds position at (x, y) = (-32000, -32000)
|
||||
// See https://devblogs.microsoft.com/oldnewthing/20041028-00/?p=37453
|
||||
// If the position is (-32000, -32000), restore to the last saved position
|
||||
@@ -108,8 +77,9 @@ public class ResizeController implements FxController {
|
||||
window.setHeight(settings.windowHeight.get());
|
||||
}
|
||||
|
||||
if (!isWithinDisplayBounds()) {
|
||||
if (isOutOfDisplayBounds()) {
|
||||
// If the position is illegal, then the window appears on the main screen in the middle of the window.
|
||||
LOG.debug("Resetting window position due to insufficient screen overlap");
|
||||
Rectangle2D primaryScreenBounds = Screen.getPrimary().getBounds();
|
||||
window.setX((primaryScreenBounds.getWidth() - window.getMinWidth()) / 2);
|
||||
window.setY((primaryScreenBounds.getHeight() - window.getMinHeight()) / 2);
|
||||
@@ -119,6 +89,22 @@ public class ResizeController implements FxController {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isOutOfDisplayBounds() {
|
||||
// define a rect which is inset on all sides from the window's rect:
|
||||
final double x = window.getX() + 20; // 20px left
|
||||
final double y = window.getY() + 5; // 5px top
|
||||
final double w = window.getWidth() - 40; // 20px left + 20px right
|
||||
final double h = window.getHeight() - 25; // 5px top + 20px bottom
|
||||
return isRectangleOutOfScreen(x, y, 0, h) // Left pixel column
|
||||
|| isRectangleOutOfScreen(x + w, y, 0, h) // Right pixel column
|
||||
|| isRectangleOutOfScreen(x, y, w, 0) // Top pixel row
|
||||
|| isRectangleOutOfScreen(x, y + h, w, 0); // Bottom pixel row
|
||||
}
|
||||
|
||||
private boolean isRectangleOutOfScreen(double x, double y, double width, double height) {
|
||||
return Screen.getScreensForRectangle(x, y, width, height).isEmpty();
|
||||
}
|
||||
|
||||
private void startResize(MouseEvent evt) {
|
||||
origX = window.getX();
|
||||
origY = window.getY();
|
||||
|
||||
@@ -20,9 +20,10 @@ import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.event.Event;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.geometry.Side;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.input.ContextMenuEvent;
|
||||
import javafx.scene.input.DragEvent;
|
||||
@@ -67,6 +68,8 @@ public class VaultListController implements FxController {
|
||||
public ListView<Vault> vaultList;
|
||||
public StackPane root;
|
||||
public Button addVaultBtn;
|
||||
@FXML
|
||||
private ContextMenu addVaultContextMenu;
|
||||
|
||||
@Inject
|
||||
VaultListController(@MainWindow Stage mainWindow, //
|
||||
@@ -140,15 +143,15 @@ public class VaultListController implements FxController {
|
||||
root.setOnDragOver(this::handleDragEvent);
|
||||
root.setOnDragDropped(this::handleDragEvent);
|
||||
root.setOnDragExited(this::handleDragEvent);
|
||||
|
||||
addVaultBtn.addEventFilter(ContextMenuEvent.CONTEXT_MENU_REQUESTED, Event::consume);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void showMenu() {
|
||||
double screenX = addVaultBtn.localToScreen(addVaultBtn.getBoundsInLocal()).getMinX();
|
||||
double screenY = addVaultBtn.localToScreen(addVaultBtn.getBoundsInLocal()).getMaxY();
|
||||
addVaultBtn.getContextMenu().show(addVaultBtn, screenX, screenY);
|
||||
private void toggleMenu() {
|
||||
if (addVaultContextMenu.isShowing()) {
|
||||
addVaultContextMenu.hide();
|
||||
} else {
|
||||
addVaultContextMenu.show(addVaultBtn, Side.BOTTOM, 0.0, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
private void deselect(MouseEvent released) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.cryptomator.ui.preferences;
|
||||
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.fxapp.UpdateChecker;
|
||||
import org.slf4j.Logger;
|
||||
@@ -19,6 +20,7 @@ public class PreferencesController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PreferencesController.class);
|
||||
|
||||
private final Environment env;
|
||||
private final Stage window;
|
||||
private final ObjectProperty<SelectedPreferencesTab> selectedTabProperty;
|
||||
private final BooleanBinding updateAvailable;
|
||||
@@ -31,7 +33,8 @@ public class PreferencesController implements FxController {
|
||||
public Tab aboutTab;
|
||||
|
||||
@Inject
|
||||
public PreferencesController(@PreferencesWindow Stage window, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, UpdateChecker updateChecker) {
|
||||
public PreferencesController(Environment env, @PreferencesWindow Stage window, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, UpdateChecker updateChecker) {
|
||||
this.env = env;
|
||||
this.window = window;
|
||||
this.selectedTabProperty = selectedTabProperty;
|
||||
this.updateAvailable = updateChecker.latestVersionProperty().isNotNull();
|
||||
@@ -42,6 +45,9 @@ public class PreferencesController implements FxController {
|
||||
window.setOnShowing(this::windowWillAppear);
|
||||
selectedTabProperty.addListener(observable -> this.selectChosenTab());
|
||||
tabPane.getSelectionModel().selectedItemProperty().addListener(observable -> this.selectedTabChanged());
|
||||
if (env.disableUpdateCheck()) {
|
||||
tabPane.getTabs().remove(updatesTab);
|
||||
}
|
||||
}
|
||||
|
||||
private void selectChosenTab() {
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.VisibleForTesting;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
@@ -58,7 +59,7 @@ public class RecoveryKeyFactory {
|
||||
}
|
||||
}
|
||||
|
||||
// visible for testing
|
||||
@VisibleForTesting
|
||||
String createRecoveryKey(byte[] rawKey) {
|
||||
Preconditions.checkArgument(rawKey.length == 64, "key should be 64 bytes");
|
||||
byte[] paddedKey = Arrays.copyOf(rawKey, 66);
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
|
||||
<HBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.keyloading.hub.RegisterDeviceController"
|
||||
fx:controller="org.cryptomator.ui.keyloading.hub.LegacyRegisterDeviceController"
|
||||
minWidth="400"
|
||||
maxWidth="400"
|
||||
minHeight="145"
|
||||
@@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.ButtonBar?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.Group?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.Region?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.shape.Circle?>
|
||||
<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
|
||||
<HBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.keyloading.hub.RegisterDeviceController"
|
||||
minWidth="400"
|
||||
maxWidth="400"
|
||||
minHeight="145"
|
||||
spacing="12"
|
||||
alignment="TOP_LEFT">
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="12"/>
|
||||
</padding>
|
||||
<children>
|
||||
<Group>
|
||||
<StackPane>
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="6"/>
|
||||
</padding>
|
||||
<Circle styleClass="glyph-icon-primary" radius="24"/>
|
||||
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="INFO" glyphSize="24"/>
|
||||
</StackPane>
|
||||
</Group>
|
||||
|
||||
<VBox HBox.hgrow="ALWAYS">
|
||||
<Label styleClass="label-large" text="%hub.register.message" wrapText="true" textAlignment="LEFT">
|
||||
<padding>
|
||||
<Insets bottom="6" top="6"/>
|
||||
</padding>
|
||||
</Label>
|
||||
<Label text="%hub.register.description" wrapText="true"/>
|
||||
<HBox spacing="6" alignment="CENTER_LEFT">
|
||||
<padding>
|
||||
<Insets top="12"/>
|
||||
</padding>
|
||||
<Label text="%hub.register.setupCodeLabel" labelFor="$setupCodeField"/>
|
||||
<TextField fx:id="setupCodeField" HBox.hgrow="ALWAYS"/>
|
||||
</HBox>
|
||||
<HBox spacing="6" alignment="CENTER_LEFT">
|
||||
<padding>
|
||||
<Insets top="12"/>
|
||||
</padding>
|
||||
<Label text="%hub.register.nameLabel" labelFor="$deviceNameField"/>
|
||||
<TextField fx:id="deviceNameField" HBox.hgrow="ALWAYS"/>
|
||||
</HBox>
|
||||
<HBox alignment="TOP_RIGHT">
|
||||
<Label text="%hub.register.occupiedMsg" textAlignment="RIGHT" alignment="CENTER_RIGHT" visible="${controller.deviceNameAlreadyExists}" managed="${controller.deviceNameAlreadyExists}" graphicTextGap="6">
|
||||
<padding>
|
||||
<Insets top="6"/>
|
||||
</padding>
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="TIMES" styleClass="glyph-icon-red"/>
|
||||
</graphic>
|
||||
</Label>
|
||||
|
||||
<Label text="%hub.register.invalidSetupCode" textAlignment="RIGHT" alignment="CENTER_RIGHT" visible="${controller.invalidSetupCode}" managed="${controller.invalidSetupCode}" graphicTextGap="6">
|
||||
<padding>
|
||||
<Insets top="6"/>
|
||||
</padding>
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="TIMES" styleClass="glyph-icon-red"/>
|
||||
</graphic>
|
||||
</Label>
|
||||
</HBox>
|
||||
|
||||
<Region VBox.vgrow="ALWAYS" minHeight="18"/>
|
||||
<ButtonBar buttonMinWidth="120" buttonOrder="+CU">
|
||||
<buttons>
|
||||
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
|
||||
<Button fx:id="registerBtn" text="%hub.register.registerBtn" ButtonBar.buttonData="OTHER" defaultButton="true" onAction="#register" contentDisplay="TEXT_ONLY" >
|
||||
<graphic>
|
||||
<FontAwesome5Spinner glyphSize="12" />
|
||||
</graphic>
|
||||
</Button>
|
||||
</buttons>
|
||||
</ButtonBar>
|
||||
</VBox>
|
||||
</children>
|
||||
</HBox>
|
||||
@@ -28,27 +28,27 @@
|
||||
<Arc VBox.vgrow="NEVER" styleClass="onboarding-overlay-arc" type="OPEN" centerX="50" centerY="0" radiusY="100" radiusX="50" startAngle="0" length="-60" strokeWidth="1"/>
|
||||
</VBox>
|
||||
</StackPane>
|
||||
<Button fx:id="addVaultBtn" onAction="#showMenu" styleClass="toolbar-button" text="%main.vaultlist.addVaultBtn" alignment="BASELINE_CENTER" maxWidth="Infinity" contentDisplay="RIGHT">
|
||||
<Button fx:id="addVaultBtn" onAction="#toggleMenu" styleClass="toolbar-button" text="%main.vaultlist.addVaultBtn" alignment="BASELINE_CENTER" maxWidth="Infinity" contentDisplay="RIGHT">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="CARET_DOWN"/>
|
||||
</graphic>
|
||||
<contextMenu>
|
||||
<ContextMenu>
|
||||
<items>
|
||||
<MenuItem styleClass="add-vault-menu-item" text="%main.vaultlist.addVaultBtn.menuItemNew" onAction="#didClickAddNewVault" >
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="PLUS" textAlignment="CENTER" wrappingWidth="14" />
|
||||
</graphic>
|
||||
</MenuItem>
|
||||
<MenuItem styleClass="add-vault-menu-item" text="%main.vaultlist.addVaultBtn.menuItemExisting" onAction="#didClickAddExistingVault" >
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="FOLDER_OPEN" textAlignment="CENTER" wrappingWidth="14" />
|
||||
</graphic>
|
||||
</MenuItem>
|
||||
</items>
|
||||
</ContextMenu>
|
||||
</contextMenu>
|
||||
</Button>
|
||||
<fx:define>
|
||||
<ContextMenu fx:id="addVaultContextMenu">
|
||||
<items>
|
||||
<MenuItem styleClass="add-vault-menu-item" text="%main.vaultlist.addVaultBtn.menuItemNew" onAction="#didClickAddNewVault" >
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="PLUS" textAlignment="CENTER" wrappingWidth="14" />
|
||||
</graphic>
|
||||
</MenuItem>
|
||||
<MenuItem styleClass="add-vault-menu-item" text="%main.vaultlist.addVaultBtn.menuItemExisting" onAction="#didClickAddExistingVault" >
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="FOLDER_OPEN" textAlignment="CENTER" wrappingWidth="14" />
|
||||
</graphic>
|
||||
</MenuItem>
|
||||
</items>
|
||||
</ContextMenu>
|
||||
</fx:define>
|
||||
</VBox>
|
||||
<Region styleClass="drag-n-drop-border" visible="${controller.draggingVaultOver}"/>
|
||||
</StackPane>
|
||||
|
||||
@@ -154,9 +154,11 @@ hub.auth.loginLink=Not redirected? Click here to open it.
|
||||
hub.receive.message=Processing response…
|
||||
hub.receive.description=Cryptomator is receiving and processing the response from Hub. Please wait.
|
||||
### Register Device
|
||||
hub.register.message=Device name required
|
||||
hub.register.description=This seems to be the first Hub access from this device. In order to identify it for access authorization, you need to name this device.
|
||||
hub.register.message=New Device
|
||||
hub.register.description=This is the first Hub access from this device. Please authorize it using your setup code.
|
||||
hub.register.nameLabel=Device Name
|
||||
hub.register.setupCodeLabel=Setup Code
|
||||
hub.register.invalidSetupCode=Invalid Setup Code
|
||||
hub.register.occupiedMsg=Name already in use
|
||||
hub.register.registerBtn=Confirm
|
||||
### Registration Success
|
||||
|
||||
@@ -41,6 +41,7 @@ traymenu.vault.reveal=إظهار
|
||||
# Add Vault Wizard
|
||||
addvaultwizard.title=أضِف مخزنًا
|
||||
## New
|
||||
addvaultwizard.new.title=إضافة مخزن جديد
|
||||
### Name
|
||||
addvaultwizard.new.nameInstruction=اختر اسم للمخزن
|
||||
addvaultwizard.new.namePrompt=اسم الخزينة
|
||||
@@ -85,11 +86,12 @@ addvault.new.readme.storageLocation.8=2. فتح المخزن باستخدام Cr
|
||||
addvault.new.readme.storageLocation.9=3. افتح موقع الوصول بالنقر على زر "إظهار".
|
||||
addvault.new.readme.storageLocation.10=إذا كنت بحاجة إلى المساعدة، قم بالاطلاع على كتيب الوثائق: %s
|
||||
addvault.new.readme.accessLocation.fileName=مرحبا.rtf
|
||||
addvault.new.readme.accessLocation.1=🔐️ المجلدات المشفرة 🔐️
|
||||
addvault.new.readme.accessLocation.1=🔐️ وحدة التخزين مشفرة 🔐️
|
||||
addvault.new.readme.accessLocation.2=هذا هو موقع الوصول للخرنة الخاص بك.
|
||||
addvault.new.readme.accessLocation.3=سيتم تشفير أي ملفات تضاف إلى هذا المجلد من قبل Cryptomator. يمكنك العمل عليه كأي مجلد آخر. هذه فقط طريقة عرض غير مشفرة لمحتوياتها، ملفاتك تبقى مشفرة على القرص الصلب الخاص بك طوال الوقت.
|
||||
addvault.new.readme.accessLocation.4=لا تتردد في إزالة هذا الملف.
|
||||
## Existing
|
||||
addvaultwizard.existing.title=أضِف مخزن موجود
|
||||
addvaultwizard.existing.instruction=اختر ملف "vault.cryptomator" من خزينتك الحالية. إذا كان يوجد فقط ملف اسمه "masterkey.cryptomator"، اختره عوضاً عنه.
|
||||
addvaultwizard.existing.chooseBtn=اختر…
|
||||
addvaultwizard.existing.filePickerTitle=حدد مِلَفّ المخزن
|
||||
@@ -132,9 +134,10 @@ unlock.success.rememberChoice=تذكر اختياري ولا تظهر هذا م
|
||||
unlock.success.revealBtn=اظهار القرص
|
||||
## Failure
|
||||
unlock.error.customPath.message=غير قادر على تركيب المخزن إلى المسار المخصص
|
||||
unlock.error.customPath.description.notSupported=إذا كنت ترغب في الاستمرار في استخدام المسار المخصص، يرجى الذَّهاب إلى التفضيلات وتحديد نوع واسطة التخزين الذي يدعمها. خلاف ذلك، انتقل إلى خيارات المخزن واختر نقطة تركيب مدعومة.
|
||||
unlock.error.customPath.description.notSupported=إذا كنت ترغب في الاستمرار في استخدام المسار المخصص، يرجى الذَّهاب إلى التفضيلات وتحديد نوع وحدة التخزين\n الذي يدعمها. خلاف ذلك، انتقل إلى خيارات المخزن واختر نقطة تركيب مدعومة.
|
||||
unlock.error.customPath.description.notExists=مسار التركيب المخصص غير موجود. قم بإنشائه في نظام الملفات المحلي الخاص بك أو اختر غيره في خيارات الخزانة.
|
||||
unlock.error.customPath.description.inUse=حرف القرص أو مسار التركيب المخصص "%s" قيد الاستخدام بالفعل.
|
||||
unlock.error.customPath.description.hideawayNotDir=لا يمكن إزالة المِلَفّ المؤقت المخفي "%3$s" المستخدم لفتح القُفْل. الرجاء التحقق من المِلَفّ ثم حذفه يدوياً.
|
||||
unlock.error.customPath.description.couldNotBeCleaned=لا يمكن تركيب خزانتك على المسار "%s". الرجاء المحاولة مرة أخرى أو اختيار مسار مختلف.
|
||||
unlock.error.customPath.description.notEmptyDir=مسار التركيب المخصص "%s" ليس مجلد فارغ. الرجاء اختيار مجلد فارغ وحاول مرة أخرى.
|
||||
unlock.error.customPath.description.generic=لقد اخترت مسار تركيب مخصص لهذه الخزانة، ولكن استخدامه فشل مع الرسالة: %2$s
|
||||
@@ -245,6 +248,11 @@ health.result.severityFilter.info=معلومات
|
||||
health.result.severityFilter.warn=تحذير
|
||||
health.result.severityFilter.crit=حرج
|
||||
health.result.severityTip.good=الخطورة: جيد\nبنية المخزن عادية.
|
||||
health.result.severityTip.info=الخطورة: معلومات\nبنية المخزن سليمة، إصلاح المقترح.
|
||||
health.result.severityTip.warn=الخطورة: تحذير\nبنية المخزن تالفة، ننصح لإصلاح.
|
||||
health.result.severityTip.crit=الخطورة: حرجة\nبنية المخزن تالفة، وتم تحديد فقدان البيانات.
|
||||
health.result.fixStateFilter.all=حالة لإصلاح - الكل
|
||||
health.result.fixStateFilter.fixable=قابلة للإصلاح
|
||||
health.result.fixStateFilter.notFixable=غير قابل للإصلاح
|
||||
health.result.fixStateFilter.fixing=يتم الإصلاح…
|
||||
health.result.fixStateFilter.fixed=تم الإصلاح
|
||||
@@ -259,15 +267,33 @@ preferences.title=تفضيلات
|
||||
## General
|
||||
preferences.general=عام
|
||||
preferences.general.startHidden=إخفاء النافذة عند بدء تشغيل Cryptomator
|
||||
preferences.general.autoCloseVaults=اقفل الخزانات المفتوحة تلقائياً عند الإقلاع عن التطبيق
|
||||
preferences.general.debugLogging=تمكين سجلات التصحيح
|
||||
preferences.general.debugDirectory=عرض ملفات السجل
|
||||
preferences.general.autoStart=تشغيل Cryptomator عند بدء تشغيل النظام
|
||||
preferences.general.keychainBackend=تخزين كلمات المرور مع
|
||||
## Interface
|
||||
preferences.interface=الواجهة
|
||||
preferences.interface.theme=الشكل والمظهر
|
||||
preferences.interface.theme.automatic=تلقائي
|
||||
preferences.interface.theme.dark=مظلم (أسود)
|
||||
preferences.interface.theme.light=فاتح (أبيض)
|
||||
preferences.interface.unlockThemes=تفعيل الوضع الداكن
|
||||
preferences.interface.language=اللغة (يتطلب إعادة التشغيل)
|
||||
preferences.interface.language.auto=النظام الافتراضي
|
||||
preferences.interface.interfaceOrientation=اتجاه الواجهة
|
||||
preferences.interface.interfaceOrientation.ltr=من اليسار إلى اليمين
|
||||
preferences.interface.interfaceOrientation.rtl=من اليمين إلى اليسار
|
||||
preferences.interface.showMinimizeButton=إظهار زر التصغير
|
||||
preferences.interface.showTrayIcon=إظهار أيقونة اللوحة (يتطلب إعادة تشغيل)
|
||||
## Volume
|
||||
preferences.volume=القرص الإفتراضي
|
||||
preferences.volume.type=نوع وحدة التخزين
|
||||
preferences.volume.type.automatic=تلقائي
|
||||
preferences.volume.docsTooltip=افتح الوثائق لمعرفة المزيد عن مختلف أنواع وحدة التخزين.
|
||||
preferences.volume.fuseRestartRequired=لتطبيق التغييرات، يحتاج Cryptomator إلى إعادة التشغيل.
|
||||
preferences.volume.tcp.port=منفذ TCP
|
||||
preferences.volume.supportedFeatures=يدعم نوع وحدة تخزين المختار الميزات التالية:
|
||||
## Updates
|
||||
preferences.updates=تحديثات
|
||||
preferences.updates.currentVersion=الإصدار الحالي: %s
|
||||
@@ -353,8 +379,8 @@ wrongFileAlert.message=هل حاولت تشفير هذه الملفات؟
|
||||
wrongFileAlert.description=لهذا الغرض، بوفر Cryptomator مجلداً في مدير ملفات النظام الخاص بك.
|
||||
wrongFileAlert.instruction.0=لتشفير الملفات، قم باتباع الخطوات التالية:
|
||||
wrongFileAlert.instruction.1=1. فتح المخزن الخاص بك.
|
||||
wrongFileAlert.instruction.2=2. انقر على "إظهار" لفتح المجلد في مدير الملفات الخاص بك.
|
||||
wrongFileAlert.instruction.3=3. أضف ملفاتك إلى هذا المجلد.
|
||||
wrongFileAlert.instruction.2=2. انقر على "إظهار" لفتح وحدة التخزين في مدير ملفاتك.
|
||||
wrongFileAlert.instruction.3=3. أضِف ملفاتك إلى هذه وحدة التخزين.
|
||||
wrongFileAlert.link=لمزيد من المساعدة، قم بزيارة
|
||||
|
||||
# Vault Options
|
||||
|
||||
@@ -153,6 +153,7 @@ hub.invalidLicense.message=Лиценза за Hub е недействителе
|
||||
|
||||
# Lock
|
||||
## Force
|
||||
lock.forced.retryBtn=Повторен опит
|
||||
## Failure
|
||||
|
||||
# Migration
|
||||
|
||||
@@ -366,8 +366,8 @@ main.vaultlist.contextMenu.unlockNow=Jetzt entsperren
|
||||
main.vaultlist.contextMenu.vaultoptions=Tresoroptionen anzeigen
|
||||
main.vaultlist.contextMenu.reveal=Laufwerk anzeigen
|
||||
main.vaultlist.addVaultBtn=Hinzufügen
|
||||
main.vaultlist.addVaultBtn.menuItemNew=Neuer Tresor...
|
||||
main.vaultlist.addVaultBtn.menuItemExisting=Bestehender Tresor...
|
||||
main.vaultlist.addVaultBtn.menuItemNew=Neuer Tresor …
|
||||
main.vaultlist.addVaultBtn.menuItemExisting=Bestehender Tresor …
|
||||
## Vault Detail
|
||||
### Welcome
|
||||
main.vaultDetail.welcomeOnboarding=Danke, dass du zum Schutz deiner Dateien Cryptomator gewählt hast. Falls du Hilfe brauchst, schau dir unsere Anleitungen an:
|
||||
|
||||
@@ -41,6 +41,7 @@ traymenu.vault.reveal=Revelar
|
||||
# Add Vault Wizard
|
||||
addvaultwizard.title=Añadir bóveda
|
||||
## New
|
||||
addvaultwizard.new.title=Añadir nueva bóveda
|
||||
### Name
|
||||
addvaultwizard.new.nameInstruction=Elegir un nombre para la bóveda
|
||||
addvaultwizard.new.namePrompt=Nombre de la bóveda
|
||||
@@ -90,6 +91,7 @@ addvault.new.readme.accessLocation.2=Este es el lugar de acceso de la bóveda.
|
||||
addvault.new.readme.accessLocation.3=Los archivos agregados a este volumen serán cifrados por Cryptomator. Se puede trabajar con ellos como en cualquier otra unidad/carpeta. Esta es sólo una vista descifrada del contenido, sus archivos permanecen cifrados en el disco duro todo el tiempo.
|
||||
addvault.new.readme.accessLocation.4=No dude en eliminar este archivo.
|
||||
## Existing
|
||||
addvaultwizard.existing.title=Añadir bóveda existente
|
||||
addvaultwizard.existing.instruction=Elija el archivo "vault.cryptomator" de su bóveda existente. Si solo existe un archivo llamado "masterkey.cryptomator", selecciónelo en su lugar.
|
||||
addvaultwizard.existing.chooseBtn=Elegir…
|
||||
addvaultwizard.existing.filePickerTitle=Seleccionar archivo de bóveda
|
||||
@@ -364,6 +366,8 @@ main.vaultlist.contextMenu.unlockNow=Desbloquear ahora
|
||||
main.vaultlist.contextMenu.vaultoptions=Mostrar opciones de la bóveda
|
||||
main.vaultlist.contextMenu.reveal=Revelar unidad
|
||||
main.vaultlist.addVaultBtn=Añadir
|
||||
main.vaultlist.addVaultBtn.menuItemNew=Nueva bóveda...
|
||||
main.vaultlist.addVaultBtn.menuItemExisting=Bóveda existente...
|
||||
## Vault Detail
|
||||
### Welcome
|
||||
main.vaultDetail.welcomeOnboarding=Gracias por elegir Cryptomator para proteger sus archivos. En caso de necesitar ayuda, revise nuestras guías:
|
||||
|
||||
@@ -20,6 +20,10 @@ error.description=Oops! Hindi inaasahan ng Cryptomator na ito'y mangyari. Maaari
|
||||
error.hyperlink.lookup=Hanapin ang solusyon
|
||||
error.hyperlink.report=I-report ang problema
|
||||
error.technicalDetails=Mga detalye:
|
||||
error.existingSolutionDescription=Hindi inaasahan ng Cryptomator na mangyayari ito. Ngunit nakakita kami ng kasalukuyang solusyon para sa error na ito. Mangyaring tingnan ang sumusunod na link.
|
||||
error.hyperlink.solution=Hanapin ang solusyon
|
||||
error.lookupPermissionMessage=Maaaring maghanap ng solusyon ang Cryptomator para sa problemang ito online. Magpapadala ito ng kahilingan sa aming database ng problema mula sa iyong IP address.
|
||||
error.dismiss=I-dismiss
|
||||
error.lookUpSolution=Itignan ang solusyon
|
||||
|
||||
# Defaults
|
||||
@@ -37,6 +41,7 @@ traymenu.vault.reveal=Ipakita
|
||||
# Add Vault Wizard
|
||||
addvaultwizard.title=Magdagdag ng Vault
|
||||
## New
|
||||
addvaultwizard.new.title=Magdagdag ng Bagong Vault
|
||||
### Name
|
||||
addvaultwizard.new.nameInstruction=Pangalanan ang vault
|
||||
addvaultwizard.new.namePrompt=Pangalan ng Vault
|
||||
@@ -47,34 +52,78 @@ addvaultwizard.new.locationPrompt=…
|
||||
addvaultwizard.new.directoryPickerLabel=Custom Location
|
||||
addvaultwizard.new.directoryPickerButton=Mamili…
|
||||
addvaultwizard.new.directoryPickerTitle=Pumili ng Direktoryo
|
||||
addvaultwizard.new.fileAlreadyExists=Mayroon nang file o direktoryo na may pangalan ng vault
|
||||
addvaultwizard.new.locationDoesNotExist=Ang isang direktoryo sa tinukoy na landas ay hindi umiiral o hindi ma-access
|
||||
addvaultwizard.new.locationIsNotWritable=Walang access sa pagsulat sa tinukoy na landas
|
||||
addvaultwizard.new.locationIsOk=Angkop na lokasyon para sa iyong vault
|
||||
addvaultwizard.new.invalidName=Di-wastong pangalan ng vault
|
||||
addvaultwizard.new.validName=Wastong pangalan ng vault
|
||||
addvaultwizard.new.validCharacters.message=Maaaring naglalaman ang pangalan ng vault ng mga sumusunod na character:
|
||||
addvaultwizard.new.validCharacters.chars=Mga character ng salita (hal. a, ж o 수)
|
||||
addvaultwizard.new.validCharacters.numbers=Numero
|
||||
addvaultwizard.new.validCharacters.dashes=Hyphen (%s) o underscore (%s)
|
||||
### Expert Settings
|
||||
addvaultwizard.new.expertSettings.enableExpertSettingsCheckbox=Paganahin ang mga setting ng eksperto
|
||||
addvaultwizard.new.expertSettings.shorteningThreshold.invalid=Maglagay ng value sa pagitan ng 36 at 220 (default 220)
|
||||
addvaultwizard.new.expertSettings.shorteningThreshold.tooltip=Ibukas ang dokumentasyon para matuto pa.
|
||||
addvaultwizard.new.expertSettings.shorteningThreshold.title=Pinakamataas na haba ng mga naka-encrypt na pangalan ng file
|
||||
addvaultwizard.new.expertSettings.shorteningThreshold.valid=Wasto
|
||||
### Password
|
||||
addvaultwizard.new.createVaultBtn=Gumawa ng bagong Vault
|
||||
addvaultwizard.new.generateRecoveryKeyChoice=Hindi mo maa-access ang iyong data nang wala ang iyong password. Gusto mo ba ng recovery key para sa kaso na nawala mo ang iyong password?
|
||||
addvaultwizard.new.generateRecoveryKeyChoice.yes=Oo pakiusap, mas ligtas kaysa sorry
|
||||
addvaultwizard.new.generateRecoveryKeyChoice.no=Hindi, salamat, hindi ko mawawala ang aking password
|
||||
### Information
|
||||
addvault.new.readme.storageLocation.fileName=MAHALAGA.rtf
|
||||
addvault.new.readme.storageLocation.1=⚠️ VAULT FILES ⚠️
|
||||
addvault.new.readme.storageLocation.2=Ito ang lokasyon ng iyong vault na imbakan.
|
||||
addvault.new.readme.storageLocation.3=HUWAG
|
||||
addvault.new.readme.storageLocation.4=• baguhin ang anumang mga file sa loob ng direktoryong ito o
|
||||
addvault.new.readme.storageLocation.5=• i-paste ang anumang mga file para sa pag-encrypt sa direktoryong ito.
|
||||
addvault.new.readme.storageLocation.6=Kung gusto mo i-encrypt ang mga files at makita nang nilalaman ng vault, gawin ang nakasabi:
|
||||
addvault.new.readme.storageLocation.7=1. Idagdag ang vault na ito sa Cryptomator.
|
||||
addvault.new.readme.storageLocation.8=2. I-unlock ang vault sa Cryptomator.
|
||||
addvault.new.readme.storageLocation.9=3. Buksan ang lokasyon ng pag-access sa pamamagitan ng pag-click sa pindutang "Ibunyag".
|
||||
addvault.new.readme.storageLocation.10=Kung kailangan mo ng tulong, bisitahin ang dokumentasyon: %s
|
||||
addvault.new.readme.accessLocation.fileName=WELCOME.rtf
|
||||
addvault.new.readme.accessLocation.1=🔐️ Naka-encrypt na VOLUME 🔐️
|
||||
addvault.new.readme.accessLocation.2=Ito ang lokasyon ng iyong vault.
|
||||
addvault.new.readme.accessLocation.3=Ang anumang mga file na idinagdag sa volume na ito ay ie-encrypt ng Cryptomator. Maaari mong gawin ito tulad ng sa anumang iba pang drive/folder. Ito ay isang decrypted view lamang ng nilalaman nito, ang iyong mga file ay mananatiling naka-encrypt sa iyong hard drive sa lahat ng oras.
|
||||
addvault.new.readme.accessLocation.4=Huwag mag-atubiling tanggalin ang file na ito.
|
||||
## Existing
|
||||
addvaultwizard.existing.title=Magdagdag ng Umiiral na Vault
|
||||
addvaultwizard.existing.instruction=Piliin ang "vault.cryptomator" file ng iyong kasalukuyang vault. Kung mayroon lamang file na may pangalang "masterkey.cryptomator," piliin iyon sa halip.
|
||||
addvaultwizard.existing.chooseBtn=Mamili…
|
||||
addvaultwizard.existing.filePickerTitle=Piliin ang Vault File
|
||||
addvaultwizard.existing.filePickerMimeDesc=Cryptomator Vault
|
||||
## Success
|
||||
addvaultwizard.success.nextStepsInstructions=Idinagdag ang vault na "%s".\nKailangan mong i-unlock ang vault na ito para ma-access o magdagdag ng mga content. Bilang kahalili, maaari mo itong i-unlock sa anumang susunod na oras.
|
||||
addvaultwizard.success.unlockNow=I-unlock Ngayon
|
||||
|
||||
# Remove Vault
|
||||
removeVault.title=Tanggalin %s
|
||||
removeVault.message=Itangal ang vault?
|
||||
removeVault.description=Makakalimutan lang nito ang Cryptomator tungkol sa vault na ito. Maaari mo itong idagdag muli. Walang matatanggal na mga naka-encrypt na file mula sa iyong hard drive.
|
||||
removeVault.confirmBtn=Itangal ang vault
|
||||
|
||||
# Change Password
|
||||
changepassword.title=Palitan ANG password
|
||||
changepassword.enterOldPassword=Ilagay ang kasalukuyang password para sa "%s"
|
||||
changepassword.finalConfirmation=Naiintindihan ko na hindi ko maa-access ang aking data kung nakalimutan ko ang aking password
|
||||
|
||||
# Forget Password
|
||||
forgetPassword.title=Kalimutan ang Password
|
||||
forgetPassword.message=Nakalimutan ang iyong password?
|
||||
forgetPassword.description=Tatanggalin nito ang naka-save na password ng vault na ito mula sa keychain ng iyong system.
|
||||
forgetPassword.confirmBtn=Kalimutan ang Password
|
||||
|
||||
# Unlock
|
||||
unlock.title=I-unlock ang "%s"
|
||||
unlock.passwordPrompt=Ipasok ang password para sa "%s":
|
||||
unlock.savePassword=Maalala ang password
|
||||
unlock.unlockBtn=I-unlock
|
||||
## Select
|
||||
unlock.chooseMasterkey.message=Hindi nahanap ang masterkey file
|
||||
unlock.chooseMasterkey.filePickerTitle=Piliin ang masterkey file
|
||||
unlock.chooseMasterkey.filePickerMimeDesc=Cryptomator Masterkey
|
||||
## Success
|
||||
@@ -100,6 +149,7 @@ lock.forced.retryBtn=Subukan muli
|
||||
## Start
|
||||
## Run
|
||||
## Success
|
||||
migration.success.unlockNow=I-unlock Ngayon
|
||||
## Missing file system capabilities
|
||||
## Impossible
|
||||
|
||||
@@ -136,9 +186,12 @@ main.closeBtn.tooltip=Isara
|
||||
main.preferencesBtn.tooltip=Mga Kagustuhan
|
||||
## Vault List
|
||||
main.vaultlist.contextMenu.lock=I-lock
|
||||
main.vaultlist.contextMenu.unlockNow=I-unlock Ngayon
|
||||
main.vaultlist.addVaultBtn=Idagdag
|
||||
## Vault Detail
|
||||
### Welcome
|
||||
### Locked
|
||||
main.vaultDetail.unlockNowBtn=I-unlock Ngayon
|
||||
### Unlocked
|
||||
main.vaultDetail.lockBtn=I-lock
|
||||
### Missing
|
||||
@@ -154,6 +207,7 @@ vaultOptions.general.vaultName=Pangalan ng Vault
|
||||
## Mount
|
||||
vaultOptions.mount.mountPoint.directoryPickerButton=Mamili…
|
||||
## Master Key
|
||||
vaultOptions.masterkey.changePasswordBtn=Palitan ANG password
|
||||
## Hub
|
||||
|
||||
# Recovery Key
|
||||
|
||||
@@ -41,6 +41,7 @@ traymenu.vault.reveal=表示
|
||||
# Add Vault Wizard
|
||||
addvaultwizard.title=金庫を追加
|
||||
## New
|
||||
addvaultwizard.new.title=新しい金庫を追加
|
||||
### Name
|
||||
addvaultwizard.new.nameInstruction=金庫の名前を入力してください
|
||||
addvaultwizard.new.namePrompt=金庫の名前
|
||||
@@ -90,6 +91,7 @@ addvault.new.readme.accessLocation.2=ここは金庫のアクセス先です。
|
||||
addvault.new.readme.accessLocation.3=このボリュームに追加したファイルは Cryptomator によって暗号化されます。一般的なドライブ/フォルダー上のように作業することができます。ここでは復号したコンテンツのビューにすぎず、ファイルは常にハードドライブ上で暗号化されています。
|
||||
addvault.new.readme.accessLocation.4=このファイルはいつでも削除できます。
|
||||
## Existing
|
||||
addvaultwizard.existing.title=既存の金庫を追加
|
||||
addvaultwizard.existing.instruction=すでにある金庫の "vault.cryptomator" ファイルを選択してください。"masterkey.cryptomator" という名前のファイルだけが存在する場合は、このファイルを代わりに選択してください。
|
||||
addvaultwizard.existing.chooseBtn=選択...
|
||||
addvaultwizard.existing.filePickerTitle=金庫のファイルを選択
|
||||
@@ -364,6 +366,8 @@ main.vaultlist.contextMenu.unlockNow=今すぐ解錠
|
||||
main.vaultlist.contextMenu.vaultoptions=金庫のオプションを表示
|
||||
main.vaultlist.contextMenu.reveal=ドライブを表示
|
||||
main.vaultlist.addVaultBtn=追加
|
||||
main.vaultlist.addVaultBtn.menuItemNew=新しい金庫...
|
||||
main.vaultlist.addVaultBtn.menuItemExisting=既存の金庫...
|
||||
## Vault Detail
|
||||
### Welcome
|
||||
main.vaultDetail.welcomeOnboarding=ファイルを保護するために Cryptomator を選んでいただきありがとうございます。ヘルプが必要であれば、スタートガイドをご覧ください:
|
||||
|
||||
@@ -41,6 +41,7 @@ traymenu.vault.reveal=Gjør synlig
|
||||
# Add Vault Wizard
|
||||
addvaultwizard.title=Legg til hvelv
|
||||
## New
|
||||
addvaultwizard.new.title=Legg til nytt hvelv
|
||||
### Name
|
||||
addvaultwizard.new.nameInstruction=Navngi hvelvet
|
||||
addvaultwizard.new.namePrompt=Navn på hvelvet
|
||||
@@ -90,6 +91,7 @@ addvault.new.readme.accessLocation.2=Dette er hvelvets lagringssted.
|
||||
addvault.new.readme.accessLocation.3=Alle filer som er lagt til i dette volumet, blir kryptert av Cryptomator. Du kan jobbe med dem som på alle andre enheter/mapper. Dette er bare en dekryptert visning av innholdet, filene dine forblir kryptert på harddisken hele tiden.
|
||||
addvault.new.readme.accessLocation.4=Denne filen kan fjernes hvis ønskelig.
|
||||
## Existing
|
||||
addvaultwizard.existing.title=Legg til et eksisterende hvelv
|
||||
addvaultwizard.existing.instruction=Velg "vault.cryptomator"-filen til ditt eksisterende hvelv. Hvis det kun finnes en fil med navnet "masterkey.cryptomator", så velger du den i stedet.
|
||||
addvaultwizard.existing.chooseBtn=Velg…
|
||||
addvaultwizard.existing.filePickerTitle=Velg hvelvfil
|
||||
@@ -364,6 +366,8 @@ main.vaultlist.contextMenu.unlockNow=Lås opp nå
|
||||
main.vaultlist.contextMenu.vaultoptions=Alternativer for hvelvet
|
||||
main.vaultlist.contextMenu.reveal=Vis enheten
|
||||
main.vaultlist.addVaultBtn=Legg til
|
||||
main.vaultlist.addVaultBtn.menuItemNew=Nytt hvelv...
|
||||
main.vaultlist.addVaultBtn.menuItemExisting=Eksisterende hvelv...
|
||||
## Vault Detail
|
||||
### Welcome
|
||||
main.vaultDetail.welcomeOnboarding=Takk for at du valgte Cryptomator for å beskytte filene dine. Hvis du trenger hjelp, sjekk ut våre guider for å komme i gang:
|
||||
|
||||
@@ -137,6 +137,7 @@ unlock.error.customPath.message=Nie można zamontować sejfu na niestandardowej
|
||||
unlock.error.customPath.description.notSupported=Jeśli chcesz nadal używać niestandardowej ścieżki, przejdź do preferencji i wybierz typ udziału, który ją obsługuje. W przeciwnym razie przejdź do opcji sejfu i wybierz obsługiwany punkt montowania.
|
||||
unlock.error.customPath.description.notExists=Niestandardowa ścieżka montowania nie istnieje. Stwórz ją w lokalnym systemie plików lub zmień ją w opcjach sejfu.
|
||||
unlock.error.customPath.description.inUse=Litera dysku lub podana ścieżka montowania "%s" jest już w użyciu.
|
||||
unlock.error.customPath.description.hideawayNotDir=Nie można usunąć ukrytego pliku tymczasowego "%3$s", używanego do odblokowywania. Sprawdź plik, po czym usuń go ręcznie.
|
||||
unlock.error.customPath.description.couldNotBeCleaned=Twój sejf nie mógł być zamontowany do ścieżki "%s". Spróbuj ponownie lub wybierz inną ścieżkę.
|
||||
unlock.error.customPath.description.notEmptyDir=Wybrana ścieżka montowania "%s" nie jest pustym katalogiem. Wybierz pusty katalog i spróbuj ponownie.
|
||||
unlock.error.customPath.description.generic=Wybrałeś własną ścieżkę montowania dla tego sejfu, ale użycie jej nie powiodło się. Powód: %2$s
|
||||
|
||||
@@ -41,6 +41,7 @@ traymenu.vault.reveal=Exibir
|
||||
# Add Vault Wizard
|
||||
addvaultwizard.title=Adicionar Cofre
|
||||
## New
|
||||
addvaultwizard.new.title=Adicionar Novo Cofre
|
||||
### Name
|
||||
addvaultwizard.new.nameInstruction=Escolha um nome para o cofre
|
||||
addvaultwizard.new.namePrompt=Nome do Cofre
|
||||
@@ -90,6 +91,7 @@ addvault.new.readme.accessLocation.2=Este é o local de acesso ao seu cofre.
|
||||
addvault.new.readme.accessLocation.3=Todos os arquivos adicionados a este volume serão criptografados de forma transparente pelo Cryptomator. Você pode trabalhar nele como se fosse qualquer unidade/pasta. Esta é apenas uma visão não criptografada do seu conteúdo, mas seus arquivos permanecem criptografados na sua unidade o tempo todo.
|
||||
addvault.new.readme.accessLocation.4=Fique à vontade para apagar este arquivo.
|
||||
## Existing
|
||||
addvaultwizard.existing.title=Adicionar Cofre Existente
|
||||
addvaultwizard.existing.instruction=Escolha o arquivo "vault.cryptomator" do seu cofre existente. Se existir apenas um arquivo chamado "masterkey.cryptomator", selecione outro.
|
||||
addvaultwizard.existing.chooseBtn=Selecionar…
|
||||
addvaultwizard.existing.filePickerTitle=Selecionar arquivo do Cofre
|
||||
@@ -364,6 +366,8 @@ main.vaultlist.contextMenu.unlockNow=Desbloquear Agora
|
||||
main.vaultlist.contextMenu.vaultoptions=Exibir Opções de Cofre
|
||||
main.vaultlist.contextMenu.reveal=Revelar Volume
|
||||
main.vaultlist.addVaultBtn=Adicionar
|
||||
main.vaultlist.addVaultBtn.menuItemNew=Novo Cofre...
|
||||
main.vaultlist.addVaultBtn.menuItemExisting=Cofre Existente...
|
||||
## Vault Detail
|
||||
### Welcome
|
||||
main.vaultDetail.welcomeOnboarding=Obrigado por escolher o Cryptomator para proteger seus arquivos. Se precisar de ajuda, confira nossos guias de introdução:
|
||||
|
||||
@@ -41,6 +41,7 @@ traymenu.vault.reveal=Visa
|
||||
# Add Vault Wizard
|
||||
addvaultwizard.title=Lägg till valv
|
||||
## New
|
||||
addvaultwizard.new.title=Lägg till nytt valv
|
||||
### Name
|
||||
addvaultwizard.new.nameInstruction=Ange namn för valvet
|
||||
addvaultwizard.new.namePrompt=Valvnamn
|
||||
@@ -90,6 +91,7 @@ addvault.new.readme.accessLocation.2=Detta är ditt valvs åtkomstplats.
|
||||
addvault.new.readme.accessLocation.3=Alla filer som läggs till denna volym kommer att krypteras av Cryptomator. Du kan arbeta med dem som i vilken annan enhet/mapp som helst. Detta är bara en dekrypterad vy av dess innehåll, dina filer förblir krypterade på din hårddisk hela tiden.
|
||||
addvault.new.readme.accessLocation.4=Du kan ta bort denna fil.
|
||||
## Existing
|
||||
addvaultwizard.existing.title=Öppna befintligt valv
|
||||
addvaultwizard.existing.instruction=Välj filen "vault.cryptomator" i ditt befintliga valv. Om det endast finns en fil som heter "masterkey.cryptomator", välj den istället.
|
||||
addvaultwizard.existing.chooseBtn=Välj…
|
||||
addvaultwizard.existing.filePickerTitle=Välj valvfil
|
||||
@@ -364,6 +366,8 @@ main.vaultlist.contextMenu.unlockNow=Lås upp nu
|
||||
main.vaultlist.contextMenu.vaultoptions=Visa inställningar för valv
|
||||
main.vaultlist.contextMenu.reveal=Visa enhet
|
||||
main.vaultlist.addVaultBtn=Lägg till
|
||||
main.vaultlist.addVaultBtn.menuItemNew=Nytt valv...
|
||||
main.vaultlist.addVaultBtn.menuItemExisting=Öppna befintligt valv...
|
||||
## Vault Detail
|
||||
### Welcome
|
||||
main.vaultDetail.welcomeOnboarding=Tack för att du väljer Cryptomator för att skydda dina filer. Om du behöver hjälp kan du kolla in våra guider:
|
||||
|
||||
@@ -20,6 +20,8 @@ error.description=Cryptomator не очікував, що таке трапит
|
||||
error.hyperlink.lookup=Дізнатися більше про цю помилку
|
||||
error.hyperlink.report=Повідомити про помилку
|
||||
error.technicalDetails=Подробиці:
|
||||
error.hyperlink.solution=Знайдіть рішення
|
||||
error.dismiss=Відхилити
|
||||
|
||||
# Defaults
|
||||
defaults.vault.vaultName=Сховище
|
||||
@@ -36,6 +38,7 @@ traymenu.vault.reveal=Показати
|
||||
# Add Vault Wizard
|
||||
addvaultwizard.title=Додати сховище
|
||||
## New
|
||||
addvaultwizard.new.title=Додайте нове сховище
|
||||
### Name
|
||||
addvaultwizard.new.nameInstruction=Оберіть назву для сховища
|
||||
addvaultwizard.new.namePrompt=Назва сховища
|
||||
@@ -57,6 +60,7 @@ addvaultwizard.new.validCharacters.chars=Літери (напр. a, ж or 수)
|
||||
addvaultwizard.new.validCharacters.numbers=Цифри
|
||||
addvaultwizard.new.validCharacters.dashes=Дефіс (%s) або підкреслення (%s)
|
||||
### Expert Settings
|
||||
addvaultwizard.new.expertSettings.shorteningThreshold.tooltip=Відкрийте документацію, щоб дізнатися більше.
|
||||
### Password
|
||||
addvaultwizard.new.createVaultBtn=Створити сховище
|
||||
addvaultwizard.new.generateRecoveryKeyChoice=Ви не зможете отримати доступ до своїх даних, якщо втратите пароль. Хочете створити ключ відновлення на випадок втрати паролю?
|
||||
@@ -80,6 +84,7 @@ addvault.new.readme.accessLocation.2=Це місце розташування в
|
||||
addvault.new.readme.accessLocation.3=Будь-які файли, додані до цього тому, будуть зашифровані за допомогою Cryptomator. Ви можете працювати із ним як із будь-якою іншою директорією або накопичувачем. Це лише розшифрований вигляд його вмісту, ваші файли завжди знаходяться в зашифрованому вигляді на диску.
|
||||
addvault.new.readme.accessLocation.4=Якщо хочете, то можете видалити цей файл.
|
||||
## Existing
|
||||
addvaultwizard.existing.title=Додати існуюче сховище
|
||||
addvaultwizard.existing.instruction=Виберіть файл "vault.cryptomator" у вашому існуючому сховищі. Якщо існує лише файл з назвою "masterkey.cryptomator", виберіть його.
|
||||
addvaultwizard.existing.chooseBtn=Обрати…
|
||||
addvaultwizard.existing.filePickerTitle=Виберіть Vault Файл
|
||||
@@ -179,6 +184,7 @@ migration.error.missingFileSystemCapabilities.title=Непідтримувана
|
||||
# Health Check
|
||||
## Start
|
||||
## Start Failure
|
||||
health.fail.moreInfo=Більше інформації
|
||||
## Check Selection
|
||||
## Detail view
|
||||
health.check.detail.checkRunning=Перевірка триває…
|
||||
|
||||
@@ -41,6 +41,7 @@ traymenu.vault.reveal=显示
|
||||
# Add Vault Wizard
|
||||
addvaultwizard.title=添加保险库
|
||||
## New
|
||||
addvaultwizard.new.title=添加新保险库
|
||||
### Name
|
||||
addvaultwizard.new.nameInstruction=为保险库创建一个名称
|
||||
addvaultwizard.new.namePrompt=保险库名称
|
||||
@@ -90,6 +91,7 @@ addvault.new.readme.accessLocation.2=这是您的保险库的访问路径。
|
||||
addvault.new.readme.accessLocation.3=任何添加到此卷的文件都将被 Cryptomator 加密。您可以像在一般磁盘/文件夹上那样操作它。 这只是对其内容的解密查看,您的文件会在硬盘上一直保持加密。
|
||||
addvault.new.readme.accessLocation.4=您可以随时删除此文件。
|
||||
## Existing
|
||||
addvaultwizard.existing.title=添加现有保险库
|
||||
addvaultwizard.existing.instruction=请选择现有保险库中的“vault.cryptomator”文件。如果只有一个名为“masterkey.cryptomator”的文件,则改为选择该文件。
|
||||
addvaultwizard.existing.chooseBtn=选择...
|
||||
addvaultwizard.existing.filePickerTitle=请选择保险库对应文件
|
||||
@@ -364,6 +366,8 @@ main.vaultlist.contextMenu.unlockNow=立即解锁
|
||||
main.vaultlist.contextMenu.vaultoptions=显示保险库选项
|
||||
main.vaultlist.contextMenu.reveal=显示驱动器
|
||||
main.vaultlist.addVaultBtn=添加
|
||||
main.vaultlist.addVaultBtn.menuItemNew=新保险库...
|
||||
main.vaultlist.addVaultBtn.menuItemExisting=现有保险库...
|
||||
## Vault Detail
|
||||
### Welcome
|
||||
main.vaultDetail.welcomeOnboarding=感谢您使用 Cryptomator 来保护您的文件。如果您需要任何帮助,请查看我们的快速开始指南:
|
||||
|
||||
@@ -22,6 +22,9 @@ error.hyperlink.report=回報錯誤
|
||||
error.technicalDetails=詳情:
|
||||
error.existingSolutionDescription=Cryptomator 沒有預料到會發生這種情況。但我們找到了一個現有的解決方案來解決這個錯誤。請查看以下連結。
|
||||
error.hyperlink.solution=查詢解決方案
|
||||
error.lookupPermissionMessage=Cryptomator 可以在線查找此問題的解決方案。 這將從您的 IP 地址向我們的問題數據庫發送請求。
|
||||
error.dismiss=忽略
|
||||
error.lookUpSolution=查詢解決方案
|
||||
|
||||
# Defaults
|
||||
defaults.vault.vaultName=加密檔案庫
|
||||
@@ -134,6 +137,7 @@ unlock.error.customPath.message=無法將檔案庫掛載至自訂路徑
|
||||
unlock.error.customPath.description.notSupported=如果要繼續使用自訂的掛載路徑,必須變更成支援的磁區空間類型,不然就必須使用不同的掛載路徑
|
||||
unlock.error.customPath.description.notExists=自訂的掛載路徑並不存在‧ 請在本機創立該路徑,或者在加密庫選項中更改
|
||||
unlock.error.customPath.description.inUse=磁碟代號或自訂掛載路徑「%s」已被使用。
|
||||
unlock.error.customPath.description.hideawayNotDir=無法移除用於解鎖的臨時隱藏檔案「%3$s」。請檢查該檔案,然後手動刪除。
|
||||
unlock.error.customPath.description.couldNotBeCleaned=無法將您的保險庫掛載至路徑「%s」。請再試一次或選擇不同的路徑。
|
||||
unlock.error.customPath.description.notEmptyDir=自訂掛載路徑「%s」不是一個空資料夾。請選擇一個空資料夾並重試。
|
||||
unlock.error.customPath.description.generic=您為此保險庫選擇了自訂掛載路徑,但使用時出現了錯誤訊息:%2$s
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.nimbusds.jose.JWEObject;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
import org.cryptomator.cryptolib.common.P384KeyPair;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
@@ -15,18 +16,106 @@ import java.text.ParseException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
|
||||
@SuppressWarnings("resource")
|
||||
public class JWEHelperTest {
|
||||
|
||||
private static final String JWE = "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IllUcEY3bGtTc3JvZVVUVFdCb21LNzBTN0FhVTJyc0ptMURpZ1ZzbjRMY2F5eUxFNFBabldkYmFVcE9jQVV5a1ciLCJ5IjoiLU5pS3loUktjSk52Nm02Z0ZJUWc4cy1Xd1VXUW9uT3A5dkQ4cHpoa2tUU3U2RzFlU2FUTVlhZGltQ2Q4V0ExMSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..BECWGzd9UvhHcTJC.znt4TlS-qiNEjxiu2v-du_E1QOBnyBR6LCt865SHxD-kwRc1JwX_Lq9XVoFj2GnK9-9CgxhCLGurg5Jt9g38qv2brGAzWL7eSVeY1fIqdO_kUhLpGslRTN6h2U0NHJi2-iE.WDVI2kOk9Dy3PWHyIg8gKA";
|
||||
// key pairs from frontend tests (crypto.spec.ts):
|
||||
private static final String USER_PRIV_KEY = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDCi4K1Ts3DgTz/ufkLX7EGMHjGpJv+WJmFgyzLwwaDFSfLpDw0Kgf3FKK+LAsV8r+hZANiAARLOtFebIjxVYUmDV09Q1sVxz2Nm+NkR8fu6UojVSRcCW13tEZatx8XGrIY9zC7oBCEdRqDc68PMSvS5RA0Pg9cdBNc/kgMZ1iEmEv5YsqOcaNADDSs0bLlXb35pX7Kx5Y=";
|
||||
private static final String USER_PUB_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAESzrRXmyI8VWFJg1dPUNbFcc9jZvjZEfH7ulKI1UkXAltd7RGWrcfFxqyGPcwu6AQhHUag3OvDzEr0uUQND4PXHQTXP5IDGdYhJhL+WLKjnGjQAw0rNGy5V29+aV+yseW";
|
||||
private static final String DEVICE_PRIV_KEY = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDB2bmFCWy2p+EbAn8NWS5Om+GA7c5LHhRZb8g2pSMSf0fsd7k7dZDVrnyHFiLdd/YGhZANiAAR6bsjTEdXKWIuu1Bvj6Y8wySlIROy7YpmVZTY128ItovCD8pcR4PnFljvAIb2MshCdr1alX4g6cgDOqcTeREiObcSfucOU9Ry1pJ/GnX6KA0eSljrk6rxjSDos8aiZ6Mg=";
|
||||
private static final String DEVICE_PUB_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEem7I0xHVyliLrtQb4+mPMMkpSETsu2KZlWU2NdvCLaLwg/KXEeD5xZY7wCG9jLIQna9WpV+IOnIAzqnE3kRIjm3En7nDlPUctaSfxp1+igNHkpY65Oq8Y0g6LPGomejI";
|
||||
|
||||
// used for JWE generation in frontend: (jwe.spec.ts):
|
||||
private static final String PRIV_KEY = "ME8CAQAwEAYHKoZIzj0CAQYFK4EEACIEODA2AgEBBDEA6QybmBitf94veD5aCLr7nlkF5EZpaXHCfq1AXm57AKQyGOjTDAF9EQB28fMywTDQ";
|
||||
private static final String PUB_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERxQR+NRN6Wga01370uBBzr2NHDbKIC56tPUEq2HX64RhITGhii8Zzbkb1HnRmdF0aq6uqmUy4jUhuxnKxsv59A6JeK7Unn+mpmm3pQAygjoGc9wrvoH4HWJSQYUlsXDu";
|
||||
|
||||
@Test
|
||||
public void testDecrypt() throws ParseException, InvalidKeySpecException {
|
||||
var jwe = JWEObject.parse(JWE);
|
||||
var keyPair = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(PRIV_KEY)));
|
||||
@DisplayName("decryptUserKey with device key")
|
||||
public void testDecryptUserKeyECDHES() throws ParseException, InvalidKeySpecException {
|
||||
var jwe = JWEObject.parse("""
|
||||
eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrZXlfb3BzIjpbXSwiZXh0Ijp\
|
||||
0cnVlLCJrdHkiOiJFQyIsIngiOiJoeHpiSWh6SUJza3A5ZkZFUmJSQ2RfOU1fbWYxNElqaDZhcnNoVX\
|
||||
NkcEEyWno5ejZZNUs4NHpZR2I4b2FHemNUIiwieSI6ImJrMGRaNWhpelZ0TF9hN2hNejBjTUduNjhIR\
|
||||
jZFdWlyNHdlclNkTFV5QWd2NWUzVzNYSG5sdHJ2VlRyU3pzUWYiLCJjcnYiOiJQLTM4NCJ9LCJhcHUi\
|
||||
OiIiLCJhcHYiOiIifQ..pu3Q1nR_yvgRAapG.4zW0xm0JPxbcvZ66R-Mn3k841lHelDQfaUvsZZAtWs\
|
||||
L2w4FMi6H_uu6ArAWYLtNREa_zfcPuyuJsFferYPSNRUWt4OW6aWs-l_wfo7G1ceEVxztQXzQiwD30U\
|
||||
TA8OOdPcUuFfEq2-d9217jezrcyO6m6FjyssEZIrnRArUPWKzGdghXccGkkf0LTZcGJoHeKal-RtyP8\
|
||||
PfvEAWTjSOCpBlSdUJ-1JL3tyd97uVFNaVuH3i7vvcMoUP_bdr0XW3rvRgaeC6X4daPLUvR1hK5Msut\
|
||||
QMtM2vpFghS_zZxIQRqz3B2ECxa9Bjxhmn8kLX5heZ8fq3lH-bmJp1DxzZ4V1RkWk.yVwXG9yARa5Ih\
|
||||
q2koh2NbQ""");
|
||||
var deviceKeyPair = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(DEVICE_PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(DEVICE_PRIV_KEY)));
|
||||
|
||||
var masterkey = JWEHelper.decrypt(jwe, keyPair.getPrivate());
|
||||
var userKey = JWEHelper.decryptUserKey(jwe, deviceKeyPair.getPrivate());
|
||||
|
||||
Assertions.assertArrayEquals(Base64.getDecoder().decode(USER_PRIV_KEY), userKey.getEncoded());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("decryptUserKey with incorrect device key")
|
||||
public void testDecryptUserKeyECDHESWrongKey() throws ParseException, InvalidKeySpecException {
|
||||
var jwe = JWEObject.parse("""
|
||||
eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrZXlfb3BzIjpbXSwiZXh0Ijp\
|
||||
0cnVlLCJrdHkiOiJFQyIsIngiOiJoeHpiSWh6SUJza3A5ZkZFUmJSQ2RfOU1fbWYxNElqaDZhcnNoVX\
|
||||
NkcEEyWno5ejZZNUs4NHpZR2I4b2FHemNUIiwieSI6ImJrMGRaNWhpelZ0TF9hN2hNejBjTUduNjhIR\
|
||||
jZFdWlyNHdlclNkTFV5QWd2NWUzVzNYSG5sdHJ2VlRyU3pzUWYiLCJjcnYiOiJQLTM4NCJ9LCJhcHUi\
|
||||
OiIiLCJhcHYiOiIifQ..pu3Q1nR_yvgRAapG.4zW0xm0JPxbcvZ66R-Mn3k841lHelDQfaUvsZZAtWs\
|
||||
L2w4FMi6H_uu6ArAWYLtNREa_zfcPuyuJsFferYPSNRUWt4OW6aWs-l_wfo7G1ceEVxztQXzQiwD30U\
|
||||
TA8OOdPcUuFfEq2-d9217jezrcyO6m6FjyssEZIrnRArUPWKzGdghXccGkkf0LTZcGJoHeKal-RtyP8\
|
||||
PfvEAWTjSOCpBlSdUJ-1JL3tyd97uVFNaVuH3i7vvcMoUP_bdr0XW3rvRgaeC6X4daPLUvR1hK5Msut\
|
||||
QMtM2vpFghS_zZxIQRqz3B2ECxa9Bjxhmn8kLX5heZ8fq3lH-bmJp1DxzZ4V1RkWk.yVwXG9yARa5Ih\
|
||||
q2koh2NbQ""");
|
||||
var userKeyPair = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(USER_PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(USER_PRIV_KEY)));
|
||||
var incorrectDevicePrivateKey = userKeyPair.getPrivate();
|
||||
|
||||
Assertions.assertThrows(JWEHelper.InvalidJweKeyException.class, () -> JWEHelper.decryptUserKey(jwe, incorrectDevicePrivateKey));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("decryptUserKey with setup code")
|
||||
public void testDecryptUserKeyPBES2() throws ParseException {
|
||||
var jwe = JWEObject.parse("""
|
||||
eyJhbGciOiJQQkVTMi1IUzUxMitBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIiwicDJzIjoiT3hMY0Q\
|
||||
xX1pCODc1c2hvUWY2Q1ZHQSIsInAyYyI6MTAwMCwiYXB1IjoiIiwiYXB2IjoiIn0.FD4fcrP4Pb\
|
||||
aKOQ9ZfXl0gpMM6Fa2rfqAvL0K5ZyYUiVeHCNV-A02Rg.urT1ShSv6qQxh8X7.gEqAiUWD98a2E\
|
||||
P7ITCPTw4DJo6-BpqrxA73D6gNIj9z4d1hN-EP99Q4mWBWLH97H8ugbG5rGsm8xsjsBqpWORQqF\
|
||||
mJZR2AhlPiwFaC7n_MDDBupSy_swDnCfj731Lal297IP5WbkFcmozKsyhmwdkctxjf_VHA.fJki\
|
||||
kDjUaxwUKqpvT7qaAQ
|
||||
""");
|
||||
|
||||
var userKey = JWEHelper.decryptUserKey(jwe, "123456");
|
||||
|
||||
Assertions.assertArrayEquals(Base64.getDecoder().decode(PRIV_KEY), userKey.getEncoded());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("decryptUserKey with incorrect setup code")
|
||||
public void testDecryptUserKeyPBES2WrongKey() throws ParseException {
|
||||
var jwe = JWEObject.parse("""
|
||||
eyJhbGciOiJQQkVTMi1IUzUxMitBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIiwicDJzIjoiT3hMY0Q\
|
||||
xX1pCODc1c2hvUWY2Q1ZHQSIsInAyYyI6MTAwMCwiYXB1IjoiIiwiYXB2IjoiIn0.FD4fcrP4Pb\
|
||||
aKOQ9ZfXl0gpMM6Fa2rfqAvL0K5ZyYUiVeHCNV-A02Rg.urT1ShSv6qQxh8X7.gEqAiUWD98a2E\
|
||||
P7ITCPTw4DJo6-BpqrxA73D6gNIj9z4d1hN-EP99Q4mWBWLH97H8ugbG5rGsm8xsjsBqpWORQqF\
|
||||
mJZR2AhlPiwFaC7n_MDDBupSy_swDnCfj731Lal297IP5WbkFcmozKsyhmwdkctxjf_VHA.fJki\
|
||||
kDjUaxwUKqpvT7qaAQ
|
||||
""");
|
||||
|
||||
Assertions.assertThrows(JWEHelper.InvalidJweKeyException.class, () -> JWEHelper.decryptUserKey(jwe, "654321"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("decryptVaultKey")
|
||||
public void testDecryptVaultKey() throws ParseException, InvalidKeySpecException {
|
||||
var jwe = JWEObject.parse("""
|
||||
eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlA\
|
||||
tMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IllUcEY3bGtTc3JvZVVUVFdCb21LNzBTN0\
|
||||
FhVTJyc0ptMURpZ1ZzbjRMY2F5eUxFNFBabldkYmFVcE9jQVV5a1ciLCJ5IjoiLU5pS3loUktjSk52N\
|
||||
m02Z0ZJUWc4cy1Xd1VXUW9uT3A5dkQ4cHpoa2tUU3U2RzFlU2FUTVlhZGltQ2Q4V0ExMSJ9LCJhcHUi\
|
||||
OiIiLCJhcHYiOiIifQ..BECWGzd9UvhHcTJC.znt4TlS-qiNEjxiu2v-du_E1QOBnyBR6LCt865SHxD\
|
||||
-kwRc1JwX_Lq9XVoFj2GnK9-9CgxhCLGurg5Jt9g38qv2brGAzWL7eSVeY1fIqdO_kUhLpGslRTN6h2\
|
||||
U0NHJi2-iE.WDVI2kOk9Dy3PWHyIg8gKA""");
|
||||
var privateKey = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(PRIV_KEY))).getPrivate();
|
||||
|
||||
var masterkey = JWEHelper.decryptVaultKey(jwe, privateKey);
|
||||
|
||||
var expectedEncKey = new byte[32];
|
||||
var expectedMacKey = new byte[32];
|
||||
@@ -44,13 +133,11 @@ public class JWEHelperTest {
|
||||
"eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IkJyYm9UQkl5Y0NDUEdJQlBUekU2RjBnbTRzRjRCamZPN1I0a2x0aWlCaThKZkxxcVdXNVdUSVBLN01yMXV5QVUiLCJ5IjoiNUpGVUI0WVJiYjM2RUZpN2Y0TUxMcFFyZXd2UV9Tc3dKNHRVbFd1a2c1ZU04X1ZyM2pkeml2QXI2WThRczVYbSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..QEq4Z2m6iwBx2ioS.IBo8TbKJTS4pug.61Z-agIIXgP8bX10O_yEMA", // json payload field "key" not a string
|
||||
"eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6ImNZdlVFZm9LYkJjenZySE5zQjUxOGpycUxPMGJDOW5lZjR4NzFFMUQ5dk95MXRqd1piZzV3cFI0OE5nU1RQdHgiLCJ5IjoiaWRJekhCWERzSzR2NTZEeU9yczJOcDZsSG1zb29fMXV0VTlzX3JNdVVkbkxuVXIzUXdLZkhYMWdaVXREM1RKayJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..0VZqu5ei9U3blGtq.eDvhU6drw7mIwvXu6Q.f05QnhI7JWG3IYHvexwdFQ" // json payload field "key" invalid base64 data
|
||||
})
|
||||
public void testDecryptInvalid(String malformed) throws ParseException, InvalidKeySpecException {
|
||||
public void testDecryptInvalidVaultKey(String malformed) throws ParseException, InvalidKeySpecException {
|
||||
var jwe = JWEObject.parse(malformed);
|
||||
var keyPair = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(PRIV_KEY)));
|
||||
var privateKey = P384KeyPair.create(new X509EncodedKeySpec(Base64.getDecoder().decode(PUB_KEY)), new PKCS8EncodedKeySpec(Base64.getDecoder().decode(PRIV_KEY))).getPrivate();
|
||||
|
||||
Assertions.assertThrows(MasterkeyLoadingFailedException.class, () -> {
|
||||
JWEHelper.decrypt(jwe, keyPair.getPrivate());
|
||||
});
|
||||
Assertions.assertThrows(MasterkeyLoadingFailedException.class, () -> JWEHelper.decryptVaultKey(jwe, privateKey));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user