├── .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 |
51 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
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 | 
3 |
4 | [](https://github.com/purejava/cryptomator-bitwarden/actions/workflows/build_develop.yml)
5 | [](https://app.codacy.com/gh/purejava/cryptomator-bitwarden/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
6 | [](https://github.com/purejava/cryptomator-bitwarden/releases)
7 | [](https://github.com/purejava/cryptomator-bitwarden/blob/master/LICENSE)
8 | [](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 | [](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 | [](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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------