Compare commits

..
Author SHA1 Message Date
Armin Schrenk 4b6afb5fd6 if linked hub vault is alread present unlock/reveal
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-08-31 18:59:34 +02:00
Armin Schrenk d2b1f86d85 Refactor Keyloading to use only the required info
* new record KeyLoadingRef(config, name)
* masterkey vaults still need Vault.java
* hub relies on KeyLoadingRef

Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-08-31 15:53:54 +02:00
Armin Schrenk b01478f0fa Remove VaultTemplate stuff
including jdk.zipfs as dependency in jlink

Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-08-26 17:08:29 +02:00
Armin Schrenk 27eb6298ae Refactor VaultCreationEvent to OpenHubVaultEvent
and change deeplink URL scheme to org.cryptomator

Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-08-26 16:50:26 +02:00
Armin Schrenk 2c53afd70f simplify
there is no intermediate directory in the zip. Also clean up tests

Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-07-21 17:06:17 +02:00
Armin Schrenk ae84f2f2fd move package related tasks to the importtemplate package
keeps FXApplicationWindows slim

Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-07-21 16:22:52 +02:00
Armin Schrenk ad17f0b835 Extract zip once
and show error if extraction fails

Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-07-21 14:21:02 +02:00
Armin Schrenk e7db172a89 restrict value of name parameter in deeplink
no path separator, no invisible chars, no trailing whitespaces

Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-07-21 11:50:31 +02:00
Armin Schrenk 222e1f18ad use fragements section for parameters
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-07-20 13:52:25 +02:00
Armin Schrenk 9a3baf7119 Move VaultTemplateExtractor to package where it is actually used.
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-07-20 12:09:23 +02:00
Armin Schrenk 53baf3c648 Improve error handling
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-07-15 17:25:49 +02:00
Armin Schrenk c8a6b7cf13 show dialog on VaultCreationEvent
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-07-15 16:34:38 +02:00
Armin Schrenk 5d12a62e38 Replace ImportTemplate stubs with impls
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-07-14 17:22:45 +02:00
Armin Schrenk 40bb0079ac add ui to import a vault from template
add scaffolding, controller are stubs for now

Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-07-14 17:00:25 +02:00
Armin Schrenk fbdf511f05 add jdk.zipfs to jlinked modules
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-07-14 15:31:59 +02:00
Armin Schrenk 410900ed51 add contraints to the zip'ed template
at most 2MiB, at most 30 files

Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-07-14 13:36:55 +02:00
Armin Schrenk 1302c1fc34 add vault template extraction
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-07-13 16:25:59 +02:00
Armin Schrenk ea806ec97a Refactor event handling
AppLauchEvent became a sealed interface with implemented Events RevealRunningApp-, OpenFile- and ValtCreateEvent.

Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-07-13 13:45:21 +02:00
Armin Schrenk 77dbf9414a Add DeeplinkAction
for handling URIs

Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-07-13 10:27:23 +02:00
Armin Schrenk a3fc50fc7e remove and exclude build artifact
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-07-13 10:12:52 +02:00
Armin Schrenk 4b0e8b5841 Merge branch 'develop' into feature/deeplinking 2026-07-07 16:23:09 +02:00
Andreas OpferkuchandGitHub 688d1a4a24 README: Update JDK version from 25 to 26 (#4269)
[ci skip]
2026-07-02 21:05:18 +02:00
Tobias Hagemann 56e655c5a9 Add ApiVoid as silver sponsor [ci skip] 2026-07-01 12:15:15 +02:00
Armin Schrenk 4c9dbb60fe fix b681c26d72
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-06-29 14:44:13 +02:00
Armin Schrenk 7ddedea5c2 [skip ci] Merge branch 'main' into develop
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-06-29 13:54:55 +02:00
Armin Schrenk fc76f5f83f Merge branch 'release/1.19.3' 2026-06-29 13:52:10 +02:00
Armin Schrenk bfc8542551 remove dependency-check from release-check workflow
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-06-29 13:50:24 +02:00
Armin Schrenk 24a20911f1 finalize 1.19.3
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-06-29 13:39:27 +02:00
Armin Schrenk b681c26d72 Replace some slim runners by regular ones
Slim ubuntu runners don't have docker, thus failing executing certain actions. (i.e. https://github.com/rtCamp/action-slack-notify/issues/240)

Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-06-25 13:16:17 +02:00
Armin Schrenk 34f5c06795 [skip ci] Update changelog
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-06-25 13:11:11 +02:00
Armin Schrenk f4928fd981 prepare 1.19.3
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-06-25 11:23:48 +02:00
Armin SchrenkandGitHub f748bfd953 replace zulu by temurin (#4263)
Closes #4242

Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-06-25 11:13:18 +02:00
Armin SchrenkandGitHub cb689946ed Merge pull request #4262 from cryptomator/feature/4260-fix-unsigned-ps1
Sign Powershell scripts (again)
2026-06-25 10:21:47 +02:00
Armin Schrenk 38bdf6d0e4 cleanup workflow
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-06-25 10:14:42 +02:00
Armin Schrenk 409209fd89 [skip ci] update changelog
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-06-25 10:09:48 +02:00
Armin Schrenk 1fd44ff651 for scripts, do not append signature
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-06-25 01:10:10 +02:00
Armin Schrenk 9bb19b0130 extra sign steps for ps1
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-06-25 00:51:46 +02:00
Armin Schrenk 1b5319f706 check ps1 scripts after signing
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-06-25 00:31:19 +02:00
Armin Schrenk 33b4aa5ba3 Revert "Revert #4169 and fix ps1 files not signed"
This reverts commit 1879c6fa77.
2026-06-25 00:23:58 +02:00
Armin Schrenk a428ae6260 Implement uri handling inside app.
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-06-10 15:48:25 +02:00
Armin Schrenk 9c41e348fa register url scheme "cryptomator" in installers
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
2026-06-10 11:53:56 +02:00
60 changed files with 1150 additions and 278 deletions
+1 -1
View File
@@ -78,7 +78,7 @@ jobs:
local-dir: ./upload/
allowlist-avast:
name: Anti Virus Allowlisting Avast
runs-on: ubuntu-slim
runs-on: ubuntu-latest
needs: download-file
if: inputs.avast
steps:
+2 -2
View File
@@ -6,7 +6,7 @@ on:
jobs:
notify:
runs-on: ubuntu-slim
runs-on: ubuntu-latest
steps:
- name: Notify about DEB build
uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # v2.4.0
@@ -102,7 +102,7 @@ jobs:
name: Notify for winget-release
if: needs.get-asset-urls.outputs.is-windows-release == 'true' && needs.check-release.outputs.release-kind == 'stable'
needs: [check-release, get-asset-urls]
runs-on: ubuntu-slim
runs-on: ubuntu-latest
steps:
- name: Slack Notification
uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # v2.4.0
+1 -16
View File
@@ -48,19 +48,4 @@ jobs:
if ! grep -q "<release date=\".*\" version=\"${{ steps.validate-pom-version.outputs.semVerStr }}\">" dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml; then
echo "Release not set in dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml"
exit 1
fi
- name: Cache NVD DB
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.m2/repository/org/owasp/dependency-check-data/
key: dependency-check-${{ github.run_id }}
restore-keys: |
dependency-check
env:
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 5
- name: Run org.owasp:dependency-check plugin
id: dependency-check
continue-on-error: true
run: ./mvnw -B verify -Pdependency-check -DskipTests
env:
NVD_API_KEY: ${{ secrets.NVD_API_KEY }}
fi
+13 -32
View File
@@ -82,7 +82,7 @@ jobs:
include:
- arch: x64
os: windows-latest
java-dist: 'zulu' #cannot use temurin, see https://github.com/cryptomator/cryptomator/issues/3824#issuecomment-2829827427
java-dist: 'temurin'
java-version: '26.0.1+8'
java-package: 'jdk'
steps:
@@ -229,22 +229,23 @@ jobs:
uses: ./.github/actions/win-sign-action
with:
base-dir: ${{ github.workspace }}\appdir
file-extensions: 'exe,dll'
recursive: true
append-signature: true
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
client-id: ${{ secrets.AZURE_CLIENT_ID }}
client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
- name: Sign DLLs with Actalis CodeSigner
if: inputs.sign || github.event_name == 'release'
uses: skymatic/workflows/.github/actions/win-sign-action@957d3c2c08c56855fdac41e5afb9a7aca8c30dd9 # no specific version
- name: Sign Scripts with Azure Trusted Signing
if: inputs.sign || github.event_name == 'schedule'
uses: ./.github/actions/win-sign-action
with:
base-dir: 'appdir'
file-extensions: 'dll,exe,ps1'
recursive: true
sign-description: 'Cryptomator'
sign-url: 'https://cryptomator.org'
username: ${{ secrets.WIN_CODESIGN_USERNAME }}
password: ${{ secrets.WIN_CODESIGN_PW }}
base-dir: ${{ github.workspace }}\appdir\Cryptomator
file-extensions: 'ps1'
recursive: false
append-signature: false # Powershell scripts cannot be signed in append mode, see #4260
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
client-id: ${{ secrets.AZURE_CLIENT_ID }}
client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
- name: Replace DLLs inside jars with signed ones
shell: pwsh
run: |
@@ -347,7 +348,7 @@ jobs:
- arch: x64
os: windows-latest
executable-suffix: x64
java-dist: 'zulu'
java-dist: 'temurin'
java-version: '26.0.1+8'
java-package: 'jdk'
steps:
@@ -429,16 +430,6 @@ jobs:
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
client-id: ${{ secrets.AZURE_CLIENT_ID }}
client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
- name: Sign burn engine with Actalis CodeSigner
if: inputs.sign || github.event_name == 'release'
uses: skymatic/workflows/.github/actions/win-sign-action@957d3c2c08c56855fdac41e5afb9a7aca8c30dd9 # no specific version
with:
base-dir: 'tmp'
file-extensions: 'exe'
sign-description: 'Cryptomator Bundle Installer'
sign-url: 'https://cryptomator.org'
username: ${{ secrets.WIN_CODESIGN_USERNAME }}
password: ${{ secrets.WIN_CODESIGN_PW }}
- name: Reattach signed burn engine to installer
if: inputs.sign || github.event_name == 'schedule'
shell: pwsh
@@ -456,16 +447,6 @@ jobs:
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
client-id: ${{ secrets.AZURE_CLIENT_ID }}
client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
- name: Sign installer with Actalis CodeSigner
if: inputs.sign || github.event_name == 'release'
uses: skymatic/workflows/.github/actions/win-sign-action@957d3c2c08c56855fdac41e5afb9a7aca8c30dd9 # no specific version
with:
base-dir: 'installer'
file-extensions: 'exe'
sign-description: 'Cryptomator Bundle Installer'
sign-url: 'https://cryptomator.org'
username: ${{ secrets.WIN_CODESIGN_USERNAME }}
password: ${{ secrets.WIN_CODESIGN_PW }}
- id: sha256sum
run: |
read -ra CMD_OUTPUT < <(sha256sum installer/Cryptomator-*.exe)
+1 -1
View File
@@ -8,7 +8,7 @@
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" project-jdk-name="25" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_26" project-jdk-name="temurin-26" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
+8 -1
View File
@@ -8,8 +8,12 @@ The changelog starts with version 1.19.0.
Changes to prior versions can be found on the [Github release page](https://github.com/cryptomator/cryptomator/releases).
## [Unreleased](https://github.com/cryptomator/cryptomator/compare/1.19.2...HEAD)
## [Unreleased](https://github.com/cryptomator/cryptomator/compare/1.19.3...HEAD)
No changes yet.
## [1.19.3](https://github.com/cryptomator/cryptomator/releases/1.19.3) - 2026-06-29
### Added
* New error dialog if importing a vault fails ([#4243](https://github.com/cryptomator/cryptomator/pull/4243))
@@ -24,6 +28,9 @@ Changes to prior versions can be found on the [Github release page](https://gith
* Refactored release pipeline to allow immutable releases ([#4205](https://github.com/cryptomator/cryptomator/pull/4205))
* Updated to JDK 26.0.1 ([#4244](https://github.com/cryptomator/cryptomator/pull/4244))
* Updated to JavaFX 25.0.3 ([#4255](https://github.com/cryptomator/cryptomator/pull/4255))
* Drop signing with Actalis issued certificate ([#4169](https://github.com/cryptomator/cryptomator/pull/4169), [#4262](https://github.com/cryptomator/cryptomator/pull/4262))
* Fix dagger binding graph issues ([#4147](https://github.com/cryptomator/cryptomator/pull/4147))
* Added flatpak build to CI ([#4199](https://github.com/cryptomator/cryptomator/pull/4199))
* Updated dependencies:
- `org.cryptomator:webdav-nio-adapter` from 3.0.1 to 3.0.2
- `org.cryptomator:integrations-api` from 1.8.0 to 1.9.0
+2 -1
View File
@@ -26,6 +26,7 @@ Become our Gold Sponsor and showcase your brand to a targeted audience! Please c
<tr>
<td><a href="https://www.gee-whiz.de/"><img src="https://cryptomator.org/img/sponsors/geewhiz.svg" alt="gee-whiz" height="56"></a></td>
<td><a href="https://www.route4me.com/"><img src="https://cryptomator.org/img/sponsors/route4me.svg" alt="Route4Me" height="56"></a></td>
<td><a href="https://www.apivoid.com/"><img src="https://cryptomator.org/img/sponsors/apivoid.svg" alt="ApiVoid" height="56"></a></td>
</tr>
</tbody>
</table>
@@ -78,7 +79,7 @@ For more information on the security details visit [cryptomator.org](https://doc
### Dependencies
* JDK 25 (e.g. temurin, zulu)
* JDK 26 (e.g. temurin, zulu)
### Run Maven
+2 -2
View File
@@ -1,11 +1,11 @@
[Desktop Entry]
Name=Cryptomator
Comment=Cloud Storage Encryption Utility
Exec=cryptomator %F
Exec=cryptomator %U
Icon=org.cryptomator.Cryptomator
Terminal=false
Type=Application
Categories=Utility;Security;FileTools;
StartupNotify=true
StartupWMClass=org.cryptomator.launcher.Cryptomator$MainApp
MimeType=application/vnd.cryptomator.encrypted;application/vnd.cryptomator.vault;
MimeType=application/vnd.cryptomator.encrypted;application/vnd.cryptomator.vault;x-scheme-handler/org.cryptomator;
@@ -84,6 +84,9 @@
</content_rating>
<releases>
<release date="2026-06-29" version="1.19.3">
<url type="details">https://github.com/cryptomator/cryptomator/releases/1.19.3</url>
</release>
<release date="2026-03-20" version="1.19.2">
<url type="details">https://github.com/cryptomator/cryptomator/releases/1.19.2</url>
</release>
+1 -1
View File
@@ -11,7 +11,7 @@ pkgdesc="Multiplatform transparent client-side encryption of your files in the c
arch=('any')
url="https://cryptomator.org/"
license=('GPL3')
depends=('fuse3' 'alsa-lib' 'hicolor-icon-theme' 'libxtst' 'libnet' 'libxrender')
depends=('fuse3' 'alsa-lib' 'hicolor-icon-theme' 'libxtst' 'libnet' 'libxrender' 'desktop-file-utils')
makedepends=('maven' 'unzip')
optdepends=('keepassxc-cryptomator: Use KeePassXC to store vault passwords' 'ttf-hanazono: Install this font when using Japanese system language')
_jdkver=26.0.1+8
+14
View File
@@ -46,6 +46,20 @@
<string>Any</string>
</dict>
</dict>
<!-- register org.cryptomator:// URL scheme -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>org.cryptomator.deeplink</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLSchemes</key>
<array>
<string>org.cryptomator</string>
</array>
</dict>
</array>
<!-- register .cryptomator extension -->
<key>CFBundleDocumentTypes</key>
<array>
+1
View File
@@ -9,3 +9,4 @@ installer
*.jmod
resources/jfxJmods.zip
license.rtf
**/FAvaultFile.properties
-4
View File
@@ -1,4 +0,0 @@
mime-type=application/vnd.cryptomator.vault
extension=cryptomator
description=Cryptomator Vault File
icon=C:\\Users\\Arbeit\\Skymatic\\cryptomator-jdk26-jpackage\\dist\\win\\resources\\Cryptomator-Vault.ico
+15
View File
@@ -27,6 +27,7 @@
<?define ProgIdContentType= "application/vnd.cryptomator.encrypted" ?>
<?define CloseApplicationTarget= "cryptomator.exe" ?>
<?define LoopbackAlias= "cryptomator-vault" ?>
<?define UrlProtocolScheme= "org.cryptomator" ?>
<?include $(var.JpConfigDir)/overrides.wxi ?>
@@ -97,6 +98,19 @@
<ns0:Extension Id="c9u" Advertise="no" ContentType="$(var.ProgIdContentType)"/>
</ns0:ProgId>
</ns0:Component>
<!-- Register "org.cryptomator://" URL protocol handler -->
<ns0:Component Bitness="always64" Id="UrlProtocolHandler" Guid="*">
<ns0:RegistryKey Root="HKMU" Key="Software\Classes\$(var.UrlProtocolScheme)">
<ns0:RegistryValue Type="string" Value="URL:$(var.JpAppName) Protocol" KeyPath="yes"/>
<ns0:RegistryValue Name="URL Protocol" Type="string" Value=""/>
<ns0:RegistryKey Key="DefaultIcon">
<ns0:RegistryValue Type="string" Value="[INSTALLDIR]$(var.JpAppName).exe,0"/>
</ns0:RegistryKey>
<ns0:RegistryKey Key="shell\open\command">
<ns0:RegistryValue Type="string" Value="&quot;[INSTALLDIR]$(var.JpAppName).exe&quot; &quot;%1&quot;"/>
</ns0:RegistryKey>
</ns0:RegistryKey>
</ns0:Component>
</ns0:DirectoryRef>
<ns0:StandardDirectory Id="CommonAppDataFolder">
@@ -126,6 +140,7 @@
<ns0:ComponentGroupRef Id="FileAssociations"/>
<!-- Ref to additional ProgIDs -->
<ns0:ComponentRef Id="nonStartingProgID"/>
<ns0:ComponentRef Id="UrlProtocolHandler"/>
<ns0:ComponentRef Id="AdminConfigDir"/>
<ns0:ComponentRef Id="AdminConfigFile"/>
</ns0:Feature>
+1 -1
View File
@@ -33,7 +33,7 @@
<nonModularGroupIds>org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents</nonModularGroupIds>
<!-- cryptomator dependencies -->
<cryptomator.cryptofs.version>2.10.0</cryptomator.cryptofs.version>
<cryptomator.cryptofs.version>2.11.0-SNAPSHOT</cryptomator.cryptofs.version>
<cryptomator.cryptolib.version>2.2.2</cryptomator.cryptolib.version>
<cryptomator.integrations.version>1.9.0</cryptomator.integrations.version>
<cryptomator.integrations.win.version>1.6.1</cryptomator.integrations.win.version>
@@ -1,13 +1,16 @@
package org.cryptomator.launcher;
import java.nio.file.Path;
import java.util.Collection;
public record AppLaunchEvent(AppLaunchEvent.EventType type, Collection<Path> pathsToOpen) {
public enum EventType {
REVEAL_APP,
OPEN_FILE
}
/**
* An event triggering an action in the running application instance.
* <p>
* Produced by the launch-argument handling (see {@link LaunchArgsParser} and the {@code *RequestHandler}s) and consumed
* by the UI's {@code AppLaunchEventHandler}. Each permitted subtype represents one supported action:
* <ul>
* <li>{@link RevealRunningEvent} - reveal the already-running app,</li>
* <li>{@link OpenFileEvent} - open one or more paths,</li>
* <li>{@link OpenHubVaultEvent} - open a Hub vault from a deeplink.</li>
* </ul>
*/
public sealed interface AppLaunchEvent permits RevealRunningEvent, OpenFileEvent, OpenHubVaultEvent {
}
@@ -25,7 +25,7 @@ class CryptomatorModule {
@Provides
@Singleton
@Named("launchEventQueue")
static BlockingQueue<AppLaunchEvent> provideFileOpenRequests() {
static BlockingQueue<AppLaunchEvent> provideLaunchEventQueue() {
return new ArrayBlockingQueue<>(10);
}
@@ -41,7 +41,7 @@ class FileOpenRequestHandler {
private void openFiles(OpenFilesEvent evt) {
Collection<Path> pathsToOpen = evt.getFiles().stream().map(File::toPath).toList();
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
AppLaunchEvent launchEvent = new OpenFileEvent(pathsToOpen);
tryToEnqueueFileOpenRequest(launchEvent);
}
@@ -60,7 +60,7 @@ class FileOpenRequestHandler {
}
}).filter(Objects::nonNull).toList();
if (!pathsToOpen.isEmpty()) {
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
AppLaunchEvent launchEvent = new OpenFileEvent(pathsToOpen);
tryToEnqueueFileOpenRequest(launchEvent);
}
}
@@ -68,7 +68,7 @@ class FileOpenRequestHandler {
private void tryToEnqueueFileOpenRequest(AppLaunchEvent launchEvent) {
if (!launchEventQueue.offer(launchEvent)) {
LOG.warn("Could not enqueue application launch event.", launchEvent);
LOG.warn("Could not enqueue application launch event {}.", launchEvent);
}
}
@@ -7,7 +7,6 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.BlockingQueue;
@@ -16,24 +15,28 @@ class IpcMessageHandler implements IpcMessageListener {
private static final Logger LOG = LoggerFactory.getLogger(IpcMessageHandler.class);
private final FileOpenRequestHandler fileOpenRequestHandler;
private final LaunchArgsParser launchArgsParser;
private final BlockingQueue<AppLaunchEvent> launchEventQueue;
@Inject
public IpcMessageHandler(FileOpenRequestHandler fileOpenRequestHandler, @Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue) {
this.fileOpenRequestHandler = fileOpenRequestHandler;
public IpcMessageHandler(LaunchArgsParser launchArgsParser, @Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue) {
this.launchArgsParser = launchArgsParser;
this.launchEventQueue = launchEventQueue;
}
@Override
public void revealRunningApp() {
launchEventQueue.add(new AppLaunchEvent(AppLaunchEvent.EventType.REVEAL_APP, Collections.emptyList()));
launchEventQueue.add(new RevealRunningEvent());
}
@Override
public void handleLaunchArgs(List<String> args) {
LOG.debug("Received launch args: {}", args.stream().reduce((a, b) -> a + ", " + b).orElse(""));
fileOpenRequestHandler.handleLaunchArgs(args);
LOG.debug("Received launch args: {}", args);
try {
launchArgsParser.process(args);
} catch (IllegalArgumentException e) {
LOG.warn("Ignoring malformed launch args: {}", e.getMessage());
}
}
}
@@ -0,0 +1,88 @@
package org.cryptomator.launcher;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.net.URI;
import java.nio.file.Path;
import java.util.List;
import java.util.regex.Pattern;
/**
* Preprocesses the launch arguments and delegates them to the matching handler.
* <p>
* An argument is treated as a URI if it starts with a (non-{@code file}) scheme of at least two characters, e.g.
* {@code cryptomator://…}. Everything else - including plain paths and {@code file://…} URIs - is treated as a file path
* and forwarded to the {@link FileOpenRequestHandler}. The two-character minimum prevents Windows drive letters
* (e.g. {@code C:\…}) from being misinterpreted as URIs.
* <p>
* URIs and file paths must not be mixed and at most a single URI is accepted, which has to be the first argument.
*/
@Singleton
class LaunchArgsParser {
private static final Pattern SCHEME_PATTERN = Pattern.compile("^([a-zA-Z][a-zA-Z0-9+.-]+):.*");
private static final String FILE_SCHEME = "file";
private final FileOpenRequestHandler fileOpenRequestHandler;
private final URIOpenRequestHandler uriOpenRequestHandler;
private final NoopRequestHandler noopRequestHandler;
@Inject
public LaunchArgsParser(FileOpenRequestHandler fileOpenRequestHandler, URIOpenRequestHandler uriOpenRequestHandler, NoopRequestHandler noopRequestHandler) {
this.fileOpenRequestHandler = fileOpenRequestHandler;
this.uriOpenRequestHandler = uriOpenRequestHandler;
this.noopRequestHandler = noopRequestHandler;
}
/**
* Classifies the given launch arguments and delegates them to the responsible handler.
*
* @param args the raw launch arguments
* @throws IllegalArgumentException if URIs and file paths are mixed, if more than one URI is given, if a URI is not
* the first argument, or if a URI argument is malformed
*/
public void process(List<String> args) {
if(args.isEmpty()) {
noopRequestHandler.revealApp();
return;
}
var classified = args.stream().map(LaunchArgsParser::classify).toList();
var uris = classified.stream().filter(arg -> arg.kind() == Kind.URI).toList();
if (uris.isEmpty()) {
var paths = classified.stream().map(Arg::value).toList();
fileOpenRequestHandler.handleLaunchArgs(paths);
return;
}
if (uris.size() > 1) {
throw new IllegalArgumentException("Only a single URI argument is accepted, but got " + uris.size() + ".");
}
if (classified.getFirst().kind() != Kind.URI) {
throw new IllegalArgumentException("URI argument must be the first parameter.");
}
if (classified.size() > 1) {
throw new IllegalArgumentException("Mixing a URI with file paths is not supported.");
}
uriOpenRequestHandler.handleLaunchArgs(URI.create(classified.getFirst().value()));
}
private static Arg classify(String arg) {
var matcher = SCHEME_PATTERN.matcher(arg);
if (!matcher.matches()) {
return new Arg(Kind.PATH, arg);
}
var scheme = matcher.group(1);
if (FILE_SCHEME.equalsIgnoreCase(scheme)) {
// file:// URIs (e.g. passed by Linux file managers) are file paths in disguise
return new Arg(Kind.PATH, Path.of(URI.create(arg)).toString());
}
return new Arg(Kind.URI, arg);
}
private enum Kind {PATH, URI}
private record Arg(Kind kind, String value) {}
}
@@ -0,0 +1,29 @@
package org.cryptomator.launcher;
import javax.inject.Inject;
import javax.inject.Named;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Singleton;
import java.util.concurrent.BlockingQueue;
@Singleton
public class NoopRequestHandler {
private static final Logger LOG = LoggerFactory.getLogger(NoopRequestHandler.class);
private final BlockingQueue<AppLaunchEvent> launchEventQueue;
@Inject
public NoopRequestHandler(@Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue) {
this.launchEventQueue = launchEventQueue;
}
public void revealApp() {
AppLaunchEvent launchEvent = new RevealRunningEvent();
if (!launchEventQueue.offer(launchEvent)) {
LOG.warn("Could not enqueue application launch event {}.", launchEvent);
}
}
}
@@ -0,0 +1,13 @@
package org.cryptomator.launcher;
import java.nio.file.Path;
import java.util.Collection;
/**
* Requests that the given paths (e.g. {@code .cryptomator} vault files) are opened.
*
* @param pathsToOpen the paths to open
*/
public record OpenFileEvent(Collection<Path> pathsToOpen) implements AppLaunchEvent {
}
@@ -0,0 +1,182 @@
package org.cryptomator.launcher;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptofs.VaultConfigLoadException;
import org.cryptomator.ui.keyloading.hub.HubConfig;
import org.cryptomator.ui.keyloading.hub.HubKeyLoadingStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
/**
* Requests opening a Hub vault from an {@code org.cryptomator://vault/open#vaultConfig=…} deeplink.
* <p>
* The single parameter is the vault's {@code vault.cryptomator}, a compact JWS embedded verbatim, carried in the fragment part.
* <p>
* Notes:
* <ul>
* <li> The config is read <em>unverified</em>, since its signature is keyed on the masterkey, which is only obtainable from Hu later on. </li>
* <li> The deeplink parsing makes a strict validation due to untrusted input</li>
* </ul>
*
* @param vaultConfig the decoded, unverified vault config
* @param vaultId the vault's id within its Hub instance, taken from the config's {@code jti} claim
*/
public record OpenHubVaultEvent(VaultConfig.UnverifiedVaultConfig vaultConfig, UUID vaultId) implements AppLaunchEvent {
private static final Logger LOG = LoggerFactory.getLogger(OpenHubVaultEvent.class);
private static final String SCHEME = "org.cryptomator";
private static final String HOST = "vault";
private static final String PATH = "/open";
private static final String PARAM_VAULT_CONFIG = "vaultConfig";
private static final String HUB_HEADER = "hub";
private static final int MAX_CONFIG_LENGTH = 8192; //real Hub vault config is ~1KB leaving some room for extensions
/**
* Attempts to interpret the given URI as an {@code org.cryptomator://vault/open#vaultConfig=…} deeplink.
*
* @param uri the deeplink URI
* @return the parsed event, or an empty optional if the URI's scheme, host or path do not identify a vault-open
* deeplink
* @throws IllegalArgumentException if the URI identifies a vault-open deeplink, but the config is missing, too
* large, not decodable, or does not describe a Hub vault
*/
public static Optional<OpenHubVaultEvent> tryParse(URI uri) {
if (!SCHEME.equalsIgnoreCase(uri.getScheme()) || !HOST.equalsIgnoreCase(uri.getHost()) || !PATH.equals(uri.getPath())) {
return Optional.empty();
}
var params = parseParams(uri.getRawFragment());
var token = params.get(PARAM_VAULT_CONFIG);
if (token == null || token.isBlank()) {
throw new IllegalArgumentException("Missing required fragment parameter '" + PARAM_VAULT_CONFIG + "'.");
}
var vaultConfig = decode(token);
requireHubVault(vaultConfig);
var vaultId = extractVaultId(vaultConfig);
var leftoverParams = params.keySet().stream().filter(k -> !k.equals(PARAM_VAULT_CONFIG)).toList();
if (!leftoverParams.isEmpty()) {
LOG.debug("Ignoring unknown parameters {}", leftoverParams);
}
return Optional.of(new OpenHubVaultEvent(vaultConfig, vaultId));
}
private static VaultConfig.UnverifiedVaultConfig decode(String token) {
// a compact JWS is ASCII, so its character count is its byte count
if (token.length() > MAX_CONFIG_LENGTH) {
throw new IllegalArgumentException("Fragment parameter '%s' must not exceed %d bytes.".formatted(PARAM_VAULT_CONFIG, MAX_CONFIG_LENGTH));
}
try {
return VaultConfig.decode(token);
} catch (VaultConfigLoadException e) {
throw new IllegalArgumentException("Fragment parameter '" + PARAM_VAULT_CONFIG + "' is not a decodable vault config.", e);
}
}
/**
* Ensures the config describes a Hub vault and that the endpoints the app will talk to are usable.
*/
private static void requireHubVault(VaultConfig.UnverifiedVaultConfig vaultConfig) {
var keyIdScheme = vaultConfig.getKeyId().getScheme();
if (keyIdScheme == null || !keyIdScheme.startsWith(HubKeyLoadingStrategy.SCHEME_PREFIX)) {
throw new IllegalArgumentException("Vault config does not describe a Hub vault, but had key id scheme '" + keyIdScheme + "'.");
}
HubConfig hubConfig;
try {
hubConfig = vaultConfig.getHeader(HUB_HEADER, HubConfig.class);
} catch (RuntimeException e) {
throw new IllegalArgumentException("Vault config contains an unreadable '" + HUB_HEADER + "' header.", e);
}
if (hubConfig == null) {
throw new IllegalArgumentException("Vault config contains no '" + HUB_HEADER + "' header.");
}
URI apiBaseUrl;
try {
apiBaseUrl = hubConfig.getApiBaseUrl();
} catch (RuntimeException e) {
throw new IllegalArgumentException("Vault config declares no usable hub api base url.", e);
}
requireUsableEndpoint("apiBaseUrl", apiBaseUrl);
requireUsableEndpoint("authEndpoint", toUri("authEndpoint", hubConfig.authEndpoint));
}
private static URI toUri(String field, String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Vault config declares no hub " + field + ".");
}
try {
return URI.create(value);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Vault config's hub " + field + " is not a valid url, but was '" + value + "'.", e);
}
}
private static void requireUsableEndpoint(String field, URI uri) {
if (!uri.isAbsolute() || uri.getHost() == null) {
throw new IllegalArgumentException("Vault config's hub " + field + " is not an absolute url with a host, but was '" + uri + "'.");
}
// Whether an http host is acceptable (it is, for local development) is decided by CheckHostTrustController
// Here we only ensure the endpoint is shaped like something that decision can be made on.
var scheme = uri.getScheme();
if (!"https".equalsIgnoreCase(scheme) && !"http".equalsIgnoreCase(scheme)) {
throw new IllegalArgumentException("Vault config's hub " + field + " is neither http nor https, but was '" + uri + "'.");
}
}
/**
* Reads the vault id from the config's {@code jti} claim.
* <p>
* Requiring a UUID matters beyond well-formedness: the id is interpolated into the {@code api/vaults/{vaultId}/…}
* request path, so it must not be able to introduce a path segment. A {@code jti} is an arbitrary string, so parsing
* it as a {@link UUID} and passing that on - rather than the raw claim - is what keeps that guarantee.
* <p>
* Hub writes the same id into the key id's trailing path segment, and the two have always agreed, so a config where
* they differ is forged or broken and is rejected.
*/
private static UUID extractVaultId(VaultConfig.UnverifiedVaultConfig vaultConfig) {
var allegedVaultId = vaultConfig.allegedVaultId();
if (allegedVaultId == null || allegedVaultId.isBlank()) {
throw new IllegalArgumentException("Vault config declares no vault id.");
}
UUID vaultId;
try {
vaultId = UUID.fromString(allegedVaultId);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Vault config's vault id is not a uuid, but was '" + allegedVaultId + "'.", e);
}
var keyId = vaultConfig.getKeyId();
var path = keyId.getPath();
var lastSegment = path == null ? "" : path.substring(path.lastIndexOf('/') + 1);
if (!vaultId.toString().equalsIgnoreCase(lastSegment)) {
throw new IllegalArgumentException("Vault config's vault id '" + vaultId + "' does not match its key id '" + keyId + "'.");
}
return vaultId;
}
private static Map<String, String> parseParams(String rawParams) {
var params = new HashMap<String, String>();
if (rawParams == null || rawParams.isEmpty()) {
return params;
}
for (var pair : rawParams.split("&")) {
var idx = pair.indexOf('=');
if (idx < 0) {
continue;
}
var key = URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8);
var value = URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8);
params.put(key, value);
}
return params;
}
}
@@ -0,0 +1,8 @@
package org.cryptomator.launcher;
/**
* Requests that the already-running application instance reveals itself (brings its main window to the front).
*/
public record RevealRunningEvent() implements AppLaunchEvent {
}
@@ -0,0 +1,58 @@
package org.cryptomator.launcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.net.URI;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.function.Function;
@Singleton
public class URIOpenRequestHandler {
private static final Logger LOG = LoggerFactory.getLogger(URIOpenRequestHandler.class);
/**
* The registered deeplink parsers, tried in order. Each returns a matching event, an empty optional if the URI is
* not its concern, or throws {@link IllegalArgumentException} if the URI is its concern but malformed.
*/
private static final List<Function<URI, Optional<? extends AppLaunchEvent>>> DEEPLINK_PARSERS = List.of( //
OpenHubVaultEvent::tryParse //
);
private final BlockingQueue<AppLaunchEvent> launchEventQueue;
@Inject
public URIOpenRequestHandler(@Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue) {
this.launchEventQueue = launchEventQueue;
}
public void handleLaunchArgs(URI uri) {
AppLaunchEvent launchEvent = toLaunchEvent(uri);
if (!launchEventQueue.offer(launchEvent)) {
LOG.warn("Could not enqueue application launch event {}.", launchEvent);
}
}
private AppLaunchEvent toLaunchEvent(URI uri) {
try {
for (var parser : DEEPLINK_PARSERS) {
var event = parser.apply(uri);
if (event.isPresent()) {
return event.get();
}
}
} catch (IllegalArgumentException e) {
LOG.warn("Received malformed deeplink {}: {}. Revealing running app instead.", uri, e.getMessage());
return new RevealRunningEvent();
}
LOG.warn("Received unsupported deeplink {}, revealing running app instead.", uri);
return new RevealRunningEvent();
}
}
@@ -62,9 +62,9 @@ public class Dialogs {
.setOkButtonKey(BUTTON_KEY_CLOSE);
}
public SimpleDialog.Builder prepareHubVaultArchived(Stage window, Vault vault) {
public SimpleDialog.Builder prepareHubVaultArchived(Stage window, String vaultDisplayName) {
return createDialogBuilder().setOwner(window) //
.setTitleKey("unlock.title", vault.getDisplayName()) //
.setTitleKey("unlock.title", vaultDisplayName) //
.setMessageKey("hub.archived.message") //
.setDescriptionKey("hub.archived.description") //
.setIcon(FontAwesome5Icon.BAN)//
@@ -4,14 +4,19 @@ import org.cryptomator.common.vaults.NotAVaultDirectoryException;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.launcher.AppLaunchEvent;
import org.cryptomator.launcher.OpenFileEvent;
import org.cryptomator.launcher.OpenHubVaultEvent;
import org.cryptomator.launcher.RevealRunningEvent;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.dialogs.Dialogs;
import org.cryptomator.ui.keyloading.hub.HubVaults;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.stage.Stage;
import java.io.IOException;
import java.nio.file.Path;
@@ -31,16 +36,18 @@ class AppLaunchEventHandler {
private final ExecutorService executorService;
private final FxApplicationWindows appWindows;
private final VaultListManager vaultListManager;
private final ObservableList<Vault> vaults;
private final VaultService vaultService;
private final Stage primaryStage;
private final Dialogs dialogs;
@Inject
public AppLaunchEventHandler(@Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue, ExecutorService executorService, FxApplicationWindows appWindows, VaultListManager vaultListManager, VaultService vaultService, @PrimaryStage Stage primaryStage, Dialogs dialogs) {
public AppLaunchEventHandler(@Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue, ExecutorService executorService, FxApplicationWindows appWindows, VaultListManager vaultListManager, ObservableList<Vault> vaults, VaultService vaultService, @PrimaryStage Stage primaryStage, Dialogs dialogs) {
this.launchEventQueue = launchEventQueue;
this.executorService = executorService;
this.appWindows = appWindows;
this.vaultListManager = vaultListManager;
this.vaults = vaults;
this.vaultService = vaultService;
this.primaryStage = primaryStage;
this.dialogs = dialogs;
@@ -63,10 +70,32 @@ class AppLaunchEventHandler {
}
private void handleLaunchEvent(AppLaunchEvent event) {
switch (event.type()) {
case REVEAL_APP -> appWindows.showMainWindow();
case OPEN_FILE -> event.pathsToOpen().forEach(this::openPotentialVault);
default -> LOG.warn("Unsupported event type: {}", event.type());
switch (event) {
case RevealRunningEvent _ -> appWindows.showMainWindow();
case OpenFileEvent openFileEvent -> openFileEvent.pathsToOpen().forEach(this::openPotentialVault);
case OpenHubVaultEvent openHubVaultEvent -> openHubVault(openHubVaultEvent);
}
}
/**
* Whether a hub vault is set up on this machine is a purely local question - hub manages the vault's key, not where
* it lives. Only if it is not set up here do we need to ask hub about it.
*/
private void openHubVault(OpenHubVaultEvent event) {
var existing = HubVaults.findByVaultId(vaults, event.vaultId());
if (existing.isPresent()) {
var vault = existing.get();
Platform.runLater(() -> {
if (vault.isUnlocked()) {
vaultService.reveal(vault);
} else if (vault.isLocked()) {
appWindows.startUnlockWorkflow(vault, null);
}
});
} else {
//TODO: authenticate, ask hub for the vault's details and offer to add it, see docs/hub-vault-open-deeplink-plan.md
LOG.info("Hub vault {} is not set up on this machine.", event.vaultId());
appWindows.showMainWindow();
}
}
@@ -16,6 +16,7 @@ import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.keyloading.KeyLoadingComponent;
import org.cryptomator.ui.keyloading.KeyLoadingRef;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import javax.inject.Named;
@@ -64,7 +65,11 @@ abstract class HealthCheckModule {
@HealthCheckWindow
@HealthCheckScoped
static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Factory compFactory, @HealthCheckWindow Vault vault, @Named("unlockWindow") Stage window ) {
return compFactory.create(vault, window).keyloadingStrategy();
try {
return compFactory.create(KeyLoadingRef.forVault(vault), vault, window).keyloadingStrategy();
} catch (IOException e) {
return KeyLoadingStrategy.failed(e);
}
}
@Provides
@@ -2,6 +2,7 @@ package org.cryptomator.ui.keyloading;
import dagger.BindsInstance;
import dagger.Subcomponent;
import org.cryptomator.common.Nullable;
import org.cryptomator.common.vaults.Vault;
import javafx.stage.Stage;
@@ -16,7 +17,14 @@ public interface KeyLoadingComponent {
@Subcomponent.Factory
interface Factory {
KeyLoadingComponent create(@BindsInstance @KeyLoading Vault vault, @KeyLoading @BindsInstance Stage window);
/**
* @param vaultRef the {@link KeyLoadingRef} containing the info to load the key
* @param vault the local vault, or {@code null} if it is not set up on this machine.
* @param window the window to show the key loading scenes in
*/
KeyLoadingComponent create(@BindsInstance @KeyLoading KeyLoadingRef vaultRef, //
@BindsInstance @KeyLoading @Nullable Vault vault, //
@BindsInstance @KeyLoading Stage window);
}
}
@@ -2,7 +2,6 @@ package org.cryptomator.ui.keyloading;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlLoaderFactory;
@@ -10,7 +9,6 @@ import org.cryptomator.ui.keyloading.hub.HubKeyLoadingModule;
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule;
import javax.inject.Provider;
import java.io.IOException;
import java.util.Map;
import java.util.ResourceBundle;
@@ -27,14 +25,10 @@ abstract class KeyLoadingModule {
@Provides
@KeyLoading
@KeyLoadingScoped
static KeyLoadingStrategy provideKeyLoadingStrategy(@KeyLoading Vault vault, Map<String, Provider<KeyLoadingStrategy>> strategies) {
try {
String scheme = vault.getVaultConfigCache().get().getKeyId().getScheme();
var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme));
return strategies.getOrDefault(scheme, () -> fallback).get();
} catch (IOException e) {
return KeyLoadingStrategy.failed(e);
}
static KeyLoadingStrategy provideKeyLoadingStrategy(@KeyLoading KeyLoadingRef vaultRef, Map<String, Provider<KeyLoadingStrategy>> strategies) {
String scheme = vaultRef.keyId().getScheme();
var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme));
return strategies.getOrDefault(scheme, () -> fallback).get();
}
}
@@ -0,0 +1,50 @@
package org.cryptomator.ui.keyloading;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.VaultConfig;
import java.io.IOException;
import java.net.URI;
/**
* Identifies the vault a key is being loaded for, independently of whether that vault exists on this machine.
* <p>
* Key loading needs the (unverified) vault config selecting the strategy and addresses
* the vault within the strategy, and the display name titles the windows.
* <p>
* The config is <em>unverified</em>: its signature is keyed on the masterkey, which is exactly what key loading is
* about to obtain.
*
* @param vaultConfig the vault's unverified config
* @param displayName the vault's name, as shown to the user
*/
public record KeyLoadingRef(VaultConfig.UnverifiedVaultConfig vaultConfig, String displayName) {
/**
* Describes a vault that is already set up on this machine.
*
* @param vault the vault to load a key for
* @throws IOException if the vault's config cannot be read
*/
public static KeyLoadingRef forVault(Vault vault) throws IOException {
return new KeyLoadingRef(vault.getVaultConfigCache().get(), vault.getDisplayName());
}
/**
* The key id, whose scheme selects the key loading strategy.
*/
public URI keyId() {
return vaultConfig.getKeyId();
}
/**
* The vault's id, i.e. how the vault is addressed within its Hub instance.
* <p>
* Read from the config's {@code jti} claim, which is the authoritative source: the key id carries the same id in its
* trailing path segment, but only its <em>scheme</em> is a source of truth here.
*/
public String vaultId() {
return vaultConfig.allegedVaultId();
}
}
@@ -7,7 +7,6 @@ import dagger.Provides;
import dagger.multibindings.IntoMap;
import dagger.multibindings.StringKey;
import org.cryptomator.common.settings.DeviceKey;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptolib.common.MessageDigestSupplier;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
@@ -15,13 +14,12 @@ import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingRef;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import javax.inject.Named;
import javafx.scene.Scene;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
@@ -32,19 +30,15 @@ public abstract class HubKeyLoadingModule {
@Provides
@KeyLoadingScoped
static HubConfig provideHubConfig(@KeyLoading Vault vault) {
try {
return vault.getVaultConfigCache().get().getHeader("hub", HubConfig.class);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
static HubConfig provideHubConfig(@KeyLoading KeyLoadingRef vaultRef) {
return vaultRef.vaultConfig().getHeader("hub", HubConfig.class);
}
@Provides
@KeyLoadingScoped
@Named("windowTitle")
static String provideWindowTitle(@KeyLoading Vault vault, ResourceBundle resourceBundle) {
return String.format(resourceBundle.getString("unlock.title"), vault.getDisplayName());
static String provideWindowTitle(@KeyLoading KeyLoadingRef vaultRef, ResourceBundle resourceBundle) {
return String.format(resourceBundle.getString("unlock.title"), vaultRef.displayName());
}
@@ -0,0 +1,59 @@
package org.cryptomator.ui.keyloading.hub;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.VaultConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Collection;
import java.util.Optional;
import java.util.UUID;
/**
* Locates Hub vaults among the vaults set up on this machine.
* <p>
* Hub manages a vault's key, not where it lives or how it is laid out, so the local vault list is the only place that
* can answer whether a given Hub vault is already set up here.
*/
public final class HubVaults {
private static final Logger LOG = LoggerFactory.getLogger(HubVaults.class);
private HubVaults() {
}
/**
* Finds the Hub vault with the given id.
* <p>
* A vault whose config cannot be read - e.g. because it sits on storage that is currently unavailable - is skipped
* rather than failing the lookup: one unreachable vault must not prevent finding a different one.
*
* @param vaults the vaults set up on this machine
* @param hubVaultId the vault's id within its Hub instance
* @return the local vault, or empty if none of them is that Hub vault
*/
public static Optional<Vault> findByVaultId(Collection<Vault> vaults, UUID hubVaultId) {
return vaults.stream() //
.filter(vault -> hasVaultId(vault, hubVaultId)) //
.findAny();
}
private static boolean hasVaultId(Vault vault, UUID hubVaultId) {
try {
return hasVaultId(vault.getVaultConfigCache().get(), hubVaultId);
} catch (IOException e) {
LOG.debug("Skipping vault {} while looking for hub vault {}, its config is not readable.", vault.getPath(), hubVaultId);
return false;
}
}
private static boolean hasVaultId(VaultConfig.UnverifiedVaultConfig config, UUID hubVaultId) {
var keyIdScheme = config.getKeyId().getScheme();
if (keyIdScheme == null || !keyIdScheme.startsWith(HubKeyLoadingStrategy.SCHEME_PREFIX)) {
return false; //not a hub vault, so it cannot be the one we are looking for
}
return hubVaultId.toString().equalsIgnoreCase(config.allegedVaultId());
}
}
@@ -7,12 +7,12 @@ import com.google.common.base.Preconditions;
import com.nimbusds.jose.JWEObject;
import dagger.Lazy;
import org.cryptomator.common.Constants;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.dialogs.Dialogs;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingRef;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -60,10 +60,10 @@ public class ReceiveKeyController implements FxController {
private final Lazy<Scene> invalidLicenseScene;
private final HttpClient httpClient;
private final Dialogs dialogs;
private final Vault vault;
private final KeyLoadingRef vaultRef;
@Inject
public ReceiveKeyController(@KeyLoading Vault vault, //
public ReceiveKeyController(@KeyLoading KeyLoadingRef vaultRef, //
ExecutorService executor, //
@KeyLoading Stage window, //
HubConfig hubConfig, //
@@ -79,7 +79,7 @@ public class ReceiveKeyController implements FxController {
Dialogs dialogs) {
this.window = window;
this.hubConfig = hubConfig;
this.vaultId = extractVaultId(vault.getVaultConfigCache().getUnchecked().getKeyId()); // TODO: access vault config's JTI directly (requires changes in cryptofs)
this.vaultId = vaultRef.vaultId();
this.deviceId = deviceId;
this.bearerToken = Objects.requireNonNull(tokenRef.get());
this.fsOwnerId = fsOwnerId;
@@ -92,7 +92,7 @@ public class ReceiveKeyController implements FxController {
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
this.httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).executor(executor).build();
this.dialogs = dialogs;
this.vault = vault;
this.vaultRef = vaultRef;
}
@FXML
@@ -313,7 +313,7 @@ public class ReceiveKeyController implements FxController {
private void accessGoneVaultArchived() {
window.close();
dialogs.prepareHubVaultArchived((Stage)window.getOwner(), vault).build().showAndWait();
dialogs.prepareHubVaultArchived((Stage)window.getOwner(), vaultRef.displayName()).build().showAndWait();
}
private void accountInitializationRequired() {
@@ -343,12 +343,6 @@ public class ReceiveKeyController implements FxController {
}
}
private static String extractVaultId(URI vaultKeyUri) {
assert vaultKeyUri.getScheme().startsWith(HubKeyLoadingStrategy.SCHEME_PREFIX);
var path = vaultKeyUri.getPath();
return path.substring(path.lastIndexOf('/') + 1);
}
@JsonIgnoreProperties(ignoreUnknown = true)
private record UserDto(@JsonProperty(value = "name", required = true) String name) {}
@@ -1,5 +1,6 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import org.cryptomator.common.Nullable;
import org.cryptomator.common.recovery.RecoveryActionType;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
@@ -18,6 +19,7 @@ import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import java.io.File;
import java.nio.file.Path;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
@@ -41,12 +43,12 @@ public class ChooseMasterkeyFileController implements FxController {
@Inject
public ChooseMasterkeyFileController(@KeyLoading Stage window, //
@KeyLoading Vault vault, //
@KeyLoading @Nullable Vault vault, //
CompletableFuture<Path> result, //
RecoveryKeyComponent.Factory recoveryKeyWindow, //
ResourceBundle resourceBundle) {
this.window = window;
this.vault = vault;
this.vault = Objects.requireNonNull(vault, MasterkeyFileLoadingModule.NO_LOCAL_VAULT);
this.result = result;
this.recoveryKeyWindow = recoveryKeyWindow;
this.resourceBundle = resourceBundle;
@@ -5,6 +5,7 @@ import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import dagger.multibindings.StringKey;
import org.cryptomator.common.Nullable;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.integrations.keychain.KeychainAccessException;
@@ -15,20 +16,27 @@ import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.slf4j.LoggerFactory;
import javax.inject.Named;
import java.util.Objects;
import java.util.Optional;
@Module(subcomponents = {ForgetPasswordComponent.class, PassphraseEntryComponent.class, ChooseMasterkeyFileComponent.class})
public interface MasterkeyFileLoadingModule {
/**
* Key loading may run for a yet-to-setup vault (i.e. deeplink with only a config) - Masterkey loading requires in the current implementation
* an already setup vault.
*/
String NO_LOCAL_VAULT = "masterkey file loading requires a local vault";
@Provides
@Named("savedPassword")
@KeyLoadingScoped
static Optional<char[]> provideStoredPassword(KeychainManager keychain, @KeyLoading Vault vault) {
static Optional<char[]> provideStoredPassword(KeychainManager keychain, @KeyLoading @Nullable Vault vault) {
if (!keychain.isSupported() || keychain.isLocked()) {
return Optional.empty();
} else {
try {
return Optional.ofNullable(keychain.loadPassphrase(vault.getId()));
return Optional.ofNullable(keychain.loadPassphrase(Objects.requireNonNull(vault, NO_LOCAL_VAULT).getId()));
} catch (KeychainAccessException e) {
LoggerFactory.getLogger(MasterkeyFileLoadingModule.class).error("Failed to load entry from system keychain.", e);
return Optional.empty();
@@ -2,6 +2,7 @@ package org.cryptomator.ui.keyloading.masterkeyfile;
import com.google.common.base.Preconditions;
import org.cryptomator.common.Constants;
import org.cryptomator.common.Nullable;
import org.cryptomator.common.Passphrase;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
@@ -25,6 +26,7 @@ import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.CancellationException;
@@ -48,8 +50,8 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
private boolean wrongPassphrase;
@Inject
public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Stage window, @Named("savedPassword") Optional<char[]> savedPassphrase, PassphraseEntryComponent.Builder passphraseEntry, ChooseMasterkeyFileComponent.Builder masterkeyFileChoice, KeychainManager keychain, ResourceBundle resourceBundle) {
this.vault = vault;
public MasterkeyFileLoadingStrategy(@KeyLoading @Nullable Vault vault, MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Stage window, @Named("savedPassword") Optional<char[]> savedPassphrase, PassphraseEntryComponent.Builder passphraseEntry, ChooseMasterkeyFileComponent.Builder masterkeyFileChoice, KeychainManager keychain, ResourceBundle resourceBundle) {
this.vault = Objects.requireNonNull(vault, MasterkeyFileLoadingModule.NO_LOCAL_VAULT);
this.masterkeyFileAccess = masterkeyFileAccess;
this.window = window;
this.passphraseEntry = passphraseEntry;
@@ -35,6 +35,7 @@ import javafx.scene.transform.Translate;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import javafx.util.Duration;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
@@ -66,9 +67,9 @@ public class PassphraseEntryController implements FxController {
public Animation unlockAnimation;
@Inject
public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, CompletableFuture<PassphraseEntryResult> result, @Nullable @Named("savedPassword") Passphrase savedPassword, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain, ExecutorService backgroundExecutorService) {
public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading @Nullable Vault vault, CompletableFuture<PassphraseEntryResult> result, @Nullable @Named("savedPassword") Passphrase savedPassword, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain, ExecutorService backgroundExecutorService) {
this.window = window;
this.vault = vault;
this.vault = Objects.requireNonNull(vault, MasterkeyFileLoadingModule.NO_LOCAL_VAULT);
this.result = result;
this.savedPassword = savedPassword;
this.forgetPassword = forgetPassword;
@@ -14,6 +14,7 @@ import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.keyloading.KeyLoadingComponent;
import org.cryptomator.ui.keyloading.KeyLoadingRef;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
import org.jetbrains.annotations.Nullable;
@@ -25,6 +26,7 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.Map;
import java.util.ResourceBundle;
@@ -58,7 +60,11 @@ abstract class UnlockModule {
@UnlockWindow
@UnlockScoped
static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Factory compFactory, @UnlockWindow Vault vault, @UnlockWindow Stage window) {
return compFactory.create(vault, window).keyloadingStrategy();
try {
return compFactory.create(KeyLoadingRef.forVault(vault), vault, window).keyloadingStrategy();
} catch (IOException e) {
return KeyLoadingStrategy.failed(e);
}
}
@Provides
@@ -18,11 +18,6 @@ generic.button.print=طباعة
generic.button.remove=حذف
## Vault state
vault.state.locked=مقفلة
vault.state.unlocked=مفتوحة
vault.state.missing=مفقودة
vault.state.migrationNeeded=الترحيل مطلوب
vault.state.processing=جارٍ المعالجة
vault.state.error=خطأ
# Error
@@ -105,17 +100,9 @@ addvault.new.readme.accessLocation.4=لا تتردد في إزالة هذا ال
## Existing
addvaultwizard.existing.title=أضِف مخزن موجود
addvaultwizard.existing.instruction=اختر ملف "vault.cryptomator" من خزينتك الحالية. إذا كان يوجد فقط ملف اسمه "masterkey.cryptomator"، اختره عوضاً عنه.
addvaultwizard.existing.restore=استعادة…
addvaultwizard.existing.chooseBtn=اختر…
addvaultwizard.existing.filePickerTitle=حدد مِلَفّ المخزن
addvaultwizard.existing.filePickerMimeDesc=مخزن Cryptomator
addvaultwizard.existing.notAVault.title=ليست خزنة
addvaultwizard.existing.notAVault.message=المجلد المحدد ليس خزنة Cryptomator
addvaultwizard.existing.notAVault.description.missingDataDir=المجلد الفرعي المطلوب "d" مفقود داخل "%s".
addvaultwizard.existing.notAVault.description.dataNotADirectory=العنصر "d" داخل "%s" ليس مجلدًا.
addvaultwizard.existing.notAVault.description.missingVaultConfig=الملف المطلوب "vault.cryptomator" مفقود داخل "%s".
addvaultwizard.existing.notAVault.description.vaultConfigAccessDenied=تعذّر قراءة الملف "vault.cryptomator" داخل "%s" بسبب حقوق وصول غير كافية.
addvaultwizard.existing.notAVault.description.unsupportedStructure=بنية الدليل لـ "%s" غير مدعومة.
## Success
addvaultwizard.success.nextStepsInstructions=تم إضافة مخزن "%s".\nتحتاج إلى فتح هذا المخزن للوصول أو إضافة محتويات إليه. بدلاً من ذلك، يمكنك فتحه في أي وقت لاحق.
addvaultwizard.success.unlockNow=افتح الان
@@ -144,7 +131,6 @@ unlock.unlockBtn=افتح
## Select
unlock.chooseMasterkey.message=لم يتم العثور على مفتاح التشفير الرئيسي
unlock.chooseMasterkey.description=لم يتمكن Cryptomator من العثور على مفتاح التشفير الرئيسي للمخزن "%s". الرجاء اختيار الملف يدوياً.
unlock.chooseMasterkey.restoreInstead=استعادة ملف المفتاح الرئيسي بدلاً من ذلك
unlock.chooseMasterkey.filePickerTitle=اختر ملف الـ Masterkey
unlock.chooseMasterkey.filePickerMimeDesc=مفتاح التشفير الرئيسي في Cryptomator
## Success
@@ -169,14 +155,7 @@ hub.noKeychain.message=غير قادر على الوصول إلى مفتاح ا
hub.noKeychain.description=من أجل فتح مركز مخازن، مطلوب مفتاح جهاز، يتم تأمينه باستخدام سلسلة مفاتيح. للمضي قدما، قم بتمكين "%s" واختر سلسلة مفاتيح في التفضيلات.
hub.noKeychain.openBtn=فتح التفضيلات
### Check Host Authenticity
hub.checkHostTrust.message.check=جارٍ التحقق من الإعدادات…
hub.checkHostTrust.message.ask=هل تثق بهذا المضيف؟
hub.checkHostTrust.message.ask.plural=هل تثق بهذه المضيفين؟
hub.checkHostTrust.description.ask=لا يتعرف Cryptomator على هذا المضيف. لفتح الخزنة، يجب الوثوق به. لا تثق به إلا إذا كنت تتعرف على العنوان أدناه.
hub.checkHostTrust.description.ask.plural=لا يتعرف Cryptomator على هذه المضيفين. لفتح الخزنة، يجب الوثوق بها. لا تثق بها إلا إذا كنت تتعرف على العناوين أدناه.
hub.checkHostTrust.copyBtn.tooltip=نسخ العنوان إلى الحافظة
hub.checkHostTrust.trustBtn=ثق
hub.checkHostTrust.denyBtn=رفض
### Waiting
hub.auth.message=جاري الانتظار للمصادقة…
hub.auth.description=يجب أن يتم إعادة توجيهك تلقائياً إلى صفحة تسجيل الدخول.
@@ -203,14 +182,10 @@ hub.registerFailed.message=فشل تسجيل الجهاز
hub.registerFailed.description.generic=حدث خطأ في عملية تسجيل الاسم. لمزيد من التفاصيل، راجع سجل التطبيق.
hub.registerFailed.description.deviceAlreadyExists=هذا الجهاز مسجل لمستخدم مختلف بالفعل. حاول تغيير حساب المستخدم أو استخدام جهاز مختلف.
### Archived
hub.archived.message=الخزنة مؤرشفة
hub.archived.description=تمت أرشفة هذه الخزنة ولم تعد قابلة للوصول. يرجى التواصل مع مالك الخزنة.
### Unauthorized
hub.unauthorized.message=تم رفض الوصول
hub.unauthorized.description=غير مسموح لك بفتح هذا المستودع. اتصل بمالك المستودع لطلب الوصول.
### Untrusted Host
hub.untrustedHost.message=المضيف غير موثوق
hub.untrustedHost.description=تم حظر الاتصال بـ Hub من أجل أمانك. إذا كنت تعتقد أن مضيف Hub آمن، فتواصل مع مسؤول Hub أو حاول مرة أخرى.
### Requires Account Initialization
hub.requireAccountInit.message=مطلوب اتخاذ إجراء
hub.requireAccountInit.description.0=للمتابعة، يرجى إكمال الخطوات المطلوبة في
@@ -288,8 +263,6 @@ health.check.detail.checkFinishedAndFound=تم انتهاء الفحص. فضلا
health.check.detail.checkFailed=الفحص توقف بسبب خطأ.
health.check.detail.checkCancelled=تم إلغاء الفحص.
health.check.detail.listFilters.label=التصفية
health.check.detail.filterSeverity=تصفية حسب الخطورة
health.check.detail.filterFixState=تصفية حسب حالة الإصلاح
health.check.detail.fixAllSpecificBtn=إصلاح كل الأنواع
health.check.exportBtn=تصدير التقرير
## Result view
@@ -324,7 +297,6 @@ preferences.general.debugDirectory=عرض ملفات السجل
preferences.general.autoStart=تشغيل Cryptomator عند بدء تشغيل النظام
preferences.general.keychainBackend=تخزين كلمات المرور مع
preferences.general.quickAccessService=إضافة الخزانات المفتوحة إلى منطقة الوصول السريع
preferences.general.resetTrustedHosts=إعادة تعيين المضيفين الموثوقين
## Interface
preferences.interface=الواجهة
preferences.interface.theme=الشكل والمظهر
@@ -363,13 +335,8 @@ preferences.updates.lastUpdateCheck.never=مطلقاً
preferences.updates.lastUpdateCheck.recently=مؤخراً
preferences.updates.lastUpdateCheck.daysAgo=منذ %s يوم
preferences.updates.lastUpdateCheck.hoursAgo=منذ %s ساعة
preferences.updates.prohibitedDueToUnlockedVaults.1=يرجى
preferences.updates.prohibitedDueToUnlockedVaults.2=قفل خزائنك
preferences.updates.prohibitedDueToUnlockedVaults.3=لتثبيت التحديث.
preferences.updates.checkFailed=فشل في البحث عن تحديثات البرنامج تحقق من اتصالك بالإنترنت أو حاول مرة أخرى لاحقاً.
preferences.updates.updateFailed=فشل التحديث. يرجى تثبيت التحديث يدويًا.
preferences.updates.upToDate=Cryptomator محدث لاخر نسخة.
preferences.updates.visitDownloadPage=زيارة صفحة التنزيل
## Contribution
preferences.contribute=ادعمنا
@@ -380,7 +347,6 @@ preferences.contribute.promptText=قم بلصق رمز شهادة الداعم
preferences.contribute.thankYou=نشكرك على دعمك لتطوير Cryptomator مفتوح المصدر!
preferences.contribute.donate=تبرع
preferences.contribute.sponsor=الراعي
preferences.contribute.removeCert.tooltip=إزالة الشهادة
### Remove License Key Dialog
removeCert.title=إزالة الشهادة
@@ -390,7 +356,6 @@ removeCert.description=الميزات الأساسية لـ Cryptomator غير
## About
preferences.about=حول البرنامج
preferences.about.thirdPartyLicenses=تراخيص الأطراف الثالثة
# Vault Statistics
stats.title=إحصائيات عن %s
@@ -430,7 +395,6 @@ stats.access.total=مجموع الوصول: %d
# Main Window
## Vault List
main.vaultlist=المخازن
main.vaultlist.listEntry=الخزنة %s (%s)
main.vaultlist.emptyList.onboardingInstruction=انقر هنا لإضافة خزنة
main.vaultlist.contextMenu.remove=حذف…
main.vaultlist.contextMenu.lock=قفل
@@ -439,20 +403,13 @@ main.vaultlist.contextMenu.unlockNow=افتح الان
main.vaultlist.contextMenu.vaultoptions=إظهار خيارات المخزن
main.vaultlist.contextMenu.reveal=اظهار القرص
main.vaultlist.contextMenu.share=مشاركة…
main.vaultlist.addVaultBtn.menuItemNew=إنشاء خزنة جديدة…
main.vaultlist.addVaultBtn.menuItemExisting=فتح خزنة موجودة…
main.vaultlist.addVaultBtn.menuItemRecover=استعادة خزنة موجودة…
main.vaultlist.addVaultButton.tooltip=أضِف مخزنًا
main.vaultlist.showEventsButton.tooltip=فتح عرض الأحداث
main.vaultlist.showPreferencesButton.tooltip=إظهار التفضيلات
##Notification
main.notification.updateAvailable=هناك تحديث متاح.
main.notification.support=دعم Cryptomator.
main.notification.closeButton.tooltip=إغلاق شريط المعلومات
## Vault Detail
### Welcome
main.vaultDetail.welcomeOnboarding=شكرا لاختيار Cryptomator لحماية ملفاتك. إذا كنت بحاجة إلى أية مساعدة، تحقق من دليل وتعليمات الإستخدام:
main.vaultDetail.storageLocation=موقع تخزين الخزنة
### Locked
main.vaultDetail.lockedStatus=مغلق
main.vaultDetail.unlockBtn=فتح…
@@ -484,8 +441,6 @@ main.vaultDetail.missing.recheck=إعادة الفحص
main.vaultDetail.missing.remove=حذف من قائمة الخزنات…
main.vaultDetail.missing.changeLocation=تغيير موقع الخزنة…
### Missing Vault Config
main.vaultDetail.missingVaultConfig.info=إعدادات الخزنة مفقودة.
main.vaultDetail.missingVaultConfig.restore=استعادة إعدادات الخزنة
### Needs Migration
main.vaultDetail.migrateButton=ترقية الحافظة
main.vaultDetail.migratePrompt=يجب ترقية المخزن الخاص بك إلى تنسيق جديد، قبل أن تتمكن من الوصول إليه
@@ -510,7 +465,6 @@ vaultOptions.general=عام
vaultOptions.general.vaultName=اسم المخزن
vaultOptions.general.autoLock.lockAfterTimePart1=قفل عند الخمول لمدة
vaultOptions.general.autoLock.lockAfterTimePart2=دقائق
vaultOptions.general.autoLock.accessibleText=مهلة القفل بالدقائق
vaultOptions.general.unlockAfterStartup=فتح قفل المخزن عند بدء تشغيل Cryptomator
vaultOptions.general.actionAfterUnlock=بعد فتح القفل بنجاح
vaultOptions.general.actionAfterUnlock.ignore=‮لا تفعل شيئاً
@@ -541,7 +495,6 @@ vaultOptions.masterkey.forgetSavedPasswordBtn=نسيان كلمة المرور
vaultOptions.masterkey.recoveryKeyExplanation=مفتاح الاسترداد هو وسيلتك الوحيدة لاستعادة الوصول إلى مخزنك إذا فقدت كلمة المرور.
vaultOptions.masterkey.showRecoveryKeyBtn=عرض مفتاح الاسترداد
vaultOptions.masterkey.recoverPasswordBtn=إعادة تعيين كلمة المرور
vaultOptions.masterkey.missingMasterkeyFile=هذه الخيارات متاحة فقط إذا كان ملف المفتاح الرئيسي موجودًا في دليل الخزنة.
## Hub
vaultOptions.hub=الإسترداد
vaultOptions.hub.convertInfo=يمكنك استخدام مفتاح الاسترداد لتحويل هذه خزانة Hub إلى خزانة تعتمد على كلمة مرور في حالة الطوارئ.
@@ -552,7 +505,6 @@ vaultOptions.hub.convertBtn=التحويل إلى خزانة قائمة على
recoveryKey.display.title=إظهار مفتاح الاسترداد
recoveryKey.create.message=مطلوب كلمة المرور
recoveryKey.create.description=أدخل كلمة المرور الخاصة بك لإظهار مفتاح الاسترداد ل "%s":
recoveryKey.recover.description=أدخل كلمة المرور لـ "%s" لاستعادة إعدادات الخزنة.
recoveryKey.display.description=يمكن استخدام مفتاح الاسترداد التالي لاستعادة الوصول إلى "%s":
recoveryKey.display.StorageHints=حافظ عليه في مكان ما آمن جداً، على سبيل المثال\n • تخزينه باستخدام مدير كلمات المرور\n • حفظه على محرك أقراص خارجي USB\n • كتابته أو طباعته على الورق
## Reset Password
@@ -565,59 +517,29 @@ recoveryKey.recover.invalidKey=مفتاح الاسترداد غير صالح
recoveryKey.printout.heading=مفتاح استرداد Cryptomator\n"%s"\n
### Reset Password
recoveryKey.recover.resetBtn=إعادة الضبط
recoveryKey.recover.recoverBtn=استعادة
### Recovery Key Password Reset Success
recoveryKey.recover.resetSuccess.message=تم إعادة تعيين كلمة المرور بنجاح
recoveryKey.recover.resetSuccess.description=يمكنك فتح الخزانة الخاصة بك بكلمة المرور الجديدة.
### Recovery Key Vault Config Reset Success
recoveryKey.recover.resetVaultConfigSuccess.message=تمت استعادة إعدادات الخزنة
recoveryKey.recover.resetMasterkeyFileSuccess.message=تمت استعادة ملف المفتاح الرئيسي
recoveryKey.recover.resetMasterkeyFileSuccess.description=يمكنك الآن فتح خزنتك باستخدام كلمة المرور.
# Recover Vault Config File and/or Masterkey
##Add Existing Vault without recovery - Dialog
recover.existing.title=تمت إضافة الخزنة
recover.existing.message=تمت إضافة الخزنة بنجاح
recover.existing.description=تمت إضافة خزنتك "%s" إلى قائمة الخزائن. لم تكن هناك حاجة لعملية استعادة.
##Vault Already Exists - Dialog
recover.alreadyExists.title=الخزنة موجودة بالفعل
recover.alreadyExists.message=تمت إضافة هذه الخزنة بالفعل
recover.alreadyExists.description=خزنتك "%s" موجودة بالفعل في قائمة الخزائن، لذلك لم تتم إضافتها مرة أخرى.
##Invalid Selection - Dialog
recover.invalidSelection.title=تحديد غير صالح
recover.invalidSelection.message=تحديدك ليس خزنة
recover.invalidSelection.description=يجب أن يكون المجلد المحدد خزنة Cryptomator صالحة.
## Contact Hub Vault Owner - Dialog
contactHubVaultOwner.title=خزانة Hub
contactHubVaultOwner.message=تم إنشاء هذه الخزنة باستخدام Cryptomator Hub
contactHubVaultOwner.description=يرجى التواصل مع مالك الخزنة لاستعادة الملف المفقود. يمكنه تنزيل قالب الخزنة من Cryptomator Hub.
##Dialog Title
recover.recoverVaultConfig.title=استعادة إعدادات الخزنة
recover.recoverMasterkey.title=استعادة المفتاح الرئيسي
## OnBoarding
recover.onBoarding.chooseMethod=اختر طريقة الاستعادة:
recover.onBoarding.useRecoveryKey=استخدام مفتاح الاستعادة
recover.onBoarding.usePassword=استخدام كلمة المرور
recover.onBoarding.intro=تأكد من التحقق مما يلي:
recover.onBoarding.pleaseConfirm=قبل المتابعة، يرجى تأكيد ما يلي:
recover.onBoarding.otherwisePleaseConfirm=وإلا، يرجى تأكيد ما يلي:
recover.onBoarding.allMissing.intro=إذا كانت هذه الخزنة مُدارة بواسطة Cryptomator Hub، فيجب على مالك الخزنة استعادتها لك.
recover.onBoarding.intro.ensure=جميع الملفات متزامنة بالكامل.
recover.onBoarding.affirmation=لقد قرأت هذه المتطلبات وفهمتها
###Vault Config Missing
recover.onBoarding.intro.recoveryKey=لديك مفتاح الاستعادة وتعرف ما إذا تم استخدام الإعدادات المتقدمة.
recover.onBoarding.intro.password=لديك كلمة مرور الخزنة وتعرف ما إذا تم استخدام الإعدادات المتقدمة.
###Masterkey Missing
recover.onBoarding.intro.masterkey.recoveryKey=لديك مفتاح استعادة الخزنة.
## Expert Settings
recover.expertSettings.shorteningThreshold.title=يجب أن تتطابق هذه القيمة مع القيمة المستخدمة قبل الاستعادة لضمان التوافق مع البيانات المشفّرة سابقًا.
# Convert Vault
convertVault.title=تحويل الخزانة
@@ -695,8 +617,6 @@ decryptNames.filePicker.title=اختر ملَف مشفر
decryptNames.filePicker.extensionDescription=ملف مشفر
decryptNames.copyTable.tooltip=نسخ الجدول
decryptNames.clearTable.tooltip=مسح الجدول
decryptNames.column.encrypted=مشفّر
decryptNames.column.decrypted=مفكوك التشفير
decryptNames.copyHint=نسخ محتوى الخلية مع %s
decryptNames.dropZone.message=إسقاط الملفات أو انقر لتحديد
decryptNames.dropZone.error.vaultInternalFiles=مخزن الملفات الداخلية مع عدم تحديد اسم قابل للتشفير
@@ -709,8 +629,6 @@ decryptNames.dropZone.error.generic=فشل فك تشفير أسماء الملف
eventView.title=أحداث
eventView.filter.allVaults=الكل
eventView.clearListButton.tooltip=تفريغ القائمة
eventView.filterVaults=تصفية حسب الخزنة
eventView.cell.actionsButton.tooltip=إجراءات الأحداث
## event list entries
eventView.entry.vaultLocked.description=فتح "%s" للحصول على التفاصيل
eventView.entry.conflictResolved.message=تم حل التضارب
@@ -725,15 +643,9 @@ eventView.entry.brokenDirFile.showEncrypted=إظهار الرابط المكسو
eventView.entry.brokenFileNode.message=عقدة ملفات النظام التافلة
eventView.entry.brokenFileNode.showEncrypted=عرض العقدة المشفّرة التافلة
eventView.entry.brokenFileNode.copyDecrypted=نسخ المسار غير المشفر
eventView.entry.inUse.message=الملف قيد الاستخدام
eventView.entry.inUse.showDecrypted=إظهار الملف غير المشفر
eventView.entry.inUse.showEncrypted=عرض ملَف المشفر
eventView.entry.inUse.copyUserAndDevice=نسخ اسم المستخدم والجهاز القافل
eventView.entry.inUse.ignoreLock=تجاهل حالة الاستخدام
# Notifications
## FileIsInUse Notification
notification.inUse.message=الملف قيد الاستخدام على جهاز آخر
notification.inUse.description=الملف مفتوح بواسطة %s على %s. اطلب منهم إغلاق الملف والسماح للمزامنة بالانتهاء. يمكنك تجاهل الحالة لفتحه الآن، لكن قد يؤدي ذلك إلى تعارضات أو الكتابة فوق تغييرات أحدث.
notification.inUse.action=تجاهل حالة الاستخدام
@@ -176,8 +176,6 @@ hub.register.registerBtn=Регистриране
hub.registerSuccess.unlockBtn=Отключване
### Registration Failed
### Archived
hub.archived.message=Хранили е архивиран
hub.archived.description=Това хранилище е архивирано и вече не е достъпно. Моля, свържете се със собственика на хранилището.
### Unauthorized
hub.unauthorized.message=Отказан достъп
### Untrusted Host
@@ -563,7 +561,6 @@ dokanySupportEnd.preferencesBtn=Към настройките
# Event View
eventView.filter.allVaults=Всички
## event list entries
@@ -172,9 +172,6 @@ hub.noKeychain.openBtn=Åbn Indstillinger
hub.checkHostTrust.message.check=Kontrollerer konfiguration…
hub.checkHostTrust.message.ask=Stol på denne vært?
hub.checkHostTrust.message.ask.plural=Stol på disse værter?
hub.checkHostTrust.description.ask=Cryptomator genkender ikke denne vært. For at låse boksen op, skal den have tillid til den. Stol kun på den, hvis du genkender adressen nedenfor.
hub.checkHostTrust.description.ask.plural=Cryptomator genkender ikke denne vært. For at låse boksen op, skal den have tillid til den. Stol kun på den, hvis du genkender adressen nedenfor.
hub.checkHostTrust.copyBtn.tooltip=Kopiér adresse til udklipsholder
hub.checkHostTrust.trustBtn=Hav tillid
hub.checkHostTrust.denyBtn=Afvis
### Waiting
@@ -158,7 +158,6 @@ hub.noKeychain.message=Zugriff auf Geräteschlüssel nicht möglich
hub.noKeychain.description=Zum Entsperren von Hub-Tresoren wird ein Geräteschlüssel benötigt, der in einem Schlüsselbund gesichert ist. Um fortzufahren, aktiviere „%s“ und wähle in den Einstellungen einen Schlüsselbund.
hub.noKeychain.openBtn=Einstellungen öffnen
### Check Host Authenticity
hub.checkHostTrust.copyBtn.tooltip=Adresse in die Zwischenablage kopieren
hub.checkHostTrust.trustBtn=Vertrauen
### Waiting
hub.auth.message=Warten auf Authentifizierung …
@@ -172,9 +172,6 @@ hub.noKeychain.openBtn=Ouvrir les préférences
hub.checkHostTrust.message.check=Vérification de configuration…
hub.checkHostTrust.message.ask=Faire confiance à cet hôte ?
hub.checkHostTrust.message.ask.plural=Faire confiance à ces hôtes ?
hub.checkHostTrust.description.ask=Cryptomator ne reconnaît pas cet hôte. Pour déverrouiller le coffre, il faut lui faire confiance. Ne lui faites confiance que si vous reconnaissez l'adresse ci-dessous.
hub.checkHostTrust.description.ask.plural=Cryptomator ne reconnaît pas ces hôtes. Pour déverrouiller le coffre, il faut leur faire confiance. Ne leur faites confiance que si vous reconnaissez les adresses ci-dessous.
hub.checkHostTrust.copyBtn.tooltip=Copier ladresse dans le presse-papier
hub.checkHostTrust.trustBtn=Approuver
hub.checkHostTrust.denyBtn=Refuser
### Waiting
@@ -127,7 +127,7 @@ removeVault.description=Ez kizárólag a Cryptomator listájából távolítja e
# Change Password
changepassword.title=Jelszó megváltoztatása
changepassword.enterOldPassword=Írja be a jelenlegi jelszót a következő tárolóhoz: "%s"
changepassword.enterOldPassword=Írja be a jelenlegi jelszavat a következő széfhez "%s"
changepassword.finalConfirmation=Megértettem, hogy nem fogok hozzáférni az adataimhoz amennyiben elfelejtem a jelszavam
# Forget Password
@@ -138,7 +138,7 @@ forgetPassword.confirmBtn=Jelszó elfelejtése
# Unlock
unlock.title="%s" feloldása
unlock.passwordPrompt=Írja be a jelszavát a következő tárolóhoz "%s":
unlock.passwordPrompt=Írja be a jelszavát a következő széfhez "%s":
unlock.savePassword=Jelszó megjegyzése
unlock.unlockBtn=Feloldás
## Select
@@ -151,7 +151,7 @@ unlock.chooseMasterkey.filePickerMimeDesc=Cryptomator Mesterkulcs
unlock.success.message=Sikeres feloldás
unlock.success.description=Mostantól elérhető a csatolási pontján keresztül a következő tároló tartalma: "%s".
unlock.success.rememberChoice=Jegyezze meg a választást és ne mutassa többet
unlock.success.revealBtn=Tároló megjelenítése
unlock.success.revealBtn=Széf megjelenítése
## Failure
unlock.error.customPath.message=Nem lehet csatolni a tárolót az egyéni útvonalhoz
unlock.error.customPath.description.notSupported=Ha szeretné továbbra is az egyéni útvonalat használni, kérem, menjen a beállításokba és válasszon egy kötet típust, amely támogatja azt. Máskülönben, menjen a tároló beállításaihoz és válasszon egy támogatott csatoláspontot.
@@ -426,7 +426,7 @@ stats.access.total=Összes hozzáférés: %d
# Main Window
## Vault List
main.vaultlist=Titkosított tárolók
main.vaultlist=Széfek
main.vaultlist.listEntry=Titkosított tároló %s (%s)
main.vaultlist.emptyList.onboardingInstruction=Kattintson ide egy titkosított tároló hozzáadásához
main.vaultlist.contextMenu.remove=Eltávolítás…
@@ -439,7 +439,7 @@ main.vaultlist.contextMenu.share=Megosztás…
main.vaultlist.addVaultBtn.menuItemNew=Új titkosított tároló létrehozása…
main.vaultlist.addVaultBtn.menuItemExisting=Titkosított tároló megnyitása…
main.vaultlist.addVaultBtn.menuItemRecover=Titkosított tároló visszaállítása…
main.vaultlist.addVaultButton.tooltip=Titkosított tároló hozzáadása
main.vaultlist.addVaultButton.tooltip=Széf hozzáadása
main.vaultlist.showEventsButton.tooltip=Eseménynapló megnyitása
main.vaultlist.showPreferencesButton.tooltip=Beállítások megnyitása
##Notification
@@ -588,7 +588,7 @@ recover.invalidSelection.message=A kiválasztott elem nem titkosított tároló
recover.invalidSelection.description=A kiválasztott mappának érvényes Cryptomator tárolónak kell lennie.
## Contact Hub Vault Owner - Dialog
contactHubVaultOwner.title=Hub tároló
contactHubVaultOwner.title=Hub széf
contactHubVaultOwner.message=Ezt a titkosító tárolót a Cryptomator Hub segítségével hozták létre
contactHubVaultOwner.description=Kérjük, vegye fel a kapcsolatot a tároló tulajdonosával a hiányzó fájl helyreállítása érdekében. A tároló-sablont a Cryptomator Hubról töltheti le.
@@ -682,7 +682,7 @@ shareVault.visitHub=Látogassa meg a Cryptomator Hubot
shareVault.hub.message=Hogyan osszon meg egy Hub-tárolót
shareVault.hub.description=Ahhoz, hogy megossza a tároló tartalmát egy másik csapattaggal, két lépést kell végrehajtania:
shareVault.hub.instruction.1=1. Ossza meg a titkosított tároló mappájának hozzáférését felhőalapú tárolón keresztül.
shareVault.hub.instruction.1=Ossza meg a titkosított széf mappájának hozzáférését felhőalapú tárolón keresztül.
shareVault.hub.instruction.2=2. Adjon hozzáférést a csapattagnak a Cryptomator Hubban.
shareVault.hub.openHub=Nyissa meg a Cryptomator Hubot
@@ -172,9 +172,6 @@ hub.noKeychain.openBtn=Apri Preferenze
hub.checkHostTrust.message.check=Controllo Configurazione…
hub.checkHostTrust.message.ask=Ti fidi di questo host?
hub.checkHostTrust.message.ask.plural=Ti fidi di questi host?
hub.checkHostTrust.description.ask=Cryptomator non riconosce questo host. Per sbloccare la cassaforte, esso deve essere affidabile. Rendilo affidabile solo se riconosci l'indirizzo qui sotto.
hub.checkHostTrust.description.ask.plural=Cryptomator non riconosce questi host. Per sbloccare la cassaforte, essi devono essere affidabili. Rendili affidabili solo se riconosci gli indirizzi qui sotto.
hub.checkHostTrust.copyBtn.tooltip=Copia indirizzo negli appunti
hub.checkHostTrust.trustBtn=Fidati
hub.checkHostTrust.denyBtn=Rifiuta
### Waiting
@@ -400,7 +400,6 @@ main.vaultlist.contextMenu.unlockNow=今すぐ解錠
main.vaultlist.contextMenu.vaultoptions=金庫のオプションを表示
main.vaultlist.contextMenu.reveal=ドライブを表示
main.vaultlist.contextMenu.share=共有…
main.vaultlist.addVaultBtn.menuItemNew=新しい金庫を作成…
main.vaultlist.addVaultButton.tooltip=金庫を追加
##Notification
main.notification.updateAvailable=アップデートがあります。
@@ -629,10 +628,7 @@ decryptNames.dropZone.error.generic=ファイル名の復号化に失敗しま
# Event View
eventView.title=イベント
eventView.filter.allVaults=すべて
## event list entries
eventView.entry.decryptionFailed.showEncrypted=暗号化されたファイルを表示
eventView.entry.inUse.showDecrypted=復号化されたファイルを表示
eventView.entry.inUse.showEncrypted=暗号化されたファイルを表示
eventView.entry.inUse.copyUserAndDevice=ロックしているユーザーとデバイス名をコピー
@@ -172,9 +172,6 @@ hub.noKeychain.openBtn=설정 열기
hub.checkHostTrust.message.check=구성 확인 중…
hub.checkHostTrust.message.ask=이 호스트를 신뢰하겠습니까?
hub.checkHostTrust.message.ask.plural=이 호스트를 신뢰하겠습니까?
hub.checkHostTrust.description.ask=Cryptomator에서 이 호스트를 인식하지 못했습니다. Vault를 잠금 해제하려면 이 호스트를 신뢰해야 합니다. 아래 주소를 알고 있는 경우에만 신뢰하도록 설정하세요.
hub.checkHostTrust.description.ask.plural=Cryptomator에서 이 호스트들을 인식하지 못했습니다. Vault를 잠금 해제하려면 해당 호스트를 신뢰 대상으로 등록해야 합니다. 아래 주소를 알고 있는 경우에만 신뢰하도록 설정하세요.
hub.checkHostTrust.copyBtn.tooltip=클립보드로 주소 복사하기
hub.checkHostTrust.trustBtn=신뢰하기
hub.checkHostTrust.denyBtn=거부
### Waiting
@@ -163,9 +163,6 @@ hub.noKeychain.description=Lai atslēgtu Hub glabātavas, ir nepieciešama ierī
hub.noKeychain.openBtn=Atvērt iestatījumus
### Check Host Authenticity
hub.checkHostTrust.message.check=Pārbauda konfigurāciju…
hub.checkHostTrust.description.ask=Criptomator neaptazīst šo saimniekdatoru. Lai atslēgtu glabātavu, tam ir jāuzticas. Uzticies tam tikai tad, ja atpazīsti zemāk esošo adresi!
hub.checkHostTrust.description.ask.plural=Criptomator neaptazīst šos saimniekdatorus. Lai atslēgtu glabātavu, tiem ir jāuzticas. Uzticies tiem tikai tad, ja atpazīsti zemāk esošās adreses!
hub.checkHostTrust.copyBtn.tooltip=Ievietot adresi starpliktuvē
hub.checkHostTrust.trustBtn=Uzticēties
hub.checkHostTrust.denyBtn=Noraidīt
### Waiting
@@ -172,9 +172,6 @@ hub.noKeychain.openBtn=Abrir preferências
hub.checkHostTrust.message.check=Verificando a configuração…
hub.checkHostTrust.message.ask=Confiar neste anfitrião?
hub.checkHostTrust.message.ask.plural=Confiar nestes anfitriões?
hub.checkHostTrust.description.ask=O Cryptomator não reconhece este provedor. Para desbloquear o cofre, é necessário confiar nele. Confie apenas se reconhecer o endereço abaixo.
hub.checkHostTrust.description.ask.plural=O Cryptomator não reconhece estes provedores. Para desbloquear o cofre, é necessário confiar neles. Confie apenas se reconhecer os endereços abaixo.
hub.checkHostTrust.copyBtn.tooltip=Copiar endereço para a área de transferência
hub.checkHostTrust.trustBtn=Confiar
hub.checkHostTrust.denyBtn=Negar
### Waiting
@@ -150,7 +150,7 @@ unlock.chooseMasterkey.filePickerMimeDesc=Masterkey do Cryptomator
## Success
unlock.success.message=Desbloqueado com sucesso
unlock.success.description=O conteúdo no cofre "%s" agora está acessível no seu ponto de montagem.
unlock.success.rememberChoice=Lembrar escolha. Não perguntar novamente
unlock.success.rememberChoice=Lembrar opção escolhida. Não perguntar novamente
unlock.success.revealBtn=Revelar volume
## Failure
unlock.error.customPath.message=Não foi possível iniciar o cofre no caminho personalizado
@@ -172,9 +172,6 @@ hub.noKeychain.openBtn=Abrir preferências
hub.checkHostTrust.message.check=Verificando configuração…
hub.checkHostTrust.message.ask=Confiar neste host?
hub.checkHostTrust.message.ask.plural=Confiar nestes hosts?
hub.checkHostTrust.description.ask=O Cryptomador não reconhece este host. Para desbloquear o cofre, ele precisa ser confiável. Apenas confie nele se você reconhecer o endereço abaixo.
hub.checkHostTrust.description.ask.plural=O Cryptomador não reconhece este host. Para desbloquear o cofre, ele precisa ser confiável. Apenas confie nele se você reconhecer os endereços abaixo.
hub.checkHostTrust.copyBtn.tooltip=Copiar endereço para a área de transferência
hub.checkHostTrust.trustBtn=Confio
hub.checkHostTrust.denyBtn=Recusar
### Waiting
@@ -218,7 +215,7 @@ hub.requireAccountInit.description.1=Perfil de usuário do Hub
hub.requireAccountInit.description.2=.
### License Exceeded
hub.invalidLicense.message=Licença Hub invalida
hub.invalidLicense.description=Sua instância do Cryptomator Hub tem uma licença inválida. Informe a um administrador do Hub para atualizar ou renovar a licença.
hub.invalidLicense.description=Sua instância do Cryptomator Hub tem uma licença inválida. Informe a um administrador do Hub para atualizar ou renová-la.
# Lock
## Force
@@ -357,7 +354,7 @@ preferences.updates=Atualizações
preferences.updates.currentVersion=Versão atual: %s
preferences.updates.autoUpdateCheck=Buscar atualizações automaticamente
preferences.updates.checkNowBtn=Verificar agora
preferences.updates.updateAvailable=Atualização para a versão %s disponível.
preferences.updates.updateAvailable=Atualizar para versão %s disponível.
preferences.updates.lastUpdateCheck=Última verificação: %s
preferences.updates.lastUpdateCheck.never=nunca
preferences.updates.lastUpdateCheck.recently=recentemente
@@ -172,9 +172,6 @@ hub.noKeychain.openBtn=Открыть настройки
hub.checkHostTrust.message.check=Проверка конфигурации…
hub.checkHostTrust.message.ask=Доверять этому хосту?
hub.checkHostTrust.message.ask.plural=Доверять этим хостам?
hub.checkHostTrust.description.ask=Cryptomator не распознаёт этот хост. Чтобы разблокировать хранилище, оно должно быть доверенным. Доверяйте, только если вам известен указанный ниже адрес.
hub.checkHostTrust.description.ask.plural=Cryptomator не распознаёт эти хосты. Чтобы разблокировать хранилище, оно должно быть доверенным. Доверяйте им, только если вам известны указанные ниже адреса.
hub.checkHostTrust.copyBtn.tooltip=Скопировать адрес в буфер
hub.checkHostTrust.trustBtn=Доверять
hub.checkHostTrust.denyBtn=Отклонить
### Waiting
@@ -170,9 +170,6 @@ hub.noKeychain.openBtn=Otvoriť predvoľby
hub.checkHostTrust.message.check=Kontrolovanie konfigurácie…
hub.checkHostTrust.message.ask=Veríte tomuto hostovi?
hub.checkHostTrust.message.ask.plural=Veríte týmto hostom?
hub.checkHostTrust.description.ask=Cryptomator nedokáže rozpoznať tohto hostiteľa. Na odomknutie trezoru je potrebné, aby bol dôveryhodný. Dôverujte mu iba v prípade, že rozpoznáte adresu uvedenú nižšie.
hub.checkHostTrust.description.ask.plural=Cryptomator tieto hostiteľské počítače nerozpoznáva. Na odomknutie trezoru musia byť dôveryhodní. Dôverujte im iba v prípade, že rozpoznáte adresy uvedené nižšie.
hub.checkHostTrust.copyBtn.tooltip=Skopírovať adresu do schránky
hub.checkHostTrust.trustBtn=Veriť
hub.checkHostTrust.denyBtn=Odmietnuť
### Waiting
@@ -202,7 +199,6 @@ hub.unauthorized.message=Prístup zamietnutý
hub.unauthorized.description=Nie ste autorizovaný na otvorenie tohto trezora. Kontaktujte vlastníka trezora pre vyžiadanie prístupu.
### Untrusted Host
hub.untrustedHost.message=Nedôveryhodný host
hub.untrustedHost.description=Pripojenie k Hubu bolo z bezpečnostných dôvodov zablokované. Ak si myslíte, že hostiteľ Hubu je bezpečný, kontaktujte správcu Hubu alebo to skúste znova.
### Requires Account Initialization
hub.requireAccountInit.message=Vyžadovaná akcia
hub.requireAccountInit.description.0=Pre pokračovanie vyplňte potrebné kroky vo vašom
@@ -529,13 +529,10 @@ recoveryKey.recover.resetSuccess.description=Yeni parola ile kasanızın kilidin
# Recover Vault Config File and/or Masterkey
##Add Existing Vault without recovery - Dialog
recover.existing.title=Kasa Eklendi
##Vault Already Exists - Dialog
recover.alreadyExists.description="%s" kasanız halihazırda kasa listenizde bulunuyor, dolayısıyla tekrar eklenmedi.
##Invalid Selection - Dialog
recover.invalidSelection.message=Seçiminiz bir kasa değil
## Contact Hub Vault Owner - Dialog
contactHubVaultOwner.title=Hub Kasası
@@ -633,8 +630,6 @@ eventView.clearListButton.tooltip=Listeyi temizle
eventView.filterVaults=Kasayı süz
## event list entries
eventView.entry.inUse.message=Kullanılan dosya
eventView.entry.inUse.showEncrypted=Şifrelenmiş dosyayı göster
eventView.entry.inUse.copyUserAndDevice=Kilitleyen kullanıcı ve cihaz adını kopyala
eventView.entry.inUse.ignoreLock=Kullanım durumunu yok say
@@ -173,9 +173,6 @@ hub.noKeychain.openBtn=Відкрити налаштування
hub.checkHostTrust.message.check=Перевірка конфігурації…
hub.checkHostTrust.message.ask=Довіряти цьому хосту?
hub.checkHostTrust.message.ask.plural=Довіряти цим хостам?
hub.checkHostTrust.description.ask=Cryptomator не розпізнає цей хост. Щоб розблокувати сховище, йому потрібно довіряти. Довіряйте йому лише в тому випадку, якщо ви впізнаєте адресу, наведену нижче.
hub.checkHostTrust.description.ask.plural=Cryptomator не розпізнає ці хости. Щоб розблокувати сховище, їм потрібно довіряти. Довіряйте їм лише в тому випадку, якщо ви впізнаєте адреси, наведені нижче.
hub.checkHostTrust.copyBtn.tooltip=Скопіювати адресу в буфер обміну
hub.checkHostTrust.trustBtn=Довіряти
hub.checkHostTrust.denyBtn=Відхилити
### Waiting
@@ -40,8 +40,7 @@ public class FileOpenRequestHandlerTest {
public void testOpenArgsWithCorrectPaths() {
inTest.handleLaunchArgs(List.of("foo", "bar"));
AppLaunchEvent evt = queue.poll();
Assertions.assertNotNull(evt);
OpenFileEvent evt = Assertions.assertInstanceOf(OpenFileEvent.class, queue.poll());
Collection<Path> paths = evt.pathsToOpen();
MatcherAssert.assertThat(paths, CoreMatchers.hasItems(Paths.get("foo"), Paths.get("bar")));
}
@@ -60,7 +59,7 @@ public class FileOpenRequestHandlerTest {
@Test
@DisplayName("./cryptomator.exe foo (with full event queue)")
public void testOpenArgsWithFullQueue() {
queue.add(new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, Collections.emptyList()));
queue.add(new OpenFileEvent(Collections.emptyList()));
Assumptions.assumeTrue(queue.remainingCapacity() == 0);
inTest.handleLaunchArgs(List.of("foo"));
@@ -0,0 +1,98 @@
package org.cryptomator.launcher;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import java.net.URI;
import java.util.List;
public class LaunchArgsParserTest {
private FileOpenRequestHandler fileOpenRequestHandler;
private URIOpenRequestHandler uriOpenRequestHandler;
private NoopRequestHandler noopRequestHandler;
private LaunchArgsParser inTest;
@BeforeEach
public void setup() {
fileOpenRequestHandler = Mockito.mock(FileOpenRequestHandler.class);
uriOpenRequestHandler = Mockito.mock(URIOpenRequestHandler.class);
noopRequestHandler = Mockito.mock(NoopRequestHandler.class);
inTest = new LaunchArgsParser(fileOpenRequestHandler, uriOpenRequestHandler, noopRequestHandler);
}
@Test
@DisplayName("only file paths are forwarded to the FileOpenRequestHandler")
public void testOnlyPaths() {
inTest.process(List.of("foo", "bar"));
Mockito.verify(fileOpenRequestHandler).handleLaunchArgs(List.of("foo", "bar"));
Mockito.verifyNoInteractions(uriOpenRequestHandler);
Mockito.verifyNoInteractions(noopRequestHandler);
}
@Test
@DisplayName("empty args are forwarded to the NoopRequestHandler")
public void testEmptyArgs() {
inTest.process(List.of());
Mockito.verify(noopRequestHandler).revealApp();
Mockito.verifyNoInteractions(uriOpenRequestHandler, fileOpenRequestHandler);
}
@Test
@DisplayName("a Windows path is not mistaken for a URI")
public void testWindowsPathIsNotAUri() {
inTest.process(List.of("C:\\Users\\foo\\vault.cryptomator"));
Mockito.verify(fileOpenRequestHandler).handleLaunchArgs(List.of("C:\\Users\\foo\\vault.cryptomator"));
Mockito.verifyNoInteractions(uriOpenRequestHandler, noopRequestHandler);
}
@Test
@DisplayName("a single cryptomator:// URI is forwarded to the URIOpenRequestHandler")
public void testSingleUri() {
inTest.process(List.of("cryptomator://vault/foo"));
Mockito.verify(uriOpenRequestHandler).handleLaunchArgs(URI.create("cryptomator://vault/foo"));
Mockito.verifyNoInteractions(fileOpenRequestHandler, noopRequestHandler);
}
@Test
@DisplayName("a file:// URI is converted to a path and forwarded to the FileOpenRequestHandler")
public void testFileUriIsTreatedAsPath() {
inTest.process(List.of("file:///tmp/vault.cryptomator"));
var captor = ArgumentCaptor.forClass(List.class);
Mockito.verify(fileOpenRequestHandler).handleLaunchArgs(captor.capture());
Mockito.verifyNoInteractions(uriOpenRequestHandler, noopRequestHandler);
Assertions.assertEquals(1, captor.getValue().size());
Assertions.assertFalse(captor.getValue().getFirst().toString().startsWith("file:"));
}
@Test
@DisplayName("mixing a URI with a file path fails")
public void testMixedUriAndPathFails() {
Assertions.assertThrows(IllegalArgumentException.class, () -> inTest.process(List.of("cryptomator://vault/foo", "bar")));
Mockito.verifyNoInteractions(fileOpenRequestHandler, uriOpenRequestHandler, noopRequestHandler);
}
@Test
@DisplayName("more than one URI fails")
public void testMultipleUrisFail() {
Assertions.assertThrows(IllegalArgumentException.class, () -> inTest.process(List.of("cryptomator://vault/foo", "cryptomator://vault/bar")));
Mockito.verifyNoInteractions(fileOpenRequestHandler, uriOpenRequestHandler, noopRequestHandler);
}
@Test
@DisplayName("a URI that is not the first parameter fails")
public void testUriNotFirstFails() {
Assertions.assertThrows(IllegalArgumentException.class, () -> inTest.process(List.of("foo", "cryptomator://vault/bar")));
Mockito.verifyNoInteractions(fileOpenRequestHandler, uriOpenRequestHandler, noopRequestHandler);
}
}
@@ -0,0 +1,220 @@
package org.cryptomator.launcher;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Optional;
import java.util.UUID;
public class OpenHubVaultEventTest {
private static final String VAULT_ID = "d3a1f0b2-7c4e-4a1d-9f3b-2e5c6a7b8c9d";
private static final String KEY_ID = "hub+https://hub.example.com/api/vaults/" + VAULT_ID;
private static final String TOKEN = hubVaultConfig(KEY_ID);
@Test
@DisplayName("a valid vault/open deeplink is parsed")
public void testValid() {
var inTest = OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + TOKEN)).orElseThrow();
Assertions.assertEquals(UUID.fromString(VAULT_ID), inTest.vaultId());
Assertions.assertEquals(URI.create(KEY_ID), inTest.vaultConfig().getKeyId());
}
@Test
@DisplayName("parameters in the query instead of the fragment are not accepted")
public void testQueryParamsRejected() {
var uri = URI.create("org.cryptomator://vault/open?vaultConfig=" + TOKEN);
Assertions.assertThrows(IllegalArgumentException.class, () -> OpenHubVaultEvent.tryParse(uri));
}
@Test
@DisplayName("an encoded separator inside a value cannot forge another parameter")
public void testNoParameterInjectionViaEncodedSeparator() {
// '%26' must stay part of the first value; decoding the fragment before splitting would turn it into a real
// separator and smuggle in a 'vaultConfig' the link never carried - hence getRawFragment(), decoding per value.
var uri = URI.create("org.cryptomator://vault/open#other=a%26vaultConfig=" + TOKEN);
Assertions.assertThrows(IllegalArgumentException.class, () -> OpenHubVaultEvent.tryParse(uri));
}
@Test
@DisplayName("a config exceeding the size limit is rejected")
public void testExceedsSizeLimit() {
var oversized = "a".repeat(8193);
Assertions.assertThrows(IllegalArgumentException.class, //
() -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + oversized)));
}
@Test
@DisplayName("a config that is not a decodable token is rejected")
public void testNotAToken() {
Assertions.assertThrows(IllegalArgumentException.class, //
() -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=not-a-jwt")));
}
@Test
@DisplayName("a config without a hub key id is rejected")
public void testNotAHubVault() {
var token = hubVaultConfig("masterkeyfile:masterkey.cryptomator");
Assertions.assertThrows(IllegalArgumentException.class, //
() -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + token)));
}
@Test
@DisplayName("a config without a hub header is rejected")
public void testNoHubHeader() {
var token = vaultConfig(KEY_ID, VAULT_ID, null);
Assertions.assertThrows(IllegalArgumentException.class, //
() -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + token)));
}
@ParameterizedTest
@DisplayName("a vault id that is not a uuid is rejected")
@ValueSource(strings = { //
"not-a-uuid", //
"", // absent
"../../evil" // must never reach the api/vaults/{vaultId}/... request path
})
public void testInvalidVaultId(String vaultId) {
var token = vaultConfig(KEY_ID, vaultId, hubHeader("https://hub.example.com/api", "https://login.example.com/auth"));
Assertions.assertThrows(IllegalArgumentException.class, //
() -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + token)));
}
@Test
@DisplayName("a vault id disagreeing with the key id is rejected")
public void testVaultIdMismatch() {
// Hub writes the same id into both, so a config where they differ is forged or broken
var token = vaultConfig(KEY_ID, "11111111-2222-3333-4444-555555555555", //
hubHeader("https://hub.example.com/api", "https://login.example.com/auth"));
Assertions.assertThrows(IllegalArgumentException.class, //
() -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + token)));
}
@ParameterizedTest
@DisplayName("an unusable hub endpoint is rejected")
@ValueSource(strings = { //
"ftp://hub.example.com/api", // neither http nor https
"/api", // not absolute
"https:///api" // no host
})
public void testUnusableApiBaseUrl(String apiBaseUrl) {
var token = vaultConfig(KEY_ID, VAULT_ID, hubHeader(apiBaseUrl, "https://login.example.com/auth"));
Assertions.assertThrows(IllegalArgumentException.class, //
() -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + token)));
}
@Test
@DisplayName("an unusable auth endpoint is rejected")
public void testUnusableAuthEndpoint() {
var token = vaultConfig(KEY_ID, VAULT_ID, hubHeader("https://hub.example.com/api", "not a url"));
Assertions.assertThrows(IllegalArgumentException.class, //
() -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + token)));
}
@Test
@DisplayName("an http endpoint is accepted here, host trust decides later")
public void testHttpEndpointAccepted() {
var token = vaultConfig(KEY_ID, VAULT_ID, hubHeader("http://localhost:8080/api", "http://localhost:8080/auth"));
Assertions.assertTrue(OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + token)).isPresent());
}
@Test
@DisplayName("a non-cryptomator scheme yields empty")
public void testWrongScheme() {
Assertions.assertEquals(Optional.empty(), OpenHubVaultEvent.tryParse(URI.create("foobar://vault/open#vaultConfig=" + TOKEN)));
}
@Test
@DisplayName("the bare cryptomator scheme is no longer recognized")
public void testLegacyScheme() {
Assertions.assertEquals(Optional.empty(), OpenHubVaultEvent.tryParse(URI.create("cryptomator://vault/open#vaultConfig=" + TOKEN)));
}
@Test
@DisplayName("an unknown host yields empty")
public void testWrongHost() {
Assertions.assertEquals(Optional.empty(), OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://foo/open#vaultConfig=" + TOKEN)));
}
@Test
@DisplayName("an unknown path yields empty")
public void testWrongPath() {
Assertions.assertEquals(Optional.empty(), OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/create#vaultConfig=" + TOKEN)));
}
@Test
@DisplayName("a matching host is recognized case-insensitively")
public void testHostCaseInsensitive() {
Assertions.assertTrue(OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://VAULT/open#vaultConfig=" + TOKEN)).isPresent());
}
@Test
@DisplayName("a missing config fails")
public void testMissingConfig() {
Assertions.assertThrows(IllegalArgumentException.class, () -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open")));
}
@Test
@DisplayName("a blank config fails")
public void testBlankConfig() {
Assertions.assertThrows(IllegalArgumentException.class, () -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=")));
}
private static String hubHeader(String apiBaseUrl, String authEndpoint) {
return """
,
"hub": {
"clientId":"cryptomator",\
"authEndpoint":"%s",\
"tokenEndpoint":"https://login.example.com/token",\
"authSuccessUrl":"https://hub.example.com/app/unlock-success",\
"authErrorUrl":"https://hub.example.com/app/unlock-error",\
"apiBaseUrl":"%s"
}""".formatted(authEndpoint, apiBaseUrl);
}
private static String hubVaultConfig(String keyId) {
return vaultConfig(keyId, VAULT_ID, hubHeader("https://hub.example.com/api", "https://login.example.com/auth"));
}
/**
* Builds a vault config token. Its signature is keyed on the masterkey, which the deeplink never carries, so a dummy
* signature is exactly what the parser operates on.
*/
private static String vaultConfig(String keyId, String vaultId, String extraHeaderFields) {
var header = """
{ "kid":"%s",\
"typ":"JWT",\
"alg":"HS256"\
%s
}""".formatted(keyId, extraHeaderFields == null ? "" : extraHeaderFields);
var payload = """
{ "jti":"%s",\
"format":8,\
"cipherCombo":"SIV_GCM",\
"shorteningThreshold":220\
}""".formatted(vaultId);
var encoder = Base64.getUrlEncoder().withoutPadding();
return encoder.encodeToString(header.getBytes(StandardCharsets.UTF_8)) //
+ "." + encoder.encodeToString(payload.getBytes(StandardCharsets.UTF_8)) //
+ "." + encoder.encodeToString("signature".getBytes(StandardCharsets.UTF_8));
}
}
@@ -0,0 +1,56 @@
package org.cryptomator.ui.fxapp;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.launcher.AppLaunchEvent;
import org.cryptomator.launcher.RevealRunningEvent;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.dialogs.Dialogs;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import javafx.collections.FXCollections;
import javafx.stage.Stage;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
public class AppLaunchEventHandlerTest {
private BlockingQueue<AppLaunchEvent> queue;
private ExecutorService executor;
private FxApplicationWindows appWindows;
private AppLaunchEventHandler handler;
@BeforeEach
public void setup() {
queue = new LinkedBlockingQueue<>();
executor = Executors.newSingleThreadExecutor();
appWindows = mock(FxApplicationWindows.class);
handler = new AppLaunchEventHandler(queue, executor, appWindows, mock(VaultListManager.class), FXCollections.observableArrayList(), mock(VaultService.class), mock(Stage.class), mock(Dialogs.class));
}
@AfterEach
public void teardown() {
executor.shutdownNow();
}
// TODO: Add test for OpenHubVaultEvent once the event opens the hub vault flow for real.
@Test
@DisplayName("a RevealRunningEvent reveals the main window")
public void testRevealRunningEventShowsMainWindow() {
queue.add(new RevealRunningEvent());
handler.startHandlingLaunchEvents();
verify(appWindows, timeout(2000)).showMainWindow();
}
}
@@ -0,0 +1,92 @@
package org.cryptomator.ui.keyloading.hub;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultConfigCache;
import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.net.URI;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class HubVaultsTest {
private static final UUID VAULT_ID = UUID.fromString("d3a1f0b2-7c4e-4a1d-9f3b-2e5c6a7b8c9d");
private static final UUID OTHER_VAULT_ID = UUID.fromString("11111111-2222-3333-4444-555555555555");
@Test
@DisplayName("the hub vault with the requested id is found")
public void testFindsMatchingVault() throws IOException {
var wanted = hubVault(VAULT_ID);
var vaults = List.of(hubVault(OTHER_VAULT_ID), wanted);
Assertions.assertEquals(Optional.of(wanted), HubVaults.findByVaultId(vaults, VAULT_ID));
}
@Test
@DisplayName("no vault with the requested id yields empty")
public void testNoMatch() throws IOException {
var vaults = List.of(hubVault(OTHER_VAULT_ID));
Assertions.assertEquals(Optional.empty(), HubVaults.findByVaultId(vaults, VAULT_ID));
}
@Test
@DisplayName("a password vault never matches, even carrying the same id")
public void testIgnoresPasswordVault() throws IOException {
var vaults = List.of(vault("masterkeyfile:masterkey.cryptomator", VAULT_ID));
Assertions.assertEquals(Optional.empty(), HubVaults.findByVaultId(vaults, VAULT_ID));
}
@Test
@DisplayName("a vault whose config cannot be read is skipped, not fatal")
public void testSkipsUnreadableVault() throws IOException {
// e.g. a vault on a network drive that is currently offline - it must not hide a vault further down the list
var wanted = hubVault(VAULT_ID);
var vaults = List.of(unreadableVault(), wanted);
Assertions.assertEquals(Optional.of(wanted), HubVaults.findByVaultId(vaults, VAULT_ID));
}
// setup/mock provider
private static Vault hubVault(UUID vaultId) throws IOException {
return vault("hub+https://hub.example.com/api/vaults/" + vaultId, vaultId);
}
private static Vault vault(String keyId, UUID vaultId) throws IOException {
var configCache = mock(VaultConfigCache.class);
var config = mockConfig(keyId, vaultId);
when(configCache.get()).thenReturn(config);
return vaultWith(configCache);
}
private static Vault unreadableVault() throws IOException {
var configCache = mock(VaultConfigCache.class);
when(configCache.get()).thenThrow(new IOException("vault directory unavailable"));
return vaultWith(configCache);
}
private static Vault vaultWith(VaultConfigCache configCache) {
var vault = mock(Vault.class);
when(vault.getVaultConfigCache()).thenReturn(configCache);
return vault;
}
private static UnverifiedVaultConfig mockConfig(String keyId, UUID vaultId) {
var mock = mock(UnverifiedVaultConfig.class);
when(mock.getKeyId()).thenReturn(URI.create(keyId));
when(mock.allegedVaultId()).thenReturn(vaultId.toString());
return mock;
}
}