diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml new file mode 100644 index 0000000..267a4dd --- /dev/null +++ b/.github/workflows/build-linux.yml @@ -0,0 +1,116 @@ +name: Java app image Linux + +on: + release: + types: [published] + workflow_dispatch: + inputs: + sem-version: + description: 'Version' + required: true + +permissions: + contents: write + +env: + JAVA_DIST: 'zulu' + JAVA_VERSION: '23.0.1+11' + +defaults: + run: + shell: bash + +jobs: + prepare: + name: Determines the versions strings for the binaries + runs-on: [ubuntu-latest] + outputs: + semVerStr: ${{ steps.determine-version.outputs.version }} + semVerNum: ${{steps.determine-number.outputs.number}} + steps: + - id: determine-version + shell: pwsh + run: | + if ( '${{github.event_name}}' -eq 'release') { + echo 'version=${{ github.event.release.tag_name}}' >> "$env:GITHUB_OUTPUT" + exit 0 + } elseif ('${{inputs.sem-version}}') { + echo 'version=${{ inputs.sem-version}}' >> "$env:GITHUB_OUTPUT" + exit 0 + } + Write-Error "Version neither via input nor by tag specified. Aborting" + exit 1 + - id: determine-number + run: | + SEM_VER_NUM=$(echo "${{ steps.determine-version.outputs.version }}" | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/') + echo "number=${SEM_VER_NUM}" >> "$GITHUB_OUTPUT" + + build-binary: + name: Build java app image + needs: [prepare] + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + architecture: x64 + native-access-lib: 'org.cryptomator.jfuse.linux.amd64' + artifact-name: cryptomator-cli-${{ needs.prepare.outputs.semVerStr }}-linux-x64.zip + - os: [self-hosted, Linux, ARM64] + architecture: aarch64 + native-access-lib: 'org.cryptomator.jfuse.linux.aarch64' + artifact-name: cryptomator-cli-${{ needs.prepare.outputs.semVerStr }}-linux-aarch64.zip + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DIST }} + - name: Set version + run: mvn versions:set -DnewVersion=${{ needs.prepare.outputs.semVerStr }} + - name: Run maven + run: mvn -B clean package -DskipTests + - name: Patch target dir + run: | + cp target/cryptomator-*.jar target/mods + - name: Run jlink + run: | + envsubst < dist/jlink.args > target/jlink.args + "${JAVA_HOME}/bin/jlink" '@./target/jlink.args' + - name: Run jpackage + run: | + envsubst < dist/jpackage.args > target/jpackage.args + "${JAVA_HOME}/bin/jpackage" '@./target/jpackage.args' + env: + JP_APP_VERSION: ${{ needs.prepare.outputs.semVerNum }} + APP_VERSION: ${{ needs.prepare.outputs.semVerStr }} + NATIVE_ACCESS_PACKAGE: ${{ matrix.native-access-lib }} + - name: Update app dir + run: | + cp LICENSE.txt target/cryptomator-cli + cp target/cryptomator-cli_completion.sh target/cryptomator-cli + - uses: actions/upload-artifact@v4 + with: + name: cryptomator-cli-linux-${{ matrix.architecture }} + path: ./target/cryptomator-cli + if-no-files-found: error + - name: Zip binary for release + run: zip -r ./${{ matrix.artifact-name}} ./target/cryptomator-cli + - 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 ./${{ matrix.artifact-name }} + env: + GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} + - name: Publish artefact on GitHub Releases + if: startsWith(github.ref, 'refs/tags/') && github.event.action == 'published' + uses: softprops/action-gh-release@v2 + with: + fail_on_unmatched_files: true + token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} + files: | + ${{ matrix.artifact-name }} + cryptomator-cli-*.asc + diff --git a/.github/workflows/build-mac.yml b/.github/workflows/build-mac.yml new file mode 100644 index 0000000..a751242 --- /dev/null +++ b/.github/workflows/build-mac.yml @@ -0,0 +1,224 @@ +name: Java app image macOS + +on: + release: + types: [published] + workflow_dispatch: + inputs: + sem-version: + description: 'Version' + required: true + notarize: + description: 'Notarize app' + required: false + type: boolean + +permissions: + contents: write + +env: + JAVA_DIST: 'zulu' + JAVA_VERSION: '23.0.1+11' + +defaults: + run: + shell: bash + +jobs: + prepare: + name: Determines the versions strings for the binaries + runs-on: [ubuntu-latest] + outputs: + semVerStr: ${{ steps.determine-version.outputs.version }} + semVerNum: ${{steps.determine-number.outputs.number}} + revisionNum: ${{steps.determine-number.outputs.revision}} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - id: determine-version + shell: pwsh + run: | + if ( '${{github.event_name}}' -eq 'release') { + echo 'version=${{ github.event.release.tag_name}}' >> "$env:GITHUB_OUTPUT" + exit 0 + } elseif ('${{inputs.sem-version}}') { + echo 'version=${{ inputs.sem-version}}' >> "$env:GITHUB_OUTPUT" + exit 0 + } + Write-Error "Version neither via input nor by tag specified. Aborting" + exit 1 + - id: determine-number + run: | + SEM_VER_NUM=$(echo "${{ steps.determine-version.outputs.version }}" | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/') + echo "number=${SEM_VER_NUM}" >> "$GITHUB_OUTPUT" + REVISION_NUM=`git rev-list --count HEAD` + echo "revision=${REVISION_NUM}" >> "$GITHUB_OUTPUT" + + build-binary: + name: Build java app image + needs: [prepare] + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + architecture: arm64 + artifact-name: cryptomator-cli-${{ needs.prepare.outputs.semVerStr }}-mac-arm64.zip + xcode-path: /Applications/Xcode_16.app + - os: macos-13 + architecture: x64 + artifact-name: cryptomator-cli-${{ needs.prepare.outputs.semVerStr }}-mac-x64.zip + xcode-path: /Applications/Xcode_15.2.app + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DIST }} + - name: Set version + run: mvn versions:set -DnewVersion=${{ needs.prepare.outputs.semVerStr }} + - name: Run maven + run: mvn -B clean package -DskipTests + - name: Patch target dir + run: | + cp target/cryptomator-*.jar target/mods + - name: Run jlink + run: | + envsubst < dist/jlink.args > target/jlink.args + "${JAVA_HOME}/bin/jlink" '@./target/jlink.args' + - name: Run jpackage + run: | + envsubst < dist/jpackage.args > target/jpackage.args + "${JAVA_HOME}/bin/jpackage" '@./target/jpackage.args' + env: + JP_APP_VERSION: '1.0.0' # see https://github.com/cryptomator/cli/issues/72 + APP_VERSION: ${{ needs.prepare.outputs.semVerStr }} + NATIVE_ACCESS_PACKAGE: org.cryptomator.jfuse.mac + - name: Patch .app dir + run: | + cp ../LICENSE.txt cryptomator-cli.app/Contents + cp cryptomator-cli_completion.sh cryptomator-cli.app/Contents + sed -i '' "s|###BUNDLE_SHORT_VERSION_STRING###|${VERSION_NO}|g" cryptomator-cli.app/Contents/Info.plist + sed -i '' "s|###BUNDLE_VERSION###|${REVISION_NO}|g" cryptomator-cli.app/Contents/Info.plist + echo -n "$PROVISIONING_PROFILE_BASE64" | base64 --decode -o "cryptomator-cli.app/Contents/embedded.provisionprofile" + working-directory: target + env: + VERSION_NO: ${{ needs.prepare.outputs.semVerNum }} + REVISION_NO: ${{ needs.prepare.outputs.revisionNum }} + PROVISIONING_PROFILE_BASE64: ${{ secrets.MACOS_PROVISIONING_PROFILE_BASE64 }} + - 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 -o $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: | + echo "Codesigning jdk files..." + find cryptomator-cli.app/Contents/runtime/Contents/Home/lib/ -name '*.dylib' -exec codesign --force -s ${CODESIGN_IDENTITY} {} \; + find cryptomator-cli.app/Contents/runtime/Contents/Home/lib/ \( -name 'jspawnhelper' -o -name 'pauseengine' -o -name 'simengine' \) -exec codesign --force -o runtime -s ${CODESIGN_IDENTITY} {} \; + echo "Codesigning jar contents..." + find cryptomator-cli.app/Contents/runtime/Contents/MacOS -name '*.dylib' -exec codesign --force -s ${CODESIGN_IDENTITY} {} \; + for JAR_PATH in `find cryptomator-cli.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-cli.app..." + sed -i '' "s|###APP_IDENTIFIER_PREFIX###|${TEAM_IDENTIFIER}.|g" ../dist/mac/cryptomator-cli.entitlements + sed -i '' "s|###TEAM_IDENTIFIER###|${TEAM_IDENTIFIER}|g" ../dist/mac/cryptomator-cli.entitlements + codesign --force --deep --entitlements ../dist/mac/cryptomator-cli.entitlements -o runtime -s ${CODESIGN_IDENTITY} cryptomator-cli.app + env: + CODESIGN_IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }} + TEAM_IDENTIFIER: ${{ secrets.MACOS_TEAM_IDENTIFIER }} + working-directory: target + # ditto must be used, see https://developer.apple.com/documentation/xcode/packaging-mac-software-for-distribution#Build-a-zip-archive + - name: Zip binary for notarization + if: (startsWith(github.ref, 'refs/tags/') && github.event.action == 'published') || inputs.notarize + run: ditto -c -k --keepParent ./target/cryptomator-cli.app ./${{ matrix.artifact-name}} + - name: Setup Xcode + if: (startsWith(github.ref, 'refs/tags/') && github.event.action == 'published') || inputs.notarize + run: sudo xcode-select -s ${{ matrix.xcode-path}} + shell: bash + #would like to uses cocoalibs/xcode-notarization-action@v1, but blocked due to https://github.com/cocoalibs/xcode-notarization-action/issues/1 + - name: Prepare Notarization Credentials + if: (startsWith(github.ref, 'refs/tags/') && github.event.action == 'published') || inputs.notarize + run: | + # create temporary keychain + KEYCHAIN_PATH=$RUNNER_TEMP/notarization.keychain-db + KEYCHAIN_PASS=$(uuidgen) + security create-keychain -p "${KEYCHAIN_PASS}" ${KEYCHAIN_PATH} + security set-keychain-settings -lut 900 ${KEYCHAIN_PATH} + security unlock-keychain -p "${KEYCHAIN_PASS}" ${KEYCHAIN_PATH} + # import credentials from secrets + xcrun notarytool store-credentials "notary" --apple-id "${{ secrets.MACOS_NOTARIZATION_APPLE_ID }}" --password "${{ secrets.MACOS_NOTARIZATION_PW }}" --team-id "${{ secrets.MACOS_NOTARIZATION_TEAM_ID }}" --keychain "${KEYCHAIN_PATH}" + shell: bash + - name: Notarize + if: (startsWith(github.ref, 'refs/tags/') && github.event.action == 'published') || inputs.notarize + run: | + KEYCHAIN_PATH=$RUNNER_TEMP/notarization.keychain-db + xcrun notarytool submit ${{ matrix.artifact-name }} --keychain-profile "notary" --keychain "${KEYCHAIN_PATH}" --wait + shell: bash + - name: Staple + if: (startsWith(github.ref, 'refs/tags/') && github.event.action == 'published') || inputs.notarize + run: xcrun stapler staple ./target/cryptomator-cli.app + shell: bash + - name: Cleanup + if: ${{ always() }} + run: | + rm -f ./${{ matrix.artifact-name}} + security delete-keychain $RUNNER_TEMP/notarization.keychain-db + shell: bash + continue-on-error: true + - name: Zip app for distribution + run: ditto -c -k --keepParent ./target/cryptomator-cli.app ./${{ matrix.artifact-name}} + - 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 ./${{ matrix.artifact-name }} + env: + GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} + - uses: actions/upload-artifact@v4 + with: + name: cryptomator-cli-mac-${{ matrix.architecture }} + path: | + ${{ matrix.artifact-name}} + *.asc + if-no-files-found: error + - name: Publish artefact on GitHub Releases + if: startsWith(github.ref, 'refs/tags/') && github.event.action == 'published' + uses: softprops/action-gh-release@v2 + with: + fail_on_unmatched_files: true + token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} + files: | + ${{ matrix.artifact-name }} + cryptomator-cli-*.asc \ No newline at end of file diff --git a/.github/workflows/build-win.yml b/.github/workflows/build-win.yml new file mode 100644 index 0000000..56bb483 --- /dev/null +++ b/.github/workflows/build-win.yml @@ -0,0 +1,154 @@ +name: Java app image Windows + +on: + release: + types: [published] + workflow_dispatch: + inputs: + sem-version: + description: 'Version' + required: true + +permissions: + contents: write + +env: + JAVA_DIST: 'zulu' + JAVA_VERSION: '23.0.1+11' + +defaults: + run: + shell: bash + +jobs: + prepare: + name: Determines the versions strings for the binaries + runs-on: [ubuntu-latest] + outputs: + semVerStr: ${{ steps.determine-version.outputs.version }} + semVerNum: ${{steps.determine-number.outputs.number}} + steps: + - id: determine-version + shell: pwsh + run: | + if ( '${{github.event_name}}' -eq 'release') { + echo 'version=${{ github.event.release.tag_name}}' >> "$env:GITHUB_OUTPUT" + exit 0 + } elseif ('${{inputs.sem-version}}') { + echo 'version=${{ inputs.sem-version}}' >> "$env:GITHUB_OUTPUT" + exit 0 + } + Write-Error "Version neither via input nor by tag specified. Aborting" + exit 1 + - id: determine-number + run: | + SEM_VER_NUM=$(echo "${{ steps.determine-version.outputs.version }}" | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/') + echo "number=${SEM_VER_NUM}" >> "$GITHUB_OUTPUT" + + build-binary: + name: Build java app image + needs: [prepare] + runs-on: windows-latest + env: + artifact-name: cryptomator-cli-${{ needs.prepare.outputs.semVerStr }}-win-x64.zip + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DIST }} + - name: Set version + run: mvn versions:set -DnewVersion=${{ needs.prepare.outputs.semVerStr }} + - name: Run maven + run: mvn -B clean package -DskipTests + - name: Patch target dir + run: | + cp LICENSE.txt target + cp target/cryptomator-*.jar target/mods + - name: Run jlink + run: | + envsubst < dist/jlink.args > target/jlink.args + "${JAVA_HOME}/bin/jlink" '@./target/jlink.args' + - name: Run jpackage + run: | + envsubst < dist/jpackage.args > target/jpackage.args + "${JAVA_HOME}/bin/jpackage" '@./target/jpackage.args' --win-console + env: + JP_APP_VERSION: ${{ needs.prepare.outputs.semVerNum }} + APP_VERSION: ${{ needs.prepare.outputs.semVerStr }} + NATIVE_ACCESS_PACKAGE: org.cryptomator.jfuse.win + - name: Update app dir + run: | + cp LICENSE.txt target/cryptomator-cli + cp target/cryptomator-cli_completion.sh target/cryptomator-cli + - name: Fix permissions + run: attrib -r target/cryptomator-cli/cryptomator-cli.exe + shell: pwsh + - name: Extract jars with DLLs for Codesigning + shell: pwsh + run: | + Add-Type -AssemblyName "System.io.compression.filesystem" + $jarFolder = Resolve-Path ".\target\Cryptomator-cli\app\mods" + $jarExtractDir = New-Item -Path ".\target\jar-extract" -ItemType Directory + + #for all jars inspect + Get-ChildItem -Path $jarFolder -Filter "*.jar" | ForEach-Object { + $jar = [Io.compression.zipfile]::OpenRead($_.FullName) + if (@($jar.Entries | Where-Object {$_.Name.ToString().EndsWith(".dll")} | Select-Object -First 1).Count -gt 0) { + #jars containing dlls extract + Set-Location $jarExtractDir + Expand-Archive -Path $_.FullName + } + $jar.Dispose() + } + - name: Codesign + uses: skymatic/code-sign-action@v3 + with: + certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }} + password: ${{ secrets.WIN_CODESIGN_P12_PW }} + certificatesha1: ${{ vars.WIN_CODESIGN_CERT_SHA1 }} + description: Cryptomator + timestampUrl: 'http://timestamp.digicert.com' + folder: target + recursive: true + - name: Replace DLLs inside jars with signed ones + shell: pwsh + run: | + $jarExtractDir = Resolve-Path ".\target\jar-extract" + $jarFolder = Resolve-Path ".\target\cryptomator-cli\app\mods" + Get-ChildItem -Path $jarExtractDir | ForEach-Object { + $jarName = $_.Name + $jarFile = "${jarFolder}\${jarName}.jar" + Set-Location $_ + Get-ChildItem -Path $_ -Recurse -File "*.dll" | ForEach-Object { + # update jar with signed dll + jar --file="$jarFile" --update $(Resolve-Path -Relative -Path $_) + } + } + - name: Zip binary for release + shell: pwsh + run: Compress-Archive -Path .\target\cryptomator-cli -DestinationPath .\${{ env.artifact-name}} + - 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 ./${{ env.artifact-name}} + env: + GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} + - uses: actions/upload-artifact@v4 + with: + name: cryptomator-cli-win-x64 + path: | + ${{ env.artifact-name}} + *.asc + if-no-files-found: error + - name: Publish artefact on GitHub Releases + if: startsWith(github.ref, 'refs/tags/') && github.event.action == 'published' + uses: softprops/action-gh-release@v2 + with: + fail_on_unmatched_files: true + token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} + files: | + ${{ env.artifact-name}} + cryptomator-cli-*.asc + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 27b3f3e..2cb1e2f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,27 +1,22 @@ name: Build on: - [push] + push: + pull_request_target: + types: [labeled] jobs: build: name: Build and Test runs-on: ubuntu-latest - #This check is case insensitive - if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" outputs: artifactVersion: ${{ steps.setversion.outputs.version }} steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: - java-version: 17 - - uses: actions/cache@v1 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- + java-version: '23' + distribution: 'temurin' - name: Ensure to use tagged version run: mvn versions:set --file ./pom.xml -DnewVersion=${GITHUB_REF##*/} # use shell parameter expansion to strip of 'refs/tags' if: startsWith(github.ref, 'refs/tags/') @@ -29,44 +24,18 @@ jobs: id: setversion run: | BUILD_VERSION=$(mvn help:evaluate "-Dexpression=project.version" -q -DforceStdout) - echo "::set-output name=version::${BUILD_VERSION}" + echo "version=${BUILD_VERSION}" >> "$GITHUB_OUTPUT" - name: Build and Test run: mvn -B install - name: Upload artifact cryptomator-cli-${{ steps.setversion.outputs.version }}.jar - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: cryptomator-cli-${{ steps.setversion.outputs.version }}.jar path: target/cryptomator-cli-*.jar - - release: - name: Draft a Release on GitHub Releases and uploads the build artifacts to it - runs-on: ubuntu-latest - needs: build - if: startsWith(github.ref, 'refs/tags/') - steps: - - name: Download cryptomator-cli.jar - uses: actions/download-artifact@v1 - with: - name: cryptomator-cli-${{ needs.build.outputs.artifactVersion }}.jar - path: . - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: ${{ github.ref }} - body: | - :construction: Work in Progress - draft: true - prerelease: false - - name: Upload cryptomator-cli-${{ needs.build.outputs.artifactVersion }}.jar to GitHub Releases - uses: actions/upload-release-asset@v1.0.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Create release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v2 with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: cryptomator-cli-${{ needs.build.outputs.artifactVersion }}.jar - asset_name: cryptomator-cli-${{ needs.build.outputs.artifactVersion }}.jar - asset_content_type: application/jar + token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} + generate_release_notes: true + draft: true \ No newline at end of file diff --git a/.github/workflows/post-publish.yml b/.github/workflows/post-publish.yml new file mode 100644 index 0000000..4f6d9ae --- /dev/null +++ b/.github/workflows/post-publish.yml @@ -0,0 +1,36 @@ +name: Post Release Publish Tasks + +on: + release: + types: [published] + +permissions: + contents: write + +defaults: + run: + shell: bash + +jobs: + get-version: + runs-on: ubuntu-latest + env: + ARCHIVE_NAME: cryptomator-cli-${{ github.event.release.tag_name }}.tar.gz + steps: + - name: Download source tarball + run: | + curl -L -H "Accept: application/vnd.github+json" https://github.com/cryptomator/cli/archive/refs/tags/${{ github.event.release.tag_name }}.tar.gz --output ${{ env.ARCHIVE_NAME }} + - name: Sign source tarball 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 ${{ env.ARCHIVE_NAME }} + env: + GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} + - name: Publish asc on GitHub Releases + uses: softprops/action-gh-release@v2 + with: + fail_on_unmatched_files: true + token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} + files: | + ${{ env.ARCHIVE_NAME }}.asc \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3ef5c20..fbf001b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,10 @@ out/ .idea_modules/ *.iws -*.iml \ No newline at end of file +*.iml + +#mvn shade plugin artifact +dependency-reduced-pom.xml + +# mvn versions:set backup +pom.xml.versionsBackup \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..d58dfb7 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 38d6fa5..0000000 --- a/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM alpine:3.12.0 - -ARG CRYPTOMATOR_CLI_VERSION=0.4.0 - -RUN adduser -D cryptomator && \ - apk add --no-cache openjdk11-jre-headless && \ - wget https://github.com/cryptomator/cli/releases/download/$CRYPTOMATOR_CLI_VERSION/cryptomator-cli-$CRYPTOMATOR_CLI_VERSION.jar -O /usr/bin/cryptomator.jar - -USER cryptomator - -VOLUME ["/vaults"] - -ENTRYPOINT ["java", "-jar", "/usr/bin/cryptomator.jar"] diff --git a/README.md b/README.md index 16c08c2..6502e12 100644 --- a/README.md +++ b/README.md @@ -3,36 +3,63 @@ # Cryptomator CLI -This is a minimal command-line application that unlocks vaults of vault format 8. -After unlocking the vaults, its vault content can be accessed via an embedded WebDAV server. -The minimum required Java version is JDK 17. +This is a minimal command-line application that unlocks a single vault of vault format 8 and mounts it into the system. -## Disclaimer +## Download and Usage -:warning: This project is in an early stage and not ready for production use. We recommend using it only for testing and evaluation purposes. +Download the zip file via [GitHub Releases](https://github.com/cryptomator/cli/releases) and unzip it to your desired directory, e.g. -## Download and Usage +```shell +curl -L https://github.com/cryptomator/cli/releases/download/0.7.0/cryptomator-cli-0.7.0-mac-arm64.zip --output cryptomator-cli.zip +unzip cryptomator-cli.zip +``` -Download the JAR file via [GitHub Releases](https://github.com/cryptomator/cli/releases). +Afterward, you can directly run Cryptomator-CLI by calling the binary from a terminal, e.g. on Linux: +```shell +./cryptomator-cli/cryptomator-cli unlock \ +--password:stdin \ +--mounter=org.cryptomator.frontend.fuse.mount.LinuxFuseMountProvider \ +--mountPoint=/path/to/empty/dir \ +/home/user/myVault +``` +> [!NOTE] +> On macOS, the path to the binary is `cryptomator-cli.app/Contents/MacOS/cryptomator-cli` -Cryptomator CLI requires that at least JDK 17 is present on your system. +To unmount, send a SIGTERM signal to the process, e.g. by pressing CTRL+C (macOS: CMD+C) in the terminal. -```sh -java -jar cryptomator-cli-x.y.z.jar \ - --vault demoVault=/path/to/vault --password demoVault=topSecret \ - --vault otherVault=/path/to/differentVault --passwordfile otherVault=/path/to/fileWithPassword \ - --vault thirdVault=/path/to/thirdVault \ - --bind 127.0.0.1 --port 8080 -# You can now mount http://localhost:8080/demoVault/ -# The password for the third vault is read from stdin -# Be aware that passing the password on the command-line typically makes it visible to anyone on your system! +For a complete list of options, use the `--help` option. +```shell +cryptomator-cli --help ``` ## Filesystem Integration -Once the vault is unlocked and the WebDAV server started, you can access the vault by any WebDAV client or directly mounting it in your filesystem. +To integrate the unlocked vault into the filesystem, cryptomator-cli relies on third party libraries which must be installed separately. +These are: +* [WinFsp](https://winfsp.dev/) for Windows +* [macFUSE](https://osxfuse.github.io/) or [FUSE-T](https://www.fuse-t.org/) for macOS +* and [libfuse](https://github.com/libfuse/libfuse) for Linux/BSD systems (normally provided by a fuse3 package of your distro, e.g. [ubuntu](https://packages.ubuntu.com/noble/fuse3)) + +As a fallback, you can [skip filesystem integration](README.md#skip-filesystem-integration) by using WebDAV. + +### Selecting the Mounter + +To list all available mounters, use the `list-mounters` subcommand: +```shell +cryptomator-cli list-mounters +``` +Pick one from the printed list and use it as input for the `--mounter` option. + +### Skip Filesystem Integration + +If you don't want a direct integration in the OS, choose `org.cryptomator.frontend.webdav.mount.FallbackMounter` for `--mounter`. +It starts a local WebDAV server, where you can access the vault with any WebDAV client or mounting it into your filesystem manually. + +> [!NOTE] +> The WebDAV protocol is supported by all major OSses. Hence, if other mounters fail or show errors when accessing the vault content, you can always use the legacy WebDAV option. +> WebDAV is not the default, because it has a low performance and might have OS dependent restrictions (e.g. maximum file size of 4GB on Windows) -### Windows via Windows Explorer +#### Windows via Windows Explorer Open the File Explorer, right click on "This PC" and click on the menu item "Map network drive...". @@ -40,7 +67,7 @@ Open the File Explorer, right click on "This PC" and click on the menu item "Map 2. In the Folder box, enter the URL logged by the Cryptomator CLI application. 3. Select Finish. -### Linux via davfs2 +#### Linux via davfs2 First, you need to create a mount point for your vault: @@ -61,57 +88,24 @@ To unmount the vault, run: sudo umount /media/your/mounted/folder ``` -### macOS via AppleScript +#### macOS via AppleScript Mount the vault with: - ```sh osascript -e 'mount volume "http://localhost:8080/demoVault/"' ``` -Unmount the vault with: - -```sh -osascript -e 'tell application "Finder" to if "demoVault" exists then eject "demoVault"' -``` - -## Using as a Docker image - -### Bridge Network with Port Forwarding +## Manual Cleanup -:warning: **WARNING: This approach should only be used to test the containerized approach, not in production.** :warning: +If a handle to a resource inside the unlocked vault is still open, a graceful unmount is not possible and cryptomator-cli just terminates without executing possible cleanup tasks. +In that case the message "GRACEFUL UNMOUNT FAILED" is printed to the console/stdout. -The reason is that with port forwarding, you need to listen on all interfaces. Other devices on the network could also access your WebDAV server and potentially expose your secret files. - -Ideally, you would run this in a private Docker network with trusted containers built by yourself communicating with each other. **Again, the below example is for testing purposes only to understand how the container would behave in production.** - -```sh -docker run --rm -p 8080:8080 \ - -v /path/to/vault:/vaults/vault \ - -v /path/to/differentVault:/vaults/differentVault \ - -v /path/to/fileWithPassword:/passwordFile \ - cryptomator/cli \ - --bind 0.0.0.0 --port 8080 \ - --vault demoVault=/vaults/vault --password demoVault=topSecret \ - --vault otherVault=/vaults/differentVault --passwordfile otherVault=/passwordFile -# You can now mount http://localhost:8080/demoVault/ +On a linux OS with the `LinuxFuseMountProvider`, the manual cleanup task is to unmount and free the mountpoint: ``` - -### Host Network - -```sh -docker run --rm --network=host \ - -v /path/to/vault:/vaults/vault \ - -v /path/to/differentVault:/vaults/differentVault \ - -v /path/to/fileWithPassword:/passwordFile \ - cryptomator/cli \ - --bind 127.0.0.1 --port 8080 \ - --vault demoVault=/vaults/vault --password demoVault=topSecret \ - --vault otherVault=/vaults/differentVault --passwordfile otherVault=/passwordFile -# You can now mount http://localhost:8080/demoVault/ +fusermount -u /path/to/former/mountpoint ``` -Then you can access the vault using any WebDAV client. +For other OSs, there is no cleanup necessary. ## License diff --git a/build_linux.sh b/build_linux.sh new file mode 100644 index 0000000..d1be9cc --- /dev/null +++ b/build_linux.sh @@ -0,0 +1,65 @@ +#!/bin/bash +set -euxo pipefail + +echo "Building cryptomator cli..." + +export APP_VERSION='0.1.0-local' + +# Check if Maven is installed +if ! command -v mvn &> /dev/null; then + echo "Maven is not installed. Please install Maven to proceed." + exit 1 +fi + +# Check if JAVA_HOME is set +if [ -z "$JAVA_HOME" ]; then + echo "Environment variable JAVA_HOME not defined" + exit 1 +fi + +# Check Java version +MIN_JAVA_VERSION=$(mvn help:evaluate "-Dexpression=jdk.version" -q -DforceStdout) +JAVA_VERSION=$("$JAVA_HOME/bin/java" -version | head -n1 | cut -d' ' -f2 | cut -d'.' -f1) +if [ "$JAVA_VERSION" -lt "$MIN_JAVA_VERSION" ]; then + echo "Java version $JAVA_VERSION is too old. Minimum required version is $MIN_JAVA_VERSION" + exit 1 +fi + +echo "Building java app with maven..." +mvn -B clean package +cp ./LICENSE.txt ./target/ +mv ./target/cryptomator-cli-*.jar ./target/mods + +echo "Creating JRE with jlink..." +envsubst < dist/jlink.args > target/jlink.args +"$JAVA_HOME/bin/jlink" '@./target/jlink.args' + +if [ $? -ne 0 ] || [ ! -d ./target/runtime ]; then + echo "JRE creation with jlink failed." + exit 1 +fi + +export NATIVE_ACCESS_PACKAGE="no.native.access.available" +_OS=$(uname -s) +if (echo "$_OS" | grep -q "Linux.*") ; then + _ARCH=$(uname -m) + if [ "$_ARCH" = "x86_64" ]; then + NATIVE_ACCESS_PACKAGE="org.cryptomator.jfuse.linux.amd64" + elif [ "$_ARCH" = "aarch64" ]; then + NATIVE_ACCESS_PACKAGE="org.cryptomator.jfuse.linux.aarch64" + else + echo "Warning: Unsupported Linux architecture for FUSE mounter: $_ARCH" + echo "FUSE supported architectures: x86_64, aarch64" + fi +fi + +export JP_APP_VERSION='99.9.9' +envsubst < dist/jpackage.args > target/jpackage.args + +echo "Creating app binary with jpackage..." +"$JAVA_HOME/bin/jpackage" '@./target/jpackage.args' + +if [ $? -ne 0 ] || [ ! -d ./target/cryptomator-cli ]; then + echo "Binary creation with jpackage failed." + exit 1 +fi diff --git a/build_mac.sh b/build_mac.sh new file mode 100644 index 0000000..a46b5c5 --- /dev/null +++ b/build_mac.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +echo "Building cryptomator cli..." + +export APP_VERSION='0.1.0-local' + +# Check if Maven is installed +if ! command -v mvn &> /dev/null; then + echo "Maven is not installed. Please install Maven to proceed." + exit 1 +fi + +# Check if JAVA_HOME is set +if [ -z "$JAVA_HOME" ]; then + echo "Environment variable JAVA_HOME not defined" + exit 1 +fi + +# Check Java version +MIN_JAVA_VERSION=$(mvn help:evaluate "-Dexpression=jdk.version" -q -DforceStdout) +JAVA_VERSION=$("$JAVA_HOME/bin/java" -version | head -n1 | cut -d' ' -f2 | cut -d'.' -f1) +if [ "$JAVA_VERSION" -lt "$MIN_JAVA_VERSION" ]; then + echo "Java version $JAVA_VERSION is too old. Minimum required version is $MIN_JAVA_VERSION" + exit 1 +fi + +echo "Building java app with maven..." +mvn -B clean package +cp ./LICENSE.txt ./target/ +mv ./target/cryptomator-cli-*.jar ./target/mods + +echo "Creating JRE with jlink..." +envsubst < dist/jlink.args > target/jlink.args +"$JAVA_HOME/bin/jlink" '@./target/jlink.args' + +if [ $? -ne 0 ] || [ ! -d ./target/runtime ]; then + echo "JRE creation with jlink failed." + exit 1 +fi + +export JP_APP_VERSION='99.9.9' +export NATIVE_ACCESS_PACKAGE="org.cryptomator.jfuse.mac" +envsubst < dist/jpackage.args > target/jpackage.args + +echo "Creating app binary with jpackage..." +"$JAVA_HOME/bin/jpackage" '@./target/jpackage.args' + +if [ $? -ne 0 ] || [ ! -d ./target/cryptomator-cli.app ]; then + echo "Binary creation with jpackage failed." + exit 1 +fi diff --git a/build_win.ps1 b/build_win.ps1 new file mode 100644 index 0000000..bfdc144 --- /dev/null +++ b/build_win.ps1 @@ -0,0 +1,50 @@ +"Building cryptomator cli..." + +$appVersion='0.1.0-local' + +# Check if maven is installed +$commands = 'mvn' +foreach ($cmd in $commands) { + Invoke-Expression -Command "${cmd} --version" -ErrorAction Stop +} + +# Check if JAVA_HOME is set +if(-not $env:JAVA_HOME) { + throw "Environment variable JAVA_HOME not defined" +} + +# Check Java version +$minJavaVersion=[int]$(mvn help:evaluate "-Dexpression=jdk.version" -q -DforceStdout) +$javaVersion = $(& "$env:JAVA_HOME\bin\java" --version) -split ' ' | Select-Object -Index 1 +if( ([int] ($javaVersion.Split('.') | Select-Object -First 1)) -lt $minJavaVersion) { + throw "Java version $javaVersion is too old. Minimum required version is $minJavaVersion" +} + +Write-Host "Building java app with maven..." +mvn -B clean package +Copy-Item ./LICENSE.txt -Destination ./target -ErrorAction Stop +Move-Item ./target/cryptomator-cli-*.jar ./target/mods -ErrorAction Stop + +Write-Host "Creating JRE with jlink..." +Get-Content -Path './dist/jlink.args' | ForEach-Object { $_.Replace('${JAVA_HOME}', "$env:JAVA_HOME")} | Out-File -FilePath './target/jlink.args' +& $env:JAVA_HOME/bin/jlink `@./target/jlink.args + +if ( ($LASTEXITCODE -ne 0) -or (-not (Test-Path ./target/runtime))) { + throw "JRE creation with jLink failed with exit code $LASTEXITCODE." +} + +## powershell does not have envsubst +$jpAppVersion='99.9.9' +Get-Content -Path './dist/jpackage.args' | ForEach-Object { + $_.Replace('${APP_VERSION}', $appVersion). + Replace('${JP_APP_VERSION}', $jpAppVersion). + Replace('${NATIVE_ACCESS_PACKAGE}', 'org.cryptomator.jfuse.win') +} | Out-File -FilePath './target/jpackage.args' + +# jpackage +Write-Host "Creating app binary with jpackage..." +& $env:JAVA_HOME/bin/jpackage `@./target/jpackage.args --win-console + +if ( ($LASTEXITCODE -ne 0) -or (-not (Test-Path ./target/cryptomator-cli))) { + throw "Binary creation with jpackage failed with exit code $LASTEXITCODE." +} \ No newline at end of file diff --git a/dist/jlink.args b/dist/jlink.args new file mode 100644 index 0000000..c42f03f --- /dev/null +++ b/dist/jlink.args @@ -0,0 +1,9 @@ +--verbose +--module-path "${JAVA_HOME}/jmods" +--output target/runtime +--add-modules java.base,java.compiler,java.naming,java.xml +--strip-native-commands +--no-header-files +--no-man-pages +--strip-debug +--compress zip-6 \ No newline at end of file diff --git a/dist/jpackage.args b/dist/jpackage.args new file mode 100644 index 0000000..599212f --- /dev/null +++ b/dist/jpackage.args @@ -0,0 +1,20 @@ +# Contains three env vars: +# JP_APP_VERSION: The version needed for jpackage. This version _must_ follow the scheme Y.X.X, where Y >= 1 and X >=0 +# APP_VERSION: The actual, semantic version displayed in the cli app +# NATIVE_ACCESS_PACKAGE: The java package containing the fuse bindings for the system +--verbose +--type app-image +--runtime-image target/runtime +--input target/libs +--module-path target/mods +--module org.cryptomator.cli/org.cryptomator.cli.CryptomatorCli +--dest target +--name cryptomator-cli +--vendor "Skymatic GmbH" +--copyright "(C) 2016 - 2024 Skymatic GmbH" +--app-version "${JP_APP_VERSION}" +--java-options "-Dorg.cryptomator.cli.version=${APP_VERSION}" +--java-options "--enable-native-access=${NATIVE_ACCESS_PACKAGE}" +--java-options "-Xss5m" +--java-options "-Xmx256m" +--java-options "-Dfile.encoding=\"utf-8\"" \ No newline at end of file diff --git a/dist/mac/cryptomator-cli.entitlements b/dist/mac/cryptomator-cli.entitlements new file mode 100644 index 0000000..b4939b1 --- /dev/null +++ b/dist/mac/cryptomator-cli.entitlements @@ -0,0 +1,18 @@ + + + + + com.apple.application-identifier + ###APP_IDENTIFIER_PREFIX###org.cryptomator.cli + com.apple.developer.team-identifier + ###TEAM_IDENTIFIER### + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-executable-page-protection + + com.apple.security.cs.disable-library-validation + + + diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..19529dd --- /dev/null +++ b/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..249bdf3 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml index 2efc757..fb3470d 100644 --- a/pom.xml +++ b/pom.xml @@ -1,21 +1,40 @@ - + 4.0.0 org.cryptomator cli - 0.5.1 + 0.6.0 Cryptomator CLI Command line program to access encrypted files via WebDAV. https://github.com/cryptomator/cli - 2.3.1 - 1.2.6 - 1.5.0 - 1.2.9 - 1.3.3 - - 17 UTF-8 + UTF-8 + 23 + + + + org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents + org.cryptomator.cli.CryptomatorCli + + + 2.7.1 + 2.0.7 + 5.0.2 + 1.5.12 + 2.0.16 + + + 24.0.1 + 4.7.6 + + + 3.13.0 + 3.3.0 + 3.6.0 + 3.4.1 @@ -34,6 +53,13 @@ cryptomator.org http://cryptomator.org + + Armin Schrenk + armin.schrenk@skymatic.de + +1 + Skymatic GmbH + https://skymatic.de + @@ -53,14 +79,18 @@ ${fuse-nio.version} - - commons-cli - commons-cli - ${commons.cli.version} + info.picocli + picocli + ${picocli.version} + + org.slf4j + slf4j-api + ${slf4j.version} + ch.qos.logback logback-core @@ -71,47 +101,125 @@ logback-classic ${logback.version} + + org.fusesource.jansi + jansi + 2.4.1 + + cryptomator-cli-${project.version} maven-compiler-plugin - 3.8.1 + ${maven-compiler.version} - ${java.version} - ${java.version} - ${java.version} + ${jdk.version} true + + + info.picocli + picocli-codegen + ${picocli.version} + + + + -Aproject=${project.groupId}/${project.artifactId} + + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar.version} + + + + true + ${mainClass} + + - - maven-assembly-plugin - 3.3.0 + org.apache.maven.plugins + maven-dependency-plugin + - make-assembly + copy-mods + prepare-package + + copy-dependencies + + + runtime + ${project.build.directory}/mods + ${nonModularGroupIds} + + + + copy-libs + prepare-package + + copy-dependencies + + + runtime + ${project.build.directory}/libs + ${nonModularGroupIds} + + + + + + org.codehaus.mojo + exec-maven-plugin + ${maven-exec.version} + + + generate-autocompletion-script package - single + exec - cryptomator-cli-${project.version} - - jar-with-dependencies - - false - - - org.cryptomator.cli.CryptomatorCli - ${project.version} - - + java + + -Dpicocli.autocomplete.systemExitOnError + -cp + + picocli.AutoComplete + --force + --completionScript + ${project.build.directory}/cryptomator-cli_completion.sh + ${mainClass} + + diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java new file mode 100644 index 0000000..734046a --- /dev/null +++ b/src/main/java/module-info.java @@ -0,0 +1,15 @@ +import ch.qos.logback.classic.spi.Configurator; +import org.cryptomator.cli.LogbackConfigurator; + +open module org.cryptomator.cli { + requires org.cryptomator.cryptofs; + requires org.cryptomator.frontend.fuse; + requires info.picocli; + requires org.slf4j; + requires org.cryptomator.integrations.api; + requires org.fusesource.jansi; + requires ch.qos.logback.core; + requires ch.qos.logback.classic; + + provides Configurator with LogbackConfigurator; +} \ No newline at end of file diff --git a/src/main/java/org/cryptomator/cli/Args.java b/src/main/java/org/cryptomator/cli/Args.java deleted file mode 100644 index 492310b..0000000 --- a/src/main/java/org/cryptomator/cli/Args.java +++ /dev/null @@ -1,165 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2016 Sebastian Stenzel and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the accompanying LICENSE.txt. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - *******************************************************************************/ -package org.cryptomator.cli; - -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.HashMap; -import java.util.Map; -import java.util.Properties; -import java.util.Set; -import java.util.stream.Collectors; - -import org.apache.commons.cli.CommandLine; -import org.apache.commons.cli.DefaultParser; -import org.apache.commons.cli.HelpFormatter; -import org.apache.commons.cli.Option; -import org.apache.commons.cli.Options; -import org.apache.commons.cli.ParseException; -import org.cryptomator.cli.pwd.PasswordFromFileStrategy; -import org.cryptomator.cli.pwd.PasswordFromStdInputStrategy; -import org.cryptomator.cli.pwd.PasswordStrategy; -import org.cryptomator.cli.pwd.PasswordFromPropertyStrategy; - -/** - * Parses program arguments. Does not validate them. - */ -public class Args { - - private static final String USAGE = "java -jar cryptomator-cli.jar" // - + " --bind localhost --port 8080" // - + " --vault mySecretVault=/path/to/vault --password mySecretVault=FooBar3000" // - + " --vault myOtherVault=/path/to/other/vault --password myOtherVault=BarFoo4000" // - + " --vault myThirdVault=/path/to/third/vault --passwordfile myThirdVault=/path/to/passwordfile"; - private static final Options OPTIONS = new Options(); - static { - OPTIONS.addOption(Option.builder() // - .longOpt("bind") // - .argName("WebDAV bind address") // - .desc("TCP socket bind address of the WebDAV server. Use 0.0.0.0 to accept all incoming connections.") // - .hasArg() // - .build()); - OPTIONS.addOption(Option.builder() // - .longOpt("port") // - .argName("WebDAV port") // - .desc("TCP port, the WebDAV server should listen on.") // - .hasArg() // - .build()); - OPTIONS.addOption(Option.builder() // - .longOpt("vault") // - .argName("Path of a vault") // - .desc("Format must be vaultName=/path/to/vault") // - .valueSeparator() // - .hasArgs() // - .build()); - OPTIONS.addOption(Option.builder() // - .longOpt("password") // - .argName("Password of a vault") // - .desc("Format must be vaultName=password") // - .valueSeparator() // - .hasArgs() // - .build()); - OPTIONS.addOption(Option.builder() // - .longOpt("passwordfile") // - .argName("Passwordfile for a vault") // - .desc("Format must be vaultName=passwordfile") // - .valueSeparator() // - .hasArgs() // - .build()); - OPTIONS.addOption(Option.builder() // - .longOpt("fusemount") // - .argName("mount point") // - .desc("Format must be vaultName=mountpoint") // - .valueSeparator() // - .hasArgs() // - .build()); - } - - private final String bindAddr; - private final int port; - private final boolean hasValidWebDavConfig; - private final Properties vaultPaths; - private final Properties vaultPasswords; - private final Properties vaultPasswordFiles; - private final Map passwordStrategies; - private final Properties fuseMountPoints; - - public Args(CommandLine commandLine) throws ParseException { - if (commandLine.hasOption("bind") && commandLine.hasOption("port")) { - hasValidWebDavConfig = true; - this.bindAddr = commandLine.getOptionValue("bind", "localhost"); - this.port = Integer.parseInt(commandLine.getOptionValue("port", "0")); - } else { - hasValidWebDavConfig = false; - this.bindAddr = ""; - this.port = -1; - } - this.vaultPaths = commandLine.getOptionProperties("vault"); - this.vaultPasswords = commandLine.getOptionProperties("password"); - this.vaultPasswordFiles = commandLine.getOptionProperties("passwordfile"); - this.passwordStrategies = new HashMap<>(); - this.fuseMountPoints = commandLine.getOptionProperties("fusemount"); - } - - public boolean hasValidWebDavConf() { - return hasValidWebDavConfig; - } - - public String getBindAddr() { - return bindAddr; - } - - public int getPort() { - return port; - } - - public Set getVaultNames() { - return vaultPaths.keySet().stream().map(String.class::cast).collect(Collectors.toSet()); - } - - public String getVaultPath(String vaultName) { - return vaultPaths.getProperty(vaultName); - } - - public static Args parse(String[] arguments) throws ParseException { - CommandLine commandLine = new DefaultParser().parse(OPTIONS, arguments); - return new Args(commandLine); - } - - public static void printUsage() { - new HelpFormatter().printHelp(USAGE, OPTIONS); - } - - public PasswordStrategy addPasswortStrategy(final String vaultName) { - PasswordStrategy passwordStrategy = new PasswordFromStdInputStrategy(vaultName); - - if (vaultPasswords.getProperty(vaultName) != null) { - passwordStrategy = new PasswordFromPropertyStrategy(vaultName, vaultPasswords.getProperty(vaultName)); - } else if (vaultPasswordFiles.getProperty(vaultName) != null) { - passwordStrategy = new PasswordFromFileStrategy(vaultName, - Paths.get(vaultPasswordFiles.getProperty(vaultName))); - } - - this.passwordStrategies.put(vaultName, passwordStrategy); - return passwordStrategy; - } - - public PasswordStrategy getPasswordStrategy(final String vaultName) { - return passwordStrategies.get(vaultName); - } - - public Path getFuseMountPoint(String vaultName) { - String mountPoint = fuseMountPoints.getProperty(vaultName); - if (mountPoint == null) { - return null; - } - Path mountPointPath = Paths.get(mountPoint); - return mountPointPath; - } -} diff --git a/src/main/java/org/cryptomator/cli/CryptomatorCli.java b/src/main/java/org/cryptomator/cli/CryptomatorCli.java index ed2fc48..971a1e8 100644 --- a/src/main/java/org/cryptomator/cli/CryptomatorCli.java +++ b/src/main/java/org/cryptomator/cli/CryptomatorCli.java @@ -1,154 +1,52 @@ -/******************************************************************************* - * Copyright (c) 2016 Sebastian Stenzel and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the accompanying LICENSE.txt. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - *******************************************************************************/ package org.cryptomator.cli; -import org.cryptomator.cli.frontend.FuseMount; -import org.cryptomator.cli.frontend.WebDav; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.Optional; -import java.util.Set; - -import com.google.common.base.Preconditions; -import org.apache.commons.cli.ParseException; -import org.cryptomator.cryptofs.CryptoFileSystemProperties; -import org.cryptomator.cryptofs.CryptoFileSystemProvider; -import org.cryptomator.cryptolib.common.MasterkeyFileAccess; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.ParseResult; +import picocli.CommandLine.RunLast; + +@Command(name = "cryptomator-cli", + mixinStandardHelpOptions = true, + version = "${org.cryptomator.cli.version}", + description = "Unlocks a cryptomator vault and mounts it into the system.", + subcommands = { Unlock.class, ListMounters.class}) public class CryptomatorCli { - private static final Logger LOG = LoggerFactory.getLogger(CryptomatorCli.class); - - private static final byte[] PEPPER = new byte[0]; - private static final String SCHEME = "masterkeyfile"; - - public static void main(String[] rawArgs) throws IOException { - try { - Args args = Args.parse(rawArgs); - validate(args); - startup(args); - } catch (ParseException e) { - LOG.error("Invalid or missing arguments", e); - Args.printUsage(); - } catch (IllegalArgumentException e) { - LOG.error(e.getMessage()); - Args.printUsage(); - } - } - - private static void validate(Args args) throws IllegalArgumentException { - Set vaultNames = args.getVaultNames(); - if (args.hasValidWebDavConf() && (args.getPort() < 0 || args.getPort() > 65536)) { - throw new IllegalArgumentException("Invalid WebDAV Port."); - } - - if (vaultNames.size() == 0) { - throw new IllegalArgumentException("No vault specified."); - } - - for (String vaultName : vaultNames) { - Path vaultPath = Paths.get(args.getVaultPath(vaultName)); - if (!Files.isDirectory(vaultPath)) { - throw new IllegalArgumentException("Not a directory: " + vaultPath); - } - args.addPasswortStrategy(vaultName).validate(); - - Path mountPoint = args.getFuseMountPoint(vaultName); - if (mountPoint != null && !Files.isDirectory(mountPoint)) { - throw new IllegalArgumentException("Fuse mount point does not exist: " + mountPoint); - } - } - } - - private static void startup(Args args) throws IOException { - Optional server = initWebDavServer(args); - ArrayList mounts = new ArrayList<>(); - - SecureRandom secureRandom; - try { - secureRandom = SecureRandom.getInstanceStrong(); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("A strong algorithm must exist in every Java platform.", e); - } - MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(PEPPER, secureRandom); - - for (String vaultName : args.getVaultNames()) { - Path vaultPath = Paths.get(args.getVaultPath(vaultName)); - LOG.info("Unlocking vault \"{}\" located at {}", vaultName, vaultPath); - String vaultPassword = args.getPasswordStrategy(vaultName).password(); - CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties() - .withKeyLoader(keyId -> { - Preconditions.checkArgument(SCHEME.equalsIgnoreCase(keyId.getScheme()), "Only supports keys with scheme " + SCHEME); - Path keyFilePath = vaultPath.resolve(keyId.getSchemeSpecificPart()); - return masterkeyFileAccess.load(keyFilePath, vaultPassword); - }) - .build(); - - Path vaultRoot = CryptoFileSystemProvider.newFileSystem(vaultPath, properties).getPath("/"); - - Path fuseMountPoint = args.getFuseMountPoint(vaultName); - if (fuseMountPoint != null) { - FuseMount newMount = new FuseMount(vaultRoot, fuseMountPoint); - if (newMount.mount()) { - mounts.add(newMount); - } - } - - server.ifPresent(serv -> serv.addServlet(vaultRoot, vaultName)); - } - - waitForShutdown(() -> { - LOG.info("Shutting down..."); - try { - server.ifPresent(serv -> serv.stop()); - - for (FuseMount mount : mounts) { - mount.unmount(); - } - LOG.info("Shutdown successful."); - } catch (Throwable e) { - LOG.error("Error during shutdown", e); - } - }); - } - - private static Optional initWebDavServer(Args args) { - Optional server = Optional.empty(); - if (args.hasValidWebDavConf()) { - server = Optional.of(new WebDav(args.getBindAddr(), args.getPort())); - } - return server; - } - - private static void waitForShutdown(Runnable runnable) { - Runtime.getRuntime().addShutdownHook(new Thread(runnable)); - LOG.info("Press Ctrl+C to terminate."); - - // Block the main thread infinitely as otherwise when using - // Fuse mounts the application quits immediately. - try { - Object mainThreadBlockLock = new Object(); - synchronized (mainThreadBlockLock) { - while (true) { - mainThreadBlockLock.wait(); - } - } - } catch (Exception e) { - LOG.error("Main thread blocking failed."); - } - } + private static final Logger LOG = LoggerFactory.getLogger(CryptomatorCli.class); + + static { + System.getProperties().putIfAbsent("org.cryptomator.cli.version", "SNAPSHOT"); + } + + @Mixin + LoggingMixin loggingMixin; + + private int executionStrategy(ParseResult parseResult) { + if (loggingMixin.isVerbose) { + activateVerboseMode(); + } + return new RunLast().execute(parseResult); // default execution strategy + } + + private void activateVerboseMode() { + var logConfigurator = LogbackConfigurator.INSTANCE.get(); + if (logConfigurator == null) { + throw new IllegalStateException("Logging is not configured."); + } + logConfigurator.switchToDebug(); + LOG.debug("Activated debug logging"); + } + + + public static void main(String... args) { + var app = new CryptomatorCli(); + int exitCode = new CommandLine(app) + .setPosixClusteredShortOptionsAllowed(false) + .setExecutionStrategy(app::executionStrategy) + .execute(args); + System.exit(exitCode); + } } diff --git a/src/main/java/org/cryptomator/cli/ListMounters.java b/src/main/java/org/cryptomator/cli/ListMounters.java new file mode 100644 index 0000000..7f33bbf --- /dev/null +++ b/src/main/java/org/cryptomator/cli/ListMounters.java @@ -0,0 +1,28 @@ +package org.cryptomator.cli; + +import org.cryptomator.integrations.common.IntegrationsLoader; +import org.cryptomator.integrations.mount.MountService; +import picocli.CommandLine; + +import java.util.concurrent.Callable; + +@CommandLine.Command(name = "list-mounters", + headerHeading = "Usage:%n%n", + synopsisHeading = "%n", + descriptionHeading = "%nDescription:%n%n", + optionListHeading = "%nOptions:%n", + header = "Lists available mounters", + description = "Prints a list of available mounters to STDIN. A mounter is is the object to mount/integrate the unlocked vault into the local filesystem. In the GUI app, mounter is named \"volume type\".", + mixinStandardHelpOptions = true) +public class ListMounters implements Callable { + + @CommandLine.Option(names = {"--withDisplayName"}, description = "Prints also the display name of each mounter, as used in the GUI app.") + boolean withDisplayName = false; + + @Override + public Integer call() throws Exception { + IntegrationsLoader.loadAll(MountService.class) + .forEach(s -> System.out.println(s.getClass().getName() + (withDisplayName? " | " + s.displayName():""))); + return 0; + } +} diff --git a/src/main/java/org/cryptomator/cli/LogbackConfigurator.java b/src/main/java/org/cryptomator/cli/LogbackConfigurator.java new file mode 100644 index 0000000..410e0c9 --- /dev/null +++ b/src/main/java/org/cryptomator/cli/LogbackConfigurator.java @@ -0,0 +1,78 @@ +package org.cryptomator.cli; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.encoder.PatternLayoutEncoder; +import ch.qos.logback.classic.spi.Configurator; +import ch.qos.logback.classic.spi.ConfiguratorRank; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.ConsoleAppender; +import ch.qos.logback.core.spi.ContextAwareBase; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +@ConfiguratorRank(ConfiguratorRank.CUSTOM_HIGH_PRIORITY) +public class LogbackConfigurator extends ContextAwareBase implements Configurator { + + public static final AtomicReference INSTANCE = new AtomicReference<>(); + + private static final Map DEFAULT_LOG_LEVELS = Map.of( // + Logger.ROOT_LOGGER_NAME, Level.INFO, // + "org.cryptomator", Level.INFO, // + "org.cryptomator.frontend.fuse.locks", Level.OFF + ); + + private static final Map DEBUG_LOG_LEVELS = Map.of( // + Logger.ROOT_LOGGER_NAME, Level.DEBUG, // + "org.cryptomator", Level.TRACE, // + "org.cryptomator.frontend.fuse.locks", Level.OFF + ); + + @Override + public ExecutionStatus configure(LoggerContext context) { + var encoder = new PatternLayoutEncoder(); + encoder.setContext(context); + encoder.setPattern("[%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg %n"); + encoder.start(); + + var stdout = new ConsoleAppender(); + stdout.setWithJansi(true); + stdout.setContext(context); + stdout.setName("STDOUT"); + stdout.setEncoder(encoder); + stdout.start(); + + // configure loggers: + for (var loglevel : DEFAULT_LOG_LEVELS.entrySet()) { + Logger logger = context.getLogger(loglevel.getKey()); + logger.setLevel(loglevel.getValue()); + logger.setAdditive(false); + logger.addAppender(stdout); + } + + //make instance accessible + INSTANCE.compareAndSet(null, this); + return ExecutionStatus.DO_NOT_INVOKE_NEXT_IF_ANY; + } + + void switchToDebug() { + setLogLevels(DEBUG_LOG_LEVELS); + } + + /** + * Adjust the log levels + * + * @param logLevels new log levels to use + */ + private void setLogLevels(Map logLevels) { + if (context instanceof LoggerContext lc) { + for (var loglevel : logLevels.entrySet()) { + Logger logger = lc.getLogger(loglevel.getKey()); + logger.setLevel(loglevel.getValue()); + } + } + } + +} diff --git a/src/main/java/org/cryptomator/cli/LoggingMixin.java b/src/main/java/org/cryptomator/cli/LoggingMixin.java new file mode 100644 index 0000000..e0baa9e --- /dev/null +++ b/src/main/java/org/cryptomator/cli/LoggingMixin.java @@ -0,0 +1,28 @@ +package org.cryptomator.cli; + +import picocli.CommandLine.Spec; +import picocli.CommandLine.Model; +import picocli.CommandLine.Option; + +public class LoggingMixin { + + @Spec(Spec.Target.MIXEE) + private Model.CommandSpec mixee; + + boolean isVerbose; + + /** + * Sets a verbose logging leven on the LoggingMixin of the top-level command. + * @param isVerbose boolean flag to activate verbose mode + */ + @Option(names = {"-v", "--verbose"}, description = { + "Activate verbose mode"}) + public void setVerbose(boolean isVerbose) { + // Each subcommand that mixes in the LoggingMixin has its own instance + // of this class, so there may be many LoggingMixin instances. + // We want to store the verbosity value in a single, central place, + // so we find the top-level command, + // and store the verbosity level on our top-level command's LoggingMixin. + ((CryptomatorCli) mixee.root().userObject()).loggingMixin.isVerbose = isVerbose; + } +} diff --git a/src/main/java/org/cryptomator/cli/MountSetup.java b/src/main/java/org/cryptomator/cli/MountSetup.java new file mode 100644 index 0000000..d636720 --- /dev/null +++ b/src/main/java/org/cryptomator/cli/MountSetup.java @@ -0,0 +1,128 @@ +package org.cryptomator.cli; + +import org.cryptomator.integrations.common.IntegrationsLoader; +import org.cryptomator.integrations.mount.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine; + +import java.nio.file.FileSystem; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Collectors; + +import static org.cryptomator.integrations.mount.MountCapability.*; + +public class MountSetup { + + private static final Logger LOG = LoggerFactory.getLogger(MountSetup.class); + + @CommandLine.Spec + CommandLine.Model.CommandSpec spec; + + @CommandLine.Option(names = {"--mounter"}, paramLabel = "fully.qualified.ClassName", description = "Name of the mounter to use", required = true) + void setMountService(String value) { + var services = IntegrationsLoader.loadAll(MountService.class).toList(); + var service = services.stream().filter(s -> s.getClass().getName().equals(value)).findFirst(); + if (service.isEmpty()) { + var availableServices = services.stream().map(s -> s.getClass().getName()).collect(Collectors.joining(",")); + var errorMessage = String.format("Invalid value '%s' for option '--mounter': Available mounters are [%s].", value, availableServices); + throw new CommandLine.ParameterException(spec.commandLine(), errorMessage); + } + this.mountService = service.get(); + } + + private MountService mountService; + + @CommandLine.Option(names = {"--mountPoint"}, paramLabel = "/path/to/mount/point", description = "Path to the mount point. Requirements for mount point depend on the chosen mount service") + Optional mountPoint; + + @CommandLine.Option(names = {"--volumeName"}, description = "Name of the virtual volume.") + Optional volumeName; + + @CommandLine.Option(names = {"--volumeId"}, description = "Id of the virtual volume.") + String volumeId = UUID.randomUUID().toString(); + + @CommandLine.Option(names = {"--mountOption", "-mop"}, description = "Additional mount option. For a list of mount options, see the WinFsp, macFUSE, FUSE-T and libfuse documentation.") + List mountOptions = new ArrayList<>(); + + @CommandLine.Option(names = {"--loopbackHostName"}, description = "Name of the loopback address.") + Optional loopbackHostName; + + @CommandLine.Option(names = {"--loopbackPort"}, description = "Port used at the loopback address.") + Optional loopbackPort; + + + MountBuilder prepareMountBuilder(FileSystem fs) { + var specifiedMOPs = listSpecifiedMountOptions(); + var builder = mountService.forFileSystem(fs.getPath("/")); + for (var capability : mountService.capabilities()) { + switch (capability) { + case FILE_SYSTEM_NAME -> builder.setFileSystemName("cryptoFs"); + case LOOPBACK_PORT -> { + loopbackPort.ifPresent(builder::setLoopbackPort); + specifiedMOPs.put(LOOPBACK_PORT, false); + } + case LOOPBACK_HOST_NAME -> { + loopbackHostName.ifPresent(builder::setLoopbackHostName); + specifiedMOPs.put(LOOPBACK_HOST_NAME, false); + } + //TODO: case READ_ONLY -> builder.setReadOnly(vaultSettings.usesReadOnlyMode.get()); + case MOUNT_FLAGS -> { + specifiedMOPs.put(MOUNT_FLAGS, false); + if (mountOptions.isEmpty()) { + var defaultFlags = mountService.getDefaultMountFlags(); + LOG.debug("Using default mount options {}", defaultFlags); + builder.setMountFlags(defaultFlags); + } else { + builder.setMountFlags(String.join(" ", mountOptions)); + } + } + case VOLUME_ID -> { + builder.setVolumeId(volumeId); + } + case VOLUME_NAME -> { + volumeName.ifPresent(builder::setVolumeName); + specifiedMOPs.put(VOLUME_NAME, false); + } + default -> { + //NO-OP + } + } + } + + var ignoredMOPs = specifiedMOPs.entrySet().stream().filter(Map.Entry::getValue).map(e -> e.getKey().name()).collect(Collectors.joining(",")); + if (!ignoredMOPs.isEmpty()) { + LOG.info("Ignoring unsupported options: {}", ignoredMOPs); + } + return builder; + } + + private Map listSpecifiedMountOptions() { + var map = new HashMap(); + loopbackPort.ifPresent(_ -> map.put(LOOPBACK_PORT, true)); + loopbackHostName.ifPresent(_ -> map.put(LOOPBACK_HOST_NAME, true)); + volumeName.ifPresent(_ -> map.put(VOLUME_NAME, true)); + if (!mountOptions.isEmpty()) { + map.put(MOUNT_FLAGS, true); + } + return map; + } + + Mount mount(FileSystem fs) throws MountFailedException { + if (!mountService.hasCapability(MOUNT_TO_SYSTEM_CHOSEN_PATH) && mountPoint.isEmpty()) { + throw new RuntimeException("Unsupported configuration: Mounter %s requires a mount point. Use --mountPoint /path/to/mount/point to specify it.".formatted(mountService.getClass().getName())); + } + + var builder = prepareMountBuilder(fs); + + try { + mountPoint.ifPresent(builder::setMountpoint); + } catch (UnsupportedOperationException e) { + var errorMessage = String.format("Unsupported configuration: Mounter '%s' does not support flag --mountpoint", mountService.getClass().getName()); + throw new RuntimeException(errorMessage); + } + LOG.debug("Mounting vault using {} to {}.", mountService.displayName(), mountPoint.isPresent() ? mountPoint.get() : "system chosen location"); + return builder.mount(); + } +} diff --git a/src/main/java/org/cryptomator/cli/PasswordSource.java b/src/main/java/org/cryptomator/cli/PasswordSource.java new file mode 100644 index 0000000..7c37a1e --- /dev/null +++ b/src/main/java/org/cryptomator/cli/PasswordSource.java @@ -0,0 +1,117 @@ +package org.cryptomator.cli; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; + +public class PasswordSource { + + public static final Logger LOG = LoggerFactory.getLogger(PasswordSource.class); + private static final int MAX_PASSPHRASE_FILE_SIZE = 5_000; //5KB + + @CommandLine.Option(names = {"--password:stdin"}, paramLabel = "Passphrase", description = "Passphrase, read from STDIN", interactive = true) + char[] passphraseStdin = null; + + @CommandLine.Option(names = "--password:env", description = "Name of the environment variable containing the passphrase") + String passphraseEnvironmentVariable = null; + + @CommandLine.Option(names = "--password:file", description = "Path of the file containing the passphrase. The password file must be utf-8 encoded") + Path passphraseFile = null; + + Passphrase readPassphrase() throws IOException { + if (passphraseStdin != null) { + System.out.println("\n"); //otherwise other output might not be clearly separated on the console + return new Passphrase(passphraseStdin); + } else if (passphraseEnvironmentVariable != null) { + return readPassphraseFromEnvironment(); + } else if (passphraseFile != null) { + return readPassphraseFromFile(); + } + throw new IllegalStateException("Passphrase source not specified, but required."); + } + + private Passphrase readPassphraseFromEnvironment() { + LOG.debug("Reading passphrase from env variable '{}'", passphraseEnvironmentVariable); + var tmp = System.getenv(passphraseEnvironmentVariable); + if (tmp == null) { + throw new ReadingEnvironmentVariableFailedException("Environment variable " + passphraseEnvironmentVariable + " is not defined."); + } + char[] result = new char[tmp.length()]; + tmp.getChars(0, tmp.length(), result, 0); + return new Passphrase(result); + } + + private Passphrase readPassphraseFromFile() throws ReadingFileFailedException { + LOG.debug("Reading passphrase from file '{}'.", passphraseFile); + byte[] fileContent = null; + CharBuffer charWrapper = null; + try { + if (Files.size(passphraseFile) > MAX_PASSPHRASE_FILE_SIZE) { + throw new ReadingFileFailedException("Password file is too big. Max supported size is " + MAX_PASSPHRASE_FILE_SIZE + " bytes."); + } + fileContent = Files.readAllBytes(passphraseFile); + charWrapper = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(fileContent)); + //strips newline, since most files on linux end with a new line + var length = charWrapper.limit(); + if(charWrapper.get(length - 1) == '\n') { + length--; + } + char[] content = new char[length]; + charWrapper.get(content); + return new Passphrase(content); + } catch (IOException e) { + throw new ReadingFileFailedException(e); + } finally { + if (fileContent != null) { + Arrays.fill(fileContent, (byte) 0); + } + if (charWrapper != null) { + Arrays.fill(charWrapper.array(), (char) 0x00); + } + } + } + + static class PasswordSourceException extends RuntimeException { + PasswordSourceException(String msg) { + super(msg); + } + + PasswordSourceException(Throwable cause) { + super(cause); + } + } + + static class ReadingFileFailedException extends PasswordSourceException { + ReadingFileFailedException(Throwable e) { + super(e); + + } + + public ReadingFileFailedException(String s) { + super(s); + } + } + + static class ReadingEnvironmentVariableFailedException extends PasswordSourceException { + ReadingEnvironmentVariableFailedException(String msg) { + super(msg); + } + } + + record Passphrase(char[] content) implements AutoCloseable { + + @Override + public void close() { + Arrays.fill(content, (char) 0); + } + } + +} diff --git a/src/main/java/org/cryptomator/cli/Unlock.java b/src/main/java/org/cryptomator/cli/Unlock.java new file mode 100644 index 0000000..798d6c4 --- /dev/null +++ b/src/main/java/org/cryptomator/cli/Unlock.java @@ -0,0 +1,126 @@ +package org.cryptomator.cli; + +import org.cryptomator.cryptofs.CryptoFileSystemProperties; +import org.cryptomator.cryptofs.CryptoFileSystemProvider; +import org.cryptomator.cryptofs.VaultConfig; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; +import org.cryptomator.integrations.mount.Mount; +import org.cryptomator.integrations.mount.UnmountFailedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine; +import picocli.CommandLine.*; + +import java.io.IOException; +import java.net.URI; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.SecureRandom; +import java.util.concurrent.Callable; + +@Command(name = "unlock", + header = "Unlocks a vault", + description = "Unlocks and mounts the given cryptomator vault, identified by the path to the vault directory." + + "The unlocked vault is mounted into the local filesystem by the specified mounter." + + "For a list of available mounters, use the `list-mounters` subcommand.", + parameterListHeading = "%nParameters:%n", + headerHeading = "Usage:%n%n", + synopsisHeading = "%n", + descriptionHeading = "%nDescription:%n%n", + optionListHeading = "%nOptions:%n", + + mixinStandardHelpOptions = true) +public class Unlock implements Callable { + + private static final Logger LOG = LoggerFactory.getLogger(Unlock.class); + private static final byte[] PEPPER = new byte[0]; + private static final String CONFIG_FILE_NAME = "vault.cryptomator"; + private static final String MASTERKEY_FILE_NAME = "masterkey.cryptomator"; + private static final String FORCED_UNMOUNT_MSG = "GRACEFUL UNMOUNT FAILED. Please check if manual cleanups are necessary"; + + @Spec + Model.CommandSpec spec; + @Mixin + LoggingMixin loggingMixin; + + @Parameters(index = "0", paramLabel = "/path/to/vaultDirectory", description = "Path to the vault directory") + Path pathToVault; + + @ArgGroup(multiplicity = "1") + PasswordSource passwordSource; + + @ArgGroup(exclusive = false, multiplicity = "1") + MountSetup mountSetup; + + @Option(names = {"--maxCleartextNameLength"}, description = "Maximum cleartext filename length limit of created files. Remark: If this limit is greater than the shortening threshold, it does not have any effect.") + void setMaxCleartextNameLength(int input) { + if (input <= 0) { + throw new CommandLine.ParameterException(spec.commandLine(), + String.format("Invalid value '%d' for option '--maxCleartextNameLength': " + + "value must be a positive Number between 1 and %d.", input, Integer.MAX_VALUE)); + } + maxCleartextNameLength = input; + } + + private int maxCleartextNameLength = 0; + + private SecureRandom csprng = null; + + @Override + public Integer call() throws Exception { + csprng = SecureRandom.getInstanceStrong(); + + var unverifiedConfig = readConfigFromStorage(pathToVault); + var fsPropsBuilder = CryptoFileSystemProperties.cryptoFileSystemProperties() // + .withKeyLoader(this::loadMasterkey) // + .withShorteningThreshold(unverifiedConfig.allegedShorteningThreshold()); //cryptofs checks, if config is signed with masterkey + if (maxCleartextNameLength > 0) { + fsPropsBuilder.withMaxCleartextNameLength(maxCleartextNameLength); + } + + try (var fs = CryptoFileSystemProvider.newFileSystem(pathToVault, fsPropsBuilder.build()); + var mount = mountSetup.mount(fs)) { + LOG.info("Unlocked and mounted vault successfully to {}", mount.getMountpoint().uri()); + Runtime.getRuntime().addShutdownHook(new Thread(() -> teardown(mount))); + Thread.currentThread().join(); + } + + throw new IllegalStateException("Application exited without error or receiving shutdown signal."); + } + + private void teardown(Mount m) { + try { + m.close(); + } catch (IOException | UnmountFailedException e) { + LOG.error(FORCED_UNMOUNT_MSG, e); + } + } + + private Masterkey loadMasterkey(URI keyId) { + Path filePath = pathToVault.resolve(MASTERKEY_FILE_NAME); + try (var passphraseContainer = passwordSource.readPassphrase()) { + return new MasterkeyFileAccess(PEPPER, csprng) + .load(filePath, CharBuffer.wrap(passphraseContainer.content())); + } catch (IOException e) { + LOG.error("Reading {} failed.", filePath, e); + throw new MasterkeyLoadingFailedException("Unable to load key from file " + filePath + ": " + e.getMessage()); + } + } + + /** + * Attempts to read the vault config file and parse it without verifying its integrity. + * + * @throws IOException if reading the file fails + */ + static VaultConfig.UnverifiedVaultConfig readConfigFromStorage(Path vaultPath) throws IOException { + Path configPath = vaultPath.resolve(CONFIG_FILE_NAME); + LOG.debug("Reading vault config from file {}.", configPath); + String token = Files.readString(configPath, StandardCharsets.US_ASCII); + return VaultConfig.decode(token); + } + +} diff --git a/src/main/java/org/cryptomator/cli/frontend/FuseMount.java b/src/main/java/org/cryptomator/cli/frontend/FuseMount.java deleted file mode 100644 index 40ca15a..0000000 --- a/src/main/java/org/cryptomator/cli/frontend/FuseMount.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.cryptomator.cli.frontend; - -import org.cryptomator.frontend.fuse.mount.EnvironmentVariables; -import org.cryptomator.frontend.fuse.mount.FuseMountException; -import org.cryptomator.frontend.fuse.mount.FuseMountFactory; -import org.cryptomator.frontend.fuse.mount.Mount; -import org.cryptomator.frontend.fuse.mount.Mounter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.file.Path; - -public class FuseMount { - private static final Logger LOG = LoggerFactory.getLogger(FuseMount.class); - - private Path vaultRoot; - private Path mountPoint; - private Mount mnt; - - public FuseMount(Path vaultRoot, Path mountPoint) { - this.vaultRoot = vaultRoot; - this.mountPoint = mountPoint; - this.mnt = null; - } - - public boolean mount() { - if (mnt != null) { - LOG.info("Already mounted to {}", mountPoint); - return false; - } - - try { - Mounter mounter = FuseMountFactory.getMounter(); - EnvironmentVariables envVars = EnvironmentVariables.create() // - .withFlags(mounter.defaultMountFlags()) // - .withFileNameTranscoder(mounter.defaultFileNameTranscoder()) // - .withMountPoint(mountPoint).build(); - mnt = mounter.mount(vaultRoot, envVars); - LOG.info("Mounted to {}", mountPoint); - } catch (FuseMountException e) { - LOG.error("Can't mount: {}, error: {}", mountPoint, e.getMessage()); - return false; - } - return true; - } - - public void unmount() { - try { - mnt.unmount(); - LOG.info("Unmounted {}", mountPoint); - } catch (FuseMountException e) { - LOG.error("Can't unmount gracefully: {}. Force unmount.", e.getMessage()); - forceUnmount(); - } - } - - private void forceUnmount() { - try { - mnt.unmountForced(); - LOG.info("Unmounted {}", mountPoint); - } catch (FuseMountException e) { - LOG.error("Force unmount failed: {}", e.getMessage()); - } - } -} diff --git a/src/main/java/org/cryptomator/cli/frontend/WebDav.java b/src/main/java/org/cryptomator/cli/frontend/WebDav.java deleted file mode 100644 index 9af6a31..0000000 --- a/src/main/java/org/cryptomator/cli/frontend/WebDav.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.cryptomator.cli.frontend; - -import java.nio.file.Path; -import java.util.ArrayList; - -import org.cryptomator.frontend.webdav.WebDavServer; -import org.cryptomator.frontend.webdav.servlet.WebDavServletController; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class WebDav { - private static final Logger LOG = LoggerFactory.getLogger(WebDav.class); - - private final WebDavServer server; - private ArrayList servlets; - - public WebDav(String bindAddr, int port) { - servlets = new ArrayList<>(); - server = WebDavServer.create(); - server.bind(bindAddr, port); - server.start(); - LOG.info("WebDAV server started: {}:{}", bindAddr, port); - } - - public void stop() { - for (WebDavServletController controller : servlets) { - controller.stop(); - } - server.stop(); - } - - public void addServlet(Path vaultRoot, String vaultName) { - WebDavServletController servlet = server.createWebDavServlet(vaultRoot, vaultName); - servlets.add(servlet); - servlet.start(); - } -} diff --git a/src/main/java/org/cryptomator/cli/pwd/PasswordFromFileStrategy.java b/src/main/java/org/cryptomator/cli/pwd/PasswordFromFileStrategy.java deleted file mode 100644 index cd20261..0000000 --- a/src/main/java/org/cryptomator/cli/pwd/PasswordFromFileStrategy.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.cryptomator.cli.pwd; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.stream.Stream; - -public class PasswordFromFileStrategy implements PasswordStrategy { - private static final Logger LOG = LoggerFactory.getLogger(PasswordFromFileStrategy.class); - - private final String vaultName; - private final Path pathToFile; - - public PasswordFromFileStrategy(final String vaultName, final Path pathToFile) { - this.vaultName = vaultName; - this.pathToFile = pathToFile; - } - - @Override - public String password() { - LOG.info("Vault " + "'" + vaultName + "'" + " password from file."); - - if (Files.isReadable(pathToFile) && Files.isRegularFile(pathToFile)) { - try (Stream lines = Files.lines(pathToFile)) { - return lines.findFirst().get().toString(); - } catch (IOException e) { - return null; - } - } - return null; - } - - @Override - public void validate() throws IllegalArgumentException { - if (!Files.isReadable(pathToFile)) { - throw new IllegalArgumentException("Cannot read password from file: " + pathToFile); - } - } - -} diff --git a/src/main/java/org/cryptomator/cli/pwd/PasswordFromPropertyStrategy.java b/src/main/java/org/cryptomator/cli/pwd/PasswordFromPropertyStrategy.java deleted file mode 100644 index 8d5909b..0000000 --- a/src/main/java/org/cryptomator/cli/pwd/PasswordFromPropertyStrategy.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.cryptomator.cli.pwd; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class PasswordFromPropertyStrategy implements PasswordStrategy { - private static final Logger LOG = LoggerFactory.getLogger(PasswordFromPropertyStrategy.class); - - private final String vaultName; - private final String password; - - public PasswordFromPropertyStrategy(final String vaultName, final String password) { - this.vaultName = vaultName; - this.password = password; - } - - @Override - public String password() { - LOG.info("Vault " + "'" + vaultName + "'" + " password from property."); - return this.password; - } - - @Override - public void validate() throws IllegalArgumentException { - if (password.equals("")) { - throw new IllegalArgumentException("Invalid password"); - } - } -} diff --git a/src/main/java/org/cryptomator/cli/pwd/PasswordFromStdInputStrategy.java b/src/main/java/org/cryptomator/cli/pwd/PasswordFromStdInputStrategy.java deleted file mode 100644 index e374e7f..0000000 --- a/src/main/java/org/cryptomator/cli/pwd/PasswordFromStdInputStrategy.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.cryptomator.cli.pwd; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.BufferedReader; -import java.io.Console; -import java.io.IOException; -import java.io.InputStreamReader; - -public class PasswordFromStdInputStrategy implements PasswordStrategy { - private static final Logger LOG = LoggerFactory.getLogger(PasswordFromStdInputStrategy.class); - - private final String vaultName; - private final String inputMessage = "Enter password for vault '%s': "; - - public PasswordFromStdInputStrategy(final String vaultName) { - this.vaultName = vaultName; - } - - @Override - public String password() { - LOG.info("Vault " + "'" + vaultName + "'" + " password from standard input."); - - String password = ""; - Console console = System.console(); - if (console == null) { - LOG.warn("No console: non-interactive mode, instead use insecure replacement, PW is shown!"); - - BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); - System.out.println(String.format(inputMessage, vaultName)); - - try { - password = reader.readLine(); - } catch (IOException e) { - LOG.error("There was an error reading line from console."); - e.printStackTrace(); - } - } else { - System.out.println(String.format(inputMessage, vaultName)); - password = new String(console.readPassword()); - } - - return password; - } - - @Override - public void validate() throws IllegalArgumentException { - if (vaultName.equals("")) { - throw new IllegalArgumentException("Invalid vault name"); - } - } -} diff --git a/src/main/java/org/cryptomator/cli/pwd/PasswordStrategy.java b/src/main/java/org/cryptomator/cli/pwd/PasswordStrategy.java deleted file mode 100644 index adf7c89..0000000 --- a/src/main/java/org/cryptomator/cli/pwd/PasswordStrategy.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.cryptomator.cli.pwd; - -public interface PasswordStrategy { - String password(); - void validate() throws IllegalArgumentException; -} diff --git a/src/main/resources/META-INF/services/ch.qos.logback.classic.spi.Configurator b/src/main/resources/META-INF/services/ch.qos.logback.classic.spi.Configurator new file mode 100644 index 0000000..e7a2757 --- /dev/null +++ b/src/main/resources/META-INF/services/ch.qos.logback.classic.spi.Configurator @@ -0,0 +1 @@ +org.cryptomator.cli.LogbackConfigurator \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml deleted file mode 100644 index a92d372..0000000 --- a/src/main/resources/logback.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - true - - %d{HH:mm:ss.SSS} [%thread] %highlight(%-5level) %logger{36} - %msg%n - - - - - - -