diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml new file mode 100644 index 000000000..aab954476 --- /dev/null +++ b/.github/workflows/appimage.yml @@ -0,0 +1,151 @@ +name: Build AppImage + +on: + release: + types: [published] + workflow_dispatch: + +env: + JAVA_VERSION: 17 + +jobs: + build: + name: Build AppImage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Setup Java + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + cache: 'maven' + - id: versions + name: Apply version information + run: | + if [[ $GITHUB_REF =~ refs/tags/[0-9]+\.[0-9]+\.[0-9]+.* ]]; then + SEM_VER_STR=${GITHUB_REF##*/} + mvn versions:set -DnewVersion=${SEM_VER_STR} + else + SEM_VER_STR=`mvn help:evaluate -Dexpression=project.version -q -DforceStdout` + fi + SEM_VER_NUM=`echo ${SEM_VER_STR} | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/'` + REVCOUNT=`git rev-list --count HEAD` + echo "::set-output name=semVerStr::${SEM_VER_STR}" + echo "::set-output name=semVerNum::${SEM_VER_NUM}" + echo "::set-output name=revNum::${REVCOUNT}" + - name: Validate Version + uses: skymatic/semver-validation-action@v1 + with: + version: ${{ steps.versions.outputs.semVerStr }} + - name: Run maven + run: mvn -B clean package -Pdependency-check,linux -DskipTests + - name: Patch target dir + run: | + cp LICENSE.txt target + cp dist/linux/launcher.sh target + cp target/cryptomator-*.jar target/mods + - name: Run jlink + run: > + ${JAVA_HOME}/bin/jlink + --verbose + --output runtime + --module-path "${JAVA_HOME}/jmods" + --add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr + --no-header-files + --no-man-pages + --strip-debug + --compress=1 + - name: Run jpackage + run: > + ${JAVA_HOME}/bin/jpackage + --verbose + --type app-image + --runtime-image runtime + --input target/libs + --module-path target/mods + --module org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator + --dest appdir + --name Cryptomator + --vendor "Skymatic GmbH" + --copyright "(C) 2016 - 2022 Skymatic GmbH" + --app-version "${{ steps.versions.outputs.semVerNum }}.${{ steps.versions.outputs.revNum }}" + --java-options "-Xss5m" + --java-options "-Xmx256m" + --java-options "-Dcryptomator.appVersion=\"${{ steps.versions.outputs.semVerStr }}\"" + --java-options "-Dfile.encoding=\"utf-8\"" + --java-options "-Dcryptomator.logDir=\"~/.local/share/Cryptomator/logs\"" + --java-options "-Dcryptomator.pluginDir=\"~/.local/share/Cryptomator/plugins\"" + --java-options "-Dcryptomator.settingsPath=\"~/.config/Cryptomator/settings.json:~/.Cryptomator/settings.json\"" + --java-options "-Dcryptomator.ipcSocketPath=\"~/.config/Cryptomator/ipc.socket\"" + --java-options "-Dcryptomator.mountPointsDir=\"~/.local/share/Cryptomator/mnt\"" + --java-options "-Dcryptomator.showTrayIcon=false" + --java-options "-Dcryptomator.buildNumber=\"appimage-${{ steps.versions.outputs.revNum }}\"" + --resource-dir dist/linux/resources + - name: Patch Cryptomator.AppDir + run: | + mv appdir/Cryptomator Cryptomator.AppDir + cp -r dist/linux/appimage/resources/AppDir/* Cryptomator.AppDir/ + envsubst '${REVISION_NO} ${SEMVER_STR}' < dist/linux/appimage/resources/AppDir/bin/cryptomator.sh > Cryptomator.AppDir/bin/cryptomator.sh + cp dist/linux/common/org.cryptomator.Cryptomator256.png Cryptomator.AppDir/usr/share/icons/hicolor/256x256/apps/org.cryptomator.Cryptomator.png + cp dist/linux/common/org.cryptomator.Cryptomator512.png Cryptomator.AppDir/usr/share/icons/hicolor/512x512/apps/org.cryptomator.Cryptomator.png + cp dist/linux/common/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg + cp dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml Cryptomator.AppDir/usr/share/metainfo/org.cryptomator.Cryptomator.metainfo.xml + cp dist/linux/common/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/usr/share/applications/org.cryptomator.Cryptomator.desktop + cp dist/linux/common/application-vnd.cryptomator.vault.xml Cryptomator.AppDir/usr/share/mime/packages/application-vnd.cryptomator.vault.xml + ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/org.cryptomator.Cryptomator.svg + ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/Cryptomator.svg + ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/.DirIcon + ln -s usr/share/applications/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/Cryptomator.desktop + ln -s bin/cryptomator.sh Cryptomator.AppDir/AppRun + env: + REVISION_NO: ${{ steps.versions.outputs.revNum }} + SEMVER_STR: ${{ steps.versions.outputs.semVerStr }} + - name: Extract libjffi.so # workaround for https://github.com/cryptomator/cryptomator-linux/issues/27 + run: | + JFFI_NATIVE_JAR=`ls lib/app/ | grep -e 'jffi-[1-9]\.[0-9]\{1,2\}.[0-9]\{1,2\}-native.jar'` + ${JAVA_HOME}/bin/jar -xf lib/app/${JFFI_NATIVE_JAR} /jni/x86_64-Linux/ + mv jni/x86_64-Linux/* lib/app/libjffi.so + working-directory: Cryptomator.AppDir + - name: Download AppImageKit + run: | + curl -L https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage -o appimagetool.AppImage + chmod +x appimagetool.AppImage + ./appimagetool.AppImage --appimage-extract + - name: Prepare GPG-Agent for signing with key 615D449FE6E6A235 + run: | + echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import + echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --dry-run --sign README.md + env: + GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} + - name: Build AppImage + run: > + ./squashfs-root/AppRun Cryptomator.AppDir cryptomator-${{ steps.versions.outputs.semVerStr }}-x86_64.AppImage + -u 'gh-releases-zsync|cryptomator|cryptomator|latest|cryptomator-*-x86_64.AppImage.zsync' + --sign --sign-key=615D449FE6E6A235 --sign-args="--batch --pinentry-mode loopback" + - name: Create detached GPG signatures + run: | + gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator-*.AppImage + gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator-*.AppImage.zsync + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: appimage + path: | + cryptomator-*.AppImage + cryptomator-*.AppImage.zsync + cryptomator-*.asc + if-no-files-found: error + - name: Publish AppImage on GitHub Releases + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v1 + with: + fail_on_unmatched_files: true + token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} + files: | + cryptomator-*.AppImage + cryptomator-*.zsync + cryptomator-*.asc \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 750b64826..97d46ae9e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,6 +32,7 @@ jobs: restore-keys: ${{ runner.os }}-sonar - name: Build and Test run: > + xvfb-run mvn -B verify jacoco:report org.sonarsource.scanner.maven:sonar-maven-plugin:sonar @@ -48,4 +49,16 @@ jobs: run: bash <(curl -Ls https://coverage.codacy.com/get.sh) env: CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} - continue-on-error: true \ No newline at end of file + continue-on-error: true + - name: Draft a release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v1 + with: + draft: true + discussion_category_name: releases + token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} + generate_release_notes: true + body: |- + :construction: Work in Progress + + --- diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml new file mode 100644 index 000000000..f7893bbaa --- /dev/null +++ b/.github/workflows/debian.yml @@ -0,0 +1,118 @@ +name: Build Debian Package + +on: + release: + types: [published] + workflow_dispatch: + inputs: + dput: + description: 'Upload to PPA' + required: true + default: false + type: boolean + +env: + JAVA_VERSION: 17 + +jobs: + build: + name: Build Debian Package + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Install build tools + run: | + sudo apt-get update + sudo apt-get install debhelper devscripts dput + - name: Setup Java + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + cache: 'maven' + - id: versions + name: Apply version information + run: | + if [[ $GITHUB_REF =~ refs/tags/[0-9]+\.[0-9]+\.[0-9]+.* ]]; then + SEM_VER_STR=${GITHUB_REF##*/} + mvn versions:set -DnewVersion=${SEM_VER_STR} + else + SEM_VER_STR=`mvn help:evaluate -Dexpression=project.version -q -DforceStdout` + fi + SEM_VER_NUM=`echo ${SEM_VER_STR} | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/'` + REVCOUNT=`git rev-list --count HEAD` + echo "::set-output name=semVerStr::${SEM_VER_STR}" + echo "::set-output name=semVerNum::${SEM_VER_NUM}" + echo "::set-output name=revNum::${REVCOUNT}" + echo "::set-output name=ppaVerStr::${SEM_VER_STR/-/\~}-${REVCOUNT}" + - name: Validate Version + uses: skymatic/semver-validation-action@v1 + with: + version: ${{ steps.versions.outputs.semVerStr }} + - name: Run maven + run: mvn -B clean package -Pdependency-check,linux -DskipTests + - name: Create orig.tar.gz with common/ libs/ mods/ + run: | + mkdir pkgdir + cp -r target/libs pkgdir + cp -r target/mods pkgdir + cp -r dist/linux/common/ pkgdir + cp target/cryptomator-*.jar pkgdir/mods + tar -cJf cryptomator_${{ steps.versions.outputs.ppaVerStr }}.orig.tar.xz -C pkgdir . + - name: Patch and rename pkgdir + run: | + cp -r dist/linux/debian/ pkgdir + export RFC2822_TIMESTAMP=`date --rfc-2822` + envsubst '${SEMVER_STR} ${VERSION_NUM} ${REVISION_NUM}' < dist/linux/debian/rules > pkgdir/debian/rules + envsubst '${PPA_VERSION} ${RFC2822_TIMESTAMP}' < dist/linux/debian/changelog > pkgdir/debian/changelog + find . -name "*.jar" >> pkgdir/debian/source/include-binaries + mv pkgdir cryptomator_${{ steps.versions.outputs.ppaVerStr }} + env: + SEMVER_STR: ${{ steps.versions.outputs.semVerStr }} + VERSION_NUM: ${{ steps.versions.outputs.semVerNum }} + REVISION_NUM: ${{ steps.versions.outputs.revNum }} + PPA_VERSION: ${{ steps.versions.outputs.ppaVerStr }}-0ppa1 + - name: Prepare GPG-Agent for signing with key 615D449FE6E6A235 + run: | + echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import + echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --dry-run --sign README.md + env: + GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} + - name: debuild + run: | + debuild -S -sa -d + debuild -b -sa -d + env: + DEBSIGN_PROGRAM: gpg --batch --pinentry-mode loopback + DEBSIGN_KEYID: 615D449FE6E6A235 + working-directory: cryptomator_${{ steps.versions.outputs.ppaVerStr }} + - name: Create detached GPG signatures + run: | + gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator_*_amd64.deb + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: linux-deb-package + path: | + cryptomator_*.dsc + cryptomator_*.orig.tar.xz + cryptomator_*.debian.tar.xz + cryptomator_*_source.buildinfo + cryptomator_*_source.changes + cryptomator_*_amd64.deb + cryptomator_*.asc + - name: Publish on PPA + if: startsWith(github.ref, 'refs/tags/') || github.event.inputs.dput == 'true' + run: dput ppa:sebastian-stenzel/cryptomator-beta cryptomator_*_source.changes + - name: Publish Debian package on GitHub Releases + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v1 + with: + fail_on_unmatched_files: true + token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} + files: | + cryptomator_*_amd64.deb + cryptomator_*.asc \ No newline at end of file diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml new file mode 100644 index 000000000..66af92d6d --- /dev/null +++ b/.github/workflows/mac-dmg.yml @@ -0,0 +1,232 @@ +name: Build macOS .dmg + +on: + release: + types: [published] + workflow_dispatch: + +env: + JAVA_VERSION: 17 + +jobs: + build: + name: Build Cryptomator.app + runs-on: macos-11 + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Setup Java + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + cache: 'maven' + - id: versions + name: Apply version information + run: | + if [[ $GITHUB_REF =~ refs/tags/[0-9]+\.[0-9]+\.[0-9]+.* ]]; then + SEM_VER_STR=${GITHUB_REF##*/} + mvn versions:set -DnewVersion=${SEM_VER_STR} + else + SEM_VER_STR=`mvn help:evaluate -Dexpression=project.version -q -DforceStdout` + fi + SEM_VER_NUM=`echo ${SEM_VER_STR} | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/'` + REVCOUNT=`git rev-list --count HEAD` + echo "::set-output name=semVerStr::${SEM_VER_STR}" + echo "::set-output name=semVerNum::${SEM_VER_NUM}" + echo "::set-output name=revNum::${REVCOUNT}" + - name: Validate Version + uses: skymatic/semver-validation-action@v1 + with: + version: ${{ steps.versions.outputs.semVerStr }} + - name: Run maven + run: mvn -B clean package -Pdependency-check,mac -DskipTests + - name: Patch target dir + run: | + cp LICENSE.txt target + cp dist/mac/launcher.sh target + cp target/cryptomator-*.jar target/mods + - name: Run jlink + run: > + ${JAVA_HOME}/bin/jlink + --verbose + --output runtime + --module-path "${JAVA_HOME}/jmods" + --add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr + --strip-native-commands + --no-header-files + --no-man-pages + --strip-debug + --compress=1 + - name: Run jpackage + run: > + ${JAVA_HOME}/bin/jpackage + --verbose + --type app-image + --runtime-image runtime + --input target/libs + --module-path target/mods + --module org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator + --dest appdir + --name Cryptomator + --vendor "Skymatic GmbH" + --copyright "(C) 2016 - 2022 Skymatic GmbH" + --app-version "${{ steps.versions.outputs.semVerNum }}" + --java-options "-Xss5m" + --java-options "-Xmx256m" + --java-options "-Dcryptomator.appVersion=\"${{ steps.versions.outputs.semVerStr }}\"" + --java-options "-Dfile.encoding=\"utf-8\"" + --java-options "-Dapple.awt.enableTemplateImages=true" + --java-options "-Dcryptomator.logDir=\"~/Library/Logs/Cryptomator\"" + --java-options "-Dcryptomator.pluginDir=\"~/Library/Application Support/Cryptomator/Plugins\"" + --java-options "-Dcryptomator.settingsPath=\"~/Library/Application Support/Cryptomator/settings.json\"" + --java-options "-Dcryptomator.ipcSocketPath=\"~/Library/Application Support/Cryptomator/ipc.socket\"" + --java-options "-Dcryptomator.showTrayIcon=true" + --java-options "-Dcryptomator.buildNumber=\"dmg-${{ steps.versions.outputs.revNum }}\"" + --mac-package-identifier org.cryptomator + --resource-dir dist/mac/resources + - name: Patch Cryptomator.app + run: | + mv appdir/Cryptomator.app Cryptomator.app + mv dist/mac/resources/Cryptomator-Vault.icns Cryptomator.app/Contents/Resources/ + sed -i '' "s|###BUNDLE_SHORT_VERSION_STRING###|${VERSION_NO}|g" Cryptomator.app/Contents/Info.plist + sed -i '' "s|###BUNDLE_VERSION###|${REVISION_NO}|g" Cryptomator.app/Contents/Info.plist + env: + VERSION_NO: ${{ steps.versions.outputs.semVerNum }} + REVISION_NO: ${{ steps.versions.outputs.revNum }} + - name: Install codesign certificate + run: | + # create variables + CERTIFICATE_PATH=$RUNNER_TEMP/codesign.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/codesign.keychain-db + + # import certificate and provisioning profile from secrets + echo -n "$CODESIGN_P12_BASE64" | base64 --decode --output $CERTIFICATE_PATH + + # create temporary keychain + security create-keychain -p "$CODESIGN_TMP_KEYCHAIN_PW" $KEYCHAIN_PATH + security set-keychain-settings -lut 900 $KEYCHAIN_PATH + security unlock-keychain -p "$CODESIGN_TMP_KEYCHAIN_PW" $KEYCHAIN_PATH + + # import certificate to keychain + security import $CERTIFICATE_PATH -P "$CODESIGN_P12_PW" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security list-keychain -d user -s $KEYCHAIN_PATH + env: + CODESIGN_P12_BASE64: ${{ secrets.MACOS_CODESIGN_P12_BASE64 }} + CODESIGN_P12_PW: ${{ secrets.MACOS_CODESIGN_P12_PW }} + CODESIGN_TMP_KEYCHAIN_PW: ${{ secrets.MACOS_CODESIGN_TMP_KEYCHAIN_PW }} + - name: Codesign + run: | + find Cryptomator.app/Contents/runtime/Contents/MacOS -name '*.dylib' -exec codesign --force -s ${CODESIGN_IDENTITY} {} \; + for JAR_PATH in `find Cryptomator.app -name "*.jar"`; do + if [[ `unzip -l ${JAR_PATH} | grep '.dylib\|.jnilib'` ]]; then + JAR_FILENAME=$(basename ${JAR_PATH}) + OUTPUT_PATH=${JAR_PATH%.*} + echo "Codesigning libs in ${JAR_FILENAME}..." + unzip -q ${JAR_PATH} -d ${OUTPUT_PATH} + find ${OUTPUT_PATH} -name '*.dylib' -exec codesign --force -s ${CODESIGN_IDENTITY} {} \; + find ${OUTPUT_PATH} -name '*.jnilib' -exec codesign --force -s ${CODESIGN_IDENTITY} {} \; + rm ${JAR_PATH} + pushd ${OUTPUT_PATH} > /dev/null + zip -qr ../${JAR_FILENAME} * + popd > /dev/null + rm -r ${OUTPUT_PATH} + fi + done + echo "Codesigning Cryptomator.app..." + codesign --force --deep --entitlements dist/mac/Cryptomator.entitlements -o runtime -s ${CODESIGN_IDENTITY} Cryptomator.app + env: + CODESIGN_IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }} + - name: Prepare .dmg contents + run: | + mkdir dmg + mv Cryptomator.app dmg + cp dist/mac/dmg/resources/macFUSE.webloc dmg + ls -l dmg + - name: Install create-dmg + run: | + brew install create-dmg + create-dmg --help + - name: Create .dmg + run: > + create-dmg + --volname Cryptomator + --volicon "dist/mac/dmg/resources/Cryptomator-Volume.icns" + --background "dist/mac/dmg/resources/Cryptomator-background.tiff" + --window-pos 400 100 + --window-size 640 694 + --icon-size 128 + --icon "Cryptomator.app" 128 245 + --hide-extension "Cryptomator.app" + --icon "macFUSE.webloc" 320 501 + --hide-extension "macFUSE.webloc" + --app-drop-link 512 245 + --eula "dist/mac/dmg/resources/license.rtf" + --icon ".background" 128 758 + --icon ".fseventsd" 320 758 + --icon ".VolumeIcon.icns" 512 758 + Cryptomator-${VERSION_NO}.dmg dmg + env: + VERSION_NO: ${{ steps.versions.outputs.semVerNum }} + - name: Install notarization credentials + if: startsWith(github.ref, 'refs/tags/') + run: | + # create temporary keychain + KEYCHAIN_PATH=$RUNNER_TEMP/notarization.keychain-db + security create-keychain -p "${NOTARIZATION_TMP_KEYCHAIN_PW}" ${KEYCHAIN_PATH} + security set-keychain-settings -lut 900 ${KEYCHAIN_PATH} + security unlock-keychain -p "${NOTARIZATION_TMP_KEYCHAIN_PW}" ${KEYCHAIN_PATH} + + # import credentials from secrets + sudo xcode-select -s /Applications/Xcode_13.0.app + xcrun notarytool store-credentials "${NOTARIZATION_KEYCHAIN_PROFILE}" --apple-id "${NOTARIZATION_APPLE_ID}" --password "${NOTARIZATION_PW}" --team-id "${NOTARIZATION_TEAM_ID}" --keychain "${KEYCHAIN_PATH}" + env: + NOTARIZATION_KEYCHAIN_PROFILE: ${{ secrets.MACOS_NOTARIZATION_KEYCHAIN_PROFILE }} + NOTARIZATION_APPLE_ID: ${{ secrets.MACOS_NOTARIZATION_APPLE_ID }} + NOTARIZATION_PW: ${{ secrets.MACOS_NOTARIZATION_PW }} + NOTARIZATION_TEAM_ID: ${{ secrets.MACOS_NOTARIZATION_TEAM_ID }} + NOTARIZATION_TMP_KEYCHAIN_PW: ${{ secrets.MACOS_NOTARIZATION_TMP_KEYCHAIN_PW }} + - name: Notarize .dmg + if: startsWith(github.ref, 'refs/tags/') + run: | + KEYCHAIN_PATH=$RUNNER_TEMP/notarization.keychain-db + sudo xcode-select -s /Applications/Xcode_13.0.app + xcrun notarytool submit Cryptomator-*.dmg --keychain-profile "${NOTARIZATION_KEYCHAIN_PROFILE}" --keychain "${KEYCHAIN_PATH}" --wait + xcrun stapler staple Cryptomator-*.dmg + env: + NOTARIZATION_KEYCHAIN_PROFILE: ${{ secrets.MACOS_NOTARIZATION_KEYCHAIN_PROFILE }} + - name: Add possible alpha/beta tags to installer name + run: mv Cryptomator-*.dmg Cryptomator-${{ steps.versions.outputs.semVerStr }}.dmg + - name: Create detached GPG signature with key 615D449FE6E6A235 + run: | + echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import + echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a Cryptomator-*.dmg + env: + GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} + - name: Clean up codesign certificate + if: ${{ always() }} + run: security delete-keychain $RUNNER_TEMP/codesign.keychain-db + continue-on-error: true + - name: Clean up notarization credentials + if: ${{ always() }} + run: security delete-keychain $RUNNER_TEMP/notarization.keychain-db + continue-on-error: true + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: dmg + path: Cryptomator-*.dmg + if-no-files-found: error + - name: Publish dmg on GitHub Releases + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v1 + with: + fail_on_unmatched_files: true + token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} + files: | + Cryptomator-*.dmg + Cryptomator-*.asc + + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index a24342da3..000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,738 +0,0 @@ -name: Installers and Release - -on: - workflow_dispatch: - inputs: - semver: - description: 'SemVer' - required: true - default: '0.99.99-SNAPSHOT' - push: - tags: # see https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet - - '[0-9]+.[0-9]+.[0-9]+' - - '[0-9]+.[0-9]+.[0-9]+-*' - -env: - JAVA_VERSION: 17 - -defaults: - run: - shell: bash - -jobs: - -# -# Buildkit -# - buildkit: - name: Build ${{ matrix.profile }}-buildkit - runs-on: ${{ matrix.os }} - strategy: - fail-fast: true - matrix: - include: - - os: ubuntu-latest - profile: linux - - os: windows-latest - profile: win - - os: macos-latest - profile: mac - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 - with: - distribution: 'temurin' - java-version: ${{ env.JAVA_VERSION }} - cache: 'maven' - - name: Ensure to use tagged version - run: mvn versions:set -DnewVersion=${GITHUB_REF##*/} # use shell parameter expansion to strip of 'refs/tags' - if: startsWith(github.ref, 'refs/tags/') - - name: Build and Test - run: mvn -B clean package -Pdependency-check,${{ matrix.profile }} - - name: Patch buildkit - run: | - cp LICENSE.txt target - cp dist/${{ matrix.profile }}/launcher* target - cp target/cryptomator-*.jar target/mods - - name: Upload ${{ matrix.profile }}-buildkit - uses: actions/upload-artifact@v2 - with: - name: ${{ matrix.profile }}-buildkit - path: | - target/libs - target/mods - target/LICENSE.txt - target/launcher* - if-no-files-found: error - -# -# Release Metadata -# - metadata: - name: Determine Version Metadata - runs-on: ubuntu-latest - outputs: - semVerNum: ${{ steps.versions.outputs.semVerNum }} - semVerStr: ${{ steps.versions.outputs.semVerStr }} - ppaVerStr: ${{ steps.versions.outputs.ppaVerStr }} - revNum: ${{ steps.versions.outputs.revNum }} - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - id: versions - run: | - if [[ $GITHUB_REF == refs/tags/* ]]; then - SEM_VER_STR=${GITHUB_REF##*/} - else - SEM_VER_STR=${{ github.event.inputs.semver }} - fi - SEM_VER_NUM=`echo ${SEM_VER_STR} | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/'` - REVCOUNT=`git rev-list --count HEAD` - echo "::set-output name=semVerStr::${SEM_VER_STR}" - echo "::set-output name=semVerNum::${SEM_VER_NUM}" - echo "::set-output name=ppaVerStr::${SEM_VER_STR/-/\~}-${REVCOUNT}" - echo "::set-output name=revNum::${REVCOUNT}" - - uses: skymatic/semver-validation-action@v1 - with: - version: ${{ steps.versions.outputs.semVerStr }} - -# -# Application Directory -# - appdir: - name: Create ${{ matrix.profile }}-appdir - needs: [buildkit, metadata] - runs-on: ${{ matrix.os }} - strategy: - fail-fast: true - matrix: - include: - - os: ubuntu-latest - profile: linux - jpackageoptions: > - --app-version "${{ needs.metadata.outputs.semVerNum }}.${{ needs.metadata.outputs.revNum }}" - --java-options "-Dfile.encoding=\"utf-8\"" - --java-options "-Dcryptomator.logDir=\"~/.local/share/Cryptomator/logs\"" - --java-options "-Dcryptomator.pluginDir=\"~/.local/share/Cryptomator/plugins\"" - --java-options "-Dcryptomator.settingsPath=\"~/.config/Cryptomator/settings.json:~/.Cryptomator/settings.json\"" - --java-options "-Dcryptomator.ipcSocketPath=\"~/.config/Cryptomator/ipc.socket\"" - --java-options "-Dcryptomator.mountPointsDir=\"~/.local/share/Cryptomator/mnt\"" - --java-options "-Dcryptomator.showTrayIcon=false" - --java-options "-Dcryptomator.buildNumber=\"appimage-${{ needs.metadata.outputs.revNum }}\"" - --resource-dir dist/linux/resources - - os: windows-latest - profile: win - jpackageoptions: > - --app-version "${{ needs.metadata.outputs.semVerNum }}.${{ needs.metadata.outputs.revNum }}" - --java-options "-Dfile.encoding=\"utf-8\"" - --java-options "-Dcryptomator.logDir=\"~/AppData/Roaming/Cryptomator\"" - --java-options "-Dcryptomator.pluginDir=\"~/AppData/Roaming/Cryptomator/Plugins\"" - --java-options "-Dcryptomator.settingsPath=\"~/AppData/Roaming/Cryptomator/settings.json\"" - --java-options "-Dcryptomator.ipcSocketPath=\"~/AppData/Roaming/Cryptomator/ipc.socket\"" - --java-options "-Dcryptomator.keychainPath=\"~/AppData/Roaming/Cryptomator/keychain.json\"" - --java-options "-Dcryptomator.mountPointsDir=\"~/Cryptomator\"" - --java-options "-Dcryptomator.showTrayIcon=true" - --java-options "-Dcryptomator.buildNumber=\"msi-${{ needs.metadata.outputs.revNum }}\"" - --resource-dir dist/win/resources - --icon dist/win/resources/Cryptomator.ico - - os: macos-latest - profile: mac - jpackageoptions: > - --app-version "${{ needs.metadata.outputs.semVerNum }}" - --java-options "-Dfile.encoding=\"utf-8\"" - --java-options "-Dapple.awt.enableTemplateImages=true" - --java-options "-Dcryptomator.logDir=\"~/Library/Logs/Cryptomator\"" - --java-options "-Dcryptomator.pluginDir=\"~/Library/Application Support/Cryptomator/Plugins\"" - --java-options "-Dcryptomator.settingsPath=\"~/Library/Application Support/Cryptomator/settings.json\"" - --java-options "-Dcryptomator.ipcSocketPath=\"~/Library/Application Support/Cryptomator/ipc.socket\"" - --java-options "-Dcryptomator.showTrayIcon=true" - --java-options "-Dcryptomator.buildNumber=\"dmg-${{ needs.metadata.outputs.revNum }}\"" - --mac-package-identifier org.cryptomator - --resource-dir dist/mac/resources - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 - with: - distribution: 'temurin' - java-version: ${{ env.JAVA_VERSION }} - - name: Download ${{ matrix.profile }}-buildkit - uses: actions/download-artifact@v2 - with: - name: ${{ matrix.profile }}-buildkit - path: buildkit - - name: Create Runtime Image - run: > - ${JAVA_HOME}/bin/jlink - --verbose - --output runtime - --module-path "${JAVA_HOME}/jmods" - --add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr - --no-header-files - --no-man-pages - --strip-debug - --compress=1 - - name: Create App Directory - run: > - ${JAVA_HOME}/bin/jpackage - --verbose - --type app-image - --runtime-image runtime - --input buildkit/libs - --module-path buildkit/mods - --module org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator - --dest appdir - --name Cryptomator - --vendor "Skymatic GmbH" - --copyright "(C) 2016 - 2022 Skymatic GmbH" - --java-options "-Xss5m" - --java-options "-Xmx256m" - --java-options "-Dcryptomator.appVersion=\"${{ needs.metadata.outputs.semVerStr }}\"" - ${{ matrix.jpackageoptions }} - - name: Create appdir.tar - run: tar -cvf appdir.tar appdir - - name: Upload ${{ matrix.profile }}-appdir - uses: actions/upload-artifact@v2 - with: - name: ${{ matrix.profile }}-appdir - path: appdir.tar - if-no-files-found: error - -# -# Debian Package -# - deb: - name: Create Debian Package - needs: [buildkit, metadata] - runs-on: ubuntu-18.04 - steps: - - uses: actions/checkout@v2 - - name: install build tools - run: | - sudo apt-get update - sudo apt-get install debhelper devscripts - - name: Download linux-buildkit - uses: actions/download-artifact@v2 - with: - name: linux-buildkit - path: pkgdir - - name: create orig.tar.gz with common/ libs/ mods/ - run: | - cp -r dist/linux/common/ pkgdir - tar -cJf cryptomator_${{ needs.metadata.outputs.ppaVerStr }}.orig.tar.xz -C pkgdir . - - name: patch and rename pkgdir - run: | - cp -r dist/linux/debian/ pkgdir - export RFC2822_TIMESTAMP=`date --rfc-2822` - envsubst '${SEMVER_STR} ${VERSION_NUM} ${REVISION_NUM}' < dist/linux/debian/rules > pkgdir/debian/rules - envsubst '${PPA_VERSION} ${RFC2822_TIMESTAMP}' < dist/linux/debian/changelog > pkgdir/debian/changelog - find . -name "*.jar" >> pkgdir/debian/source/include-binaries - mv pkgdir cryptomator_${{ needs.metadata.outputs.ppaVerStr }} - env: - SEMVER_STR: ${{ needs.metadata.outputs.semVerStr }} - VERSION_NUM: ${{ needs.metadata.outputs.semVerNum }} - REVISION_NUM: ${{ needs.metadata.outputs.revNum }} - PPA_VERSION: ${{ needs.metadata.outputs.ppaVerStr }}-0ppa1 - - name: import gpg key 615D449FE6E6A235 - run: | - echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import - echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --dry-run --sign dist/linux/debian/rules - env: - GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} - GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} - - name: debuild - run: | - debuild -S -sa -d - debuild -b -sa -d - env: - DEBSIGN_PROGRAM: gpg --batch --pinentry-mode loopback - DEBSIGN_KEYID: 615D449FE6E6A235 - working-directory: cryptomator_${{ needs.metadata.outputs.ppaVerStr }} - - name: Upload artifacts - uses: actions/upload-artifact@v2 - with: - name: linux-deb-package - path: | - cryptomator_*.dsc - cryptomator_*.orig.tar.xz - cryptomator_*.debian.tar.xz - cryptomator_*_source.buildinfo - cryptomator_*_source.changes - cryptomator_*_amd64.deb - -# -# Upload Source Package to PPA -# - ppa: - name: Upload Source Package to PPA - needs: [deb] - runs-on: ubuntu-18.04 - steps: - - name: install dput - run: | - sudo apt-get update - sudo apt-get install dput - - name: import public key - run: curl -sSL https://github.com/cryptobot.gpg | gpg --import - - - name: download linux-deb-package - uses: actions/download-artifact@v2 - with: - name: linux-deb-package - path: . - - name: dput to beta repo - run: dput ppa:sebastian-stenzel/cryptomator-beta cryptomator_*_source.changes - -# -# Linux Cryptomator.AppImage -# - linux-appimage: - name: Build Cryptomator.AppImage - runs-on: ubuntu-latest - needs: [appdir, metadata] - steps: - - uses: actions/checkout@v2 - - name: Download linux-appdir - uses: actions/download-artifact@v2 - with: - name: linux-appdir - - name: Untar appdir.tar - run: | - tar -xvf appdir.tar - - name: Patch Cryptomator.AppDir - run: | - mv appdir/Cryptomator Cryptomator.AppDir - cp -r dist/linux/appimage/resources/AppDir/* Cryptomator.AppDir/ - envsubst '${REVISION_NO} ${SEMVER_STR}' < dist/linux/appimage/resources/AppDir/bin/cryptomator.sh > Cryptomator.AppDir/bin/cryptomator.sh - cp dist/linux/common/org.cryptomator.Cryptomator256.png Cryptomator.AppDir/usr/share/icons/hicolor/256x256/apps/org.cryptomator.Cryptomator.png - cp dist/linux/common/org.cryptomator.Cryptomator512.png Cryptomator.AppDir/usr/share/icons/hicolor/512x512/apps/org.cryptomator.Cryptomator.png - cp dist/linux/common/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg - cp dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml Cryptomator.AppDir/usr/share/metainfo/org.cryptomator.Cryptomator.metainfo.xml - cp dist/linux/common/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/usr/share/applications/org.cryptomator.Cryptomator.desktop - cp dist/linux/common/application-vnd.cryptomator.vault.xml Cryptomator.AppDir/usr/share/mime/packages/application-vnd.cryptomator.vault.xml - ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/org.cryptomator.Cryptomator.svg - ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/Cryptomator.svg - ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/.DirIcon - ln -s usr/share/applications/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/Cryptomator.desktop - ln -s bin/cryptomator.sh Cryptomator.AppDir/AppRun - env: - REVISION_NO: ${{ needs.metadata.outputs.revNum }} - SEMVER_STR: ${{ needs.metadata.outputs.semVerStr }} - - name: Extract libjffi.so # workaround for https://github.com/cryptomator/cryptomator-linux/issues/27 - run: | - JFFI_NATIVE_JAR=`ls lib/app/ | grep -e 'jffi-[1-9]\.[0-9]\{1,2\}.[0-9]\{1,2\}-native.jar'` - ${JAVA_HOME}/bin/jar -xf lib/app/${JFFI_NATIVE_JAR} /jni/x86_64-Linux/ - mv jni/x86_64-Linux/* lib/app/libjffi.so - working-directory: Cryptomator.AppDir - - name: Download AppImageKit - run: | - curl -L https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage -o appimagetool.AppImage - chmod +x appimagetool.AppImage - ./appimagetool.AppImage --appimage-extract - - name: Prepare GPG-Agent for signing with key 615D449FE6E6A235 - run: | - echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import - echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --dry-run --sign Cryptomator.AppDir/AppRun - env: - GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} - GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} - - name: Build AppImage - run: > - ./squashfs-root/AppRun Cryptomator.AppDir cryptomator-${{ needs.metadata.outputs.semVerStr }}-x86_64.AppImage - -u 'gh-releases-zsync|cryptomator|cryptomator|latest|cryptomator-*-x86_64.AppImage.zsync' - --sign --sign-key=615D449FE6E6A235 --sign-args="--batch --pinentry-mode loopback" - - name: Upload AppImage - uses: actions/upload-artifact@v2 - with: - name: linux-appimage - path: | - cryptomator-*.AppImage - cryptomator-*.AppImage.zsync - if-no-files-found: error - -# -# macOS Cryptomator.app -# - mac-app: - name: Build Cryptomator.app - runs-on: macos-latest - needs: [appdir, metadata] - steps: - - uses: actions/checkout@v2 - - name: Download mac-appdir - uses: actions/download-artifact@v2 - with: - name: mac-appdir - - name: Untar appdir.tar - run: tar -xvf appdir.tar - - name: Patch Cryptomator.app - run: | - mv appdir/Cryptomator.app Cryptomator.app - mv dist/mac/resources/Cryptomator-Vault.icns Cryptomator.app/Contents/Resources/ - sed -i '' "s|###BUNDLE_SHORT_VERSION_STRING###|${VERSION_NO}|g" Cryptomator.app/Contents/Info.plist - sed -i '' "s|###BUNDLE_VERSION###|${REVISION_NO}|g" Cryptomator.app/Contents/Info.plist - env: - VERSION_NO: ${{ needs.metadata.outputs.semVerNum }} - REVISION_NO: ${{ needs.metadata.outputs.revNum }} - - name: Install codesign certificate - env: - CODESIGN_P12_BASE64: ${{ secrets.MACOS_CODESIGN_P12_BASE64 }} - CODESIGN_P12_PW: ${{ secrets.MACOS_CODESIGN_P12_PW }} - CODESIGN_TMP_KEYCHAIN_PW: ${{ secrets.MACOS_CODESIGN_TMP_KEYCHAIN_PW }} - run: | - # create variables - CERTIFICATE_PATH=$RUNNER_TEMP/codesign.p12 - KEYCHAIN_PATH=$RUNNER_TEMP/codesign.keychain-db - - # import certificate and provisioning profile from secrets - echo -n "$CODESIGN_P12_BASE64" | base64 --decode --output $CERTIFICATE_PATH - - # create temporary keychain - security create-keychain -p "$CODESIGN_TMP_KEYCHAIN_PW" $KEYCHAIN_PATH - security set-keychain-settings -lut 900 $KEYCHAIN_PATH - security unlock-keychain -p "$CODESIGN_TMP_KEYCHAIN_PW" $KEYCHAIN_PATH - - # import certificate to keychain - security import $CERTIFICATE_PATH -P "$CODESIGN_P12_PW" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH - security list-keychain -d user -s $KEYCHAIN_PATH - - name: Codesign - env: - CODESIGN_IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }} - run: | - find Cryptomator.app/Contents/runtime/Contents/MacOS -name '*.dylib' -exec codesign --force -s ${CODESIGN_IDENTITY} {} \; - for JAR_PATH in `find Cryptomator.app -name "*.jar"`; do - if [[ `unzip -l ${JAR_PATH} | grep '.dylib\|.jnilib'` ]]; then - JAR_FILENAME=$(basename ${JAR_PATH}) - OUTPUT_PATH=${JAR_PATH%.*} - echo "Codesigning libs in ${JAR_FILENAME}..." - unzip -q ${JAR_PATH} -d ${OUTPUT_PATH} - find ${OUTPUT_PATH} -name '*.dylib' -exec codesign --force -s ${CODESIGN_IDENTITY} {} \; - find ${OUTPUT_PATH} -name '*.jnilib' -exec codesign --force -s ${CODESIGN_IDENTITY} {} \; - rm ${JAR_PATH} - pushd ${OUTPUT_PATH} > /dev/null - zip -qr ../${JAR_FILENAME} * - popd > /dev/null - rm -r ${OUTPUT_PATH} - fi - done - echo "Codesigning Cryptomator.app..." - codesign --force --deep --entitlements dist/mac/Cryptomator.entitlements -o runtime -s ${CODESIGN_IDENTITY} Cryptomator.app - - name: Clean up codesign certificate - if: ${{ always() }} - run: security delete-keychain $RUNNER_TEMP/codesign.keychain-db - - name: Create app.tar - run: tar -cvf app.tar Cryptomator.app - - name: Upload mac-app - uses: actions/upload-artifact@v2 - with: - name: mac-app - path: app.tar - if-no-files-found: error - -# -# macOS Cryptomator.dmg -# - mac-dmg: - name: Build Cryptomator.dmg - runs-on: macos-11 - needs: [mac-app, metadata] - steps: - - uses: actions/checkout@v2 - - name: Download mac-appdir - uses: actions/download-artifact@v2 - with: - name: mac-app - - name: Untar app.tar - run: tar -xvf app.tar - - name: Prepare .dmg contents - run: | - mkdir dmg - mv Cryptomator.app dmg - cp dist/mac/dmg/resources/macFUSE.webloc dmg - ls -l dmg - - name: Install create-dmg - run: | - brew install create-dmg - create-dmg --help - - name: Create .dmg - run: > - create-dmg - --volname Cryptomator - --volicon "dist/mac/dmg/resources/Cryptomator-Volume.icns" - --background "dist/mac/dmg/resources/Cryptomator-background.tiff" - --window-pos 400 100 - --window-size 640 694 - --icon-size 128 - --icon "Cryptomator.app" 128 245 - --hide-extension "Cryptomator.app" - --icon "macFUSE.webloc" 320 501 - --hide-extension "macFUSE.webloc" - --app-drop-link 512 245 - --eula "dist/mac/dmg/resources/license.rtf" - --icon ".background" 128 758 - --icon ".fseventsd" 320 758 - --icon ".VolumeIcon.icns" 512 758 - Cryptomator-${VERSION_NO}.dmg dmg - env: - VERSION_NO: ${{ needs.metadata.outputs.semVerNum }} - - name: Install notarization credentials - env: - NOTARIZATION_KEYCHAIN_PROFILE: ${{ secrets.MACOS_NOTARIZATION_KEYCHAIN_PROFILE }} - NOTARIZATION_APPLE_ID: ${{ secrets.MACOS_NOTARIZATION_APPLE_ID }} - NOTARIZATION_PW: ${{ secrets.MACOS_NOTARIZATION_PW }} - NOTARIZATION_TEAM_ID: ${{ secrets.MACOS_NOTARIZATION_TEAM_ID }} - NOTARIZATION_TMP_KEYCHAIN_PW: ${{ secrets.MACOS_NOTARIZATION_TMP_KEYCHAIN_PW }} - run: | - # create temporary keychain - KEYCHAIN_PATH=$RUNNER_TEMP/notarization.keychain-db - security create-keychain -p "${NOTARIZATION_TMP_KEYCHAIN_PW}" ${KEYCHAIN_PATH} - security set-keychain-settings -lut 900 ${KEYCHAIN_PATH} - security unlock-keychain -p "${NOTARIZATION_TMP_KEYCHAIN_PW}" ${KEYCHAIN_PATH} - - # import credentials from secrets - sudo xcode-select -s /Applications/Xcode_13.0.app - xcrun notarytool store-credentials "${NOTARIZATION_KEYCHAIN_PROFILE}" --apple-id "${NOTARIZATION_APPLE_ID}" --password "${NOTARIZATION_PW}" --team-id "${NOTARIZATION_TEAM_ID}" --keychain "${KEYCHAIN_PATH}" - - name: Notarize .dmg - env: - NOTARIZATION_KEYCHAIN_PROFILE: ${{ secrets.MACOS_NOTARIZATION_KEYCHAIN_PROFILE }} - run: | - KEYCHAIN_PATH=$RUNNER_TEMP/notarization.keychain-db - sudo xcode-select -s /Applications/Xcode_13.0.app - xcrun notarytool submit Cryptomator-*.dmg --keychain-profile "${NOTARIZATION_KEYCHAIN_PROFILE}" --keychain "${KEYCHAIN_PATH}" --wait - xcrun stapler staple Cryptomator-*.dmg - - name: Clean up notarization credentials - if: ${{ always() }} - run: security delete-keychain $RUNNER_TEMP/notarization.keychain-db - - name: Add possible alpha/beta tags to installer name - run: mv Cryptomator-*.dmg Cryptomator-${{ needs.metadata.outputs.semVerStr }}.dmg - - name: Upload mac-dmg - uses: actions/upload-artifact@v2 - with: - name: mac-dmg - path: Cryptomator-*.dmg - if-no-files-found: error - -# -# MSI package -# - win-msi: - name: Build Cryptomator.msi - runs-on: windows-latest - needs: [appdir, metadata] - steps: - - uses: actions/checkout@v2 - - name: Download win-appdir - uses: actions/download-artifact@v2 - with: - name: win-appdir - - name: Untar appdir.tar - run: tar -xvf appdir.tar - - uses: actions/setup-java@v2 - with: - distribution: 'temurin' - java-version: ${{ env.JAVA_VERSION }} - cache: 'maven' - - name: Patch Application Directory - run: | - cp dist/win/contrib/* appdir/Cryptomator - - name: Fix permissions - run: attrib -r appdir/Cryptomator/Cryptomator.exe - shell: pwsh - - name: Codesign - uses: skymatic/code-sign-action@v1 - with: - certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }} - password: ${{ secrets.WIN_CODESIGN_P12_PW }} - certificatesha1: FF52240075AD7D14AF25629FDF69635357C7D14B - description: Cryptomator - timestampUrl: 'http://timestamp.digicert.com' - folder: appdir/Cryptomator - recursive: true - - name: Generate license - run: > - mvn -B license:add-third-party - "-Dlicense.thirdPartyFilename=license.rtf" - "-Dlicense.fileTemplate=dist/win/resources/licenseTemplate.ftl" - "-Dlicense.outputDirectory=dist/win/resources" - - name: Create MSI - run: > - ${JAVA_HOME}/bin/jpackage - --verbose - --type msi - --win-upgrade-uuid bda45523-42b1-4cae-9354-a45475ed4775 - --app-image appdir/Cryptomator - --dest installer - --name Cryptomator - --vendor "Skymatic GmbH" - --copyright "(C) 2016 - 2022 Skymatic GmbH" - --app-version "${{ needs.metadata.outputs.semVerNum }}" - --win-menu - --win-dir-chooser - --win-shortcut-prompt - --win-update-url "https:\\cryptomator.org" - --win-menu-group Cryptomator - --resource-dir dist/win/resources - --license-file dist/win/resources/license.rtf - --file-associations dist/win/resources/FAvaultFile.properties - env: - JP_WIXWIZARD_RESOURCES: ${{ github.workspace }}/dist/win/resources # requires abs path, used in resources/main.wxs - - name: Codesign MSI - uses: skymatic/code-sign-action@v1 - with: - certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }} - password: ${{ secrets.WIN_CODESIGN_P12_PW }} - certificatesha1: FF52240075AD7D14AF25629FDF69635357C7D14B - description: Cryptomator Installer - timestampUrl: 'http://timestamp.digicert.com' - folder: installer - - name: Add possible alpha/beta tags to installer name - run: mv installer/Cryptomator-*.msi installer/Cryptomator-${{ needs.metadata.outputs.semVerStr }}-x64.msi - - name: Upload win-msi - uses: actions/upload-artifact@v2 - with: - name: win-msi - path: installer/*.msi - if-no-files-found: error - -# -# Windows Cryptomator.exe bundle -# - win-exe: - name: Build Cryptomator.exe bundle - runs-on: windows-latest - needs: [win-msi, metadata] - steps: - - uses: actions/checkout@v2 - - name: Download Windows msi - uses: actions/download-artifact@v2 - with: - name: win-msi - path: dist/win/bundle/resources - - name: Strip version info from msi file name - run: mv dist/win/bundle/resources/Cryptomator*.msi dist/win/bundle/resources/Cryptomator.msi - - uses: actions/setup-java@v2 - with: - distribution: 'temurin' - java-version: ${{ env.JAVA_VERSION }} - cache: 'maven' - - name: Generate license - run: > - mvn -B license:add-third-party - "-Dlicense.thirdPartyFilename=license.rtf" - "-Dlicense.fileTemplate=dist/win/bundle/resources/licenseTemplate.ftl" - "-Dlicense.outputDirectory=dist/win/bundle/resources" - - name: Download winfsp - run: - curl --output dist/win/bundle/resources/winfsp.msi -L https://github.com/winfsp/winfsp/releases/download/v1.10/winfsp-1.10.22006.msi - - name: Compile to wixObj file - run: > - "${WIX}/bin/candle.exe" dist/win/bundle/bundleWithWinfsp.wxs - -ext WixBalExtension - -out dist/win/bundle/ - -dBundleVersion="${{ needs.metadata.outputs.semVerNum }}.${{ needs.metadata.outputs.revNum }}" - -dBundleVendor="Skymatic GmbH" - -dBundleCopyright="(C) 2016 - 2022 Skymatic GmbH" - -dAboutUrl="https://cryptomator.org" - -dHelpUrl="https://cryptomator.org/contact" - -dUpdateUrl="https://cryptomator.org/downloads/" - - name: Create executable with linker - run: > - "${WIX}/bin/light.exe" -b dist/win/ dist/win/bundle/bundleWithWinfsp.wixobj - -ext WixBalExtension - -out installer/Cryptomator.exe - - name: Codesign EXE - uses: skymatic/code-sign-action@v1 - with: - certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }} - password: ${{ secrets.WIN_CODESIGN_P12_PW }} - certificatesha1: FF52240075AD7D14AF25629FDF69635357C7D14B - description: Cryptomator Installer - timestampUrl: 'http://timestamp.digicert.com' - folder: installer - - name: Add possible alpha/beta tags to installer name - run: mv installer/Cryptomator.exe installer/Cryptomator-${{ needs.metadata.outputs.semVerStr }}-x64.exe - - name: Upload win-exe - uses: actions/upload-artifact@v2 - with: - name: win-exe - path: installer/*.exe - if-no-files-found: error - -# -# Release -# - release: - name: Draft a release on Github - runs-on: ubuntu-latest - needs: [metadata,linux-appimage,mac-dmg,win-msi,win-exe,ppa] - if: startsWith(github.ref, 'refs/tags/') && github.repository == 'cryptomator/cryptomator' - steps: - - uses: actions/checkout@v2 - - name: Create tarball - run: git archive --prefix="cryptomator-${{ needs.metadata.outputs.semVerStr }}/" -o "cryptomator-${{ needs.metadata.outputs.semVerStr }}.tar.gz" ${{ github.ref }} - - name: Download Debian package - uses: actions/download-artifact@v2 - with: - name: linux-deb-package - - name: Download linux appimage - uses: actions/download-artifact@v2 - with: - name: linux-appimage - - name: Download macOS dmg - uses: actions/download-artifact@v2 - with: - name: mac-dmg - - name: Download Windows msi - uses: actions/download-artifact@v2 - with: - name: win-msi - - name: Download Windows exe - uses: actions/download-artifact@v2 - with: - name: win-exe - - name: Create detached GPG signature for all release files with key 615D449FE6E6A235 - run: | - echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import - for FILE in `find . -name "*.AppImage" -o -name "*.deb" -o -name "*.dmg" -o -name "*.exe" -o -name "*.msi" -o -name "*.zsync" -o -name "*.tar.gz"`; do - echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a ${FILE} - done - env: - GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} - GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} - - name: Compute SHA256 checksums of release artifacts - run: | - SHA256_SUMS=`find . -name "*.AppImage" -o -name "*.deb" -o -name "*.dmg" -o -name "*.exe" -o -name "*.msi" -o -name "*.tar.gz" | xargs sha256sum` - echo "SHA256_SUMS<> $GITHUB_ENV - echo "${SHA256_SUMS}" >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV - continue-on-error: true - - name: Create release draft - uses: softprops/action-gh-release@v1 - with: - draft: true - fail_on_unmatched_files: true - discussion_category_name: releases - token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} - files: | - *.AppImage - *.zsync - *.asc - *.deb - *.dmg - *.msi - *.exe - body: |- - :construction: Work in Progress - ## What's New - ## Bugfixes - ## Misc - - --- - - :scroll: A complete list of closed issues is available [here](LINK). - - --- - - :floppy_disk: SHA-256 checksums of release artifacts: - ``` - ${{ env.SHA256_SUMS }} - ``` diff --git a/.github/workflows/win-exe.yml b/.github/workflows/win-exe.yml new file mode 100644 index 000000000..25a0575fb --- /dev/null +++ b/.github/workflows/win-exe.yml @@ -0,0 +1,274 @@ +name: Build Windows Installer + +on: + release: + types: [published] + workflow_dispatch: + +env: + JAVA_VERSION: 17 + WINFSP_MSI: https://github.com/winfsp/winfsp/releases/download/v1.10/winfsp-1.10.22006.msi + +defaults: + run: + shell: bash + +jobs: + build-msi: + name: Build .msi Installer + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Setup Java + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + cache: 'maven' + - id: versions + name: Apply version information + run: | + if [[ $GITHUB_REF =~ refs/tags/[0-9]+\.[0-9]+\.[0-9]+.* ]]; then + SEM_VER_STR=${GITHUB_REF##*/} + mvn versions:set -DnewVersion=${SEM_VER_STR} + else + SEM_VER_STR=`mvn help:evaluate -Dexpression=project.version -q -DforceStdout` + fi + SEM_VER_NUM=`echo ${SEM_VER_STR} | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/'` + REVCOUNT=`git rev-list --count HEAD` + echo "::set-output name=semVerStr::${SEM_VER_STR}" + echo "::set-output name=semVerNum::${SEM_VER_NUM}" + echo "::set-output name=revNum::${REVCOUNT}" + - name: Validate Version + uses: skymatic/semver-validation-action@v1 + with: + version: ${{ steps.versions.outputs.semVerStr }} + - name: Run maven + run: mvn -B clean package -Pdependency-check,win -DskipTests + - name: Patch target dir + run: | + cp LICENSE.txt target + cp dist/linux/launcher.sh target + cp target/cryptomator-*.jar target/mods + - name: Run jlink + run: > + ${JAVA_HOME}/bin/jlink + --verbose + --output runtime + --module-path "${JAVA_HOME}/jmods" + --add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr + --strip-native-commands + --no-header-files + --no-man-pages + --strip-debug + --compress=1 + - name: Run jpackage + run: > + ${JAVA_HOME}/bin/jpackage + --verbose + --type app-image + --runtime-image runtime + --input target/libs + --module-path target/mods + --module org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator + --dest appdir + --name Cryptomator + --vendor "Skymatic GmbH" + --copyright "(C) 2016 - 2022 Skymatic GmbH" + --app-version "${{ steps.versions.outputs.semVerNum }}.${{ steps.versions.outputs.revNum }}" + --java-options "-Xss5m" + --java-options "-Xmx256m" + --java-options "-Dfile.encoding=\"utf-8\"" + --java-options "-Dcryptomator.logDir=\"~/AppData/Roaming/Cryptomator\"" + --java-options "-Dcryptomator.pluginDir=\"~/AppData/Roaming/Cryptomator/Plugins\"" + --java-options "-Dcryptomator.settingsPath=\"~/AppData/Roaming/Cryptomator/settings.json\"" + --java-options "-Dcryptomator.ipcSocketPath=\"~/AppData/Roaming/Cryptomator/ipc.socket\"" + --java-options "-Dcryptomator.keychainPath=\"~/AppData/Roaming/Cryptomator/keychain.json\"" + --java-options "-Dcryptomator.mountPointsDir=\"~/Cryptomator\"" + --java-options "-Dcryptomator.showTrayIcon=true" + --java-options "-Dcryptomator.buildNumber=\"msi-${{ steps.versions.outputs.revNum }}\"" + --resource-dir dist/win/resources + --icon dist/win/resources/Cryptomator.ico + - name: Patch Application Directory + run: | + cp dist/win/contrib/* appdir/Cryptomator + - name: Fix permissions + run: attrib -r appdir/Cryptomator/Cryptomator.exe + shell: pwsh + - name: Codesign + uses: skymatic/code-sign-action@v1 + with: + certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }} + password: ${{ secrets.WIN_CODESIGN_P12_PW }} + certificatesha1: FF52240075AD7D14AF25629FDF69635357C7D14B + description: Cryptomator + timestampUrl: 'http://timestamp.digicert.com' + folder: appdir/Cryptomator + recursive: true + - name: Generate license + run: > + mvn -B license:add-third-party + "-Dlicense.thirdPartyFilename=license.rtf" + "-Dlicense.fileTemplate=dist/win/resources/licenseTemplate.ftl" + "-Dlicense.outputDirectory=dist/win/resources" + - name: Create MSI + run: > + ${JAVA_HOME}/bin/jpackage + --verbose + --type msi + --win-upgrade-uuid bda45523-42b1-4cae-9354-a45475ed4775 + --app-image appdir/Cryptomator + --dest installer + --name Cryptomator + --vendor "Skymatic GmbH" + --copyright "(C) 2016 - 2022 Skymatic GmbH" + --app-version "${{ steps.versions.outputs.semVerNum }}" + --win-menu + --win-dir-chooser + --win-shortcut-prompt + --win-update-url "https:\\cryptomator.org" + --win-menu-group Cryptomator + --resource-dir dist/win/resources + --license-file dist/win/resources/license.rtf + --file-associations dist/win/resources/FAvaultFile.properties + env: + JP_WIXWIZARD_RESOURCES: ${{ github.workspace }}/dist/win/resources # requires abs path, used in resources/main.wxs + - name: Codesign MSI + uses: skymatic/code-sign-action@v1 + with: + certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }} + password: ${{ secrets.WIN_CODESIGN_P12_PW }} + certificatesha1: FF52240075AD7D14AF25629FDF69635357C7D14B + description: Cryptomator Installer + timestampUrl: 'http://timestamp.digicert.com' + folder: installer + - name: Add possible alpha/beta tags to installer name + run: mv installer/Cryptomator-*.msi Cryptomator-${{ steps.versions.outputs.semVerStr }}-x64.msi + - name: Create detached GPG signature with key 615D449FE6E6A235 + run: | + echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import + echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a Cryptomator-*.msi + env: + GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: msi + path: | + Cryptomator-*.msi + Cryptomator-*.asc + if-no-files-found: error + - name: Publish .msi on GitHub Releases + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v1 + with: + fail_on_unmatched_files: true + token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} + files: | + *.msi + *.asc + outputs: + semVerNum: ${{ steps.versions.outputs.semVerNum }} + semVerStr: ${{ steps.versions.outputs.semVerStr }} + revNum: ${{ steps.versions.outputs.revNum }} + + build-exe: + name: Build .exe installer + runs-on: windows-latest + needs: [build-msi] + steps: + - uses: actions/checkout@v2 + - name: Download .msi + uses: actions/download-artifact@v2 + with: + name: msi + path: dist/win/bundle/resources + - name: Strip version info from msi file name + run: mv dist/win/bundle/resources/Cryptomator*.msi dist/win/bundle/resources/Cryptomator.msi + - uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + cache: 'maven' + - name: Generate license + run: > + mvn -B license:add-third-party + "-Dlicense.thirdPartyFilename=license.rtf" + "-Dlicense.fileTemplate=dist/win/bundle/resources/licenseTemplate.ftl" + "-Dlicense.outputDirectory=dist/win/bundle/resources" + - name: Download WinFsp + run: + curl --output dist/win/bundle/resources/winfsp.msi -L ${{ env.WINFSP_MSI }} + - name: Compile to wixObj file + run: > + "${WIX}/bin/candle.exe" dist/win/bundle/bundleWithWinfsp.wxs + -ext WixBalExtension + -out dist/win/bundle/ + -dBundleVersion="${{ needs.build-msi.outputs.semVerNum }}.${{ needs.build-msi.outputs.revNum }}" + -dBundleVendor="Skymatic GmbH" + -dBundleCopyright="(C) 2016 - 2022 Skymatic GmbH" + -dAboutUrl="https://cryptomator.org" + -dHelpUrl="https://cryptomator.org/contact" + -dUpdateUrl="https://cryptomator.org/downloads/" + - name: Create executable with linker + run: > + "${WIX}/bin/light.exe" -b dist/win/ dist/win/bundle/bundleWithWinfsp.wixobj + -ext WixBalExtension + -out installer/unsigned/Cryptomator.exe + - name: Detach burn engine in preparation to sign + run: > + "${WIX}/bin/insignia.exe" + -ib installer/unsigned/Cryptomator.exe + -o tmp/engine.exe + - name: Codesign burn engine + uses: skymatic/code-sign-action@v1 + with: + certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }} + password: ${{ secrets.WIN_CODESIGN_P12_PW }} + certificatesha1: FF52240075AD7D14AF25629FDF69635357C7D14B + description: Cryptomator Installer + timestampUrl: 'http://timestamp.digicert.com' + folder: tmp + - name: Reattach signed burn engine to installer + run : > + "${WIX}/bin/insignia.exe" + -ab tmp/engine.exe installer/unsigned/Cryptomator.exe + -o installer/Cryptomator.exe + - name: Codesign EXE + uses: skymatic/code-sign-action@v1 + with: + certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }} + password: ${{ secrets.WIN_CODESIGN_P12_PW }} + certificatesha1: FF52240075AD7D14AF25629FDF69635357C7D14B + description: Cryptomator Installer + timestampUrl: 'http://timestamp.digicert.com' + folder: installer + - name: Add possible alpha/beta tags to installer name + run: mv installer/Cryptomator.exe Cryptomator-${{ needs.build-msi.outputs.semVerStr }}-x64.exe + - name: Create detached GPG signature with key 615D449FE6E6A235 + run: | + echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import + echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a Cryptomator-*.exe + env: + GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: exe + path: | + Cryptomator-*.exe + Cryptomator-*.asc + if-no-files-found: error + - name: Publish .msi on GitHub Releases + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v1 + with: + fail_on_unmatched_files: true + token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} + files: | + Cryptomator-*.exe + Cryptomator-*.asc \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8e239b35e..5c84c0dfb 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,6 @@ pom.xml.versionsBackup .idea/dictionaries/** !.idea/dictionaries/dict_* .idea/compiler.xml -.idea/encodings.xml .idea/jarRepositories.xml .idea/uiDesigner.xml .idea/**/libraries/ diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 000000000..63574ec0a --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/dist/mac/dmg/.gitignore b/dist/mac/dmg/.gitignore index c186170c9..b8ef35283 100644 --- a/dist/mac/dmg/.gitignore +++ b/dist/mac/dmg/.gitignore @@ -1,4 +1,5 @@ # created during build +Cryptomator.app/ runtime/ dmg/ -*.dmg +*.dmg \ No newline at end of file diff --git a/pom.xml b/pom.xml index 2c18c2170..c69e4922a 100644 --- a/pom.xml +++ b/pom.xml @@ -34,24 +34,28 @@ 1.1.0-beta1 1.3.3 1.3.3 - 1.2.6 + 1.2.7 - 17.0.2 + 18 3.12.0 - 3.18.3 + 3.19.0 2.2 - 31.0-jre - 2.40.3 - 2.8.9 + 31.1-jre + 2.41 + 2.9.0 1.5.2 - 1.7.32 - 1.2.9 + 1.7.36 + 1.2.11 5.8.1 - 3.12.4 + 4.4.0 2.2 + + + 7.0.0 + 0.8.7 @@ -228,7 +232,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.10.1 org.apache.maven.plugins @@ -238,7 +242,7 @@ org.apache.maven.plugins maven-dependency-plugin - 3.2.0 + 3.3.0 org.apache.maven.plugins @@ -253,17 +257,17 @@ org.apache.maven.plugins maven-jar-plugin - 3.2.0 + 3.2.2 org.jacoco jacoco-maven-plugin - 0.8.7 + ${jacoco.version} org.owasp dependency-check-maven - 6.3.1 + ${dependency-check.version} diff --git a/src/main/java/org/cryptomator/common/Passphrase.java b/src/main/java/org/cryptomator/common/Passphrase.java new file mode 100644 index 000000000..cf64c7a10 --- /dev/null +++ b/src/main/java/org/cryptomator/common/Passphrase.java @@ -0,0 +1,108 @@ +package org.cryptomator.common; + +import javax.security.auth.Destroyable; +import java.util.Arrays; + +/** + * A destroyable CharSequence. + */ +public class Passphrase implements Destroyable, CharSequence { + + private final char[] data; + private final int offset; + private final int length; + private boolean destroyed; + + /** + * Wraps (doesn't copy) the given data. + * + * @param data The wrapped data. Any changes to this will be reflected in this passphrase + */ + public Passphrase(char[] data) { + this(data, 0, data.length); + } + + /** + * Wraps (doesn't copy) a subarray of the given data. + * + * @param data The wrapped data. Any changes to this will be reflected in this passphrase + * @param offset The subarray offset, i.e. the first character of this passphrase + * @param length The subarray length, i.e. the length of this passphrase + */ + public Passphrase(char[] data, int offset, int length) { + if (offset < 0 || length < 0 || offset + length > data.length) { + throw new IndexOutOfBoundsException("[%1$d %1$d + %2$d[ not within [0, %3$d[".formatted(offset, length, data.length)); + } + this.data = data; + this.offset = offset; + this.length = length; + } + + public static Passphrase copyOf(CharSequence cs) { + char[] result = new char[cs.length()]; + for (int i = 0; i < cs.length(); i++) { + result[i] = cs.charAt(i); + } + return new Passphrase(result); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Passphrase that = (Passphrase) o; + // time-constant comparison + int diff = 0; + for (int i = 0; i < length; i++) { + diff |= charAt(i) ^ that.charAt(i); + } + return diff == 0; + } + + @Override + public int hashCode() { + // basically Arrays.hashCode, but only for a certain subarray + int result = 1; + for (int i = 0; i < length; i++) { + result = 31 * result + charAt(i); + } + return result; + } + + @Override + public String toString() { + return new String(data, offset, length); + } + + @Override + public int length() { + return length; + } + + @Override + public char charAt(int index) { + if (index < 0 || index >= length) { + throw new IndexOutOfBoundsException("%d not within [0, %d[".formatted(index, length)); + } + return data[offset + index]; + } + + @Override + public Passphrase subSequence(int start, int end) { + if (start < 0 || end < 0 || end > length || start > end) { + throw new IndexOutOfBoundsException("[%d, %d[ not within [0, %d[".formatted(start, end, length)); + } + return new Passphrase(Arrays.copyOfRange(data, offset + start, offset + end)); + } + + @Override + public boolean isDestroyed() { + return destroyed; + } + + @Override + public void destroy() { + Arrays.fill(data, offset, offset + length, '\0'); + destroyed = true; + } +} diff --git a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java index 35f2be069..1fd463432 100644 --- a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java +++ b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java @@ -35,14 +35,13 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.ResourceBundle; -import java.util.UUID; @AddVaultWizardScoped public class CreateNewVaultLocationController implements FxController { private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultLocationController.class); private static final Path DEFAULT_CUSTOM_VAULT_PATH = Paths.get(System.getProperty("user.home")); - private static final String TEMP_FILE_FORMAT = "cryptomator-%s.tmp"; + private static final String TEMP_FILE_FORMAT = ".locationTest.cryptomator.tmp"; private final Stage window; private final Lazy chooseNameScene; @@ -112,7 +111,7 @@ public class CreateNewVaultLocationController implements FxController { } private boolean isActuallyWritable(Path p) { - Path tmpFile = p.resolve(String.format(TEMP_FILE_FORMAT, UUID.randomUUID())); + Path tmpFile = p.resolve(TEMP_FILE_FORMAT); try (var chan = Files.newByteChannel(tmpFile, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE, StandardOpenOption.DELETE_ON_CLOSE)) { return true; } catch (IOException e) { diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java index b8d5bbff0..bc952f9d1 100644 --- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -42,7 +42,7 @@ public enum FxmlFile { this.ressourcePathString = ressourcePathString; } - String getRessourcePathString() { + public String getRessourcePathString() { return ressourcePathString; } } diff --git a/src/main/java/org/cryptomator/ui/common/FxmlLoaderFactory.java b/src/main/java/org/cryptomator/ui/common/FxmlLoaderFactory.java index c10054ef4..dcff93aab 100644 --- a/src/main/java/org/cryptomator/ui/common/FxmlLoaderFactory.java +++ b/src/main/java/org/cryptomator/ui/common/FxmlLoaderFactory.java @@ -22,6 +22,10 @@ public class FxmlLoaderFactory { this.resourceBundle = resourceBundle; } + public static FxmlLoaderFactory forController(T controller, Function sceneFactory, ResourceBundle resourceBundle) { + return new FxmlLoaderFactory(Map.of(controller.getClass(), () -> controller), sceneFactory, resourceBundle); + } + /** * @return A new FXMLLoader instance */ diff --git a/src/main/java/org/cryptomator/ui/common/UserInteractionLock.java b/src/main/java/org/cryptomator/ui/common/UserInteractionLock.java deleted file mode 100644 index 12c394533..000000000 --- a/src/main/java/org/cryptomator/ui/common/UserInteractionLock.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.cryptomator.ui.common; - -import javafx.application.Platform; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -public class UserInteractionLock> { - - private final Lock lock = new ReentrantLock(); - private final Condition condition = lock.newCondition(); - private final BooleanProperty awaitingInteraction = new SimpleBooleanProperty(); - private final AtomicBoolean interacted = new AtomicBoolean(); - private final AtomicReference state; - - public UserInteractionLock(E initialValue) { - this.state = new AtomicReference<>(initialValue); - } - - public synchronized void reset(E value) { - state.set(value); - interacted.set(false); - } - - public void interacted(E result) { - assert Platform.isFxApplicationThread(); - lock.lock(); - try { - state.set(result); - interacted.set(true); - awaitingInteraction.set(false); - condition.signal(); - } finally { - lock.unlock(); - } - } - - public E awaitInteraction() throws InterruptedException { - assert !Platform.isFxApplicationThread(); - lock.lock(); - try { - Platform.runLater(() -> awaitingInteraction.set(true)); - while (!interacted.get()) { - condition.await(); - } - return state.get(); - } finally { - lock.unlock(); - } - } - - public ReadOnlyBooleanProperty awaitingInteraction() { - return awaitingInteraction; - } - -} diff --git a/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java b/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java index 4a4e43fff..4d09707b9 100644 --- a/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java +++ b/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java @@ -1,5 +1,7 @@ package org.cryptomator.ui.controls; +import org.cryptomator.common.Passphrase; + import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.property.StringProperty; @@ -82,7 +84,7 @@ public class NiceSecurePasswordField extends StackPane { return passwordField.textProperty(); } - public CharSequence getCharacters() { + public Passphrase getCharacters() { return passwordField.getCharacters(); } diff --git a/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java b/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java index 0290f512d..66df79394 100644 --- a/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java +++ b/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java @@ -9,6 +9,7 @@ package org.cryptomator.ui.controls; import com.google.common.base.Strings; +import org.cryptomator.common.Passphrase; import javafx.application.Platform; import javafx.beans.NamedArg; @@ -28,7 +29,6 @@ import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; import javafx.scene.input.TransferMode; -import java.nio.CharBuffer; import java.text.Normalizer; import java.text.Normalizer.Form; import java.util.Arrays; @@ -203,8 +203,8 @@ public class SecurePasswordField extends TextField { * @see #wipe() */ @Override - public CharSequence getCharacters() { - return CharBuffer.wrap(content, 0, length); + public Passphrase getCharacters() { + return new Passphrase(content, 0, length); } /** diff --git a/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java b/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java index bff757b1a..616e7e5e0 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java @@ -26,7 +26,7 @@ abstract class KeyLoadingModule { @Provides @KeyLoading @KeyLoadingScoped - static KeyLoadingStrategy provideKeyLoaderProvider(@KeyLoading Vault vault, Map> strategies) { + static KeyLoadingStrategy provideKeyLoadingStrategy(@KeyLoading Vault vault, Map> strategies) { try { String scheme = vault.getVaultConfigCache().get().getKeyId().getScheme(); var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme)); diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileComponent.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileComponent.java new file mode 100644 index 000000000..a548cd47d --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileComponent.java @@ -0,0 +1,25 @@ +package org.cryptomator.ui.keyloading.masterkeyfile; + +import dagger.Subcomponent; + +import javafx.scene.Scene; +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; + +@ChooseMasterkeyFileScoped +@Subcomponent(modules = {ChooseMasterkeyFileModule.class}) +public interface ChooseMasterkeyFileComponent { + + @ChooseMasterkeyFileScoped + Scene chooseMasterkeyScene(); + + @ChooseMasterkeyFileScoped + CompletableFuture result(); + + @Subcomponent.Builder + interface Builder { + + ChooseMasterkeyFileComponent build(); + } + +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileController.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileController.java new file mode 100644 index 000000000..11cf7bd6b --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileController.java @@ -0,0 +1,57 @@ +package org.cryptomator.ui.keyloading.masterkeyfile; + +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.keyloading.KeyLoading; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javafx.fxml.FXML; +import javafx.stage.FileChooser; +import javafx.stage.Stage; +import javafx.stage.WindowEvent; +import java.io.File; +import java.nio.file.Path; +import java.util.ResourceBundle; +import java.util.concurrent.CompletableFuture; + +@ChooseMasterkeyFileScoped +public class ChooseMasterkeyFileController implements FxController { + + private static final Logger LOG = LoggerFactory.getLogger(ChooseMasterkeyFileController.class); + + private final Stage window; + private final CompletableFuture result; + private final ResourceBundle resourceBundle; + + @Inject + public ChooseMasterkeyFileController(@KeyLoading Stage window, CompletableFuture result, ResourceBundle resourceBundle) { + this.window = window; + this.result = result; + this.resourceBundle = resourceBundle; + this.window.setOnHiding(this::windowClosed); + } + + @FXML + public void cancel() { + window.close(); + } + + private void windowClosed(WindowEvent windowEvent) { + result.cancel(true); + } + + @FXML + public void proceed() { + LOG.trace("proceed()"); + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle(resourceBundle.getString("unlock.chooseMasterkey.filePickerTitle")); + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator")); + File masterkeyFile = fileChooser.showOpenDialog(window); + if (masterkeyFile != null) { + LOG.debug("Chose masterkey file: {}", masterkeyFile); + result.complete(masterkeyFile.toPath()); + } + } + +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java new file mode 100644 index 000000000..21ae2b26c --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java @@ -0,0 +1,29 @@ +package org.cryptomator.ui.keyloading.masterkeyfile; + +import dagger.Module; +import dagger.Provides; +import org.cryptomator.ui.common.DefaultSceneFactory; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlLoaderFactory; + +import javafx.scene.Scene; +import java.nio.file.Path; +import java.util.ResourceBundle; +import java.util.concurrent.CompletableFuture; + +@Module +interface ChooseMasterkeyFileModule { + + @Provides + @ChooseMasterkeyFileScoped + static CompletableFuture provideResult() { + return new CompletableFuture<>(); + } + + @Provides + @ChooseMasterkeyFileScoped + static Scene provideChooseMasterkeyScene(ChooseMasterkeyFileController controller, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { + return FxmlLoaderFactory.forController(controller, sceneFactory, resourceBundle).createScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE); + } + +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileScoped.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileScoped.java new file mode 100644 index 000000000..4bf8c5c24 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileScoped.java @@ -0,0 +1,13 @@ +package org.cryptomator.ui.keyloading.masterkeyfile; + +import javax.inject.Scope; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Scope +@Documented +@Retention(RetentionPolicy.RUNTIME) +@interface ChooseMasterkeyFileScoped { + +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingFinisher.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingFinisher.java deleted file mode 100644 index 44d7ebfb0..000000000 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingFinisher.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.cryptomator.ui.keyloading.masterkeyfile; - -import org.cryptomator.common.keychain.KeychainManager; -import org.cryptomator.common.vaults.Vault; -import org.cryptomator.integrations.keychain.KeychainAccessException; -import org.cryptomator.ui.keyloading.KeyLoading; -import org.cryptomator.ui.keyloading.KeyLoadingScoped; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.inject.Inject; -import javax.inject.Named; -import java.nio.CharBuffer; -import java.util.Arrays; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -@KeyLoadingScoped -class MasterkeyFileLoadingFinisher { - - private static final Logger LOG = LoggerFactory.getLogger(MasterkeyFileLoadingFinisher.class); - - private final Vault vault; - private final Optional storedPassword; - private final AtomicReference enteredPassword; - private final AtomicBoolean shouldSavePassword; - private final KeychainManager keychain; - - @Inject - MasterkeyFileLoadingFinisher(@KeyLoading Vault vault, @Named("savedPassword") Optional storedPassword, AtomicReference enteredPassword, @Named("savePassword") AtomicBoolean shouldSavePassword, KeychainManager keychain) { - this.vault = vault; - this.storedPassword = storedPassword; - this.enteredPassword = enteredPassword; - this.shouldSavePassword = shouldSavePassword; - this.keychain = keychain; - } - - public void cleanup(boolean successfullyUnlocked) { - if (successfullyUnlocked && shouldSavePassword.get()) { - savePasswordToSystemkeychain(); - } - wipePassword(storedPassword.orElse(null)); - wipePassword(enteredPassword.getAndSet(null)); - } - - private void savePasswordToSystemkeychain() { - if (keychain.isSupported()) { - try { - keychain.storePassphrase(vault.getId(), vault.getDisplayName(), CharBuffer.wrap(enteredPassword.get())); - } catch (KeychainAccessException e) { - LOG.error("Failed to store passphrase in system keychain.", e); - } - } - } - - private void wipePassword(char[] pw) { - if (pw != null) { - Arrays.fill(pw, ' '); - } - } -} diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java index 72e902ded..9375b0cff 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java @@ -8,54 +8,17 @@ import dagger.multibindings.StringKey; import org.cryptomator.common.keychain.KeychainManager; import org.cryptomator.common.vaults.Vault; import org.cryptomator.integrations.keychain.KeychainAccessException; -import org.cryptomator.ui.common.FxController; -import org.cryptomator.ui.common.FxControllerKey; -import org.cryptomator.ui.common.FxmlFile; -import org.cryptomator.ui.common.FxmlLoaderFactory; -import org.cryptomator.ui.common.FxmlScene; -import org.cryptomator.ui.common.UserInteractionLock; import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent; import org.cryptomator.ui.keyloading.KeyLoading; import org.cryptomator.ui.keyloading.KeyLoadingScoped; import org.cryptomator.ui.keyloading.KeyLoadingStrategy; -import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Named; -import javafx.scene.Scene; -import javafx.stage.Stage; -import java.nio.file.Path; import java.util.Optional; -import java.util.ResourceBundle; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -@Module(subcomponents = {ForgetPasswordComponent.class}) -public abstract class MasterkeyFileLoadingModule { - - private static final Logger LOG = LoggerFactory.getLogger(MasterkeyFileLoadingModule.class); - - public enum PasswordEntry { - PASSWORD_ENTERED, - CANCELED - } - - public enum MasterkeyFileProvision { - MASTERKEYFILE_PROVIDED, - CANCELED - } - - @Provides - @KeyLoadingScoped - static UserInteractionLock providePasswordEntryLock() { - return new UserInteractionLock<>(null); - } - - @Provides - @KeyLoadingScoped - static UserInteractionLock provideMasterkeyFileProvisionLock() { - return new UserInteractionLock<>(null); - } +@Module(subcomponents = {ForgetPasswordComponent.class, PassphraseEntryComponent.class, ChooseMasterkeyFileComponent.class}) +public interface MasterkeyFileLoadingModule { @Provides @Named("savedPassword") @@ -67,67 +30,12 @@ public abstract class MasterkeyFileLoadingModule { try { return Optional.ofNullable(keychain.loadPassphrase(vault.getId())); } catch (KeychainAccessException e) { - LOG.error("Failed to load entry from system keychain.", e); + LoggerFactory.getLogger(MasterkeyFileLoadingModule.class).error("Failed to load entry from system keychain.", e); return Optional.empty(); } } } - @Provides - @KeyLoadingScoped - static AtomicReference provideUserProvidedMasterkeyPath() { - return new AtomicReference<>(); - } - - @Provides - @KeyLoadingScoped - static AtomicReference providePassword(@Named("savedPassword") Optional storedPassword) { - return new AtomicReference<>(storedPassword.orElse(null)); - } - - @Provides - @Named("savePassword") - @KeyLoadingScoped - static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional storedPassword) { - return new AtomicBoolean(storedPassword.isPresent()); - } - - @Provides - @FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD) - @KeyLoadingScoped - static Scene provideUnlockScene(@KeyLoading FxmlLoaderFactory fxmlLoaders, @KeyLoading Stage window, @KeyLoading Vault v, ResourceBundle resourceBundle) { - var scene = fxmlLoaders.createScene(FxmlFile.UNLOCK_ENTER_PASSWORD); - scene.windowProperty().addListener((prop, oldVal, newVal) -> { - if (window.equals(newVal)) { - window.setTitle(String.format(resourceBundle.getString("unlock.title"), v.getDisplayName())); - } - }); - return scene; - } - - @Provides - @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) - @KeyLoadingScoped - static Scene provideUnlockSelectMasterkeyFileScene(@KeyLoading FxmlLoaderFactory fxmlLoaders, @KeyLoading Stage window, @KeyLoading Vault v, ResourceBundle resourceBundle) { - var scene = fxmlLoaders.createScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE); - scene.windowProperty().addListener((prop, oldVal, newVal) -> { - if (window.equals(newVal)) { - window.setTitle(String.format(resourceBundle.getString("unlock.chooseMasterkey.title"), v.getDisplayName())); - } - }); - return scene; - } - - @Binds - @IntoMap - @FxControllerKey(PassphraseEntryController.class) - abstract FxController bindUnlockController(PassphraseEntryController controller); - - @Binds - @IntoMap - @FxControllerKey(SelectMasterkeyFileController.class) - abstract FxController bindUnlockSelectMasterkeyFileController(SelectMasterkeyFileController controller); - @Binds @IntoMap @KeyLoadingScoped diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java index 1fa7dd986..b4964f9a0 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java @@ -1,32 +1,33 @@ package org.cryptomator.ui.keyloading.masterkeyfile; import com.google.common.base.Preconditions; -import dagger.Lazy; +import org.cryptomator.common.Passphrase; +import org.cryptomator.common.keychain.KeychainManager; import org.cryptomator.common.vaults.Vault; import org.cryptomator.cryptofs.common.BackupHelper; import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.cryptomator.cryptolib.common.MasterkeyFileAccess; +import org.cryptomator.integrations.keychain.KeychainAccessException; import org.cryptomator.ui.common.Animations; -import org.cryptomator.ui.common.FxmlFile; -import org.cryptomator.ui.common.FxmlScene; -import org.cryptomator.ui.common.UserInteractionLock; import org.cryptomator.ui.keyloading.KeyLoading; import org.cryptomator.ui.keyloading.KeyLoadingStrategy; import org.cryptomator.ui.unlock.UnlockCancelledException; import javax.inject.Inject; +import javax.inject.Named; import javafx.application.Platform; -import javafx.scene.Scene; import javafx.stage.Stage; import javafx.stage.Window; import java.io.IOException; import java.net.URI; -import java.nio.CharBuffer; import java.nio.file.Files; import java.nio.file.Path; -import java.util.concurrent.atomic.AtomicReference; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; @KeyLoading public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { @@ -36,28 +37,26 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { private final Vault vault; private final MasterkeyFileAccess masterkeyFileAccess; private final Stage window; - private final Lazy passphraseEntryScene; - private final Lazy selectMasterkeyFileScene; - private final UserInteractionLock passwordEntryLock; - private final UserInteractionLock masterkeyFileProvisionLock; - private final AtomicReference password; - private final AtomicReference filePath; - private final MasterkeyFileLoadingFinisher finisher; + private final PassphraseEntryComponent.Builder passphraseEntry; + private final ChooseMasterkeyFileComponent.Builder masterkeyFileChoice; + private final KeychainManager keychain; + private final ResourceBundle resourceBundle; - private boolean wrongPassword; + private Passphrase passphrase; + private boolean savePassphrase; + private boolean wrongPassphrase; @Inject - public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Stage window, @FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD) Lazy passphraseEntryScene, @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) Lazy selectMasterkeyFileScene, UserInteractionLock passwordEntryLock, UserInteractionLock masterkeyFileProvisionLock, AtomicReference password, AtomicReference filePath, MasterkeyFileLoadingFinisher finisher) { + public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Stage window, @Named("savedPassword") Optional savedPassphrase, PassphraseEntryComponent.Builder passphraseEntry, ChooseMasterkeyFileComponent.Builder masterkeyFileChoice, KeychainManager keychain, ResourceBundle resourceBundle) { this.vault = vault; this.masterkeyFileAccess = masterkeyFileAccess; this.window = window; - this.passphraseEntryScene = passphraseEntryScene; - this.selectMasterkeyFileScene = selectMasterkeyFileScene; - this.passwordEntryLock = passwordEntryLock; - this.masterkeyFileProvisionLock = masterkeyFileProvisionLock; - this.password = password; - this.filePath = filePath; - this.finisher = finisher; + this.passphraseEntry = passphraseEntry; + this.masterkeyFileChoice = masterkeyFileChoice; + this.keychain = keychain; + this.resourceBundle = resourceBundle; + this.passphrase = savedPassphrase.map(Passphrase::new).orElse(null); + this.savePassphrase = savedPassphrase.isPresent(); } @Override @@ -66,9 +65,11 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { try { Path filePath = vault.getPath().resolve(keyId.getSchemeSpecificPart()); if (!Files.exists(filePath)) { - filePath = getAlternateMasterkeyFilePath(); + filePath = askUserForMasterkeyFilePath(); + } + if (passphrase == null) { + askForPassphrase(); } - CharSequence passphrase = getPassphrase(); var masterkey = masterkeyFileAccess.load(filePath, passphrase); //backup if (filePath.startsWith(vault.getPath())) { @@ -90,8 +91,9 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { @Override public boolean recoverFromException(MasterkeyLoadingFailedException exception) { if (exception instanceof InvalidPassphraseException) { - this.wrongPassword = true; - password.set(null); + this.wrongPassphrase = true; + passphrase.destroy(); + this.passphrase = null; return true; // reattempting key load } else { return false; // nothing we can do @@ -100,23 +102,29 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { @Override public void cleanup(boolean unlockedSuccessfully) { - finisher.cleanup(unlockedSuccessfully); - } - - private Path getAlternateMasterkeyFilePath() throws UnlockCancelledException, InterruptedException { - if (filePath.get() == null) { - return switch (askUserForMasterkeyFilePath()) { - case MASTERKEYFILE_PROVIDED -> filePath.get(); - case CANCELED -> throw new UnlockCancelledException("Choosing masterkey file cancelled."); - }; - } else { - return filePath.get(); + if (unlockedSuccessfully && savePassphrase) { + savePasswordToSystemkeychain(passphrase); + } + if (passphrase != null) { + passphrase.destroy(); } } - private MasterkeyFileLoadingModule.MasterkeyFileProvision askUserForMasterkeyFilePath() throws InterruptedException { + private void savePasswordToSystemkeychain(Passphrase passphrase) { + if (keychain.isSupported()) { + try { + keychain.storePassphrase(vault.getId(), vault.getDisplayName(), passphrase); + } catch (KeychainAccessException e) { + LOG.error("Failed to store passphrase in system keychain.", e); + } + } + } + + private Path askUserForMasterkeyFilePath() throws InterruptedException { + var comp = masterkeyFileChoice.build(); Platform.runLater(() -> { - window.setScene(selectMasterkeyFileScene.get()); + window.setScene(comp.chooseMasterkeyScene()); + window.setTitle(resourceBundle.getString("unlock.chooseMasterkey.title").formatted(vault.getDisplayName())); window.show(); Window owner = window.getOwner(); if (owner != null) { @@ -126,24 +134,20 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { window.centerOnScreen(); } }); - return masterkeyFileProvisionLock.awaitInteraction(); - } - - private CharSequence getPassphrase() throws UnlockCancelledException, InterruptedException { - if (password.get() == null) { - return switch (askForPassphrase()) { - case PASSWORD_ENTERED -> CharBuffer.wrap(password.get()); - case CANCELED -> throw new UnlockCancelledException("Password entry cancelled."); - }; - } else { - // e.g. pre-filled from keychain or previous unlock attempt - return CharBuffer.wrap(password.get()); + try { + return comp.result().get(); + } catch (CancellationException e) { + throw new UnlockCancelledException("Choosing masterkey file cancelled."); + } catch (ExecutionException e) { + throw new MasterkeyLoadingFailedException("Failed to select masterkey file.", e); } } - private MasterkeyFileLoadingModule.PasswordEntry askForPassphrase() throws InterruptedException { + private void askForPassphrase() throws InterruptedException { + var comp = passphraseEntry.savedPassword(passphrase).build(); Platform.runLater(() -> { - window.setScene(passphraseEntryScene.get()); + window.setScene(comp.passphraseEntryScene()); + window.setTitle(resourceBundle.getString("unlock.title").formatted(vault.getDisplayName())); window.show(); Window owner = window.getOwner(); if (owner != null) { @@ -152,11 +156,19 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { } else { window.centerOnScreen(); } - if (wrongPassword) { + if (wrongPassphrase) { Animations.createShakeWindowAnimation(window).play(); } }); - return passwordEntryLock.awaitInteraction(); + try { + var result = comp.result().get(); + this.passphrase = result.passphrase(); + this.savePassphrase = result.savePassphrase(); + } catch (CancellationException e) { + throw new UnlockCancelledException("Password entry cancelled."); + } catch (ExecutionException e) { + throw new MasterkeyLoadingFailedException("Failed to ask for password.", e); + } } } diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryComponent.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryComponent.java new file mode 100644 index 000000000..5e072efd0 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryComponent.java @@ -0,0 +1,31 @@ +package org.cryptomator.ui.keyloading.masterkeyfile; + +import dagger.BindsInstance; +import dagger.Subcomponent; +import org.cryptomator.common.Nullable; +import org.cryptomator.common.Passphrase; + +import javax.inject.Named; +import javafx.scene.Scene; +import java.util.concurrent.CompletableFuture; + +@PassphraseEntryScoped +@Subcomponent(modules = {PassphraseEntryModule.class}) +public interface PassphraseEntryComponent { + + @PassphraseEntryScoped + Scene passphraseEntryScene(); + + @PassphraseEntryScoped + CompletableFuture result(); + + @Subcomponent.Builder + interface Builder { + + @BindsInstance + PassphraseEntryComponent.Builder savedPassword(@Nullable @Named("savedPassword") Passphrase savedPassword); + + PassphraseEntryComponent build(); + } + +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java index f6ce79e51..35b1b1903 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java @@ -1,16 +1,14 @@ package org.cryptomator.ui.keyloading.masterkeyfile; +import org.cryptomator.common.Nullable; import org.cryptomator.common.keychain.KeychainManager; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxController; -import org.cryptomator.ui.common.UserInteractionLock; +import org.cryptomator.common.Passphrase; import org.cryptomator.ui.common.WeakBindings; -import org.cryptomator.ui.controls.FontAwesome5IconView; import org.cryptomator.ui.controls.NiceSecurePasswordField; import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent; import org.cryptomator.ui.keyloading.KeyLoading; -import org.cryptomator.ui.keyloading.KeyLoadingScoped; -import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule.PasswordEntry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,8 +19,8 @@ import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; +import javafx.application.Platform; import javafx.beans.binding.Bindings; -import javafx.beans.binding.BooleanBinding; import javafx.beans.binding.ObjectBinding; import javafx.beans.binding.StringBinding; import javafx.beans.property.BooleanProperty; @@ -37,33 +35,27 @@ import javafx.scene.transform.Translate; import javafx.stage.Stage; import javafx.stage.WindowEvent; import javafx.util.Duration; -import java.util.Arrays; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.CompletableFuture; -@KeyLoadingScoped +@PassphraseEntryScoped public class PassphraseEntryController implements FxController { private static final Logger LOG = LoggerFactory.getLogger(PassphraseEntryController.class); private final Stage window; private final Vault vault; - private final AtomicReference password; - private final AtomicBoolean savePassword; - private final Optional savedPassword; - private final UserInteractionLock passwordEntryLock; + private final CompletableFuture result; + private final Passphrase savedPassword; private final ForgetPasswordComponent.Builder forgetPassword; private final KeychainManager keychain; - private final ObjectBinding unlockButtonContentDisplay; - private final BooleanBinding userInteractionDisabled; - private final BooleanProperty unlockButtonDisabled; private final StringBinding vaultName; + private final BooleanProperty unlockInProgress = new SimpleBooleanProperty(); + private final ObjectBinding unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, unlockInProgress); + private final BooleanProperty unlockButtonDisabled = new SimpleBooleanProperty(); /* FXML */ public NiceSecurePasswordField passwordField; public CheckBox savePasswordCheckbox; - public FontAwesome5IconView unlockInProgressView; public ImageView face; public ImageView leftArm; public ImageView rightArm; @@ -72,29 +64,25 @@ public class PassphraseEntryController implements FxController { public Animation unlockAnimation; @Inject - public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, AtomicReference password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional savedPassword, UserInteractionLock passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) { + public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, CompletableFuture result, @Nullable @Named("savedPassword") Passphrase savedPassword, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) { this.window = window; this.vault = vault; - this.password = password; - this.savePassword = savePassword; + this.result = result; this.savedPassword = savedPassword; - this.passwordEntryLock = passwordEntryLock; this.forgetPassword = forgetPassword; this.keychain = keychain; - this.unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, passwordEntryLock.awaitingInteraction()); - this.userInteractionDisabled = passwordEntryLock.awaitingInteraction().not(); - this.unlockButtonDisabled = new SimpleBooleanProperty(); this.vaultName = WeakBindings.bindString(vault.displayNameProperty()); - this.window.setOnHiding(this::windowClosed); + window.setOnHiding(this::windowClosed); + result.whenCompleteAsync((r, t) -> unlockInProgress.set(false), Platform::runLater); } @FXML public void initialize() { - savePasswordCheckbox.setSelected(savedPassword.isPresent()); - if (password.get() != null) { - passwordField.setPassword(password.get()); + if (savedPassword != null) { + savePasswordCheckbox.setSelected(true); + passwordField.setPassword(savedPassword); } - unlockButtonDisabled.bind(userInteractionDisabled.or(passwordField.textProperty().isEmpty())); + unlockButtonDisabled.bind(unlockInProgress.or(passwordField.textProperty().isEmpty())); var leftArmTranslation = new Translate(24, 0); var leftArmRotation = new Rotate(60, 16, 30, 0); @@ -132,7 +120,7 @@ public class PassphraseEntryController implements FxController { new KeyFrame(Duration.millis(1000), faceVisible) // ); - passwordEntryLock.awaitingInteraction().addListener(observable -> stopUnlockAnimation()); + result.whenCompleteAsync((r, t) -> stopUnlockAnimation()); } @FXML @@ -141,26 +129,17 @@ public class PassphraseEntryController implements FxController { } private void windowClosed(WindowEvent windowEvent) { - // if not already interacted, mark this workflow as cancelled: - if (passwordEntryLock.awaitingInteraction().get()) { - LOG.debug("Unlock canceled by user."); - passwordEntryLock.interacted(PasswordEntry.CANCELED); - } + LOG.debug("Unlock canceled by user."); + result.cancel(true); } @FXML public void unlock() { LOG.trace("UnlockController.unlock()"); + unlockInProgress.set(true); CharSequence pwFieldContents = passwordField.getCharacters(); - char[] newPw = new char[pwFieldContents.length()]; - for (int i = 0; i < pwFieldContents.length(); i++) { - newPw[i] = pwFieldContents.charAt(i); - } - char[] oldPw = password.getAndSet(newPw); - if (oldPw != null) { - Arrays.fill(oldPw, ' '); - } - passwordEntryLock.interacted(PasswordEntry.PASSWORD_ENTERED); + Passphrase pw = Passphrase.copyOf(pwFieldContents); + result.complete(new PassphraseEntryResult(pw, savePasswordCheckbox.isSelected())); startUnlockAnimation(); } @@ -184,8 +163,7 @@ public class PassphraseEntryController implements FxController { @FXML private void didClickSavePasswordCheckbox() { - savePassword.set(savePasswordCheckbox.isSelected()); - if (!savePasswordCheckbox.isSelected() && savedPassword.isPresent()) { + if (!savePasswordCheckbox.isSelected() && savedPassword != null) { forgetPassword.vault(vault).owner(window).build().showForgetPassword().thenAccept(forgotten -> savePasswordCheckbox.setSelected(!forgotten)); } } @@ -205,15 +183,15 @@ public class PassphraseEntryController implements FxController { } public ContentDisplay getUnlockButtonContentDisplay() { - return passwordEntryLock.awaitingInteraction().get() ? ContentDisplay.TEXT_ONLY : ContentDisplay.LEFT; + return unlockInProgress.get() ? ContentDisplay.LEFT : ContentDisplay.TEXT_ONLY; } - public BooleanBinding userInteractionDisabledProperty() { - return userInteractionDisabled; + public ReadOnlyBooleanProperty userInteractionDisabledProperty() { + return unlockInProgress; } public boolean isUserInteractionDisabled() { - return userInteractionDisabled.get(); + return unlockInProgress.get(); } public ReadOnlyBooleanProperty unlockButtonDisabledProperty() { @@ -227,4 +205,6 @@ public class PassphraseEntryController implements FxController { public boolean isKeychainAccessAvailable() { return keychain.isSupported(); } + + } diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java new file mode 100644 index 000000000..2c65d440b --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java @@ -0,0 +1,28 @@ +package org.cryptomator.ui.keyloading.masterkeyfile; + +import dagger.Module; +import dagger.Provides; +import org.cryptomator.ui.common.DefaultSceneFactory; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlLoaderFactory; + +import javafx.scene.Scene; +import java.util.ResourceBundle; +import java.util.concurrent.CompletableFuture; + +@Module +interface PassphraseEntryModule { + + @Provides + @PassphraseEntryScoped + static CompletableFuture provideResult() { + return new CompletableFuture<>(); + } + + @Provides + @PassphraseEntryScoped + static Scene provideUnlockScene(PassphraseEntryController controller, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { + return FxmlLoaderFactory.forController(controller, sceneFactory, resourceBundle).createScene(FxmlFile.UNLOCK_ENTER_PASSWORD); + } + +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryResult.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryResult.java new file mode 100644 index 000000000..19057acca --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryResult.java @@ -0,0 +1,8 @@ +package org.cryptomator.ui.keyloading.masterkeyfile; + +import org.cryptomator.common.Passphrase; + +// TODO: change to package-private, as soon as this works for Dagger -.- +public record PassphraseEntryResult(Passphrase passphrase, boolean savePassphrase) { + +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryScoped.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryScoped.java new file mode 100644 index 000000000..a077bcf81 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryScoped.java @@ -0,0 +1,13 @@ +package org.cryptomator.ui.keyloading.masterkeyfile; + +import javax.inject.Scope; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Scope +@Documented +@Retention(RetentionPolicy.RUNTIME) +@interface PassphraseEntryScoped { + +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java deleted file mode 100644 index 39be2b36e..000000000 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.cryptomator.ui.keyloading.masterkeyfile; - -import org.cryptomator.ui.common.FxController; -import org.cryptomator.ui.common.UserInteractionLock; -import org.cryptomator.ui.keyloading.KeyLoading; -import org.cryptomator.ui.keyloading.KeyLoadingScoped; -import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule.MasterkeyFileProvision; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.inject.Inject; -import javafx.fxml.FXML; -import javafx.stage.FileChooser; -import javafx.stage.Stage; -import javafx.stage.WindowEvent; -import java.io.File; -import java.nio.file.Path; -import java.util.ResourceBundle; -import java.util.concurrent.atomic.AtomicReference; - -@KeyLoadingScoped -public class SelectMasterkeyFileController implements FxController { - - private static final Logger LOG = LoggerFactory.getLogger(SelectMasterkeyFileController.class); - - private final Stage window; - private final AtomicReference masterkeyPath; - private final UserInteractionLock masterkeyFileProvisionLock; - private final ResourceBundle resourceBundle; - - @Inject - public SelectMasterkeyFileController(@KeyLoading Stage window, AtomicReference masterkeyPath, UserInteractionLock masterkeyFileProvisionLock, ResourceBundle resourceBundle) { - this.window = window; - this.masterkeyPath = masterkeyPath; - this.masterkeyFileProvisionLock = masterkeyFileProvisionLock; - this.resourceBundle = resourceBundle; - this.window.setOnHiding(this::windowClosed); - } - - @FXML - public void cancel() { - window.close(); - } - - private void windowClosed(WindowEvent windowEvent) { - // if not already interacted, mark this workflow as cancelled: - if (masterkeyFileProvisionLock.awaitingInteraction().get()) { - LOG.debug("Unlock canceled by user."); - masterkeyFileProvisionLock.interacted(MasterkeyFileProvision.CANCELED); - } - } - - @FXML - public void proceed() { - LOG.trace("proceed()"); - FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle(resourceBundle.getString("unlock.chooseMasterkey.filePickerTitle")); - fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator")); - File masterkeyFile = fileChooser.showOpenDialog(window); - if (masterkeyFile != null) { - LOG.debug("Chose masterkey file: {}", masterkeyFile); - masterkeyPath.set(masterkeyFile.toPath()); - masterkeyFileProvisionLock.interacted(MasterkeyFileProvision.MASTERKEYFILE_PROVIDED); - } - } - -} diff --git a/src/main/java/org/cryptomator/ui/lock/LockForcedController.java b/src/main/java/org/cryptomator/ui/lock/LockForcedController.java index c3a452acc..15cf119be 100644 --- a/src/main/java/org/cryptomator/ui/lock/LockForcedController.java +++ b/src/main/java/org/cryptomator/ui/lock/LockForcedController.java @@ -2,56 +2,48 @@ package org.cryptomator.ui.lock; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxController; -import org.cryptomator.ui.common.UserInteractionLock; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import javax.inject.Inject; import javafx.fxml.FXML; import javafx.stage.Stage; import javafx.stage.WindowEvent; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; @LockScoped public class LockForcedController implements FxController { - private static final Logger LOG = LoggerFactory.getLogger(LockForcedController.class); - private final Stage window; private final Vault vault; - private final UserInteractionLock forceLockDecisionLock; + private final AtomicReference> forceRetryDecision; @Inject - public LockForcedController(@LockWindow Stage window, @LockWindow Vault vault, UserInteractionLock forceLockDecisionLock) { + public LockForcedController(@LockWindow Stage window, @LockWindow Vault vault, AtomicReference> forceRetryDecision) { this.window = window; this.vault = vault; - this.forceLockDecisionLock = forceLockDecisionLock; + this.forceRetryDecision = forceRetryDecision; this.window.setOnHiding(this::windowClosed); } @FXML public void cancel() { - forceLockDecisionLock.interacted(LockModule.ForceLockDecision.CANCEL); window.close(); } @FXML public void retry() { - forceLockDecisionLock.interacted(LockModule.ForceLockDecision.RETRY); + forceRetryDecision.get().complete(false); window.close(); } @FXML public void force() { - forceLockDecisionLock.interacted(LockModule.ForceLockDecision.FORCE); + forceRetryDecision.get().complete(true); window.close(); } private void windowClosed(WindowEvent windowEvent) { - // if not already interacted, set the decision to CANCEL - if (forceLockDecisionLock.awaitingInteraction().get()) { - LOG.debug("Lock canceled in force-lock-phase by user."); - forceLockDecisionLock.interacted(LockModule.ForceLockDecision.CANCEL); - } + forceRetryDecision.get().cancel(true); } // ----- Getter & Setter ----- diff --git a/src/main/java/org/cryptomator/ui/lock/LockModule.java b/src/main/java/org/cryptomator/ui/lock/LockModule.java index d1eb5f189..ddee13dff 100644 --- a/src/main/java/org/cryptomator/ui/lock/LockModule.java +++ b/src/main/java/org/cryptomator/ui/lock/LockModule.java @@ -6,13 +6,12 @@ import dagger.Provides; import dagger.multibindings.IntoMap; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.DefaultSceneFactory; -import org.cryptomator.ui.common.FxmlLoaderFactory; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxControllerKey; import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlLoaderFactory; import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.StageFactory; -import org.cryptomator.ui.common.UserInteractionLock; import javax.inject.Named; import javax.inject.Provider; @@ -22,20 +21,16 @@ import javafx.stage.Stage; import java.util.Map; import java.util.Optional; import java.util.ResourceBundle; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; @Module abstract class LockModule { - enum ForceLockDecision { - CANCEL, - RETRY, - FORCE; - } - @Provides @LockScoped - static UserInteractionLock provideForceLockDecisionLock() { - return new UserInteractionLock<>(null); + static AtomicReference> provideForceRetryDecisionRef() { + return new AtomicReference<>(); } @Provides diff --git a/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java b/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java index 00b25c507..1e05ceb73 100644 --- a/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java +++ b/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java @@ -8,7 +8,6 @@ import org.cryptomator.common.vaults.Volume; import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; -import org.cryptomator.ui.common.UserInteractionLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,6 +17,10 @@ import javafx.concurrent.Task; import javafx.scene.Scene; import javafx.stage.Stage; import javafx.stage.Window; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicReference; /** * The sequence of actions performed and checked during lock of a vault. @@ -34,43 +37,48 @@ public class LockWorkflow extends Task { private final Stage lockWindow; private final Vault vault; - private final UserInteractionLock forceLockDecisionLock; + private final AtomicReference> forceRetryDecision; private final Lazy lockForcedScene; private final Lazy lockFailedScene; private final ErrorComponent.Builder errorComponent; @Inject - public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, UserInteractionLock forceLockDecisionLock, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy lockFailedScene, ErrorComponent.Builder errorComponent) { + public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, AtomicReference> forceRetryDecision, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy lockFailedScene, ErrorComponent.Builder errorComponent) { this.lockWindow = lockWindow; this.vault = vault; - this.forceLockDecisionLock = forceLockDecisionLock; + this.forceRetryDecision = forceRetryDecision; this.lockForcedScene = lockForcedScene; this.lockFailedScene = lockFailedScene; this.errorComponent = errorComponent; } @Override - protected Void call() throws Volume.VolumeException, InterruptedException, LockNotCompletedException { + protected Void call() throws Volume.VolumeException, InterruptedException, LockNotCompletedException, ExecutionException { lock(false); return null; } - private void lock(boolean forced) throws InterruptedException { + private void lock(boolean forced) throws InterruptedException, ExecutionException { try { vault.lock(forced); } catch (Volume.VolumeException | LockNotCompletedException e) { LOG.info("Locking {} failed (forced: {}).", vault.getDisplayName(), forced, e); - var decision = askUserForAction(); - switch (decision) { - case RETRY -> lock(false); - case FORCE -> lock(true); - case CANCEL -> cancel(false); - } + retryOrCancel(); } } - private LockModule.ForceLockDecision askUserForAction() throws InterruptedException { - forceLockDecisionLock.reset(null); + private void retryOrCancel() throws ExecutionException, InterruptedException { + try { + boolean forced = askWhetherToUseTheForce().get(); + lock(forced); + } catch (CancellationException e) { + cancel(false); + } + } + + private CompletableFuture askWhetherToUseTheForce() { + var decision = new CompletableFuture(); + forceRetryDecision.set(decision); // show forcedLock dialogue ... Platform.runLater(() -> { lockWindow.setScene(lockForcedScene.get()); @@ -83,8 +91,7 @@ public class LockWorkflow extends Task { lockWindow.centerOnScreen(); } }); - // ... and wait for answer - return forceLockDecisionLock.awaitInteraction(); + return decision; } @Override diff --git a/src/main/java/org/cryptomator/ui/traymenu/TrayMenuBuilder.java b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuBuilder.java index 5a83c77c2..037d6dbe0 100644 --- a/src/main/java/org/cryptomator/ui/traymenu/TrayMenuBuilder.java +++ b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuBuilder.java @@ -83,7 +83,8 @@ public class TrayMenuBuilder { menu.add(new SeparatorItem()); for (Vault vault : vaults) { List submenu = buildSubmenu(vault); - menu.add(new SubMenuItem(vault.getDisplayName(), submenu)); + var label = vault.isUnlocked() ? "* ".concat(vault.getDisplayName()) : vault.getDisplayName(); + menu.add(new SubMenuItem(label, submenu)); } menu.add(new SeparatorItem()); menu.add(new ActionItem(resourceBundle.getString("traymenu.lockAllVaults"), this::lockAllVaults)); diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java index 3be38568d..b7b01fdd6 100644 --- a/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java +++ b/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java @@ -24,6 +24,7 @@ import javafx.stage.DirectoryChooser; import javafx.stage.Stage; import javafx.util.StringConverter; import java.io.File; +import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.util.ResourceBundle; @@ -122,8 +123,11 @@ public class MountOptionsController implements FxController { DirectoryChooser directoryChooser = new DirectoryChooser(); directoryChooser.setTitle(resourceBundle.getString("vaultOptions.mount.mountPoint.directoryPickerTitle")); try { - var initialDir = vault.getVaultSettings().getCustomMountPath().orElse(System.getProperty("user.home")); - directoryChooser.setInitialDirectory(Path.of(initialDir).toFile()); + var initialDir = Path.of(vault.getVaultSettings().getCustomMountPath().orElse(System.getProperty("user.home"))); + + if(Files.exists(initialDir)) { + directoryChooser.setInitialDirectory(initialDir.toFile()); + } } catch (InvalidPathException e) { // no-op } diff --git a/src/main/resources/fxml/unlock_select_masterkeyfile.fxml b/src/main/resources/fxml/unlock_select_masterkeyfile.fxml index b6539f88f..d37289fca 100644 --- a/src/main/resources/fxml/unlock_select_masterkeyfile.fxml +++ b/src/main/resources/fxml/unlock_select_masterkeyfile.fxml @@ -11,7 +11,7 @@