diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 93066b9d9..722c63d44 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -20,6 +20,10 @@ Translations are not managed directly in this repository. Instead, we use [Crowdin](https://translate.cryptomator.org/), which automatically synchronizes translations with this repository. If you want to help us with translations, please visit our translation project on Crowdin. +## Use of Generative AI + +AI tools may assist your work, but every contribution must be fully understood, reviewed, and tested by you. Only submit changes you can clearly explain and justify. Unverified or low-quality AI output that wastes our time and resources will be closed without further review. + ## Code of Conduct Help us keep Cryptomator open and inclusive. Please read and follow our [Code of Conduct](https://github.com/cryptomator/cryptomator/blob/develop/.github/CODE_OF_CONDUCT.md). diff --git a/.github/actions/win-sign-action/action.yml b/.github/actions/win-sign-action/action.yml new file mode 100644 index 000000000..ce4423883 --- /dev/null +++ b/.github/actions/win-sign-action/action.yml @@ -0,0 +1,76 @@ +name: 'Windows Code Signing' +description: 'Sign files on Windows with the Azure Trusted Signing' +inputs: + base-dir: + description: 'Absolute path to the base directory to search for files' + required: true + recursive: + description: 'Whether to search recursively in subdirectories' + required: false + default: 'false' + file-extensions: + description: 'List of file extensions to sign, separated by comma' + required: true + default: 'exe,dll,ps1' + description: + description: 'Signature description' + required: true + default: 'Cryptomator' + url: + description: 'Signature URL' + required: false + default: 'https://cryptomator.org' + append-signature: + description: 'Whether to append the signature to existing signatures' + required: false + default: 'false' + tenant-id: + description: 'Azure Tenant ID' + required: true + client-id: + description: 'Azure Client ID' + required: true + client-secret: + description: 'Azure Client Secret' + required: true + +runs: + using: 'composite' + steps: + - name: Generate, mask, and output the input secrets + id: set-secrets + run: | + echo "::add-mask::${{ inputs.tenant-id }}" + echo "::add-mask::${{ inputs.client-id }}" + echo "::add-mask::${{ inputs.client-secret }}" + echo "tenant-id=${{ inputs.tenant-id }}" >> "$GITHUB_OUTPUT" + echo "client-id=${{ inputs.client-id }}" >> "$GITHUB_OUTPUT" + echo "client-secret=${{ inputs.client-secret }}" >> "$GITHUB_OUTPUT" + shell: bash + - name: Sign DLLs with Azure Trusted Signing + uses: azure/trusted-signing-action@fc390cf8ed0f14e248a542af1d838388a47c7a7c # v0.5.10 + with: + files-folder: ${{ inputs.base-dir }} + files-folder-filter: ${{ inputs.file-extensions }} + files-folder-recurse: ${{ inputs.recursive }} + append-signature: ${{ inputs.append-signature }} + description: ${{ inputs.description }} + description-url: ${{ inputs.url }} + azure-tenant-id: ${{ steps.set-secrets.outputs.tenant-id }} + azure-client-id: ${{ steps.set-secrets.outputs.client-id }} + azure-client-secret: ${{ steps.set-secrets.outputs.client-secret }} + trusted-signing-account-name: cryptomatorSigning + certificate-profile-name: production + endpoint: https://weu.codesigning.azure.net/ + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 + exclude-environment-credential: false + exclude-workload-identity-credential: true + exclude-managed-identity-credential: true + exclude-shared-token-cache-credential: true + exclude-visual-studio-credential: true + exclude-visual-studio-code-credential: true + exclude-azure-cli-credential: true + exclude-azure-powershell-credential: true + exclude-azure-developer-cli-credential: true + exclude-interactive-browser-credential: true diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index 11206ca54..20fea5c58 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -19,7 +19,7 @@ on: env: JAVA_DIST: 'temurin' - JAVA_VERSION: '24.0.1+9' + JAVA_VERSION: '25.0.1+8.0.LTS' jobs: get-version: @@ -42,7 +42,7 @@ jobs: - os: ubuntu-24.04-arm appimage-suffix: aarch64 openjfx-url: 'https://download2.gluonhq.com/openjfx/25/openjfx-25_linux-aarch64_bin-jmods.zip' - openjfx-sha: '951c52481af0ec5885b06f1ebaa8a10da7e8ea23c5e1ef3e2f6f11fa1b3a7ce1' + openjfx-sha: '9ad4ca7b769ca4ee6419f1e99143dd6ff812f8be4fddb46a7d7cacbeea148af4' steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b528cfb49..c9a0ea0c9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ on: env: JAVA_DIST: 'temurin' - JAVA_VERSION: 24 + JAVA_VERSION: 25 defaults: run: diff --git a/.github/workflows/check-jdk-updates.yml b/.github/workflows/check-jdk-updates.yml index 432cf16b7..cad852dc1 100644 --- a/.github/workflows/check-jdk-updates.yml +++ b/.github/workflows/check-jdk-updates.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: env: - JDK_VERSION: '24.0.1+9' + JDK_VERSION: '25.0.1+8.0.LTS' JDK_VENDOR: temurin RUNTIME_VERSION_HELPER: > public class Test { @@ -23,7 +23,7 @@ jobs: JDK_MAJOR_VERSION: 'toBeFilled' steps: - name: Determine current major version - run: echo 'JDK_MAJOR_VERSION=${{ env.JDK_VERSION }}'.substring(0,20) >> "$env:GITHUB_ENV" + run: echo 'JDK_MAJOR_VERSION=${{ env.JDK_VERSION }}'.substring(0,2) >> "$env:GITHUB_ENV" shell: pwsh - name: Checkout latest JDK ${{ env.JDK_MAJOR_VERSION }} uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml index 715fde74d..17a3d487f 100644 --- a/.github/workflows/debian.yml +++ b/.github/workflows/debian.yml @@ -23,13 +23,12 @@ on: env: JAVA_DIST: 'temurin' - JAVA_VERSION: '24.0.1+9' - COFFEELIBS_JDK: 24 - COFFEELIBS_JDK_VERSION: '24.0.1+9-0ppa3' + JAVA_VERSION: '25.0.1+8.0.LTS' + DEB_BUILD_DEPENDS: 'debhelper (>=10), openjdk-25-jdk (>= 25+36), libgtk-3-0 (>= 3.20.0), libxxf86vm1, libgl1' OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/25/openjfx-25_linux-x64_bin-jmods.zip' OPENJFX_JMODS_AMD64_HASH: '96e520f48610d8ffb94ca30face1f11ffe8a977ddc1c4ff80b1a9e9f048bd94e' OPENJFX_JMODS_AARCH64: 'https://download2.gluonhq.com/openjfx/25/openjfx-25_linux-aarch64_bin-jmods.zip' - OPENJFX_JMODS_AARCH64_HASH: '951c52481af0ec5885b06f1ebaa8a10da7e8ea23c5e1ef3e2f6f11fa1b3a7ce1' + OPENJFX_JMODS_AARCH64_HASH: '9ad4ca7b769ca4ee6419f1e99143dd6ff812f8be4fddb46a7d7cacbeea148af4' jobs: get-version: @@ -55,9 +54,11 @@ jobs: fi - name: Install build tools run: | - sudo add-apt-repository ppa:coffeelibs/openjdk sudo apt-get update - sudo apt-get install debhelper devscripts dput coffeelibs-jdk-${{ env.COFFEELIBS_JDK }}=${{ env.COFFEELIBS_JDK_VERSION }} + sudo apt-get install devscripts dput + sudo apt-get satisfy "${DEB_BUILD_DEPENDS}" + env: + DEB_BUILD_DEPENDS: ${{ env.DEB_BUILD_DEPENDS }} - name: Setup Java uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 with: diff --git a/.github/workflows/dependency-check.yml b/.github/workflows/dependency-check.yml index ddce37384..22d2a5fc5 100644 --- a/.github/workflows/dependency-check.yml +++ b/.github/workflows/dependency-check.yml @@ -11,7 +11,7 @@ jobs: with: runner-os: 'ubuntu-latest' java-distribution: 'temurin' - java-version: 24 + java-version: 25 secrets: nvd-api-key: ${{ secrets.NVD_API_KEY }} ossindex-username: ${{ secrets.OSSINDEX_USERNAME }} diff --git a/.github/workflows/get-version.yml b/.github/workflows/get-version.yml index 5585d7ac5..b1c728fa8 100644 --- a/.github/workflows/get-version.yml +++ b/.github/workflows/get-version.yml @@ -23,7 +23,7 @@ on: env: JAVA_DIST: 'temurin' - JAVA_VERSION: 24 + JAVA_VERSION: 25 jobs: determine-version: diff --git a/.github/workflows/mac-dmg-x64.yml b/.github/workflows/mac-dmg-x64.yml index 9e6f48a7c..6a9c3f644 100644 --- a/.github/workflows/mac-dmg-x64.yml +++ b/.github/workflows/mac-dmg-x64.yml @@ -24,7 +24,7 @@ on: env: JAVA_DIST: 'temurin' - JAVA_VERSION: '24.0.1+9' + JAVA_VERSION: '25.0.1+8.0.LTS' jobs: get-version: diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml index c47b4309a..4f9ff6e71 100644 --- a/.github/workflows/mac-dmg.yml +++ b/.github/workflows/mac-dmg.yml @@ -22,7 +22,7 @@ on: env: JAVA_DIST: 'temurin' - JAVA_VERSION: '24.0.1+9' + JAVA_VERSION: '25.0.1+8.0.LTS' jobs: get-version: diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index 8bfddf2d4..cfb013c05 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -5,7 +5,7 @@ on: env: JAVA_DIST: 'temurin' - JAVA_VERSION: 24 + JAVA_VERSION: 25 defaults: run: diff --git a/.github/workflows/release-check.yml b/.github/workflows/release-check.yml index e1739326e..e6c12a2d1 100644 --- a/.github/workflows/release-check.yml +++ b/.github/workflows/release-check.yml @@ -12,7 +12,7 @@ defaults: env: JAVA_DIST: 'temurin' - JAVA_VERSION: 23 + JAVA_VERSION: 25 jobs: check-preconditions: diff --git a/.github/workflows/win-exe.yml b/.github/workflows/win-exe.yml index bf2e5cce7..95bc6aed0 100644 --- a/.github/workflows/win-exe.yml +++ b/.github/workflows/win-exe.yml @@ -8,10 +8,6 @@ on: version: description: 'Version' required: false - isDebug: - description: 'Build debug version with console output' - type: boolean - default: false sign: description: 'Sign binaries' required: false @@ -51,8 +47,8 @@ jobs: include: - arch: x64 os: windows-latest - java-dist: 'zulu' - java-version: '24.0.1+9' + java-dist: 'zulu' #cannot use temurin, see https://github.com/cryptomator/cryptomator/issues/3824#issuecomment-2829827427 + java-version: '25.0.1+8' java-package: 'jdk' steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -192,6 +188,16 @@ jobs: New-Item -Path appdir/jpackage-jmod -ItemType Directory & $env:JAVA_HOME\bin\jmod.exe extract --dir jpackage-jmod "${env:JAVA_HOME}\jmods\jdk.jpackage.jmod" Get-ChildItem -Recurse -Path "jpackage-jmod" -File wixhelper.dll | Select-Object -Last 1 | Copy-Item -Destination "appdir" + - name: Sign DLLs with Azure Trusted Signing + if: inputs.sign || github.event_name == 'release' + uses: ./.github/actions/win-sign-action + with: + base-dir: ${{ github.workspace }}\appdir + 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@450e322ff2214d0be0b079b63343c894f3ef735f # no specific version @@ -251,16 +257,16 @@ jobs: env: JP_WIXWIZARD_RESOURCES: ${{ github.workspace }}/dist/win/resources # requires abs path, used in resources/main.wxs JP_WIXHELPER_DIR: ${{ github.workspace }}\appdir - - name: Sign msi with Actalis CodeSigner + - name: Sign MSI with Azure Trusted Signing if: inputs.sign || github.event_name == 'release' - uses: skymatic/workflows/.github/actions/win-sign-action@450e322ff2214d0be0b079b63343c894f3ef735f # no specific version + uses: ./.github/actions/win-sign-action with: - base-dir: 'installer' - file-extensions: 'msi' - sign-description: 'Cryptomator Installer' - sign-url: 'https://cryptomator.org' - username: ${{ secrets.WIN_CODESIGN_USERNAME }} - password: ${{ secrets.WIN_CODESIGN_PW }} + base-dir: ${{ github.workspace }}\installer + file-extensions: msi + description: 'Cryptomator Installer' + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + client-id: ${{ secrets.AZURE_CLIENT_ID }} + client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} - name: Add possible alpha/beta tags and architecture to installer name run: mv installer/Cryptomator-*.msi Cryptomator-${{ needs.get-version.outputs.semVerStr }}-${{ matrix.arch }}.msi - name: Create detached GPG signature with key 615D449FE6E6A235 @@ -357,6 +363,17 @@ jobs: - name: Detach burn engine in preparation to sign run: > wix burn detach installer/unsigned/Cryptomator-Installer.exe -engine tmp/engine.exe + - name: Sign WiX burn engine with Azure Trusted Signing + if: inputs.sign || github.event_name == 'release' + uses: ./.github/actions/win-sign-action + with: + base-dir: ${{ github.workspace }}\tmp + file-extensions: exe + append-signature: true + description: 'Cryptomator Bundle Installer' + 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@450e322ff2214d0be0b079b63343c894f3ef735f # no specific version @@ -370,6 +387,17 @@ jobs: - name: Reattach signed burn engine to installer run: > wix burn reattach installer/unsigned/Cryptomator-Installer.exe -engine tmp/engine.exe -o installer/Cryptomator-Installer.exe + - name: Sign EXE installer with Azure Trusted Signing + if: inputs.sign || github.event_name == 'release' + uses: ./.github/actions/win-sign-action + with: + base-dir: ${{ github.workspace }}\installer + file-extensions: exe + append-signature: true + description: 'Cryptomator Bundle Installer' + 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@450e322ff2214d0be0b079b63343c894f3ef735f # no specific version diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 1256745d3..2d9504948 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -14,16 +14,15 @@ - + \ No newline at end of file diff --git a/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml b/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml index e986431a8..8654f7156 100644 --- a/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml +++ b/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml @@ -83,6 +83,9 @@ + + https://github.com/cryptomator/cryptomator/releases/1.18.0 + https://github.com/cryptomator/cryptomator/releases/1.17.1 diff --git a/dist/linux/debian/control b/dist/linux/debian/control index 95c8c6871..713f03972 100644 --- a/dist/linux/debian/control +++ b/dist/linux/debian/control @@ -2,7 +2,7 @@ Source: cryptomator Maintainer: Cryptobot Section: utils Priority: optional -Build-Depends: debhelper (>=10), coffeelibs-jdk-24 (>= 24.0.1+9-0ppa3), libgtk-3-0 (>= 3.20.0), libxxf86vm1, libgl1 +Build-Depends: debhelper (>=10), openjdk-25-jdk (>= 25+36), libgtk-3-0 (>= 3.20.0), libxxf86vm1, libgl1 Standards-Version: 4.5.0 Homepage: https://cryptomator.org Vcs-Git: https://github.com/cryptomator/cryptomator.git diff --git a/dist/linux/debian/rules b/dist/linux/debian/rules index c6e697283..456a97e89 100755 --- a/dist/linux/debian/rules +++ b/dist/linux/debian/rules @@ -4,11 +4,12 @@ # Uncomment this to turn on verbose mode. #export DH_VERBOSE=1 -JAVA_HOME = /usr/lib/jvm/java-24-coffeelibs DEB_BUILD_ARCH ?= $(shell dpkg-architecture -qDEB_BUILD_ARCH) ifeq ($(DEB_BUILD_ARCH),amd64) +JAVA_HOME = /usr/lib/jvm/java-25-openjdk-amd64 JMODS_PATH = jmods/amd64:${JAVA_HOME}/jmods else ifeq ($(DEB_BUILD_ARCH),arm64) +JAVA_HOME = /usr/lib/jvm/java-25-openjdk-arm64 JMODS_PATH = jmods/aarch64:${JAVA_HOME}/jmods endif diff --git a/license/merges b/license/merges index eb3a32a5b..a47203dfa 100644 --- a/license/merges +++ b/license/merges @@ -1,6 +1,6 @@ -Apache License v2.0|Apache License, Version 2.0|The Apache Software License, Version 2.0|Apache 2.0|Apache Software License - Version 2.0|Apache-2.0 -MIT License|The MIT License (MIT)|The MIT License|MIT license -LGPL 2.1|LGPL, version 2.1|GNU Lesser/Library General Public License version 2|GNU Lesser General Public License Version 2.1 +Apache License v2.0|Apache License, Version 2.0|The Apache License, Version 2.0|The Apache Software License, Version 2.0|Apache 2.0|Apache Software License - Version 2.0|Apache-2.0 +MIT License|MIT|The MIT License (MIT)|The MIT License|MIT license +LGPL 2.1|LGPL, version 2.1|GNU Lesser/Library General Public License version 2|GNU Lesser General Public License Version 2.1|GNU Lesser General Public License GPLv2|GNU General Public License Version 2 GPLv2+CE|CDDL + GPLv2 with classpath exception Eclipse Public License - Version 1.0|Eclipse Public License - v 1.0 diff --git a/pom.xml b/pom.xml index 7c8e5c7a1..412375ce5 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.cryptomator cryptomator - 1.18.0-SNAPSHOT + 1.19.0-SNAPSHOT Cryptomator Desktop App @@ -26,7 +26,7 @@ UTF-8 - 24 + 25 @@ -62,7 +62,7 @@ 26.0.2-1 12.1.5 - 0.8.13 + 0.8.14 2.7.0 1.4.0 3.14.1 diff --git a/src/main/java/org/cryptomator/common/recovery/BackupRestorer.java b/src/main/java/org/cryptomator/common/recovery/BackupRestorer.java new file mode 100644 index 000000000..e5afc5112 --- /dev/null +++ b/src/main/java/org/cryptomator/common/recovery/BackupRestorer.java @@ -0,0 +1,53 @@ +package org.cryptomator.common.recovery; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileTime; +import java.util.stream.Stream; + +import static org.cryptomator.common.Constants.MASTERKEY_BACKUP_SUFFIX; + +public final class BackupRestorer { + + private static final Logger LOG = LoggerFactory.getLogger(BackupRestorer.class); + + private BackupRestorer() {} + + public static void restoreIfBackupPresent(Path vaultPath, String filePrefix) { + Path targetFile = vaultPath.resolve(filePrefix); + + try (Stream files = Files.list(vaultPath)) { + files.filter(file -> isFileMatchingPattern(file.getFileName().toString(), filePrefix)) + .max((f1, f2) -> { + try { + FileTime time1 = Files.getLastModifiedTime(f1); + FileTime time2 = Files.getLastModifiedTime(f2); + return time1.compareTo(time2); + } catch (IOException e) { + return 0; + } + }) + .ifPresent(backupFile -> copyBackupFile(backupFile, targetFile)); + } catch (IOException e) { + LOG.info("Unable to restore backup files in '{}'", vaultPath, e); + } + } + + private static boolean isFileMatchingPattern(String fileName, String filePrefix) { + return fileName.startsWith(filePrefix) && fileName.endsWith(MASTERKEY_BACKUP_SUFFIX); + } + + private static void copyBackupFile(Path backupFile, Path configPath) { + try { + Files.copy(backupFile, configPath, StandardCopyOption.REPLACE_EXISTING); + LOG.debug("Backup restored - file: '{}' path: '{}'", backupFile, configPath); + } catch (IOException e) { + LOG.warn("Unable to copy backup file from '{}' to '{}'", backupFile, configPath, e); + } + } +} diff --git a/src/main/java/org/cryptomator/common/recovery/CryptoFsInitializer.java b/src/main/java/org/cryptomator/common/recovery/CryptoFsInitializer.java new file mode 100644 index 000000000..359405d15 --- /dev/null +++ b/src/main/java/org/cryptomator/common/recovery/CryptoFsInitializer.java @@ -0,0 +1,33 @@ +package org.cryptomator.common.recovery; + +import java.io.IOException; +import java.nio.file.Path; + +import org.cryptomator.cryptofs.CryptoFileSystemProperties; +import org.cryptomator.cryptofs.CryptoFileSystemProvider; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.CryptorProvider; +import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.MasterkeyLoader; + +import static org.cryptomator.common.Constants.DEFAULT_KEY_ID; + +public final class CryptoFsInitializer { + + private CryptoFsInitializer() {} + + public static void init(Path recoveryPath, + Masterkey masterkey, + int shorteningThreshold, + CryptorProvider.Scheme scheme) throws IOException, CryptoException { + + MasterkeyLoader loader = ignored -> masterkey.copy(); + CryptoFileSystemProperties fsProps = CryptoFileSystemProperties // + .cryptoFileSystemProperties() // + .withCipherCombo(scheme) // + .withKeyLoader(loader) // + .withShorteningThreshold(shorteningThreshold) // + .build(); + CryptoFileSystemProvider.initialize(recoveryPath, fsProps, DEFAULT_KEY_ID); + } +} diff --git a/src/main/java/org/cryptomator/common/recovery/MasterkeyService.java b/src/main/java/org/cryptomator/common/recovery/MasterkeyService.java new file mode 100644 index 000000000..7d487ec54 --- /dev/null +++ b/src/main/java/org/cryptomator/common/recovery/MasterkeyService.java @@ -0,0 +1,101 @@ +package org.cryptomator.common.recovery; + +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.CryptorProvider; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; +import org.cryptomator.ui.recoverykey.RecoveryKeyFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; + +import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; +import static org.cryptomator.cryptofs.common.Constants.DATA_DIR_NAME; + +public final class MasterkeyService { + + private static final Logger LOG = LoggerFactory.getLogger(MasterkeyService.class); + + private MasterkeyService() {} + + public static void recoverFromRecoveryKey(String recoveryKey, RecoveryKeyFactory recoveryKeyFactory, Path recoveryPath, CharSequence newPassword) throws IOException { + recoveryKeyFactory.newMasterkeyFileWithPassphrase(recoveryPath, recoveryKey, newPassword); + } + + public static Masterkey load(MasterkeyFileAccess masterkeyFileAccess, Path masterkeyFilePath, CharSequence password) throws IOException { + return masterkeyFileAccess.load(masterkeyFilePath, password); + } + + public static CryptorProvider.Scheme validateRecoveryKeyAndDetectCombo(RecoveryKeyFactory recoveryKeyFactory, // + Vault vault, String recoveryKey, // + MasterkeyFileAccess masterkeyFileAccess) throws IOException, CryptoException, NoSuchElementException { + String tmpPass = UUID.randomUUID().toString(); + try (RecoveryDirectory recoveryDirectory = RecoveryDirectory.create(vault.getPath())) { + Path tempRecoveryPath = recoveryDirectory.getRecoveryPath(); + recoverFromRecoveryKey(recoveryKey, recoveryKeyFactory, tempRecoveryPath, tmpPass); + Path masterkeyFilePath = tempRecoveryPath.resolve(MASTERKEY_FILENAME); + + try (Masterkey mk = load(masterkeyFileAccess, masterkeyFilePath, tmpPass)) { + return detect(mk, vault.getPath()).orElseThrow(); + } + } + } + + public static Optional detect(Masterkey masterkey, Path vaultPath) { + try (Stream paths = Files.walk(vaultPath.resolve(DATA_DIR_NAME))) { + Optional c9rFile = paths // + .filter(p -> p.toString().endsWith(".c9r")) // + .filter(p -> !p.endsWith("dir.c9r")) // + .findFirst(); + if (c9rFile.isEmpty()) { + LOG.info("Unable to detect Crypto scheme: No *.c9r file found in {}", vaultPath); + return Optional.empty(); + } + return determineScheme(c9rFile.get(), masterkey); + } catch (IOException e) { + LOG.info("Unable to detect Crypto scheme: Failed to inspect vault", e); + return Optional.empty(); + } + } + + private static Optional determineScheme(Path c9rFile, Masterkey masterkey) { + return Arrays.stream(CryptorProvider.Scheme.values()).filter(scheme -> { + try (Cryptor cryptor = CryptorProvider.forScheme(scheme).provide(masterkey.copy(), SecureRandom.getInstanceStrong())) { + int headerSize = cryptor.fileHeaderCryptor().headerSize(); + + ByteBuffer headerBuf = ByteBuffer.allocate(headerSize); + + try (FileChannel channel = FileChannel.open(c9rFile, StandardOpenOption.READ)) { + channel.read(headerBuf, 0); + } + + headerBuf.flip(); + cryptor.fileHeaderCryptor().decryptHeader(headerBuf.duplicate()); + LOG.debug("Detected Crypto scheme: {}", scheme); + return true; + } catch (IllegalArgumentException | CryptoException e) { + LOG.debug("Could not decrypt with scheme: {}", scheme); + return false; + } catch (IOException | NoSuchAlgorithmException e) { + LOG.warn("Unable to detect Crypto scheme: Failed to decrypt .c9r file", e); + return false; + } + }).findFirst(); + } + +} diff --git a/src/main/java/org/cryptomator/common/recovery/RecoveryActionType.java b/src/main/java/org/cryptomator/common/recovery/RecoveryActionType.java new file mode 100644 index 000000000..a05073dad --- /dev/null +++ b/src/main/java/org/cryptomator/common/recovery/RecoveryActionType.java @@ -0,0 +1,10 @@ +package org.cryptomator.common.recovery; + +public enum RecoveryActionType { + RESTORE_ALL, + RESTORE_MASTERKEY, + RESTORE_VAULT_CONFIG, + RESET_PASSWORD, + SHOW_KEY, + CONVERT_VAULT +} diff --git a/src/main/java/org/cryptomator/common/recovery/RecoveryDirectory.java b/src/main/java/org/cryptomator/common/recovery/RecoveryDirectory.java new file mode 100644 index 000000000..be2af245b --- /dev/null +++ b/src/main/java/org/cryptomator/common/recovery/RecoveryDirectory.java @@ -0,0 +1,56 @@ +package org.cryptomator.common.recovery; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Comparator; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class RecoveryDirectory implements AutoCloseable { + + private static final Logger LOG = LoggerFactory.getLogger(RecoveryDirectory.class); + + private final Path recoveryPath; + private final Path vaultPath; + + private RecoveryDirectory(Path vaultPath, Path recoveryPath) { + this.vaultPath = vaultPath; + this.recoveryPath = recoveryPath; + } + + public static RecoveryDirectory create(Path vaultPath) throws IOException { + Path tempDir = Files.createTempDirectory("cryptomator"); + return new RecoveryDirectory(vaultPath, tempDir); + } + + public void moveRecoveredFile(String file) throws IOException { + Files.move(recoveryPath.resolve(file), vaultPath.resolve(file), StandardCopyOption.REPLACE_EXISTING); + } + + private void deleteRecoveryDirectory() { + try (var paths = Files.walk(recoveryPath)) { + paths.sorted(Comparator.reverseOrder()).forEach(p -> { + try { + Files.delete(p); + } catch (IOException e) { + LOG.info("Unable to delete {}. Please delete it manually.", p); + } + }); + } catch (IOException e) { + LOG.error("Failed to clean up recovery directory", e); + } + } + + @Override + public void close() { + deleteRecoveryDirectory(); + } + + public Path getRecoveryPath() { + return recoveryPath; + } + +} diff --git a/src/main/java/org/cryptomator/common/recovery/VaultPreparator.java b/src/main/java/org/cryptomator/common/recovery/VaultPreparator.java new file mode 100644 index 000000000..d99ba50a0 --- /dev/null +++ b/src/main/java/org/cryptomator/common/recovery/VaultPreparator.java @@ -0,0 +1,54 @@ +package org.cryptomator.common.recovery; + +import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.common.settings.VaultSettings; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.common.vaults.VaultComponent; +import org.cryptomator.common.vaults.VaultConfigCache; +import org.cryptomator.common.vaults.VaultListManager; +import org.cryptomator.integrations.mount.MountService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.ResourceBundle; + +import static org.cryptomator.common.vaults.VaultState.Value.LOCKED; + +public final class VaultPreparator { + + private static final Logger LOG = LoggerFactory.getLogger(VaultPreparator.class); + + private VaultPreparator() {} + + public static Vault prepareVault(Path selectedDirectory, // + VaultComponent.Factory vaultComponentFactory, // + List mountServices, // + ResourceBundle resourceBundle) { + VaultSettings vaultSettings = VaultSettings.withRandomId(); + vaultSettings.path.set(selectedDirectory); + if (selectedDirectory.getFileName() != null) { + vaultSettings.displayName.set(selectedDirectory.getFileName().toString()); + } else { + vaultSettings.displayName.set(resourceBundle.getString("defaults.vault.vaultName")); + } + + var wrapper = new VaultConfigCache(vaultSettings); + Vault vault = vaultComponentFactory.create(vaultSettings, wrapper, LOCKED, null).vault(); + try { + VaultListManager.determineVaultState(vault.getPath()); + } catch (IOException e) { + LOG.warn("Failed to determine vault state for {}", vaultSettings.path.get(), e); + } + + //due to https://github.com/cryptomator/cryptomator/issues/2880#issuecomment-1680313498 + var nameOfWinfspLocalMounter = "org.cryptomator.frontend.fuse.mount.WinFspMountProvider"; + if (SystemUtils.IS_OS_WINDOWS && vaultSettings.path.get().toString().contains("Dropbox") && mountServices.stream().anyMatch(s -> s.getClass().getName().equals(nameOfWinfspLocalMounter))) { + vaultSettings.mountService.setValue(nameOfWinfspLocalMounter); + } + + return vault; + } +} diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java index 7b1267df8..75790507c 100644 --- a/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/src/main/java/org/cryptomator/common/vaults/Vault.java @@ -23,7 +23,6 @@ import org.cryptomator.cryptofs.event.FilesystemEvent; import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.MasterkeyLoader; import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; -import org.cryptomator.event.VaultEvent; import org.cryptomator.integrations.mount.MountFailedException; import org.cryptomator.integrations.mount.Mountpoint; import org.cryptomator.integrations.mount.UnmountFailedException; @@ -35,7 +34,6 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Named; -import javafx.application.Platform; import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; @@ -75,6 +73,7 @@ public class Vault { private final BooleanBinding missing; private final BooleanBinding needsMigration; private final BooleanBinding unknownError; + private final BooleanBinding missingVaultConfig; private final ObjectBinding mountPoint; private final Mounter mounter; private final Settings settings; @@ -103,6 +102,7 @@ public class Vault { this.processing = Bindings.createBooleanBinding(this::isProcessing, state); this.unlocked = Bindings.createBooleanBinding(this::isUnlocked, state); this.missing = Bindings.createBooleanBinding(this::isMissing, state); + this.missingVaultConfig = Bindings.createBooleanBinding(this::isMissingVaultConfig, state); this.needsMigration = Bindings.createBooleanBinding(this::isNeedsMigration, state); this.unknownError = Bindings.createBooleanBinding(this::isUnknownError, state); this.mountPoint = Bindings.createObjectBinding(this::getMountPoint, state); @@ -337,6 +337,14 @@ public class Vault { return state.get() == VaultState.Value.ERROR; } + public BooleanBinding missingVaultConfigProperty() { + return missingVaultConfig; + } + + public boolean isMissingVaultConfig() { + return state.get() == VaultState.Value.VAULT_CONFIG_MISSING || state.get() == VaultState.Value.ALL_MISSING; + } + public ReadOnlyStringProperty displayNameProperty() { return vaultSettings.displayName; } diff --git a/src/main/java/org/cryptomator/common/vaults/VaultConfigCache.java b/src/main/java/org/cryptomator/common/vaults/VaultConfigCache.java index b879b1f81..4a95fe50b 100644 --- a/src/main/java/org/cryptomator/common/vaults/VaultConfigCache.java +++ b/src/main/java/org/cryptomator/common/vaults/VaultConfigCache.java @@ -20,7 +20,7 @@ public class VaultConfigCache { private final VaultSettings settings; private final AtomicReference config; - VaultConfigCache(VaultSettings settings) { + public VaultConfigCache(VaultSettings settings) { this.settings = settings; this.config = new AtomicReference<>(null); } diff --git a/src/main/java/org/cryptomator/common/vaults/VaultListManager.java b/src/main/java/org/cryptomator/common/vaults/VaultListManager.java index ce1b2433c..e73075d0d 100644 --- a/src/main/java/org/cryptomator/common/vaults/VaultListManager.java +++ b/src/main/java/org/cryptomator/common/vaults/VaultListManager.java @@ -9,6 +9,7 @@ package org.cryptomator.common.vaults; import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.common.recovery.BackupRestorer; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.cryptofs.CryptoFileSystemProvider; @@ -34,9 +35,7 @@ import java.util.ResourceBundle; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME; -import static org.cryptomator.common.vaults.VaultState.Value.ERROR; -import static org.cryptomator.common.vaults.VaultState.Value.LOCKED; -import static org.cryptomator.common.vaults.VaultState.Value.NEEDS_MIGRATION; +import static org.cryptomator.common.vaults.VaultState.Value.*; @Singleton public class VaultListManager { @@ -67,6 +66,12 @@ public class VaultListManager { autoLocker.init(); } + public boolean isAlreadyAdded(Path vaultPath) { + assert vaultPath.isAbsolute(); + assert vaultPath.normalize().equals(vaultPath); + return vaultList.stream().anyMatch(v -> vaultPath.equals(v.getPath())); + } + public Vault add(Path pathToVault) throws IOException { Path normalizedPathToVault = pathToVault.normalize().toAbsolutePath(); if (CryptoFileSystemProvider.checkDirStructureForVault(normalizedPathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) == DirStructure.UNRELATED) { @@ -114,59 +119,122 @@ public class VaultListManager { .findAny(); } + public void addVault(Vault vault) { + Path path = vault.getPath().normalize().toAbsolutePath(); + if (!isAlreadyAdded(path)) { + vaultList.add(vault); + } + } + private Vault create(VaultSettings vaultSettings) { var wrapper = new VaultConfigCache(vaultSettings); try { var vaultState = determineVaultState(vaultSettings.path.get()); - if (vaultState == LOCKED) { //for legacy reasons: pre v8 vault do not have a config, but they are in the NEEDS_MIGRATION state - wrapper.reloadConfig(); - if (Objects.isNull(vaultSettings.lastKnownKeyLoader.get())) { - var keyIdScheme = wrapper.get().getKeyId().getScheme(); - vaultSettings.lastKnownKeyLoader.set(keyIdScheme); - } - } else if (vaultState == NEEDS_MIGRATION) { - vaultSettings.lastKnownKeyLoader.set(MasterkeyFileLoadingStrategy.SCHEME); - } + initializeLastKnownKeyLoaderIfPossible(vaultSettings, vaultState, wrapper); + return vaultComponentFactory.create(vaultSettings, wrapper, vaultState, null).vault(); } catch (IOException e) { - LOG.warn("Failed to determine vault state for " + vaultSettings.path.get(), e); + LOG.warn("Failed to determine vault state for {}", vaultSettings.path.get(), e); return vaultComponentFactory.create(vaultSettings, wrapper, ERROR, e).vault(); } } + private void initializeLastKnownKeyLoaderIfPossible(VaultSettings vaultSettings, VaultState.Value vaultState, VaultConfigCache wrapper) throws IOException { + if (vaultSettings.lastKnownKeyLoader.get() != null) { + return; + } + + switch (vaultState) { + case LOCKED -> { + wrapper.reloadConfig(); + vaultSettings.lastKnownKeyLoader.set(wrapper.get().getKeyId().getScheme()); + } + case NEEDS_MIGRATION -> { + //for legacy reasons: pre v8 vault do not have a config, but they are in the NEEDS_MIGRATION state + vaultSettings.lastKnownKeyLoader.set(MasterkeyFileLoadingStrategy.SCHEME); + } + case VAULT_CONFIG_MISSING -> { + //Nothing to do here, since there is no config to read + } + case MISSING, ALL_MISSING, ERROR, PROCESSING -> { + // no config available or not safe to load + } + default -> { + if (Files.exists(vaultSettings.path.get().resolve(VAULTCONFIG_FILENAME))) { + try { + wrapper.reloadConfig(); + vaultSettings.lastKnownKeyLoader.set(wrapper.get().getKeyId().getScheme()); + } catch (IOException e) { + LOG.debug("Unable to load config for {}", vaultSettings.path.get(), e); + } + } + } + } + } + public static VaultState.Value redetermineVaultState(Vault vault) { VaultState state = vault.stateProperty(); - VaultState.Value previousState = state.getValue(); - return switch (previousState) { - case LOCKED, NEEDS_MIGRATION, MISSING -> { - try { - var determinedState = determineVaultState(vault.getPath()); - if (determinedState == LOCKED) { - vault.getVaultConfigCache().reloadConfig(); - } - state.set(determinedState); - yield determinedState; - } catch (IOException e) { - LOG.warn("Failed to determine vault state for " + vault.getPath(), e); - state.set(ERROR); - vault.setLastKnownException(e); - yield ERROR; - } + VaultState.Value previous = state.getValue(); + + if (previous.equals(UNLOCKED) || previous.equals(PROCESSING)) { + return previous; + } + + try { + VaultState.Value determined = determineVaultState(vault.getPath()); + + if (determined == LOCKED) { + vault.getVaultConfigCache().reloadConfig(); } - case ERROR, UNLOCKED, PROCESSING -> previousState; - }; + + state.set(determined); + return determined; + } catch (IOException e) { + LOG.warn("Failed to (re)determine vault state for {}", vault.getPath(), e); + vault.setLastKnownException(e); + state.set(ERROR); + return ERROR; + } } - private static VaultState.Value determineVaultState(Path pathToVault) throws IOException { + public static VaultState.Value determineVaultState(Path pathToVault) throws IOException { if (!Files.exists(pathToVault)) { - return VaultState.Value.MISSING; + return MISSING; } + + VaultState.Value structureResult = checkDirStructure(pathToVault); + + if (structureResult == LOCKED || structureResult == NEEDS_MIGRATION) { + return structureResult; + } + + Path pathToVaultConfig = pathToVault.resolve(VAULTCONFIG_FILENAME); + Path pathToMasterkey = pathToVault.resolve(MASTERKEY_FILENAME); + + if (!Files.exists(pathToVaultConfig)) { + BackupRestorer.restoreIfBackupPresent(pathToVault, VAULTCONFIG_FILENAME); + } + if (!Files.exists(pathToMasterkey)) { + BackupRestorer.restoreIfBackupPresent(pathToVault, MASTERKEY_FILENAME); + } + + boolean hasConfig = Files.exists(pathToVaultConfig); + + if (!hasConfig && !Files.exists(pathToMasterkey)) { + return ALL_MISSING; + } + if (!hasConfig) { + return VAULT_CONFIG_MISSING; + } + + return checkDirStructure(pathToVault); + } + + private static VaultState.Value checkDirStructure(Path pathToVault) throws IOException { return switch (CryptoFileSystemProvider.checkDirStructureForVault(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME)) { - case VAULT -> VaultState.Value.LOCKED; - case UNRELATED -> VaultState.Value.MISSING; - case MAYBE_LEGACY -> Migrators.get().needsMigration(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) ? // - VaultState.Value.NEEDS_MIGRATION // - : VaultState.Value.MISSING; + case VAULT -> LOCKED; + case UNRELATED -> MISSING; + case MAYBE_LEGACY -> Migrators.get().needsMigration(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) ? NEEDS_MIGRATION : MISSING; }; } diff --git a/src/main/java/org/cryptomator/common/vaults/VaultState.java b/src/main/java/org/cryptomator/common/vaults/VaultState.java index ff09c8b82..f8b9b412a 100644 --- a/src/main/java/org/cryptomator/common/vaults/VaultState.java +++ b/src/main/java/org/cryptomator/common/vaults/VaultState.java @@ -25,6 +25,16 @@ public class VaultState extends ObservableValueBase implements */ MISSING, + /** + * No vault config found at the provided path + */ + VAULT_CONFIG_MISSING, + + /** + * No vault config and masterkey found at the provided path + */ + ALL_MISSING, + /** * Vault requires migration to a newer vault format */ diff --git a/src/main/java/org/cryptomator/ui/changepassword/PasswordStrengthUtil.java b/src/main/java/org/cryptomator/ui/changepassword/PasswordStrengthUtil.java index 202e2f9cb..4889a32f1 100644 --- a/src/main/java/org/cryptomator/ui/changepassword/PasswordStrengthUtil.java +++ b/src/main/java/org/cryptomator/ui/changepassword/PasswordStrengthUtil.java @@ -20,7 +20,6 @@ import java.util.ResourceBundle; public class PasswordStrengthUtil { private static final int PW_TRUNC_LEN = 100; // truncate very long passwords, since zxcvbn memory and runtime depends vastly on the length - private static final String RESSOURCE_PREFIX = "passwordStrength.messageLabel."; private static final List SANITIZED_INPUTS = List.of("cryptomator"); private final ResourceBundle resourceBundle; @@ -48,13 +47,15 @@ public class PasswordStrengthUtil { } public String getStrengthDescription(Number score) { - if (score.intValue() == -1) { - return String.format(resourceBundle.getString(RESSOURCE_PREFIX + "tooShort"), minPwLength); - } else if (resourceBundle.containsKey(RESSOURCE_PREFIX + score.intValue())) { - return resourceBundle.getString(RESSOURCE_PREFIX + score.intValue()); - } else { - return ""; - } + return switch (score.intValue()) { + case -1 -> String.format(resourceBundle.getString("passwordStrength.messageLabel.tooShort"), minPwLength); + case 0 -> resourceBundle.getString("passwordStrength.messageLabel.0"); + case 1 -> resourceBundle.getString("passwordStrength.messageLabel.1"); + case 2 -> resourceBundle.getString("passwordStrength.messageLabel.2"); + case 3 -> resourceBundle.getString("passwordStrength.messageLabel.3"); + case 4 -> resourceBundle.getString("passwordStrength.messageLabel.4"); + default -> ""; + }; } } diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java index ce8c65a37..68607808d 100644 --- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -42,9 +42,10 @@ public enum FxmlFile { QUIT("/fxml/quit.fxml"), // QUIT_FORCED("/fxml/quit_forced.fxml"), // RECOVERYKEY_CREATE("/fxml/recoverykey_create.fxml"), // + RECOVERYKEY_EXPERT_SETTINGS("/fxml/recoverykey_expert_settings.fxml"), // + RECOVERYKEY_ONBOARDING("/fxml/recoverykey_onboarding.fxml"), // RECOVERYKEY_RECOVER("/fxml/recoverykey_recover.fxml"), // RECOVERYKEY_RESET_PASSWORD("/fxml/recoverykey_reset_password.fxml"), // - RECOVERYKEY_RESET_PASSWORD_SUCCESS("/fxml/recoverykey_reset_password_success.fxml"), // RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), // SHARE_VAULT("/fxml/share_vault.fxml"), // SIMPLE_DIALOG("/fxml/simple_dialog.fxml"), // diff --git a/src/main/java/org/cryptomator/ui/convertvault/ConvertVaultModule.java b/src/main/java/org/cryptomator/ui/convertvault/ConvertVaultModule.java index f70242d2b..d010ede43 100644 --- a/src/main/java/org/cryptomator/ui/convertvault/ConvertVaultModule.java +++ b/src/main/java/org/cryptomator/ui/convertvault/ConvertVaultModule.java @@ -4,8 +4,10 @@ import dagger.Binds; import dagger.Module; import dagger.Provides; import dagger.multibindings.IntoMap; +import org.cryptomator.common.recovery.RecoveryActionType; import org.cryptomator.common.vaults.Vault; import org.cryptomator.cryptofs.VaultConfig; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; import org.cryptomator.ui.changepassword.NewPasswordController; import org.cryptomator.ui.changepassword.PasswordStrengthUtil; import org.cryptomator.ui.common.DefaultSceneFactory; @@ -20,6 +22,7 @@ import org.cryptomator.ui.recoverykey.RecoveryKeyValidateController; import javax.inject.Named; import javax.inject.Provider; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.scene.Scene; @@ -119,8 +122,8 @@ abstract class ConvertVaultModule { @Provides @IntoMap @FxControllerKey(RecoveryKeyValidateController.class) - static FxController bindRecoveryKeyValidateController(@ConvertVaultWindow Vault vault, @ConvertVaultWindow VaultConfig.UnverifiedVaultConfig vaultConfig, @ConvertVaultWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory) { - return new RecoveryKeyValidateController(vault, vaultConfig, recoveryKey, recoveryKeyFactory); + static FxController provideRecoveryKeyValidateController(@ConvertVaultWindow Vault vault, @ConvertVaultWindow VaultConfig.UnverifiedVaultConfig vaultConfig, @ConvertVaultWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory, MasterkeyFileAccess masterkeyFileAccess) { + return new RecoveryKeyValidateController(vault, vaultConfig, recoveryKey, recoveryKeyFactory, masterkeyFileAccess, new SimpleObjectProperty<>(RecoveryActionType.CONVERT_VAULT), new SimpleObjectProperty<>(null)); } } diff --git a/src/main/java/org/cryptomator/ui/decryptname/DecryptFileNamesViewController.java b/src/main/java/org/cryptomator/ui/decryptname/DecryptFileNamesViewController.java index 453762f55..5450e1d48 100644 --- a/src/main/java/org/cryptomator/ui/decryptname/DecryptFileNamesViewController.java +++ b/src/main/java/org/cryptomator/ui/decryptname/DecryptFileNamesViewController.java @@ -194,7 +194,7 @@ public class DecryptFileNamesViewController implements FxController { } } - //obvservable getter + //observable getter public ObservableValue dropZoneTextProperty() { return dropZoneText; diff --git a/src/main/java/org/cryptomator/ui/dialogs/Dialogs.java b/src/main/java/org/cryptomator/ui/dialogs/Dialogs.java index 837bea012..07ce3c4f6 100644 --- a/src/main/java/org/cryptomator/ui/dialogs/Dialogs.java +++ b/src/main/java/org/cryptomator/ui/dialogs/Dialogs.java @@ -2,6 +2,7 @@ package org.cryptomator.ui.dialogs; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.vaults.Vault; +import org.cryptomator.ui.common.DefaultSceneFactory; import org.cryptomator.ui.common.StageFactory; import org.cryptomator.ui.controls.FontAwesome5Icon; import org.cryptomator.ui.fxapp.FxApplicationScoped; @@ -19,17 +20,21 @@ public class Dialogs { private final ResourceBundle resourceBundle; private final StageFactory stageFactory; + private final DefaultSceneFactory sceneFactory; + + private static final String BUTTON_KEY_CLOSE = "generic.button.close"; @Inject - public Dialogs(ResourceBundle resourceBundle, StageFactory stageFactory) { + public Dialogs(ResourceBundle resourceBundle, StageFactory stageFactory, DefaultSceneFactory sceneFactory) { this.resourceBundle = resourceBundle; this.stageFactory = stageFactory; + this.sceneFactory = sceneFactory; } private static final Logger LOG = LoggerFactory.getLogger(Dialogs.class); private SimpleDialog.Builder createDialogBuilder() { - return new SimpleDialog.Builder(resourceBundle, stageFactory); + return new SimpleDialog.Builder(resourceBundle, stageFactory, sceneFactory); } public SimpleDialog.Builder prepareRemoveVaultDialog(Stage window, Vault vault, ObservableList vaults) { @@ -47,6 +52,43 @@ public class Dialogs { }); } + public SimpleDialog.Builder prepareContactHubVaultOwner(Stage window) { + return createDialogBuilder().setOwner(window) // + .setTitleKey("contactHubVaultOwner.title") // + .setMessageKey("contactHubVaultOwner.message") // + .setDescriptionKey("contactHubVaultOwner.description") // + .setIcon(FontAwesome5Icon.EXCLAMATION)// + .setOkButtonKey(BUTTON_KEY_CLOSE); + } + + public SimpleDialog.Builder prepareRecoveryVaultAdded(Stage window, String displayName) { + return createDialogBuilder().setOwner(window) // + .setTitleKey("recover.existing.title") // + .setMessageKey("recover.existing.message") // + .setDescriptionKey("recover.existing.description", displayName) // + .setIcon(FontAwesome5Icon.CHECK)// + .setOkButtonKey(BUTTON_KEY_CLOSE); + } + public SimpleDialog.Builder prepareRecoveryVaultAlreadyExists(Stage window, String displayName) { + return createDialogBuilder().setOwner(window) // + .setTitleKey("recover.alreadyExists.title") // + .setMessageKey("recover.alreadyExists.message") // + .setDescriptionKey("recover.alreadyExists.description", displayName) // + .setIcon(FontAwesome5Icon.EXCLAMATION)// + .setOkButtonKey(BUTTON_KEY_CLOSE); + } + + public SimpleDialog.Builder prepareRecoverPasswordSuccess(Stage window) { + return createDialogBuilder() + .setOwner(window) // + .setTitleKey("recoveryKey.recover.title") // + .setMessageKey("recoveryKey.recover.resetSuccess.message") // + .setDescriptionKey("recoveryKey.recover.resetSuccess.description") // + .setIcon(FontAwesome5Icon.CHECK) + .setOkAction(Stage::close) + .setOkButtonKey(BUTTON_KEY_CLOSE); + } + public SimpleDialog.Builder prepareRemoveCertDialog(Stage window, Settings settings) { return createDialogBuilder() // .setOwner(window) // @@ -69,7 +111,7 @@ public class Dialogs { .setMessageKey("dokanySupportEnd.message") // .setDescriptionKey("dokanySupportEnd.description") // .setIcon(FontAwesome5Icon.EXCLAMATION) // - .setOkButtonKey("generic.button.close") // + .setOkButtonKey(BUTTON_KEY_CLOSE) // .setCancelButtonKey("dokanySupportEnd.preferencesBtn") // .setOkAction(Stage::close) // .setCancelAction(cancelAction); @@ -83,8 +125,20 @@ public class Dialogs { .setDescriptionKey("retryIfReadonly.description") // .setIcon(FontAwesome5Icon.EXCLAMATION) // .setOkButtonKey("retryIfReadonly.retry") // - .setCancelButtonKey("generic.button.close") // + .setCancelButtonKey(BUTTON_KEY_CLOSE) // .setOkAction(okAction) // .setCancelAction(Stage::close); } + + public SimpleDialog.Builder prepareNoDDirectorySelectedDialog(Stage window) { + return createDialogBuilder() // + .setOwner(window) // + .setTitleKey("recover.invalidSelection.title") // + .setMessageKey("recover.invalidSelection.message") // + .setDescriptionKey("recover.invalidSelection.description") // + .setIcon(FontAwesome5Icon.EXCLAMATION) // + .setOkButtonKey("generic.button.change") // + .setOkAction(Stage::close); + } + } diff --git a/src/main/java/org/cryptomator/ui/dialogs/SimpleDialog.java b/src/main/java/org/cryptomator/ui/dialogs/SimpleDialog.java index 08f77849e..2c447300d 100644 --- a/src/main/java/org/cryptomator/ui/dialogs/SimpleDialog.java +++ b/src/main/java/org/cryptomator/ui/dialogs/SimpleDialog.java @@ -1,5 +1,6 @@ package org.cryptomator.ui.dialogs; +import org.cryptomator.ui.common.DefaultSceneFactory; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlLoaderFactory; import org.cryptomator.ui.common.StageFactory; @@ -30,15 +31,15 @@ public class SimpleDialog { FxmlLoaderFactory loaderFactory = FxmlLoaderFactory.forController( // new SimpleDialogController(resolveText(builder.messageKey, null), // - resolveText(builder.descriptionKey, null), // + resolveText(builder.descriptionKey, builder.descriptionArgs), // builder.icon, // resolveText(builder.okButtonKey, null), // builder.cancelButtonKey != null ? resolveText(builder.cancelButtonKey, null) : null, // () -> builder.okAction.accept(dialogStage), // () -> builder.cancelAction.accept(dialogStage)), // - Scene::new, builder.resourceBundle); + builder.sceneFactory, builder.resourceBundle); - dialogStage.setScene(new Scene(loaderFactory.load(FxmlFile.SIMPLE_DIALOG.getRessourcePathString()).getRoot())); + dialogStage.setScene(loaderFactory.createScene(FxmlFile.SIMPLE_DIALOG)); } public void showAndWait() { @@ -62,19 +63,22 @@ public class SimpleDialog { private Stage owner; private final ResourceBundle resourceBundle; private final StageFactory stageFactory; + private final DefaultSceneFactory sceneFactory; private String titleKey; private String[] titleArgs; private String messageKey; private String descriptionKey; + private String[] descriptionArgs; private String okButtonKey; private String cancelButtonKey; private FontAwesome5Icon icon; private Consumer okAction = Stage::close; private Consumer cancelAction = Stage::close; - public Builder(ResourceBundle resourceBundle, StageFactory stageFactory) { + public Builder(ResourceBundle resourceBundle, StageFactory stageFactory, DefaultSceneFactory sceneFactory) { this.resourceBundle = resourceBundle; this.stageFactory = stageFactory; + this.sceneFactory = sceneFactory; } public Builder setOwner(Stage owner) { @@ -93,8 +97,9 @@ public class SimpleDialog { return this; } - public Builder setDescriptionKey(String descriptionKey) { + public Builder setDescriptionKey(String descriptionKey, String... args) { this.descriptionKey = descriptionKey; + this.descriptionArgs = args; return this; } diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java index 8eb221883..74abac546 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java @@ -15,15 +15,13 @@ import org.cryptomator.ui.lock.LockComponent; import org.cryptomator.ui.mainwindow.MainWindowComponent; import org.cryptomator.ui.preferences.PreferencesComponent; import org.cryptomator.ui.quit.QuitComponent; +import org.cryptomator.ui.recoverykey.RecoveryKeyComponent; import org.cryptomator.ui.sharevault.ShareVaultComponent; import org.cryptomator.ui.traymenu.TrayMenuComponent; import org.cryptomator.ui.unlock.UnlockComponent; import org.cryptomator.ui.updatereminder.UpdateReminderComponent; import org.cryptomator.ui.vaultoptions.VaultOptionsComponent; -import javax.inject.Named; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; import javafx.scene.image.Image; import java.io.IOException; import java.io.InputStream; @@ -40,7 +38,8 @@ import java.io.InputStream; HealthCheckComponent.class, // UpdateReminderComponent.class, // ShareVaultComponent.class, // - EventViewComponent.class}) + EventViewComponent.class, // + RecoveryKeyComponent.class}) abstract class FxApplicationModule { private static Image createImageFromResource(String resourceName) throws IOException { diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationTerminator.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationTerminator.java index 89258dc6c..91ff918e3 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationTerminator.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationTerminator.java @@ -28,7 +28,7 @@ import static org.cryptomator.common.vaults.VaultState.Value.*; @FxApplicationScoped public class FxApplicationTerminator { - private static final Set STATES_ALLOWING_TERMINATION = EnumSet.of(LOCKED, NEEDS_MIGRATION, MISSING, ERROR); + private static final Set STATES_ALLOWING_TERMINATION = EnumSet.of(LOCKED, NEEDS_MIGRATION, MISSING, ERROR, VAULT_CONFIG_MISSING, ALL_MISSING); private static final Set STATES_PREVENT_TERMINATION = EnumSet.of(PROCESSING); private static final Logger LOG = LoggerFactory.getLogger(FxApplicationTerminator.class); diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileController.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileController.java index 9b2231921..4e1c663e9 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileController.java @@ -1,14 +1,18 @@ package org.cryptomator.ui.keyloading.masterkeyfile; +import org.cryptomator.common.recovery.RecoveryActionType; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.keyloading.KeyLoading; +import org.cryptomator.ui.recoverykey.RecoveryKeyComponent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; -import javafx.beans.binding.StringBinding; +import javafx.beans.property.SimpleObjectProperty; import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; import javafx.stage.FileChooser; import javafx.stage.Stage; import javafx.stage.WindowEvent; @@ -27,17 +31,41 @@ public class ChooseMasterkeyFileController implements FxController { private final Stage window; private final Vault vault; private final CompletableFuture result; + private final RecoveryKeyComponent.Factory recoveryKeyWindow; private final ResourceBundle resourceBundle; + @FXML + private CheckBox restoreInsteadCheckBox; + @FXML + private Button forwardButton; + @Inject - public ChooseMasterkeyFileController(@KeyLoading Stage window, @KeyLoading Vault vault, CompletableFuture result, ResourceBundle resourceBundle) { + public ChooseMasterkeyFileController(@KeyLoading Stage window, // + @KeyLoading Vault vault, // + CompletableFuture result, // + RecoveryKeyComponent.Factory recoveryKeyWindow, // + ResourceBundle resourceBundle) { this.window = window; this.vault = vault; this.result = result; + this.recoveryKeyWindow = recoveryKeyWindow; this.resourceBundle = resourceBundle; this.window.setOnHiding(this::windowClosed); } + @FXML + private void initialize() { + restoreInsteadCheckBox.selectedProperty().addListener((_, _, newVal) -> { + if (newVal) { + forwardButton.setText(resourceBundle.getString("addvaultwizard.existing.restore")); + forwardButton.setOnAction(_ -> restoreMasterkey()); + } else { + forwardButton.setText(resourceBundle.getString("generic.button.choose")); + forwardButton.setOnAction(_ -> proceed()); + } + }); + } + @FXML public void cancel() { window.close(); @@ -47,6 +75,13 @@ public class ChooseMasterkeyFileController implements FxController { result.cancel(true); } + @FXML + void restoreMasterkey() { + Stage ownerStage = (Stage) window.getOwner(); + window.close(); + recoveryKeyWindow.create(vault, ownerStage, new SimpleObjectProperty<>(RecoveryActionType.RESTORE_MASTERKEY)).showOnboardingDialogWindow(); + } + @FXML public void proceed() { LOG.trace("proceed()"); @@ -62,7 +97,7 @@ public class ChooseMasterkeyFileController implements FxController { //--- Setter & Getter --- - public String getDisplayName(){ + public String getDisplayName() { return vault.getDisplayName(); } diff --git a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java index fa1b441d9..a186fbe71 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java @@ -18,6 +18,7 @@ import org.cryptomator.ui.error.ErrorComponent; import org.cryptomator.ui.fxapp.FxApplicationTerminator; import org.cryptomator.ui.fxapp.PrimaryStage; import org.cryptomator.ui.migration.MigrationComponent; +import org.cryptomator.ui.recoverykey.RecoveryKeyComponent; import org.cryptomator.ui.stats.VaultStatisticsComponent; import org.cryptomator.ui.traymenu.TrayMenuComponent; import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent; @@ -32,7 +33,7 @@ import javafx.stage.Stage; import java.util.Map; import java.util.ResourceBundle; -@Module(subcomponents = {AddVaultWizardComponent.class, MigrationComponent.class, VaultStatisticsComponent.class, WrongFileAlertComponent.class, ErrorComponent.class}) +@Module(subcomponents = {AddVaultWizardComponent.class, MigrationComponent.class, VaultStatisticsComponent.class, WrongFileAlertComponent.class, ErrorComponent.class, RecoveryKeyComponent.class}) abstract class MainWindowModule { @Provides diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailController.java index 7e309fdaf..be4f7f78c 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailController.java @@ -52,7 +52,7 @@ public class VaultDetailController implements FxController { case LOCKED -> FontAwesome5Icon.LOCK; case PROCESSING -> FontAwesome5Icon.SPINNER; case UNLOCKED -> FontAwesome5Icon.LOCK_OPEN; - case NEEDS_MIGRATION, MISSING, ERROR -> FontAwesome5Icon.EXCLAMATION_TRIANGLE; + case NEEDS_MIGRATION, MISSING, VAULT_CONFIG_MISSING, ALL_MISSING, ERROR -> FontAwesome5Icon.EXCLAMATION_TRIANGLE; }; } else { return FontAwesome5Icon.EXCLAMATION_TRIANGLE; diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailMissingVaultController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailMissingVaultController.java index 6f57a0d17..85e71937b 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailMissingVaultController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailMissingVaultController.java @@ -1,20 +1,26 @@ package org.cryptomator.ui.mainwindow; +import org.cryptomator.common.recovery.RecoveryActionType; import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.dialogs.Dialogs; +import org.cryptomator.ui.keyloading.KeyLoadingStrategy; +import org.cryptomator.ui.recoverykey.RecoveryKeyComponent; import javax.inject.Inject; import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.stage.FileChooser; import javafx.stage.Stage; import java.io.File; +import java.nio.file.Files; import java.util.ResourceBundle; import static org.cryptomator.common.Constants.CRYPTOMATOR_FILENAME_GLOB; +import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; @MainWindowScoped public class VaultDetailMissingVaultController implements FxController { @@ -23,6 +29,7 @@ public class VaultDetailMissingVaultController implements FxController { private final ObservableList vaults; private final ResourceBundle resourceBundle; private final Stage window; + private final RecoveryKeyComponent.Factory recoveryKeyWindow; private final Dialogs dialogs; @Inject @@ -30,11 +37,13 @@ public class VaultDetailMissingVaultController implements FxController { ObservableList vaults, // ResourceBundle resourceBundle, // @MainWindow Stage window, // - Dialogs dialogs) { + Dialogs dialogs, // + RecoveryKeyComponent.Factory recoveryKeyWindow) { this.vault = vault; this.vaults = vaults; this.resourceBundle = resourceBundle; this.window = window; + this.recoveryKeyWindow = recoveryKeyWindow; this.dialogs = dialogs; } @@ -48,6 +57,19 @@ public class VaultDetailMissingVaultController implements FxController { dialogs.prepareRemoveVaultDialog(window, vault.get(), vaults).build().showAndWait(); } + @FXML + void restoreVaultConfig() { + if(KeyLoadingStrategy.isHubVault(vault.get().getVaultSettings().lastKnownKeyLoader.get())){ + dialogs.prepareContactHubVaultOwner(window).build().showAndWait(); + } + else if(Files.exists(vault.get().getPath().resolve(MASTERKEY_FILENAME))){ + recoveryKeyWindow.create(vault.get(), window, new SimpleObjectProperty<>(RecoveryActionType.RESTORE_VAULT_CONFIG)).showOnboardingDialogWindow(); + } + else { + recoveryKeyWindow.create(vault.get(), window, new SimpleObjectProperty<>(RecoveryActionType.RESTORE_ALL)).showOnboardingDialogWindow(); + } + } + @FXML void changeLocation() { // copied from ChooseExistingVaultController class diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellController.java index 75ce21dfe..9324c8c7b 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellController.java @@ -55,7 +55,7 @@ public class VaultListCellController implements FxController { case LOCKED -> FontAwesome5Icon.LOCK; case PROCESSING -> FontAwesome5Icon.SPINNER; case UNLOCKED -> FontAwesome5Icon.LOCK_OPEN; - case NEEDS_MIGRATION, MISSING, ERROR -> FontAwesome5Icon.EXCLAMATION_TRIANGLE; + case NEEDS_MIGRATION, MISSING, VAULT_CONFIG_MISSING, ALL_MISSING, ERROR -> FontAwesome5Icon.EXCLAMATION_TRIANGLE; }; } else { return FontAwesome5Icon.EXCLAMATION_TRIANGLE; diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultListContextMenuController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultListContextMenuController.java index 5e1fb8c35..db667f111 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultListContextMenuController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultListContextMenuController.java @@ -20,11 +20,13 @@ import javafx.stage.Stage; import java.util.EnumSet; import java.util.Objects; +import static org.cryptomator.common.vaults.VaultState.Value.ALL_MISSING; import static org.cryptomator.common.vaults.VaultState.Value.ERROR; import static org.cryptomator.common.vaults.VaultState.Value.LOCKED; import static org.cryptomator.common.vaults.VaultState.Value.MISSING; import static org.cryptomator.common.vaults.VaultState.Value.NEEDS_MIGRATION; import static org.cryptomator.common.vaults.VaultState.Value.UNLOCKED; +import static org.cryptomator.common.vaults.VaultState.Value.VAULT_CONFIG_MISSING; @MainWindowScoped public class VaultListContextMenuController implements FxController { @@ -63,7 +65,7 @@ public class VaultListContextMenuController implements FxController { this.selectedVaultState = selectedVault.flatMap(Vault::stateProperty).orElse(null); this.selectedVaultPassphraseStored = selectedVault.map(this::isPasswordStored).orElse(false); - this.selectedVaultRemovable = selectedVaultState.map(EnumSet.of(LOCKED, MISSING, ERROR, NEEDS_MIGRATION)::contains).orElse(false); + this.selectedVaultRemovable = selectedVaultState.map(EnumSet.of(LOCKED, MISSING, ERROR, NEEDS_MIGRATION, ALL_MISSING, VAULT_CONFIG_MISSING)::contains).orElse(false); this.selectedVaultUnlockable = selectedVaultState.map(LOCKED::equals).orElse(false); this.selectedVaultLockable = selectedVaultState.map(UNLOCKED::equals).orElse(false); } diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java index a457ade3f..f25528498 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java @@ -1,11 +1,16 @@ package org.cryptomator.ui.mainwindow; import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.common.recovery.RecoveryActionType; +import org.cryptomator.common.recovery.VaultPreparator; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.vaults.Vault; +import org.cryptomator.common.vaults.VaultComponent; import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.cryptofs.CryptoFileSystemProvider; import org.cryptomator.cryptofs.DirStructure; +import org.cryptomator.cryptofs.common.Constants; +import org.cryptomator.integrations.mount.MountService; import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.VaultService; @@ -13,6 +18,7 @@ import org.cryptomator.ui.dialogs.Dialogs; import org.cryptomator.ui.fxapp.FxFSEventList; import org.cryptomator.ui.fxapp.FxApplicationWindows; import org.cryptomator.ui.preferences.SelectedPreferencesTab; +import org.cryptomator.ui.recoverykey.RecoveryKeyComponent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,6 +28,7 @@ import javafx.beans.binding.BooleanBinding; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; @@ -37,11 +44,14 @@ import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; import javafx.scene.input.TransferMode; import javafx.scene.layout.StackPane; +import javafx.stage.DirectoryChooser; import javafx.stage.Stage; import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.EnumSet; +import java.util.List; import java.util.Optional; import java.util.ResourceBundle; import java.util.Set; @@ -50,10 +60,12 @@ import java.util.stream.Collectors; import static org.cryptomator.common.Constants.CRYPTOMATOR_FILENAME_EXT; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME; +import static org.cryptomator.common.vaults.VaultState.Value.ALL_MISSING; import static org.cryptomator.common.vaults.VaultState.Value.ERROR; import static org.cryptomator.common.vaults.VaultState.Value.LOCKED; import static org.cryptomator.common.vaults.VaultState.Value.MISSING; import static org.cryptomator.common.vaults.VaultState.Value.NEEDS_MIGRATION; +import static org.cryptomator.common.vaults.VaultState.Value.VAULT_CONFIG_MISSING; @MainWindowScoped public class VaultListController implements FxController { @@ -75,6 +87,10 @@ public class VaultListController implements FxController { private final ObservableValue cellSize; private final Dialogs dialogs; + private final VaultComponent.Factory vaultComponentFactory; + private final RecoveryKeyComponent.Factory recoveryKeyWindow; + private final List mountServices; + public ListView vaultList; public StackPane root; @FXML @@ -94,6 +110,9 @@ public class VaultListController implements FxController { FxApplicationWindows appWindows, // Settings settings, // Dialogs dialogs, // + RecoveryKeyComponent.Factory recoveryKeyWindow, // + VaultComponent.Factory vaultComponentFactory, // + List mountServices, // FxFSEventList fxFSEventList) { this.mainWindow = mainWindow; this.vaults = vaults; @@ -105,6 +124,9 @@ public class VaultListController implements FxController { this.resourceBundle = resourceBundle; this.appWindows = appWindows; this.dialogs = dialogs; + this.recoveryKeyWindow = recoveryKeyWindow; + this.vaultComponentFactory = vaultComponentFactory; + this.mountServices = mountServices; this.emptyVaultList = Bindings.isEmpty(vaults); this.unreadEvents = fxFSEventList.unreadEventsProperty(); @@ -204,6 +226,26 @@ public class VaultListController implements FxController { VaultListManager.redetermineVaultState(newValue); } + private Optional chooseValidVaultDirectory() { + DirectoryChooser directoryChooser = new DirectoryChooser(); + File selectedDirectory; + + do { + selectedDirectory = directoryChooser.showDialog(mainWindow); + if (selectedDirectory == null) { + return Optional.empty(); + } + + Path selectedPath = selectedDirectory.toPath(); + if (!Files.isDirectory(selectedPath.resolve(Constants.DATA_DIR_NAME))) { + dialogs.prepareNoDDirectorySelectedDialog(mainWindow).build().showAndWait(); + selectedDirectory = null; + } + } while (selectedDirectory == null); + + return Optional.of(selectedDirectory.toPath()); + } + @FXML public void didClickAddNewVault() { addVaultWizard.build().showAddNewVaultWizard(resourceBundle); @@ -214,9 +256,40 @@ public class VaultListController implements FxController { addVaultWizard.build().showAddExistingVaultWizard(resourceBundle); } + @FXML + public void didClickRecoverExistingVault() { + Optional selectedDirectory = chooseValidVaultDirectory(); + if (selectedDirectory.isEmpty()) { + return; + } + + Path path = selectedDirectory.get(); + Optional matchingVaultListEntry = vaultListManager.get(path); + if (matchingVaultListEntry.isPresent()) { + dialogs.prepareRecoveryVaultAlreadyExists(mainWindow, matchingVaultListEntry.get().getDisplayName()) // + .setOkAction(Stage::close) // + .build().showAndWait(); + return; + } + + Vault preparedVault = VaultPreparator.prepareVault(path, vaultComponentFactory, mountServices, resourceBundle); + VaultListManager.redetermineVaultState(preparedVault); + + switch (preparedVault.getState()) { + case VAULT_CONFIG_MISSING -> recoveryKeyWindow.create(preparedVault, mainWindow, new SimpleObjectProperty<>(RecoveryActionType.RESTORE_VAULT_CONFIG)).showOnboardingDialogWindow(); + case ALL_MISSING -> recoveryKeyWindow.create(preparedVault, mainWindow, new SimpleObjectProperty<>(RecoveryActionType.RESTORE_ALL)).showOnboardingDialogWindow(); + case LOCKED, NEEDS_MIGRATION -> { + vaultListManager.addVault(preparedVault); + dialogs.prepareRecoveryVaultAdded(mainWindow, preparedVault.getDisplayName()).setOkAction(Stage::close).build().showAndWait(); + } + default -> LOG.warn("Unhandled vault state during recovery: {}", preparedVault.getState()); + } + + } + private void pressedShortcutToRemoveVault() { final var vault = selectedVault.get(); - if (vault != null && EnumSet.of(LOCKED, MISSING, ERROR, NEEDS_MIGRATION).contains(vault.getState())) { + if (vault != null && EnumSet.of(LOCKED, MISSING, ERROR, NEEDS_MIGRATION, ALL_MISSING, VAULT_CONFIG_MISSING).contains(vault.getState())) { dialogs.prepareRemoveVaultDialog(mainWindow, vault, vaults).build().showAndWait(); } } diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyComponent.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyComponent.java index 3986fa01d..6bfe36a4f 100644 --- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyComponent.java +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyComponent.java @@ -3,11 +3,13 @@ package org.cryptomator.ui.recoverykey; import dagger.BindsInstance; import dagger.Lazy; import dagger.Subcomponent; +import org.cryptomator.common.recovery.RecoveryActionType; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; import javax.inject.Named; +import javafx.beans.property.ObjectProperty; import javafx.scene.Scene; import javafx.stage.Stage; @@ -24,6 +26,9 @@ public interface RecoveryKeyComponent { @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy recoverScene(); + @FxmlScene(FxmlFile.RECOVERYKEY_ONBOARDING) + Lazy recoverOnboardingScene(); + default void showRecoveryKeyCreationWindow() { Stage stage = window(); stage.setScene(creationScene().get()); @@ -38,11 +43,19 @@ public interface RecoveryKeyComponent { stage.show(); } + default void showOnboardingDialogWindow() { + Stage stage = window(); + stage.setScene(recoverOnboardingScene().get()); + stage.sizeToScene(); + stage.show(); + } @Subcomponent.Factory interface Factory { - RecoveryKeyComponent create(@BindsInstance @RecoveryKeyWindow Vault vault, @BindsInstance @Named("keyRecoveryOwner") Stage owner); + RecoveryKeyComponent create(@BindsInstance @RecoveryKeyWindow Vault vault, // + @BindsInstance @Named("keyRecoveryOwner") Stage owner, // + @BindsInstance @Named("recoverType") ObjectProperty recoverType); } } diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java index 77f191015..456de11f4 100644 --- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java @@ -1,28 +1,45 @@ package org.cryptomator.ui.recoverykey; import dagger.Lazy; +import org.cryptomator.common.recovery.CryptoFsInitializer; +import org.cryptomator.common.recovery.MasterkeyService; +import org.cryptomator.common.recovery.RecoveryActionType; +import org.cryptomator.common.recovery.RecoveryDirectory; import org.cryptomator.common.vaults.Vault; +import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.InvalidPassphraseException; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; import org.cryptomator.ui.common.Animations; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; +import org.cryptomator.ui.controls.FormattedLabel; import org.cryptomator.ui.controls.NiceSecurePasswordField; +import org.cryptomator.ui.dialogs.Dialogs; import org.cryptomator.ui.fxapp.FxApplicationWindows; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; +import javax.inject.Named; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; import javafx.beans.property.StringProperty; import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.scene.Scene; +import javafx.scene.control.Button; import javafx.stage.Stage; import java.io.IOException; +import java.nio.file.Path; import java.util.ResourceBundle; import java.util.concurrent.ExecutorService; +import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; +import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME; + @RecoveryKeyScoped public class RecoveryKeyCreationController implements FxController { @@ -30,23 +47,71 @@ public class RecoveryKeyCreationController implements FxController { private final Stage window; private final Lazy successScene; + private final Lazy recoverykeyExpertSettingsScene; + private final MasterkeyFileAccess masterkeyFileAccess; private final Vault vault; private final ExecutorService executor; private final RecoveryKeyFactory recoveryKeyFactory; private final StringProperty recoveryKeyProperty; private final FxApplicationWindows appWindows; public NiceSecurePasswordField passwordField; + private final IntegerProperty shorteningThreshold; + private final ObjectProperty recoverType; + private final ResourceBundle resourceBundle; + public FormattedLabel descriptionLabel; + public Button cancelButton; + public Button nextButton; + private final VaultListManager vaultListManager; + private final Dialogs dialogs; @Inject - public RecoveryKeyCreationController(@RecoveryKeyWindow Stage window, @FxmlScene(FxmlFile.RECOVERYKEY_SUCCESS) Lazy successScene, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, FxApplicationWindows appWindows, ResourceBundle resourceBundle) { + public RecoveryKeyCreationController(FxApplicationWindows appWindows, // + @RecoveryKeyWindow Stage window, // + @FxmlScene(FxmlFile.RECOVERYKEY_SUCCESS) Lazy successScene, // + @FxmlScene(FxmlFile.RECOVERYKEY_EXPERT_SETTINGS) Lazy recoverykeyExpertSettingsScene, // + @RecoveryKeyWindow Vault vault, // + RecoveryKeyFactory recoveryKeyFactory, // + MasterkeyFileAccess masterkeyFileAccess, // + ExecutorService executor, // + @RecoveryKeyWindow StringProperty recoveryKey, // + @Named("shorteningThreshold") IntegerProperty shorteningThreshold, // + @Named("recoverType") ObjectProperty recoverType, // + VaultListManager vaultListManager, // + ResourceBundle resourceBundle, // + Dialogs dialogs) { this.window = window; - window.setTitle(resourceBundle.getString("recoveryKey.display.title")); this.successScene = successScene; + this.recoverykeyExpertSettingsScene = recoverykeyExpertSettingsScene; this.vault = vault; this.executor = executor; this.recoveryKeyFactory = recoveryKeyFactory; this.recoveryKeyProperty = recoveryKey; this.appWindows = appWindows; + this.recoverType = recoverType; + this.resourceBundle = resourceBundle; + this.masterkeyFileAccess = masterkeyFileAccess; + this.shorteningThreshold = shorteningThreshold; + this.vaultListManager = vaultListManager; + this.dialogs = dialogs; + } + + @FXML + public void initialize() { + if (recoverType.get() == RecoveryActionType.SHOW_KEY) { + window.setTitle(resourceBundle.getString("recoveryKey.display.title")); + } else if (recoverType.get() == RecoveryActionType.RESTORE_VAULT_CONFIG) { + window.setTitle(resourceBundle.getString("recover.recoverVaultConfig.title")); + descriptionLabel.formatProperty().set(resourceBundle.getString("recoveryKey.recover.description")); + cancelButton.setOnAction((_) -> back()); + cancelButton.setText(resourceBundle.getString("generic.button.back")); + nextButton.setOnAction((_) -> restoreWithPassword()); + } + } + + @FXML + public void back() { + window.setScene(recoverykeyExpertSettingsScene.get()); + window.centerOnScreen(); } @FXML @@ -71,6 +136,42 @@ public class RecoveryKeyCreationController implements FxController { executor.submit(task); } + @FXML + public void restoreWithPassword() { + + try (RecoveryDirectory recoveryDirectory = RecoveryDirectory.create(vault.getPath())) { + Path recoveryPath = recoveryDirectory.getRecoveryPath(); + + Path masterkeyFilePath = vault.getPath().resolve(MASTERKEY_FILENAME); + + try (Masterkey masterkey = MasterkeyService.load(masterkeyFileAccess, masterkeyFilePath, passwordField.getCharacters())) { + var combo = MasterkeyService.detect(masterkey, vault.getPath()) + .orElseThrow(() -> new IllegalStateException("Could not detect combo for vault path: " + vault.getPath())); + + CryptoFsInitializer.init(recoveryPath, masterkey, shorteningThreshold.get(), combo); + } + + recoveryDirectory.moveRecoveredFile(VAULTCONFIG_FILENAME); + + if (!vaultListManager.isAlreadyAdded(vault.getPath())) { + vaultListManager.add(vault.getPath()); + } + window.close(); + dialogs.prepareRecoverPasswordSuccess((Stage)window.getOwner()) // + .setTitleKey("recover.recoverVaultConfig.title") // + .setMessageKey("recoveryKey.recover.resetVaultConfigSuccess.message") // + .setDescriptionKey("recoveryKey.recover.resetMasterkeyFileSuccess.description") + .build().showAndWait(); + + } catch (InvalidPassphraseException e) { + LOG.info("Password invalid", e); + Animations.createShakeWindowAnimation(window).play(); + } catch (IOException | CryptoException | IllegalStateException e) { + LOG.error("Recovery process failed", e); + appWindows.showErrorWindow(e, window, null); + } + } + @FXML public void close() { window.close(); diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyExpertSettingsController.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyExpertSettingsController.java new file mode 100644 index 000000000..5e72b8969 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyExpertSettingsController.java @@ -0,0 +1,123 @@ +package org.cryptomator.ui.recoverykey; + +import javax.inject.Inject; +import javax.inject.Named; +import javafx.application.Application; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; +import javafx.fxml.FXML; +import javafx.scene.Scene; +import javafx.scene.control.CheckBox; +import javafx.stage.Stage; + +import dagger.Lazy; +import org.cryptomator.common.recovery.RecoveryActionType; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.common.vaults.VaultState; +import org.cryptomator.ui.addvaultwizard.CreateNewVaultExpertSettingsController; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; +import org.cryptomator.ui.controls.NumericTextField; + +@RecoveryKeyScoped +public class RecoveryKeyExpertSettingsController implements FxController { + + public static final int MAX_SHORTENING_THRESHOLD = 220; + public static final int MIN_SHORTENING_THRESHOLD = 36; + private static final String DOCS_NAME_SHORTENING_URL = "https://docs.cryptomator.org/security/vault/#name-shortening"; + + private final Stage window; + private final Lazy application; + private final Vault vault; + private final ObjectProperty recoverType; + private final IntegerProperty shorteningThreshold; + private final Lazy resetPasswordScene; + private final Lazy createScene; + private final Lazy onBoardingScene; + private final Lazy recoverScene; + private final BooleanBinding validShorteningThreshold; + + @FXML + public CheckBox expertSettingsCheckBox; + @FXML + public NumericTextField shorteningThresholdTextField; + + @Inject + public RecoveryKeyExpertSettingsController(@RecoveryKeyWindow Stage window, // + Lazy application, // + @RecoveryKeyWindow Vault vault, // + @Named("recoverType") ObjectProperty recoverType, // + @Named("shorteningThreshold") IntegerProperty shorteningThreshold, // + @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) Lazy resetPasswordScene, // + @FxmlScene(FxmlFile.RECOVERYKEY_CREATE) Lazy createScene, // + @FxmlScene(FxmlFile.RECOVERYKEY_ONBOARDING) Lazy onBoardingScene, // + @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy recoverScene) { + this.window = window; + this.application = application; + this.vault = vault; + this.recoverType = recoverType; + this.shorteningThreshold = shorteningThreshold; + this.resetPasswordScene = resetPasswordScene; + this.createScene = createScene; + this.onBoardingScene = onBoardingScene; + this.recoverScene = recoverScene; + this.validShorteningThreshold = Bindings.createBooleanBinding(this::isValidShorteningThreshold, shorteningThreshold); + } + + @FXML + public void initialize() { + shorteningThresholdTextField.setPromptText(MIN_SHORTENING_THRESHOLD + "-" + MAX_SHORTENING_THRESHOLD); + shorteningThresholdTextField.setText(Integer.toString(MAX_SHORTENING_THRESHOLD)); + shorteningThresholdTextField.textProperty().addListener((_, _, newValue) -> { + try { + int intValue = Integer.parseInt(newValue); + shorteningThreshold.set(intValue); + } catch (NumberFormatException e) { + shorteningThreshold.set(0); //the value is set to 0 to ensure that an invalid value assignment is detected during a NumberFormatException + } + }); + } + + @FXML + public void toggleUseExpertSettings() { + if (!expertSettingsCheckBox.isSelected()) { + shorteningThresholdTextField.setText(Integer.toString(CreateNewVaultExpertSettingsController.MAX_SHORTENING_THRESHOLD)); + } + } + + public void openDocs() { + application.get().getHostServices().showDocument(DOCS_NAME_SHORTENING_URL); + } + + public BooleanBinding validShorteningThresholdProperty() { + return validShorteningThreshold; + } + + public boolean isValidShorteningThreshold() { + var value = shorteningThreshold.get(); + return value >= MIN_SHORTENING_THRESHOLD && value <= MAX_SHORTENING_THRESHOLD; + } + + @FXML + public void back() { + if (recoverType.get() == RecoveryActionType.RESTORE_ALL && vault.getState() == VaultState.Value.VAULT_CONFIG_MISSING) { + window.setScene(recoverScene.get()); + } else if (recoverType.get() == RecoveryActionType.RESTORE_ALL && vault.getState() == VaultState.Value.ALL_MISSING) { + window.setScene(recoverScene.get()); + } else if (recoverType.get() == RecoveryActionType.RESTORE_VAULT_CONFIG) { + window.setScene(onBoardingScene.get()); + } + } + + @FXML + public void next() { + if (recoverType.get() == RecoveryActionType.RESTORE_VAULT_CONFIG) { + window.setScene(createScene.get()); + } else { + window.setScene(resetPasswordScene.get()); + } + } +} diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java index 06095eebc..809d16b61 100644 --- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java @@ -5,8 +5,12 @@ import dagger.Module; import dagger.Provides; import dagger.multibindings.IntoMap; import org.cryptomator.common.Nullable; +import org.cryptomator.common.recovery.RecoveryActionType; import org.cryptomator.common.vaults.Vault; import org.cryptomator.cryptofs.VaultConfig; +import org.cryptomator.cryptolib.api.CryptorProvider; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; +import org.cryptomator.ui.addvaultwizard.CreateNewVaultExpertSettingsController; import org.cryptomator.ui.common.DefaultSceneFactory; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxControllerKey; @@ -19,6 +23,10 @@ import org.cryptomator.ui.common.StageFactory; import javax.inject.Named; import javax.inject.Provider; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.scene.Scene; @@ -99,12 +107,18 @@ abstract class RecoveryKeyModule { } @Provides - @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD_SUCCESS) + @FxmlScene(FxmlFile.RECOVERYKEY_ONBOARDING) @RecoveryKeyScoped - static Scene provideRecoveryKeyResetPasswordSuccessScene(@RecoveryKeyWindow FxmlLoaderFactory fxmlLoaders) { - return fxmlLoaders.createScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD_SUCCESS); + static Scene provideRecoveryKeyOnboardingScene(@RecoveryKeyWindow FxmlLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene(FxmlFile.RECOVERYKEY_ONBOARDING); } + @Provides + @FxmlScene(FxmlFile.RECOVERYKEY_EXPERT_SETTINGS) + @RecoveryKeyScoped + static Scene provideRecoveryKeyExpertSettingsScene(@RecoveryKeyWindow FxmlLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene(FxmlFile.RECOVERYKEY_EXPERT_SETTINGS); + } // ------------------ @@ -120,6 +134,25 @@ abstract class RecoveryKeyModule { return new RecoveryKeyDisplayController(window, vault.getDisplayName(), recoveryKey.get(), localization); } + @Provides + @Named("shorteningThreshold") + @RecoveryKeyScoped + static IntegerProperty provideShorteningThreshold() { + return new SimpleIntegerProperty(CreateNewVaultExpertSettingsController.MAX_SHORTENING_THRESHOLD); + } + + @Provides + @Named("cipherCombo") + @RecoveryKeyScoped + static ObjectProperty provideCipherCombo() { + return new SimpleObjectProperty<>(); + } + + @Binds + @IntoMap + @FxControllerKey(RecoveryKeyExpertSettingsController.class) + abstract FxController provideRecoveryKeyExpertSettingsController(RecoveryKeyExpertSettingsController controller); + @Binds @IntoMap @FxControllerKey(RecoveryKeyRecoverController.class) @@ -137,14 +170,14 @@ abstract class RecoveryKeyModule { @Binds @IntoMap - @FxControllerKey(RecoveryKeyResetPasswordSuccessController.class) - abstract FxController bindRecoveryKeyResetPasswordSuccessController(RecoveryKeyResetPasswordSuccessController controller); + @FxControllerKey(RecoveryKeyOnboardingController.class) + abstract FxController bindRecoveryKeyOnboardingController(RecoveryKeyOnboardingController controller); @Provides @IntoMap @FxControllerKey(RecoveryKeyValidateController.class) - static FxController bindRecoveryKeyValidateController(@RecoveryKeyWindow Vault vault, @RecoveryKeyWindow @Nullable VaultConfig.UnverifiedVaultConfig vaultConfig, @RecoveryKeyWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory) { - return new RecoveryKeyValidateController(vault, vaultConfig, recoveryKey, recoveryKeyFactory); + static FxController bindRecoveryKeyValidateController(@RecoveryKeyWindow Vault vault, @RecoveryKeyWindow @Nullable VaultConfig.UnverifiedVaultConfig vaultConfig, @RecoveryKeyWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory, @Named("recoverType") ObjectProperty recoverType, @Named("cipherCombo") ObjectProperty cipherCombo, @Nullable MasterkeyFileAccess masterkeyFileAccess) { + return new RecoveryKeyValidateController(vault, vaultConfig, recoveryKey, recoveryKeyFactory, masterkeyFileAccess, recoverType, cipherCombo); } @Provides diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyOnboardingController.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyOnboardingController.java new file mode 100644 index 000000000..dd15413d8 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyOnboardingController.java @@ -0,0 +1,176 @@ +package org.cryptomator.ui.recoverykey; + +import dagger.Lazy; +import org.cryptomator.common.recovery.RecoveryActionType; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.common.vaults.VaultState; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; + +import javax.inject.Inject; +import javax.inject.Named; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.ObjectProperty; +import javafx.fxml.FXML; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.Toggle; +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; +import java.util.ResourceBundle; + +import static org.cryptomator.common.recovery.RecoveryActionType.RESTORE_ALL; +import static org.cryptomator.common.recovery.RecoveryActionType.RESTORE_VAULT_CONFIG; + +@RecoveryKeyScoped +public class RecoveryKeyOnboardingController implements FxController { + + private final Stage window; + private final Vault vault; + private final Lazy recoverykeyRecoverScene; + private final Lazy recoverykeyExpertSettingsScene; + private final ObjectProperty recoverType; + private final ResourceBundle resourceBundle; + + public Label titleLabel; + public Label messageLabel; + public Label pleaseConfirm; + public Label secondTextDesc; + + @FXML + private CheckBox affirmationBox; + @FXML + private RadioButton recoveryKeyRadio; + @FXML + private RadioButton passwordRadio; + @FXML + private Button nextButton; + @FXML + private VBox chooseMethodeBox; + @FXML + private ToggleGroup methodToggleGroup = new ToggleGroup(); + @FXML + private HBox hBox; + + @Inject + public RecoveryKeyOnboardingController(@RecoveryKeyWindow Stage window, // + @RecoveryKeyWindow Vault vault, // + @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy recoverykeyRecoverScene, // + @FxmlScene(FxmlFile.RECOVERYKEY_EXPERT_SETTINGS) Lazy recoverykeyExpertSettingsScene, // + @Named("recoverType") ObjectProperty recoverType, // + ResourceBundle resourceBundle) { + this.window = window; + this.vault = vault; + this.recoverykeyRecoverScene = recoverykeyRecoverScene; + this.recoverykeyExpertSettingsScene = recoverykeyExpertSettingsScene; + this.recoverType = recoverType; + this.resourceBundle = resourceBundle; + } + + @FXML + public void initialize() { + recoveryKeyRadio.setToggleGroup(methodToggleGroup); + passwordRadio.setToggleGroup(methodToggleGroup); + + BooleanBinding showMethodSelection = Bindings.createBooleanBinding( + () -> recoverType.get() == RecoveryActionType.RESTORE_VAULT_CONFIG, recoverType); + chooseMethodeBox.visibleProperty().bind(showMethodSelection); + chooseMethodeBox.managedProperty().bind(showMethodSelection); + + nextButton.disableProperty().bind( + affirmationBox.selectedProperty().not() + .or(methodToggleGroup.selectedToggleProperty().isNull().and(showMethodSelection)) + ); + + switch (recoverType.get()) { + case RESTORE_MASTERKEY -> { + window.setTitle(resourceBundle.getString("recover.recoverMasterkey.title")); + messageLabel.setVisible(false); + messageLabel.setManaged(false); + pleaseConfirm.setText(resourceBundle.getString("recover.onBoarding.pleaseConfirm")); + } + case RESTORE_ALL -> { + window.setTitle(resourceBundle.getString("recover.recoverVaultConfig.title")); + messageLabel.setVisible(true); + messageLabel.setManaged(true); + pleaseConfirm.setText(resourceBundle.getString("recover.onBoarding.otherwisePleaseConfirm")); + } + case RESTORE_VAULT_CONFIG -> { + window.setTitle(resourceBundle.getString("recover.recoverVaultConfig.title")); + messageLabel.setVisible(false); + messageLabel.setManaged(false); + pleaseConfirm.setText(resourceBundle.getString("recover.onBoarding.pleaseConfirm")); + } + default -> window.setTitle(""); + } + + if (vault.getState() == VaultState.Value.ALL_MISSING) { + messageLabel.setText(resourceBundle.getString("recover.onBoarding.allMissing.intro")); + } else { + messageLabel.setText(resourceBundle.getString("recover.onBoarding.intro")); + } + + titleLabel.textProperty().bind(Bindings.createStringBinding(() -> + recoverType.get() == RecoveryActionType.RESTORE_MASTERKEY + ? resourceBundle.getString("recover.recoverMasterkey.title") + : resourceBundle.getString("recover.recoverVaultConfig.title"), recoverType)); + + BooleanBinding isRestoreMasterkey = Bindings.createBooleanBinding( + () -> recoverType.get() == RecoveryActionType.RESTORE_MASTERKEY, recoverType); + hBox.minHeightProperty().bind(Bindings.when(isRestoreMasterkey).then(206.0).otherwise(Region.USE_COMPUTED_SIZE)); + + secondTextDesc.textProperty().bind(Bindings.createStringBinding(() -> { + RecoveryActionType type = recoverType.get(); + Toggle sel = methodToggleGroup.getSelectedToggle(); + return switch (type) { + case RESTORE_VAULT_CONFIG -> resourceBundle.getString(sel == passwordRadio + ? "recover.onBoarding.intro.password" + : "recover.onBoarding.intro.recoveryKey"); + case RESTORE_MASTERKEY -> resourceBundle.getString("recover.onBoarding.intro.masterkey.recoveryKey"); + case RESTORE_ALL -> resourceBundle.getString("recover.onBoarding.intro.recoveryKey"); + default -> ""; + }; + }, recoverType, methodToggleGroup.selectedToggleProperty())); + + showMethodSelection.addListener((_, _, nowShown) -> { + if (nowShown && methodToggleGroup.getSelectedToggle() == null) { + methodToggleGroup.selectToggle(recoveryKeyRadio); + } + }); + } + + @FXML + public void close() { + window.close(); + } + + @FXML + public void next() { + switch (recoverType.get()) { + case RESTORE_VAULT_CONFIG, RESTORE_ALL -> { + Object selectedToggle = methodToggleGroup.getSelectedToggle(); + if (selectedToggle == recoveryKeyRadio) { + recoverType.set(RESTORE_ALL); + window.setScene(recoverykeyRecoverScene.get()); + } else if (selectedToggle == passwordRadio) { + recoverType.set(RESTORE_VAULT_CONFIG); + window.setScene(recoverykeyExpertSettingsScene.get()); + } else { + window.setScene(recoverykeyRecoverScene.get()); + } + } + case RESTORE_MASTERKEY -> window.setScene(recoverykeyRecoverScene.get()); + default -> window.setScene(recoverykeyRecoverScene.get()); // Fallback + } + window.centerOnScreen(); + } + +} diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java index 944c52043..8eb505d47 100644 --- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java @@ -1,54 +1,105 @@ package org.cryptomator.ui.recoverykey; import dagger.Lazy; -import org.cryptomator.common.Nullable; +import org.cryptomator.common.recovery.RecoveryActionType; import org.cryptomator.common.vaults.Vault; -import org.cryptomator.cryptofs.VaultConfig; +import org.cryptomator.common.vaults.VaultState; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import javax.inject.Inject; -import javafx.beans.Observable; -import javafx.beans.property.StringProperty; -import javafx.beans.value.ObservableValue; +import javax.inject.Named; +import javafx.beans.property.ObjectProperty; import javafx.fxml.FXML; import javafx.scene.Scene; +import javafx.scene.control.Button; import javafx.stage.Stage; import java.util.ResourceBundle; @RecoveryKeyScoped public class RecoveryKeyRecoverController implements FxController { - private static final Logger LOG = LoggerFactory.getLogger(RecoveryKeyCreationController.class); - private final Stage window; - private final Lazy resetPasswordScene; + private final Vault vault; + private final Lazy nextScene; + private final Lazy onBoardingScene; + private final ResourceBundle resourceBundle; + public ObjectProperty recoverType; + + @FXML + private Button cancelButton; @FXML RecoveryKeyValidateController recoveryKeyValidateController; @Inject - public RecoveryKeyRecoverController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, @RecoveryKeyWindow StringProperty recoveryKey, @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) Lazy resetPasswordScene, ResourceBundle resourceBundle) { + public RecoveryKeyRecoverController(@RecoveryKeyWindow Stage window, // + @RecoveryKeyWindow Vault vault, // + ResourceBundle resourceBundle, // + @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) Lazy resetPasswordScene, // + @FxmlScene(FxmlFile.RECOVERYKEY_EXPERT_SETTINGS) Lazy expertSettingsScene, // + @FxmlScene(FxmlFile.RECOVERYKEY_ONBOARDING) Lazy onBoardingScene, // + @Named("recoverType") ObjectProperty recoverType) { this.window = window; - window.setTitle(resourceBundle.getString("recoveryKey.recover.title")); - this.resetPasswordScene = resetPasswordScene; + this.vault = vault; + this.resourceBundle = resourceBundle; + this.onBoardingScene = onBoardingScene; + this.recoverType = recoverType; + this.nextScene = switch (recoverType.get()) { + case RESTORE_ALL, RESTORE_VAULT_CONFIG -> { + window.setTitle(resourceBundle.getString("recover.recoverVaultConfig.title")); + yield expertSettingsScene; + } + case RESTORE_MASTERKEY -> { + window.setTitle(resourceBundle.getString("recover.recoverMasterkey.title")); + yield resetPasswordScene; + } + case RESET_PASSWORD -> { + window.setTitle(resourceBundle.getString("recoveryKey.recover.title")); + yield resetPasswordScene; + } + case SHOW_KEY -> { + window.setTitle(resourceBundle.getString("recoveryKey.display.title")); + yield resetPasswordScene; + } + default -> throw new IllegalArgumentException("Unexpected recovery action type: " + recoverType.get()); + }; } @FXML public void initialize() { + if (recoverType.get() == RecoveryActionType.RESET_PASSWORD) { + cancelButton.setText(resourceBundle.getString("generic.button.cancel")); + } else { + cancelButton.setText(resourceBundle.getString("generic.button.back")); + } } @FXML - public void close() { - window.close(); + public void closeOrReturn() { + switch (recoverType.get()) { + case RESET_PASSWORD -> window.close(); + case RESTORE_MASTERKEY -> { + window.setScene(onBoardingScene.get()); + window.centerOnScreen(); + } + default -> { + if(vault.getState().equals(VaultState.Value.ALL_MISSING)){ + recoverType.set(RecoveryActionType.RESTORE_ALL); + } + else { + recoverType.set(RecoveryActionType.RESTORE_VAULT_CONFIG); + } + window.setScene(onBoardingScene.get()); + window.centerOnScreen(); + } + } } @FXML public void recover() { - window.setScene(resetPasswordScene.get()); + window.setScene(nextScene.get()); } /* Getter/Setter */ diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java index 18a952ea5..0c06ba9b2 100644 --- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java @@ -1,25 +1,44 @@ package org.cryptomator.ui.recoverykey; import dagger.Lazy; +import org.cryptomator.common.recovery.CryptoFsInitializer; +import org.cryptomator.common.recovery.MasterkeyService; +import org.cryptomator.common.recovery.RecoveryActionType; +import org.cryptomator.common.recovery.RecoveryDirectory; import org.cryptomator.common.vaults.Vault; +import org.cryptomator.common.vaults.VaultListManager; +import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.CryptorProvider; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; +import org.cryptomator.ui.changepassword.NewPasswordController; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; -import org.cryptomator.ui.changepassword.NewPasswordController; +import org.cryptomator.ui.dialogs.Dialogs; import org.cryptomator.ui.fxapp.FxApplicationWindows; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; +import javax.inject.Named; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.StringProperty; import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.scene.Scene; +import javafx.scene.control.Button; import javafx.stage.Stage; import java.io.IOException; +import java.nio.file.Path; +import java.util.ResourceBundle; import java.util.concurrent.ExecutorService; +import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; +import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME; + @RecoveryKeyScoped public class RecoveryKeyResetPasswordController implements FxController { @@ -30,48 +49,140 @@ public class RecoveryKeyResetPasswordController implements FxController { private final RecoveryKeyFactory recoveryKeyFactory; private final ExecutorService executor; private final StringProperty recoveryKey; - private final Lazy recoverResetPasswordSuccessScene; + private final Lazy recoverExpertSettingsScene; + private final Lazy recoverykeyRecoverScene; private final FxApplicationWindows appWindows; + private final MasterkeyFileAccess masterkeyFileAccess; + private final VaultListManager vaultListManager; + private final IntegerProperty shorteningThreshold; + private final ObjectProperty recoverType; + private final ObjectProperty cipherCombo; + private final ResourceBundle resourceBundle; + private final Dialogs dialogs; public NewPasswordController newPasswordController; + public Button nextButton; @Inject - public RecoveryKeyResetPasswordController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD_SUCCESS) Lazy recoverResetPasswordSuccessScene, FxApplicationWindows appWindows) { + public RecoveryKeyResetPasswordController(@RecoveryKeyWindow Stage window, // + @RecoveryKeyWindow Vault vault, // + RecoveryKeyFactory recoveryKeyFactory, // + ExecutorService executor, // + @RecoveryKeyWindow StringProperty recoveryKey, // + @FxmlScene(FxmlFile.RECOVERYKEY_EXPERT_SETTINGS) Lazy recoverExpertSettingsScene, // + @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy recoverykeyRecoverScene, // + FxApplicationWindows appWindows, // + MasterkeyFileAccess masterkeyFileAccess, // + VaultListManager vaultListManager, // + @Named("shorteningThreshold") IntegerProperty shorteningThreshold, // + @Named("recoverType") ObjectProperty recoverType, // + @Named("cipherCombo") ObjectProperty cipherCombo, // + ResourceBundle resourceBundle, // + Dialogs dialogs) { this.window = window; this.vault = vault; this.recoveryKeyFactory = recoveryKeyFactory; this.executor = executor; this.recoveryKey = recoveryKey; - this.recoverResetPasswordSuccessScene = recoverResetPasswordSuccessScene; + this.recoverExpertSettingsScene = recoverExpertSettingsScene; + this.recoverykeyRecoverScene = recoverykeyRecoverScene; this.appWindows = appWindows; + this.masterkeyFileAccess = masterkeyFileAccess; + this.vaultListManager = vaultListManager; + this.shorteningThreshold = shorteningThreshold; + this.cipherCombo = cipherCombo; + this.recoverType = recoverType; + this.resourceBundle = resourceBundle; + this.dialogs = dialogs; + } + + @FXML + public void initialize() { + switch (recoverType.get()) { + case RESTORE_MASTERKEY, RESTORE_ALL -> nextButton.setText(resourceBundle.getString("recoveryKey.recover.recoverBtn")); + case RESET_PASSWORD -> nextButton.setText(resourceBundle.getString("recoveryKey.recover.resetBtn")); + default -> nextButton.setText(resourceBundle.getString("recoveryKey.recover.recoverBtn")); // Fallback + } } @FXML public void close() { - window.close(); + switch (recoverType.get()) { + case RESTORE_ALL -> window.setScene(recoverExpertSettingsScene.get()); + case RESTORE_MASTERKEY, RESET_PASSWORD -> window.setScene(recoverykeyRecoverScene.get()); + default -> window.close(); + } + } + + @FXML + public void next() { + switch (recoverType.get()) { + case RESTORE_ALL -> restorePassword(); + case RESTORE_MASTERKEY, RESET_PASSWORD -> resetPassword(); + default -> resetPassword(); // Fallback + } + } + + @FXML + public void restorePassword() { + try (RecoveryDirectory recoveryDirectory = RecoveryDirectory.create(vault.getPath())) { + Path recoveryPath = recoveryDirectory.getRecoveryPath(); + MasterkeyService.recoverFromRecoveryKey(recoveryKey.get(), recoveryKeyFactory, recoveryPath, newPasswordController.passwordField.getCharacters()); + + try (Masterkey masterkey = MasterkeyService.load(masterkeyFileAccess, recoveryPath.resolve(MASTERKEY_FILENAME), newPasswordController.passwordField.getCharacters())) { + CryptoFsInitializer.init(recoveryPath, masterkey, shorteningThreshold.get(), cipherCombo.get()); + } + + recoveryDirectory.moveRecoveredFile(MASTERKEY_FILENAME); + recoveryDirectory.moveRecoveredFile(VAULTCONFIG_FILENAME); + + if (!vaultListManager.isAlreadyAdded(vault.getPath())) { + vaultListManager.add(vault.getPath()); + } + window.close(); + dialogs.prepareRecoverPasswordSuccess((Stage)window.getOwner()) // + .setTitleKey("recover.recoverVaultConfig.title") // + .setMessageKey("recoveryKey.recover.resetVaultConfigSuccess.message") // + .build().showAndWait(); + + } catch (IOException | CryptoException e) { + LOG.error("Recovery process failed", e); + appWindows.showErrorWindow(e, window, null); + } } @FXML public void resetPassword() { Task task = new ResetPasswordTask(); - task.setOnScheduled(event -> { + + task.setOnScheduled(_ -> { LOG.debug("Using recovery key to reset password for {}.", vault.getDisplayablePath()); }); - task.setOnSucceeded(event -> { - LOG.info("Used recovery key to reset password for {}.", vault.getDisplayablePath()); - window.setScene(recoverResetPasswordSuccessScene.get()); + + task.setOnSucceeded(_ -> { + LOG.debug("Used recovery key to reset password for {}.", vault.getDisplayablePath()); + window.close(); + switch (recoverType.get()){ + case RESET_PASSWORD -> dialogs.prepareRecoverPasswordSuccess((Stage)window.getOwner()).build().showAndWait(); + case RESTORE_MASTERKEY -> dialogs.prepareRecoverPasswordSuccess((Stage)window.getOwner()).setTitleKey("recover.recoverMasterkey.title").setMessageKey("recoveryKey.recover.resetMasterkeyFileSuccess.message").build().showAndWait(); + default -> dialogs.prepareRecoverPasswordSuccess(window).build().showAndWait(); // Fallback + } }); - task.setOnFailed(event -> { + + task.setOnFailed(_ -> { LOG.error("Resetting password failed.", task.getException()); appWindows.showErrorWindow(task.getException(), window, null); }); + executor.submit(task); } private class ResetPasswordTask extends Task { - private ResetPasswordTask() { - setOnFailed(event -> LOG.error("Failed to reset password", getException())); + private static final Logger LOG = LoggerFactory.getLogger(ResetPasswordTask.class); + + public ResetPasswordTask() { + setOnFailed(_ -> LOG.error("Failed to reset password", getException())); } @Override @@ -79,7 +190,6 @@ public class RecoveryKeyResetPasswordController implements FxController { recoveryKeyFactory.newMasterkeyFileWithPassphrase(vault.getPath(), recoveryKey.get(), newPasswordController.passwordField.getCharacters()); return null; } - } /* Getter/Setter */ diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordSuccessController.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordSuccessController.java deleted file mode 100644 index b8b106d8b..000000000 --- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordSuccessController.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.cryptomator.ui.recoverykey; - -import org.cryptomator.ui.common.FxController; - -import javax.inject.Inject; -import javafx.fxml.FXML; -import javafx.stage.Stage; - -@RecoveryKeyScoped -public class RecoveryKeyResetPasswordSuccessController implements FxController { - - private final Stage window; - - @Inject - public RecoveryKeyResetPasswordSuccessController(@RecoveryKeyWindow Stage window) { - this.window = window; - } - - @FXML - public void close() { - window.close(); - } - -} diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyValidateController.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyValidateController.java index 4a8224ffe..35f4c15ed 100644 --- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyValidateController.java +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyValidateController.java @@ -1,18 +1,23 @@ package org.cryptomator.ui.recoverykey; - import com.google.common.base.CharMatcher; import com.google.common.base.Strings; import org.cryptomator.common.Nullable; import org.cryptomator.common.ObservableUtil; +import org.cryptomator.common.recovery.MasterkeyService; +import org.cryptomator.common.recovery.RecoveryActionType; import org.cryptomator.common.vaults.Vault; import org.cryptomator.cryptofs.VaultConfig; import org.cryptomator.cryptofs.VaultConfigLoadException; import org.cryptomator.cryptofs.VaultKeyInvalidException; +import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.CryptorProvider; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; import org.cryptomator.ui.common.FxController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.inject.Named; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.StringProperty; @@ -22,10 +27,12 @@ import javafx.scene.control.TextArea; import javafx.scene.control.TextFormatter; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; +import java.io.IOException; +import java.util.NoSuchElementException; public class RecoveryKeyValidateController implements FxController { - private static final Logger LOG = LoggerFactory.getLogger(RecoveryKeyCreationController.class); + private static final Logger LOG = LoggerFactory.getLogger(RecoveryKeyValidateController.class); private static final CharMatcher ALLOWED_CHARS = CharMatcher.inRange('a', 'z').or(CharMatcher.is(' ')); private final Vault vault; @@ -36,13 +43,23 @@ public class RecoveryKeyValidateController implements FxController { private final ObservableValue recoveryKeyInvalid; private final RecoveryKeyFactory recoveryKeyFactory; private final ObjectProperty recoveryKeyState; + private final ObjectProperty cipherCombo; private final AutoCompleter autoCompleter; + private final ObjectProperty recoverType; + private final MasterkeyFileAccess masterkeyFileAccess; private volatile boolean isWrongKey; public TextArea textarea; - public RecoveryKeyValidateController(Vault vault, @Nullable VaultConfig.UnverifiedVaultConfig vaultConfig, StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory) { + public RecoveryKeyValidateController(Vault vault, // + @Nullable VaultConfig.UnverifiedVaultConfig vaultConfig, // + StringProperty recoveryKey, // + RecoveryKeyFactory recoveryKeyFactory, // + MasterkeyFileAccess masterkeyFileAccess, // + @Named("recoverType") ObjectProperty recoverType, // + @Named("cipherCombo") ObjectProperty cipherCombo + ) { this.vault = vault; this.unverifiedVaultConfig = vaultConfig; this.recoveryKey = recoveryKey; @@ -52,6 +69,9 @@ public class RecoveryKeyValidateController implements FxController { this.recoveryKeyCorrect = ObservableUtil.mapWithDefault(recoveryKeyState, RecoveryKeyState.CORRECT::equals, false); this.recoveryKeyWrong = ObservableUtil.mapWithDefault(recoveryKeyState, RecoveryKeyState.WRONG::equals, false); this.recoveryKeyInvalid = ObservableUtil.mapWithDefault(recoveryKeyState, RecoveryKeyState.INVALID::equals, false); + this.recoverType = recoverType; + this.cipherCombo = cipherCombo; + this.masterkeyFileAccess = masterkeyFileAccess; } @FXML @@ -117,14 +137,37 @@ public class RecoveryKeyValidateController implements FxController { } private void validateRecoveryKey() { - isWrongKey = false; - var valid = recoveryKeyFactory.validateRecoveryKey(recoveryKey.get(), unverifiedVaultConfig != null ? this::checkKeyAgainstVaultConfig : null); - if (valid) { - recoveryKeyState.set(RecoveryKeyState.CORRECT); - } else if (isWrongKey) { //set via side effect in checkKeyAgainstVaultConfig() - recoveryKeyState.set(RecoveryKeyState.WRONG); - } else { - recoveryKeyState.set(RecoveryKeyState.INVALID); + switch (recoverType.get()) { + case RESTORE_ALL, RESTORE_VAULT_CONFIG -> { + try { + var scheme = MasterkeyService.validateRecoveryKeyAndDetectCombo(recoveryKeyFactory, vault, recoveryKey.get(), masterkeyFileAccess); + cipherCombo.set(scheme); + recoveryKeyState.set(RecoveryKeyState.CORRECT); + } catch (CryptoException e) { + LOG.info("Recovery key is valid but crypto scheme couldn't be determined", e); + recoveryKeyState.set(RecoveryKeyState.WRONG); + } catch (IllegalArgumentException e) { + LOG.info("Recovery key is syntactically invalid", e); + recoveryKeyState.set(RecoveryKeyState.INVALID); + } catch (IOException e) { + LOG.warn("IO error while validating recovery key", e); + recoveryKeyState.set(RecoveryKeyState.INVALID); + } catch (NoSuchElementException e) { + LOG.warn("Could not determine scheme from masterkey during recovery key validation, because no valid *.c9r file is present in vault", e); + recoveryKeyState.set(RecoveryKeyState.INVALID); + } + } + case RESTORE_MASTERKEY, RESET_PASSWORD, SHOW_KEY, CONVERT_VAULT -> { + isWrongKey = false; + boolean valid = recoveryKeyFactory.validateRecoveryKey(recoveryKey.get(), unverifiedVaultConfig != null ? this::checkKeyAgainstVaultConfig : null); + if (valid) { + recoveryKeyState.set(RecoveryKeyState.CORRECT); + } else if (isWrongKey) { //set via side effect in checkKeyAgainstVaultConfig() + recoveryKeyState.set(RecoveryKeyState.WRONG); + } else { + recoveryKeyState.set(RecoveryKeyState.INVALID); + } + } } } diff --git a/src/main/java/org/cryptomator/ui/sharevault/ShareVaultController.java b/src/main/java/org/cryptomator/ui/sharevault/ShareVaultController.java index 93fb298ed..b0c490a3f 100644 --- a/src/main/java/org/cryptomator/ui/sharevault/ShareVaultController.java +++ b/src/main/java/org/cryptomator/ui/sharevault/ShareVaultController.java @@ -58,9 +58,15 @@ public class ShareVaultController implements FxController { private static URI getHubUri(Vault vault) { try { - var keyID = new URI(vault.getVaultConfigCache().get().getKeyId().toString()); - assert keyID.getScheme().startsWith(SCHEME_PREFIX); - return new URI(keyID.getScheme().substring(SCHEME_PREFIX.length()) + "://" + keyID.getHost() + "/app/vaults"); + var keyId = new URI(vault.getVaultConfigCache().get().getKeyId().toString()); + assert keyId.getScheme().startsWith(SCHEME_PREFIX); + var path = keyId.getPath(); + var apiIdx = path.indexOf("/api/"); + if (apiIdx < 0) { + throw new IllegalArgumentException("Path does not contain /api/: " + path); + } + var appPath = path.substring(0, apiIdx) + "/app/vaults"; + return new URI(keyId.getScheme().substring(SCHEME_PREFIX.length()), keyId.getAuthority(), appPath, null, null); } catch (IOException e) { throw new UncheckedIOException(e); } catch (URISyntaxException e) { diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java b/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java index 95f13d383..1c8d758fc 100644 --- a/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java +++ b/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java @@ -15,6 +15,7 @@ import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.StageFactory; import org.cryptomator.ui.keyloading.KeyLoadingComponent; import org.cryptomator.ui.keyloading.KeyLoadingStrategy; +import org.cryptomator.ui.recoverykey.RecoveryKeyComponent; import org.jetbrains.annotations.Nullable; import javax.inject.Named; @@ -27,7 +28,7 @@ import javafx.stage.Stage; import java.util.Map; import java.util.ResourceBundle; -@Module(subcomponents = {KeyLoadingComponent.class}) +@Module(subcomponents = {KeyLoadingComponent.class, RecoveryKeyComponent.class}) abstract class UnlockModule { @Provides diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java index dd003d93d..67ae2f42d 100644 --- a/src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java +++ b/src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java @@ -1,16 +1,16 @@ package org.cryptomator.ui.vaultoptions; import org.cryptomator.common.keychain.KeychainManager; +import org.cryptomator.common.recovery.RecoveryActionType; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.changepassword.ChangePasswordComponent; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.forgetpassword.ForgetPasswordComponent; import org.cryptomator.ui.recoverykey.RecoveryKeyComponent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import javax.inject.Inject; import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; import javafx.stage.Stage; @@ -18,8 +18,6 @@ import javafx.stage.Stage; @VaultOptionsScoped public class MasterkeyOptionsController implements FxController { - private static final Logger LOG = LoggerFactory.getLogger(MasterkeyOptionsController.class); - private final Vault vault; private final Stage window; private final ChangePasswordComponent.Builder changePasswordWindow; @@ -51,12 +49,12 @@ public class MasterkeyOptionsController implements FxController { @FXML public void showRecoveryKey() { - recoveryKeyWindow.create(vault, window).showRecoveryKeyCreationWindow(); + recoveryKeyWindow.create(vault, window, new SimpleObjectProperty<>(RecoveryActionType.SHOW_KEY)).showRecoveryKeyCreationWindow(); } @FXML public void showRecoverVaultDialog() { - recoveryKeyWindow.create(vault, window).showRecoveryKeyRecoverWindow(); + recoveryKeyWindow.create(vault, window, new SimpleObjectProperty<>(RecoveryActionType.RESET_PASSWORD)).showRecoveryKeyRecoverWindow(); } @FXML diff --git a/src/main/resources/fxml/convertvault_hubtopassword_convert.fxml b/src/main/resources/fxml/convertvault_hubtopassword_convert.fxml index 7ea190fd4..7797b41b2 100644 --- a/src/main/resources/fxml/convertvault_hubtopassword_convert.fxml +++ b/src/main/resources/fxml/convertvault_hubtopassword_convert.fxml @@ -6,7 +6,12 @@ - + + + + + - + + + + + + + + + + @@ -26,7 +40,7 @@