├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.yml │ └── config.yml ├── dependabot.yml └── workflows │ ├── build_and_release_github.yml │ ├── build_develop.yml │ ├── codeql-analysis.init.gradle │ ├── codeql-analysis.yml │ ├── no-response.yml │ └── stale.yml ├── .gitignore ├── .idea ├── gradle.xml ├── icon.svg └── misc.xml ├── LICENSE ├── README.md ├── build.gradle.kts ├── cryptomator-bitwarden.png ├── cryptomator-bitwarden.svg ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src ├── main ├── java │ └── org │ │ └── purejava │ │ └── integrations │ │ └── keychain │ │ └── BitwardenAccess.java └── resources │ └── META-INF │ └── services │ └── org.cryptomator.integrations.keychain.KeychainAccessProvider └── test ├── java └── org │ └── purejava │ └── integrations │ └── keychain │ └── BitwardenAccessTest.java └── resources └── simplelogger.properties /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: purejava 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a report to help us improve the software 3 | labels: ["bug"] 4 | body: 5 | - type: checkboxes 6 | id: terms 7 | attributes: 8 | label: Please agree to the following 9 | options: 10 | - label: I have searched [existing issues](https://github.com/purejava/cryptomator-bitwarden/issues?q=) for duplicates 11 | required: true 12 | - type: input 13 | id: summary 14 | attributes: 15 | label: Summary 16 | placeholder: Please summarize your problem. 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: software-versions 21 | attributes: 22 | label: What software is involved? 23 | description: | 24 | Examples: 25 | - Operating System: Windows 11 26 | - Cryptomator: 1.11.1 27 | - cryptomator-bitwarden-0.4.0-SNAPSHOT.jar 28 | value: | 29 | - Operating System: 30 | - Cryptomator: 31 | - … 32 | validations: 33 | required: true 34 | - type: textarea 35 | id: reproduction-steps 36 | attributes: 37 | label: Steps to Reproduce 38 | value: | 39 | 1. [First Step] 40 | 2. [Second Step] 41 | 3. … 42 | validations: 43 | required: true 44 | - type: textarea 45 | id: expected-behaviour 46 | attributes: 47 | label: Expected Behavior 48 | placeholder: What you expect to happen. 49 | validations: 50 | required: true 51 | - type: textarea 52 | id: actual-behaviour 53 | attributes: 54 | label: Actual Behavior 55 | placeholder: What actually happens. 56 | validations: 57 | required: true 58 | - type: dropdown 59 | id: reproducibility 60 | attributes: 61 | label: Reproducibility 62 | description: How often does the described behaviour occur? 63 | options: 64 | - Always 65 | - Intermittent 66 | - Only once 67 | validations: 68 | required: true 69 | - type: textarea 70 | id: logs 71 | attributes: 72 | label: Relevant Log Output 73 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 74 | render: shell 75 | - type: textarea 76 | id: further-info 77 | attributes: 78 | label: Anything else? 79 | description: Links? References? Screenshots? Configurations? Any data that might be necessary to reproduce the issue? 80 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Documentation 4 | url: https://github.com/purejava/cryptomator-bitwarden/wiki 5 | about: Read the plugin documentation here 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gradle" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | day: "saturday" 8 | time: "06:00" 9 | timezone: "Etc/UTC" 10 | groups: 11 | java-bundles: 12 | patterns: 13 | - "org.junit.jupiter:*" 14 | - "org.slf4j:*" 15 | 16 | - package-ecosystem: "github-actions" 17 | directory: "/" # even for `.github/workflows` 18 | schedule: 19 | interval: "monthly" 20 | groups: 21 | github-actions: 22 | patterns: 23 | - "*" 24 | -------------------------------------------------------------------------------- /.github/workflows/build_and_release_github.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy releases to GitHub 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | if: startsWith(github.ref, 'refs/tags/') 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - uses: actions/setup-java@v4 16 | with: 17 | distribution: 'temurin' 18 | java-version: '20' 19 | 20 | - name: Setup Gradle 21 | uses: gradle/actions/setup-gradle@v4 22 | 23 | - name: Import GPG key 24 | run: | 25 | echo "$GPG_SIGNING_KEY_PW" | gpg --batch --import --yes --passphrase-fd 0 <(echo -n "$GPG_SIGNING_KEY_B64" | base64 --decode) 26 | env: 27 | GPG_SIGNING_KEY_B64: ${{ secrets.GPG_PRIVATE_KEY_B64 }} 28 | GPG_SIGNING_KEY_PW: ${{ secrets.GPG_PASSPHRASE }} 29 | 30 | - name: Setup GPG key information 31 | run: | 32 | mkdir -p ~/.gradle 33 | echo "signing.gnupg.passphrase=${GPG_SIGNING_KEY_PW}" >> ~/.gradle/gradle.properties 34 | env: 35 | GPG_SIGNING_KEY_PW: ${{ secrets.GPG_PASSPHRASE }} 36 | 37 | - name: Build package 38 | run: ./gradlew clean build 39 | env: 40 | PACKAGES_USER: ${{ secrets.PACKAGES_USER }} 41 | PACKAGES_ACCESS_TOKEN: ${{ secrets.PACKAGES_ACCESS_TOKEN }} 42 | 43 | - name: Release package 44 | run: ./gradlew githubRelease 45 | env: 46 | RELEASE_GRADLE_PLUGIN_TOKEN: ${{ secrets.RELEASE_GRADLE_PLUGIN_TOKEN }} 47 | -------------------------------------------------------------------------------- /.github/workflows/build_develop.yml: -------------------------------------------------------------------------------- 1 | name: Java CI with Gradle 2 | 3 | on: 4 | push: 5 | branches: [develop] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - uses: actions/setup-java@v4 15 | with: 16 | distribution: 'temurin' 17 | java-version: '20' 18 | 19 | - name: Setup Gradle 20 | uses: gradle/actions/setup-gradle@v4 21 | 22 | - name: Import GPG key 23 | run: | 24 | echo "$GPG_SIGNING_KEY_PW" | gpg --batch --import --yes --passphrase-fd 0 <(echo -n "$GPG_SIGNING_KEY_B64" | base64 --decode) 25 | env: 26 | GPG_SIGNING_KEY_B64: ${{ secrets.GPG_PRIVATE_KEY_B64 }} 27 | GPG_SIGNING_KEY_PW: ${{ secrets.GPG_PASSPHRASE }} 28 | 29 | - name: Setup GPG key information 30 | run: | 31 | mkdir -p ~/.gradle 32 | echo "signing.gnupg.passphrase=${GPG_SIGNING_KEY_PW}" >> ~/.gradle/gradle.properties 33 | env: 34 | GPG_SIGNING_KEY_PW: ${{ secrets.GPG_PASSPHRASE }} 35 | 36 | - name: Build package 37 | run: ./gradlew clean build 38 | env: 39 | PACKAGES_USER: ${{ secrets.PACKAGES_USER }} 40 | PACKAGES_ACCESS_TOKEN: ${{ secrets.PACKAGES_ACCESS_TOKEN }} 41 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.init.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | allprojects { 18 | tasks.withType(JavaCompile).configureEach { 19 | outputs.doNotCacheIf("CodeQL scanning", { true }) 20 | } 21 | } -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [develop] 6 | pull_request: 7 | branches: [develop] 8 | schedule: 9 | - cron: '34 10 * * 4' 10 | 11 | permissions: {} 12 | 13 | jobs: 14 | CodeQL-Build: 15 | permissions: 16 | actions: read # for github/codeql-action/init to get workflow details 17 | contents: read # for actions/checkout to fetch code 18 | security-events: write # for github/codeql-action/analyze to upload SARIF results 19 | runs-on: ubuntu-latest 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | # Override automatic language detection by changing the below list 25 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 26 | language: ['java', 'javascript'] 27 | # Learn more... 28 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 29 | 30 | steps: 31 | - name: Checkout repository 32 | uses: actions/checkout@v4 33 | # Checkout must run before the caching key is computed using the `hashFiles` method 34 | 35 | - name: Cache Gradle Modules 36 | uses: actions/cache@v4 37 | with: 38 | path: | 39 | ~/.gradle/caches/modules-2/ 40 | ~/.gradle/caches/build-cache-1/ 41 | ~/.gradle/caches/signatures/ 42 | ~/.gradle/caches/keyrings/ 43 | key: ${{ runner.os }}-gradle-cache-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }} 44 | if: ${{ matrix.language == 'java' }} 45 | 46 | - name: Disable checksum offloading 47 | # See: https://github.com/actions/virtual-environments/issues/1187#issuecomment-686735760 48 | run: sudo ethtool -K eth0 tx off rx off 49 | 50 | # Install and setup JDK 20 51 | - name: Setup JDK 20 52 | uses: actions/setup-java@v4 53 | with: 54 | distribution: temurin 55 | java-version: 20 56 | 57 | # Initializes the CodeQL tools for scanning. 58 | - name: Initialize CodeQL 59 | uses: github/codeql-action/init@v3 60 | with: 61 | languages: ${{ matrix.language }} 62 | tools: latest 63 | # If you wish to specify custom queries, you can do so here or in a config file. 64 | # By default, queries listed here will override any specified in a config file. 65 | # Prefix the list here with "+" to use these queries and those in the config file. 66 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 67 | 68 | - name: Compile with Gradle with Build Scan 69 | if: ${{ matrix.language == 'java' && github.repository_owner == 'gradle' }} 70 | run: ./gradlew --init-script .github/workflows/codeql-analysis.init.gradle -DcacheNode=us -S testClasses -Dhttp.keepAlive=false 71 | env: 72 | # Set the DEVELOCITY_ACCESS_KEY so that Gradle Build Scans are generated 73 | DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} 74 | # Potential stop-gap solution for ReadTimeout issues with the Gradle Build Cache 75 | # https://gradle.slack.com/archives/CHDLT99C6/p1636477584059200 76 | GRADLE_OPTS: -Dhttp.keepAlive=false 77 | ORG_GRADLE_PROJECT_signingKey: ${{ secrets.GPG_PRIVATE_KEY }} 78 | ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.GPG_PRIVATE_KEY_ID }} 79 | ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.GPG_PASSPHRASE }} 80 | 81 | - name: Compile with Gradle without Build Scan 82 | if: ${{ matrix.language == 'java' && github.repository_owner != 'gradle' }} 83 | run: ./gradlew --init-script .github/workflows/codeql-analysis.init.gradle -S testClasses 84 | env: 85 | PACKAGES_USER: ${{ secrets.PACKAGES_USER }} 86 | PACKAGES_ACCESS_TOKEN: ${{ secrets.PACKAGES_ACCESS_TOKEN }} 87 | 88 | - name: Cleanup Gradle Daemons 89 | run: ./gradlew --stop 90 | if: ${{ matrix.language == 'java' }} 91 | 92 | # ℹ️ Command-line programs to run using the OS shell. 93 | # 📚 https://git.io/JvXDl 94 | 95 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 96 | # and modify them (or add more) to build your code if your project 97 | # uses a compiled language 98 | 99 | #- run: | 100 | # make bootstrap 101 | # make release 102 | 103 | - name: Perform CodeQL Analysis 104 | uses: github/codeql-action/analyze@v3 105 | with: 106 | config-file: ./.github/codeql/codeql-config.yml 107 | 108 | - name: Cleanup Gradle Cache 109 | # Cleans up the Gradle caches before being cached 110 | run: | 111 | rm -f ~/.gradle/caches/modules-2/modules-2.lock 112 | rm -f ~/.gradle/caches/modules-2/gc.properties 113 | if: ${{ matrix.language == 'java' }} 114 | -------------------------------------------------------------------------------- /.github/workflows/no-response.yml: -------------------------------------------------------------------------------- 1 | # Configuration for close-stale-issues - https://github.com/marketplace/actions/close-stale-issues 2 | 3 | name: 'Close awaiting response issues' 4 | on: 5 | schedule: 6 | - cron: '00 06 * * *' 7 | 8 | jobs: 9 | no-response: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | issues: write 13 | pull-requests: write 14 | steps: 15 | - uses: actions/stale@v9 16 | with: 17 | days-before-stale: 14 18 | days-before-close: 0 19 | days-before-pr-close: -1 20 | stale-issue-label: 'stale' 21 | close-issue-message: "This issue has been automatically closed because the original author has not responded to our request for more information. With the information currently in the issue, we don't have enough information to take action. Please contact us if you have or find the answers we need so that we can investigate further." 22 | only-labels: 'more-information-needed' 23 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for close-stale-issues - https://github.com/marketplace/actions/close-stale-issues 2 | 3 | name: 'Close stale issues' 4 | on: 5 | schedule: 6 | - cron: '00 09 * * *' 7 | 8 | jobs: 9 | stale: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | issues: write 13 | pull-requests: write 14 | steps: 15 | - uses: actions/stale@v9 16 | with: 17 | days-before-stale: 30 18 | days-before-close: 7 19 | exempt-issue-labels: 'security-issue,feature-request,upstream-bug,more-information-needed,blocked,confirmed' 20 | exempt-all-milestones: true 21 | stale-issue-label: 'state:stale' 22 | stale-pr-label: 'state:stale' 23 | stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' 24 | stale-pr-message: 'This PR has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.jar 3 | 4 | # Gradle 5 | .gradle 6 | build/ 7 | !gradle/wrapper/gradle-wrapper.jar 8 | !**/src/main/**/build/ 9 | !**/src/test/**/build/ 10 | 11 | # IntelliJ Settings Files (https://intellij-support.jetbrains.com/hc/en-us/articles/206544839-How-to-manage-projects-under-Version-Control-Systems) # 12 | .idea/**/workspace.xml 13 | .idea/**/tasks.xml 14 | .idea/dictionaries 15 | .idea/**/libraries/ 16 | .idea/.name 17 | .idea/encodings.xml 18 | .idea/compiler.xml 19 | .idea/jarRepositories.xml 20 | *.iml 21 | 22 | # macOS 23 | .DS_Store 24 | 25 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 26 | hs_err_pid* 27 | replay_pid* -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 19 | 37 | 44 | 50 | 51 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-2025 Ralph Plawetzki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cryptomator-bitwarden 2 | ![cryptomator-bitwarden](cryptomator-bitwarden.png) 3 | 4 | [![Java CI with Gradle](https://github.com/purejava/cryptomator-bitwarden/actions/workflows/build_develop.yml/badge.svg)](https://github.com/purejava/cryptomator-bitwarden/actions/workflows/build_develop.yml) 5 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/a5c7592b9a9d415fbe648c85c1c0a3e5)](https://app.codacy.com/gh/purejava/cryptomator-bitwarden/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) 6 | [![GitHub Release](https://img.shields.io/github/v/release/purejava/cryptomator-bitwarden)](https://github.com/purejava/cryptomator-bitwarden/releases) 7 | [![License](https://img.shields.io/github/license/purejava/cryptomator-bitwarden.svg)](https://github.com/purejava/cryptomator-bitwarden/blob/master/LICENSE) 8 | [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/donate?hosted_button_id=XVX9ZM7WE4ANL) 9 | 10 | Plug-in for Cryptomator to store vault passwords in Bitwarden Secrets Manager 11 | 12 | # Documentation 13 | For documentation please take a look at the [Wiki](https://github.com/purejava/cryptomator-bitwarden/wiki). 14 | 15 | # Donation 16 | If you like this project, you can give me a cup of coffee :) 17 | 18 | [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/donate?hosted_button_id=XVX9ZM7WE4ANL) 19 | 20 | # Thank you 21 | Thanks to Luka Potkonjak, who added a Java wrapper to the Bitwarden native C library for the SDK and exposed its commands through a BitwardenClient class. 22 | 23 | # Copyright 24 | Copyright (C) 2024-2025 Ralph Plawetzki 25 | 26 | The Shield Minimalistic SVG Vector logo is made by [Solar Icons](https://www.svgrepo.com/author/Solar%20Icons/) and is published under the [CC Attribution License](https://creativecommons.org/licenses/by/4.0/?ref=chooser-v1) (CC BY 4.0 Deed) 27 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.gradleup.shadow") version "8.3.6" 3 | id("com.github.breadmoirai.github-release") version "2.5.2" 4 | id("com.palantir.git-version") version "3.3.0" 5 | id("java") 6 | id("java-library") 7 | id("signing") 8 | } 9 | 10 | val gitHubPackagesUser: String = System.getenv("PACKAGES_USER") ?: "" 11 | val gitHubPackagesToken: String = System.getenv("PACKAGES_ACCESS_TOKEN") ?: "" 12 | val releaseGradlePluginToken: String = System.getenv("RELEASE_GRADLE_PLUGIN_TOKEN") ?: "" 13 | 14 | repositories { 15 | mavenCentral() 16 | mavenLocal() 17 | maven { 18 | name = "GitHubPackages" 19 | url = uri("https://maven.pkg.github.com/bitwarden/sdk") 20 | credentials { 21 | username = gitHubPackagesUser 22 | password = gitHubPackagesToken 23 | } 24 | } 25 | maven { 26 | url = uri("https://repo.maven.apache.org/maven2/") 27 | } 28 | } 29 | 30 | dependencies { 31 | api(libs.org.cryptomator.integrations.api) 32 | api(libs.com.bitwarden.sdk.secrets) 33 | api(libs.org.slf4j.slf4j.api) 34 | testImplementation(libs.org.slf4j.slf4j.simple) 35 | testImplementation(libs.org.junit.jupiter.junit.jupiter.api) 36 | testImplementation(libs.org.junit.jupiter.junit.jupiter.engine) 37 | testImplementation(libs.org.junit.jupiter.junit.jupiter) 38 | testRuntimeOnly(libs.org.junit.platform.junit.platform.launcher) 39 | } 40 | 41 | group = "org.purejava" 42 | val gitVersion: groovy.lang.Closure by extra 43 | version = gitVersion() // version set by the plugin, based on the Git tag 44 | 45 | java { 46 | sourceCompatibility = JavaVersion.VERSION_20 47 | withSourcesJar() 48 | withJavadocJar() 49 | } 50 | 51 | tasks.test { 52 | useJUnitPlatform() 53 | filter { 54 | includeTestsMatching("BitwardenAccessTest") 55 | } 56 | } 57 | 58 | // Optional publishing section commented out 59 | /* 60 | publishing { 61 | publications { 62 | create("mavenJava") { 63 | from(components["java"]) 64 | pom { 65 | name.set("cryptomator-bitwarden") 66 | description.set("Plug-in for Cryptomator to store vault passwords in Bitwarden") 67 | url.set("https://github.com/purejava/cryptomator-bitwarden") 68 | licenses { 69 | license { 70 | name.set("MIT License") 71 | url.set("https://opensource.org/licenses/MIT") 72 | } 73 | } 74 | developers { 75 | developer { 76 | id.set("purejava") 77 | name.set("Ralph Plawetzki") 78 | email.set("ralph@purejava.org") 79 | } 80 | } 81 | scm { 82 | connection.set("scm:git:git://github.com/purejava/cryptomator-bitwarden.git") 83 | developerConnection.set("scm:git:ssh://github.com/purejava/cryptomator-bitwarden.git") 84 | url.set("https://github.com/purejava/cryptomator-bitwarden/tree/develop") 85 | } 86 | issueManagement { 87 | system.set("GitHub Issues") 88 | url.set("https://github.com/purejava/cryptomator-bitwarden/issues") 89 | } 90 | } 91 | } 92 | } 93 | } 94 | */ 95 | 96 | tasks.named("shadowJar") { 97 | archiveClassifier.set("") 98 | } 99 | 100 | tasks.named("githubRelease") { 101 | dependsOn("sourcesJar") 102 | dependsOn("javadocJar") 103 | dependsOn("signArchives") 104 | } 105 | 106 | artifacts { 107 | archives(tasks.named("shadowJar")) 108 | } 109 | 110 | signing { 111 | useGpgCmd() 112 | sign(configurations.archives.get()) 113 | } 114 | 115 | githubRelease { 116 | token(releaseGradlePluginToken) 117 | tagName = project.version.toString() 118 | releaseName = project.version.toString() 119 | targetCommitish = "develop" 120 | draft = true 121 | body = """ 122 | [![Downloads](https://img.shields.io/github/downloads/purejava/cryptomator-bitwarden/latest/cryptomator-bitwarden-${project.version}.jar)](https://github.com/purejava/cryptomator-bitwarden/releases/latest/download/cryptomator-bitwarden-${project.version}.jar) 123 | 124 | - xxx 125 | """.trimIndent() 126 | generateReleaseNotes = true 127 | releaseAssets.from( 128 | fileTree("${layout.buildDirectory.get()}/libs") { 129 | include( 130 | "cryptomator-bitwarden-${version}.jar", 131 | "cryptomator-bitwarden-${version}.jar.asc", 132 | ) 133 | } 134 | ) 135 | } 136 | 137 | tasks.withType { 138 | options.encoding = "UTF-8" 139 | } 140 | 141 | tasks.withType { 142 | if (JavaVersion.current().isJava9Compatible) { 143 | (options as StandardJavadocDocletOptions).addBooleanOption("html5", true) 144 | } 145 | (options as StandardJavadocDocletOptions).encoding = "UTF-8" 146 | } 147 | -------------------------------------------------------------------------------- /cryptomator-bitwarden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purejava/cryptomator-bitwarden/a02659d5baa2d26609a8b964b4bfca9615130c1e/cryptomator-bitwarden.png -------------------------------------------------------------------------------- /cryptomator-bitwarden.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # This file was generated by the Gradle 'init' task. 2 | # https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties 3 | 4 | org.gradle.configuration-cache=false 5 | org.gradle.parallel=true 6 | org.gradle.caching=true 7 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | # This file was generated by the Gradle 'init' task. 2 | # https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format 3 | 4 | [versions] 5 | com-bitwarden-sdk-secrets = "1.0.1" 6 | org-cryptomator-integrations-api = "1.6.0" 7 | org-junit-jupiter-junit-jupiter = "5.13.0" 8 | org-junit-jupiter-junit-jupiter-api = "5.13.0" 9 | org-junit-jupiter-junit-jupiter-engine = "5.13.0" 10 | org-slf4j-slf4j-api = "2.0.17" 11 | org-slf4j-slf4j-simple = "2.0.17" 12 | 13 | [libraries] 14 | com-bitwarden-sdk-secrets = { module = "com.bitwarden:sdk-secrets", version.ref = "com-bitwarden-sdk-secrets" } 15 | org-cryptomator-integrations-api = { module = "org.cryptomator:integrations-api", version.ref = "org-cryptomator-integrations-api" } 16 | org-junit-jupiter-junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "org-junit-jupiter-junit-jupiter" } 17 | org-junit-jupiter-junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "org-junit-jupiter-junit-jupiter-api" } 18 | org-junit-jupiter-junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "org-junit-jupiter-junit-jupiter-engine" } 19 | org-junit-platform-junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } 20 | org-slf4j-slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "org-slf4j-slf4j-api" } 21 | org-slf4j-slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "org-slf4j-slf4j-simple" } 22 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purejava/cryptomator-bitwarden/a02659d5baa2d26609a8b964b4bfca9615130c1e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | org.gradle.wrapper.GradleWrapperMain \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * This project uses @Incubating APIs which are subject to change. 5 | */ 6 | 7 | rootProject.name = "cryptomator-bitwarden" 8 | -------------------------------------------------------------------------------- /src/main/java/org/purejava/integrations/keychain/BitwardenAccess.java: -------------------------------------------------------------------------------- 1 | package org.purejava.integrations.keychain; 2 | 3 | import com.bitwarden.sdk.BitwardenClient; 4 | import com.bitwarden.sdk.BitwardenClientException; 5 | import com.bitwarden.sdk.BitwardenSettings; 6 | import com.bitwarden.sdk.schema.SecretIdentifierResponse; 7 | import org.cryptomator.integrations.keychain.KeychainAccessException; 8 | import org.cryptomator.integrations.keychain.KeychainAccessProvider; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import java.util.Arrays; 13 | import java.util.Optional; 14 | import java.util.UUID; 15 | 16 | public class BitwardenAccess implements KeychainAccessProvider { 17 | 18 | private static final Logger LOG = LoggerFactory.getLogger(BitwardenAccess.class); 19 | 20 | private final BitwardenSettings bitwardenSettings = new BitwardenSettings(); 21 | private BitwardenClient bitwardenClient; 22 | private final String accessToken; 23 | private UUID organizationId = null; 24 | private final String stateFile; 25 | private boolean isSupported = false; 26 | private final String boID; 27 | private final String apiUrl; 28 | private final String identityUrl; 29 | private final String APP_NAME = "Cryptomator"; 30 | 31 | public BitwardenAccess() { 32 | this.accessToken = System.getenv("BITWARDEN_ACCESS_TOKEN"); 33 | this.boID = System.getenv("BITWARDEN_ORGANIZATION_ID"); 34 | this.stateFile = System.getenv("BITWARDEN_STATE_FILE"); 35 | 36 | var envApiUrl = System.getenv("BITWARDEN_API_URL"); 37 | var envIdentityUrl = System.getenv("BITWARDEN_IDENTITY_URL"); 38 | 39 | var endpoint = resolveEndpoint(envApiUrl, envIdentityUrl); 40 | 41 | this.apiUrl = endpoint.apiUrl; 42 | this.identityUrl = endpoint.identityUrl; 43 | 44 | if (isEnvVarValid(accessToken) && isEnvVarValid(boID)) { 45 | try { 46 | this.organizationId = UUID.fromString(boID); 47 | this.bitwardenSettings.setApiUrl(apiUrl); 48 | this.bitwardenSettings.setIdentityUrl(identityUrl); 49 | this.bitwardenClient = new BitwardenClient(bitwardenSettings); 50 | this.bitwardenClient.auth().loginAccessToken(accessToken, stateFile); 51 | this.isSupported = true; 52 | 53 | } catch (BitwardenClientException | IllegalArgumentException e) { 54 | LOG.error(e.toString(), e.getCause()); 55 | } 56 | } 57 | } 58 | 59 | @Override 60 | public String getName() { return "Bitwarden"; } 61 | 62 | @Override 63 | public boolean isSupported() { return isSupported; } 64 | 65 | @Override 66 | public boolean isLocked() { return false; } 67 | 68 | @Override 69 | public void storePassphrase(String vault, String name, CharSequence password) throws KeychainAccessException { 70 | try { 71 | var projectId = getprojectId(); 72 | var secret = getSecret(vault); 73 | if (secret.isEmpty()) { 74 | bitwardenClient.secrets().create(organizationId, vault, password.toString(), "Password for vault: " + name, new UUID[]{ projectId }); 75 | } 76 | LOG.debug("Passphrase successfully stored"); 77 | } catch (BitwardenClientException | IllegalArgumentException e) { 78 | throw new KeychainAccessException("Storing the passphrase failed", e); 79 | } 80 | } 81 | 82 | @Override 83 | public char[] loadPassphrase(String vault) throws KeychainAccessException { 84 | try { 85 | var secret = getSecret(vault); 86 | if (secret.isEmpty()) { 87 | LOG.debug("No Passphrase found"); 88 | return null; 89 | } else { 90 | LOG.debug("Passphrase loaded"); 91 | return bitwardenClient.secrets().get(secret.get().getID()).getValue().toCharArray(); 92 | } 93 | } catch (BitwardenClientException | IllegalArgumentException e) { 94 | throw new KeychainAccessException("Loading the passphrase failed", e); 95 | } 96 | } 97 | 98 | @Override 99 | public void deletePassphrase(String vault) throws KeychainAccessException { 100 | try { 101 | var secret = getSecret(vault); 102 | if (secret.isEmpty()) { 103 | LOG.debug("Passphrase not found"); 104 | } else { 105 | LOG.debug("Passphrase found and deleted"); 106 | bitwardenClient.secrets().delete(new UUID[]{ secret.get().getID() }); 107 | } 108 | } catch (BitwardenClientException | IllegalArgumentException e) { 109 | throw new KeychainAccessException("Deleting the passphrase failed", e); 110 | } 111 | } 112 | 113 | @Override 114 | public void changePassphrase(String vault, String name, CharSequence password) throws KeychainAccessException { 115 | try { 116 | var projectId = getprojectId(); 117 | var secret = getSecret(vault); 118 | if (secret.isEmpty()) { 119 | LOG.debug("Passphrase not found"); 120 | } else { 121 | LOG.debug("Passphrase found and updated"); 122 | bitwardenClient.secrets().update(organizationId, secret.get().getID(), vault, password.toString(), "Password for vault: " + name, new UUID[]{ projectId }); 123 | } 124 | } catch (BitwardenClientException | IllegalArgumentException e) { 125 | throw new KeychainAccessException("Updating the passphrase failed", e); 126 | } 127 | } 128 | 129 | /** 130 | * Lookup projectId or generate a new project, in case none with the given name exists. 131 | * @return The projectId of the project. 132 | */ 133 | private UUID getprojectId() throws BitwardenClientException { 134 | var project = Arrays.stream(bitwardenClient.projects().list(organizationId).getData()) 135 | .filter(r -> r.getName().equals(APP_NAME)) 136 | .findFirst(); 137 | if (project.isPresent()) { 138 | return project.get().getID(); 139 | } else { 140 | return bitwardenClient.projects().create(organizationId, APP_NAME).getID(); 141 | } 142 | } 143 | 144 | /** 145 | * Find a secret for the given key (vault). 146 | * @param vault The identifier for the secret we are looking for. 147 | * @return An Optional containing the secret or an empty Optional, in case, no secret was found. 148 | * @throws BitwardenClientException Communication with the Bitwarden back end failed due to technical reasons. 149 | */ 150 | private Optional getSecret(String vault) throws BitwardenClientException { 151 | return Arrays.stream(bitwardenClient.secrets().list(organizationId).getData()) 152 | .filter(r -> r.getKey().equals(vault)) 153 | .findFirst(); 154 | } 155 | 156 | /** 157 | * Resolve the Bitwarden endpoint based on the provided environment variables. 158 | * @param apiUrlEnv The API URL environment variable. 159 | * @param identityUrlEnv The Identity URL environment variable. 160 | * @return The resolved Bitwarden endpoint. 161 | */ 162 | private BitwardenEndpoint resolveEndpoint(String apiUrlEnv, String identityUrlEnv) { 163 | if (isEnvVarValid(apiUrlEnv) && isEnvVarValid(identityUrlEnv)) { 164 | for (BitwardenEndpoint endpoint : BitwardenEndpoint.values()) { 165 | if (endpoint.apiUrl.equals(apiUrlEnv) && endpoint.identityUrl.equals(identityUrlEnv)) { 166 | return endpoint; 167 | } 168 | } 169 | LOG.warn("Provided API/Identity URLs are invalid. Falling back to default (US)."); 170 | } else { 171 | LOG.info("API/Identity URLs not set. Falling back to default (US)."); 172 | } 173 | return BitwardenEndpoint.US; 174 | } 175 | 176 | /** 177 | * Enum to represent the Bitwarden endpoints. 178 | */ 179 | enum BitwardenEndpoint { 180 | US("https://api.bitwarden.com", "https://identity.bitwarden.com"), 181 | EU("https://api.bitwarden.eu", "https://identity.bitwarden.eu"); 182 | 183 | private final String apiUrl; 184 | private final String identityUrl; 185 | 186 | BitwardenEndpoint(String apiUrl, String identityUrl) { 187 | this.apiUrl = apiUrl; 188 | this.identityUrl = identityUrl; 189 | } 190 | } 191 | 192 | /** 193 | * Check if the given environment variable is set. 194 | * @param var The environment variable to check. 195 | * @return true if the variable is set and is not empty, false otherwise. 196 | */ 197 | private boolean isEnvVarValid(String var) { 198 | return null != var && !var.isEmpty() && !var.isBlank(); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/org.cryptomator.integrations.keychain.KeychainAccessProvider: -------------------------------------------------------------------------------- 1 | org.purejava.integrations.keychain.BitwardenAccess -------------------------------------------------------------------------------- /src/test/java/org/purejava/integrations/keychain/BitwardenAccessTest.java: -------------------------------------------------------------------------------- 1 | package org.purejava.integrations.keychain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | class BitwardenAccessTest { 8 | 9 | /** 10 | * Rigorous Test :-) 11 | */ 12 | @Test 13 | public void shouldAnswerWithTrue() 14 | { 15 | assertTrue( true ); 16 | } 17 | } -------------------------------------------------------------------------------- /src/test/resources/simplelogger.properties: -------------------------------------------------------------------------------- 1 | org.slf4j.simpleLogger.defaultLogLevel=info --------------------------------------------------------------------------------